天天看點

pytorch unsqueeze_轉換PyTorch模型到CoreML

pytorch unsqueeze_轉換PyTorch模型到CoreML

背景

Apple官方雖然不支援pytorch到coreml的直接轉換。然而借助蘋果的coremltools、pytorch的onnx、社群的onnx到coreml的轉換工具這三大力量,這個轉換過程還是很容易的。

本文以PyTorch 1.4為基礎,以。将PyTorch模型轉換為CoreML模型分為如下5個基本步驟:

  1. 使用PyTorch訓練并儲存一個模型(并對save的模型進行測試);
  2. PyTorch模型轉換為ONNX模型;
  3. ONNX模型轉換為CoreML模型;
  4. 在macOS上使用python腳本驗證該模型;
  5. 內建到XCode上然後在iOS上驗證該模型。

下面分步驟介紹下。

使用PyTorch訓練并儲存一個模型

這是PyTorch的基礎了,在此不予贅述。你可以參考專欄文章:

Gemfield:詳解Pytorch中的網絡構造​zhuanlan.zhihu.com

pytorch unsqueeze_轉換PyTorch模型到CoreML

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-coreml​github.com

pytorch unsqueeze_轉換PyTorch模型到CoreML
1,安裝依賴
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.py​github.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
           

遇到類似錯誤,可以評論留言。