深度学习模型优化、经典网络与进阶技术全解析(附完整 PyTorch 代码)

在计算机视觉与深度学习领域,模型性能的提升不仅依赖于网络结构的创新,更需要数据处理、训练策略、可视化验证等全流程的优化。本文基于实战经验,系统梳理数据获取与处理、模型优化、经典神经网络、注意力机制、可变形卷积五大核心板块,涵盖所有关键知识点,配套完整可运行的 PyTorch 代码与详细注释,助力从理论到工程落地的全面掌握。

一、模型优化基础:数据与训练策略

深度学习的三大核心要素是数据、算法(神经网络)、算力,其中数据质量与训练策略直接决定模型上限。本节从数据获取到训练可视化,完整覆盖模型优化的基础流程。

1. 数据获取:从来源到标注

高质量数据是模型训练的前提,根据成本与需求可选择以下 4 种获取方式:

1.1 数据集分类
  • 分类数据:用于图像分类任务,通常以目录形式按类别存储(如 MNIST 的 train/0、train/1 文件夹);
  • 标注数据:用于目标检测、图像分割任务,需包含坐标或掩码标注(如 VOC 格式的 xml 文件、COCO 格式的 json 文件)。
1.2 四大数据获取方法
方法 特点 工具 / 平台
开源数据集 免费、成本低 PyTorch 官方:https://pytorch.org/vision/stable/datasets.html
Kaggle:https://www.kaggle.com/datasets
Hugging Face:https://huggingface.co/datasets
Roboflow:https://public.roboflow.com/
外包平台 效果好、成本高 Amazon Mechanical Turk、阿里众包、百度数据众包、京东微工
手动采集与标注 质量高、效率低 标注工具:labelimg(安装:pip install labelimg)、labelme
网络爬虫 批量获取、需注意版权 Scrapy、BeautifulSoup、Selenium(需遵守网站 robots 协议)

2. 数据本地化:存储与加载

使用公开数据集时,需将数据保存到本地以便观察与复用,以下是完整实现方案:

2.1 图片本地化(以 MNIST 为例)

将数据集按类别保存为图片文件,方便后续可视化与重新处理:

import os
import torch
import torchvision
from torchvision import datasets, transforms
from PIL import Image

def save_mnist_to_local(datapath="./data", savedir="./MNIST"):
    """
    将MNIST数据集保存为本地图片(按类别分文件夹)
    :param datapath: 数据集下载路径
    :param savedir: 图片保存根目录
    """
    # 1. 创建保存目录(trainimgs/testimgs/类别)
    train_save_dir = os.path.join(savedir, "trainimgs")
    test_save_dir = os.path.join(savedir, "testimgs")
    os.makedirs(train_save_dir, exist_ok=True)
    os.makedirs(test_save_dir, exist_ok=True)

    # 2. 定义数据转换(转为Tensor后再转回PIL,便于保存)
    transform = transforms.Compose([transforms.ToTensor()])

    # 3. 加载训练集并保存
    train_set = datasets.MNIST(
        root=datapath, train=True, download=True, transform=transform
    )
    for idx, (img_tensor, label) in enumerate(train_set):
        # 按类别创建子目录
        label_dir = os.path.join(train_save_dir, str(label))
        os.makedirs(label_dir, exist_ok=True)
        # Tensor转PIL(MNIST为单通道灰度图,需指定模式"L")
        pil_img = transforms.ToPILImage()(img_tensor).convert("L")
        # 保存图片(命名格式:索引.png)
        pil_img.save(os.path.join(label_dir, f"{idx}.png"))

    # 4. 加载测试集并保存(逻辑与训练集一致)
    test_set = datasets.MNIST(
        root=datapath, train=False, download=True, transform=transform
    )
    for idx, (img_tensor, label) in enumerate(test_set):
        label_dir = os.path.join(test_save_dir, str(label))
        os.makedirs(label_dir, exist_ok=True)
        pil_img = transforms.ToPILImage()(img_tensor).convert("L")
        pil_img.save(os.path.join(label_dir, f"{idx}.png"))

    print(f"MNIST数据集已保存至 {savedir},训练集{len(train_set)}张,测试集{len(test_set)}张")

# 调用函数执行保存
if __name__ == "__main__":
    save_mnist_to_local()
2.2 本地图片加载

