食物照片分类实战

随机种子

定义一个 seed_everything() 函数,统一设置多个库的随机种子,用于设置各种随机种子以确保实验的可重复性。

在需要对比不同模型或参数时,确保随机性不会影响实验结果。

复制代码
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)
# 调用函数,设置随机种子为0
seed_everything(0)
# 后续的随机操作将是确定性的

数据增广

对原始数据进行一系列有目的的变换人工扩展数据集的技术。

核心目的:

  1. 增加数据量和多样性 :尤其在数据稀缺时,避免模型因训练数据不足而过拟合(即模型过度记忆训练数据,无法泛化到新数据)。

  2. 提升模型泛化能力:通过让模型看到更多样的数据变体,使其对噪声、变换等更具鲁棒性,从而提高在真实场景中的表现。

Softmax 函数

Softmax 是一个数学函数,它将一组任意实数转换为概率分布。简单说,它能把模型的"原始得分"变成"概率"。

迁移学习------数据量少时的最佳选择

大佬们的模型:花了百万美元,在千万上亿的数据集上训练,提取的特征特别好。

我的模型:5000元电脑,在千张照片上训练,提取的特征:二分类准确率有百分之五十吧(基本瞎猜)

迁移学习可以理解为你把大佬的模型拿过来,用在自己的数据集训练上

有预训练模型的时候,尽量使用预训练模型。
我们使用他人模型架构的原因,最大的原因不是因为他们架构好,而是因为可以迁移

迁移学习是一类机器学习方法,通过在源领域(source domain)或任务(source task)中学得的知识来帮助目标领域(target domain)或任务(target task)的学习。

核心思想是利用已有的模型或知识,减少在目标任务中对大规模标注数据的依赖,提高学习效率和模型性能。

优势分析

  1. 预训练模型已学习通用特征

  2. 避免从零开始学习

迁移学习的三种主要策略

  1. 特征提取(Feature Extraction)

  2. 微调(Fine-tuning)

  3. 预训练+新头(Pretrain + New Head)

保留预训练的特征提取器,替换最后的全连接层

完全相信大佬的模型,线性探测(一般不用);否则微调

把大佬的特征提取器拿过来用,但是分类头需要用自己的。大佬分1000类,你只有11类。所以必须改分类头


归一化

归一化 是一种数据预处理技术,将数据缩放 到一个特定的范围 (通常是[0,1]或[-1,1]),或者使其具有零均值和单位方差

复制代码
# 假设一个数据集有两个特征:
特征1: 年龄 [20, 50, 80]  # 范围 20-80
特征2: 收入 [30000, 80000, 150000]  # 范围 30000-150000

# 如果不归一化:
# 收入的变化会完全主导年龄的影响
# 因为收入的数值比年龄大得多

Adam和AdamW 优化器

SGD简单有效,但有两个主要缺点:

  1. 所有参数使用相同的学习率:对于稀疏特征或不常更新的参数,这可能不是最优的。

  2. 梯度可能不稳定:遇到陡峭或平缓的峡谷地形时,容易震荡或收敛缓慢。

为了解决这些问题,自适应优化器 应运而生。它们的核心思想是:为每个参数计算并维护其自身的、随时间变化的学习率

Adam 结合了 动量法RMSProp 的优点:

  • 自适应学习率:每个参数有自己的步长。

  • 包含动量:加速收敛并减少震荡。

Adam 的三大特性:

  1. 动量(Momentum):加速收敛,减少振荡

  2. 自适应学习率:每个参数有自己的学习率

  3. 偏差修正:解决初始估计偏差问题

    基本使用

    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    完整参数

    optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.001, # 学习率
    betas=(0.9, 0.999), # β1(一阶矩衰减率),β2(二阶矩衰减率)
    eps=1e-8, # 数值稳定性常数
    weight_decay=0, # L2正则化(权重衰减)
    amsgrad=False # 是否使用AMSGrad变体
    )

DatasetDataLoader 的关系

Dataset 类(数据提供者)

复制代码
class Dataset:
    #Dataset提供给DataLoader的标准数据格式
    def __getitem__(self, index):
        """返回单个样本:数据 + 标签"""
        return data, label
    
    def __len__(self):
        """返回数据集大小"""
        return dataset_size

DataLoader 类(数据组织者)

DataLoader 不需要自己定义,直接使用就可以PyTorch 内置了完整的 DataLoader 实现)

复制代码
class DataLoader:
    def __init__(self, dataset, batch_size, shuffle):
        self.dataset = dataset  # 持有Dataset实例
        self.batch_size = batch_size
        self.shuffle = shuffle
    
    def __iter__(self):
        """返回一个迭代器,每次迭代返回一个batch"""
        # 1. 生成索引序列
        # 2. 分批调用 dataset.__getitem__()
        # 3. 整理成batch返回

PyTorch 的哲学是:

  • 你需要自己定义 Dataset(数据如何存储和获取)

  • PyTorch 提供 DataLoader(数据如何组织和迭代)

