Lecture 5 · 深度学习直觉:用案例学会设计输入、输出、数据与损失函数 · Deep Learning Intuition through Case Studies

Deep Learning · Lecture 05。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:Stanford CS230 Fall 2025 Lecture 2(9/30,“Key AI Concepts Through Case Studies”);Autumn 2018 Lecture 2 “Deep Learning Intuition”(Kian Katanforoosh,YouTube 公开录像);Fall 2017 的课堂课件 “CS230: Lecture 3 – Various Deep Learning Topics”(Kian Katanforoosh、Andrew Ng,CS230 网站上的公开 PDF)。相关的 Coursera 编程作业:Face Recognition、Art Generation with Neural Style Transfer(C4 第 4 周)、Trigger Word Detection(C5 第 3 周)。 依据说明:Fall 2025 的 slides 和录像只对选课学生开放,本讲没有用到它们。2018 年的录像是公开的,但本讲义没有逐字核对录像,开场回顾(多标签 vs softmax、encoding)和触发词案例的安排,参考的是这一讲的内容提纲,细节可能和录像有出入。具体的设计数字来自 Fall 2017 的公开课件:白天 / 黑夜用 10,000 张图、输入 (64, 64, 3);人脸验证输入 (412, 412, 3)、128 维编码、triplet loss、K 近邻、K-means;风格迁移”学的是一张图,不是参数”。触发词的数字来自 Coursera C5 的编程作业:10 秒音频、5511 × 101 的频谱、1375 个输出时间步、结束后标 50 个 1。技术细节以原始论文为准(FaceNet、Gatys et al.,见 §10)。§6.2 “今天的例子”是本讲义的补充,不是原课内容。 配套代码 Code:code/lecture05/(numpy;4 个脚本 + 2 个共享模块;最慢的 01_day_night_resolution.py 约 20 秒,其余都在 10 秒左右或更快) 预计阅读时间:2.5–3 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 拿到一个新问题时,用 Kian 的五问(数据?输入?输出?架构?损失?)把它拆开,并且知道每一问通常在哪里”埋着坑”。
  2. 用”把图缩小,看人还能不能分辨”的办法估计输入分辨率;解释为什么分辨率、数据量和模型三者要一起决定,以及为什么”人类水平”可以当作不可避免误差(Bayes error)的估计。
  3. 说清楚人脸验证为什么不能直接比像素,也不该当成”每个学生一类”的分类问题;解释编码(encoding / embedding)+ 距离的方案,写出 triplet loss 和它的梯度,说出 margin 和”推开负样本”那一项各防止了什么。
  4. 解释风格迁移里”内容”和”风格”分别用什么表示(激活值 vs Gram 矩阵),以及”固定参数、优化输入”这个想法还能用在哪里(可视化、对抗样本)。
  5. 为触发词检测设计输出和标签:为什么输出是一个序列;为什么”结束后标 k 个 1”比”只标 1 个”好;为什么逐帧准确率会骗人,产品该看什么指标;怎样用数据合成几乎免费地得到大量标注。
  6. 认出”编码”这个贯穿所有案例的概念,并能用五问去设计一个今天常见的 embedding 检索 / 推荐系统。
  7. 记住这一讲最重要的一句话:损失函数定义了任务。标签怎么标、损失怎么写,往往比换一个更大的架构更重要。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
第 1 讲:神经元、网络、x→yx \to y、记号、训练四步逻辑回归、交叉熵、梯度下降的推导(第 2 讲)
二元交叉熵是什么(第 1 讲 tiny_nn 用过)隐藏层、反向传播的完整推导(第 3、4 讲)
一点线性代数:向量、距离、矩阵乘法训练 / 开发 / 测试集划分,偏差与方差(第 6 讲)
Python 基础;numpy 不熟也没关系softmax 与多分类交叉熵的完整讲解(第 9 讲)
precision / recall、Bayes error、误差分析、数据不匹配(第 10 讲)
卷积、人脸识别和风格迁移的网络细节(第 12–14 讲)
RNN / GRU、触发词检测的模型细节(第 15–16 讲)
对抗样本(第 18 讲)、可解释性与特征可视化(第 21 讲)

这是一节”设计问题”的直觉课。前四讲(和后面的大部分技术讲次)回答”网络怎么算、怎么训练”;这一讲回答一个更早的问题:在写第一行代码之前,你要决定什么? 每个案例的技术细节都只讲到”够理解设计”为止,并标出后面哪一讲会展开。

0.3 本讲地图 Map

§1  开场:模型 = 架构 + 参数;改输出就是改任务;"编码";五问
§2  案例 1 白天还是黑夜:数据要多少?输入分辨率选多大?            ← 代码 01
§3  案例 2 学校门禁的人脸验证:编码 + 距离;triplet loss;识别与聚类   ← 代码 02
§4  案例 3 艺术风格迁移:内容 vs 风格;优化的是像素,不是参数         ← 代码 03
§5  案例 4 触发词检测:序列输出;标签怎么标;数据合成;指标           ← 代码 04
§6  贯穿始终的"编码";用五问设计一个今天的 embedding 系统(补充)
§7–11  小结、常见误区、练习、延伸阅读、下一讲预告

0.4 记号 Notation

沿用第 1 讲 §0.4 的全部记号(mm、nxn_x、X∈Rnx×mX \in \mathbb{R}^{n_x \times m}、W[l]W^{[l]}、L\mathcal{L}、JJ ……)。本讲新增:

符号含义出现在
f(x)f(x)编码器(encoder):把输入映射成一个向量,叫编码 / 嵌入(encoding / embedding)§1.4、§3
d(x1,x2)=∥f(x1)−f(x2)∥2d(x_1, x_2) = \lVert f(x_1) - f(x_2) \rVert^2两个输入的编码之间的平方距离§3
A,P,NA, P, Ntriplet 里的 anchor(锚)、positive(同一个人)、negative(另一个人)§3.4
α\alpha(本讲)triplet loss 的 margin(间隔)。注意:第 1 讲里 α\alpha 是学习率,本讲沿用 FaceNet 的写法把 α\alpha 留给 margin,学习率改写成 η\eta§3.4
a[l](x)a^{[l]}(x)输入 xx 在第 ll 层的激活值(特征图)§4
G[l](x)G^{[l]}(x)第 ll 层激活值的 Gram 矩阵(风格表示)§4.2
JcontentJ_{content}、JstyleJ_{style}、ww内容损失、风格损失、风格损失的权重§4.3
y⟨t⟩y^{\langle t \rangle}、TyT_y第 tt 个时间步的标签(Ng 在序列模型里用尖括号上标表示时间步)、输出序列的长度§5
kk触发词结束后标几个 1§5.4

0.5 关于数字 · About the numbers

讲义里所有实验数字都来自配套代码的实际输出。种子是固定的,同一台机器上重复运行,输出逐字相同。四个案例的数据都是合成的,“真相”由我们写下的公式决定(daynight_world.py,以及 02、04 文件开头的说明),所以能精确地说出”最多能做到多好”。为了让现象清楚,有些世界的设定是刻意选的,比如白天 / 黑夜世界里”阴天”和”城市夜景”的亮度分布完全一样。这些设定都在代码开头写明了。有好几处结果和我写代码前的预想不一样,讲义按实际结果写,这些情况列在代码 README 的”关于实验结果”里。数字只说明机制,不代表任何真实系统的性能。


1. 开场:模型、输出与”五问” · Recap & the five questions

1.1 模型 = 架构 + 参数

先把第 1 讲的骨架换一种说法,这一讲会反复用到:

模型(model)=架构(architecture)+参数(parameters)\text{模型(model)} = \text{架构(architecture)} + \text{参数(parameters)}

架构是你设计的”形状”:几层、每层多宽、用什么激活函数、输出是什么。参数是训练学出来的数。训练的循环是:

 输入 x ──→ 模型 f(x; θ) ──→ 输出 ŷ ──→ 损失 L(ŷ, y) ──→ 梯度 ∂L/∂θ ──→ 更新 θ ← θ − η·∂L/∂θ
   ↑                                        ↑
 你决定"喂什么"                          你决定"什么叫错"

在这个循环里,你能直接设计的是这几样东西:数据(喂进去哪些样本、配什么标签 yy)、输入 xx 的形式、输出的形式、架构,以及损失函数。参数是梯度下降替你找的,而它找到的只会是”在你给的数据上、按你写的损失、最好的参数”。数据不对,或者损失写得不对,梯度下降会非常认真地学会一件错误的事。本讲的四个案例会一次次看到这一点。

1.2 一个熟悉的例子:猫识别

Coursera C1 第 2 周的编程作业(Logistic Regression with a Neural Network Mindset):判断一张 64×64 的彩色照片里有没有猫。训练集只有 209 张图。

  • 输入:一张 (64, 64, 3) 的图,拉平成 nx=64×64×3=12,288n_x = 64 \times 64 \times 3 = 12{,}288 维的列向量(第 1 讲 §3.4)。
  • 输出:一个数 y^=σ(w⊤x+b)∈(0,1)\hat y = \sigma(w^\top x + b) \in (0, 1),表示”有猫”的概率。
  • 架构:逻辑回归,也就是没有隐藏层的神经网络(第 2 讲)。参数是 12,288 个权重加 1 个偏置。
  • 损失:二元交叉熵 L(y^,y)=−[ylog⁡y^+(1−y)log⁡(1−y^)]\mathcal{L}(\hat y, y) = -\big[y \log \hat y + (1 - y)\log(1 - \hat y)\big]。

这四个选择看起来”理所当然”,但每一个都是决定。比如为什么是 64×64 而不是 256×256?§2 会专门讨论。

1.3 改输出,就是改任务:多标签 vs softmax

现在要识别三种动物:猫、狗、长颈鹿。输出层该怎么改?取决于你到底想问什么:

你想问的问题标签 yy 的样子输出层损失
“照片里有哪些动物?“(可以同时有猫和狗)multi-hot,比如 (1,1,0)(1, 1, 0)3 个独立的 sigmoid,每类一个3 个二元交叉熵相加
“这是哪一种动物?“(恰好一种)one-hot,比如 (0,1,0)(0, 1, 0)1 个 softmax:y^c=ezc/∑jezj\hat y_c = e^{z_c} / \sum_j e^{z_j}多分类交叉熵 −∑cyclog⁡y^c-\sum_c y_c \log \hat y_c

同一组输出层的原始分数(logits)z=(2.0,1.0,−1.0)z = (2.0, 1.0, -1.0),在两种输出层下给出完全不同的答案:

  • 三个 sigmoid:(0.881,0.731,0.269)(0.881, 0.731, 0.269)。猫和狗都”很可能有”,三个数加起来不等于 1,也不需要等于 1。
  • softmax:(0.705,0.259,0.035)(0.705, 0.259, 0.035)。三个数加起来等于 1,表示”三选一”。

