本文基于d2l项目内容整理,深入介绍残差网络ResNet的核心思想和技术创新,包括残差块设计、跳跃连接机制,以及ResNeXt的分组卷积改进。

深度网络训练的困境:

向模型添加更多的层以增加深度,期望降低任务误差。从 LeNet 到 GoogLeNet,深度逐渐增加的模型也获得了更好的性能。但训练更深的网络时,即使使用了批量归一化方法,误差仍然不降反升。


1. 残差网络的理论基础

1.1 函数嵌套与表达能力

神经网络的函数抽象:

特定的神经网络模型本质上可抽象为一个从输入数据映射到输出结果的函数 $f$,网络架构(层的连接方式、激活函数等)、可学习的参数(权重与偏置等)与超参数的微调本质上是微调了函数 $f$。对于特定的一类神经网络架构,从输入数据到输出结果的一系列可能的映射均可涵盖在函数类 $\mathcal{F}$ 中,即 $f \in \mathcal{F}$。

1.2 最优函数寻找问题

只要问题是可计算的,理论上就一定存在最佳函数 $f^$ 将问题完美解决。使用现有神经网络 $\mathcal{F}$ 解决该问题的前提,是找到当前架构 $\mathcal{F}$ 下的最佳函数 $f_{\mathcal{F}}^$ 作为问题的近似解。训练模型,调整现有函数 $f$ 的参数,使输入数据 $x$ 经网络 $f$ 输出后与真实标签 $y$ 的损失值 $L(f(x), y)$ 最小,最终得到使损失值最小的函数 $f_{\mathcal{F}}^*$:

1.3 函数嵌套的重要性

架构设计的关键原则:

当数据的复杂性超出了原有架构 $\mathcal{F}$ 的表达能力时,即使是原有架构 $\mathcal{F}$ 下的最佳函数 $f_{\mathcal{F}}^*$,也可能无法得到太好的结果。于是尝试设计新架构 $\mathcal{F}’$:

