前段时间看了几个笔试题,涉及模型复杂度,主要是参数量和计算复杂度的问题。当时搜了一下感觉中文网上的内容比较乱。刚好本文是对神经网络模型资源消耗情况的一篇介绍,就不自己写了,把关键内容搬运一下。
原文见 http://machinethink.net/blog/how-fast-is-my-model/ 。
深度网络的计算消耗是学术 paper 相对少见的话题。当然,早期网络精度不够的情况下讨论压缩也没有意义。工程师需要实现模型并让网络尽可能地在各类环境下工作,模型的资源消耗情况和运行速度非常关键。
原文以移动端的模型应用为例,列出了四个主要问题:
- 空间占用——单个模型的参数文件要占用多大空间
- 内存占用——运行在手机或平板上时需要占用多大的 RAM
- 运行速度——尤其考虑实时的视频和大图像处理情形
- 耗电情况——我可不想要暖手宝
原文内容还是挺丰富的。为了节约时间,博主只把关键的部分略作摘录和演绎,即网络中一般操作的运算量、内存占用的计算方法,其他的如 MobileNet 模型的优化就不做介绍了。如有需要请查阅原文。
原文以外的内容将斜体显示,以作区别。
正文
案例:作者的一位客户最近用 MobileNetV2 替换掉了 V1 模型,按理说V2 的计算量远小于 V1 ,
(网上随手找的图:https://blog.youkuaiyun.com/u011995719/article/details/79135818)
然鹅实际情况却是 V2 比 V1 慢得多。
(注:可参考
https://www.zhihu.com/question/265709710/answer/299136290,https://www.reddit.com/r/MachineLearning/comments/8a7sf6/d_mobilenet_v2_paper_said_depthwise_separable/。
官方已经放出模型 https://github.com/tensorflow/models/tree/master/research/slim/nets/mobilenet 页面上也有实验测试结果。看完全文也会发现 V2 不比 V1 慢。作者这里有点标题党。)
为什么会这样呢?
1.计算消耗
可以用 FLOPS(floating point operations per second,每秒浮点运算数)来衡量模型的速度。另一种方法是 MACCs(multiply-accumulate operations,乘-加操作),也叫 MAdds。但说穿了,都是点积运算而已。
什么叫乘-加?神经网络里的运算大都是这样的:
y = w[0]*x[0] + w[1]*x[1] + w[2]*x[2] + ... + w[n-1]*x[n-1]
w
和 x
都是向量,y
是标量。上式是全连接层或卷积层的典型运算。一次乘-加运算即一次乘法+一次加法运算,所以上式的 MACCs 是n
。
不过可以看到,加法运算的次数并非 n
而是 n-1
。但考虑 MACCs 时可以类比算法复杂度估算的 big-O
,即结果可以是近似的。
而换到 FLOPS 的情况,点积做了 2n-1
FLOPS,即 n-1
次加法和 n
次乘法。可以看到,MACCs 大约是 FLOPS 的一半。
1.1 全连接层
全连接层的计算
y = matmul(x, W) + b
权重 W W W 是一个 I × J I \times J I×J 矩阵,输入 x x x 是 I I I 维实值向量, b b b 是 J J J 维偏置。输出 y y y 也是 J J J 维实值向量。FC 层的 MACCs 也不难计算。
上文例子是向量与向量的点积,FC 是向量与矩阵的点积,每一组点积发生在输入 x x x 同权重 W W W 某一列之间,计有 I I I MACCs,一共要计算 J J J 组点积,所以 FC 层的 MACCs 总计 I × J I \times J I×J,跟权重的尺寸一致。
偏置项 b b b 对 MACCs 的影响可以忽略不计。而上面也提到 MACCs 中加法比乘法少一次, b b b 刚好补上了这个缺。
所以,对 I I I 的输入、权重为 I × J I \times J I×J 的权重矩阵和 J J J 的输出,MACCs 为 I × J I \times J I×J ,FLOPS 为 ( 2 I − 1 ) × J (2I-1) \times J (2I−1)×J 。
举例:
一个全连接层,输入 100 维,输出 300 维,MACCs 有 300 × 100 = 30 , 000 300 \times 100 = 30,000 300×100=30,000 。不过,如果一个全连接层紧接着卷积层,输入可能没有指定长度 I I I 但有 feature map 的尺寸比如(512, 7, 7)。在 Keras 里就需要写一行 Flatten 把它展平,这样此时的 I I I 就是 512 × 7 × 7 512 \times 7 \times 7 512×7×7 了。
1.2 激活函数
FC 完了接下来通常有个激活函数,ReLU 或者 Sigmoid。激活函数的计算没有点积,所以只用 FLOPS 衡量。
对输出为 J J J 的 FC 层,ReLU 有 J J J FLOPS:
y = max(x, 0)
相比之下 Sigmoid 就复杂很多。
y = 1/(1+exp(-x))
我们把加减乘除、指数、平方根等等运算都算作一次 FLOPS,这里有除法、加法、指数和减法四种运算,所以 FLOPS 就是 J × 4 J \times 4 J×4 。
相对于全连接的矩阵运算,激活函数的计算量通常忽略不计*(博主注:不一定,看情况)*。
1.3 卷积层
卷积层要单独算而不是用全连接层的结论,是因为输入至少是三维的: H × W × C H \times W \times C H×W×C 。对于这样的卷积层,MACCs 有:
K × K × C i n × H o u t × W o u t × C o u t K \times K \times Cin \times Hout \times Wout \times Cout K×K×Cin×Hout×Wout×Cout
解释一下:
- 输出的 feature map 里每个通道上有 H o u t × W o u t Hout \times Wout Hout×Wout 个元素,
- 权重以 K × K K \times K K×K 大小的窗口,在所有的 C i n Cin Cin 个通道上做点积,
- 共有 C o u t Cout Cout 个卷积核,上述操作重复了 C o u t Cout Cout 次
同样,这里也忽略了偏置和激活函数。不应该忽略的是 stride(步长)、dilation factors(漏孔/膨胀卷积)、padding(填充),这就是为什么直接从输出尺寸 H o u t × W o u t Hout \times Wout Hout×Wout 开始算的原因——都已经考虑在内了。
举例:
3
×
3
3 \times 3
3×3 卷积,128 个 filer,输入的 feature map 是
112
×
112
×
64
112 \times 112 \times 64
112×112×64 ,stride=1
,padding=same
,MACCs 有:
3 × 3 × 64 × 112 × 112 × 128 = 924 , 844 , 032 3 \times 3 \times 64 \times 112 \times 112 \times 128 = 924, 844, 032 3×3×64×112×112×128=924,844,032
接近十亿的乘-加操作。
1.4 Batch Normalization
计算公式:
z = gamma * (y - mean) / sqrt(variance + epsilon) + beta
首先以输入为卷积层的情况为例。
每个通道上都存在一组 mean
、beta
、gamma
、variance
,
C
C
C 个通道就有
C
×
4
C \times 4
C×4 个可学习的参数。而且 BN 是作用在每一个元素上的,这样看来,造成的 FLOPS 应该不少。
但有趣的是,在 BN 直接连接卷积层的情况下,即 Conv-BN-ReLU 时,通过一组推导,可以将 BN 的计算整合到卷积层当中*(注意这是 inference 的情况,跟训练阶段差别很大)*,从而消去的 BN 层造成的 FLOPS。如果是 Conv-ReLU-BN 的结构这一套就行不通了。
( BN 层的计算结合到 Conv 层中去,BN 层的 FLOPS 消失了,Conv 层需要乘一个常系数)
即从结果上来说,在 inference 时模型中的 BN 层实际被消去了。
1.5 其他层
像 Pooling 层虽然确实很关键,但没有用到点积运算,所以 MACCs 不能很好地衡量这部分计算消耗。如果用 FLOPS,可以取 feature map 的尺寸然后乘一个常系数。
如 maxpooling 层,stride=2
、filter_sz=2
(即输出保持相同尺寸),112 x 112 x 128
的feature map,FLOPS 就是 112 x 112 x 128 = 1,605,632
。相对卷积层和全连接层的运算,这个计算量比较小,所以也可以忽略不计。
RNN 这里不做讨论。简单来说,以 LSTM 为例,计算主要是两个大的矩阵乘法,sigmoid,tanh 和一些元素级的操作。可以看成两个全连接层的运算,所以 MACCs 主要取决于输入、输出和隐状态向量的尺寸。点积运算还是占了大头。
2. 内存占用
内存带宽其实比 MACCs 更重要。目前的计算机结构下,单次内存访问比单次运算慢得多的多。
对每一层网络,设备需要:
- 从主内存中读取输入向量 / feature map
- 从主内存中读取权重并计算点积
- 将输出向量或 feature map 写回主内存
涉及大量的内存访问。内存是很慢的,所以网络层的内存读写对速度有很大的影响,可能比计算耗时还要多。
2.1 权重的内存占用
全连接层有 I x J
大小的权重矩阵,加上偏置向量共计 (I + 1) x J
。
卷积层的 kernel 通常是正方形的,对 kernel_sz = K
和输入通道为 Cin
、输出 Cout
和额外的 Cout
个偏置的情况,共有 (K x K x Cin + 1) x Cout
个参数。对比之下卷积层的参数量远小于全连接。
举例:
全连接层有4096个输入和4096个输出,所以权重数 (4096+1) x 4096 = 16.8M
。
3 x 3
、48个卷积核,在64x64
、32个通道的输入上计算,共有 3 x 3 x 32 x 48 + 48 = 13, 872
个权重。
注意到此处卷积层的输入实际是全连接层的32倍(通道),输出是48倍,然鹅权重数只有后者的千分之一不到。全连接层的内存占用真的很可怕。
作者注:卷积层可以看作一个受限连接的全连接层,即权重对 k x k
以外的输入置零,不使用。
2.2 feature maps 和中间结果
CS231n 的 Lesson 9 专门花了很多篇幅讲 feature map 的计算,可以参考。
还是举例说明。卷积层的输入是 224x224x3
,把所有这些值读出来需要访问 150,528
次内存。 还要乘上这个KxKxCout
系数和滑动窗的倒数。拿 stride=2
, kernel
数为32的情况来说,输出的 feature map 尺寸为 112x112x32
,共计 401,408
次内存访问。
所以,每层的内存访问总数如下:
input = Hin x Win x Cin x K x K x Cout x 1/stride
output = Hout x Wout x Cout
weights = K x K x Cin x Cout + Cout
,按上例:
input = 224 x 224 x 3 x 3 x 3 x 32 x 1/2 = 21,676,032
output = 112 x 112 x 32 = 401,408
weights = 3 x 3 x 3 x 32 + 32 = 896
total = 22,078,336
当网络层数加深时,Hin
Win
会越来越小,但通道数会变得很大:
input = 28 x 28 x 256 x 3 x 3 x 512 = 924,844,032
output = 28 x 28 x 512 = 401,408
weights = 3 x 3 x 256 x 512 + 512 = 1,180,160
total = 926,425,600
这种情况下 weights 部分也会变得很大,所以是不能忽略的。
2.3 Fusion
这一节的意思是,像 ReLU 这样比较简单的运算,如果不做优化,在计算时近乎是从输入到输出做了一次拷贝。计算可以认为不耗时间,但内存访问还是有消耗的,所以可以把这一步同卷积层的计算合成,从而节省了一轮内存读写。
3. MobileNet V2 vs. V1
这部分作者讲了他认为 V2 不会比 V1 快的分析过程。结论跟开头博主引的图相近,即乘子都为1.0时,V2是显著快于V1的,但V2在乘子为1.4时速度比V1稍慢。
至于原因嘛,简单来说就是 V2 的层数更深,每层的输入输出参数读写导致内存访问量大增。因此作者认为影响 inference 速度的瓶颈其实不在 MACCs,而是内存访问数(memory accesses)。
V2 with multiplier=1.4 的速度略慢于 V1,但精度高出不少;V2 with multiplier=1.0 速度比 V1 快很多。可以根据需要进行取舍。官方页面上也给了很多实验参考。
然后作者对 VGG16 做了一点考察,结论很有意思。
VGG16 经常被当作图像方面的特征提取器,结构很简单,层数也不多,看起来好像计算比较多、内存访问会少一些,真的是这样吗?对比 MobileNet(输入按移动设备16:9的规格,是126x224
,可以算出以下结果:
VGG16 params: 15M
VGG16 MACCs : 8380M
VGG16 MAes : 8402M
所以更大的 feature map 导致了更多的内存访问。
4 结论
论文中 MobileNet V2 主要比较了 MACCs 和参数量,指出因为这两项规模更小所以速度更快。但实际上还要考虑内存访问的情况。
另外本文给出的 MACCs、内存访问、参数量都是估计值,只用于同类模型的复杂度比较,出了这个语境是毫无意义的。
进一步阅读
论文:
-
Convolutional Neural Networks at Constrained Time Cost by He & Sun (2014) gives a nice overview of the computation costs and trade-offs between depth, filter sizes, etc. in convnets.
-
Learning both Weights and Connections for Efficient Neural Networks by Han et al. (2015) has a table with the relative costs of computations versus memory accesses. Plus it talks about pruning neural networks, which is a cool topic in its own right.
工具:
-
Alchemy from fritz.ai lets you analyze ML models to see if they’re ready for mobile development.
-
Netscope shows the structure of models and also analyzes their computational cost. Currently supports Caffe only.
不过这俩工具还不支持对内存访问量的计算。