本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
beginning
之前给盆友们介绍了卷积神经网络CNN的基本结构,并简单实现了图像的二分类,相信大家早已对卷积层、池化层等结构了然于胸(在这里➡深度学习入门—CNN基本原理及实战)🌻🌻🌻大家在看代码的时候,有没有遇到Adam、backward、dropout等一些陌生的名词腻?其实,这些属于神经网络的训练及优化方法,旨在通过调整网络的参数,使其能够更好地拟合训练数据并提高泛化能力。本文就带领大家利用Pytorch手把手搭建全连接神经网络,在实战中讲解原理,学习并使用这些训练优化策略减少存在的过拟合现象,实现物品的多分类。
1.搭建神经网络分类(含训练优化方法)
1.1下载Fasion-MNIST数据集
我们选择Fasion-MNIST数据集实现十种(T恤、裙子、包包等)时尚物品分类。Fasion-MNIST数据集包含六万张训练图片和一万张测试图片,这是数据集的中文文档说明(为什么要用Fasion-MNIST呢?因为作为初学者入门的MNIST数据集已经被用的太多太多了,并且一些算法在MNIST上表现的好,但在其他数据集上就未必好,特别是对于生成对抗网络这样的算法,它在Fasion-MNIST和MNIST数据集上效果是完全不一样滴)。
下载时不需要额外在它的官网上下载其他文件,直接通过代码的形式就可以下载到本地的文件夹里:
import torch # 导入pytorch
from torch import nn, optim # 导入神经网络与优化器对应的类
import torch.nn.functional as F
from torchvision import datasets, transforms ## 导入数据集与数据预处理的方法
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,), (0.5,))])
# 下载Fashion-MNIST训练集数据
trainset = datasets.FashionMNIST('dataset/', download=True, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
# 下载Fashion-MNIST测试集数据
testset = datasets.FashionMNIST('dataset/', download=True, train=False, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)
以上代码的大白话理解color{blue}{以上代码的大白话理解}:首先进行数据的预处理,构造一个transform方法,把图像转化成pytorch的tensor(张量)类型,之后对它进行标准化让每一个数字都减去平均值再除以标准差,将其控制在-1到+1之间,这样训练出的神经网络的权重都会在零附近,易于训练。然后使用datasets.FashionMNIST()方法把数据集下载到dataset/里面,构造trainloader把训练集图片传进来,每次传进来64张图片,直到把这六万张图片全部过一遍,其中shuffle=True表示每次都是从训练集中随机的选取64张图片(很好理解,训练集总不可能先全都学鞋子的图片,再学全是包包的图片叭,而是应该把各种各样的图片都喂给它)。测试集同理。
下载好之后,我们来用代码看一看数据集中的图片:
image, label = next(iter(trainloader))
# image图片中有64张图片,我们查看索引为2的图片
imagedemo = image[3]
imagedemolabel = label[3]
imagedemo = imagedemo.reshape((28,28))
import matplotlib.pyplot as plt
%matplotlib inline
plt.imshow(imagedemo)
labellist = ['T恤','裤子','套衫','裙子','外套','凉鞋','汗衫','<img src="运动鞋" alt="" width="30%" />','包包','靴子']
print(f'这张图片对应的标签是 {labellist[imagedemolabel]}')
因为它是个iter迭代器,所以每次运行都载入64张图片,我们可以不停的运行看到新图片:
1.2搭建并训练四层全连接神经网络
下载好数据集之后,开始搭建最原始朴素的全连接神经网络:
神经网络的输入为28 * 28 = 784 个像素(因为数据集图片的尺寸为28 * 28)
第一个隐含层包含256个神经元
第二个隐含层包含128个神经元
第三个隐含层包含64个神经元(这里定义了三个隐含层,大家可以自定义多个)
输出层输出10个结果,对应图片的10种分类
具体代码如下color{blue}{具体代码如下}:
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)#第一个层输入784,输出256
self.fc2 = nn.Linear(256, 128)#第二个层输入256,输出128
self.fc3 = nn.Linear(128, 64)#以此类推
self.fc4 = nn.Linear(64, 10)
def forward(self, x):
# make sure input tensor is flattened
x = x.view(x.shape[0], -1)
x = F.relu(self.fc1(x))#之前讲的relu激活函数
x = F.relu(self.fc2(x))
x = F.relu(self.fc3(x))
x = F.log_softmax(self.fc4(x), dim=1)#使用对数的softmax分类
return x
# 对上面定义的Classifier类进行实例化
model = Classifier()
# 定义损失函数为负对数损失函数,目标是把损失函数降到最低
criterion = nn.NLLLoss()
# 优化方法为Adam梯度下降方法,学习率为0.003
optimizer = optim.Adam(model.parameters(), lr=0.003)
# 对训练集的全部数据学习15遍,这个数字越大,训练时间越长
epochs = 15
# 将每次训练的训练误差和测试误差存储在这两个列表里,后面绘制误差变化折线图用
train_losses, test_losses = [], []
print('开始训练')
for e in range(epochs):
running_loss = 0
# 对训练集中的所有图片都过一遍
for images, labels in trainloader:
# 将优化器中的求导结果都设为0,否则会在每次反向传播之后叠加之前的
optimizer.zero_grad()
# 对64张图片进行推断,计算损失函数,反向传播优化权重,将损失求和
log_ps = model(images)
loss = criterion(log_ps, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
# 每次学完一遍数据集,都进行以下测试操作
else:
test_loss = 0
accuracy = 0
# 测试的时候不需要开自动求导和反向传播
with torch.no_grad():
# 关闭Dropout
model.eval()
# 对测试集中的所有图片都过一遍
for images, labels in testloader:
# 对传入的测试集图片进行正向推断、计算损失,accuracy为测试集一万张图片中模型预测正确率
log_ps = model(images)
test_loss += criterion(log_ps, labels)
ps = torch.exp(log_ps)
top_p, top_class = ps.topk(1, dim=1)
equals = top_class == labels.view(*top_class.shape)
# 等号右边为每一批64张测试图片中预测正确的占比
accuracy += torch.mean(equals.type(torch.FloatTensor))
# 恢复Dropout
model.train()
# 将训练误差和测试误差存在两个列表里,后面绘制误差变化折线图用
train_losses.append(running_loss/len(trainloader))
test_losses.append(test_loss/len(testloader))
理解都在注释里说啦,除此之外,还需要对代码中的Loss、Adam、lr、backward、batch_sizecolor{blue}{Loss、Adam、lr、backward、batch_size}等训练优化方法详细介绍一下:
1.2.1损失函数
在神经网络中,损失函数(Loss Function)是用来衡量模型预测结果与真实标签之间的差异程度的函数。它是训练神经网络的关键组成部分,通过最小化损失函数来优化模型的参数。下面介绍几种常见的损失函数。
-
均方误差color{blue}{均方误差}(Mean Squared Error,MSE):均方误差是最常见的损失函数之一,用于回归问题。它计算预测值与真实值之间的平方差,并求取平均值。公式如下:
MSE=1n∑(y−y∧)2MSE = frac{1}{n}sum {{{(y – mathop ylimits^ wedge )}^2}}
其中,n表示样本数量,y表示模型的预测值,y^表示真实标签。
-
交叉熵损失color{blue}{交叉熵损失}(Cross-Entropy Loss):通常用于分类问题,特别是多类别分类。它基于信息论中的概念,衡量两个概率分布之间的差异。对于每个样本,交叉熵损失计算预测标签的概率分布与真实标签的概率分布之间的交叉熵。公式如下:
CrossEntropy=−∑(y∧∗log(y))CrossEntropy = – sum ( mathop ylimits^ wedge *log (y))
-
对数损失color{blue}{对数损失}(Log Loss):也常用于二分类问题,尤其是在逻辑回归中。它衡量模型预测样本属于正类的概率与真实标签的负对数似然之间的差异。公式如下:
LogLoss=−∑[y∧∗log(y)+(1−y∧)∗log(1−y)]LogLoss = – sum [ mathop ylimits^ wedge *log (y) + (1 – mathop ylimits^ wedge )*log (1 – y)]
在训练过程中,通过反向传播算法计算损失函数对模型参数的梯度,并使用优化算法(如梯度下降)来更新参数,以最小化损失函数。这样模型就能够逐步优化并提高预测性能。
1.2.2梯度下降
梯度下降是最常用的优化算法之一。在梯度下降中,它计算损失函数对于模型参数的梯度,并沿着梯度的反方向更新参数,以最小化损失函数。包含以下三种变体:
- 批量梯度下降color{blue}{批量梯度下降}(Batch Gradient Descent):批量梯度下降使用整个训练数据集计算损失函数的梯度,并根据梯度更新模型参数。它在每次迭代中都要遍历整个数据集,因此计算效率较低,但通常能够获得较好的收敛性。
- 随机梯度下降color{blue}{随机梯度下降}(Stochastic Gradient Descent):随机梯度下降在每次迭代中只使用一个样本计算损失函数的梯度,并根据梯度更新模型参数。相比于批量梯度下降,随机梯度下降计算效率更高,但由于单个样本的噪声较大,可能导致收敛性不稳定。
- 小批量梯度下降color{blue}{小批量梯度下降}(Mini-batch Gradient Descent):小批量梯度下降是介于批量梯度下降和随机梯度下降之间的一种方法。它在每次迭代中使用一小批量的样本计算损失函数的梯度,并根据梯度更新模型参数。小批量梯度下降综合了两者的优点,既能够减少噪声的影响,又能够提高计算效率。
除了上述常见的梯度下降算法,还有一些改进的变体,如动量梯度下降(Momentum Gradient Descent)、自适应学习率方法(如AdaGrad、RMSprop)等,它们通过引入额外的技巧或调整学习率来提高梯度下降的性能和收敛速度。
本代码使用的是Adam梯度下降算法,到底该怎么通俗理解腻?color{blue}{Adam梯度下降算法,到底该怎么通俗理解腻?}就好比我们要从珠穆朗玛峰从上往下走,目的地是去海拔为零的地方,但是在下山的路上可能会遇到一个地方叫四川盆地,我们掉到这个四川盆地里面可能就会乐不思蜀,会以为这里就是世界上海拔最低的地方,我们就会困在这个局部最优点下不去。为了解决这一问题,我们可以采用其他方法,比如我们可以以很大的动量,想象一个大胖子一路从珠穆朗玛峰跑下来,跑到四川盆地他可能刹不住车,一路往前跑,这样他就会跨过局部最优点,找到更低的地方。Adam就是类似这样的算法。怎么样,太形象贴切易于理解了叭🍭🍭🍭
1.2.3学习率
在神经网络中,学习率(learning rate)是一个重要的超参数,用于控制模型在每次迭代中更新权重和偏差的速度。学习率决定了模型在训练过程中每一步的调整幅度。
学习率的选择对于神经网络的训练非常关键。如果学习率设置得太小,模型收敛的速度会很慢,需要更多的迭代才能达到较好的性能;而如果学习率设置得太大,模型可能会发生震荡或无法收敛。
通常情况下,学习率的选择是一个经验性的过程,需要根据具体问题和数据集进行调整。以下是一些常见的学习率调整策略:
- 固定学习率color{blue}{固定学习率}:最简单的方法是使用固定的学习率,在整个训练过程中保持不变。这种方法适用于较小的数据集或者简单的问题,但对于复杂的问题可能需要更精细的调整。
- 学习率衰减color{blue}{学习率衰减}:学习率衰减是一种常见的策略,它可以使学习率随着训练的进行逐渐减小。常见的衰减方式包括按照固定的步长进行衰减,或者根据训练损失的变化进行自适应调整。
- 动量法color{blue}{动量法}:动量法可以帮助加速模型的收敛,并且在参数空间中更快地穿过平坦区域。它通过引入一个动量项来更新权重和偏差,该动量项考虑了之前迭代步骤的梯度信息。动量法可以减少震荡并提高模型的稳定性。
- 自适应学习率方法color{blue}{自适应学习率方法}:自适应学习率方法根据模型的表现动态地调整学习率。常见的自适应学习率方法包括Adagrad、RMSprop和Adam等。这些方法根据参数的历史梯度信息来自适应地调整学习率,以便更好地适应不同的数据分布和问题。
需要注意的是,选择合适的学习率也需要考虑其他超参数的影响,如批量大小(batch size)、正则化等。通常情况下,需要进行多次实验和交叉验证来找到最佳的学习率设置。
1.2.4反向传播
反向传播(Backpropagation)是神经网络中用于训练模型的一种常用算法。它通过计算损失函数对模型参数的梯度,然后利用梯度下降法来更新参数,从而使得模型能够逐步优化并适应给定的训练数据。
下面是反向传播算法的详细步骤color{blue}{反向传播算法的详细步骤}:
- 初始化:首先,需要随机初始化神经网络的权重和偏置。这些参数将在训练过程中进行调整以最小化损失函数。
- 前向传播:使用当前的权重和偏置,将输入样本通过神经网络进行前向传播,计算出每个神经元的输出值。这个过程可以看作是对输入数据进行一系列的线性和非线性变换。
- 计算损失函数:将神经网络的输出与真实标签进行比较,计算出模型的预测误差。常见的损失函数正如上文所介绍的。
- 反向传播:根据损失函数,计算每个参数对损失的梯度。这一步是反向传播算法的核心。具体而言,从输出层开始,根据链式法则逐层计算每个神经元的梯度。梯度表示了损失函数对参数的变化率,可以告诉我们在参数空间中应该朝哪个方向更新参数。
- 参数更新:根据计算得到的梯度,使用梯度下降法或其他优化算法来更新神经网络的权重和偏置。梯度下降法通过不断迭代地沿着梯度的反方向调整参数,逐渐减小损失函数的值,
- 重复训练:重复执行步骤2到步骤5,直到达到预定的停止条件,例如达到最大迭代次数或损失函数的变化很小。
1.2.5Mini-batch
为什么要一批一批的导入数据集的图片和标签呢?这就叫Mini-batch(小批量)。Minibatch是一种常用的训练方法,它将训练数据集划分为较小的批次进行处理,每个批次由一组输入样本和对应的目标值组成。通俗理解color{blue}{通俗理解},我们当然可以把六万张图片一起传进去一起训练,但是那样的话运算量特别大,而且我们把这六万张图片都吃一遍之后,只能修改一丢丢的权重,因为梯度下降方法是走小碎步的方法,我们是从珠穆朗玛峰走小碎步到达海拔为零的地方,如果每走一条小碎步都要处理六万条数据的话,那么走路的速度会大打折扣。所以我们采取的一种策略是每处理64张图片就走一步,这样运算量就特别小,就会更快地走到目的地。
训练完毕,就会得到以下训练结果:
1.3验证模型效果
接着,我们绘制训练误差和测试误差随学习次数增加的变化:
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(train_losses, label='Training loss')
plt.plot(test_losses, label='Validation loss')
plt.legend()
从图中可以看到,训练误差一直在减小,但是测试误差却居高不下,这就是一种过拟合overfitting的现象。该怎么通俗理解腻?color{blue}{该怎么通俗理解腻?}就好比是一个高分低能的学生,你给他喂了很多题,他通过题海战术把每一题的答案都死记硬背下来,但是当他真正的跨上高考考场,遇到新的问题他就不会做了,这就是一个过拟合的现象。
1.4采用Dropout方法防止过拟合
那么该如何避免过拟合现象呢?我们可以采用Dropout方法。也就是在每次正向推断训练神经元的时候随机“掐死”一部分神经元,阻断其输入输出,这样可以起到正则化的作用。该怎么通俗理解Dropout腻?color{blue}{该怎么通俗理解Dropout腻?}我们训练神经网络是大家一起来提供参数,一起向前输出,如果是最普通的网络的话,就会出现一些网络一家独大只手遮天,很可能一些神经元拥有话语权,一些神经元就会被打入冷宫,那么有了Dropout之后,今天受宠的神经元明天可能就被打入冷宫了,所以就避免了杨贵妃那样的神经元存在,所有神经元处于平等的地位,防止过拟合。
需要注意的是,在测试阶段,不再进行Dropout操作。而是将所有神经元都保留,并按照原始权重进行前向传播。这是因为在测试阶段,我们希望获得模型在完整状态下的性能评估。
具体到代码上也非常简单,构造如下:
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 64)
self.fc4 = nn.Linear(64, 10)
# 构造Dropout方法,在每次训练过程中都随机“掐死”百分之二十的神经元,防止过拟合。
self.dropout = nn.Dropout(p=0.2)
def forward(self, x):
# 确保输入的tensor是展开的单列数据,把每张图片的通道、长度、宽度三个维度都压缩为一列
x = x.view(x.shape[0], -1)
# 在训练过程中对隐含层神经元的正向推断使用Dropout方法
x = self.dropout(F.relu(self.fc1(x)))
x = self.dropout(F.relu(self.fc2(x)))
x = self.dropout(F.relu(self.fc3(x)))
# 在输出单元不需要使用Dropout方法
x = F.log_softmax(self.fc4(x), dim=1)
return x
之后按照上边同样的方法对其训练。使用了Dropout之后,我们再来看看误差和预测准确率的情况:
可以看到训练误差和测试误差都随学习次数增加逐渐降低,没有出现“高分低能”和“死记硬背”的过拟合现象。预测的准确率也大大提升,高达99%✌✌✌
ending
看到这里相信盆友们对图像多分类、准确率与误差分析、搭建全连接神经网络、通过Adam算法进行梯度下降训练并利用Dropout防止过拟合有了更多的认识。
后续还会出一系列关于深度学习的文章,所以如果你也对计算机视觉感兴趣,想了解更多关于深度学习的知识,请多多关注我叭🌴🌴🌴
please一键三连嗷!!!下期见