張高興的 Raspberry Pi AI 開發指南:(三)將自定義模型編譯為 Hailo NPU 的 .hef 模型

张高兴發表於2024-12-10

目錄
  • Python 環境配置
  • 轉換
  • 量化
  • 編譯
  • 參考

在上一篇部落格中,探討了如何使用 Python 和 hailo_model_zoo 中預編譯的模型來實現目標檢測。本篇部落格將深入介紹如何將使用者自定義訓練的模型轉換並最佳化為能夠在 Hailo NPU 上高效執行的 .hef 模型。

Python 環境配置

為了將自定義模型編譯為 .hef 模型,需要安裝 Hailo Dataflow Compiler(DFC) 工具。登入 Hailo 的網站 https://hailo.ai/developer-zone/software-downloads,找到對應 Python 版本的 .whl 檔案,並按照下面的步驟建立虛擬環境並安裝必要的軟體包:

conda create -n hailo-model python=3.10  # 建立虛擬環境
conda activate hailo-model  # 啟用虛擬環境
sudo apt install libgraphviz-dev
pip install hailo_dataflow_compiler-3.29.0-py3-none-linux_x86_64.whl  # 安裝 Hailo Dataflow Compiler Python 包

將自定義模型轉換為 .hef 模型需要三步:

  1. 將 Tensorflow 或 ONNX 模型轉換成 Hailo Archive 模型(.har)。
  2. .har 模型進行量化。
  3. 編譯為 Hailo Executable File 模型(.hef)。

轉換

Tensorflow 與 ONNX 模型都可以進行轉換,這裡以 yolov8n 的 ONNX 模型為例,首先引入軟體包並定義相關變數。

from hailo_sdk_client import ClientRunner
import os
import cv2
import numpy as np

input_size = 640  # 模型輸入的尺寸
chosen_hw_arch = "hailo8l"  # 要使用的 Hailo 硬體架構,這裡是 Hailo-8L
onnx_model_name = "yolov8n"  # 模型的名字
onnx_path = "yolov8n.onnx"  # 模型的路徑
hailo_model_har_path = f"{onnx_model_name}_hailo_model.har"  # 轉換後模型的儲存路徑
hailo_quantized_har_path = f"{onnx_model_name}_hailo_quantized_model.har"  # 量化後模型的儲存路徑
hailo_model_hef_path = f"{onnx_model_name}.hef"  # 編譯後模型的儲存路徑

接著例項化 ClientRunner 類,並呼叫 translate_onnx_model() 方法進行轉換。

runner = ClientRunner(hw_arch=chosen_hw_arch)
hn, npz = runner.translate_onnx_model(model=onnx_path, net_name=onnx_model_name)  # 將 onnx 模型轉為 har
runner.save_har(hailo_model_har_path)  # 儲存轉換後的模型

在模型結構較為簡單時,通常不會報錯。當模型結構較為複雜時,會存在 Hailo NPU 不支援的運算元,從而報錯導致轉換失敗。NPU 支援的運算元可以查詢官網的資料手冊,或者檢視下文參考中的連結。例如在轉換 YOLOv8 模型時會提示以下錯誤資訊:

hailo_sdk_client.model_translator.exceptions.ParsingWithRecommendationException: Parsing failed. The errors found in the graph are:
 UnsupportedShuffleLayerError in op /model.22/dfl/Reshape: Failed to determine type of layer to create in node /model.22/dfl/Reshape
Please try to parse the model again, using these end node names: /model.22/Concat_3

出現錯誤時有兩種解決方案。一是根據報錯資訊,使用 Netron https://netron.app 檢視模型結構,並修改原始模型,移除或替換 Hailo NPU 不支援的運算元。二是報錯資訊中會推薦解決方法,在轉換時繞過不支援的運算元,那麼 translate_onnx_model() 方法則需要傳遞額外的引數:

  • start_node_names:原始模型中開始轉換的節點(對應新模型的輸入)的名稱。
  • end_node_names:原始模型中停止轉換的節點(對應新模型的輸出)的名稱。
  • net_input_shapesstart_node_names 輸入的尺寸,如常見的 [b, c, h, w]

節點的名稱可以使用 Netron 檢視,或者使用下面的程式遍歷列印節點的名稱。

import onnx

onnx_path = "yolov8n.onnx"
model = onnx.load(onnx_path)

print("Input Nodes:")
for input in model.graph.input:
    print(input.name)
print("Output Nodes:")
for output in model.graph.output:
    print(output.name)
print("Nodes:")
for node in model.graph.node:
    print(node.name)