直接加载本地按目录存储的图片,无需重新下载数据集:

def load_local_images(img_root="./MNIST/trainimgs", batch_size=32):
    """
    加载本地图片数据集(ImageFolder要求目录按类别划分)
    :param img_root: 图片根目录
    :param batch_size: 批次大小
    :return: DataLoader对象
    """
    # 定义数据预处理(根据任务调整,此处为示例)
    transform = transforms.Compose([
        transforms.Resize((28, 28)),
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))  # MNIST的均值与标准差
    ])

    # 加载数据集(ImageFolder自动按子目录分类)
    dataset = datasets.ImageFolder(root=img_root, transform=transform)
    # 创建DataLoader(shuffle=True打乱训练数据)
    dataloader = torch.utils.data.DataLoader(
        dataset, batch_size=batch_size, shuffle=True, num_workers=2
    )

    print(f"加载完成:共{len(dataset)}张图片,{len(dataloader)}个批次")
    return dataloader

# 测试加载
train_loader = load_local_images()
2.3 本地图片序列化(Pickle+Tar)

将图片序列化为二进制格式(Pickle),再用 Tar 打包分发,便于跨环境传输:

import pickle
import tarfile

def serialize_images(img_root="./MNIST/trainimgs", save_tar="./mnist_serialized.tar"):
    """
    将本地图片序列化为Pickle并打包为Tar
    :param img_root: 图片根目录
    :param save_tar: Tar包保存路径
    """
    # 1. 加载图片并转换为列表(格式:(img_tensor, label))
    transform = transforms.Compose([transforms.Resize((28,28)), transforms.ToTensor()])
    dataset = datasets.ImageFolder(root=img_root, transform=transform)
    data_list = [(img.numpy(), label) for img, label in dataset]

    # 2. Pickle序列化
    pickle_data = pickle.dumps(data_list)

    # 3. Tar打包(压缩存储)
    with tarfile.open(save_tar, "w:gz") as tar:
        # 创建TarInfo对象(指定文件名)
        tar_info = tarfile.TarInfo(name="mnist_data.pkl")
        tar_info.size = len(pickle_data)
        # 写入Tar包
        tar.addfile(tar_info, fileobj=pickle.BytesIO(pickle_data))

    print(f"序列化完成,Tar包大小:{os.path.getsize(save_tar)/1024/1024:.2f}MB")

# 执行序列化
serialize_images()

3. 过拟合与欠拟合处理

模型训练中常见的两类问题,需针对性解决:

3.1 过拟合:训练集优、验证集差

过拟合是模型 “死记硬背” 训练数据的结果,核心解决方案是增加数据多样性限制模型复杂度,以下是 5 种实战方法:

3.1.1 数据增强(最有效)

通过随机变换生成 “新数据”,提升模型泛化能力。使用 PyTorch 的transforms实现,关键方法如下:

def get_augmentation_transform():
    """定义数据增强策略(适用于图像分类任务)"""
    return transforms.Compose([
        transforms.RandomRotation(45),  # 随机旋转(-45°~45°)
        transforms.RandomCrop(32, padding=4),  # 随机裁剪(补边后裁剪为32×32)
        transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转(50%概率)
        transforms.RandomVerticalFlip(p=0.5),  # 随机垂直翻转(50%概率)
        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),  # 亮度/对比度/饱和度调整
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2471, 0.2435, 0.2616))  # CIFAR-10的均值/标准差
    ])

# 多样本增强(Mixup:标签平滑,适用于分类任务)
def mixup_data(x, y, alpha=1.0):
    """
    Mixup数据增强:随机混合两张图片,标签按比例分配
    :param x: 输入特征(b, c, h, w)
    :param y: 标签(b, )
    :param alpha: 混合系数(alpha=0时不混合)
    :return: 混合后的特征与标签
    """
    if alpha > 0:
        lam = np.random.beta(alpha, alpha)  # 生成0~1的混合系数
    else:
        lam = 1.0
    batch_size = x.size()[0]
    index = torch.randperm(batch_size).cuda()  # 随机打乱索引

    # 混合特征与标签
    mixed_x = lam * x + (1 - lam) * x[index, :]
    y_a, y_b = y, y[index]
    return mixed_x, y_a, y_b, lam
3.1.2 标准化(Normalization)

