深度学习基础案例3--构建CNN卷积神经网络实现对不同天气的识别(测试集准确率百分之90+)

基础阶段目标目标

  • 熟悉CNN、RNN神经网络,了解yolo、transfomer等模型
  • 熟练使用Pytorch框架,了解tensorflow

本次目标

  • 了解CNN神经网络构建思路
  • 熟悉pytroch框架

本次案例:

  • 测试集准确率达到了百分之90+,整体预测效果不错
  • 不足: 没有测试集准确率没有达到百分之95,对神经网络的优化和修改能力欠缺

环境:

  • python:3.8.19
  • gpu:cuda
  • torch:2.4.0
  • torchvision:0.19.0
  • 编译器:vscode、jupyter

1、前期准备

1、导入库和检查GPU设备

import torch 
import torch.nn as nn 
import torchvision 
import torchvision.transforms as transforms  # 这个库种包含了大量的图像处理函数,可以对图像数据进行缩放、裁剪、颜色增强、转置、归一化等
from torchvision import transforms, datasets  # 可以省略 .torchvision 前缀,datasets里卖弄包含了大量的数据

import os,PIL,pathlib,random 
# os:提供了与操作系统交互的接口,包含了一系列处理路径、文件、目录、环境变量的功能
# PIL:处理图像,包括打开、编辑、保存多种格式的图片
# pathlib:处理文件,包括创建、检查、删除
# random:随机数

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(torch.__version__)
print(torchvision.__version__)
print(device)

结果:

2.4.0
0.19.0
cuda

2、导入数据

显示图像

文件目录:data(有四类天气图片的目录)

data_dir = './data/'
data_dir = pathlib.Path(data_dir)  # 转化为 pathlib 对象

data_paths =  list(data_dir.glob('*'))  # 查找 该目录 下的所有 文件路径,如:WindowsPath('data/cloudy')
classNames = [str(name).split("\\")[1] for name in data_paths]    # .split 返回的是一个数组
classNames  # 输出该目录下四类天气文件名字

结果:

['cloudy', 'rain', 'shine', 'sunrise']
API介绍
  • pathlib.Path(): 将文件转化成 pathlib 对象
  • data_dir.glob(‘*’):显示 data_dir 目录下所有的文件
  • [str(name).split(“\”)[1] for name in data_paths] 列表推导式
# 显示一部分图片
import matplotlib.pyplot as plt 
from PIL import Image   # PIL 下的图像处理中的 Image库

# 指定图片路径
image_dir = './data/cloudy/'

# 遍历所有文件
image_files = [f for f in os.listdir(image_dir) if f.endswith((".jpg", ".png", ".jpeg"))]

# 创建子图
fig, axes = plt.subplots(3, 8, figsize=(16, 6))   #fig 是子图对象 ,axes 相当于一个子图框, 

# 加载数据和现实图像
for ax, img_file in zip(axes.flat, image_files):
    img_path = os.path.join(image_dir, img_file)  # 拼接
    img = Image.open(img_path)
    ax.imshow(img)     # ax 一个子图框
    ax.axis('off') 

# 显示
plt.show()


在这里插入图片描述

API介绍
  • os.listdir():遍历该目录的所有文件
  • zip():结合成对象,例如:
# 输入
names = ['Alice', 'Bob', 'Charlie']
ages = [24, 30, 35]

# 结果
[('Alice', 24), ('Bob', 30), ('Charlie', 35)]
  • os.path.join():拼接路径
  • Image.open(): 打开图片文件

加载所有图像

# 加载所有的图片
total_dir = './data/'

# 使用 trainsfroms.Compose 函数
train_transforms = transforms.Compose([
    transforms.Resize([224, 224]),    # 统一图片大小
    transforms.ToTensor(),            # 将PIL、Image、numpy转化成 Tensor
    transforms.Normalize(              # 数据标准化处理---> 转化为 标准状态分布,使模型更容易收敛
        mean=[0.485, 0.456, 0.406],  # rgb,均值
        std=[0.229, 0.224, 0.225]    # rgb,标准差,这两个从数据集中随机抽样得到的
    )
])

total_data = datasets.ImageFolder(total_dir, transform=train_transforms)
total_data

结果:

Dataset ImageFolder
    Number of datapoints: 1125
    Root location: ./data/
    StandardTransform
Transform: Compose(
               Resize(size=[224, 224], interpolation=bilinear, max_size=None, antialias=True)
               ToTensor()
               Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
           )
