跟着李沐老师学习深度学习(十)
卷积层
从全连接层到卷积
例子

从这张PPT讲起,比如有一个猫狗分类的例子,比如一张图片的输入是36M像素,那么它的参数就有3600万;我们这里使用一个100大小的单隐藏层的MLP,那么此时整个模型就有100 * 36M的像素(即:36亿个参数);如果我们考虑多层感知机,在考虑大量数据的时候,那么此时无法处理了;说明:全连接层处理图片的时候会遇到参数过多、模型过大的问题。
不变性
根据“沃尔多在哪里” 可得到两个不变性:
1.平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置 ,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”
2.局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域 ,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。

多层感知机局限性——破局
首先补充一些关于 “卷积” 的知识
连续域的卷积与离散域的卷积
首先聊聊卷积(这部分可以直接跳过,影响不大)。卷积大家一般都是聊连续域比较多啊,其定义为
结果被称之为f(x)与g(x)的卷积。
而离散域与之类似,为
注意,离散域中n的取值为整数,其他则同理,不再赘述。
举个例子:记x(n),y(n)满足如下关系:
* x(1)=1,x(2)=-1,当n取其他值时,x(n)=0;
* y(1)=1, y(2)=2,当n取其他值时,y(n)=0;
那么:
很自然的h(1)=0;
其他同理。那么到此为止,我们卷积部分算是差不多讲完了。

- 如果还是使用全连接层解决以上例子的问题,为了使每个隐藏神经元都能接收到每个输入像素的信息 ,我们将参数从权重矩阵(如同先前在多层感知机中所做的那样)替换为四阶权重张量。可以得到上图中公式1的式子;W → V做了形式上的变换(k=i+a, l=j+b)
- 在此基础上,首先引入“平移不变性” : 检测对象在输入X中的平移,应该仅导致隐藏表示H中的平移,因此U和V不依赖于 (i, j)的值;可以将公式转变成上图中的公式(2);此时看公式可以得出V就是我们所说的卷积;
- 然后,在引入“局部性” :收集[H]i, j的参数不应该偏离到(i, j)很远的地方,也就是说,可以规定一个范围▲;使得**|a| <= ▲ 并且 |b| <= ▲** ,此时公式又能化简为公式(3);此时V被称为卷积核(convolution kernel)或者滤波器(filter);
经过以上的转变,当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而现在卷积神经网络通常只需要几百个参数 ,而且不需要改变输入或隐藏表示的维数 。
CNN中的关键操作:
卷积
“卷积” 说法其实是错误的,这里表达的运算其实是互相关运算(cross-correlation),而不是卷积运算;真正的卷积运算:需先将滤波器(核)水平和垂直翻转,再在输入信号或图像上滑动并计算点积;而相互关不需要翻转,直接在输入信号或图像上滑动并计算点积。


总结
* 卷积层将输入和核矩阵进行交叉相关,加上偏移后得到输
* 核矩阵和偏移是可学习的参数
* 核矩阵的大小是超参数
代码
# 相互关运算
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K):
"""计算二维相互关运算"""
h, w = K.shape
# 所以,输出大小等于输入大小 𝑛ℎ×𝑛𝑤, 减去卷积核大小 𝑘ℎ×𝑘𝑤
# 即:(𝑛ℎ−𝑘ℎ+1)×(𝑛𝑤−𝑘𝑤+1).
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y
# 验证上述二维互相关运算的输出
X = torch.tensor([[0.0, 1.0, 2.0],
[3.0, 4.0, 5.0],
[6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0],
[2.0, 3.0]])
corr2d(X, K)
# 实现二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def froward(self, x):
return corr2d(x, self.weight) + self.bias
# 简单应用:检测图像中不同颜色的边缘
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
K = torch.tensor([[1.0, -1.0]])
# 输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘
Y = corr2d(X, K)
Y
# 卷积核K只能检验垂直边缘
corr2d(X.t(), K)
# 学习由X生成的Y的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y)**2
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'batch{i+1}, loss {l.sum():.3f}')
conv2d.weight.data.reshape((1, 2))
填充和步幅

这里不多说,就用这个PPT覆盖,这两种操作都是为了调整数据的维度。
代码:
# 填充和步幅
# 在所有侧边填充1个像素
import torch
from torch import nn
def comp_conv2d(conv2d, X):
# 将输入张量 X 的形状进行调整,在原形状前添加两个维度,使其变为 (1, 1, 8, 8)
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 将卷积后的输出张量 Y 的形状进行调整,去除前两个维度(批量大小和通道数),只保留高度和宽度维度
return Y.reshape(Y.shape[2:])
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
# 填充不同的高度和宽度
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
# 步幅的例子
# 将高度和宽度的步幅设置为2
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
# 稍微复杂的例子
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
# 高:(8 + 2 * 0 - 3)/ 3 + 1
# 宽:(8 + 2 * 1 - 5)/ 4 + 1
# 一般使用填充和步幅是对称的。
Q:核大小、填充、步幅 重要程度
A:填充一般是核大小 - 1;步幅:通常为1更好,不选1的情况是因为计算量太大了。
多输入和多输出通道
到目前为止,仅展示了单个输入和单个输出通道的简化例子。 这使得我们可以将输入、卷积核和输出看作二维张量。