嵌套情况 ($\mathcal{F} \subseteq \mathcal{F}$):新架构拥有更复杂的函数空间、更多的灵活性,其最佳函数更可能接近理想函数 %}
{% checkbox unchecked


深度网络设计的关键洞察:

添加层以加深神经网络的目的,是为了扩展原有模型 $\mathcal{F}$ 的表达能力,使其能够表示更复杂的输入输出映射。在最差的情况下,新添加的层 $g$ 没有学到任何有用的特征,新模型也能完整地退化为原有模型,不改变已有的输入输出映射:

这一恒等映射的引入,限制了深度网络扩展时的性能下界、避免了不必要的复杂度的产生、提供了进一步优化的可能性。

ResNet 的革命性思想:

这正是何凯明等人于 2016 年提出的残差网络 (Residual Network, ResNet) 的核心思想:让新添加的层学习来自输入的残差,而不直接拟合输出,实现更稳定、高效的网络。该网络模型在 2015 年 ImageNet 图像识别挑战赛中夺魁,深刻影响了后来的深度神经网络设计。


2. ResNet 网络架构设计

2.1 残差块的设计原理

残差块的核心概念:

残差块不是直接学习从输入 $x$ 到输出 $y$ 的映射,而是学习残差映射。假设我们希望学习的映射为 $H(x)$,残差块让堆叠的层学习残差函数 $F(x) = H(x) - x$,因此原映射变为 $F(x) + x$。

传统网络层的学习方式

  • 直接学习 $H(x)$:输入到输出的完整映射
  • 当网络很深时,直接学习复杂映射变得困难
  • 梯度消失和训练困难问题

残差学习方式

  • 学习残差 $F(x) = H(x) - x$
  • 假设残差比原映射更容易学习
  • 最终输出:$H(x) = F(x) + x$
  • 当 $F(x) = 0$ 时,自然退化为恒等映射

3. ResNet 与 ResNeXt 完整网络架构

3.1 ResNet 网络结构

ResNet 网络架构特点:

ResNet 网络架构参考了 GoogLeNet,从输出通道数为 64、步幅为 2 的 7×7 卷积层和步幅为 2 的 3×3 最大池化层开始,最后依次由全局平均池化层展平后经过全连接层输出。中间部分由一系列残差块堆叠而成。

3.2 ResNet-18 结构分析

网络开始部分

  • 7×7 卷积层 (输出通道64,步幅2)
  • 3×3 最大池化层 (步幅2)

残差块分组设计

第1组:两个连续的64通道残差块

第2组:特征图减半、通道数翻倍 + 瓶颈结构

第3组:重复第2组的设计模式

第4组:继续重复,形成深层特征提取

ResNet-18 层数计算

  • 不考虑跳跃连接的1×1卷积层
  • 共计:1 + 2×2 + (2+2)×3 = 17 个卷积层
  • 考虑最后一个全连接层:共18个计算密集层
  • 因此称为 ResNet-18
  • 其他变体:ResNet-101、ResNet-152等

3.3 ResNeXt 的改进

ResNeXt 的分组卷积创新:

ResNeXt 网络与 ResNet 网络类似,只是将残差块用分组残差块替代,通过引入”cardinality”(基数)概念,在不显著增加参数的情况下提升模型性能。

ResNet与ResNeXt架构对比图
ResNet与ResNeXt架构对比图

3.4 PyTorch 实现

架构优势对比:

与 GoogLeNet 相比,ResNet 架构更简单、更易于扩展和训练,且性能更佳,这些因素都促进了 ResNet 的广泛使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
from typing import Optional

import torch.nn.functional as F
from torch import nn, Tensor


class ResNetBlock(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int, *,
stride: int = 1,
bottleneck_ratio: int = 4,
downsample: Optional[nn.Module] = None):
"""
后激活的残差块

:param in_channels: 输入通道数
:param out_channels: 输出通道数
:param stride: 卷积步长
:param bottleneck_ratio: 瓶颈比率,控制中间层宽度
:param downsample: 下采样层,用于维度匹配
"""
super().__init__()

# 计算中间层通道数
assert bottleneck_ratio >= 1, f'瓶颈比率 ({bottleneck_ratio}) 应大于或等于 1'
mid_channels = out_channels // bottleneck_ratio if bottleneck_ratio > 1 else out_channels

if bottleneck_ratio > 1:
# 1x1 Conv -> 3x3 Conv -> 1x1 Conv
assert mid_channels > 0, f'中间通道数 ({mid_channels}) 必须大于 0'

self.main_path = nn.Sequential(
nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(),

nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(),

nn.Conv2d(mid_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(out_channels)
)
else:
# 3x3 Conv -> 3x3 Conv
self.main_path = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(),

nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(out_channels)
)

# 匹配维度用于跳跃连接
if downsample is not None:
self.shortcut = downsample
elif stride != 1 or in_channels != out_channels: # 空间尺寸、通道数发生变化时,再调整维度
self.shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False)
else:
self.shortcut = nn.Identity() # 恒等映射

def forward(self, x: Tensor) -> Tensor:
out = self.main_path(x) + self.shortcut(x) # 主路径与跳跃连接相加
out = F.relu(out) # 激活加和后的结果
return out


class ResNeXtBlock(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int, *,
stride: int = 1,
groups: int = 1,
bottleneck_ratio: int = 4,
downsample: Optional[nn.Module] = None):
"""
预激活的分组残差块

:param in_channels: 输入通道数
:param out_channels: 输出通道数
:param stride: 卷积步长
:param groups: 分组卷积的组数
:param bottleneck_ratio: 瓶颈比率,控制中间层宽度
:param downsample: 下采样层,用于维度匹配
"""
super().__init__()

# 计算中间层通道数
assert bottleneck_ratio >= 1, f'瓶颈比率 ({bottleneck_ratio}) 应大于或等于 1'
mid_channels = out_channels // bottleneck_ratio if bottleneck_ratio > 1 else out_channels

