- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
学习博客是一个很好的学习留痕方式,可以让学习者本身加深对知识的理解和印象,记录学习历程,进行总结反思。本文章是博主跟随《365深度学习训练营》进行深度学习代码学习的Pytorch第三周。本周的内容是用Pytorch实现四中天气(cloudy,rain,shine,sunrise)的识别。本人将以自己探索的视角,尽可能的详细得当地展现出由浅入深、逻辑严明的代码逻辑过程,以及自己的感悟和总结,供初学者进行机器学习和深度学习的参考。如有不足欢迎指正。
本次的数据集由四种天气的大量图片组成。其中包含“cloudy”的300张图片,“rain”的215张图片,“shine”的253张图片,“sunrise”的357张照片。
一、前期准备:
1.支持GPU就用GPU,不支持就用CPU
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
from torchvision import transforms, datasets
import os,PIL,pathlib,random
#os模块提供了与操作系统交互的功能
#PIL模块提供了图像处理的功能
#pathlib模块提供了文件系统路径操作的功能
#random模块用于生成随机数
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#再说一下,CUDA是一种由NVIDIA GPU加速计算平台,若支持则用CUDA(得有GPU),没有就用CPU
device
示例输出:
2.导入数据
data_dir ='./data/'#设置文件路径,得是./path1/path2/path3/这种形式
data_dir = pathlib.Path(data_dir)#把字符串类型的文件夹路径转换为pathlib.Path对象
data_paths = list(data_dir.glob('*'))#glob()获取指定文件路径下所有文件的路径
classeNames = [str(path).split("\\")[1] for path in data_paths]
classeNames
● 第一步:使用pathlib.Path()函数将字符串类型的文件夹路径转换为pathlib.Path对象。
● 第二步:使用glob()方法获取data_dir路径下的所有文件路径,并以列表形式存储在data_paths中。
● 第三步:通过split()函数对data_paths中的每个文件路径执行分割操作,获得各个文件所属的类别名称,并存储在classeNames中
● 第四步:打印classeNames列表,显示每个文件所属的类别名称。
import matplotlib.pyplot as plt
from PIL import Image
# 指定图像文件夹路径
image_folder = './data/cloudy/'
# 获取文件夹中的所有图像文件
image_files = [f for f in os.listdir(image_folder) if f.endswith((".jpg", ".png", ".jpeg"))]
# 创建Matplotlib图像(3行8列)
fig, axes = plt.subplots(3, 8, figsize=(16, 6))
# 使用列表推导式加载和显示图像
for ax, img_file in zip(axes.flat, image_files):
img_path = os.path.join(image_folder, img_file)
img = Image.open(img_path)
ax.imshow(img)
ax.axis('off')
# 显示图像
plt.tight_layout()
plt.show()
total_datadir = './data/'
#先定义数据预处理的操作
#transforms.Compose用于将多个图像预处理操作组合成一个序列
train_transforms = transforms.Compose([
transforms.Resize([224, 224]), # 将输入图片resize成统一尺寸
transforms.ToTensor(), # 这个很重要,将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间
transforms.Normalize( # 标准化处理,转换为标准正态分布(高斯分布),使模型更容易收敛
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]) # 其中 mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的。
])
#加载数据集:指定根目录和进行数据预处理操作
total_data = datasets.ImageFolder(total_datadir,transform=train_transforms)
total_data
# 关于transforms.Compose的更多介绍可以参考:https://blog.csdn.net/qq_38251616/article/details/124878863
3.划分数据集
train_size = int(0.8 * len(total_data))
#train_size表示训练集大小,通过将总体数据长度的80%转换为整数得到;
test_size = len(total_data) - train_size
#test_size表示测试集大小,是总体数据长度减去训练集大小。
train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])
train_dataset, test_dataset
我们使用torch.utils.data.random_split()方法进行数据集划分。该方法将总体数据total_data按照指定的大小比例([train_size, test_size])随机划分为训练集和测试集,并将划分结果分别赋值给train_dataset和test_dataset两个变量。
train_size,test_size
batch_size = 32
train_dl = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=1)
test_dl = torch.utils.data.DataLoader(test_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=1)
# 遍历测试数据集的数据加载器 test_dl
for X, y in test_dl:
# 打印输入数据 X 的形状,格式为 [N, C, H, W]
# N 表示批量大小,C 表示通道数,H 表示高度,W 表示宽度
print("Shape of X [N, C, H, W]: ", X.shape)
# 打印标签数据 y 的形状和数据类型
print("Shape of y: ", y.shape, y.dtype)
# 仅打印第一个批次的数据信息,然后跳出循环
break
torch.utils.data.DataLoader()参数详解
torch.utils.data.DataLoader是 PyTorch 中用于加载和管理数据的一个实用工具类。它允许你以小批次的方式迭代你的数据集,这对于训练神经网络和其他机器学习任务非常有用。DataLoader 构造函数接受多个参数,下面是一些常用的参数及其解释:
- dataset(必需参数):这是你的数据集对象,通常是 torch.utils.data.Dataset 的子类,它包含了你的数据样本。
- batch_size(可选参数):指定每个小批次中包含的样本数。默认值为 1。
- shuffle(可选参数):如果设置为 True,则在每个 epoch 开始时对数据进行洗牌,以随机打乱样本的顺序。这对于训练数据的随机性很重要,以避免模型学习到数据的顺序性。默认值为 False。
- num_workers(可选参数):用于数据加载的子进程数量。通常,将其设置为大于 0 的值可以加快数据加载速度,特别是当数据集很大时。默认值为 0,表示在主进程中加载数据。
- pin_memory(可选参数):如果设置为 True,则数据加载到 GPU 时会将数据存储在 CUDA 的锁页内存中,这可以加速数据传输到 GPU。默认值为 False。
- drop_last(可选参数):如果设置为 True,则在最后一个小批次可能包含样本数小于 batch_size 时,丢弃该小批次。这在某些情况下很有用,以确保所有小批次具有相同的大小。默认值为 False。
- timeout(可选参数):如果设置为正整数,它定义了每个子进程在等待数据加载器传递数据时的超时时间(以秒为单位)。这可以用于避免子进程卡住的情况。默认值为 0,表示没有超时限制。
- worker_init_fn(可选参数):一个可选的函数,用于初始化每个子进程的状态。这对于设置每个子进程的随机种子或其他初始化操作很有用。
二、构建简单的CNN网络
和之前一样,CNN网络仍由特征提取网络和分类网络构成。前者提取图片特征,后者对其进行分类。
1.三个重要函数及其参数说明
1.torch.nn.Conv2d()
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode=‘zeros’, device=None, dtype=None)
关键参数说明:
● in_channels ( int ) – 输入图像中的通道数
● out_channels ( int ) – 卷积产生的通道数
● kernel_size ( int or tuple ) – 卷积核的大小
● stride ( int or tuple , optional ) – 卷积的步幅。默认值:1
● padding ( int , tuple或str , optional ) – 添加到输入的所有四个边的填充。默认值:0
● dilation (int or tuple, optional) - 扩张操作:控制kernel点(卷积核点)的间距,默认值:1。
● groups(int,可选):将输入通道分组成多个子组,每个子组使用一组卷积核来处理。默认值为 1,表示不进行分组卷积。
● padding_mode (字符串,可选) – ‘zeros’, ‘reflect’, ‘replicate’或’circular’. 默认:‘zeros’
2.torch.nn.Linear()
torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)
关键参数说明:
● in_features:每个输入样本的大小
● out_features:每个输出样本的大小n.Linetorch.nn.Linear()
3.torch.nn.MaxPool2d()
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
关键参数说明:
● kernel_size:最大的窗口大小
● stride:窗口的步幅,默认值为kernel_size
● padding:填充值,默认为0
● dilation:控制窗口中元素步幅的参数
注意一下:在卷积层和全连接层之间,可以用之前的torch.flatten(),也可以使用下面的x.view()或torch.nn.Flatten()。
torch.nn.Flatten()与TensorFlow中的Flatten()层类似,前两者则仅仅是一种数据集拉伸操作(将二维数据拉伸为一维)。
torch.flatten()方法不会改变x本身,而是返回一个新的张量。
x.view()方法则是直接在原有数据上进行操作。
网络结构图:
上面的网络数据shape变化过程为:
3, 224, 224(输入数据)-> 12, 220, 220(经过卷积层1)
-> 12, 216, 216(经过卷积层2)-> 12, 108, 108(经过池化层1)
-> 24, 104, 104(经过卷积层3)-> 24, 100, 100(经过卷积层4)->
24, 50, 50(经过池化层2)-> 60000 -> num_classes(4)
可根据“深度学习日志:第P2.5周”这篇文章中的描述手动推导这个过程
import torch.nn.functional as F
# 定义一个包含批量归一化层的神经网络类
class Network_bn(nn.Module):
def __init__(self):
"""
类的构造函数,用于初始化网络的各个层
"""
# 调用父类nn.Module的构造函数
super(Network_bn, self).__init__()
"""
nn.Conv2d()函数:
第一个参数(in_channels)是输入的channel数量
第二个参数(out_channels)是输出的channel数量
第三个参数(kernel_size)是卷积核大小
第四个参数(stride)是步长,默认为1
第五个参数(padding)是填充大小,默认为0
"""
# 第一个卷积层,输入通道数为3,输出通道数为12,卷积核大小为5x5
self.conv1 = nn.Conv2d(in_channels=3, out_channels=12, kernel_size=5, stride=1, padding=0)
# 第一个批量归一化层,对12个通道进行归一化
self.bn1 = nn.BatchNorm2d(12)
# 第二个卷积层,输入通道数为12,输出通道数为12,卷积核大小为5x5
self.conv2 = nn.Conv2d(in_channels=12, out_channels=12, kernel_size=5, stride=1, padding=0)
# 第二个批量归一化层,对12个通道进行归一化
self.bn2 = nn.BatchNorm2d(12)
# 第一个最大池化层,池化核大小为2x2,步长为2
self.pool1 = nn.MaxPool2d(2, 2)
# 第四个卷积层,输入通道数为12,输出通道数为24,卷积核大小为5x5
self.conv4 = nn.Conv2d(in_channels=12, out_channels=24, kernel_size=5, stride=1, padding=0)
# 第四个批量归一化层,对24个通道进行归一化
self.bn4 = nn.BatchNorm2d(24)
# 第五个卷积层,输入通道数为24,输出通道数为24,卷积核大小为5x5
self.conv5 = nn.Conv2d(in_channels=24, out_channels=24, kernel_size=5, stride=1, padding=0)
# 第五个批量归一化层,对24个通道进行归一化
self.bn5 = nn.BatchNorm2d(24)
# 第二个最大池化层,池化核大小为2x2,步长为2
self.pool2 = nn.MaxPool2d(2, 2)
# 第一个全连接层,输入特征数为24*50*50,输出特征数为类别数量
self.fc1 = nn.Linear(24*50*50, len(classeNames))
def forward(self, x):
"""
前向传播函数,定义了数据在网络中的流动过程
:param x: 输入数据
:return: 网络的输出
"""
# 通过第一个卷积层,然后进行批量归一化,最后使用ReLU激活函数
x = F.relu(self.bn1(self.conv1(x)))
# 通过第二个卷积层,然后进行批量归一化,最后使用ReLU激活函数
x = F.relu(self.bn2(self.conv2(x)))
# 通过第一个最大池化层进行下采样
x = self.pool1(x)
# 通过第四个卷积层,然后进行批量归一化,最后使用ReLU激活函数
x = F.relu(self.bn4(self.conv4(x)))
# 通过第五个卷积层,然后进行批量归一化,最后使用ReLU激活函数
x = F.relu(self.bn5(self.conv5(x)))
# 通过第二个最大池化层进行下采样
x = self.pool2(x)
# 将多维的特征图展平为一维向量
x = x.view(-1, 24*50*50)
# 通过第一个全连接层
x = self.fc1(x)
return x
# 选择计算设备,如果有可用的GPU则使用CUDA,否则使用CPU
device = "cuda" if torch.cuda.is_available() else "cpu"
# 打印当前使用的计算设备
print("Using {} device".format(device))
# 实例化网络模型,并将其移动到指定的计算设备上
model = Network_bn().to(device)
# 打印模型的结构信息
model
三、训练模型
1.设置超参数
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-4 # 学习率
opt = torch.optim.SGD(model.parameters(),lr=learn_rate)
2.编写训练函数
optimizer.zero_grad()
此函数遍历模型的所有参数,用内置方法截断反向传播的梯度流,再把每个参数的梯度值设为0,上一次梯度记录被清空
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()之前。
optimizer.step()——用梯度下降更新参数值
step()函数的作用是执行一次优化步骤,通过梯度下降法来更新参数的值。因为梯度下降是基于梯度的,所以在执行optimizer.step()函数前应先执行loss.backward()函数来计算梯度。
注意:optimizer只负责通过梯度下降进行优化,而不负责产生梯度,梯度是tensor.backward()方法产生的。
#训练循环
def train(dataloader,model,loss_fn,optimizer):
size=len(dataloader.dataset)#训练集大小为一共60000张图片
num_batches=len(dataloader)#每批次(batches)数目=60000/32=1875
train_loss=0
train_acc=0
for X,y in dataloader:
X,y=X.to(device),y.to(device)
#计算预测误差
pred=model(X)#pred为预测输出
loss=loss_fn(pred,y)#计算预测输出和实际输出之间的差距
#反向传播更新参数
optimizer.zero_grad()#梯度清零
loss.backward()#反向传播
optimizer.step()#更新参数
#记录acc和loss
train_acc+=(pred.argmax(1)==y).type(torch.float).sum().item()
train_loss+=loss.item()#累加当前批次损失,.item()把结果转换为标量值
#1:pred.argmax(1)返回pred在行上最大值的所在索引
#2:pred.argmax(1)==y是一个布尔值,表示样本预测是否正确,true为对,false为错
#3:.type(torch.float)将布尔值转换位浮点型,true为1,false为0
#4:.sum()将所有样本的预测结果求和,得到正确预测的样本数
#5:.item()将结果转换为标量值
train_acc /= size
train_loss /= num_batches
return train_acc, train_loss
3.编写测试函数
测试函数和训练函数大致相同,但是由于不进行梯度下降对网络权重进行更新,所以不需要传入优化器
#此函数用于在测试集上评估模型性能
def test(dataloader,model,loss_fn):#三个输入参数:dataloader加载数据的迭代器,model模型,loss_fn损失函数
size=len(dataloader.dataset)#计算样本数
num_batches=len(dataloader)#计算测试集批次数目
test_loss,test_acc=0,0#初始化损失和正确率
with torch.no_grad():#上下文管理器,在其作用域内禁用梯度计算。测试时无需梯度计算,这样可以节省内存提高速度
for imgs,target in dataloader:#遍历dataloader数据中每个批次的图像img和标签target
imgs,target=imgs.to(device),target.to(device)#把数据移动到指定设备上,和模型同一设备
#计算loss和acc
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上
#计算平均损失和平均正确率
test_acc/=size
test_loss/= num_batches
#平均损失和平均正确率即为输出结果
return test_acc,test_loss
4. 正式训练
model.train()
model.train()的作用是启用 Batch Normalization 和 Dropout。
如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train()。model.train()是保证BN层能够用到每一批数据的均值和方差。对于Dropout,model.train()是随机取一部分网络连接来训练更新参数。
model.eval()
model.eval()的作用是不启用 Batch Normalization 和 Dropout。
如果模型中有BN层(Batch Normalization)和Dropout,在测试时添加model.eval()。model.eval()是保证BN层能够用全部训练数据的均值和方差,即测试过程中要保证BN层的均值和方差不变。对于Dropout,model.eval()是利用到了所有网络连接,即不进行随机舍弃神经元。
训练完train样本后,生成的模型model要用来测试样本。在model(test)之前,需要加上model.eval(),否则的话,有输入数据,即使不训练,它也会改变权值。这是model中含有BN层和Dropout所带来的的性质。
epochs = 20#总周期数为20
#预定义训练集和测试集的loss和acc
train_loss = []
train_acc = []
test_loss = []
test_acc = []
for epoch in range(epochs):#对于每个epoch
model.train()#作用:把模型设置为“训练模式”,启动Batch Normalization和Dropout
#对于BN层,它保证其能够用到每一批数据的均值和方差;对于Dropout层,他随机取一部分网络连接来训练更新参数
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
model.eval()#作用:把模型设置为“评估模式”,停止Batch Normalization和Dropout
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
#记录每个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)
#template定义一个格式化字符串,来打印每一次训练和测试的结果
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))#把这里面的逐个填入template的公式里
print('Done')#训练完成的提示
四、结果可视化
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#分辨率设置
from datetime import datetime
current_time=datetime.now()#用datatime库的.now()来获取当前时间
epoch_range=range(epochs)#定义训练的周期范围,生成从0到(epochs-1)的整数序列
plt.figure(figsize=(12,3))#创建图形,尺寸为12*3 inch
#把图形分为1行2列两个字图
#1:对于第一个子图
plt.subplot(1,2,1)
#绘制测试集和训练集的准确率曲线
plt.plot(epoch_range,train_acc,label='Training Accuracy')
plt.plot(epoch_range,test_acc,label='Test Accuracy')
plt.legend(loc='lower right')#图例放在右下角
plt.title('Training and Validation Accuracy')#图形命名
plt.xlabel(current_time)#X轴命名
#2:对于第二个子图
plt.subplot(1,2,2)
#绘制测试集和训练集的损失函数曲线
plt.plot(epoch_range,train_loss,label='Training Loss')
plt.plot(epoch_range,test_loss,label='Test Loss')
plt.legend(loc='upper right')#图例放在右上角
plt.title('Training and Validation Loss')#图形命名
plt.show()#显示图形
#打卡要带上时间戳否则代码截图无效
示例输出: