深度学习:残差网络ResNet与迁移学习

发布于:2025-09-07 ⋅ 阅读:(22) ⋅ 点赞:(0)


在卷积神经网络(CNN)的发展历程中,网络深度的增加曾被认为是提升模型性能的关键。然而,当网络深度超过一定限度后,会出现 梯度消失/爆炸退化问题,导致模型训练困难、性能不升反降。2015 年,何凯明等人提出的残差网络(ResNet)彻底打破了这一瓶颈,通过创新的残差结构和 Batch Normalization(BN)技术,成功训练出上千层的深层网络,不仅斩获当年 ImageNet 竞赛多项冠军,更成为后续深度学习模型设计的“基石”。本文将从 ResNet 的痛点出发,深入剖析其原理、结构,并结合 PyTorch 实现实战,带大家全面掌握这一经典网络。


一、ResNet 诞生的背景:深层 CNN 的两大“致命”问题

在 ResNet 出现之前,研究者们发现,当 CNN 的层数堆叠到一定程度(如超过 20 层),模型会出现两个难以解决的问题,直接限制了深层网络的应用。

1.1 梯度消失与梯度爆炸

神经网络的训练依赖反向传播:通过计算损失函数对各层参数的梯度,迭代更新权重。但在深层网络中,梯度会沿着网络层“反向传播”时逐渐放大或缩小,最终导致训练失效。

  • 梯度消失:若每一层的梯度都小于 1(如激活函数使用 Sigmoid 时,导数最大值仅 0.25),经过多层传播后,梯度会呈指数级衰减,趋近于 0。此时,浅层网络的参数几乎无法更新,模型陷入“停滞”,ResNet 出现前的深层网络常因此训练失败。
  • 梯度爆炸:若每一层的梯度都大于 1,反向传播时梯度会呈指数级增长,最终超出计算机的数值表示范围(如 NaN),导致训练崩溃。

传统解决方案(如参数初始化、使用 ReLU 激活函数)只能缓解浅层网络的问题,对深层网络(如 50 层以上)效果有限。

1.2 退化问题(Degradation)

除了梯度问题,深层网络还会出现“退化”:当网络层数增加时,训练误差和测试误差反而会上升,且这种上升并非由过拟合导致(过拟合仅表现为测试误差上升,训练误差下降)。

下图是论文中 20 层和 56 层 CNN 的性能对比:随着层数从 20 层增加到 56 层,训练误差和测试误差均显著上升,说明深层网络不仅没有学到更复杂的特征,反而丢失了浅层网络的学习能力。
在这里插入图片描述

退化问题的本质是:深层网络难以学习“恒等映射”(即让输入等于输出, H ( x ) = x H(x) = x H(x)=x)。当某一层的最优映射就是恒等映射时,浅层网络可直接拟合,但深层网络需要通过复杂的参数调整来逼近这一简单映射,反而容易引入误差。


二、ResNet 的核心创新:残差结构与 Batch Normalization

为解决上述两大问题,ResNet 提出了两个关键设计:残差结构(Residual Block)Batch Normalization(BN 层)

2.1 残差结构:让网络“跳过”层,直接学习残差

ResNet 的核心思想是:不要求深层网络直接学习复杂的映射 H ( x ) H(x) H(x),而是学习“残差” F ( x ) = H ( x ) − x F(x) = H(x) - x F(x)=H(x)x。此时,原映射可表示为 H ( x ) = F ( x ) + x H(x) = F(x) + x H(x)=F(x)+x,其中 x x x 是输入特征,通过“shortcut 连接”(捷径连接)直接传递到输出端,与 F ( x ) F(x) F(x) 相加。

这种结构的优势在于:

  • 若最优映射是恒等映射( H ( x ) = x H(x) = x H(x)=x),则网络只需让 F ( x ) = 0 F(x) = 0 F(x)=0 即可,无需复杂参数调整,从根本上解决了退化问题;
  • shortcut 连接直接传递梯度,避免了梯度在深层传播时的衰减,缓解了梯度消失问题。

2.1.1 残差结构的两种形式

根据输入输出特征图的维度是否一致,残差结构分为 恒等映射(Identity Mapping)维度匹配(Dimensionality Matching) 两种:

类型 适用场景 结构特点
恒等映射 输入、输出特征图的通道数、尺寸完全一致(如同一阶段的残差块) shortcut 连接直接传递 x x x,无需额外参数
维度匹配 输入、输出特征图的通道数或尺寸不同(如不同阶段的过渡) shortcut 连接需通过 1 × 1 1×1 1×1 卷积调整 x x x 的维度,确保 F ( x ) F(x) F(x) x x x 可相加

