ResNet 详解:让深度学习真正"深"起来

没有残差连接的深层网络,越深效果越差。ResNet 用一条"跳跃连接"彻底解决了这个问题。

1. ResNet 解决了什么问题?

为什么想要更深的网络?

深层网络的每一层都在前一层的基础上提取更抽象的特征:

层次 学到的特征
浅层(1-3 层) 边缘、颜色、纹理
中层(4-8 层) 眼睛、轮子、轮廓
深层(9+ 层) "这是一只狗"、"这是一辆车"

理论上,网络越深,表达能力越强------能组合出更复杂的特征。VGG、GoogLeNet 等网络的成功也验证了这一点。

深层网络反而更差

2015 年之前,大家以为"网络越深越好"。但实验发现了一个反直觉的现象------退化问题

网络深度 训练误差
20 层 5.6%
56 层 8.3% ← 更深反而更差!

注意:这不是过拟合(训练误差也更高),而是优化本身失败了。更深的网络连训练集都学不好。

为什么"不是过拟合"?

  • 过拟合 = 训练误差很低,测试误差很高。模型"死记硬背"了训练数据,泛化能力差。
  • 但这里 56 层网络在训练集上就表现很差,说明它不是"学太多了",而是"压根学不会"------优化器在更深的网络中找不到好的解。
  • 这是"退化问题"(degradation),需要架构层面的创新来解决,而不是正则化、dropout 等常规手段。

理论上不应该更差

何恺明团队的推理:

  1. 56 层网络至少不应该比 20 层差
  2. 因为最差情况下,多出来的 36 层可以学成"什么都不做"(恒等映射),那就跟 20 层一样
  3. 但实际上网络学不会恒等映射 → 说明优化器找不到这个解
  4. 解决方案:显式提供一条恒等映射的路径(shortcut),让网络只需要学"偏差"

2. 核心思想:残差学习

普通网络让每层学完整的映射 <math xmlns="http://www.w3.org/1998/Math/MathML"> H ( x ) H(x) </math>H(x)。

ResNet 让每层学残差 (输出与输入的差) <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) = H ( x ) − x F(x) = H(x) - x </math>F(x)=H(x)−x。

为什么学残差更容易?

普通网络要学"输出等于输入",需要让若干层的复合精确 等于恒等映射 <math xmlns="http://www.w3.org/1998/Math/MathML"> H ( x ) = x H(x) = x </math>H(x)=x------这是一个特定的非平凡函数,优化器很难精确找到。

ResNet 只需让残差 <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) = 0 F(x) = 0 </math>F(x)=0,相当于"把权重推向零"------而这恰好是网络的默认状态,也就是说,优化器只需在"几乎什么都不做"的基础上微调,比让普通深层网络从头拟合恒等映射容易得多。

ResNet 让"什么都不做"成为优化器的默认解,再在此基础上学有用的偏差,这就是残差学习的本质。

残差块结构图:

scss 复制代码
输入 x ──────────────────────────┐
   │                            │ (shortcut: 直接连接)
   ▼                            │
 Conv → BN → ReLU               │
   │                            │
   ▼                            │
 Conv → BN                      │
   │                            │
   ▼                            │
  (+) ◀─────────────────────────┘
   │
   ▼
  ReLU
   │
   ▼
输出 H(x) = F(x) + x

3. 残差块(ResidualBlock)详解

3.1 代码实现

python 复制代码
# 3x3 卷积工具函数
def conv3x3(in_channels, out_channels, stride=1):
    return nn.Conv2d(in_channels, out_channels, kernel_size=3,
                     stride=stride, padding=1, bias=False)

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(ResidualBlock, self).__init__()
        self.conv1 = conv3x3(in_channels, out_channels, stride)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = conv3x3(out_channels, out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample

    def forward(self, x):
        residual = x                    # 保存输入
        out = self.conv1(x)             # 第一个卷积
        out = self.bn1(out)             # 归一化
        out = self.relu(out)            # 激活
        out = self.conv2(out)           # 第二个卷积
        out = self.bn2(out)             # 归一化(注意:这里没有 ReLU)
        if self.downsample:
            residual = self.downsample(x)
        out += residual                 # 核心:输出 = F(x) + x
        out = self.relu(out)            # 相加之后再 ReLU
        return out

代码结构说明:

每个残差块包含两条路径:

  • 主路径conv3×3 → bn → relu → conv3×3 → bn,负责提取特征(即残差 <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) F(x) </math>F(x))
  • shortcut 路径 :要么直连(尺寸/通道相同),要么经过 downsample(conv1×1 + bn,对齐尺寸和通道)

