目录
目录
前言
- 🍨本文为[🔗365天深度学习训练营](https://mp.weixin.qq.com/s/rbOOmire8OocQ90QM78DRA) 中的学习记录博客
- 🍖 原作者:K同学啊
说在前面:
- 本周学习目标:基本目标——本地读取并加载数据、测试集上的Accuracy达到93;拔高目标——通过调整使得Accuracy达到95%;调用模型识别一张本地的图片
- 学习重点:通过本地数据加载,调整模型参数以提高在测试集上的Accuracy
- 我的环境:Python3.8、Pycharm2020、torch1.12.1+cu113(ps:由于电脑无英伟达显卡,所以这里实际上还是用的cpu在运行)
一、前期准备
1.1 设置GPU
由于电脑硬件原因,这里仅支持cpu运行(首先导入需要的包,然后再查看系统支持的运行设备GPU or 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
import matplotlib.pyplot as plt
from PIL import Image
#Python图像库PIL(Python Image Library)是python的第三方图像处理库
import torch.nn.functional as F
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
输出显示为:cpu
1.2 导入数据
1.首先下载含天气的图片数据集,在代码文件相同的目录下建立一个名为data的目录,将下载的天气图片文件复制到data目录下
- 数据集介绍:该数据集是包括了四类天气的图片,分为为多云、雨、晴、日落四类天气,分别存储在四个子文件夹下,每类天气包含了300张图片,所以一共就是1200张图片
- 导入数据集的步骤如下:
1)使用函数将字符串类型的文件夹路径转换为pathlib.Path对象
2)使用glob方法获取data_dir路径下的所有文件路径,并以列表的形式存储在data_paths中
3)利用split()函数对data_paths中的每个文件路径执行分割操作,获取各个文件所属的类别名称并储存在classNames中
示例代码如下:
#1.2 导入数据
data_dir = './data/'
data_dir = pathlib.Path(data_dir) #使用函数将字符串类型的文件夹路径转换为pathlib.Path对象
data_paths = list(data_dir.glob('*')) #使用glob方法获取data_dir路径下的所有文件路径,并以列表的形式存储在data_paths中
classNames = [str(path).split('\\')[1] for path in data_paths]
#利用split()函数对data_paths中的每个文件路径执行分割操作,获取各个文件所属的类别名称并储存在classNames中
# 4类天气,各300张图片
print(classNames)
打印结果为:['cloudy', 'rain', 'shine', 'sunrise']
2.选取24张图片进行图片打印展示
1.image_files = [f for f in os.listdir(image_folder) if f.endswith((".jpg", ".png", ".jpeg"))]:使用列表推导式加载和显示图像用于在Matplotlib中的多个子图中显示从文件夹中加载的图像 1)在Matplotlib中,plt.subplots()函数用于创建一个包含多个子图的图形对象fig和子图对象的数组axes,当指定行数和列数创建子图时,返回的axes是一个二维数组,其中包含了所有的子图对象 2)axes.flat是一个迭代器,它可以让您按照一维的顺序访问axes数组中的所有子图对象 2.for循环:通过zip()函数将axes.flat和image_files两个可迭代对象进行配对,每次循环从中获取一个子图对象ax和一个图像文件名img_file 1)os.path.join()方法将文件夹路径image_folder 和图像文件名img_file拼接成完整的图像文件路径img_path 2)使用Image.open()方法从指定路径打开图像文件,将其加载为一个图像对象img 3)在当前子图ax中显示加载的图像img 4)关闭子图ax的坐标轴显示,以展现一个干净的画布
示例代码如下:
image_folder = './data/shine/' #指定图像文件夹路径
image_files = [f for f in os.listdir(image_folder) if f.endswith((".jpg", ".png", ".jpeg"))]
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()
运行结果如下:为晴天的图片,一共选取了24张,按3×8的格式排列
3.图片数据处理
torchvision.transforms是pytorch中的图像预处理包。一般用Compose把多个步骤整合到一起 1)Resize:将输入图片resize成统一尺寸 2)ToTensor:将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间 3)Normalize:标准化处理-->转换为标准正态分布,使模型更容易收敛;其中mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的 ImageFolder类来创建一个数据集对象,total_data将是一个包含所有图像数据的数据集对象,可以用于训练神经网络模型
示例代码如下:
total_datadir = './data/'
'''
torchvision.transforms是pytorch中的图像预处理包。一般用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], # 其中 mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的
std=[0.229, 0.224, 0.225])
])
total_data = datasets.ImageFolder(total_datadir, transform=train_transforms)
print(total_data)
打印数据集对象为:
4.划分数据集
train_size:表示训练集大小,为总体数据长度的80%
test_size:表示测试集大小,是总体数据长度减去训练集大小
torch.utils.data.random_split():将总体数据total_data按照指定的比例大小随机划分为训练集和测试集,并将划分的结果分别赋值给train_dataset和test_dataset两个变量
代码如下:
train_size = int(0.8 * len(total_data))
test_size = len(total_data) - train_size
#将总体数据total_data按照指定的大小比例([train_size, test_size])随机划分为训练集和测试集
train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])
print(train_dataset, test_dataset)
print(train_size, test_size)
打印结果如下:
5.查看一个batch_size的数据结构
torch.utils.data.DataLoader():这是Pytorch中用于加载和管理数据的一个实用工具类,它允许以小批次的方式迭代数据集,这对于训练神经网络和其他机器学习任务非常有用。具体参数解释如下:
1)dataset(必需参数):这是你的数据集对象,通常是
torch.utils.data.Dataset
的子类,它包含了你的数据样本。2)batch_size(可选参数):指定每个小批次中包含的样本数。默认值为 1。
3)shuffle(可选参数):如果设置为
True
,则在每个 epoch 开始时对数据进行洗牌,以随机打乱样本的顺序。这对于训练数据的随机性很重要,以避免模型学习到数据的顺序性。默认值为False
。4)num_workers(可选参数):用于数据加载的子进程数量。通常,将其设置为大于 0 的值可以加快数据加载速度,特别是当数据集很大时。默认值为 0,表示在主进程中加载数据。注意这里如果是使用CPU运行的话,要设置为默认值0,如果设置为1的话,会出现报错说是不能多进程运行,而且会反复反复的运行前面的内容
5)pin_memory(可选参数):如果设置为
True
,则数据加载到 GPU 时会将数据存储在 CUDA 的锁页内存中,这可以加速数据传输到 GPU。默认值为False
。6)drop_last(可选参数):如果设置为
True
,则在最后一个小批次可能包含样本数小于batch_size
时,丢弃该小批次。这在某些情况下很有用,以确保所有小批次具有相同的大小。默认值为False
。7)timeout(可选参数):如果设置为正整数,它定义了每个子进程在等待数据加载器传递数据时的超时时间(以秒为单位)。这可以用于避免子进程卡住的情况。默认值为 0,表示没有超时限制。
8)worker_init_fn(可选参数):一个可选的函数,用于初始化每个子进程的状态。这对于设置每个子进程的随机种子或其他初始化操作很有用。
代码如下:
batch_size = 32
train_dl = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
test_dl = torch.utils.data.DataLoader(test_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
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
打印结果如下:
Shape of X [N, C, H, W]: torch.Size([32, 3, 224, 224])
Shape of y: torch.Size([32]) torch.int64
二、构建CNN网络
对于一般的CNN网络来说,都是由特征提取网络和分类网络构成,其中特征提取网络用于提取图片的特征,分类网络用于将图片进行分类
2.1 网络结构推导
1.本次使用的网络结构如下:
2.卷积层和池化层的计算:
1)网络数据shape变化的过程如下:
上面的网络数据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)
2)手算过程见下图
2.2 代码实现
代码如下(示例):
class Network_bn(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)
self.conv2 = nn.Conv2d(in_channels=12, out_channels=12, kernel_size=5, stride=1, padding=0)
self.bn2 = nn.BatchNorm2d(12)
self.pool = 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(classNames))
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = self.pool(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)
模型结构打印如下:
三、训练模型
3.1 设置超参数:损失函数、学习率、优化器
这里一开始选用的是SGD的优化器,但是由于精度未达到要求,所以我修改了优化器为Adam,之后精度有了进一步的提升
loss_fn = nn.CrossEntropyLoss() #交叉熵函数
learn_rate = 1e-4
opt = torch.optim.Adam(model.parameters(), lr=learn_rate)
3.2 编写训练函数
代码如下:
def train(dataloader, model, loss_fn, optimier):
size = len(dataloader.dataset) #训练集的大小
num_batches = len(dataloader)
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)
#反向传播
optimier.zero_grad() #grad属性归零
loss.backward() #反向传播
optimier.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.3 编写测试函数
代码如下:
def test(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:
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
3.4 正式训练
一开始设置的epoch为20,为提升精度,我把epoch改为了25
代码如下:
epochs = 25
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')
运行结果如下:
1)epoch等于20,优化器为SGD,学习率为0.01时的输出结果
2)epoch为25,学习率为0.0001,优化器为Adam时
PS:其实这里还有一个实验,但没截图,就是设置的epooch=20,lr=0.0001时的结果,此时其实已经在测试集上的运行结果已经挺好的了,不像lr=0.01时,但是还是没能达到93%,所以就进一步的修改了优化器
四、结果可视化
代码如下:
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()
打印训练集和测试集的损失和精度如下图:
从上图可以发现,随着epoch的增加,整体的预测精度是上升的,但是不稳定,这个曲线波动很大,不确定这种问题该怎么解决,希望能在后面的学习中进一步了解如何使得训练更加稳定
总结
- 前面两周的学习中,数据的加载都是来自pytorch自带的数据集,直接下载运行,但实际情况中,大多时候都是根据自己的研究内容使用相应的本地数据集,通过本周的学习大致清楚了如何在模型中使用本地数据集加载运行
- 但有一个问题,就是在使用本地数据集的时候如何将这个样本数量和batch_size,以及卷积层和池化层等模型的各层之间的网络shape设置合理,这块还不是很清楚,希望可以在后续的学习中弄明白这些问题
- 一开始我把学习率设置为0.01,导致我的运行结果精度很差,accuracy有的高达300%多,后面将其改为0.0001之后,模型的精度大大提升,所以模型的超参数对性能的影响还是很大的,如何根据具体问题设置合理的超参数也是比较关键的内容,这样可以帮助我们减少训练去试超参数的时间
- 对于优化器的选择也是同样的,在我将优化器从SGD转换为Adam后,模型的性能也有很大的提升,以及Epoch的设置
- 如何根据实际问题来设置和选择这里模型的相关参数是以后学习的进一步目标