卷积神经网络调优

今天介绍卷积神经网络调优的方法,包括学习率调度器,残差网络结构等,以我们前面实现的食物分类项目为例。

在食物分类任务中:手动搭建的卷积神经网络(CNN)要么训练收敛缓慢、准确率卡在瓶颈,要么随着网络层数加深,准确率不升反降。这两个核心问题,恰恰对应CNN调优中最关键的两个方向------学习率调度和退化问题解决。本文将聚焦这两大板块,结合食物分类的实际场景,拆解调优逻辑、提供实操代码,帮你快速突破准确率瓶颈。

先明确任务背景:本文所有调优方案均基于「20类食物分类任务」验证,数据集包含八宝粥、汉堡、炸鸡、草莓等常见食物,原始模型为6层手动搭建CNN,初始准确率仅45%。通过以下调优,最终准确率提升至80%+。

一、学习率调整

学习率是CNN训练的"命脉":学习率太大,模型会在最优解附近震荡,无法收敛;学习率太小,训练速度极慢,甚至陷入局部最优解。在食物分类任务中,由于食物特征多样(纹理、颜色、形状差异大),固定学习率很难适配全程训练,这时候就需要学习率调度器来动态调整。

调整方法

案例:

python 复制代码
scheduler=torch.optim.lr_scheduler.StepLR(optimizer,step_size=5,gamma=0.5)
epochs = 30 # 训练10轮
for t in range(epochs):
    print(f"\n训练轮数 {t+1}/{epochs}")
    train(train_dataloader, model, loss_fn, optimizer)
    scheduler.step()#固定步长的学习率调度器

    test_loss=test(test_dataloader, model, loss_fn)  # 每轮训练后测试
    scheduler.step()  # 基于性能的调度器这里是
    print("训练完成!")

    print("开始测试:")
    test(test_dataloader, model, loss_fn)

基于学习率调度器(scheduler)实现固定补偿机制的学习率调度器。食物分类其他代码不需要改变,仅增加:

scheduler=torch.optim.lr_scheduler.StepLR(optimizer,step_size=5,gamma=0.5)

scheduler.step()#固定步长的学习率调度器。

二、ResNet 迁移学习:基于残差网络结构解决深度网络退化问题

很多开发者会有一个误区:"网络层数越深,特征提取能力越强,准确率越高"。但在食物分类任务中,当手动搭建的CNN层数从6层增加到12层时,准确率不仅没提升,反而从45%降到38%------这就是CNN的"退化问题",也是深层网络训练的核心障碍。

1. 退化问题在食物分类中的具体表现

  • 深层网络(12层以上)的训练误差和测试误差同步上升,模型连基础的食物类别都难以区分;

  • 训练过程中损失下降缓慢,甚至出现"平台期"后突然上升;

  • 浅层网络(6层)的泛化能力反而优于深层网络,比如6层CNN能正确识别70%的汉堡图片,12层CNN仅能识别50%。

2. 退化问题的核心原因

退化问题的本质是:深层网络难以学习"恒等映射"。当浅层网络已经能拟合较好的食物特征时,深层网络需要在浅层基础上,让新增的层学习"输入=输出"的恒等变换(即不改变原有特征),这样深层网络的性能至少不会低于浅层。但普通的卷积层很难拟合这种简单的恒等映射,新增层反而会破坏原有特征,导致性能下降。

3. 解决方案:ResNet残差连接------给深层网络"开绿灯"

ResNet(残差网络)通过"短路连接(shortcut connection)+残差学习",完美解决了退化问题。其核心逻辑是:

不要求新增层直接拟合目标函数H(x),而是拟合残差F(x) = H(x) - x,最终网络输出为"输入x + 残差F(x)"。当需要学习恒等映射时,只需让F(x)=0即可,极大降低了学习难度------新增层不用"硬学"恒等映射,只需专注于学习能提升性能的"残差特征"。

resnet:
残差链接:

