卷积和池化之后是一个三阶张量,要摊平成一列才能接全连接。 36 个数接 10 类,权重 360 个加偏置 10 个,共 370 个——这是整个网络里参数最多的一层。 1x1 卷积其实就是「权重共享的全连接」:两种写法算出来的差是 4.441e-16。 同一个 4 乘 10 的矩阵在 9 个位置各用一次,参数从 370 降到 50。
卷积和池化把图变成了 (3, 3, 4) 这样的三阶张量。 全连接层只认向量,所以要先摊平。 摊平本身没有信息损失,但有一个隐含要求:顺序必须固定。 训练时怎么排,预测时就得怎么排,否则同一个位置对不上。 这一节还要澄清一个常被误解的东西:1x1 卷积。 它看起来像「什么都没做」,实际上是通道之间的全连接。
一、摊平:顺序就是约定
一个 (3, 3, 4) 的张量,摊平成一列 36 个数。 排法是「先按行、再按列、最后按通道」,也就是 4 个通道各摊成 9 个,再接起来。 这个顺序没有对错,只有「一致」。 PyTorch 和 TensorFlow 的默认顺序都是通道在最后,所以摊平的结果一样。 如果手工改了维度顺序又忘了改摊平,训练和预测就会不一致——这种 bug 极难查。
二、参数量:这一层是重灾区
| 输入 | 输出 | 权重 | 偏置 | 合计 |
|---|---|---|---|---|
| 36 | 10 | 360 | 10 | 370 |
| 36 | 128 | 4608 | 128 | 4736 |
| 576 | 10 | 5760 | 10 | 5770 |
| 输入从 36 涨到 576,参数量涨 16 倍——因为它是乘积关系。 | ||||
| 这也是为什么卷积网络总是先把尺寸压小再摊平:不是为了省卷积的力气,是为了省这一层的参数。 | ||||
| VGG 那种网络里,最后三层全连接占了全网络参数的绝大部分,原因就在这里。 | ||||
| 后来的网络改用全局平均池化,就是为了把这一大坨参数直接去掉。 |
三、1x1 卷积:通道之间的全连接
核是 1 乘 1,所以它只看一个像素,但同时看这个像素的所有通道。 对每个像素位置,它做的是:4 个通道的值加权求和,得到 10 个新值。 这和一个「输入 4、输出 10」的全连接层完全一样。 区别在于:全连接只在摊平之后做一次,1x1 卷积在每个位置都做一次,共用同一套权重。 实测两种写法的最大差异 4.441e-16,就是浮点乘加的舍入误差。 所以「1x1 卷积」不是「卷积的退化版」,它是「权重共享的全连接」。
四、它省了多少,为什么重要
全连接 36 到 10:370 个参数。 1x1 卷积 4 到 10:50 个参数。 少了 7.4 倍。 更关键的是:1x1 卷积的参数和输入尺寸无关。 输入是 3 乘 3,它用 50 个参数;输入换成 30 乘 30,它还是那 50 个。 全连接就做不到:输入一变,权重矩阵的形状就得变。 这就是「全卷积网络」能处理任意大小输入的原因。 检测、分割这类任务输入尺寸经常变,用全连接会直接卡死。
五、那为什么还要全连接层
因为 1x1 卷积只做「逐像素」的线性组合,它不混合不同位置的信息。 左上角和右下角在这层里永远碰不到一起。 摊平之后接全连接,任意两个位置都会连起来。 要判断「上面有个圈、下面有根竖线」这种组合,就需要这种全局的连接。 所以常见的折中是:卷积抽特征、池化降尺寸、最后接一两层全连接做判断。 现代的折中是:卷积抽特征、全局平均池化压成一个向量、再接一层全连接。
六、动手 Demo:摊平之后,1x1 卷积其实就是权重共享的全连接
这段代码造一个 (3, 3, 4) 的张量,先看摊平的顺序,再算两档全连接的参数量; 然后用两种写法做同一个运算:逐像素全连接和 1x1 卷积,比较结果的差异。 重点看三处。第二段 370 和 4736;第三段两种写法的差 4.441e-16;第四段 370 对 50。
# -*- coding: utf-8 -*-
import numpy as np
rg = np.random.default_rng(5)
print('[1] 池化之后拿到的是一个三阶张量')
h = w = 3
F = 4
A = rg.normal(0.0, 1.0, (h, w, F))
print(' 这里的例子是 3 乘 3 乘 4 = %d 个数' % (h * w * F))
print(' 摊平(flatten)就是把它按固定顺序排成一列:先按行、再按列、最后按通道')
print(' 顺序必须固定,否则同一个位置在训练和预测时对不上')
flat = A.reshape(-1)
print(' 摊平后长度 %d,前 8 个是 %s' % (len(flat), np.round(flat[:8], 3).tolist()))
print('')
print('[2] 接全连接的参数账单')
for n_out in (10, 128):
npar = h * w * F * n_out + n_out
print(' %d -> %d:权重 %d 个,偏置 %d 个,共 %d 个'
% (h * w * F, n_out, h * w * F * n_out, n_out, npar))
print(' 这一层几乎总是整个网络里参数最多的一层')
print(' 输入从 36 涨到 576(比如 8 乘 8 乘 9)时,这一层的参数会涨 16 倍')
print('')
print('[3] 1x1 卷积其实就是「逐像素的全连接」')
W1 = rg.normal(0.0, 0.5, (F, 10))
b1 = rg.normal(0.0, 0.1, 10)
manual = np.zeros((h, w, 10))
for i in range(h):
for j in range(w):
manual[i, j] = A[i, j] @ W1 + b1
conv = A @ W1 + b1
print(' 两种写法:一种是每个像素位置单独做一次全连接,一种是 1x1 卷积一次算完')
print(' 结果的最大差异 %s' % ('%.3e' % float(np.abs(manual - conv).max())))
print(' 所以 1x1 卷积不是「卷积的退化版」,它就是权重共享的全连接:')
print(' 同一个 %d 乘 10 的矩阵,在 %d 个位置上各用一次。' % (F, h * w))
print('')
print('[4] 省了多少参数')
n_fc = h * w * F * 10 + 10
n_11 = F * 10 + 10
print(' 全连接 36 -> 10:%d 个参数' % n_fc)
print(' 1x1 卷积 4 -> 10:%d 个参数' % n_11)
print(' 少了 %.1f 倍,而且输入尺寸变大时 1x1 卷积的参数一个都不变' % (n_fc / n_11))
print('')
print(' 这就是「全卷积网络」能处理任意大小输入的原因:')
print(' 全连接层要求输入尺寸固定,1x1 卷积不要求——尺寸变了权重还是那几十个。')
print('')
print('[5] 那为什么还要全连接层')
print(' 因为 1x1 卷积只做「逐像素」的线性组合,它不混合不同位置的信息。')
print(' 摊平之后接全连接,任意两个位置都会连起来,表达力更强。')
print(' 所以常见的折中是:卷积抽特征、池化降尺寸、最后接一两层全连接做判断。')
实跑输出:
--------------------------------------------------------------------
全连接层:摊平之后,1x1 卷积其实就是权重共享的全连接
--------------------------------------------------------------------
[1] 池化之后拿到的是一个三阶张量
这里的例子是 3 乘 3 乘 4 = 36 个数
摊平(flatten)就是把它按固定顺序排成一列:先按行、再按列、最后按通道
顺序必须固定,否则同一个位置在训练和预测时对不上
摊平后长度 36,前 8 个是 [-0.802, -1.324, -0.248, 0.42, 1.136, 0.11, -0.553, -0.785]
[2] 接全连接的参数账单
36 -> 10:权重 360 个,偏置 10 个,共 370 个
36 -> 128:权重 4608 个,偏置 128 个,共 4736 个
这一层几乎总是整个网络里参数最多的一层
输入从 36 涨到 576(比如 8 乘 8 乘 9)时,这一层的参数会涨 16 倍
[3] 1x1 卷积其实就是「逐像素的全连接」
两种写法:一种是每个像素位置单独做一次全连接,一种是 1x1 卷积一次算完
结果的最大差异 4.441e-16
所以 1x1 卷积不是「卷积的退化版」,它就是权重共享的全连接:
同一个 4 乘 10 的矩阵,在 9 个位置上各用一次。
[4] 省了多少参数
全连接 36 -> 10:370 个参数
1x1 卷积 4 -> 10:50 个参数
少了 7.4 倍,而且输入尺寸变大时 1x1 卷积的参数一个都不变
这就是「全卷积网络」能处理任意大小输入的原因:
全连接层要求输入尺寸固定,1x1 卷积不要求——尺寸变了权重还是那几十个。
[5] 那为什么还要全连接层
因为 1x1 卷积只做「逐像素」的线性组合,它不混合不同位置的信息。
摊平之后接全连接,任意两个位置都会连起来,表达力更强。
所以常见的折中是:卷积抽特征、池化降尺寸、最后接一两层全连接做判断。
第一段那张摊平后的前 8 个数,可以和原张量对一下。
它就是第一行第一个像素的 4 个通道,接着是第一个像素右边的 4 个通道。
记住这个顺序,手写代码时才不会把维度搞反。
第二段那个 16 倍要说清楚:输入从 36 涨到 576 是 16 倍,参数量也跟着 16 倍。
所以「把特征图尺寸减半」这件事对全连接层来说是 4 倍的收益。
第三段的 4.441e-16 是双精度浮点的舍入误差,不是算法差异。
它证明两种写法在数学上是同一个运算。
这也是为什么「1x1 卷积」这个名字有点误导——它不是卷积,是共享的全连接。
第四段那个「输入尺寸变了参数不变」才是 1x1 卷积真正的价值。
它让网络可以吃任意大小的图,这是检测和分割任务的前提。
代码里 manual 用了双重循环逐像素算,conv 直接一次矩阵乘。
两者的差在小数点后十六位,说明广播机制没有引入额外误差。
30 秒实验
- 把张量尺寸从 (3, 3, 4) 改成 (6, 6, 4),重跑。 全连接的参数从 370 涨到 1450,1x1 卷积还是 50。 这就是「参数与输入尺寸无关」的直接体现。
- 把输出维度从 10 改成 128,重跑。 全连接变成 36 乘 128 加 128 等于 4736,1x1 卷积变成 4 乘 128 加 128 等于 640。 差距从 7.4 倍拉大到 7.4 倍——比例不变,因为两者都是「输入通道乘输出通道」。
- 把摊平顺序改成「先通道、再行、再列」(
A.transpose(2,0,1).reshape(-1)),重跑。 结果会完全不同。 顺序变了,同一个位置对应到全连接的不同权重上,模型就学乱了。
常见误区
- 「1x1 卷积是没用的卷积,等于什么都没做」。 它做的是通道之间的线性组合,能升维也能降维。 ResNet 的瓶颈结构、MobileNet 的逐点卷积,全靠它。
- 「摊平不损失信息,所以随便怎么摊」。 信息确实不损失,但「位置」这个含义依赖顺序。 训练和预测用了不同顺序,等于把权重对应到了错误的位置。
- 「全连接层太占参数,应该全部去掉」。 1x1 卷积不能混合不同位置的信息,某些任务确实需要全局连接。 该去的是「大尺寸输入接大隐层」那种配置,不是全连接本身。
- 「卷积网络的最后一定要接全连接」。 今天的分类网络多用全局平均池化加一层线性层,参数几乎为零。 全连接不是必须的,它是「想要全局连接」时的一种选择。
自测三问(点开对答案)
1. 为什么摊平的顺序必须固定? —— 因为摊平之后每个位置对应全连接层的一个权重。 训练时按一种顺序排,预测时按另一种排,同一个位置就落到不同的权重上,结果全乱。 信息没丢,但对应关系错了。 2. 1x1 卷积和全连接层是什么关系? —— 1x1 卷积就是「权重共享的全连接」:对每个像素位置,把通道维做一次线性组合。 两种写法的数值差只有 4.441e-16,是浮点舍入。 区别是全连接只做一次,1x1 卷积在每个位置都做一次、共用同一套权重。 3. 为什么说全连接层是参数重灾区? —— 因为参数量是输入维度乘输出维度再加偏置,是乘积关系。 36 到 10 是 370 个,36 到 128 是 4736 个,输入涨到 576 就是 5770 个。 所以卷积网络总要把尺寸压小再摊平。卷积、池化、全连接都齐了,最后一层是输出层。 它要把一堆特征变成一个「属于哪一类」的答案,还要给训练提供可导的损失。 下一节看 softmax 和 sigmoid 各自适合什么场合。
- ☐ (本节完,下一节 3.5 输出层)