就像餐厅的比喻

  • Dataset:你是厨师,定义菜怎么做(数据格式)

  • DataLoader:服务员,负责把菜端给客人(数据提供给模型)

  • 你不需要自己当服务员,PyTorch 已经提供了专业的"服务员"


food_Dataset(train_path, "train") 返回的是什么?

  1. 返回的是对象,不是数据

    train_set = food_Dataset(train_path, "train")

    train_set 是一个 Dataset 对象,不是数据本身!

  2. 什么时候才真正获取数据?

    情况1:通过索引直接访问

    x, y = train_set[0] # 这里才调用 getitem(0)

    情况2:通过 DataLoader 批量访问

    train_loader = DataLoader(train_set, batch_size=16)
    for batch_data, batch_labels in train_loader: # DataLoader内部调用 getitem
    # 这里才真正加载数据
    pass


复制代码
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
  • 16个样本的数据批次(不是16个Dataset对象)

  • 每个批次包含:

    • batch_x: 16个图像数据(形状 [16, 3, 224, 224])

    • batch_y: 16个对应的标签(形状 [16])

  • x和y是严格对应

shuffle=True体现在


半监督学习

同时利用少量有标签数据和大量无标签数据来训练模型,以获得比仅使用有标签数据(监督学习)或仅使用无标签数据(无监督学习)更好的性能。

当模型在有标签数据上达到一定准确度后,用它对无标签数据进行预测,筛选出预测置信度高于阈值(如99%)的样本,将这些样本及其预测标签作为伪标签数据,加入到模型的训练中,从而提升模型性能。


代码

class food_Dataset(Dataset)

复制代码
class food_Dataset(Dataset):
    def __init__(self, path, mode="train"):
        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)
            X = 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)
                img = img.resize((HW, HW))
                X[j, ...] = img
            print("读到了%d个数据" % len(X))
            return X
        else:
            for i in tqdm(range(11)):
                file_dir = path + "/%02d" % i
                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
                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):
        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)

def read_file

首先我们肯定要读出这个照片数据,这里分为有标签的数据和无标签的数据:

1、有标签的数据,调用read_file 函数,获取他的图片和标签

根据文件我们知道一共有11个类别,每个文件夹中有若干照片。首先遍历这11个文件夹,拿到文件夹中每一张照片,放到xi数组中,对应的yi数组存放类别。

一个文件夹遍历一遍,每次遍历结束后都把收集到的xi和yi合并到大的X和Y数组中,等11个文件夹全部遍历结束,我们就能拿到完整的X和Y数组

2、无标签的数据,调用read_file 函数,只能获取到他的图片

def init

init中我们拿到了初始的数据,根据模式mode我们分配不同的transform,当**Dataset获取数据的时候自动进行图片变换**

def getitem

当 food_Dataset(train_path, "train") 调用时,如果是semi返回的就是预处理后的数据和原始数据;如果不是semi,返回的就是变换后的数据和原始标签


transform

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

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

如果是训练数据,我们不仅需要预处理,还要数据增强

  1. 将输入数据转换为 PIL Image 格式

2. 随机调整图像大小并裁剪到 224x224

3. 随机旋转图像

  1. 将 PIL Image 转换为 PyTorch Tensor

如果是semi或者val数据,我们只需要基本的预处理

  1. 将输入数据转换为 PIL Image 格式

  2. 将 PIL Image 转换为 PyTorch Tensor


class myModel(nn.Module)

复制代码
class myModel(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
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU()
        self.pool1 = nn.MaxPool2d(2)   #64*112*112
        self.layer1 = nn.Sequential(
            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
        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)
        x = self.fc1(x)
        x = self.relu2(x)
        x = self.fc2(x)
        return x

卷积

nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)

3:输入通道数(RGB图像的3个通道)

64:输出通道数

3:卷积核大小3×3

1:步长(每次移动1像素)

1:填充(在图像四周填充1圈0值像素)

self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) #输出:64×224×224(保持了原始图像尺寸)

批量归一化

self.bn1 = nn.BatchNorm2d(64) # 对64个通道的特征图进行批量归一化

激活函数

复制代码
self.relu = nn.ReLU()

池化

复制代码
self.pool1 = nn.MaxPool2d(2)

展开

卷积网络连接到全连接层的必要操作

复制代码
x = x.flatten(1)  # 从第1维开始展平

全连接层

复制代码
self.fc1 = nn.Linear(25088, 1000) 

class semiDataset(Dataset)

伪标签数据集

复制代码
class semiDataset(Dataset):
    def __init__(self, no_label_loder, model, device, thres=0.99):
        x, y = self.get_label(no_label_loder, model, device, thres)
        if x == []:
            self.flag = False
        else:
            self.flag = True
            self.X = np.array(x)
            self.Y = torch.LongTensor(y)
            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()
        with torch.no_grad():
            for bat_x, _ in no_label_loder:
                bat_x = bat_x.to(device)
                pred = model(bat_x)
                pred_soft = soft(pred)
                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):
            if prob > thres:
                x.append(no_label_loder.dataset[index][1])   #调用到原始的getitem
                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_label 获取伪标签

什么样的伪标签是可以用的?

