date: 2019/01/07


这一两个月比较忙,没什么时间空下来写写博文,加上最近处于摸索阶段,各种思路还没有理清,不敢瞎写。


这两天看到Lyken17的pytorch-OpCounter,萌生了一个写一个MXNet的计数器的想法,项目已经开源到github上,并且做个pip的包,嘻嘻……第一次做包,虽然只是一个简单的工具,还是截图留个念——

mxop-pip.png


参数量与计算量

(注意,还有另外一种MAC(Memory Access Cost,访存开销))


这两者其实是评估模型时非常重要的参数,一个实际要应用的模型不应当仅仅考虑它在准确率上有多出色的表现,还应该要考虑它的鲁棒性、扩展性以及对资源的依赖程度,但事实上很多论文都不讨论他们模型需要多少计算力,可能是他们的定位还是纯学术研究——提出一种新的思路,即使这种思路不便于应用,但未来说不定计算力上来了,或者有什么飞跃性的改进方法来改进这一问题,或者提出自己的思路来启发其他研究者的研究(抛砖引玉)。


接下来,我们试着用一个新视角重新审视以前那些常用的CNN OP。


全连接

首先考虑一个3输入、3输出、有偏置的全连接层(Layer2),

fc.png


其参数数量为 ,乘加次数为

这是一个典型的矩阵和向量之间的乘法运算,      

推广到n输入、m输出、有偏置的全连接层,其参数数量为 ,乘加次数为 。  


卷积

首先考虑一个单通道输入输出,输出图大小为 ,核大小为 ,带偏置,步长1,不补零的卷积,

convolution.gif

其参数数量为 ,乘加次数为 。        

推广到通道输入,通道输出的情况,其参数数量为 ,乘加次数为 。       


这是标准卷积的情况,如果是深度向分解的卷积,参考博文《MobileNets v1模型解析/深度向卷积分解/效率比较 | Hey~YaHei!》可以知道,其参数数量为 ,乘加次数为 ,这里为简化运算忽略了偏置。


池化

池化跟卷积的操作比较相近,最大池化仅仅是比较操作,其计算量往往可以忽略不计;平均池化则会涉及到 次加法和 次除法(输入输出通道均为 c),两者都没有参数。


批归一化BN

假设输入数量为N,

可以很容易看到,其参数数量为 2N,运算包含 2N 次加法(包括减法)和 N 次乘法。

这里要注意,在推断过程中,variance和mean都是已知的,所以 可以直接合并为一个值,

甚至,博文《MobileNet-SSD网络解析/BN层合并 | Hey~YaHei!》提到过BN层可以直接融入前边的线性层(如卷积和全连接),此时BN层不会造成任何开销。


OpSummary

代码:hey-yahei/OpSummary.MXNet | github

知道了如何计算各层的参数数量和运算次数,我们就可以编写一个小工具来为MXNet模型统计参数数量和计算量。


钩子

为了计算运算量,必须能够取得每一层的参数、输入和输出大小,当然可以根据各层的参数一层层的推算,但这似乎太麻烦了。受Lyken17的pytorch-OpCounter启发,我们可以为每个Block注册一个hook,每次Block经过前向传播后都会调用这个hook(准确的说,有两种hook,pre_hook在前向传播前调用,hook在前向传播后调用)。


超参数获取