最后两路相加 out += residual,再统一 ReLU 激活。

BatchNorm(批归一化)是什么?

每次卷积后,不同通道的数值范围可能差异很大(有的在 0--100,有的在 -0.01--0.01)。BatchNorm2d 对每个通道做标准化,拉到均值≈0、方差≈1 的范围:

  • 训练更稳定:每层输入分布一致,数值不会层层放大或缩小
  • 可以用更大学习率:不用担心某层数值失控
  • 轻微正则化效果:因为每个 mini-batch 的统计量有随机性

简单记:卷积后、激活前,先做一次"数据拉平"

3.2 两种运行情况

残差块的核心是 out += residual(主路径 + shortcut 相加)。

为什么要相加? 这就是残差学习的实现:主路径学到的是"偏差" <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) F(x) </math>F(x),shortcut 保留了原始输入 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x,两者相加得到最终输出 <math xmlns="http://www.w3.org/1998/Math/MathML"> H ( x ) = F ( x ) + x H(x) = F(x) + x </math>H(x)=F(x)+x。这样即使主路径什么都没学到( <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) = 0 F(x)=0 </math>F(x)=0),输出也等于输入,不会变差------这就解决了退化问题。

但相加要求两边形状完全一致。根据输入输出的形状关系,分两种情况:

直连(identity shortcut) 下采样(downsample shortcut)
含义 shortcut 什么都不做,原样返回 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x shortcut 经过 conv1×1+bn,把 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 变换到目标形状
何时触发 输入输出的通道数、尺寸都相同 通道数加倍 或 尺寸减半(stride=2)
代码体现 downsample=None,跳过 if downsample=Conv1×1(stride=2)+BN

情况 A:直连(stride=1,通道相同)

shortcut 什么都不做,x 和 out 形状完全一致,直接相加。

csharp 复制代码
x (16ch, 32×32)
  ├── 主路径: Conv→BN→ReLU→Conv→BN → out (16ch, 32×32)
  └── shortcut: 直连 ─────────────→ residual = x
                                     out += residual → ReLU → 输出

情况 B:下采样(stride=2,通道加倍)

x 和 out 形状不同,需要 downsample 对齐维度。

erlang 复制代码
x (16ch, 32×32)
  ├── 主路径: Conv(stride=2)→BN→ReLU→Conv→BN → out (32ch, 16×16)
  └── shortcut: downsample(Conv+BN) ──────────→ residual (32ch, 16×16)
                                                  out += residual → ReLU → 输出

3.3 Conv 为什么设 bias=False?

因为后面紧跟 BatchNorm。BN 会先减均值,相当于把 bias 抵消了------所以 Conv 加 bias 是多余的。去掉可以少几个参数,不影响效果。

3.4 为什么 ReLU 用 inplace=True?

inplace=True 直接在原始 tensor 上修改(而不是创建新 tensor),节省显存。在 ResNet 的前向路径中,被 ReLU 覆盖的值之后不会再用到,所以安全。

3.5 为什么 bn2 后面没有 ReLU?

python 复制代码
out = self.bn2(out)     # 这里没有 ReLU
out += residual         # 先加
out = self.relu(out)    # 后激活

先加再 ReLU,是为了让残差 F(x) 保留正负值。

如果 bn2 后面先 ReLU 再相加:

  • F(x) 被截断成非负 → 残差只能往"正方向"调整
  • 不能往"负方向"修正 → 表达能力减半

一个直觉例子:假设输入 <math xmlns="http://www.w3.org/1998/Math/MathML"> x = 5 x = 5 </math>x=5,这一层的最优输出应该是 <math xmlns="http://www.w3.org/1998/Math/MathML"> H ( x ) = 3 H(x) = 3 </math>H(x)=3(即需要把值往下调)。

那么残差就应该是 <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) = H ( x ) − x = 3 − 5 = − 2 F(x) = H(x) - x = 3 - 5 = -2 </math>F(x)=H(x)−x=3−5=−2(负数!)。

