# Lecture 2 · 线性回归

> **CS221: Artificial Intelligence — Principles and Techniques** · Stanford · Autumn 2025
> 📅 Sep 24 · 💻 [`linear_regression.py`](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/linear_regression.py)

---

## 承上启下

**上一讲 [[Lecture 1 · 张量、梯度与监督学习]]**：

- **张量**是数据、参数、梯度的统一表示，用少量张量运算把计算写快；
- **梯度 / 反向传播**：计算图 + 链式法则，自动求出「往哪走损失变小」；
- 末尾端到端跑通了一个 **1 维线性回归**的玩具例子（`weight * x + bias`），但没有停下来审视其中的每个部件。

**本讲（线性回归 · Learning I）** 把那个例子拆开重装成一条**可复用的流水线**：

- 引入**特征提取 $\phi(x)$**：让同一套线性机器去拟合**曲线**、处理文本/图像；
- 把假设类统一写成向量内积 $f_w(x) = w \cdot \phi(x)$；
- 从**梯度下降**升级到**随机梯度下降（SGD）**——回答大数据下怎么训练；
- 关心一个新问题：模型在**没见过的数据**上好不好（**泛化**）。

**下一讲 [[Lecture 3 · 线性分类]]**：输出从实数换成离散类别，同一套 $w \cdot \phi(x)$ 换损失函数就变成分类器。

> [!note] 一句话总览
> **监督学习 = 假设类 + 损失函数 + 优化算法**；线性回归 = 「权重向量 · 特征向量」，靠 SGD 拟合，靠泛化验收。

这一讲对应源文件 `linear_regression.py` 的叙事主线是 `main()` 里依次调用的
`prediction_task → machine_learning_problem → hypothesis_class → loss_function → optimization_algorithm`。

---

## 1. 监督学习的一般框架（回顾并抬升）

[[Lecture 1 · 张量、梯度与监督学习]] 已经把这条流水线跑通过一遍，这里把它**抽象成通用模板**，之后每一讲（分类、深度学习……）都会往里填不同的零件。

```mermaid
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 分）。

```python
# 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）**是一个 (输入, 输出) 对：

```python
# 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） |

> [!tip] 直觉
> 为什么要把学习拆成三个正交的部件？因为这样每个部件可以**独立替换**、独立分析：[[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）**——对输入某方面性质的一个数值刻画。

```mermaid
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$ 就是「每平方米的边际价格」）。

> [!note] 把偏置塞进特征里
> 约定 $\phi_1(x) \equiv 1$，那么 $w_1$ 就自动扮演偏置 $b$ 的角色。这样 $f(x) = w \cdot \phi(x)$ 一个内积就同时含了权重和偏置，公式更干净——后面推导都用这个约定。上一讲代码里的 `np.array([example.input, 1])` 正是这个技巧的一维版本。

### 2.2 关键洞见：线性回归可以拟合非线性曲线

「线性回归」的**线性**指的是**对参数 $w$ 线性**，不是对原始输入 $x$ 线性。只要把非线性的东西塞进 $\phi$ 里，同一台线性机器就能画曲线：

```python
# 想拟合一条抛物线？把 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$ 的函数仍是线性的——损失面依旧是凸的碗形，优化不会变难。

```mermaid
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
```

> [!tip] 直觉
> 这就是**特征工程（feature engineering）**的威力：模型结构（线性）一直没变，**变的是 $\phi$**——预测能力的上限其实由特征决定。选对特征往往比换更复杂的模型更管用。深度学习的另一条路是：干脆让网络**自己学出 $\phi$**（[[Lecture 4 · 深度学习]] 的核心思想，神经网络的前几层就是可学习的特征提取器）。

---

## 3. 假设类：f_w(x) = w · φ(x)

把 §2 的想法固化成假设类。源码在 1 维情形下写成 `weight * x + bias`；一般化到 $d$ 维就是权重向量与特征向量的内积。

```python
# 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$）：

```python
# 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*）。

```mermaid
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
```

> [!tip] 直觉
> 假设类是在**学习开始之前**就划定的「可能性范围」：它注入了我们对问题的先验（比如「输出大致随特征线性变化」），同时也限定了模型能力的天花板——学习永远不可能学出假设类之外的函数。假设类太小会欠拟合、太大会过拟合（§6），所以它是第一个、也是最重要的设计决策。

---

## 4. 损失函数：平方损失与训练损失

第二个设计决策——**怎么给每个预测器打分**。回归任务用**平方损失（squared loss）**：预测值离真值差多远，取残差的平方。

### 4.1 单样本损失

单个样本 $(x, y)$ 上的平方损失：

$$\ell(w; x, y) = \big(\underbrace{w \cdot \phi(x) - y}_{\text{残差 } r}\big)^2$$

```python
# 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$ 个样本的平方损失；取平均使得这个数字的量纲不随数据集大小变化，不同规模的数据集之间可以直接比较。

