动手学深度学习--卷积神经网络
从全连接层到卷积
原理
现在我们给自己一个任务:用神经网络去识别区分出百万级像素的不同图片
回顾一下以前:我们是通过多层感知机来实现的,当面对一张图片的时候,我们将其看成一个像素点矩阵,然后将其从二维拉直到一维上,再通过MLP进行训练
但是我们这次的任务每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。 即使将隐藏层维度降低到1000,这个全连接层也将有 1e6×1e3=1e9个参数。 想要训练这个模型将不可实现,因为需要有大量的GPU、分布式优化训练的经验和超乎常人的耐心。
或许我们是时候需要进行改变了
识别一张图片我们真的需要看全部的像素吗?(不用,局部性)
识别一张图片我们训练的模型对于取图片不同位置(不同像素点矩阵)处就不适用了吗?(不,平移不变性)
我们下面尝试用公式表示上述性质:
来自评论区的一条好评论:w_{i,j,k,l}中,i,j代表输出的点在输出矩阵中的位置,k,l代表输入点在输入的图(或者矩阵)中的位置。那么这个权重矩阵应该记录输入中的每一个点对于输出中的每一个点的影响(也就是权重)。举例来说,比如输入图是4x4的,输出图是2x2的。我需要记录输入图中(1,1), (1,2), ..., (2,1), ..., (4,4)这些所有的点对输出图(1,1)的影响,同理也需要记录这些所有点对输出图中(1,2), ...., (2,2)的影响。那么这时候对于每一组点就有4个参数:输入图的横坐标、纵坐标,输出图的横坐标、纵坐标。所以要想完全记录所有的权重,需要一个4维张量。
将上述两条性质结合起来的形象产物就是卷积核:
f(z)与 g(x-z)中的两个参数z与x-z是否相加为x是判断是否是卷积的一个关键
在深度学习中f(z)可以看成源值,g(x-z)可以看成是对源值的权重影响,积分型号可以变成求和符合
z可以看成我们的起始点,x可以看成我们的目标点
一个f(z)g(x-z)可以表示为起始点的源值f(z)对目标点的影响大小是g(x-z),所以影响值是:f(z)g(x-z)
进一步到二维情况:
这里起始点为(x-1,y-1),目标点是(x,y)
那么g函数的参数是(x-(x-1),y-(y-1))=(1,1)
然后通过我们的卷积核可以知道对应的g(1,1)的值
卷积的平移不变性体现在:卷积核不会随着在图片上的位置的改变而改变,其是不变的
卷积的局部性体现在:卷积核大小不再是图片一整个大小了,一般是较小的一部分区域
代码实现
已知卷积核,训练出想要的Y
import torch
from torch import nn
from d2l import torch as d2l
# 获取数据
X = torch.ones((6, 8))
X[:, 2:6] = 0
print(X)
# 定义二维卷积运算
def corr2d(X, K): #@save
"""计算二维互相关运算"""
h, w = K.shape
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
# 定义模型
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 forward(self, x):
return corr2d(x, self.weight) + self.bias
# 定义模型参数
K = torch.tensor([[1.0, -1.0]])
# 训练
Y = corr2d(X, K)
print(Y)
已知X和Y,要训练出卷积核
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')
卷积层里的填充和步幅
假设输入形状为 𝑛ℎ×𝑛𝑤,卷积核形状为 𝑘ℎ×𝑘𝑤,那么输出形状将是 (𝑛ℎ−𝑘ℎ+1)×(𝑛𝑤−𝑘𝑤+1)
填充
-
为何要填充?
假设以下情景: 有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于 1所导致的。
比如,一个 240×240像素的图像,经过 10层 5×5的卷积后,将减少到 200×200像素。如此一来,原始图像的边界丢失了许多有用信息 -
如何填充?
在许多情况下,我们需要设置 𝑝ℎ=𝑘ℎ−1和 𝑝𝑤=𝑘𝑤−1,使输入和输出具有相同的高度和宽度
假设 𝑘ℎ是奇数,我们将在高度的两侧填充 𝑝ℎ/2行。 如果 𝑘ℎ是偶数,则一种可能性是在输入顶部填充 ⌈𝑝ℎ/2⌉行,在底部填充 ⌊𝑝ℎ/2⌋行。同理,我们填充宽度的两侧。
步幅
-
为何要步幅?
有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。步幅则可以在这类情况下提供帮助。 -
如何设置?
代码实现
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
可以看到我们设置了填充为ph=0,pw=2*1 (注意这里是左右两边都填充了一列,总共填充了2列)
设置了步幅为sh=3,sw=4
还需要注意,在nn库中实现的这个卷积核模型是在卷积核中设置填充的,并不是在我们的输入数据中设置填充
卷积层里的多输入多输出通道
多输入通道
彩色图像具有标准的RGB通道来代表红、绿和蓝。 但是到目前为止,我们仅展示了单个输入和单个输出通道的简化例子
每个RGB输入图像具有 3×ℎ×𝑤的形状。我们将这个大小为 3的轴称为通道(channel)维度
所以实际上我们的输入图形是有多张二维矩阵形成的三维形状
彩色是通过RGB通过颜色比例进行组合得到的
所以我们可以想象我们的多维卷积核是对不同维度上二维图片的权重
同一维度上的二维输入与二维卷积核进行卷积操作,最终将各维上计算出来的矩阵相加
这就是一种“组合”
多输出通道
当是单通道的时候我们只能输出一种图形的模式
(比如我们这个卷积核可能是学习边缘化用的,当只有单通道的时候,图片输出的就只有边缘化的样子了)
当我们可能希望输入后,通过设置学“边缘”的卷积核,学“锐化”的卷积核,....能够输出的图像也有边缘化的,锐化的...
如这里我们设置了输入通道Ci为3,输出通道C0为2
即简单来说,可以看做这个输入通道为3的卷积核为学“边缘”的
这个是学“锐化”的
然后输出两张图片,一张边缘化的,一张锐化的
1×1卷积层
池化层
- 需要池化层的原因
当检测较底层的特征时,我们通常希望这些特征保持某种程度上的平移不变性。
例如,如果我们拍摄黑白之间轮廓清晰的图像X,并将整个图像向右移动一个像素,
即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。
而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。
它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
汇聚层(也就是池化层)不包含参数。 相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
代码实现
torch的nn库中实现了最大汇聚层(maximum pooling)和平均汇聚层(average pooling)
# 将上下左右都填充了1行,上下左右步幅都为2
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
需要注意的是池化层在多输入通道时是不进行合并的,这与卷积层不同
卷积神经网络CNN 之 LeNet
卷积神经网络中的超参数
其实输入通道也是超参数,只不过输入通过是由上一层的输出通道数决定的(想象一下我们像多层感知机一样,会用很多层的卷积层进行训练),所以最后其实还是输出通道数是卷积层的超参数
我们真正需要训练的是卷积核中的数,以及如果用到了多层感知机,其中的weight和bais也是要进行训练的
LeNet
LeNet被广泛用于自动取款机(ATM)机中,帮助识别处理支票的数字。现在,我们已经掌握了卷积层的处理方法,我们可以在图像中保留空间结构。 同时,用卷积层代替全连接层的另一个好处是:模型更简洁、所需的参数更少。
第一层卷积层要进行适当的填充,因为很可能原图中的图像信息在图片边缘上,如果步幅设置的不好,会导致信息缺失
所以可以看到因为我们添加了填充,所以第一层卷积操作并未让图片的大小减少,同时还将输出通道数变多,即将图片不同方面的信息提取出来。
然后给池化层,池化层对于多输入通道只是对每个通道进行卷积操作,所以输出通道数=输入通道数
第二次卷积层将输出通道再次增多,同时压缩了图片空间大小
第一层全连接层(隐藏层)是120个输出,输入为最后一个池化层的16个输出通道5x5的图片大小:165*5=400
import torch
from torch import nn
from d2l import torch as d2l
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10))
需要注意的是每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层。
请注意,虽然ReLU和最大汇聚层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用 5×5卷积核和一个sigmoid激活函数。
具体为何要激活函数,感觉还是卷积层某些地方可能有点像多层感知机(如我们输入1x28x28的图片经过第一层卷积层得到6x28x28的输出,是不是有点像输入1x28的数据经过一层隐藏层得到6x28的输出?)
训练完整代码
import torch
from torch import nn
from d2l import torch as d2l
# 获取数据
## batch_size 表示一次训练要用到batch_size个数据
batch_size = 256
## 我们将batch_size放到数据迭代器中
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# 初始化模型参数
# 这里的超参数是卷积核大小,填充,步幅;池化层的大小,填充,步幅;多层感知机的输入输出神经元个数
# 干脆在下面的模型定义中设置好吧
# 需要训练的是卷积核的数值,多层感知机中的w与b
# 卷积核的数值在nn库中应该在定义的时候(nn.Conv2d())会自动给好 nn.Conv2d()
# 多层感知机的w与b也同样在net.parameters()中了,optimizer = torch.optim.SGD(net.parameters(), lr=lr)
# 而net是我们的nn.Sequential,也就是在nn.Linear()时也自动生成了
# 定义模型
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
# nn.Flatten() 是 PyTorch 中的一个函数,它用于将输入的多维张量(通常是二维或更高维的张量)展平成一维的张量。
# 这通常在神经网络的模型中用于将多维数据转换为一维以供全连接层(或者其他期望一维输入的层)处理。
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10))
# 定义损失函数
# 调用loss = nn.CrossEntropyLoss()即可
# 然后 l = loss(y_hat, y) l.backward() 即可进行反向传播
# 定义损失函数
# 调用optimizer = torch.optim.SGD(net.parameters(), lr=lr)即可
# 然后optimizer.step()即可进行参数更新
# 训练
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型(在第六章定义)"""
# 这里用xavier的方法进行参数的初始化,在避免梯度爆炸或消失那个章节有讲
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
# 查看是在CPU上训练还是在GPU上
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
#动画化训练过程
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
# num_epochs表示训练次数
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
X, y = X.to(device), y.to(device)
optimizer.zero_grad()
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
# 训练时的损失值大小
train_l = metric[0] / metric[2]
# 训练时的精度
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
# 计算在测试集上的精度
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
net.train()
是在 PyTorch 中用于将神经网络模型设置为训练模式的方法。当你调用 net.train()
时,它会将神经网络中的一些层(如批量归一化和丢弃层)切换到训练模式,以便在训练期间执行特定的操作。具体来说,它做以下几件事情:
-
启用梯度计算:在训练模式下,PyTorch 会自动启用梯度计算,以便在反向传播中计算和更新模型的权重。这意味着在训练模式下,你可以执行反向传播和优化步骤。
-
启用批量归一化(Batch Normalization):批量归一化层在训练模式下会根据每个小批量数据的均值和方差来标准化数据,有助于加速训练过程。
-
启用丢弃(Dropout):丢弃是一种正则化技术,它在训练期间随机丢弃一些神经元,以减小过拟合的风险。在训练模式下,丢弃层会执行丢弃操作。
在训练完成后,通常需要将模型切换到评估模式,这可以通过调用 net.eval()
方法来实现。在评估模式下,模型不会执行丢弃操作,并且批量归一化层的行为可能会略有不同,因为不再需要计算均值和方差。
下面是一个示例:
import torch.nn as nn
# 创建一个神经网络模型
net = nn.Sequential(
nn.Linear(100, 64),
nn.ReLU(),
nn.BatchNorm1d(64),
nn.Dropout(0.5),
nn.Linear(64, 10)
)
# 切换到训练模式
net.train()
# 在训练模式下执行训练步骤,包括前向传播和反向传播
# 训练完成后,通常需要切换到评估模式
net.eval()
# 在评估模式下执行模型的评估,如测试或推断
在训练和评估期间,使用 net.train()
和 net.eval()
可以确保模型在不同的阶段执行适当的操作,以获得最佳性能和结果。
实战训练--叶子分类问题
参考资料
如何在使用Dataset和DataLoader的情况下使用K折交叉验证
个人想法
本文作者:次林梦叶的小屋
本文链接:https://www.cnblogs.com/cilinmengye/p/17764227.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。