第一部分:导入库与环境配置
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//导入 torchvision 中的图像预处理工具(如裁剪、旋转、转张量)。
import time
import matplotlib.pyplot as plt
from model_utils.model import initialize_model//从自定义的 model_utils 模块中导入 initialize_model 函数,用于加载预训练模型(如 VGG)。
def seed_everything(seed)://定义一个函数,用于设置所有可能的随机种子,确保每次运行代码的结果一致。
    torch.manual_seed(seed)//设置 PyTorch CPU 的随机种子。
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)//设置 PyTorch 所有 GPU 的随机种子(多 GPU 训练时使用)。
    torch.backends.cudnn.benchmark = False//关闭 cuDNN 的自动优化。如果为 True,cuDNN 会寻找最适合当前硬件的卷积算法,但这会导致结果不可复现。
    torch.backends.cudnn.deterministic = True//设置 cuDNN 为确定性模式,确保卷积操作的结果是固定的。
    random.seed(seed)//设置 Python 标准 random 库的种子。
    np.random.seed(seed)//设置 NumPy 的随机种子。
    os.environ['PYTHONHASHSEED'] = str(seed)//设置 Python 的环境变量,确保哈希随机化被禁用。

设置 PYTHONHASHSEED 就是告诉 Python:“别随机了,用我指定的这个固定种子来初始化哈希系统。”

2. 代码详解

  • os.environ: 这是 Python 用来操作系统环境变量的字典对象。
  • 'PYTHONHASHSEED': 这是 Python 解释器启动时读取的一个特定环境变量。
  • str(seed): 必须将其设置为字符串格式的整数(例如 "42" 或 "0")。
    • 如果设置为整数(如 0),则哈希值会固定。
    • 如果设置为 "random"(默认情况),则每次启动都会随机。
#################################################################
seed_everything(0)
###############################################


HW = 224

//定义全局变量,图像的高度和宽度均为 224 像素(符合大多数预训练模型的输入要求)。

train_transform = transforms.Compose(
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224
        transforms.RandomResizedCrop(224),
        transforms.RandomRotation(50),
        transforms.ToTensor()
    ]
)

//定义训练集的数据增强流水线。
将 NumPy 数组转换为 PIL 图像,以便进行后续的图像增强操作。
随机裁剪图像并调整大小为 224x224。增加模型对物体大小的鲁棒性。
随机旋转图像,角度范围在 (-50, 50) 度之间。增加模型对角度的鲁棒性。
将 PIL 图像转换为 PyTorch 张量 (Tensor),并将像素值归一化到 [0, 1]。
结束训练变换定义。

val_transform = transforms.Compose(
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224
        transforms.ToTensor()
    ]
)

//定义验证集的数据预处理流水线。
第二部分:数据集类定义 (food_Dataset)

class food_Dataset(Dataset)://解释:定义一个继承自 torch.utils.data.Dataset的类。
    def __init__(self, path, mode="train")://初始化函数,接收数据路径和模式(train/val/semi)。
        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_transform//应用包含数据增强的变换。
        else:
            self.transform = val_transform//应用简单的变换(仅转张量)。

    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)
            //简单来说,它的作用是预先创建一个空的“大容器”(NumPy数组),用来批量存储从硬盘读取出来的图片数据。
下面我为你详细拆解这行代码的每一个部分及其背后的逻辑:
. 代码逐字拆解
表格
代码部分    含义    详细解释
np.zeros(...)    创建全零数组    这是 NumPy 库的函数,用于创建一个指定形状、所有元素初始化为 0 的数组。在图像中,全 0 代表纯黑色。
len(file_list)    样本数量 (Batch Size)    file_list 是文件名列表。len() 获取该目录下图片的总张数。这决定了数组的第一维大小,即这个容器能装多少张图片。
HW    图片高度    通常定义为 224 或 256。这是为了将所有图片统一缩放到相同的大小,以便模型能处理。
HW    图片宽度    同上,保持图片为正方形(如 224x224)。
    颜色通道    代表 RGB 三个通道(红、绿、蓝)。如果是灰度图,这里通常是 1。