```python
# 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
```

> [!example] 数值演算：比较两个预测器
> 用训练数据 $(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$。
>
> 后者训练损失更高，**所以更差**——损失函数把「哪个预测器好」变成了一个可比较的数字。

> [!note]
> 有了训练损失，「找最好的预测器」正式变成一道**优化题**：$\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)})$$

```python
# 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
```

> [!tip] 直觉
> 一般形式记牢这条：对平方损失，**单样本梯度 = 2 · 残差 · $\phi(x)$**。它的结构很有讲头——残差决定**幅度与方向**（预测偏高就把 $w$ 往回拽），$\phi(x)$ 决定**责任分配**（哪个特征大，哪个特征的权重就被调得多）。源码里 `np.array([example.input, 1])` 正是 1 维情形的 $\phi(x) = [x, 1]$。

完整的梯度下降循环（源码 `gradient_descent()`）：

```python
# 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 都在下降
```

```mermaid
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
```

> [!note] 延伸：其实线性回归有闭式解
> 无正则的最小二乘存在解析解（正规方程，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$ 个样本求平均）换成便宜的单样本梯度。

```python
# 随机梯度下降（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 次 |
| 梯度噪声 | 无（精确） | 大 | 中 |
| 收敛轨迹 | 平滑 | 抖动 | 较平滑 |
| 大数据适用性 | 差 | 好 | **最常用** |

```mermaid
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
```

> [!warning] 易错点
> SGD 的噪声不全是坏事：它能帮参数**跳出**尖锐的坏局部极小/鞍点，这在深度学习（非凸）里反而有益。噪声太大导致损失不降时，靠**减小学习率**或**增大 batch** 来压。不要看到损失曲线抖动就断定训练坏了——SGD 的损失曲线本来就该抖。

### 5.5 超参数：学习率、迭代、收敛

源码结尾的三条 Notes 正是这一节的精华：

> [!note] 源码 `optimization_algorithm()` 的原话（意译）
> - 学习率控制「你开多快」——**速度 vs 稳定性**的权衡；
> - 对**凸函数**保证收敛；**深度学习（非凸）没有保证**，但实践中有效；
> - 其他算法：**随机梯度下降 SGD、Adam**。

| 超参数（hyperparameter） | 作用 | 太小 | 太大 |
| :--- | :--- | :--- | :--- |
| **学习率 η** | 每步走多远 | 收敛慢、卡住 | 震荡、发散 |
| **迭代步数 / epochs** | 训练多久 | 欠拟合（没学够） | 浪费算力、可能过拟合 |
| **batch 大小 B** | 每步样本数 | 抖、慢（更新多） | 吃内存、更新粗 |

```mermaid
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
```

> [!note] 收敛（convergence）
> 收敛指损失不再明显下降。判据可以是「梯度接近 0」「损失变化小于阈值」或「到达最大步数」。凸问题（如无正则的线性回归——平方损失对 $w$ 是凸的）梯度下降能保证收敛到**全局最优**；非凸问题只能保证走到梯度为 0 的驻点附近。「超参数」与「参数」的分界：参数 $w$ 由优化算法学出来，超参数（$\eta$、epochs、$B$、$\phi$ 的选择）由人在训练前设定——怎么科学地选超参数，正是 §6.3 验证集存在的意义。

---

## 6. 泛化：训练误差 vs 测试误差

到这里我们只在**训练集**上把损失压低了。但学习的真正目标不是背下训练数据，而是**在没见过的新数据上也预测得准**——这就是**泛化（generalization）**。

> [!tip] 直觉
> 为什么「训练损失低」不等于「学得好」？极端反例：一个把训练集完整背下来的查表预测器，训练误差恰好为 0，但对任何新输入束手无策。训练误差衡量的是**记忆**，测试误差衡量的才是**规律**。机器学习与单纯优化的分野就在这里：优化只关心把目标压到最低，学习还要求压低的方式能**迁移**到新数据。

### 6.1 两种误差

| 误差 | 在哪算 | 衡量什么 |
| :--- | :--- | :--- |
| **训练误差**（training error） | 训练集 | 拟合已见数据的程度（易乐观） |
| **测试误差**（test error） | 独立的测试集 | **真正关心**的泛化能力 |

> [!warning] 易错点
> 只看训练损失会被骗。一个只会「死记硬背」的模型训练误差可以是 0，但换新数据就崩——这叫**过拟合（overfitting）**。

### 6.2 欠拟合 vs 过拟合

用 §2 的多项式特征做直觉：**$\phi$ 里放几阶多项式** = 模型复杂度旋钮。

```mermaid
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$ 的阶数、学习率……）？把数据切三份：

