深度学习入门系列(五)之图片分类实战及数据处理方法
本篇主要讲解食物分类实战训练和验证部分代码并引入半监督学习,包含迁移学习和图片增广等数据处理方法。食物图片一共分为11个类别,其中带标签的数据:28011,不带标签的训练数据: 6786验证集3011,测试集3347。这个可看可不看,因为我们主要用别人训练好的模型nn.BatchNorm2d(64), # 归一化,要和输出的通道数保持一致nn.ReLU(),nn.BatchNorm2d(128),
前言
本篇主要讲解食物分类实战训练和验证部分代码并引入半监督学习,包含迁移学习和图片增广等数据处理方法。
食物图片一共分为11个类别,其中带标签的数据:280 11,不带标签的训练数据: 6786
验证集3011,测试集3347。
其中带标签数据用于正常训练,不带标签数据用于半监督学习。
`
1,主文件预处理工作
1.1主函数导包
先导入所需的包并采用多文件编程处理,设定整个代码结构。
主文件设为main,自定义模型设为model,训练验证函数设为train_val,数据处理设为data。
用多文件编程可以避免代码都放在一个文件很混乱,条理不清晰。
import random
import torch
import torch.nn as nn
import numpy as np
import os
from torch.utils.data import DataLoader
from model_utils.model import initialize_model # 使用其他人设定的函数,方便快速选择模型
from train_val_sim import train_val # 我的训练验证函数
from data_sim import food_dataset # 我的数据处理
# from torchvision.models import resnet18 # 迁移学习
# from model_sim import my_model # 我的初始自定义模型类
1.2定义随机数种子
使用此代码定义,设种子为0,保证每次随机结果保持一致。
def seed_everything(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
###############################################
seed_everything(0)
# 随机数种子指定以后,每次随机出来的内容都是一样的
# 不指定的话随机就是随机出来一个种子,还是种子控制
# 就像mc生成地图那个种子
# print(random.randint(1, 8)) # 每次都一样
###############################################
随机数种子
在深度学习和机器学习中,随机种子(Random Seed)是一个非常重要的概念,它用于控制随机数生成器的初始状态,从而使得随机过程具有可重复性。下面为你详细介绍随机种子在你当前代码中的作用、使用方法以及重要性。
在深度学习中,很多操作都涉及到随机数的生成,例如:
数据的随机划分:将数据集划分为训练集、验证集和测试集时,通常会进行随机划分。
模型参数的随机初始化:神经网络的权重和偏置通常是随机初始化的。
数据增强:在数据增强过程中,如随机裁剪、随机旋转等操作也会使用随机数。
使用随机种子可以确保每次运行代码时,这些随机操作生成的结果都是相同的,从而使得实验具有可重复性。也就是说,不同的人在相同的代码和随机种子设置下,能够得到相同的实验结果。
我使用了一个随机种子函数,目的:设置随机种子,确保实验的可重复性。
1.3读入文件
整个文件结构如图所示

设定路径,写代码调试尽量使用样本图片的数据,样本数据含有少量图片,可以进行快速处理。
(r""可以让反斜杠失效。)
# simple设为含有少量图片,用于测试代码
train_path = r"\food_classification\food-11_sample\training\labeled"
# train_path = r"\food_classification\food-11\training\labeled"
val_path = r"\food_classification\food-11_sample\validation"
# val_path = r"\food_classification\food-11\validation"
no_label_path = r"\food_classification\food-11\training\unlabeled\00"
1.4设定整体结构
预处理操作完事以后,要划分所有代码文件具体细节要做什么。
train_set = food_dataset(train_path, "train")
val_set = food_dataset(val_path, "val")
no_label_set = food_dataset(no_label_path, "semi")
首先要设定数据处理,从设定好的路径读入图片,然后处理成数组,链接,并进行数据增强操作。
train_loader = DataLoader(train_set, batch_size=8, shuffle=True)
val_loader = DataLoader(val_set, batch_size=8, shuffle=True)
no_label_loader = DataLoader(no_label_set, batch_size=8, shuffle=False)
然后创建数据加载器,将数据集按照设定的批次大小和是否打乱等参数,分批加载到模型中进行训练。
1)train_set:这是训练数据集,包含了用于模型训练的所有样本。
2)batch_size=8:表示每次从数据集中取出的样本数量为8。模型会以这8个样本为一组进行训练,这样可以提高训练效率并减少内存占用。如果想快一点,看显存,可以增大batch_size。
3)shuffle=True:意味着在每个训练周期(epoch)开始时,数据集中的样本顺序会被随机打乱。这样做有助于模型避免学习到数据的固定顺序模式,使训练更加随机和全面。其中无标签数据不进行打乱,无标签数据最后用预测的标签作为其真实标签,打乱的话标签也会打乱,不是很好处理。
# from torchvision.models import resnet18
# model = resnet18(pretrained=True) # 迁移学习,pretrained=True用已经训练好的参数
# in_features = model.fc.in_features # 改一下全连接层的输出个数,in_features就是resnet18的分类头
# model.fc = nn.Linear(in_features, 11) 将大佬的分类头修改成自己所需的分类
# linear_prob线性探测,就是完全用预训练模型参数不变,不计算梯度,否则会微调参数
# 用别人已经训练好的网络进行训练
model, _ = initialize_model("resnet18", 11, use_pretrained=True, linear_prob=False)
# model = my_model(11)
# for batch_x, batch_y in train_loader:
# pred = model(batch_x)
# print(batch_x)
定义模型架构,可以使用官方定义好的模型进行迁移学习,或者自己定义的模型,官方定义好的模型需要改全连接输出层的输出个数,改为要分类的类别数。然后需要使用官方训练好的参数并设置允许微调参数,进行参数更新,会比自定义模型的效果要好很多。直接用大佬模型的训练参数(即use_pretrained=true)验证准确率可达60以上,如果设为false只有30%准确率
initialize_model给出你想要的模型名字和分类数,返回你想要的模型。
linear_prob:线性探测指设为true用于冻结大佬模型的参数,以后训练过程中参数不会在梯度下降中更新。
微调:linear_prob设为false则可以将大佬的参数在自己的训练集上继续进行训练,参与梯度下降同时也会变更大佬的参数值,这就是微调,此处为全量微调,后面bert项目中使用了lora微调
use_pretrained:表示是否使用大佬模型训练好的参数
迁移学习
迁移学习:调用了大佬的模型(大佬模型已上传至torch官方),采用resnet18,vgg等模型分别进行训练,如采用resnet18模型作为特征提取器进行预训练,固定其参数(迁移学习最重要的就是沿用大佬设定好的参数),取出他的分类头,再增加一层,输出到我的分类数,即在预测结果时修改最后一层全连接数(自己的目标分类)进行微调。
迁移学习的主要步骤包括:
- 选择预训练模型:选择一个在大规模数据集上预训练好的模型,例如在 ImageNet 数据集上预训练的卷积神经网络(CNN)模型。
- 特征提取:使用预训练模型的卷积层部分作为特征提取器,将输入数据转换为特征向量。这些特征向量可以作为新任务的输入。
- 微调:在新任务的数据集上,对预训练模型的部分或全部参数进行微调,以适应新任务的特定需求。微调可以包括调整模型的最后几层(全连接层),或者对整个模型进行微调。
- 评估和优化:使用新任务的验证集或测试集对微调后的模型进行评估,并根据评估结果进行优化,例如调整学习率、增加训练轮数等。
adamw优化器
1.采用自适应学习率优化器Adamw比随机梯度下降优化器SGD好很多,区别:梯度的改变,学习率的改变。相比SGD只看当前点的梯度,Adam不仅看当前点的梯度也看以前点的梯度。Adam会自动调整学习率,梯度大时学习率调小,反之调大,合适的学习率能加快收敛并避免震荡。故可以防止模型坍塌和梯度爆炸。
2.Adam 算法的核心在于自适应地调整每个参数的学习率。学习率可以理解为我们在调整参数时迈出的 “步子大小”。步子太大,可能会错过最优解;步子太小,又会让训练速度变得很慢。Adam 算法会根据每个参数的历史梯度信息,为它们动态地分配合适的学习率,就像给每个参数都配备了一个 “智能导航”,让它们能以合适的速度朝着最优解前进。W为权重衰减,让loss曲线更平缓, 训练过程更加稳定。
3.Adam(Adaptive Moment Estimation)原理:Adam 结合了 AdaGrad 和 RMSProp 的优点,它计算每个参数的自适应学习率。具体来说,它使用一阶矩估计(均值)和二阶矩估计(未中心化的方差)来动态调整每个参数的学习率。优点:收敛速度快,对不同类型的问题有较好的适应性,不需要手动调整学习率。缺点:可能会导致模型过拟合,特别是在使用 L2 正则化时。AdamW(Adam with Weight Decay Fix)原理:AdamW 是对 Adam 的改进,主要解决了 Adam 在使用 L2 正则化时的问题。在 Adam 中,L2 正则化和自适应学习率的结合可能会导致权重衰减的效果不理想。AdamW 通过将权重衰减直接应用到参数更新步骤中,避免了这个问题。优点:在使用 L2 正则化时表现更好,能有效防止模型过拟合。缺点:与 Adam 相比,可能需要更多的调参工作。
lr = 0.001
epochs = 15
loss = nn.CrossEntropyLoss() # 交叉熵
# adam可以灵活调整参数变化,每次更新会综合历史的梯度和现在的梯度
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
device = "cuda" if torch.cuda.is_available() else "cpu"
save_path = "model_save/best_model_sim.pth"
threshold = 0.99
config = {
'epochs': epochs,
'loss': loss,
'optimizer': optimizer,
'device': device,
'save_path': save_path,
'threshold': threshold,
'model': model,
'train_loader': train_loader,
'val_loader': val_loader,
'no_label_loader': no_label_loader
}
weight_decay(权重衰减) 是一种常用的正则化技术,主要用于防止模型过拟合。它的核心作用是通过对模型权重施加约束,降低模型的复杂度,从而提高泛化能力。
L2正则化可以看作权重衰减,使用时要么在损失函数中自定义加入权重衰减(如第三章自定义的L2正则化损失函数),要么在优化器的参数中加入权重衰减weight_decay进行正则化。
weight_decay常见取值范围:
设定各种超参数,优化器,损失等内容,并将其上面设定的所有内容都封装到一个字典中,以便传入到训练函数中进行读取
lr是学习率,epochs是周期数,设置loss为交叉熵损失,上一篇文章说过概率值损失计算要使用交叉熵
设置adam优化器,设备,保存路径等。
if __name__ == '__main__': # 如果运行的当前文件就执行,调用的话不执行
train_val(config)
最后声明一个函数,训练和验证,如果不用config,就是要列很多很长的参数。
2,数据处理部分
2.1导入包
import numpy as np
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
import os
from torchvision.transforms import transforms # 进行数据增强用
from tqdm import tqdm # 展示循环进度
from PIL import Image # 读取图片数据
from torch.utils.data.dataset import T_co
2.2数据集处理
放data文件。
先定义一个food_dataset数据集的类,同样主要包含三个方法init,getitem和len
class food_dataset(Dataset):
def __init__(self, path, mode):
self.mode = mode
if mode == "semi":
self.x = self.read_file(path)
else:
self.x, self.y = self.read_file(path)
self.y = torch.LongTensor(self.y) # 将标签变成长整型,才行
if mode == "train":
self.transform = train_transforms
else:
self.transform = val_transforms
先看init方法,先将mode传给自己,如果模式是semi半监督的话,只接受整理好的数据,如果不是的话要接受整理好的数据和标签,标签要转为长整型,因为交叉熵损失要求使用长整型数据,并转为张量。
下面如果训练模式下就用训练用的transforms函数,否则用验证用的transforms函数,这个就是数据增强函数的选择。
# 要在每次读一个批次的数据时进行数据增强
# compose表示要进行多个数据变换,但要把它们合在一起
train_transforms = transforms.Compose(
[
transforms.ToPILImage(), # 将大小为224*224的图片转为模型可以接受的3*224*224
transforms.RandomResizedCrop(224), # 随机放大再裁剪到224
transforms.RandomRotation(50), # 随机旋转50度以内
transforms.ToTensor() # 转为张量
]
)
val_transforms = transforms.Compose(
[
transforms.ToPILImage(), # 224*224*3转为模型可以接受的3*224*224
transforms.ToTensor() # 转为张量
]
)
在class 外面整体定义好所需的数据增强内容,对于训练集我们要对数据进行一系列变换操作,比如旋转,裁剪,改灰度,色调,加噪点…等,为了使训练出来的模型能适应各种情况,同时扩充了数据集。这里我们只进行简单放大裁剪旋转。对于验证集不需要进行这些操作,直接读入调整维度并转为张量。
数据增强、图片增广
在训练模式下,使用训练集的图像变换操作。通过调用transform对一个预先定义好的图像变换函数或变换组合,用于对训练图像进行增强、归一化等操作包括训练和验证的数据增强,比如随机裁剪、旋转,或对图片进行亮度和色调的调整。torchvision.transforms 模块来进行图像数据增广。在训练集取数据的时候进行图片增广,但验证集取数据验证时没必要进行增广,会导致准确率降低
def __getitem__(self, index) -> T_co:
if self.mode == "semi":
# transform过的用来模型进行检测,x用来及加入semi数据集
return self.transform(self.x[index]), self.x[index]
else:
return self.transform(self.x[index]), self.y[index]
def __len__(self): # x长度和y一样,都是一个图一个长度
return len(self.x)
先看getitem和len,每次对于实例化的对象要读数据时候就会调用getitem这个方法,统一返回两个内容,len方法返回长度,创建dataloader时候会取长度,从定义好的loader对象取数据时候做for循环操作也会先访问len,之后调用batch_size次数getitem来读取数据。
最后是这个最麻烦的读入数据的函数
def read_file(self, path):#读文件的函数,用于根据路径读取数据
if self.mode == "semi":
file_list = os.listdir(path)
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8) #xi表示每一类的图片,所有图片读取列表长度,图片长宽以及深度(通道数)
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
img_path = os.path.join(path, img_name)
img = Image.open(img_path)#读取图片
img = img.resize((HW, HW))
#定义图片大小 hw为224因为输入的图片数据集大小不一致,有的是512*512所以要调整为3*224*224
xi[j, ...] = img
print("读到了%d个数据" % len(xi))
return xi
先看训练和验证所用的数据处理:
else:
x, y = None, None
for i in tqdm(range(11)): # i也是标签
# 文件夹是从00到10共11个,要格式化2位整数
folder_path = path + r"\%02d" % i
# 列出文件夹下所有图片文件名
file_list = os.listdir(folder_path)
# 创建一个arr,从上到下每个格里面放一个图片,用来存储一个类别的图片
# unit8表示读成整数形式,图片的颜色数据没有小数
xi = np.zeros((len(file_list), 224, 224, 3), dtype=np.uint8)
# 创建一个arr,存一个类别的label
yi = np.zeros(len(file_list), dtype=np.uint8)
# 遍历
for j, img_name in enumerate(file_list):
# 将图片文件名和前置路径结合起来
img_path = os.path.join(folder_path, img_name)
img = Image.open(img_path) # 使用PIL读取图片
img = img.resize((224, 224)) # 重新将图片变成224*,训练用
# ...表示后面的维度一样
xi[j, ...] = img # 存入图片数据
yi[j] = i # i也是标签
if i == 0: # 如果是第一个类别,直接定义赋值
x = xi
y = yi
else: # 不然就是将它们链接到x后面,axis=0是在竖着连接
x = np.concatenate((x, xi), axis=0)
y = np.concatenate((y, yi), axis=0)
return x, y # 返回所有图片/标签链接在一起的矩阵arr
先把x和y定义一个none不然编辑器总写警告
进行11次循环,分别取00到10文件夹里面的图片数据,先用os包读文件名列表。
(tqdm会显示一个循环进行轮数的进度条,可以方便查看读取进度)
然后定义xi和yi用来存储一个类别数据和对应标签。

xi就是上图这样的,竖着每个维度存一张图片,文件数=长度
yi就是简单的一维数据,每一个都存本次取得第几个文件夹号就行。
对于每一个文件夹对应的file_list,进行遍历,读取每个图片文件名,以及它是第几张图。读取图片并进行尺寸变更。最后将所有图片都拼接到一起,concatenate的0轴就是继续竖着链接,比如上面那个图就是后面加11,12…这样的。
if self.mode == "semi":
folder_path = path
# 列出文件夹下所有图片文件名
file_list = os.listdir(folder_path)
# 创建一个arr,从上到下每个格里面放一个图片,用来存储一个类别的图片
# unit8表示读成整数形式,图片的颜色数据没有小数
xi = np.zeros((len(file_list), 224, 224, 3), dtype=np.uint8)
for j, img_name in enumerate(file_list):
# 将图片文件名和前置路径结合起来
img_path = os.path.join(folder_path, img_name)
img = Image.open(img_path) # 使用PIL读取图片
img = img.resize((224, 224)) # 重新将图片变成224*,训练用
# ...表示后面的维度一样
xi[j, ...] = img # 存入图片数据
return xi
对于semi的模式是类似的,semi不需要y标签,上面理解了这个就很简单了。
3,自定义模型
这个可看可不看,因为我们主要用别人训练好的模型
批归一化在神经网络的隐含层间进行BatchNormed
归一化的好处就是防止过拟合并加速模型收敛
class my_model(nn.Module):
def __init__(self, num_class):
super(my_model, self).__init__()
#目标将3*224*224的图像卷积到512*7*7再拉直全连接
self.layer1 = nn.Sequential(
nn.Conv2d(3, 64, 3, 1, 1),
# padding为1,步长为1,然后经过64个3*3*3的卷积核得到特征图64*224*224
nn.BatchNorm2d(64), # 批归一化,要和输出的通道数保持一致
nn.ReLU(),
nn.MaxPool2d(2) # 64*112*112
)
self.layer2 = nn.Sequential(
nn.Conv2d(64, 128, 3, 1, 1), # 128*112*112
nn.BatchNorm2d(128), # 归一化
nn.ReLU(),
nn.MaxPool2d(2) # 128*56*56
)
self.layer3 = nn.Sequential(
nn.Conv2d(128, 256, 3, 1, 1), # 256 56 56
nn.BatchNorm2d(256), # 归一化
nn.ReLU(),
nn.MaxPool2d(2) # 256 28 28
)
self.layer4 = nn.Sequential(
nn.Conv2d(256, 512, 3, 1, 1), # 512 28 28
nn.BatchNorm2d(512), # 归一化
nn.ReLU(),
nn.MaxPool2d(2) # 512 14 14
)
self.pool = nn.MaxPool2d(2) # 512 7 7
self.flatten = nn.Flatten() #拉直
self.linear1 = nn.Linear(25088, 1000) #全连接
self.relu = nn.ReLU()
self.linear2 = nn.Linear(1000, num_class)
几个卷积层,每一层内容差不多,由3 * 224 * 224转变为512 * 7 * 7,然后拉直转为25088到1000的全连接,最后输出需要的类别数num_class。每一层都是一个卷积接批归一化,接一个激活函数然后进行一次池化来改变尺寸。 达到目标大小后,flatten拉直,然后全连接分类
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.pool(x)
# x = x.view(x.size()[0], -1)
x = self.flatten(x)
x = self.linear1(x)
x = self.relu(x)
x = self.linear2(x)
return x
前向过程。 让图片通过自己制定的模型
4,训练验证
4.1导入内容
import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from data_sim import get_semi_loader
def train_val(config_hy):
epochs = config_hy['epochs']
loss = config_hy['loss']
optimizer = config_hy['optimizer']
device = config_hy['device']
save_path = config_hy['save_path']
threshold = config_hy['threshold']
model = config_hy['model']
train_loader = config_hy['train_loader']
val_loader = config_hy['val_loader']
no_label_loader = config_hy['no_label_loader']
导入包,定义函数,并把字典里面的东西取出来。
model = model.to(device)
semi_loader = None # 初始化
# 分别记录每次loss
plt_train_loss = []
plt_val_loss = []
# 分别记录每次准确率
plt_train_acc = []
plt_val_acc = []
# 记录最大的acc
max_acc = 0.0
初始化变量。整体函数和之前写全连接的数据模型差不多(找6,训练验证函数)。增加一个记录准确率的列表,就是看预测正确多少个。
记录最大总的准确值max(正确样本计数),用来在后面保存最好的模型。(不除以数据个数)验证准确率>最大准确率时保存模型
4.2训练
for epoch in range(epochs):
train_loss = 0.0
val_loss = 0.0
train_acc = 0.0
val_acc = 0.0 # 预测成功的数量记录,预测成功就是1,失败就是0
semi_loss = 0.0
semi_acc = 0.0
start_time = time.time()
先还是初始化这些内容。用来对每一个epoch的训练结果进行统计。
除了记录
# 模型进入训练模式
model.train()
for batch_x, batch_y in train_loader:
# 先把数据放到设备上
x, target = batch_x.to(device), batch_y.to(device)
pred_y = model(x)
train_batch_loss = loss(pred_y, target)
train_batch_loss.backward()
optimizer.step() # 更新模型,梯度回传
optimizer.zero_grad() # 模型的梯度清零
# train_batch_loss是放在gpu上的,要把它放到cpu上,然后取出其数值item
train_loss += train_batch_loss.cpu().item()
train_acc += np.sum(np.argmax(pred_y.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
# train_loss是一个epoch里面所有批次的总loss,因此要除以批次的数
plt_train_loss.append(train_loss / train_loader.__len__())
plt_train_acc.append(train_acc / train_loader.dataset.__len__()) # 准确率绘制,对于一整个数据集来说的
每个批次开始前调用optimizer.zero_grad(),确保梯度仅反映当前批次的信息。这是为了防止当前批次的梯度与之前批次的梯度累积,导致参数更新错误。例如,在PyTorch中,每次调用loss.backward()后,梯度会累积到张量的.grad属性中。若不手动清零,后续批次的梯度会与之前的相加,导致模型无法正确学习当前批次的特征。
训练模型,先要调用train_loader的len方法读取总数据长度(个数),然后每一个循环调用getitem读取具体数据,返回给batch_x和batch_y。然后就是经典的放上gpu,经过模型,求每一批次的train_bat_loss,每一批次进行一次反向,并优化器更新模型,本批次梯度清零。
记录loss,item是PyTorch中Tensor类的一个方法,用于从只包含一个元素的张量中提取这个元素的值。
然后这个准确值计数器要在什么时候增加呢,就是当预测的和真值类别相同时候才会增加,pred_y.detach().cpu().numpy(), axis=1表示对预测值先进行分离操作避免带着梯度,然后放到cpu上,因为训练在显卡上,再转为数组,1轴就是竖着对每一个样本进行一次这样的操作。np.argmax表示取指定轴的最大值的索引,也就是预测最有可能的类别索引,如果这个值==真值的索引,那么就增加,==取值为true时就是1,也就是计数器加一。当然每次比较的内容是一个batch的。
将本次epoch的loss放入列表,这里除以的是trainloader的长度,即一个epoch中批次数(batch size)的总和。这样做是为了计算整个训练集上的平均损失。因为是将每一批次的平均值loss (train_bat_loss)累计在train_loss中,所以一轮结束后除以批次数总和可以得到平均每个样本的损失。
再将本次的准确率放入列表,准确率除的是数据集的样本数量,因为准确率是对于每一个图片预测正确与否而言的。train_acc累积的是每个批次内预测对的样本数量,结果即是一轮内预测对的样本数量。
train_loss和train_acc用于衡量本轮次的训练好坏,并不参与梯度下降模型更新
if semi_loader:
...
先不看semi的部分,先看val部分(semi数据处理后面讲)
# 将模型调为验证模式
model.eval()
with torch.no_grad():
for batch_x, batch_y in val_loader:
# 先把数据放到设备上
x, target = batch_x.to(device), batch_y.to(device)
pred_y = model(x)
val_batch_loss = loss(pred_y, target)
val_loss += val_batch_loss.cpu().item()
# 求和,求满足条件的
# pred.detach().cpu().numpy()取下转化为矩阵
# np.argmax沿着横着x轴求最大值的下标,其中最大值表示模型预测结果是哪种类型的食物
# 看预测的类型和实际真值是否一样,一样就加和
val_acc += np.sum(np.argmax(pred_y.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
plt_val_loss.append(val_loss / val_loader.__len__())
plt_val_acc.append(val_acc / val_loader.dataset.__len__()) # 准确率绘制,对于一整个数据集来说的
验证过程和训练的差不多,但不需要更新模型,不需要累积梯度
if semi_loader:
for batch_x, batch_y in semi_loader:
# 先把数据放到设备上
x, target = batch_x.to(device), batch_y.to(device)
pred_y = model(x)
semi_batch_loss = loss(pred_y, target)
semi_batch_loss.backward()
optimizer.step() # 更新模型,梯度回传
optimizer.zero_grad() # 模型的梯度清零
semi_loss += semi_batch_loss.cpu().item()
semi_acc += np.sum(np.argmax(pred_y.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
print("半监督准确率为%.6f" % (semi_acc / train_loader.dataset.__len__()))
再来看半监督的,
# 如果当前轮次验证准确率达到一定值0.7,引入半监督数据,测试调试的话改成0.05
# epoch % 5 == 0不要每次都取数据,浪费时间
if plt_val_acc[-1] >= 0.7 and epoch % 5 == 0:
semi_loader = get_semi_loader(no_label_loader, model, device, threshold)
# 保存最好的模型,准确度最大的模型
if val_acc > max_acc:
max_acc = val_acc
torch.save(model, save_path)
print("[%02d/%02d] %2.2f s TrainLoss:%.6f ValLoss:%.6f TrainAcc:%.6f ValAcc:%.6f" \
% (epoch, epochs, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1], plt_train_acc[-1],
plt_val_acc[-1]))
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title("loss")
plt.legend(["train", "val"])
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title("acc")
plt.legend(["train", "val"])
plt.show()
如果某一轮训练过程中模型够好的话,如最近一轮的验证准确率设为大于0.7的情况下就启用获取semiloader获取半监督数据,如果semiloader不为空即代表有达到置信值的无标签数据被获取,则对semiloader中的数据也进行一次训练,参与模型更新。并且不要每次都去看一下是否能取到,不然会增加不必要的时间消耗。
semiloader不为空则参与训练:如下代码块
if semi_loader!= None:
for batch_x, batch_y in semi_loader:
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
semi_bat_loss = loss(pred, target)
semi_bat_loss.backward()
optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度
optimizer.zero_grad()
semi_loss += semi_bat_loss.cpu().item()
semi_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
print("半监督数据集的训练准确率为", semi_acc/semi_loader.dataset.__len__())
在第五轮时验证准确率达到61%,所以获取semiloader,并在第六轮用到了获取的无标签数据进行了训练预测,半监督数据集的准确率 达到83%
最后保存最好的模型,并输出画图。
半监督学习
1.一部分数据有标签,另一部分无标签。为了节约数据,对于无标签的数据通过模型进行预测,当预测后的置信度高于0.9时,用预测值当作真实标签,将无标签数据归入有标签数据进行训练。
2.流程,
①经过一定轮次训练后,在验证时,得到模型准确率大于0.6时(模型准确率足够高才可进行半监督训练):
②则取无标签数据X经过模型,得到predY和acc,若该X对应的标签的“概率"(soft(predY))大于一定的数,则该图片有效,将X和predY一起加入新的数据集semi_data ,只存放半监督打上标签的数据。
③如果取得了半监督学习后的带标签图片,则这也图片也可以继续用于训练。
3.具体实现:自己训练的11类,准确率只有0.09(图片数量少,和随机猜测差不多了)。使用了resnet18后,准确率到达了0.6。
5,半监督数据处理
放data文件。
为将没有标签的数据也能在训练中使用上,不浪费数据。
def get_semi_loader(no_label_loader, model, device, threshold):
# 获取半监督数据集
semi = semi_dataset(no_label_loader, model, device, threshold)
if not semi.flag:
# 如果是空的,训练还达不到水平
return None
else:
return DataLoader(semi, batch_size=8, shuffle=True)
接上面准确率大于0.7那里,调用的函数,获取半监督数据集,如果没有成功获取,就返回空,这个后面说,否则就返回一个dataloader包装的数据集叫做semi_loader.
然后实例化一个semi数据集,semi_dataset用于存放打上标签的半监督数据。
进入init
class semi_dataset(Dataset):
def __init__(self, no_label_loader, model, device, threshold=0.99):
x, y = self.get_label(no_label_loader, model, device, threshold)
if not x:
self.flag = False
else:
self.flag = True
self.xarr = np.array(x)
self.yt = torch.LongTensor(y)
self.transform = train_transforms
先调用自身的get_label方法,如果没获取到x的数据就使得flag为假,返回到get_semi_loader函数判断,否则数据转为数组,标签转为长整型的张量,使用训练的数据变换函数处理,调用时候通过下面的getitem方法返回批次的数据。
def get_label(self, no_label_loader, model, device, threshold=0.99):
model = model.to(device)
# 让数据通过模型
# 预测概率列表
pred_prob_list = []
# 预测标签(第几类)列表
labels_list = []
x = []
y = []
get_label方法,初始化数组。
with torch.no_grad():
for bat_x, p in no_label_loader:
bat_x = bat_x.to(device)
pred = model(bat_x) #得到预测标签
# 经过softmax得到
soft = nn.Softmax()
pred_soft = soft(pred)
# 取最大值,既能返回最大值,也能返回最大值下标,1表示横着的最大值
pred_max, pred_value = pred_soft.max(1)
# 每个批次的内容存入
pred_prob_list.extend(pred_max.cpu().numpy().tolist())
labels_list.extend(pred_value.cpu().numpy().tolist())
无标签数据no_label_dataset在semi_dataset中要通过模型预测进而打标签,这个过程对训练模型没有意义,所以不能累计梯度,取一批无标签数据,通过模型预测一个标签,经过softmax以后转化为概率输出,取预测的最大值和下标,放入列表,对应好标签。
所有数据都经过一次模型以后。
for index, prob in enumerate(pred_prob_list):
if prob >= threshold: # 调试时手动将threshold写为0.1就行
# get_item方法,这个方法返回两个参数,因此要取一个
# 取到数据集对应内容,1是原始图片
x.append(no_label_loader.dataset[index][1])
#
y.append(labels_list[index])
return x, y
判断一下计算的结果数据,如果大于设定的门限值,才能被使用,放入x中,如果预测不准确的结果也拿来用了,可能还会对训练产生反作用,这样不如不用半监督。
注:无标签数据集no_label_dataset进行预测的时候,图片不进行增广,即使用的transform与验证集一致,图片输入模型后若达到置信值则将原始的无标签图片加入semi_dataset,无标签数据集只参与训练,不参与验证和测试
def __getitem__(self, item):
return self.transforms(self.xarr[item]), self.yt[item]
def __len__(self):
return len(self.xarr)
循环调用getitem时,返回变换过的x和y标签。
总结
主文件预处理:导入所需库,设定随机种子保证可复现性,定义数据路径,划分代码结构,初始化模型、优化器、损失函数等超参数。
数据处理:通过自定义food_dataset类加载图像数据,支持训练、验证和半监督模式。对训练数据进行增强(如随机裁剪、旋转),并将数据转换为张量格式,使用DataLoader进行批次加载。
模型定义:支持自定义CNN模型或使用预训练模型(如SqueezeNet)进行迁移学习,修改全连接层输出类别数,并允许微调参数。
训练与验证:在训练过程中计算损失和准确率,支持半监督学习,当验证准确率超过阈值时,利用无标签数据生成高置信度伪标签扩充训练集。记录损失和准确率曲线,保存最佳模型。
半监督数据处理:通过模型预测无标签数据的伪标签,筛选高置信度样本加入训练集,进一步提升模型性能。
结果可视化:绘制训练和验证的损失、准确率曲线,直观展示模型性能变化。
遇到的问题
在一开始训练的时候,验证集准确率过低,等到训练至中间轮次时准确率来到了65,开启半监督,但随后的轮次验证准确率一直下降至四十左右,训练集和半监督数据集准确率却一直上升,发生了过拟合现象
解决方法:
将batchsize调整至32,lr学习率调整至0.0001
使得模型一开始验证的准确率就十分高
更多推荐



所有评论(0)