将特征值归一化到相近范围,加速训练收敛并减少过拟合:

  • 公式:output = (input - mean) / std
  • 示例:MNIST 标准化Normalize((0.1307,), (0.3081,)),CIFAR-10 标准化Normalize((0.4914, 0.4822, 0.4465), (0.2471, 0.2435, 0.2616))
3.1.3 Dropout(随机失活)

训练时随机 “关闭” 部分神经元,防止模型过度依赖特定特征:

除了数据增强和 Dropout,以下技术也是模型训练中不可或缺的优化手段:

  1. 网络层归一化(Normalization) 除了对输入数据进行标准化,在神经网络内部使用归一化层可以极大地加速训练并提高模型稳定性,防止梯度消失或爆炸。

    • Batch Normalization (批量归一化):在小批量数据上对每个通道进行归一化,是目前最常用的归一化技术。

    • Layer Normalization (层归一化):对单个样本的所有通道进行归一化,常用于 NLP 任务的 Transformer 模型中。

    • Group Normalization (组归一化):将通道分组进行归一化,不受批量大小限制,在批量很小(如目标检测)时效果优于 BN。

  2. 更丰富的正则化方法

    • L1/L2 正则化:在损失函数中加入模型参数的 L1 或 L2 范数(也叫权重衰减),通过惩罚过大的权重来防止过拟合。

    • 标签平滑(Label Smoothing):在分类任务中,用一个很小的值(如 0.1)代替硬标签(如 1),防止模型对正确标签的预测概率过高,从而提高泛化能力。

  3. 学习率调度器(Learning Rate Scheduler) 在训练过程中动态调整学习率,帮助模型跳出局部最优,达到更好的收敛效果。

    • torch.optim.lr_scheduler.StepLR: 每隔固定步数降低学习率。

    • torch.optim.lr_scheduler.CosineAnnealingLR: 使用余弦函数曲线调整学习率,平滑且有效。

    • torch.optim.lr_scheduler.ReduceLROnPlateau: 当某个指标(如验证集损失)不再改善时,降低学习率。

class CNNWithDropout(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.5)  # 50%概率失活
        self.fc = nn.Linear(64*32*32, 10)

    def forward(self, x):
        x = self.relu(self.conv1(x))
        x = self.dropout(x)  # 仅训练时生效(eval()模式下自动关闭)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x
3.2 欠拟合:训练集与验证集均差

欠拟合是模型复杂度不足的结果,解决方案:

  • 增加模型层数(如从 LeNet5 改为 ResNet18);
  • 延长训练周期(增加 epochs);
  • 减少正则化强度(如降低 Dropout 概率)。

4. 训练过程可视化:实时监控与调试

可视化是发现训练问题(如梯度消失、过拟合)的关键工具,常用WandB(在线)与TensorBoard(本地)。

4.1 WandB(Weights & Biases):在线可视化

支持多设备同步、实验对比,适合团队协作:

import wandb

# 1. 安装与登录(首次使用需在官网获取API Key)
# !pip install wandb
# wandb login  # 执行后输入API Key

# 2. 初始化项目
wandb.init(
    project="cifar10_cnn",  # 项目名
    config={  # 超参数记录(便于后续对比)
        "learning_rate": 0.001,
        "architecture": "CNN",
        "dataset": "CIFAR-10",
        "batch_size": 32,
        "epochs": 10
    }
)

# 3. 训练过程中记录指标
model = CNNWithDropout().cuda()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

for epoch in range(10):
    model.train()
    total_loss = 0.0
    correct = 0
    total = 0
    for inputs, labels in train_loader:
        inputs, labels = inputs.cuda(), labels.cuda()
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # 计算指标
        total_loss += loss.item() * inputs.size(0)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    # 记录epoch级指标(损失、准确率)
    avg_loss = total_loss / total
    acc = correct / total
    wandb.log({"epoch": epoch, "train_loss": avg_loss, "train_acc": acc})
    print(f"Epoch {epoch}: Loss={avg_loss:.4f}, Acc={acc:.4f}")

# 4. 记录模型与结束实验
wandb.watch(model, log="all")  # 记录模型结构与梯度
wandb.finish()  # 必须调用,确保数据保存
4.2 TensorBoard:本地可视化

无需联网,适合个人调试:

from torch.utils.tensorboard import SummaryWriter

