1. 为什么2020年的cs231n笔记至今仍值得翻出来看
卷积神经网络这个方向,每年都有新东西冒出来,但真正把CNN的来龙去脉讲透的课程其实不多。2020年那版cs231n的课程设计有一个很明显的特征:它把卷积神经网络从“怎么用”到“为什么这样设计”这条线拉得很完整。很多后来出的教程直接跳到Transformer或者大模型微调,反而把CNN的基础环节压缩得很厉害,导致不少人在做视觉任务时遇到问题只能靠试参数,不知道问题出在哪一层。
这份学习笔记的核心价值在于,它覆盖了卷积神经网络从底层原理到工程实现的完整链路。具体来说,它涉及的核心技术点包括:卷积操作的数学本质、感受野与特征图尺寸的推导、池化层的设计动机、批归一化的训练与推理差异、残差连接解决梯度问题的机制、以及经典网络结构(如VGG、ResNet)的设计取舍。这些内容不是孤立的,而是相互关联的——你理解了卷积的数学形式,才能算清楚特征图尺寸;算清楚了尺寸,才能理解为什么ResNet的降采样策略要那样设计。
适合看这份笔记的人其实比想象中广。如果你是刚入门深度学习的同学,它能帮你建立一套从输入到输出的完整计算直觉,而不是只会调库;如果你已经做过一些视觉项目,它能帮你把之前“凭感觉”调参的经验对应到具体的原理上,下次遇到问题知道往哪个方向排查;如果你在准备面试或者做技术分享,这份笔记里的推导过程和参数计算可以直接作为素材。
我自己的习惯是,每隔一段时间就把这份笔记翻出来看一遍,每次都能发现之前忽略的细节。比如第一次看的时候只记住了“卷积可以提取特征”,后来再看才注意到步长和填充对特征图尺寸的影响其实有严格的公式约束,再后来才意识到这些约束会直接影响网络能堆多深。这种层层递进的理解,是单纯看API文档得不到的。
2. 卷积神经网络的核心设计思路拆解
2.1 从全连接到卷积:为什么参数共享是关键
如果只用全连接层处理图像,第一个问题就是参数量爆炸。假设输入是一张224×224×3的彩色图,展平后是150528个像素。如果第一个隐藏层有1000个神经元,光这一层就有1.5亿个参数。这还只是一层,堆几层之后参数量会大到无法训练。更麻烦的是,全连接层把每个像素都当作独立特征,完全忽略了图像的空间结构——相邻像素之间的关系、局部纹理的重复性,这些信息全丢了。
卷积层解决这个问题的思路很直接:既然图像的特征在空间上是局部重复的,那就不要让每个神经元都连接全部像素,而是让一组权重在整张图上滑动。这就是参数共享。一个3×3的卷积核只有9个权重(加上偏置是10个),但它可以在整张图上反复使用。参数量从1.5亿直接降到个位数,而且滑动的过程天然保留了空间位置信息。
这里有一个容易被忽略的细节:参数共享的前提是“同一个特征在不同位置出现时应该被同样地检测”。这个假设对图像成立,因为一只猫出现在左上角还是右下角,检测它的卷积核应该是一样的。但对某些任务,比如人脸识别中的关键点定位,不同位置的特征可能需要不同的处理方式,这时候就会在网络的后面几层用全连接层来整合全局信息。
2.2 卷积核尺寸的选择逻辑:为什么3×3成了主流
在2020年的课程里,卷积核尺寸的选择是一个重点讨论的话题。早期的一些网络用过7×7甚至11×11的卷积核,但后来3×3逐渐成为主流。这个变化背后有明确的数学理由。
两个3×3卷积层的感受野等价于一个5×5卷积层,三个3×3卷积层的感受野等价于一个7×7卷积层。但参数量上,三个3×3卷积层的参数量是3×9=27(忽略偏置),而一个7×7卷积层是49。也就是说,用更小的卷积核堆叠,可以用更少的参数获得相同的感受野。同时,每层之间都加了非线性激活函数,三个3×3层意味着三次非线性变换,表达能力比单个7×7层更强。
还有一个工程上的考虑:3×3的卷积在GPU上的计算效率很高。大多数深度学习框架对3×3卷积都有专门的优化,内存访问模式也更友好。7×7的卷积核在计算时会有更多的内存跳转,实际训练速度反而不如堆叠的小卷积核。
注意:卷积核尺寸的选择不是绝对的。在网络的第一个卷积层,有时候会用7×7的大核来快速降低空间分辨率,减少后续层的计算量。这种设计在计算资源有限的情况下仍然有效。
2.3 池化层的取舍:最大池化与平均池化的实际差异
池化层的作用是降低特征图的空间尺寸,同时保留主要信息。最大池化取局部区域的最大值,平均池化取平均值。课程里对这两种池化的对比分析很值得细看。
最大池化在视觉任务中更常用,原因是它保留了局部区域中最强的激活响应。在图像中,边缘、角点这些特征往往表现为局部区域的极值,最大池化能把这些极值传递到下一层。平均池化则会把极值平滑掉,对于纹理识别这类需要统计信息的任务可能更合适,但在分类任务中通常不如最大池化。
池化层的另一个作用是提供一定程度的平移不变性。输入图像稍微平移几个像素,经过池化后特征图的变化会被压缩。这种不变性对于分类任务很重要,因为同一类物体在图像中的位置是不固定的。但池化也有代价:它丢失了精确的空间位置信息。所以在目标检测和语义分割任务中,池化层用得越来越少,取而代之的是步长卷积或者空洞卷积。
2.4 批归一化的训练与推理差异:一个容易踩坑的地方
批归一化在训练时对每个mini-batch计算均值和方差,然后归一化。但在推理时,如果还依赖当前batch的统计量,结果就会不稳定——单张图片推理时batch size为1,均值和方差没有意义。所以推理时必须使用训练过程中累积的全局均值和方差。
这个差异在实际部署时经常出问题。我见过不少人在训练时一切正常,导出模型后推理结果完全不对,排查半天才发现是批归一化的模式没有切换。大多数框架在调用model.eval()之后会自动切换到推理模式,但如果你自己实现了批归一化层,或者用了某些自定义层,就需要手动处理。
训练时累积全局统计量的方式通常是移动平均。具体来说,维护一个running_mean和running_var,每次训练时用当前batch的统计量去更新它们,更新公式是:running_mean = momentum * running_mean + (1 - momentum) * batch_mean。momentum通常取0.1或0.01。这个值的选择会影响推理时的稳定性:momentum太小,全局统计量更新太慢,训练初期推理效果差;momentum太大,全局统计量波动大,推理结果不稳定。
3. 核心细节解析与实操要点
3.1 特征图尺寸的推导:一个必须练到条件反射的计算
特征图尺寸的计算公式是:输出尺寸 = (输入尺寸 - 卷积核尺寸 + 2 * 填充) / 步长 + 1。这个公式看起来简单,但在设计网络结构时,每一步都要算清楚,否则到了后面几层特征图尺寸可能变成负数或者非整数。
举个例子:输入是32×32,卷积核是5×5,填充是0,步长是1。输出尺寸是(32-5+0)/1+1=28。如果填充是2,输出尺寸是(32-5+4)/1+1=32,保持了输入尺寸不变。如果步长是2,输出尺寸是(32-5+0)/2+1=14.5,这不是整数,说明这个配置有问题——要么调整填充,要么调整步长。
在实际操作中,我习惯在纸上画一个表格,每一层记录输入尺寸、卷积核尺寸、填充、步长、输出尺寸。这样做虽然看起来笨,但能避免很多低级错误。特别是当网络很深的时候,某一层的尺寸算错,后面全错,而且报错信息往往不会直接告诉你哪一层出了问题。
提示:当输出尺寸不是整数时,大多数框架会向下取整,但这意味着有一部分输入信息被丢弃了。在设计网络时应该尽量避免这种情况,因为它会让特征图的信息利用不充分。
3.2 感受野的计算:理解网络“看到”了什么
感受野是指输出特征图上的一个像素对应输入图像上的区域大小。感受野的计算是逐层累积的:第一层的感受野就是卷积核尺寸,第二层的感受野是第一层感受野 + (第二层卷积核尺寸 - 1) * 第一层步长,以此类推。
假设网络结构是:3×3卷积(步长1)→ 3×3卷积(步长1)→ 2×2最大池化(步长2)→ 3×3卷积(步长1)。第一层感受野是3,第二层是3+(3-1)*1=5,池化层不改变感受野但把步长累积为2,第三层感受野是5+(3-1)*2=9。也就是说,第三层的一个像素对应输入图像上9×9的区域。
理解感受野的意义在于:如果感受野太小,网络只能看到局部细节,无法捕捉全局结构;如果感受野太大,可能会引入无关的背景信息。对于分类任务,通常希望最后一层的感受野覆盖整个输入图像,这样每个输出神经元都能“看到”全图。对于检测任务,感受野需要和目标尺寸匹配,太小会漏检大目标,太大会把多个目标混在一起。
3.3 权重初始化的选择:为什么不能全零初始化
权重初始化是训练能否收敛的关键因素之一。如果所有权重都初始化为零,那么同一层的所有神经元在前向传播时输出相同,反向传播时梯度也相同,它们会一直保持相同的值,相当于只有一个神经元在工作。这就是所谓的对称性问题。
常用的初始化方法有Xavier初始化和He初始化。Xavier初始化适用于Sigmoid和Tanh激活函数,它让每一层的输出方差保持一致。He初始化适用于ReLU激活函数,因为ReLU会把一半的输入置零,所以方差需要乘以2来补偿。
具体来说,He初始化的标准差是sqrt(2 / 输入神经元数量)。假设一个卷积层的输入通道数是64,卷积核是3×3,那么输入神经元数量是64×3×3=576,标准差是sqrt(2/576)≈0.059。这个值看起来很小,但经过多层累积后能保持激活值的方差稳定。
在实际操作中,大多数框架的默认初始化已经足够好,不需要手动调整。但如果你在训练一个很深的网络,或者用了非标准的激活函数,就需要检查一下初始化是否合适。一个简单的判断方法是:训练开始后观察每一层的激活值分布,如果某一层的激活值几乎全为零或者方差急剧增大,说明初始化可能有问题。
3.4 学习率与批大小的配合:一个被低估的调参维度
学习率和批大小是联合起作用的。批大小决定了梯度的噪声水平:批越大,梯度估计越准确,但计算成本越高;批越小,梯度噪声越大,但可能有助于跳出局部最优。学习率决定了参数更新的步长。
经验规则是:批大小翻倍时,学习率也可以翻倍。这个规则的背后逻辑是,批大小翻倍后梯度的方差减半,所以可以用更大的步长而不至于发散。但这个规则不是绝对的,特别是在使用批归一化时,批大小对训练的影响会更复杂。
我自己的做法是:先用一个较小的批大小(比如32)和一个适中的学习率(比如0.01)跑几百个iteration,观察loss曲线。如果loss下降太慢,增大学习率;如果loss震荡或者发散,减小学习率。确定学习率之后,再尝试增大批大小来加速训练。如果增大批大小后效果变差,可能需要同步调整学习率或者换用其他的优化器。
4. 实操过程与核心环节实现
4.1 从零实现一个卷积层的前向传播
理解卷积层最好的方式是自己实现一遍前向传播。下面是一个简化的实现,假设输入是单通道,卷积核也是单通道,步长为1,填充为0。
import numpy as np def conv_forward(inputs, kernel): # inputs: (H, W) # kernel: (K, K) H, W = inputs.shape K = kernel.shape[0] out_H = H - K + 1 out_W = W - K + 1 output = np.zeros((out_H, out_W)) for i in range(out_H): for j in range(out_W): output[i, j] = np.sum(inputs[i:i+K, j:j+K] * kernel) return output这段代码虽然效率不高,但把卷积的本质说清楚了:卷积就是在输入上滑动一个窗口,每个位置做逐元素乘法再求和。实际框架里会用im2col或者FFT来加速,但计算逻辑是一样的。
如果要处理多通道输入,比如输入是3通道,卷积核也必须是3通道,每个通道分别做卷积然后求和。如果要处理多个卷积核,每个卷积核产生一个输出通道,最终输出的通道数等于卷积核的数量。
4.2 用PyTorch搭建一个完整的CNN分类器
下面是一个完整的CNN分类器实现,用于32×32的彩色图像分类,类别数为10。
import torch import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(32) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(64) self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.bn3 = nn.BatchNorm2d(128) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(128 * 4 * 4, 256) self.fc2 = nn.Linear(256, num_classes) self.dropout = nn.Dropout(0.5) def forward(self, x): x = self.pool(F.relu(self.bn1(self.conv1(x)))) x = self.pool(F.relu(self.bn2(self.conv2(x)))) x = self.pool(F.relu(self.bn3(self.conv3(x)))) x = x.view(-1, 128 * 4 * 4) x = F.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x这个网络的结构是:三个卷积块,每个卷积块包含卷积、批归一化、ReLU激活和最大池化。经过三次池化后,32×32的输入变成4×4,通道数从3增加到128。然后展平后接两个全连接层,中间加了dropout来防止过拟合。
训练这个网络的典型配置是:交叉熵损失,Adam优化器,学习率0.001,批大小64,训练50个epoch。如果数据集比较小,可以适当增加dropout的比例或者加权重衰减。如果训练集准确率很高但验证集准确率低,说明过拟合了,需要增加正则化;如果训练集准确率也上不去,说明模型容量不够或者训练不充分。
4.3 数据增强的实际操作与参数选择
数据增强是提升CNN泛化能力最直接的手段之一。常用的增强方式包括随机裁剪、随机翻转、颜色抖动、旋转等。在2020年的课程里,随机裁剪和随机翻转是标配,颜色抖动和旋转则根据任务特点选择。
随机裁剪的参数是裁剪后的尺寸和裁剪位置的范围。比如从32×32的图像中随机裁剪28×28的区域,然后缩放到32×32。这样做的好处是让网络看到物体在不同位置和不同尺度下的样子。但裁剪的幅度不能太大,否则可能把物体裁掉或者引入太多背景。
随机翻转通常是水平翻转,概率设为0.5。垂直翻转在自然图像中不常用,因为上下颠倒的物体不符合物理规律。但在医学图像或者卫星图像中,垂直翻转可能是合理的。
颜色抖动包括调整亮度、对比度、饱和度和色调。参数范围需要根据数据集的特点来定。比如对于光照变化大的数据集,亮度调整的范围可以大一些;对于颜色区分度高的数据集,色调调整的范围要小一些,否则可能把不同类别的颜色特征混淆。
注意:数据增强只在训练时使用,验证和测试时不要用。另外,增强的强度需要根据数据集大小来调整。数据集越小,增强可以越强;数据集越大,增强可以越弱,因为过强的增强可能引入太多噪声。
4.4 训练过程中的监控与调试
训练CNN时,有几个关键指标需要持续监控:训练loss、验证loss、训练准确率、验证准确率。这四个指标的组合能告诉你很多信息。
如果训练loss下降但验证loss上升,说明过拟合了。解决办法包括增加数据增强、增加dropout、增加权重衰减、减小模型容量。如果训练loss和验证loss都下降但验证loss始终比训练loss高很多,说明模型欠拟合或者数据分布不一致。如果训练loss震荡很厉害,说明学习率太大或者批大小太小。
除了这些常规指标,我还会监控每一层的激活值分布和梯度范数。激活值分布可以帮助判断批归一化是否正常工作,梯度范数可以帮助判断是否出现了梯度消失或梯度爆炸。如果某一层的梯度范数接近零,说明这一层几乎没有学习;如果梯度范数非常大,说明可能需要梯度裁剪。
还有一个实用的技巧是可视化第一层的卷积核。第一层的卷积核通常学习到的是边缘、颜色斑块等低级特征。如果第一层的卷积核看起来像随机噪声,说明训练可能没有正常进行。如果第一层的卷积核有明显的结构,说明网络至少在底层学到了东西。
5. 常见问题与排查技巧实录
5.1 训练不收敛的常见原因与排查顺序
训练不收敛是新手最常遇到的问题。排查的时候不要东试一下西试一下,按照固定的顺序来效率最高。
第一步,检查数据。把输入数据可视化出来,确认没有标签错误、数据损坏、归一化不当等问题。我见过有人把标签和图像搞混了,训练了几十个epoch才发现。也见过有人忘了做归一化,输入像素值在0到255之间,导致梯度爆炸。
第二步,检查损失函数。分类任务用交叉熵,回归任务用均方误差,不要搞混。另外要注意输出的维度是否匹配,比如10分类任务的输出应该是10维,如果写成1维就会出问题。
第三步,检查学习率。学习率太大loss会震荡或者变成NaN,学习率太小loss下降极慢。可以先试几个数量级的学习率,比如0.1、0.01、0.001、0.0001,看哪个下降最快。
第四步,检查网络结构。确认每一层的输入输出维度匹配,确认激活函数没有用错,确认没有忘记加非线性激活。如果网络很深,检查是否有梯度消失的问题,可以尝试加残差连接或者用批归一化。
5.2 过拟合的判断与应对策略
过拟合的判断标准是:训练集表现很好,验证集表现明显差于训练集。比如训练准确率99%,验证准确率只有70%,这就是典型的过拟合。
应对过拟合的策略按优先级排序:增加数据增强、增加dropout、增加权重衰减、减小模型容量、早停。数据增强是最有效的手段,因为它直接增加了训练数据的多样性。dropout和权重衰减是正则化手段,通过限制模型的复杂度来防止过拟合。减小模型容量是最后的手段,因为可能会欠拟合。
早停是一个简单但有效的策略:在验证集loss不再下降时停止训练。具体操作是每个epoch结束后计算验证集loss,如果连续几个epoch验证集loss都没有下降,就停止训练并保存验证集loss最低时的模型。早停的耐心值通常设为5到10个epoch。
5.3 批归一化层的常见坑
批归一化虽然好用,但有几个坑需要注意。
第一个坑是批大小太小。批归一化依赖batch内的统计量,如果批大小只有1或2,统计量噪声很大,训练效果会变差。如果显存有限无法增大批大小,可以考虑用Group Normalization或者Layer Normalization替代。
第二个坑是训练和推理模式切换。前面提到过,推理时必须用全局统计量。如果忘了切换,推理结果会不稳定。大多数框架在model.eval()时会自动切换,但自定义层需要手动处理。
第三个坑是批归一化层的位置。通常放在卷积层之后、激活函数之前。如果放在激活函数之后,效果可能会变差,因为激活函数后的分布已经被改变了。但这个也不是绝对的,有些网络会尝试不同的位置,需要根据具体任务来定。
5.4 学习率调度器的选择与配置
学习率调度器的作用是在训练过程中动态调整学习率。常用的调度器包括StepLR、MultiStepLR、CosineAnnealingLR、ReduceLROnPlateau。
StepLR每隔固定的epoch数把学习率乘以一个衰减因子。比如每30个epoch学习率乘以0.1。这种调度器简单直接,适合训练周期固定的场景。
CosineAnnealingLR让学习率按照余弦曲线从初始值降到零。这种调度器在训练后期学习率很小,有助于模型收敛到更优的局部最优。
ReduceLROnPlateau在验证集loss不再下降时降低学习率。这种调度器自适应性强,不需要手动设置衰减时间点,但需要指定耐心值和衰减因子。
我自己的习惯是:如果训练周期比较短(比如50个epoch以内),用CosineAnnealingLR;如果训练周期比较长,用StepLR或者MultiStepLR;如果不确定训练多久,用ReduceLROnPlateau。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss变成NaN | 学习率太大 | 检查学习率数量级 | 减小学习率 |
| loss不下降 | 学习率太小或数据有问题 | 可视化数据,试不同学习率 | 调整学习率或修复数据 |
| 训练准确率高但验证准确率低 | 过拟合 | 对比训练和验证曲线 | 增加数据增强或正则化 |
| 训练准确率也低 | 欠拟合 | 检查模型容量和训练时长 | 增大模型或延长训练 |
| 推理结果不稳定 | 批归一化模式未切换 | 检查model.eval()是否调用 | 切换到推理模式 |
| 显存不足 | 批大小太大或模型太大 | 检查显存占用 | 减小批大小或模型 |
6. 从经典网络结构中学到的设计经验
6.1 VGG的贡献与局限
VGG的核心贡献是证明了用3×3的小卷积核堆叠可以构建很深的网络,并且效果比大卷积核更好。VGG16有16层,VGG19有19层,在当年的ImageNet上取得了很好的成绩。
VGG的设计很规整:每个卷积块包含两个或三个3×3卷积层,然后接一个2×2最大池化。通道数从64开始,每经过一个池化层翻倍,直到512。最后接三个全连接层,输出1000类。
但VGG的局限也很明显:参数量太大,大部分参数集中在全连接层。VGG16的参数量大约是1.38亿,其中全连接层占了1.2亿以上。这意味着VGG的推理速度慢,显存占用高,部署到移动端几乎不可能。
从VGG学到的经验是:小卷积核堆叠是有效的,但全连接层是参数量的瓶颈。后来的网络设计都尽量避免使用全连接层,改用全局平均池化来替代。
6.2 ResNet的残差连接:为什么它能让网络变得更深
ResNet的核心创新是残差连接。传统的网络是y = F(x),ResNet是y = F(x) + x。这个加法的意义在于:如果F(x)学习不到有用的东西,网络至少可以保持恒等映射,不会比浅层网络更差。
残差连接解决了深层网络的退化问题。在ResNet之前,人们发现网络加深到一定程度后,训练误差反而上升了。这不是过拟合,因为训练误差也上升了。原因是梯度在反向传播时经过太多层后变得非常小,导致前面的层几乎无法更新。残差连接提供了一条梯度的“高速公路”,梯度可以直接通过加法操作传回前面的层,不需要经过所有的中间层。
ResNet的另一个设计是瓶颈结构:对于深层ResNet(如ResNet50),每个残差块用1×1、3×3、1×1三个卷积层。第一个1×1卷积降低通道数,第二个3×3卷积在低维空间做卷积,第三个1×1卷积恢复通道数。这样做减少了计算量,同时保持了表达能力。
6.3 全局平均池化的作用与适用场景
全局平均池化是把每个通道的特征图取平均值,得到一个标量。如果有512个通道,全局平均池化后得到一个512维的向量,可以直接接全连接层或者softmax。
全局平均池化的好处是参数量为零,不像全连接层那样需要大量的权重。同时,它对空间位置不敏感,适合分类任务。但它的缺点是丢失了空间信息,不适合需要精确定位的任务。
在实际操作中,全局平均池化通常用在分类网络的最后一层。如果任务需要保留空间信息(比如分割),就不要用全局平均池化,而是用卷积层或者上采样来恢复空间分辨率。
6.4 网络深度与性能的权衡
网络越深,表达能力越强,但训练难度也越大。ResNet证明了在残差连接的帮助下,网络可以深到100层甚至1000层。但并不是越深越好,因为深层网络的推理速度慢,显存占用高,而且性能提升会逐渐饱和。
选择网络深度时需要考虑任务复杂度、数据集大小、计算资源。对于简单任务和小数据集,浅层网络(如10层以内)可能就够了。对于复杂任务和大数据集,深层网络(如50层以上)可能更合适。如果计算资源有限,可以考虑用轻量级网络(如MobileNet)或者模型压缩技术。
我自己的经验是:先用一个中等深度的网络(比如ResNet18或ResNet34)跑一个baseline,如果效果不够好再尝试更深的网络。不要一上来就用最深的网络,因为训练时间和调试成本都很高。
7. 一些实操中的个人体会
卷积神经网络的学习曲线其实挺陡的,因为涉及的东西很多:数学推导、工程实现、调参经验,缺一不可。我刚开始学的时候,花了很多时间在推导公式上,但真正动手写代码时才发现,很多问题不是公式能解决的。比如数据归一化没做好,公式推导再清楚也没用。
后来我养成了一个习惯:每学一个概念,就写一段代码验证一下。比如学完卷积的尺寸计算公式,就写一个函数随机生成输入和卷积核,验证输出尺寸是否符合预期。学完感受野的计算,就手动算一遍再和代码对比。这种“手算+代码验证”的方式,比单纯看笔记有效得多。
还有一个体会是,不要怕犯错。训练不收敛、过拟合、推理结果不对,这些问题我都遇到过。每次排查问题的过程,其实都是对原理理解加深的过程。比如有一次推理结果完全不对,排查后发现是批归一化的模式没有切换,从此以后我对训练和推理的差异印象特别深。
最后分享一个小技巧:如果你在复现一个网络结构,先把每一层的输入输出尺寸列一个表格,然后逐层核对。这个表格不仅能帮你发现尺寸计算错误,还能帮你理解每一层的作用。我到现在还保留着这个习惯,每次设计新网络都会先画表格再写代码。