前者叫多标签分类(multi-label),后者叫多类别分类(multi-class)。网络的其余部分可以一模一样,差别只在最后一层和损失函数,但它们回答的是两个不同的问题。选错了,模型就会认真地学一个你不想要的东西。比如用 softmax 做”照片里有哪些动物”,它会被迫在猫和狗之间二选一。softmax 的完整推导在第 9 讲。

1.4 编码:隐藏层在”重新描述”输入

第 1 讲 §2.3 用线性探针看到,网络的隐藏层自己学出了”步行便利度”这种输入里没有的中间特征。把这个观察推广一下:每一个隐藏层的激活值 a[l]a^{[l]},都是输入的一种新的描述,叫做编码(encoding)或表示(representation)。

在图像上,这种描述逐层变得越来越抽象。经典的可视化研究(如 Zeiler & Fergus, 2014)显示,卷积网络的第一层学出边缘和颜色斑块的检测器,中间层学出纹理和部件(眼睛、轮子),更深的层对整个物体(人脸、狗)有反应。所以网络倒数第二层的输出,往往就是一个”把图像里和任务有关的信息浓缩起来”的向量。

记住这个词,它是整讲的主线:

  • §2 白天 / 黑夜:有了隐藏层,网络才能感受到”有没有小亮点”(逻辑回归做不到);
  • §3 人脸验证:编码本身就是产品,我们直接比较两张脸的编码;
  • §4 风格迁移:用一个训练好的网络的编码,来定义什么叫”内容”、什么叫”风格”;
  • §5 触发词:网络对”最近 1.6 秒声音”的编码,决定此刻要不要响应;
  • §6:今天几乎所有的检索、推荐、多模态系统,核心都是一个编码器。

1.5 Kian 的五问

CS230 的课堂案例都按同一个模板来分析。Fall 2017 的课件在每个案例里都列出这五个问题:

  1. 数据 Data? 要多少?从哪里来?标签怎么得到?怎么划分?有没有偏差(bias)?
  2. 输入 Input? 是什么、多大(分辨率、长度)?要不要预处理?
  3. 输出 Output? 是一个数、一个向量、还是一个序列?最后一层用什么激活函数?
  4. 架构 Architecture? 浅还是深?全连接、卷积还是循环?要不要用预训练的网络?
  5. 损失 Loss? 怎样衡量”错了多少”?它真的衡量了你在乎的东西吗?

先看一眼四个案例的答案(后面逐个展开)。注意每个案例”难点”所在的那一问都不一样:

案例数据输入输出架构损失难点在哪一问
白天 / 黑夜约 1 万张带标签的图小图(多小?)0 / 1浅层网络二元交叉熵输入的分辨率、数据量
人脸验证公开人脸库(很多人,每人多张)两张脸同一人?→ 先输出编码深层 CNN 编码器triplet loss输出(编码而非类别)和损失
风格迁移两张图 + 一个预训练网络内容图、风格图一张新图预训练 CNN(固定)内容 + 风格损失损失(而且优化的是输入)
触发词合成的 10 秒音频频谱图序列每个时间步 0 / 1卷积 + RNN逐时间步交叉熵数据来源和标签怎么标

2. 案例 1:白天还是黑夜 · Day’n’Night classification(热身)

代码:01_day_night_resolution.py、daynight_world.py

2.1 五问的快速回答

任务:给一张照片,判断它是白天(y=0y = 0)还是夜晚(y=1y = 1)拍的。Fall 2017 课件给的参考答案是:

  • 数据:约 10,000 张图(课件上还追问了两句:“怎么划分?有没有偏差?”);
  • 输入:(64, 64, 3);
  • 输出:y∈{0,1}y \in \{0, 1\},最后一层用 sigmoid;
  • 架构:“浅层网络应该就能做得很好”;
  • 损失:二元交叉熵。课件的评价只有一个词:“Easy”。

看起来像送分题。但”64×64”和”1 万张”这两个数是怎么来的?如果你的老板问”为什么不用 512×512 的原图,信息不是更多吗?“,你怎么回答?这一节就来回答这两个问题。

2.2 数据:要多少?怎么分?有没有偏差?

要多少? 一个粗略但有用的思路是和你做过的任务比。白天 / 黑夜比”有没有猫”简单得多:线索很粗(整体亮度、天空的颜色),不需要识别任何具体的物体。所以如果猫识别需要几万张图,白天 / 黑夜大概几千到一万张就够。这只是起点,真正的答案要靠实验(§2.5)。

怎么分? 训练、开发、测试三份(第 6 讲),而且开发集和测试集要来自你真正要用的分布。

有没有偏差? 这是课件特意追问的一句,值得停下来想:假设你从网上搜”夜晚”得到的照片大多是城市夜景,搜”白天”得到的大多是郊外风景,模型会学到什么?它很可能学会了”城市 vs 郊外”,而不是”白天 vs 夜晚”。到了真实使用时,碰到一张城市白天的照片,它就会说”夜晚”。数据里任何和标签”碰巧相关”的东西,都可能变成模型的捷径。

2.3 输入:分辨率选多大?“缩小了人还能分辨吗”

分辨率是一个真实的取舍:

  • 太大:输入维度 nx=3×(边长)2n_x = 3 \times (\text{边长})^2。从 64×64 到 512×512,nxn_x 从 12,288 涨到 786,432,第一层的参数和计算量也涨 64 倍。参数越多,需要的数据越多(§2.5)。如果要在手机上运行,模型越小越好(§2.6)。
  • 太小:细节被平均掉了,谁也分不出来。

Kian 在课上给的办法(据 2018 年录像的提纲)非常朴素:把图片缩小到不同的分辨率,看人还能不能分辨。 人还能分辨的最小分辨率,大致就是”信息还在”的最小分辨率。这背后的道理是:在图像、语音这类人类很擅长的任务上,人的表现接近”这些信息里最多能读出多少”的上限。

为了能把这个办法完整地走一遍,代码 01 用了一个合成的照片世界(daynight_world.py)。图片是 32×32 的彩色小照片,有五种场景:

场景占比标签画面
晴天30%白天明亮的蓝天,有时有太阳
阴天20%白天灰蒙蒙,光线均匀,没有点光源
晴朗的夜晚20%夜晚很暗,有星星、月亮、几盏灯
城市夜景20%夜晚天空被灯光照成灰色,亮度分布和阴天完全一样;地面上有很多盏只占 1 个像素的极亮路灯
黄昏10%随机橙色的天空;标签由”日落前还是日落后”决定,画面本身看不出来

两个刻意设计的难点:阴天 vs 城市夜景只能靠很小的亮点区分;黄昏谁来都只能猜。所以这个世界的准确率上限(Bayes 最优)是 1−10%×50%=95%1 - 10\% \times 50\% = 95\%。

代码 01 演示 1 把一张阴天和一张城市夜景并排画成字符画,分辨率依次降低。32×32 和 16×16 时,城市夜景的地面上能看到一个个比周围亮的小点;到了 8×8,一个 1 像素的路灯被平均进了 4×4 = 16 个像素的块里,“凸起”只剩原来的 1/16,两张图已经很难分辨。你可以自己先看一遍那张字符画,当一次”人”。

代码 01 演示 2 用一个手写的”人类代理”(human proxy)代替人去看 3000 张测试图。这个规则先看天空:又亮又蓝就判白天,很暗就判夜晚。拿不准时,再找”比周围每个邻居都亮 0.1 以上”的孤立小亮点,至少 2 个就判夜晚,否则判白天。

分辨率nxn_x准确率晴天阴天晴朗的夜晚城市夜景黄昏
32×3230720.9431.0001.0001.0001.0000.511
16×167680.9011.0001.0001.0000.7790.506
8×81920.7541.0001.0001.0000.0000.494
4×4480.7541.0001.0001.0000.0000.494
2×2120.7541.0001.0001.0000.0000.494
1×130.7531.0001.0001.0000.0000.486

读这张表:

  1. 晴天和晴朗的夜晚在任何分辨率下都能分对,连 1×1 都够(整张图平均成一个颜色,nx=3n_x = 3)。如果世界里只有这两种场景,输入只要 3 个数。
  2. 城市夜景那一列决定了曲线的形状:32×32 全对,16×16 认出 78%,8×8 及以下全错(都被判成阴天)。准确率掉得最多的一步是 16×16 → 8×8(0.901 → 0.754)。这就是悬崖。按 Kian 的办法,这个任务”人还能分辨”的最小分辨率是 16×16,比全分辨率的输入小 4 倍。
  3. 全分辨率下人类代理是 0.943,和上限 0.95 几乎一样,剩下的误差全来自黄昏。这就是第 10 讲要讲的 Bayes error(不可避免的误差):标签本身就不确定,任何模型都消不掉。在真实任务里我们不知道 Bayes error 是多少,常用”人类水平”(human-level performance)当作它的估计。

真实项目里怎么做? 你不需要写规则。把几十张验证图缩小到三四个分辨率,自己看,或者请两三个同学标一下,花一个下午就够了。Fall 2017 的课件选了 64×64。课件没有写理由,合理的推测是:对真实照片来说,这个分辨率下人还能可靠地分辨白天和黑夜,输入又比原图小两个数量级。

2.4 模型怎么说:分辨率、架构和过拟合

人类代理告诉我们”信息在哪个分辨率还在”。那真正训练出来的模型呢?代码 01 演示 3 在 6 个分辨率上各训练两个模型:逻辑回归和一层隐藏层的网络 [nx,64,1][n_x, 64, 1]。训练集 8000 张,测试集 3000 张,全部用第 1 讲的 tiny_nn.train:

分辨率模型参数个数训练准确率测试准确率城市夜景人类代理
32×32逻辑回归3,0730.7790.7350.6640.943
32×32网络196,7370.9660.8690.6740.943
16×16逻辑回归7690.7750.7400.5210.901
16×16网络49,2810.9780.9240.9150.901
8×8逻辑回归1930.7530.7350.5690.754
8×8网络12,4170.9470.9160.9130.754
4×4网络3,2010.8230.7980.6330.754
1×1网络3210.7640.7460.5840.753

(4×4、2×2、1×1 的逻辑回归都在 0.738–0.750 之间,完整的表见脚本输出。)

先预测一下:逻辑回归在 32×32 上会比在 1×1 上好吗?

结果是不会。逻辑回归在所有分辨率上都差不多(0.735–0.750),和 1×1 的人类代理一个水平,城市夜景基本在猜。原因不在分辨率,而在模型:逻辑回归只能对像素做加权求和 w⊤xw^\top x,而”某处有个小亮点”是一个位置不固定的特征。亮点出现在哪里都行,加权求和只能感受到它对整体亮度贡献的那一点点。给它再多像素也没用,信息在输入里,但模型够不着。