# 1. 初始化写入器(日志保存路径)
log_dir = "./tensorboard_logs"
writer = SummaryWriter(log_dir=log_dir)

# 2. 训练过程中记录指标(损失、准确率)
for epoch in range(10):
    # ...(训练逻辑同上)
    avg_loss = total_loss / total
    acc = correct / total
    # 记录 scalar 指标(曲线形式)
    writer.add_scalar("Loss/Train", avg_loss, epoch)
    writer.add_scalar("Accuracy/Train", acc, epoch)

    # 3. 记录模型结构(需输入示例张量)
    if epoch == 0:
        dummy_input = torch.randn(1, 3, 32, 32).cuda()
        writer.add_graph(model, dummy_input)

    # 4. 记录模型参数分布(直方图)
    for name, param in model.named_parameters():
        writer.add_histogram(f"Params/{name}", param.clone().cpu().data.numpy(), epoch)

# 5. 关闭写入器
writer.close()

# 6. 启动TensorBoard(终端执行)
# !pip install tensorboard
# tensorboard --logdir=tensorboard_logs
# 访问 http://localhost:6006 查看结果

5. 验证结果数据化:量化评估模型

仅看训练指标不够,需通过混淆矩阵、分类报告等量化验证结果,定位模型薄弱环节。

5.1 验证结果保存为 Excel(CSV)

记录所有样本的预测概率与真实标签,便于后续分析:

import pandas as pd
import numpy as np

def save_validation_results(model, val_loader, class_names, save_path="./val_results.csv"):
    """
    保存验证结果到CSV(包含预测概率、真实标签、类别名)
    :param model: 训练好的模型
    :param val_loader: 验证集DataLoader
    :param class_names: 类别名列表(如["cat", "dog", ...])
    :param save_path: CSV保存路径
    """
    model.eval()
    all_data = []  # 存储所有样本的结果

    with torch.no_grad():
        for inputs, labels in val_loader:
            inputs, labels = inputs.cuda(), labels.cuda()
            outputs = model(inputs)
            # 计算预测概率(softmax)
            probs = torch.softmax(outputs, dim=1).cpu().numpy()
            # 真实标签与类别名
            true_labels = labels.cpu().numpy()
            true_names = np.array([class_names[label] for label in true_labels])

            # 拼接数据(概率 + 真实标签 + 真实类别名)
            batch_data = np.concatenate([
                probs,
                true_labels.reshape(-1, 1),
                true_names.reshape(-1, 1)
            ], axis=1)
            all_data.append(batch_data)

    # 合并所有批次并保存为CSV
    all_data = np.vstack(all_data)
    # 列名:类别概率 + "target"(真实标签) + "true_class"(真实类别名)
    columns = class_names + ["target", "true_class"]
    df = pd.DataFrame(all_data, columns=columns)
    df.to_csv(save_path, encoding="GBK", index=False)
    print(f"验证结果已保存至 {save_path},共{len(df)}个样本")

# 调用函数(示例:CIFAR-10类别名)
cifar10_classes = ["airplane", "car", "bird", "cat", "deer", "dog", "frog", "horse", "ship", "truck"]
save_validation_results(model, val_loader, cifar10_classes)
5.2 混淆矩阵与关键指标计算

通过sklearn计算准确率、精确度、召回率、F1 分数,并可视化混淆矩阵:

from sklearn.metrics import confusion_matrix, classification_report, accuracy_score, precision_score, recall_score, f1_score
import matplotlib.pyplot as plt