if bottleneck_ratio > 1:
# 1x1 Conv -> 3x3 Conv -> 1x1 Conv
assert mid_channels % groups == 0, f'中间通道数 ({mid_channels}) 必须能被卷积分组数 ({groups}) 整除'
assert mid_channels > 0, f'中间通道数 ({mid_channels}) 必须大于 0'

self.main_path = nn.Sequential(
nn.BatchNorm2d(in_channels), nn.ReLU(),
nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1, padding=0, bias=False),

nn.BatchNorm2d(mid_channels), nn.ReLU(),
nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1, groups=groups,
bias=False),

nn.BatchNorm2d(mid_channels), nn.ReLU(),
nn.Conv2d(mid_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False)
)
else:
# 3x3 Conv -> 3x3 Conv
assert out_channels % groups == 0, f'输出通道数 ({out_channels}) 必须能被卷积分组数 ({groups}) 整除'

self.main_path = nn.Sequential(
nn.BatchNorm2d(in_channels), nn.ReLU(),
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, groups=groups,
bias=False),

nn.BatchNorm2d(out_channels), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, groups=groups, bias=False)
)

# 匹配维度用于跳跃连接
if downsample is not None:
self.shortcut = downsample
elif stride != 1 or in_channels != out_channels: # 空间尺寸、通道数发生变化时,再调整维度
self.shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False)
else:
self.shortcut = nn.Identity()

def forward(self, x: Tensor) -> Tensor:
return self.main_path(x) + self.shortcut(x)


class ResNet(nn.Module):
def __init__(self, num_classes: int):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=64, kernel_size=7, stride=2, padding=3, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),

ResNetBlock(64, 64, bottleneck_ratio=1),
ResNetBlock(64, 64, bottleneck_ratio=1),

# 组 1
ResNetBlock(64, 128, stride=2, bottleneck_ratio=1),
ResNetBlock(128, 128, bottleneck_ratio=4),

# 组 2
ResNetBlock(128, 256, stride=2, bottleneck_ratio=1),
ResNetBlock(256, 256, bottleneck_ratio=4),

# 组 3
ResNetBlock(256, 512, stride=2, bottleneck_ratio=1),
ResNetBlock(512, 512, bottleneck_ratio=4),

nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(in_features=512, out_features=num_classes),
)

self._initialize_weights()

def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
if m.bias is not None: nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None: nn.init.constant_(m.bias, 0)

def forward(self, x) -> Tensor:
return self.model(x)


class ResNeXt(nn.Module):
def __init__(self, num_classes: int, cardinality: int = 32):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=64, kernel_size=7, stride=2, padding=3, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),

ResNeXtBlock(64, 64, groups=1, bottleneck_ratio=1),
ResNeXtBlock(64, 64, groups=1, bottleneck_ratio=1),

# 组 1
ResNeXtBlock(64, 128, stride=2, groups=cardinality, bottleneck_ratio=4),
ResNeXtBlock(128, 128, groups=cardinality, bottleneck_ratio=4),

# 组 2
ResNeXtBlock(128, 256, stride=2, groups=cardinality, bottleneck_ratio=4),
ResNeXtBlock(256, 256, groups=cardinality, bottleneck_ratio=4),

# 组 3
ResNeXtBlock(256, 512, stride=2, groups=cardinality, bottleneck_ratio=4),
ResNeXtBlock(512, 512, groups=cardinality, bottleneck_ratio=4),

nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(in_features=512, out_features=num_classes),
)

self._initialize_weights()

def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
if m.bias is not None: nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None: nn.init.constant_(m.bias, 0)

def forward(self, x) -> Tensor:
return self.model(x)

4. 网络结构分析

4.1 ResNet 与 ResNeXt 参数对比

使用torchinfo库的summary函数执行输出维度测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
from torchinfo import summary