datasets.ImageFolder()函数解释

torchvision.datasets.ImageFolder 是 PyTorch 中一个非常有用的类,用于从磁盘上的文件夹结构中加载图像数据集。它假设你的数据集是按照类别组织的,每个类别有一个独立的文件夹,文件夹的名称通常代表该类别的标签。

参数:

  • root (string): 指定包含类别文件夹的根目录。
  • transform (callable, optional): 一个可选的函数/变换,它将被应用于每个加载的样本,通常用于数据预处理。
  • target_transform (callable, optional): 一个可选的函数/变换,它将被应用于目标/标签,例如将标签转换为 one-hot 编码。
  • loader (callable, optional): 一个可选的函数,用于从给定的文件路径加载一个样本。默认情况下,它使用 default_loader,这是一个简单的函数,使用 PIL 来加载图像。
transforms.Compose函数解释

transforms:里面含有常见的图片变换操作,如旋转、剪裁等。
详细请看:https://blog.csdn.net/qq_38251616/article/details/124878863

3、划分数据集与加载数据

划分数据

# 训练集 0.8,测试集 0.2
train_size = int(len(total_data) * 0.8)    # 注意要转换成 int 类型,默认是 float类型
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)
# 结果:
<torch.utils.data.dataset.Subset object at 0x000001DC5B727D00>
<torch.utils.data.dataset.Subset object at 0x000001DC572722B0>
torch.utils.data.random_split()方法总结

这个函数作用是随机划分数据,将总体大小划分为[train_size, test_size]的大小

动态加载数据

batch_size = 32   # 自定义:每一批加载 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)

# 查看数据大小
imgs, labels = next(iter(test_dl))
print("Shape of imgs [N, C, H, W]: ", imgs.shape)
print("Shape of labels: ", labels.shape, labels.dtype)
labels   # labels 是一维张量,大小同N,代表分类的信息,如下:0为一类,3为1类,2为一类,在datasets.ImageFolder加载数据中分好类了
# 结果:
Shape of imgs [N, C, H, W]:  torch.Size([32, 3, 224, 224])
Shape of labels:  torch.Size([32]) torch.int64
tensor([2, 3, 1, 1, 3, 0, 2, 0, 3, 2, 0, 1, 3, 3, 3, 3, 2, 3, 3, 0, 2, 0, 0, 2,
        2, 1, 3, 3, 2, 0, 3, 1])
torch.utils.data.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(可选参数):一个可选的函数,用于初始化每个子进程的状态。这对于设置每个子进程的随机种子或其他初始化操作很有用。

2、构建CNN模型

torch.nn.Linear()解释

全连接层作用,进行线性和非线性变换,起到降维作用,输出想要的维度,即类别
函数原型:
torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)

关键参数说明:

  • in_features:每个输入样本的大小
  • out_features:每个输出样本的大小

注意: 卷积层和池化层API、原理讲解,参考:CIRFAR10才是图片识别

模型的构建

本文构建的模型中,有四层卷积层,两层池化层,最后一层全连接层,模型原理和计算流程如下:
模型原理图:

在这里插入图片描述

计算:
输入数据:[3, 224, 224]–> 卷积层1:[12, 220, 220]–> 卷积层2:[12, 216, 216]–> 池化层1:[12, 108, 108]–> 卷积层3:[24, 104, 104]–> 卷积层4:[24, 100, 100]–> 池化层2:[24, 50, 50]–> 全连接层展开降维: 24 * 50 * 50–> num_classes(4)

import torch.nn.functional as F  # 激活函数

class Network_bn(nn.Module):  # nn.Module 继承父类
    def __init__(self):
        super(Network_bn, self).__init__()
        
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=12, kernel_size=5, stride=1, padding=0)
        self.bn1 = nn.BatchNorm2d(12)  # 对二维特征进行归一化,提高稳定性,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.conv3 = nn.Conv2d(in_channels=12, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn3 = nn.BatchNorm2d(24)
        self.conv4 = nn.Conv2d(in_channels=24, out_channels=24, kernel_size=5, stride=1, padding=0)
        self.bn4 = nn.BatchNorm2d(24)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(24 * 50 * 50, len(classNames))
        
    # 构建神经网络
    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.bn3(self.conv3(x)))
        x = F.relu(self.bn4(self.conv4(x)))
        x = self.pool2(x)
        x = x.view(-1, 24 * 50 * 50)  # 用 torch.flatten 也行
        x = self.fc1(x)
        
        return x