在实际中,处理多输出多输出通道的例子比较多,例如彩色图像具有标准的RGB通道来代表红、绿和蓝。
代码:
# 多输入多输出通道
# 实现多输入通道互相关运算:
import torch
from d2l import torch as d2l
def corr2d_multi_in(X, K):
# zip:对最外面的通道进行遍历;将输入数据 X 和卷积核 K 按通道进行配对,每一对 (x, k) 分别表示输入数据的一个通道和对应的卷积核通道。
# d2l.corr2d(x, k):对每一对通道进行二维单通道的互相关运算,得到该通道的运算结果。
# sum(...):将所有通道的运算结果进行逐元素相加,得到最终的多输入通道互相关运算结果。
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
# 验证互相关运算的输出:
# 输入数据 X:形状为 (2, 3, 3),表示有 2 个输入通道,每个通道的大小为 3x3。
# 卷积核 K:形状为 (2, 2, 2),表示有 2 个卷积核通道,每个通道的大小为 2x2。
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]],
[[1.0, 2.0], [3.0, 4.0]]])
corr2d_multi_in(X, K)
# 计算多个通道的输出的互相关函数
# X 3d, K:4d
def corr2d_multi_in_out(X, K):
# torch.stack 函数用于在指定维度上对张量进行拼接。这里将列表中的多个运算结果张量在第 0 维上进行拼接,形成一个新的张量。
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
# 通过对原始卷积核 K 进行简单的数值变换(分别加 0、加 1、加 2),生成多个不同的卷积核,然后将这些卷积核在第 0 维上进行堆叠,以模拟多个输出通道对应的卷积核。
K = torch.stack((K, K + 1, K + 2), 0)
K.shape
corr2d_multi_in_out(X, K)
# 1 * 1 卷积
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
# assert ... < 1e6:使用 assert 语句进行断言,如果 Y1 和 Y2 的差值之和小于 \(10^6\),则断言通过,说明两种卷积方式得到的结果在一定误差范围内是等价的
assert float(torch.abs(Y1 - Y2).sum()) < 1e6
汇聚层(池化层)
卷积神经网络中,通常会在相邻的卷积层之间加入一个池化层 ,池化层可以有效的缩小参数矩阵的尺寸,从而减少最后连接层的中的参数数量。所以加入池化层可以加快计算速度和防止过拟合 的作用。
* 池化的原理或过程:pooling是在**不同的通道上分开执行的** (就是池化操作不改变通道数),且不需要参数控制。然后根据窗口大小进行相应的操作,一般有**max pooling、average pooling** 等。
池化的作用:
* 特征不变性:池化操作是模型更加关注是否存在某些特征而不是特征具体的位置。其中不变形性包括平移不变性、旋转不变性和尺度不变性。
* 特征降维(下采样):池化相当于在空间范围内做了维度约减,从而使模型可以抽取更加广范围的特征。同时减小了下一层的输入大小,进而减少计算量和参数个数。
* 在一定程度上防止过拟合。(类似ReLU)
汇聚层中的相关操作
代码:
# 实现池化层的正向传播
import torch
from torch import nn
from d2l import torch as d2l
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i:i + p_h, j:j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i:i + p_h, j:j + p_w].mean()
return Y
# 验证二维池化层的实现:
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
# 验证平均池化
pool2d(X, (2, 2), 'avg')
# 填充和步幅
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X
# 深度学习框架(pytorch)中的步幅与池化窗口的大小相同
pool2d = nn.MaxPool2d(3)
pool2d(X)
# 填充和步幅可以指定
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
# 设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度
pool2d = nn.MaxPool2d((2, 3), padding=(1, 1), stride=(2, 3))
pool2d(X)
# 在每个输入通道上单独运算
X = torch.cat((X, X + 1), 1)
X
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
CNN的基本构造
以上所有部分是CNN中的组件,CNN可以概括为以下部分:
1 输入层
输入层接收原始图像数据 。图像通常由三个颜色通道(红、绿、蓝)组成,形成一个二维矩阵,表示像素的强度值。
2 卷积和激活
卷积层将输入图像与卷积核进行卷积操作 ,然后,通过应用激活函数(如ReLU)来引入非线性。这一步使网络能够学习复杂的特征。
3 池化层
通过减小特征图的大小来减少计算复杂性 (选择池化窗口内的最大值或平均值来实现)这有助于提取最重要的特征。
4 多层堆叠
CNN通常由多个卷积和池化层的堆叠 组成,以逐渐提取更高级别的特征。深层次的特征可以表示更复杂的模式。
5 全连接和输出
最后,全连接层将提取的特征映射转化为网络的最终输出 。这可以是一个分类标签、回归值或其他任务的结果。
