第P3周:Pytorch实现天气识别

第P3周:Pytorch实现天气识别

📌第P3周:Pytorch实现天气识别

  • 难度:新手入门⭐

  • 语言:Python3、Pytorch

🍺要求:

  1. 本地读取并加载数据。

  2. 测试集accuracy到达93%

🍻拔高:

  1. 测试集accuracy到达95%
  2. 调用模型识别一张本地图片

🏡 我的环境:

  • 语言环境:Python3.9

  • 编译器:Pycharm

  • 深度学习环境:Pytorch

文章目录

一、前期准备

1、设置GPU

import torch
import torch.nn as nn
import torchvision.transforms as transforms # 导入 torchvision 中的数据变换模块,主要是用于图形变换,例如裁剪、旋转等
import torchvision # 是pytorch的一个图形库,它服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。
from torchvision import transforms, datasets # datasets:一些加载数据的函数及常用的数据集接口,transform:常用的图片变换,例如裁剪、旋转等;

import os,PIL,pathlib,random

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

print(device)

代码输出
在这里插入图片描述

2、加载数据

  • **第一步:**使用pathlib.Path()函数将字符串类型的文件夹路径转换为pathlib.Path对象。
  • **第二步:**使用glob()方法获取data_dir路径下的所有文件路径,并以列表形式存储在data_paths中。
  • **第三步:**通过split()函数对data_paths中的每个文件路径执行分割操作,获得各个文件所属的类别名称,并存储在classeNames
  • **第四步:**打印classeNames列表,显示每个文件所属的类别名称。
# 导入数据
data_dir = "./weather_photos"
# 将数据目录路径转换为Path对象,将字符串对象转换为"Path"对象,方便后续的路径操作
data_dir = pathlib.Path(data_dir)

# 返回匹配通配符’*‘的所有路径对象,这些对象表示data_dir目录下的所有子目录和文件
# list将这些对象转换为一个列表’data_paths‘
data_paths = list(data_dir.glob('*'))
classNames =[str(path).split("\\")[1] for path in data_paths]
print(classNames)
在文件路径中:
  1. ../ (上一级目录)
    • 表示当前目录的上一级目录。
    • 例如,如果当前目录是 P3Pytroch实现天气识别/weather_photos,使用 ../ 会指向 P3Pytroch实现天气识别 目录。
    • 可以连续使用多个 ../ 来向上多级目录,如 ../../ 表示上上一级目录。
  2. ./ (当前目录)
    • 表示当前目录。
    • 这是相对路径的一种表示方法,通常用于明确说明路径相对于当前目录,而不是使用绝对路径。
    • 例如,./file.txt 表示当前目录下的 file.txt 文件。
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from PIL import Image

# 指定图像文件夹路径
image_folder = 'P3Pytroch实现天气识别/weather_photos/cloudy'

# 获取文件夹中的所有图像文件
image_files = [f for f in os.listdir(image_folder) if f.endswith((".jpg", ".png", ".jpeg"))]

# 创建Matplotlib图像
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()

代码输出:
在这里插入图片描述

列表推导式结构:

[expression for item in iterable if condition]
  • expression: 表示列表中每个元素的计算方式或表达形式。在这段代码中是 f
  • for item in iterable: 迭代器,从中逐一取出元素。在这段代码中是 for f in os.listdir(image_folder)
  • if condition: 条件筛选,仅在条件为真的情况下才会保留元素。在这段代码中是 if f.endswith((".jpg", ".png", ".jpeg"))

个人理解:

先进行for item in iterable(从中取出每个元素)-> if condition(条件筛选,满足条件保留)-> expression最终保留在新列表当中的元素,最后把这些元素添加到需要赋值的列表当中

total_datadir = './weather_photos'

# 关于transforms.Compose的更多介绍可以参考:https://blog.csdn.net/qq_38251616/article/details/124878863
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)
print(total_data)

😄代码输出:
在这里插入图片描述

以下参考[K同学啊](torchvision.transforms.Compose()详解【Pytorch入门手册】_from torchvision.transforms import compose-CSDN博客)

torchvision是pytorch的一个图形库,它服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。torchvision.transforms主要是用于常见的一些图形变换。以下是torchvision的构成:

1.torchvision.datasets: 一些加载数据的函数及常用的数据集接口;
2.torchvision.models: 包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
3.torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;
4.torchvision.utils: 其他的一些有用的方法

from torchvision.transforms import transforms

