天天看點

c#和java部署pytorch同僚識别兩個圖檔_Pytorch轉NCNN的流程記錄

最近有一個比較火的ocr項目:chineseocr_lite[1],項目中很貼心地提供了ncnn的模型推理代碼,隻需要

  1. 交叉編譯opencv
  2. 添加一點bitmap轉cv::Mat的代碼
  3. 寫個簡單的界面
具體過程參考:安卓端深度學習模型部署-以NCNN為例 - 帶蘿蔔的文章 - 知乎 https:// zhuanlan.zhihu.com/p/13 7453394

就可以得到一個安卓端的OCR工具了。

可能因為項目針對的是通用的自然場景,是以對小尺寸文本的識别效果不太理想,我對psenet進行了重訓練,再轉成NCNN進行部署。

PyTorch轉NCNN的流程十分簡單,如果順利的話隻需要兩步:

  • PyTorch轉ONNX
torch.onnx._export(model, x, path, opset_version=11) 
           
  • ONNX轉NCNN
./onnx2ncnn model.onnx model.param model.bin 
           

可是世上哪有那麼多一帆風順的事,這篇文章記錄的就是模型重訓練之後轉成NCNN的過程中遇到的問題和解決方案。

重訓練代碼參考:wenmuzhou/PSENet.pytorch[2],backbone選擇了mobilenetv3。

注:下文中的resize、interp、interpolate都是一個意思

問題1: ReLU6不支援

概述:ReLU6算子在轉換的時候容易出現不支援的情況,需要使用其他算子替代

解決:使用torch.clamp替代(雖然ReLU6可以通過組合ReLU的方式實作,但是組合得到的ReLU6在NCNN中容易轉換失敗,不建議使用。)

def relu6(x,inplace=True):
     return torch.clamp(x,0,6) 
           

問題2:Resize算子轉換問題

概述:因為各個架構對Resize算子的支援都不盡相同[3],在轉換過程中總會出現一些問題,pytorch中的interpolate算子轉換成ONNX之後變成很多零散的算子,如cast、shape等,這些在ncnn裡面不支援。你可以選擇手動修改檔案[4],也可以使用下面這個自動的方法:

解決:使用onnx_simplifier[5]對onnx模型進行簡化,可以合并這些零散的算子。

python -m onnxsim model.onnx model_sim.onnx 
           

問題3:關于轉ONNX及使用onnx_simplifier過程中出現的一系列奇怪問題

概述:使用不同版本的ONNX可能會遇到不同的問題,比如提示conv層無輸入等(具體錯誤名稱記不清了)。

解決:下載下傳最新ONNX源碼編譯安裝(onnx_simplifier中出現的一些錯誤也可以通過安裝最新ONNX來解決)

git clone https://github.com/onnx/onnx.git
sudo apt-get install protobuf-compiler libprotoc-dev 
cd ONNX 
python setup.py install 
           

問題4:模型輸出結果的尺寸固定

概述:直接轉換得到的onnx模型的Resize算子都是固定輸出尺寸的,無論輸入多大的圖檔都會輸出同樣大小的特征圖,這無疑會影響到模型的精度及靈活性。

解決:修改NCNN模型的param檔案,将Resize算子修改成按比例resize。

直接轉換得到的param檔案中的Interp算子是這樣的:

Interp    913      1 1 901 913 0=2 1=1.000000e+00 2=1.000000e+00 3=640 4=640 
           

從下面的ncnn源碼中可以看到,0代表resize_type,1和2分别是高和寬的縮放比例,3和4分别是輸出的高和寬。

int Interp::load_param(const ParamDict& pd) 
{
     resize_type = pd.get(0, 0);
     height_scale = pd.get(1, 1.f);
     width_scale = pd.get(2, 1.f);
     output_height = pd.get(3, 0);
     output_width = pd.get(4, 0);
     return 0; 
} 
           

我們隻需将其修改成如下格式即可實作按比例resize:

Interp      913       1 1 901 913 0=1 1=4.000000e+00 2=4.000000e+00 
           

問題5:NCNN模型輸出結果與ONNX模型不同

解決:逐層對比NCNN與onnx模型的輸出結果

使用onnxruntime(Python)和NCNN(C++)分别提取每個節點的輸出,進行對比。對于ncnn比較簡單,可以使用

extractor.extract(node_name,preds); 
           

來提取不同節點的輸出。

問題5衍生問題:ONNX沒有提供提取中間層輸出的方法

解決:給要提取的層添加一個輸出節點,代碼[6]如下:

def find_node_by_name(graph, node_name):
         for node in graph.node:
                 if node.output[0] == node_name:
                         return node
         return None          
def add_extra_output_node(model,target_node, output_name):
         extra_output = helper.make_empty_tensor_value_info(output_name)
         target_output = target_node.output[0]
         identity_node = helper.make_node("Identity",inputs=[target_output],outputs=[output_name],name=output_name)
         model.graph.node.append(identity_node)
         model.graph.output.append(extra_output)
         return model 
           

修改模型之後再使用

out = sess.run([output_name],{"input.1":img.astype(np.float32)}) 
           

就可以擷取到模型的中間層輸出了。

問題5衍生問題:發現最後一個Resize層的輸出有差異

解決:參考chineseocr_lite裡面的代碼把mode由bilinear改成了nearest(這裡錯誤的原因可能是wenmuzhou/PSENet.pytorch中的模型最後一個F.interpolate中的align_corners參數設定成了True。據說NCNN隻實作了align_corners為False的情況[7])。

這裡修改之後的模型跟原模型之間是會有少許誤差的,如果誤差不可接受,就要重新訓練才行。

一些關于chineseocr項目的細節:

細節1:需要去掉推理代碼中的normalize步驟(原因應該是WenmuZhou/PSENet.pytorch的訓練過程中沒有使用normalize)。

細節2:wenmuzhou/PSENet.pytorch代碼中沒有把sigmoid加入到模型類中,而是放在了推理代碼中,在轉換ONNX的時候需要加上sigmoid。

細節3:角度檢測在對于小尺寸文字的識别精度不高,尤其是對較長的數字序列,可能需要重新訓練。

參考:

1. chineseocr_lite: https://github.com/ouyanghuiyu/chineseocr_lite

2. PSENet: https://github.com/WenmuZhou/PSENet.pytorch

3. 是什麼引起了各個架構 Resize 操作的結果不同: https://zhuanlan.zhihu.com/p/107761106

4. 手動修改ncnn模型檔案:https://zhuanlan.zhihu.com/p/93017149

5. onnx_simplifier: https://github.com/daquexian/onnx-simplifier

6. 修改onnx節點: https://github.com/bindog/onnx-surgery/blob/master/surgery.py

7. 關于align_corners: https://github.com/Tencent/ncnn/issues/1610

最後,對pytorch和移動端部署感興趣的話,來交個朋友吧,群号:747537854

繼續閱讀