最近有一個比較火的ocr項目:chineseocr_lite[1],項目中很貼心地提供了ncnn的模型推理代碼,隻需要
- 交叉編譯opencv
- 添加一點bitmap轉cv::Mat的代碼
- 寫個簡單的界面
具體過程參考:安卓端深度學習模型部署-以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