将模型转到GPU

model = Network_bn().to(device)
model

结果(模型的结构和参数):

Network_bn(
  (conv1): Conv2d(3, 12, kernel_size=(5, 5), stride=(1, 1))
  (bn1): BatchNorm2d(12, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (conv2): Conv2d(12, 12, kernel_size=(5, 5), stride=(1, 1))
  (bn2): BatchNorm2d(12, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv3): Conv2d(12, 24, kernel_size=(5, 5), stride=(1, 1))
  (bn3): BatchNorm2d(24, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (conv4): Conv2d(24, 24, kernel_size=(5, 5), stride=(1, 1))
  (bn4): BatchNorm2d(24, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=60000, out_features=4, bias=True)
)

3、模型训练

设置超参数

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

训练函数

# 注意:optimizer只负责通过梯度下降进行优化,而不负责产生梯度,梯度是tensor.backward()方法产生的。
def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)  # 训练集大小 
    num_batches = len(dataloader)   # 批次
    
    train_loss, trian_acc = 0, 0   # 记录损失率
    
    for X, y in dataloader:
        X, y = X.to(device), y.to(device)  # 转入GPU
        
        # 预测
        pred = model(X)
        # 计算误差
        loss = loss_fn(pred, y)
        
        # 反向传播
        optimizer.zero_grad()  # 梯度归 0
        loss.backward()     # 反向传播
        optimizer.step()     # 自动跟新权重
        
        # 记录acc和loss
        trian_acc += (pred.argmax(1) == y).type(torch.float64).sum().item()
        train_loss += loss.item()   # .item() 为转化成标准类型项
    
    # 计算acc和loss
    trian_acc /= size           # 总体准确率
    train_loss /= num_batches   # 得到的是平均损失,每一批次的loss
    
    return trian_acc, train_loss 

pred.argmax(1) == y:pred 是模型对一批次数据的预测输出,通常是一个二维张量,其中每一行代表一个样本,每一列代表该样本属于不同类别的预测得分。.argmax(1) 函数会找到每一行(即每个样本)中最大值的索引,这通常代表模型预测的类别标签。y 是这批数据的实际标签。

编写测试函数

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

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)  # 测试集大小,10000张
    num_batches = len(dataloader)   # 测试集批次, 313, 每一批 32 张
    
    test_acc, test_loss = 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_acc += (target_pred.argmax(1) == target).type(torch.float64).sum().item()
            test_loss += loss.item()
            
    
    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()是利用到了所有网络连接,即不进行随机舍弃神经元。

模型训练

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

for epoch in range(epochs):
    model.train()   # 有bn层设置
    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')
