本文记录mxnet转ncnn的其中一个坑。前面几个都成功了,最后一条调试了一天终于搞定。
- caffe转ncnn mobilenet ssd物体检测
- mxnet转ncnn vgg物体检测
- mxnet转ncnn vgg文本检测
- mxnet转ncnn mobilenet ssd物体检测
- mxnet转ncnn mobilenet ssd文本检测
背景
故事的背景是:
用mxnet训练的基于ssd的文本检测模型。准确率不错,但是放到tango上需要10s。更换成mobilenet+ssd的方式大约3s。考虑用mobilenet+ssd的方式。ncnn的mobilenet+ssd是caffe训练的,用于物体检测。速度500ms,看起来收益不错。之前陆续解决了mobilenet ssd物体检测,vgg文本检测。但最后一步将mobilenet ssd用于文本检测,模型转化结果总是不对。调试了一天终于搞定。最终在手机上达到500ms的速度。本文记录调试过程。
昨天中午完成了vgg文本检测模型的转化,结果没有问题。但用mobilenet ssd文本检测总是出问题。没有任何检测结果。刚开始以为是mxnet版本或者网络结构的问题。对mxnet版本,网络结构都做了不同的实验,搞了一下午没有收获。
转机
然后抱着试试看的态度,用当初那台训练mxnet mobilenet ssd,并且成功转化为ncnn的机器来训练文本检测。
切换代码到文本检测分支(其实心里是不确定的,因为这样等于说,这两台机器训练代码基本没有区别了), 开始训练。训练后,一转模型之后,居然就可以了!
有一台可以,那么离成功也就不远了。
定位
仔细比对代码之后,发现两者的网络结构的确没有差别,代码版本也没有差别。
这时候突然想起之前搞pytorch的经历,用过pytorch的都知道,pytorch分布式训练,cpu或者gpu训练保存的模型是不一样的,需要不同的加载方式。
所以就开始怀疑是否mxnet也像pytorch这么愚蠢。因为转换模型成功的确实是单卡训练,失败的是多卡训练。
这样简单做个试验,在多卡机器上,把多卡模型加载进去,用单卡微调几个epoch,再保存出来,转一下模型,居然真的没有问题。那么就确定问题出在多卡上面了。
原因
问题解决到这里变成可重现的了。那就好办了。本来这样可以告一段落了,但是担心这只是问题的冰山一角,下次可能还会猜到坑里,于是就继续追查原因。
仔细看mxnet模型加载,保存代码,没有看出任何端倪。而且mxnet的模型都有一个magic来保护,不至于会加载错误。将转化时候,每层的shape都打印出来没有发现。
刚开始以为是batch norm的moving_mean, moving_var的问题,因为这部分数据保存在auxs上,调试后发现无误。
本来想通过把某个批次的多gpu模型加载进去,再用单gpu保存出来,看看是否数据相同来看是否模型flag导致了结果的差异。后来发现这样操作后,结果也是错的。无法用这种方式来确定差异。
只剩下最后一种方式了,把每层的权重打印出来。
cnn的权重维度都很高,只打印前面10维。如果10维没有问题就基本没有问题了。如果有问题应该很明显。因为无法取得同一个epoch多gpu和单gpu的模型数据,只能用相邻批次来代替。打印如下:
正确的:
/Users/sooda/deep/framework/ncnn/cmake-build-debug/tools/mxnet/mxnet2ncnn
shape0: 128
0.227 -0.476 0.117 0.261 -0.478 0.037 0.088 0.220 -0.224 0.824 0.463 0.188
shape1: 512
0.000 -0.000 0.000 0.000 0.000 -0.000 0.000 0.000 -0.000 0.000 0.000 0.000
...
shape8: 128
1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000
...
shape14: 128
1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000
有问题的:
/Users/sooda/deep/framework/ncnn/cmake-build-debug/tools/mxnet/mxnet2ncnn
shape0: 128
0.227 -0.477 0.120 0.262 -0.479 0.040 0.091 0.223 -0.226 0.825 0.468 0.190
shape1: 512
...
shape8: 128
0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
...
shape14: 128
0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
从上面权重可以看出,第8层,第14层权重明显有问题。
再将其名称打印出来,发现都是batchnorm的gamma。我们都知道batchnorm层的设置fix gamma的时候,权重是1. 而且根据batchnorm的计算方式,gamma为0简直不可理解。
一定有猫腻。想起了之前知乎上看到mxnet member @piiswrong抱怨,batchnorm实现起来非常麻烦。就猜想应该是mxnet实现上的问题。
于是去看到mxnet batchnorm的实现,一番阅读,你猜看到了啥。如果权重为0,就将其设置为1
const AccReal *weight = weights.dptr<AccReal>();
const AccReal w = weight ? weight[channel] : AccReal(1);
没想到啊,mxnet居然也写如此ugly的代码。
在ncnn转换中,加入同样的判断,搞定。
猜想
mxnet之所以这么实现,在多gpu模型下应该是假定所有参数都是可学习的,对于不需要学习(fix gamma时候,gamma是1)的怎么处理呢?有没有什么方式来区别呢?将其设置为0??
只是猜想,待验证。
总结
- 运气。如果不是一开始用单gpu的来转换mxnet mobilenet ssd。那么一开始就会遇到这个问题,这时候变量很多,哪里都会有问题,调试起来可能还需要脱层皮。
- 积累挺重要。pytorch的经历帮助排查。
- 知乎上看看水文,多了解别人的看法也是有帮助的。
- 不要过分迷信某些大牛的实现
- 理解代码原理是很重要的。比如说fix gamma表示什么。batch norm有哪些控制参数。
- 不要怕麻烦,把权重打印出来,一起尽在眼前。