卷积神经网络——食物分类

整体框架:

  • 导入库

    • 导入了各种必需的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: 包含DatasetDataLoader类,用于加载数据。
  • 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__方法是PyTorch Dataset类的一个特殊方法,用于返回一个样本。
  • 如果是"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

代码解释

  1. 函数定义

    • def get_semi_loader(no_label_loder, model, device, thres):定义了一个名为 get_semi_loader 的函数,接收四个参数:
      • no_label_loder:无标签数据的数据加载器。
      • model:用于预测无标签数据的模型。
      • device:计算设备(如 'cuda' 表示 GPU,'cpu' 表示 CPU)。
      • thres:用于确定是否将无标签数据作为有标签数据的阈值。
  2. 创建 semiDataset 实例

    • semiset = semiDataset(no_label_loder, model, device, thres)
      • 使用传入的 no_label_lodermodeldevice 和 thres 创建一个 semiDataset 实例 semiset
      • 在 semiDataset 的 __init__ 方法中,会调用 get_label 方法,利用 model 对 no_label_loder 中的无标签数据进行预测,根据 thres 为部分数据分配伪标签。
  3. 检查是否有伪标签数据

    • if semiset.flag == False
      • semiset.flag 是在 semiDataset 的 __init__ 方法中设置的属性,用于标记是否有为无标签数据分配到伪标签。
      • 如果 semiset.flag 为 False,说明没有无标签数据的预测概率超过 thres,即没有数据被分配为伪标签。
  4. 创建 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 个样本,且不打乱数据顺序。
  5. 返回结果

    • 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,用于图像分类任务。它包含了多个卷积层、池化层、批归一化层和全连接层,将输入的图像数据通过一系列的卷积和池化操作提取特征,最终通过全连接层将特征映射到不同的类别上。
  • __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()

代码解释

  1. 函数定义和初始化

    • 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:保存模型的路径。
  2. 模型准备和变量初始化

    • model = model.to(device):将模型移动到指定设备。
    • 初始化各种存储训练和验证指标的列表(plt_train_lossplt_val_lossplt_train_accplt_val_acc)以及 max_acc
  3. 训练过程

    • for epoch in range(epochs)
      • 对于每个 epoch,初始化该 epoch 的训练和验证指标(train_lossval_losstrain_accval_acc 等)。
      • model.train():将模型设置为训练模式,启用梯度计算。
      • 遍历 train_loader
        • 将数据和标签移动到 device
        • 前向传播得到预测结果 pred
        • 计算损失 train_bat_loss
        • 反向传播计算梯度。
        • 使用 optimizer 更新模型参数。
        • 梯度清零。
        • 累加训练损失和准确率。
  4. 半监督训练(如果有)

    • 如果 semi_loader 不为 None,遍历 semi_loader
      • 类似训练数据的处理,计算半监督数据的损失,更新模型参数,并计算半监督准确率。
  5. 验证过程

    • model.eval():将模型设置为评估模式,关闭梯度计算。
    • 遍历 val_loader
      • 计算验证损失和准确率,不更新模型参数。
  6. 半监督数据加载器更新和模型保存

    • 每 3 个 epoch 且验证准确率大于 0.6 时,使用 get_semi_loader 尝试更新 semi_loader
    • 如果当前验证准确率大于 max_acc,保存模型。
  7. 结果打印和可视化

    • 打印每个 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)

代码解释

  1. 数据准备
    • 首先,定义了训练数据、验证数据和无标签数据的存储路径。
    • 然后,使用自定义的 food_Dataset 类创建了相应的数据集实例,分别为 train_setval_set 和 no_label_set,并为它们设置了不同的模式("train""val" 和 "semi")。
    • 接着,使用 DataLoader 为每个数据集创建了数据加载器,数据加载器负责将数据分成批次,方便训练时按批次加载。对于训练集和验证集,数据会被打乱(shuffle=True),而无标签数据集不打乱(shuffle=False)。
  2. 模型创建
    • 可以选择使用自定义的 myModel 模型,也可以使用 initialize_model 函数初始化一个预训练的 VGG 模型,并将类别数设置为 11。
  3. 训练配置
    • 设定学习率 lr 为 0.001。
    • 使用 nn.CrossEntropyLoss() 作为损失函数,适用于多分类任务。
    • 使用 torch.optim.AdamW 作为优化器,对模型参数进行优化,同时设置了权重衰减以防止过拟合。
    • 检查是否有可用的 GPU,如果有使用 cuda,否则使用 cpu 作为计算设备。
    • 设定保存模型的路径 save_path 和训练的轮数 epochs,以及半监督学习的阈值 thres
  4. 训练过程
    • 最后,调用 train_val 函数开始训练和验证,传入之前创建的模型、数据加载器、设备、优化器、损失函数等参数。

补充:迁移学习