Epoch: 1, Train_acc:94.6%, Train_loss:0.211, Test_acc:90.2%, Test_loss:0.244
Epoch: 2, Train_acc:95.9%, Train_loss:0.182, Test_acc:88.9%, Test_loss:0.261
Epoch: 3, Train_acc:95.0%, Train_loss:0.210, Test_acc:90.2%, Test_loss:0.257
Epoch: 4, Train_acc:95.6%, Train_loss:0.206, Test_acc:84.9%, Test_loss:0.335
Epoch: 5, Train_acc:95.8%, Train_loss:0.167, Test_acc:90.7%, Test_loss:0.241
Epoch: 6, Train_acc:96.3%, Train_loss:0.190, Test_acc:88.4%, Test_loss:0.306
Epoch: 7, Train_acc:96.8%, Train_loss:0.173, Test_acc:88.0%, Test_loss:0.748
Epoch: 8, Train_acc:96.2%, Train_loss:0.155, Test_acc:91.1%, Test_loss:0.327
Epoch: 9, Train_acc:96.7%, Train_loss:0.140, Test_acc:90.2%, Test_loss:0.223
Epoch:10, Train_acc:96.9%, Train_loss:0.140, Test_acc:91.6%, Test_loss:0.243
Epoch:11, Train_acc:97.1%, Train_loss:0.140, Test_acc:90.2%, Test_loss:0.299
Epoch:12, Train_acc:97.3%, Train_loss:0.160, Test_acc:90.2%, Test_loss:0.251
Epoch:13, Train_acc:97.2%, Train_loss:0.165, Test_acc:91.1%, Test_loss:0.247
Epoch:14, Train_acc:97.6%, Train_loss:0.150, Test_acc:88.0%, Test_loss:0.301
Epoch:15, Train_acc:97.1%, Train_loss:0.170, Test_acc:87.1%, Test_loss:0.268
Epoch:16, Train_acc:97.3%, Train_loss:0.132, Test_acc:91.1%, Test_loss:0.265
Epoch:17, Train_acc:97.8%, Train_loss:0.136, Test_acc:91.1%, Test_loss:0.253
Epoch:18, Train_acc:97.3%, Train_loss:0.117, Test_acc:89.8%, Test_loss:0.220
Epoch:19, Train_acc:97.7%, Train_loss:0.142, Test_acc:91.6%, Test_loss:0.421
Epoch:20, Train_acc:96.9%, Train_loss:0.159, Test_acc:90.7%, Test_loss:0.261
Epoch:21, Train_acc:97.9%, Train_loss:0.102, Test_acc:90.2%, Test_loss:0.229
Epoch:22, Train_acc:98.4%, Train_loss:0.093, Test_acc:90.7%, Test_loss:0.210
Epoch:23, Train_acc:98.3%, Train_loss:0.110, Test_acc:89.3%, Test_loss:0.244
Epoch:24, Train_acc:98.4%, Train_loss:0.144, Test_acc:91.1%, Test_loss:0.245
Epoch:25, Train_acc:97.4%, Train_loss:0.114, Test_acc:91.1%, Test_loss:0.227
Epoch:26, Train_acc:97.9%, Train_loss:0.161, Test_acc:88.4%, Test_loss:0.307
Epoch:27, Train_acc:97.4%, Train_loss:0.108, Test_acc:91.1%, Test_loss:0.238
Epoch:28, Train_acc:98.6%, Train_loss:0.092, Test_acc:91.6%, Test_loss:0.268
Epoch:29, Train_acc:99.2%, Train_loss:0.080, Test_acc:91.1%, Test_loss:0.206
Epoch:30, Train_acc:99.0%, Train_loss:0.086, Test_acc:91.6%, Test_loss:0.287
Done

5、结果显示

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        #分辨率

x = range(epochs)
# 创建画板
plt.figure(figsize=(12, 3))
# 子图一
plt.subplot(1, 2, 1)
plt.plot(x, train_acc, label='Train Accurary')
plt.plot(x, test_acc, label='Test Accurary')
plt.legend(loc='lower right')
plt.title("Train and test Accurary")
# 子图二
plt.subplot(1, 2, 2)
plt.plot(x, train_loss, label='Train loss')
plt.plot(x, test_loss, label='Test loss')
plt.legend(loc='upper right')
plt.title("Train and test Loss")

plt.show()


在这里插入图片描述

Accurary:

  • 训练集准确率逐步提升均在百分之90以上
  • 测试集也均在85% - 90%
    Loss:
  • 训练集均在0.2一下
  • 测试集后面稳定在0.3一下

总体效果不错

6、总结

1、学习总结

  • 进一步熟悉了pytorch的使用
  • 进一步深入了解了cnn神经网络:卷积层、池化层、全连接层的作用,以及自动微分、清理梯度、自动跟新权重的步骤
  • 进一步了解了准确率(Accurary)和损失率(Loss)的意义,以及API是如何计算的,计算场景是怎么样子的
  • 了解了Python如何处理图像文件方法
  • 不足:无法自己修改神经网络结构,不怎么如何做优化

2、API总结

  • pathlib.Path(): 将文件转化成 pathlib 对象

  • data_dir.glob('*'):显示 data_dir 目录下所有的文件

  • [str(name).split("\\")[1] for name in data_paths] : 列表推导式

  • os.listdir():遍历该目录的所有文件

  • zip():结合成对象

  • datasets.ImageFolder():pytorch在文件中加载图片

  • transforms.Compose:torchvision中处理图片的API,可以统一图片大小,旋转、分割等作用

  • torch.utils.data.random_split():按照比例随机划分数据

  • torch.nn.Linear():全连接层,本质定义线性和非线性函数,可以将多维展开然后降维输出特征

  • model.train(): model.train()是保证BN层能够用到每一批数据的均值和方差。

  • model.eval(): model.eval()是不启用BN层(不对数据进行进一步归一化,稳定性处理),全过程用到的均值和方差是全部test集的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值