方式 计算过程 最终输出 是否正确
先 ReLU 再加 <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) = − 2 F(x)=-2 </math>F(x)=−2 → ReLU(截断负数)→ <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 → <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 + x = 0 + 5 = 5 0 + x = 0+5 = 5 </math>0+x=0+5=5 5 ✖ 调不下来
先加再 ReLU <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) = − 2 F(x)=-2 </math>F(x)=−2 → 先加 <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) + x = − 2 + 5 = 3 F(x)+x = -2+5 = 3 </math>F(x)+x=−2+5=3 → ReLU <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 3 ) = 3 (3) = 3 </math>(3)=3 3 ✔ 正确

结论:先加再 ReLU,残差 <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) F(x) </math>F(x) 可以是负数,网络既能往上调也能往下调。

3.6 为什么是 2 次卷积一个残差?

1 次太少: 只有一个卷积的残差 <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) = W x F(x) = Wx </math>F(x)=Wx,跟 shortcut 相加后 <math xmlns="http://www.w3.org/1998/Math/MathML"> y = ( W + I ) x y = (W+I)x </math>y=(W+I)x,等价于普通线性层,shortcut 加了等于没加。必须有 ReLU 夹在中间才有意义,最少需要 2 个卷积。

超过 2 次: shortcut 间隔过大,残差学习的"微调"语义被稀释,实验证明效果下降。

间隔 是否可行 实际使用
1 层 不行 F(x) 是纯线性,shortcut 无意义
2 层 最优 ResNet-18/34(BasicBlock)
3+ 层 不推荐 shortcut 间隔过大,效果下降

3.7 downsample 用 1×1 卷积

downsample 用 1×1 卷积,只做维度对齐(通道映射 + 下采样),不提取空间特征:

python 复制代码
downsample = nn.Sequential(
    nn.Conv2d(in_ch, out_ch, kernel_size=1, stride=stride, bias=False),
    nn.BatchNorm2d(out_ch))

shortcut 的设计哲学:尽可能简单、轻量,让主路径负责学特征。

4. 完整训练代码

下面用我们自己实现的 ResNet 在 CIFAR-10 数据集上进行训练。

4.0 CIFAR-10 数据集简介

CIFAR-10 是计算机视觉领域最经典的入门数据集之一:

属性
图片尺寸 32×32 像素,RGB 3 通道
类别数 10 类(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车)
训练集 50,000 张
测试集 10,000 张
特点 小图片、多类别、均衡分布

CIFAR-10 是验证网络架构的理想选择------图片小(训练快)、类别少(容易调试)、社区基准丰富(方便对比)。

为什么不用 ImageNet?

  • ImageNet 输入 224×224,训练慢得多
  • CIFAR-10 输入 32×32,单卡几分钟就能跑完
  • 对于理解 ResNet 原理,小数据集足够
python 复制代码
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms

device = torch.device(
    'cuda' if torch.cuda.is_available()
    else 'mps' if torch.backends.mps.is_available()
    else 'cpu'
)

# 3x3 卷积工具函数
def conv3x3(in_channels, out_channels, stride=1):
    return nn.Conv2d(in_channels, out_channels, kernel_size=3,
                     stride=stride, padding=1, bias=False)

# 残差块
class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(ResidualBlock, self).__init__()
        self.conv1 = conv3x3(in_channels, out_channels, stride)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = conv3x3(out_channels, out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample

    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        if self.downsample:
            residual = self.downsample(x)
        out += residual
        out = self.relu(out)
        return out

# ResNet
class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=10):
        super(ResNet, self).__init__()
        self.in_channels = 16
        self.conv = conv3x3(3, 16)
        self.bn = nn.BatchNorm2d(16)
        self.relu = nn.ReLU(inplace=True)
        self.layer1 = self.make_layer(block, 16, layers[0])
        self.layer2 = self.make_layer(block, 32, layers[1], 2)
        self.layer3 = self.make_layer(block, 64, layers[2], 2)
        self.avg_pool = nn.AvgPool2d(8)
        self.fc = nn.Linear(64, num_classes)

    def make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        if (stride != 1) or (self.in_channels != out_channels):
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels, kernel_size=1,
                          stride=stride, bias=False),
                nn.BatchNorm2d(out_channels))
        layers = []
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        self.in_channels = out_channels
        for i in range(1, blocks):
            layers.append(block(out_channels, out_channels))
        return nn.Sequential(*layers)

    def forward(self, x):
        out = self.conv(x)
        out = self.bn(out)
        out = self.relu(out)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.avg_pool(out)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out

