1.CNN

2.池化层

3.过拟合与欠拟合

4.迁移学习

微调,比如只微调最后几层的权重(比如把 “识别 1000 类” 改成 “识别你的 11类(猫 / 狗)”),用你的小样本数据稍微改一改就行。

场景:

你手里只有 3000 张食物照片(苹果、面包、蛋糕…),数量很少。

微调训练做了什么?

  1. VGG 已经会看图片(红色、圆形、松软),这些能力直接白嫖
  2. 我不让模型重新学 “怎么看图片”,只教它认你的 11 种食物
  3. 用你这 3000 张少量食物图,轻轻调整一下模型的参数
  4. 模型瞬间就学会了食物分类

问:什么是迁移学习?为什么要用预训练模型?

答:迁移学习就是把在大数据集上预训练好的模型权重,迁移到我们的小样本任务上,只做微调。用预训练模型的原因是,我们的食品数据集样本量少,从零训练模型很难学到通用的图像特征,而预训练模型在 ImageNet 百万级数据集上学到了通用的图像特征,只需要微调就能适配我们的项目,大幅提升准确率,同时加快模型收敛。

复试专用回答(流畅且体现思考)

各位老师,我项目的代码里,实际运行的是预训练 VGG 模型(预训练模型)而不是自己定义的模型

  1. 我先定义了一个基础 CNN 模型(myModel),但测试发现从零训练时,因为食品数据集样本量少,模型收敛慢、准确率只有 10% 左右;
  2. 所以我注释掉了自定义模型的调用,改用initialize_model加载带预训练权重的 VGG 模型(开启了use_pretrained=True),实际训练、验证、半监督学习全流程,都是基于这个预训练 VGG 模型完成的;
  3. 这样的设计也做了对比实验,能清晰体现迁移学习对小样本分类任务的提升作用。

5.半监督学习

半监督学习的 “核心价值”—— 补充食品专属的训练数据

项目里有大量无标签的食品图片(no_label_path对应的路径),这些图片是 “食品专属” 的,但没有人工标注;你用训练到一定程度的预训练 VGG 模型,给这些无标签图片做预测,只选 “模型 99% 确定类别” 的图片(比如模型确定这张是 “面包”),打上 “伪标签” 后加入训练集 —— 相当于把 “无标签的食品图片” 变成了 “可靠的标注数据”,直接扩充了食品分类任务的训练数据量;这些数据是 “食品专属” 的,能让预训练 VGG 模型进一步学习食品的细节特征,而不是只依赖通用特征。

  • vgg它已经见过 1000 类东西(ImageNet),学会了 “通用识人特征”(比如人有眼睛、鼻子、嘴巴);
  • 你的标注食品数据(3000 多),相当于教它 “认 11 个熟人”(比如张三、李四、王五),但只教了 30 次,它只记得这 30 次的样子(比如张三只穿红衣服);
  • 半监督学习就是找了 100 张 “没标名字但 99% 确定是张三 / 李四” 的照片(无标签高置信度图片),补充教它 “张三也穿蓝衣服、李四也戴眼镜”——不是学 “人有眼睛” 这种通用特征,而是学 “张三 / 李四的专属特征最终它能认各种样子的张三 / 李四,而不只是红衣服的张三

6.批量归一化

我在自定义 CNN 模型中加入 BatchNorm 层,核心是通过标准化输入分布提升训练效率和泛化能力:

  1. 加快收敛:BatchNorm 解决了 “内部协变量偏移” 问题 —— 标准化每一层的输入数据为均值 0、方差 1 的稳定分布,让模型不用反复适应输入分布的变化,聚焦学习特征本身,收敛速度提升数倍;
  2. 缓解过拟合:BatchNorm 基于批次样本计算均值 / 方差,引入轻微的随机正则化效果,降低模型对训练数据细节(比如食品图片的背景、角度)的依赖,增强泛化能力;

归一化核心:把不同范围的数,统一缩到 0-1 之间,公平参与计算;
好处:模型训练更快,不容易过拟合