def analyze_validation_results(csv_path="./val_results.csv", class_names=None):
    """
    分析验证结果:混淆矩阵 + 分类报告 + 关键指标
    :param csv_path: 验证结果CSV路径
    :param class_names: 类别名列表
    """
    # 1. 读取CSV数据
    df = pd.read_csv(csv_path, encoding="GBK")
    # 提取真实标签与预测概率
    true_labels = df["target"].astype(int).values
    pred_probs = df[class_names].values
    pred_labels = np.argmax(pred_probs, axis=1)  # 预测标签(概率最大的类别)

    # 2. 计算关键指标(macro:对所有类别取平均)
    acc = accuracy_score(true_labels, pred_labels)
    precision = precision_score(true_labels, pred_labels, average="macro")
    recall = recall_score(true_labels, pred_labels, average="macro")
    f1 = f1_score(true_labels, pred_labels, average="macro")

    # 3. 打印指标与分类报告
    print("="*50)
    print(f"准确率(Accuracy): {acc:.4f}")
    print(f"精确度(Precision): {precision:.4f}")
    print(f"召回率(Recall): {recall:.4f}")
    print(f"F1分数(F1-Score): {f1:.4f}")
    print("="*50)
    print("分类报告:")
    print(classification_report(true_labels, pred_labels, target_names=class_names))

    # 4. 绘制混淆矩阵
    cm = confusion_matrix(true_labels, pred_labels)
    plt.rcParams["font.sans-serif"] = ["SimHei"]  # 解决中文乱码
    plt.figure(figsize=(10, 8))
    plt.matshow(cm, cmap=plt.cm.Greens, fignum=1)
    plt.colorbar()  # 颜色条(表示样本数量)

    # 在混淆矩阵中添加数值标签
    for i in range(len(cm)):
        for j in range(len(cm)):
            plt.annotate(
                cm[i, j],
                xy=(j, i),
                horizontalalignment="center",
                verticalalignment="center",
                fontsize=10
            )

    # 设置坐标轴与标题
    plt.xlabel("预测标签(Predicted Label)")
    plt.ylabel("真实标签(True Label)")
    plt.xticks(range(len(class_names)), class_names, rotation=45)
    plt.yticks(range(len(class_names)), class_names)
    plt.title("验证集混淆矩阵")
    plt.tight_layout()
    plt.savefig("./confusion_matrix.png", dpi=300)
    print("混淆矩阵已保存至 ./confusion_matrix.png")

# 调用分析函数
analyze_validation_results("./val_results.csv", cifar10_classes)

二、经典神经网络模型:从基础到移动端优化

经典网络是深度学习的基石,从 LeNet5 到 MobileNet 系列,每一代网络都针对特定问题进行创新。本节梳理 6 种核心网络的结构、创新点与实战代码。

1. LeNet5:卷积神经网络的开山之作

1998 年由 Yann LeCun 提出,首个成功应用于手写数字识别(邮政行业)的 CNN,奠定了 “卷积 - 池化 - 全连接” 的经典架构。

1.1 网络结构(共 7 层)
  • 输入:32×32×1(灰度图);
  • 卷积层(3 层):C1(6 个 5×5 核)→ C3(16 个 5×5 核,非密集连接)→ C5(120 个 5×5 核);
  • 池化层(2 层):S2(2×2 平均池化)→ S4(2×2 平均池化);
  • 全连接层(2 层):F6(84 个神经元)→ Output(10 个神经元,对应 0-9);
  • 激活函数:Sigmoid(早期无 ReLU)。
1.2 PyTorch 实现
class LeNet5(nn.Module):
    def __init__(self, num_classes=10):
        super(LeNet5, self).__init__()
        # C1: 卷积 + ReLU(替换原Sigmoid,提升性能)
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=0)  # 32×32→28×28
        self.relu = nn.ReLU(inplace=True)
        # S2: 平均池化(2×2,步长2)→28×28→14×14
        self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
        
        # C3: 卷积(非密集连接,此处简化为普通卷积)→14×14→10×10
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5, padding=0)
        # S4: 平均池化→10×10→5×5
        self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
        
        # C5: 卷积(等效全连接,5×5→1×1)→16×5×5→120
        self.conv3 = nn.Conv2d(16, 120, kernel_size=5, padding=0)
        
        # F6: 全连接→120→84
        self.fc1 = nn.Linear(120, 84)
        # Output: 全连接→84→num_classes
        self.fc2 = nn.Linear(84, num_classes)

    def forward(self, x):
        x = self.pool1(self.relu(self.conv1(x)))
        x = self.pool2(self.relu(self.conv2(x)))
        x = self.relu(self.conv3(x))
        x = x.view(x.size(0), -1)  # 展平:(b, 120, 1, 1)→(b, 120)
        x = self.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# 测试模型
model = LeNet5()
input = torch.randn(1, 1, 32, 32)  # 符合LeNet5输入尺寸
output = model(input)
print(f"LeNet5输出尺寸: {output.shape}")  # torch.Size([1, 10])

2. AlexNet:深度学习浪潮的推动者

