FROM
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
我的环境
- 语言环境:Python 3.8.10
- 开发工具:Jupyter Lab
- 深度学习环境:
- torch==1.12.1+cu113
- torchvision==0.13.1+cu113
1. 准备知识
1.1 检查环境
import torch # 导入PyTorch库,用于构建深度学习模型
import torch.nn as nn # 导入torch.nn模块,包含构建神经网络所需的类和函数
import matplotlib.pyplot as plt # 导入matplotlib.pyplot模块,用于数据可视化
import torchvision # 导入torchvision库,包含处理图像和视频的工具和预训练模型
# 设置硬件设备,如果有GPU则使用,没有则使用cpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检查系统是否有可用的GPU,如果有则使用GPU,否则使用CPU
device # 打印当前设备,以确认是使用GPU还是CPU
输出:
1.2 数据导入
torchvision.datasets.MNIST
torchvision.datasets
是Pytorch自带的一个数据库,可以通过代码在线下载数据。
这行代码是用于加载MNIST数据集的。MNIST是一个大型的手写数字数据库,常用于训练各种图像处理系统。代码中的参数含义如下:
root
: 数据集的本地存储路径。train
: 布尔值,如果为True,则加载训练集;如果为False,则加载测试集。transform
: 一个可选的transform,用于对图像进行预处理操作,例如缩放、裁剪等。这里设置为None,表示不进行任何预处理。target_transform
: 一个可选的transform,用于对标签进行预处理操作。这里设置为None,表示不进行任何预处理。download
: 布尔值,如果为True,则从网上下载数据集并存储到root指定的路径;如果为False,则假定数据集已经存在于该路径下。
torchvision.datasets.MNIST( # 调用torchvision中的MNIST数据集类
root, # 指定数据集的存储路径
train=True, # 指定加载训练集,如果设置为False,则加载测试集
transform=None, # 指定如何对图像进行预处理,例如缩放、裁剪等,这里设置为None,表示不进行预处理
target_transform=None, # 指定如何对标签进行预处理,这里设置为None,表示不进行预处理
download=False # 指定是否需要下载数据集,如果数据集已经存在于root路径下,则设置为False
)
train_ds = torchvision.datasets.MNIST( # 创建MNIST训练集数据对象
'data', # 指定数据集的存储路径
train=True, # 指定加载训练集
transform=torchvision.transforms.ToTensor(), # 指定预处理操作,将图像转换为Tensor
download=True # 如果数据集不存在,则下载数据集
)
test_ds = torchvision.datasets.MNIST( # 创建MNIST测试集数据对象
'data', # 指定数据集的存储路径
train=False, # 指定加载测试集
transform=torchvision.transforms.ToTensor(), # 指定预处理操作,将图像转换为Tensor
download=True # 如果数据集不存在,则下载数据集
)
输出:
torch.utils.data.DataLoader
torch.utils.data.DataLoader
是Pytorch自带的一个数据加载器,结合了数据集和取样器,并且可以提供多个线程处理数据集。
torch.utils.data.DataLoader(
dataset, # 要加载的数据集对象
batch_size=1, # 每个批次的样本数,默认为1
shuffle=None, # 是否在每个epoch开始时打乱数据,默认为None,根据sampler和batch_sampler的设置决定
sampler=None, # 定义从数据集中抽取样本的策略,如果设置了sampler,则shuffle应为False
batch_sampler=None, # 与sampler类似,但返回的是样本索引的批次
num_workers=0, # 加载数据时使用的进程数,0表示单进程,正整数表示多进程
collate_fn=None, # 决定如何将多个样本合并为一个批次的函数,默认为'default_collate'
pin_memory=False, # 如果设置为True,则在将数据从CPU传输到GPU之前,先将数据放到CUDA固定内存中
drop_last=False, # 如果为True,则在数据集大小不能被batch_size整除时,丢弃最后一个不完整的批次
timeout=0, # 如果大于0,则设置为非零超时时间,用于数据加载
worker_init_fn=None, # 每个工作进程启动时调用的函数
multiprocessing_context=None, # 用于数据加载进程的多进程上下文
generator=None, # 当使用随机采样时使用的随机数生成器
*, # 确保以下参数必须作为关键字参数传递
prefetch_factor=2, # 数据加载器的预取因子,用于控制预取的数据量
persistent_workers=False, # 如果为True,则在所有数据加载完成后,工作进程不会退出
pin_memory_device='', # 指定用于pin_memory的设备,默认为CPU
)
dataset
: 你想要加载的数据集对象。batch_size
: 通常设置为大于1的整数,这样可以在每个批次中加载多个样本。shuffle=True
: 在训练过程中通常设置为True,以便每个epoch数据都是随机的。num_workers
: 在多核CPU上设置大于0的值可以加速数据加载。collate_fn
: 可以自定义,用于处理最后的数据整理。pin_memory=True
: 如果你在使用GPU,并且希望加速数据从CPU到GPU的传输,可以设置为True。
batch_size = 32 # 设置每个批次的大小为32
train_dl = torch.utils.data.DataLoader( # 创建训练集的DataLoader
train_ds, # 使用之前创建的MNIST训练集数据对象
batch_size=batch_size, # 设置每个批次的大小为32
shuffle=True # 设置为True,以便在每个epoch开始时打乱数据
)
test_dl = torch.utils.data.DataLoader( # 创建测试集的DataLoader
test_ds, # 使用之前创建的MNIST测试集数据对象
batch_size=batch_size # 设置每个批次的大小为32
)
# 取一个批次查看数据格式
# 数据的shape为:[batch_size, channel, height, weight]
# 其中batch_size为自己设定,channel,height和weight分别是图片的通道数,高度和宽度。
imgs, labels = next(iter(train_dl)) # 从训练集的DataLoader中获取一个批次的数据和标签
imgs.shape # 打印图像数据的shape
输出:
这段代码做了以下几件事情:
- 设置批次大小:将
batch_size
设置为32
,这意味着每个批次将包含32个样本。 - 创建训练集
DataLoader
:
train_dl = torch.utils.data.DataLoader(...)
: 创建一个用于训练集的DataLoader
对象。train_ds
: 使用之前创建的MNIST训练集数据对象。batch_size=batch_size
: 设置每个批次的大小为32
。shuffle=True
: 设置为True
,以便在每个epoch
开始时打乱数据。
- 创建测试集
DataLoader
:
test_dl = torch.utils.data.DataLoader(...)
: 创建一个用于测试集的DataLoader
对象。test_ds
: 使用之前创建的MNIST
测试集数据对象。batch_size=batch_size
: 设置每个批次的大小为32
。
- 查看数据格式:
imgs, labels = next(iter(train_dl))
: 从训练集的DataLoader
中获取一个批次的数据和标签。imgs.shape
: 打印图像数据的shape
,以查看数据的维度。
对于MNIST
数据集,图像是灰度的,所以channel
为1
,height
和weight
都是28
。因此,imgs
的形状应该是[32, 1, 28, 28]
,其中32
是批次大小,1
是通道数,28x28
是图像的尺寸。
1.3 数据可视化
squeeze()
函数的功能是从矩阵shape
中,去掉维度为1
的。例如一个矩阵是的shape
是(5, 1)
,使用过这个函数后,结果为(5, )
。
import numpy as np # 导入NumPy库,用于进行科学计算
# 指定图片大小,图像大小为20宽、5高的绘图(单位为英寸inch)
plt.figure(figsize=(20, 5)) # 设置绘图窗口的大小
for i, imgs in enumerate(imgs[:20]): # 遍历前20张图像
# 维度缩减,将图像的维度从[1, 28, 28]变为[28, 28]
npimg = np.squeeze(imgs.numpy()) # 使用numpy.squeeze()函数去除单维度条目
# 将整个figure分成2行10列,绘制第i+1个子图。
plt.subplot(2, 10, i+1) # 创建子图,2行10列的布局,当前是第i+1个位置
# 显示图像,cmap=plt.cm.binary表示使用二值颜色映射
plt.imshow(npimg, cmap=plt.cm.binary)
# 关闭坐标轴
plt.axis('off')
# 显示绘制的图像
输出:
2. 构建简单的CNN网络
卷积神经网络(CNN)是一种深度学习模型,广泛应用于图像识别、分类和分割等任务。下面是一些主要组件的详细说明:
- 卷积层 (
nn.Conv2d
)
- 作用:用于提取图像的特征。通过学习图像中局部区域的特征,卷积层可以捕捉到图像的局部特征。
- 参数:
in_channels
:输入图像的通道数(例如,RGB
图像的通道数为3
)。out_channels
:输出图像的通道数,即卷积核(过滤器)的数量。kernel_size
:卷积核的大小,通常是一个元组或整数,如(3, 3)
。
- 池化层 (
nn.MaxPool2d
)
- 作用:用于降低特征图的空间尺寸,减少计算量和防止过拟合。池化层通常跟在卷积层后面。
- 参数:
kernel_size
:池化窗口的大小。
- 激活函数 (
nn.ReLU
)
- 作用:引入非线性,使得模型可以学习和模拟更复杂的函数。ReLU(Rectified Linear Unit)是最常用的激活函数之一,定义为
f(x) = max(0, x)
。 - 优点:计算简单,收敛速度快。
- 全连接层 (
nn.Linear
)
- 作用:在CNN中,全连接层通常用于最终的分类或回归任务。在特征提取的最后阶段,全连接层将学习到的高级特征映射到最终的输出类别。
- 参数:
in_features
:输入特征的数量。out_features
:输出特征的数量,即类别数或你想要的输出维度。
- 序列模型 (
nn.Sequential
)
- 作用:按顺序包含一系列的模块,使得模型的构建更加直观和简单。在
nn.Sequential
中定义的模块将按照它们被添加的顺序执行。 - 用法:非常适合构建简单的线性堆叠模型。
import torch.nn.functional as F # 导入PyTorch的函数库,用于提供一些常用的函数,如ReLU和池化操作
num_classes = 10 # 定义图片的类别数,例如在MNIST数据集中有10个数字类别
class Model(nn.Module): # 定义一个名为Model的类,继承自nn.Module
def __init__(self): # 类的初始化函数
super().__init__() # 调用父类的初始化函数
# 特征提取网络
# 添加第一层卷积层,输入通道数为1,输出通道数为32,卷积核大小为3x3
self.conv1 = nn.Conv2d(1, 32, kernel_size=3)
# 添加第一层池化层,池化核大小为2x2
self.pool1 = nn.MaxPool2d(2)
# 添加第二层卷积层,输入通道数为32,输出通道数为64,卷积核大小为3x3
self.conv2 = nn.Conv2d(32, 64, kernel_size=3)
# 添加第二层池化层,池化核大小为2x2
self.pool2 = nn.MaxPool2d(2)
# 分类网络
# 添加第一层全连接层,输入特征数为1600,输出特征数为64
self.fc1 = nn.Linear(1600, 64)
# 添加第二层全连接层,输入特征数为64,输出特征数为类别数
self.fc2 = nn.Linear(64, num_classes)
# 前向传播
def forward(self, x): # 定义前向传播函数
# 通过第一层卷积层,然后应用ReLU激活函数,最后通过第一层池化层
x = self.pool1(F.relu(self.conv1(x)))
# 通过第二层卷积层,然后应用ReLU激活函数,最后通过第二层池化层
x = self.pool2(F.relu(self.conv2(x)))
x = torch.flatten(x, start_dim=1) # 将特征图展平为一维向量,以便输入到全连接层
x = F.relu(self.fc1(x)) # 通过第一层全连接层,并应用ReLU激活函数
x = self.fc2(x) # 通过第二层全连接层,得到最终的分类结果
return x # 返回网络的输出
首先通过两个卷积层和池化层来提取图像的特征,然后将特征图展平,并通过两层全连接层进行分类。F.relu
用于应用ReLU激活函数,torch.flatten
用于将多维特征图展平为一维向量。最后,模型输出一个长度为num_classes
的向量,表示每个类别的预测概率。
代码中self.fc1
的输入特征数设置为1600
,这是基于假设的输入特征图大小。实际的输入特征数需要根据卷积层和池化层之后的输出特征图大小来确定。如果输入图像的尺寸或网络结构发生变化,这个数值可能需要相应地调整。
打印并加载模型:
from torchinfo import summary # 导入summary函数,这个函数可以打印出模型的详细信息。
# 将模型转移到GPU中(我们模型运行均在GPU中进行)
model = Model().to(device) # 创建模型实例并将其转移到之前定义的设备(GPU或CPU)
summary(model) # 打印模型的摘要
输出:
3. 模型训练
3.1 设置超参数
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-2 # 学习率
opt = torch.optim.SGD(model.parameters(),lr=learn_rate)
nn.CrossEntropyLoss()
是PyTorch中的交叉熵损失函数,它通常用于多分类问题。这个损失函数结合了nn.LogSoftmax()
和nn.NLLLoss()
,即它首先应用对数softmax
函数,然后计算负对数似然损失(Negative Log Likelihood Loss)。- 学习率是优化算法中的一个重要参数,它决定了模型在训练过程中参数更新的幅度。如果学习率设置得过大,可能会导致训练过程中的梯度爆炸,从而无法收敛;如果学习率设置得过小,可能会导致训练过程缓慢,甚至陷入局部最优。
torch.optim.SGD
是PyTorch中的随机梯度下降(Stochastic Gradient Descent)优化器。model.parameters()
是一个生成器,它返回模型中所有可训练的参数。lr=learn_rate
设置优化器的学习率为之前定义的learn_rate
。
3.2 训练函数
optimizer.zero_grad()
- 这个函数用于清零(重置)模型所有参数的梯度。在PyTorch中,梯度是累加的,因此每次进行参数更新前,都需要将梯度清零,以避免将上次迭代的梯度累积到本次迭代中。
- 这一步通常在每次迭代的开始进行,以确保每次计算的梯度都是当前批次数据的梯度,而不是累积的梯度。
loss.backward()
- 这个函数用于进行反向传播。在PyTorch中,当您对一个Tensor调用
.backward()
方法时,会自动计算这个Tensor的梯度,并且沿着计算图反向传播,直到所有的叶子节点(即模型的参数)。 loss.backward()
会将损失函数关于模型参数的梯度计算出来,并将这些梯度保存到对应参数的.grad
属性中。- 需要注意的是,只有设置了
requires_grad=True
的Tensor才会在反向传播中计算梯度。
optimizer.step()
- 这个函数用于根据计算出的梯度更新模型的参数。在调用
optimizer.step()
之前,需要确保已经调用了loss.backward()来计算梯度。 optimizer.step()
会根据优化器的算法(例如SGD、Adam等)和学习率来更新参数的值,实现模型的学习。
optimizer只负责通过梯度下降进行优化,而不负责产生梯度,梯度是tensor.backward()
方法产生的。
# 训练循环
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的大小,例如MNIST数据集有60000张图片
# 计算批次数目,例如MNIST数据集有60000张图片,批次大小为32,则有1875个批次
num_batches = len(dataloader)
train_loss, train_acc = 0, 0 # 初始化训练损失和准确率
for X, y in dataloader: # 遍历数据加载器,获取每个批次的图片和标签
X, y = X.to(device), y.to(device) # 将图片和标签转移到指定的设备(GPU或CPU)
# 计算预测误差
pred = model(X) # 前向传播,获取模型的预测输出
loss = loss_fn(pred, y) # 计算模型预测输出和真实标签之间的损失
# 反向传播和优化
optimizer.zero_grad() # 清零模型参数的梯度
loss.backward() # 反向传播,计算梯度
optimizer.step() # 根据梯度更新模型参数
# 记录准确率和损失
# 计算预测正确的数量
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss += loss.item() # 累加损失值
train_acc /= size # 计算平均准确率
train_loss /= num_batches # 计算平均损失
return train_acc, train_loss # 返回训练过程中的平均准确率和平均损失
pred.argmax(1) == y
计算的是预测类别是否与真实类别一致,结果是一个布尔类型的Tensor,当预测正确时为 True
,预测错误时为False
。通过 .type(torch.float)
将其转换为浮点数,这样就可以进行求和操作,得到正确预测的数量。最后,将正确预测的数量除以数据集的总大小,得到平均准确率。
3.3 测试函数
def test(dataloader, model, loss_fn):
# 测试集的大小,例如MNIST数据集有10000张图片
size = len(dataloader.dataset)
# 计算批次数目,例如10000张图片,批次大小为32,则有312.5个批次,实际取整为313
num_batches = len(dataloader)
test_loss, test_acc = 0, 0 # 初始化测试损失和准确率
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad(): # 使用torch.no_grad()上下文管理器,停止计算和存储梯度
for imgs, target in dataloader: # 遍历数据加载器,获取每个批次的图片和标签
# 将图片和标签转移到指定的设备(GPU或CPU)
imgs, target = imgs.to(device), target.to(device)
# 计算损失
target_pred = model(imgs) # 前向传播,获取模型的预测输出
loss = loss_fn(target_pred, target) # 计算模型预测输出和真实标签之间的损失
test_loss += loss.item() # 累加损失值
# 计算预测正确的数量
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size # 计算平均准确率
test_loss /= num_batches # 计算平均损失
return test_acc, test_loss # 返回测试过程中的平均准确率和平均损失
with torch.no_grad()
: 是一个上下文管理器,它告诉PyTorch在该块内部不跟踪梯度,因此不会计算梯度,这样可以减少内存消耗并加速计算。因为在测试过程中,我们不需要进行反向传播和参数更新。
target_pred.argmax(1) == target
计算的是预测类别是否与真实类别一致,结果是一个布尔类型的Tensor,当预测正确时为 True
,预测错误时为 False
。通过 .type(torch.float)
将其转换为浮点数,这样就可以进行求和操作,得到正确预测的数量。最后,将正确预测的数量除以数据集的总大小,得到平均准确率。
3.4 训练
# 设置训练周期数为5
epochs = 5
# 初始化用于存储每个epoch训练损失的列表
train_loss = []
# 初始化用于存储每个epoch训练准确率的列表
train_acc = []
# 初始化用于存储每个epoch测试损失的列表
test_loss = []
# 初始化用于存储每个epoch测试准确率的列表
test_acc = []
# 开始训练循环
for epoch in range(epochs):
# 设置模型到训练模式
model.train()
# 训练模型,并返回当前epoch的训练准确率和损失
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
# 设置模型到评估模式
model.eval()
# 测试模型,并返回当前epoch的测试准确率和损失
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
# 将训练准确率和损失添加到对应的列表中
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
# 将测试准确率和损失添加到对应的列表中
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
# 定义输出格式模板
template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}')
# 打印当前epoch的进度信息
print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss, epoch_test_acc*100, epoch_test_loss))
# 训练完成后打印'Done'
print('Done')
输出:
4. 结果可视化
import matplotlib.pyplot as plt # 导入matplotlib的pyplot模块,用于数据可视化
# 隐藏警告
import warnings
warnings.filterwarnings("ignore") # 忽略警告信息,避免绘图时出现警告提示
# 设置matplotlib的配置参数
plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体,使得图表可以正常显示中文
plt.rcParams['axes.unicode_minus'] = False # 设置正常显示负号
plt.rcParams['figure.dpi'] = 100 # 设置图像的分辨率
epochs_range = range(epochs) # 创建一个从0到epochs-1的范围,用于x轴的刻度
# 设置图像大小
plt.figure(figsize=(12, 3))
# 创建一个1行2列的子图布局,并定位到第1个子图
plt.subplot(1, 2, 1)
# 在第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') # 设置子图的标题
# 创建一个1行2列的子图布局,并定位到第2个子图
plt.subplot(1, 2, 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()
输出: