- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
基础阶段目标目标
- 熟悉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设备
- 2、导入数据
- 显示图像
- API介绍
- API介绍
- 加载所有图像
- datasets.ImageFolder()函数解释
- transforms.Compose函数解释
- 3、划分数据集与加载数据
- 划分数据
- torch.utils.data.random_split()方法总结
- 动态加载数据
- torch.utils.data.DataLoader()简介
- 2、构建CNN模型
- torch.nn.Linear()解释
- 模型的构建
- 将模型转到GPU
- 3、模型训练
- 设置超参数
- 训练函数
- 编写测试函数
- 4、正式训练
- model.train()
- model.eval()
- 模型训练
- 5、结果显示
- 6、总结
- 1、学习总结
- 2、API总结
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()简介
作用:动态加载和管理数据,并且支持并行化
- 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(可选参数):一个可选的函数,用于初始化每个子进程的状态。这对于设置每个子进程的随机种子或其他初始化操作很有用。
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集的