2012 年 ImageNet 竞赛冠军(top-5 错误率 15.3%,远超第二名 26.2%),首次证明深度学习在图像分类的优越性。

2.1 核心创新
  • GPU 并行训练:使用两块 GTX 580 GPU,将模型参数拆分到不同 GPU,减少显存占用;
  • ReLU 激活函数:替换 Sigmoid,解决梯度消失问题(f(x)=max(0,x));
  • LRN 局部响应归一化:借鉴 “侧抑制” 原理,增强泛化能力(现被 BatchNorm 替代);
  • Dropout:全连接层随机失活 50% 神经元,防止过拟合;
  • 数据增强:随机裁剪、翻转、颜色抖动,提升模型鲁棒性。
2.2 网络结构(共 8 层)
  • 输入:224×224×3(实际训练为 227×227);
  • 卷积层(5 层):5 个卷积层 + 3 个全连接层,最终输出 1000 类(ImageNet)。

3. VGGNet:小卷积核的深度胜利

2014 年 ImageNet 竞赛第二名,核心创新是用多个 3×3 小卷积核替代大卷积核,在减少参数量的同时增加网络深度。

3.1 关键设计
  • 小卷积核优势:3 个 3×3 卷积的感受野与 1 个 7×7 卷积相同,但参数量更少(3×3×3=27 < 7×7=49),且引入更多非线性;
  • 网络版本:根据深度分为 VGG11、VGG13、VGG16、VGG19,常用 VGG16(D 版本);
  • 缺陷:参数量大(VGG16 约 138M),不适合移动端。
3.2 VGG16 结构示例(D 版本)
def vgg16_config():
    """VGG16的网络配置(D版本)"""
    config = [
        64, 64, "M",  # 第1块:2卷积+池化
        128, 128, "M",  # 第2块
        256, 256, 256, "M",  # 第3块
        512, 512, 512, "M",  # 第4块
        512, 512, 512, "M",  # 第5块
        "FC", 4096, "FC", 4096, "FC", 1000  # 全连接层
    ]
    return config

4. GoogleNet(InceptionNet):多尺度特征捕获

2014 年 ImageNet 竞赛冠军,核心创新是Inception 模块,通过并行不同尺度的卷积与池化,高效捕获多尺度特征。

4.1 Inception 模块设计
  • 并行分支:1×1 卷积(降维)、3×3 卷积(中尺度)、5×5 卷积(大尺度)、3×3 池化(下采样);
  • 优势:无需人工设计卷积核尺寸,模型自动学习多尺度特征;
  • 辅助分类器:在 Inception4a、4d 插入两个辅助分类器(训练时权重 0.3),缓解梯度消失。

5. ResNet:残差结构解决深度难题

2015 年由何凯明提出,通过残差连接(Residual Connection)解决深层网络训练困难问题,可训练 152 层甚至更深的网络。

5.1 核心创新:残差结构
  • 原理:主分支进行卷积操作,shortcut 分支直接传递输入特征,两者相加后激活(H(x) = F(x) + x);
  • 两种结构
    • 基础结构(ResNet18/34):2 个 3×3 卷积;
    • Bottleneck 结构(ResNet50/101/152):1×1(降维)→3×3(特征提取)→1×1(升维),减少计算量。
5.2 ResNet50 Bottleneck 实现
class Bottleneck(nn.Module):
    expansion = 4  # 输出通道扩展倍数(1×1卷积升维)

    def __init__(self, inplanes, planes, stride=1, downsample=None):
        super(Bottleneck, self).__init__()
        # 1×1卷积:降维(inplanes→planes)
        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        # 3×3卷积:特征提取(步长stride控制下采样)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        # 1×1卷积:升维(planes→planes×expansion)
        self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(planes * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        # shortcut分支:当步长≠1或通道不匹配时,用1×1卷积调整
        self.downsample = downsample

    def forward(self, x):
        residual = x  # shortcut分支

        out = self.relu(self.bn1(self.conv1(x)))
        out = self.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))

        # 调整shortcut分支维度(若需要)
        if self.downsample is not None:
            residual = self.downsample(x)

        # 残差连接:主分支 + shortcut分支
        out += residual
        out = self.relu(out)

        return out