通俗来讲,"退化" 指的是模型在学习图像特征的过程中走偏了,没能抓住图像原本的核心特征;而残差连接的作用,就是让原始图像数据能在网络各层之间增加直接传递,避免模型学偏,从而解决这种退化问题

迁移学习:

三、案例:基于resnet迁移学习解决食物分类

代码:

python 复制代码
# -*- coding: gbk -*-
import torch
import torch.nn as nn
from torchvision import models

from torch.utils.data import Dataset,DataLoader
import numpy as np
from PIL import Image
from torchvision import transforms  #对数据进行处理工具 转换

#真的数据增强,加入旋转,色彩变幻等
data_transforms = { #机器学习的时候, 数据进行归一化?? 图片做归一化  ->0~1
    'train':
        transforms.Compose([
            transforms.Resize([300,300]),  #是图像变换大小  opencv  int8 0~
            transforms.RandomRotation(45),#随机旋转, -45到45度之间随机选
            transforms.CenterCrop(256),#从中心开始裁剪[256,256]
            transforms.RandomHorizontalFlip(p=0.5),#随机水平翻转 选择一个概率概率
            transforms.RandomVerticalFlip(p=0.5),#随机垂直翻转
            transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),#参数1为亮度, 参数2为对比度, 参数3为饱和度, 参
            transforms.RandomGrayscale(p=0.1),#概率转换成灰度率, 3通道就是R=G=B
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])#标准化, 均值, 标准差,
        ]),
    'valid':#验证集 不需要对图像进行数据增强
        transforms.Compose([
            transforms.Resize([256,256]),  #
            transforms.ToTensor(),
            transforms.Normalize( [0.485, 0.456, 0.406],  [0.229, 0.224, 0.225])
        ])
}

class food_dataset(Dataset):  # food_dataset是自己创建的类名称,可以改为你需要的名称 2用法
    def __init__(self, file_path,transform=None): #类的初始化,解析数据文件txt
        self.file_path = file_path
        self.imgs = []
        self.labels = []
        self.transform = transform
        with open(self.file_path) as f:#是把train.txt文件中图片的路径保存在 self.imgs,train.txt文件中标签保存在 self.labels
            samples = [x.strip().split(' ') for x in f.readlines()]
            for img_path, label in samples:
                self.imgs.append(img_path) #图像的路径
                self.labels.append(label) #标签,还不是tensor
#初始化:把图片目录加载到self.imgs.
    def __len__(self): #类实例化对象后,可以使用len函数测量对象的个数  ls=[12,3,4,4] len(training_data)
        return len(self.imgs)
#training_data[1]
    def __getitem__(self, idx): #关键,可通过索引的形式获取每一个图片数据及标签
        image = Image.open(self.imgs[idx]) #读取到图片数据,还不是tensor,BGR
        if self.transform:  #将PIL图像数据转换为tensor
            image = self.transform(image) #图像处理为256×256,转换为tenor
        label = self.labels[idx]  #label还不是tensor
        label = torch.from_numpy(np.array(label,dtype = np.int64)) #label也转换为tensor,
        return image, label
#training_data包含了本次需要训练的全部数据集?
training_data = food_dataset(file_path = './train.txt',transform = data_transforms['train']) #
test_data = food_dataset(file_path = './test.txt',transform = data_transforms['valid'])

#training_data需要具备索引的功能,还要确保数据是tensor
train_dataloader = DataLoader(training_data, batch_size=64,shuffle=True)#64张图片为一个包,
test_dataloader = DataLoader(test_data,batch_size=64,shuffle=True)


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 设备配置:优先用GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


