一,依賴庫知識速學
aarch64
aarch64
,也被稱為 ARM64,是一種基於 ARMv8-A
架構的 64
位指令集體系結構。它是 ARM 體系結構的最新版本,旨在提供更好的效能和能效比。與先前的 32
位 ARM
架構相比,aarch64 具有更大的定址空間、更多的暫存器和更好的浮點效能。
在 Linux 系統終端下輸入以下命令,檢視 cpu
架構。
uname -m # 我的英特爾伺服器輸出 x86_64,m1 pro 蘋果電腦輸出 arm64
OpenMP
OpenMP
(Open Multi-Processing)是一種基於共享記憶體的並行程式設計 API,用於編寫多執行緒並行程式。使用 OpenMP
,程式設計師可以透過在程式中插入指令來指示程式中的並行性。這些指令是以 #pragma
開頭的編譯指示符,告訴編譯器如何並行化程式碼。
#include <stdio.h>
#include <omp.h>
int main() {
int i;
#pragma omp parallel for
for(i = 0; i < 10; i++) {
printf("Thread %d executing iteration %d\n", omp_get_thread_num(), i);
}
return 0;
}
AVX512
AVX
全稱是 Advanced Vector Extension,高階向量擴充套件,用於處理 N
維資料的,例如 8
維及以下的 64
位雙精度浮點向量或 16
維及以下的單精度浮點向量。
AVX512
是 SIMD
指令(單指令多資料),x86
架構上最早的 SIMD 指令是 128bit 的 SSE
,然後是 256bit 的 AVX/AVX2,最後是現在 512bit 的 AVX512。
submodule
github submodule(子模組)允許你將一個 Git 倉庫作為另一個 Git 倉庫的子目錄。 它能讓你將另一個倉庫克隆到自己的專案中,同時還保持提交的獨立。
apt upgrade
apt update
:只檢查,不更新(已安裝的軟體包是否有可用的更新,給出彙總報告)。apt upgrade
:更新已安裝的軟體包。
二,硬體基礎知識速學
2.1,記憶體
RAM
(隨機訪問儲存)的一些關鍵特性是頻寬(bandwidth
)和延遲(latency
)。
2.2,CPU
中央處理器(central processing unit,CPU
)是任何計算機的核心,其由許多關鍵元件組成:
- 處理器核心 (processor cores): 用於執行機器程式碼的。
- 匯流排(bus): 用於連線不同元件(注意,匯流排會因為處理器型號、 各代產品和供應商之間的特定拓撲結構有明顯不同)
- 快取(cache): 一般是三級緩(L1/L2/L3 cache),相比主記憶體實現更高的讀取頻寬和更低的延遲記憶體訪問。
現代 CPU 都包含向量處理單元,都提供了 SIMD
指令,可以在單個指令中同時處理多個資料,從而支援高效能線性代數和卷積運算。這些 SIMD
指令有不同的名稱: 在 ARM 上叫做 NEON,在 x86 上被稱 為AVX2156。
一個典型的 Intel Skylake 消費級四核 CPU,其核心架構如下圖所示。
三,ncnn 推理模型
3.1,shufflenetv2 模型推理解析
這裡以分類網路 shufflenetv2 為例,分析如何使用 ncnn
框架模型推理。先原始碼在 ncnn/examples/shufflenetv2.cpp
檔案中,程式主要分為兩個函式,分別是 detect_shufflenetv2()
和 print_topk()
。前者用於執行圖片分類網路,後者用於輸出前 N 個分類結果。程式碼流程總結如下:
-
在
detect_shufflenetv2
函式中,主要使用了ncnn::Net
類進行模型載入和推理,主要流程如下:- 載入模型引數和模型二進位制檔案。
- 將輸入圖片
cv::Mat
格式轉換為ncnn::Mat
格式,同時進行 resize 和歸一化操作。 - 建立
ncnn::Extractor
物件,並設定輸入和輸出。 - 進行推理計算,得到分類輸出結果。
- 對輸出結果進行
softmax
操作。 - 將輸出結果轉換為 vector
型別的資料,儲存到 cls_scores 中。
-
呼叫
print_topk
函式輸出 cls_scores 的前topk
個類別及其得分,具體實現步驟如下:- 定義一個向量
std::vector<std::pair<float, int>> vec
,其元素型別為<float, int>
,其中第一個元素為分類得分,第二個元素為該分類的索引。 - 遍歷分類模型輸出結果
cls_scores
,將其與索引值組成一個<float, int>
型別的元素,放入向量vec
中。 - 使用
std::partial_sort()
函式,將向量vec
進行部分排序,按照得分從大到小的順序排列。 - 遍歷排好序的向量
vec
,輸出前topk
個元素的索引和得分值。
- 定義一個向量
-
最後主函式 main 中先呼叫 cv::imread 函式完成影像的讀取操作,而後呼叫
detect_shufflenetv2
和print_topk
函式,完成 shufflenetv2 網路推理和圖片分類結果機率值輸出的操作。
print_topk
函式程式碼及其註釋如下:
// 定義函式,輸入為一個向量 cls_scores 和需要輸出的 topk 數量
static int print_topk(const std::vector<float>& cls_scores, int topk)
{
// 1,定義一個向量 vec,其元素型別為 <float, int>,用於儲存分類得分和索引值
int size = cls_scores.size();
std::vector<std::pair<float, int> > vec;
vec.resize(size);
// 2,遍歷分類得分,將其與索引值組成 <float, int> 元素,並存入向量 vec 中
for (int i = 0; i < size; i++)
{
vec[i] = std::make_pair(cls_scores[i], i);
}
// 3,使用 std::partial_sort() 函式,將向量 vec 進行部分排序,按照得分從大到小的順序排列
std::partial_sort(vec.begin(), vec.begin() + topk, vec.end(),
std::greater<std::pair<float, int> >());
// 4,遍歷排好序的向量 vec,輸出前 topk 個元素的索引和得分值
for (int i = 0; i < topk; i++)
{
float score = vec[i].first;
int index = vec[i].second;
fprintf(stderr, "%d = %f\n", index, score);
}
return 0;
}
值得注意的是,雖然呼叫 print_topk
函式得到了最高得分及其類別索引,但還需要將類別索引轉換為類別字串。這通常需要預先定義一個包含所有類別字串的向量 class_names
,並將其與類別索引一一對應。另外, class_names
的定義需與模型訓練時的類別標籤一致,否則會出現類別不匹配的情況。
最後,實際跑下 sample 看下執行結果,這裡模型用的是 imagenet 訓練的 shufflenetv2 模型,然後用編譯好的 shufflenetv2 程式去跑測試圖片,輸入圖片和程式執行結果如下:
/ncnn/build/examples# ./shufflenetv2 demo.jpeg
270 = 0.455700
279 = 0.303561
174 = 0.057936
輸入影像的類別索引是 270
,參考文章ImageNet 2012 1000分類名稱和編號,可知該類別是 dog(狗)。
3.2,網路推理過程解析
下面再看下網路推理程式碼的整體流程解析:
1,首先需要 Net
物件,然後使用 load_param
和 load_bin
兩個介面載入模型結構引數和模型權重引數檔案:
// 為了方便閱讀,和官方程式碼比有所刪減
ncnn::Net shufflenetv2;
shufflenetv2.load_param("shufflenet_v2_x0.5.param")
shufflenetv2.load_model("shufflenet_v2_x0.5.bin")
2,定義好 Net 物件後,可以呼叫相應的 create_extractor 介面建立 Extractor
,Extractor 物件是完成影像資料輸入和模型推理的類,雖然它也是對 Net 的相關介面做了封裝。
ncnn::Extractor ex = shufflenetv2.create_extractor();
ex.input("data", in);
ncnn::Mat out;
ex.extract("fc", out); // 提取網路輸出結果到 out 矩陣中
3,模型推理結果後處理,對網路推理結果執行 softmax 操作得到機率矩陣,而後轉換為 vector
// 對輸出結果矩陣進行 softmax 操作
// manually call softmax on the fc output
// convert result into probability
// skip if your model already has softmax operation
{
ncnn::Layer* softmax = ncnn::create_layer("Softmax");
ncnn::ParamDict pd;
softmax->load_param(pd);
softmax->forward_inplace(out, shufflenetv2.opt);
delete softmax;
}
// 將softmax輸出結果轉換為 vector<float> 型別的資料,儲存到 cls_scores 中
out = out.reshape(out.w * out.h * out.c);
cls_scores.resize(out.w);
for (int j = 0; j < out.w; j++)
{
cls_scores[j] = out[j];
}
這裡之所以需要手動呼叫 softmax 層,是因為官方提供的 shufflenetv2 模型結構檔案的最後一層是 fc
層,沒有 softmax
層。
值得注意的是,ncnn::Mat 型別預設採用的是 NCHW (通道在前,即 Number-Channel-Height-Width)的格式。在常見的分類任務中,ncnn 網路輸出的一般是一個大小為 [1, 1, num_classes] 的張量,其中第三個維度的大小為類別數,上述程式碼即 out.w
表示類別數量,而 out.h 和 out.c 都為 1。
3.3,模型推理過程總結
1,模型推理過程可總結為下述步驟:
- 輸入資料準備:輸入資料可以是影像、文字或其他形式的資料。在ncnn中,輸入資料通常被轉化為多維張量,其中第一維是資料的數量,其餘維度表示資料的形狀和尺寸。
- 載入模型引數和模型權重檔案:透過 Net 類的
load_param
和load_bin
兩個介面實現。 - 模型前向計算:從模型的輸入層開始,逐層計算模型的輸出。每個層接收上一層的輸出作為輸入,並執行特定的運算元,比如:卷積、池化、全連線等。在逐層計算過程中,模型各層的引數和權重資料也被用於更新模型的輸出。最終,模型的輸出被傳遞到模型的輸出層。
- 輸出資料解析:模型的輸出資料通常被轉化為外部應用程式可用的格式。例如,在影像分類任務中,模型的輸出可以是一個機率向量,表示輸入影像屬於每個類別的機率分佈。在ncnn中,輸出資料可以轉化為多維張量或其他形式的資料。
2,ncnn 載入/解析模型引數和權重檔案的步驟還是很複雜的,可總結如下:
- 讀取二進位制引數和權重檔案,並儲存為位元組陣列。
- 解析位元組陣列中的頭部資訊,包括檔案版本號、模型結構資訊等。
- 解析層級資訊,包括每個層的名稱、型別、輸入輸出維度等資訊,並儲存在
blobs
中,Blob 類由:網路層 name、依賴層索引:producer 和 consumer,及上一層和下一網路層索引、網路層 shape 組成。 - 解析每個層的引數和權重資料,將其儲存為矩陣或向量。