前几天有个刚入门的朋友问我,都这个年代了,YOLO系列已经迭代到v11,Transformer在各种任务上横扫榜单,再回头啃一个2012年的AlexNet网络结构,是不是有点浪费时间?我当时没直接回答,而是让他先说说AlexNet里第一个卷积层的卷积核尺寸是多少、步长是多少、输出特征图是多大——他卡住了。这个场景我见过太多次:很多人上手就是调库、跑demo,模型能训起来,但对网络结构内部的信息流动毫无概念,一旦遇到shape不匹配或者想改结构就彻底懵掉。AlexNet恰恰是解决这个问题的最佳切口,它的网络结构足够简单直白,五层卷积加三层全连接,没有任何花哨的残差连接或者注意力机制,每一层的张量形状都能手算出来。这份内容我会带着你把AlexNet网络结构从头到尾拆一遍,再用PyTorch把网络结构的实现完整写出来,顺带把训练里踩过的坑和排查思路一起交代清楚。不管你是刚学完PyTorch张量基础想找个完整项目练手,还是已经能跑通pytorch官方的分类示例、但对底层结构一知半解,这份内容都值得你花时间读完。
1. 2012年的AlexNet,放到今天到底还值不值得啃
1.1 它在深度学习历史上的坐标位置
2012年的ImageNet大规模视觉识别挑战赛上,AlexNet拿下了top-5错误率15.3%的成绩,而那一年的第二名错误率是26.2%,差了将近11个百分点。这个差距在竞赛语境下是碾压级别的,也正是从这一年开始,卷积神经网络正式取代了以SIFT、HOG特征加SVM分类器为主的传统视觉方案,成为图像识别的主流路线。这个成绩背后有两个当时看来非常奢侈的条件:一是120万张带标注的训练图片,二是两块GTX 580显卡提供的算力。前者让模型有足够的样本去学习,后者让研究者第一次能把一个6000万参数的深层网络真正训起来。
我一直觉得理解AlexNet的历史处境比记住它的结构更重要。那个年代显存只有3GB,一块卡根本放不下整个网络,所以论文里才出现了"分组卷积"这种把网络切成两半、分别放在两块卡上跑的设计。今天你在PyTorch里写groups=2只需要一个参数,但当年这是一个被硬件逼出来的工程妥协。搞明白这一点,你看后面的ResNet、Inception、DenseNet这些结构演进时,就能理解每一个设计决策背后的约束条件是什么,而不是死记硬背网络结构图。
1.2 手写一遍和直接调torchvision的区别
torchvision.models.alexnet(pretrained=True)这行代码三秒钟就能给你一个能用的模型,那我为什么还建议你手写一遍?原因很直接:调库版本和你手写的版本在细节上是有差异的,而这些差异恰恰是理解网络结构的关键。torchvision里的AlexNet为了适配224×224的输入和现代训练习惯,把第一层的64个卷积核改成了64(原论文是96),并且在分类器前面加了一个AdaptiveAvgPool2d。你要是直接拿这个版本去对照论文里的结构图,会发现对不上号,然后就开始怀疑自己是不是看错了资料。
手写一遍还有个隐性收益:你会被迫去算每一层的输出尺寸。我见过太多人写网络时把卷积层堆在一起,最后一个Linear层的输入维度靠试错凑出来,报错了就改数字,改到不报错为止。这种写法在简单网络上能蒙对,但一旦网络结构复杂一点,或者你想插入一个新的模块,就彻底玩不转了。自己动手推一遍(H + 2p - k) / s + 1这个公式,把每一层的输出形状写在纸上,这个习惯的价值远超一次性的代码复现。
2. 拆解网络结构:从227×227到1000类的那条链路
2.1 输入尺寸的争论:227还是224
这里有个很多人忽略的细节。原始论文里写的输入是227×227×3,但你在各种教程和开源实现里看到的输入尺寸经常是224×224,包括PyTorch官方版本用的也是224。这不是谁抄错了,而是历史遗留问题。227这个数字的来源是这样的:ImageNet的原始图片尺寸不一,论文里先把图片缩放到256×256,然后随机裁剪出224×224的区域送进网络。但227×227这个数字出现在论文的表格里,是因为当时那个表格统计的是不同的输入设定。
真正影响你实现的是:你选227还是224,会直接决定后面所有层的张量形状。选227的话,第一层卷积输出是56×56;选224的话,输出是55×55。听起来只差1,但经过三次池化之后,227路线得到的特征图是6×6,224路线得到的是6×6(因为(55-3)/2+1 = 27,再经过两层池化变13,再池化变6),两者最终都能对上9216这个全连接输入维度。所以两种输入在PyTorch里都能跑通,这也是为什么大家不太在意这个差别。
提示:如果你打算加载预训练权重,务必用224×224的输入,因为预训练权重是在这个尺寸下训练出来的。用227去加载虽然形状能对上,但特征分布会有细微偏移,精度会掉一点。
2.2 逐层形状推演与手算过程
我把整个前向传播的形状变化整理成了一张表,你对照着看会非常清楚每一步发生了什么。卷积输出尺寸的计算公式是out = floor((in + 2 * padding - kernel_size) / stride) + 1,池化层同理。
| 层名 | 操作 | 卷积核/窗口 | 步长 | 填充 | 输出形状 |
|---|---|---|---|---|---|
| 输入 | - | - | - | - | 3×227×227 |
| conv1 | 卷积 | 11×11, 96 | 4 | 2 | 96×56×56 |
| pool1 | 最大池化 | 3×3 | 2 | 0 | 96×27×27 |
| conv2 | 卷积(分组2) | 5×5, 256 | 1 | 2 | 256×27×27 |
| pool2 | 最大池化 | 3×3 | 2 | 0 | 256×13×13 |
| conv3 | 卷积 | 3×3, 384 | 1 | 1 | 384×13×13 |
| conv4 | 卷积(分组2) | 3×3, 384 | 1 | 1 | 384×13×13 |
| conv5 | 卷积(分组2) | 3×3, 256 | 1 | 1 | 256×13×13 |
| pool3 | 最大池化 | 3×3 | 2 | 0 | 256×6×6 |
| flatten | 展平 | - | - | - | 9216 |
| fc1 | 全连接 | - | - | - | 4096 |
| fc2 | 全连接 | - | - | - | 4096 |
| fc3 | 全连接 | - | - | - | 1000 |
拿第一层举例,手算过程是(227 + 2*2 - 11) / 4 + 1 = 220 / 4 + 1 = 55 + 1 = 56。注意这里220除以4正好是55,是整除的,所以没有向下取整的损失。第二层的池化(56 - 3) / 2 + 1 = 26.5 + 1,这里必须向下取整成26,所以结果是27。这个取整操作是最容易出错的地方,PyTorch默认的floor行为和论文一致,但你自己手算的时候要记得取整。
2.3 分组卷积:被显存逼出来的巧妙设计
分组卷积这个概念在今天看来有点陌生,因为现在显存动辄24G、80G,很少有人还需要靠拆分网络来塞进显卡。但在AlexNet那个年代,这是必需的。具体做法是把卷积核和输入通道都分成两组,每组只处理自己那一半的输入通道,两组之间不通信,最后把输出拼接起来。
看这张对比表会更直观:
| 层 | 是否分组 | 每组输入通道 | 每组输出通道 | 实际意义 |
|---|---|---|---|---|
| conv1 | 否 | 3 | 96 | 输入通道太少,分不了 |
| conv2 | 是 | 48 | 128 | 两块卡各算一半 |
| conv3 | 否 | 256 | 384 | 跨组连接,信息融合 |
| conv4 | 是 | 192 | 192 | 再次分组 |
| conv5 | 是 | 192 | 128 | 输出前最后一次分组 |
conv3这一层特别有意思,它是唯一一个把两组信息重新连通的卷积层。前两层分组算完之后,如果继续分组下去,两组特征就永远不交流了,模型的表达能力会严重受限。所以论文在中间插了一个不分组的conv3,把256个输入通道全部连接起来,做一次信息融合,然后再分组往下走。这个设计思路其实和后来Inception里的分支融合、ResNet里的残差连接有异曲同工之处,都是在解决"分支之间如何交流"这个问题。
2.4 参数量分布:6000万参数都藏在哪
我算了一下每一层的参数量,结果非常颠覆直觉:
| 层 | 权重参数量 | 占总参数比例 |
|---|---|---|
| conv1 | 34,848 | 0.06% |
| conv2 | 307,200 | 0.50% |
| conv3 | 884,736 | 1.45% |
| conv4 | 663,552 | 1.09% |
| conv5 | 442,368 | 0.73% |
| fc1 | 37,748,736 | 61.9% |
| fc2 | 16,777,216 | 27.5% |
| fc3 | 4,096,000 | 6.7% |
五个卷积层加起来只有233万参数,占总量的3.8%,而三个全连接层吃掉了剩下的96%。这个数据我第一次算出来的时候也愣了一下。原因在于卷积层的权重是共享的,一个11×11×3的卷积核扫过整张图,参数量只有363个;而全连接层每个神经元都要和上一层的每一个输出相连,9216×4096这一层就堆了3775万个参数。
这个发现直接解释了AlexNet为什么容易过拟合、为什么dropout这么关键。论文在前两层全连接后都加了dropout,概率0.5,就是为了压住这部分参数量带来的过拟合风险。后来的网络结构演进中,全连接层被逐步削减,直到ResNet之后基本被全局平均池化替代,本质上就是在解决参数冗余的问题。
3. PyTorch落地实现:一份能直接跑通的代码
3.1 环境准备与版本选择
先把环境弄干净。我的习惯是用conda单独建一个环境,避免和系统的Python环境互相污染:
conda create -n alexnet python=3.10 -y conda activate alexnet现在装PyTorch。这里有个高频的踩坑点:不要直接pip install torch,那样装到的可能是CPU版本,也可能因为源的问题装到和你的CUDA不匹配的版本。先查一下你的CUDA版本:
nvidia-smi假设输出显示CUDA Version是12.1,那就装对应的版本:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121如果你机器上根本没有NVIDIA显卡,或者只想先在CPU上把结构跑通,那就用:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu装完之后一定要验证一下:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU only")注意:如果这里报了
OSError: [WinError 1114] 动态链接库(DLL)初始化例程失败,绝大多数情况下是VC++运行库缺失,去装一个微软的Visual C++ Redistributable就能解决。还有一种可能是conda环境和pip混装导致的DLL冲突,这时候最简单粗暴的办法是删掉环境重建,全程只用pip装PyTorch。
关于anaconda和PyCharm的配合,我个人的建议是不要在PyCharm里创建虚拟环境,而是在终端里用conda建好环境,然后在PyCharm的项目解释器设置里指向这个环境的python.exe。这样环境管理会清晰很多,换编辑器的时候环境照样能用。
3.2 从零搭建网络层
下面这份代码是忠实还原论文结构的版本,包括分组卷积:
import torch import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes=1000): super(AlexNet, self).__init__() self.features = nn.Sequential( # conv1: 3 -> 96, 11x11, stride 4, pad 2 nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # conv2: 96 -> 256, 5x5, groups=2 nn.Conv2d(96, 256, kernel_size=5, padding=2, groups=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # conv3: 256 -> 384, 3x3, 跨组连接 nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(inplace=True), # conv4: 384 -> 384, groups=2 nn.Conv2d(384, 384, kernel_size=3, padding=1, groups=2), nn.ReLU(inplace=True), # conv5: 384 -> 256, groups=2 nn.Conv2d(384, 256, kernel_size=3, padding=1, groups=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), ) self.classifier = nn.Sequential( nn.Dropout(p=0.5), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Linear(4096, num_classes), ) def forward(self, x): x = self.features(x) x = torch.flatten(x, 1) x = self.classifier(x) return x这份代码里有几个地方值得单独说。inplace=True这个参数是省显存的小技巧,ReLU直接修改输入张量而不新建一个,在显存紧张的场景下能省下不少空间,代价是副作用会覆盖原始输入,调试时看不到激活前的值。torch.flatten(x, 1)里的1表示从第1维开始展平,保留batch维度,这是最容易写错的地方,写成torch.flatten(x)会把batch也拍平,后面全连接层的维度就对不上了。
3.3 权重初始化与形状自检
AlexNet论文里给了一个明确的初始化方案:权重用均值为0、标准差0.01的高斯分布,第二、四、五层卷积和全连接层的偏置初始化为1,其余层的偏置初始化为0。这个偏置为1的细节是为了给ReLU提供正输入,避免大量神经元在训练初期就死掉。
def init_weights(m): if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear): nn.init.normal_(m.weight, mean=0.0, std=0.01) if m.bias is not None: nn.init.constant_(m.bias, 0) model = AlexNet(num_classes=1000) model.apply(init_weights) # 把第二、四、五层卷积和全连接层的bias置为1 for name, module in model.named_modules(): if name in ['features.3', 'features.8', 'features.10', 'classifier.1', 'classifier.4']: nn.init.constant_(module.bias, 1)写完一定要做形状自检,这里一个小技巧就够用:
x = torch.randn(1, 3, 227, 227) out = model(x) print(out.shape) # 期望 torch.Size([1, 1000]) # 统计参数量 total = sum(p.numel() for p in model.parameters()) print(f"Total params: {total:,}") # 期望约 61,000,000 上下如果形状报错,用逐层打印的方式定位:
x = torch.randn(1, 3, 227, 227) for i, layer in enumerate(model.features): x = layer(x) print(f"layer {i}: {layer.__class__.__name__} -> {x.shape}")这个方法比看报错信息快得多,一眼就能看出是哪一层开始对不上的。
3.4 分组卷积在PyTorch里的写法与几个细节
groups=2这个参数看起来简单,但有几个约束条件必须记住:输入通道数和输出通道数都必须能被groups整除。conv2里输入96除以2得48,输出256除以2得128,都整除,没问题。如果你手改网络结构时把输出通道改成255,程序会直接报错。第一层为什么不能分组?因为输入只有3个通道,3除以2不是整数,PyTorch会直接抛异常,所以那些Caffe版本里第一层带分组参数的配置,在PyTorch里需要把输入先复制成两份6通道,这显然不划算,官方实现也就放弃了。
分组卷积的一个副作用是组间信息不流通。如果你在两层分组卷积之间不插入不分组层,模型相当于在并行训练两个独立的子网络,表达能力会明显下降。这就是为什么conv3必须存在。你在自己改结构做实验的时候,如果打算用分组卷积来减少参数量,一定要规划好"哪一层做融合",否则精度会掉得很厉害。
4. 训练侧的调参经验与论文超参对比
4.1 论文里的超参数还原
论文给的训练配置在今天的代码里需要做一些调整才能复现。我把关键参数整理如下:
| 超参数 | 论文取值 | 现在复现的建议 |
|---|---|---|
| 优化器 | SGD | SGD,暂时不用Adam |
| 动量 | 0.9 | 0.9 |
| 权重衰减 | 0.0005 | 0.0005 |
| batch size | 128 | 128(显存不够降到64) |
| 初始学习率 | 0.01 | 0.01 |
| 学习率衰减 | 验证误差不降时除以10 | ReduceLROnPlateau |
| 训练轮数 | 约90轮 | 视数据集而定 |
| dropout | 0.5 | 0.5 |
这里有个细节:论文里用了一个手动触发的学习率衰减策略,当验证集错误率不再下降时把学习率除以10。在PyTorch里我一般用ReduceLROnPlateau来实现:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.1, patience=3, verbose=True)每轮训练结束后调用scheduler.step(val_loss),它就会自动判断要不要降学习率。patience=3表示验证损失连续3轮不下降就降一次。
另外,论文里提到LRN层(局部响应归一化)用在conv1和conv2之后,但我在实际复现时基本都省略掉了。原因是LRN对精度的提升很小(论文里说贡献了1.4%的top-1),而计算开销不小,后来的BatchNorm在这方面的效果全面碾压LRN。如果你想忠实还原,加两行nn.LocalResponseNorm(size=5, alpha=1e-4, beta=0.75, k=2.0)也行,但没必要。
4.2 数据增强与归一化的实际做法
论文的数据增强方案有两块,一是随机裁剪加水平翻转,二是PCA颜色抖动。第二块在今天的实现里基本被颜色抖动(ColorJitter)替代了,效果差不多但实现简单得多。
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])验证集用CenterCrop而不是RandomCrop,这是为了评估的稳定性和可复现性。归一化用的这组均值方差是ImageNet的统计值,如果你换了自己的数据集,最好重新统计一下,尤其是医学影像或者工业质检这类和自然图像分布差异很大的场景,直接套用ImageNet的归一化参数会让训练初期很不稳定。
4.3 学习率调度与优化器选择
我知道你可能想问为什么不用Adam。我的答案很直接:在AlexNet这种结构上,SGD加动量训练出来的模型泛化性通常更好,尤其是配合dropout和权重衰减的时候。Adam收敛快,但容易在小数据集上过拟合,而且对权重衰减的处理方式和SGD不一样,你需要重新调超参。如果你只是想快速验证结构能不能跑通,用Adam也无妨;但如果是要认真复现一个结果,老老实实用SGD。
还有一个高频问题:batch size能不能改。能改,但学习率要跟着调。经验法则是batch size翻倍,学习率也翻倍左右(线性缩放规则)。比如你把batch size从128降到64,学习率可以从0.01降到0.005。这是粗略估计,实际还要看你的数据集大小和训练轮数。
如果你的数据集很小(比如只有几千张图),我建议直接加载预训练权重然后微调,而不是从头训。冻结前面几层卷积、只训练后面的分类器是个常用的做法:
for name, param in model.named_parameters(): if 'features' in name: param.requires_grad = False这样训练速度快很多,显存占用也小,几百张图就能拿到不错的效果。
5. 踩坑实录:那些报错信息和它们的解法
5.1 形状不匹配的排查套路
最常见的报错就是RuntimeError: mat1 and mat2 shapes cannot be multiplied,出现在全连接层。根因八成是卷积输出展平后的维度和nn.Linear的输入维度对不上。排查顺序是这样的:先确认输入图片尺寸对不对,再逐层打印卷积输出形状,最后反推全连接层应该填多少。我在3.3节给的那个逐层打印方法在这里特别好用,比盯着报错信息瞎猜高效得多。
还有个隐蔽的坑是AdaptiveAvgPool2d。如果你从torchvision的AlexNet源码里抄了一部分,可能会带上这个层,它会让特征图自动缩放到指定尺寸,从而掩盖输入尺寸不匹配的问题。用起来方便,但你就不清楚真实的特征图尺寸是多少了。学习阶段我建议先不要用它,等你能把固定尺寸的流程跑通了再说。
5.2 分组卷积相关的报错
ValueError: in_channels must be divisible by groups这个报错就是因为通道数不能被整除。改通道数的时候记住这个约束。另一个容易忽略的点是权重加载。如果你手写的分组版本想去加载torchvision的预训练权重,会直接失败,因为torchvision版本没有分组,对应的参数张量形状完全不一样。这种情况要么放弃加载权重,要么手动写一个映射脚本,把官方权重的通道切分成两半填进去,工作量不小。
5.3 显存爆炸与batch size选择
AlexNet那6000万参数里有9600万是发生在全连接层的中间激活值上(9216×128这个中间张量,如果batch size是128,单个样本就是9216个float,乘以4096个输出,显存占用很容易爆)。如果你在4G或6G显存的卡上跑,把batch size降到32甚至16是必须的。另外可以开启混合精度训练来省显存:
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() with autocast(): output = model(images) loss = criterion(output, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()混合精度大概能把显存占用降到原来的六成左右,训练速度也能提升,代价是数值精度略有损失,对分类任务来说基本无感。
5.4 训练不收敛的几种典型症状
训练不动、loss一直卡在高位,通常有几个原因。学习率太大导致loss震荡甚至发散,这时候把学习率降一个数量级试试;权重初始化不合理导致梯度爆炸或消失,尤其是如果忘了做初始化、用了PyTorch默认的Kaiming初始化而不是论文里的0.01标准差高斯,前几轮的loss曲线会很难看;数据没有做归一化,输入值域在0-255之间,梯度会非常大,收敛几乎不可能;还有一种情况是标签出错,比如分类任务的标签从1开始而不是从0开始,CrossEntropyLoss会直接报越界或者给出错误结果。
我把常见问题和排查方向整理成了一张表:
| 症状 | 可能原因 | 排查动作 |
|---|---|---|
| loss不下降 | 学习率过大 | 降一个数量级重试 |
| loss变NaN | 梯度爆炸 | 加梯度裁剪、检查学习率 |
| 训练准但验证差 | 过拟合 | 加大dropout、加数据增强 |
| 显存不足 | batch太大 | 降batch、开混合精度 |
| 形状报错 | 输入尺寸或通道不匹配 | 逐层打印张量形状 |
| 加载权重失败 | 结构不一致(分组、通道数) | 对比参数形状逐层检查 |
注意:遇到报错不要第一反应就是改代码,先把报错信息完整读两遍。PyTorch的报错信息其实写得很详细,往往直接点出了是哪一层、哪个维度出了问题,只是很多人看到红色就慌了。
5.5 一个容易被忽视的评测细节
训练完最后一轮的时候,一定要用model.eval()切换到评估模式,同时用torch.no_grad()包住推理过程。前者让dropout和BatchNorm进入评估行为,后者禁止梯度计算从而省显存。很多人忘了model.eval(),结果验证精度比实际低了十几个点,还以为模型没训好,排查半天才发现是这个开关没切。这个坑我踩过不止一次,现在写训练脚本第一件事就是把评估模式的代码块写好,训练循环里复制粘贴。
6. 从AlexNet延伸出去的一些想法
6.1 LRN为什么被淘汰了
LRN这个层在AlexNet里做了局部响应归一化,思路是让相邻通道之间形成侧向抑制,模拟生物神经元的竞争机制。但后来的实践发现,它的收益有限,计算开销却不小,而且BatchNorm在归一化这件事上做得更彻底、更稳定。BatchNorm是在一个batch内对每个通道做标准化,直接解决了内部协变量偏移问题,效果远好于LRN。现在你看任何一个现代网络,都找不到LRN的影子了。这个案例说明一个问题:不是所有论文里的设计都值得保留,理解它存在的原因和它被替代的原因,比记住它的公式更重要。
6.2 结构演进的一条线索
从AlexNet往后看,网络结构演进有一条很清晰的线索:减少全连接层的参数、增加卷积层的深度和复杂度、引入跨层连接。VGG把卷积核统一成3×3并堆到19层,参数依然庞大;ResNet用残差连接把网络推到上百层,同时用全局平均池化干掉了大部分全连接参数;到了MobileNet这类轻量网络,干脆用深度可分离卷积把参数量压到极致。而YOLO这类检测网络,backbone的设计思路其实就是在分类网络的基础上做剪裁和改造,AlexNet的那套卷积加池化的堆叠方式,在YOLO的早期版本里还能看到影子。
理解这条线索的好处是,你学新网络的时候不会觉得是全新的东西。看到YOLOv11的网络结构,你能认出哪些部分是继承自经典分类网络的backbone思想,哪些部分是针对检测任务做的专门设计。这种迁移能力靠死记硬背是练不出来的,一定要从最基础的结构开始一行行地推。
6.3 我个人的一些实践体会
我刚开始学深度学习那阵子,也犯过和开头那个朋友一样的错误,觉得老网络过时了不值得看。后来做项目的时候遇到一个网络结构改不动的情况,回头把AlexNet重新推了一遍,才发现问题在于自己一直没建立起"张量形状在层间怎么流动"的直觉。这个直觉建立起来之后,改任何网络都不慌了,不管是插入一个注意力模块还是替换backbone,都能快速定位到需要改哪一层的参数。
如果你正在学PyTorch,我的建议是把AlexNet当做一个解剖标本,自己从头到尾写一遍、跑一遍、改一遍。改什么都可以,把卷积核尺寸改小、把分组数量改成4、把全连接层换成全局平均池化,看看输出形状怎么变、精度怎么变。这种折腾带来的理解深度,是看十篇教程也换不来的。等你把这个网络吃透了,再去看那些复杂的结构,会发现它们不过是同一套基本操作的组合和变体,没你想的那么可怕。