网络(一层 ReLU 隐藏层)好得多,而且出现了三个值得记住的现象:

  1. 最好的分辨率不是最大的。16×16 的网络 0.924,8×8 是 0.916,32×32 反而只有 0.869。32×32 的网络有 196,737 个参数,训练准确率 0.966、测试只有 0.869,这是典型的过拟合(第 6 讲):输入维度越大,同样 8000 张图”摊”到每个参数上的信息就越少。(8×8 和 16×16 之间相差 0.008,在随机波动范围内。草稿里换一组数据种子时,8×8 略好。)
  2. 8×8 的网络认出了 91.3% 的城市夜景,而 8×8 的人类代理一张都认不出。小亮点被平均后只剩 1/16 的凸起,人眼(阈值 0.1)看不出来,但网络能把很多像素上的微弱统计差异”攒”起来。所以人类代理是一个保守的参考,不是硬性的下限:它告诉你”在这个分辨率以上,信息肯定够”,但模型有时在更低的分辨率上也能做到。
  3. 计算量:分辨率每翻一倍,nxn_x、第一层参数和每张图的乘加次数都变成 4 倍。从 32×32 降到 16×16 或 8×8,模型大小依次只剩 1/4、1/16,准确率反而都更高。

2.5 数据量和分辨率要一起决定

代码 01 演示 4 让网络只用前 500 / 2000 张训练图:

训练图张数32×3216×168×84×4
5000.7680.7650.7560.751
2,0000.7950.8260.7800.770
8,0000.8690.9240.9160.798

白天 / 黑夜:分辨率、模型和数据量

(这张图由 01_day_night_resolution.py 生成;没装 matplotlib 时脚本只打印表格。)

  • 数据少的时候,分辨率高也是浪费。500 张图时四个分辨率都在 0.75–0.77,网络连”小亮点”这条线索都学不出来,只学会了”看天空亮不亮”。
  • 数据越多,高分辨率越能发挥作用。从 500 张到 8000 张,32×32 涨了 0.101,4×4 只涨了 0.047。4×4 的图里小亮点的凸起只剩 1/64,几乎没有信息,数据再多也学不到多少。
  • 但即使有 8000 张,32×32 仍然不如 16×16 和 8×8。“需要多少数据”和”输入多大”是同一个问题的两面:输入越大,要喂饱它的数据越多。五问里”数据”和”输入”挨在一起,道理就在这里。

实际做项目的顺序:先用”人眼看缩小的图”定一个候选范围(这里是 ≥ 16×16,再往下人就看不出了),再在这个范围附近做实验,比较两三个分辨率(这里发现 8×8 对模型也几乎一样好)。直觉给起点,实验做决定。

2.6 放在服务器上,还是放在手机上?

Fall 2017 课件在这个案例里还问了一个部署问题:模型(架构 + 训练好的参数)放在哪里运行?

放在服务器上(server-based)放在设备上(on-device)
好处App 很轻,模型可以很大预测更快(不用上传图片),没网也能用,图片不离开手机
代价每次都要上传图片,要联网、有延迟模型必须小,手机的内存、算力和电量都有限

如果选择放在手机上,§2.4 那张表里的”参数个数”就成了硬约束。16×16 的网络有 49,281 个参数,32×32 的有 196,737 个:模型小了 4 倍,准确率反而更高。输入的设计不只影响准确率,也决定了模型能不能部署。 第 11 讲会把部署作为项目全周期的一部分来讲。


3. 案例 2:学校门禁的人脸验证 · Face verification

代码:02_face_verification_triplet.py

3.1 问题

Fall 2017 课件的场景:学校想在食堂、体育馆、游泳池门口用人脸验证学生证。 学生刷卡后,门口的摄像头拍一张照片,系统判断”这是不是学生证上的那个人”。

  • 数据:每个学生一张照片,标着姓名(学生证照片);
  • 输入:门口拍的照片 + 学生证上的照片。课件选的分辨率是 (412, 412, 3),比白天 / 黑夜大得多。想想为什么:区分两个人要靠五官的细节,而区分白天黑夜只要看整体亮度;
  • 输出:y=1y = 1(是本人)或 y=0y = 0(不是)。

这种问题叫人脸验证(face verification):一对一,“是不是这个人”。另一个相关的问题叫人脸识别(face recognition / identification):一对多,“这是谁”(§3.6)。

3.2 最简单的办法:比像素

最直接的想法:把两张照片相减,算像素距离,小于某个阈值就算同一个人。课件立刻列出了它的问题:

  • 背景、光照不同;
  • 人会化妆、留胡子……
  • 学生证照片可能是几年前拍的。

代码 02 造了一个能把这些问题量化的玩具人脸世界。每个人有一个 6 维的”身份向量” zz(脸型、五官)。每张照片还有一个 6 维的”干扰向量” uu(光照、姿态、表情),拍每张照片时都重新抽。照片的 100 个”像素”由一个固定的随机非线性函数生成:x=Ctanh⁡(Az+Bu)+噪声x = C \tanh(Az + Bu) + \text{噪声}。模型只看得到 xx。

代码 02 演示 1 先做了一个方差分解:像素的总方差里,来自”是谁”的只占 33.3%,来自光照、姿态、表情的占 66.7%。 然后在 100 个新同学身上抽 2000 对”同一个人”和 2000 对”不同的人”,比较像素的平方距离:

平均中位数25% 分位75% 分位
同一个人142.3135.694.4180.7
不同的人197.8192.9149.7240.9

两种配对的距离分布大面积重叠:20.9% 的”同一个人”配对,比一半以上的”不同人”配对离得还远。 无论阈值放在哪里,都会把很多本人拒之门外,或者把很多别人放进来。用最好的阈值,验证准确率只有 0.668(瞎猜是 0.5)。

问题的本质是:像素距离衡量的是”两张图长得像不像”,而我们要的是”是不是同一个人”。 我们需要另一种描述照片的方式,它只保留身份,扔掉干扰。

3.3 想法:把脸编码成一个向量

课件的方案:用一个深层 CNN 把每张照片编码成一个向量(课件里是 128 维),然后比较编码之间的距离:

  学生证照片 ──→ [深层 CNN] ──→ f(x₁) = (0.931, 0.433, 0.331, …, 0.039)   ← 128 维,提前算好存进数据库
                                                  ↘
                                                    距离 d = ‖f(x₁) − f(x₂)‖²  ──→  d < 阈值 ? → y = 1
                                                  ↗
  门口拍的照片 ──→ [同一个 CNN] ──→ f(x₂) = (0.922, 0.343, 0.312, …, 0.024)

两张照片过的是同一个网络(共享参数)。这种结构叫 Siamese network(孪生网络),最早用于签名验证(Bromley et al., 1993)。我们希望 ff 满足:

同一个人: ∥f(x1)−f(x2)∥2 小;不同的人: ∥f(x1)−f(x2)∥2 大\text{同一个人:}\ \lVert f(x_1) - f(x_2) \rVert^2 \ \text{小}; \qquad \text{不同的人:}\ \lVert f(x_1) - f(x_2) \rVert^2 \ \text{大}

注意这里输出的设计变了:网络的直接输出不再是”0 / 1”,而是一个编码。“是不是同一个人”由编码之间的距离和一个阈值决定。这就是 §1.4 说的”编码本身就是产品”。

3.4 怎样训练编码器:triplet loss

我们没有”每张脸的正确编码应该是多少”这种标签,只知道”哪些照片是同一个人”。Kian 的问题是:怎样把”同一个人近、不同人远”写成一个损失函数?

数据:学生证照片每人只有一张,不够教会网络”怎样编码一张脸”。所以要用公开的人脸数据集:很多人,每人多张照片,在不同光照、姿态、年龄下拍摄。编码器在这些人身上学会的是一种通用的本领,以后用在学校的学生身上(§3.5)。

三元组(triplet):每次取三张照片:

  • A(anchor,锚):某个人的一张照片;
  • P(positive):同一个人的另一张照片;
  • N(negative):另一个人的照片。

我们想要 d(A,P)+α≤d(A,N)d(A, P) + \alpha \le d(A, N):负样本至少比正样本远一个间隔 α\alpha。把”违反了多少”当成损失:

L(A,P,N)=max⁡(∥f(A)−f(P)∥2−∥f(A)−f(N)∥2+α, 0)\mathcal{L}(A, P, N) = \max\Big(\lVert f(A) - f(P) \rVert^2 - \lVert f(A) - f(N) \rVert^2 + \alpha,\ 0\Big)

这就是 triplet loss(FaceNet,Schroff et al., 2015)。max⁡(⋅,0)\max(\cdot, 0) 的意思是:已经分开够了的三元组损失为 0,不再管它。

梯度是理解它在”推拉”什么的最好方式。记 a=f(A)a = f(A)、p=f(P)p = f(P)、n=f(N)n = f(N),当损失大于 0 时:

∂L∂a=2(n−p),∂L∂p=−2(a−p),∂L∂n=2(a−n)\frac{\partial \mathcal{L}}{\partial a} = 2(n - p), \qquad \frac{\partial \mathcal{L}}{\partial p} = -2(a - p), \qquad \frac{\partial \mathcal{L}}{\partial n} = 2(a - n)

梯度下降沿着负梯度走:pp 被拉向 aa,nn 被推离 aa,aa 往”离 pp 近、离 nn 远”的方向挪。这三个梯度再通过反向传播传回编码器的参数(代码 02 的 encoder_backward)。

两个容易忽略的细节,都和”网络会钻空子”有关:

  1. 为什么要 margin α\alpha? 如果 α=0\alpha = 0,网络只要做到”负样本比正样本远一点点”就满足了。更糟的是,把所有编码都变成同一个点,d(A,P)=d(A,N)=0d(A, P) = d(A, N) = 0,损失也是 0。这是一个零损失的”作弊解”:梯度下降不一定会走到那里,但损失函数里没有任何东西阻止它。
  2. 为什么要把编码归一化到单位长度? FaceNet 把每个编码都除以它的长度,放在单位球面上。如果不这样做,网络可以把所有编码一起放大 cc 倍,距离就放大 c2c^2 倍,固定的 α\alpha 相对来说越来越小。损失就能靠”放大”而不是”分得更开”降下来(练习 5)。

代码 02 演示 2 训练了一个小编码器:100 → 64(ReLU)→ 16 → 归一化。训练数据是 300 个人、每人 8 张,用随机三元组,margin α=0.2\alpha = 0.2(FaceNet 用的值)。训练前先做梯度检验,相对误差是 6.55e-11,说明手写的反向传播是对的。然后在训练时从没见过的 100 个新同学上测试(每人 6 张):

表示训练人群验证新同学验证新同学 one-shot 识别不同人 / 同一人距离
像素距离(100 维)0.6960.6680.1061.39
随机初始化的编码器(训练前)0.6320.6030.0801.27
triplet loss 训练后的编码器0.9860.9650.80010.07
上限:真实身份向量 zz1.0001.0001.000∞

(验证的阈值在训练人群上选好,原封不动地用到新同学身上。one-shot 识别:每个新同学的第 1 张照片当”学生证照片”,其余照片去库里找最近的。100 个人里瞎猜的准确率是 1%。)

  • 验证从 0.668 提到 0.965,识别从 0.106 提到 0.800。每个新同学只有一张注册照片,也没有重新训练任何东西。
  • 对照组很重要:随机初始化的编码器(0.603 / 0.080)比像素还差一点。随机的非线性变换不会自己”扔掉光照”,是 triplet loss 教会了它哪些变化该忽略。
  • 编码器在训练人群上(0.986)比新同学(0.965)好一点,但差距不大。说明它学到的是”怎样从照片里读出身份”这种通用的本领,而不是记住了 300 个人。

