🔗 声明
- 本文为「365 天深度学习训练营」内部学习记录。
- 本文参考 K 同学啊课程内容完成,仅用于个人学习与交流。
- 猴痘病识别数据集仅用于学习交流,请勿对外分享数据集。
- 本篇为个人在 J8 关卡上的实践记录。
- 论文: Aggregated Residual Transformations for Deep Neural Networks(ResNeXt,CVPR 2017)
第 J8 周:ResNeXt-50 算法解析
ResNeXt(Residual Networks with Aggregated Transformations)由 Facebook AI Research 在 2016 年提出,2017 年发表于 CVPR。它沿用 ResNet 的残差相加,把 Inception 那种「多路变换」收成同构分支 ,用分组卷积一次写完,再用 基数(cardinality) 控制并行路径数。本周对照 ResNet50V2 与 DenseNet,并用 ResNeXt-50(32×4d)完成猴痘二分类。
📌 本周任务
- 阅读 ResNeXt 论文,弄清作者为什么要加「基数」这一维
- 对比此前的 ResNet50V2 、DenseNet
- 用 ResNeXt-50 完成猴痘病识别
🏡 我的环境
- 语言:Python 3.10
- 工具:脚本
train_j8.py - 框架:PyTorch
2.5.1+cu121,GPU:RTX 4060 Ti - 数据集:与 T4/J5/J6 同源
data→../t4/45-data(两类:Monkeypox / Others,共 2142 张)
训练设定(与 J5/J6 对齐):
| 项 | 设定 |
|---|---|
| 划分 | 8:2,seed=42 |
| 输入 | 224×224,ImageNet 归一化;训练水平翻转 |
| 优化 | AdamW,lr=1e-4,batch=16,epochs=10 |
| 初始化 | 随机初始化(不加载预训练) |
| 结构 | ResNeXt-50 32×4d (cardinality=32,base_width=4,层数 [3,4,6,3]) |
一、作者要解决什么问题?
加深、加宽都能涨点,但手工设计每一条分支(Inception 那种)又难又贵。论文的策略是:在相近参数量和计算量下,用一个新维度换精度------基数 C:一个 block 里相同变换路径的条数。
容量可以沿三轴扩:
| 维度 | 含义 | 谁在用 |
|---|---|---|
| 深度 Depth | 堆更多层 | ResNet、DenseNet |
| 宽度 Width | 加通道 | Wide ResNet |
| 基数 Cardinality | 加并行同构路径 | ResNeXt |
聚合方式:各路径做变换后 求和 (实现上就是 groups=C 的 3×3),再和恒等映射残差相加。
二、一个 block:ResNet 与 ResNeXt
以输入 256 通道的 bottleneck 为例。
ResNet: 1×1 把 256 压到 64(4 倍),3×3 处理这 64 通道,再 1×1 拉回 256,与输入相加。
ResNeXt-50 32×4d: 中间宽度按
\\text{width} = \\text{planes} \\times \\frac{\\text{base_width}}{64} \\times C
第一阶段 planes=64、C=32、base_width=4 时,width = 128。也就是 32 组、每组 4 通道(课程里说的「压缩 64 倍到 4 通道」),分组 3×3 各自卷积,组间不混通道,最后 1×1 还原到 256 再残差相加。