7.VGG分类识别模型(误区!!!

VGG是预训练模型不是训练好的模型,他只是有通用的识别特征,但如何识别需要进行训练,他现在只是有特征而不会对图片进行识别分类

VGG 与 TrainVal:食物分类项目揭秘

完全没问题!我严格结合你的「food-11食物分类项目」,用最接地气的例子,把 VGG预训练模型 + 你的数据 + train_val微调训练 讲得明明白白!

先重申你的项目固定信息:

  • 分类目标:11种食物(苹果、面包、蛋糕、饼干、芝士、鸡蛋……)

  • 你的数据:带标签的食物图片、无标签食物图片

  • 核心任务:让模型看到食物图片 → 说出是11类里的哪一种


一、先看:VGG 天生会什么?(通用特征)

VGG 看过百万张图片,只学会了「看图的基本功」完全不认识你的食物

  1. 能看出颜色:红、黄、棕、白

  2. 能看出形状:圆、方、长条

  3. 能看出纹理:光滑、酥脆、松软

  4. 能看出轮廓:边缘、大小

举个直观的:

VGG 看到一张苹果图片,它只能识别出:

「这是红色 + 圆形 + 光滑的东西」

但它绝对不知道这东西叫「苹果」,更不知道这是你项目里的类别0


二、再看:train_val + 你的食物数据,教了VGG什么?(微调核心)

这就是针对性教学!把 VGG 会的「通用特征」和你的11类食物强行绑定

我拿你数据集中的 3类典型食物 举例子:

例子1:教 VGG 认【苹果(类别0)】

  1. 你的训练数据train_loader 里几百张带标签的苹果图

  2. 喂给模型:train_val 函数一批批(16张)喂给 VGG

  3. 教学过程

    1. VGG 提取特征:红色、圆形、光滑

    2. train_val 告诉它:这个特征组合 = 类别0(苹果)

    3. 优化器(AdamW)调整模型参数

  4. 学会后

  1. 只要看到「红色圆形光滑」→ 直接输出类别0

例子2:教 VGG 认【面包(类别1)】

  1. 你的数据:面包的图片

  2. VGG 原有能力:黄色、长条、松软纹理

  3. train_val 教学这个组合 = 类别1(面包)

  4. 结果:一眼认出面包

例子3:教 VGG 认【饼干(类别3)】

  1. VGG 原有能力:浅棕色、方形、酥脆纹理

  2. train_val 教学这个组合 = 类别3(饼干)


三、终极总结(针对你的项目)

1. 没有你的训练(train_val)

VGG 就是个**「睁眼瞎」**:

能看到食物的颜色形状,但完全不知道这是你项目里的哪一类食物,输出的11个分数全是乱的。

2. 有你的训练(train_val + 食物数据)

VGG 变成**「食物识别专家」**:

把它天生会的通用视觉特征精准适配到你的11类食物上,看到图片就能输出正确的类别(0~10)。

3. 半监督还在帮它巩固

你无标签文件夹里的食物图片,模型学会后99%确定是苹果,train_val 就用这些图再教一遍,让它认的更准。


一句话戳穿本质(你的项目专属)

**VGG 自带「眼睛」,

train_val 用你的食物数据教它「认字」,

让它从「能看清图片」→ 变成「能分清你的11种食物」!**的

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

#生成固定的随机数
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)
###############################################

#224*224
HW = 224


#数据增广  训练时才进行数据增广,让模型见到尽可能多的数据
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()
    ]
)

class food_Dataset(Dataset):
    def __init__(self, path, mode="train"):
        self.mode = mode
        #半监督模式,只读入x
        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(存储图片)数组
            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)
                img = img.resize((HW, HW))
                xi[j, ...] = img
            print("读到了%d个数据" % len(xi))
            return xi
        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


                #存11类图片,如果是第0类,怎创建x,y数组,否则直接与前边的类别的数组直接合并(else)
                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)

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_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

#第二大步  定义模型
class myModel(nn.Module):
    #num_class分类个数
    def __init__(self, num_class):
        super(myModel, self).__init__()
        #3 *224 *224  -> 512*7*7 -> 拉直 -》全连接分类
        #表示64个卷积核,卷积核大小为3,步长1,padding为1
        self.conv1 = nn.Conv2d(3, 64, 3, 1, 1)    # 64*224*224
        #归一化通道数
        # 归一化核心:把不同范围的数,统一缩到 0-1 之间,公平参与计算;
        # 好处:模型训练更快,不容易过拟合
        self.bn1 = nn.BatchNorm2d(64)
        # 加了 ReLU 就能学 “复杂规律”(比如 “红色 + 圆形 = 苹果”)。
        self.relu = nn.ReLU()
        #池化
        self.pool1 = nn.MaxPool2d(2)   #64*112*112


        #相比于第0层,写法简便
        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

        #模型流程
        #3×224×224图片 → conv1筛出64种基础特征(红、弧线)→ layer1-layer2拼出红色圆形 →
        # layer3-pool2提炼出苹果核心轮廓 → fc1-fc2打11个得分 → 选最高分(苹果)


    #x为图片
    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)
        #拉直  类比:把16个“512层7×7的礼盒”,
        # 全拆成16串“25088颗珠子”,方便后续打包
        x = x.view(x.size()[0], -1)
        x = self.fc1(x)
        x = self.relu2(x)
        x = self.fc2(x)
        return x

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):
        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()
        # 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()
        #     #每一轮的准确率 pred为预测值
        #     train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
        model.train()
        # ✅ 新增1:获取本轮训练的总批次数(比如3080个数据,batch_size=16,总批次就是3080/16≈193)
        total_batches = len(train_loader)
        # ✅ 新增2:初始化批次计数器,从0开始数
        current_batch = 0

        for batch_x, batch_y in train_loader:
            # ✅ 新增3:每跑一个批次,计数器+1,并打印进度
            current_batch += 1
            print(f"👉 训练进度:第{current_batch}/{total_batches}个批次 | 本轮第{epoch + 1}/{epochs}轮")

            # 下面的代码和原来完全一样,不用改!
            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())


        plt_train_loss.append(train_loss / train_loader.__len__())
        #准确率
        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())
        plt_val_loss.append(val_loss / val_loader.dataset.__len__())
        plt_val_acc.append(val_acc / val_loader.dataset.__len__())

        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

        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_path=r"D:\ligeproject\第四五节_分类代码\food_classification\food-11_sample\training\labeled"
val_path = r"D:\ligeproject\第四五节_分类代码\food_classification\food-11_sample\validation"
no_label_path = r"D:\ligeproject\第四五节_分类代码\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")

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)

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

#学习率  学习率(lr)是模型训练时的“调整步长”
lr = 0.001
#交叉熵损失
loss = nn.CrossEntropyLoss()
#优化器
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.pth"
epochs = 15
thres = 0.99



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

Logo

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

更多推荐