查看全课大纲(第 78 / 82 节)
  1. 1.函数与数据泛化
  2. 2.向量:从几何直觉到高维空间
  3. 3.矩阵与张量
  4. 4.特征值与特征向量:矩阵变换的不动方向
  5. 5.凸函数:从向量求导的矩阵记号到好优化的函数形状
  6. 6.概率与统计:机器学习的数学基础
  7. 7.独立性与随机变量:贝叶斯公式与二项分布
  8. 8.期望、方差与协方差:随机变量的数字画像
  9. 9.联合概率:当一条轴不够用的时候
  10. 10.概率分布(一):从伯努利到伽马函数
  11. 11.概率分布(二):贝塔、泊松、正态与两个贝叶斯故事
  12. 12.机器学习的模型:找输入与输出的关系
  13. 13.机器学习的应用
  14. 14.过拟合
  15. 15.机器学习的分类
  16. 16.有监督学习:从房价预测说起
  17. 17.损失函数
  18. 18.梯度下降(上):更新规则与学习速率
  19. 19.梯度下降(2)
  20. 20.牛顿方法
  21. 21.KNN简介
  22. 22.距离测算
  23. 23.朴素贝叶斯
  24. 24.朴素贝叶斯的实际应用
  25. 25.信息熵:决策树构建的度量基础
  26. 26.信息增益与决策树的构建
  27. 27.ID3算法
  28. 28.决策树节点的划分
  29. 29.CART树
  30. 30.Bagging
  31. 31.Boosting:自适应提升算法
  32. 32.Gradient Boosting
  33. 33.线性模型:回归与分类的同一副骨架
  34. 34.最小二乘:把最优参数直接解出来
  35. 35.最小二乘法的几何与概率视角
  36. 36.极大似然:给最小二乘一个概率出身
  37. 37.逻辑回归(1):把线性模型接到分类上
  38. 38.逻辑回归(2):损失函数与多分类
  39. 39.---- 手造数据: 5 个样本, 2 个特征; x2 几乎 = 2*x1 (偏差手工指定, 无随机) ----
  40. 40.讲师例: 向量 "23014" -- 三把范数尺子量同一个向量
  41. 41.优化方法(1):坐标下降与前向选择
  42. 42.优化方法(2):前向策略与最小角回归
  43. 43.拉格朗日乘子法(1):从无约束到等式约束
  44. 44.拉格朗日乘子法(2):等式约束下的平行条件与拉格朗日函数
  45. 45.拉格朗日乘子法(3):不等式约束与 KKT 条件
  46. 46.最大间隔:把分类问题变成优化问题
  47. 47.拉格朗日对偶问题:把 w 和 b 全部换成 α
  48. 48.支持向量:边界上的少数派
  49. 49.SVM 计算过程:一笔到底算完,再走进核方法
  50. 50.贝叶斯公式:概率即信念
  51. 51.隐含变量和概率模型:给看不见的原因建模
  52. 52.EM 算法:期望与最大化的迭代
  53. 53.琴生不等式:EM 的数学钥匙
  54. 54.三个硬币问题:EM 的复盘与衔接
  55. 55.高斯混合模型:用隐变量解释双峰数据
  56. 56.隐马尔可夫模型
  57. 57.隐马尔可夫模型(2):前向算法与动态混合模型
  58. 58.隐马尔可夫模型在 NLP 中的运用
  59. 59.EM 算法:把「为什么收敛」推到底
  60. 60.HMM 在 NLP 中的运用:从分词到机器翻译
  61. 61.HMM 参数估计:Baum-Welch 的三块拉格朗日
  62. 62.高维下的高斯模型
  63. 63.主题模型:从词袋到隐含主题
  64. 64.Dirichlet 分布(1)
  65. 65.Dirichlet 分布(2)
  66. 66.统计学习内容回顾
  67. 67.K-means 算法
  68. 68.PCA 降维
  69. 69.SVD 降维
  70. 70.从聚类到流形
  71. 71.局部线性嵌入
  72. 72.随机邻域嵌入:SNE 与 t-SNE
  73. 73.无参模型:让数据自己说话
  74. 74.高斯过程:函数空间里的正态分布
  75. 75.高斯过程(2):噪声、正则化与核的线性组合
  76. 76.神经网络简介:从感知器到自编码器
  77. 77.反向传播:把误差一层层传回去
  78. 78.卷积神经网络(CNN):让特征自己学出来
  79. 79.循环神经网络(RNN):给网络装上记忆
  80. 80.深度学习回顾:静态一类,动态一类
  81. 81.增强学习(1):蒙特卡洛、树搜索与 Q-learning
  82. 82.强化学习(2):从乌鸦吃核桃到井字棋的 value 回传