dtype=np.uint8    数据类型    无符号8位整数。这是图像存储的标准格式,取值范围是 0~255。使用这种类型比默认的 float64 更节省内存。
            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))//调整图像大小为 224x224。
                xi[j, ...] = img//将图像存入数组。
            print("读到了%d个数据" % len(xi))//打印读取数量。
            return xi//返回读图像数据
        else:
            for i in tqdm(range(11))://遍历 11 个类别(00 到 10 文件夹)。在一个标准的 Python 循环中加入了一个可视化的进度条。
                file_dir = path + "/%02d" % i//构建类别文件夹路径(如 .../00, .../01)。
                file_list = os.listdir(file_dir)

                xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.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)
                    img = img.resize((HW, HW))
                    xi[j, ...] = img
                    yi[j] = i//存入标签(文件夹名即为类别 ID)。

                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))
            return X, Y

    def __getitem__(self, item)://定义获取单个样本的方法(DataLoader 会调用此方法)。
        if self.mode == "semi":
            return self.transform(self.X[item]), self.X[item]//返回增强后的图像和原始图像(此处逻辑稍显奇怪,通常半监督无标签数据只返回图像,这里返回了图像和图像作为伪标签占位)。
        else:
            return self.transform(self.X[item]), self.Y[item]//返回增强后的图像和对应的标签。

    def __len__(self):
        return len(self.X)//返回图像的总数量

//第三部分:半监督数据集类 
class semiDataset(Dataset)://这是代码中最核心的部分,实现了自训练(Self-Training)算法。
    def __init__(self, no_label_loder, model, device, thres=0.99)://初始化函数,接收无标签数据加载器、当前模型、设备和置信度阈值。初始化函数,接收无标签数据加载器、当前模型、设备和置信度阈值。
        x, y = self.get_label(no_label_loder, model, device, thres)//调用 get_label 方法,利用当前模型给无标签数据打标签。
        if x == []:如果没有找到高置信度的样本。
            self.flag = False//设置标志位为 False。

        else:
            self.flag = True
            self.X = np.array(x)//保存筛选出的图像。
            self.Y = torch.LongTensor(y)//保存筛选出的伪标签将原始的标签数据(列表 y)转换为 PyTorch 专用的长整型张量(LongTensor),以便模型能够正确计算损失。            self.transform = train_transform//应用训练变换(包含数据增强)
    def get_label(self, no_label_loder, model, device, thres):
        model = model.to(device)
        pred_prob = []//存储预测概率。
        labels = []
        x = []
        y = []
        soft = nn.Softmax()//定义 Softmax 函数,将 logits 转换为概率。
        with torch.no_grad()://关闭梯度计算,因为这只是推理过程,不需要反向传播
            for bat_x, _ in no_label_loder://遍历无标签数据加载器(注意:这里的 _是因为原始 semi 模式返回了图像作为伪标签,这里忽略了它)。                bat_x = bat_x.to(device)//将数据移到设备上。
                pred = model(bat_x)
                pred_soft = soft(pred)//应用 Softmax 得到概率分布。
                pred_max, pred_value = pred_soft.max(1)//获取最大概率值 (pred_max) 和对应的类别索引 (pred_value)。
                pred_prob.extend(pred_max.cpu().numpy().tolist())//将最大概率值存入列表
                labels.extend(pred_value.cpu().numpy().tolist())//将预测类别存入列表

        for index, prob in enumerate(pred_prob)://遍历所有预测结果
            if prob > thres://如果预测概率大于设定的阈值(0.99)
                x.append(no_label_loder.dataset[index][1])   #调用到原始的getitem//解释:这里存在一个潜在的逻辑问题。它试图从原始数据集中获取图像。[1]可能是想获取图像,但根据 food_Dataset的 __getitem__semi模式下索引 1 是原始图像(因为返回的是 (transform(img), img))。这行代码逻辑比较混乱,通常应该直接保存 bat_x。                y.append(labels[index])//将该样本的预测类别作为伪标签保存。
        return x, y

    def __getitem__(self, item)://定义获取样本的方法
        return self.transform(self.X[item]), self.Y[item]//返回经过增强的图像和伪标签
    def __len__(self):
        return len(self.X)