train_transforms = transforms.Compose([
    transforms.Resize([224, 224]),                  # 将输入图片resize成统一尺寸
    transforms.RandomRotation(degrees=(-10, 10)),   # 随机旋转,-10到10度之间随机选
    transforms.RandomHorizontalFlip(p=0.5),         # 随机水平翻转 选择一个概率概率
    transforms.RandomVerticalFlip(p=0.5),           # 随机垂直翻转
    transforms.RandomPerspective(distortion_scale=0.6, p=1.0),    # 随机视角
    transforms.GaussianBlur(kernel_size=(5, 9), sigma=(0.1, 5)),  # 随机选择的高斯模糊模糊图像
    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] 从数据集中随机抽样计算得到的。
])

3、划分数据集

使用torch.utils.data.random_split()方法进行数据集划分。该方法将总体数据total_data按照指定的大小比例([train_size, test_size])随机划分为训练集和测试集,并将划分结果分别赋值给train_dataset和test_dataset两个变量。

train_size = int(0.8 * len(total_data))
test_size  = len(total_data) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])
print(train_dataset)
print(test_dataset)

😄代码输出:

在这里插入图片描述

print(train_size)
print(test_size)

😄代码输出:

在这里插入图片描述

使用PyTorch库创建数据加载器,加载并显示测试数据集中的一个批次代码如下:

batch_size = 32

def main():
    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)
    for X, y in test_dl:
        print("Shape of X [N, C, H, W]: ", X.shape)
        print("Shape of y: ", y.shape, y.dtype)
        break
if __name__ == "__main__":
    main()

😄代码输出:
在这里插入图片描述

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网络

大家注意一下在卷积层和全连接层之间,我们可以使用之前是torch.flatten()也可以使用我下面的x.view()亦或是torch.nn.Flatten()torch.nn.Flatten()与TensorFlow中的Flatten()层类似,前两者则仅仅是一种数据集拉伸操作(将二维数据拉伸为一维),torch.flatten()方法不会改变x本身,而是返回一个新的张量。x.view()方法则是直接在原有数据上进行操作。

image.png

上面的网络数据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)

import torch.nn.functional as F 

class Network_bn(nn.Module):
    def __init__(self):
        super(Network_bn, self).__init__()
        """
        nn.Conv2d()函数:
        第一个参数(in_channels)是输入的channel数量
        第二个参数(out_channels)是输出的channel数量
        第三个参数(kernel_size)是卷积核大小
        第四个参数(stride)是步长,默认为1
        第五个参数(padding)是填充大小,默认为0
        """
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=12, kernel_size=5, stride=1, padding=0)
        self.bn1 = nn.BatchNorm2d(12)
        self.conv2 = nn.Conv2d(in_channels=12, out_channels=12, kernel_size=5, stride=1, padding=0)
        self.bn2 = nn.BatchNorm2d(12)
        self.pool1 = nn.MaxPool2d(2,2)
        self.conv4 = nn.Conv2d(in_channels=12, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn4 = nn.BatchNorm2d(24)
        self.conv5 = nn.Conv2d(in_channels=24, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn5 = nn.BatchNorm2d(24)
        self.pool2 = nn.MaxPool2d(2,2)
        self.fc1 = nn.Linear(24*50*50, len(classeNames))

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))      
        x = F.relu(self.bn2(self.conv2(x)))     
        x = self.pool1(x)                        
        x = F.relu(self.bn4(self.conv4(x)))     
        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

device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using {} device".format(device))

model = Network_bn().to(device)
print(model)

😄代码输出:

在这里插入图片描述

三、训练模型

1、设置模型超参数:

loss_fn    = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-4 # 学习率
opt        = torch.optim.SGD(model.parameters(),lr=learn_rate)

2、编写训练函数

1. optimizer.zero_grad()

函数会遍历模型的所有参数,通过内置方法截断反向传播的梯度流,再将每个参数的梯度值设为0,即上一次的梯度记录被清空。

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()之前。

3. 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)   # 批次数目,1875(60000/32)

    train_loss, train_acc = 0, 0  # 初始化训练损失和正确率
    
    for X, y in dataloader:  # 获取图片及其标签
        X, y = X.to(device), y.to(device)
        
        # 计算预测误差
        pred = model(X)          # 网络输出
        loss = loss_fn(pred, y)  # 计算网络输出和真实值之间的差距,targets为真实值,计算二者差值即为损失
        
        # 反向传播
        optimizer.zero_grad()  # grad属性归零
        loss.backward()        # 反向传播
        optimizer.step()       # 每一步自动更新
        
        # 记录acc与loss
        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