model = ResNet(num_classes=10)
summary(model, input_size=(1, 1, 224, 224))
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
ResNet [1, 10] --
├─Sequential: 1-1 [1, 10] --
│ └─Conv2d: 2-1 [1, 64, 112, 112] 3,136
│ └─BatchNorm2d: 2-2 [1, 64, 112, 112] 128
│ └─ReLU: 2-3 [1, 64, 112, 112] --
│ └─MaxPool2d: 2-4 [1, 64, 56, 56] --
│ └─ResNetBlock: 2-5 [1, 64, 56, 56] --
│ │ └─Sequential: 3-1 [1, 64, 56, 56] 73,984
│ │ └─Identity: 3-2 [1, 64, 56, 56] --
│ └─ResNetBlock: 2-6 [1, 64, 56, 56] --
│ │ └─Sequential: 3-3 [1, 64, 56, 56] 73,984
│ │ └─Identity: 3-4 [1, 64, 56, 56] --
│ └─ResNetBlock: 2-7 [1, 128, 28, 28] --
│ │ └─Sequential: 3-5 [1, 128, 28, 28] 221,696
│ │ └─Conv2d: 3-6 [1, 128, 28, 28] 8,192
│ └─ResNetBlock: 2-8 [1, 128, 28, 28] --
│ │ └─Sequential: 3-7 [1, 128, 28, 28] 17,792
│ │ └─Identity: 3-8 [1, 128, 28, 28] --
│ └─ResNetBlock: 2-9 [1, 256, 14, 14] --
│ │ └─Sequential: 3-9 [1, 256, 14, 14] 885,760
│ │ └─Conv2d: 3-10 [1, 256, 14, 14] 32,768
│ └─ResNetBlock: 2-10 [1, 256, 14, 14] --
│ │ └─Sequential: 3-11 [1, 256, 14, 14] 70,400
│ │ └─Identity: 3-12 [1, 256, 14, 14] --
│ └─ResNetBlock: 2-11 [1, 512, 7, 7] --
│ │ └─Sequential: 3-13 [1, 512, 7, 7] 3,540,992
│ │ └─Conv2d: 3-14 [1, 512, 7, 7] 131,072
│ └─ResNetBlock: 2-12 [1, 512, 7, 7] --
│ │ └─Sequential: 3-15 [1, 512, 7, 7] 280,064
│ │ └─Identity: 3-16 [1, 512, 7, 7] --
│ └─AdaptiveAvgPool2d: 2-13 [1, 512, 1, 1] --
│ └─Flatten: 2-14 [1, 512] --
│ └─Linear: 2-15 [1, 10] 5,130
==========================================================================================
Total params: 5,345,098
Trainable params: 5,345,098
Non-trainable params: 0
Total mult-adds (Units.GIGABYTES): 1.08
==========================================================================================
Input size (MB): 0.20
Forward/backward pass size (MB): 36.93
Params size (MB): 21.38
Estimated Total Size (MB): 58.51
==========================================================================================
from torchinfo import summary