根據上面的錯誤資訊提示,要將停止轉換的節點修改為 /model.22/Concat_3,修改後的程式如下。

hn, npz = runner.translate_onnx_model(model=onnx_path, net_name=onnx_model_name, start_node_names=["images"], end_node_names=["/model.22/Concat_3"], net_input_shapes={"images": [1, 3, input_size, input_size]})

程式執行後並未報錯,但在最後一步編譯時會出現 Hailo NPU 記憶體不夠的情況,我們再觀察一下轉換時輸出的日誌:

[info] Translation started on ONNX model yolov8n
[info] Restored ONNX model yolov8n (completion time: 00:00:00.06)
[info] Extracted ONNXRuntime meta-data for Hailo model (completion time: 00:00:00.21)
[info] NMS structure of yolov8 (or equivalent architecture) was detected.
[info] In order to use HailoRT post-processing capabilities, these end node names should be used: /model.22/cv2.0/cv2.0.2/Conv /model.22/cv3.0/cv3.0.2/Conv /model.22/cv2.1/cv2.1.2/Conv /model.22/cv3.1/cv3.1.2/Conv /model.22/cv2.2/cv2.2.2/Conv /model.22/cv3.2/cv3.2.2/Conv.
...

日誌建議將停止轉換的節點修改為 /model.22/cv2.0/cv2.0.2/Conv /model.22/cv3.0/cv3.0.2/Conv /model.22/cv2.1/cv2.1.2/Conv /model.22/cv3.1/cv3.1.2/Conv /model.22/cv2.2/cv2.2.2/Conv /model.22/cv3。即在 NMS 處理前將模型切割,查閱 Hailo 開發者論壇得知,Hailo NPU 不具備進行 NMS 運算的能力,這一部分將在 CPU 上執行。Hailo 的 GitHub 倉庫提供了主流模型轉換時結束節點的名稱,具體請檢視下文參考中的連結。最終,程式修改為:

hn, npz = runner.translate_onnx_model(model=onnx_path, net_name=onnx_model_name, start_node_names=["images"], end_node_names=["/model.22/cv2.0/cv2.0.2/Conv", "/model.22/cv3.0/cv3.0.2/Conv", "/model.22/cv2.1/cv2.1.2/Conv", "/model.22/cv3.1/cv3.1.2/Conv", "/model.22/cv2.2/cv2.2.2/Conv", "/model.22/cv3.2/cv3.2.2/Conv"], net_input_shapes={"images": [1, 3, input_size, input_size]})

量化

模型量化(Quantization)是將深度學習模型中的權重和啟用值(輸出)從高精度的浮點數(如 float32)轉換為低精度的資料型別(如 int8),以減少模型的儲存需求、加快推理速度並降低功耗,這一過程對於將深度學習模型部署到邊緣裝置中特別重要。這裡使用的是訓練後量化,即在已經訓練好的模型上直接進行量化,無需重新訓練或微調,但可能會導致一些準確性的損失。

首先需要準備好量化時使用的校準資料集。校準資料集主要用於幫助確定量化引數,以儘量減少量化過程對模型效能的影響。校準資料集的質量直接影響到量化模型的最終效能,應該儘可能涵蓋所有的資料變化,以確保量化後的模型在不同條件下都能有良好的泛化能力。校準資料集不需要標籤,其主要用於收集每一層啟用值的統計資料,例如最小值、最大值、平均值和標準差等。這些統計資訊用於確定如何最佳地對映浮點數到整數,從而保持模型效能,這個過程不需要知道輸入資料對應的標籤,只需要瞭解資料的分佈特性。

本篇部落格用到的 YOLOv8 模型是使用 COCO 資料集訓練的,下面就以此為例進行校準資料集的準備。

images_path = "data/images"  # 資料集影像路徑
dataset_output_path = "calib_set.npy"  # 處理完成後的儲存路徑

images_list = [img_name for img_name in os.listdir(images_path) if os.path.splitext(img_name)[1] in [".jpg", ".png", "bmp"]][:1500]  # 獲取影像名稱列表
calib_dataset = np.zeros((len(images_list), input_size, input_size, 3))  # 初始化 numpy 陣列

for idx, img_name in enumerate(sorted(images_list)):
    img = cv2.imread(os.path.join(images_path, img_name))
    resized = cv2.resize(img, (input_size, input_size))  # 調整原始影像的尺寸為模型輸入的尺寸
    calib_dataset[idx,:,:,:]=np.array(resized)
np.save(dataset_output_path, calib_dataset)