3、编写测试函数

测试函数和训练函数大致相同,但是由于不进行梯度下降对网络权重进行更新,所以不需要传入优化器

def test (dataloader, model, loss_fn):
    size        = len(dataloader.dataset)  # 测试集的大小,一共10000张图片
    num_batches = len(dataloader)          # 批次数目,313(10000/32=312.5,向上取整)
    test_loss, test_acc = 0, 0
    
    # 当不进行训练时,停止梯度更新,节省计算内存消耗
    with torch.no_grad():
        for imgs, target in dataloader:
            imgs, target = imgs.to(device), target.to(device)
            
            # 计算loss
            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

4、正式训练

1. model.train()

model.train()的作用是启用 Batch Normalization 和 Dropout。

如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train()model.train()是保证BN层能够用到每一批数据的均值和方差。对于Dropoutmodel.train()是随机取一部分网络连接来训练更新参数。

2. model.eval()

model.eval()的作用是不启用 Batch Normalization 和 Dropout。

如果模型中有BN层(Batch Normalization)和Dropout,在测试时添加model.eval()model.eval()是保证BN层能够用全部训练数据的均值和方差,即测试过程中要保证BN层的均值和方差不变。对于Dropoutmodel.eval()是利用到了所有网络连接,即不进行随机舍弃神经元。

训练完train样本后,生成的模型model要用来测试样本。在model(test)之前,需要加上model.eval(),否则的话,有输入数据,即使不训练,它也会改变权值。这是model中含有BN层和Dropout所带来的的性质。

epochs     = 20
train_loss = []
train_acc  = []
test_loss  = []
test_acc   = []

for epoch in range(epochs):
    model.train()
    epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
    
    model.eval()
    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}')
    print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss, epoch_test_acc*100, epoch_test_loss))
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        #分辨率

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()

😄代码输出:

在这里插入图片描述

遇到问题解决以及总结:

1、在上述打印 x 输入数据张量,y 是对应的标签张量中遇到的问题

遇到的错误 RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase 通常发生在使用多进程数据加载器时,尤其是在 Windows 系统上。这是由于 Windows 上的多进程启动方式与 Unix 系统(如 Linux 和 macOS)不同。

在这里插入图片描述

解决:为了避免这个错误,你需要确保你的脚本在调用多进程代码时遵循正确的 Python 习惯。具体来说,你需要将多进程相关的代码放在 if __name__ == "__main__": 结构中。这样可以确保在脚本被执行时,多进程相关的代码不会在模块导入时被执行。

确保你的数据加载和训练代码位于 if __name__ == "__main__": 结构中。更改之后的代码如下

"""
使用 spawn 或 forkserver 启动方法(在一些情况下更稳定),通过在脚本的开头添加如下代码:
import multiprocessing
multiprocessing.set_start_method('spawn', force=True)
这段代码确保使用 spawn 方法来启动子进程,这是 Windows 上推荐的方式。
"""

import multiprocessing
multiprocessing.set_start_method('spawn', force=True)

batch_size = 32

def main():
    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)
    for X, y in test_dl:
        print("Shape of X [N, C, H, W]: ", X.shape)
        print("Shape of y: ", y.shape, y.dtype)
        break
if __name__ == "__main__":
    main()

针对于上面那个问题总结,因为博客是边做便携,在过程中深究了一下上述问题,刚刚方法固然可以解决但是并没有理解问题根本

以下部分都是个人理解部分:

问题截图:
在这里插入图片描述

错误原因:

这个错误是由 torch.utils.data.DataLoader 使用多进程进行数据加载时引起的。

具体错误出现在下面代码

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)

num_worker=1表示使用一个子进程用来加载数据。

解决方法:

1、如果将其设置为num_worker=0,即不使用多进程,则不会出现问题,但是这个会影响数据加载的效率。

2、为了在 Windows 系统上正确使用多进程,需要确保所有多进程相关代码都在 if __name__ == "__main__": 块中运行

​ 主程序入口缺少 if __name__ == "__main__": 块导致了这个问题

​ ① 确保将数据加载和模型训练的代码放在 if __name__ == "__main__": 块中。

​ ② 在 Windows 平台上使用 freeze_support()

😄代码得到正确输出(解决方法我选择了第二种)

在这里插入图片描述

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值