一个卷积神经网络的基本构成一般有卷积层、归一化层、激活层和线性层。这里我们就通过逐步计算这些层来计算一个CNN模型所需要的参数量和FLOPs吧. 另外,FLOPs的全程为floating point operations的缩写(小写s表复数),意指浮点运算数,理解为计算量。可以用来衡量算法/模型的复杂度。
卷积层,最常用的是2D卷积,因此我们以飞桨中的Conv2D来表示。
Conv2D的参数量计算较为简单,先看下列的代码,如果定义一个Conv2D,卷积层中的参数会随机初始化,如果打印其shape,就可以知道一个Conv2D里大致包含的参数量了,Conv2D的参数包括两部分,一个是用于卷积的weight,一个是用于调节网络拟合输入特征的bias。如下
import paddle
import numpy as np
cv2d = paddle.nn.Conv2D(in_channels=2, out_channels=4, kernel_size=(3, 3), stride=1, padding=(1, 1))
params = cv2d.parameters()
print("shape of weight: ", np.array(params[0]).shape)
print("shape of bias: ", np.array(params[1]).shape)
shape of weight: (4, 2, 3, 3)
shape of bias: (4,)
这里解释一下上面的代码,我们先定义了一个卷积层cv2d,然后输出了这个卷积层的参数的形状,参数包含两部分,分别是weight和bias,这两部分相加才是整个卷积的参数量。因此,可以看到,我们定义的cv2d的参数量为:$423*3+4 = 76$, 4对应的是输出的通道数,2对应的是输入的通道数,两个3是卷积核的尺寸,最后的4就是bias的数量了, 值得注意的是, bias是数量与输出的通道数保持一致。因此,我们可以得出,一个卷积层的参数量的公式,如下:
其中,$C_{in}$ 表示输入的通道数,$C_{out}$ 表示输出的通道数,
参数量会计算了,那么FLOPs其实也是很简单的,就一个公式:
这里,$M_{outh}$,$M_{outw}$ 为输出的特征图的高和宽,而不是输入的,这里需要注意一下。
Paddle有提供计算FLOPs和参数量的API,paddle.flops, 这里我们用我们的方法和这个API的方法来测一下,看看一不一致吧。代码如下:
import paddle
from paddle import nn
from paddle.nn import functional as F
class TestNet(nn.Layer):
def __init__(self):
super(TestNet, self).__init__()
self.conv2d = nn.Conv2D(in_channels=2, out_channels=4, kernel_size=(3, 3), stride=1, padding=1)
def forward(self, x):
x = self.conv2d(x)
return x
if __name__ == "__main__":
net = TestNet()
paddle.flops(net, input_size=[1, 2, 320, 320])
Total GFlops: 0.00778 Total Params: 76.00
API得出的参数量为76,GFLOPs为0.00778,这里的GFLOPs就是FLOPs的10$^9$倍,我们的参数量求得的也是76,那么FLOPs呢?我们来算一下,输入的尺寸为320 * 320, 卷积核为3 * 3, 且padding为1,那么图片输入的大小和输出的大小一致,即输出也是320 * 320, 那么根据我们的公式可得:
最常用的归一化层为BatchNorm2D啦,我们这里就用BatchNorm2D来做例子介绍。在算参数量和FLOPs,先看看BatchNorm的算法流程吧!
输入为:Values of
$\quad\quad\quad$Params to be learned:
输出为:{$y_i$=BN$_{\gamma}$,$\beta(x_i)$}
流程如下:
$\quad\quad\quad\hat{x}i\gets\frac{x_i-\mu_B}{\sqrt{\sigma{B}^2+\epsilon}}$
$\quad\quad\quad y_i\gets\gamma\hat{x}i+\beta\equiv BN{\gamma}$,$\beta(x_i)$
在这个公式中,$B$ 为一个Batch的数据,$\beta$ 和
由于归一化层较为简单,这里直接写出公式:
其中4表示四个参数值,每个特征图对应一组四个元素的参数组合;
beta_initializer
gamma_initializer
moving_mean_initializer
moving_variance_initializer
因为只有两个可以学习的权重,$\beta$ 和
与1.3相似,欢迎大家使用上面的代码进行验证。
线性层也是常用的分类层了,我们以飞桨的Linear为例来介绍。
其实线性层是比较简单的,它就是相当于卷积核为1的卷积层,线性层的每一个参数与对应的数据进行矩阵相乘,再加上偏置项bias,线性层没有类似于卷积层的“卷”的操作的,所以计算公式如下:
我们这里打印一下线性层参数的形状看看。
import paddle
import numpy as np
linear = paddle.nn.Linear(in_features=2, out_features=4)
params = linear.parameters()
print("shape of weight: ", np.array(params[0]).shape)
print("shape of bias: ", np.array(params[1]).shape)
shape of weight: (2, 4)
shape of bias: (4,)
可以看到,线性层相较于卷积层还是简单的,这里我们直接计算这个定义的线性层的参数量为
与卷积层不同的是,线性层没有”卷“的过程,所以线性层的FLOPs计算公式为:
这里我们就以LeNet为例子,计算出LeNet的所有参数量和计算量。LeNet的结构如下。输入的图片大小为28 * 28
LeNet(
(features): Sequential(
(0): Conv2D(1, 6, kernel_size=[3, 3], padding=1, data_format=NCHW)
(1): ReLU()
(2): MaxPool2D(kernel_size=2, stride=2, padding=0)
(3): Conv2D(6, 16, kernel_size=[5, 5], data_format=NCHW)
(4): ReLU()
(5): MaxPool2D(kernel_size=2, stride=2, padding=0)
)
(fc): Sequential(
(0): Linear(in_features=400, out_features=120, dtype=float32)
(1): Linear(in_features=120, out_features=84, dtype=float32)
(2): Linear(in_features=84, out_features=10, dtype=float32)
)
)
我们先来手动算一下参数量和FLOPs。
features[0] 参数量:$ 6 * 1 * 3 * 3 + 6 = 60$, FLOPs : $ 60 * 28 * 28 = 47040$
features[1] 参数量和FLOPs均为0
features[2] 参数量和FLOPs均为0, 输出尺寸变为14 * 14
features[3] 参数量:$ 16 * 6 * 5 * 5 + 16 = 2416$, FLOPs : $ 2416 * 10 * 10 = 241600$, 需要注意的是,这个卷积没有padding,所以输出特征图大小变为 10 * 10
features[4] 参数量和FLOPs均为0
features[5] 参数量和FLOPs均为0,输出尺寸变为5 * 5, 然后整个被拉伸为[1, 400]的尺寸,其中400为5 * 5 * 16。
fc[0] 参数量:$ 400 * 120 + 120 = 48120$, FLOPs : $ 400 * 120 = 48000$ (输出尺寸变为[1, 120])
fc[1] 参数量:$ 120 * 84 + 84 = 10164$, FLOPs : $ 120 * 84 = 10080$ (输出尺寸变为[1, 84])
fc[2] 参数量:$ 84 * 10 + 10 = 850$, FLOPs : $ 84 * 10 = 840 $ (输出尺寸变为[1, 10])。
总参数量为:
总FLOPs为:$47040 + 241600 + 48000 + 10080 + 840 = 347560$
下面我们用代码验证以下:
from paddle.vision.models import LeNet
net = LeNet()
print(net)
paddle.flops(net, input_size=[1, 1, 28, 28], print_detail=True)
+--------------+-----------------+-----------------+--------+--------+
| Layer Name | Input Shape | Output Shape | Params | Flops |
+--------------+-----------------+-----------------+--------+--------+
| conv2d_0 | [1, 1, 28, 28] | [1, 6, 28, 28] | 60 | 47040 |
| re_lu_0 | [1, 6, 28, 28] | [1, 6, 28, 28] | 0 | 0 |
| max_pool2d_0 | [1, 6, 28, 28] | [1, 6, 14, 14] | 0 | 0 |
| conv2d_1 | [1, 6, 14, 14] | [1, 16, 10, 10] | 2416 | 241600 |
| re_lu_1 | [1, 16, 10, 10] | [1, 16, 10, 10] | 0 | 0 |
| max_pool2d_1 | [1, 16, 10, 10] | [1, 16, 5, 5] | 0 | 0 |
| linear_0 | [1, 400] | [1, 120] | 48120 | 48000 |
| linear_1 | [1, 120] | [1, 84] | 10164 | 10080 |
| linear_2 | [1, 84] | [1, 10] | 850 | 840 |
+--------------+-----------------+-----------------+--------+--------+
Total GFlops: 0.00034756 Total Params: 61610.00
可以看到,与我们的计算是一致的,大家可以自己把VGG-16的模型算一下参数量FLOPs,相较于LeNet, VGG-16只是模型深了点,并没有其余额外的结构。