ncnn运行silero_vad

840 阅读5分钟

最近看vad相关的库发现了一个使用神经网络进行检测的仓库,试了一下感觉效果还可以,刚好这个库提供了onnx,于是想把onnx转成ncnn的模型试试看,因为是第一次做模型转换,遇到了很多问题,整理了一下对应的解决方案以供参考

相关仓库

转换的onnx模型在该仓库的files的文件里面,也可以使用torhchub下载,仓库里面也有说明

ncnn提供了编译好的二进制文件,但我是用源码编译的二进制文件,建议自己编译下debug版本的工具和库,遇到一些意想不到的问题方便调试,onnx2ncnn的源码也在仓库里面

简化onnx算子的库,提供了onnxsim

onnx转换相关工具,里面有一个onnxconverter_common.onnx2py的文件可以互相转换onnx和python文件,使用pip安装即可

pip install onnxconverter_common

编译ncnn需要用到的库

编译ncnn[可选]

开始我并不打算编译ncnn,但是因为转换过程中遇到了很多问题的错误提示信息比较模糊,只能编译一个debug版本来进行调试

ncnn的编译问题不大,但是因为ncnn提供的onnx2ncnn的工具使用protobuf,不幸的是我在安装protobuf出了问题,于是我又编译了下protobuf

  • protobuf编译

以下是编译相关命令

cmake . -B build -DCMAKE_INSTALL_PREFIX:PATH=install -Dprotobuf_BUILD_SHARED_LIBS=ON
cmake --build build --target install
  • 修改ncnn的CMakeLists.txt

参考下面的文档

github.com/Tencent/ncn…

  • ncnn编译
cmake . -B build

ncnn我是用visual studio打开来运行并调试的,所以只生成了工程文件

onnx转ncnn

  • 删除If算子

原始的onnx包含If很多算子,直接使用onnxsim没有办法删除,我尝试了一些修改onnx的工具,但是感觉效果并不是很好,最后是使用下面的命令

python -m onnxconverter_common.onnx2py silero_vad.onnx model.py

把onnx转换成python文件,然后修python代码再转换成onnx,幸运的是我删除了一个If算子后使用onnxsim后就把其余的If都简化了

  • 删除Slice算子

转换完的onnx使用onnx2ncnn仍然会报下面的错误

 Unsupported slice step !

查了相关的文档,很多都是转yolov5的模型遇到的问题,具体可以看一下github.com/Tencent/ncn… 里面的讨论

因为不知道这里问题出在哪,我查看了onnx2ncnn.cpp里面打印错误的位置,发现是onnx里面的Slice算子的step的值在onnx2ncnn里面不支持,虽然并不清楚是ncnn本身的算子不支持还是onnx2ncnn的工具没有支持

我看用netron看一下onnx的模型结构,里面有两个Slice的step为-1导致的,-1作用是对输入的数据进行翻转,我尝试删除了对应的Slice后测试了下对最终的结果影响不大,所以就没有再尝试别的方案

加载ncnn

通过上面的操作我已经可以把onnx转换成对应的ncnn模型,正当我尝试加载ncnn模型的时候,又遇到了新的问题

  • load_param 报find_blob_index_by_name 144_splitncnn_-1

ncnn的仓库里面有关于这个错误的一些讨论,总结下来就是ncnn的param文件里面的有算子的输入层找不到,我查看了param的模型文件确实出现了很多splitncnn_后面为负数的层名字,推测还是onnx2ncnn的处理有问题,于是我又调试下代码同时也阅读了下代码的逻辑

onnx2ncnn在转换Slice层会替换成Split和Crop两个算子,Split把输入数据分成多个部分,而Crop分割Split的输出,正常来说Split应该输入应该是Crop的输出,而实际上Crop并没有用Split的输出而是用了MemoryData

找到了问题的位置后面就是调试onnx2ncnn的源码,最终发现问题是Squeeze和Unsqueeze这两个算子处理有问题导致的,在onnx2ncnn.cpp后添加

else if (op == "Squeeze")
{
    if (node.input_size() == 2) {
        node_reference[node.input(1)] -= 1;
    }
}
else if (op == "Unsqueeze")
{
    if (node.input_size() == 2) {
        node_reference[node.input(1)] -= 1;
    }
}

重新编译onnx2ncnn,再次转换后加载模型就成功了

运行ncnn对齐结果

虽然模型转换成功,但是运行的结果却完全不对,这种问题只能一层一层和onnx的模型去对结果

  1. onnx获取中间层结果是通过在onnx对应的python代码里添加output层获取输出
  2. ncnn则是使用extract来获取
  • Conv卷积结果不正确

这个问题应该也是ncnn2onnx转换的问题,我把ncnn对应的param里面Convolution1D替换成ConvolutionDepthWise1D后就正常了

  • BinaryOp里Pow结果不正确

这个可以参考下面的ncnn里面的isuue

github.com/Tencent/ncn…

github.com/Tencent/ncn…

github.com/Tencent/ncn…

总结下是因为Pow的实现在部分平台上有问题,导致输入为0时结果不正确,如果只是平方操作,比较简单的方案是把Pow换成Mul就可以

BinaryOp Pow_89 2 1 188 188 193 0=2

用上面两个输入层相乘替换下面的Pow即可

BinaryOp Pow_89 1 1 188 193 0=6 1=1 2=2.000000e+00

总结

本来以为onnx转ncnn应该是一件比较容易的事情,实际操作后才发现还是有很多问题,实际上onnx转ncnn的中途我还尝试过pnnx的方案,不过没有成功.虽然花费了很长的时间,但还是学习到了许多自己之前不了解的内容,受限于本人的水平上面的办法可能不是最好的,也欢迎指正

参考资料

  1. github.com/Tencent/ncn…

  2. onnx.ai/onnx/operat…

  3. github.com/Tencent/ncn…