Files
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

409 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 梯度机器学习
*基于梯度的学习通过沿着损失曲面的斜率迭代优化模型参数。本文涵盖线性回归、逻辑回归、softmax分类、梯度下降变体、正则化(L1/L2)和偏差-方差权衡*
- 第01篇中的经典方法使用巧妙的启发式或闭式解。本文涵盖通过沿着梯度学习、在损失曲面上小步下坡直到找到好参数的算法。基于梯度的学习是从线性回归到最大神经网络的一切背后的引擎。
- **线性回归**是最简单的基于梯度的模型,它也有闭式解,这使其成为完美的起点。模型是一条直线(或更高维的超平面):
$$\hat{y} = w \cdot x + b = \sum_{i=1}^{d} w_i x_i + b$$
- 用矩阵符号(来自第02章),如果我们将所有训练输入堆叠为矩阵 $X$ 的行,并通过追加一列1将偏置吸收到 $w$ 中,这就变成了 $\hat{y} = Xw$。
- 目标是最小化**均方误差(MSE)**,即预测值与实际值之间平均平方差:
$$\mathcal{L}(w) = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 = \frac{1}{n} \|y - Xw\|^2$$
- 为什么采用平方误差?它有概率论上的依据:如果你假设目标值由 $y = Xw + \epsilon$ 生成,其中 $\epsilon \sim \mathcal{N}(0, \sigma^2)$,那么最大化数据的高斯似然(第05章)等价于最小化MSE。平方误差还比小错误更严厉地惩罚大错误,这通常是可取的。
![具有最佳拟合线和显示误差的虚线垂直残差线的数据点散点图](../images/linear_regression_fit.svg)
- 由于MSE是 $w$ 的二次函数,它具有唯一的全局最小值,我们可以通过解析方法找到。求导、设为零并求解,得到**正规方程**:
$$w^{*} = (X^T X)^{-1} X^T y$$
- 这直接使用了第02章的矩阵逆运算。表达式 $X^T X$ 是一个 $d \times d$ 矩阵(其中 $d$ 是特征数量),$X^T y$ 是一个 $d$ 维向量。正规方程一次性给出精确的最优权重。
- 正规方程何时失效?当 $X^T X$ 奇异(不可逆)时,这发生在特征线性相关或特征数量多于样本数量($d > n$)的情况下。在这些情况下,你需要正则化(后续介绍)或梯度下降。
- **逻辑回归**将线性模型适用于二元分类。我们不预测连续值,而是想要一个介于0和1之间的概率。**Sigmoid函数**将所有实数压缩到这个范围内:
$$\sigma(z) = \frac{1}{1 + e^{-z}}$$
- 模型计算 $z = w \cdot x + b$(线性得分,与线性回归相同),然后将其通过sigmoid$\hat{y} = \sigma(w \cdot x + b)$。输出 $\hat{y}$ 被解释为 $P(y = 1 \mid x)$。
![带有0.5阈值标记的Sigmoid曲线,显示预测0和预测1的分类区域](../images/sigmoid_logistic.svg)
- Sigmoid具有良好的性质:$\sigma(0) = 0.5$$\sigma(z) \to 1$ 当 $z \to \infty$$\sigma(z) \to 0$ 当 $z \to -\infty$,且其导数具有优雅的形式 $\sigma'(z) = \sigma(z)(1 - \sigma(z))$。
- 逻辑回归的损失函数是**二元交叉熵(BCE)**,直接来自于伯努利似然(第05章):
$$\mathcal{L} = -\frac{1}{n} \sum_{i=1}^{n} \left[ y_i \log(\hat{y}_i) + (1 - y_i) \log(1 - \hat{y}_i) \right]$$
- 当真实标签为1时,只有第一项起作用,它惩罚过低的预测。当真实标签为0时,只有第二项起作用,它惩罚过高的预测。对数使得对于自信的错误预测,惩罚极其陡峭:当真实标签为1时预测0.01,代价远高于预测0.4。
- 与线性回归的MSE不同,BCE最小化权重没有闭式解。我们需要一种迭代方法:**梯度下降**。
- 梯度下降的直觉很简单:想象你身处大雾中的丘陵地带(损失曲面)。你看不到全局最小值,但可以感受到脚下的坡度。你向下坡走一步,再次感受坡度,然后重复。最终你到达一个山谷。
$$w \leftarrow w - \eta \frac{\partial \mathcal{L}}{\partial w}$$
- 学习率 $\eta$ 控制你的步长。太大则越过山谷,来回弹跳而不收敛。太小则缓慢前行,可能陷入局部最小值。
![一维损失曲线,三个球:大学习率越过,好学习率收敛,小学习率卡住](../images/gradient_descent_landscape.svg)
- 梯度 $\frac{\partial \mathcal{L}}{\partial w}$ 是一个指向最陡上升方向的向量。我们减去它是因为想向下坡走。这是第03章中的链式法则应用于损失函数。
- **批量梯度下降**每一步使用整个训练集计算梯度。这给出精确梯度,但当 $n$ 很大时计算代价高昂。
- **随机梯度下降(SGD)** 每一步使用单个随机样本。梯度带有噪声(它从一个样本估计真实梯度),但每一步非常快。噪声实际上可以帮助逃离浅的局部极小值。
- **小批量梯度下降**折中:每一步使用 $B$ 个样本的批次(通常为32、64或256)。这平衡了计算效率(对批次的向量化操作)与梯度质量。几乎所有深度学习都使用小批量SGD。
- **反向传播**是我们实际计算具有许多参数的模型(如神经网络)中梯度的方法。它是第03章的链式法则通过计算图系统化地应用。
- 任何模型都可以表示为操作的有向无环图:输入流入,乘以权重,加在一起,通过非线性函数传递,最终产生损失值。**前向传播**通过让数据从输入到输出流经此图来计算输出(和损失)。
- **反向传播**反向流动梯度。从损失开始,你使用每个节点的链式法则计算损失相对于每个中间值的变化。如果 $L$ 依赖于 $z$,而 $z$ 依赖于 $w$,则:
$$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial z} \cdot \frac{\partial z}{\partial w}$$
- 每个节点只需要知道自己的局部导数和从上方流入的梯度。这使得反向传播模块化且高效:代价大约是前向传播的两倍(一次前向,一次反向)。
- 原始SGD有一个问题:它在陡峭曲率方向上振荡,而在平坦方向上进展缓慢。**优化器**通过根据梯度历史调整步长来改进这一点。
- **带动量的SGD**维护过去梯度的运行平均值(指数移动平均,来自第04章)。这平滑了振荡并加速了沿一致方向的进展:
$$v_t = \beta v_{t-1} + (1 - \beta) \nabla \mathcal{L}$$
$$w \leftarrow w - \eta \, v_t$$
- 想象一个滚下山的球:动量让它沿一致方向积累速度并抑制侧向抖动。典型值为 $\beta = 0.9$。
- **内斯特罗夫加速梯度(NAG)** 是一个小巧但巧妙的调整:不在当前位置计算梯度,而是在"前瞻"位置 $w - \eta \beta v_{t-1}$ 计算梯度。这一修正步骤减少了过冲:
$$v_t = \beta \, v_{t-1} + \nabla \mathcal{L}(w - \eta \beta \, v_{t-1})$$
$$w \leftarrow w - \eta \, v_t$$
- **Adagrad** 为每个参数调整学习率。接收大梯度的参数获得较小的学习率,反之亦然。它累积平方梯度:
$$G_t = G_{t-1} + g_t^2, \quad w \leftarrow w - \frac{\eta}{\sqrt{G_t + \epsilon}} g_t$$
- 问题在于:$G_t$ 只增不减,因此有效学习率单调递减,最终变得太小而无法学习任何东西。
- **RMSprop** 通过使用平方梯度的指数移动平均而非求和来修复此问题,使得近期梯度比早期梯度更重要:
$$s_t = \beta \, s_{t-1} + (1 - \beta) g_t^2, \quad w \leftarrow w - \frac{\eta}{\sqrt{s_t + \epsilon}} g_t$$
- **Adam**(自适应矩估计)结合了动量和RMSprop。它同时维护一阶矩估计(梯度的均值,像动量)和二阶矩估计(平方梯度的均值,像RMSprop):
$$m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t$$
$$v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2$$
- 由于 $m_t$ 和 $v_t$ 初始化为零,它们在早期步骤中有偏近于零。偏差修正解决了这个问题:
$$\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}$$
$$w \leftarrow w - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t$$
![二维等高线图显示SGD呈锯齿形、动量沿更平滑路径、Adam走最直接路线到达最小值](../images/optimizer_trajectories.svg)
- 默认超参数($\beta_1 = 0.9$, $\beta_2 = 0.999$, $\epsilon = 10^{-8}$)在广泛的问题上表现良好,这就是为什么Adam是大多数深度学习工作中的默认优化器。
- **AdamW** 将权重衰减与梯度更新解耦。标准L2正则化和权重衰减对于SGD是等价的,但对于Adam则不然。AdamW直接将权重衰减应用于参数,而不是将 $\lambda w$ 加到梯度上。这带来了更好的泛化性能,现在是Transformer训练的标准:
$$w \leftarrow w - \eta \left( \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda \, w \right)$$
- **LION**(演化符号动量)是通过程序搜索发现的新优化器。它只使用动量更新的符号(而不是幅度),使得每次更新的尺度均匀。LION比Adam使用更少的内存(没有二阶矩缓冲区),并且在许多任务上可以匹配或超越Adam:
$$w \leftarrow w - \eta \cdot \text{sign}(\beta_1 \, m_{t-1} + (1 - \beta_1) \, g_t)$$
$$m_t = \beta_2 \, m_{t-1} + (1 - \beta_2) \, g_t$$
- **Muon**(动量 + 正交化)应用内斯特罗夫动量,然后使用Newton-Schulz迭代对更新矩阵进行正交化,该迭代近似极分解。得到的更新方向位于Stiefel流形上,每次更新在所有奇异方向上具有大致相等的幅度,防止任何单一方向主导。这消除了对自适应二阶矩估计(如Adam的 $v_t$ 缓冲区)的需求,减少了内存使用。Muon在Transformer训练中表现出色,通常以更快的收敛速度达到与AdamW相当的质量,尤其适用于注意力矩阵和MLP权重矩阵。嵌入层和输出层通常仍由AdamW处理。
$$G_t = \text{NesterovMomentum}(\nabla \mathcal{L})$$
$$U_t = \text{NewtonSchulz}(G_t) \approx G_t (G_t^T G_t)^{-1/2}$$
$$W \leftarrow W - \eta \, U_t$$
- Newton-Schulz迭代通过重复 $X_{k+1} = \frac{1}{2} X_k (3I - X_k^T X_k)$ 几个步骤(通常5-10步)来计算正交因子。这避免了完整SVD的计算代价,同时提供了良好的近似。
![Muon正交化:动量更新具有偏斜的奇异值,Newton-Schulz迭代使它们均衡,所有方向均匀更新](../images/optimizer_muon.svg)
![优化器内存比较:每个优化器在每个参数上存储的内容](../images/optimizer_memory.svg)
- 除了MSE和BCE之外,还有几种常用的**损失函数**。
- **平均绝对误差(MAE)**,或L1损失,取绝对差的平均值:$\frac{1}{n}\sum|y_i - \hat{y}_i|$。它对异常值比MSE更鲁棒,因为它不对大误差进行平方。
- **Huber损失**结合了两者的优点:对于小误差表现像MSE(平滑,易于优化),对于大误差表现像MAE(对异常值鲁棒)。它有一个控制过渡的阈值 $\delta$。
- **分类交叉熵(CCE)** 将BCE推广到多个类别。如果 $\hat{y}_k$ 是类别 $k$ 的预测概率,真实类别为 $c$:
$$\mathcal{L} = -\log(\hat{y}_c)$$
- 这只是正确类别的负对数概率。最小化交叉熵等价于最大化似然,这联系到第05章的信息论:交叉熵衡量当你使用预测分布代替真实分布时需要多少额外比特。
- **Hinge损失** 被SVM使用:$\mathcal{L} = \max(0, 1 - y \cdot f(x))$。它只惩罚在间隔错误一侧或间隔内的预测。一旦一个点被足够置信地正确分类,损失为零。
- **正则化**通过添加对复杂模型的惩罚来防止过拟合。正则化后的损失为:
$$\mathcal{L}_{\text{reg}} = \mathcal{L}_{\text{data}} + \lambda \, R(w)$$
- **L2正则化**(Ridge,权重衰减)惩罚平方权重之和:$R(w) = \|w\|^2 = \sum w_i^2$。它阻止任何单个权重变得过大,有效地将所有权重向零收缩,但很少使它们精确为零。
- **L1正则化**Lasso)惩罚绝对权重之和:$R(w) = \|w\|_1 = \sum |w_i|$。它鼓励稀疏性,将许多权重驱动到精确为零,实现自动特征选择。
- **弹性网络** 结合了两者:$R(w) = \alpha \|w\|_1 + (1 - \alpha) \|w\|^2$,融合了稀疏性和收缩。
- 有一个优美的贝叶斯解释(来自第05章)。L2正则化等价于在权重上放置高斯先验并寻找MAP估计。L1正则化对应于拉普拉斯先验。正则化强度 $\lambda$ 控制你相对于数据信任先验的程度。
- **评估指标**告诉你模型是否真正有效。对于回归,MSE和MAE是标准指标。对于分类,情况更为微妙。
- **混淆矩阵**是一个二元分类的四格表:
- 真正例(TP):预测为正,实际为正
- 假正例(FP):预测为正,实际为负
- 真负例(TN):预测为负,实际为负
- 假负例(FN):预测为负,实际为正
- **准确率** = $\frac{TP + TN}{TP + TN + FP + FN}$ 在类别不平衡时可能具有误导性。如果99%的电子邮件不是垃圾邮件,一个总是预测"非垃圾邮件"的模型有99%的准确率,但没有用处。
- **精确率** = $\frac{TP}{TP + FP}$ 回答:在所有预测为正的样本中,有多少实际为正?高精确率意味着误报少。
- **召回率**(敏感度)= $\frac{TP}{TP + FN}$ 回答:在所有实际为正的样本中,你捕获了多少?高召回率意味着漏检少。
- **F1分数** = $\frac{2 \cdot \text{precision} \cdot \text{recall}}{\text{precision} + \text{recall}}$ 是精确率和召回率的调和平均数,平衡了两者。
- **ROC曲线**绘制了真正率(召回率)对假正率($\frac{FP}{FP + TN}$)随分类阈值从0到1变化的曲线。完美分类器紧贴左上角。**AUC**(ROC曲线下面积)用一个数字概括性能:1.0为完美,0.5为随机猜测。
- **交叉验证**提供了更可靠的泛化性能估计。在 $k$ 折交叉验证中,你将数据分成 $k$ 份,在 $k-1$ 份上训练,在剩余一份上测试,然后轮换。所有 $k$ 折的平均测试性能就是你的估计。这使用了所有数据进行训练和测试(只是不在同一时间),在数据稀缺时尤为宝贵。
- **偏差-方差权衡**(来自第04章)是ML中的基本张力。模型期望误差分解为:
$$\text{Error} = \text{Bias}^2 + \text{Variance} + \text{Irreducible Noise}$$
- **偏差**是错误假设带来的系统性误差(例如,用直线拟合曲线数据)。**方差**是对训练数据波动的敏感度(例如,20次多项式拟合噪声)。简单模型具有高偏差和低方差;复杂模型具有低偏差和高方差。最优在两者之间。
- **学习率调度**在训练期间调整 $\eta$。常见策略:
- 步长衰减:每 $N$ 个epoch将 $\eta$ 乘以一个因子(如0.1)
- 余弦退火:按照余弦曲线从初始值平滑降低 $\eta$ 到接近零
- 预热:从一个非常小的 $\eta$ 开始,在前几千步线性增加,然后衰减。这防止了大的初始梯度破坏训练稳定性
- 1cycle:一个先升后降的余弦周期,可以带来更快的收敛
- **超参数调优**是找到学习率、批量大小、正则化强度和其他不由梯度下降学习的设置的良好值的过程。常用方法:
- 网格搜索:在预定义的网格上尝试每一种组合(穷举但代价高)
- 随机搜索:随机采样组合,通常更高效,因为并非所有超参数同等重要
- 贝叶斯优化:构建目标函数的模型并智能选择下一个要尝试的超参数
- **ASHA**(异步连续减半算法):使用小预算并行运行许多试验,然后将最有希望的提升到更大预算,同时及早终止其余试验。它结合了早停的高效性和大规模并行性——不是运行100次完整的训练,而是廉价地启动所有100次,在每级保留前四分之一,只有少数运行到完成。这是现代大规模调优框架(如Ray Tune)的骨干。
- **无调度学习**完全消除了对学习率调度的需求。它不是在固定曲线上衰减 $\eta$,而是维护两个序列:一个缓慢移动的迭代平均值 $z_t$(收敛到最优值)和一个快速探索的迭代 $y_t$(在其上评估梯度)。最终输出是平均序列,被证明在事后能匹配最佳调度的收敛速度。这完全消除了调度作为一个超参数——你只需设置基础学习率,优化器处理其余部分。SGD和Adam的无调度变体已被证明能达到或超越其经过调度的对应版本。
## 编程任务(在CoLab或笔记本中完成)
1. 使用正规方程和梯度下降两种方法实现线性回归。比较求解结果,并绘制GD损失随迭代的收敛曲线。
```python
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
# 生成合成数据:y = 3x + 2 + noise
key = jax.random.PRNGKey(42)
n = 100
X = jax.random.uniform(key, (n, 1), minval=0, maxval=10)
y = 3 * X[:, 0] + 2 + jax.random.normal(key, (n,)) * 1.5
# 添加偏置列
X_b = jnp.column_stack([X, jnp.ones(n)])
# 正规方程
w_exact = jnp.linalg.solve(X_b.T @ X_b, X_b.T @ y)
print(f"Normal equation: w={w_exact[0]:.4f}, b={w_exact[1]:.4f}")
# 梯度下降
w_gd = jnp.zeros(2)
lr = 0.005
losses = []
for step in range(500):
pred = X_b @ w_gd
error = pred - y
loss = jnp.mean(error ** 2)
losses.append(float(loss))
grad = (2 / n) * X_b.T @ error
w_gd = w_gd - lr * grad
print(f"Gradient descent: w={w_gd[0]:.4f}, b={w_gd[1]:.4f}")
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].scatter(X[:, 0], y, s=15, alpha=0.5, color='#3498db')
axes[0].plot([0, 10], [w_exact[1], w_exact[0]*10 + w_exact[1]], color='#e74c3c', linewidth=2)
axes[0].set_title("Linear Regression Fit")
axes[0].set_xlabel("x"); axes[0].set_ylabel("y")
axes[1].plot(losses, color='#27ae60', linewidth=1.5)
axes[1].set_title("GD Loss Convergence")
axes[1].set_xlabel("Step"); axes[1].set_ylabel("MSE")
axes[1].set_yscale('log')
plt.tight_layout()
plt.show()
```
2. 从头实现带梯度下降的逻辑回归。在二维数据集上训练并可视化学习到的决策边界。
```python
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
# 生成数据
X, y = make_moons(n_samples=300, noise=0.2, random_state=42)
X, y = jnp.array(X), jnp.array(y, dtype=jnp.float32)
def sigmoid(z):
return 1 / (1 + jnp.exp(-z))
# 添加偏置列
X_b = jnp.column_stack([X, jnp.ones(len(X))])
w = jnp.zeros(3)
lr = 0.5
losses = []
for step in range(2000):
z = X_b @ w
pred = sigmoid(z)
# BCE损失
loss = -jnp.mean(y * jnp.log(pred + 1e-8) + (1 - y) * jnp.log(1 - pred + 1e-8))
losses.append(float(loss))
# 梯度
grad = X_b.T @ (pred - y) / len(y)
w = w - lr * grad
# 决策边界
xx, yy = jnp.meshgrid(jnp.linspace(-2, 3, 200), jnp.linspace(-1.5, 2, 200))
grid = jnp.column_stack([xx.ravel(), yy.ravel(), jnp.ones(xx.size)])
zz = sigmoid(grid @ w).reshape(xx.shape)
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, zz, levels=[0, 0.5, 1], alpha=0.3, colors=['#e74c3c', '#3498db'])
plt.contour(xx, yy, zz, levels=[0.5], colors='#9b59b6', linewidths=2)
plt.scatter(X[y==0, 0], X[y==0, 1], c='#e74c3c', s=15, label='Class 0')
plt.scatter(X[y==1, 0], X[y==1, 1], c='#3498db', s=15, label='Class 1')
plt.title("Logistic Regression Decision Boundary")
plt.legend()
plt.grid(alpha=0.3)
plt.show()
```
3. 在二维二次曲面上比较优化器的轨迹。从相同的起点运行SGD、SGD+Momentum和Adam,绘制它们的路径。
```python
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
# 拉长的二次曲面:L(w1, w2) = 0.5*w1^2 + 10*w2^2
def loss_fn(w):
return 0.5 * w[0]**2 + 10 * w[1]**2
grad_fn = jax.grad(loss_fn)
def run_sgd(w0, lr=0.05, steps=80):
w = w0.copy()
path = [w.copy()]
for _ in range(steps):
g = grad_fn(w)
w = w - lr * g
path.append(w.copy())
return jnp.stack(path)
def run_momentum(w0, lr=0.05, beta=0.9, steps=80):
w, v = w0.copy(), jnp.zeros(2)
path = [w.copy()]
for _ in range(steps):
g = grad_fn(w)
v = beta * v + (1 - beta) * g
w = w - lr * v
path.append(w.copy())
return jnp.stack(path)
def run_adam(w0, lr=0.05, b1=0.9, b2=0.999, eps=1e-8, steps=80):
w, m, v = w0.copy(), jnp.zeros(2), jnp.zeros(2)
path = [w.copy()]
for t in range(1, steps + 1):
g = grad_fn(w)
m = b1 * m + (1 - b1) * g
v = b2 * v + (1 - b2) * g**2
m_hat = m / (1 - b1**t)
v_hat = v / (1 - b2**t)
w = w - lr * m_hat / (jnp.sqrt(v_hat) + eps)
path.append(w.copy())
return jnp.stack(path)
w0 = jnp.array([8.0, 3.0])
sgd_path = run_sgd(w0)
mom_path = run_momentum(w0)
adam_path = run_adam(w0)
# 绘图
fig, ax = plt.subplots(figsize=(8, 6))
w1 = jnp.linspace(-10, 10, 100)
w2 = jnp.linspace(-4, 4, 100)
W1, W2 = jnp.meshgrid(w1, w2)
L = 0.5 * W1**2 + 10 * W2**2
ax.contour(W1, W2, L, levels=20, cmap='Greys', alpha=0.4)
ax.plot(sgd_path[:,0], sgd_path[:,1], 'o-', color='#3498db', markersize=2, linewidth=1, label='SGD')
ax.plot(mom_path[:,0], mom_path[:,1], 'o-', color='#27ae60', markersize=2, linewidth=1, label='Momentum')
ax.plot(adam_path[:,0], adam_path[:,1], 'o-', color='#e74c3c', markersize=2, linewidth=1, label='Adam')
ax.plot(0, 0, 'k*', markersize=15, label='Minimum')
ax.set_xlabel('w₁'); ax.set_ylabel('w₂')
ax.set_title("Optimizer Trajectories on Elongated Quadratic")
ax.legend()
plt.grid(alpha=0.3)
plt.show()
```
4. 展示L1与L2正则化对权重稀疏性的影响。使用两种惩罚训练线性回归,并比较得到的权重向量。
```python
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
# 合成数据:20个特征中只有前3个是相关的
key = jax.random.PRNGKey(0)
n, d = 200, 20
w_true = jnp.zeros(d).at[:3].set(jnp.array([3.0, -2.0, 1.5]))
X = jax.random.normal(key, (n, d))
y = X @ w_true + 0.5 * jax.random.normal(key, (n,))
def train_ridge(X, y, lam=1.0, lr=0.01, steps=2000):
"""通过GD进行L2正则化线性回归。"""
w = jnp.zeros(X.shape[1])
for _ in range(steps):
pred = X @ w
grad = (2/len(y)) * X.T @ (pred - y) + 2 * lam * w
w = w - lr * grad
return w
def train_lasso(X, y, lam=1.0, lr=0.01, steps=2000):
"""通过近端GD进行L1正则化线性回归。"""
w = jnp.zeros(X.shape[1])
for _ in range(steps):
pred = X @ w
grad = (2/len(y)) * X.T @ (pred - y)
w = w - lr * grad
# 软阈值(L1的近端算子)
w = jnp.sign(w) * jnp.maximum(jnp.abs(w) - lr * lam, 0)
return w
w_l2 = train_ridge(X, y, lam=0.1)
w_l1 = train_lasso(X, y, lam=0.1)
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
axes[0].bar(range(d), w_true, color='#333', alpha=0.7)
axes[0].set_title("True Weights"); axes[0].set_xlabel("Feature")
axes[1].bar(range(d), w_l2, color='#3498db', alpha=0.7)
axes[1].set_title("L2 (Ridge): shrinks all"); axes[1].set_xlabel("Feature")
axes[2].bar(range(d), w_l1, color='#e74c3c', alpha=0.7)
axes[2].set_title("L1 (Lasso): zeros out irrelevant"); axes[2].set_xlabel("Feature")
plt.tight_layout()
plt.show()
print(f"L2 non-zero weights: {int(jnp.sum(jnp.abs(w_l2) > 0.01))}/{d}")
print(f"L1 non-zero weights: {int(jnp.sum(jnp.abs(w_l1) > 0.01))}/{d}")
```