def get_semi_loader(no_label_loder, model, device, thres)://定义一个函数,用于生成半监督数据加载器
    semiset = semiDataset(no_label_loder, model, device, thres)//实例化
    if semiset.flag == False://如果数据集为空
        return None
    else:
        semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)//创建数据加载器。注意 shuffle=False,因为在打标签时顺序很重要
        return semi_loader

第四部分:模型定义 (myModel)
//这是一个简单的 CNN 模型

“CNN”这个词通常指代两个截然不同的事物:一个是人工智能领域的算法模型,另一个是国际新闻媒体。

结合你之前询问的深度学习代码问题,你大概率是在问卷积神经网络。为了确保全面,我将这两个概念都为你做个介绍。


🧠 1. 卷积神经网络

卷积神经网络是一种专门用于处理具有网格结构数据(如图像、语音)的深度学习模型。它是目前计算机视觉领域(如人脸识别、自动驾驶)最核心的技术基石。

🤔 为什么它叫“卷积”?

它得名于其核心操作——卷积。你可以把它想象成一个“特征提取器”或“扫描仪”。

  • 局部感知: 它不像传统神经网络那样一次性看整张图,而是用一个小的“窗口”(卷积核)在图片上滑动。
  • 提取特征: 这个窗口在滑动过程中,能捕捉到图像的边缘、纹理、颜色等局部特征。
  • 层层递进: 浅层的卷积核可能只看到“线条”,深层的卷积核能把线条组合成“眼睛”或“车轮”。
⚙️ 核心结构(像三明治一样堆叠)

一个典型的 CNN 通常由以下几层组成:

  • 卷积层: 核心层,负责提取特征。
  • 池化层: 负责“压缩”信息,降低计算量,同时保留最重要的特征(比如把图片缩小但保留轮廓)。
  • 全连接层: 位于网络末端,负责把提取到的所有特征汇总,给出一个最终的分类结果(比如“这是一只猫”)。

class myModel(nn.Module)://定义一个继承自 nn.Module的神经网络
    def __init__(self, num_class)://初始化函数,接收分类数量
        super(myModel, self).__init__()
        #3 *224 *224  -> 512*7*7 -> 拉直 -》全连接分类
        self.conv1 = nn.Conv2d(3, 64, 3, 1, 1)    # 64*224*224//定义第一个卷积层:输入 3 通道,输出 64 通道,卷积核 3x3,步长 1,填充 1。
        self.bn1 = nn.BatchNorm2d(64)//定义第一个批归一化层
        self.relu = nn.ReLU()//定义激活函数 ReLU
        self.pool1 = nn.MaxPool2d(2)   #64*112*112
//定义第一个最大池化层,将特征图缩小一半

        self.layer1 = nn.Sequential(//定义一个顺序容器,包含卷积、BN、激活、池化
            nn.Conv2d(64, 128, 3, 1, 1),    # 128*112*112
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2)   #128*56*56
        )
        self.layer2 = nn.Sequential(
            nn.Conv2d(128, 256, 3, 1, 1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(2)   #256*28*28
        )
        self.layer3 = nn.Sequential(
            nn.Conv2d(256, 512, 3, 1, 1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(2)   #512*14*14
        )

        self.pool2 = nn.MaxPool2d(2)    #512*7*7//最后的池化层
        self.fc1 = nn.Linear(25088, 1000)   #25088->1000//第一个全连接层。输入维度 25088 (512 * 7 * 7)
        self.relu2 = nn.ReLU()//全连接层后的激活函数
        self.fc2 = nn.Linear(1000, num_class)  #1000-11//输出层,输出类别数

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        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)//展平操作,将 4D 张量 (Batch, C, H, W) 转为 2D 张量 (Batch, CHW),以适应全连接层//`C`(通道)、`H`(高)、`W`(宽
        x = self.fc1(x)
        x = self.relu2(x)
        x = self.fc2(x)
        return x

第五部分:训练与验证循环 (train_val)
def train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path):
    model = model.to(device)
    semi_loader = None//初始化半监督加载器为空
    plt_train_loss = []
    plt_val_loss = []

    plt_train_acc = []
    plt_val_acc = []

    max_acc = 0.0//记录最高验证准确率,用于保存最佳模型

    for epoch in range(epochs):
