一、参考资料
神经网络参数量、计算量(FLOPS)、内存访问量(MAC)计算详解
深度学习中不同类型卷积的综合介绍:2D卷积、3D卷积、转置卷积、扩张卷积、可分离卷积、扁平卷积、分组卷积、随机分组卷积、逐点分组卷积等pytorch代码实现和解析。
二、相关介绍
1. 引言
1.1 为什么要计算模型参数量和计算量?
- 好的网络模型不仅要求精度准,还要要求模型的参数量和计算量不大,才能有利于部署。
- 统计模型的参数量和计算量可以用于不同网络模型之间的对比分析。
- 有的模型虽然参数量相同,但是可能因为连接方式和结构等不同而导致计算量不同。
1.2 计算量与参数量对比?
- 计算量是指网络模型需要计算的运算次数,参数量是指网络模型自带的参数数量多少。
- 计算量对应时间复杂度,参数量对应于空间复杂度。
- 计算量决定了网络执行时间的长短,参数量决定了占用显存的大小。
2. 参数量(Parameters
, Weights
)
参数量是模型中的参数的总和,跟模型在磁盘中所需的空间大小直接相关。对于 CNN 来说参数主要由 Conv/FC 层的 Weight 构成,当然其他的一些算子也有参数,不过一般忽略不计了。
参数量往往是被算作访存量(MAC)的一部分,因此参数量不直接影响模型推理性能。但是参数量一方面会影响内存占用,另一方面也会影响程序初始化的时间。
参数量会直接影响软件包的大小。当软件包大小是很重要的指标时,参数量至关重要,例如手机 APP 场景,往往对 APK 包的大小有比较严格的限制;此外有些嵌入式设备的 Flash 空间很小,如果模型磁盘所需空间很大的话,可能会放不下,因此也会对参数量有所要求。
除了在设计模型时减少参数量外,还可以通过压缩模型的方式降低软件包大小。例如 Caffe 和 ONNX 采用的 Protobuf 就会对模型进行高效的编码压缩。不过压缩模型会带来解压缩开销,会一定程度增加程序初始化的时间。
3. 计算量(FLOPs
)
计算量是模型所需的计算次数,反映了模型对硬件计算单元的需求。计算量一般用 OPs (Operations) ,即计算次数来表示。由于最常用的数据格式为 float32,因此也常常被写作 FLOPs (Floating Point Operations),即浮点计算次数。
模型的整体计算量等于模型中每个算子的计算量之和。而每个算子的计算量计算方法各不一致。例如对于 Elementwise Sum
来讲,两个大小均为 (N, C, H, W) 的 Tensor 相加,计算量为
N
∗
C
∗
H
∗
W
N * C * H * W
N∗C∗H∗W。
对于计算量主要有 MAdds
和 MFlops
两个概念。shufflenet的论文用的是MFlops
,Mobilenet用的是MAdds
,MAdds
是MFlops
的两倍。
3.1 乘加累积操作(MACs
, MAdds
)
MACs=MAdds=2*FLOPs
乘加累积操作(Multiply-Accumulate Operations, MACs
),MACs和MAdds是同一个概念。MACs
常常与FLOPs
概念混淆,实际上 1MACs 包含一个乘法操作和一个加法操作, 1MACs等于2倍FLOPs。
3.2 FLOPs
与FLOPS
FLOPS
:注意全大写,是 floating point operations per second
的缩写,意指每秒浮点运算次数,理解为计算速度。是一个衡量硬件性能的指标。
FLOPs
:注意s小写,是 floating point operations
的缩写(s表复数),意指浮点运算数,理解为计算量。可以用来衡量算法/模型的复杂度。
4. 访存量(MAC)
访存量(memory access cost, MAC),又称为内存访问量,是指模型运行时所需访问存储单元的字节大小,反映了模型对存储单元带宽的需求。访存量一般用 Bytes(或者 KB/MB/GB)来表示,即模型计算到底需要存/取多少 Bytes 的数据。
和计算量一样,模型整体MAC等于模型各个算子的访存量之和。对于 Elementwise Sum
来讲,两个大小均为 (N, C, H, W) 的 Tensor 相加,MAC为
(
2
+
1
)
∗
N
∗
C
∗
H
∗
W
∗
s
i
z
e
o
f
(
d
a
t
a
_
t
y
p
e
)
(2 + 1) * N * C * H * W * sizeof(data\_type)
(2+1)∗N∗C∗H∗W∗sizeof(data_type),其中 2 代表读两个 Tensor,1 代表写一个 Tensor。
5. 内存占用
内存占用是指模型 运行时 所占用的内存/显存大小。一般有工程意义的是最大内存占用,当然有的场景下会使用平均内存占用。这里要注意的是,内存占用 ≠ 访存量。 内存占用在论文里不常用,主要原因是其大小除了受模型本身影响外,还受软件实现的影响。例如有的框架为了保证推理速度,会将模型中每一个 Tensor 所需的内存都提前分配好,因此内存占用为网络所有 Tensor 大小的总和;但更多的框架会提供 lite 内存模式,即动态为 Tensor 分配内存,以最大程度节省内存占用(当然可能会牺牲一部分性能)。
和参数量一样,内存占用不会直接影响推理速度,往往算作访存量的一部分。但在同一平台上有多个任务并发的环境下,如推理服务器、车载平台、手机 APP,往往要求内存占用可控。可控一方面是指内存/显存占用量,如果占用太多,其他任务就无法在平台上运行;另一方面是指内存/显存的占用量不会大幅波动,影响其他任务的可用性。
三、通俗理解参数量的计算
下图中,输入特征图尺寸为 32x32x3
,用一个 5x5x3
的卷积核对其中某个位置的计算,这里计算的是一个点积,所以输出是一个单独的标量值。
因为卷积的操作是通过一个滑动窗口实现的,通过卷积操作,输出特征图尺寸为 28x28x1
。
如果有6个filter,输出特征图尺寸为 28x28x6
。
这就是一个最基础的卷积操作,那么这里用到的参数量是多少呢?我们只需要把每个filter的参数累加起来,当然,不要忘了加上bias偏置:5x5x3x6 + 6 = 456。
另外,计算卷积以后的输出的大小,如下图所示,N是输入图像的size,F是filter的size,stride是滑动的步长。
然而,从上图中最后一个例子可以看到,stride大于1的时候不一定能整除,此时,需要在原图像上加上一层padding填充,然后再用前面的公式计算即可。
另外,还有一个 maxpooling
操作,该操作会改变输入输出,但不会有参数。所以使用和计算卷积一样的公式计算即可。
四、参数量、计算量的数学表达
1. 卷积层
1.1 参数量
Conv2d(C_in, C_out, (K_h, K_w)):参数量为 C_in × C_out × K_h × K_w
对于卷积层来说,参数量就是卷积核里所有参数的数量。
假设,每个卷积核的尺寸是 D K ∗ D K ∗ M D_K * D_K*M DK∗DK∗M,一共有N个卷积核。
如果考虑bias偏置项,则标准卷积的参数量为: D K ∗ D K ∗ M ∗ N + N D_K * D_K * M * N + N DK∗DK∗M∗N+N
如果不考虑bias偏置项,则标准卷积的参数量为: D K ∗ D K ∗ M ∗ N D_K * D_K * M * N DK∗DK∗M∗N
从计算公式可以看出,参数量仅与卷积核有关,与输入尺寸无关。
1.2 计算量
原始论文:[1]
对于卷积层来说,输出的特征图都是进行一系列的乘加运算得到的。
假设,每个卷积核的尺寸是 D K ∗ D K ∗ M D_K * D_K * M DK∗DK∗M,一共有N个卷积核,输出的特征图尺寸是 H o u t ∗ W o u t H_{out} * W_{out} Hout∗Wout。
一次卷积乘法运算次数为: D K ∗ D K ∗ M D_K * D_K * M DK∗DK∗M
一次卷积加法运算次数为: D K ∗ D K ∗ M − 1 ( 27 个数相加,做 26 次加法运算 ) D_K * D_K * M-1 \quad \textcolor{red}{(27个数相加,做26次加法运算)} DK∗DK∗M−1(27个数相加,做26次加法运算)
一共进行 H o u t ∗ W o u t ∗ N 次卷积运算 ( 输出 f e a t u r e m a p 大小为 H o u t ∗ W o u t ∗ N ) H_{out} * W_{out}*N \ 次卷积运算 \quad \textcolor{red}{(输出feature \ map大小为H_{out} * W_{out}*N)} Hout∗Wout∗N 次卷积运算(输出feature map大小为Hout∗Wout∗N)
乘加运算总次数: ( 2 ∗ D K ∗ D K ∗ M − 1 ) ∗ ( H o u t ∗ W o u t ∗ N ) (2*D_K * D_K * M-1)*(H_{out} * W_{out}*N) (2∗DK∗DK∗M−1)∗(Hout∗Wout∗N)
乘加运算总次数的近似表示: 2 ∗ ( D K ∗ D K ∗ M ) ∗ ( H o u t ∗ W o u t ∗ N ) 2*(D_K * D_K * M)*(H_{out} * W_{out}*N) 2∗(DK∗DK∗M)∗(Hout∗Wout∗N)
通常,标准卷积的计算量只考虑乘法运算: ( D K ∗ D K ∗ M ) ∗ ( H o u t ∗ W o u t ∗ N ) (D_K * D_K * M) * (H_{out} * W_{out} * N) (DK∗DK∗M)∗(Hout∗Wout∗N)。
从计算公式可以看出,计算量与输出特征图的尺寸有关。
1.3 访存量(MAC)
原始论文:[1]
对于标准卷积,MAC的计算公式为:
M A C ( C o n v ) = M A C ( I n p u t ) + M A C ( W e i g h t ) + M A C ( O u t p u t ) = ( C i n ∗ H i n ∗ W i n + C i n ∗ D K ∗ D K ∗ C o u t + C o u t ∗ H o u t ∗ W o u t ) ∗ s i z e o f ( d a t a _ t y p e ) \begin{aligned} MAC(Conv)& =MAC(Input) + MAC(Weight) + MAC(Output) \\ & =(C_{in} * H_{in} * W_{in} + C_{in} * D_K * D_K * C_{out} + C_{out} * H_{out} * W_{out} ) * sizeof(data\_{type}) \end{aligned} MAC(Conv)=MAC(Input)+MAC(Weight)+MAC(Output)=(Cin∗Hin∗Win+Cin∗DK∗DK∗Cout+Cout∗Hout∗Wout)∗sizeof(data_type)
MAC对模型的推理速度至关重要,设计模型时需要予以关注。
1.4 注意
- 参数量只与定义的网络结构有关,和forward的任何操作无关。即定义好了网络结构,参数就已经确定了。FLOPs和不同的层运算结构有关。但是如果forward时在同一层(同一名字命名的层)多次运算,FLOPs不会增加。
Model_size = 4*params
模型大小约为参数量的4倍,4表示按照32位存储,即4字节。
2. 全连接层
2.1 参数量
Linear(M->N):参数量为M×N+N
假设,输入 C i C_i Ci 个神经元,输出 C o C_o Co 个神经元。
如果考虑bias偏置项,则参数量为: C i ∗ C o + C o C_i * C_o + C_o Ci∗Co+Co
如果不考虑bias偏置项,则参数量为: C i ∗ C o C_i * C_o Ci∗Co
2.2 计算量
假设,输入 C i C_i Ci 个神经元,输出 C o C_o Co 个神经元。
一个神经元乘法运算次数为: C i C_i Ci
一个神经元加法运算次数为: C o C_o Co
乘加运算总次数为: ( 2 ∗ C i − 1 ) ∗ C o (2*C_i - 1)*C_o (2∗Ci−1)∗Co
2.3 访存量MAC
输入: C i C_i Ci
输出: C o C_o Co
权重: C i ∗ C o C_i*C_o Ci∗Co
那么,上述三项之和为MAC: C i + C o + C i ∗ C o C_i+C_o+C_i*C_o Ci+Co+Ci∗Co
3. BN层
BatchNorm(N):参数量为 2N
参数量: 2 ∗ C i ( b n . w e i g h t + b n . b i a s ) 2*C_i \quad \textcolor{red}{(bn.weight + bn.bias)} 2∗Ci(bn.weight+bn.bias)
4. Embedding层
Embedding(N,W):参数量为 N × W
5. 代码示例
import torch
import torch.nn as nn
from torchvision import models
class MyModel(nn.Module):
def __init__(self, ): # input the dim of output fea-map of Resnet:
super(MyModel, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.gap = nn.AdaptiveAvgPool1d(1)
self.fc = nn.Linear(2048, 512)
def forward(self, input): # input is 2048!
x = self.conv1(input)
x = self.bn1(x)
x = self.relu(x)
x = self.gap(x)
x = self.fc(x)
return x
##############################
# 模型准备
model = MyModel()
blank = ' '
print('-----------------------------------------------')
print('| weight name | weight shape |')
print('-----------------------------------------------')
for index, (key, w_variable) in enumerate(model.named_parameters()):
if len(key)<=15: key = key + (15-len(key))*blank
w_variable_blank = ''
if len(w_variable.shape) == 1:
if w_variable.shape[0] >= 100: w_variable_blank = 8*blank
else: w_variable_blank = 9*blank
elif len(w_variable.shape) == 2:
if w_variable.shape[0] >= 100: w_variable_blank = 2*blank
else: w_variable_blank = 3*blank
print('| {} | {}{} |'.format(key, w_variable.shape, w_variable_blank))
key = 0
print('-----------------------------------------------')
输出:
-----------------------------------------------
| weight name | weight shape |
-----------------------------------------------
| conv1.weight | torch.Size([64, 3, 7, 7]) |
| bn1.weight | torch.Size([64]) |
| bn1.bias | torch.Size([64]) |
| fc.weight | torch.Size([512, 2048]) |
| fc.bias | torch.Size([512]) |
-----------------------------------------------
解释说明
- CNN卷积层参数量: D K ∗ D K ∗ M ∗ N = 7 ∗ 7 ∗ 3 ∗ 64 D_K * D_K * M * N=7*7*3*64 DK∗DK∗M∗N=7∗7∗3∗64;
- BN层参数量参数量: 2 ∗ C i = b n . w e i g h t + b n . b i a s = 64 + 64 2*C_i=bn.weight+bn.bias=64+64 2∗Ci=bn.weight+bn.bias=64+64;
- FC全连接层参数量: C i ∗ C o + C o = 2048 ∗ 512 + 512 C_i * C_o + C_o = 2048*512 + 512 Ci∗Co+Co=2048∗512+512。
五、常见网络模型的参数量
LeNet-5
LeNet-5, convolutional neural networks
Gradient-Based Learning Applied to DocumentRecognition
LeNet-5网络结构如下:
LeNet-5网络参数量如下:
网络层(操作) | 输入 | filter | stride | padding | 输出 | 计算公式 | 参数量 |
---|---|---|---|---|---|---|---|
Input | 32x32x1 | 32x32x1 | 0 | ||||
Conv1 | 32x32x1 | 5x5x1x6 | 1 | 0 | 28x28x6 | 5x5x1x6+6 | 156 |
MaxPool1 | 28x28x6 | 2x2 | 2 | 0 | 14x14x6 | 0 | |
Conv2 | 14x14x6 | 5x5x6x16 | 1 | 0 | 10x10x16 | 5x5x6x16+16 | 2416 |
MaxPool2 | 10x10x16 | 2x2 | 2 | 0 | 5x5x16 | 0 | |
FC1 | 5x5x16 | 120 | 5x5x16x120+120 | 48120 | |||
FC2 | 120 | 84 | 120x84+84 | 10164 | |||
FC3 | 84 | 10 | 84x10+10 | 850 |
参数总量: 61706
AlexNet
ImageNet Classification with Deep Convolutional Neural Networks
AlexNet网络结构如下:
AlexNet的结构图有些奇怪。但其实是因为要把网络拆分到两个GPU上,才画成了两层,两层的结构是一样的,下面计算的时候的结构相当于合并以后的网络。
AlexNet网络参数量如下:
网络层(操作) | 输入 | filter | stride | padding | 输出 | 计算公式 | 参数量 |
---|---|---|---|---|---|---|---|
Input | 224x224x3 | 224x224x3 | 0 | ||||
Conv1 | 224x224x3 | 11x11x3x96 | 4 | 0 | 55x55x96 | 11x11x3x96+96 | 34,944 |
MaxPool1 | 55x55x96 | 3x3 | 2 | 0 | 27x27x96 | 0 | |
Norm1 | 27x27x96 | 27x27x96 | 0 | ||||
Conv2 | 27x27x96 | 5x5x96x256 | 1 | 2 | 27x27x256 | 5x5x96x256+256 | 614,656 |
MaxPool2 | 27x27x256 | 3x3 | 2 | 0 | 13x13x256 | 0 | |
Norml2 | 13x13x256 | 13x13x256 | 0 | ||||
Conv3 | 13x13x256 | 3x3x256x384 | 1 | 1 | 13x13x384 | 3x3x256x384+384 | 885,120 |
Conv4 | 13x13x384 | 3x3x384x384 | 1 | 1 | 13x13x384 | 3x3x384x384+384 | 1,327,488 |
Conv5 | 13x13x384 | 3x3x384x256 | 1 | 1 | 13x13x256 | 3x3x384x256+256 | 884,992 |
MaxPool3 | 13x13x256 | 3x3 | 2 | 0 | 6x6x256 | 0 | |
FC6 | 6x6x256 | 4096 | 6x6x256x4096+4096 | 37,752,832 | |||
FC7 | 4096 | 4096 | 4096x4096+4096 | 16,781,312 | |||
FC8 | 4096 | 1000 | 4096x1000+1000 | 4,097,000 |
参数总量: 62,378,344
VGG-16
Very Deep Convolutional Networks for Large-Scale Image Recognition
VGG-16网络结构如下:
VGG-16网络参数量如下:
网络层(操作) | 输入 | filter | stride | padding | 输出 | 计算公式 | 参数量 |
---|---|---|---|---|---|---|---|
Input | 224x224x3 | 224x224x3 | 0 | ||||
Conv3-64 | 224x224x3 | 3x3x3x64 | 1 | 1 | 224x224x64 | 3x3x3x64 + 64 | 1,792 |
Conv3-64 | 224x224x64 | 3x3x64x64 | 1 | 1 | 224x224x64 | 3x3x64x64 + 64 | 36,928 |
MaxPool2 | 224x224x64 | 2x2 | 2 | 0 | 112x112x64 | 0 | |
Conv3-128 | 112x112x64 | 3x3x64x128 | 1 | 1 | 112x112x128 | 3x3x64x128 + 128 | 73,856 |
Conv3-128 | 112x112x128 | 3x3x128x128 | 1 | 1 | 112x112x128 | 3x3x128x128 + 128 | 147,584 |
MaxPool2 | 112x112x128 | 2x2 | 2 | 0 | 56x56x128 | 0 | |
Conv3-256 | 56x56x128 | 3x3x128x256 | 1 | 1 | 56x56x256 | 3x3x128x256 + 256 | 295,168 |
Conv3-256 | 56x56x256 | 3x3x256x256 | 1 | 1 | 56x56x256 | 3x3x256x256 + 256 | 590,080 |
Conv3-256 | 56x56x256 | 3x3x256x256 | 1 | 1 | 56x56x256 | 3x3x256x256 + 256 | 590,080 |
MaxPool2 | 56x56x256 | 2x2 | 2 | 0 | 28x28x256 | 0 | |
Conv3-512 | 28x28x256 | 3x3x256x512 | 1 | 1 | 28x28x512 | 3x3x256x512 + 512 | 1,180,160 |
Conv3-512 | 28x28x512 | 3x3x512x512 | 1 | 1 | 28x28x512 | 3x3x512x512 + 512 | 2,359,808 |
Conv3-512 | 28x28x512 | 3x3x512x512 | 1 | 1 | 28x28x512 | 3x3x512x512 + 512 | 2,359,808 |
MaxPool2 | 28x28x512 | 2x2 | 2 | 0 | 14x14x512 | 0 | |
Conv3-512 | 14x14x512 | 3x3x512x512 | 1 | 1 | 14x14x512 | 3x3x512x512 + 512 | 2,359,808 |
Conv3-512 | 14x14x512 | 3x3x512x512 | 1 | 1 | 14x14x512 | 3x3x512x512 + 512 | 2,359,808 |
Conv3-512 | 14x14x512 | 3x3x512x512 | 1 | 1 | 14x14x512 | 3x3x512x512 + 512 | 2,359,808 |
MaxPool2 | 14x14x512 | 2x2 | 2 | 0 | 7x7x512 | 0 | |
FC1 | 7x7x512 | 4096 | 7x7x512x4096 + 4096 | 102,764,544 | |||
FC2 | 4096 | 4096 | 4096*4096 + 4096 | 16,781,312 | |||
FC3 | 4096 | 1000 | 4096*1000 + 1000 | 4,097,000 |
参数总量: 138,357,544
六、FAQ
Q:计算量越小,模型推理就越快吗?
答案是否定的。
当年头一次实习做算法的时候,主管给的第一个任务就是“把一个大的分割模型砍成一个小的”。当时并不理解模型“大”、“小”的真正含义,就简单的选取计算量作为评价指标,疯狂砍计算量(backbone 换 MobileNet/ShuffleNet、Conv 换成 DepthWise Conv、以及一些奇奇怪怪的融合结构等等),把模型计算量砍了将近 10 倍,结果一部署发现速度并没有快多少,反而是把最初的 ResNet 简单砍掉几个 block 效果更好。
实际上计算量和实际的推理速度之间没有直接的因果关系。计算量仅能作为模型推理速度的一个参考依据。**模型在特定硬件上的推理速度,除了受计算量影响外,还会受访存量、硬件特性、软件实现、系统环境等诸多因素影响,呈现出复杂的特性。**因此,在手头有硬件且测试方便的情况下,实测是最准确的性能评估方式。
在设计网络结构时,如果有实测的条件,建议在模型迭代早期对性能也进行测试。一些 NAS 的方法也会对搜索出来的网络结构进行测速,或者干脆对硬件速度进行了建模,也作为初期搜索的重要参数。这种方法设计出来的网络在后期部署时,会极大减少因性能问题迭代优化的时间和人力开销。