```mermaid
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） | 估计最终泛化误差 | **只在最后看一次**，绝不用来调任何东西 |

> [!warning] 铁律：测试集是「圣物」
> 任何一次用测试集去做选择（哪怕只是挑学习率），它就被「污染」了：你实际上在往测试集上「拟合」超参数，报出来的数字会系统性偏乐观，不再是对真实泛化误差的无偏估计。对付过拟合的常规武器还有：正则化、早停（early stopping）、加数据。

---

## 7. 总结

```mermaid
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 · 概览与分词]]）。

---

## 复习自测

> [!question]- Q1：「线性回归」的「线性」是对什么而言的？为什么 $f_w(x) = w_0 + w_1 x + w_2 x^2$ 仍算线性回归？
> 线性是**对参数 $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$ 依然线性，所以优化难度与拟合直线毫无区别。

> [!question]- Q2：从 $\ell(w) = (w \cdot \phi(x) - y)^2$ 出发，推导 $\nabla_w \ell$。
> 记残差 $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 · 残差 · 特征向量」。

> [!question]- Q3：算一算：$\phi(x) = [x, 1]$，样本 $(x, y) = (1, 4)$，当前 $w = [0, 0]$，学习率 $\eta = 0.1$，做一步 SGD 后 $w$ 是多少？
> 预测 $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]$。预测偏低（残差为负），所以两个分量都被往上调。

> [!question]- Q4：训练集有 100 万个样本。扫一遍数据（一个 epoch），GD 和 SGD 各做几次参数更新？为什么说同等算力下 SGD 进展更快？
> GD 用全部样本算一次平均梯度，每 epoch 只更新 **1 次**；SGD 每个样本更新一次，每 epoch 更新 **100 万次**。两者每 epoch 算的单样本梯度总数相同，但 SGD 把同样的算力换成了多得多的参数更新——虽然每步方向带噪声（无偏估计），整体收敛远快于 GD，这正是大数据时代 SGD 成为默认选择的原因。

> [!question]- Q5：验证集和测试集都不参与训练 $w$，它们的区别是什么？为什么用测试集挑学习率会导致汇报的性能偏乐观？
> 验证集可以**反复使用**，用来比较超参数与模型方案；测试集**只在最后用一次**，给出泛化误差的无偏估计。若用测试集挑学习率，你就是在测试集上做了一次「选择最小者」的优化——选出的配置部分是因为恰好迎合了测试集的随机波动，因此其测试成绩系统性高估真实泛化能力，测试集也从此失去公正裁判的资格。

---

## 参考资料

- 💻 [`linear_regression.py`](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/linear_regression.py) — 本讲代码均出自此文件（`Example1D` / `Parameters1D` / `compute_loss` / `compute_train_loss` / `compute_grad_loss` / `gradient_descent`）
- 💻 [stanford-cs221/autumn2025-lectures](https://github.com/stanford-cs221/autumn2025-lectures) — 可执行讲义仓库
- 📖 [Autumn 2023 讲义 · 线性回归模块（图文版）](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=machine-learning%2Flinear-regression.js&mode=print6pp)
- 📖 [Autumn 2023 讲义 · 特征提取模块](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=machine-learning%2Ffeatures.js&mode=print6pp)
- 📄 [DeepSeek-V3 技术报告（真实世界的海量参数示例）](https://arxiv.org/abs/2412.19437) · [权重文件清单](https://huggingface.co/deepseek-ai/DeepSeek-V3?show_file_info=model.safetensors.index.json)
- 📄 [Bottou, *Large-Scale Machine Learning with SGD*（2010）](https://leon.bottou.org/publications/pdf/compstat-2010.pdf)
- 🌐 [课程主页 Autumn 2025](https://stanford-cs221.github.io/autumn2025/)