层数配置与 ResNet-50 相同:[3, 4, 6, 3],所以叫 ResNeXt-50;101 层则把第三阶段从 6 改成 23。
三、分组卷积在省什么?
普通卷积里每个核看全部输入通道。分组卷积把通道切开,每个核只看本组通道,输出再在通道维拼回去。组数等于基数时,3×3 的参数大约按组数下降,省下来的预算可以拿去增加路径数,而不是把单路做得更宽。
论文在 ImageNet 上的结论:固定复杂度时,加大基数往往比单纯加宽或加深更划算。这是大规模数据上的结论;小数据从零短训不一定复现这个排序(见第六节)。
四、和 ResNet50V2、DenseNet 怎么比
三者都在解决「深层怎么把特征传下去」,接法不同。
| ResNet50V2(J2) | DenseNet121(J3) | ResNeXt-50(本周) | |
|---|---|---|---|
| 跨层方式 | 残差 相加,且相加后不再 ReLU(预激活) | 块内全层 concat | 残差 相加(后激活,同 ResNet V1) |
| 块内变换 | 单路 bottleneck | 每层只长出 (k) 个新通道 | C 条同构路径,用分组卷积实现 |
| 设计旋钮 | 深度、预激活顺序 | growth rate、压缩率 (\theta) | cardinality / base_width |
| 参数量级 | ~2351 万(3 类头) | ~696 万 | ~2298 万(2 类头;1000 类约 2500 万) |
| 当时任务 | 膝关节三分类,最优 Test ACC 89.2% | 膝关节三分类,88.9% | 猴痘二分类,见下 |
结构上可以记三句话:
- ResNet50V2 关心恒等路径是否被 ReLU 截断:BN-ReLU-Conv 放在支路里,相加后直接输出。
- DenseNet 不相加,把前面所有层在通道维接起来,用很少的新增通道换特征重用,所以更轻。
- ResNeXt 不改残差公式,只把 (F(x)) 从「一条宽卷积」换成「多条窄的分组卷积再聚合」。它更接近 ResNet,和 DenseNet 的 concat 不是一条路线。
J2/J3 的准确率来自膝关节数据,不能和本周猴痘数字直接比高低。同协议、同数据的参照是 J5 的 SE-DenseNet121(79.0%)和 J6 的 Inception v1(87.2%)。
五、PyTorch 实现要点
中间宽度和分组 3×3:
python
width = int(planes * (base_width / 64.0)) * cardinality
self.conv2 = nn.Conv2d(
width, width, kernel_size=3, stride=stride, padding=1,
groups=cardinality, bias=False,
)
前向仍是标准 bottleneck,最后残差相加:
python
out = self.relu(self.bn1(self.conv1(x)))
out = self.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
if self.downsample is not None:
identity = self.downsample(x)
return self.relu(out + identity)
工厂函数给出 50 / 101:
python
resnext_config = {
"ResNeXt50": {"layers": [3, 4, 6, 3], "cardinality": 32, "base_width": 4},
"ResNeXt101_32x4d": {"layers": [3, 4, 23, 3], "cardinality": 32, "base_width": 4},
"ResNeXt101_64x4d": {"layers": [3, 4, 23, 3], "cardinality": 64, "base_width": 4},
}
形状自检(1000 类头):输入 [2, 3, 224, 224] → 输出 [2, 1000]。本周分类头改为 2 类。
六、训练日志
device: cuda
classes: ['Monkeypox', 'Others'] n= 2142
ResNeXt50 params=22,984,002
Epoch: 1, Train_acc:59.9%, Train_loss:0.728, Test_acc:60.4%, Test_loss:0.812, Lr:1.00E-04
Epoch: 2, Train_acc:64.2%, Train_loss:0.650, Test_acc:60.1%, Test_loss:0.790, Lr:1.00E-04
Epoch: 3, Train_acc:66.1%, Train_loss:0.650, Test_acc:66.4%, Test_loss:0.597, Lr:1.00E-04
Epoch: 4, Train_acc:67.7%, Train_loss:0.605, Test_acc:63.6%, Test_loss:0.648, Lr:1.00E-04
Epoch: 5, Train_acc:67.5%, Train_loss:0.606, Test_acc:61.1%, Test_loss:0.669, Lr:1.00E-04
Epoch: 6, Train_acc:70.1%, Train_loss:0.572, Test_acc:62.9%, Test_loss:0.711, Lr:1.00E-04
Epoch: 7, Train_acc:71.2%, Train_loss:0.556, Test_acc:71.6%, Test_loss:0.540, Lr:1.00E-04
Epoch: 8, Train_acc:73.4%, Train_loss:0.532, Test_acc:74.1%, Test_loss:0.532, Lr:1.00E-04
Epoch: 9, Train_acc:76.9%, Train_loss:0.492, Test_acc:70.2%, Test_loss:0.572, Lr:1.00E-04
Epoch:10, Train_acc:77.8%, Train_loss:0.480, Test_acc:70.9%, Test_loss:0.588, Lr:1.00E-04
ResNeXt50 best_test_acc=0.7413

结果解读
| 项 | 数值 |
|---|---|
| 参数量 | 22,984,002(2 类头) |
| 最优 Test ACC | 74.1%(第 8 轮) |
| 最优 Test Loss | 0.532 |
同数据、同超参:
| 模型 | 参数量 | 最优 Test ACC |
|---|---|---|
| SE-DenseNet121(J5) | ~699 万 | 79.0% |
| Inception v1(J6) | ~598 万 | 87.2% |
| ResNeXt-50 32×4d(J8) | ~2298 万 | 74.1% |
训练 ACC 还在爬(第 10 轮 77.8%),测试在第 8 轮见顶后回落。模型比 J5/J6 大一个数量级,2142 张图、10 个 epoch、从零训练,容量用不满,基数带来的收益在这个设定里体现不出来。这和论文「同等复杂度下 ResNeXt 优于 ResNet」并不矛盾:那边是 ImageNet 规模和充分训练。
复现命令:
bash
/home/sqc/micromamba/envs/torch2.1-cuda12.1/bin/python train_j8.py
权重:best_model_resnext50.pth;明细:j8_result.txt / j8_result.json。
七、个人小结
- 基数 是第三条扩容轴:不手写异构分支,用
groups=cardinality得到 C 条同构变换,再求和并残差相加。 - 和 ResNet50V2:残差公式同类,但 ResNeXt 仍是后激活 bottleneck;V2 的预激活解决的是恒等路径被截断,ResNeXt 解决的是 (F(x)) 怎么拆成多路。
- 和 DenseNet:一个相加、一个 concat。DenseNet 用特征重用换更少参数;ResNeXt 参数量仍在 ResNet-50 量级。
- 实验 :猴痘二分类最优 Test ACC 74.1%。小数据短训下它不如更轻的 Inception v1 / SE-DenseNet;要看见论文里的基数优势,需要更长训练或预训练微调。