model = ResNeXt(num_classes=10, cardinality=16)
summary(model, input_size=(1, 1, 224, 224))
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
ResNeXt [1, 10] --
├─Sequential: 1-1 [1, 10] --
│ └─Conv2d: 2-1 [1, 64, 112, 112] 3,136
│ └─BatchNorm2d: 2-2 [1, 64, 112, 112] 128
│ └─ReLU: 2-3 [1, 64, 112, 112] --
│ └─MaxPool2d: 2-4 [1, 64, 56, 56] --
│ └─ResNeXtBlock: 2-5 [1, 64, 56, 56] --
│ │ └─Sequential: 3-1 [1, 64, 56, 56] 73,984
│ │ └─Identity: 3-2 [1, 64, 56, 56] --
│ └─ResNeXtBlock: 2-6 [1, 64, 56, 56] --
│ │ └─Sequential: 3-3 [1, 64, 56, 56] 73,984
│ │ └─Identity: 3-4 [1, 64, 56, 56] --
│ └─ResNeXtBlock: 2-7 [1, 128, 28, 28] --
│ │ └─Sequential: 3-5 [1, 128, 28, 28] 6,976
│ │ └─Conv2d: 3-6 [1, 128, 28, 28] 8,192
│ └─ResNeXtBlock: 2-8 [1, 128, 28, 28] --
│ │ └─Sequential: 3-7 [1, 128, 28, 28] 9,152
│ │ └─Identity: 3-8 [1, 128, 28, 28] --
│ └─ResNeXtBlock: 2-9 [1, 256, 14, 14] --
│ │ └─Sequential: 3-9 [1, 256, 14, 14] 27,392
│ │ └─Conv2d: 3-10 [1, 256, 14, 14] 32,768
│ └─ResNeXtBlock: 2-10 [1, 256, 14, 14] --
│ │ └─Sequential: 3-11 [1, 256, 14, 14] 35,840
│ │ └─Identity: 3-12 [1, 256, 14, 14] --
│ └─ResNeXtBlock: 2-11 [1, 512, 7, 7] --
│ │ └─Sequential: 3-13 [1, 512, 7, 7] 108,544
│ │ └─Conv2d: 3-14 [1, 512, 7, 7] 131,072
│ └─ResNeXtBlock: 2-12 [1, 512, 7, 7] --
│ │ └─Sequential: 3-15 [1, 512, 7, 7] 141,824
│ │ └─Identity: 3-16 [1, 512, 7, 7] --
│ └─AdaptiveAvgPool2d: 2-13 [1, 512, 1, 1] --
│ └─Flatten: 2-14 [1, 512] --
│ └─Linear: 2-15 [1, 10] 5,130
==========================================================================================
Total params: 658,122
Trainable params: 658,122
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 571.92
==========================================================================================
Input size (MB): 0.20
Forward/backward pass size (MB): 39.04
Params size (MB): 2.63
Estimated Total Size (MB): 41.87
==========================================================================================

4.2 网络参数统计对比

ResNet 网络参数:

  • 总参数量:5,345,098 个参数(约 535 万)
  • 计算复杂度:1.08 GB 乘加运算
  • 内存占用:总计约 58.51 MB
  • 特点:深层网络但参数控制合理

ResNeXt 网络参数:

  • 总参数量:658,122 个参数(约 66 万)
  • 计算复杂度:571.92 MB 乘加运算
  • 内存占用:总计约 41.87 MB
  • 特点:分组卷积显著减少参数数量

ResNet vs ResNeXt 对比:

参数效率:ResNeXt 参数量仅为 ResNet 的 12.3%

计算量:ResNeXt 计算量约为 ResNet 的 52.8%

内存效率:ResNeXt 内存占用减少约 28.5%

设计哲学:ResNeXt 分而治之 vs ResNet 深度加宽


5. 模型训练与评估

5.1 训练配置设置

继续使用training_tools.py中的工具训练评估模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if __name__ == '__main__':
import torch
from torch import optim

from training_tools import fashionMNIST_loader, Trainer

BATCH_SIZE = 256
EPOCHS_NUM = 30
LEARNING_RATE = 0.01
USE_RESNEXT = False

model = ResNeXt(num_classes=10, cardinality=16) if USE_RESNEXT else ResNet(num_classes=10)
train_loader, test_loader = fashionMNIST_loader(BATCH_SIZE, resize=96)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), LEARNING_RATE)
platform = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

with Trainer(model, train_loader, test_loader, criterion, optimizer, platform) as trainer:
trainer.train(EPOCHS_NUM)

5.2 ResNet 训练结果