一、概念

  • 迁移学习是一种机器学习方法,它将在一个任务(源任务)上训练好的模型应用于另一个相关任务(目标任务)。其核心思想是利用源任务中已经学到的知识,加速目标任务的学习过程或提高目标任务的性能,尤其在目标任务的数据量较少时非常有用。

二、优点

  • 节省时间和资源:避免从头开始训练模型,利用已有的模型和参数,减少了对大量标注数据的需求,加快训练速度。
  • 提高性能:对于数据较少的任务,迁移已有的知识可以避免过拟合,增强模型的泛化能力,获得更好的性能。

三、常见方法

  • 微调(Fine-Tuning)

    • 从一个预训练好的模型开始,通常是在大规模数据集上训练的模型,如在 ImageNet 上训练的卷积神经网络(CNN)。
    • 保持模型的大部分层不变,仅调整最后几层(通常是全连接层)的参数,以适应新的任务。这是因为前面的层通常学习到了通用的特征,如边缘、纹理等,而最后几层更针对源任务的特定特征。
    • 在新任务的数据上重新训练模型,允许所有(或部分)层的参数更新。
  • 特征提取(Feature Extraction)

    • 利用预训练模型的特征提取能力。
    • 固定预训练模型的参数,将其作为一个特征提取器,只使用它的中间层输出作为新任务的输入特征。
    • 新任务的模型仅使用这些提取的特征,结合自己的分类器(如新的全连接层)进行训练,通常在数据量非常少的情况下使用。

四、应用场景

  • 计算机视觉:在图像分类、目标检测、语义分割等任务中,常使用在 ImageNet 等大规模数据集上预训练的 CNN 模型,如 VGG、ResNet 等。
  • 自然语言处理:利用在大规模文本数据上预训练的语言模型,如 BERT、GPT 等,进行文本分类、命名实体识别、机器翻译等任务。

五、实现步骤(以微调为例)

  • 选择一个合适的预训练模型,如 VGG、ResNet 等。
  • 替换预训练模型的最后几层,使其输出维度与目标任务的类别数相匹配。
  • 选择合适的优化器和损失函数。
  • 在目标任务的数据上进行训练,调整模型的参数。

六、注意事项

  • 源任务和目标任务越相似,迁移学习的效果越好。
  • 需要根据目标任务的特点,合理调整训练策略,如选择哪些层进行微调,使用多大的学习率等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/954856.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Dify应用-工作流

目录 DIFY 工作流参考 DIFY 工作流 2025-1-15 老规矩感谢参考文章的作者,避免走弯路。 2025-1-15 方便容易上手 在dify的一个桌面上,添加多个节点来完成一个任务。 每个工作流必须有一个开始和结束节点。 节点之间用线连接即可。 每个节点可以有输入和输出 输出类型有,字符串,…

LLM实现视频切片合成 前沿知识调研

1.相关产品 产品链接腾讯智影https://zenvideo.qq.com/可灵https://klingai.kuaishou.com/即梦https://jimeng.jianying.com/ai-tool/home/Runwayhttps://aitools.dedao.cn/ai/runwayml-com/Descripthttps://www.descript.com/?utm_sourceai-bot.cn/Opus Cliphttps://www.opu…

ASP.NET Core - 依赖注入(四)

ASP.NET Core - 依赖注入(四) 4. ASP.NET Core默认服务5. 依赖注入配置变形 4. ASP.NET Core默认服务 之前讲了中间件,实际上一个中间件要正常进行工作,通常需要许多的服务配合进行,而中间件中的服务自然也是通过 Ioc…

刷刷题刷题刷题

springaop 和 aspect aop的区别 springaop 是动态代理增强 aspect aop 是静态代理,在编译阶段生成aop代理类。这个时候是编译时增强 aop通知执行顺序 AOP 、OOP是啥 aop是面向切面 oop是面向对象 ComponentScan 不设置 basepackage也能进行扫描 没有配置&…

【6】Word:海名公司文秘❗

目录 题目 List.docx Word.docx List.docx和Word.docx 题目 List.docx 选中1/4全角空格复制→选中全部文本→开始→替换:粘贴将1/4全角空格 替换成 空格选中全部文本→插入→表格→将文本转化成表格→勾选和布局→自动调整→勾选 选中第一列,单机右键…

【Linux】gawk编辑器二

一、变量 gawk编程语言支持两种变量:内建变量和自定义变量。 1、内建变量 gawk使用内建变量来引用一些特殊的功能。 字段和记录分隔符变量 数据字段变量 此变量允许使用美元符号($)和字段在记录中的位置值来引用对应的字段。要引用记录…

Kafka客户端-“远程主机强迫关闭了一个现有的连接”故障排查及解决

