对卷积的极佳可视化(受到李宏毅老师的启发 ↗): 原本的全连接网络,实际上是将一个矩阵(即图片)通过一个四维张量权重转换为另一个矩阵(feature map)。这种操作当然可以打包看成一个张量乘法,但也可以展开来看具体是怎么实现的:权重 可以看成输出矩阵的一个元素 的取值相对于输入矩阵的一个元素 的线性系数。因此,权重张量中的一个二维切片
w[i,j,:,:]可以看成一个长宽都和 相同的矩阵,指定了 的取值如何受到整个 的影响。可以把它看成一个和 相同尺寸的一个滤镜片,叠加在上面,两层对应位置的元素分别相乘再相加,得到 的值。 而卷积的发明基于几个想法:
- 我们不喜欢全连接层,因为它可调权重太多,容量/弹性太大,容易过拟合,难以训练
- 我们希望提取图片中的特征,特征应该是局部的而不是全局的
- 特征对绝对位置不敏感,而对相对位置敏感
因此,我们的想法非常自然:
- 把刚刚的全尺寸滤镜片裁剪一下,比方说裁成 个像素那么大,并且让每个 只和其对应位置附近的像素有关。也就是说,我们的
w[i,j,:,:]切片的尺寸从“与 一样大” 变成了 ,同时我们手动让“叠加滤镜”的时候,每个w[i,j,:,:]的中心与 重合(而不是像全尺寸的时候那样永远对齐左上角或者图片中心)。这就体现了局部性,大大减少了参数数量(每个 不再关心整个 ), 张量的尺寸来到 .- 进一步的,切片
w[i,j,:,:]的内容(这里是一个 的矩阵)应当与 的值无关,因为我们相信图片中的 pattern 与其绝对位置无关,同样的特征可能出现在图片的各个位置,因此完全可以让每个位置的滤镜片都相同,因此整个 张量被缩减为一个 的矩阵( ).- 但是,只有一种滤镜片,只能学习十分局限的 pattern(想想感知机要求样本线性可分才能分类),因此不妨增加到 种滤镜片,此时 又被扩充至 . 对 的同一个位置,我每一种都叠加一次,得到一个结果。对整个 处理之后,可以得到一个 的一个输出, 可以类比颜色通道,称为特征通道。
kernel_size:窗口大小,可以是一个数(正方形),或者一个元组(长方形的高和宽)。 stride:步长,可以是一个数或者元组,类似 kernel_size;如果等于 kernel_size 则窗口之间刚好相邻而没有重叠。 padding:指定周边填充 0(或者对于 max pooling 是 -inf)的宽度;当 padding 等于 (kernel_size - 1)/2 的时候,输出 feature map 的宽高会等于输入;特别地,如果 kernel_size 是偶数,想要 feature map 输出大小等于输入,需要做不对称 padding,此时需要单独使用 nn.ZeroPad2d 层指定不对称 padding,而不能简单地在这里输入参数。
nn.Conv2d#
牢记 CNN 的几个关键参数以及其作用、结构 一个卷积核:形状是 长度×宽度×输入通道数,是一个三维张量,运算时与窗口内的数据点乘,得到一个数(而不是多个数) 一个 Conv2d 层:由 输出通道 个上述卷积和组成,也可以看成一个四维张量
操作的时候,对于输入图片,依次对每个感受野区域应用多个卷积核,一个卷积核对应一个输出通道。
关于 卷积核和全连接的关系,我的理解是这样的:
并不是全连接,全连接要求输出的每一个值都能收到输入的每一个值的影响,并且这个影响唯一独立地收到 大小的 weight 矩阵指定。
而 卷积的输出的每一个值只会受到相同位置的输入的不同通道的元素的影响,而不同的位置(像素)被当做相互平行的 batch,互相不影响。因此,只看 channel 维度的话, 确实也可以被认为是一个从输入 channels 到输出 channels 的全连接。
这里可以提及 Mobilenet,一种计算量更小、准确度没有下降太多的卷积网络。
Mobilenet 的核心是 Depthwise Separable Convolution 深度可分离卷积。 它的思想是:
- Depthwise Convolution:每次卷积不做“混合”,一个输出通道只接受一个输入通道的作用。这意味着每个卷积核的大小得以缩小。
- Pointwise Convolution:最后进行 卷积,混合所有 channel
nn.MaxPool2d#
和 nn.Conv2d 的参数类似,区别是池化层的 stride 默认等于 kernel_size,而不是卷积层的 1;池化层和卷积层的 padding 默认都是 0.
池化层只是一个 operator,类似于 ReLU,没有需要学习的参数。
LeNet 是一个 1980 年代的手写数字识别模型,用于解决美国邮局手写邮编识别问题,由 Yann Lecun 等人在 Lua 上训练其训练数据集就是后来知名的 MNIST. 网络十分简单,就是两个卷积层、两个 Avg Pooling、两个线性层,激活函数使用 Sigmoid,在 MNIST 上可以达到很高精度(实际上 MLP 也可以),而在 Fashion MNIST 上也可以达到 82% 左右。 其结构虽简单,但卷积层 channel 数增加、高宽减少的构造思想影响了后来的很多网络,其中就有 Alex Net
整个深度学习就是老中医+炼丹+神农尝百草,各种玄学+实验+不可解释
LeNet 和 AlexNet#
卷积神经网络 更深的卷积神经网络+两个全连接层
AlexNet 在最后使用两个全连接层,这是有必要的,可以补充先前的卷积没有充分抽取出的剩余特征。如果减少到一个或者去掉,都会导致模型能力下降。
VGG#
VGG 引入了神经网络“局部块”的想法 深度学习的火爆很大程度上不是技术先进,而是“那帮人”会包装 ——李沐
诚然,ReLU 实则只是一个 max,Deep Network 的 Deep 也是一个噱头而无定量化的要求(比如十分武断地认为 AlexNet 深而 LeNet 浅)。
VGG 也是类似,这只不过是一个自动生成多个 Conv2d 的包装器,却被赋予了高大上的名字。
![[./images/Pasted image 20251127160538.png]]
VGG 块#
基于 Alex Net 更深更大的思想,我们希望能方便地构造出深层卷积神经网络。因此有人将 数个卷积层和一个 MaxPooling 层打包,称为一个 VGG 块。
具体地,每个卷积的 kernel_size 都是 3,padding 都是 1;总共 n 层卷积,每个卷积都有 m 个通道;池化层在最顶上,kernel_size 是 2,stride 是 2,padding 是 0.
import torch
import torch.nn as nn
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
def vgg(conv_arch):
conv_blks = []
in_channels = 1
for (num_convs, out_channels) in conv_arch:
conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 10))py然后就可以填入参数来设计网络了
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
net = vgg(conv_arch)py可以看到,基本上每次都是宽高除以 2(每个 VGG 块 都有 stride=2 的 MaxPooling),通道数乘以 2。以后将这种高宽减半一次的局部网络称为一个 Stage
NIN(Network in Network) 网络中的网络#
由于在上述几个卷积神经网络的架构中,网络最后的全连接层权重数量极大(占据整个网络的大半),导致计算慢、内存占用大、容易过拟合。
NIN 秉持着这种想法,构造了一种不使用全连接层的架构。它将 VGG(或 AlexNet)中的全连接层替换为了 卷积和 MaxPooling(kernel_size=2, stride=2) 交替多层的结构,然后分散到先前的每次卷积中,合并到每个 VGG 块中,称为 NIN 块。
每个 NIN 块的结构是:正常卷积 + 卷积 + 卷积 + MaxPool(kernel_size=3, stride=2)
其中 卷积 不改变输入的 CHW
这里每个 NIN 块使用两次 卷积,作用类似于 AlexNet 最后的两层全连接层,只不过这里仅在 channel 维度上全连接。
前面的 NIN 块的 channel 数按照传统方法构造(C加倍,HW减半),最后一个 NIN 块的 channel 数设置为要分类的类别数,然后将 MaxPool 换成全局 AvgPool,这样每一个 channel 能得到一个值,对应对每一个类的预测。
实测 NIN 的精度并没有比 VGG 显著高,但训练难度确实下降了
GoogLeNet#
使用更多的 卷积层、抛弃最后的全连接层,都使得它相比 VGG 参数量更小、更难过拟合、更好训练,并且事实证明精度高于 VGG。GoogLeNet 是第一个“层个数”(不是深度)达上百的网络,并且有诸多迭代和变种,其中的超参数各式各样,没有什么规律,可能是试错所得。
Inception 块#
来自电影 Inception
创造性地加入了“并行联结”,一个 Inception 块的输入被多个不同的路径共享,每个路径处理完成之后,输出同样 HW 但不同 channel 数量的 feature map,然后在 channel 维度上 concat 起来。
Inception 块的作用类似于一个特殊设计过的卷积层,能够增加 channel 数量,但不同 channel 的分工受到了更显式地指定(通过不同路径的不同结构),而不再是 kernel 能够自由学习出来的。 ![[./images/Pasted image 20251127203020.png]]
整体结构#
多个不同结构的 Inception 块串联起来,每个都用来增加 channel;相邻两个 Inception 块之间插入一个 MaxPool(kernel_size=3, stride=2) 用来减半 HW;最后使用一个全局 AvgPool(或称 AdaptiveAvgPool)来坍缩 HW,并且将 channel 展平之后输入一个全连接层,获得分类。