接著例項化 ClientRunner 類,並呼叫 optimize() 方法進行量化。

calib_dataset = np.load(dataset_output_path)
runner = ClientRunner(har=hailo_model_har_path)
runner.optimize(calib_dataset)  # 量化模型
runner.save_har(hailo_quantized_har_path)  # 儲存量化後的模型

在量化過程中還可以新增一些指令碼對引數進行設定,例如 model_optimization_flavor() 設定量化的級別、resources_param() 設定模型能夠使用的資源量等。hailo_model_zoo 倉庫提供了主流模型的引數設定指令碼,具體請檢視下文參考中的連結。程式示例如下。

alls_lines = [
    'model_optimization_flavor(optimization_level=1, compression_level=2)',
    'resources_param(max_control_utilization=0.6, max_compute_utilization=0.6, max_memory_utilization=0.6)',
    'performance_param(fps=5)'
]
runner.load_model_script('\n'.join(alls_lines))
runner.optimize(calib_dataset)

編譯

最後使用 compile() 方法完成模型的編譯。

runner = ClientRunner(har=hailo_quantized_har_path)
compiled_hef = runner.compile()
with open(hailo_model_hef_path, "wb") as f:
    f.write(compiled_hef)

完整程式如下。

from hailo_sdk_client import ClientRunner
import os
import cv2
import numpy as np

input_size = 640  # 模型輸入的尺寸
chosen_hw_arch = "hailo8l"  # 要使用的 Hailo 硬體架構,這裡是 Hailo-8L
onnx_model_name = "yolov8n"  # 模型的名字
onnx_path = "yolov8n.onnx"  # 模型的路徑
hailo_model_har_path = f"{onnx_model_name}_hailo_model.har"  # 轉換後模型的儲存路徑
hailo_quantized_har_path = f"{onnx_model_name}_hailo_quantized_model.har"  # 量化後模型的儲存路徑
hailo_model_hef_path = f"{onnx_model_name}.hef"  # 編譯後模型的儲存路徑
images_path = "data/images"  # 資料集影像路徑

# 將 onnx 模型轉為 har
runner = ClientRunner(hw_arch=chosen_hw_arch)
hn, npz = runner.translate_onnx_model(model=onnx_path, net_name=onnx_model_name, start_node_names=["images"], end_node_names=["/model.22/cv2.0/cv2.0.2/Conv", "/model.22/cv3.0/cv3.0.2/Conv", "/model.22/cv2.1/cv2.1.2/Conv", "/model.22/cv3.1/cv3.1.2/Conv", "/model.22/cv2.2/cv2.2.2/Conv", "/model.22/cv3.2/cv3.2.2/Conv"], net_input_shapes={"images": [1, 3, input_size, input_size]})
runner.save_har(hailo_model_har_path)

# 校準資料集準備
images_list = [img_name for img_name in os.listdir(images_path) if os.path.splitext(img_name)[1] in [".jpg", ".png", "bmp"]][:1500]  # 獲取影像名稱列表
calib_dataset = np.zeros((len(images_list), input_size, input_size, 3))  # 初始化 numpy 陣列
for idx, img_name in enumerate(sorted(images_list)):
    img = cv2.imread(os.path.join(images_path, img_name))
    resized = cv2.resize(img, (input_size, input_size))  # 調整原始影像的尺寸為模型輸入的尺寸
    calib_dataset[idx,:,:,:]=np.array(resized)

# 量化模型
runner = ClientRunner(har=hailo_model_har_path)
alls_lines = [
    'model_optimization_flavor(optimization_level=1, compression_level=2)',
    'resources_param(max_control_utilization=0.6, max_compute_utilization=0.6, max_memory_utilization=0.6)',
    'performance_param(fps=5)'
]
runner.load_model_script('\n'.join(alls_lines))
runner.optimize(calib_dataset)
runner.save_har(hailo_quantized_har_path)

# 編譯為 hef
runner = ClientRunner(har=hailo_quantized_har_path)
compiled_hef = runner.compile()
with open(hailo_model_hef_path, "wb") as f:
    f.write(compiled_hef)

參考

  1. Supported operators - Hailo Community:https://community.hailo.ai/t/supported-operators/5046/2
  2. hailo_model_zoo - GitHub:https://github.com/hailo-ai/hailo_model_zoo/tree/master/hailo_model_zoo/cfg/networks
  3. Dataflow Compiler v3.29.0:https://hailo.ai/developer-zone/documentation/dataflow-compiler-v3-29-0

相關文章