1. 变量初始化与计时
        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 的损失和准确率累加器,用于计算平均 Loss 和 Accuracy

        start_time = time.time()
//记录当前时间,用于计算该 Epoch 的耗时
2. 训练阶段 (Train)
        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()//将当前批次(Batch)的平均损失值,累加到整个 Epoch 的总损失变量中
            train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
//

准确率累加代码

train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())

这行代码比较长,它的目的是:计算当前批次有多少个样本预测正确,并累加到总正确数中。

我们可以把它像剥洋葱一样拆开来看:

第一步:处理预测值 pred

pred.detach().cpu().numpy()

  • pred: 模型输出的原始预测结果(张量,在 GPU 上,带梯度)。
  • .detach(): 切断梯度。因为我们只是在看结果,不需要反向传播更新参数,所以把梯度信息扔掉,节省内存。
  • .cpu().numpy(): 把数据从 GPU 搬到 CPU,并转换成 NumPy 数组,方便用 NumPy 的函数进行计算。
第二步:找出预测类别

np.argmax(..., axis=1)

  • 模型输出通常是概率分布(例如 [0.1, 0.8, 0.1])。
  • argmax 会找出概率最大的那个位置的下标(例如 1)。
  • axis=1 表示横向比较(比较每一行里的类别概率),得出模型认为的“预测类别”。
第三步:处理真实标签 target

target.cpu().numpy()

  • 把真实的标签(比如 [0, 1, 2, 1])也从 GPU 搬到 CPU,转成 NumPy 数组。
第四步:比对与求和

... == ... 和 np.sum(...)

  • ==: 将“预测类别”和“真实标签”一一对比。结果是一个布尔数组(例如 [True, False, True, True])。
  • np.sum(): 在 Python/NumPy 中,True 被视为 1,False 被视为 0。求和就是统计猜对了多少个

        plt_train_loss.append(train_loss / train_loader.__len__())
        plt_train_acc.append(train_acc/train_loader.dataset.__len__()) 
#记录准确率,
//计算整个训练集的平均 Loss 和 Accuracy,并存入列表,用于后续绘图

3. 半监督训练阶段 (Semi-Supervised)

        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)//
  • 解释:计算模型预测结果 pred 和伪标签 target 之间的误差。
  • 逻辑:虽然 target 是模型自己猜的,但我们假设高置信度(Confidence > 0.99)的猜测是正确的。通过最小化这个误差,我们强迫模型“相信”自己之前的判断,并让模型学习无标签数据中的特征。
  • 公式逻辑: Loss = 预测值 - 标签

                semi_bat_loss.backward()
                optimizer.step()  # 更新参数 之后要梯度清零否则会累积梯度
                optimizer.zero_grad()
//
  • 解释:
    1. .backward(): 计算损失函数对模型参数的梯度(导数)。
    2. .step(): 根据梯度,更新模型的权重(让模型变得更好)。
    3. .zero_grad(): 将梯度清零,防止这一次的梯度累加到下一次计算中。

                semi_loss += semi_bat_loss.cpu().item()
                semi_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
//
  • 解释:
    1. np.argmax(pred...): 找出模型预测概率最高的类别(例如预测为“寿司”)。
    2. == target: 将预测结果与伪标签进行对比。
    3. np.sum(...): 统计这一批数据中预测正确的数量。
    4. +=: 将正确的数量累加到 semi_acc 变量中。
  • 目的:用于计算半监督数据上的准确率,监控模型在无标签数据上的表现。

            print("半监督数据集的训练准确率为", semi_acc/train_loader.dataset.__len__())
//
  • 解释:在完成一轮对所有半监督数据的遍历后,打印出准确率。
  • 计算公式:半监督准确率 = 预测正确的总数 (semi_acc) / 半监督数据总数量。


        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())
