
背景
Apple官方雖然不支援pytorch到coreml的直接轉換。然而借助蘋果的coremltools、pytorch的onnx、社群的onnx到coreml的轉換工具這三大力量,這個轉換過程還是很容易的。
本文以PyTorch 1.4為基礎,以。将PyTorch模型轉換為CoreML模型分為如下5個基本步驟:
- 使用PyTorch訓練并儲存一個模型(并對save的模型進行測試);
- PyTorch模型轉換為ONNX模型;
- ONNX模型轉換為CoreML模型;
- 在macOS上使用python腳本驗證該模型;
- 內建到XCode上然後在iOS上驗證該模型。
下面分步驟介紹下。
使用PyTorch訓練并儲存一個模型
這是PyTorch的基礎了,在此不予贅述。你可以參考專欄文章:
Gemfield:詳解Pytorch中的網絡構造zhuanlan.zhihu.com
PyTorch模型轉換為ONNX模型
使用PyTorch中的onnx子產品的export方法:
# -*- coding:utf-8 -*-
執行成功,輸出syszux_scene.onnx模型。你可以使用Netron軟體打開onnx模型來看它的網絡結構(是不是預期中的)。
如果你手頭沒有自己的網絡,則可以使用torchvision提供的mobilenet v2來試驗下:
import torch
import torch.nn as nn
import torchvision
model = torchvision.models.mobilenet_v2(pretrained=True)
# torchvision的models中沒有softmax層,我們添加一個
model = nn.Sequential(model, nn.Softmax())
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, 'mobilenet_v2.onnx', verbose=True,
input_names=['image'], output_names=['gemfield_out'])
執行成功,輸出mobilenet_v2.onnx模型。
轉換為onnx模型後,可以使用onnx進行推理測試,以確定從pytorch轉換到onnx的正确性:
import
你也可以使用opencv來代替Pillow的Image來讀取圖檔,并且使用numpy來代替pytorch的transform進行normalize計算,如下所示:
import cv2
import onnxruntime
import numpy as np
import sys
import torch
from PIL import Image
from torchvision import transforms
session = onnxruntime.InferenceSession("../syszux_scene.onnx")
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
input_shape = session.get_inputs()[0].shape
print("gemfield debug required input shape", input_shape)
img = cv2.imread(sys.argv[1])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
#INTER_NEAREST, INTER_LINEAR, INTER_AREA, INTER_CUBIC
img = cv2.resize(img, (224, 224),interpolation = cv2.INTER_LINEAR)
img = img.astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406])
val = np.array([0.229, 0.224, 0.225])
img = (img - mean) / val
print(img)
print("gemfield debug img shape1: ",img.shape)
img= img.astype(np.float32)
img = img.transpose((2,0,1))
#img = img.transpose((2,1,0))
print("gemfield debug img shape2: ",img.shape)
img = np.expand_dims(img,axis=0)
print("gemfield debug img shape3: ",img.shape)
res = session.run([output_name], {input_name: img})
print(res)
ONNX模型轉換為CoreML模型
借助下面的開源社群的項目,可以将syszux_scene.onnx轉換為蘋果的CoreML模型:syszux_scene.mlmodel。
onnx/onnx-coremlgithub.com
pip install --upgrade onnx-coreml
#安裝上面的onnx-coreml的時候會自動安裝coremltools
pip install --upgrade coremltools
2,轉換成mlmodel from onnx_coreml import convert
# Load the ONNX model as a CoreML model
model = convert(model='syszux_scene.onnx',minimum_ios_deployment_target='13')
# Save the CoreML model
model.save('syszux_scene.mlmodel')
這裡面主要用到的就是convert函數。這個函數提供了相當多的參數,在這個例子中提供了其它參數使用的範例:
https://github.com/CivilNet/Gemfield/blob/master/src/python/coreml/onnx2coreml.pygithub.com
注意這裡的參數:minimum_ios_deployment_target='13',指定了該CoreML模型隻能運作在iOS 13上,否則XCode就會給出如下告警:
'mobilenet_v2' is only available on iOS 13.0 or newer
'mobilenet_v2Input' is only available on iOS 13.0 or newer
如果在上面使用的是torchvision提供的mobilenet v2,則這裡使用同樣的腳本來進行coreml的轉換:
import
這個轉換過程不會總是一帆風順,事實上,除了幾個經典的網絡,或者極其簡單的網絡外,一般都會有各種各樣的問題,這是不同軟體生态之間磨合必然的代價。
在macOS上使用Python腳本驗證CoreML模型
1,檢視網絡結構使用Netron軟體可以直接打開CoreML模型來檢視網絡結構和參數,除此之外,還可以使用下面的代碼來檢視.mlmodel模型檔案網絡結構(該代碼可以運作在Linux/Mac/Win上):
import sys
import coremltools
from coremltools.models.neural_network import flexible_shape_utils
spec = coremltools.utils.load_spec('syszux_scene.mlmodel')
print(spec)
2,直接在macOS上使用CoreML模型進行推理測試 使用下面的代碼可以在手機之外debug CoreML模型(該代碼隻能運作在Mac OS上):
from PIL import Image
import coremltools
import numpy as np
mlmodel = coremltools.models.MLModel('gemfield.mlmodel')
pil_img = Image.open('civilnet.jpg')
pil_img = pil_img.resize((768,1280))
#forward
out = mlmodel.predict({'gemfield': pil_img})
#visualize the model output
b = np.argmax(out['gemfieldout'],0)
im = np.where(b==1,255,0)
im = im.astype(np.uint8)
im=Image.fromarray(im)
im.show()
如果遇到錯誤:Required input feature not passed to neural network,則是因為輸入輸出的名字不比對。
要想在Mac OS上執行上面的腳本,需要安裝如下依賴:
sudo easy_install pip
pip install --user pillow
pip install --user coremltools
內建到XCode上然後在iOS上驗證該模型
有了mlmodel檔案後,便可将其加入到xcode工程中,最終将算法部署到iOS上。這個時候你需要一位熟練的iOS開發者。注意:如果你使用了vision架構配合CoreML處理圖檔的輸入,切記VNCoreMLRequest這個類的執行個體上的imageCropAndScaleOption屬性,因為預設的值會導緻vision從中間将你的輸入圖檔crop為正方形。 你可以更改該屬性的值來改變這個狀況:
request.imageCropAndScaleOption = .scaleFill
CoreML模型的輸入
CoreML模型期待的輸入是什麼格式的圖像呢?我們從3個方面來說:
1,RGB或者BGR?大多數模型訓練的時候使用的是RGB像素順序,而Caffe使用的是BGR。如果你的神經網絡架構使用的是OpenCV來加載圖像(又沒有做其它處理的話),那麼大機率使用的也是BGR像素順序。
2,是否歸一化?0~255還是0~1?
3,是否進行normalization?适用normalized = (data - mean) / std 對每個像素進行計算?
另外,有些模型自帶layer用來進行圖像的預處理,這個不在Gemfield本文的讨論範圍之内,需要你仔細檢查。
在iOS SDK中,CoreML模型期待的輸入就是CVPixelBufferRef,CVPixelBuffer通常包含的像素是ARGB或者BGRA 格式,每個通道是8bits where each color channel ,值為0~255。比如在由mobilenet_v3.mlmodel生成出來的頭檔案中,你可以看到如下的代碼:
@interface mobilenet_v3Input : NSObject<MLFeatureProvider>
/// image as color (kCVPixelFormatType_32BGRA) image buffer, 224 pixels wide by 224 pixels high
@property (readwrite, nonatomic) CVPixelBufferRef image;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithImage:(CVPixelBufferRef)image;
@end
是以這裡的問題就變成了——CoreML輸入的CVPixelBufferRef究竟是kCVPixelFormatType_32BGRA還是kCVPixelFormatType_32ARGB?究竟是0~255的取值還是0~1還是-1~1?究竟要不要适用mean還是std?
1,RGB還是BGR關于是RGB還是BGR,你在使用coremltools進行轉換的時候,通過is_bgr參數來指明:
args = dict(is_bgr=False, ......)
2,是否歸一化 使用image_scale參數來指明:
scale = 1.0 / 255.0
args = dict(is_bgr=False, image_scale = scale,......)
3,是否做normalization 标準的normalization的公式為:
normalized = (data - mean) / std,像素的值減去均值,再除以方差。CoreML使用的是類似的公式:
normalized = data*image_scale + bias。如此換算下來,大緻相當于:
red_bias = -red_mean
green_bias = -green_mean
blue_bias = -blue_mean
image_scale參數又要扮演方差的角色了:
image_scale = 1 / std
//bias也要除以std
red_bias /= std
green_bias /= std
blue_bias /= std
Gemfield來舉幾個例子:
1,輸入是0-255
image_scale = 1
red_bias = 0
green_bias = 0
blue_bias = 0
2,輸入是0 - 1
image_scale = 1/255.0
red_bias = 0
green_bias = 0
blue_bias = 0
3,輸入是-1 ~ 1
image_scale = 2/255.0
red_bias = -1
green_bias = -1
blue_bias = -1
4,Caffe模型
red_bias = -123.68
green_bias = -116.779
blue_bias = -103.939
is_bgr = True
//需要normalize的話,方差是58.8, 1/58.8 = 0.017
image_scale = 0.017
red_bias = -123.68 * 0.017
green_bias = -116.779 * 0.017
blue_bias = -103.939 * 0.017
is_bgr = True
5,PyTorch模型
PyTorch訓練的時候,一般會對圖像進行如下預處理:
def preprocess_input(x):
x /= 255.0
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
return (x - mean) / std
這個就麻煩了,因為每個通道的方差不一樣,而我們隻有一個image_scale參數來扮演這個角色!隻能大緻權重出來一個了方差了:
image_scale = 1.0 / (255.0 * 0.226)
red_bias = -0.485 / 0.226
green_bias = -0.456 / 0.226
blue_bias = -0.406 / 0.226
0.226就是這樣估計出來的。要想更精确,隻能手寫代碼來對每個像素進行計算了:
//R
normalizedBuffer[i] = (Float32(dstData.load(fromByteOffset: i * 4 + 2, as: UInt8.self)) / 255.0 - 0.485) / 0.229
//G
normalizedBuffer[width * height + i] = (Float32(dstData.load(fromByteOffset: i * 4 + 1, as: UInt8.self)) / 255.0 - 0.456) / 0.224
//B
normalizedBuffer[width * height * 2 + i] = (Float32(dstData.load(fromByteOffset: i * 4 + 0, as: UInt8.self)) / 255.0 - 0.406) / 0.225
順便的把BGR轉換成了RGB三個平面(從hwc到chw)。
總結
總結下來,這個轉換過程就是從PyTorch到ONNX,再從ONNX到CoreML,這個轉換過程不會總是一帆風順。前者從PyTorch到ONNX轉換所用到的函數都屬于PyTorch項目,是以出現問題的機率相對比較小;而從ONNX到CoreML轉換的過程中,除了幾個經典的網絡,或者極其簡單的網絡外,一般都會有各種各樣的問題,這是不同軟體生态之間磨合必然的代價。比如下面的一些錯誤:
NotImplementedError: Unsupported ONNX ops of type: Shape,Gather,Expand
再比如錯誤:
TypeError: Error while converting op of type: Conv. Error message: provided number axes -1 not supported
遇到類似錯誤,可以評論留言。