随机三元组很快就”太容易”了。 训练记录显示:第 0 步时一批里有 47.7% 的三元组有梯度,第 500 步时是 0.0%。随便抽的”另一个人”通常本来就离得很远,损失为 0,什么也学不到。FaceNet 因此提出 semi-hard 负样本挖掘:用当前的编码器算一下距离,专门挑”比正样本远、但还没远出 margin”的负样本。代码 02 演示 3 比较了它和损失函数的几种改法:

损失函数 / 三元组新同学验证识别 top-1编码的分散程度训练结束时仍有梯度
完整的 triplet loss(α=0.2\alpha = 0.2,随机三元组)0.9650.8000.2411.1%
同上,但用 semi-hard 负样本0.9610.8440.2445.4%
去掉 margin(α=0\alpha = 0)0.9520.6520.0920.6%
只拉近同一个人:L=∥f(A)−f(P)∥2\mathcal{L} = \lVert f(A) - f(P) \rVert^20.5670.0320.003100.0%
  • semi-hard 挖掘让更多三元组提供梯度(5.4% 对 1.1%),识别从 0.800 升到 0.844,验证基本不变。在这个只有 300 人的小世界里差别不大。FaceNet 的训练集有约 800 万个不同的人,论文强调在那样的规模下,怎样挑三元组对收敛至关重要。
  • 去掉 margin:验证几乎不变,但 one-shot 识别从 0.800 掉到 0.652,编码也挤在一起(分散程度 0.092)。识别要在 100 个人里挑最近的那个,人和人之间需要留出余量。
  • 只拉近、不推开:网络找到了一个”作弊”的解,把所有照片都映射到几乎同一个点。损失几乎为 0,编码的分散程度塌缩到 0.003,验证 0.567、识别 0.032,和瞎猜差不多。这叫表示坍缩(representation collapse)。

最后一行是整讲最重要的一个例子:损失函数定义了任务。 你写下的每一项都在告诉网络”什么是好”。少了”推开”那一项,“所有人都一样”就成了完美答案,梯度下降一定会找到它。

3.5 为什么不把它当成”每个学生一类”的分类问题?

一个自然的想法:学校有 2000 个学生,就训练一个 2000 类的 softmax 分类器,看门口的照片属于哪一类。为什么不这样做?

  1. 每类只有一两张照片:学生证照片每人一张,训练一个 2000 类的分类器远远不够(这叫 one-shot learning 问题)。
  2. 类别会变:每年有新生入学、老生毕业。分类器的输出层是”每个学生一个单元”,每来一个新生,就要改网络结构、重新训练。编码的方案只需要把新生的照片过一遍编码器、存下一个向量。代码 02 里 100 个新同学就是这样”入学”的。
  3. 我们真正想要的是”距离”:验证问的是”这两张照片是不是同一个人”,不管这个人是谁。编码 + 距离直接回答这个问题。

不过有一个细节要说清楚:训练编码器时,用”分类”其实是可以的,而且很常用。在公开人脸库的几万个训练用的人上训练一个分类器(比如 ArcFace,Deng et al., 2019,就是在分类损失上加了角度间隔),然后拿掉最后的分类层,用倒数第二层当编码。部署时永远是”编码 + 距离”,因为部署时遇到的人不在训练的类别里。triplet loss 和分类式的损失,是训练编码器的两条路线。

3.6 延伸:识别、聚类,以及无处不在的编码

同一个编码器 ff,后面接不同的”小算法”,就是不同的产品(课件里的三个场景):

  • 验证(verification):d(f(x1),f(x2))<d(f(x_1), f(x_2)) < 阈值?
  • 识别(identification):门口没有刷卡,只拍了一张照片,“这是谁?“在数据库里找编码最近的人,也就是 K 近邻(K-nearest neighbors)。
  • 聚类(clustering):手机相册里有几千张照片,没有任何标签,想把同一个人的照片归到一起。对所有编码做 K-means。

代码 02 演示 4 把 10 个新同学的 60 张照片用 k-means 分成 10 簇:

表示纯度(每簇按多数人算对)
像素0.583
triplet 编码0.983
上限:真实身份向量1.000

用像素聚类,簇是按”光照、姿态相似”聚起来的;用编码聚类,几乎完美地按人分组。没有任何新标签,也没有重新训练。课件在这一页还加了一句很实际的提醒:“也许我们需要先检测出人脸?“真实的照片里人脸只占一小块,真实系统是一条流水线:先用检测器把脸找出来、裁剪、对齐(第 14 讲),再送进编码器。

“学一个编码,让相似的东西靠近、不同的东西远离”后来成了深度学习最通用的套路之一。它的名字叫度量学习(metric learning)或对比学习(contrastive learning):

  • SimCLR(Chen et al., 2020)把同一张图的两种随机变换当作”正样本对”,不需要任何标签就能学到好的图像编码;
  • CLIP(Radford et al., 2021)把图片和描述它的文字当作正样本对,学出图片和文字共享的编码空间;
  • 句子、商品、用户、歌曲……今天的检索和推荐系统几乎都建立在编码上(§6.2)。

这些方法都要面对 §3.4 里的那个敌人:坍缩。需要负样本(或者别的机制)来防止”所有东西都一样”。

一个不能省略的提醒:人脸识别是最容易被滥用的技术之一。它涉及隐私和知情同意;而且已有研究发现,商业人脸分析系统在不同肤色、性别的人群上错误率差别很大(如 Buolamwini & Gebru, 2018,研究的是性别分类)。如果你的项目用到人脸,“数据从哪里来、征得同意了吗、在不同人群上表现一样吗”本身就属于五问里的”数据”那一问。

3.7 五问回顾

问题人脸验证的答案
数据公开人脸库(很多人、每人多张)训练编码器;学生证照片只用来”注册”
输入一张人脸(课件:412×412×3;实际系统先检测、裁剪、对齐)
输出一个编码向量;“是不是同一个人”由编码的距离 + 阈值决定
架构深层 CNN(第 12–14 讲),两张照片共享同一个编码器
损失triplet loss(margin + 推开负样本 + 归一化),或分类式损失(ArcFace)

4. 案例 3:艺术风格迁移 · Neural style transfer

代码:03_optimize_the_input.py

4.1 问题:没有标签,只有两张图

任务:给一张内容图(比如你拍的一张照片)和一张风格图(比如梵高的《星月夜》),生成一张新图:内容是你的照片,“画法”是梵高的。这是 Gatys、Ecker 和 Bethge 在 2015 年提出的方法(A Neural Algorithm of Artistic Style)。

先用五问试试:

  • 数据? 课件的回答很有意思:“随便什么数据都行”。我们没有”这张照片画成梵高风格应该长什么样”的标签,也不需要。
  • 输入?输出? 输入是内容图 CC 和风格图 SS,输出是生成图 GG。
  • 架构? 我们需要一个”很懂图像”的网络,于是拿一个在 ImageNet 上训练好的分类网络来用(Gatys 用的是 VGG-19)。图像在这个网络里前向传播时,查看各层的激活值,就能得到它的内容和风格信息。
  • 损失? 这是整个方法的核心。

4.2 什么是”内容”,什么是”风格”?

内容:某一层的激活值 a[l](x)a^{[l]}(x),记录的是”图里哪里有什么”。较深的层描述的是物体和布局,不太关心精确的像素值。

风格:同一层激活值的 Gram 矩阵。设第 ll 层有 nCn_C 个通道,每个通道是一张 nH×nWn_H \times n_W 的特征图:

Gkk′[l](x)=1nHnW∑i=1nH∑j=1nWaijk[l](x) aijk′[l](x)G^{[l]}_{kk'}(x) = \frac{1}{n_H n_W} \sum_{i=1}^{n_H} \sum_{j=1}^{n_W} a^{[l]}_{ijk}(x)\, a^{[l]}_{ijk'}(x)

