整体框架:
-
导入库
- 导入了各种必需的Python库,用于数据处理、图像读取、模型构建和训练。
-
设置随机种子
seed_everything
: 用于设置所有随机数生成器的种子,确保每次运行时的结果都是相同的。
-
图像预处理(transform)
- 对训练集和验证集的图像进行不同的预处理(如旋转、裁剪、归一化等)。
-
数据集类
food_Dataset
: 自定义的数据集类,用于加载和处理食物图像数据。semiDataset
: 半监督学习的数据集,用于处理无标签数据。
-
模型定义
myModel
: 自定义的卷积神经网络(CNN)模型,用于图像分类。
-
训练与验证
train_val
: 用于训练和验证模型,并记录训练过程中的损失和准确率。- 在训练过程中使用了半监督学习策略,动态生成带标签的样本。
-
半监督学习
- 使用无标签数据进行预测,并根据预测结果生成标签,以此进一步训练模型。
-
模型保存和加载
- 在验证过程中根据准确率动态保存最佳模型。
详细解释
1. 导入库
import random
import torch
import torch.nn as nn
import numpy as np
import os
from PIL import Image #读取图片数据
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm
from torchvision import transforms
import time
import matplotlib.pyplot as plt
from model_utils.model import initialize_model
作用: 导入各种库和模块来帮助数据处理、模型训练和图像操作。
torch
: PyTorch的核心库,用于构建神经网络和执行张量计算。PIL.Image
: 用于加载和处理图像。torch.utils.data
: 包含Dataset
和DataLoader
类,用于加载数据。torchvision.transforms
: 图像处理的常用变换(如裁剪、旋转等)。tqdm
: 用于显示进度条。matplotlib.pyplot
: 用于可视化训练过程中的损失和准确率。
2. 设置随机种子
def seed_everything(seed):
# 为 PyTorch 的 CPU 部分设置随机种子,确保在 CPU 上的随机操作结果可重复
torch.manual_seed(seed)
# 为 PyTorch 的当前 GPU 设备设置随机种子,确保在当前 GPU 上的随机操作结果可重复
torch.cuda.manual_seed(seed)
# 为 PyTorch 的所有可用 GPU 设备设置随机种子,确保在所有 GPU 上的随机操作结果可重复
torch.cuda.manual_seed_all(seed)
# 关闭 cuDNN 的自动寻找最优算法功能,以确保结果的确定性
torch.backends.cudnn.benchmark = False
# 启用 cuDNN 的确定性模式,使卷积操作结果可重复
torch.backends.cudnn.deterministic = True
# 为 Python 的内置 random 模块设置随机种子,确保使用 random 模块的随机操作结果可重复
random.seed(seed)
# 为 numpy 模块设置随机种子,确保使用 numpy 的随机操作结果可重复
np.random.seed(seed)
# 为 Python 的哈希函数设置随机种子,确保哈希操作结果可重复
os.environ['PYTHONHASHSEED'] = str(seed)
作用: 通过设置随机种子,确保程序在每次运行时都能得到相同的结果。这对于调试和复现实验结果非常重要。
以下是对确保深度学习实验结果可重复的更通俗解释:
一、开发和调试方面:
- 当你修改代码时,你想知道是修改的部分起了作用,还是因为运气好或不好(随机因素)让结果变了。如果结果不可重复,你就搞不清是自己改代码的原因,还是像抛硬币一样的随机因素造成的,这会让你很难找到代码中的错误,也很难确定修改代码是不是真的有效果。
二、科研方面:
- 如果你在写论文,其他研究人员需要重复你的实验,看看你说的对不对。要是结果不能重复,别人就会觉得你的结果不靠谱,而且在比较不同模型好坏时,也不知道是模型本身的差异,还是因为随机因素导致的结果不同。
三、实际应用方面:
- 在一些重要领域,像医疗和金融,模型得稳定可靠。如果模型一会儿一个样,会做出错误的诊断或决策,所以要保证结果可重复,这样才能放心使用。
四、资源使用方面:
- 知道实验可重复,你就能更准确地计算需要多少计算资源和时间,不然每次结果不一样,你就不知道要准备多少资源,得反复实验,浪费时间和资源。
简单来说,保证结果可重复就是让深度学习实验和应用更靠谱,让你能更好地开发、做研究和实际使用,避免结果受随机因素影响而变得混乱,保证在相同环境下得到相同的结果。
3. 图像预处理(transform)
# 定义训练集的图像变换组合
train_transform = transforms.Compose(
[
# 将输入的数据转换为 PIL 图像,因为有些后续的图像变换操作要求输入是 PIL 图像格式
transforms.ToPILImage(),
# 对图像进行随机裁剪和缩放,将其裁剪为 224x224 大小,这有助于增强模型的泛化能力,防止过拟合
transforms.RandomResizedCrop(224),
# 对图像进行随机旋转,旋转角度在 -50 度到 50 度之间,进一步增强数据的多样性
transforms.RandomRotation(50),
# 将变换后的 PIL 图像转换为 PyTorch 的张量(Tensor),并将像素值归一化到 [0, 1] 范围
transforms.ToTensor()
]
)
# 定义验证集的图像变换组合
val_transform = transforms.Compose(
[
# 将输入的数据转换为 PIL 图像
transforms.ToPILImage(),
# 将变换后的 PIL 图像转换为 PyTorch 的张量(Tensor),并将像素值归一化到 [0, 1] 范围
transforms.ToTensor()
]
)
作用:
train_transform
:应用于训练集的图像预处理步骤,包括随机裁剪、旋转(图片增广)和转化为Tensor。val_transform
:应用于验证集的图像预处理步骤,只包括转换为Tensor。(验证集不需要做图片增广,直接224,224,3模型转为3,224,224模型再转化为张量Tensor即可)
图片增广(Image Augmentation)是一种在深度学习中广泛使用的技术,旨在通过对原始图像进行一系列随机变换,生成更多样化的训练样本,以提高模型的泛化能力,避免过拟合。
1.1. food_Dataset类定义和初始化
class food_Dataset(Dataset):
def __init__(self, path, mode="train"):
# 存储数据集的模式("train", "val", "semi")
self.mode = mode
# 如果是半监督模式,只读取数据,不读取标签
if mode == "semi":
self.X = self.read_file(path)
# 其他模式下,读取数据和标签,并将标签转换为 PyTorch 的 LongTensor 类型
else:
self.X, self.Y = self.read_file(path)
self.Y = torch.LongTensor(self.Y)
# 根据模式选择不同的图像变换操作
if mode == "train":
self.transform = train_transform
else:
self.transform = val_transform
作用:
food_Dataset
继承自Dataset
,这是PyTorch中自定义数据集的标准做法。- 初始化函数
__init__
接受两个参数:path
: 数据所在的路径。mode
: 数据加载模式。它的值可以是"train"
,"val"
, 或"semi"
。分别代表训练模式、验证模式和半监督学习模式。
① 如果是半监督学习模式,只需要读取数据,而不需要读取标签,我们需要通过val_transform(x),来预测数据标签,若预测值大于某一规定阈值时,加入semi_dataeset来当作训练集。
② 如果是训练模式,我们就需要把数据、标签一起传入,并进行train_transform图片增广操作。
③ 如果是验证模式,我们就也要把数据与标签传入,来验证我们的训练模型所进行的分类的预测值是否与真实值相同。
def read_file(self, path):
# 检查当前模式是否为半监督模式
if self.mode == "semi":
# 获取指定路径下的文件列表
file_list = os.listdir(path)
# 初始化一个 numpy 数组,用于存储图像数据,形状为 (文件数量, HW, HW, 3),数据类型为 uint8
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
# 遍历文件列表 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
# 拼接文件的完整路径
img_path = os.path.join(path, img_name)
# 打开图像文件
img = Image.open(img_path)
# 将图像调整为 (HW, HW) 的大小
img = img.resize((HW, HW))
# 将图像数据存储到 numpy 数组中
xi[j,...] = img
print("读到了%d个数据" % len(xi))
# 返回存储图像数据的 numpy 数组
return xi
else:
# 初始化存储图像数据和标签的 numpy 数组
X = None
Y = None
# 遍历 11 个类别文件夹(假设类别编号为 00 到 10)
for i in tqdm(range(11)):
# 拼接类别文件夹的完整路径
file_dir = path + "/%02d" % i
# 获取类别文件夹下的文件列表
file_list = os.listdir(file_dir)
# 初始化一个 numpy 数组,用于存储图像数据,形状为 (文件数量, HW, HW, 3),数据类型为 uint8
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
# 初始化一个 numpy 数组,用于存储图像的标签,长度为文件数量,数据类型为 uint8
yi = np.zeros(len(file_list), dtype=np.uint8)
# 遍历文件列表
for j, img_name in enumerate(file_list):
# 拼接文件的完整路径
img_path = os.path.join(file_dir, img_name)
# 打开图像文件
img = Image.open(img_path)
# 将图像调整为 (HW, HW) 的大小
img = img.resize((HW, HW))
# 将图像数据存储到 numpy 数组中
xi[j,...] = img
# 将类别标签存储到标签数组中
yi[j] = i
# 对于第一个类别,初始化存储图像数据和标签的数组
if i == 0:
X = xi
Y = yi
# 对于其他类别,将新的数据和标签数组与已有的数组拼接
else:
X = np.concatenate((X, xi), axis=0)
Y = np.concatenate((Y, yi), axis=0)
print("读到了%d个数据" % len(Y))
# 返回存储图像数据和标签的 numpy 数组
return X, Y
作用:
- 该函数是
food_Dataset
类中的一个方法,用于从文件系统中读取图像数据和相应的标签。 - 首先,根据
self.mode
判断是半监督模式还是其他模式(通常是监督学习模式)。- 半监督模式:
file_list = os.listdir(path)
:使用os.listdir
列出path
目录下的所有文件,存储在file_list
中。xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
:创建一个形状为(len(file_list), HW, HW, 3)
的零数组,用于存储图像数据。HW
是图像的高度和宽度,3
表示图像是 RGB 三通道的,dtype=np.uint8
表示数据类型是无符号 8 位整数(范围是 0 到 255)。for j, img_name in enumerate(file_list):
:遍历文件列表,对于每个文件:img_path = os.path.join(path, img_name)
:使用os.path.join
构建完整的文件路径。img = Image.open(img_path)
:使用PIL
的Image.open
打开图像文件。img = img.resize((HW, HW))
:将图像大小调整为(HW, HW)
。xi[j,...] = img
:将调整大小后的图像存储在xi
数组中。
- 最后返回存储图像数据的
xi
数组。
- 其他模式(监督学习):
for i in tqdm(range(11)):
:使用tqdm
显示进度条,假设数据分为 11 个类别,类别编号从00
到10
。file_dir = path + "/%02d" % i
:构建类别文件夹的路径。file_list = os.listdir(file_dir)
:列出类别文件夹下的文件列表。xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
:创建一个形状为(len(file_list), HW, HW, 3)
的零数组,用于存储图像数据。yi = np.zeros(len(file_list), dtype=np.uint8)
:创建一个长度为文件数量的零数组,用于存储标签。for j, img_name in enumerate(file_list):
:遍历文件列表,对于每个文件:img_path = os.path.join(file_dir, img_name)
:构建文件的完整路径。img = Image.open(img_path)
:打开图像文件。img = img.resize((HW, HW))
:调整图像大小。xi[j,...] = img
:存储图像数据。yi[j] = i
:存储图像的类别标签,标签为当前的类别编号i
。
- 对于第一个类别,初始化存储图像数据和标签的数组
X
和Y
;对于其他类别,使用np.concatenate
沿轴0
将新的数据和标签数组与已有的数组拼接,最终返回存储图像数据的X
数组和存储标签的Y
数组。
- 半监督模式:
1、此函数根据模式的不同,处理不同的数据读取任务。对于半监督模式,只读取图像数据;对于监督学习模式,读取图像数据并根据类别存储标签,同时使用 tqdm 显示进度,方便观察数据读取进度。数据存储在 numpy 数组中,图像被调整为统一的大小,方便后续处理和输入到深度学习模型中。
2、os.path.join 是 Python 的 os 模块中的一个函数,它将多个路径组合成一个完整的路径,自动处理路径分隔符,确保生成的路径在不同操作系统(Windows、Linux 等)上都是正确的。通过 os.path.join(path, img_name) 得到完整的图像文件路径,以便 Image.open 函数可以正确打开图像文件。例如:如果你想使用 PIL 的 Image.open 函数打开图像文件,只使用 image1.jpg 是不够的,因为 Python 不知道这个文件在哪里。你需要完整的路径,像 /data/images/00/image1.jpg。
3、这里的拼接是将存储不同类别图像数据的 numpy 数组和存储不同类别标签的 numpy 数组按类别顺序依次连接在一起,形成一个大的数组,方便后续将整个数据集作为一个整体输入到深度学习模型中进行训练。这种操作有助于将来自不同类别文件夹的分散数据整合为一个统一的训练集,而不是将图片本身拼接成一个大图片。这样,模型可以同时处理来自不同类别、不同文件夹的图像数据和相应的标签,实现多类别分类任务。 例如:相当于每次都是用小篮子读,然后第一个小篮子当作大篮子,后面的小篮子读了都放进第一个大篮子里。
1.2. food_Dataset类获取单个数据项
def __getitem__(self, item):
if self.mode == "semi":
return self.transform(self.X[item]), self.X[item]
else:
return self.transform(self.X[item]), self.Y[item]
作用:
__getitem__
方法是PyTorchDataset
类的一个特殊方法,用于返回一个样本。- 如果是
"semi"
模式,它只返回图像数据,并对图像进行预处理(通过transform
),但不返回标签。 - 否则,返回图像数据和对应的标签,标签已经在
__init__
中通过torch.LongTensor
转换为PyTorch的张量。
1.3. food_Dataset类获取数据项长度
def __len__(self):
return len(self.X)
作用:
__len__
方法返回数据集的大小,即样本数量。对于food_Dataset
类来说,返回的是X
的长度(即图像数量)。
2.1. semiDataset类定义和初始化:
class semiDataset(Dataset):
def __init__(self, no_label_loder, model, device, thres=0.99):
# 调用 get_label 方法,根据阈值 thres 为无标签数据分配伪标签
x, y = self.get_label(no_label_loder, model, device, thres)
if x == []:
# 如果没有数据被分配伪标签,则标记 flag 为 False
self.flag = False
else:
# 如果有数据被分配伪标签,则标记 flag 为 True
self.flag = True
self.X = np.array(x)
self.Y = torch.LongTensor(y)
# 使用训练集的图像变换操作
self.transform = train_transform
作用:
class semiDataset(Dataset)
:定义了一个名为semiDataset
的类,它继承自 PyTorch 的Dataset
类,用于表示半监督学习中的数据集。def __init__(self, no_label_loder, model, device, thres=0.99)
:no_label_loder
:无标签数据的数据加载器。model
:用于预测无标签数据的模型。device
:计算设备(CPU 或 GPU)。thres
:阈值,用于决定是否将无标签数据作为有标签数据使用,默认为 0.99。x, y = self.get_label(no_label_loder, model, device, thres)
:调用get_label
方法为无标签数据分配伪标签。- 根据
x
是否为空,设置self.flag
的值,以标记是否存在可作为有标签数据使用的无标签数据。 - 如果存在,将
x
和y
转换为numpy
数组和torch.LongTensor
类型,并使用train_transform
进行图像变换。
2.2. get_label
方法:
def get_label(self, no_label_loder, model, device, thres):
# 将模型移动到指定的设备(CPU 或 GPU)上
model = model.to(device)
pred_prob = []
labels = []
x = []
y = []
# 创建 Softmax 层
soft = nn.Softmax()
# 不计算梯度,仅进行前向传播
with torch.no_grad():
# 遍历无标签数据加载器
for bat_x, _ in no_label_loder:
# 将批次数据移动到指定设备
bat_x = bat_x.to(device)
# 使用模型进行预测
pred = model(bat_x)
# 对预测结果应用 Softmax 操作,得到概率分布
pred_soft = soft(pred)
# 获取每个样本的最大概率及其对应的类别
pred_max, pred_value = pred_soft.max(1)
# 将最大概率添加到 pred_prob 列表中
pred_prob.extend(pred_max.cpu().numpy().tolist())
# 将预测类别添加到 labels 列表中
labels.extend(pred_value.cpu().numpy().tolist())
# 根据阈值 thres 为无标签数据分配伪标签
for index, prob in enumerate(pred_prob):
if prob > thres:
# 将满足阈值的数据添加到 x 列表中
x.append(no_label_loder.dataset[index][1])
# 将对应的预测类别添加到 y 列表中
y.append(labels[index])
return x, y
作用:
model = model.to(device)
:将模型移动到指定设备。soft = nn.Softmax()
:创建一个Softmax
层,用于将模型的输出转换为概率分布。with torch.no_grad()
:在不计算梯度的情况下进行前向传播,因为这里只是进行预测,不需要计算梯度。for bat_x, _ in no_label_loder:
:遍历无标签数据加载器,每次取出一个批次的数据bat_x
。bat_x = bat_x.to(device)
:将批次数据移动到指定设备。pred = model(bat_x)
:使用模型对批次数据进行预测。pred_soft = soft(pred)
:对预测结果应用Softmax
操作,得到概率分布。pred_max, pred_value = pred_soft.max(1)
:找出每个样本的最大概率及其对应的类别。pred_prob.extend(pred_max.cpu().numpy().tolist())
和labels.extend(pred_value.cpu().numpy().tolist())
:将最大概率和预测类别添加到相应的列表中。for index, prob in enumerate(pred_prob):
:根据阈值thres
筛选出概率大于thres
的样本,将其添加到x
和y
列表中,作为伪标签数据。
2.3. __getitem__
方法:
def __getitem__(self, item):
# 根据索引 item 获取经过变换的数据和对应的标签
return self.transform(self.X[item]), self.Y[item]
作用:
def __getitem__(self, item)
:根据索引item
获取数据集中的一个样本,对该样本应用self.transform
进行图像变换,并返回变换后的数据和对应的标签。
2.4__len__
方法:
def __len__(self):
# 返回数据集的长度
return len(self.X)
作用:
def __len__(self)
:返回数据集的长度,即self.X
的长度。
1、这个类的主要目的是利用现有的模型和阈值,为无标签数据分配伪标签,将高置信度的无标签数据视为有标签数据,以便在半监督学习中使用。
2、get_label 方法使用模型对无标签数据进行预测,并根据 Softmax 输出的概率和阈值来确定哪些数据可以作为有标签数据。
3、__getitem__ 方法用于根据索引获取样本,而 __len__ 方法用于获取数据集的长度,这两个方法是继承 Dataset 类所需要实现的,以便与 DataLoader 一起使用,实现数据的批量加载和迭代。4、这个类在半监督学习中非常有用,它可以帮助你将无标签数据转化为有标签数据,从而利用更多的数据来训练模型,提高模型的性能和泛化能力。
3.1. get_semi_loader类:
def get_semi_loader(no_label_loder, model, device, thres):
# 创建 semiDataset 实例,该实例会根据传入的模型、设备和阈值为无标签数据分配伪标签
semiset = semiDataset(no_label_loder, model, device, thres)
# 检查 semiDataset 实例的 flag 属性,如果为 False,表示没有为无标签数据分配到伪标签
if semiset.flag == False:
# 没有伪标签数据,返回 None
return None
else:
# 创建一个 DataLoader 实例,用于批量加载经过 semiDataset 处理的数据
semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
# 返回创建的 DataLoader 实例
return semi_loader
代码解释:
-
函数定义:
def get_semi_loader(no_label_loder, model, device, thres)
:定义了一个名为get_semi_loader
的函数,接收四个参数:no_label_loder
:无标签数据的数据加载器。model
:用于预测无标签数据的模型。device
:计算设备(如'cuda'
表示 GPU,'cpu'
表示 CPU)。thres
:用于确定是否将无标签数据作为有标签数据的阈值。
-
创建
semiDataset
实例:semiset = semiDataset(no_label_loder, model, device, thres)
:- 使用传入的
no_label_loder
、model
、device
和thres
创建一个semiDataset
实例semiset
。 - 在
semiDataset
的__init__
方法中,会调用get_label
方法,利用model
对no_label_loder
中的无标签数据进行预测,根据thres
为部分数据分配伪标签。
- 使用传入的
-
检查是否有伪标签数据:
if semiset.flag == False
:semiset.flag
是在semiDataset
的__init__
方法中设置的属性,用于标记是否有为无标签数据分配到伪标签。- 如果
semiset.flag
为False
,说明没有无标签数据的预测概率超过thres
,即没有数据被分配为伪标签。
-
创建
DataLoader
实例:semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
:- 如果
semiset.flag
为True
,表示有数据被分配了伪标签,创建一个DataLoader
实例semi_loader
。 DataLoader
是 PyTorch 中用于批量加载数据的工具,这里将semiset
作为数据源,设置batch_size
为 16,shuffle
为False
,表示每个批次包含 16 个样本,且不打乱数据顺序。
- 如果
-
返回结果:
return semi_loader
或return None
:- 如果有伪标签数据,返回
DataLoader
实例semi_loader
,以便后续在训练过程中使用该数据加载器进行批量加载数据。 - 如果没有伪标签数据,返回
None
,表示没有可用的半监督数据。
- 如果有伪标签数据,返回
1、这个函数的主要目的是将无标签数据通过 semiDataset 类进行处理,根据模型预测结果和阈值为部分无标签数据分配伪标签,并将有伪标签的数据封装到 DataLoader 中,方便后续的批量加载和训练。如果没有满足条件的数据,函数将返回 None。
2、这个函数在半监督学习中非常有用,它允许你在训练过程中动态地将部分无标签数据转换为有标签数据,并以批处理的方式进行加载,提高了数据的利用率,有助于提高模型的性能和泛化能力。
3.模型部分
class myModel(nn.Module):
def __init__(self, num_class):
super(myModel, self).__init__()
# 输入通道数为 3(RGB 图像),输出通道数为 64,卷积核大小为 3x3,步长为 1,填充为 1
# 输入图像尺寸为 3x224x224,输出特征图尺寸为 64x224x224
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1)
# 对卷积层输出进行批归一化,有助于加快收敛速度,稳定训练
self.bn1 = nn.BatchNorm2d(64)
# 激活函数 ReLU,增加非线性,帮助网络学习复杂模式
self.relu = nn.ReLU()
# 最大池化层,池化核大小为 2x2,将特征图尺寸减半,输出尺寸为 64x112x112
self.pool1 = nn.MaxPool2d(2)
# 第一个卷积块,包含卷积、批归一化、ReLU 激活和最大池化
self.layer1 = nn.Sequential(
# 输入通道数为 64,输出通道数为 128,卷积核大小为 3x3,步长为 1,填充为 1
# 输入尺寸为 64x112x112,输出尺寸为 128x112x112
nn.Conv2d(64, 128, 3, 1, 1),
# 批归一化,输出尺寸为 128x112x112
nn.BatchNorm2d(128),
# ReLU 激活,输出尺寸为 128x112x112
nn.ReLU(),
# 最大池化,池化核大小为 2x2,输出尺寸为 128x56x56
nn.MaxPool2d(2)
)
# 第二个卷积块,包含卷积、批归一化、ReLU 激活和最大池化
self.layer2 = nn.Sequential(
# 输入通道数为 128,输出通道数为 256,卷积核大小为 3x3,步长为 1,填充为 1
# 输入尺寸为 128x56x56,输出尺寸为 256x56x56
nn.Conv2d(128, 256, 3, 1, 1),
# 批归一化,输出尺寸为 256x56x56
nn.BatchNorm2d(256),
# ReLU 激活,输出尺寸为 256x56x56
nn.ReLU(),
# 最大池化,池化核大小为 2x2,输出尺寸为 256x28x28
nn.MaxPool2d(2)
)
# 第三个卷积块,包含卷积、批归一化、ReLU 激活和最大池化
self.layer3 = nn.Sequential(
# 输入通道数为 256,输出通道数为 512,卷积核大小为 3x3,步长为 1,填充为 1
# 输入尺寸为 256x28x28,输出尺寸为 512x28x28
nn.Conv2d(256, 512, 3, 1, 1),
# 批归一化,输出尺寸为 512x28x28
nn.BatchNorm2d(512),
# ReLU 激活,输出尺寸为 512x28x28
nn.ReLU(),
# 最大池化,池化核大小为 2x2,输出尺寸为 512x14x14
nn.MaxPool2d(2)
)
# 最大池化层,池化核大小为 2x2,将特征图尺寸减半,输出尺寸为 512x7x7
self.pool2 = nn.MaxPool2d(2)
# 第一个全连接层,输入维度为 25088(512*7*7),输出维度为 1000
self.fc1 = nn.Linear(25088, 1000)
# 激活函数 ReLU
self.relu2 = nn.ReLU()
# 第二个全连接层,输入维度为 1000,输出维度为 num_class,用于最终的分类
self.fc2 = nn.Linear(1000, num_class)
def forward(self, x):
# 前向传播过程
# 输入 x 通过第一个卷积层
x = self.conv1(x)
# 对卷积结果进行批归一化
x = self.bn1(x)
# 使用 ReLU 激活函数
x = self.relu(x)
# 进行最大池化
x = self.pool1(x)
# 通过第一个卷积块
x = self.layer1(x)
# 通过第二个卷积块
x = self.layer2(x)
# 通过第三个卷积块
x = self.layer3(x)
# 进行最大池化
x = self.pool2(x)
# 将特征图展平,以便输入全连接层
x = x.view(x.size()[0], -1)
# 通过第一个全连接层
x = self.fc1(x)
# 使用 ReLU 激活函数
x = self.relu2(x)
# 通过第二个全连接层,得到最终的分类结果
x = self.fc2(x)
return x
代码解释:
-
整体功能:
- 这是一个自定义的卷积神经网络(CNN)模型类
myModel
,继承自nn.Module
,用于图像分类任务。它包含了多个卷积层、池化层、批归一化层和全连接层,将输入的图像数据通过一系列的卷积和池化操作提取特征,最终通过全连接层将特征映射到不同的类别上。
- 这是一个自定义的卷积神经网络(CNN)模型类
-
__init__
方法:- 首先,定义了多个卷积层和池化层,以及批归一化层和激活函数。
- 卷积层通过
nn.Conv2d
进行定义,不同的卷积层通过nn.Sequential
组合在一起形成卷积块,每个卷积块包含卷积、批归一化、ReLU 激活和池化操作,以逐步提取图像的特征,同时减少特征图的尺寸。 - 最后使用
nn.Linear
定义全连接层,将提取的特征映射到最终的类别数量上。
-
forward
方法:- 定义了模型的前向传播过程。
- 输入数据
x
依次通过各个卷积层、池化层和激活函数,逐步提取特征。 - 最后将特征图展平(
x = x.view(x.size()[0], -1)
),通过全连接层进行分类。
这个模型是一个典型的卷积神经网络结构,通过多个卷积块和池化层提取特征,然后使用全连接层进行分类。这种结构在图像分类任务中非常常见,可以通过训练学习图像的特征,最终将输入图像分类到不同的类别中。不同的卷积层和池化层组合可以学习到不同层次的特征,从低层次的边缘、纹理特征到高层次的语义特征。
4.模型训练及验证
import torch
import time
import numpy as np
import matplotlib.pyplot as plt
def train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path):
# 将模型移动到指定的设备(GPU 或 CPU)上
model = model.to(device)
# 初始化为 None 的半监督数据加载器
semi_loader = None
# 存储训练损失的列表
plt_train_loss = []
# 存储验证损失的列表
plt_val_loss = []
# 存储训练准确率的列表
plt_train_acc = []
# 存储验证准确率的列表
plt_val_acc = []
# 存储最大的验证准确率
max_acc = 0.0
for epoch in range(epochs):
# 初始化每个 epoch 的训练损失、验证损失、训练准确率、验证准确率、半监督损失和半监督准确率
train_loss = 0.0
val_loss = 0.0
train_acc = 0.0
val_acc = 0.0
semi_loss = 0.0
semi_acc = 0.0
# 记录每个 epoch 的开始时间
start_time = time.time()
# 将模型设置为训练模式
model.train()
# 遍历训练数据加载器
for batch_x, batch_y in train_loader:
# 将数据和标签移动到指定设备上
x, target = batch_x.to(device), batch_y.to(device)
# 前向传播,得到预测结果
pred = model(x)
# 计算损失
train_bat_loss = loss(pred, target)
# 反向传播,计算梯度
train_bat_loss.backward()
# 更新模型参数
optimizer.step()
# 梯度清零,避免梯度累积
optimizer.zero_grad()
# 累加训练损失
train_loss += train_bat_loss.cpu().item()
# 计算训练准确率并累加
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
# 将当前 epoch 的平均训练损失添加到列表中
plt_train_loss.append(train_loss / train_loader.__len__())
# 将当前 epoch 的平均训练准确率添加到列表中
plt_train_acc.append(train_acc/train_loader.dataset.__len__())
# 如果存在半监督数据加载器,进行半监督训练
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 += train_bat_loss.cpu().item()
semi_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
print("半监督数据集的训练准确率为", semi_acc/train_loader.dataset.__len__())
# 将模型设置为评估模式
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 = model(x)
val_bat_loss = loss(pred, target)
val_loss += val_bat_loss.cpu().item()
val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
# 将当前 epoch 的平均验证损失添加到列表中
plt_val_loss.append(val_loss / val_loader.dataset.__len__())
# 将当前 epoch 的平均验证准确率添加到列表中
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
# 每 3 个 epoch 且验证准确率大于 0.6 时,尝试获取半监督数据加载器
if epoch % 3 == 0 and plt_val_acc[-1] > 0.6:
semi_loader = get_semi_loader(no_label_loader, model, device, thres)
# 如果当前验证准确率高于最大验证准确率,保存模型
if val_acc > max_acc:
torch.save(model, save_path)
max_acc = val_loss
# 打印当前 epoch 的训练结果
print('[%03d/%03d] %2.2f sec(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()
代码解释:
-
函数定义和初始化:
def train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path)
:model
:要训练的神经网络模型。train_loader
:训练数据的数据加载器。val_loader
:验证数据的数据加载器。no_label_loader
:无标签数据的数据加载器。device
:计算设备(如'cuda'
表示 GPU,'cpu'
表示 CPU)。epochs
:训练的轮数。optimizer
:优化器,用于更新模型参数。loss
:损失函数。thres
:用于半监督学习的阈值。save_path
:保存模型的路径。
-
模型准备和变量初始化:
model = model.to(device)
:将模型移动到指定设备。- 初始化各种存储训练和验证指标的列表(
plt_train_loss
、plt_val_loss
、plt_train_acc
、plt_val_acc
)以及max_acc
。
-
训练过程:
for epoch in range(epochs)
:- 对于每个
epoch
,初始化该epoch
的训练和验证指标(train_loss
、val_loss
、train_acc
、val_acc
等)。 model.train()
:将模型设置为训练模式,启用梯度计算。- 遍历
train_loader
:- 将数据和标签移动到
device
。 - 前向传播得到预测结果
pred
。 - 计算损失
train_bat_loss
。 - 反向传播计算梯度。
- 使用
optimizer
更新模型参数。 - 梯度清零。
- 累加训练损失和准确率。
- 将数据和标签移动到
- 对于每个
-
半监督训练(如果有):
- 如果
semi_loader
不为None
,遍历semi_loader
:- 类似训练数据的处理,计算半监督数据的损失,更新模型参数,并计算半监督准确率。
- 如果
-
验证过程:
model.eval()
:将模型设置为评估模式,关闭梯度计算。- 遍历
val_loader
:- 计算验证损失和准确率,不更新模型参数。
-
半监督数据加载器更新和模型保存:
- 每 3 个
epoch
且验证准确率大于 0.6 时,使用get_semi_loader
尝试更新semi_loader
。 - 如果当前验证准确率大于
max_acc
,保存模型。
- 每 3 个
-
结果打印和可视化:
- 打印每个
epoch
的训练和验证结果。 - 使用
matplotlib
绘制训练和验证的损失和准确率曲线。
- 打印每个
1、这个函数实现了一个完整的训练和验证流程,包括标准的监督训练、半监督训练(如果有)和模型评估。
2、它会在训练过程中根据验证准确率更新半监督数据加载器,动态调整半监督学习的数据。
3、它会保存表现最好的模型,并可视化训练和验证的损失和准确率曲线,以便监控训练过程和模型性能。
5.整体训练
# 定义训练数据、验证数据和无标签数据的路径
train_path = r"E:\food_classification\food-11_sample\training\labeled"
val_path = r"E:\food_classification\food-11_sample\validation"
no_label_path = r"E:\food_classification\food-11_sample\training\unlabeled\00"
# 创建训练数据集,模式为 "train"
train_set = food_Dataset(train_path, "train")
# 创建验证数据集,模式为 "val"
val_set = food_Dataset(val_path, "val")
# 创建无标签数据集,模式为 "semi"
no_label_set = food_Dataset(no_label_path, "semi")
# 创建训练数据加载器,批次大小为 16,打乱数据顺序
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
# 创建验证数据加载器,批次大小为 16,打乱数据顺序
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)
# 创建无标签数据加载器,批次大小为 16,不打乱数据顺序
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)
# 创建自定义的模型实例,类别数为 11
# model = myModel(11)
# 或者使用预训练的 VGG 模型并初始化,类别数为 11
model, _ = initialize_model("vgg", 11, use_pretrained=True)
# 定义学习率
lr = 0.001
# 定义交叉熵损失函数
loss = nn.CrossEntropyLoss()
# 定义 AdamW 优化器,使用模型参数,学习率为 lr,权重衰减为 1e-4
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
# 检查是否有可用的 GPU,如果有则使用 GPU,否则使用 CPU
device = "cuda" if torch.cuda.is_available() else "cpu"
# 定义保存模型的路径
save_path = "model_save/best_model.pth"
# 定义训练的轮数
epochs = 15
# 定义半监督学习的阈值
thres = 0.99
# 调用 train_val 函数进行训练和验证,传入模型、数据加载器、设备、训练轮数、优化器、损失函数、阈值和保存路径
train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path)
代码解释:
- 数据准备:
- 首先,定义了训练数据、验证数据和无标签数据的存储路径。
- 然后,使用自定义的
food_Dataset
类创建了相应的数据集实例,分别为train_set
、val_set
和no_label_set
,并为它们设置了不同的模式("train"
、"val"
和"semi"
)。 - 接着,使用
DataLoader
为每个数据集创建了数据加载器,数据加载器负责将数据分成批次,方便训练时按批次加载。对于训练集和验证集,数据会被打乱(shuffle=True
),而无标签数据集不打乱(shuffle=False
)。
- 模型创建:
- 可以选择使用自定义的
myModel
模型,也可以使用initialize_model
函数初始化一个预训练的 VGG 模型,并将类别数设置为 11。
- 可以选择使用自定义的
- 训练配置:
- 设定学习率
lr
为 0.001。 - 使用
nn.CrossEntropyLoss()
作为损失函数,适用于多分类任务。 - 使用
torch.optim.AdamW
作为优化器,对模型参数进行优化,同时设置了权重衰减以防止过拟合。 - 检查是否有可用的 GPU,如果有使用
cuda
,否则使用cpu
作为计算设备。 - 设定保存模型的路径
save_path
和训练的轮数epochs
,以及半监督学习的阈值thres
。
- 设定学习率
- 训练过程:
- 最后,调用
train_val
函数开始训练和验证,传入之前创建的模型、数据加载器、设备、优化器、损失函数等参数。
- 最后,调用
补充:迁移学习
一、概念:
- 迁移学习是一种机器学习方法,它将在一个任务(源任务)上训练好的模型应用于另一个相关任务(目标任务)。其核心思想是利用源任务中已经学到的知识,加速目标任务的学习过程或提高目标任务的性能,尤其在目标任务的数据量较少时非常有用。
二、优点:
- 节省时间和资源:避免从头开始训练模型,利用已有的模型和参数,减少了对大量标注数据的需求,加快训练速度。
- 提高性能:对于数据较少的任务,迁移已有的知识可以避免过拟合,增强模型的泛化能力,获得更好的性能。
三、常见方法:
-
微调(Fine-Tuning):
- 从一个预训练好的模型开始,通常是在大规模数据集上训练的模型,如在 ImageNet 上训练的卷积神经网络(CNN)。
- 保持模型的大部分层不变,仅调整最后几层(通常是全连接层)的参数,以适应新的任务。这是因为前面的层通常学习到了通用的特征,如边缘、纹理等,而最后几层更针对源任务的特定特征。
- 在新任务的数据上重新训练模型,允许所有(或部分)层的参数更新。
-
特征提取(Feature Extraction):
- 利用预训练模型的特征提取能力。
- 固定预训练模型的参数,将其作为一个特征提取器,只使用它的中间层输出作为新任务的输入特征。
- 新任务的模型仅使用这些提取的特征,结合自己的分类器(如新的全连接层)进行训练,通常在数据量非常少的情况下使用。
四、应用场景:
- 计算机视觉:在图像分类、目标检测、语义分割等任务中,常使用在 ImageNet 等大规模数据集上预训练的 CNN 模型,如 VGG、ResNet 等。
- 自然语言处理:利用在大规模文本数据上预训练的语言模型,如 BERT、GPT 等,进行文本分类、命名实体识别、机器翻译等任务。
五、实现步骤(以微调为例):
- 选择一个合适的预训练模型,如 VGG、ResNet 等。
- 替换预训练模型的最后几层,使其输出维度与目标任务的类别数相匹配。
- 选择合适的优化器和损失函数。
- 在目标任务的数据上进行训练,调整模型的参数。
六、注意事项:
- 源任务和目标任务越相似,迁移学习的效果越好。
- 需要根据目标任务的特点,合理调整训练策略,如选择哪些层进行微调,使用多大的学习率等。