Lecture 11 · 深度学习项目的完整周期:从想法到部署 · Full Cycle of a Deep Learning Project

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

课程源码 ZIP ↓

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

本篇目录

对应 Maps to:Stanford CS230 Fall 2025 Lecture 3(10/7,“Full Cycle of a DL Project”);Autumn 2018 Lecture 3 “Full-Cycle Deep Learning Projects”(Andrew Ng)、Lecture 5 “AI + Healthcare”(Pranav Rajpurkar)、Lecture 6 “Deep Learning Project Strategy”(Andrew Ng)(均为 YouTube 公开录像);Fall 2025 Section 1(Getting Started with Your Project)、Section 3(Implementation First Steps: Codebases, Baselines)、Section 7(Debugging DL Projects)(公开网页);CS230 博客 “Tips for the Final Project”。 依据说明:Fall 2025 的课堂 slides 和录像只对选课学生开放,本讲没有用到。2018 年的录像是公开的,但本讲义没有逐字核对录像:§1 里 Ng 的”语音激活设备”主线是按这一讲的内容提纲写的,细节和措辞可能和录像有出入,凡是”Ng 认为……”的地方都只是转述大意。Section 1、3、7 依据公开网页(https://cs230.stanford.edu/section/1 、/section/3 、/section/7 )。§9 的医疗案例依据原始论文(CheXNet、Hannun et al. 2019、MURA、Zech et al. 2018),文中的数字已对照论文。调试清单的几项参考了 Karpathy 的公开博客,部署与监控部分依据公开的工程资料(Google Rules of ML、Sculley et al. 2015 等,见 §14)。配套代码全部用合成数据。 配套代码 Code:code/lecture11/(PyTorch;5 个脚本 + 3 个模块;一个贯穿全讲的”工厂质检”迷你项目,加一个合成的”类胸片”医疗世界;每个脚本 5–25 秒) 预计阅读时间:3 小时(含运行代码)


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

0.1 学习目标 Learning goals

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

  1. 说出一个深度学习项目从选题到维护的七个步骤,以及每一步最常见的失败方式;知道它们是一个循环,而不是一条直线。
  2. 评估一个选题的可行性(数据拿不拿得到、要多少、有没有人类水平可以参照、值不值得做),并在动手之前写下”成功是什么”:一个优化指标加几个满足指标。
  3. 用实验数字解释**“先快后精”**:为什么应该先花一两天采一个小数据集、尽快走完一整圈;会用学习曲线边采边估计”还要采多少”。
  4. 快速搭出第一个系统:先复现现成代码,按”多数类 → 规则 → 线性模型 → 网络”的阶梯建 baseline,先在小数据上过拟合。
  5. 按 Section 7 的四类错误(语法 / 逻辑 / 算法 / 实验)排查问题,熟练使用一份调试清单:看数据、初始损失 ≈ ln C、在一个 batch 上过拟合、打乱标签测试、学习率扫描、检查训练模式;知道每一项能抓住什么、抓不住什么。
  6. 把模型变成系统:选阈值(它是产品决策)、加前置过滤器、权衡端侧与云端;上线后区分数据漂移和概念漂移,知道哪些能用不需要标签的统计量(PSI、KS、预测分布)发现,哪些只能靠人工抽检;比较几种重训策略。
  7. 读懂医疗 AI 论文里的”专家水平”结论:专家标签的分歧、参考标准怎样定、置信区间、跨医院泛化与捷径学习。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉、后面再展开 Previewed only
第 5 讲:五问框架;触发词检测的标签设计与数据合成(§5.3–5.4);服务器 vs 手机(§2.6)卷积网络(第 12–13 讲):本讲的迷你项目只用全连接网络,CNN 会做得更好
第 6 讲:train / dev / test、bias 与 variance、basic recipe序列模型做语音唤醒的细节(第 15–16 讲)
第 7 讲:输入归一化、初始化;第 9 讲:torch_utils 训练模板、softmax、model.eval() 的坑(§4.7)校准(calibration)、主动学习、联邦学习等(§7、§8、§9 只点名)
第 10 讲的方法:单一数字指标、satisficing / optimizing、误差分析与天花板、错误标签、数据不匹配大模型时代的项目流程(第 20 讲)

第 10 讲讲的是 ML 策略的方法本身;这一讲把这些方法放回一个完整的项目里用,并补上方法之外的部分:选题、采数据、搭第一个系统、调试、部署、监控。

0.3 本讲地图 Map

§1  一个项目的一生:七个步骤;Ng 的语音台灯;本讲的迷你项目(工厂质检)
§2  选题与可行性;先写下"成功是什么"
§3  数据:先快后精;两支队伍的 60 天;第 3 天的诊断;学习曲线          ← 代码 01 演示 1–4
§4  第一个系统:复现现成代码;baseline 阶梯;先过拟合小数据              ← 代码 01 演示 5
§5  调试:四类错误;检查清单;注入 9 个 bug                            ← 代码 02
§6  迭代:误差分析告诉你下一批数据去哪里采;实验管理                    ← 代码 03
§7  从模型到系统:阈值、前置过滤器、端侧 vs 云端、延迟与吞吐            ← 代码 04 演示 1–2
§8  部署之后:数据漂移 vs 概念漂移;监控面板;什么时候重训              ← 代码 04 演示 3–4
§9  AI + 医疗:专家标签、"超过医生"怎么读、换一家医院                  ← 代码 05
§10 你的 CS230 项目
§11–15  小结、常见误区、练习、延伸阅读、下一讲预告

0.4 记号 Notation(沿用第 1 讲 §0.4,本讲新增)

符号含义出现在
CC类别数;随机初始化时交叉熵约为 ln⁡C\ln C§5
召回率 recall、误报率 false alarm rate缺陷被拦下的比例;正常品被误拦的比例(= 1 − 特异度)§3、§7、§8
τ\tau阈值:P(缺陷)>τP(\text{缺陷}) > \tau 就拦下§7
cmiss,cFAc_{\text{miss}}, c_{\text{FA}}漏检一个、误报一个的代价;本讲假设 cmiss=10 cFAc_{\text{miss}} = 10\,c_{\text{FA}}§7
P(x)P(x)、P(y∣x)P(y \mid x)输入的分布;给定输入时标签的分布。前者变了叫数据漂移,后者变了叫概念漂移§8
PSI、KS 的 DD两个衡量”分布挪了多少”的统计量§8.2
κ\kappaCohen’s kappa,两个标注者”超出偶然”的一致程度§9
F1、AUC精确率与召回率的调和平均;ROC 曲线下面积§9

0.5 关于数字 · About the numbers

讲义里所有实验数字都来自配套代码的实际输出。PyTorch 在 CPU 上单线程运行、每个演示用自己的随机种子,同一台机器上重复运行逐字相同(只有 04_deploy_and_monitor.py --timing 打印的计时每次不同)。两个世界都是合成的:工厂质检世界(factory_world.py)和”类胸片”世界(clinic_world.py,不是真实医学影像)。世界里的参数(噪声、缺陷对比度、灰尘和水渍的强度、两个工位的亮度差……)是我先在草稿里试了几组之后选的,目的是让”规则 baseline 在小数据时领先、网络在大数据时追上”这样的现象落在几千张图的范围里;这些选择都写在代码开头。有很多结果和我写代码前的预想不一样,讲义按实际结果写,并列在代码 README 的”关于实验结果”里。


1. 一个项目的一生 · The life of a project

1.1 Ng 的例子:一个听得懂一句话的设备

2018 年这一讲,Andrew Ng 用一个语音激活的设备贯穿全程:比如一盏台灯,你对它说一句唤醒词,它就打开。这个例子小到一个学生项目就能做,又完整到包含了真实产品会遇到的几乎所有问题。(以下按这一讲的提纲转述,细节可能和录像有出入。)

先想一想:如果让你从零做这盏台灯,第一周你会做什么?很多人的第一反应是”先读几篇论文,选一个最好的模型”,或者”先采一个大数据集”。Ng 的建议恰恰相反:先用一两天采一个小数据集,尽快做出一个能跑的、粗糙的系统(quick and dirty),再靠误差分析决定往哪里投入。 理由很简单:你事先不知道哪里会出问题。也许是麦克风离得太远,也许是某种口音,也许是电视的声音总把它叫醒。这些问题只有系统真的跑起来、放到真实环境里才会暴露。

1.2 七个步骤

步骤要决定什么台灯的例子最常见的失败方式本讲
1 选题做什么、值不值得、做不做得到一句唤醒词点亮台灯题目太大;数据根本拿不到§2
2 获取数据从哪来、要多少、谁来标、怎么标找人录唤醒词、别的词、背景噪声,合成训练音频(第 5 讲 §5.3)花几个月采数据,采法本身有缺陷§3
3 设计模型输入输出、架构、损失频谱图 → 每个时间步一个 0/1(第 5 讲 §5.2)一开始就追求最复杂的模型§4
4 训练让它学起来先在小数据上调通流水线有 bug 却以为”模型就这水平”§5
5 测试在什么数据上、用什么指标在真实房间里试,数误唤醒和漏唤醒dev 集和真实使用的数据不一样§3、§6
6 部署跑在哪里、多快、阈值多少设备上常开一个便宜的语音活动检测(VAD),只把有声音的片段送进网络只看模型,不看系统(延迟、功耗、阈值)§7
7 维护上线后世界变了怎么办新的口音、新的背景噪声(换了一台空调)不监控,等用户投诉才知道§8

1.3 不是一条直线

这张表看起来是按顺序走的,实际上是一个循环:测试时发现数据有问题,就回到第 2 步;部署后发现新的噪声,就回到第 2、4 步。第 1 讲 §4.5 的”Idea → Code → Experiment”是这个大循环里的小循环。

两个习惯能让这个循环转得快:

  • 每一步都尽早做一个”够用”的版本,让整个循环先转一圈,再回头加强最弱的一环;
  • 每一步都写下”出口标准”(exit criteria):做到什么程度就进入下一步。比如”dev 上召回率 ≥ 0.9、误报率 ≤ 5% 就开始试点”。没有出口标准的步骤,很容易一直做下去。

1.4 本讲的迷你项目:工厂质检

语音的例子需要序列模型(第 15–16 讲),而第 5 讲已经详细讲过触发词的标签设计。所以本讲的代码换一个主题,把七个步骤真的走一遍:

一家工厂想用摄像头自动检查金属零件的表面。相机拍 16×16 的灰度小图,模型判断:正常、划痕(一条亮的细线)、还是污点(一个暗斑)。

这个世界(factory_world.py)有几个刻意放进去的”真实感”:金属表面有横向的拉丝纹理;缺陷可以出现在中间 9×9 区域的任何位置;光线暗时缺陷更淡;正常品上也会有灰尘(单个亮点)和水渍(一大片很淡的明暗变化),它们无害,但长得像缺陷。产线上 90% 的零件是正常的。代码 01 演示 1 打印了每类一张(以每张图的中位数为中心 ±0.12 映射到字符):

正常               划痕               污点
+=++*=*+++=++=:+   =-++==++-+++==*+   +#++=+*++==-++*+
++*-=++==*-+=*+*   ++#+-+-+++==:===   +*****=++=. ::++
==--+*++=*++++**   ++-=++===-+*=++-   =+#*+=++-     -+
=+===++*+-==+===   *-====+++@#+-=*#   *==++*++.     .+
+=++=@+-++=====+   ++@+++*=++@@*===   #*#*=###-     :+
=++++*++*++==++-   ==+--=+++=-*@%+=   +=%+*+++-     :+

(只截了前 6 行。)正常品上那个孤立的 @ 是一粒灰尘;划痕是一串斜着的 @;污点是一片空白(暗)。


2. 选题与可行性 · Choosing a project

2.1 三个问题:值不值得、做不做得到、数据从哪来

选题时最常见的两种失败:题目大到做不完(“用深度学习诊断所有疾病”),或者做完了才发现数据根本拿不到。动手之前问三个问题:

  1. 价值:做成了,谁会用?比现在的做法好多少?(产线上人工目检很累、会漏,这就是价值。)
  2. 技术可行性:现在的技术做得到吗?一个粗略的经验来自 Ng 的一篇短文(Harvard Business Review, 2016):一个普通人想一秒钟就能做出的判断,往往是监督学习可以自动化的。人看一眼能分出划痕和灰尘,这就是个好兆头;要靠十年经验和整本病历才能下的判断,就难得多。
  3. 数据:能不能拿到?要多少?谁来标?标注员之间会不会意见不一?涉及隐私和许可吗?(医疗数据尤其如此,§9。)

还有一个常被忽略的问题:有没有一个参照?人类水平、现有的规则系统、竞争对手的产品——有了参照,你才知道”0.9 的准确率”是好还是坏(第 10 讲:human-level performance 是 Bayes error 的代理)。

可行性检查工厂质检语音台灯
一个人一秒钟能判断吗能(看一眼)能(听一句)
数据能自己采吗能:产线每天几千个零件,质检员会标能:找人录,再合成
每天能采多少本讲假设 150 张(拍照 + 标注)一天能录几十个人
参照人工目检;一条手写规则(§4.2)现有的唤醒词产品
两种错误的代价漏检(流到客户)≫ 误报(人工复检)误唤醒(烦人、隐私)vs 漏唤醒

2.2 CS230 的项目:四类

CS230 Section 1(公开网页)把往届成功的项目分成四类:把流行的架构用到一个新任务上;为任务设计定制的架构;复现一篇著名论文;研究型项目(研究网络的某种行为)。不管哪一类,都要有自己的东西(新的数据、新的问题、新的分析)。同一页的数据建议也值得照抄:不要在采数据上花太多时间;可以自己采、爬、合成,也可以用现成的数据集;“几万个样本”是很多任务的一个粗略目标(医学影像常常少得多),任务越复杂、模型越大,需要的越多;数据少时考虑多采、数据增强、迁移学习;注意许可和隐私。

2.3 先写下”成功是什么”

在采第一张图之前,就要写下怎样判断项目成功了。第 10 讲讲过两个工具:单一数字的评估指标(一个数,能比较两个方案谁更好),以及 optimizing vs satisficing(优化一个指标,其余的只要满足门槛)。放到工厂里:

  • 优化指标:缺陷召回率(漏检的零件会流到客户手里);
  • 满足指标:误报率 ≤ 5%(复检的人手有限);每个零件的推理时间 ≤ 产线节拍允许的时间。

注意这里用的不是准确率。产线上 90% 是正常品,一个**永远说”正常”**的系统准确率就有 0.90,却一个缺陷都拦不住(代码 01 演示 5 会看到这个数)。指标选错了,后面所有的迭代都在优化错误的东西。


3. 数据:先快后精 · Get data fast, then iterate

3.1 为什么要快

“先采一个大数据集再说”听起来稳妥,实际上是把所有风险都押在了”采法没有问题”上。数据的问题往往只有训练之后才暴露:采的环境和真实使用的不一样、标注标准不一致、某类样本几乎没有、数据里藏着一条捷径……你越晚发现,浪费的数据越多。

所以 Ng 建议:先花一两天采一个小数据集,马上训练,马上放到真实环境里试。这不是说小数据就够了,而是说先用小数据找到方向,再放心地大量投入。

3.2 实验:两支队伍的 60 天

代码 01 演示 2 把这个论证变成了数字。两支队伍做同一个项目,每天都能采并标 150 张图。他们的采法一样,而且有同一个问题:正常品在产线工位上拍,缺陷品是从废品箱里挑出来、拿到质检实验室拍的——那里有现成的台子和灯,拍起来方便。标签全部正确,图也清楚。

  • 快队:第 1–2 天采 300 张 → 第 3 天训练,并在产线上随手拿 500 个零件试一试(试点)→ 发现问题 → 第 4 天起改在产线工位上采。
  • 慢队:第 1–40 天采 6000 张 → 第 41 天第一次训练 + 试点 → 发现同一个问题 → 第 42 天起改在产线工位上采。

我们(写讲义的人)有一个两队都看不到的”产线评估集”:4000 个零件,按产线的真实比例(90% 正常)。每格是缺陷召回率 / 误报率:

第几天快队手里的训练数据召回 / 误报慢队手里的训练数据召回 / 误报
3300 张(实验室拍缺陷)0.073 / 0.030在采:已有 450 张—
101050 张(产线工位)0.825 / 0.270在采:已有 1500 张—
202550 张(产线工位)0.924 / 0.130在采:已有 3000 张—
304050 张(产线工位)0.935 / 0.085在采:已有 4500 张—
415700 张(产线工位)0.948 / 0.0706000 张(实验室拍缺陷)0.110 / 0.016
507050 张(产线工位)0.948 / 0.0601350 张(产线工位)0.843 / 0.264
608550 张(产线工位)0.958 / 0.0732850 张(产线工位)0.919 / 0.110

几件事:

  • 数据多了 20 倍,问题一点没少:300 张时召回 0.073,6000 张时 0.110。问题出在”怎么采”,不在”采多少”。
  • 快队浪费了 2 天的数据,慢队浪费了 40 天。到第 60 天,快队召回 0.958、误报 0.073,慢队 0.919 / 0.110:先快后精的好处会一直保持下去。
  • 快队第 10 天的模型也不好(误报 0.270)。误报普遍偏高,一部分原因是这里直接用”三类里概率最大的那一类”做决定,而训练数据里缺陷占 2/3、产线上只占 10%——§7.4 会讲怎样选阈值。

3.3 第 3 天发生了什么

快队是怎样在第 3 天找到问题的?代码 01 演示 3 复盘了这一天。快队把 300 张图按 80/20 分成训练集和 dev 集:

同一个模型(240 张训练)准确率(三类)缺陷召回率误报率
自己的 dev(60 张)0.8170.9760.056
产线试点(500 个零件)0.8660.0800.047

dev 上几乎所有缺陷都抓到了,试点上 50 个缺陷只拦下 4 个。先停下来想:为什么 dev 没有报警? 因为 dev 和训练集来自同一种采法,带着同一个偏差。dev 只能告诉你”在和训练一样的数据上”好不好(第 6 讲 §1.4:dev 要来自你将来面对的分布)。另外,试点上三类准确率 0.866 看着还行,那是因为 90% 的零件本来就是正常的。

接下来是一个小小的错误分析:漏掉的缺陷有什么共同点?把每类图的平均亮度列出来:

正常划痕污点
采来的 300 张0.4970.6140.594
产线试点 500 个0.5030.5060.486

采来的缺陷图比正常图亮了约 0.11:实验室的灯更亮。产线上三类一样亮。两个检查坐实了这个猜测:

  • “笨模型”检查:只用一个特征(平均亮度)、一个阈值(亮度 > 0.547 就判缺陷),在自己的 dev 上”是否缺陷”就判对了 0.933,在试点上召回率 0.100。当一个”不该有用”的特征就能解决大半问题时,几乎可以肯定数据里有捷径(shortcut)。
  • 反事实检查:同样 300 个缺陷零件,缺陷的位置、形状、对比度完全一样,只换拍摄的灯。模型给出的平均 P(缺陷)P(\text{缺陷}):实验室拍 0.979,产线工位拍 0.082。模型学会的是”亮 = 有缺陷”。

这一切在演示 1 里本来就能看到,只要”看数据”时顺手打印了按类别分组的简单统计量。所以”先看数据”不只是看几张图,还要看每类的均值、方差、来源、采集时间。这个”实验室的灯”是虚构的,但同类的事在医疗影像里真实发生过(§9.4)。

3.4 要采多少?边采边估计

小数据集还有一个用处:画学习曲线,估计”要采多少张才够”。代码 01 演示 4 在产线工位的数据上训练了 7 个大小(dev 是 3000 张、三类均衡):

训练张数1503006001200240048009600
采集天数1248163264
dev 错误率0.3980.3890.3720.2700.1820.1310.081

第 1 讲 §4.6 说过,错误率随数据量常常近似按幂律下降:err(n)≈a n−b\text{err}(n) \approx a\, n^{-b},在双对数坐标里是一条直线。假装我们在项目进行中,每次用”手里已有的点”拟合一次,外推到 9600 张,并估计”错误率降到 0.10 要多少张”:

拟合用的点bb外推到 9600降到 0.10 需要
前 4 个(≤ 1200 张,8 天)0.1750.205576,374 张
前 5 个(≤ 2400 张,16 天)0.2780.14334,841 张
前 6 个(≤ 4800 张,32 天)0.3360.11915,973 张
实际0.0814800–9600 之间

学习曲线与三次外推

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

我原以为早期的点外推出来会”大致对”。实际上前 600 张几乎是平的(0.398 → 0.372):网络还没学会划痕和灰尘的形状差别,多给一点数据几乎没用,之后曲线才陡然下降。用这段”起步阶段”拟合的直线严重悲观,估计要 57 万张,差了几十倍。随着数据增加、重新拟合,估计从 57 万张降到 3.5 万张、再到 1.6 万张,一步步接近实际(4800 到 9600 张之间就降到了 0.10 以下)。

所以学习曲线的正确用法是边采边重新估计,而不是在第一周算一次就定下标注预算。早期估计哪怕错得离谱,也逼你在第一周就问:“照这个速度,这个项目值不值得做?要不要换方法?”

3.5 标注策略

数据的另一半是标签。几条经验:

  • 先写标注指南:什么算划痕?多淡的算?灰尘和污点怎么区分?指南不清楚,两个标注员会给同一张图不同的标签(§9.2 会看到专家之间也是这样)。最好先让两三个人各标同一批图,比一比,把分歧写进指南。
  • 能合成就合成:第 5 讲 §5.3 的触发词数据就是”录一点、合成很多”,标签免费而且精确。
  • dev / test 的标签要比训练集的更干净:训练集里一点随机噪声,大量样本能把它平均掉一部分(§9.2 有实验);dev / test 的标签决定你相信哪个结论,值得多花人力(多人标注、裁决)。
  • 把”难的样本”留下来:质检员复检时发现的误报和漏检,就是下一批最有价值的训练数据(§6)。

4. 第一个系统:baseline · Build your first system quickly

4.1 先复现现成的代码

CS230 Section 3(公开网页)给的第一条建议是:先找一份现成的实现,把它的结果复现出来;如果你的任务没有现成实现,就找一个已经被解决的”代理任务”,再换上自己的数据和标签。为什么?因为从零写一个训练流水线,大部分时间会花在调 bug 上(§5),而一份别人已经跑通的代码把这部分风险去掉了。

怎样判断一个 GitHub 仓库值不值得用?Section 3 列了一张清单:

看什么为什么
star 数、开源许可(license)用的人多,坑少;许可决定你能不能用在项目里
README 清楚、报告了结果你知道”复现成功”应该是什么数字
提供数据处理脚本和预训练权重省掉最容易出错的部分
框架和你熟悉的一致;代码可读你迟早要改它
有对应的论文 / 参考文献出问题时能对照原文

同一页还建议:先在数据的一个子集上、在本地或 Colab 上调通,再搬到云 GPU 上跑全量;用完记得关机器。本讲的代码也是这么做的:没有从零写训练循环,而是直接用第 9 讲的 torch_utils.py 模板。

4.2 baseline 的阶梯

baseline 是第一个简单的模型,它的作用是给后面所有的改进一个比较的起点。不要只建一个,建一个阶梯:从”什么都不做”开始,一级一级往上。代码 01 演示 5 用 20 天的数据(3000 张,产线工位):

方法参数个数dev 准确率缺陷召回误报率
多数类:永远判”正常”00.3250.0000.000
规则:行中位数残差(两个阈值 0.145 / 0.105)20.8840.9140.102
softmax 回归 [256, 3]7710.6600.7910.413
MLP [256, 128, 64, 3](第 9 讲模板,默认超参数)413470.8470.9500.105

(dev 三类均衡;后两列在产线评估集上算。)

  • 多数类:在均衡的 dev 上 0.325;放到产线评估集上准确率有 0.904,却漏掉所有缺陷。任何指标先和这个”什么都不做”的数比一比。
  • 规则是一个有经验的工程师会先写出来的东西:每个像素减去它所在行的中位数(去掉横向的拉丝纹理),最亮的点超过阈值就判”划痕”,最暗的点低于阈值就判”污点”。只有两个参数,dev 准确率 0.884,比 3000 张图训练出来的 MLP(0.847)还高。放大看它的错误:误报的 97 张正常品 100% 带灰尘或水渍;漏掉的缺陷平均对比度 0.131(全部缺陷 0.250)。它只看”最突出的那一点有多突出”,不懂形状。
  • 对照演示 4 的学习曲线,MLP 要到 4800 到 9600 张之间才超过规则。这就是第 1 讲代码 03 那张”规模”图的左半边:小数据时,领域知识(手工特征)领先。
  • softmax 回归只有 0.660:一个线性模板对付不了”缺陷可以出现在任何位置”(第 2 讲 §9.3)。

这张表是项目的第一个里程碑:你知道了下限、一个强 baseline、网络现在在哪。Google 的 Rules of Machine Learning 第一条是”不要害怕在没有机器学习的情况下上线”:规则可以先上线、开始收集真实数据,网络在数据够了之后再替换它,或者留下来当网络前面的过滤器(§7.3)。

4.3 先在小数据上过拟合

Section 3 的另一条建议:先在很小的一组样本上训练,确认模型能把它们背下来,再逐步加数据。 网络连 32 张图都背不下来,说明流水线有 bug,或者学习率之类的设置有问题,而不是”数据不够”。小数据上一次实验只要几秒,你可以很快地试架构和超参数。这一条是下一节检查清单里最重要的一项。


5. 调试 · Debugging

5.1 四类错误

CS230 Section 7(公开网页)把深度学习项目里的错误分成四类:

类别含义例子难度
1 语法 / 运行错误代码跑不起来少了冒号;张量形状对不上最容易:Python 会告诉你哪一行
2 逻辑错误能跑,但没有按你的意图做打乱数据时只打乱了图片;在训练集上算 dev 准确率最危险:不报错,结果”差一点”或”好得不真实”
3 算法错误代码照你写的做了,但方法不适合这个数据 / 任务用线性模型检测位置不固定的缺陷需要判断”是 bug 还是能力不够”
4 实验错误方法对,但超参数或设置错了学习率差了 100 倍靠扫描和对照实验

Section 7 的练习是一个预置了 8 个 bug 的图像分割代码库(UNet),要学生修到能训练。它给的技巧包括:直接打印各层的输出、权重和梯度;画出输入数据和权重的大小;用 profiler 找慢的地方;一次只改一处、看有没有帮助;每次改动之后从头运行一遍(notebook 里的”Restart & Run All”),免得漏掉某个单元格。

5.2 一份检查清单

把 Section 7、Section 3 和 Karpathy 的博客 A Recipe for Training Neural Networks(2019)里的建议合在一起,得到一份每一项都只要几秒钟的清单:

检查正常的样子为什么
A能跑吗前向传播不报错先让它能跑
B看数据:送进网络的 (x,y)(x, y) 对得上吗按标签分组后,各组的图明显不同(这里看”最亮点突出程度”:划痕组应该比正常组高)看的是进网络之前最后一步的数据,而不是原始文件
C初始损失随机初始化时 ≈ln⁡C\approx \ln C(3 类时 1.099)输出层的 logits 一开始应该很小,对各类”一视同仁”:−log⁡(1/C)=ln⁡C-\log(1/C) = \ln C
D在一个 batch 上过拟合32 张图训练几百步,损失降到接近 0网络有能力背下 32 张图;背不下来说明梯度、更新或损失有问题
E打乱标签测试用打乱的训练标签训练,汇报的 dev 准确率 ≈ 1/C1/C,而且明显低于正常标签模型确实在用”图片 ↔ 标签”的关系;能抓住”评估写错了”
F学习率扫描每个数量级(1e−5 … 1e−1)训练几个 epoch,当前学习率在最好的那一档附近实验错误里最常见的一种
G训练时在训练模式吗每一步 model.training 都是 True第 9 讲 §4.7 的反面:评估后忘了切回 train()

关于 E 有一个细节:网络有足够的容量时,连随机标签都能背下来(Zhang et al. 2017),所以打乱标签后训练准确率仍可能上升。要看的是 dev:打乱的标签和图片没有任何关系,dev 准确率不可能高于瞎猜。

清单之外还有几条通用的习惯:可视化预测(把模型最自信地判错的图拿出来看);逐步加复杂度(先小网络、小数据、不加正则化,跑通了再一样一样加);一次只改一处。

5.3 实验:往同一份代码里注入 9 个 bug

代码 02 写了一份”项目训练代码”(数据准备 → 模型 → 训练循环,训练循环完整写出来,不用 torch_utils.fit),里面有 9 个开关,每个开关注入一个真实项目里常见的 bug。正确的代码([256, 128, 64, 3] + dropout 0.2,Adam 0.001,15 个 epoch)七项全部通过:初始损失 1.111,32 张图 300 步后损失 0.0005,打乱标签后 dev 0.287,学习率扫描最好的一档恰好是 1e−3,最终 dev 准确率 0.828。然后逐个打开 bug(✗ = 这一项报警):

bug类别ABCDEFG真实 dev 准确率
忘了把 16×16 的图拉平1 语法/运行✗······报错
忘了归一化(直接喂 0–255)2 逻辑✓✓✗✗✗✓✓0.323
打乱时只打乱了图片、没打乱标签2 逻辑✓✗✓✓✗✓✓0.328
模型最后加了 softmax,再交给 CrossEntropyLoss2 逻辑✓✓✓✗✓✓✓0.751
在训练集上算”dev 准确率”2 逻辑✓✓✓✓✗✓✓0.828
评估后没有切回 model.train()2 逻辑✓✓✓✓✓✓✗0.833
用 softmax 回归(没有隐藏层)3 算法✓✓✓✓✓✓✓0.664
学习率大了 100 倍(0.1)4 实验✓✓✓✗✗✗✓0.330
学习率小了 100 倍(1e−5)4 实验✓✓✓✗✓✗✓0.449
(正确的代码)✓✓✓✓✓✓✓0.828

(· = 代码在第一步就报错了,后面的检查没法做。)

先盯着这张表想一想:哪一列最值钱?哪个 bug 最危险?

5.4 逐个看

  • 忘了拉平:报错信息直接说出了形状对不上(1024x16 and 256x128)。能报错的 bug 是好 bug。
  • 忘了归一化:初始损失 9.465,是 ln 3 的八倍多。一秒钟的 C 项就抓住了。不修的话:32 张图都背不下来(损失停在 0.7125),完整训练后训练集准确率只有 0.352。
  • 只打乱了图片:能跑、初始损失正常、32 张图也能背下来(随便什么标签都能背)。只有 B 和 E 报警:按标签分组后,“划痕”组和”正常”组的图一模一样(“最亮点突出程度”都是 2.42);正常标签(0.328)和打乱标签(0.341)一样差。症状很典型:训练集 0.691(网络在硬背),dev 0.328(瞎猜)。
  • softmax 两次:初始损失 1.100,C 项抓不住;但在一个 batch 上过拟合时损失停在 0.5517,降不下去。这就是第 9 讲 §3.8 的”地板”:3 类时是 ln⁡(1+2/e)≈0.551\ln(1 + 2/e) \approx 0.551。最终 dev 0.751,比正确代码低 0.077——低得不多,很像”模型还不够好”。
  • 在训练集上评估:汇报的 dev 准确率 0.967,比正确代码还好看。只有打乱标签测试抓住了它:标签打乱以后”dev 准确率”还有 0.695。打乱的标签和图片毫无关系,这个数只可能来自”网络背下了训练集”。好得不真实的结果,第一反应应该是怀疑。
  • 没切回 train():只有 G 项(训练循环里一行 assert model.training)能抓住。我原以为它会让结果明显变差;这一次它几乎没有代价(0.833 vs 0.828):dropout 在第 1 个 epoch 之后就被关掉了,训练集准确率升到 0.996,过拟合更重,但 dev 刚好没变差。bug 不一定让结果变差,这正是它难被发现的原因。
  • softmax 回归:七项全部通过,代码没有 bug。它只是能力不够:32 张图能背下来,整个训练集却只有 0.739。“能在一个 batch 上过拟合,却拟合不好整个训练集” 说明问题在方法(high bias,第 6 讲),该换模型,而不是找 bug。D 项的真正价值就在这里:它帮你分清第 2 类和第 3 类错误。
  • 学习率大 100 倍:网络彻底学不动(训练集 0.321),D、E、F 报警;小 100 倍:300 步不够把 32 张图背下来(D),F 的扫描同样指向 1e−3。

5.5 换一个架构,清单的灵敏度会变

代码 02 演示 4 把模型里的 dropout 换成 BatchNorm,再查三份代码:

代码(BatchNorm 版)C 初始损失D 一个 batchE 正常 / 打乱真实 dev(dropout 版)
正确的代码1.299 ✓0.0031 ✓0.843 / 0.370 ✓0.8430.828
忘了归一化1.313 ✓0.0031 ✓0.323 / 0.297 ✗0.3230.323
学习率大 100 倍1.299 ✓0.0000 ✓0.791 / 0.323 ✓0.7910.330
  • 有了 BN,忘了归一化的初始损失只有 1.313,C 项抓不住了:BN 会把第一层的输出重新标准化,输入是 0–1 还是 0–255,对初始 logits 几乎没影响。但网络照样学不好,只有 E 项还在报警。
  • 到底缺的是哪一半?一个对照实验:只减均值、仍然是 0–255 的尺度,BN 版 0.830、dropout 版 0.816;只除以 255、不减均值,BN 版 0.323、dropout 版 0.647。缺的是”减均值”:每个像素都在同一个大偏移附近,第一层的输出被这个共同的偏移主导,缺陷那一点点变化被淹没了。这是第 7 讲 §1.5”数据离原点远”的问题在真实流水线里的样子。
  • 学习率大 100 倍在 BN 网络里不再是灾难(0.791,dropout 版 0.330),和第 9 讲 §2.6”BN 让网络扛得住大学习率”一致。

所以清单里的每一项都是必要条件的检查,不是证明:通过了不代表没 bug,它的灵敏度取决于你的架构。几项互相印证,比依赖任何一项都可靠。排查的顺序就是四类错误的顺序:先让它能跑;再确认它在做你想做的事(B–E、G);然后看方法够不够(D 过、整个训练集不行);最后才调超参数(F,第 9 讲 §1)。


6. 迭代:误差分析驱动 · Iterate with error analysis

6.1 从”更多数据”到”去哪里采”

接着工厂的故事:快队在产线工位采了 20 天,3000 张。但他们只在 1 号产线架了相机。工厂其实有两条线:2 号线占产量的 25%,零件是转了 90° 放的,拉丝纹理是竖的。dev 集按”模型将来要面对的数据”来建:1125 张来自 1 号线、375 张来自 2 号线。另外,标注员大约有 2% 的时候会标错。

基线模型(第 9 讲模板,默认超参数)3 个种子的 dev 准确率是 0.789、0.799、0.789,训练集 1.000:明显的 high variance(第 6 讲),药方首先是更多的数据。但是,下一批数据该去哪里采?

6.2 误差分析表

第 10 讲讲过误差分析的做法:拿出约 100 个 dev 上的错例,一个个看,像电子表格一样逐列打勾,再数一数每一类占多少。代码 03 演示 1 照做了(真实项目里要人一张张看图;这个世界里每张图怎么来的都记在 meta 里,打勾可以自动完成)。随机抽的 100 个错例里,前几行是这样的:

#标签 → 预测错误类型2 号线淡缺陷(c < 0.2)正常品带灰尘 / 水渍标签本身标错了
1正常 → 污点误报✓
3划痕 → 正常漏检✓
4正常 → 污点误报✓✓
6划痕 → 正常漏检✓✓
合计(100 个)25%44%28%7%

再在全部 317 个错例上算每一类的天花板:如果这一类错误全部修好,dev 错误率最多能降到多少:

类别错例个数占错例dev 里这类图的比例这类图的错误率天花板
2 号线7624%25%0.2030.161
淡缺陷(c < 0.2)13141%23%0.3720.124
正常品带灰尘 / 水渍8326%22%0.2550.156
标签本身标错了289%2%0.9330.193

(dev 错误率 0.211。一个错例可以同时属于几类,所以各类加起来超过 100%,天花板也不能相加。)

这张表和我的预想不一样。 训练集里一张 2 号线的图都没有,我以为”没见过的产线”会是最大的问题。数出来,2 号线的错误率(0.203)和 1 号线(0.214)差不多,它的错误只占它在 dev 里的份额。竖的纹理并没有让模型”失明”。最大的一类是淡缺陷。先数,再下结论。 另外注意:抽 100 个错例数出来的比例(44%、28%、25%、7%)和全部错例(41%、26%、24%、9%)很接近,这就是为什么 100 个通常就够用。

6.3 同样的预算,五种花法

误差分析给出的是天花板,它准不准?代码 03 演示 2 用同样的预算(再采并标 1500 张,相当于 10 天)试了五种做法,每种 3 个种子:

做法dev 准确率1 号线2 号线比基线
基线(1 号线 3000 张)0.7920.7970.778
(a) 随机多采 1 号线0.820(±0.006)0.8300.788+0.028
(b) 专门去 2 号线采0.831(±0.003)0.8300.835+0.039
(c) 按产线比例采(3:1)0.820(±0.001)0.8250.804+0.028
(d) 专门采难的图(淡缺陷 + 带干扰的正常品)0.832(±0.003)0.8440.798+0.040
(e) 不采数据,网络加宽到 [512, 256]0.812(±0.002)0.8190.792+0.020
  • 两种有针对性的做法并列第一:(d) 瞄准误差分析里最大的两类,(b) 瞄准没见过的产线,都比”随机多采”多涨了约 0.01。同样 10 天的预算,收益多了三分之一以上。
  • (b) 的天花板最低,收益却和 (d) 一样多。 看分产线的两列:2 号线的图让 2 号线从 0.778 涨到 0.835,也让 1 号线涨到 0.830,和”随机多采 1 号线”一样多;反过来,1 号线的图对 2 号线帮助很小(0.778 → 0.788)。转了 90° 的图对 1 号线来说就像免费的数据增强(缺陷的形状不分方向)。天花板告诉你最多能拿到多少,不告诉你拿到它有多容易:“没见过的情况”(覆盖缺口)通常补一点数据就好,“本来就难的样本”补很多数据也只能改善一部分(§6.4)。
  • 换更大的网络(e)有一点帮助,但不如任何一种加数据的做法。high variance 的主药方是数据和正则化。
  • 误差分析的价值不在于算得精确,而在于几分钟就排除掉不值得的方向(这里:先别换模型、别急着清理标签),把候选缩小到两三个,再用小实验比较。

6.4 第二轮

代码 03 演示 3 把 (b) 和 (d) 各 1500 张都加进训练集(6000 张),dev 准确率 0.849,再做一次误差分析:

每一类图的错误率第一轮第二轮
2 号线0.2030.155
淡缺陷(c < 0.2)0.3720.349
正常品带灰尘 / 水渍0.2550.126
其余的图0.1260.078

带干扰的正常品错误率减半,淡缺陷几乎没动,尽管 (d) 里有 750 张专门挑的淡缺陷。现在淡缺陷占全部错例的 54%(第一轮 41%),标签错误占 12%(第一轮 9%)。

淡缺陷降不动,可能是因为最淡的那些本来就和灰尘、噪声分不开——接近这个输入下的不可避免误差(第 6 讲 §2.3 的 Bayes 错误率)。如果是这样,下一步该改的是输入(更好的相机、更合适的光照),而不是继续堆数据。(这只是一个猜测,代码里没有验证;验证的办法是请人在同样的图上判断这些淡缺陷,估计 human-level performance,第 10 讲。)标签错误的比例在上升:模型越好,剩下的错误里”其实是标签错了”的比例越高,到这一步清理 dev 标签才值得考虑。

这就是迭代:误差分析 → 投入最值得的方向 → 再做一次误差分析。每一轮的排序都可能变,所以不要一次规划好几个月。

6.5 实验管理

做了十几个实验之后,最常见的问题不是”模型不够好”,而是”我不记得那个好结果是怎么来的了”。代码 03 演示 4 把每次实验记成一行:名字、数据版本(训练数据的 SHA-1 指纹,前 8 位)、种子、配置、结果:

实验                    数据版本  种子  配置                              结果
baseline(1 号线 3000) 2d4f2336  0     hidden=128x64, n_train=3000       dev=0.789, line1=0.786, line2=0.797
baseline(1 号线 3000) 2d4f2336  1     hidden=128x64, n_train=3000       dev=0.799, line1=0.808, line2=0.771
(b) +1500               351cb05a  0     hidden=128x64, n_train=4500       dev=0.829, line1=0.827, line2=0.837
(e) 更大的网络          2d4f2336  0     hidden=512x256, n_train=3000      dev=0.810, line1=0.816, line2=0.792

(节选。)同一个配置换种子,dev 准确率的标准差最大是 0.006。两种做法的差别比这个小,就不能说谁更好(第 9 讲 §5:“最后一步在挑噪声”)。几条实践:

  • 固定随机种子,并且把种子记下来(本讲义所有代码都这样做);
  • 每个实验一个目录、一个配置文件。CS230 博客的项目示例代码(“Tips for the Final Project”那一页)就是这样组织的:experiments/ 下每个实验一个目录,里面一个 params.json(学习率、batch 大小、epoch 数……),训练日志、最后几个 epoch 的权重、dev 上最好的权重都存在同一个目录里;search_hyperparams.py 为每组超参数建一个子目录,synthesize_results.py 把所有实验汇总成一张表;test 集只在项目最后跑一次;
  • 数据只追加、不覆盖,文件名带版本号,或者像这里一样记下哈希(工具有 DVC、git-lfs 等);
  • 结果自动写进表格(TensorBoard、Weights & Biases、MLflow),而不是靠记忆。

7. 从模型到系统 · From model to system

7.1 系统 = 模型 + 很多别的东西

上线的从来不只是一个模型。Sculley et al.(2015)那张常被引用的图里,ML 代码只是中间一个小方块,周围是数据采集、特征提取、数据校验、服务基础设施、监控……一个质检系统至少包括:相机和光照、预处理(标准化参数 μ、σ)、模型权重、阈值、前置规则、人工复检流程、监控。它们要一起版本化、一起上线:只换了模型却沿用旧的阈值,系统的行为也会变(§8.4 会看到)。

7.2 跑在哪里:端侧 vs 云端

第 5 讲 §2.6 简单比较过服务器和手机。放到本讲的两个例子里:

端侧(edge / on-device)云端(cloud / server)
延迟不用上传,快;没网也能用要上传数据,受网络影响
隐私数据不离开设备(台灯:你家里的声音)数据要传出去
算力、功耗很有限(台灯里的芯片要常开)几乎不限,模型可以很大
更新要推送到每台设备改服务器上的一份就行
典型选择唤醒词检测;手机相册的人脸分组唤醒之后的完整语音识别;大模型

很多产品两者结合:台灯里一个小模型常开、只负责”听到唤醒词了没有”,唤醒之后的复杂请求再交给云端。工厂产线上通常是靠近相机的一台工控机(“边缘服务器”),因为产线节拍要求每个零件在几十毫秒内出结果,而且工厂不希望生产数据离开厂区。

7.3 延迟、吞吐与前置过滤器

延迟(latency)是一个请求从进来到出结果要多久;吞吐(throughput)是每秒能处理多少个。把多个输入凑成一个 batch 能提高吞吐,但每个输入都要等 batch 凑齐,延迟变大。04_deploy_and_monitor.py --timing 在我的电脑上测了一次(数字每次运行都不同):batch 1 时每秒约 7.8 万张、每次 0.013 毫秒;batch 4096 时每秒约 1180 万张,但每个 batch 要 0.347 毫秒。产线一个零件一个零件地来,延迟更重要;离线复查一整天的图片,吞吐更重要。

Ng 在台灯例子里提到的一个系统设计是前置过滤器:设备一直在听,但绝大部分时间是安静的。先用一个便宜的、非 ML 的语音活动检测(voice activity detection,VAD)判断”有没有人在说话”,只把有声音的片段送进神经网络。代码 04 演示 2 在质检系统里做了同样的事:用 §4.2 那条规则的”最突出的一点”做过滤器,明显平静的零件直接放行,其余送进网络(网络每张图约 41,088 次乘加,规则约 1,024 次运算,粗略估计):

过滤器设定送进网络平均运算 / 件召回率误报率代价 / 千件
不用过滤器100.0%42,1120.9320.03698.2
让验证集里 99.5% 的缺陷通过48.2%20,8370.9280.03097.0
让 98.0% 的缺陷通过35.5%15,6270.9180.025102.6
让 95.0% 的缺陷通过28.6%12,7590.8990.020116.0

(代价的定义见 §7.4。)第二行:一半多的零件不用过网络,平均计算量降到约一半,召回只从 0.932 掉到 0.928,误报还少了一点(被直接放行的零件里有一些本来会被网络误报)。但过滤器放行的零件永远不会被网络看到:过滤器漏掉的缺陷,网络再好也救不回来,所以它要设得”宁可多送、不可漏放”,它的召回率是整个系统召回率的上限。在 16×16 的小图上省这点计算没什么意义;但在一直在听的语音设备、绝大多数帧什么都没发生的监控视频里,它决定了模型能不能在芯片上跑、电池能撑多久。

7.4 阈值是一个产品决策

上线的质检系统只做一件事:放行还是拦下。所以上线的是一个二分类模型,输出 P(缺陷)P(\text{缺陷}),超过阈值 τ\tau 就拦下。τ\tau 选多少?很多人默认 0.5。先想一想:0.5 有什么特别的吗?

把两种错误的代价写下来:漏检一个缺陷(流到客户、退货索赔)的代价 cmissc_{\text{miss}},误报一个正常品(人工复检一次)的代价 cFAc_{\text{FA}}。总代价

cost(τ)=cmiss⋅#{缺陷且 p≤τ}+cFA⋅#{正常且 p>τ}\text{cost}(\tau) = c_{\text{miss}} \cdot \#\{\text{缺陷且 } p \le \tau\} + c_{\text{FA}} \cdot \#\{\text{正常且 } p > \tau\}

选让它最小的 τ\tau。本讲假设 cmiss=10 cFAc_{\text{miss}} = 10\,c_{\text{FA}}。代码 04 演示 1 在验证集(5000 个零件,产线比例,缺陷 9.7%)上扫了一遍:

τ\tau召回率误报率漏检 / 千件误报 / 千件代价 / 千件
0.50.9510.0694.862.6110.6
0.70.9510.0564.850.498.4
0.90.9320.0366.632.898.8
0.950.9180.0288.025.4105.4
0.990.8890.01810.815.8123.8
0.9990.8440.00815.27.6159.6

细扫之后代价最小的是 τ=0.903\tau = 0.903(召回 0.932、误报率 0.036)。几点:

  • τ=0.5\tau = 0.5 没有任何特别之处。 模型是在”缺陷占 2/3”的数据上训练的(故意多采了缺陷),它给出的概率偏向”缺陷”;产线上缺陷只占 10%。
  • 概率不一定可信。 这个模型的输出非常极端:正常品的 P(缺陷)P(\text{缺陷}) 中位数 0.0004,缺陷品 1.0000。有意义的阈值都挤在 0.9 以上,“误报率 ≤ 1%“要把 τ\tau 推到 0.9984,召回掉到 0.846。过拟合的网络常常这样”过度自信”,需要的话可以做校准(calibration,比如 temperature scaling,Guo et al. 2017)。
  • 阈值是产品决策,不是模型参数。 代价比变了(客户换了、复检变便宜了),τ\tau 就要重选,模型不用动。选 τ\tau 必须用和真实使用比例一样的数据,并且和模型、标准化参数一起保存、一起上线。台灯也一样:误唤醒和漏唤醒哪个更烦人,决定了阈值(第 5 讲 §5.4 的”固定误唤醒次数”)。

8. 部署之后:监控与维护 · Monitoring & maintenance

8.1 世界会变:数据漂移与概念漂移

模型上线的那一天,就开始”过时”了。台灯会遇到新的口音、新买的空调;产线会换相机、灯会老化、会出现新的缺陷类型、质量标准会改。按”变的是什么”,可以分成:

  • 数据漂移(data drift,也叫 covariate shift):输入的分布 P(x)P(x) 变了,但”什么样的 xx 对应什么 yy“没变。换相机、灯变暗都是。
  • 概念漂移(concept drift):P(y∣x)P(y \mid x) 变了:同样的图,答案变了。比如质量标准改成”小污点算合格”。
  • 还有一种常见的是标签漂移(label shift / prior shift):P(y)P(y) 变了,比如某批原料让缺陷率从 10% 涨到 20%。

区分它们很重要,因为能发现它们的手段不同:数据漂移不需要标签就能看出来;概念漂移只看输入是看不出来的。

8.2 监控什么

不需要标签的统计量(当天就能算):

  • 输入的统计量:每张图的平均亮度、图内的标准差(对比度 + 噪声)……和上线时的参考分布比。
  • 模型行为:拦下率(预测为缺陷的比例)、P(缺陷)P(\text{缺陷}) 的分布、平均置信度。
  • 系统指标:延迟、报错率。

需要标签的(慢、贵):每天随机抽一些零件请质检员人工检验(人在回路,human-in-the-loop),用它们估计真实的召回率和误报率。

怎样衡量”分布挪了多少”?两个常用的统计量:

PSI(population stability index,群体稳定性指数):在参考数据上按分位数切 KK 个桶,每个桶里参考数据占 eie_i(约 1/K1/K),当前数据占 aia_i:

PSI=∑i=1K(ai−ei) ln⁡aiei\text{PSI} = \sum_{i=1}^{K} (a_i - e_i)\,\ln\frac{a_i}{e_i}

它是两个方向的 KL 散度之和,总是 ≥ 0,分布完全一样时为 0。风控行业流传的经验门槛是 < 0.1 稳定、0.1–0.25 有变化、> 0.25 显著(只是经验值)。

KS 统计量:两条经验累积分布函数之间最大的竖直距离,D=max⁡x∣Fref(x)−Fcur(x)∣D = \max_x |F_{\text{ref}}(x) - F_{\text{cur}}(x)|,在 0 到 1 之间。

但是报警门槛不该拍脑袋定:同分布的两批数据之间,PSI 也不是 0(有限样本的噪声)。正确的做法是先在”一切照旧”的数据上看每个统计量自己的波动,再决定多大的偏离算报警。

8.3 实验:监控面板

代码 04 演示 3 先在 20 个”一切照旧”的星期(每周 5000 个零件)上算出每个统计量的均值和标准差,把”偏离 4 个标准差以上”定为报警(带 !);然后模拟五种变化,各一周。抽检是每天 200 个:

情况亮度 PSI标准差 PSI分数 KS拦下率置信度抽检误报抽检召回真实召回真实误报
“一切照旧” 20 周的均值0.0030.0030.0160.1230.9730.0360.904
        标准差0.0010.0010.0040.0040.0010.0060.022
一切照旧0.0060.0030.0250.1220.9710.0380.9190.9100.036
换了相机(噪声 0.02 → 0.03)0.0024.582!0.397!0.333!0.928!0.264!0.9260.9440.265
灯变暗(亮度 −0.08)4.157!0.011!0.061!0.1160.9700.0510.8210.8180.044
光照不均(左右差 0.10)0.0065.732!0.425!0.352!0.925!0.277!0.9370.9330.288
新缺陷类型:裂纹(占缺陷一半)0.0020.0040.0110.1260.9710.0370.9150.9120.035
质量标准变了:小污点算合格0.0020.0050.0220.1150.9730.0520.8970.8910.057

逐行读:

  • 换相机、光照不均:误报率暴涨到 0.265、0.288,不需要标签的列几乎全亮了,当天就能发现。
  • 灯变暗更隐蔽:真实召回从约 0.90 掉到 0.818,拦下率却几乎没变(0.116)——少拦了一些缺陷,又多拦了一些正常品,两边抵消了。只有输入统计量(亮度 PSI 4.16)报了警。输入监控能告诉你”有东西变了”,但不能告诉你”变好还是变坏”。抽检召回 0.821 偏离了 3.7 个标准差,还没到报警线。
  • 新缺陷类型:什么都没报警,而模型对裂纹拦得还行(真实召回 0.912)。一个”正常 vs 不正常”的分类器对没见过的缺陷有一定的泛化。我原以为它会漏掉大部分裂纹;这次是运气好,没见过的东西会被拦下还是放行,事先没人知道。
  • 概念漂移:图片一模一样,只是”什么算缺陷”变了。所有不需要标签的统计量都看不出来,只有抽检的标签有反应:抽检误报率 0.052(平时 0.036,偏离 2.9 个标准差)。一周 1400 个抽检还不够让它越过报警线。 靠标签的监控是唯一能发现概念漂移的手段,但它又慢又贵:抽检越多,越早发现,人工成本越高(练习 8 会把抽检加到每天 600 个)。

8.4 什么时候重训

代码 04 演示 4 模拟了上线后的 60 天:每天 1000 个零件,每天抽检 200 个(一半进”训练池”,一半进”验证池”)。第 15 天换了相机,第 31 天起灯每天暗一点。三种策略看到的产线数据完全相同:

  • 从不更新;
  • 每 14 天重训;
  • 报警时重训:亮度 PSI 或标准差 PSI > 0.25,或者拦下率偏离上线时 0.03 以上,就重训,之后冷却 7 天。

两种主动策略的重训流程一样:用”上线时的训练集 + 训练池”训练一个候选模型 → 在最近 14 天的验证池上为它重新选 τ\tau(训练数据的缺陷比例在变,旧阈值不再合适)→ 在同一批验证数据上和线上系统比,候选更好才替换。

策略第 1–14 天(一切照旧)召回 / 误报 / 日均代价第 15–30 天(新相机)第 31–60 天(新相机 + 灯渐暗)总代价换模型次数
从不更新0.904 / 0.032 / 1240.948 / 0.256 / 2820.915 / 0.261 / 32115,8890
每 14 天重训0.904 / 0.032 / 1240.959 / 0.301 / 3120.793 / 0.047 / 25114,2713
报警时重训0.904 / 0.032 / 1240.917 / 0.169 / 2370.781 / 0.043 / 25913,3084

每 14 天重训在第 14、28、42 天换了模型,第 56 天的候选没通过验证;报警重训在第 15、22、34、41 天换了模型,第 48、55 天的候选被放弃。

三种重训策略的日代价

(这张图由 04_deploy_and_monitor.py 生成。前 14 天三种策略完全一样,三条线重合。)

  • 从不更新:换相机后误报率从 0.032 跳到 0.256。它在第 31–60 天的召回(0.915)看起来最高,那是因为它把四分之一的正常品都拦下了——只看召回会被骗。
  • 报警时重训:换相机当天就报警,第二天换上新模型,第 15–30 天日均代价 237。每 14 天重训这一段反而是 312,比从不更新(282)还高:它第 14 天那次重训发生在换相机之前,换上的新模型碰巧对新相机更敏感,下一次重训要等到第 28 天。
  • 第 31–60 天灯越来越暗,缺陷越来越淡,两种重训策略的日均代价都只压到 251–259,召回掉到 0.8 左右。重训能让模型适应新数据,但补不回输入里已经丢掉的信息——该修的是灯。
  • 几次候选模型没通过验证:重训不等于变好。每次都要在新鲜的、带标签的数据上和线上系统比一比再替换。

8.5 维护的几条原则

  • 人在回路:重训用的数据全部来自人工抽检;抽检同时也是唯一能发现概念漂移的监控。台灯的对应物是”用户说了唤醒词却没反应、又说了一遍”这类隐式反馈。
  • 定期 + 触发:定期重训简单、可预期,适合慢慢变的世界;触发式重训对突变反应快,但依赖监控统计量选得对、门槛定得好(它对概念漂移无能为力)。实践中常常两者结合。
  • 安全地替换:先在验证数据上比;再”影子部署”(shadow deployment,新模型跟着跑、但不做决定)或先在一小部分流量上试(canary);出问题能一键回滚到旧版本。
  • 当心反馈回路:如果只有被拦下的零件才会被人检验,你就永远看不到漏检的那些——训练数据会越来越偏。所以抽检要包括随机抽取的、被放行的零件。

9. AI + 医疗 · AI + Healthcare

2018 年的第 5 讲由 Pranav Rajpurkar(当时是 Ng 组的博士生)讲 AI 在医疗中的应用。医疗是”项目全周期”里每一步都格外难的领域:数据受隐私保护、标签要专家来给而且专家之间意见不一、一家医院训练的模型换一家医院可能就不灵了、出错的代价是病人的健康。这一节先介绍几个代表性的工作,再用一个合成的”类胸片”世界(clinic_world.py,不是真实医学影像)把三个难题做成实验。

9.1 三个代表性的工作

  • CheXNet(Rajpurkar et al., 2017):在 ChestX-ray14 数据集(112,120 张正位胸片,每张最多 14 种疾病的标签,标签是用自然语言处理从放射科报告里抽出来的)上训练一个 121 层的 DenseNet,检测肺炎。测试集 420 张,由斯坦福 4 位执业放射科医生独立标注(从业 4、7、25、28 年)。评估方式:每个标注者(4 位医生和模型)把另外 4 个标注者轮流当参考答案算 F1,取平均。结果:CheXNet F1 0.435(95% CI 0.387–0.481),医生平均 0.387(0.330–0.442),差值 0.051(0.005–0.084)不包含 0。论文自己列了局限:医生和模型都只看到正位片(有些诊断需要侧位片),也都不能看病史。值得注意的是表里第 4 位医生的 F1 是 0.442,比 CheXNet 还高。
  • 心电图心律失常检测(Hannun, Rajpurkar et al., Nature Medicine 2019):91,232 份单导联动态心电图(约 5.4 万名病人),12 类心律,一个直接读原始信号的深度神经网络(不手工设计特征)。测试集由多位执业心脏科医生组成的委员会共同裁决标注。平均 AUC 0.97;平均 F1 0.837,高于心脏科医生的平均 0.780。
  • MURA(Rajpurkar et al., 2017):40,561 张骨骼肌肉 X 光片(14,863 个检查),判断正常 / 异常,模型是 169 层的 DenseNet。测试集的 207 个检查由 6 位放射科医生额外标注,其中 3 位的多数票作金标准,用 Cohen’s κ 比较模型和其余的医生:在手指和手腕上和最好的医生相当,在肘、前臂、手、肱骨、肩上不如最好的医生。

三篇论文的共同点:参考标准都来自医生,评估方式各不相同(轮流当参考、委员会裁决、多数票),比较的对象也不同(“医生平均”还是”最好的医生”)。读这类结论时,这些细节和结论本身一样重要。

9.2 专家标签:噪声与分歧

代码 05 演示 1:5 位”医生”读同一批 2000 张片子。每个病人有一个看不见的严重程度 ss,s>0.5s > 0.5 为阳性(真相);医生 rr 看到的是 ss 加上自己的偏差和噪声。两两之间的 Cohen’s κ(超出偶然的一致程度,κ = 1 完全一致,0 和瞎猜一样)在 0.72–0.84 之间,5 人意见完全一致的片子 79.7%。分歧集中在哪里?

真实严重程度 ss片子数5 人不一致的比例多数票错的比例
[0.00, 0.30)10183.9%0.0%
[0.30, 0.45)25551.8%3.5%
[0.45, 0.55)14294.4%27.5%
[0.55, 0.70)20443.1%2.9%
[0.70, 1.00)3813.1%0.0%

分歧几乎全部落在边界病例上:ss 接近 0.5 的片子,94.4% 有人意见不同,连 5 人的多数票都有 27.5% 是错的。难的病例,专家也拿不准。

用谁的标签训练? 代码 05 演示 2 在 A 院 3000 人上比较了几种训练标签,测试时和真相比(只有模拟才做得到):

训练标签病人数读片次数训练标签错的比例测试 AUC测试准确率
真相(现实中拿不到)3000—0.0%0.9830.942
3000 人 × 随机 1 位医生300030007.1%0.9700.920
3000 人 × 3 位医生多数票300090002.9%0.9810.933
3000 人 × 5 位医生多数票3000150003.0%0.9800.928
1000 人 × 3 位医生多数票100030003.2%0.9600.905
  • 用”随便一位医生”的标签训练(0.920)和用多数票训练(0.928–0.933)差得不多,离”用真相训练”(0.942)也不远:这里的标签噪声是随机的、落在边界病例上,大量样本会把它平均掉一部分。
  • 同样 3000 次读片:3000 个病人各读 1 次(0.920)比 1000 个病人各读 3 次(0.905)好。这个模型还缺数据,更多的病人比更干净的标签更值钱(Sheng et al. 2008 讨论过”要不要再找一个人标”的一般规律:取决于噪声大小和数据量)。
  • 但测试集反过来:宁可少一点病人,也要多位专家、仔细裁决,因为它决定你相信哪个结论。如果某位医生有系统性偏差(总把某种表现判错),平均也消不掉它。

9.3 “超过医生”怎么读

代码 05 演示 3 照 CheXNet 论文的方式评估了一次:420 张测试片,4 位医生各自读一遍;模型用 3000 个病人、每张片子随机一位医生的标签训练。每个标注者的 F1 = 把另外 4 个标注者轮流当参考答案、取平均:

F1(CheXNet 口径)95% 置信区间F1(对真相)
医生 #00.881[0.851, 0.906]0.955
医生 #10.870[0.837, 0.896]0.895
医生 #20.842[0.804, 0.876]0.871
医生 #30.853[0.815, 0.887]0.896
医生平均0.861[0.832, 0.890]0.904
模型0.825[0.783, 0.863]0.856

模型 − 医生平均 = −0.037,同一批 bootstrap 样本上的 95% 置信区间 [−0.064, −0.011]。我原以为这个模型会和医生不相上下;实际上在这个口径下它比医生平均差,而且区间不包含 0。从这张表能学到读这类结论的几条规则:

  1. 算差值的置信区间。每个 F1 自己的区间都有 0.06–0.08 宽(420 张片子里只有 137 个阳性)。比较”模型 vs 医生”要在同一批 bootstrap 样本上算差值的区间(配对比较),而不是看两个区间重不重叠。CheXNet 论文就是这样做的。
  2. “比平均好”不等于”比每个人都好”。CheXNet 比医生平均高,但不比第 4 位医生高。
  3. 参考标准不是真相。这里和真相比,医生之间的排名也变了:按 CheXNet 口径 #1(0.870)明显高于 #3(0.853),按真相两人几乎一样(0.895 vs 0.896)。拿别人当参考答案,会”奖励”和多数人想法一样的读片者,而不一定是读得最准的。
  4. 看实验条件:医生能不能看病史和旧片?测试集来自几家医院?F1 依赖阈值和阳性比例。

9.4 换一家医院

Zech et al.(2018,PLOS Medicine)用 NIH、Mount Sinai、Indiana 三个医疗系统的胸片训练肺炎检测 CNN,发现 5 组自然比较里有 3 组的内部表现好于外部:比如在 Mount Sinai + NIH 上联合训练的模型,内部 AUC 0.931,到 Indiana 只有 0.815。他们还发现 CNN 能以很高的准确率认出片子来自哪个医院(NIH 99.95%、Mount Sinai 99.98%),甚至哪个科室——医院、设备、病人群体的差异,都是模型能”看到”、也可能偷偷利用的信号。

代码 05 演示 4 在合成世界里重现了这种机制。A 院(训练)和 B 院(外部测试)的扫描仪不同、阳性比例不同(34.1% vs 23.7%);而且 A 院的重症病人常用床旁便携机拍,片子左上角会有一个亮的标记——它和病情相关,却不是病本身。B 院一半的片子都有这个标记,和病情无关。

模型A 院 AUC灵敏度特异度PPVB 院 AUC灵敏度特异度PPV
原样训练0.9780.8970.9520.9070.9390.7990.9450.819
训练和测试都遮住左上角0.9670.8630.9400.8820.9580.8010.9710.896
  • 原样训练:A 院内部 AUC 0.978,到 B 院掉到 0.939。PPV(判阳性的人里真阳性的比例)从 0.907 掉到 0.819,一部分是因为 B 院阳性比例更低——同一个分类器,病人群体变了,PPV 就变(练习 4)。
  • 模型在看标记吗? 同一批 A 院的阴性片子,左上角加上标记,平均 P(阳性)P(\text{阳性}) 从 0.038 变成 0.165。它学会了这条捷径。
  • 遮住左上角重新训练:内部 AUC 从 0.978 降到 0.967,外部 AUC 从 0.939 升到 0.958。去掉捷径,内部分数变差、外部分数变好——只看内部测试集,你会选错模型。这正是 §3.3”实验室的灯”的医疗版本(Geirhos et al. 2020 把这类现象统称为 shortcut learning)。
  • 遮住标记以后,一个小网络从片子本身认医院的准确率是 0.659(瞎猜 0.5):扫描仪的差别留下了一点”指纹”。在这个玩具世界里它很弱,真实胸片上要强得多(上面 Zech et al. 的数字)。

9.5 医疗项目的几条特殊要求

  • 多中心、外部验证:至少在一家没参与训练的医院测试;按设备、科室、人群分组报告。
  • 参考标准写清楚:谁标的、几个人、怎样裁决、能看到什么信息。
  • 报告置信区间和配对比较,而不是只报一个点估计。
  • 前瞻性评估:回顾性数据上的好成绩,不等于放进临床流程以后病人结局变好。
  • 隐私与许可(如美国的 HIPAA)、监管审批、医生怎样使用模型的输出(辅助还是替代)——这些都会反过来影响你能拿到什么数据、能做什么题目。

10. 你的 CS230 项目 · Your CS230 project

把这一讲落到课程项目上。Fall 2025 的时间线是:项目提案(Proposal,10/14)、中期报告(Milestone,11/11)、最终视频和报告(12/7)(来自 Section 1 页面)。几条建议:

  • 提案:写清楚 xx 是什么、yy 是什么、数据从哪来(已经拿到了吗?)、用什么指标、和什么比(baseline、人类水平)。数据还没拿到的题目,风险最大。
  • 中期报告之前:至少走完一整圈——数据 → baseline 阶梯 → 一个能训练的网络 → 一次误差分析。中期报告里最有说服力的不是一个高分,而是”我们发现了什么问题、下一步打算怎么改”。
  • 最终报告和海报:问题和动机 → 数据(来源、划分、统计量)→ 方法(baseline 和你的模型)→ 实验(主结果表、消融实验、学习曲线)→ 误差分析(附几张典型的错例)→ 局限与未来工作。数字要附上种子之间的波动;test 集只用一次(CS230 示例代码的建议)。
  • 用好本讲的调试清单和实验记录:项目最后两周最痛苦的,往往是”复现不出自己三周前的结果”。

11. 小结 · Summary

  • 七个步骤:选题 → 数据 → 模型 → 训练 → 测试 → 部署 → 维护。它是一个循环;每一步先做一个”够用”的版本,写下出口标准。
  • 选题:值不值得、做不做得到(“一个人一秒钟能做出的判断”)、数据从哪来、有没有参照。动手前写下优化指标和满足指标;类别不平衡时别用准确率(永远说”正常”就有 0.90)。
  • 先快后精:采法的缺陷只有训练并放到真实环境里试才会暴露。快队第 3 天就发现了(召回 0.073),慢队第 41 天(6000 张也只有 0.110),到第 60 天快队 0.958 vs 慢队 0.919。用”笨模型”和反事实检查找捷径(同一个零件换个灯,P(缺陷)P(\text{缺陷}) 0.979 → 0.082)。学习曲线要边采边重估(早期外推差了几十倍)。
  • 第一个系统:先复现现成代码(Section 3 的仓库清单);baseline 阶梯:多数类 → 规则(0.884)→ softmax 回归(0.660)→ MLP(3000 张时 0.847,要到 4800–9600 张才超过规则);先在小数据上过拟合。
  • 调试:四类错误(语法 / 逻辑 / 算法 / 实验)。清单 A–G 每项几秒钟;9 个 bug 抓住了 8 个,剩下的 softmax 回归本来就不是 bug(能背下 32 张、拟合不好整个训练集 = 方法不够)。打乱标签是唯一能抓住”在训练集上评估”(汇报 0.967)的检查;BatchNorm 会让”初始损失”对忘了归一化失明。
  • 迭代:误差分析数出最大的一类(淡缺陷 41%,不是我预想的 2 号线);同样 1500 张,有针对性的两种做法(+0.040、+0.039)比随机多采(+0.028)好,换大网络只有 +0.020。天花板说”最多多少”,不说”有多容易”。记下数据版本、种子和配置;种子波动 0.006 以内的差别不算数。
  • 系统:阈值是产品决策(代价最小的 τ=0.903\tau = 0.903,不是 0.5);前置过滤器(VAD 的同类)省一半计算、召回只掉 0.004,但它的召回是系统的上限;端侧 vs 云端看延迟、隐私、算力、更新。
  • 监控:数据漂移不用标签就能发现,当天报警;灯变暗时拦下率不变、召回却掉了;概念漂移只有抽检的标签能发现,而且慢。重训策略:报警重训总代价 13,308 < 定期 14,271 < 从不更新 15,889;重训前要验证,重训补不回输入里丢掉的信息。
  • 医疗:专家之间 κ 0.72–0.84,分歧集中在边界病例;训练时更多病人 > 更干净的标签,测试时反过来;“超过医生”要看参考标准、差值的置信区间、比的是平均还是最好的;去掉捷径,内部分数降、外部分数升;必须做外部验证。

12. 常见误区 · Common pitfalls

  1. “先把数据采够再开始。” 采法的问题只有训练、试点之后才暴露;6000 张有缺陷的数据和 300 张一样没用(代码 01 演示 2)。
  2. “dev 上很好,就可以上线了。” dev 和训练用同一种采法时,会带着同一个偏差(dev 召回 0.976,试点 0.080)。dev 要来自真实使用的分布,上线前要在真实环境里试点。
  3. “第一个模型就该用最新的网络。” 先建 baseline 阶梯。一条两个参数的规则在 3000 张图时比 MLP 还好。
  4. “损失在下降,所以代码没问题。” 9 个 bug 里有好几个损失照样下降、最终准确率”说得过去”。跑一遍检查清单。
  5. “结果比预期好,太好了。” 好得不真实的结果(在训练集上评估:0.967)要先怀疑。打乱标签测试能抓住它。
  6. “更多数据总是对的,随便采就行。” 先做误差分析:同样 10 天,采对地方的收益多三分之一以上。
  7. “模型输出 0.5 以上就是缺陷。” 阈值要按两种错误的代价、在真实比例的数据上选;训练数据的类别比例会让概率整体偏移。
  8. “上线之后看看拦下率就够了。” 灯变暗时拦下率几乎不变,召回却从 0.90 掉到 0.818;概念漂移只有人工抽检才看得出来。
  9. “重训一定会让模型变好。” 候选模型要在新鲜的带标签数据上和线上系统比;有几次重训的候选就没通过。
  10. “模型的 F1 超过了医生平均,所以它比医生强。” 要看参考标准怎么定的、差值的置信区间、是平均还是最好的医生、能不能推广到别的医院。

13. 练习 · Exercises

练习 1(初始损失与”地板”)。(a) 10 类分类问题,随机初始化时交叉熵大约是多少?(b) 如果模型最后已经做了 softmax,又交给 CrossEntropyLoss(再做一次 softmax),在一个样本上完美预测(输入的”概率”是 one-hot)时,损失最低是多少?分别算 C=3C = 3 和 C=10C = 10。(c) 代码 02 里正确代码的初始损失是 1.111,BatchNorm 版是 1.299,为什么都比 ln⁡3\ln 3 略大?

参考答案

(a) ln⁡10≈2.303\ln 10 \approx 2.303。

(b) 第二次 softmax 的输入是 one-hot 向量 (1,0,…,0)(1, 0, \dots, 0),输出正确类的概率是 e/(e+(C−1))e / (e + (C - 1)),损失 =ln⁡(1+(C−1)/e)= \ln\big(1 + (C-1)/e\big)。C=3C = 3:ln⁡(1+2/e)≈0.5514\ln(1 + 2/e) \approx 0.5514(代码 02 实测 0.5517);C=10C = 10:ln⁡(1+9/e)≈1.461\ln(1 + 9/e) \approx 1.461。类别越多,地板越高。

(c) ln⁡C\ln C 是 logits 全部为 0 时的值。随机初始化的 logits 有一点随机的大小,各类不完全相等,交叉熵就会比 ln⁡C\ln C 略大,logits 越分散,偏得越多。我在代码 02 里加了一行打印初始 logits 的标准差:dropout 版约 0.15,BN 版约 0.61。BN 把隐藏层的输出标准化成方差约 1,送进最后一层的数更大,logits 也就更分散。Karpathy 的建议是把最后一层初始化得很小,让初始损失非常接近 ln⁡C\ln C,这样真正的偏离更容易看出来。

练习 2(手算 PSI)。参考数据按四分位数切成 4 个桶,每桶 25%;本周的数据落在四个桶里的比例是 10%、20%、30%、40%。(a) 算 PSI。(b) 按”> 0.25 显著”的经验门槛,要不要报警?(c) 代码 04 演示 3 的监控面板为什么不用 0.25,而用”偏离一切照旧的均值 4 个标准差”当报警线?演示 4 为了简单用了 0.25,什么情况下这样做没问题?

参考答案

(a) PSI=(−0.15)ln⁡0.4+(−0.05)ln⁡0.8+0.05ln⁡1.2+0.15ln⁡1.6≈0.137+0.011+0.009+0.071=0.228\text{PSI} = (-0.15)\ln 0.4 + (-0.05)\ln 0.8 + 0.05\ln 1.2 + 0.15\ln 1.6 \approx 0.137 + 0.011 + 0.009 + 0.071 = 0.228。

(b) 0.228 < 0.25,按经验门槛不报警,但已经在”有变化”的区间里。

(c) PSI 本身有抽样噪声,门槛应该相对于”一切照旧”时的波动来定,而且每个统计量的噪声大小不同(演示 3 里抽检误报率这种只有一千多个样本的量,噪声比 PSI 大得多),用同一个”偏离几个标准差”的规则可以一视同仁。演示 3 里同分布的一周(5000 个零件)亮度 PSI 是 0.003 左右;演示 4 每天只有 1000 个,噪声大一些(同分布时 PSI 的期望大约是 (K−1)(1/n+1/m)(K-1)(1/n + 1/m),这里约 0.01),但仍然远小于 0.25,而真正的漂移(换相机后标准差 PSI 4.582)又大了两个多数量级,0.25 落在中间很宽的空白里,用它没问题。如果样本很少(比如每天只有几十个),同分布的两批数据之间 PSI 也可能超过 0.25,那时就会频繁误报。

练习 3(阈值与代价比)。用 §7.4 的表(每千件的漏检和误报个数)回答:(a) 如果漏检的代价是误报的 50 倍,表里六个 τ\tau 中哪个最好?(b) 先预测:在 0.01–0.999 上细扫,最优 τ\tau 会更大还是更小?误报率大约多少?然后把 04_deploy_and_monitor.py 里的 COST_MISS 改成 50 运行验证。

参考答案

(a) 代价 = 50 × 漏检 + 误报:τ=0.5\tau = 0.5:50×4.8+62.6=302.650 \times 4.8 + 62.6 = 302.6;0.7:50×4.8+50.4=290.450 \times 4.8 + 50.4 = 290.4;0.9:362.8;0.95:425.4;0.99:50×10.8+15.8=555.850 \times 10.8 + 15.8 = 555.8;0.999:50×15.2+7.6=767.650 \times 15.2 + 7.6 = 767.6。表里最好的是 0.7。

(b) 会更小:漏检越贵,越要”宁可错拦”。我实际运行的结果是 τ=0.016\tau = 0.016,召回 0.990,误报率 0.212,代价 241.0 / 千件——每 5 个正常品就拦下 1 个。这时你可能会回头质疑那个 50 倍的假设,或者加一个满足指标(误报率 ≤ 5%),在满足它的阈值里选召回最高的。这正是 optimizing vs satisficing(第 10 讲)。

练习 4(PPV 和阳性比例)。一个模型的灵敏度 0.9、特异度 0.95,不随医院改变。(a) 在阳性比例 34% 的医院,PPV 是多少?(b) 23.7% 呢?(c) 5% 呢?(d) 这和代码 05 演示 4 的 PPV 从 0.907 掉到 0.819 有什么关系?

参考答案

PPV=灵敏度⋅p灵敏度⋅p+(1−特异度)(1−p)\text{PPV} = \dfrac{\text{灵敏度} \cdot p}{\text{灵敏度} \cdot p + (1 - \text{特异度})(1 - p)}。

(a) 0.306/(0.306+0.033)≈0.9030.306 / (0.306 + 0.033) \approx 0.903。(b) 0.2133/(0.2133+0.0382)≈0.8480.2133 / (0.2133 + 0.0382) \approx 0.848。(c) 0.045/(0.045+0.0475)≈0.4860.045 / (0.045 + 0.0475) \approx 0.486:阳性很少时,判阳性的人里一半以上是假阳性。

(d) A 院阳性 34.1%、B 院 23.7%,单是阳性比例的变化就会让 PPV 从约 0.90 降到约 0.85;演示 4 里 B 院的 PPV(0.819)掉得更多,是因为灵敏度也从 0.897 掉到了 0.799(扫描仪和捷径的影响)。所以报告医疗模型的 PPV 时,一定要同时报告人群的阳性比例。

练习 5(学习曲线的局部斜率)。用 §3.4 的表:(a) 只用 1200 和 2400 两个点算幂律指数 bb(err∝n−b\text{err} \propto n^{-b})。(b) 用它从 2400 外推到 9600,和实际的 0.081 比。(c) 为什么它比”用前 4 个点拟合”准得多?这对边采边估计有什么启示?

参考答案

(a) b=log⁡2(0.270/0.182)≈0.569b = \log_2(0.270 / 0.182) \approx 0.569。

(b) 9600 是 2400 的 4 倍:0.182×4−0.569≈0.0830.182 \times 4^{-0.569} \approx 0.083,实际 0.081,非常接近。

(c) 前 4 个点里有 3 个落在”起步阶段”(150–600 张几乎是平的),把直线拉得很平。用最近的、已经进入稳定下降段的两三个点估计局部斜率,外推近一些的距离(一两倍到四倍),通常更可靠。但越往后,曲线会因为接近不可避免误差而变平,局部斜率又会偏乐观。所以每多采一批,就用最新的几个点重估一次。

练习 6(改代码,先预测再运行:去掉亮度捷径)。在代码 01 演示 3 里,把训练和试点的图片都减去每张图自己的平均亮度(X - X.mean(axis=(1, 2), keepdims=True))再训练。先预测:试点上的缺陷召回率会从 0.080 变成多少?dev 上的表现会变好还是变差?然后运行验证。

参考答案

我实际运行的结果:试点召回率从 0.080 升到 0.700,但误报率从 0.047 涨到 0.402;自己 dev 上的三类准确率从 0.817 降到 0.583。

解释:减去每张图的平均亮度,把”整体更亮”这条捷径基本拿掉了,模型只好去看缺陷本身,所以试点上能拦下大部分缺陷。但 240 张图太少,模型学不好形状,误报很多;dev 变差是因为 dev 上那条捷径本来是”有效”的——去掉捷径,同分布的分数下降、换分布的分数上升,和代码 05 演示 4 遮住标记是同一个现象。另外,亮度差还通过另一条路影响缺陷:这个世界里缺陷的幅度和底色亮度成正比,实验室拍的缺陷”更明显”,减均值去不掉这种差别。真正的修复仍然是在产线工位上重新采。

练习 7(调试清单)。下面三种情况,各最可能是四类错误中的哪一类?清单里哪一项最先报警?(a) 32 张图训练 300 步后损失停在 0.69 附近(二分类),换小 10 倍的学习率也一样。(b) 32 张图能背下来,完整训练后训练集准确率 0.75、dev 0.74。(c) dev 准确率 0.99,远好于论文报告的 0.85;打乱训练标签后 dev 准确率仍有 0.80。

参考答案

(a) 逻辑错误的可能性最大(D 项报警):连 32 张图都背不下来,而且和学习率无关,说明梯度传不到参数、标签和输入没对上、损失写错了等。0.69 ≈ ln 2,正是二分类”什么都没学到”时的损失。先查 B(看送进网络的数据和标签),再查损失函数和 zero_grad / step 有没有调用。

(b) 算法错误(或者说 high bias):代码能背下 32 张图,说明流水线本身是通的;训练集都拟合不好,是模型能力不够(或者特征不够),对应第 6 讲的 basic recipe:更大的网络、训练更久、换架构。

(c) 逻辑错误,而且是”评估写错了”或”数据泄露”那一类(E 项报警):打乱的标签和输入没有关系,dev 准确率不可能远高于瞎猜。检查评估时用的是不是 dev 数据、dev 和训练集之间有没有重复样本、特征里有没有”偷偷带着标签”的东西。

练习 8(改代码:抽检的数量)。把 04_deploy_and_monitor.py 里的 AUDIT_PER_DAY 从 200 改成 600。先预测:演示 3 里”质量标准变了”那一行,抽检误报率会不会越过报警线?“灯变暗”的抽检召回呢?然后运行验证。代价是什么?

参考答案

我实际运行的结果:每天 600 个抽检时,“一切照旧”周之间抽检误报率的标准差从 0.006 降到 0.003,抽检召回的从 0.022 降到 0.015。“质量标准变了”那一行抽检误报率 0.058(偏离 8.5 个标准差)报警了;“灯变暗”的抽检召回 0.831(偏离 5.3 个标准差)也报警了。

抽检量增加 3 倍,标准差约缩小 3\sqrt 3 倍,所以更小的变化也能被发现。代价是人工:每天多检 400 个零件。也可以不加人,而是把窗口拉长(两周的抽检合在一起),代价是发现得更晚。

练习 9(分类漂移)。下面每种情况是数据漂移、概念漂移还是标签漂移?哪些能被不需要标签的监控发现?(a) 台灯卖到了一个新的国家,用户的口音不同。(b) 产品改名,唤醒词从一个词换成了另一个词。(c) 一批新原料让缺陷率从 10% 涨到 20%,缺陷的样子没变。(d) 胸片检测模型部署到一家新医院,那里的病人更年轻、病情更轻。

参考答案

(a) 数据漂移:P(x)P(x) 变了(声音的分布),“说没说唤醒词”的含义没变。输入统计量可能看出来,也可能看不出来(口音的差别不一定反映在简单统计量上),最可靠的信号是用户行为(重复唤醒)。

(b) 概念漂移:同样的声音,答案变了(旧唤醒词不该再唤醒)。只看输入看不出来;实际上这种情况你自己知道,应该直接重新采数据、重训。

(c) 标签漂移(P(y)P(y) 变了,P(x∣y)P(x \mid y) 不变):拦下率会明显上升,不需要标签就能看到;模型本身可能还是对的,但最优阈值和 PPV 会变。

(d) 数据漂移和标签漂移都有:人群变了(P(x)P(x)),阳性比例也变了(P(y)P(y));如果新医院的诊断标准也不同,还会有概念漂移。这就是为什么医疗模型要做外部验证(§9.4)。

练习 10(设计题:台灯的系统)。为语音台灯设计一个上线方案:(a) 模型放在设备上还是云端?(b) 用不用前置过滤器?它的设定原则是什么?(c) 选一个优化指标和两个满足指标。(d) 上线后监控什么?怎样拿到标签?

参考答案

参考思路(没有唯一答案):(a) 唤醒词检测放在设备上:要常开、低延迟、声音不离开家(隐私);唤醒后的复杂指令可以交给云端。(b) 用:一个便宜的 VAD 常开,只在有声音时启动网络,省电;设定原则是”宁可多送、不可漏放”,它的召回是系统召回的上限(§7.3)。(c) 优化:唤醒召回率;满足:每 24 小时误唤醒 ≤ 1 次、唤醒延迟 ≤ 几百毫秒(还可以加:在设备的内存和功耗预算以内)。(d) 监控输入的能量分布、唤醒率(每天被唤醒几次)、唤醒后用户是否马上取消(误唤醒的信号)、用户是否连说两遍(漏唤醒的信号)。标签可以来自这些隐式反馈,加上征得用户同意后的少量抽样人工标注。


14. 延伸阅读 · Further reading

课程材料

  • CS230 Autumn 2018 Lecture 3 “Full-Cycle Deep Learning Projects”、Lecture 5 “AI + Healthcare”、Lecture 6 “Deep Learning Project Strategy”(YouTube,Stanford Online 频道)。
  • CS230 Fall 2025 Section 1 / 3 / 7(https://cs230.stanford.edu/section/1 、/section/3 、/section/7 );CS230 博客 “Tips for the Final Project”(https://cs230.stanford.edu/blog/tips ,项目代码示例的组织方式)。
  • Andrew Ng, Machine Learning Yearning(免费电子书):误差分析、“先快速搭一个系统再迭代”、dev / test 的设置。
  • Andrew Ng (2016). What Artificial Intelligence Can and Can’t Do Right Now. Harvard Business Review。

工程实践

  • Karpathy (2019). A Recipe for Training Neural Networks(博客):调试清单的来源之一。
  • Zinkevich. Rules of Machine Learning: Best Practices for ML Engineering(Google)。
  • Sculley et al. (2015). Hidden Technical Debt in Machine Learning Systems. NeurIPS。
  • Breck et al. (2017). The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction. IEEE Big Data。
  • Chip Huyen (2022). Designing Machine Learning Systems. O’Reilly:部署、监控、数据漂移的系统性讲解。
  • Zhang et al. (2017). Understanding deep learning requires rethinking generalization. ICLR:网络能记住随机标签。
  • Guo et al. (2017). On Calibration of Modern Neural Networks. ICML:temperature scaling。

数据漂移与捷径

  • Gama et al. (2014). A survey on concept drift adaptation. ACM Computing Surveys。
  • Rabanser, Günnemann & Lipton (2019). Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift. NeurIPS。
  • Geirhos et al. (2020). Shortcut learning in deep neural networks. Nature Machine Intelligence。

标签噪声

  • Sheng, Provost & Ipeirotis (2008). Get another label? Improving data quality and data mining using multiple, noisy labelers. KDD。
  • Northcutt, Athalye & Mueller (2021). Pervasive label errors in test sets destabilize machine learning benchmarks. NeurIPS Datasets and Benchmarks。

AI + 医疗

  • Rajpurkar et al. (2017). CheXNet: Radiologist-Level Pneumonia Detection on Chest X-Rays with Deep Learning. arXiv:1711.05225。
  • Hannun, Rajpurkar et al. (2019). Cardiologist-level arrhythmia detection and classification in ambulatory electrocardiograms using a deep neural network. Nature Medicine 25。
  • Rajpurkar et al. (2017). MURA: Large Dataset for Abnormality Detection in Musculoskeletal Radiographs. arXiv:1712.06957。
  • Irvin et al. (2019). CheXpert: A Large Chest Radiograph Dataset with Uncertainty Labels and Expert Comparison. AAAI。
  • Zech et al. (2018). Variable generalization performance of a deep learning model to detect pneumonia in chest radiographs: A cross-sectional study. PLOS Medicine。
  • Wang et al. (2017). ChestX-ray8: Hospital-scale chest X-ray database and benchmarks… CVPR(ChestX-ray14 的来源)。

15. 下一讲预告 · Next lecture

第 12 讲:卷积神经网络基础。 这一讲的迷你项目一直在用全连接网络,它有一个明显的弱点:缺陷可以出现在图上的任何位置,全连接网络要在每个位置上分别学会”划痕长什么样”,所以要几千张图才能超过一条两个参数的规则。第 12 讲会讲:

  • 为什么全连接网络处理图像会”参数爆炸”、而且没有平移不变性;
  • 卷积:一个小的、共享权重的特征检测器在整张图上滑动——用 numpy 从零实现,再和 PyTorch 对照;
  • padding、stride、多通道、池化,以及输出尺寸的公式;
  • 在”平移的图案”任务上比较 MLP 和 CNN 的参数量与泛化。

学完之后,你可以回头用一个小 CNN 重做本讲的质检项目,看看学习曲线会往左移多少。

← 返回课程目录