- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍦 参考文章:365天深度学习训练营-第P1周:实现mnist手写数字识别
- 🍖 原作者:K同学啊|接辅导、项目定制
目录
- 一、 前期准备
- 1. 设置GPU
- 2. 导入数据
- ⭐ torchvision.datasets.MNIST详解
- torch.utils.data.DataLoader介绍
- 3. 数据可视化
- 二、构建简单的CNN网络
- 1.网络结构介绍
- 2.代码解释
- 3.加载并打印模型
- 三、 训练模型
- 1. 设置超参数
- 2. 编写训练函数
- 2.1 optimizer.zero_grad()
- 2.2 loss.backward()
- 2.3 optimizer.step()
- 2.4 代码解释
- 2.5 训练函数总结
- 3. 编写测试函数
- 代码解释
- 4. 正式训练
- 1. model.train()和model.eval()作用介绍
- 2. 编写训练循环
- 四、 结果可视化
一、 前期准备
1. 设置GPU
如果设备上支持GPU就使用GPU,否则使用CPU。
导入包
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import torchvision
2. 导入数据
官方网站下载:mnist手写数字数据集
文件名称 | 大小 | 内容 |
---|---|---|
train-labels-idx1-ubyte.gz | 9,681 kb | 55000张训练集,5000张验证集 |
train-labels-idx1-ubyte.gz | 29 kb | 训练集图片对应的标签 |
t10k-images-idx3-ubyte.gz | 1,611 kb | 10000张测试集 |
t10k-labels-idx1-ubyte.gz | 5 kb | 测试集图片对应的标签 |
本文章使用dataset下载MNIST数据集,并划分好训练集与测试集
使用dataloader加载数据,并设置好基本的batch_size
⭐ torchvision.datasets.MNIST详解
torchvision.datasets
是Pytorch自带的一个数据库,我们可以通过代码在线下载数据,这里使用的是torchvision.datasets
中的MNIST
数据集。
函数原型:
torchvision.datasets.MNIST(root, train=True, transform=None, target_transform=None, download=False)
参数说明:
- root (string) :数据地址
- train (string) :
True
= 训练集,False
= 测试集 - download (bool,optional) : 如果为
True
,从互联网上下载数据集,并把数据集放在root目录下。 - transform (callable, optional ):这里的参数选择一个你想要的数据转化函数,直接完成数据转化
- target_transform (callable,optional) :接受目标并对其进行转换的函数/转换。
train_ds = torchvision.datasets.MNIST('data',
train=True,
transform=torchvision.transforms.ToTensor(), # 将数据类型转化为Tensor
download=True)
test_ds = torchvision.datasets.MNIST('data',
train=False,
transform=torchvision.transforms.ToTensor(), # 将数据类型转化为Tensor
download=True)
这段代码使用 PyTorch 中的 torchvision 库来加载 MNIST 数据集。
MNIST 是一个手写数字数据集,包含 60,000 张训练图像和 10,000 张测试图像,每张图片大小为 28x28 像素。该数据集常被用来作为图像分类、图像识别等机器学习任务的基准数据集。
具体地,这段代码中:
- train_ds 表示训练数据集;
- test_ds 表示测试数据集;
- ‘data’ 是数据集存储的路径;
- transform=torchvision.transforms.ToTensor() 是对数据集进行预处理的一种方式,将图像数据类型转换为 PyTorch 中的 Tensor 类型;
- download=True 表示如果本地没有缓存数据集,则在下载数据集后再加载。
总之,这段代码主要是通过 PyTorch 提供的 API 来方便地加载 MNIST 数据集,并对数据集进行了简单的预处理。
torch.utils.data.DataLoader介绍
torch.utils.data.DataLoader
是PyTorch中的一个模块,用于加载数据并生成批次。它可以自动分批、打乱数据并使用多进程加速数据加载。
具体来说,DataLoader
接收一个Dataset
对象作为输入,并允许用户定义一些参数,如批大小、是否打乱数据等。然后它会将数据集分成若干个大小相等的批次,并返回一个迭代器,用户可以使用这个迭代器来逐批地加载数据。
DataLoader
还支持多进程数据加载,可以通过设置num_workers
参数来指定所需的进程数。此外,它可以自动对数据进行随机重排以增加模型的泛化能力。
DataLoader
使得数据加载和处理过程更加方便快捷,特别是对于大规模的数据集,可以极大地提高数据加载的效率和速度。
代码示例:
batch_size = 32
train_dl = torch.utils.data.DataLoader(train_ds,
batch_size=batch_size,
shuffle=True)
test_dl = torch.utils.data.DataLoader(test_ds,
batch_size=batch_size)
上面的代码定义了两个DataLoader
对象:train_dl
和test_dl
,分别用于训练数据集和测试数据集。参数batch_size
指定了每个批次中包含多少个样本。在这里,设为32,即每个批次包含32个样本。
对于train_dl
,参数shuffle=True
表示需要将训练集随机打乱,并按照指定的batch_size
大小划分成若干个批次。这是为了避免模型过度拟合训练集中的某些数据点。
对于test_dl
,由于没有必要对测试集进行随机重排,因此不需要设置shuffle
参数(默认值为False
)。同样,也会按照指定的batch_size
大小划分成若干个批次,方便模型进行测试评估。
综上所述,这段代码实现了将训练集和测试集按照指定的批次大小加载到内存中,并且对于训练集进行了随机打乱,以便更好地训练深度学习模型。
# 取一个批次查看数据格式
# 数据的shape为:[batch_size, channel, height, weight]
# 其中batch_size为自己设定,channel,height和weight分别是图片的通道数,高度和宽度。
imgs, labels = next(iter(train_dl))
imgs.shape
3. 数据可视化
squeeze()
函数的功能是从矩阵shape中,去掉维度为1的。例如一个矩阵是的shape是(5, 1),使用过这个函数后,结果为(5, )。
import numpy as np
# 指定图片大小,图像大小为20宽、5高的绘图(单位为英寸inch)
plt.figure(figsize=(20, 5))
for i, imgs in enumerate(imgs[:20]):
# 维度缩减
npimg = np.squeeze(imgs.numpy())
# 将整个figure分成2行10列,绘制第i+1个子图。
plt.subplot(2, 10, i+1)
plt.imshow(npimg, cmap=plt.cm.binary)
plt.axis('off')
上面的代码用于绘制一组图像,并将其分成2行10列,每行有10张图片。具体来说,它包含以下步骤:
-
首先指定了整个图像的大小,
plt.figure(figsize=(20, 5))
将整个figure分成宽20英寸,高5英寸的绘图区域。 -
然后使用循环绘制一组图像。对于每个图像,首先使用
np.squeeze()
函数将维度压缩,然后使用plt.subplot(2, 10, i+1)
将整个figure分成2行10列,绘制第i+1个子图(即第i+1张图片)。 -
然后使用
plt.imshow()
函数显示当前的图像,并使用cmap=plt.cm.binary
将其转换为二进制颜色映射。最后使用plt.axis('off')
取消坐标轴显示。
这段代码实现了一种简单的方式来可视化一组图像,并将其按照指定的布局显示在一个大的绘图区域中。
二、构建简单的CNN网络
1.网络结构介绍
介绍如何构建一个简单的卷积神经网络(CNN),用于图像分类任务。具体来说,该网络由特征提取网络和分类网络两部分组成。
特征提取网络主要包括卷积层、池化层和激活函数,用于提取图像的特征;而分类网络则包括一个或多个全连接层,用于根据提取到的特征对图像进行分类。
- nn.Conv2d表示卷积层,其输入参数包括输入通道数、输出通道数和池化核大小;
- nn.MaxPool2d表示池化层,用于下采样并提高抽象程度;
- nn.ReLU为激活函数,使得模型可以拟合非线性数据;
- nn.Linear为全连接层,其输入参数为输入特征数和输出特征数;
- nn.Sequential可以将这些层按照设定好的顺序连接起来,形成完整的网络结构。
2.代码解释
代码定义了一个简单的卷积神经网络用于图片分类。具体解释如下:
num_classes
:图片的类别数,即输出层神经元的数量。Model(nn.Module)
:定义一个继承自nn.Module
的模型类。__init__(self)
:初始化函数,在此函数中定义网络结构。super().__init__()
:调用父类的__init__()
函数来初始化模型。self.conv1 = nn.Conv2d(1, 32, kernel_size=3)
:定义第一层卷积层,输入通道数为1,输出通道数为32,卷积核大小为3*3。self.pool1 = nn.MaxPool2d(2)
:定义第一层池化层,池化核大小为2*2,使用最大池化。self.conv2 = nn.Conv2d(32, 64, kernel_size=3)
:定义第二层卷积层,输入通道数为32,输出通道数为64,卷积核大小为3*3。self.pool2 = nn.MaxPool2d(2)
:定义第二层池化层,池化核大小为2*2,使用最大池化。self.fc1 = nn.Linear(1600, 64)
:定义一个全连接层,输入特征数为1600,输出特征数为64。self.fc2 = nn.Linear(64, num_classes)
:定义一个全连接层,输入特征数为64,输出特征数为num_classes
。forward(self, x)
:定义前向传播函数,其中x
是输入的数据。x = self.pool1(F.relu(self.conv1(x)))
:第一层卷积、激活和池化操作。x = self.pool2(F.relu(self.conv2(x)))
:第二层卷积、激活和池化操作。x = torch.flatten(x, start_dim=1)
:将卷积得到的特征图展平成一个向量。x = F.relu(self.fc1(x))
:全连接层和激活函数操作。x = self.fc2(x)
:输出层操作。return x
:返回输出结果。
3.加载并打印模型
使用torchinfo
库中的summary()
函数,打印出模型的结构和参数信息。具体解释如下:
from torchinfo import summary
:导入summary()
函数。model = Model().to(device)
:创建一个模型实例,并将其移动到指定设备(例如GPU)中以进行运算。summary(model)
:调用summary()
函数来打印模型的详细信息,包括每层的输入输出形状、参数数量等。这可以帮助我们更好地理解模型的结构和性能,并且检查模型是否符合预期。
from torchinfo import summary
model = Model().to(device)
summary(model)
三、 训练模型
1. 设置超参数
定义损失函数、学习率和优化器。具体解释如下:
loss_fn = nn.CrossEntropyLoss()
:使用交叉熵损失函数来衡量模型输出结果和真实标签之间的差异。该损失函数通常用于多分类任务中。learn_rate = 1e-2
:设置学习率,即参数更新时的步长大小。学习率越大,模型更新得越快,但可能会导致过拟合。学习率越小,模型更新得越慢,但可能会更准确和稳定。opt = torch.optim.SGD(model.parameters(),lr=learn_rate)
:使用随机梯度下降(SGD)算法来优化模型。这里通过model.parameters()
获取所有需要更新的参数,同时设置学习率为learn_rate
。优化器将根据损失函数计算得到的梯度来对模型中的参数进行更新,以最小化损失函数的值。
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-2 # 学习率
opt = torch.optim.SGD(model.parameters(),lr=learn_rate)
2. 编写训练函数
2.1 optimizer.zero_grad()
函数会遍历模型的所有参数,通过内置方法截断反向传播的梯度流,再将每个参数的梯度值设为0,即上一次的梯度记录被清空。
2.2 loss.backward()
PyTorch的反向传播(即tensor.backward()
)是通过autograd包来实现的,autograd包会根据tensor进行过的数学运算来自动计算其对应的梯度。
具体来说,torch.tensor是autograd包的基础类,如果你设置tensor的requires_grads为True,就会开始跟踪这个tensor上面的所有运算,如果你做完运算后使用tensor.backward()
,所有的梯度就会自动运算,tensor的梯度将会累加到它的.grad属性里面去。
更具体地说,损失函数loss是由模型的所有权重w经过一系列运算得到的,若某个w的requires_grads为True,则w的所有上层参数(后面层的权重w)的.grad_fn属性中就保存了对应的运算,然后在使用loss.backward()
后,会一层层的反向传播计算每个w的梯度值,并保存到该w的.grad属性中。
如果没有进行tensor.backward()
的话,梯度值将会是None,因此loss.backward()
要写在optimizer.step()
之前。
2.3 optimizer.step()
step()函数的作用是执行一次优化步骤,通过梯度下降法来更新参数的值。因为梯度下降是基于梯度的,所以在执行optimizer.step()
函数前应先执行loss.backward()
函数来计算梯度。
注意:optimizer只负责通过梯度下降进行优化,而不负责产生梯度,梯度是tensor.backward()
方法产生的。
2.4 代码解释
代码定义一个训练函数,用于在训练数据集上对模型进行训练。具体解释如下:
def train(dataloader, model, loss_fn, optimizer):
:定义一个名为“train”的函数,该函数有四个参数,包括数据集、模型、损失函数和优化器。size = len(dataloader.dataset)
:获取数据集中样本的总数,等于每个epoch需要处理的图片数量。num_batches = len(dataloader)
:计算数据集中批次的数量,即每个epoch需要迭代的次数。train_loss, train_acc = 0, 0
:初始化训练损失和正确率。for X, y in dataloader:
:遍历数据集中的每个batch,获取输入数据X和对应的标签y。X, y = X.to(device), y.to(device)
:将输入数据X和标签y移动到指定设备上,以便在GPU上进行计算。pred = model(X)
:使用模型对输入数据进行前向传播,得到模型的输出结果pred。loss = loss_fn(pred, y)
:根据模型的输出结果和真实标签y,计算损失值loss。这里使用CrossEntropyLoss作为损失函数。optimizer.zero_grad()
:清空之前的梯度信息,以准备接收新的梯度值。loss.backward()
:根据当前的损失值,计算模型中所有可训练参数的梯度,即反向传播过程。optimizer.step()
:使用指定的优化算法来更新模型中所有可训练参数的权重和偏置等值,即参数更新过程。train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
:计算当前batch中模型预测正确的样本数量,并累加到总的正确数train_acc中。train_loss += loss.item()
:将当前batch的损失值loss累加到总的损失值train_loss中。train_acc /= size
:在一个epoch结束后,计算模型在训练集上的平均正确率train_acc,即分类准确率。train_loss /= num_batches
:在一个epoch结束后,计算模型在训练集上的平均损失train_loss。
2.5 训练函数总结
主要涉及到PyTorch中深度学习的三个基本步骤,即前向传播、反向传播和参数更新。具体解释如下:
optimizer.zero_grad()
:将优化器上一次计算出的梯度清零,以准备接收新的梯度值。loss.backward()
:根据当前的损失值,通过自动求导计算模型中所有可训练参数的梯度,即反向传播过程。这里是在整个batch数据上计算的损失函数,因此损失值是一个标量,而梯度相对于每个参数都是一个向量。optimizer.step()
:根据梯度值,使用指定的优化算法(例如SGD)来更新模型中所有可训练参数的权重和偏置等值,即参数更新过程。
在训练模型时,这三个步骤通常会在一个循环中不断重复,直到模型收敛或达到预设的迭代次数。其中,前向传播和反向传播是计算模型的梯度信息,而参数更新是根据梯度信息来调整模型中的参数值,以最小化损失函数。需要注意的是,每次进行参数更新时,都需要清空之前的梯度信息,以免梯度累积导致结果出错。
3. 编写测试函数
测试函数和训练函数大致相同,但是由于不进行梯度下降对网络权重进行更新,所以不需要传入优化器。
代码解释
代码定义了一个测试函数,用于评估模型在测试集上的性能。具体解释如下:
dataloader
:传入测试数据集的DataLoader对象。model
:需要被测试的模型。loss_fn
:定义损失函数。size = len(dataloader.dataset)
:获取测试集的大小,即测试集中图片的数量。num_batches = len(dataloader)
:获取批次数目,即测试集被分成多少个批次进行预测和评估。test_loss, test_acc = 0, 0
:初始化测试集损失和准确率为0。with torch.no_grad():
:在测试时不需要计算梯度,因此可以使用torch.no_grad()
上下文管理器来关闭梯度计算,从而节省内存。for imgs, target in dataloader:
:从测试集的DataLoader中循环读取每个批次的数据。imgs, target = imgs.to(device), target.to(device)
:将读取的数据放到设备上(CPU或GPU)。target_pred = model(imgs)
:使用模型进行预测,得到预测结果。loss = loss_fn(target_pred, target)
:用损失函数计算预测结果与真实值之间的误差。test_loss += loss.item()
:累加每个批次的损失。(target_pred.argmax(1) == target).type(torch.float).sum().item()
:计算每个批次预测正确的图片数量。test_acc /= size
:计算测试集准确率。test_loss /= num_batches
:计算测试集平均损失。return test_acc, test_loss
:返回测试集准确率和平均损失。
4. 正式训练
1. model.train()和model.eval()作用介绍
在训练和测试过程中,使用model.train()
和model.eval()
的作用。具体来说:
model.train()
:启用 Batch Normalization 和 Dropout。在训练时,需要加上model.train()
以保证BN层能够用到每一批数据的均值和方差,而对于Dropout,也是随机取一部分网络连接来训练更新参数。model.eval()
:不启用 Batch Normalization 和 Dropout。在测试时,需要加上model.eval()
以保证BN层能够用全部训练数据的均值和方差,即测试过程中要保证BN层的均值和方差不变。对于Dropout,在测试时也需要将所有网络连接都利用起来,即不进行随机舍弃神经元。
需要注意的是,在使用训练好的模型进行预测时,需要先调用model.eval()
来禁用Dropout和BN层的随机性,否则即使不训练,输入数据也会改变权值,导致预测结果不准确。
2. 编写训练循环
编写一个简单的训练循环,包括了多个部分。具体解释如下:
epochs = 5
:设置训练循环的迭代次数。train_loss=[]
,train_acc=[]
,test_loss=[]
,test_acc=[]
:初始化训练过程中损失和准确率记录列表为空。for epoch in range(epochs):
:循环训练每个epoch。model.train()
:启用 Batch Normalization 和 Dropout,进入训练模式。epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
:调用train
函数对当前epoch进行训练,并返回当前epoch的训练准确率和损失。model.eval()
:不启用 Batch Normalization 和 Dropout,进入评估模式。epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
:调用test
函数对当前epoch进行评估,并返回当前epoch的测试准确率和损失。train_acc.append(epoch_train_acc)
,train_loss.append(epoch_train_loss)
,test_acc.append(epoch_test_acc)
,test_loss.append(epoch_test_loss)
:将当前epoch的训练准确率、训练损失、测试准确率和测试损失记录到列表中。template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%,Test_loss:{:.3f}')
:定义输出模板。print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss, epoch_test_acc*100, epoch_test_loss))
:输出当前epoch的训练准确率、训练损失、测试准确率和测试损失。print('Done')
:输出训练过程结束标志。
总体来说,这是一个典型的训练循环,每个epoch包括了训练和评估两个阶段,并记录了每个epoch的训练/测试准确率和损失,以便后续分析和可视化。
结果图展示:
四、 结果可视化
使用Python可视化库matplotlib来绘制训练和验证准确率、训练和验证损失的曲线图。具体解释如下:
- 第一行代码导入了matplotlib.pyplot库,并起了个别名plt。
- 第3-6行代码屏蔽了警告信息,用于避免在运行过程中出现不必要的提示信息。
- 第7行代码设置了字体为SimHei,该字体可以显示中文标签。
- 第8行代码设置了负数显示时不带负号。
- 第9行代码设置了分辨率为100。
- 第11行代码创建一个12*3大小的画布,左右分成两个部分,左边用于绘制训练和验证准确率的曲线,右边用于绘制训练和验证损失的曲线。
- 第13行代码定义了一个子图,将左半部分划分为1行2列,当前画图位置为第一个子图。
- 第15-16行代码用plot函数绘制了训练和验证准确率曲线,其中epochs_range是指横坐标轴,train_acc和test_acc是纵坐标轴上的数据点,label参数表示图例标签,legend函数用于显示图例,title函数用于设置图的标题。
- 第18行代码定义了第二个子图,将右半部分划分为1行2列,当前画图位置为第二个子图。
- 第20-21行代码用plot函数绘制了训练和验证损失曲线,其中epochs_range是指横坐标轴,train_loss和test_loss是纵坐标轴上的数据点,label参数表示图例标签,legend函数用于显示图例,title函数用于设置图的标题。
- 最后一行代码使用show函数显示画布。
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings("ignore") #忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 #分辨率
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()