文章目录
- 1模型介绍
- 2 模型搭建
- 3 模型训练
- 4 模型预测
猫狗二分类,模型简单,训练精度并不高。数据集下载:<https://aistudio.baidu.com/datasetdetail/26884> 百度飞浆上找的大小只有60多M
1模型介绍
AlexNet是一个卷积神经网络的名字,最初是与CUDA一起使用GPU支持运行的,AlexNet是2012年ImageNet竞赛冠军获得者Alex Krizhevsky设计的。该网络的错误率与前一届冠军相比减小了10%以上,比亚军高出10.8个百分点。AlexNet是由多伦多大学SuperVision组设计的,由Alex Krizhevsky, Geoffrey Hinton和Ilya Sutskever组成。
AlexNet模型共有5个卷积层,3个全连接层,前两个卷积层和第五个卷积层有pool池化层,其他两个卷积层没有池化。
AlexNet介绍:https://www.jiqizhixin.com/graph/technologies/a0955638-affc-472d-80b0-f645a4fd4604
计算过程举例:
卷积操作后的输出尺寸可以通过以下公式计算
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)
官方介绍 https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html#torch.nn.Conv2d
一般默认dilation=1,简化之后:
在 PyTorch 中,卷积操作会按照上述公式计算输出特征图的尺寸,并且会使用 floor 函数来向下取整。padding 和 stride 参数在卷积计算中会影响最终的输出维度。
例如:
输入的一批张量的维度为 (64, 3, 224, 224),这个张量代表一个批量大小为 64 的图片数据集合,每张图片有 3 个通道(RGB ),每个通道的分辨率【图片大H和W】为 224x224。经过该nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2)卷积操作,输出张量是什么样的情况?
输出张量的形状将是 (64, 96, 55, 55)。其中:
批量大小仍然是 64;
输出通道数变为 96;【上图是借助两个GPU运算,分两个out_c=48,合起来就是out_c=96】
每个通道的高和宽均为 55x55。
最大池化操作后的输出尺寸可以通过以下公式计算
官方文档:https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html#torch.nn.MaxPool2d
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
带入默认值简化之后如下:【注意:向下取整符号】
将上一步卷积后的输出张量 (64, 96, 55, 55),传递给最大池化层 nn.MaxPool2d(kernel_size=3, stride=2) 池化操作后输出结果是什么?
输出张量的形状将是 (64, 96, 27, 27)。其中:
批量大小仍然是 64;
输出通道数仍然是 96;
每个通道的高和宽现在是 27x27。
2 模型搭建
import torch
from torch import nn
class AlexNet(nn.Module):
def __init__(self, num_classes=2):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), # [None, 3, 224, 224] --> [None, 96, 55, 55]
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2), # [None, 96, 55, 55] --> [None, 96, 27, 27]
nn.Conv2d(96, 256, kernel_size=5, padding=2), # [None, 96, 27, 27] --> [None, 256, 27, 27]
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2), # [None, 256, 27, 27] --> [None, 256, 13, 13]
nn.Conv2d(256, 384, kernel_size=3, padding=1), # [None, 256, 27, 27] --> [None, 384, 13, 13]
nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), # [None, 384, 13, 13] --> [None, 384, 13, 13]
nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), # [None, 384, 13, 13] --> [None, 256, 13, 13]
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2) # [None, 256, 13, 13] --> [None, 256, 6, 6]
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.2),
nn.Linear(256 * 6 * 6, 2048),
nn.ReLU(),
nn.Dropout(p=0.2),
nn.Linear(2048, 2048),
nn.ReLU(),
nn.Linear(2048, num_classes)
)
def forward(self, inputs):
x = self.features(inputs)
x = torch.flatten(x, start_dim=1)
outputs = self.classifier(x)
return outputs
3 模型训练
train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform)
的作用是使用 PyTorch 提供的 ImageFolder 类来加载指定目录 【例如目录:ROOT_TRAIN】中的图像数据,并应用数据预处理或增强变换 (train_transform)。【专门用于加载文件夹格式的数据集。】
它要求数据集的组织形式为:【每个子文件夹的名称 (class1, class2, 等等) 代表一个类别,文件夹中的所有图片都属于该类别。】
ROOT_TRAIN/
├── class1/
│ ├── img1.jpg
│ ├── img2.jpg
│ └── …
├── class2/
│ ├── img1.jpg
│ ├── img2.jpg
│ └── …
└── …
ImageFolder 会自动为每个类别生成一个标签(从0开始的整数),并将其与相应的图像关联。例如,如果目录下有两个子文件夹 class1 和 class2,则 class1 中的所有图像的标签为 0,class2 中的图像标签为 1。
为什么说这个呢,因为我遇到问题了,还困扰了我好一会儿
我是在colab上跑的,第一次用这个ImageFolder函数,也不太知道详情,我第一的数据集是这样的,就是猫狗未分文件夹,统一放在train或者val中的。
data/
├── train/
│ ├── cat.jpg
│ ├── dog.jpg
│ └── …
├── val/
│ ├── cat.jpg
│ ├── dog.jpg
│ └── …
└── …
后边了解之后,就将train和val下的猫狗分类放入对应的文件夹
data/
├── train/
│ ├── cat/
│ │ ├── cat.jpg
│ │ └── …
│ ├── dog/
│ │ ├── dog.jpg
│ │ └── …
│ └── …
├── val/
│ ├── cat/
│ │ ├── cat.jpg
│ │ └── …
│ ├── dog/
│ │ ├── dog.jpg
│ │ └── …
│ └── …
这里介绍两个自定义工具类:
SortUtil.py
将数据集从样式
随机分割组装成第二种格式
import os
from shutil import copy
import random
def make_dir(file):
if not os.path.exists(file):
os.makedirs(file)
# 获取data_class文件夹下所有文件夹名(即需要分类的类名)
file_path = '../data_class' # ../data_class 是数据文件目录
flower_class = [cla for cla in os.listdir(file_path)]
# 创建 训练集train 文件夹,并由类名在其目录下创建5个子目录
make_dir('../data/train')
for cla in flower_class:
make_dir('../data/train/' + cla)
# 创建 验证集val 文件夹,并由类名在其目录下创建子目录
make_dir('../data/val')
for cla in flower_class:
make_dir('../data/val/' + cla)
# 划分比例,训练集 : 验证集 = 7 : 3
split_rate = 0.3
# 遍历所有类别的全部图像并按比例分成训练集和验证集
for cla in flower_class:
cla_path = file_path + '/' + cla + '/' # 某一类别的子目录
images = os.listdir(cla_path) # iamges 列表存储了该目录下所有图像的名称
num = len(images)
eval_index = random.sample(images, k=int(num * split_rate)) # 从images列表中随机抽取 k 个图像名称
for index, image in enumerate(images):
# eval_index 中保存验证集val的图像名称
if image in eval_index:
image_path = cla_path + image
new_path = '../data/val/' + cla
copy(image_path, new_path) # 将选中的图像复制到新路径
# 其余的图像保存在训练集train中
else:
image_path = cla_path + image
new_path = '../data/train/' + cla
copy(image_path, new_path)
print("\r[{}] processing [{}/{}]".format(cla, index + 1, num), end="") # processing bar
print()
print("processing done!")
ZipExtract.py 【zip文件解压缩】
这里是因为我用了colab 去跑模型,然后遇到了一些问题。https://colab.google/
目前使用的感受:
弊端1:GPU有时候没有资源,抢不到,只能用cpu
弊端2:上传的文件他不能保存,当你使用的文本和服务器断联后,再进去,文件就没了
弊端3:上传数据很慢,下载也是
原由:我需要将自己的数据集上传到colab上面,但是直接上传文件中的图片,大几万张,比较乱还比较慢。我就想到能不能上传个压缩包,上传之后,发现他没有解压选项,所以就有了这个工具类,去解压zip数据。
import zipfile
import os
def unzip_file(zip_file_path, extract_to_dir):
"""
解压 ZIP 文件到指定目录。
:param zip_file_path: ZIP 文件的路径
:param extract_to_dir: 目标解压目录
"""
# 检查目标目录是否存在,如果不存在则创建
if not os.path.exists(extract_to_dir):
os.makedirs(extract_to_dir)
# 打开 ZIP 文件
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
# 解压所有内容到目标目录
zip_ref.extractall(extract_to_dir)
print(f"解压缩完成,文件解压到: {extract_to_dir}")
# 你的 ZIP 文件路径
zip_file_path = './example'
# 你希望解压到的目录
extract_to_dir = './output'
unzip_file(zip_file_path, extract_to_dir)
模型训练搭建
import torch
from torch import nn
from AlexNet import AlexNet
from torch.optim import lr_scheduler
import os
from datetime import datetime
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
ROOT_TRAIN = r'../data/train'
ROOT_TEST = r'../data/val'
# 将图像RGB三个通道的像素值分别减去0.5,再除以0.5.从而将所有的像素值固定在[-1,1]范围内
normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
train_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.RandomVerticalFlip(), # 随机垂直旋转
transforms.ToTensor(), # 将0-255范围内的像素转为0-1范围内的tensor
normalize])
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
normalize])
train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform)
val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
train_dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True)
val_dataloader = DataLoader(val_dataset, batch_size=64, shuffle=True)
# 如果显卡可用,则用显卡进行训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("当前设备{}".format(device))
# 调用net里面的定义的网络模型, 如果GPU可用则将模型转到GPU
my_nn = AlexNet(num_classes=2).to(device)
# 定义损失函数(交叉熵损失)
loss_fn = nn.CrossEntropyLoss().to(device)
# 定义优化器(SGD)
optimizer = torch.optim.SGD(my_nn.parameters(), lr=0.01, momentum=0.9)
# 学习率衰减 ,每十轮衰减一次 ,衰减为原来的0.1
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
# 定义训练函数
def train_net(dataloader, model, loss_fn, optim, lr_scheduler):
model.train()
# 一轮次的损失值、一轮次的正确率、一轮次被分为几个批次
epoch_loss, epoch_acc, batch_count = 0.0, 0.0, 0
# 学习率衰减的几轮
cur_epoch = lr_scheduler.last_epoch + 1
print("当前为第{}轮次,此时的学习率为{}".format(cur_epoch, optim.param_groups[0]['lr']))
for data in dataloader:
img, tag = data
# 将数据移动到设备上【cpu或者gpu,看支持情况】
img, tag = img.to(device), tag.to(device)
# 记录该批次模型训练输出结果
output = model(img)
# 记录该批次的损失值
batch_loss = loss_fn(output, tag)
# 对 output 张量的每一行取最大值和对应的索引。_ 保存最大值,pred 保存最大值的索引,也就是预测的类别。
pred_num, pred = torch.max(output, axis=1)
# 将正确的预测数量除以总的样本数量,得到当前批次的准确率
cur_acc = torch.sum(tag == pred) / output.shape[0]
# 梯度清零,pytorch框架需要手动调用
optim.zero_grad()
# 反向传播计算梯度
batch_loss.backward()
# 更新模型参数
optim.step()
# 累加批次损失值
epoch_loss += batch_loss.item()
# 累加批次正确率
epoch_acc += cur_acc.item()
# 记录总批次数
batch_count += 1
"""
lr_scheduler.step() 有内部机制来管理学习率的调整,有一个内部计数器来跟踪训练进度。
每次调用 step() 方法时,StepLR 会检查当前的 epoch 数量是否达到了 step_size 的倍数。
如果是,它会按照设定的 gamma 参数更新学习率;如果不是,它不会调整学习率。
"""
# 每个epoch结束后,更新学习率
lr_scheduler.step()
print("Train_Avg_Batch_Loss:{} ".format(epoch_loss / batch_count))
print("Train_Avg_Batch_Acc: {}".format(epoch_acc / batch_count))
return epoch_loss / batch_count, epoch_acc / batch_count
# 定义模型测试方法
def test_net(dataloader, model, loss_fn):
model.eval()
# 一轮次的损失值、一轮次的正确率、一轮次被分为几个批次
epoch_loss, epoch_acc, batch_count = 0.0, 0.0, 0
# 禁用梯度计算,节省内存和加速计算【在test阶段,是不需要梯度计算的】
with torch.no_grad():
for data in dataloader:
img, tag = data
# 将数据移动到设备上【cpu或者gpu,看支持情况】
img, tag = img.to(device), tag.to(device)
# 记录该批次模型训练输出结果
output = model(img)
# 记录该批次的损失值
batch_loss = loss_fn(output, tag)
# 对 output 张量的每一行取最大值和对应的索引。pred_num存最大值,pred 保存最大值的索引,也就是预测的类别。
pred_num, pred = torch.max(output, axis=1)
# 将正确的预测数量除以总的样本数量,得到当前批次的准确率
cur_acc = torch.sum(tag == pred) / output.shape[0]
# 累加批次损失值
epoch_loss += batch_loss.item()
# 累加批次正确率
epoch_acc += cur_acc.item()
# 记录总批次数
batch_count += 1
print("Test_Avg_Batch_Loss:{} ".format(epoch_loss / batch_count))
print("Test_Avg_Batch_Acc: {}".format(epoch_acc / batch_count))
# 返回批次正确率
return epoch_loss / batch_count, epoch_acc / batch_count
# 画图函数
def matplot_loss(train_loss, val_loss):
plt.plot(train_loss, label='train_loss')
plt.plot(val_loss, label='val_loss')
plt.legend(loc='best')
plt.ylabel('loss', fontsize=12)
plt.xlabel('epoch', fontsize=12)
plt.title("训练集和验证集loss值对比图")
plt.show()
def matplot_acc(train_acc, val_acc):
plt.plot(train_acc, label='train_acc')
plt.plot(val_acc, label='val_acc')
plt.legend(loc='best')
plt.ylabel('acc', fontsize=12)
plt.xlabel('epoch', fontsize=12)
plt.title("训练集和验证集精确度值对比图")
plt.show()
# 开始训练
loss_train = []
acc_train = []
loss_val = []
acc_val = []
"""
只保存两个模型:
1、测试结果最好的那一个模型
2、最后的那一个模型
"""
# 开始训练模型
epoch = 25
max_acc = 0.0
# 计时
start_time = datetime.now()
print("当前训练模型是AlexNet,猫狗二分类,预定训练轮次-{}".format(epoch))
for t in range(epoch):
print("-----第{}轮训练开始-----".format(t + 1))
train_loss, train_acc = train_net(train_dataloader, my_nn, loss_fn, optimizer, lr_scheduler)
val_loss, val_acc = test_net(val_dataloader, my_nn, loss_fn)
loss_train.append(train_loss)
acc_train.append(train_acc)
loss_val.append(val_loss)
acc_val.append(val_acc)
# 保存最好的模型权重文件
if val_acc > max_acc:
folder = '../save_models'
if not os.path.exists(folder):
os.mkdir('../save_models')
max_acc = val_acc
print(f'save best model,第{t + 1}轮')
torch.save(my_nn.state_dict(), '../save_models/best_model.pth')
# 保存最后的权重模型文件
if t == epoch - 1:
torch.save(my_nn.state_dict(), '../save_models/last_model.pth')
print("save last ok!")
print('Done!')
end_time = datetime.now()
print("start_time:{}".format(start_time))
print("end_time:{}".format(end_time))
print("{}训练总用时:{}".format(device, end_time - start_time))
print("Done!")
matplot_loss(loss_train, loss_val)
matplot_acc(acc_train, acc_val)
问题1:处理方式对比
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
Resize(256): 将图像的较长边调整为256像素。
CenterCrop(224): 从调整后的图像中中心裁剪224x224的区域。
ToTensor(): 将图像从[0, 255]范围内的像素值转换为[0, 1]范围内的浮点数,并将图像转换为PyTorch的Tensor格式。
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]): 使用ImageNet数据集的均值和标准差对图像进行归一化。
train_transform = transforms.Compose([
transforms.Resize((224, 224)), # 裁剪为224x224
transforms.RandomVerticalFlip(), # 随机垂直翻转
transforms.ToTensor(), # 将0-255范围内的像素转为0-1范围内的tensor
normalize
])
Resize((224, 224)): 将图像调整为224x224的大小(这与标准处理中的CenterCrop不同,可能会影响图像的内容)。
RandomVerticalFlip(): 在训练时随机进行垂直翻转,这是一种数据增强方法,有助于提高模型的泛化能力。
ToTensor(): 将图像从[0, 255]范围内的像素值转换为[0, 1]范围内的浮点数,并将图像转换为PyTorch的Tensor格式。
Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]): 将图像像素值范围从[0, 1]调整到[-1, 1]。【更适合与自定义数据集配合使用】
问题2:归一化计算方式
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
ransforms.Normalize(mean=[0.5, 0.5, 0.5], std= [0.5, 0.5, 0.5])
首先,需要知道ToTensor()的处理结果。
然后,是Normalize(mean, std)是如何处理计算的。
以 transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])为例。
计算下线【pixel=0】,计算上线【pixel=1】。
问题3:在深度学习中,normalize函数的作用是什么,有他没他会对训练造成什么影响?
在深度学习中,normalize
函数通常用于将数据的数值范围调整到一个标准范围(例如0到1,或者均值为0,标准差为1)。
其主要作用包括:
- 加速收敛:将输入数据归一化到相同的尺度可以帮助加速训练过程。梯度下降法在训练时会更稳定,减少了训练过程中出现的梯度消失或梯度爆炸的问题。
- 提高模型稳定性:通过归一化,模型在训练时可以更稳定地更新权重,因为不同特征的数值范围被标准化到类似的尺度,这样模型可以更容易地学习到有用的模式。
- 避免特征间的不平衡:如果不同特征的尺度差异很大,某些特征可能会对模型的训练产生不成比例的影响。归一化可以确保所有特征在训练过程中对模型的贡献更加均衡。
如果不进行归一化,训练可能会遇到以下问题:
- 训练速度慢:由于特征的尺度不同,模型可能需要更长的时间来收敛,因为每个特征的更新幅度不同。
- 收敛不稳定:特征值范围差异大可能导致梯度在训练过程中变化剧烈,增加了梯度消失或爆炸的风险。
- 模型性能下降:特征不均衡可能导致模型无法有效地学习所有特征的有用信息,从而影响最终的预测性能。
问题4:如果我的数据训练集上都加上了normalize,那么我在模型训练结束之后的测试集,是不是也应该加上相同的normalize处理,不加会怎么样,或者加的normalize参数和训练集的不一样会怎么样?
在进行模型评估或测试时,测试集也应该进行相同的归一化处理。归一化是数据预处理的一部分,保持训练集和测试集的一致性非常重要。
测试集也要进行与训练集相同的归一化
如果测试数据的归一化方式与训练数据不一致或没有归一化,模型的输出将无法反映真实性能,测试结果将不可比。这样会导致测试结果失真,因为模型已经适应了训练数据的特定尺度。模型在训练时是基于归一化后的数据进行学习的,因此在测试时也必须使用相同的归一化标准。
4 模型预测
import torch
from AlexNet import AlexNet
from torch.autograd import Variable
from torchvision import datasets, transforms
from torchvision.transforms import ToPILImage
from torchvision.datasets import ImageFolder
import matplotlib.pyplot as plt
ROOT_TEST = r'../data2/train'
val_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
print(len(val_dataset))
# 如果显卡可用,则用显卡进行训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("当前设备{}".format(device))
# 调用net里面的定义的网络模型, 如果GPU可用则将模型转到GPU
model = AlexNet().to(device)
# 加载模型train.py里面训练的模型
model.load_state_dict(torch.load('../save_models/best_model.pth', weights_only=True))
# 获取预测结果
classes = ['cat', 'dog']
# 把tensor转成Image,方便可视化
show = ToPILImage()
# 进入验证阶段
model.eval()
arr = [1500, 1123, 45, 36, 78, 50, 1999, 1998]
# 对val_dataset里面的照片进行推理验证
for i in arr:
x, y = val_dataset[i][0], val_dataset[i][1]
# show(x).show()
img = show(x)
# 使用 matplotlib 显示图像并设置标题
plt.imshow(img)
plt.title(i)
plt.axis('off') # 如果不想显示坐标轴,可以关闭
plt.show()
x = Variable(torch.unsqueeze(x, dim=0).float(), requires_grad=False).to(device)
with torch.no_grad():
pred = model(x)
# print(pred)
predicted, actual = classes[torch.argmax(pred[0])], classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')