
别急着堆卷积层用张量形状把 CNN 从输入推到输出CNN 入门代码常常“看起来都对”真正运行时却在全连接层报形状错误或者训练半天只得到一条没有解释力的准确率。本文用一次形状实验把卷积、池化、通道和分类头串起来先在纸面计算再用断言验证最后构造一个可替换输入尺寸的小模型。重点不是背公式而是建立一套遇到新网络也能自行检查的推导方法。先把目标改成可验证问题很多 CNN 教程从Conv2d开始十分钟后就跳到训练曲线。中间最关键的事情却被一笔带过一张28×28的图片经过卷积和池化究竟变成了什么如果这个问题靠猜模型越深报错就越像拆盲盒。这次不先追求准确率。我们把实验目标改成一句更可验证的话给定输入形状能够在每一层之前说出输出形状并让代码中的断言证明推导一致。实验一先读懂四个维度PyTorch 图像批次通常采用NCHWN批大小一次送入多少张图C通道数灰度图通常是 1RGB 图通常是 3H高度W宽度。MNIST 的一个 32 张图片批次是[32, 1, 28, 28]不是[28, 28, 32]。卷积层的in_channels必须等于输入的C而不是图片宽度。二维卷积在单个空间维度上的输出公式是out floor((in 2 * padding - dilation * (kernel - 1) - 1) / stride 1)当卷积核为 3、步长为 1、填充为 1 时28仍是28紧接一个核为 2、步长为 2 的池化层空间尺寸减半为14。先用纯 Python 把公式变成可执行检查frommathimportfloordefout_size(size,kernel,stride1,padding0,dilation1):returnfloor((size2*padding-dilation*(kernel-1)-1)/stride1)heightout_size(28,kernel3,stride1,padding1)heightout_size(height,kernel2,stride2)assertheight14heightout_size(height,kernel3,stride1,padding1)heightout_size(height,kernel2,stride2)assertheight7flatten_features32*height*heightassertflatten_features1568print(flatten_features)两组“同尺寸卷积 二倍池化”后空间尺寸从28变成7若最后通道数是 32展平长度就是32×7×71568。这正是分类头第一层需要的输入数。实验二让模型自己报告形状下面是一个用于 MNIST 的小型 CNN。它不追求排行榜成绩重点是结构清楚、输入输出可检查。importtorchfromtorchimportnnclassShapeFirstCNN(nn.Module):def__init__(self,classes10):super().__init__()self.featuresnn.Sequential(nn.Conv2d(1,16,kernel_size3,padding1),nn.ReLU(),nn.MaxPool2d(2),nn.Conv2d(16,32,kernel_size3,padding1),nn.ReLU(),nn.MaxPool2d(2),)self.classifiernn.Sequential(nn.Flatten(),nn.Linear(32*7*7,64),nn.ReLU(),nn.Dropout(0.2),nn.Linear(64,classes),)defforward(self,x):xself.features(x)returnself.classifier(x)modelShapeFirstCNN()fake_batchtorch.randn(8,1,28,28)logitsmodel(fake_batch)assertlogits.shape(8,10)print(logits.shape)这里输出的是 logits不要在模型最后手动加Softmax再交给CrossEntropyLoss。该损失函数期望未归一化分数并在内部完成数值更稳定的LogSoftmax与负对数似然计算。推理展示概率时再使用logits.softmax(dim1)。如果环境尚未安装 PyTorch仍可先运行上一段纯 Python 形状测试本段需要与系统和加速设备匹配的 PyTorch 2.x 环境。不要把“代码能导入”误写成“模型已完成训练”。实验三消灭写死的 1568一旦输入从28×28换成32×32写死的Linear(1568, 64)就会报错。可以选择自适应池化让分类头收到固定尺寸classFlexibleCNN(nn.Module):def__init__(self,in_channels1,classes10):super().__init__()self.featuresnn.Sequential(nn.Conv2d(in_channels,16,3,padding1),nn.ReLU(),nn.MaxPool2d(2),nn.Conv2d(16,32,3,padding1),nn.ReLU(),nn.AdaptiveAvgPool2d((4,4)),)self.headnn.Sequential(nn.Flatten(),nn.Linear(32*4*4,classes),)defforward(self,x):returnself.head(self.features(x))modelFlexibleCNN()assertmodel(torch.randn(2,1,28,28)).shape(2,10)assertmodel(torch.randn(2,1,32,32)).shape(2,10)自适应池化不是“永远更好”它只是明确了一份接口契约无论前面的空间尺寸是多少分类头看到的都是4×4。如果任务需要保留精细位置例如分割或关键点检测就不能随意压缩空间信息。实验四卷积学到的不是“整张猫”而是局部模式一个3×3卷积核每次只观察九个位置同一组权重会在整张图上滑动。这带来两个关键性质局部连接减少参数共享权重让同一种边缘或纹理可以在不同位置被识别。第一层往往响应方向、明暗变化等简单模式多层叠加后后层才可能组合出更大的结构。感受野描述一个输出位置能“看到”多大的原图区域。连续两个步长为 1 的3×3卷积感受野会从 3 扩展到 5而不是简单得到 6。池化或步长卷积还会增大相邻输出点在原图上的间隔。网络很深却识别不了大结构时不只要看参数量也要看有效感受野是否覆盖目标。卷积参数量可以直接计算。输入通道为 16、输出通道为 32、卷积核为3×3时权重数量是32×16×3×3若启用偏置再加 32。它与图片是28×28还是256×256无关分辨率主要影响计算量和中间特征图内存。defconv_parameters(in_channels,out_channels,kernel,biasTrue):weightsout_channels*in_channels*kernel*kernelreturnweights(out_channelsifbiaselse0)assertconv_parameters(16,32,3)4640这个简单计算能识别另一个常见误区把全连接层替换成卷积层后参数可能明显减少但特征图很大时训练内存和计算时间仍然可能很高。实验五先尝试过拟合一个小批次正式训练前从训练集中固定抽取 16 或 32 张样本关闭随机增强让模型反复训练同一小批数据。一个有足够容量、训练循环正确的模型通常应该能把这批样本的损失压得很低。若做不到优先检查标签、损失输入、优化器参数、梯度是否清零而不是立刻增加层数。这个测试不是为了获得泛化能力恰恰是在有意制造过拟合。它像电路通电前的导通测试证明数据确实流经模型并能改变参数。通过之后再恢复数据增强、训练/验证划分和正则化。还应固定随机种子并记录软件版本但要理解“固定种子”不等于所有硬件上完全逐位复现。某些 GPU 算法、并行数据加载和第三方库仍可能引入非确定性。实验记录至少包含数据版本、划分方式、模型提交号、超参数、设备和主要依赖版本才能解释两条曲线为什么不同。输入归一化也属于模型契约。训练集使用的均值和标准差必须原样用于验证、测试和部署不能为每张待预测图片临时计算一套。灰度图扩成三通道、RGB 顺序变成 BGR、像素范围从0-1变为0-255都会让形状看似正确而语义完全改变。因此测试不仅要断言张量尺寸还应检查数据类型、数值范围、通道顺序和标签取值范围。从一条准确率升级为可解释评估假设 100 张图片中有 90 张数字 0、10 张数字 1。模型全部猜 0也有 90% 准确率却完全认不出数字 1。因此分类实验至少同时查看混淆矩阵以及每一类的精确率、召回率和 F1。对某一类别precision TP / (TP FP) recall TP / (TP FN) F1 2 * precision * recall / (precision recall)分母可能为零计算时必须设定处理策略。多分类任务还要说明使用宏平均、微平均还是按样本数加权平均只写一个“F10.92”并不完整。训练、验证和测试集也要分工明确训练集更新参数验证集选择结构和超参数测试集只用于最终评估。如果反复根据测试集调整模型测试集就被间接“训练”了。一个不会偷看测试集的训练骨架defrun_epoch(model,loader,loss_fn,optimizerNone,devicecpu):trainingoptimizerisnotNonemodel.train(training)total_loss0.0total_correct0total_samples0contexttorch.enable_grad()iftrainingelsetorch.no_grad()withcontext:forimages,labelsinloader:images,labelsimages.to(device),labels.to(device)logitsmodel(images)lossloss_fn(logits,labels)iftraining:optimizer.zero_grad()loss.backward()optimizer.step()batchlabels.size(0)total_lossloss.item()*batch total_correct(logits.argmax(1)labels).sum().item()total_samplesbatchreturn{loss:total_loss/total_samples,accuracy:total_correct/total_samples,}损失累加时乘以当前批大小最后除以真实样本数可以正确处理最后一个不足整批的批次。验证时不创建梯度既节省内存也避免误更新参数。常见错误观察表现象优先检查mat1 and mat2 shapes cannot be multiplied池化后的C×H×W与Linear输入是否一致第一层就提示通道错误数据是灰度还是 RGB是否错误排列 NHWC/NCHW损失几乎不下降标签范围、学习率、输入归一化、参数是否加入优化器训练准确率高、验证低过拟合、数据泄漏、增强差异、划分是否合理指标异常漂亮测试集是否参与调参、类别是否严重不均衡GPU 报内存不足批大小、图片分辨率、中间特征图、是否忘记关闭验证梯度验证顺序正式训练前按固定顺序做五次检查单样本形状、单批前向、损失可计算、一个批次反向传播、极小数据集能否过拟合。最后一项很实用如果模型连几十个样本都记不住通常是数据、标签或训练循环有错误而不是“模型还不够大”。总结CNN 不是一串神秘层名而是一条形状不断变化的数据管道。先写出NCHW逐层计算空间尺寸和通道数用假数据断言接口再开始训练评价时从单一准确率扩展到类别级指标。掌握这种形状优先的实验方式后换数据集、加残差块或迁移到更深网络仍然有一条可重复的检查路径。先验证再训练。