1. 从零拆解AlexNet:为什么它值得反复学
1.1 一个“老古董”为什么还在学习清单里
如果你翻过任何一本深度学习入门书,AlexNet几乎必然出现在前几章。有人觉得它太老了,2012年的东西,现在随便一个轻量级网络在ImageNet上的表现都碾压它。但我的看法恰恰相反:AlexNet是理解现代卷积神经网络设计哲学的最佳标本。它不像VGG那样规整到近乎单调,也不像ResNet那样引入了跳跃连接这种需要额外理解成本的结构。AlexNet身上有一种“工程直觉驱动”的粗粝感,每一个设计选择背后都有非常具体的现实约束。
我最初接触AlexNet是在做一个图像分类的小项目,当时想的是“随便找个经典网络跑跑就行”。结果真正把论文翻来覆去读了几遍、又自己动手复现之后,才发现之前对CNN的理解有多浮于表面。比如为什么第一层卷积核是11×11而不是3×3?为什么用了LRN后来又不用了?Dropout加在哪几层、比例是多少?这些问题如果不亲手搭一遍、跑一遍,很难有切身体会。
这篇学习笔记面向的是已经了解卷积基本概念、但还没有完整复现过一个经典网络的读者。我会从整体设计思路讲到每一层的参数计算,再到实际训练中会遇到的各种坑,尽量把“为什么这么设计”和“我怎么复现”这两件事都说清楚。全文基于我自己的复现经验,代码用PyTorch实现,参数配置会给出具体数值和计算过程。
1.2 先搞清楚AlexNet到底解决了什么问题
2012年之前的图像分类主流方法是手工特征加传统机器学习分类器。SIFT、HOG这些特征提取算子需要大量领域知识来设计,而且泛化能力有限。AlexNet的核心贡献不在于发明了卷积神经网络——CNN的概念早在1980年代就有了——而在于用工程手段把CNN做到足够大、足够深,让它真正在大规模数据上跑出了碾压性的效果。
具体来说,它解决了三个关键问题:第一,计算资源问题,用GPU替代CPU训练,把训练时间从数周缩短到几天;第二,过拟合问题,通过数据增强和Dropout让一个6000万参数的网络不至于在120万张图片上过拟合;第三,训练稳定性问题,用ReLU替代Sigmoid/Tanh,缓解了深层网络的梯度消失。这三个问题每一个都是当时实实在在的瓶颈,AlexNet给出的方案虽然不算优雅,但极其有效。
理解这一点很重要,因为后面我们看到的所有“奇怪”设计——比如把网络拆到两块GPU上训练、比如LRN这种后来被证明效果有限的操作——都是在这个背景下做出的工程妥协。学习的时候不能脱离这个背景去评判。
2. 网络结构逐层拆解与参数计算
2.1 整体架构一览
AlexNet一共8层带权重的层:5个卷积层加3个全连接层。输入是224×224×3的RGB图像(论文中实际用了227×227,后面会解释这个差异)。整体结构可以分成两个阶段:前5层做特征提取,后3层做分类。
先给一个全局的表格,把每层的输入输出尺寸、卷积核大小、步长、通道数都列清楚:
| 层 | 类型 | 输入尺寸 | 卷积核/操作 | 输出通道 | 输出尺寸 |
|---|---|---|---|---|---|
| Conv1 | 卷积 | 227×227×3 | 11×11, stride 4 | 96 | 55×55×96 |
| Pool1 | 最大池化 | 55×55×96 | 3×3, stride 2 | 96 | 27×27×96 |
| Conv2 | 卷积 | 27×27×96 | 5×5, stride 1, pad 2 | 256 | 27×27×256 |
| Pool2 | 最大池化 | 27×27×256 | 3×3, stride 2 | 256 | 13×13×256 |
| Conv3 | 卷积 | 13×13×256 | 3×3, stride 1, pad 1 | 384 | 13×13×384 |
| Conv4 | 卷积 | 13×13×384 | 3×3, stride 1, pad 1 | 384 | 13×13×384 |
| Conv5 | 卷积 | 13×13×384 | 3×3, stride 1, pad 1 | 256 | 13×13×256 |
| Pool3 | 最大池化 | 13×13×256 | 3×3, stride 2 | 256 | 6×6×256 |
| FC1 | 全连接 | 6×6×256=9216 | - | 4096 | 4096 |
| FC2 | 全连接 | 4096 | - | 4096 | 4096 |
| FC3 | 全连接 | 4096 | - | 1000 | 1000 |
这张表建议自己动手推导一遍每一层的输出尺寸,公式是:输出尺寸 = (输入尺寸 - 卷积核 + 2×padding) / stride + 1。推导过程中你会发现一些有意思的细节,比如Conv1用11×11的大核加stride 4,直接把227降到了55,降采样非常激进。这在当时是有意为之的——减少计算量,同时让第一层就捕捉到大尺度的边缘和纹理。
2.2 第一层卷积:为什么是11×11和stride 4
第一层卷积核11×11、步长4,这个配置在现代网络里几乎见不到了。现在主流做法是3×3堆叠,比如VGG就是全部用3×3。那AlexNet为什么用这么大的核?
核心原因是计算效率。227×227的输入如果直接用3×3卷积,第一层的计算量会非常恐怖。用11×11加stride 4,输出直接降到55×55,后续层的计算压力大幅降低。而且第一层卷积核大一点,感受野大,能捕捉到更全局的结构信息,对于纹理、边缘这些低级特征来说,大核其实够用。
但大核也有代价:参数量大。第一层卷积的参数数量是11×11×3×96 + 96 = 34848,看起来还好,但计算量是55×55×96×11×11×3 ≈ 1.05亿次乘法。这个计算量在当时已经是很大的负担了,所以必须用GPU。
实操提示:如果你现在复现AlexNet,第一层用11×11 stride 4是没问题的,但要注意输入尺寸。论文里写的是224×224,但实际计算下来224经过11×11 stride 4的卷积后是
(224-11)/4+1=54.25,不是整数。所以实际实现中要么把输入改成227×227,要么加padding。PyTorch的torchvision实现用的是224×224加padding=2,输出是55×55。这个细节很多人会忽略,导致自己搭的时候尺寸对不上。
2.3 卷积层的通道数变化规律
从通道数来看,AlexNet的走向是96→256→384→384→256。先增后减,中间在Conv3和Conv4达到最大384。这个设计逻辑是:浅层通道少,因为浅层特征种类有限(边缘、颜色、纹理);深层通道多,因为深层需要组合出更多语义模式;最后再降下来,为全连接层做过渡。
Conv3、Conv4、Conv5都是3×3卷积,padding=1,stride=1,所以空间尺寸保持不变,一直是13×13。这三层是连续的小卷积堆叠,目的是在不大幅增加参数量的情况下增加网络深度,提取更复杂的特征组合。这个思路后来被VGG发扬光大,证明了小卷积堆叠的有效性。
Conv2用了5×5的核,padding=2,stride=1,输出保持27×27。这里用5×5而不是3×3,我理解是为了在浅层稍微扩大一下感受野,同时计算量还在可接受范围内。到了Conv3之后,特征图已经比较小了(13×13),再用大核就没什么意义了。
2.4 全连接层的参数爆炸问题
FC1的输入是6×6×256=9216,输出4096,参数量是9216×4096+4096 ≈ 3775万。FC2是4096×4096+4096 ≈ 1678万。FC3是4096×1000+1000 ≈ 410万。三个全连接层加起来接近5900万参数,占了整个网络参数的绝大部分。
这也是AlexNet后来被诟病的地方:全连接层参数太多,容易过拟合,而且推理时内存占用大。后续的网络如ResNet用全局平均池化替代了全连接层,参数量大幅下降。但AlexNet当时用全连接层也是无奈之举——全局平均池化这个思路还没有被广泛接受。
注意事项:如果你自己复现AlexNet,全连接层的参数量会让你的显存吃紧。建议用较小的batch size,或者把FC层的维度适当调小(比如从4096降到2048),对分类效果影响不会太大,但显存占用会明显改善。
3. 那些容易被忽略的关键设计
3.1 ReLU:不只是“快一点”
AlexNet用ReLU替代Sigmoid和Tanh,这个选择现在看起来理所当然,但在当时是一个重要的实验结论。Sigmoid在输入较大或较小时梯度接近0,深层网络反向传播时梯度会逐层衰减,导致浅层几乎学不到东西。ReLU在正区间梯度恒为1,梯度可以顺畅地传回去。
论文里给了一个具体的对比数据:在CIFAR-10上,用ReLU的网络达到25%错误率需要的迭代次数,比用Tanh的网络快了6倍。这个加速效果不是“快一点”,而是“快一个数量级”。我自己的复现也验证了这一点:用ReLU训练时loss下降非常干脆,用Sigmoid则会出现明显的停滞期。
但ReLU也有问题:负区间梯度为0,神经元可能“死亡”。AlexNet的解决方案是用较小的学习率初始化偏置,把偏置初始化为1而不是0,这样在训练初期神经元更容易被激活。这个技巧现在很少提了,但在复现时值得注意。
3.2 Dropout加在哪、加多少
AlexNet在前两个全连接层后面加了Dropout,比例是0.5。注意是前两个全连接层,不是所有层。为什么不全加?因为卷积层的参数共享机制本身就有一定的正则化效果,再加Dropout反而可能影响特征提取。全连接层参数多、容易记住训练样本,所以需要Dropout来强制网络学习更鲁棒的特征。
0.5这个比例也是实验出来的。太低(比如0.2)正则化效果不够,太高(比如0.8)训练时有效容量太小,收敛慢。0.5是一个比较平衡的值。我试过在FC1和FC2上都用0.5,FC3不加,效果最好。如果在FC3也加Dropout,分类层的表达能力会被削弱,反而掉点。
实操心得:Dropout在训练和推理时的行为不一样。训练时随机置零,推理时所有神经元都激活但输出要乘以(1-p)。PyTorch的nn.Dropout已经帮你处理好了,但如果你自己手写,千万别忘了推理时的缩放。我见过有人推理时忘了缩放,结果准确率直接掉10个点。
3.3 数据增强的具体操作
AlexNet用了两种数据增强:随机裁剪和水平翻转。训练时从256×256的图片中随机裁出224×224的区域,然后以0.5的概率水平翻转。测试时从四个角和中心各裁一次,加上水平翻转,一共10次,取平均结果。
随机裁剪的尺寸选择有讲究:256裁到224,裁剪比例大约是0.875,保留了大部分内容但引入了位置扰动。如果裁得太小(比如从256裁到200),可能把目标物体裁掉;裁得太大(比如从256裁到250),扰动不够,正则化效果弱。
水平翻转只对水平方向对称的物体有效。对于猫狗这种左右对称的物体没问题,但对于文字、数字这种有方向性的物体,翻转会改变语义。所以数据增强要根据任务来选,不能无脑套用。
3.4 LRN:一个被时代淘汰的设计
AlexNet用了Local Response Normalization(LRN),对每个通道的响应做局部归一化。公式是:
b_{x,y}^i = a_{x,y}^i / (k + α Σ_{j=max(0,i-n/2)}^{min(N-1,i+n/2)} (a_{x,y}^j)^2)^β
参数设置是k=2, n=5, α=0.0001, β=0.75。这个操作的动机是模仿生物神经元的侧抑制机制,让响应大的通道抑制相邻通道,增强对比度。
但后来的研究(比如VGG论文)发现LRN对最终精度的影响很小,而且增加了计算和调参的负担。现在基本被Batch Normalization替代了。BN通过归一化每层的输入分布来加速训练,效果更稳定,而且不需要手动调α、β这些参数。
复现建议:如果你只是想理解AlexNet的结构,LRN可以跳过,直接不加或者用BN替代。但如果你想严格复现论文结果,LRN还是要加上,虽然提升有限(论文里说top-1提升1.4%,top-5提升1.2%),但毕竟是原版设计。
4. 动手复现:从数据准备到模型训练
4.1 环境配置和依赖
我用的是PyTorch 1.12 + CUDA 11.3,显卡是单张8GB显存的卡。AlexNet在8GB显存上跑batch size 64是没问题的,如果显存更小可以降到32或16。依赖很简单:
pip install torch torchvision numpy matplotlibtorchvision里已经内置了AlexNet的实现,但建议自己手搭一遍,这样才能真正理解每一层的细节。下面是我自己写的版本,和torchvision的略有不同(比如输入尺寸的处理)。
4.2 数据准备与预处理
我用的是CIFAR-10数据集做快速验证,因为ImageNet太大了,下载和训练成本高。CIFAR-10是32×32的图片,直接喂给AlexNet尺寸不匹配,需要先上采样到224×224。虽然这样会损失一些细节,但作为学习用途足够了。
import torch import torchvision import torchvision.transforms as transforms transform_train = transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_test = 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]) ]) trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train) trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True, num_workers=4) testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test) testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False, num_workers=4)归一化用的均值和方差是ImageNet的统计值,虽然CIFAR-10的分布不完全一样,但影响不大。如果你追求极致,可以自己算一下CIFAR-10的均值和方差。
4.3 模型定义与参数初始化
import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes=10): super(AlexNet, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), ) self.classifier = nn.Sequential( nn.Dropout(0.5), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplace=True), nn.Dropout(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注意第一层卷积我加了padding=2,这样224的输入经过11×11 stride 4的卷积后输出是(224-11+4)/4+1=55,和论文一致。如果不加padding,输出是54,后面全连接层的输入维度就要改成256×5×5,整个结构都会变。
参数初始化我用的是Kaiming初始化,比论文里的高斯初始化更稳定:
def init_weights(m): if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') if m.bias is not None: nn.init.constant_(m.bias, 0) model = AlexNet(num_classes=10) model.apply(init_weights)4.4 训练循环与超参数设置
import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) for epoch in range(100): model.train() running_loss = 0.0 for i, (inputs, labels) in enumerate(trainloader): inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() scheduler.step() print(f'Epoch {epoch+1}, Loss: {running_loss/len(trainloader):.4f}')学习率0.01、momentum 0.9、weight decay 5e-4,这是论文里的配置。但论文用的是batch size 128,我用64,学习率可以稍微调小一点,比如0.005。实际跑下来,CIFAR-10上大概30个epoch就能到75%左右的准确率,继续训练能到80%以上。
踩坑记录:我第一次跑的时候忘了加weight decay,结果训练loss降到很低但验证loss一直在涨,典型的过拟合。加上5e-4的weight decay之后,验证loss就稳定了。weight decay对全连接层特别重要,因为参数太多了。
4.5 训练过程中的显存优化
AlexNet的全连接层参数量大,训练时显存占用主要在这几块:模型参数(约240MB)、梯度(约240MB)、优化器状态(SGD没有额外状态,Adam的话会翻倍)、激活值(和batch size成正比)。8GB显存跑batch size 64是够的,但如果用Adam优化器,显存会紧张。
几个实用的显存优化技巧:用torch.cuda.amp做混合精度训练,显存占用能降30%左右;把batch size调小但用梯度累积模拟大batch;如果不需要严格复现,把FC层的4096降到2048,参数量直接减半。
5. 常见问题与排查实录
5.1 尺寸不匹配报错
这是复现AlexNet时最常见的问题。报错信息通常是RuntimeError: mat1 and mat2 shapes cannot be multiplied,原因是全连接层的输入维度和卷积层的输出维度对不上。
排查方法:在forward里打印每一层的输出尺寸,或者手动算一遍。关键是确认输入尺寸、padding、stride这三个参数。如果输入是224,Conv1用11×11 stride 4 padding 2,输出是55;Pool1用3×3 stride 2,输出是27;Conv2用5×5 padding 2,输出是27;Pool2输出13;Conv3/4/5都是3×3 padding 1,输出13;Pool3输出6。所以FC1的输入是256×6×6=9216。
如果输入是227,Conv1不加padding,输出是55,后面都一样。但如果输入是224且Conv1不加padding,输出是54,Pool1输出26,Conv2输出26,Pool2输出12,Conv3输出12,Pool3输出5,FC1输入变成256×5×5=6400。这就是尺寸不匹配的根源。
5.2 Loss不下降或震荡
可能的原因有几个:学习率太大(试试降到0.001)、没有做数据归一化(输入值范围差异大导致梯度不稳定)、ReLU神经元大量死亡(检查一下初始化,偏置初始化为0.1或1)、batch size太小(梯度噪声大)。
我遇到过一次loss完全不降的情况,排查了半天发现是数据预处理忘了做归一化,图片像素值还是0-255的范围,导致第一层卷积的输出值非常大,ReLU之后梯度爆炸。加上Normalize之后立刻就正常了。
5.3 过拟合严重
训练准确率95%但验证准确率只有60%,这是典型的过拟合。解决方案按优先级排序:加数据增强(随机裁剪、翻转、颜色抖动)、加Dropout(如果还没加)、加weight decay、减小模型容量(比如FC层从4096降到2048)、早停(验证loss连续几个epoch不降就停)。
AlexNet原版用了大量数据增强和Dropout,如果你复现时省略了这些,过拟合几乎是必然的。特别是在小数据集上(比如CIFAR-10只有5万张训练图),过拟合会非常严重。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 尺寸不匹配报错 | 输入尺寸/padding/stride不一致 | 手动推导每层输出尺寸 | 统一输入为224或227,检查padding |
| Loss不下降 | 学习率过大/未归一化/初始化不当 | 打印每层输出范围 | 降学习率、加归一化、换初始化 |
| Loss震荡 | batch size太小/学习率过大 | 观察loss曲线 | 增大batch size、降学习率 |
| 过拟合 | 数据增强不足/正则化不够 | 对比训练和验证准确率 | 加数据增强、Dropout、weight decay |
| 显存不足 | batch size太大/FC层参数多 | 查看显存占用 | 减小batch size、混合精度、降FC维度 |
| 训练速度慢 | 数据加载瓶颈/未用GPU | 检查GPU利用率 | 增加num_workers、用pin_memory |
独家避坑技巧:训练时用
torch.cuda.memory_summary()查看显存分配,能快速定位是哪一层占用了大量显存。另外,用torch.autograd.set_detect_anomaly(True)可以在反向传播出现NaN时定位到具体操作,虽然会拖慢训练速度,但排查问题时非常有用。
6. 从AlexNet延伸出去的学习路径
6.1 对比VGG理解设计演进
VGG把AlexNet的大卷积核全部换成了3×3堆叠,证明了“小核堆叠比大核更有效”。两个3×3卷积的感受野等于一个5×5,但参数量更少(2×3×3=18 vs 5×5=25),而且中间多了一次非线性激活,表达能力更强。VGG还去掉了LRN,用更规整的结构替代了AlexNet的“工程拼凑感”。
对比着看这两个网络,能清楚地看到CNN设计从“直觉驱动”向“原则驱动”的转变。AlexNet的每一层配置都有具体的工程理由,而VGG的配置则遵循一个简单的原则:所有卷积都用3×3,所有池化都用2×2。这种简洁性让VGG更容易理解和修改,也是它成为经典backbone的原因。
6.2 Batch Normalization如何替代LRN
BN的核心思想是对每一层的输入做归一化,让分布稳定在均值0方差1附近,然后再用一个可学习的缩放和平移参数恢复表达能力。和LRN的区别在于:LRN是在通道维度做局部归一化,BN是在batch维度做全局归一化;LRN是固定的归一化参数,BN的缩放平移是可学习的。
BN的效果比LRN好得多:训练更稳定、可以用更大的学习率、对初始化不敏感。现在几乎所有的CNN都用BN,LRN已经成了历史。但理解LRN有助于理解“为什么要归一化”这个问题,以及归一化的不同维度选择会带来什么差异。
6.3 现代轻量级网络对AlexNet的改进
AlexNet的6000万参数在今天是不可接受的,移动端部署根本跑不动。后续的网络从几个方向做了改进:用全局平均池化替代全连接层(ResNet)、用深度可分离卷积减少计算量(MobileNet)、用分组卷积和通道重排(ShuffleNet)、用神经架构搜索自动设计结构(EfficientNet)。
这些改进的核心思路是一致的:在保持精度的前提下降低参数量和计算量。AlexNet作为一个“大而笨”的基线,正好可以用来对比这些改进的效果。我建议的学习路径是:先复现AlexNet,然后复现VGG,再复现ResNet,最后看MobileNet。每一步都能清楚地看到设计选择的变化和背后的权衡。
6.4 在自定义数据集上微调AlexNet
如果你有一个自己的图像分类任务,数据量不大(几千张),可以直接用预训练的AlexNet做微调。做法是:加载ImageNet预训练权重,把最后一层全连接层的输出改成你的类别数,然后冻结前面的卷积层,只训练全连接层。训练几个epoch后,再解冻所有层做端到端的微调,学习率调小到1e-4。
微调时要注意数据预处理要和预训练时一致(同样的归一化参数、同样的输入尺寸)。如果自定义数据集的分布和ImageNet差异很大(比如医学影像、卫星图像),冻结卷积层可能不是好选择,因为预训练学到的特征可能不适用。这种情况下建议从头训练,或者只加载部分层的权重。
个人体会:AlexNet的预训练权重在自然图像上效果不错,但在纹理差异大的数据集上(比如X光片),微调的效果可能不如从头训练。我试过在医学影像数据集上微调AlexNet,冻结卷积层时准确率只有70%左右,解冻后从头训练能到85%。所以微调策略要根据数据分布来定,不能一概而论。
7. 一些训练技巧和参数调优经验
7.1 学习率调度策略
AlexNet论文里用的是固定学习率,初始0.01,当验证准确率不再提升时手动除以10。这种策略现在基本被余弦退火或StepLR替代了。我自己的经验是:StepLR(每30个epoch降为原来的0.1)在CIFAR-10上效果不错,余弦退火在更长的训练周期(100+ epoch)上更有优势。
学习率预热(warmup)也值得一试:前几个epoch用很小的学习率(比如0.001),然后线性增加到初始值。这样可以避免训练初期因为参数随机初始化导致的梯度不稳定。特别是用大batch size的时候,warmup几乎是必需的。
7.2 Batch Size的选择与影响
Batch size影响梯度的噪声水平和训练稳定性。小batch(16-32)梯度噪声大,有正则化效果,但训练慢;大batch(128-256)梯度稳定,训练快,但可能陷入尖锐极小值,泛化差。AlexNet原版用128,我建议根据显存来选,8GB显存用64比较合适。
如果显存不够但又想用大batch,可以用梯度累积:每算几个小batch的梯度再更新一次参数。比如batch size 16累积4次,等效于batch size 64。这样显存占用按16算,但梯度统计特性接近64。
7.3 权重初始化的重要性
AlexNet论文里用的是均值为0、标准差0.01的高斯初始化。这个初始化在当时的网络深度下勉强能用,但如果网络更深(比如VGG的16层),就会出现梯度消失或爆炸。Kaiming初始化(也叫He初始化)根据每层的输入维度自动调整标准差,公式是std = sqrt(2 / fan_in),对ReLU激活特别有效。
我对比过两种初始化:用高斯初始化时,Conv1的权重标准差是0.01,但Conv1的fan_in是11×11×3=363,Kaiming建议的标准差是sqrt(2/363)≈0.074,差了7倍。用高斯初始化时训练初期loss下降很慢,换成Kaiming之后明显快了很多。
7.4 数据增强的进阶操作
除了随机裁剪和翻转,还可以加颜色抖动(调整亮度、对比度、饱和度、色调)、随机旋转(小角度)、Cutout(随机遮挡一块区域)。颜色抖动对光照变化大的数据集特别有用,Cutout能强迫网络关注物体的多个部分而不是只依赖某个局部特征。
但数据增强不是越多越好。我试过在CIFAR-10上同时加裁剪、翻转、颜色抖动、旋转、Cutout,结果训练准确率上不去,因为增强太强导致训练分布和测试分布差异过大。最后只保留了裁剪、翻转和轻微的颜色抖动,效果最好。增强策略要根据数据集的特点来选,不能盲目堆叠。
8. 从学习笔记到实际项目
8.1 什么时候该用AlexNet
AlexNet在今天已经不是一个“好用”的网络了。它的参数量大、计算量高、精度也不如后续的ResNet、EfficientNet。但在几种场景下它仍然有价值:教学和入门(结构简单、每一层的设计都有明确的工程理由)、快速原型验证(torchvision有现成实现,几行代码就能跑)、对比实验的基线(作为“老一代”方法的代表)。
如果你要做一个实际的图像分类项目,我建议直接用ResNet-18或EfficientNet-B0,精度更高、参数量更少、训练更快。AlexNet更适合作为理解CNN设计原理的教材,而不是生产环境的工具。
8.2 从AlexNet学到的通用经验
虽然AlexNet本身已经过时,但它身上的一些设计经验是通用的:数据增强是防止过拟合的第一道防线,比任何正则化方法都有效;ReLU是深层网络能训练起来的前提,没有它梯度根本传不回去;Dropout对全连接层特别有效,因为全连接层的参数冗余度最高;学习率调度比固定学习率好,能让模型在训练后期更精细地收敛。
这些经验在今天的网络设计中依然适用。你去看ResNet、EfficientNet的论文,数据增强、ReLU、Dropout、学习率调度这些技术一个都没少,只是具体形式变了(比如Dropout变成了Stochastic Depth,学习率调度变成了余弦退火)。理解AlexNet就是理解这些技术的起点。
8.3 后续可以深入的方向
如果你已经完整复现了AlexNet,接下来可以尝试:把LRN换成BN,对比训练稳定性和最终精度;把全连接层换成全局平均池化,看参数量下降多少、精度变化多少;用深度可分离卷积替换标准卷积,计算量能降多少;在自定义数据集上微调,体验迁移学习的效果。
每一个方向都能让你对CNN的理解更深一层。我自己的习惯是每学一个新网络,就把它和之前学过的网络对比着看,找出设计上的差异和背后的原因。这种对比学习的方式比孤立地看单个网络有效得多。
最后分享一个我踩过的坑:复现AlexNet时不要一上来就追求完全复现论文结果。论文用了两块GPU、训练了90个epoch、在ImageNet上跑了好几天。你用单卡、小数据集、几十个epoch,结果肯定对不上。先保证结构正确、能正常训练、loss能下降,然后再逐步调参逼近论文结果。学习的过程比结果重要。