MXNet中想读取一个Block的超参数实在有些麻烦,因为它把超参数全都存在私有属性里了!(mxnet/gluon/nn/conv_layers.py#L105 | github),不像Pytorch的Module是直接把超参数放在公共属性上(torch/nn/modules/conv.py#L20 | github)。

有两种思路来获取超参数——

  1. 从输入输出、公共变量(如Conv的weight和bias)的shape来推断
  2. 解析字符串
    MXNet的Block都重载了 __repr__ 方法,比如mxnet/gluon/nn/conv_layers.py#L143 | github,用于打印Block的超参数。那……我们其实可以用 str(nn.Block) 的方式来取得这个字符串,然后进行解析= =好麻烦啊


统计

yken17的pytorch-OpCounter在统计各个模块的参数数量和运算次数时,是注册了一个公共的缓冲区来进行累加(参考pytorch-OpCounter/thop/utils | github),而MXNet并没有提供这样的缓冲区(或许只是我不知道?),我的解决办法是——

写一个拥有静态变量的函数作为累加函数,调用Block的apply方法,让每个Children把自己的统计结果依次累加给这个静态变量,最后从静态变量取出统计结果。


结果

用OpSummary把MXNet提供的model_zoo里所有的模型都测试了一遍,结果如下表所示:

Top1 Acc和Top5 Acc数据来源于 MXNet文档

ModelParams(M)Muls(G)*Params(M)*Muls(G)Top1 AccTop5 Acc
AlexNet61.100.712.470.660.54920.7803
VGG11132.867.619.227.490.66620.8734
VGG13133.0411.309.4011.180.67740.8811
VGG16138.6315.4714.7115.350.73230.9132
VGG19143.6719.6320.0219.510.74110.9135
VGG11_bn132.877.629.237.490.68590.8872
VGG13_bn133.0611.329.4211.200.68840.8882
VGG16_bn138.3715.4814.7315.360.73100.9176
VGG19_bn143.6919.6520.0519.520.74330.9185
Inception_v323.875.7221.825.720.77550.9364
ResNet18_v111.701.8211.191.820.70930.8992
ResNet34_v121.813.6721.33.670.74370.9187
ResNet50_v125.633.8723.583.870.76470.9313
ResNet101_v144.707.5942.657.580.78340.9401
ResNet152_v160.4011.3058.3611.300.79000.9438
ResNet18_v211.701.8211.181.820.71000.8992
ResNet34_v221.813.6721.303.670.74400.9208
ResNet50_v225.604.1023.554.100.77110.9343
ResNet101_v244.647.8242.597.810.78530.9417
ResNet152_v260.3311.5458.2811.530.79210.9431
DenseNet1218.062.857.042.850.74970.9225
DenseNet16128.907.7626.697.760.77700.9380
DenseNet16914.313.3812.643.380.76170.9317
DenseNet20120.244.3218.324.310.77320.9362
MobileNet_v1_1.004.250.573.230.570.71050.9006
MobileNet_v1_0.752.600.331.830.330.67380.8782
MobileNet_v1_0.501.340.150.830.150.63070.8475
MobileNet_v1_0.250.480.040.220.040.51850.7608
MobileNet_v2_1.003.540.322.260.320.71920.9056
MobileNet_v2_0.752.650.191.370.190.69610.8895
MobileNet_v2_0.501.980.100.700.090.64490.8547
MobileNet_v2_0.251.530.030.250.030.50740.7456
SqueezeNet1_01.250.820.740.730.56110.7909
SqueezeNet1_11.240.350.720.260.54960.7817


由于分类网络经常用作其他框架(如目标检测的SSD)的backbone,所以这里增加了*Params列和*Muls列用于表示除去最后几个分类的Layer之后的结果。具体丢弃的层参见 mxop/tests/test_gluon_utils.py | github 文件的 dropped_layers 变量。

Parameters.jpg

Multiplication.jpg


emmm还是比较直观的嘛!希望上述的图表对大家挑选backbone的时候能有所帮助~


下一步

目前我只模仿Lyken17的pytorch-OpCounter实现了简单的参数与计算量计数,并且制作了pip包(你可以按照我github页面上的说明用pip安装 mxop 包);

等之后有时间,我想继续


  1. 依次输出各个层的参数与计算量而不是整个模型,分析各个层的比例
  2. 支持MXNet的静态图模型(根据json文件解析参数并推算参数量和计算量,而不是用动态图的hook)
  3. 支持MXNet的量化模型
  4. ……