//遍历验证集,计算 Loss 和 Accuracy,逻辑同训练阶段,但不进行反向传播
        plt_val_loss.append(val_loss / val_loader.dataset.__len__())
        plt_val_acc.append(val_acc / val_loader.dataset.__len__())

        5. 伪标签更新与模型保存

        if epoch%3 == 0 and plt_val_acc[-1] > 0.6:
            semi_loader = get_semi_loader(no_label_loader, model, device, thres)

//

解释:半监督的核心逻辑。

  • 每隔 3 个 Epoch(epoch % 3 == 0)。
  • 且验证集准确率超过 60% 时(plt_val_acc[-1] > 0.6)。
  • 调用 get_semi_loader 函数,利用当前模型对无标签数据进行预测,生成高置信度(>0.99)的伪标签数据集,并创建新的 DataLoader。

        if val_acc > max_acc:
            torch.save(model, save_path)
            max_acc = val_loss

        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])
              )  # 打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。

第二部分:绘图代码
    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()

第三部分:数据与模型初始化
# path = r"F:\pycharm\beike\classification\food_classification\food-11\training\labeled"
# train_path = r"F:\pycharm\beike\classification\food_classification\food-11\training\labeled"
# val_path = r"F:\pycharm\beike\classification\food_classification\food-11\validation"
train_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\training\labeled"
val_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\validation"
no_label_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\training\unlabeled\00"
//定义训练集、验证集、无标签数据集的路径

train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")
no_label_set = food_Dataset(no_label_path, "semi")
//实例化之前定义的 food_Dataset类
 

train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)
//创建 DataLoader,用于批量加载数据。无标签数据不需要打乱(Shuffle)

# model = myModel(11)
model, _ = initialize_model("vgg", 11, use_pretrained=True)

这行代码 model, _ = initialize_model("vgg", 11, use_pretrained=True) 的作用是:调用一个自定义的辅助函数,快速构建并配置好一个 VGG 神经网络模型,以便直接用于训练或推理。

这通常出现在 PyTorch 的教程或封装好的代码库中(如 pytorchcv 或某些教学代码),用来简化创建模型的繁琐步骤。

下面我为你详细拆解每个参数的含义:

1. 代码拆解

表格

代码部分含义详细解释
initialize_model初始化函数这是一个自定义函数(不是 PyTorch 原生的)。它的作用是把“创建模型架构”、“加载预训练权重”、“修改输出层”这些步骤封装在一起,让你一行代码就能搞定。
"vgg"模型架构指定要使用的模型名称。这里选择的是 VGG 网络(一种经典的卷积神经网络,以深层卷积著称)。
11类别数量指定你的任务有多少个分类(num_classes)。
这意味着模型的最后一层(全连接层)会被修改,输出大小为 11,而不是 ImageNet 默认的 1000。
use_pretrained=True使用预训练权重这是关键点。表示不要从一个随机初始化的模型开始学,而是加载在大数据集(如 ImageNet)上已经训练好的权重。
model, _返回值函数返回了两个值:
1. model:构建好的神经网络对象。
2. _:通常返回的是优化器或数据变换,但这里用了下划线,表示“我不需要第二个返回值,直接忽略它”。

2. 为什么要这样写?(核心逻辑)

这行代码背后其实帮你自动完成了以下三件复杂的事情:

第一步:加载骨架

它去 PyTorch 的模型库中找到了 VGG 的结构代码。

第二步:迁移学习

因为 use_pretrained=True,它下载并加载了官方在 ImageNet 数据集上训练好的权重。

  • 好处: 模型已经学会了如何提取边缘、纹理等基础特征,你不需要从零开始训练,收敛速度会快很多。
第三步:修改“头部”

因为你的任务是 11 分类(而不是 ImageNet 的 1000 分类),函数会自动把 VGG 原本最后的输出层(Linear 层)替换掉,换成一个新的、输出维度为 11 的全连接层。

  • 注意: 这个新换的层是随机初始化的,只有它是需要从头学的,前面的卷积层都是带着预训练权重的。


