今天我們要進入新的主題,那就是 Open Neural Network Exchange 格式,簡稱 ONNX。這個主題在筆者前一年的鐵人賽中就曾簡單介紹。在該文中,並使用 ONNX 將 pre-trained 的模型用於增加像素解析度的任務上。
基本上要取得 ONNX 的模型有三種方式:
讓我們再回到計算圖,了解計算圖在不同的深度學習中所扮演的角色。無論是動態建構或靜態編譯的計算圖,都提供深度學習的後端一個藍圖,可供編譯器就計算圖的結構做最佳化,如昨天所提到的,對多個運算元做融合(fusing)便是一個針對計算圖編譯最佳化的例子。計算圖在不同的深度學習架構中,可以做為 Intermediate Representation 或 IR。
IR 的概念來自於編譯器的設計,也就是在人類可讀的原始碼到編譯完成的機械碼之間產生的語言。IR 的功能在於產生硬體架構獨立的中介表示方法,可以完整地捕捉原始碼的原貌。而 ONNX 在深度學習架構中所扮演的角色辨識便是提供一個共通的 IR 介面,能讓撰寫模型的原始碼在不同的深度學習架構中流通使用。
ONNX 涵蓋了下列的單元:
針對可以涵蓋的資料型態,ONNX 有兩種架構,一是針對類神經網路產生的 IR,另外一個則是針對一般的機械學習的 ONNX-ML。針對類神經網路而建制的 ONNX 只有定義張量(Tensor)型態。而 ONNX-ML 除了包括類神經網路 ONNX 所支持的運算元,更定義一般機械學習眼算法會用到的型態,如 python 的 list 和 dict。
完整的類神經網路運算元支援,類神經網路 ONNX 可以在類神經網路運算元查看。而完整的機械學習運算元支援,則可以在Classical Machine Learning operators中查看。
現在進入實作的部分,我們先解說 ONNX 常用的幾個 python 函式:
import onnx
# 載入 ONNX 模型
onnx_model = onnx.load('super_resolution.onnx')
# 儲存 ONNX 模型
onnx.save(onnx_model, 'super_resolution_copy.onnx')
from_array
可以將 np.ndarray
轉為 onnx.TensoProto
。也有to_array
的方法把 onnx.TensoProto
轉為 numpy.ndarray
物件。onnx.TensoProto
除了有 dim
屬性外,還有一個 raw_data 屬性來儲存資料 buffer。onnx.TensoProto
物件可以呼叫物件方法SerializeToString()
來對模型做序列化,存成 protobuf 格式,也可利用物件方法ParseFromString()
完成載入。ParseFromString()
並沒有防止讀進的 buffer 覆蓋 onnx.TensoProto
原有的 buffer,所以一旦你用一個已擁有 buffer 的 onnx.TensoProto
物件來做 tensor.ParseFromString(f.read())
的呼叫,原資料就會被覆蓋。from onnx import numpy_helper
# 建立一個 3x2 的 numpy.ndarray
numpy_array = numpy.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=float)
print('Original Numpy array:\n{}\n'.format(numpy_array))
# => Original Numpy array:
#[[1. 2. 3.]
# [4. 5. 6.]]
# 轉換 Numpy array 使其為 TensorProto 物件
tensor = numpy_helper.from_array(numpy_array)
print('TensorProto:\n{}'.format(tensor))
#=>TensorProto:
dims: 2
dims: 3
data_type: 11
raw_data: "\000\000\000\000\000\000\360?\000\000\000\000\000\000\000@\000\000\000\000\000\000\010@\000\000\000\000\000\000\020@\000\000\000\000\000\000\024@\000\000\000\000\000\000\030@"
# 儲存 TensorProto 物件
with open('tensor.pb', 'wb') as f:
f.write(tensor.SerializeToString())
# 從硬碟載入 TensorProto 物件
new_tensor = onnx.TensorProto() # 先建立一個空的 TensorProto 物件
with open(os.path.join('resources', 'tensor.pb'), 'rb') as f:
new_tensor.ParseFromString(f.read()) # 解析儲存序列檔的 protobuf 檔案
print('After saving and loading, new TensorProto:\n{}'.format(new_tensor))
get_available_passes()
獲得,該函式會回傳一個 python list of strings,每個 string 都是一個最佳化 pass名稱。大家可以自行印出 all_passes 內容。optimizer.optimize
的第二個引數。該函式的第一個引數為 ONNX 模型。若想使用預設的最佳化 passes,無需指派第二個引述即可。若想知道預設的最佳化 passes 有哪些可以到原始碼查看。from onnx import optimizer
# 列出可使用的 optimization passes
all_passes = optimizer.get_available_passes()
original_model = onnx_model
# 從 all_passes 中選擇一種 pass 來執行
passes = ['fuse_consecutive_transposes']
# 應用 passes 內的 pass 名稱在模型上
optimized_model = optimizer.optimize(original_model, passes)
#印出經過最佳化後的 ModelProto
#print('The model after optimization:\n{}'.format(optimized_model))
version_converter.convert_version
函式來進行轉換。version_converter
是 ONNX 的一個模組,該模組的函式 convert_version
會接收一個 ModelProto 物件其 opset 版本可以由模型的 opset_import 中的 field 得知,還需指定欲轉換成的版本號碼。呼叫例子為 converted_model = version_converter.convert_version(original_model, 4)
轉換 original_model
到版本 4。polished_model = onnx.utils.polish_model(model)
onnx.tools.update_inputs_outputs_dims
會更新模型的輸入和輸出的維度,並以傳入的參數值取代from onnx.tools import update_model_dims
variable_length_model = update_model_dims.update_inputs_outputs_dims(model, {'input_name': ['seq', 'batch', 3, -1]}, {'output_name': ['seq', 'batch', 1, -1]})```