查看ResNet完整训练过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
001/30 轮,训练损失:0.7456,训练精度:75.69%,测试损失:0.5445,测试精度:81.32%
002/30 轮,训练损失:0.4158,训练精度:85.33%,测试损失:0.4383,测试精度:84.61%
003/30 轮,训练损失:0.3470,训练精度:87.80%,测试损失:0.3901,测试精度:86.17%
004/30 轮,训练损失:0.3044,训练精度:89.31%,测试损失:0.3702,测试精度:87.16%
005/30 轮,训练损失:0.2712,训练精度:90.54%,测试损失:0.3472,测试精度:87.71%
006/30 轮,训练损失:0.2454,训练精度:91.48%,测试损失:0.3242,测试精度:88.35%
007/30 轮,训练损失:0.2219,训练精度:92.31%,测试损失:0.3324,测试精度:88.03%
008/30 轮,训练损失:0.2010,训练精度:93.13%,测试损失:0.3449,测试精度:88.19%
009/30 轮,训练损失:0.1823,训练精度:93.92%,测试损失:0.3210,测试精度:88.26%
010/30 轮,训练损失:0.1638,训练精度:94.73%,测试损失:0.3600,测试精度:87.18%
011/30 轮,训练损失:0.1469,训练精度:95.39%,测试损失:0.3198,测试精度:88.87%
012/30 轮,训练损失:0.1310,训练精度:95.96%,测试损失:0.3082,测试精度:89.11%
013/30 轮,训练损失:0.1142,训练精度:96.72%,测试损失:0.3256,测试精度:88.75%
014/30 轮,训练损失:0.1003,训练精度:97.25%,测试损失:0.3263,测试精度:88.85%
015/30 轮,训练损失:0.0884,训练精度:97.69%,测试损失:0.3254,测试精度:89.12%
016/30 轮,训练损失:0.0743,训练精度:98.27%,测试损失:0.3286,测试精度:89.86%
017/30 轮,训练损失:0.0629,训练精度:98.67%,测试损失:0.3608,测试精度:88.71%
018/30 轮,训练损失:0.0535,训练精度:98.98%,测试损失:0.3162,测试精度:89.73%
019/30 轮,训练损失:0.0440,训练精度:99.36%,测试损失:0.3553,测试精度:89.56%
020/30 轮,训练损失:0.0376,训练精度:99.48%,测试损失:0.3171,测试精度:90.01%
021/30 轮,训练损失:0.0311,训练精度:99.69%,测试损失:0.3264,测试精度:89.91%
022/30 轮,训练损失:0.0249,训练精度:99.81%,测试损失:0.3746,测试精度:88.89%
023/30 轮,训练损失:0.0214,训练精度:99.90%,测试损失:0.3387,测试精度:89.95%
024/30 轮,训练损失:0.0185,训练精度:99.92%,测试损失:0.3616,测试精度:89.70%
025/30 轮,训练损失:0.0155,训练精度:99.96%,测试损失:0.3775,测试精度:89.90%
026/30 轮,训练损失:0.0134,训练精度:99.97%,测试损失:0.3476,测试精度:90.39%
027/30 轮,训练损失:0.0117,训练精度:99.99%,测试损失:0.3604,测试精度:90.26%
028/30 轮,训练损失:0.0103,训练精度:99.99%,测试损失:0.3939,测试精度:89.50%
029/30 轮,训练损失:0.0095,训练精度:99.99%,测试损失:0.3723,测试精度:90.18%
030/30 轮,训练损失:0.0084,训练精度:100.00%,测试损失:0.3758,测试精度:90.04%
ResNet训练过程可视化
ResNet训练过程可视化

5.3 ResNeXt 训练结果