lr = 0.001
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
//设置学习率、交叉熵损失函数、以及 AdamW 优化器

这行代码是在实例化一个 AdamW 优化器,它是目前深度学习(尤其是 Transformer 和大模型训练)中最流行、效果最好的优化器之一。

简单来说,它的作用是:告诉模型如何根据误差来更新自己的参数(权重),以便在下一次预测时更准确。

下面我为你详细拆解每个参数的含义,以及为什么要用 AdamW 而不是普通的 Adam。

1. 代码拆解

表格

代码部分含义详细解释
torch.optim.AdamW优化器类这是 PyTorch 提供的 AdamW 算法实现。它是 Adam 的改进版,核心区别在于“解耦权重衰减”(后面会细说)。
model.parameters()待优化参数这里传入的是模型中所有需要学习的参数(权重 weight 和偏置 bias)。优化器会负责更新这些张量。
lr=lr学习率控制参数更新的步长。比如 lr=0.001。如果太大,模型会“学飞了”(不收敛);如果太小,模型学得“太慢”(收敛慢)。
weight_decay=1e-4权重衰减这是一个正则化手段,用于防止过拟合。
1e-4 即 0.0001。它的作用是惩罚过大的权重值,强迫模型保持简单,不要死记硬背训练数据。

2. 核心亮点:为什么是 AdamW 而不是 Adam?

这是这行代码最关键的技术点。虽然名字只差一个字母,但原理有很大不同:

  • Adam (旧版):
    它在计算梯度时,把“权重衰减”(L2正则化)混在梯度里一起算。但在自适应学习率(Adam 的核心特性)的机制下,权重衰减的效果会被学习率抵消掉,导致正则化失效或效果不稳定。
  • AdamW (新版):
    它把权重衰减从梯度计算中“解耦”出来,直接在参数更新时独立减去。
    • 好处:weight_decay 真正发挥了作用,能更有效地防止过拟合,提升模型的泛化能力。
    • 地位:现在训练 BERT、GPT、ViT 等大模型,或者做计算机视觉任务,AdamW 都是默认的首选优化器。

3. 形象类比

如果把训练模型比作下山(寻找最低点/最小误差):

  • lr (学习率):是你迈步的大小。
  • weight_decay (权重衰减):就像是背包里的重力。
    • 在 Adam 中,重力会受地形(梯度)影响,有时候你觉得背包重,有时候觉得轻,效果不稳定。
    • 在 AdamW 中,重力是恒定的,不管你走什么路,背包都会实实在在地把你往“轻量级”的方向拉,强迫你走更简单的路径(防止过拟合)。

4. 总结

这行代码的意思是:

“我要用 AdamW 算法来训练模型。请根据数据计算梯度来更新参数,步长由 lr 决定。同时,为了防止模型死记硬背,请每走一步都施加 0.0001 强度的‘拉力’(权重衰减),限制参数不要变得太大。



device = "cuda" if torch.cuda.is_available() else "cpu"
save_path = "model_save/best_model.pth"
epochs = 15
thres = 0.99

设置运行设备(GPU优先)、模型保存路径、训练轮数(15轮)、以及伪标签的置信度阈值(99%)。

train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path)

//调用训练函数,开始整个流程。

总结与建议

这段代码实现了一个周期性伪标签(Periodic Pseudo-labeling)的半监督算法。

代码中存在的明显 Bug 和改进建议:

  1. 变量名混淆(严重):
    • 在保存模型的地方,max_acc = val_loss 是错误的。
    • 建议:改为 best_acc = val_acc,或者如果想保存 Loss 最低的模型,改为 min_loss = val_loss。
  2. 分母计算错误:
    • semi_acc/train_loader.dataset.__len__() 这里的分母应该是 semi_loader 的长度。
  3. 半监督训练时机:
    • 代码在 Epoch 结尾才更新 semi_loader,这意味着当前 Epoch 训练的 semi_loader 是上一次生成的(或者初始为 None)。这是半监督学习中常见的 "Delayed" 更新,虽然可行,但需要注意第一轮没有半监督数据参与。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