Kafka客户端-“远程主机强迫关闭了一个现有的连接”故障排查及解决 1. 故障现象 Kafka客户端发送数据时,出现“远程主机强迫关闭了一个现有的连接”错误,导致数据发送失败。错误信息如下: 2. 故障排查 【1】. 查看服务网络状态 出现故障…

机器视觉5-全连接神经网络

机器视觉5-全连接神经网络1 图像表示多层感知器全连接神经网络一、两层全连接网络表达式二、三层全连接网络表达式三、关于非线性操作的说明四、全连接神经网络的映射原理 全连接神经网络的权值一、线性分类器二、两层全连接网络三、总结 全连接神经网络线性不可分全连接神经网…

Android BottomNavigationView不加icon使text垂直居中,完美解决。

这个问题网上千篇一律的设置iconsize为0,labale固定什么的,都没有效果。我的这个基本上所有人用都会有效果。 问题解决之前的效果:垂直方向,文本不居中,看着很难受 问题解决之后:舒服多了 其实很简单&…

1️⃣Java中的集合体系学习汇总(List/Map/Set 详解)

目录 01. Java中的集合体系 02. 单列集合体系​ 1. Collection系列集合的遍历方式 (1)迭代器遍历(2)增强for遍历​编辑(3)Lambda表达式遍历 03.List集合详解 04.Set集合详解 05.总结 Collection系列…

聚铭网络6款产品入选CCIA《网络安全专用产品指南》

近日,中国网络安全产业联盟CCIA正式发布《网络安全专用产品指南》(第二版)(以下简称《指南》)。聚铭网络凭借突出技术优势、创新能力以及市场积累,旗下安全产品成功入选防火墙、网络安全审计、日志分析、网…

nacos环境搭建以及SpringCloudAlibaba脚手架启动环境映射开发程序

1:下载nacos 地址:https://github.com/alibaba/nacos/tags 2:选择server的zip包下载 3:启动mysql服务,新建数据库:nacos_yh 4:解压下载的nacos_server 进入conf目录 5:mysql运行sql脚本变得到下面的表 6&a…

Mac安装配置使用nginx的一系列问题

brew安装nginx https://juejin.cn/post/6986190222241464350 使用brew安装nginx,如下命令所示: brew install nginx 如下图所示: 2.查看nginx的配置信息,如下命令: brew info nginxFrom:xxx 这样的,是n…

Linux系统离线部署MySQL详细教程(带每步骤图文教程)

1、登录官网下载对应的安装包 MySQL :: Developer Zone 2、将压缩包上传到服务器上,这里直接上传到/usr/local路径上 使用sftp工具上传到/usr/local目录上 3、解压压缩包 tar -xf mysql-8.0.39-linux-glibc2.17-x86_64.tar.xz 4、将mysql-8.0.39-linux-glibc2.17…

鸿蒙开发实战二 TypeScript和JavaScript的区别

简介 基于第一篇文章中提到的:https://mp.csdn.net/mp_blog/creation/editor/143515906 声明式开发范式:采用基于TypeScript声明式UI语法扩展而来的ArkTS语言,从组件、动画和状态管理三个维度提供UI绘制能力。 类Web开发范式:采…

初学stm32 --- CAN

目录 CAN介绍 CAN总线拓扑图 CAN总线特点 CAN应用场景 CAN物理层 CAN收发器芯片介绍 CAN协议层 数据帧介绍 CAN位时序介绍 数据同步过程 硬件同步 再同步 CAN总线仲裁 STM32 CAN控制器介绍 CAN控制器模式 CAN控制器模式 CAN控制器框图 发送处理 接收处理 接收过…

Mybatis-Plus:乐观锁与悲观锁

文章目录 一、场景二、乐观锁与悲观锁三、模拟修改冲突3.1 数据库中增加商品表3.2 添加数据3.3 添加实体3.4 添加mapper3.5 测试 四、乐观锁实现流程4.1 Mybatis-Plus实现乐观锁 一、场景 一件商品,成本价是80元,售价是100元。老板先是通知小李&#xf…

MySQL 8.0在windows环境安装及配置

文章目录 一、下载二、安装三、配置环境变量 一、下载 1、先彻底卸载之前的MySQL,并清理其 残留文件 。 2、登录网址https://www.mysql.com/ 3、点击网址左下角“中文”按钮,切换到中文界面 4、点击网页上方的“下载”按钮,然后点击网…

Python 实现 NLP 的完整流程

💖 欢迎来到我的博客! 非常高兴能在这里与您相遇。在这里,您不仅能获得有趣的技术分享,还能感受到轻松愉快的氛围。无论您是编程新手,还是资深开发者,都能在这里找到属于您的知识宝藏,学习和成长…

redux 结合 @reduxjs/toolkit 的使用

1,使用步骤 使用React Toolkit 创建 counterStore(store目录下) --> 为React注入store(src下面的index) --> React组件使用store中的数据(组件) 2,例如下面有一个简单加减的…