深度学习日志——第P3周:Pytorch实现天气识别

  • 🍨 本文为🔗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 构造函数接受多个参数,下面是一些常用的参数及其解释:

  1. dataset(必需参数):这是你的数据集对象,通常是 torch.utils.data.Dataset 的子类,它包含了你的数据样本。
  2. batch_size(可选参数):指定每个小批次中包含的样本数。默认值为 1。
  3. shuffle(可选参数):如果设置为 True,则在每个 epoch 开始时对数据进行洗牌,以随机打乱样本的顺序。这对于训练数据的随机性很重要,以避免模型学习到数据的顺序性。默认值为 False。
  4. num_workers(可选参数):用于数据加载的子进程数量。通常,将其设置为大于 0 的值可以加快数据加载速度,特别是当数据集很大时。默认值为 0,表示在主进程中加载数据。
  5. pin_memory(可选参数):如果设置为 True,则数据加载到 GPU 时会将数据存储在 CUDA 的锁页内存中,这可以加速数据传输到 GPU。默认值为 False。
  6. drop_last(可选参数):如果设置为 True,则在最后一个小批次可能包含样本数小于 batch_size 时,丢弃该小批次。这在某些情况下很有用,以确保所有小批次具有相同的大小。默认值为 False。
  7. timeout(可选参数):如果设置为正整数,它定义了每个子进程在等待数据加载器传递数据时的超时时间(以秒为单位)。这可以用于避免子进程卡住的情况。默认值为 0,表示没有超时限制。
  8. 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()#显示图形

#打卡要带上时间戳否则代码截图无效

示例输出:
在这里插入图片描述

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值