查看ResNeXt完整训练过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
001/30 轮,训练损失:1.1331,训练精度:73.02%,测试损失:0.5520,测试精度:80.88%
002/30 轮,训练损失:0.4683,训练精度:83.26%,测试损失:0.5709,测试精度:79.78%
003/30 轮,训练损失:0.4076,训练精度:85.35%,测试损失:0.4815,测试精度:83.95%
004/30 轮,训练损失:0.3679,训练精度:86.83%,测试损失:0.4586,测试精度:83.86%
005/30 轮,训练损失:0.3410,训练精度:87.82%,测试损失:0.4085,测试精度:85.31%
006/30 轮,训练损失:0.3200,训练精度:88.52%,测试损失:0.4353,测试精度:84.45%
007/30 轮,训练损失:0.3013,训练精度:89.15%,测试损失:0.3780,测试精度:86.73%
008/30 轮,训练损失:0.2866,训练精度:89.76%,测试损失:0.3776,测试精度:86.71%
009/30 轮,训练损失:0.2721,训练精度:90.30%,测试损失:0.3855,测试精度:86.57%
010/30 轮,训练损失:0.2600,训练精度:90.77%,测试损失:0.4883,测试精度:82.86%
011/30 轮,训练损失:0.2459,训练精度:91.23%,测试损失:0.3821,测试精度:86.15%
012/30 轮,训练损失:0.2366,训练精度:91.65%,测试损失:0.5613,测试精度:82.66%
013/30 轮,训练损失:0.2259,训练精度:92.01%,测试损失:0.4315,测试精度:84.59%
014/30 轮,训练损失:0.2160,训练精度:92.39%,测试损失:0.3506,测试精度:87.52%
015/30 轮,训练损失:0.2065,训练精度:92.66%,测试损失:0.3490,测试精度:87.52%
016/30 轮,训练损失:0.1982,训练精度:93.01%,测试损失:0.3982,测试精度:85.88%
017/30 轮,训练损失:0.1889,训练精度:93.42%,测试损失:0.3827,测试精度:86.97%
018/30 轮,训练损失:0.1802,训练精度:93.83%,测试损失:0.3707,测试精度:87.14%
019/30 轮,训练损失:0.1715,训练精度:94.11%,测试损失:0.3806,测试精度:87.30%
020/30 轮,训练损失:0.1661,训练精度:94.28%,测试损失:0.4515,测试精度:85.46%
021/30 轮,训练损失:0.1573,训练精度:94.65%,测试损失:0.3794,测试精度:87.28%
022/30 轮,训练损失:0.1514,训练精度:94.88%,测试损失:0.3444,测试精度:88.06%
023/30 轮,训练损失:0.1449,训练精度:95.07%,测试损失:0.4038,测试精度:86.90%
024/30 轮,训练损失:0.1397,训练精度:95.25%,测试损失:0.3766,测试精度:87.71%
025/30 轮,训练损失:0.1312,训练精度:95.71%,测试损失:0.5073,测试精度:86.13%
026/30 轮,训练损失:0.1256,训练精度:95.88%,测试损失:0.4904,测试精度:85.24%
027/30 轮,训练损失:0.1181,训练精度:96.25%,测试损失:0.4093,测试精度:86.69%
028/30 轮,训练损失:0.1125,训练精度:96.36%,测试损失:0.5151,测试精度:85.10%
029/30 轮,训练损失:0.1063,训练精度:96.60%,测试损失:0.3796,测试精度:87.71%
030/30 轮,训练损失:0.0987,训练精度:96.85%,测试损失:0.4043,测试精度:87.98%
ResNeXt训练过程可视化
ResNeXt训练过程可视化

5.4 性能对比分析

ResNet 训练表现:

  • 最终训练精度:100.00%
  • 最终测试精度:90.04%
  • 收敛特点:快速收敛,过拟合明显
  • 参数优势:结构简单,参数集中

ResNeXt 训练表现:

  • 最终训练精度:96.85%
  • 最终测试精度:87.98%
  • 收敛特点:较稳定,泛化性更好
  • 参数优势:分组卷积,参数更少

实验结论分析:

ResNet 在本次实验中的表现优于 ResNeXt,训练和测试准确率更高、损失更低,收敛也更快。可能的主要原因是:

数据集规模:ResNet 结构更简单、参数更集中,适合小数据集

训练轮数:有限的训练轮数下,ResNet 能更快收敛

任务复杂度:Fashion-MNIST 相对简单,ResNeXt 优势未充分体现

大数据集:ResNeXt 的优势或许在大数据集和更复杂任务中才能体现


总结

本文深入探讨了残差网络的理论基础和实际应用:

  1. 理论创新:残差学习解决了深度网络训练中的梯度消失问题,通过跳跃连接实现恒等映射
  2. 架构设计:ResNet 的残差块设计简洁有效,确保网络能退化为浅层网络的性能下界
  3. 技术进步:ResNeXt 通过分组卷积引入”基数”概念,在参数效率上有显著提升
  4. 实验验证:两种网络在 Fashion-MNIST 上的表现证明了残差连接的有效性
  5. 应用价值:残差网络的思想影响了后续几乎所有的深度学习架构设计

残差网络不仅解决了深度网络的训练难题,更重要的是为深度学习提供了新的设计范式,成为现代深度学习架构的基石。