计算机视觉作为一门让机器学会如何去“看”的学科,具体的说,就是让机器去识别摄像机拍摄的图片或视频中的物体,检测出物体所在的位置,并对目标物体进行跟踪,从而理解并描述出图片或视频里的场景和故事,以此来模拟人脑视觉系统。因此,计算机视觉也通常被叫做机器视觉,其目的是建立能够从图像或者视频中“感知”信息的人工系统。
计算机视觉技术经过几十年的发展,已经在交通(车牌识别、道路违章抓拍)、安防(人脸闸机、小区监控)、金融(刷脸支付、柜台的自动票据识别)、医疗(医疗影像诊断)、工业生产(产品缺陷自动检测)等多个领域应用,影响或正在改变人们的日常生活和工业生产方式。未来,随着技术的不断演进,必将涌现出更多的产品和应用,为我们的生活创造更大的便利和更广阔的机会。
图1 计算机视觉技术在各领域的应用
计算机视觉的发展历程要从生物视觉讲起。对于生物视觉的起源,目前学术界尚没有形成定论。有研究者认为最早的生物视觉形成于距今约7亿年前的水母之中,也有研究者认为生物视觉产生于距今约5亿年前寒武纪。寒武纪生物大爆发的原因一直是个未解之谜,不过可以肯定的是在寒武纪动物具有了视觉能力,捕食者可以更容易地发现猎物,被捕食者也可以更早的发现天敌的位置。视觉系统的形成有力地推动了食物链的演化,加速了生物进化过程,是生物发展史上重要的里程碑。经过几亿年的演化,目前人类的视觉系统已经具备非常高的复杂度和强大的功能,人脑中神经元数目达到了1000亿个,这些神经元通过网络互相连接,这样庞大的视觉神经网络使得我们可以很轻松的观察周围的世界,如 图2 所示。
图2 人类视觉感知
对人类来说,识别猫和狗是件非常容易的事。但对计算机来说,即使是一个精通编程的高手,也很难轻松写出具有通用性的程序(比如:假设程序认为体型大的是狗,体型小的是猫,但由于拍摄角度不同,可能一张图片上猫占据的像素比狗还多)。那么,如何让计算机也能像人一样看懂周围的世界呢?研究者尝试着从不同的角度去解决这个问题,由此也发展出一系列的子任务,如 图3 所示。
图3 计算机视觉子任务示意图
(a) Image Classification: 图像分类,用于识别图像中物体的类别(如:bottle、cup、cube)。
(b) Object Localization: 目标检测,用于检测图像中每个物体的类别,并准确标出它们的位置。
(c) Semantic Segmentation: 语义分割,用于标出图像中每个像素点所属的类别,属于同一类别的像素点用一个颜色标识。
(d) Instance Segmentation: 实例分割,值得注意的是,目标检测任务只需要标注出物体位置,而实例分割任务不仅要标注出物体位置,还需要标注出物体的外形轮廓。
这里以图像分类任务为例,为大家介绍计算机视觉技术的发展历程。在早期的图像分类任务中,通常是先人工提取图像特征,再用机器学习算法对这些特征进行分类,分类的结果强依赖于特征提取方法,往往只有经验丰富的研究者才能完成,如 图4 所示。
图4 早期的图像分类任务
在这种背景下,基于神经网络的特征提取方法应运而生。Yann LeCun是最早将卷积神经网络应用到图像识别领域的,其主要逻辑是使用卷积神经网络提取图像特征,并对图像所属类别进行预测,通过训练数据不断调整网络参数,最终形成一套能自动提取图像特征并对这些特征进行分类的网络,如 图5 所示。
图5 早期的卷积神经网络处理图像任务示意
这一方法在手写数字识别任务上取得了极大的成功,但在接下来的时间里,却没有得到很好的发展。其主要原因一方面是数据集不完善,只能处理简单任务,在大尺寸的数据上容易发生过拟合;另一方面是硬件瓶颈,网络模型复杂时,计算速度会特别慢。
目前,随着互联网技术的不断进步,数据量呈现大规模的增长,越来越丰富的数据集不断涌现。另外,得益于硬件能力的提升,计算机的算力也越来越强大。不断有研究者将新的模型和算法应用到计算机视觉领域。由此催生了越来越丰富的模型结构和更加准确的精度,同时计算机视觉所处理的问题也越来越丰富,包括分类、检测、分割、场景描述、图像生成和风格变换等,甚至还不仅仅局限于2维图片,包括视频处理技术和3D视觉等。
卷积神经网络是目前计算机视觉中使用最普遍的模型结构。图6 是一个典型的卷积神经网络结构,多层卷积和池化层组合作用在输入图片上,在网络的最后通常会加入一系列全连接层,ReLU激活函数一般加在卷积或者全连接层的输出上,网络中通常还会加入Dropout来防止过拟合。
图6 卷积神经网络经典结构
卷积层:卷积层用于对输入的图像进行特征提取。卷积的计算范围是在像素点的空间邻域内进行的,因此可以利用输入图像的空间信息。卷积核本身与输入图片大小无关,它代表了对空间邻域内某种特征模式的提取。比如,有些卷积核提取物体边缘特征,有些卷积核提取物体拐角处的特征,图像上不同区域共享同一个卷积核。当输入图片大小不一样时,仍然可以使用同一个卷积核进行操作。
池化层:池化层通过对卷积层输出的特征图进行约减,实现了下采样。同时对感受域内的特征进行筛选,提取区域内最具代表性的特征,保留特征图中最主要的信息。
激活函数:激活函数给神经元引入了非线性因素,对输入信息进行非线性变换,从而使得神经网络可以任意逼近任何非线性函数,然后将变换后的输出信息作为输入信息传给下一层神经元。
全连接层:全连接层用于对卷积神经网络提取到的特征进行汇总,将多维的特征映射为二维的输出。其中,高维代表样本批次大小,低维代表分类或回归结果。
在图像处理中,由于图像中存在较多冗余信息,可用某一区域子块的统计信息(如最大值或均值等)来刻画该区域中所有像素点呈现的空间分布模式,以替代区域子块中所有像素点取值,这就是卷积神经网络中池化(pooling)操作。
池化操作对卷积结果特征图进行约减,实现了下采样,同时保留了特征图中主要信息。比如:当识别一张图像是否是人脸时,我们需要知道人脸左边有一只眼睛,右边也有一只眼睛,而不需要知道眼睛的精确位置,这时候通过池化某一片区域的像素点来得到总体统计特征会显得很有用。
池化的几种常见方法包括:平均池化、最大池化、K-max池化。其中平均池化和最大池化如 图1 所示,K-max池化如 图2 所示。
图1 平均池化和最大池化
平均池化: 计算区域子块所包含所有像素点的均值,将均值作为平均池化结果。如 图1(a),这里使用大小为$2\times2$的池化窗口,每次移动的步幅为2,对池化窗口覆盖区域内的像素取平均值,得到相应的输出特征图的像素值。池化窗口的大小也称为池化大小,用$k_h \times k_w$表示。在卷积神经网络中用的比较多的是窗口大小为$2 \times 2$,步幅为2的池化。
最大池化: 从输入特征图的某个区域子块中选择值最大的像素点作为最大池化结果。如 图1(b),对池化窗口覆盖区域内的像素取最大值,得到输出特征图的像素值。当池化窗口在图片上滑动时,会得到整张输出特征图。
图2 K-max池化
图3 微小位置变化时的最大池化结果
在飞桨中,各种Pooling API中的Padding参数, 接收值类型都包括int、list、tuple和string。下面用代码和公式来介绍一下这些方式吧。
int输入即接收一个int类型的数字n,对图片的四周包裹n行n列的0来填充图片。如果要保持图片尺寸不变,n的值和池化窗口的大小是有关的。假如 $H_{in}, W_{in}$ 为图片输入的大小,$k_h, k_w$ 为池化窗口的大小,$H_{out}, H_{out}$ 为结果图的大小的话,他们之间有着这样的关系。 $$H_{out} = \frac{H_{in} + 2p_h - k_h}{s_h} + 1 \ W_{out} = \frac{W_{out} + 2p_w -k_w}{s_w} + 1$ $在使用3×3的池化窗口且步长为1的情况下,还要保持图片大小不变,则需要使用padding=1的填充。 那么,公式就变为了$ $H_{out} = \frac{6 - 3 + 21}{1} + 1 \ W_{out} = \frac{6 - 3 + 21}{1} + 1$ $另外,在stride不为1且不能被整除的情况下,整体结果向下取整。 关于Padding和K的公式如下$ $Padding = \frac{(k-1)}{2} \quad \quad (k % 2 != 0)$ $
关于上面的讲解,下面用飞桨的API来看看吧。
import paddle # No padding x = paddle.rand((1, 1, 6, 6)) avgpool = paddle.nn.AvgPool2D(kernel_size=3, stride=1, padding=0) y = avgpool(x) print('result:', 'shape of x:', x.shape, 'shape of result:', y.shape) result: shape of x: [1, 1, 6, 6] shape of result: [1, 1, 4, 4]
这是池化中不padding,stride为1的结果,可以根据公式填入,$H_{out} = W_{out} = (6 + 0 - 3) + 1 = 4$,因此池化后的结果为4。如果填充呢?
import paddle # Padding 1 x = paddle.rand((1, 1, 6, 6)) avgpool = paddle.nn.AvgPool2D(kernel_size=3, stride=1, padding=1) y = avgpool(x) print('result:', 'shape of x:', x.shape, 'shape of result:', y.shape) result: shape of x: [1, 1, 6, 6] shape of result: [1, 1, 6, 6]
正如我们上面的公式,$H_{out} = W_{out} = (6 + 2 - 3) + 1 = 6$, 填充为1的时候图像保持为原大小。
因为图像有宽和高,所以list和tuple的长度应该为2,里面的两个值分别对应了高和宽,计算方式和上面int输入的一样,单独计算。一般用作输入图片宽高不一致,或者池化窗口大小不一的情况。我们直接用飞桨的API来看看吧。
import paddle # No padding and different kernel size x = paddle.rand((1, 1, 12, 6)) # 12为高H, 6为宽W avgpool = paddle.nn.AvgPool2D(kernel_size=(3, 5), stride=1, padding=0) y = avgpool(x) print('result:', 'shape of x:', x.shape, 'shape of result:', y.shape) result: shape of x: [1, 1, 12, 6] shape of result: [1, 1, 10, 2]
这里我们带入公式推理一下,$H_{out} = 12 - 3 + 1 = 10, W_{out} = 6 - 5 + 1 = 2$.与结果相符。下面是有填充的情况,且3的滑动窗口大小我们需要填充1,5的话则需要填充2了。下面来看看吧。
import paddle # No padding and different kernel size x = paddle.rand((1, 1, 12, 6)) # 12为高H, 6为宽W avgpool = paddle.nn.AvgPool2D(kernel_size=(3, 5), stride=1, padding=(1, 2)) y = avgpool(x) print('result:', 'shape of x:', x.shape, 'shape of result:', y.shape) result: shape of x: [1, 1, 12, 6] shape of result: [1, 1, 12, 6]
这里的结果与我们的期望一致,大家试着带入公式算一下吧。
string输入有两个值,一个是SAME,一个是VALID。这两个的计算公式如下:
SAME:$H_{out} = \lceil \frac{H_{in}}{s_h} \rceil$, $W_{out} = \lceil\frac{W_{in}}{s_w}\rceil$
VALID:$H_{out} = \frac{H_{in} - k_h}{s_h} + 1$, $W_{out} = \frac{W_{in} - k_w}{s_w} + 1$
可以看到,VALID方式就是默认采用的不填充的方式,与上面不Padding的公式一样。而SAME则与池化窗口的大小无关,若$s_h$和$s_w$为1,无论池化窗口的大小,输出的特征图的大小都与原图保持一致。当任意一个大于1时,如果能整除,输出的尺寸就是整除的结果,如果不能整除,则通过padding的方式继续向上取整。理论过于难懂,我们直接用飞桨的API来看看吧。
import paddle # Padding SAME kernel_size 2 x = paddle.rand((1, 1, 6, 6)) avgpool = paddle.nn.AvgPool2D(kernel_size=2, padding='SAME') y = avgpool(x) print('result:', 'shape of x:', x.shape, 'shape of result:', y.shape) result: shape of x: [1, 1, 6, 6] shape of result: [1, 1, 3, 3]
代码的结果出来了,我们来直接带入公式来计算吧,$H_{out} = 6/2 = 3, W_{out} = 6/2 = 3$,结果一致。
import paddle # Padding SAME kernel_size 1 x = paddle.rand((1, 1, 6, 6)) avgpool = paddle.nn.AvgPool2D(kernel_size=1, padding='SAME') y = avgpool(x) print('result:', 'shape of x:', x.shape, 'shape of result:', y.shape) result: shape of x: [1, 1, 6, 6] shape of result: [1, 1, 6, 6]
这个呢,就和我们上面说的一致。下面来看看VALID填充方式吧。
import paddle # Padding VALID x = paddle.rand((1, 1, 6, 6)) avgpool = paddle.nn.AvgPool2D(kernel_size=2, padding='VALID') y = avgpool(x) print('result:', 'shape of x:', x.shape, 'shape of result:', y.shape) result: shape of x: [1, 1, 6, 6] shape of result: [1, 1, 3, 3]
这就是VALID的填充方式的结果啦。大家自己按照公式算算,看看你的答案和程序输出的对不对哦。
与卷积核类似,池化窗口在图片上滑动时,每次移动的步长称为步幅,当宽和高方向的移动大小不一样时,分别用$s_w$和$s_h$表示。也可以对需要进行池化的图片进行填充,填充方式与卷积类似,假设在第一行之前填充$p_{h1}$行,在最后一行后面填充$p_{h2}$行。在第一列之前填充$p_{w1}$列,在最后一列之后填充$p_{w2}$列,则池化层的输出特征图大小为:
$$H_{out} = \frac{H + p_{h1} + p_{h2} - k_h}{s_h} + 1$$
$$W_{out} = \frac{W + p_{w1} + p_{w2} - k_w}{s_w} + 1$$
在卷积神经网络中,通常使用$2\times2$大小的池化窗口,步幅也使用2,填充为0,则输出特征图的尺寸为:
$$H_{out} = \frac{H}{2}$$
$$W_{out} = \frac{W}{2}$$
通过这种方式的池化,输出特征图的高和宽都减半,但通道数不会改变。
这里以 图1 中的2个池化运算为例,此时,输入大小是$4 \times 4$ ,使用大小为$2 \times 2$ 的池化窗口进行运算,步幅为2。此时,输出尺寸的计算方式为:
$$H_{out} = \frac{H + p_{h1} + p_{h2} - k_h}{s_h} + 1=\frac{4 + 0 + 0 - 2}{2} + 1=\frac{4}{2}=2$$
$$W_{out} = \frac{W + p_{w1} + p_{w2} - k_w}{s_w} + 1=\frac{4 + 0 + 0 - 2}{2} + 1=\frac{4}{2}=2$$
图1(a) 中,使用平均池化进行运算,则输出中的每一个像素均为池化窗口对应的 $2 \times 2$ 区域求均值得到。计算步骤如下:
池化窗口的初始位置为左上角,对应粉色区域,此时输出为 $3.5 = \frac{1 + 2 + 5 + 6}{4}$ ;
由于步幅为2,所以池化窗口向右移动两个像素,对应蓝色区域,此时输出为 $5.5 = \frac{3 + 4 + 7 + 8}{4}$ ;
遍历完第一行后,再从第三行开始遍历,对应绿色区域,此时输出为 $11.5 = \frac{9 + 10 + 13 + 14}{4}$ ;
池化窗口向右移动两个像素,对应黄色区域,此时输出为 $13.5 = \frac{11 + 12 + 15 + 16}{4}$ 。
图1(b) 中,使用最大池化进行运算,将上述过程的求均值改为求最大值即为最终结果。
一个卷积神经网络的基本构成一般有卷积层、归一化层、激活层和线性层。这里我们就通过逐步计算这些层来计算一个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是数量与输出的通道数保持一致。因此,我们可以得出,一个卷积层的参数量的公式,如下: $ $Param_{conv2d} = C_{in} * C_{out} * K_h * K_w + C_{out}$ $其中,$C_{in} $表示输入的通道数,$C_{out} $表示输出的通道数,$ K_h$,$ K_w $表示卷积核的大小。当然了,有些卷积会将bias设置为False,那么我们不加最后的$C_{out}$即可。
参数量会计算了,那么FLOPs其实也是很简单的,就一个公式: $$FLOP_{conv2d} = Param_{conv2d} * M_{outh} * M_{outw}$ $这里,$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, 那么根据我们的公式可得: $76 * 320 * 320 = 7782400$, 与API的一致!因此大家计算卷积层的参数和FLOPs的时候就可以用上面的公式。
最常用的归一化层为BatchNorm2D啦,我们这里就用BatchNorm2D来做例子介绍。在算参数量和FLOPs,先看看BatchNorm的算法流程吧!
输入为:Values of $x$ over a mini-batch:$B={x_1,...,m}$,
$\quad\quad\quad$Params to be learned: $\beta$, $\gamma$
输出为:{$y_i$=BN$_{\gamma}$,$\beta(x_i)$}
流程如下:
$\quad\quad\quad$$\mu_B\gets$ $\frac{1}{m}\sum_{1}^mx_i$
$\quad\quad\quad\sigma_{B}2\gets\frac{1}{m}\sum_{1}m(x_i-\mu_B)^2$
$\quad\quad\quad\hat{x}i\gets\frac{x_i-\mu_B}{\sqrt{\sigma^2+\epsilon}}$
$\quad\quad\quad y_i\gets\gamma\hat{x}i+\beta\equiv BN$,$\beta(x_i)$
在这个公式中,$B$ 为一个Batch的数据,$\beta$ 和 $\gamma$ 为可学习的参数,$\mu$ 和 $\sigma^2$ 为均值和方差,由输入的数据的值求得。该算法先求出整体数据集的均值和方差,然后根据第三行的更新公式求出新的x,最后根据可学习的$\beta$ 和 $\gamma$调整数据。第三行中的 $\epsilon$ 在飞桨中默认为 1e-5, 用于处理除法中的极端情况。
由于归一化层较为简单,这里直接写出公式: $$Param_{bn2d} = 4 * C_{out}$ $ 其中4表示四个参数值,每个特征图对应一组四个元素的参数组合;
beta_initializer $\beta$ 权重的初始值设定项。
gamma_initializer $\gamma$ 伽马权重的初始值设定项。
moving_mean_initializer $\mu$ 移动均值的初始值设定项。
moving_variance_initializer $\sigma^2$ 移动方差的初始值设定项。
因为只有两个可以学习的权重,$\beta$ 和 $\gamma$,所以FLOPs只需要2乘以输出通道数和输入的尺寸即可。 归一化的FLOPs计算公式则为: $ $FLOP_{bn2d} = 2 * C_{out} * M_{outh} * M_{outw}$ $ 与1.3相似,欢迎大家使用上面的代码进行验证。
线性层也是常用的分类层了,我们以飞桨的Linear为例来介绍。
其实线性层是比较简单的,它就是相当于卷积核为1的卷积层,线性层的每一个参数与对应的数据进行矩阵相乘,再加上偏置项bias,线性层没有类似于卷积层的“卷”的操作的,所以计算公式如下: $$Param_{linear} = C_{in} * C_{out} + C_{out}$ $。我们这里打印一下线性层参数的形状看看。
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,)
可以看到,线性层相较于卷积层还是简单的,这里我们直接计算这个定义的线性层的参数量为 $2 * 4 + 4 = 12$。具体对不对,我们在下面的实例演示中检查。
与卷积层不同的是,线性层没有”卷“的过程,所以线性层的FLOPs计算公式为: $ $FLOP_{linear} = C_{in} * C_{out}$$
这里我们就以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])。
总参数量为: $60 + 2416 + 48120 + 10164 + 850 = 61610$
总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只是模型深了点,并没有其余额外的结构。