- 文档
- 教程
- 人工智能
- 深度学习
- NLP
- 计算机视觉
- 强化学习
【免费下载链接】d2l-en
Interactive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.
导读
《动手学深度学习》(d2l-en)在 线性回归的从零实现 中演示了如何仅依靠张量与自动微分搭建回归模型;而本文档chapter_linear-regression/linear-regression-concise.md则展示同一模型的高层 API 简洁实现:直接使用各框架预置的全连接层、损失函数与优化器,配合 d2l 仓库的面向对象训练基建(d2l.Module、d2l.Trainer、d2l.DataModule),把代码量压缩到数行。读完本文,你将掌握 MXNet、PyTorch、TensorFlow、JAX 四种框架下用高层 API 声明模型、定义损失、配置优化器并调用统一fit训练流程的完整方法,并理解其与手写实现之间的对应关系。
为什么需要高层 API:从手写实现到框架封装
在 从零实现 一节中,我们只依赖两类底层能力:
- 张量(tensor):用于数据存储与线性代数运算;
- 自动微分(autograd):用于计算梯度。
然而在实际工程中,数据迭代器、损失函数、优化器和神经网络层是所有模型都反复使用的公共组件,现代深度学习框架早已把这些组件内置并做了深度优化。正如本文档开篇所述,现代框架提供的自动微分与 Python 便利性,让我们能把"实现基于梯度的学习算法"中大量重复性工作自动化与模块化。自己实现一次是有教育意义的,但如果每天都在重造轮子,效率就太低了——这好比"手写博客引擎":写一两次有收获,花一个月重造轮子就不是合格的工程师了。
本文的定位正是 从零实现 的"简洁版":训练循环本身保持不变,只是把"显式分配参数、手写损失、手写 minibatch SGD"替换为框架自带的高层组件。
准备工作:四种框架的环境导入
文档开头通过 d2lbook 的 tab 交互机制让读者在四种后端之间切换,对应导入代码如下:
# MXNet from d2l import mxnet as d2l from mxnet import autograd, gluon, init, np, npx from mxnet.gluon import nn npx.set_np() # PyTorch from d2l import torch as d2l import numpy as np import torch from torch import nn # TensorFlow from d2l import tensorflow as d2l import numpy as np import tensorflow as tf # JAX from d2l import jax as d2l from flax import linen as nn import jax from jax import numpy as jnp import optax从源码结构看,仓库在 d2l/ 目录下按后端拆分了torch.py、mxnet.py、tensorflow.py、jax.py四个模块,from d2l import torch as d2l即按需导入对应后端的统一 API 封装。本文后续代码示例均以%%tab标注所属后端,其中 PyTorch / MXNet / TensorFlow / JAX 分别对应各自生态的组件。
定义模型:一行代码声明全连接层
手写实现中我们需要自己初始化w、b并手写forward的前向计算;高层 API 则直接使用框架预定义的全连接层。回忆单层网络架构:输入中的每个分量都与输出的每个分量通过矩阵-向量乘法相连,因此该层被称为fully connected(全连接)层。各框架的对应类如下:
| 框架 | 全连接层类 | 输入维度声明 |
|---|---|---|
| MXNet(Gluon) | gluon.nn.Dense | 不需要显式指定,首次前向时自动推断 |
| PyTorch | nn.Linear/nn.LazyLinear(1.8.0 起) | Linear需给出输入维度;LazyLinear只需输出维度,首次前向时推断 |
| TensorFlow(Keras) | tf.keras.layers.Dense | 不需要显式指定,首次前向时自动推断 |
| JAX(Flax) | flax.linen.Dense | 需给出输出维度,输入维度首次调用时推断 |
其中 PyTorch 的LazyLinear允许只声明输出维度,免去在卷积层等场景下手工推算输入维度的麻烦,因此本文档在 PyTorch 分支统一使用这种 "lazy" 层。
在 d2l 的面向对象设计中,模型继承d2l.Module(基类实现在 d2l/torch.py),构造函数通过save_hyperparameters()自动把超参数保存为实例属性。完整的模型定义如下:
%%tab pytorch, mxnet, tensorflow class LinearRegression(d2l.Module): #@save """The linear regression model implemented with high-level APIs.""" def __init__(self, lr): super().__init__() self.save_hyperparameters() if tab.selected('mxnet'): self.net = nn.Dense(1) self.net.initialize(init.Normal(sigma=0.01)) if tab.selected('tensorflow'): initializer = tf.initializers.RandomNormal(stddev=0.01) self.net = tf.keras.layers.Dense(1, kernel_initializer=initializer) if tab.selected('pytorch'): self.net = nn.LazyLinear(1) self.net.weight.data.normal_(0, 0.01) self.net.bias.data.fill_(0)%%tab jax class LinearRegression(d2l.Module): #@save """The linear regression model implemented with high-level APIs.""" lr: float def setup(self): self.net = nn.Dense(1, kernel_init=nn.initializers.normal(0.01))各分支的初始化细节对应着 从零实现 中的约定:权重从均值为 0、标准差 0.01 的正态分布采样,偏置置为 0(PyTorch 使用带下划线的原地方法.normal_()、.fill_()完成替换式初始化)。MXNet 与 TensorFlow 分支分别通过init.Normal(sigma=0.01)和tf.initializers.RandomNormal(stddev=0.01)指定同样的分布;JAX 分支则在setup中用 Flax 的kernel_init=nn.initializers.normal(0.01)实现。
forward方法只需调用预定义层的__call__:
%%tab all @d2l.add_to_class(LinearRegression) #@save def forward(self, X): return self.net(X)注意@d2l.add_to_class装饰器(实现在 d2l/torch.py)本质上是setattr(Class, obj.__name__, obj)——把外部定义的函数在类创建之后动态注册为类方法,这是 d2l 全书通用的面向对象设计模式(详见 面向对象设计 相关章节的sec_oo-design约定)。
定义损失函数:直接选用内置 MSE 损失
手写实现中的均方误差是(y_hat - y) ** 2 / 2(含 1/2 因子,便于求导)。高层实现直接选用框架内置损失,且默认返回的是对 mini-batch 内所有样本的平均损失(注意不含 1/2 因子,与文档eq_mse公式存在该系数差异):
| 框架 | 损失类 | 说明 |
|---|---|---|
| MXNet(Gluon) | gluon.loss.L2Loss | 返回逐样本平方误差,需再.mean()求均值 |
| PyTorch | nn.MSELoss | 默认返回样本平均 |
| TensorFlow(Keras) | tf.keras.losses.MeanSquaredError | 默认返回样本平均 |
| JAX(Optax) | optax.l2_loss+d2l.reduce_mean | 返回逐样本误差后求均值 |
%%tab pytorch, mxnet, tensorflow @d2l.add_to_class(LinearRegression) #@save def loss(self, y_hat, y): if tab.selected('mxnet'): fn = gluon.loss.L2Loss() return fn(y_hat, y).mean() if tab.selected('pytorch'): fn = nn.MSELoss() return fn(y_hat, y) if tab.selected('tensorflow'): fn = tf.keras.losses.MeanSquaredError() return fn(y, y_hat)%%tab jax @d2l.add_to_class(LinearRegression) #@save def loss(self, params, X, y, state): y_hat = state.apply_fn({'params': params}, *X) return d2l.reduce_mean(optax.l2_loss(y_hat, y))JAX 分支的函数签名与其余框架略有差异:由于 JAX 遵循纯函数式编程范式,loss需要显式接收params与state,其中state.apply_fn({'params': params}, *X)负责用当前参数执行前向计算(参数被解包成元组传给 Flax 模型)。
定义优化算法:一行配置 Minibatch SGD
minibatch SGD 是训练神经网络的标配优化器,四个框架均在各自优化器模块中内置。configure_optimizers方法统一返回优化器实例:
%%tab all @d2l.add_to_class(LinearRegression) #@save def configure_optimizers(self): if tab.selected('mxnet'): return gluon.Trainer(self.collect_params(), 'sgd', {'learning_rate': self.lr}) if tab.selected('pytorch'): return torch.optim.SGD(self.parameters(), self.lr) if tab.selected('tensorflow'): return tf.keras.optimizers.SGD(self.lr) if tab.selected('jax'): return optax.sgd(self.lr)各框架的要点:
- MXNet:
gluon.Trainer类在 d2l 语境中专指优化算法本身(区别于d2l.Trainer训练器,后者封装训练方法、负责反复调用优化器更新参数)。实例化时需传入待优化参数net.collect_params()、算法名'sgd'以及算法所需超参字典{'learning_rate': self.lr}。 - PyTorch:
torch.optim.SGD(self.parameters(), self.lr),参数通过模型自身的parameters()获得,学习率来自save_hyperparameters保存的self.lr。 - TensorFlow:
tf.keras.optimizers.SGD(self.lr),Keras 优化器内部自动关联模型的trainable_variables。 - JAX:
optax.sgd(self.lr),Optax 以GradientTransformation(由init与update两个函数构成)抽象优化算法,apply_updates负责把负梯度步长加到参数上。
训练:复用统一的 fit 训练循环
高层 API 的价值在训练阶段体现得最明显:我们不再需要逐个分配参数、手写损失、实现 minibatch SGD,只需三行代码完成数据构造、训练器实例化与拟合:
%%tab all model = LinearRegression(lr=0.03) data = d2l.SyntheticRegressionData(w=d2l.tensor([2, -3.4]), b=4.2) trainer = d2l.Trainer(max_epochs=3) trainer.fit(model, data)其中:
- 数据:
d2l.SyntheticRegressionData生成合成回归数据(实现见 synthetic-regression-data.md),真实参数为 $\mathbf{w} = [2, -3.4]^\top$、$b = 4.2$,标签由 $\mathbf{y} = \mathbf{X}\mathbf{w} + b + \boldsymbol{\epsilon}$ 生成,噪声 $\boldsymbol{\epsilon} \sim \mathcal{N}(0, 0.01^2)$,默认 1000 个训练样本与 1000 个验证样本、batch_size=32。因为真实参数已知,训练后可以精确核对学习结果。 - 训练器:
d2l.Trainer(max_epochs=3)的fit方法(实现在 d2l/torch.py)内部依次执行prepare_data(取得训练/验证 dataloader)、prepare_model(绑定模型与进度面板)、configure_optimizers(取得优化器),然后循环max_epochs轮调用fit_epoch。 - 单轮训练:
fit_epoch(d2l/torch.py)遍历训练 dataloader,对每个 mini-batch 调用model.training_step计算损失,optim.zero_grad()清零梯度,loss.backward()反向传播,最后optim.step()更新参数;若存在验证集,再以model.eval()模式跑一遍验证集。这套循环与 从零实现 中手写的fit_epoch完全同构——高层 API 改变的只是"组件从哪来",而不是"训练怎么跑"。
参数核对:验证学习结果是否逼近真实参数
由于数据是我们自己合成的,真实参数已知,因此训练结束后可以精确比较估计参数与真实参数。为此文档为模型注册了get_w_b辅助方法,按框架从层对象中取出权重与偏置:
%%tab pytorch, mxnet, tensorflow @d2l.add_to_class(LinearRegression) #@save def get_w_b(self): if tab.selected('mxnet'): return (self.net.weight.data(), self.net.bias.data()) if tab.selected('pytorch'): return (self.net.weight.data, self.net.bias.data) if tab.selected('tensorflow'): return (self.get_weights()[0], self.get_weights()[1]) w, b = model.get_w_b()%%tab jax @d2l.add_to_class(LinearRegression) #@save def get_w_b(self, state): net = state.params['net'] return net['kernel'], net['bias'] w, b = model.get_w_b(trainer.state)注意 JAX 分支的差异:Flax 把参数保存在trainer.state.params中,且命名与其余框架不同(权重名为kernel),因此get_w_b需要接收trainer.state作为参数。随后计算估计误差:
print(f'error in estimating w: {data.w - d2l.reshape(w, data.w.shape)}') print(f'error in estimating b: {data.b - b}')在默认设置(lr=0.03、3 个 epoch、1000 个训练样本)下,估计参数与真实参数 $\mathbf{w}=[2,-3.4]^\top$、$b=4.2$ 非常接近。需要强调的是:不要理所当然地认为总能精确恢复真实参数——一般而言深度模型并不存在唯一的参数解;即便在线性模型上,也只有在特征之间不存在线性相关时才能精确恢复参数。机器学习更关心的往往是能给出高精度预测的参数,而随机梯度下降即使在困难的优化问题上也常常能找到相当好的解,部分原因正是深度网络中存在大量参数配置都能给出高精度预测。
高层 API 的收益与代价
高层 API 带来的收益显而易见:
- 代码量大幅缩减:模型、损失、优化器各一行,训练复用统一
fit; - 性能与可靠性有保障:框架对数据加载、损失、优化器等组件的库实现通常经过重度性能优化与充分测试,直接使用比自写版本更快、更稳;
- 输入维度自动推断:MXNet 的
Dense、PyTorch 的LazyLinear、Keras 的Dense均在首次前向时自动推断输入维度并实例化正确的模型(惰性初始化),省去手工计算。
同时也要注意各框架的边界条件:
- MXNet:
nn模块提供大量网络层、loss模块提供常见损失函数、initializer提供多种参数初始化选择。惰性初始化的后果是参数实例化(并初始化)之前不能访问它们。 - PyTorch:
nn模块同时提供层与损失函数;参数初始化通过以_结尾的原地方法替换数值实现。非 lazy 的Linear需要显式指定输入维度——对当前这个简单模型微不足道,但在设计多层复杂网络时会产生连锁影响,需要仔细考虑网络的参数化方式以保持可移植性。 - TensorFlow:
keras模块提供层与损失函数,initializers模块提供各种参数初始化方法;维度与存储自动推断,但同样要注意参数初始化前不要访问。
而代价是:这些模块并非不可替代,研究者恰恰需要理解它们"可以"被直接实现。对处于模型研发前沿、需要发明新组件的学习者来说,"从零实现"的功力依然不可替代——这正是 d2l 全书先讲 scratch 再讲 concise 的根本原因。
练习与进阶思考
文档末尾给出了一组与训练行为直接相关的练习,可作为自检清单:
- 聚合 vs 平均:如果把 mini-batch 上的损失从"平均"改成"求和",学习率需要如何调整才能保持相同的更新步长?
- 替换损失函数:查阅框架文档,看内置了哪些损失函数;特别是把平方损失替换为 Huber 鲁棒损失 $$l(y,y') = \begin{cases}|y-y'| -\frac{\sigma}{2} & \text{if } |y-y'| > \sigma \ \frac{1}{2 \sigma} (y-y')^2 & \text{otherwise}\end{cases}$$ 并观察对异常值的鲁棒性差异(可对照 从零实现 练习中"主动把某个标签扰动为 $y_5 = 10000$"的实验)。
- 梯度访问:如何访问模型权重的梯度?提示:可结合
d2l.Trainer内部optim.zero_grad()→loss.backward()→optim.step()的时序,在backward之后读取param.grad。 - 超参数敏感性:改变学习率与 epoch 数,解会如何变化?是否持续改善?(实践表明学习率过大易发散、过小收敛慢;可参考 优化算法 各章的系统讨论。)
- 数据量的影响:改变生成的数据量,画出 $\hat{\mathbf{w}} - \mathbf{w}$ 与 $\hat{b} - b$ 的估计误差随数据量的变化曲线。提示:数据量应按对数刻度递增(5, 10, 20, 50, ..., 10000),而不是线性递增——因为误差随样本量大致以 $\mathcal{O}(n^{-1/2})$ 量级衰减,线性刻度会掩盖低样本区间的显著变化。
小结
本节是本书中第一个借助现代深度学习框架便利性实现的深度网络示例。我们使用了框架默认能力来加载数据、定义网络层、损失函数、优化器与训练循环:凡是框架已提供且功能完备的组件,通常都值得直接用——库实现经过性能优化与可靠性测试。与此同时,务必牢记这些组件本身都是可实现的,这对立志深耕模型研发前沿、需要发明尚不存在于任何库中的新组件的学习者尤其重要。
若需查看本节的完整可运行代码与配套讨论,可继续阅读 从零实现(手写版对照)、合成回归数据(数据生成实现)以及 d2l/torch.py 中Module、DataModule、Trainer三个基类的完整源码(d2l/torch.py)。
- 文档
- 教程
- 人工智能
- 深度学习
- NLP
- 计算机视觉
- 强化学习
【免费下载链接】d2l-en
Interactive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.
相关推荐
从零实现线性回归:手写模型、损失函数、优化器与训练循环(d2l-en 多框架实战)
从零实现线性回归:手写模型、损失函数、优化器与训练循环(d2l en 多框架实战) 导读 本文基于《Dive into Deep Learning》(d2l e
文档教程人工智能深度学习NLP计算机视觉强化学习3步解锁英雄联盟游戏资源:Obsidian终极Wad文件编辑器完全指南
3步解锁英雄联盟游戏资源:Obsidian终极Wad文件编辑器完全指南 你是否曾经好奇英雄联盟游戏中的精美皮肤、炫酷特效和界面元素是如何存储和管理的?当你想要自
桌面应用游戏开发D2L项目教程:线性回归的简洁实现
D2L项目教程:线性回归的简洁实现 本文基于D2L项目中的线性回归章节,将介绍如何使用现代深度学习框架简洁地实现线性回归模型。我们将重点对比手工实现与框架实现的
文档教程人工智能深度学习NLP计算机视觉强化学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考