# 数据增强
transform = transforms.Compose([
    transforms.Pad(4),
    transforms.RandomHorizontalFlip(),
    transforms.RandomCrop(32),
    transforms.ToTensor()])

train_dataset = torchvision.datasets.CIFAR10(
    root='./data/', train=True, transform=transform, download=True)
test_dataset = torchvision.datasets.CIFAR10(
    root='./data/', train=False, transform=transforms.ToTensor())

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=100, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=100, shuffle=False)

# 构建模型
model = ResNet(ResidualBlock, [2, 2, 2]).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 训练
for epoch in range(80):
    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()

    # 每 20 个 epoch 衰减学习率
    if (epoch+1) % 20 == 0:
        for param_group in optimizer.param_groups:
            param_group['lr'] /= 3

4.1 代码讲解

初始卷积

python 复制代码
self.conv = conv3x3(3, 16)     # RGB 3通道 → 16通道,尺寸不变
self.bn = nn.BatchNorm2d(16)
self.relu = nn.ReLU(inplace=True)

CIFAR-10 图像是 32×32,尺寸较小,所以用 3×3 卷积(stride=1)作为入口,不做下采样。(ImageNet 版本用 7×7 stride=2 + MaxPool,因为输入是 224×224)

为什么需要这一步?

  1. 通道对齐:原始图像只有 3 个通道(RGB),而 layer1 内部全程是 16→16。如果不先提升到 16 通道,layer1 第一个残差块的 shortcut 就必须做 downsample(3→16),破坏了 layer1 "通道不变、全部直连"的干净设计。
  2. 提供有意义的输入 :残差块学的是"偏差" <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( x ) F(x) </math>F(x),如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 还是原始像素值,残差块需要同时完成"基础特征提取"和"残差学习"两件事。先做一次 conv+bn+relu,相当于把原始像素变成初步特征图(边缘、纹理等),让后面的残差块专注于精细调整。

make_layer ------ 构建残差层的工厂方法

python 复制代码
def make_layer(self, block, out_channels, blocks, stride=1):
    downsample = None
    if (stride != 1) or (self.in_channels != out_channels):
        downsample = nn.Sequential(
            nn.Conv2d(self.in_channels, out_channels, kernel_size=1,
                      stride=stride, bias=False),
            nn.BatchNorm2d(out_channels))
    layers = []
    layers.append(block(self.in_channels, out_channels, stride, downsample))
    self.in_channels = out_channels        # ← 关键:立即更新
    for i in range(1, blocks):
        layers.append(block(out_channels, out_channels))
    return nn.Sequential(*layers)

核心逻辑:

  1. 第 1 个 block:带 stride 和 downsample,负责维度变换(通道加倍 + 尺寸减半)
  2. 后续 block:同尺寸同通道,纯粹的特征提炼

self.layer2 = self.make_layer(block, 32, 2, stride=2) 为例:

Block in_channels out_channels stride downsample
第 1 个 16 32 2 Conv1×1 + BN
第 2 个 32 32 1

为什么 layer1 不需要 downsample?

注意上面 downsample 的触发条件:(stride != 1) or (self.in_channels != out_channels)------两者任一成立才触发。

调用 self.layer1 = self.make_layer(block, 16, layers[0]) 时:

  • stride = 1(用了默认值,没显式传)
  • self.in_channels = 16(初始卷积后赋的值),out_channels = 16

两个条件都不满足,downsample = None,所以 layer1 的两个 block 都是纯直连。

self.in_channels = out_channels 这一行很关键

第 1 个 block 处理完后,特征图的通道数变成了 out_channels。立即更新 self.in_channels,是为了让:

  • 同一个 layer 里的后续 block :进入循环时 in = out = out_channels,对称结构,无需 downsample
  • 下一次调用 make_layer 时 (构造下一个 layer):self.in_channels 已经是上一个 layer 的输出通道数,新 layer 的第一个 block 能正确判断是否需要通道变换