置信度超过阈值的伪标签,可以用作正式训练的标签

第一步:使用训练好的模型批量预测无标签数据,获取每个样本的预测置信度和预测类别,并收集起来用于后续的伪标签生成。

复制代码
        with torch.no_grad():
            for batch_x , _ in no_label_loader:
                batch_x = batch_x .to(device)
                predict = model(batch_x)
                predict_soft = Softmax(predict)
                pred_max, pred_index = predict_soft.max(1)
                predict_proba.extend(pred_max.cpu().numpy().tolist())
                predict_labels.extend(pred_index.cpu().numpy().tolist())

第二步:筛选出预测置信度高于阈值的高质量预测,将对应的原始数据和预测类别作为伪标签数据返回。

复制代码
        for index, prob in enumerate(predict_proba):
            if prob > thres:
                x.append(no_label_loader.dataset[index][1])   #调用到原始的x
                y.append(predict_labels[index])
        return x, y

def init

初始化semiDataset对象,通过生成伪标签创建新的带标签数据集,并根据是否成功生成有效伪标签设置标志位和数据属性。

复制代码
def __init__(self, no_label_loder, model, device, thres=0.99):
        x, y = self.get_label(no_label_loder, model, device, thres)
        if x == []:
            self.flag = False
        else:
            self.flag = True
            self.X = np.array(x)
            self.Y = torch.LongTensor(y)
            self.transform = train_transform

def getitem

Dataset提供给DataLoader的标准数据格式

复制代码
    def __getitem__(self, item):
        return self.transform(self.X[item]), self.Y[item]

def get_semi_loader

复制代码
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)
        return semi_loader

创建一个包含伪标签数据的DataLoader,如果成功生成了有效的伪标签数据集,则返回该DataLoader用于训练,否则返回None。

def train_val

  1. 初始化

    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        # 记录最佳验证准确率

设置训练环境,初始化跟踪变量,为训练循环做准备。

  1. 训练主循环

    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

    复制代码
     start_time = time.time()  # 计时开始
     model.train()  # 切换到训练模式

控制训练轮次,每轮重置指标,设置训练模式。

3. 有标签数据训练

复制代码
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())
  1. 半监督数据训练

    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__())

关键点

  • 只有在生成伪标签数据后才执行

  • 训练逻辑与有标签数据完全相同

  • 但使用的是伪标签而不是真实标签

  • 打印的是伪标签训练准确率(模型对伪标签的拟合程度)

  1. 验证eval

    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())

  2. 伪标签更新

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

  • 周期性更新:每3个epoch更新一次伪标签

  • 条件触发:只在验证准确率>60%时才生成伪标签

  • 为什么需要条件

    • 初期模型不准 → 伪标签质量差 → 影响训练

    • 中期模型较准 → 高质量伪标签 → 提升性能

  • 动态调整:每次用最新模型重新生成伪标签

  1. 模型保存

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

  • 保存验证集上表现最好的模型

  • 避免过拟合后模型性能下降

  • torch.save(model, save_path):保存整个模型结构和参数

  1. 日志输出

    print('[%03d/%03d] %2.2f 秒 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]))

训练流程

  1. 渐进式学习

先学简单的(有标签数据)

再学困难的(伪标签数据)

  1. 质量控制

只使用高置信度伪标签 , 加入训练

if prob > thres: # thres=0.99

  1. 动态调整

定期更新伪标签

随着模型变好,伪标签质量提高

形成良性循环

  1. 防止误差传播

条件触发机制

if plt_val_acc[-1] > 0.6:

只有模型够好时才生成伪标签

这个训练函数实现了完整的自训练半监督学习框架,通过有标签数据和伪标签数据的混合训练,有效利用无标签数据提升模型性能。

相关推荐
浔川python社7 小时前
【维护期间重要提醒】请勿使用浔川 AI 翻译 v6.0 翻译违规内容
人工智能
CS创新实验室7 小时前
AI 与编程
人工智能·编程·编程语言
min1811234567 小时前
深度伪造内容的检测与溯源技术
大数据·网络·人工智能
_codemonster7 小时前
高斯卷积的可加性定理
人工智能·计算机视觉
数据智研8 小时前
【数据分享】(2005–2016年)基于水资源承载力的华北地区降水与地下水要素数据
大数据·人工智能·信息可视化·数据分析
likuolei8 小时前
Spring AI框架完整指南
人工智能·python·spring
梵得儿SHI8 小时前
(第四篇)Spring AI 核心技术攻坚:多轮对话与记忆机制,打造有上下文的 AI
java·人工智能·spring·springai生态·上下文丢失问题·三类记忆·智能客服实战案
二哈喇子!8 小时前
PyTorch生态与昇腾平台适配:环境搭建与详细安装指南
人工智能·pytorch·python
lingzhilab8 小时前
零知ESP32-S3 部署AI小智 2.1,继电器和音量控制以及页面展示音量
人工智能
ASD125478acx9 小时前
多类型孢子与真菌的智能识别与分类系统YOLO模型优化方法
yolo·目标跟踪·分类