两种结构的示意图如下:
在这里插入图片描述
在这里插入图片描述

以常用的 3 × 3 3×3 3×3 卷积残差块为例,恒等映射的计算流程为:

  1. 输入 x x x 经过第一个 3 × 3 3×3 3×3 卷积层,激活函数为 ReLU;
  2. 经过第二个 3 × 3 3×3 3×3 卷积层(无激活);
  3. 通过 shortcut 连接将原始输入 x x x 与第二步的输出相加;
  4. 最后经过 ReLU 激活,得到残差块的输出。

2.2 Batch Normalization(BN 层):解决梯度消失

为进一步缓解梯度消失问题,ResNet 引入了 Batch Normalization(批量归一化) 技术,其核心作用是:对每一层的输入特征进行归一化,使特征分布满足“均值为 0、方差为 1”,避免因特征值过大或过小导致梯度消失。

2.2.1 BN 层的工作原理

BN 层通常插入在卷积层之后、激活函数之前,具体步骤如下:

  1. 计算批次均值和方差:对当前批次(Batch)的特征图,计算每个通道的均值 μ \mu μ 和方差 σ 2 \sigma^2 σ2
  2. 归一化:将特征图的每个元素减去均值、除以标准差(加 ϵ \epsilon ϵ 避免分母为 0),得到归一化后的特征 x ^ = x − μ σ 2 + ϵ \hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} x^=σ2+ϵ xμ
  3. 缩放与偏移:引入可学习参数 γ \gamma γ(缩放因子)和 β \beta β(偏移因子),对归一化后的特征进行调整: y = γ ⋅ x ^ + β y = \gamma \cdot \hat{x} + \beta y=γx^+β,确保网络可自主学习最优的特征分布。

2.2.2 BN 层的优势

  • 加速训练收敛:归一化后的特征分布更稳定,可使用更大的学习率;
  • 缓解梯度消失:避免特征值过大导致激活函数进入饱和区(如 Sigmoid);
  • 降低过拟合:批次内的随机归一化相当于一种轻微的数据增强。

三、ResNet 的网络结构:从 18 层到 152 层的演变

ResNet 通过堆叠残差块形成不同深度的网络,常见的有 ResNet-18、ResNet-34、ResNet-50、ResNet-101、ResNet-152 等。其核心区别在于残差块的类型和数量:

  • 浅层 ResNet(18/34 层):使用2 个 3 × 3 3×3 3×3 卷积组成的残差块(Basic Block)
  • 深层 ResNet(50/101/152 层):使用** 1 × 1 + 3 × 3 + 1 × 1 1×1 + 3×3 + 1×1 1×1+3×3+1×1 卷积组成的残差块(Bottleneck Block)**,减少参数数量和计算量。

3.1 ResNet 的整体结构

以 ResNet-50 为例,网络分为 5 个阶段(conv1 ~ conv5_x),每个阶段由多个残差块组成,最后通过全局平均池化和全连接层输出分类结果。具体结构如下表:

阶段 层类型 输出尺寸 残差块数量 卷积核参数
conv1 7×7 卷积 + BN + ReLU 112×112×64 - 7×7, 64, stride=2
- 3×3 最大池化 56×56×64 - stride=2
conv2_x Bottleneck Block 56×56×256 3 1×1(64) → 3×3(64) → 1×1(256)
conv3_x Bottleneck Block 28×28×512 4 1×1(128) → 3×3(128) → 1×1(512)
conv4_x Bottleneck Block 14×14×1024 6 1×1(256) → 3×3(256) → 1×1(1024)
conv5_x Bottleneck Block 7×7×2048 3 1×1(512) → 3×3(512) → 1×1(2048)
- 全局平均池化 1×1×2048 - -
fc 全连接层 1×1×1000 - 2048 → 1000(ImageNet 分类)

注:stride=2 的卷积层用于缩小特征图尺寸, 1 × 1 1×1 1×1 卷积用于调整通道数(维度匹配)。

3.2 不同深度 ResNet 的对比

不同深度的 ResNet 在参数数量、计算量(FLOPs)和性能上存在差异,实际应用中需根据任务需求选择:

网络型号 残差块类型 总层数 参数数量(M) FLOPs( 1 0 9 10^9 109 ImageNet Top-1 准确率
ResNet-18 Basic Block 18 11.7 1.8 69.76%
ResNet-34 Basic Block 34 21.8 3.6 73.31%
ResNet-50 Bottleneck Block 50 25.6 3.8 76.13%
ResNet-101 Bottleneck Block 101 44.5 7.6 77.37%
ResNet-152 Bottleneck Block 152 60.2 11.3 78.31%

可以看出:随着深度增加,ResNet 的准确率逐渐提升,但参数数量和计算量也随之增长。因此,在资源有限的场景(如移动端),ResNet-18/34 更合适;而在追求高准确率的场景(如服务器端图像分类),可选择 ResNet-50/101。


四、ResNet 实战:PyTorch 实现与迁移学习

ResNet 不仅是经典的网络结构,更是迁移学习的“利器”。PyTorch 的 torchvision.models 库提供了预训练的 ResNet 模型,可直接用于新任务的微调。下面以“人脸关键点预测”(回归任务)为例,演示 ResNet 的实现与迁移学习。

4.1 任务背景

人脸关键点预测是典型的回归任务:输入一张人脸图像,输出 68 个关键点的坐标(共 136 个数值,每个关键点含 x、y 坐标)。由于数据集通常较小,直接训练深层网络易过拟合,因此采用迁移学习:基于预训练的 ResNet-18,微调全连接层以适应回归任务。

4.2 实现步骤

步骤 1:导入依赖库

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torchvision import models, transforms
import pandas as pd
import cv2

步骤 2:定义数据集类(人脸关键点数据集)

假设数据集包含图像路径和对应的关键点坐标(标签已归一化到 0~1):

class FaceKeyPointDataset(Dataset):
    def __init__(self, csv_file, root_dir, transform=None):
        self.key_pts_frame = pd.read_csv(csv_file)
        self.root_dir = root_dir
        self.transform = transform

    def __len__(self):
        return len(self.key_pts_frame)

    def __getitem__(self, idx):
        # 读取图像(假设路径在csv第0列)
        img_name = os.path.join(self.root_dir, self.key_pts_frame.iloc[idx, 0])
        img = cv2.imread(img_name)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 转为RGB
        img = cv2.resize(img, (224, 224))  # 统一尺寸
        img = img.transpose((2, 0, 1))  # (H,W,C) → (C,H,W),适应PyTorch
        label = self.key_pts_frame.iloc[idx, 1:].values.astype('float')
        label = (label + 1) / 2  # 假设原始标签为[-1,1],归一化到[0,1]
        label = torch.tensor(label, dtype=torch.float32)
        # 应用数据增强
        if self.transform:
            img = self.transform(img)
        return img, label

步骤 3:加载预训练 ResNet,修改全连接层

预训练的 ResNet-18 默认输出 1000 类(ImageNet 分类),需将最后一层全连接层改为输出 136 个值(回归任务):

class ResNetKeyPointModel(nn.Module):
    def __init__(self, pretrained=True):
        super(ResNetKeyPointModel, self).__init__()
        self.resnet = models.resnet18(pretrained=pretrained)
        # 修改最后一层全连接层
        self.resnet.fc = nn.Linear(self.resnet.fc.in_features, 136)  # 回归136个关键点
        self.sigmoid = nn.Sigmoid()  # 确保输出在[0,1]

    def forward(self, x):
        x = self.resnet(x)
        x = self.sigmoid(x)
        return x

步骤 4:数据预处理与加载

# 数据增强与预处理
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# 加载数据集
train_dataset = FaceKeyPointDataset(csv_path="train.csv", root_dir="data", transform=transform)
test_dataset = FaceKeyPointDataset(csv_path="test.csv", root_dir="data", transform=transform)
# 数据加载器
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

步骤 5:定义训练与评估函数

回归任务使用均方误差(MSE)损失,优化器用 Adam:

def train_model(model, train_loader, criterion, optimizer, epochs=5):
    model = model.to(device)  # device = "cuda" if torch.cuda.is_available() else "cpu"
    for epoch in range(epochs):
        running_loss = 0.0
        for i, (images, labels) in enumerate(train_loader):
            images, labels = images.to(device), labels.to(device)
            # 前向传播
            outputs = model(images)
            loss = criterion(outputs, labels)
            # 反向传播与优化
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            running_loss += loss.item() * images.size(0)
        # 打印 epoch 损失
        epoch_loss = running_loss / len(train_loader.dataset)
        print(f"Epoch {epoch+1}/{epochs}, Loss: {epoch_loss:.4f}")
        # 评估模型
        test_loss = evaluate_model(model, test_loader, criterion)
        print(f"Epoch {epoch+1}/{epochs}, Test Loss: {test_loss:.4f}")

def evaluate_model(model, test_loader, criterion):
    model = model.to(device)
    model.eval()
    total_loss = 0.0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model
odel.to(device)
    model.eval()
    total_loss = 0.0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model

网站公告

今日签到

点亮在社区的每一天
去签到