如果忘了这行,要么循环里后续 block 的输入通道对不上,要么下一个 layer 的 downsample 判断出错------这是手写 ResNet 时最容易踩的坑。

模型实例化

python 复制代码
model = ResNet(ResidualBlock, [2, 2, 2])

[2, 2, 2] = 3 个 layer × 每个 2 个残差块 = 6 个残差块。每个残差块含 2 个卷积层,加上初始卷积和 fc,总深度 6×2 + 2 = 14 层

训练策略

  • 数据增强Pad(4)RandomHorizontalFlipRandomCrop(32),通过随机翻转和裁剪增加训练多样性
  • 损失函数CrossEntropyLoss(交叉熵损失)------ 多分类任务的标配。它把模型输出的 10 个原始分数先做 Softmax(变成概率),再计算与真实标签的差距。模型对正确类别越确信,loss 越小
  • 优化器:Adam(lr=0.001),自适应学习率
  • 学习率衰减:每 20 个 epoch 将学习率除以 3,越往后越精细

全局平均池化

python 复制代码
self.avg_pool = nn.AvgPool2d(8)
self.fc = nn.Linear(64, num_classes)

nn.AvgPool2d(kernel_size) 对输入特征图的每个通道,用 kernel_size × kernel_size 的窗口取平均值。当 kernel_size 等于输入的空间尺寸时,就是"全局平均池化"------整张特征图压缩成一个数:

scss 复制代码
一个通道的 8×8 特征图:
┌─────────────────────────┐
│ 1  3  2  4  5  6  7  8  │
│ 2  4  1  3  6  7  8  9  │
│ ...  (共 8×8 = 64 个值)  │
└─────────────────────────┘
         ↓ AvgPool2d(8)
         ↓ 把整个 8×8 区域取平均
       [4.2] ← 一个数(该通道的"全局摘要")

对 64 个通道各做一次,整体数据流:

scss 复制代码
(64, 8, 8) → 每个通道 8×8=64 个值取平均 → (64, 1, 1) → flatten → (64,) → fc → (10,)

为什么用 AvgPool 而不是 MaxPool?

AvgPool MaxPool
操作 区域内取平均 区域内取最大值
保留信息 整体激活强度 只保留最强响应
网络末尾 更全面地总结特征图 易被异常值干扰

对比直接 Flatten:

方式 连接 fc 的参数量
直接 Flatten: (64, 8, 8) → (4096,) 4096×10 = 40,960
全局平均池化: (64, 8, 8) → (64,) 64×10 = 640

参数少 64 倍,有效防止过拟合。

4.2 ResNet 的层级结构

完整的数据流:

ini 复制代码
输入图像 (3, 32, 32)
│
├─ conv + bn + relu                                         → (16, 32, 32)  ← 初始特征提取
│
├─ layer1: ResidualBlock × 2                                → (16, 32, 32)  ← 通道不变,尺寸不变
│    ├─ Block1: conv3×3(stride=1)→bn→relu→conv3×3(stride=1)→bn
│    │          + shortcut(直连,无 downsample)                              ← 尺寸/通道相同,直接相加
│    │          → 相加 → relu                                → (16, 32, 32)
│    └─ Block2: conv3×3(stride=1)→bn→relu→conv3×3(stride=1)→bn
│               + shortcut(直连)                                             ← 尺寸/通道相同,直接相加
│               → 相加 → relu                                → (16, 32, 32)
│
├─ layer2: ResidualBlock × 2                                → (32, 16, 16)  ← 通道加倍,尺寸减半
│    ├─ Block1: conv3×3(stride=2)→bn→relu→conv3×3(stride=1)→bn
│    │          + downsample(conv1×1 stride=2 + bn)                         ← 主路径下采样 + shortcut 对齐
│    │          → 相加 → relu                                → (32, 16, 16)
│    └─ Block2: conv3×3(stride=1)→bn→relu→conv3×3(stride=1)→bn
│               + shortcut(直连)                                             ← 尺寸/通道相同,直接相加
│               → 相加 → relu                                → (32, 16, 16)
│
├─ layer3: ResidualBlock × 2                                → (64, 8, 8)    ← 通道加倍,尺寸减半
│    ├─ Block1: conv3×3(stride=2)→bn→relu→conv3×3(stride=1)→bn
│    │          + downsample(conv1×1 stride=2 + bn)                         ← 主路径下采样 + shortcut 对齐
│    │          → 相加 → relu                                → (64, 8, 8)
│    └─ Block2: conv3×3(stride=1)→bn→relu→conv3×3(stride=1)→bn
│               + shortcut(直连)                                             ← 尺寸/通道相同,直接相加
│               → 相加 → relu                                → (64, 8, 8)
│
├─ avg_pool(8)                                              → (64, 1, 1)    ← 全局平均池化
├─ flatten                                                  → (64,)
└─ fc                                                       → (10,)         ← 10 类分类