卷积神经网络(CNN):让特征自己学出来

约 25 分钟

📺 正在播放小象官方高清录播(支持倍速与清晰度调节)

小象实战讲义 · 机器学习算法精讲

我们所说的深度学习,其实都是基于已有的神经网络;而深度学习的标志之一,就是 ConvNet(卷积神经网络)——里面是分层的特征架构。本节回答四个问题:为什么网络要「深」而不要「宽」(一个诚实的开放问题 + 视网膜的启发);卷积到底是什么(扔两次小球就懂了);卷积核、局部连接、共享权重、max pooling 这几个零件怎么拼出完整的 CNN(含 32×32×3 的完整数字链);以及 CNN 的本质收尾——特征学习:不知道哪个特征重要,就让网络按目标自己调。

💡 核心导读

  • 为什么要深:加宽(每层多放节点)参数也一样多——讲师先定调「很多解释都是因为它效果好了,我才类似的解释」,再给最主流的一条:视网膜就是层状结构、细胞只对小块区域敏感。
  • 卷积的直观:扔两次小球,问停在离原点 3 的概率——把所有 a+b=3a + b = 3 的组合概率相加,这就是两个函数的卷积 (f∗g)(c)=∑af(a)⋅g(c−a)(f * g)(c) = \sum_a f(a) \cdot g(c - a)。
  • 两个省钱设计:partial connection(局部连接——5×5 全连接要 25 个权重,3×3 核只要 9 个)+ share weight(一个核扫全场、权重共用)。
  • max pooling 与美图相反定律:均值把痘痘磨平(美图干的事),max 把痘痘留到最后——图缩多小,它还是一个黑点。
  • 特征学习:不同卷积核关注图像不同侧面——「再造特征」;调整各层权重,实际是在调整各个特征里边的重要性。

为什么要「深」:一个诚实的问题

讲师开场先问了一个特别简单、也特别扎心的问题。神经网络参数特别多、模型能力强,那——为什么参数非要往「加深」的方向加?我不要很多层,把每一层多加一些节点,参数不仍然很多嘛?为什么反过来了:每层节点不要多、但要很多层,模型才具有更好的能力、更好的准确率?

讲师先诚实地定调:这实际上有很多不同的解释,「但这种解释都是因为它效果好了,我才类似的解释」——效果先好、解释后到,学术史的常态。然后给最主流的一条。

视网膜的启发:1981 年的那批工作

这种层状的形式,更类似于我们人的视网膜。1981 年那批诺贝尔奖工作(讲师点名的斯佩里、哈勃等人;课件上写的是 Hubel 与 Wiesel,「因视觉系统信息处理方面的发现」获奖)发现:视网膜神经本身就是一个层状的结构,而且视网膜细胞有的时候只对某些小的区域才产生敏感——这就是 receptive field(感受野)。敏感的程度还不一样:有些细胞对 edge(边)特别敏感,有的对不同东西敏感。不同的敏感性激活不同的细胞,做成层状的结构,我们才去识别这个物体——这是一个重大的发现。

也许我们再造的神经网络需要多层,也是希望这台计算的机器通过多层的建构,找到不同 level 的特征:给定一个图像,最底层都是一些简单的、非常底层的特征;通过一种计算方式把它提升,变成高层的特征,再更高层——一直到最后,通过某些组合或其他计算方式,对这个图像进行分类或处理。这就是我们认为的 ConvNet 思想的本质。

爱因斯坦的马

课件上引了爱因斯坦的一句话,讲师逐句念了、也逐句解读了:

I admire the elegance of your method of computation; it must be nice to ride through these fields upon the horse of true mathematics while the like of us have to make our way laboriously on foot. (我钦佩你计算方法的优雅;骑着「真数学」的骏马驰骋在这些原野上想必很美妙,而我们这样的人只能步履蹒跚地徒步前行。)

会计算的人、有好的计算方法,就像骑着一匹特别漂亮的马——在整个数学的、抽象的空间里游荡,很快找到最好的结果;而我们没有这好工具的,只好用腿慢慢走、一步一步找,追寻真理。

卷积:扔两次小球

卷积(convolution)有各种不同的说法,尤其在信号处理里面有完整的定义——讲师不推那一套,给一个稍稍简单、比较直观的。

想象一个小球,落到某一个点的时候,它会以中心为均值、以某种概率分布停在周围的地方。回过头来,如果它停在了某处,在原地拿起小球再扔一遍,它会以另一个分布(比如这个分布叫 ff、那个叫 gg)停在原点附近。我们想问:执行这样两次扔小球,它停在离原点为 3 的距离处的概率有多大?

拆开算:第一种情况,它以「到 2」的概率乘上「到 1」的概率;但 2+12+1 只是凑 3 的一种,0+30+3、1+21+2、2+12+1……所有可能的组合都有份。每个组合都是概率,全部加在一起,就得到两次的结果相加为 cc 的概率。把第二段距离写成 c−ac - a(a+b=ca + b = c,把 bb 替换掉),再对 aa 从负无穷到正无穷全部叠加——这就是两个函数 ff 跟 gg 的卷积在 cc 这个点上的值:

(f∗g)(c)=∑a=−∞+∞f(a)⋅g(c−a)(f * g)(c) = \sum_{a=-\infty}^{+\infty} f(a) \cdot g(c - a)

从一维延伸到二维、三维完全同理:cc 变成一个向量,AA 和 BB 也是向量,A1+B1=C1A_1 + B_1 = C_1、A2+B2=C2A_2 + B_2 = C_2,跟刚才没有任何区别。

卷积核扫图:5×5 变 3×3

回到图像。看一个例子:一个 3×3 的卷积核(讲师口中叫「卷积盒」,kernel),它每一个值都是 0、1 构成的数;给定的原始图片,一个绿色的 5×5 的黑白图像,也是由 0、1 构成。图像卷积执行的就是:把 3×3 的卷积核沿着图、从左上角到右下角扫一遍;每次扫过,核的每个值乘上对应位置绿色图像的值,加出一个和,写到输出上——这就变成了我们最后卷出来的特征。算一下尺寸:5×5 通过 3×3 的卷积核,就变成 3×3 的一个新特征表示——原来的图片变小了之后的一种高层特征。

把三个不同的核扫过同一张图(后两个核——「−1 与 9」「1 与 −7」——正是讲师板书点名的两种卷积核;图是讲义设定的 5×5 黑白小方块):

def convolve2d(img, kernel):
    """二维卷积(无 stride/padding): 核在图上滑动, 每个窗口对应相乘再相加(内积)"""
    h, w = len(img), len(img[0])
    kh, kw = len(kernel), len(kernel[0])
    out = [[0] * (w - kw + 1) for _ in range(h - kh + 1)]
    for i in range(h - kh + 1):
        for j in range(w - kw + 1):
            s = 0
            for ki in range(kh):
                for kj in range(kw):
                    s += img[i + ki][j + kj] * kernel[ki][kj]
            out[i][j] = s
    return out

# 5x5 黑白图(0/1): 中间一个亮方块(讲义设定)
img = [[0, 0, 0, 0, 0],
       [0, 1, 1, 1, 0],
       [0, 1, 1, 1, 0],
       [0, 1, 1, 1, 0],
       [0, 0, 0, 0, 0]]

