CS221 · 人工智能:原理与技术
Lecture 2 · 线性回归
源文件:lecture-02.md
Lecture 2 · 线性回归
CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Sep 24 · 💻linear_regression.py
承上启下
- 张量是数据、参数、梯度的统一表示,用少量张量运算把计算写快;
- 梯度 / 反向传播:计算图 + 链式法则,自动求出「往哪走损失变小」;
- 末尾端到端跑通了一个 1 维线性回归的玩具例子(
weight * x + bias),但没有停下来审视其中的每个部件。
本讲(线性回归 · Learning I) 把那个例子拆开重装成一条可复用的流水线:
- 引入特征提取 $\phi(x)$:让同一套线性机器去拟合曲线、处理文本/图像;
- 把假设类统一写成向量内积 $f_w(x) = w \cdot \phi(x)$;
- 从梯度下降升级到随机梯度下降(SGD)——回答大数据下怎么训练;
- 关心一个新问题:模型在没见过的数据上好不好(泛化)。
下一讲 Lecture 3 · 线性分类:输出从实数换成离散类别,同一套 $w \cdot \phi(x)$ 换损失函数就变成分类器。
说明
监督学习 = 假设类 + 损失函数 + 优化算法;线性回归 = 「权重向量 · 特征向量」,靠 SGD 拟合,靠泛化验收。
这一讲对应源文件 linear_regression.py 的叙事主线是 main() 里依次调用的
prediction_task → machine_learning_problem → hypothesis_class → loss_function → optimization_algorithm。
1. 监督学习的一般框架(回顾并抬升)
Lecture 1 · 张量、梯度与监督学习 已经把这条流水线跑通过一遍,这里把它抽象成通用模板,之后每一讲(分类、深度学习……)都会往里填不同的零件。
flowchart LR
D["训练数据<br/>{(x, y)} 样本对"] --> L["学习算法<br/>= 损失 + 优化"]
L --> P["预测器 f_w"]
X["新输入 x"] --> P
P --> Y["预测输出 y ∈ ℝ"]
style D fill:#e8f5e9,stroke:#2e7d32
style L fill:#e1f5fe,stroke:#0277bd
style P fill:#ffe0b2,stroke:#e65100
预测器(predictor) 就是「输入 → 输出」的函数。源码里的引子任务:用学习时长预测考试分数(比如 3 小时 → 70 分)。
# linear_regression.py · prediction_task()
def fixed_f(x: float) -> float:
y = 2 * x + 1 # 一个「写死」的预测器
return y
x1 = 1.0; y1 = fixed_f(x1) # 输入 → 输出
x2 = 2.0; y2 = fixed_f(x2)
# 关键追问:预测器(这里的 2 和 1)应该从哪来?—— 从数据里学
训练数据(training data) 是一组样本,每个样本(example)是一个 (输入, 输出) 对:
# linear_regression.py
@dataclass(frozen=True)
class Example1D:
input: float
output: float
def get_training_data():
return [
Example1D(input=1, output=4),
Example1D(input=2, output=6),
Example1D(input=4, output=7),
]
1.1 三个关键设计问题
machine_learning_problem() 把整门课的机器学习部分浓缩成三问:
| # | 问题 | 答案 | 本讲落点 |
|---|---|---|---|
| 1 | 哪些预测器是可能的? | 假设类(hypothesis class) | $\phi(x)$ 与 $w \cdot \phi(x)$(§2、§3) |
| 2 | 一个预测器有多好? | 损失函数(loss function) | 平方损失、训练损失(§4) |
| 3 | 如何找到最好的? | 优化算法(optimization algorithm) | 梯度下降 → SGD(§5) |
提示
为什么要把学习拆成三个正交的部件?因为这样每个部件可以独立替换、独立分析:Lecture 3 · 线性分类 换损失函数(平方 → logistic),Lecture 4 · 深度学习 换假设类(线性 → 神经网络),大规模训练换优化算法(GD → SGD → Adam),而框架本身从不改变。学任何新模型时先问这三个问题,就不会迷路。
2. 特征提取 φ(x):本讲的真正新东西
上一讲的 $x$ 是一个标量、直接就是数字。现实里输入五花八门——一段文本、一张图片、一栋房子的属性——而机器只会算数。中间必须有一步把「原始输入」翻译成「一个实数向量」:
特征提取(feature extraction):一个函数 $\phi: \mathcal{X} \to \mathbb{R}^d$,把任意原始输入 $x$ 映到 $d$ 维实向量。输出 $\phi(x) = [\phi_1(x), \phi_2(x), \dots, \phi_d(x)]$ 叫特征向量(feature vector),每个分量 $\phi_i(x)$ 是一个特征(feature)——对输入某方面性质的一个数值刻画。
flowchart LR
RAW["原始输入 x<br/>(文本 / 图片 / 房子…)"] --> PHI["特征提取 φ(·)"]
PHI --> VEC["特征向量 φ(x) ∈ ℝ^d<br/>[φ₁(x), φ₂(x), …, φ_d(x)]"]
VEC --> DOT["w · φ(x)"]
DOT --> OUT["预测 y"]
style RAW fill:#fce4ec,stroke:#c2185b
style PHI fill:#e1f5fe,stroke:#0277bd
style VEC fill:#f3e5f5,stroke:#7b1fa2
style DOT fill:#ffe0b2,stroke:#e65100
style OUT fill:#c8e6c9,stroke:#2e7d32
2.1 例:房价预测
| 特征 φᵢ(x) | 含义 | 取值示例 |
|---|---|---|
| φ₁(x) = 1 | 偏置项(bias) | 1 |
| φ₂(x) | 面积(平方米) | 120 |
| φ₃(x) | 卧室数 | 3 |
| φ₄(x) | 房龄(年) | 8 |
于是 $\phi(x) = [1, 120, 3, 8]$,预测房价为
$$w \cdot \phi(x) = w_1 \cdot 1 + w_2 \cdot 120 + w_3 \cdot 3 + w_4 \cdot 8$$
每个权重 $w_i$ 的可解释含义:对应特征每增加一个单位,预测值变化多少(比如 $w_2$ 就是「每平方米的边际价格」)。
说明
约定 $\phi_1(x) \equiv 1$,那么 $w_1$ 就自动扮演偏置 $b$ 的角色。这样 $f(x) = w \cdot \phi(x)$ 一个内积就同时含了权重和偏置,公式更干净——后面推导都用这个约定。上一讲代码里的 np.array([example.input, 1]) 正是这个技巧的一维版本。
2.2 关键洞见:线性回归可以拟合非线性曲线
「线性回归」的线性指的是对参数 $w$ 线性,不是对原始输入 $x$ 线性。只要把非线性的东西塞进 $\phi$ 里,同一台线性机器就能画曲线:
# 想拟合一条抛物线?把 x, x² 都当特征喂进去
def phi(x: float) -> np.ndarray:
return np.array([1, x, x**2]) # φ(x) = [1, x, x²]
def f(w: np.ndarray, x: float) -> float:
return w @ phi(x) # = w₀ + w₁·x + w₂·x² ← 一条抛物线!
此时 $f_w(x) = w_0 + w_1 x + w_2 x^2$ 作为 $x$ 的函数是条抛物线,但作为 $w$ 的函数仍是线性的——损失面依旧是凸的碗形,优化不会变难。
graph LR
A["φ(x) = [1, x]"] --> A2["拟合直线"]
B["φ(x) = [1, x, x²]"] --> B2["拟合抛物线"]
C["φ(x) = [1, x, x², x³]"] --> C2["拟合三次曲线"]
D["φ(x) = [1, sin x, cos x]"] --> D2["拟合周期信号"]
style A2 fill:#e1f5fe,stroke:#0277bd
style B2 fill:#ffe0b2,stroke:#e65100
style C2 fill:#f3e5f5,stroke:#7b1fa2
style D2 fill:#fce4ec,stroke:#c2185b
提示
这就是特征工程(feature engineering)的威力:模型结构(线性)一直没变,变的是 $\phi$——预测能力的上限其实由特征决定。选对特征往往比换更复杂的模型更管用。深度学习的另一条路是:干脆让网络自己学出 $\phi$(Lecture 4 · 深度学习 的核心思想,神经网络的前几层就是可学习的特征提取器)。
3. 假设类:f_w(x) = w · φ(x)
把 §2 的想法固化成假设类。源码在 1 维情形下写成 weight * x + bias;一般化到 $d$ 维就是权重向量与特征向量的内积。
# linear_regression.py · 1 维原型
@dataclass(frozen=True)
class Parameters1D:
weight: float
bias: float
def f(params: Parameters1D, x: float) -> float:
"""线性预测器:weight * x + bias"""
y = params.weight * x + params.bias
return y
一般化(把 $\phi$ 引进来,偏置并入 $\phi_1 \equiv 1$):
# d 维一般形式(本讲的核心假设类)
def f(w: np.ndarray, x) -> float:
return w @ phi(x) # f_w(x) = w · φ(x),一个实数
写成集合的形式,本讲的假设类就是
$$\mathcal{F} = \{ f_w : f_w(x) = w \cdot \phi(x),\ w \in \mathbb{R}^d \}$$
- 权重向量 $w \in \mathbb{R}^d$:就是参数,$w_i$ 表示「特征 $\phi_i$ 对预测的贡献/斜率」;
- 假设类 = 所有 $w$ 取值得到的预测器的集合:固定 $\phi$ 后,挑预测器等价于挑一个 $d$ 维向量;
- 对照深度学习:假设类 ↔ 模型架构,预测器 ↔ 模型,参数 ↔ 一组张量(源码原话:hypothesis class is a model architecture,predictor is a model)。
graph TD
H["假设类 F<br/>{ f_w : w ∈ ℝ^d }"]
H --> W1["w = (2, 0.2) → 预测器 A"]
H --> W2["w = (1, 1) → 预测器 B"]
H --> W3["w = … → 预测器 …"]
W1 & W2 & W3 --> Q["学习 = 从中挑出损失最小的那个 w"]
style H fill:#e1f5fe,stroke:#0277bd
style Q fill:#ffe0b2,stroke:#e65100
提示
假设类是在学习开始之前就划定的「可能性范围」:它注入了我们对问题的先验(比如「输出大致随特征线性变化」),同时也限定了模型能力的天花板——学习永远不可能学出假设类之外的函数。假设类太小会欠拟合、太大会过拟合(§6),所以它是第一个、也是最重要的设计决策。
4. 损失函数:平方损失与训练损失
第二个设计决策——怎么给每个预测器打分。回归任务用平方损失(squared loss):预测值离真值差多远,取残差的平方。
4.1 单样本损失
单个样本 $(x, y)$ 上的平方损失:
$$\ell(w; x, y) = \big(\underbrace{w \cdot \phi(x) - y}_{\text{残差 } r}\big)^2$$
# linear_regression.py
def compute_loss(params: Parameters1D, example: Example1D) -> float:
"""线性预测器在单个样本上的平方损失"""
residual = f(params, example.input) - example.output # 残差 = 预测 − 真值
loss = residual ** 2 # 平方损失
return loss
- 残差(residual) $r = w \cdot \phi(x) - y$:预测减真值,可正可负;
- 平方之后:误差无论方向都被惩罚,且离得越远,惩罚增长得越快(差 2 倍 → 罚 4 倍);
- 平方函数处处可微且导数简单($\frac{d}{dr} r^2 = 2r$),这让 §5 的梯度推导干净利落——相比之下绝对值损失在 0 点不可导。
4.2 训练损失
训练损失(training loss) = 所有训练样本单样本损失的平均。设训练集为 $\{(x^{(1)}, y^{(1)}), \dots, (x^{(n)}, y^{(n)})\}$:
$$\mathrm{TrainLoss}(w) = \frac{1}{n} \sum_{i=1}^{n} \big(w \cdot \phi(x^{(i)}) - y^{(i)}\big)^2$$
其中 $n$ 是样本数,求和项是第 $i$ 个样本的平方损失;取平均使得这个数字的量纲不随数据集大小变化,不同规模的数据集之间可以直接比较。
# linear_regression.py
def compute_train_loss(params, training_data) -> float:
"""训练损失 = 各样本损失的平均"""
losses = [compute_loss(params, example) for example in training_data]
train_loss = np.mean(losses)
return train_loss
例子
用训练数据 $(1,4), (2,6), (4,7)$ 比较源码里的两组参数($\phi(x) = [x, 1]$,即 $f(x) = wx + b$):
- $w=2, b=1$:预测 $3, 5, 9$,残差 $-1, -1, 2$,损失 $1, 1, 4$,训练损失 $= 6/3 = 2$;
- $w=1, b=1$:预测 $2, 3, 5$,残差 $-2, -3, -2$,损失 $4, 9, 4$,训练损失 $= 17/3 \approx 5.67$。
后者训练损失更高,所以更差——损失函数把「哪个预测器好」变成了一个可比较的数字。
说明
有了训练损失,「找最好的预测器」正式变成一道优化题:$\min_{w} \mathrm{TrainLoss}(w)$。三个设计问题至此串成一条线:假设类给出变量($w$),损失函数给出目标,接下来只差求解器。
5. 优化算法:梯度下降 → 随机梯度下降
5.1 梯度下降(回顾 + 完整实现)
梯度指向损失上升最快的方向 ⇒ 往负梯度方向走就下降(原理见 Lecture 1 · 张量、梯度与监督学习 §2):
$$w \leftarrow w - \eta \cdot \nabla_w \mathrm{TrainLoss}(w)$$
其中 $\eta$ 是学习率。平方损失的梯度可以手推。对单个样本,记残差 $r = w \cdot \phi(x) - y$,用链式法则:
$$\nabla_w\, \ell(w; x, y) = \nabla_w\, r^2 = 2r \cdot \nabla_w r = 2\,\big(w \cdot \phi(x) - y\big)\,\phi(x)$$
最后一步用了 $\nabla_w (w \cdot \phi(x)) = \phi(x)$(内积对 $w$ 的梯度就是另一个向量)。训练损失是各样本损失的平均,所以其梯度也是各样本梯度的平均:
$$\nabla_w \mathrm{TrainLoss}(w) = \frac{1}{n} \sum_{i=1}^{n} 2\,\big(w \cdot \phi(x^{(i)}) - y^{(i)}\big)\,\phi(x^{(i)})$$
# linear_regression.py · 单样本梯度
def compute_grad_loss(params, example) -> np.ndarray:
residual = (params.weight * example.input + params.bias) - example.output
# grad[0] = ∂loss/∂weight, grad[1] = ∂loss/∂bias
grad = 2 * residual * np.array([example.input, 1])
return grad
# 训练损失的梯度 = 各样本梯度的平均
def compute_gradient_train_loss(params, training_data) -> np.ndarray:
grads = [compute_grad_loss(params, example) for example in training_data]
grad = np.mean(grads, axis=0)
return grad
提示
一般形式记牢这条:对平方损失,单样本梯度 = 2 · 残差 · $\phi(x)$。它的结构很有讲头——残差决定幅度与方向(预测偏高就把 $w$ 往回拽),$\phi(x)$ 决定责任分配(哪个特征大,哪个特征的权重就被调得多)。源码里 np.array([example.input, 1]) 正是 1 维情形的 $\phi(x) = [x, 1]$。
完整的梯度下降循环(源码 gradient_descent()):
# linear_regression.py · gradient_descent()
def gradient_descent():
training_data = get_training_data()
params = Parameters1D(weight=0, bias=1) # 初始化
learning_rate = 0.01 # η:学习率
for step in range(10): # 迭代 10 步
train_loss = compute_train_loss(params, training_data)
grad = compute_gradient_train_loss(params, training_data) # 用【全部】数据算梯度
params = Parameters1D(
weight=params.weight - learning_rate * grad[0],
bias=params.bias - learning_rate * grad[1],
) # 每步之后 train_loss 都在下降
flowchart TD
A["初始化 w"] --> B["用【全部 n 个】样本算 ∇TrainLoss(w)"]
B --> C["更新 w ← w − η·∇TrainLoss(w)"]
C --> E{"收敛 / 到步数?"}
E -- 否 --> B
E -- 是 --> F["输出预测器 f_w"]
style B fill:#ffcdd2,stroke:#c62828
style C fill:#ffe0b2,stroke:#e65100
说明
无正则的最小二乘存在解析解(正规方程,normal equations):$w^{*} = (\Phi^\top \Phi)^{-1} \Phi^\top y$,其中 $\Phi \in \mathbb{R}^{n \times d}$ 是把每个样本的 $\phi(x^{(i)})$ 按行堆起来的设计矩阵。既然一步就能算出最优解,为什么课程还教梯度下降?因为闭式解求逆的代价约 $O(d^3)$、要求全部数据在手,且只对线性 + 平方损失这个特例成立;而梯度下降是通用引擎——换成 logistic 损失(Lecture 3 · 线性分类)或神经网络(Lecture 4 · 深度学习)后闭式解不复存在,梯度法照用不误。
5.2 痛点:每一步都要扫全量数据
上面标红的那一步是瓶颈:每更新一次 $w$,都要遍历全部 $n$ 个样本。训练集只有 3 个点时无所谓;但真实数据集动辄百万、上亿条,走一步就要扫全库——慢到无法接受。
5.3 随机梯度下降(SGD)
核心思想:不用全量梯度,每次只用一个(或一小批)样本估计梯度,立刻更新:
$$w \leftarrow w - \eta \cdot \nabla_w\, \ell(w;\, x^{(i)}, y^{(i)})$$
即把更新公式里昂贵的 $\nabla \mathrm{TrainLoss}$(对 $n$ 个样本求平均)换成便宜的单样本梯度。
# 随机梯度下降(Stochastic Gradient Descent)
def sgd(training_data, learning_rate=0.01, num_epochs=10):
w = np.zeros(d)
for epoch in range(num_epochs):
np.random.shuffle(training_data) # 每轮打乱顺序
for example in training_data: # 遍历【单个】样本
grad = 2 * (w @ phi(example.input) - example.output) * phi(example.input)
w = w - learning_rate * grad # 每个样本都更新一次 w
return w
- 随机抽样时,单样本梯度是全量梯度的无偏估计:$\mathbb{E}_i\big[\nabla \ell(w; x^{(i)}, y^{(i)})\big] = \nabla \mathrm{TrainLoss}(w)$。每一步的方向带噪声,但平均方向是对的,所以整体仍朝着谷底走;
- 一个 epoch(轮)= 完整过一遍训练集;$n$ 个样本 = $n$ 次更新。同样扫一遍数据,GD 只更新 1 次,SGD 更新 $n$ 次——同等算力下 SGD 的「进度」多得多;
- 小批量(mini-batch):折中方案——每次用 $B$ 个样本(如 32/64/128)的平均梯度更新,噪声比单样本小、又能吃满 GPU 的并行度,是实践中的默认选择。
5.4 GD vs SGD 对比
| 维度 | 批量梯度下降 GD | 随机梯度下降 SGD | 小批量 mini-batch |
|---|---|---|---|
| 每步用多少样本 | 全部 n | 1 | B(如 64) |
| 每步计算量 | 大(扫全库) | 极小 | 小 |
| 更新频率 | 每 epoch 1 次 | 每样本 1 次 | 每 B 样本 1 次 |
| 梯度噪声 | 无(精确) | 大 | 中 |
| 收敛轨迹 | 平滑 | 抖动 | 较平滑 |
| 大数据适用性 | 差 | 好 | 最常用 |
graph LR
GD["GD:精确但慢<br/>直奔谷底"] -.->|大数据不可行| PROB["扫一遍全库<br/>才走一步"]
SGD["SGD:快但抖<br/>边走边估"] -->|每样本就更新| WIN["大数据首选"]
style PROB fill:#ffcdd2,stroke:#c62828
style WIN fill:#c8e6c9,stroke:#2e7d32
style SGD fill:#e1f5fe,stroke:#0277bd
注意
SGD 的噪声不全是坏事:它能帮参数跳出尖锐的坏局部极小/鞍点,这在深度学习(非凸)里反而有益。噪声太大导致损失不降时,靠减小学习率或增大 batch 来压。不要看到损失曲线抖动就断定训练坏了——SGD 的损失曲线本来就该抖。
5.5 超参数:学习率、迭代、收敛
源码结尾的三条 Notes 正是这一节的精华:
说明
- 学习率控制「你开多快」——速度 vs 稳定性的权衡;
- 对凸函数保证收敛;深度学习(非凸)没有保证,但实践中有效;
- 其他算法:随机梯度下降 SGD、Adam。
| 超参数(hyperparameter) | 作用 | 太小 | 太大 |
|---|---|---|---|
| 学习率 η | 每步走多远 | 收敛慢、卡住 | 震荡、发散 |
| 迭代步数 / epochs | 训练多久 | 欠拟合(没学够) | 浪费算力、可能过拟合 |
| batch 大小 B | 每步样本数 | 抖、慢(更新多) | 吃内存、更新粗 |
graph TD
LR["学习率 η"] --> S["太小:像蜗牛,半天到不了谷底"]
LR --> M["合适:稳步下坡"]
LR --> B["太大:在谷两侧反复横跳,甚至冲出去"]
style S fill:#e1f5fe,stroke:#0277bd
style M fill:#c8e6c9,stroke:#2e7d32
style B fill:#ffcdd2,stroke:#c62828
说明
收敛指损失不再明显下降。判据可以是「梯度接近 0」「损失变化小于阈值」或「到达最大步数」。凸问题(如无正则的线性回归——平方损失对 $w$ 是凸的)梯度下降能保证收敛到全局最优;非凸问题只能保证走到梯度为 0 的驻点附近。「超参数」与「参数」的分界:参数 $w$ 由优化算法学出来,超参数($\eta$、epochs、$B$、$\phi$ 的选择)由人在训练前设定——怎么科学地选超参数,正是 §6.3 验证集存在的意义。
6. 泛化:训练误差 vs 测试误差
到这里我们只在训练集上把损失压低了。但学习的真正目标不是背下训练数据,而是在没见过的新数据上也预测得准——这就是泛化(generalization)。
提示
为什么「训练损失低」不等于「学得好」?极端反例:一个把训练集完整背下来的查表预测器,训练误差恰好为 0,但对任何新输入束手无策。训练误差衡量的是记忆,测试误差衡量的才是规律。机器学习与单纯优化的分野就在这里:优化只关心把目标压到最低,学习还要求压低的方式能迁移到新数据。
6.1 两种误差
| 误差 | 在哪算 | 衡量什么 |
|---|---|---|
| 训练误差(training error) | 训练集 | 拟合已见数据的程度(易乐观) |
| 测试误差(test error) | 独立的测试集 | 真正关心的泛化能力 |
注意
只看训练损失会被骗。一个只会「死记硬背」的模型训练误差可以是 0,但换新数据就崩——这叫过拟合(overfitting)。
6.2 欠拟合 vs 过拟合
用 §2 的多项式特征做直觉:$\phi$ 里放几阶多项式 = 模型复杂度旋钮。
graph LR
U["欠拟合 underfitting<br/>φ=[1,x] 直线拟合曲线数据<br/>训练误差高 + 测试误差高"]
G["恰好 good fit<br/>复杂度匹配数据<br/>训练误差低 + 测试误差低"]
O["过拟合 overfitting<br/>φ 太高阶,穿过每个点<br/>训练误差≈0 + 测试误差高"]
U --> G --> O
style U fill:#e1f5fe,stroke:#0277bd
style G fill:#c8e6c9,stroke:#2e7d32
style O fill:#ffcdd2,stroke:#c62828
- 欠拟合:假设类太简单,连训练数据都拟合不好。比如数据本身沿抛物线分布,却只给 $\phi = [1, x]$ 的直线假设类——模型没能力表达规律,训练误差和测试误差都高。
- 过拟合:假设类太复杂或训练太久,模型把训练集里的噪声也当成规律学了进去。比如 3 个数据点用 9 阶多项式去穿——曲线完美通过每个点,但点与点之间疯狂震荡,新数据一来就错得离谱。
- 目标是找到中间的甜点:假设类复杂度与数据量、噪声水平相匹配,泛化误差最低。
6.3 训练 / 验证 / 测试集划分
怎么在不偷看测试集的前提下调超参数(选 $\phi$ 的阶数、学习率……)?把数据切三份:
flowchart LR
ALL["全部数据"] --> TR["训练集 train<br/>~80%<br/>拟合 w"]
ALL --> VA["验证集 val<br/>~10%<br/>选超参 / 模型"]
ALL --> TE["测试集 test<br/>~10%<br/>最后一次性汇报"]
style TR fill:#c8e6c9,stroke:#2e7d32
style VA fill:#e1f5fe,stroke:#0277bd
style TE fill:#ffe0b2,stroke:#e65100
| 数据集 | 用途 | 注意 |
|---|---|---|
| 训练集(train) | 拟合参数 w | 用来跑梯度下降 / SGD |
| 验证集(validation / dev) | 选超参数、比模型 | 反复用,但不训练 w |
| 测试集(test) | 估计最终泛化误差 | 只在最后看一次,绝不用来调任何东西 |
注意
任何一次用测试集去做选择(哪怕只是挑学习率),它就被「污染」了:你实际上在往测试集上「拟合」超参数,报出来的数字会系统性偏乐观,不再是对真实泛化误差的无偏估计。对付过拟合的常规武器还有:正则化、早停(early stopping)、加数据。
7. 总结
mindmap
root((线性回归))
通用框架
预测器 输入→输出
假设类 / 损失 / 优化
三问决定一切
特征提取 φ
原始输入 → 特征向量
对参数线性 ≠ 对x线性
多项式特征可拟合曲线
假设类
内积 w·φ
权重向量 w
偏置并入 φ₁≡1
损失
平方损失 = 残差²
训练损失 取平均
梯度 = 2·残差·φ
优化
梯度下降 全量慢
SGD 单样本快
mini-batch 最常用
学习率/迭代/收敛
泛化
训练误差 vs 测试误差
欠拟合/过拟合
train/val/test 划分
关键要点:
- 监督学习 = 假设类 + 损失函数 + 优化算法——这条三段式模板贯穿整门课,本讲只是第一次填零件。
- 特征提取 $\phi(x)$ 是本讲相对上一讲的真正新意:把原始输入变成实数向量,线性回归靠非线性特征就能拟合曲线(「线性」是对 $w$ 而言)。
- 假设类写成内积 $f_w(x) = w \cdot \phi(x)$;把偏置约定成 $\phi_1 \equiv 1$ 让公式统一。
- 平方损失处处可微,其单样本梯度是好记的 $2 \cdot r \cdot \phi(x)$($r$ 为残差);训练损失取平均,把「找最优预测器」变成优化题。
- SGD 用单个/小批样本估计梯度(无偏但带噪),牺牲每步精度换更新频率,是大数据下训练的默认选择;学习率是速度与稳定的权衡。
- 学习的目标是泛化:警惕过拟合,靠 train / val / test 划分诚实地评估——测试集只看最后一眼。
下一讲预告 Lecture 3 · 线性分类:把输出从实数换成离散类别,就从回归走到了线性分类——同一套 $w \cdot \phi(x)$ 加个阈值 / softmax,损失函数从平方损失换成 logistic 损失 / 交叉熵,并顺带讲文本如何变成张量(分词 → 词表 → 词袋;分词的工业级做法见 CS336 的 Lecture 1 · 概览与分词(未找到对应页面))。
复习自测
题目
线性是对参数 $w$ 而言:固定 $\phi(x)$ 后,$f_w(x) = w \cdot \phi(x)$ 关于 $w$ 是线性的,损失面因此是凸的。$w_0 + w_1 x + w_2 x^2$ 只是取了 $\phi(x) = [1, x, x^2]$——它对 $x$ 是抛物线,对 $w$ 依然线性,所以优化难度与拟合直线毫无区别。
题目
记残差 $r = w \cdot \phi(x) - y$。由链式法则 $\nabla_w r^2 = 2r \cdot \nabla_w r$;而 $r$ 对 $w$ 的梯度是 $\nabla_w (w \cdot \phi(x) - y) = \phi(x)$。所以 $\nabla_w \ell = 2\,(w \cdot \phi(x) - y)\,\phi(x)$,即「2 · 残差 · 特征向量」。
题目
预测 $w \cdot \phi(x) = 0$,残差 $r = 0 - 4 = -4$;梯度 $= 2 \times (-4) \times [1, 1] = [-8, -8]$;更新 $w \leftarrow [0,0] - 0.1 \times [-8,-8] = [0.8,\ 0.8]$。预测偏低(残差为负),所以两个分量都被往上调。
题目
GD 用全部样本算一次平均梯度,每 epoch 只更新 1 次;SGD 每个样本更新一次,每 epoch 更新 100 万次。两者每 epoch 算的单样本梯度总数相同,但 SGD 把同样的算力换成了多得多的参数更新——虽然每步方向带噪声(无偏估计),整体收敛远快于 GD,这正是大数据时代 SGD 成为默认选择的原因。
题目
验证集可以反复使用,用来比较超参数与模型方案;测试集只在最后用一次,给出泛化误差的无偏估计。若用测试集挑学习率,你就是在测试集上做了一次「选择最小者」的优化——选出的配置部分是因为恰好迎合了测试集的随机波动,因此其测试成绩系统性高估真实泛化能力,测试集也从此失去公正裁判的资格。
参考资料
- 💻
linear_regression.py— 本讲代码均出自此文件(Example1D/Parameters1D/compute_loss/compute_train_loss/compute_grad_loss/gradient_descent) - 💻 stanford-cs221/autumn2025-lectures — 可执行讲义仓库
- 📖 Autumn 2023 讲义 · 线性回归模块(图文版)
- 📖 Autumn 2023 讲义 · 特征提取模块
- 📄 DeepSeek-V3 技术报告(真实世界的海量参数示例) · 权重文件清单
- 📄 Bottou, Large-Scale Machine Learning with SGD(2010)
- 🌐 课程主页 Autumn 2025