1×1 卷积在通道方向做全连接:32 通道压到 8 通道只要 264 个参数;3×3 做同样的事要 2312 个。 深度可分离卷积把 3×3 拆成「逐通道 3×3」加「1×1 混合通道」:9216 个参数变成 1312 个, 只剩 14%。 im2col 把卷积变成矩阵乘:同样一次卷积,逐点循环 237 毫秒,im2col 加矩阵乘 0.1 毫秒, 结果最大差 2.2e-15。
前面十一节讲卷积「是什么」,这一节讲它「怎么算得快、算得省」。 5.9 节的全连接层参数爆炸,1×1 卷积和全局平均池化都是对付它的手段。 这一节在教材里属于「进一步详细资讯」,是选读,但工程上最实用。
一、1×1 卷积:在通道上做全连接
窗口只有一个像素,看起来什么都没做。但它跨通道做了一次加权求和。 32 通道进、8 通道出,参数是 1 乘 1 乘 32 乘 8 加 8,等于 264。 换成 3×3 就是 2312,正好差一个核面积。 用途是「先把通道压下来,再做 3×3」。这叫瓶颈结构,ResNet 的残差块里到处都是。 它还能在不改尺寸的前提下加一层非线性——因为后面还跟着一个 ReLU。
二、深度可分离卷积:把空间和通道拆开
标准 3×3 从 32 通道到 32 通道,参数 9216。 拆成两步:先逐通道 3×3(32 个核,每个只看自己那个通道,288 个参数), 再用 1×1 把 32 个通道混合成 32 个(1024 个参数)。合计 1312。 它假设「空间滤波」和「通道混合」可以分开做。 这个假设在多数情况下成立,所以 MobileNet 系列靠它把模型塞进手机。 代价是表达力弱一点。所以它是「用精度换体积」的手段,不是白捡的便宜。
三、im2col:把卷积变成矩阵乘
把每个窗口的像素按列摊开,凑成一个大矩阵,卷积就变成一个矩阵乘。 实测 8×8×32×32 的一次卷积:逐点循环 237 毫秒,im2col 加矩阵乘 0.1 毫秒。差两千多倍。 两者结果最大差 2.2e-15,只是浮点累加顺序不同,算子本身没变。 代价是内存:那个矩阵是原图的 8 倍。典型的时间换空间。
四、还有些什么
分组卷积:把通道分成几组,各组独立卷积。 AlexNet 当年是为了拆到两块 GPU 上,后来发现它顺带省参数,于是成了 ResNeXt 的核心。 膨胀卷积就是 5.6 节的空洞,语义分割里是主力。 可变形卷积让采样点自己学偏移,对不规则形状特别有效,代价是实现复杂。 卷积的花样很多,但核心只有一个:在某个邻域里共享一组权重。剩下的都是在这句话上做优化。
五、这一节的三个数字
264 对 2312:1×1 相对 3×3,同样压缩通道,省九倍。 1312 对 9216:可分离相对标准,14%。 0.1 毫秒对 237 毫秒:im2col 相对循环。 三个数字对应三种省法:省参数、省参数又省计算、省时间。工程上真正在用卷积时,这三件事天天要算。
六、动手 Demo:1×1、可分离、im2col
三件事各算一遍:1×1 卷积和 3×3 卷积压缩同样通道数,参数差多少;深度可分离卷积相对标准卷积省多少; im2col 相对逐点循环快多少。
p11 = 1 * 1 * 32 * 8 + 8 # 1×1 卷积,32 通道压到 8
p33 = 3 * 3 * 32 * 8 + 8 # 3×3 卷积,同样 32 压到 8
std = 3 * 3 * 32 * 32 # 标准 3×3,32 到 32
sep = 3 * 3 * 32 + 32 * 32 # 逐通道 3×3 加 1×1 混合
print("1×1 %d 3×3 %d" % (p11, p33))
print("标准 %d 可分离 %d 即 %.0f%%" % (std, sep, 100.0 * sep / std))
# im2col:把每个窗口摊成一列,卷积变成矩阵乘
M = np.array([x[:, i:i + 3, j:j + 3].ravel()
for i in range(30) for j in range(30)]).T
Y = (K.reshape(8, 72) @ M + b.reshape(-1, 1))
实跑输出:
--------------------------------------------------------------------
卷积的进一步详细资讯:1×1、可分离、im2col
--------------------------------------------------------------------
先说 1×1 卷积。它的窗口只有一个像素,看起来什么都没做,但它跨通道做了一次加权求和。
核 作用 参数量
--------------------------------------------
1×1(32 到 8) 通道压缩 264
3×3(32 到 8) 通道压缩加空间 2312
**1×1 卷积就是「在通道这个方向上做全连接」。**
它的用处是先把通道数压下来,再做 3×3,计算量能省一个数量级。
再说深度可分离卷积:把一个 3×3 卷积拆成「逐通道 3×3」加「1×1 混合通道」两步。
做法 参数量 相对
------------------------------------------
标准 3×3(32 到 32) 9216 100%
深度可分离 1312 14%
差不多是原来的七分之一。**手机上的模型基本都用它,因为参数量和计算量一起降。**
最后说 im2col。循环版卷积慢,是因为 Python 每算一个点都要绕一圈。
**把每个窗口在通道方向摊平成一列,卷积就变成了一个大矩阵乘。**
做法 形状 耗时
----------------------------------------------------------
逐点循环 8×8×32×32 直接算 237 毫秒
im2col 加矩阵乘 8×72 乘 72×900 0.1 毫秒
两种做法算出来的东西一样:最大差 2.2e-15。**变的只是记账方式,算子本身没变。**
**摊开之后,卷积就退化成一个标准的矩阵乘法,可以交给 BLAS 跑满 CPU。**
caffe 当年就是靠这一手把卷积跑快的,现在的框架底层还是这个思路。
代价是内存:那个矩阵有 72 乘 900 个元素,是原图元素数的 8 倍。
**所以 im2col 是典型的「用内存换速度」。**
1×1 那一行:264 对 2312,比例是 8.75 倍,差不多就是核面积 9 倍的关系。(差的那些是偏置项。) 深度可分离:9216 变 1312,14%。这个 14% 可以算出来:1/9 加 1/32,正好约等于 0.142。 两个数字说明同一件事:卷积的开销是「核面积 × 输入通道 × 输出通道」,把任何一个因子拆开都能省。 im2col 那一行最戏剧:237 毫秒对 0.1 毫秒,差两千多倍,而两者算的是同一个东西,最大差 2.2e-15。 差距来自哪里?循环版每一格都要回到 Python 解释器走一遍; 矩阵乘交给了编译好的 BLAS,一次调用跑完几十万次乘加。这就是「把热循环交给 C」的威力。 代价是内存 8 倍。 所以显存紧张时会退化成「分块 im2col」,一块一块摊。所有工程优化最后都是这个形状:换一点内存, 换很多时间。
30 秒实验
- 如果 3×3 换成 5×5,标准卷积和可分离卷积的比例会变成多少?
- 为什么 im2col 的矩阵元素数是原图的 9 倍?(提示:每个位置都要存 9 个数)
- 分组卷积分成 4 组,参数量会变成原来的几分之几?
常见误区
- 以为 1×1 卷积「什么都没做」。它在通道方向做了一次完整的线性变换, 参数量是 32 乘 8,不是零。
- 以为可分离卷积没有代价。表达力确实弱一些,所以在精度要求高的场景要谨慎评估。
- 以为 im2col 一定更快。它吃内存。显存不够时,分块加循环的混合实现反而更快。
自测三问(点开对答案)
1. 1×1 卷积的作用是什么? —— 在通道方向做线性组合,常用来压缩或扩张通道数,也可以在不改尺寸的前提下加非线性。 2. 深度可分离卷积省在哪? —— 把空间滤波和通道混合拆成两步,参数量约降到原来的七分之一(1/9 加 1/32)。 3. im2col 为什么快? —— 把卷积变成一个大矩阵乘,交给高度优化的 BLAS 库执行。代价是要多占几倍内存。第 5 章到此结束。卷积解决了「空间结构」,但它假设输入是固定大小的图。 如果输入是一句话、一段语音,长度可变、前后有顺序,那就是第 6 章的事。
- ☐ (本章完,下一节 6.1 递回神经网络(RNN))