# ResNet50整体结构(基于Bottleneck)
class ResNet50(nn.Module):
    def __init__(self, num_classes=1000):
        super(ResNet50, self).__init__()
        self.inplanes = 64
        # 初始卷积层(7×7,步长2)
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # 4个残差块(Bottleneck)
        self.layer1 = self._make_layer(Bottleneck, 64, 3)  # 64×3个Bottleneck
        self.layer2 = self._make_layer(Bottleneck, 128, 4, stride=2)
        self.layer3 = self._make_layer(Bottleneck, 256, 6, stride=2)
        self.layer4 = self._make_layer(Bottleneck, 512, 3, stride=2)
        # 全局平均池化 + 全连接
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * Bottleneck.expansion, num_classes)

    def _make_layer(self, block, planes, blocks, stride=1):
        """创建残差块层"""
        downsample = None
        # 若步长≠1或输入通道≠输出通道,创建downsample调整维度
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion),
            )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.relu(self.bn1(self.conv1(x)))
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)

        return x

6. MobileNet 系列:移动端高效网络

专为移动设备设计,通过深度可分离卷积大幅减少参数量与计算量,兼顾精度与速度。

6.1 MobileNet V1:深度可分离卷积
  • 核心创新:将标准卷积拆分为 “深度卷积(DW)+ 点卷积(PW)”:
    • 深度卷积(DW):1 个卷积核对应 1 个输入通道,仅提取空间特征;
    • 点卷积(PW):1×1 卷积,融合通道特征;
  • 参数量减少:标准卷积参数量 / (DW+PW) 参数量 ≈ (1/DW 通道数) + (1/PW 通道数),通常减少 8-9 倍。
6.2 MobileNet V2:倒残差与 ReLU6
  • 倒残差结构:先通过 1×1 卷积升维(扩展通道),再 DW 卷积,最后 1×1 卷积降维(与 ResNet 相反);
  • ReLU6 激活函数f(x)=min(max(x,0),6),限制输出范围,适合移动端量化。
6.3 MobileNet V3:SE 模块与 h-swish
  • SE 通道注意力:在倒残差中加入 SE 模块,增强有用通道特征;
  • h-swish 激活函数f(x)=x * ReLU6(x+3)/6,替代 Swish,减少计算量且适合量化;
  • 两个版本:MobileNetV3-Large(高精度)、MobileNetV3-Small(高速度)。

三、进阶技术:注意力机制与可变形卷积

1. 注意力机制:让模型 “聚焦关键特征”

注意力机制使模型动态关注输入中重要的部分,分为通道注意力、空间注意力、混合注意力三类。

1.1 通道注意力:SE 模块(Squeeze-and-Excitation)
class SEBlock(nn.Module):
    def __init__(self, channel, reduction=16):
        super(SEBlock, self).__init__()
        # Squeeze:全局平均池化(压缩空间维度)
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        # Excitation:全连接层(学习通道权重)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channel // reduction, channel, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        # 压缩→激励→调整形状
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        # 应用通道权重
        return x * y
