2.5 全连接层:把特征图收拢成判决


2.5 全连接层:把特征图收拢成判决

本节摘要:全连接层是主车间的最后一站:把多层卷积提炼出的特征图摊平成向量,做一次全局加权汇总,输出每个类别的分数。它强在"全局混合",弱在"参数爆炸"——一个小型 CNN 里,它常凭一层就占去总参数的八成。本节拆开它的参数账,完整复盘一起经典的形状不匹配报错,并给出全局平均池化这条省参数的绕行道。

展平那一刻发生了什么

池化输出的 32×8×8 特征图是一摞二维评分图,而分类需要的是一个分数列表。展平(flatten)把这一摞图按固定顺序摊平成 2048 维向量——空间结构到此退休,之后的全连接层把 2048 个数与 10 个输出神经元两两连线,每条连线一个权重。卷积看局部、池化管压缩,全连接负责把散落各处的证据汇总成结论:"左上有耳、右下有胡须、中段有纹理"的加权和足够高,"猫"这个输出就被点亮。

学习目标

阅读完本节,你应当能够:

  1. 写出全连接层的参数量公式,解释它为什么是参数大头;
  2. 复盘"尺寸不匹配"报错的完整排查过程,形成固定排查动作;
  3. 用自适应池化把网络改造成对输入尺寸免疫的版本;
  4. 说出全连接与 1×1 卷积在功能上的等价关系。

一、参数大头是怎么养成的

公式一眼见底:参数量 = 输入维数 × 输出维数 + 输出维数。贵在输入维数——它是整张特征图的元素总数。卷积层的参数只随核尺寸与通道数走,与特征图面积无关;全连接层却是"每个输入连每个输出",特征图翻一倍面积,参数翻一倍。

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 的标准做法。

三、全连接与 1×1 卷积的等价性

把 2048 维向量看成 1×1×2048 的"特征图",全连接就是一次核尺寸 1×1、输入通道 2048、输出通道 10 的卷积——两者在数学上完全等价。区别只在习惯用法:全连接吃摊平的向量,1×1 卷积直接在空间特征图上逐像素做同样的线性组合。不少网络用 1×1 卷积替代全连接做分类头(所谓"全卷积网络"的思路),好处是输入尺寸可以任意,且少一次显存的摊平拷贝。

巡检记录

  • 职责:全连接做全局汇总表决,是流水线上唯一"每个输入连每个输出"的工位;
  • 参数账:输入维 × 输出维 + 输出维;2048 维输入接 10 类要 20,490 个参数,占小网络总参数八成;
  • 报错三步法:读形状、倒推特征图尺寸、定位硬编码常数——尺寸不匹配的排查动作应当条件反射;
  • 治本方案:自适应平均池化让分类头与输入尺寸脱钩,顺带把参数砍掉两个数量级。

⚠️ 常见坑:RuntimeError: mat1 and mat2 shapes cannot be multiplied 十有八九不是矩阵库的错,而是"输入尺寸改了、展平常数没改"。先算特征图到达全连接时的真实尺寸,再回头改 Linear 的入参。

各工位已经单独巡检完毕。下一节把它们连成整线,给那张 32×32 的猫图办一次从头到尾的形状与参数总对账——全册的数值锚点就在那里。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U