# 定义自定义ResNet模型(保留原fc,新增分类层)
class ResNetWithExtraLayer(nn.Module):
    def __init__(self, num_classes=20):  # num_classes是你的食物分类数
        super().__init__()
        # 1. 加载预训练ResNet18,保留原有所有层(包括原fc层)
        self.resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

        # # 2. 冻结ResNet的所有层(只训练新增的层,也可按需解冻)
        # for param in self.resnet.parameters():
        #     param.requires_grad = False

        # 3. 新增分类层:接收原fc的1000维输出,映射到num_classes类
        # 加Dropout防止过拟合
        self.extra_classifier = nn.Sequential(
            nn.Dropout(p=0.5),  # 随机丢弃50%的神经元,防过拟合
            nn.Linear(in_features=1000, out_features=num_classes)  # 1000→20
        )

    def forward(self, x):
        # 第一步:ResNet提取特征(输出1000维)
        resnet_output = self.resnet(x)
        # 第二步:新增层映射到食物分类数
        final_output = self.extra_classifier(resnet_output)
        return final_output


# 初始化模型(假设你的食物分类是20类)
model = ResNetWithExtraLayer(num_classes=20).to(device)
print(model)

def train(dataloader, model, loss_fn, optimizer):
    model.train()  # 切换到训练模式
    batch_size_num = 1  # 统计batch数量

    for X, y in dataloader: # dataloader是函数形参,调用时传入train_dataloader/test_dataloader;迭代返回批次级数据:X为批次图片张量(shape[batch_size,1,28,28]),y为批次标签张量(shape[batch_size]),对应批次内所有样本的图片和标签
        # 数据移动到设备
        X, y = X.to(device), y.to(device)

        # 前向传播计算预测值
        pred = model(X)  # 可省略.forward,model(X)会自动调用forward
        loss = loss_fn(pred, y)  # 计算损失

        # 反向传播更新参数
        optimizer.zero_grad()  # 梯度清零
        loss.backward()  # 反向传播计算梯度(原代码中Loss大写,修正为loss)
        optimizer.step()  # 更新模型参数


        # 每100个batch打印一次损失
        loss_value = loss.item()
        if batch_size_num % 100 == 0:
            print(f"Loss: {loss_value:>7f}  [batch: {batch_size_num}]")
        batch_size_num += 1

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)  # 测试集总样本数
    num_batches = len(dataloader)  # 测试集batch数量
    model.eval()  # 切换到测试模式
    test_loss, correct = 0, 0

    # 测试时关闭梯度计算,节省资源
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()  # 累计损失
            # 计算正确预测数(取预测最大值对应的索引)
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    # 计算平均损失和准确率
    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy: {(100 * correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
    return test_loss

loss_fn = nn.CrossEntropyLoss()  # 交叉熵损失(适用于分类任务)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)  # SGD优化器
# 原代码的optimizer替换为Adam
# optimizer = torch.optim.Adam(model.parameters(), lr=0.001)  # Adam默认lr=0.001即可
scheduler=torch.optim.lr_scheduler.StepLR(optimizer,step_size=5,gamma=0.5)

# # 替换原优化器+调度器代码
# # 分层设置学习率:解冻层小lr,新增层稍大
# optimizer = torch.optim.AdamW([
#     # 解冻的layer3/layer4:小学习率(避免破坏预训练特征)
#     {'params': model.resnet.layer3.parameters(), 'lr': 1e-5},
#     {'params': model.resnet.layer4.parameters(), 'lr': 1e-5},
#     # 新增分类层:稍大的学习率
#     {'params': model.extra_classifier.parameters(), 'lr': 1e-4}
# ], weight_decay=1e-3)  # 权重衰减防过拟合
# 
# # 替换StepLR为余弦退火调度器(效果更好)
# scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
#     optimizer, T_max=20, eta_min=1e-6  # T_max=训练轮数*0.7左右
# )






epochs = 10# 训练10轮
for t in range(epochs):
    print(f"\n训练轮数 {t+1}/{epochs}")
    train(train_dataloader, model, loss_fn, optimizer)
    scheduler.step()#固定步长的学习率调度器

    test_loss=test(test_dataloader, model, loss_fn)  # 每轮训练后测试
    # scheduler.step()  # 基于性能的调度器这里是
    print("训练完成!")

    print("开始测试:")
    test(test_dataloader, model, loss_fn)


def predict_single_image(image_path, model, class_names):
    """
    输入单张图片路径,输出预测结果
    :param image_path: 图片路径(如"test_hamburger.jpg")
    :param model: 训练好的模型
    :param class_names: 分类类别名列表(如["汉堡", "薯条", ...])
    :return: 预测类别名、预测置信度
    """
    # 1. 图片预处理(必须和训练时一致!)
    transform = transforms.Compose([
        transforms.Resize([256, 256]),  #
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    # 2. 加载并预处理图片
    image = Image.open(image_path).convert("RGB")  # 确保是RGB格式
    image_tensor = transform(image).unsqueeze(0)  # 增加batch维度(模型需要[batch, c, h, w])
    image_tensor = image_tensor.to(device)

    # 3. 预测(关闭梯度计算,提升速度)
    model.eval()  # 模型切换到评估模式(关闭Dropout/BatchNorm的训练模式)
    with torch.no_grad():
        output = model(image_tensor)
        # 计算置信度(softmax)和预测类别
        probabilities = torch.softmax(output, dim=1)
        max_prob, pred_idx = torch.max(probabilities, dim=1)

    # 4. 解析结果
    pred_class = class_names[pred_idx.item()]
    pred_conf = max_prob.item() * 100  # 转百分比

    return pred_class, pred_conf

class_names = [
    "八宝粥", "哈密瓜", "圣女果", "巴旦木", "板栗",
    "汉堡", "火龙果", "炸鸡", "瓜子", "生肉",
    "白萝卜", "胡萝卜", "草莓", "菠萝", "薯条",
    "蛋", "蛋挞", "青菜", "骨肉相连", "鸡翅"
]

import os
print("===== 食物分类预测工具 =====")
print("提示:输入图片路径(如test.jpg)进行预测,输入'q'退出程序\n")

while True:
    # 获取用户输入的图片路径
    image_path = input("请输入图片路径:").strip()

    # 输入q则退出循环
    if image_path.lower() == 'q':
        print("程序已退出!")
        break

    # 检查路径是否存在
    if not os.path.exists(image_path):
        print(f"错误:路径'{image_path}'不存在,请重新输入!\n")
        continue

    # 尝试预测(捕获图片格式错误等异常)
    try:
        pred_class, pred_conf = predict_single_image(image_path, model, class_names)
        # 输出预测结果
        print(f"预测结果:{pred_class}")
        print(f"置信度:{pred_conf:.2f}%\n")
    except Exception as e:
        print(f"错误:无法识别该文件(可能不是图片),错误信息:{str(e)}\n")

运行结果:

相关推荐
阿里云大数据AI技术4 小时前
面向 Interleaved Thinking 的大模型 Agent 蒸馏实践
人工智能
AI Echoes4 小时前
LangChain 非分割类型的文档转换器使用技巧
人工智能·python·langchain·prompt·agent
哔哔龙4 小时前
LangChain核心组件可用工具
人工智能
全栈独立开发者4 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring
2501_941878744 小时前
在班加罗尔工程实践中构建可持续演进的机器学习平台体系与技术实现分享
人工智能·机器学习
guoketg4 小时前
BERT的技术细节和面试问题汇总
人工智能·深度学习·bert
永远在Debug的小殿下4 小时前
SLAM开发环境(虚拟机的安装)
人工智能
MF_AI4 小时前
大型烟雾火灾检测识别数据集:25w+图像,2类,yolo标注
图像处理·人工智能·深度学习·yolo·计算机视觉
百家方案5 小时前
航空港应急安全科教园区 — 应急安全产业园建设项目投标技术方案
人工智能·智慧园区
奔跑草-5 小时前
【AI日报】每日AI最新消息2026-01-06
人工智能·github