第 (k,k′)(k, k') 个元素衡量的是:第 kk 种图案和第 k′k' 种图案,在同一个位置同时出现的平均程度。比如”短的斜线”和”橙色”总是一起出现,这就是一种笔触。关键在于,对所有位置 (i,j)(i, j) 求和之后,“在哪里”的信息被扔掉了,只剩下”有哪些图案、哪些图案经常一起出现”。这正是我们直觉里的”风格”或”纹理”。

代码 03 演示 1 验证了这一点。代码里没有 VGG,“预训练网络”是一个固定的、随机初始化的小卷积层:8 个 3×3 滤波器加 ReLU。研究表明,随机滤波器的特征统计已经能抓住不少纹理信息(Ustyuzhaninov et al., 2016)。内容表示取这一层特征的 4×4 平均池化,用来代替”更深的一层”。

比较内容表示的相对距离风格表示(Gram)的相对距离
房子 vs 右移 6 像素的房子0.9600.028
房子 vs 斜条纹0.8790.317

同一座房子平移 6 个像素:内容表示几乎完全变了(“哪里有什么”变了),风格表示只变了 0.028。房子和条纹:两种表示都差得很远。

4.3 损失函数,以及那个反转

有了两种表示,损失就很自然了:

J(G)=∥a[l](G)−a[l](C)∥2⏟Jcontent: 内容像内容图  +  w⋅∑l′∥G[l′](G)−G[l′](S)∥2⏟Jstyle: 风格像风格图J(G) = \underbrace{\big\lVert a^{[l]}(G) - a^{[l]}(C) \big\rVert^2}_{J_{content}:\ \text{内容像内容图}} \; + \; w \cdot \underbrace{\sum_{l'} \big\lVert G^{[l']}(G) - G^{[l']}(S) \big\rVert^2}_{J_{style}:\ \text{风格像风格图}}

(注意有两个 GG:G[l′](⋅)G^{[l']}(\cdot) 是 Gram 矩阵,括号里的 GG 是生成图。Coursera 作业也是这样写的,读公式时别混。Gatys 的原文写成 αJcontent+βJstyle\alpha J_{content} + \beta J_{style},并在多个层上算风格损失。各种实现的归一化常数不同,Coursera 作业里的也不一样,但结构都是这样。)

然后是这个案例最重要的一点。课件上的原话是:“We are not learning parameters by minimizing L. We are learning an image!”

训练一个网络风格迁移
固定不动的数据网络参数(预训练好的)
用梯度下降改的参数 θ\theta生成图的像素 GG
更新公式θ←θ−η ∂J/∂θ\theta \leftarrow \theta - \eta\, \partial J / \partial \thetaG←G−η ∂J/∂GG \leftarrow G - \eta\, \partial J / \partial G

梯度下降并不关心它在改什么。给它一个变量和”损失对这个变量的梯度”,它就去改那个变量。反向传播也一样:平时它停在第一层的 ∂J/∂W[1]\partial J / \partial W^{[1]},这里多走一步,走到 ∂J/∂G\partial J / \partial G(对输入像素的梯度)。

4.4 实验:从噪声画出一座带条纹的房子

代码 03 演示 2 从一张灰色噪声图出发,用 Adam 改像素 800 步。每一步之后把像素裁剪回 [0, 1](像素必须是合法的亮度)。对像素的梯度先做了数值检验,最大相对误差 4.12e-08。

损失函数JcontentJ_{content}:开始 → 结束JstyleJ_{style}:开始 → 结束
只用内容损失1.1682 → 0.00010.4185 → 0.5312
只用风格损失1.1682 → 1.23370.4185 → 0.0039
内容 + 风格(w=1w = 1)1.1682 → 0.05600.4185 → 0.1617
内容 + 风格(w=10w = 10)1.1682 → 0.43610.4185 → 0.0364
内容 + 风格(w=100w = 100)1.1682 → 1.08920.4185 → 0.0047

风格迁移:内容图、风格图和五种损失下的生成图

(这张图由 03_optimize_the_input.py 生成;终端里也会打印同样内容的字符画。)

  • 只用内容损失:从噪声重建出了房子(边缘毛糙,因为内容表示是 6×6 的粗粒度)。
  • 只用风格损失:满屏的斜条纹,但没有房子。Gram 矩阵不记录位置,只要求”斜线图案的统计”对得上。条纹在哪里断开、在哪里拐弯,取决于初始噪声。
  • 两者都用:权重 ww 决定了”像房子”和”像条纹”之间的取舍。w=1w = 1 时基本是房子;w=10w = 10 时房子的轮廓还在,到处都长出了条纹;w=100w = 100 时几乎只剩条纹。没有”正确”的权重,损失函数里的权重就是你对”想要什么”的定义。(老实说:ww 的这三个值,是我在草稿里看到 w=1w = 1 几乎看不出条纹之后才选的,见代码 README。)
  • 程序最后检查了:8 个滤波器在 5 次优化前后完全相同。

Gatys 的方法每生成一张图都要跑几百上千步优化(课件上写的是”2000 次迭代之后”),很慢。后来 Johnson、Alahi 和 Fei-Fei(2016)把问题又”翻”了回去:训练一个网络,输入内容图、一次前向传播直接输出风格化的图,损失用的仍然是上面的 JJ(他们叫 perceptual loss)。这样每种风格要训练一次,但之后生成一张图只要一次前向传播。“优化输入”还是”训练参数”,本身就是一个设计选择。

4.5 同一招的其他用法:可视化与对抗样本

“固定参数、对输入求梯度、改输入”还有两个重要的用法。代码 03 演示 3 先训练一个白天 / 黑夜分类器:8×8 输入,网络 [192, 16, 1],验证准确率 0.844。然后:

(a) 网络心中的”夜晚”长什么样? 从一张全灰的图出发,改像素让网络输出”夜晚”的 logit 最大(再反过来做一次”白天”)。

  • “最像夜晚”的图,天空亮度 0.31;“最像白天”的图,天空亮度 0.89。“天空暗不暗”正是这个世界里最强的线索。
  • 但两张图都满是亮暗相间的斑点(亮度标准差 0.34 和 0.36),“最像白天”的天空还是黄白色的,不像任何一张真实的照片。网络却给出了接近 0 和 1 的概率。分类器只学会了”往哪边推概率”,没有学会”真实的照片长什么样”。这种可视化方法叫特征可视化(feature visualization)。实际使用时要加正则项让图保持”自然”(第 21 讲)。

(b) 对抗样本:取 473 张被正确判为白天的验证图,每个像素沿梯度的符号方向改 ε\varepsilon(x′=x+ε⋅sign(∂z/∂x)x' = x + \varepsilon \cdot \text{sign}(\partial z / \partial x)):

ε\varepsilon(像素值 0–1)约等于 0–255 里的被翻成”夜晚”的比例
0.012.633.0%
0.025.162.8%
0.0512.895.6%

每个像素只改约 5 个灰度级,肉眼几乎看不出来,就有 62.8% 的白天照片被判成夜晚。这就是对抗样本(adversarial examples;Szegedy et al., 2014;Goodfellow et al., 2015)。第 18 讲会解释它为什么存在、怎样防御。

风格迁移、可视化、对抗样本,三者的数学是同一个:固定参数,对输入求梯度,改输入。区别只在于损失函数写的是什么。又一次,损失函数定义了任务。

4.6 五问回顾

问题风格迁移的答案
数据不需要标注数据:一张内容图、一张风格图,加一个预训练好的网络
输入 / 输出内容图 + 风格图 → 生成图。被优化的”变量”是生成图本身
架构预训练 CNN(VGG),参数固定,只用来提取特征
损失内容损失(激活值之差)+ ww × 风格损失(Gram 矩阵之差)

5. 案例 4:触发词检测 · Trigger word detection

代码:04_trigger_word_labels.py

5.1 问题

“Hey Siri”、“OK Google”、“Alexa”:设备一直在听,听到某个触发词(trigger word / wake word)就醒来。Coursera C5 的编程作业用的触发词是 “activate”。

这个任务有几个特点,会直接影响五问的答案:

  • 一直在运行:必须小、快、省电,通常在设备上运行(§2.6 的取舍又来了);
  • 两种错误的代价不同:说了”activate”没反应(漏检)很烦人;没说却醒了(误唤醒,false alarm)更烦人,而且可能涉及隐私;
  • 正样本极少:一天 24 小时的声音里,触发词只占几秒钟。

5.2 输入和输出:为什么输出是一个序列

输入:一段 10 秒的音频,转成频谱图(spectrogram):每个时间片上各个频率的能量。在 Coursera 作业里,它是 Tx=5511T_x = 5511 个时间步 × 101 个频率。

输出:一个直觉的设计是”这 10 秒里有没有 activate”,一个 0 / 1。但设备需要知道什么时候响应。所以作业的设计是每个输出时间步一个 0 / 1:y⟨1⟩,…,y⟨Ty⟩y^{\langle 1 \rangle}, \dots, y^{\langle T_y \rangle},Ty=1375T_y = 1375。在线运行时,网络每处理一小段音频,就输出一个”刚才是不是说完了 activate”的概率。

损失:每个时间步一个二元交叉熵,对所有时间步取平均(或求和):

J=1Ty∑t=1Ty−[y⟨t⟩log⁡y^⟨t⟩+(1−y⟨t⟩)log⁡(1−y^⟨t⟩)]J = \frac{1}{T_y} \sum_{t=1}^{T_y} -\Big[ y^{\langle t \rangle} \log \hat y^{\langle t \rangle} + (1 - y^{\langle t \rangle}) \log (1 - \hat y^{\langle t \rangle}) \Big]

架构:作业用的是一维卷积加两层 GRU(一种 RNN),第 15–16 讲会讲。代码 04 用了一个简单得多的替代品:在每个时间步看过去 16 帧 × 8 个频带,送进一层隐藏层的网络 [128, 32, 1],所有时间步共享参数。这相当于最简单的一维卷积。“标签怎么标”的道理是一样的。

5.3 数据:录一点,合成很多

带逐帧标签的音频从哪里来? 请人一段段地听,标出”activate 在第几毫秒结束”,又慢又贵。Coursera 作业的做法是合成:

  1. 录一些”activate”(来自不同的人);
  2. 录一些别的词(负样本),特别是听起来有点像的;
  3. 录一些背景噪声(咖啡馆、街道、家里),每段 10 秒;
  4. 随机把几个正样本词和负样本词叠加到背景上,位置互不重叠。

因为词是我们自己放进去的,每个词在哪一帧结束是精确知道的,标签免费且精确。想要多少段都可以。

代码 04 的玩具音频世界是这样的:一段 10 秒 = 100 帧,每帧 8 个频带。activate 有 7 帧,另有 12 种别的词,其中 3 种的前 3 帧和 activate 一模一样(像 “active”、“actually” 这种开头相同的词)。每次”录音”都有说话人差异、语速变化、音量变化、渐入渐出。背景噪声和词的能量差不多大。训练用 30 个 activate 录音和 48 个别的词,测试用另一批说话人。

代码 04 演示 1 把一段合成音频画了出来(A = activate,n = 别的词):

  能量        :.==.::..:.-.-=:::::+-.-.:-.-----=:=:-- .-:=--==:=+*:-+:= .:+::::-: .-::::-....:*@@+:-:---.=:-=:+::=
  放了什么词  ................................................AAAAAAAA........................nnnn................
  标签 k = 1  ........................................................1...........................................
  标签 k = 5  ........................................................11111.......................................
  标签 k = 10 ........................................................1111111111..................................

合成 800 段(2.2 小时)训练音频,里面一共有 1234 个 activate、80,000 个带标签的时间步。所有这些都来自 30 + 48 个录音。

5.4 标签:只标 1 个,还是标 k 个?

上面那张图里有三种标签方式。最”准确”的似乎是 k=1k = 1:只在 activate 结束后的那一个时间步标 1,毕竟”刚说完”只发生在一个时刻。Coursera 作业却在结束后连续标 50 个 1(共 1375 个输出时间步)。为什么?

先看正样本比例(代码 04 演示 1):

标签方式正样本比例“永远输出 0”的逐帧准确率
结束后 k=1k = 1 个时间步标 11.54%98.46%
结束后 k=5k = 5 个7.52%92.48%
结束后 k=10k = 10 个14.57%85.43%

k=1k = 1 时,一个”永远输出 0”、什么都没学会的模型,逐帧准确率有 98.46%。这是第一个警报:在这种任务上,准确率几乎没有意义。

代码 04 演示 2 用三种标签分别训练(800 段训练音频),在 1 小时(360 段)新说话人的音频上测试:

kk逐帧准确率“永远输出 0”预测为 1 的比例precisionrecall词后最大概率(平均)
199.12%98.60%1.07%0.7450.5680.555
597.10%93.14%6.21%0.8190.7410.897
1094.91%86.71%12.05%0.8400.7620.916

如果只看逐帧准确率,k=1k = 1 最好(99.12%)。但它只比”永远输出 0”高 0.5 个百分点。而且 kk 不同,“永远输出 0”的基准也不同,三个准确率根本没法直接比。正样本很少时,要看 precision(预测为 1 的里面有多少真是 1)和 recall(真的 1 里有多少被找到了)。这是第 10 讲的内容。

k=1k = 1 的模型对很多 activate 没反应:每个 activate 之后,它给出的最大概率平均只有 0.555(k=5k = 5 时 0.897)。为什么?我原以为原因是”词的结尾是模糊的,概率摊在相邻几帧上”。但代码打印的”结束前后每一帧的平均概率”并不支持这个猜测:

kk−1+0+1+2+3+4+5+6+7+8
10.050.030.550.070.010.000.000.000.000.00
50.070.130.630.740.800.750.680.120.020.00

k=1k = 1 的平均概率集中在 +1 那一帧,相邻帧都很低。真正的情况是两极分化:57% 的 activate 最大概率 > 0.5,29% 的 < 0.2。模型学会了一个很”窄”的检测器,只在”这个词恰好在 1 帧之前结束”时响应,换了说话人(长度、音量不同)就常常对不上。同一个模型在训练用的说话人上有 76% 的 activate 能过 0.5,到了新说话人只剩 57%;k=5k = 5 的模型两边都是 94%。

k=5k = 5 好在哪里?每个词贡献 5 个正样本(训练集一共 6,014 个,k=1k = 1 只有 1,234 个),而且是 5 个不同的对齐位置。模型学到的是”最近说过这个词”这种更宽容的模式,对新说话人也稳得多。

产品真正在乎的指标:说了 activate 之后 1.5 秒内有没有反应(唤醒召回率),以及每小时误唤醒几次:

kk阈值 0.5:召回率误唤醒 / 小时误唤醒 ≤ 10 / 小时的阈值(验证集上选)召回率误唤醒 / 小时(测试)
10.612630.820.42829
50.9411900.960.66920
100.9582480.980.57014
  • 阈值 0.5 下的比较会误导人:k=5k = 5 召回率高很多,误唤醒也多了 3 倍,看起来像一个取舍。但 kk 大的模型整体输出更高的概率,同一个阈值对它们意味着不同的东西。公平的比较是固定误唤醒次数:在验证集上把阈值调到”每小时误唤醒 ≤ 10 次”,k=1k = 1 的召回率只有 0.428,而且误唤醒(29 次)比 k=5k = 5(20 次)还多。k=5k = 5 全面胜出。
  • kk 也不是越大越好:模型只看 16 帧,k=10k = 10 时,对较长的录音,结束后第 8–10 帧的窗口里已经装不下整个词了。kk 越大,“反应”也可能来得越晚。k=10k = 10 召回率低一些(0.570),误唤醒也少一些(14),大致在同一条权衡曲线上。重要的是别只标 1 个。
  • 测试集上的误唤醒都超过了预算:在验证集上定的是 10 次,到测试集变成了 29、20、14 次。阈值是在”训练用的说话人”上调的,换成新的说话人就不一定满足了。这是第 10 讲”数据不匹配”的一个小例子。
  • 这个玩具模型的误唤醒仍然很多,真实产品要求远低于每小时 1 次。真实系统用更大的模型、多得多的数据,还会专门收集难的负样本。

5.5 数据越多越好,而且几乎免费

代码 04 演示 3 只改合成的训练段数(“召回率 @ ≤10” 是”误唤醒 ≤ 10 次 / 小时”时的召回率):

训练段数小时kk预测为 1 的比例召回率 @ 0.5召回率 @ ≤10
250.0710.45%0.2590.168
250.0754.36%0.7960.244
1000.2810.72%0.4060.261
1000.2855.17%0.8670.519
4001.1155.99%0.9310.638
8002.2211.07%0.6120.428
8002.2256.21%0.9410.669
16004.4411.06%0.6060.461
16004.4456.14%0.9450.699
  • 数据少时,“只标 1 个”的问题最严重:25 段、k=1k = 1 的模型只在 0.45% 的时间步上输出 1(真实约 1.5%),阈值 0.5 下的召回率只有 0.259,明显偏向”永远输出 0”。同样 25 段,k=5k = 5 的召回率是 0.796。
  • 合成的数据越多越好:k=5k = 5 时,从 25 段到 1600 段,召回率(误唤醒 ≤ 10 次)从 0.244 涨到 0.699。所有训练音频都来自同一批 30 个 activate 录音:同一个词被放到不同的背景、不同的位置,模型就学会了”不管背景是什么,认的是这个词”。不过 800 → 1600 段已经涨得很慢了,再合成也造不出新的说话人。
  • 标签策略比多收一倍数据更值钱:在 800 段时,把 k=1k = 1 换成 k=5k = 5,召回率涨 0.242;k=1k = 1 时把数据从 800 段翻倍到 1600 段,只涨 0.034。

5.6 “专家的建议能省下几个月”

据 2018 年录像的提纲,Kian 讲这个案例时强调过一个观点:有经验的人的一句建议,可能省下你几个月的摸索。(本讲义没有逐字核对录像,这里只转述大意。)放到这个案例里,这样的建议就是”在词后标一串 1”、“用合成造数据”、“测试集要用真实录音”。代码 04 把这个观点变成了数字:一个标签上的改动,效果超过把数据翻倍。

这也是为什么第 10、11 讲会花大量篇幅讲”怎样决定下一步做什么”。在深度学习项目里,最贵的资源是时间。知道”该在哪里使劲”,比”使更大的劲”重要。

5.7 五问回顾

问题触发词检测的答案
数据合成:正样本词 + 负样本词 + 背景噪声,随机叠加,标签自动生成。测试最好用真实录音
输入10 秒音频的频谱图(作业里是 5511 × 101)
输出每个时间步一个 0 / 1(作业里 Ty=1375T_y = 1375),触发词结束后连续标 k 个 1(作业里 50 个)
架构一维卷积 + GRU(第 15–16 讲),必须能在设备上实时运行
损失每个时间步的二元交叉熵求平均;评估看唤醒召回率和每小时误唤醒,而不是逐帧准确率

6. 贯穿始终的”编码”,以及今天的五问 · Encodings everywhere

6.1 四个案例里的编码

案例编码是什么它的作用
白天 / 黑夜隐藏层的 64 个激活值把”某处有小亮点”这种位置不固定的线索变成可以线性读出的特征(逻辑回归做不到)
人脸验证f(x)f(x),单位长度的 16 维(FaceNet 128 维)向量编码本身就是产品:验证、识别、聚类都只是”编码 + 距离”
风格迁移预训练网络各层的激活值,以及它们的 Gram 矩阵用一个”很懂图像”的网络的编码,来定义什么叫内容、什么叫风格
触发词网络对”最近 1.6 秒声音”的隐藏层表示把一段有噪声、语速不定的声音,变成”刚才说没说 activate”

一个好的编码有两个特点。第一,它保留了和任务有关的信息,扔掉了无关的变化(光照、背景、说话人)。第二,它常常可以复用:同一个人脸编码器用于验证、识别、聚类;同一个 ImageNet 网络的编码可以用于风格迁移,也可以用于迁移学习(第 13 讲)。

6.2 补充:用五问设计一个 embedding 检索系统

本小节是本讲义的补充,不是原课内容。Fall 2025 的这一讲标题是 “Key AI Concepts Through Case Studies”,可能加入了更新的案例,但具体内容不公开,这里不做猜测。

假设你要给一个在线课程平台做”用一句话搜索课程笔记”的功能。用户输入”为什么深层网络的梯度会消失”,系统要找出最相关的几段笔记,哪怕笔记里没有出现”梯度会消失”这几个字。

问题设计和本讲的哪个案例相似
数据(查询, 相关段落) 配对:可以来自用户的点击日志,也可以请人标几百对做评估;还可以用大模型为每段笔记生成可能的问题(合成数据)触发词:合成标签
输入一段文字(查询或笔记段落),切成 token
输出一个编码向量;检索 = 在所有笔记的编码里找离查询最近的 KK 个人脸识别:K 近邻
架构一个预训练的文本编码器(Transformer,第 17 讲)在这些配对上微调;查询和段落可以共用一个编码器,也可以用两个(“双塔”,two-tower)人脸:Siamese 网络;风格迁移:用预训练网络
损失对比损失:一个查询和它的相关段落要近,和同一批里其他查询的段落要远(in-batch negatives,Karpukhin et al., 2020 的 DPR 就是这样做的),本质上和 triplet loss 是一家人脸:triplet loss;防止坍缩
评估recall@K:相关段落有没有出现在前 KK 个结果里。不要用”编码之间的平均距离”这类和产品无关的数触发词:看产品指标,不看逐帧准确率

你会发现,五问里几乎每一个答案,都能在本讲的四个老案例里找到”原型”。这就是这一讲想教的直觉:工具在变(CNN → Transformer,triplet → in-batch 对比学习),但”数据从哪来、输出是什么、损失是不是在衡量你在乎的东西”这些问题没有变。

6.3 一张随身清单

拿到一个新问题,在写代码之前问自己:

  1. 数据:从哪来?标签怎么得到,能不能合成?有没有”碰巧和标签相关”的偏差?测试数据和真实使用时的数据一样吗?
  2. 输入:人在多小的分辨率 / 多短的片段下还能完成任务?更大的输入需要更多的数据。
  3. 输出:真正的问题是”有哪些”、“哪一个”、“是不是同一个”、还是”什么时候”?输出是一个数、一个编码、还是一个序列?
  4. 架构:输入有什么结构(局部性、顺序)?有没有现成的预训练网络可以用?要在哪里运行?
  5. 损失:它有没有”作弊”的解(坍缩、永远输出 0)?它的权重是不是反映了你的偏好?
  6. 评估(第六问):用什么数字判断好坏?这个数字和产品真正在乎的东西一致吗?

7. 小结 · Summary

  • 模型 = 架构 + 参数。参数由梯度下降去找,你能设计的是数据、输入、输出、架构、损失。Kian 的五问就是把这五样逐一想清楚。
  • 改输出就是改任务:三个 sigmoid(“有哪些”)和一个 softmax(“哪一个”)用的是同样的网络,回答的是不同的问题。
  • 白天 / 黑夜:用”缩小了人还能不能分辨”估计分辨率。人类代理在 16×16 → 8×8 之间跌下悬崖(0.901 → 0.754),全分辨率时接近上限 0.95(黄昏只能猜,这就是 Bayes error)。模型方面有两点:逻辑回归在所有分辨率上都停在 0.735–0.750,因为它够不着位置不固定的线索;网络在 16×16 最好(0.924),32×32 过拟合(0.869)。分辨率、数据量和模型要一起决定,直觉给起点,实验做决定。
  • 人脸验证:像素距离不行,像素方差里身份只占 33.3%。改为学一个编码 f(x)f(x),比较编码之间的距离。用 triplet loss 训练:max⁡(∥f(A)−f(P)∥2−∥f(A)−f(N)∥2+α,0)\max(\lVert f(A) - f(P) \rVert^2 - \lVert f(A) - f(N) \rVert^2 + \alpha, 0)。在新同学上,验证从 0.668 提到 0.965,one-shot 识别从 0.106 提到 0.800,而且不需要重新训练。去掉”推开”那一项,编码会坍缩(识别 0.032)。部署时不当分类问题做;训练编码器时可以用 triplet,也可以用分类式损失。
  • 风格迁移:内容 = 激活值,风格 = Gram 矩阵(平移 6 像素只变 0.028)。固定参数、优化像素;损失里的权重就是你对”想要什么”的定义。同一招还能用来做特征可视化和构造对抗样本(每像素改 0.02 就翻转 62.8% 的预测)。
  • 触发词检测:输出是一个序列。数据靠合成,标签免费且精确。“只标 1 个”时,模型的反应两极分化,换了说话人就不稳。逐帧准确率会骗人(“永远输出 0”就有 98.6%)。要在固定误唤醒次数下比较召回率:k=5k = 5 是 0.669,k=1k = 1 是 0.428。改标签的收益比把数据翻倍还大。
  • 编码 / embedding 贯穿所有案例,也是今天检索、推荐、多模态系统的核心。损失函数定义了任务:网络会认真地学会你写下的东西,包括你没想到的捷径。

8. 常见误区 · Common pitfalls

  1. “输入分辨率越高越好,信息更多。” 信息多了,参数和需要的数据也多了。代码 01 里 32×32 的网络(0.869)不如 16×16(0.924),因为同样 8000 张图喂不饱 196,737 个参数。
  2. “人在这个分辨率下看不出来,模型一定也不行。” 人类代理是保守的参考。8×8 的网络认出了 91.3% 的城市夜景,人类代理一张都没认出来。反过来,人能看出来,模型也不一定行:逻辑回归在任何分辨率下认城市夜景的准确率都只有 0.34–0.66,接近瞎猜。
  3. “准确率 99% 就是好模型。” 正样本只有 1.5% 时,“永远输出 0”就有 98.5%。要看 precision / recall,或者产品指标(召回率、每小时误唤醒),并且在相同的误唤醒次数下比较(第 10 讲)。
  4. “人脸识别就是一个类别很多的分类问题。” 部署时类别会变、每类只有一两张照片,所以用”编码 + 距离”。(训练编码器时用分类式损失是可以的,§3.5。)
  5. “损失降到 0 就说明学好了。” “只拉近同一个人”的损失几乎为 0,模型却把所有人映射到同一个点。先问自己:这个损失有没有作弊的解?
  6. “训练就是改参数。” 风格迁移、特征可视化、对抗样本改的都是输入。梯度下降只需要”一个变量 + 损失对它的梯度”。
  7. “标签就是事实,越精确越好。” 触发词”只在结束的那一帧标 1”最”精确”,却让模型学得最差。标签是你和模型之间的约定,要设计成模型能学、而且对产品有用的样子。
  8. “合成数据是’假’数据,不值得用。” 合成数据给了触发词检测精确的标签和几乎无限的数量(25 → 1600 段,召回率 0.244 → 0.699)。要注意的是它和真实数据的差距:开发集 / 测试集最好用真实数据(第 10 讲)。

9. 练习 · Exercises

练习 1(五问练习)。一家停车场想用安装在灯杆上的摄像头判断每个车位是否有车,结果显示在手机 App 上。用五问设计一个方案:数据从哪来、标签怎么得到?输入是整张图还是每个车位裁出来的小图?多大?输出是什么?用什么架构、什么损失?有没有你预见到的偏差?

参考答案

(开放题,下面是一种合理的设计。)

  • 数据:从现有的摄像头录像里每隔几分钟截一帧,覆盖白天 / 夜晚、晴天 / 雨雪、不同季节。标签可以半自动得到:先人工标少量帧,再利用”车停着时画面在几分钟内几乎不变”这一点,把一个标签传播到相邻的帧。偏差:只在晴天白天采集,模型到了夜晚和雨天就不行(§2.2 的”城市夜景”问题)。
  • 输入:每个车位位置固定,可以按事先画好的框把车位裁出来,每个车位一张小图。用 §2.3 的办法定分辨率:缩小到几种尺寸,看人还能不能分辨”有车 / 没车”。这类问题通常几十像素见方就够了,远小于整张图。
  • 输出:每个车位一个 sigmoid(0 / 1)。如果一次输入整张图,就是”每个车位一个 sigmoid”,即 §1.3 的多标签。
  • 架构:小的 CNN(第 12 讲),或在预训练网络上做迁移学习(第 13 讲)。如果要在摄像头端运行,就要小模型。
  • 损失:二元交叉熵。评估:每个车位的准确率之外,还要看”App 显示有空位但其实没有”的比例,那才是用户最在乎的。

练习 2(数参数和计算量)。输入是 64×64×3 的图。(a) 逻辑回归有多少个参数?(b) 网络 [nx,64,1][n_x, 64, 1] 有多少个参数?每张图的前向传播大约要做多少次乘加?(c) 把输入换成 16×16×3,(a)(b) 的数字分别是多少?参数个数缩小了几倍?(d) 人脸验证用的 412×412×3 输入,nxn_x 是多少?

参考答案

(a) nx=12,288n_x = 12{,}288,参数 12,288+1=12,28912{,}288 + 1 = 12{,}289。 (b) 12,288×64+64+64×1+1=786,56112{,}288 \times 64 + 64 + 64 \times 1 + 1 = 786{,}561 个参数;乘加次数约为权重个数,12,288×64+64=786,49612{,}288 \times 64 + 64 = 786{,}496 次。 (c) nx=768n_x = 768:逻辑回归 769 个参数;网络 768×64+64+64+1=49,281768 \times 64 + 64 + 64 + 1 = 49{,}281 个参数(和代码 01 的表一致),乘加 49,216 次。边长缩小 4 倍,nxn_x 和参数都缩小约 42=164^2 = 16 倍。 (d) 412×412×3=509,232412 \times 412 \times 3 = 509{,}232。如果用全连接网络,第一层就有几千万个参数,这是第 12 讲用卷积的动机之一。

练习 3(多标签 vs softmax)。一个网络对 (猫, 狗, 长颈鹿) 的输出层 logits 是 z=(2.0,1.0,−1.0)z = (2.0, 1.0, -1.0)。(a) 分别用三个 sigmoid 和一个 softmax,算出三个输出。(b) 下面两个产品各该用哪种输出层?“给相册里的照片自动打标签(可以同时有好几个)”;“动物园的 App,拍一只动物告诉你它是什么”。(c) 如果把 softmax 用在 (b) 的第一个产品上,一张”猫和狗在一起”的照片会出什么问题?

参考答案

(a) sigmoid:σ(2)=0.881\sigma(2) = 0.881,σ(1)=0.731\sigma(1) = 0.731,σ(−1)=0.269\sigma(-1) = 0.269。softmax:e2=7.389e^2 = 7.389,e1=2.718e^1 = 2.718,e−1=0.368e^{-1} = 0.368,和为 10.475,所以是 (0.705,0.259,0.035)(0.705, 0.259, 0.035)。 (b) 相册标签:多标签,三个 sigmoid + 三个二元交叉熵。动物园:恰好一种,softmax + 多分类交叉熵。 (c) softmax 的输出加起来必须等于 1,“猫”和”狗”在争同一份概率。训练时标签是 (1, 1, 0) 也没法表示(one-hot 只能有一个 1)。模型会被迫在猫和狗之间犹豫,比如各给 0.5 左右,用阈值 0.5 去判断时可能两个都判不出来。

练习 4(手算 triplet loss 和梯度)。三个已经归一化的二维编码:a=(1,0)a = (1, 0),p=(0.8,0.6)p = (0.8, 0.6),n=(0.6,0.8)n = (0.6, 0.8)。(a) 算出 d(a,p)d(a, p)、d(a,n)d(a, n)。(b) α=0.2\alpha = 0.2 时损失是多少?这个三元组还会提供梯度吗?(c) α=0.5\alpha = 0.5 时呢?算出 ∂L/∂a\partial \mathcal{L} / \partial a、∂L/∂p\partial \mathcal{L} / \partial p、∂L/∂n\partial \mathcal{L} / \partial n,并说明梯度下降会怎样移动这三个点。

参考答案

(a) d(a,p)=0.22+0.62=0.40d(a, p) = 0.2^2 + 0.6^2 = 0.40,d(a,n)=0.42+0.82=0.80d(a, n) = 0.4^2 + 0.8^2 = 0.80。 (b) max⁡(0.40−0.80+0.2,0)=0\max(0.40 - 0.80 + 0.2, 0) = 0。负样本已经比正样本远了 0.4,超过了 margin 0.2,这个三元组”太容易”,没有梯度。这正是代码 02 里随机三元组在第 500 步就 0% 有梯度的原因。 (c) 0.40−0.80+0.5=0.1>00.40 - 0.80 + 0.5 = 0.1 > 0,损失 0.1。梯度:∂L/∂a=2(n−p)=(−0.4,0.4)\partial \mathcal{L}/\partial a = 2(n - p) = (-0.4, 0.4);∂L/∂p=−2(a−p)=(−0.4,1.2)\partial \mathcal{L}/\partial p = -2(a - p) = (-0.4, 1.2);∂L/∂n=2(a−n)=(0.8,−1.6)\partial \mathcal{L}/\partial n = 2(a - n) = (0.8, -1.6)。梯度下降沿负梯度走:pp 往 (0.4,−1.2)(0.4, -1.2) 方向移,也就是朝 aa 靠近;nn 往 (−0.8,1.6)(-0.8, 1.6) 方向移,也就是远离 aa;aa 往 (0.4,−0.4)(0.4, -0.4) 方向移。(在单位球面上,归一化的反向传播还会去掉沿着编码自身方向的分量,见代码 02 的 encoder_backward。)可以用数值梯度验证:∂L/∂a\partial \mathcal{L}/\partial a 的数值结果约为 (−0.400,0.400)(-0.400, 0.400)。

练习 5(推导:为什么要归一化)。假设编码没有归一化,某个三元组满足 d(a,p)<d(a,n)d(a, p) < d(a, n)。证明:把编码器的最后一层权重和偏置都乘以 c>1c > 1(所有编码都放大 cc 倍),这个三元组的损失会单调不增,并且 cc 足够大时变成 0。这说明了什么?为什么归一化能堵住这个漏洞?

参考答案

最后一层乘以 cc,所有编码都变成 c⋅f(x)c \cdot f(x),所以 dd 变成 c2dc^2 d。损失变成 max⁡(c2[d(a,p)−d(a,n)]+α,0)\max(c^2 [d(a,p) - d(a,n)] + \alpha, 0)。因为方括号里是负数,cc 越大,第一项越负,损失单调不增;当 c2≥α/[d(a,n)−d(a,p)]c^2 \ge \alpha / [d(a,n) - d(a,p)] 时损失为 0。 这说明网络可以不改变任何”谁和谁近”的关系,只靠把编码整体放大就降低损失,margin α\alpha 形同虚设。(对于 d(a,p)>d(a,n)d(a,p) > d(a,n) 的三元组,放大反而会让损失变大,所以实际训练中网络会在两者之间找一个尺度,但 margin 的含义已经被尺度”稀释”了。)归一化后所有编码都在单位球面上,平方距离被限制在 [0, 4],没法靠放大作弊,α=0.2\alpha = 0.2 才有固定的含义。

练习 6(Gram 矩阵为什么不管位置)。某一层有 2 个通道、3 个位置,激活值是 F=[102011]F = \begin{bmatrix} 1 & 0 & 2 \\ 0 & 1 & 1 \end{bmatrix}(每行一个通道,每列一个位置)。(a) 算出 Gram 矩阵 G=FF⊤/3G = F F^\top / 3。(b) 把三个位置的顺序打乱(比如列的顺序变成 3, 1, 2),GG 变了吗?证明对任意的列置换都成立。(c) 这和代码 03 演示 1 里”平移 6 像素只变 0.028”有什么关系?为什么不是正好 0?

参考答案

(a) FF⊤=[1+0+40+0+220+1+1]=[5222]F F^\top = \begin{bmatrix} 1 + 0 + 4 & 0 + 0 + 2 \\ 2 & 0 + 1 + 1 \end{bmatrix} = \begin{bmatrix} 5 & 2 \\ 2 & 2 \end{bmatrix},除以 3 得 [1.6670.6670.6670.667]\begin{bmatrix} 1.667 & 0.667 \\ 0.667 & 0.667 \end{bmatrix}。 (b) 不变。Gkk′=13∑jFkjFk′jG_{kk'} = \frac{1}{3}\sum_{j} F_{kj} F_{k'j} 是对所有位置 jj 求和,加法和顺序无关。用矩阵写:列置换是右乘一个置换矩阵 Π\Pi,(FΠ)(FΠ)⊤=FΠΠ⊤F⊤=FF⊤(F\Pi)(F\Pi)^\top = F \Pi \Pi^\top F^\top = F F^\top,因为置换矩阵满足 ΠΠ⊤=I\Pi \Pi^\top = I。 (c) 平移是一种(近似的)位置置换,所以 Gram 矩阵几乎不变。不是正好 0 的原因有两个:一是图像边缘补了 0(padding),房子移动后,靠近边缘的那些 3×3 小方块变了;二是 np.roll 是循环平移,平移本身也会改变边界附近的内容。

练习 7(触发词的标签和指标)。Coursera 作业的输出有 Ty=1375T_y = 1375 个时间步,每个 activate 结束后标 50 个 1。(a) 一段只含 1 个 activate 的音频,正样本比例是多少?如果只标 1 个呢?“永远输出 0”的逐帧准确率各是多少?(b) 除了”标一串 1”,另一个对付类别不平衡的办法是在损失里给正样本加权(比如正样本的交叉熵乘以 50)。这两种做法有什么不同?(提示:想想代码 04 里”不同的对齐位置”那一点。)(c) 为什么评估触发词检测时要在”相同的每小时误唤醒次数”下比较召回率?

参考答案

(a) 标 50 个:50/1375=3.6%50 / 1375 = 3.6\%,“永远输出 0”的准确率 96.4%。标 1 个:1/1375=0.07%1 / 1375 = 0.07\%,“永远输出 0”的准确率 99.93%。 (b) 加权只是让那一个正样本在损失里”更响”。模型看到的仍然只有”恰好在结束后 1 帧”这一种对齐位置,学到的检测器依然很窄(代码 04 里 k=1k = 1 换了说话人就对不上的问题)。标一串 1 则同时增加了正样本的个数(不只是权重),而且给了模型很多不同的对齐位置,让它学”最近说过这个词”这种更宽容的模式。另外,加权会改变输出概率的含义(整体偏高),阈值也要跟着重新调。实践中两种方法也可以一起用。 (c) 阈值是部署时可以调的旋钮:调低阈值,召回率升高、误唤醒也增多。两个模型在同一个阈值下的输出概率尺度可能完全不同(代码 04 里 kk 大的模型整体输出更高),直接比较”阈值 0.5 下的召回率”不公平。产品的约束通常是”误唤醒不能超过每小时 X 次”,所以应该在这个约束下比较谁的召回率高。这就是第 10 讲的”satisficing(满足型)指标 + optimizing(优化型)指标”。

练习 8(改代码:先预测,再运行)。(a) 把 daynight_world.py 里的 POINT_TAU = 0.10 改成 0.05(人类代理对亮点更敏感),然后运行 01_day_night_resolution.py。预测:32×32、16×16、8×8 的人类代理准确率会怎样变?“悬崖”会移动吗?(b) 把 02_face_verification_triplet.py 里的 NUISANCE_SCALE = 1.5 改成 3.0(光照、姿态的变化更大),预测像素距离和 triplet 编码器的验证、识别准确率会怎样变,然后运行验证。

参考答案

(下面是我运行的结果,改完后记得改回原值。) (a) 16×16 从 0.901 升到 0.937(城市夜景从 0.779 升到 0.968),因为凸起只剩 1/4 的路灯现在也能看出来了。但 32×32 反而从 0.943 降到 0.933:全分辨率下像素噪声偶尔也能比邻居亮 0.05,阴天的准确率从 1.000 降到 0.963,出现了”假亮点”。8×8 几乎不变(0.755,城市夜景 0.004),1/16 的凸起仍然看不出来。悬崖的位置不变,还是在 16 → 8 之间。阈值是在”漏掉真亮点”和”看到假亮点”之间做取舍,和第 10 讲的 precision / recall 是同一回事。 (b) 像素距离:验证 0.668 → 0.557,识别 0.106 → 0.028,几乎和瞎猜一样。编码器:验证 0.965 → 0.858,识别 0.800 → 0.360。两者都变差了,但编码器仍然远好于像素。干扰越强,“扔掉干扰”越难:要么需要更多的人、更多张照片,要么需要更大的编码器。

练习 9(改代码:模型的视野)。把 04_trigger_word_labels.py 里的 WINDOW = 16 改成 8(模型只看过去 0.8 秒;activate 的录音长 5–9 帧)。预测:k=1k = 1 和 k=5k = 5 哪个受的影响更大?为什么?然后运行,看演示 2 的”误唤醒 ≤ 10 / 小时”那一列。

参考答案

我运行的结果(800 段训练音频,误唤醒 ≤ 10 次 / 小时下的召回率):k=1k = 1 从 0.428 变成 0.416,几乎不变;k=5k = 5 从 0.669 降到 0.590,受影响更大。 原因:k=1k = 1 的标签在结束后第 1 帧,此时 8 帧的窗口刚好还能装下大部分词。k=5k = 5 的标签一直延续到结束后第 5 帧,这时 8 帧的窗口里只剩词的后半段甚至一点点尾巴,很多”1”已经无法从窗口里判断,变成了难学的标签。标签的设计要和模型能”看到”的范围匹配:标签说”现在应该响”,那么在这个时刻,输入里必须还有足以判断的信息。真实系统用 RNN / GRU 带着”记忆”往前走(第 15 讲),就是为了让模型能看到更长的过去。


10. 延伸阅读 · Further reading

课程材料

  • CS230 Autumn 2018 Lecture 2 “Deep Learning Intuition”(Kian Katanforoosh,YouTube,Stanford Online 频道;cs230.stanford.edu 的 lecture 页面有链接)。
  • CS230 Fall 2017 课件 “CS230: Lecture 3 – Various Deep Learning Topics”(Kian Katanforoosh、Andrew Ng;web.stanford.edu/class/cs230/fall2017/files/CS230_Handout4.pdf)。本讲的白天 / 黑夜、人脸验证、风格迁移就来自这里,课件还讲了目标检测和图像分割(第 14 讲)。
  • Coursera Deep Learning Specialization:C4 第 4 周 “Special Applications: Face Recognition & Neural Style Transfer”(及编程作业 Face Recognition、Art Generation with Neural Style Transfer);C5 第 3 周(Sequence Models & Attention Mechanism)里的 Speech Recognition、Trigger Word Detection 两个视频(及编程作业 Trigger Word Detection)。

人脸验证与度量学习

  • Bromley, Guyon, LeCun, Säckinger & Shah (1993). Signature verification using a “Siamese” time delay neural network. NeurIPS. ——Siamese 网络。
  • Chopra, Hadsell & LeCun (2005). Learning a similarity metric discriminatively, with application to face verification. CVPR.
  • Schroff, Kalenichenko & Philbin (2015). FaceNet: A unified embedding for face recognition and clustering. CVPR. ——triplet loss、semi-hard 挖掘、128 维归一化编码。
  • Deng, Guo, Xue & Zafeiriou (2019). ArcFace: Additive angular margin loss for deep face recognition. CVPR. ——分类式的编码训练。
  • Buolamwini & Gebru (2018). Gender Shades: Intersectional accuracy disparities in commercial gender classification. FAT*.

风格迁移与”优化输入”

  • Gatys, Ecker & Bethge (2015). A neural algorithm of artistic style. arXiv:1508.06576(CVPR 2016 版题为 Image style transfer using convolutional neural networks)。
  • Johnson, Alahi & Fei-Fei (2016). Perceptual losses for real-time style transfer and super-resolution. ECCV.
  • Ustyuzhaninov, Brendel, Gatys & Bethge (2016). Texture synthesis using shallow convolutional networks with random filters. arXiv:1606.00021.
  • Zeiler & Fergus (2014). Visualizing and understanding convolutional networks. ECCV.
  • Olah, Mordvintsev & Schubert (2017). Feature visualization. Distill.
  • Szegedy et al. (2014). Intriguing properties of neural networks. ICLR;Goodfellow, Shlens & Szegedy (2015). Explaining and harnessing adversarial examples. ICLR.
  • Nguyen, Yosinski & Clune (2015). Deep neural networks are easily fooled: High confidence predictions for unrecognizable images. CVPR.

触发词 / 关键词检测

  • Chen, Parada & Heigold (2014). Small-footprint keyword spotting using deep neural networks. ICASSP.
  • Sainath & Parada (2015). Convolutional neural networks for small-footprint keyword spotting. Interspeech.
  • Apple Siri Team (2017). Hey Siri: An on-device DNN-powered voice trigger for Apple’s personal assistant. Apple Machine Learning Research.

对比学习与 embedding 检索(§3.6、§6.2)

  • Chen, Kornblith, Norouzi & Hinton (2020). A simple framework for contrastive learning of visual representations (SimCLR). ICML.
  • Radford et al. (2021). Learning transferable visual models from natural language supervision (CLIP). ICML.
  • Karpukhin et al. (2020). Dense passage retrieval for open-domain question answering. EMNLP.

11. 下一讲预告 · Next lecture

第 6 讲:实践要点 I——数据划分、偏差与方差、正则化。 这一讲里好几次出现了同一个现象,只是没有展开:

  • 32×32 的网络训练准确率 0.966、测试只有 0.869(§2.4);
  • 人脸编码器在训练人群上比在新同学上好(§3.4);
  • 阈值在验证集上选好,到了测试集上误唤醒超标了(§5.4)。

第 6 讲会把这些现象系统化:

  • 训练 / 开发 / 测试集怎样划分,为什么开发集和测试集必须来自同一个分布;
  • 用训练误差和开发误差诊断 high bias(欠拟合)和 high variance(过拟合),以及各自该怎么办;
  • 正则化:L2(weight decay)、dropout、数据增强、early stopping。它们怎样让 32×32 那样”参数比数据多”的网络不再过拟合。

到第 6 讲结束时,你会知道代码 01 里 32×32 那一行”该怎么救”,也会明白第 1 讲 tiny_nn.train 里的 weight_decay 和 keep_best 两个参数在做什么。

← 返回课程目录