1.2 空间注意力:Spatial Attention
class SpatialAttention(nn.Module):
    def __init__(self, kernel_size=7):
        super(SpatialAttention, self).__init__()
        self.conv = nn.Conv2d(2, 1, kernel_size=kernel_size, padding=kernel_size//2, bias=False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        # 通道维度池化(Max+Avg)
        max_out, _ = torch.max(x, dim=1, keepdim=True)
        avg_out = torch.mean(x, dim=1, keepdim=True)
        # 融合池化结果→卷积学习空间权重
        x_cat = torch.cat([max_out, avg_out], dim=1)
        spatial_weight = self.sigmoid(self.conv(x_cat))
        # 应用空间权重
        return x * spatial_weight
1.3 混合注意力:CBAM 模块(通道 + 空间串行)
class CBAM(nn.Module):
    def __init__(self, channel, reduction=16, spatial_kernel_size=7):
        super(CBAM, self).__init__()
        self.channel_att = SEBlock(channel, reduction)  # 通道注意力
        self.spatial_att = SpatialAttention(spatial_kernel_size)  # 空间注意力

    def forward(self, x):
        # 先通道加权,再空间加权(串行最优)
        x = x * self.channel_att(x)
        x = self.spatial_att(x)
        return x

2. 可变形卷积:适应目标变形

传统卷积核形状固定,难以处理目标的非刚性变形(如扭曲、缩放)。可变形卷积通过学习偏移量,动态调整卷积核采样位置。

2.1 核心原理(DCNv1)
  1. 学习偏移量:通过额外卷积层预测每个卷积核位置的偏移量(x、y 方向);
  2. 双线性插值:偏移量通常为非整数,通过插值计算采样值;
  3. 自适应采样:根据偏移量调整卷积核位置,贴合目标形状。
2.2 论文参考

四、模型移植:ONNX 跨框架部署

训练好的 PyTorch 模型需转为 ONNX 格式,才能在 Python、Java、C++ 等环境中推理,且不依赖 PyTorch 框架。

1. ONNX 导出(以 ResNet18 为例)

def export_model_to_onnx(model, input_shape, onnx_path="./resnet18.onnx"):
    """
    将PyTorch模型导出为ONNX格式
    :param model: 训练好的模型
    :param input_shape: 输入形状(如(1,3,224,224))
    :param onnx_path: ONNX保存路径
    """
    model.eval()
    # 创建示例输入
    dummy_input = torch.randn(*input_shape).cuda()
    # 导出ONNX
    torch.onnx.export(
        model,
        dummy_input,
        onnx_path,
        verbose=True,  # 打印导出过程
        input_names=["input"],  # 输入节点名
        output_names=["output"],  # 输出节点名
        opset_version=11  # ONNX版本(兼容多数框架)
    )
    print(f"模型已导出为ONNX:{onnx_path}")

# 导出ResNet18
from torchvision.models import resnet18
model = resnet18(pretrained=False, num_classes=10).cuda()
model.load_state_dict(torch.load("./resnet18.pth"))  # 加载权重
export_model_to_onnx(model, input_shape=(1, 3, 224, 224))

2. ONNX 推理(CPU/GPU)

import onnxruntime as ort
import cv2
import numpy as np

def onnx_inference(onnx_path, img_path, class_names, use_gpu=True):
    """
    ONNX模型推理(支持CPU/GPU)
    :param onnx_path: ONNX模型路径
    :param img_path: 测试图片路径
    :param class_names: 类别名列表
    :param use_gpu: 是否使用GPU推理
    :return: 预测类别与概率
    """
    # 1. 初始化ONNX Runtime(CPU/GPU选择)
    providers = ["CUDAExecutionProvider"] if use_gpu else ["CPUExecutionProvider"]
    sess = ort.InferenceSession(onnx_path, providers=providers)

    # 2. 图片预处理(与训练时一致)
    img = cv2.imread(img_path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # BGR→RGB
    img = cv2.resize(img, (224, 224))  # 调整尺寸
    img = img / 255.0  # 归一化
    img = (img - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225])  # 标准化
    img = img.transpose(2, 0, 1)  # HWC→CHW
    img = np.expand_dims(img, axis=0).astype(np.float32)  # 增加batch维度

    # 3. 推理
    input_name = sess.get_inputs()[0].name
    output_name = sess.get_outputs()[0].name
    outputs = sess.run([output_name], {input_name: img})[0]

    # 4. 解析结果
    probs = torch.softmax(torch.tensor(outputs), dim=1).numpy()[0]
    pred_idx = np.argmax(probs)
    pred_class = class_names[pred_idx]
    pred_prob = probs[pred_idx]

    print(f"预测类别:{pred_class},概率:{pred_prob:.4f}")
    return pred_class, pred_prob

# 调用推理函数(CIFAR-10类别)
onnx_inference("./resnet18.onnx", "./test.jpg", cifar10_classes, use_gpu=True)

五、总结

本文从数据处理、模型优化、经典网络到进阶技术,完整覆盖深度学习计算机视觉的核心知识点,配套的 PyTorch 代码可直接应用于实际项目。关键总结如下:

  1. 数据与训练:数据质量决定上限,过拟合需通过数据增强、Dropout 解决,可视化工具(WandB/TensorBoard)是调试关键;
  2. 经典网络:从 LeNet5 的基础架构,到 ResNet 的残差创新,再到 MobileNet 的移动端优化,每一代网络都针对特定痛点;
  3. 进阶技术:注意力机制(SE/CBAM)让模型聚焦关键特征,可变形卷积适应目标变形,显著提升复杂任务性能;
  4. 部署落地:ONNX 格式实现跨框架部署,满足工业界多环境需求

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