本节摘要:全连接层是主车间的最后一站:把多层卷积提炼出的特征图摊平成向量,做一次全局加权汇总,输出每个类别的分数。它强在"全局混合",弱在"参数爆炸"——一个小型 CNN 里,它常凭一层就占去总参数的八成。本节拆开它的参数账,完整复盘一起经典的形状不匹配报错,并给出全局平均池化这条省参数的绕行道。
池化输出的 32×8×8 特征图是一摞二维评分图,而分类需要的是一个分数列表。展平(flatten)把这一摞图按固定顺序摊平成 2048 维向量——空间结构到此退休,之后的全连接层把 2048 个数与 10 个输出神经元两两连线,每条连线一个权重。卷积看局部、池化管压缩,全连接负责把散落各处的证据汇总成结论:"左上有耳、右下有胡须、中段有纹理"的加权和足够高,"猫"这个输出就被点亮。
阅读完本节,你应当能够:
公式一眼见底:参数量 = 输入维数 × 输出维数 + 输出维数。贵在输入维数——它是整张特征图的元素总数。卷积层的参数只随核尺寸与通道数走,与特征图面积无关;全连接层却是"每个输入连每个输出",特征图翻一倍面积,参数翻一倍。
import torch import torch.nn as nn feat = torch.randn(1, 32, 8, 8) flat = feat.flatten(1) print("展平后维度:", flat.shape[1]) # 2048 = 32×8×8 fc = nn.Linear(32 * 8 * 8, 10) n_fc = sum(p.numel() for p in fc.parameters()) print("全连接参数量:", n_fc) # 20490 = 2048×10 + 10 # 对比:本册小流水线里两个卷积层的参数总和 c1 = 3 * 3 * 3 * 16 + 16 # 448 c2 = 16 * 3 * 3 * 32 + 32 # 4640 print("两层卷积合计:", c1 + c2, "占全网络比例约", round(n_fc / (n_fc + c1 + c2) * 100), "%") # 两层卷积合计 5088,全连接占 80%
20,490 对 5,088——一个分类头占了全网络八成参数,而它做的事只是"表决"。这解释了两件业内常识:其一,参数量爆炸的锅常在全连接而不在卷积;其二,压缩模型的第一个手术对象往往就是分类头(第四章的迁移学习里,换头是最常见的操作)。
这是每个 CNN 学习者都会撞上的报错,值得按巡检流程完整走一遍。
背景:把 2.6 节要出场的小流水线改成处理 64×64 的输入图,只改了第一个卷积层的注释,全连接照旧写成 nn.Linear(32 * 8 * 8, 10)。
操作:构造 64×64 输入做一次前向。
结果:训练循环启动的瞬间抛出异常,关键一行是——
RuntimeError: mat1 and mat2 shapes cannot be multiplied (1x8192 and 2048x10)
解读:排查按固定三步走。第一步读报错:框架想把 8192 维向量乘上 2048×10 的矩阵,维度对不上;8192 = 32×16×16,说明特征图到达全连接时是 16×16 而不是预想的 8×8。第二步倒推:输入 64×64,经过两次"减半"的池化是 16×16——根因是改了输入尺寸却没改分类头的期望尺寸。第三步定责:错的不是特征提取部分,是展平尺寸这个"硬编码常数"。
变式与修法:直接把 Linear 改成 nn.Linear(32 * 16 * 16, 10) 当然能跑,但换一个输入尺寸又会崩。治本的写法是在展平前插一道全局平均池化,把任意尺寸的特征图压成 32 维,分类头从此与输入尺寸脱钩:
import torch import torch.nn as nn class SizeProofHead(nn.Module): def __init__(self): super().__init__() self.gap = nn.AdaptiveAvgPool2d(1) # 任意 H×W 压成 1×1 self.fc = nn.Linear(32, 10) def forward(self, x): x = self.gap(x) return self.fc(x.flatten(1)) head = SizeProofHead() print(head(torch.randn(1, 32, 8, 8)).shape) # torch.Size([1, 10]) print(head(torch.randn(1, 32, 16, 16)).shape) # torch.Size([1, 10]) print("分类头参数量:", sum(p.numel() for p in head.parameters())) # 330 = 32×10 + 10,比 20490 省了 98%
修法一石二鸟:尺寸免疫之外,参数从两万降到三百余。代价是丢掉了特征图的空间排布信息——对多数分类任务无伤大雅,这正是 3.5 节 ResNet 的标准做法。
把 2048 维向量看成 1×1×2048 的"特征图",全连接就是一次核尺寸 1×1、输入通道 2048、输出通道 10 的卷积——两者在数学上完全等价。区别只在习惯用法:全连接吃摊平的向量,1×1 卷积直接在空间特征图上逐像素做同样的线性组合。不少网络用 1×1 卷积替代全连接做分类头(所谓"全卷积网络"的思路),好处是输入尺寸可以任意,且少一次显存的摊平拷贝。
⚠️ 常见坑:
RuntimeError: mat1 and mat2 shapes cannot be multiplied十有八九不是矩阵库的错,而是"输入尺寸改了、展平常数没改"。先算特征图到达全连接时的真实尺寸,再回头改 Linear 的入参。
各工位已经单独巡检完毕。下一节把它们连成整线,给那张 32×32 的猫图办一次从头到尾的形状与参数总对账——全册的数值锚点就在那里。