kernels = [
    ("竖边缘核 [1,0,-1]x3", [[1, 0, -1], [1, 0, -1], [1, 0, -1]]),
    ("锐化核 -1 与 9",      [[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]),
    ("纹理核 1 与 -7",      [[1, 1, 1], [1, -7, 1], [1, 1, 1]]),
]

for name, k in kernels:
    out = convolve2d(img, k)
    print(f"{name}:")
    for row in out:
        print("   ", row)
    print()
竖边缘核 [1,0,-1]x3:
    [-2, 0, 2]
    [-3, 0, 3]
    [-2, 0, 2]

锐化核 -1 与 9:
    [6, 4, 6]
    [4, 1, 4]
    [6, 4, 6]

纹理核 1 与 -7:
    [-4, -2, -4]
    [-2, 1, -2]
    [-4, -2, -4]

同一张图、三个核、三种看法。竖边缘核只看左右差:方块左边界处输出 −3(左列全白、右列全黑)、右边界 +3、中间平地 0——它是一个「左右比较器」。锐化核(−1 与 9)反过来把亮方块的边缘抬高(6、4),均匀的内部压到 1——轮廓清晰。纹理核(1 与 −7)又反着来:均匀的内部是唯一正数(1)、四周全负——它把「这一块长得均不均匀」点了出来。这正是讲师说的:不同的卷积核作为乘积和内积之后,关注了图像的不同层面——通过卷积核,我们「再造」了很多不同的特征。

partial connection:为什么 9 个权重就够

关键来了:卷积核,在神经网络里面就可以理解成是权重——只不过,它只有部分的权重,这就是 partial connection(局部连接)。

算一笔账。5×5 的图片,如果是一个正常的全连接神经网络、隐层有一个节点,权重应该是 25 个;而 3×3 的卷积核只带 9 个权重(讲师口播「三个」,按 3×3 的核应为 9 个——数目以核的大小为准)——只用了一部分。

再用一维的平面例子看清楚。从 x0x_0 到 x8x_8,原则上 8 个输入、隐层一个节点的话,应该是 1×81 \times 8 这么多个权重;但实际上我们只用了两个权重:x0x_0 乘 w0w_0 加 x1x_1 乘 w1w_1,输出 y0y_0;往后挪一格,x1x_1、x2x_2 还是乘同一个 w0w_0、w1w_1,输出 y1y_1;再往后 x2x_2、x3x_3 配 w0w_0、w1w_1 得 y2y_2……把整个权重矩阵写出来:完整矩阵有 8 列那么大,但只有两列有数,其他全是 0——这就叫 partial connection,只是部分的连接。

好处很直接:减少了计算量,我们不需要算那么多 ww 了。

那信息会不会丢?——为什么必须多层

这是局部连接最要紧的一段推演,讲师替大家问了出来。看这个一维例子:y0y_0 到 y7y_7 里,其实包含了所有 x0x_0 到 x8x_8 的信息——但是它缺了一个东西:跟普通神经网络比,它缺少了比如 x8x_8 和 x4x_4 之间的组合——现在所有的组合都是「相邻域之间」的,x4x_4 和 x8x_8 隔了四位,这一层组合不出来。那怎么办?

看连接:x4x_4 跟 y3y_3、y4y_4 有关;x8x_8 跟 y7y_7 有关。如果再多一层——y3y_3、y4y_4 汇集到一个节点上,y7y_7 汇到另一个节点,这两个节点再相连——那这个新节点就同时包含 y3,y4,y7y_3, y_4, y_7,也就是包含了 x4x_4 和 x8x_8 之间的关联。

所以结论是:用 partial connection 的形式,层数必须增多,才能找到更底层数据之间的关联。以前不做 partial connection、一个节点跟 x0,x1,x2x_0, x_1, x_2……全都相连,信息一次就完全了;现在每一层只拿到部分信息,为了得到全面的信息,只能架构多层来保证信息的完整。这也是「神经网络为什么需要多层」的另一半答案。

share weight:一个核扫全场

刚才卷积核里边的参数,就是 share weight(共享权重):大家共同用这样一个 w0,w1w_0, w_1——每一次扫描出来的卷积核,都是同样的权重。同一个核,扫到哪里都用同一套数。把这个一维的例子引申到二维、三维的图像上,就是大家看到的卷积层。

max pooling:美图做的恰恰是相反的事

有了卷积,就能搭基础架构了:从最底层的 XX 开始,用卷积提最明显的信息,然后用 max(最大值)保留,之后再进行权重、再提取、再取最大值……这个取最大值的过程,叫 pooling(池化),又叫降采样(subsampling)——它主要的问题就是:找到底层特征里边最重要的信息。

那为什么要取 max,取别的行不行?——不是不行,比如取均值可不可以?可以,但取均值的话,实际上会模糊里边重要的东西:把这个邻域所有的值叠加起来做平均。讲师给了一个谁都用过的例子:美图。你脸上有一个坑、一个包、一个麻子、一个痦子——周围皮肤都是白的,只有这个地方有一个青春痘;做一个均值,大家都变成白的了。所以美图磨皮、让皮肤光滑,主要的操作就是类均值的操作。

但反过来,我们取最大值:给了这一块皮肤,里面最重要的特征就是这个痘痘——所以痘痘得以保留。比如你有一个 600×800 的大图,把它缩成 1/4 的大小(边长减半),脸上的痘痘还在;再缩得更小,人已经变得很小了,但那个痘痘仍然是一个黑点。为什么?因为它取的是这块区域里的最大值——这个痘痘是周围所有脸里边最重要的信息,一定要保留。图像从大变小的过程里,最重要的是把特殊的、不同于别人的信息保留下来——或者是一个痣,或者是一个痘痘,它会一直在。这跟美图恰恰做的是相反的事情。所以一般我们用 max pooling:保留不同层级的最大值信息。

板书上有个现成数例(右上角):{1,1,5,6}\lbrace 1, 1, 5, 6 \rbrace 这一块取最大值得 6;{2,4,7,8}\lbrace 2, 4, 7, 8 \rbrace 取最大值得 8——最明显的特征得以保留;一张 224×224 的图取一半,出来的是 112×112。把这几件事跑成代码:

# --- GT 板书数例: 右上角 {1,1,5,6} 取最大值得 6; {2,4,7,8} 取 8 ---
feature = [[1, 1, 2, 4],
           [5, 6, 7, 8]]

def pool2x2(block, mode):
    """2x2 池化: mode='max' 取最大值, mode='avg' 取均值"""
    h, w = len(block), len(block[0])
    out = []
    for i in range(0, h, 2):
        row = []
        for j in range(0, w, 2):
            win = [block[i][j], block[i][j + 1], block[i + 1][j], block[i + 1][j + 1]]
            row.append(max(win) if mode == "max" else sum(win) / 4)
        out.append(row)
    return out

print("输入 2x4:", feature)
print("max  池化 ->", pool2x2(feature, "max"))
print("avg  池化 ->", pool2x2(feature, "avg"))
print("224x224 取一半(2x2 池化边长减半): 224 ->", 224 // 2, "-> 112x112")
print()

# --- 美图相反定律: 皮肤全白(0)、一个痘痘(9), 两轮 max 池化后痘痘还在 ---
face = [[0, 0, 0, 0],
        [0, 9, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0]]

p1 = pool2x2(face, "max")
p2 = pool2x2(p1, "max")
print("4x4 皮肤(一个痘痘=9):")
print("  第 1 轮 max 池化 ->", p1)
print("  第 2 轮 max 池化 ->", p2, "  痘痘还在")

a1 = pool2x2(face, "avg")
a2 = pool2x2(a1, "avg")
print("  对照: 第 2 轮 avg 池化 ->", a2, "  痘痘被抹平(美图干的活)")
输入 2x4: [[1, 1, 2, 4], [5, 6, 7, 8]]
max  池化 -> [[6, 8]]
avg  池化 -> [[3.25, 5.25]]
224x224 取一半(2x2 池化边长减半): 224 -> 112 -> 112x112

4x4 皮肤(一个痘痘=9):
  第 1 轮 max 池化 -> [[9, 0], [0, 0]]
  第 2 轮 max 池化 -> [[9]]   痘痘还在
  对照: 第 2 轮 avg 池化 -> [[0.5625]]   痘痘被抹平(美图干的活)

三行对照就是本节的「美图定律」。同一块 {1,1,5,6}\lbrace 1, 1, 5, 6 \rbrace:max 留下 6(那个大数是这块的特征),均值只剩 3.25(被三个小数摊平了)。再看痘痘实验:max 两轮之后图缩到只剩一个数 9——痘痘从头活到尾,「这块皮肤里最重要的信息一定保留」逐轮兑现;而均值两轮之后只剩 0.5625——9/169 / 16,痘痘的能量被摊到了每一块皮肤上,这就是磨皮。

完整的 CNN:从 32×32×3 到全连接

搭齐零件,就有了完整的 full CNN(Convolutional Neural Networks)——最早由 1998 年 LeCun(ASR 里的「乐坤」)他们提出来。这里还有个故事:98 年的时候,大家觉得神经网络已经不好用了,所以这个东西提出来以后没有得到很多关注,甚至还被人拒稿——这几个人也比较生气。讲师的感慨:学术界有的时候也是很盲从的,不知道什么该做、什么好坏。这是最早出现的卷积神经网络。

从零到一:图像怎么变成「一点」

现在从零到一,看卷积到底怎么做。一个大向量,比如 1×30721 \times 3072(32×32×332 \times 32 \times 3 的图像拉直),乘上一个 WW,比如 10×307210 \times 3072 的——一乘之后,就把 3072 维压缩成 10 维,每一个最后得到的结果就是一个数。这就是我们怎么把图像变成「一点点」的:叫卷积也好、叫内积也好,都可以。

一层卷积:5×5×3 的核扫过 32×32×3

给定一个 32×32×332 \times 32 \times 3 的图像(三个通道的彩色图),配一个 5×5×3 的卷积核。怎么卷?把核「压」进图像的每个位置:每个点互相对应相乘、最后再相加——因为区域是 5×5 的、厚度是 3,它们乘完做内积,得到的是一个数(wx+bwx + b 的那个数)。这一个数,是核在这个位置上的「响应」。

把 5×5 的核在 32×32 的整个区域里扫过一遍:32−5+1=2832 - 5 + 1 = 28,所以变成 28×28×1——这是一组数、一个 feature map。换一个卷积核(另外一组不同的值),又卷出新的一个 feature map;用 6 个不同的核,原来 32×32×3 的图像就变成了 28×28×6——被卷积了一层特征之后的图像。

再卷一层:厚度要对上

在 28×28×6 上继续:再取一个卷积核——讲师先说「5×5×3」马上纠正——5×5×6(厚度必须跟输入的 6 对上),用 10 个这样的核:28−5+1=2428 - 5 + 1 = 24,就变成了 24×24×10。这样一层一层不断深化地卷下去。

但你会发现一件有意思的事:每卷一层,特征变化得很明显(不同的高级特征),可图像的 size 还是比较大。为了方便计算,把主要特征保留就可以了——这就回到刚才的 pooling。

stride 与 padding:两个具体技巧

先看卷积本身还有两个可调的地方。7×7 的图被 3×3 卷,默认 stride(步长) 是 1——每一步只挪一个位置,出来 5×5;如果每一步挪两个位置(stride=2),7×7 就变成 3×3 的输出。还有一种方式是为了保持大小统一:加 padding(填充)——在周围加一圈 0,本来 7×7 变成 9×9,再被 3×3 卷出来还是 7×7。这些都是满足不同需求的实际技巧。把整条尺寸链跑一遍:

# --- 尺寸链: GT 的 7x7/3x3 与 32x32x3 主链 ---
def out_size(n, k, stride=1, padding=0):
    return (n + 2 * padding - k) // stride + 1

print("7x7 图, 3x3 核:")
print(f"  stride=1, padding=0 -> {out_size(7, 3)}x{out_size(7, 3)}")
print(f"  stride=2, padding=0 -> {out_size(7, 3, stride=2)}x{out_size(7, 3, stride=2)}")
print(f"  stride=1, padding=1(周围加一圈 0) -> {out_size(7, 3, padding=1)}x{out_size(7, 3, padding=1)}")
print()
print("32x32x3 主链:")
print(f"  32*32*3 = {32 * 32 * 3} 维(全连接对照: 1x3072 的向量 x W(10x3072) -> 10 维, 内积)")
print(f"  5x5x3 核扫过 32x32: 32-5+1 = {32 - 5 + 1} -> 28x28x1")
print(f"  换 6 个核 -> 28x28x6")
print(f"  再用 5x5x6 核(10 个): 28-5+1 = {28 - 5 + 1} -> 24x24x10")
print()
print("这一层的权重数: 6 个 5x5x3 的核 =", 6 * 5 * 5 * 3, "个(共享)")
7x7 图, 3x3 核:
  stride=1, padding=0 -> 5x5
  stride=2, padding=0 -> 3x3
  stride=1, padding=1(周围加一圈 0) -> 7x7

32x32x3 主链:
  32*32*3 = 3072 维(全连接对照: 1x3072 的向量 x W(10x3072) -> 10 维, 内积)
  5x5x3 核扫过 32x32: 32-5+1 = 28 -> 28x28x1
  换 6 个核 -> 28x28x6
  再用 5x5x6 核(10 个): 28-5+1 = 24 -> 24x24x10

这一层的权重数: 6 个 5x5x3 的核 = 450 个(共享)

值得盯住最后一行:6 个 5×5×3 的核,一共只有 450 个权重——而且是 28×28×1 整张 feature map 共享的。要是按全连接来算,一个接住整个 3072 维输入的节点就要 3072 个权重——这就是「部分连接 + 共享权重」省出来的钱。

最后一步:越来越厚的小 patch,接一个全连接

之后的事情就是不断地「卷积、pooling、卷积、pooling」——用了不同的激活函数(上一节的 sigmoid 等),到厚层的时候会变来越来越厚;但实际上它每一小块都是特别小的 patch。把这些 patch 再通过一个全连接层组合到一起,就变成了对物体、对东西的最后分析。

课件上还留了一个可视化 demo(讲师原话:「这个 demo 我不知道现在还能不能用,大家试一下这个网站」——scs.ryerson.ca/~aharley/vis/conv/):在上面写一个手写的数字,比如写个 3——它告诉你 3 的最底层长什么样;第一层卷积出来的 feature map,通过不同的卷积核卷成各种各样的 3;这个 3 经过 pooling,变成大小是原先 1/4 的那些 3;再用不同的 feature map 重新卷积,变成新的图像;再 pooling、再卷积、再 pooling;最后 pooling 合在一起,直接全连接到背后的一个节点——所谓输出。它甚至可以点开看:第几层、第几个 filter 里边的什么权重。这是把「每层实际上是什么样子」摊开给你看的好例子。

特征学习:CNN 的本质收尾

把本节串起来。不同的卷积核作为乘积和内积,关注了图像的不同的层面——通过这种卷积核,我们把图像不同的侧面「再造」出了很多不同的特征;而这些特征,在某种程度上就作为后来分类、预测、识别的重要特点。

所以深度学习、尤其是 CNN,很多时候在讲它是可以做特征学习(feature learning)的——这是里边最重要的一部分。不只是参数大就好:需要去构造不同的特征,而这种层状的结构,一层一层不断地卷积和迭代,构建了很多不同可能型的特征。之后自动地根据我们的目标调整不同层数的权重——实际上是在调整各个特征里边的「重要性」。给了一个图片,我们提了从底层、中层、中高层各个不同 level 的特征,甚至不知道哪个特征重要——怎么办?主动地让它去学习,根据我们的目标(分类或其他)学到我们的特征。

所以对神经网络来讲,很大的一个层面——尤其对深度神经网络——它是一个特征学习。这个特征学习,是它跟其他机器学习模型比起来非常明显的优势,也使它具有了更好的能力、更好的准确率。

发展史上再一笔(讲师口径):UIUC 和 NEC 合作做了 2010 年那样的网络;2012 年那个大家经常叫 SuperVision 的网络(即后来熟知的 AlexNet 那一队);之后到 googlenet;再后来到 VGG——层数很多(讲师口播 24 层;常见的 VGG-16/VGG-19 是 16/19 层);到微软亚洲研究院(MSRA)做了更复杂的——ResNet;还有 DenseNet、各种不同的卷积网络。形式很多,但都还是以卷积操作和这种层状架构为主的模型。这些都是卷积神经网络的重要部分。下次课讲深度学习里另一个常用的东西——除了 CNN 之外,还有 RNN。

📝 动手练一练

  1. 手算一个卷积窗口:用正文代码块 1 的图和「竖边缘核」,窗口放在左上角(覆盖前三行、前三列)。(a) 手算这个窗口的输出;(b) 输出矩阵里 −3 那个位置,对应原图的什么地方?为什么中间那行是 −3、上下两行只有 −2?
👉 点击查看参考答案

(a) 窗口覆盖的九个数是 [0,0,0; 0,1,1; 0,1,1][0,0,0;\ 0,1,1;\ 0,1,1],核每行是 [1,0,−1][1, 0, -1]:每行贡献「左列减右列」,三行分别是 0−0=00-0=0、0−1=−10-1=-1、0−1=−10-1=-1,合计 −2(与代码输出左上角一致)。(b) −3 在输出矩阵的中间行第一列——它的窗口左列全 0、右列全 1,完整地压在亮方块的左边界上;上下两行的窗口因为碰到了图的白边(第一行/最后一行是 0),右列有一个 0,所以只有 −2。「窗口完全落在边界上时响应最强」——这就是边缘检测核的原理。

  1. 池化与美图:对正文代码块 2 的 4×4「皮肤」(一个痘痘=9,其余全 0),把 max 换成平均池化,做两轮。(a) 两轮平均池化后剩多少?(b) 用一句话说清「磨皮」和「max pooling」分别干了什么。
👉 点击查看参考答案

(a) 第一轮每块取平均:含痘痘的块是 (0+9+0+0)/4=2.25(0+9+0+0)/4 = 2.25,其余三块都是 0;第二轮再平均,(2.25+0+0+0)/4=0.5625=9/16(2.25+0+0+0)/4 = 0.5625 = 9/16(代码对照行:[[0.5625]])。(b) 磨皮(均值)把特殊的能量摊到每一块皮肤上,痘痘从 9 被稀释成 0.5625,图「变干净」;max pooling 反过来只留每块里最大的那个,痘痘从头活到尾——图像从大变小,特殊信息不丢。

本章小结

本节五个零件拼一台完整的 CNN。第一,为什么要深:一个诚实的开放问题(加宽参数一样多),最主流的解释来自视网膜——层状结构、感受野、对 edge 敏感的细胞层层激活(1981 年那批诺奖工作);多层就是机器找不同 level 特征的方式。第二,卷积:扔两次小球、所有 a+b=ca + b = c 的组合概率相加——(f∗g)(c)=∑af(a)g(c−a)(f * g)(c) = \sum_a f(a) g(c-a)。第三,卷积核=权重,但只连局部:5×5 全连接 25 个权重、3×3 核 9 个(partial connection),且一个核扫全场(share weight);代价是丢掉了远距离组合——所以必须多层才能把 x4x_4 与 x8x_8 的关联重新接上。第四,max pooling:均值磨皮(美图的活)、max 保痘(缩到多小都是黑点),{1,1,5,6}→6\lbrace 1,1,5,6 \rbrace \to 6、{2,4,7,8}→8\lbrace 2,4,7,8 \rbrace \to 8、224→112。第五,完整链路:32×32×3 被 5×5×3 核扫成 28×28×6,再被 5×5×6×10 卷成 24×24×10,卷积、pooling 交替,最后小 patch 接全连接——6 个核只有 450 个权重、全场共享。收尾的本质一句话:CNN 是特征学习——不同核「再造」不同侧面的特征,调各层权重就是调各特征的重要性;特征哪个重要不知道,就让网络按目标自己学。

📋 行动清单

  • 口述两遍「为什么深」:视网膜层状结构版、partial connection 必须多层版——不看笔记各讲一遍。
  • 手推扔小球:在纸上列出 a+b=3a + b = 3 的全部四种组合(0+3、1+2、2+1、3+0),算出 (f∗g)(3)(f*g)(3)。
  • 跑改代码:把代码块 1 的图换成一个「左暗右亮」的图,看竖边缘核的输出符号怎么变;再给纹理核换一组值。
  • 玩 demo:打开课件里的可视化 demo,写一个手写数字,点开第几层、第几个 filter,看看「每层长什么样」。

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第19章课件:神经网络(BP/CNN/RNN)
    下载