设计规律:通道每翻倍一次,空间尺寸减半一次,总信息量大致守恒。

4.3 为什么是 3 层 layer?

3 层不是随意选的,是输入尺寸决定的。CIFAR-10 输入 32×32,每次 stride=2 尺寸减半,最后需要用 AvgPool 压到 1×1:

ini 复制代码
32×32(输入)
  │
  ├─ layer1 (stride=1) → 32×32    保持
  ├─ layer2 (stride=2) → 16×16    减半 ①
  ├─ layer3 (stride=2) → 8×8      减半 ②
  │
  └─ AvgPool(8)        → 1×1      刚好压完

如果再加一层 layer4 (stride=2) → 4×4,空间信息已经太少,效果反而下降。

对比 ImageNet 版本(输入 224×224,空间更大):

版本 输入尺寸 layer 数 空间变化
CIFAR-10 ResNet 32×32 3 32→32→16→8→1
ImageNet ResNet 224×224 4 56→56→28→14→7→1

结论:输入尺寸决定能下采样几次,下采样次数决定 layer 数。

5. ResNet 是经验算法吗?

既有理论直觉,也有大量经验验证。

阶段 方式 内容
观察问题 经验发现 深层网络训练误差反而更高(退化问题)
提出方案 理论推导 shortcut 让网络学残差而非完整映射,更容易优化
验证效果 实验验证 110 层 > 56 层 > 20 层,退化问题解决
事后解释 理论分析 集成学习视角、loss landscape 平滑性等

具体设计中,哪些是理论、哪些是经验:

设计选择 来源
跳跃连接的思想 理论驱动
每 2 层一个 shortcut 经验:最小有效单位
通道数 16→32→64 翻倍 经验:来自 VGG
Conv→BN→ReLU 的顺序 经验:多种变体对比

大方向靠理论,细节靠实验。 这在深度学习中非常常见。

6. 一句话总结

ResNet 的核心发明是跳跃连接(shortcut):让网络学"输出与输入的差"而非"输出本身"。这解决了深层网络的退化问题,使得 100+ 层的网络成为可能。结构上,它由"初始卷积 + N 组残差层(通道加倍、尺寸减半)+ 全局池化 + 分类头"组成,是计算机视觉最重要的基础架构之一。

相关推荐
xingyuzhisuan3 小时前
2026实测:租用RTX 4090 CUDA适配与PyTorch精准安装教程
人工智能·pytorch·python·深度学习·gpu算力
QiZhang | UESTC4 小时前
InstructGPT_论文精读笔记
人工智能·笔记·深度学习
搞科研的小刘选手4 小时前
【大连市计算机学会主办】第三届图像处理、智能控制与计算机工程国际学术会议(IPICE 2026)
图像处理·人工智能·深度学习·算法·计算机·数据挖掘·智能控制
灰灰勇闯IT4 小时前
ops-softmax:Transformer 推理中的概率归一化引擎
人工智能·深度学习·transformer
星浩AI4 小时前
(四)Hugging Face 与魔搭实战:模型下载、API 调用与本地推理
人工智能·深度学习·llm
放下华子我只抽RuiKe54 小时前
React 从入门到生产(六):路由与导航
前端·人工智能·深度学习·react.js·前端框架·html·claude code
不懒不懒4 小时前
Python+AI 大模型实现课堂教学质量智能分析|加权评分 + 自动诊断 + 改进建议
人工智能·python·深度学习·ai大模型·智慧教育·nlp算法
AI人工智能+4 小时前
基于高精度OCR与大模型融合的智能文档抽取系统,著提升政务服务效率,推动从“自动化“向“智能化“转型
深度学习·语言模型·ocr·文档抽取
啦啦啦_99995 小时前
RNN 入门
人工智能·rnn·深度学习