• 首页
  • 博客
  • 留言墙

动态更新

喜欢我的内容的话不妨订阅支持一下 🫶
加入其他 2 位订阅者,随缘更新,欢迎订阅🥰。

首页博客留言墙

© 2026 async. fork自:GitHub

总浏览量 5.3万
最近访客来自 Qingdao, CN🇨🇳
  • 基础概念
  • 神经网络的基本结构
  • 梯度下降法
  • 损失函数
  • 前向传播
  • 激活函数:赋予网络非线性能力
  • 链式法则:反向传播的数学基础
  • 反向传播
  • 数据集划分与用途
  • 训练集(Training Set)
  • 验证集(Validation Set)
  • 3. 测试集(Test Set)
  • AutoGrad & AutoCast
  • 自动微分机制 autograd
  • 构建 Function 计算图
  • 梯度计算规则
  • 局部禁用 AutoGrad
  • model.eval() 不会关闭 Autograd
  • torch.no_grad() 局部关闭梯度记录
  • torch.inference_mode():更彻底的推理模式
  • 原地操作
  • 混合精度训练
  • torch.amp
  • NVIDIA APEX
  • 分层混合精度策略
  • 动态损失缩放技术
训练基础

相关文章

AI模型导论与推理基础原理
2026/08/04
infinitensor, 训练系统

训练基础

介绍了关于infinitensor夏令营课程中训练系统模块部分的训练基础。

6次点击25分钟阅读

基础概念

神经网络的基本结构

神经网络是由大量相互连接的人工神经元构成的计算模型,其结构受到生物神经网络的启发。输入层接收原始特征向量 x=(x1,x2,…,xn)x=(x _1,x_ 2,\ldots,x_n)x=(x1​,x2​,…,xn​),隐藏层中的神经元对输入进行加权求和并经过非线性激活函数:

h(l)=σ(W(l)h(l−1)+b(l))h^{(l)}=\sigma\left(W^{(l)}h^{(l-1)}+b^{(l)}\right)h(l)=σ(W(l)h(l−1)+b(l))

多层隐藏层会逐步提取和组合特征,输出层再根据最后一层特征生成预测结果 (\hat{y})。训练过程中,神经网络利用样本数据比较预测结果与真实答案之间的误差,并不断调整连接权重和偏置,从而自动学习输入与输出之间的映射关系。简单来说,输入层负责接收数据,隐藏层负责加工和提取特征,输出层负责给出最终预测。

梯度下降法

梯度下降法是神经网络训练中最基本的参数优化方法,其目标是不断调整模型参数 θ\thetaθ,使损失函数 L(θ)L(\theta)L(θ) 尽可能小。梯度 ∇θL(θ)\nabla_{\theta}L(\theta)∇θ​L(θ) 表示损失函数在当前位置上升最快的方向,因此沿着梯度的反方向更新参数,就可以让损失逐步下降:

θ←θ−η∇θL(θ)\theta \leftarrow \theta-\eta\nabla_{\theta}L(\theta)θ←θ−η∇θ​L(θ)

其中,η\etaη 表示学习率,用于控制每次更新的步长。学习率过小会导致训练速度缓慢,过大则可能越过最低点,使损失震荡甚至无法收敛。整个过程可以理解为在起伏的山坡上不断寻找下坡方向,经过多次迭代逐渐到达较低的位置。需要注意的是,梯度下降通常只能保证找到一个较低的点,不一定能够找到整个损失函数的全局最低点。

损失函数

损失函数用于衡量模型预测值 y^\hat{y}y^​ 与真实标签 yyy 之间的差异,是模型训练时需要最小化的目标。损失越小,说明预测结果通常越接近真实值。分类任务常使用交叉熵损失:

L=−∑iyilog⁡(y^i)L=-\sum_i y_i\log(\hat{y}_i)L=−i∑​yi​log(y^​i​)

它会对模型给正确类别较低概率的情况施加较大的惩罚;回归任务则常使用均方误差:

L=1n∑i=1n(yi−y^i)2L=\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat{y}_i)^2L=n1​i=1∑n​(yi​−y^​i​)2

它通过计算预测值与真实值之间误差的平方来衡量预测偏差。模型训练时,先根据输入 xxx 得到预测结果 y^\hat{y}y^​,再计算损失 L(y^,y)L(\hat{y},y)L(y^​,y),通过反向传播求出参数梯度,并使用梯度下降更新参数。这个过程不断重复,使损失逐渐减小,模型也就逐步学会拟合数据中的规律。

前向传播

前向传播是指输入数据从输入层开始,依次经过各个隐藏层,最终得到模型预测结果 (\hat{y}) 的过程。在这一阶段,网络只使用当前的权重和偏置进行计算,不会更新参数。
对于第 lll 层,通常包含两步计算:

z[l]=W[l]a[l−1]+b[l]z^{[l]}=W^{[l]}a^{[l-1]}+b^{[l]}z[l]=W[l]a[l−1]+b[l]a[l]=σ(z[l])a^{[l]}=\sigma\left(z^{[l]}\right)a[l]=σ(z[l])

其中,a[l−1]a^{[l-1]}a[l−1] 是上一层的输出,W[l]W^{[l]}W[l] 和 b[l]b^{[l]}b[l] 分别是当前层的权重矩阵与偏置向量, 是线性计算结果, 是 ReLU、Sigmoid 等非线性激活函数。输出层采用哪种激活方式取决于具体任务:回归任务可以直接输出连续数值,二分类任务常使用 Sigmoid,多分类任务则通常输出 logits 或经过 Softmax 得到类别概率。训练时,最终预测 会与真实标签 一起用于计算损失:

L=L(y^,y)L=L(\hat{y},y)L=L(y^​,y)

随后再通过反向传播计算梯度并更新网络参数。简而言之,前向传播负责根据当前参数得到预测结果,反向传播负责根据预测误差调整参数。

激活函数:赋予网络非线性能力

神经网络中的每一层通常先进行线性变换,再通过激活函数处理:

z[l]=W[l]a[l−1]+b[l],a[l]=σ(z[l])z^{[l]}=W^{[l]}a^{[l-1]}+b^{[l]}, \qquad a^{[l]}=\sigma\left(z^{[l]}\right)z[l]=W[l]a[l−1]+b[l],a[l]=σ(z[l])

如果没有激活函数,多层线性变换仍然可以合并为一次线性变换:

W2(W1x+b1)+b2=W′x+b′W _2(W_ 1 x+b_ 1)+b_ 2=W'x+b'W2​(W1​x+b1​)+b2​=W′x+b′

因此,无论网络堆叠多少层,都只能表示简单的线性关系。激活函数通过引入非线性,使不同神经元能够组合出弯曲的决策边界,从而拟合更加复杂的函数。
常见激活函数包括 Sigmoid、Tanh、ReLU 和 Leaky ReLU。

对应的图像如下所示:

链式法则:反向传播的数学基础

反向传播本质上是利用链式法则,在计算图中从输出端向输入端逐步计算梯度。当变量沿同一条链路依次传递时,各节点之间的局部导数相乘。若

y=g(x),z=h(y)y=g(x),\qquad z=h(y)y=g(x),z=h(y)

则:

dzdx=dzdydydx\frac{dz}{dx}=\frac{dz}{dy}\frac{dy}{dx}dxdz​=dydz​dxdy​

当一个变量通过多条路径共同影响最终结果时,需要分别计算每条路径对结果的贡献,再将它们相加。例如:

x=g(s),y=h(s),z=k(x,y)x=g(s),\qquad y=h(s),\qquad z=k(x,y)x=g(s),y=h(s),z=k(x,y)

则:

dzds=∂z∂xdxds+∂z∂ydyds\frac{dz}{ds} = \frac{\partial z}{\partial x}\frac{dx}{ds} + \frac{\partial z}{\partial y}\frac{dy}{ds}dsdz​=∂x∂z​dsdx​+∂y∂z​dsdy​

因此,反向传播可以概括为:同一条路径上的梯度相乘,多条并行路径上的梯度相加。
以函数 y=xex2y=xe^{x^ 2}y=xex2 为例,变量 xxx 通过两条路径影响 yyy:一条直接进入乘法节点,另一条先经过平方和指数运算。根据乘积法则与链式法则:

dydx=dxdxex2+xdex2dx\frac{dy}{dx} = \frac{d x}{dx}e^{x^ 2} + x\frac{d e^{x^ 2}}{dx}dxdy​=dxdx​ex2+xdxdex2​

而:

dex2dx=ex2⋅2x\frac{d e^{x^ 2}}{dx} = e^{x^ 2}\cdot 2 xdxdex2​=ex2⋅2x

所以:

dydx=ex2(1+2x2)\frac{dy}{dx} =e^{x^ 2}(1+2 x^ 2)dxdy​=ex2(1+2x2)

这里虽然计算图中多次出现 xxx,但它们实际上共享同一个输入变量,因此反向传播时必须把所有相关路径产生的梯度贡献相加。神经网络中的反向传播正是按照这一规则,逐层计算损失函数对每个权重和偏置的梯度。

反向传播

反向传播是神经网络根据损失函数,计算各层参数梯度的过程。
前向传播首先从输入 (x) 出发,逐层计算中间结果:

z[l]=W[l]a[l−1]+b[l]z^{[l]} = W^{[l]}a^{[l-1]} + b^{[l]}z[l]=W[l]a[l−1]+b[l]a[l]=σ(z[l])a^{[l]} = \sigma\left(z^{[l]}\right)a[l]=σ(z[l])

其中:

  • W[l]W^{[l]}W[l] 是第 lll 层的权重矩阵;
  • b[l]b^{[l]}b[l] 是偏置向量;
  • z[l]z^{[l]}z[l] 是线性计算结果;
y^=a[L]\hat{y} = a^{[L]}y^​=a[L]

再根据预测结果 y^\hat{y}y^​ 和真实标签 yyy 计算损失:

L=L(y^,y)\mathcal{L} = \mathcal{L}(\hat{y}, y)L=L(y^​,y)

反向传播从损失函数开始,按照与前向传播相反的方向,利用链式法则逐层计算梯度。
首先计算损失对输出层激活值的梯度:

da[L]=∂L∂a[L]da^{[L]} = \frac{\partial \mathcal{L}} {\partial a^{[L]}}da[L]=∂a[L]∂L​

对于第 lll 层,先经过激活函数反向传播:

dz[l]=da[l]⊙σ′(z[l])dz^{[l]} = da^{[l]} \odot \sigma'\left(z^{[l]}\right)dz[l]=da[l]⊙σ′(z[l])

其中,⊙\odot⊙ 表示对应位置的元素相乘。
再根据线性计算:

z[l]=W[l]a[l−1]+b[l]z^{[l]} = W^{[l]}a^{[l-1]} + b^{[l]}z[l]=W[l]a[l−1]+b[l]

计算权重和偏置的梯度:

dW[l]=dz[l](a[l−1])TdW^{[l]} = dz^{[l]} \left(a^{[l-1]}\right)^TdW[l]=dz[l](a[l−1])Tdb[l]=dz[l]db^{[l]} = dz^{[l]}db[l]=dz[l]

同时,将梯度继续传递到前一层:

da[l−1]=(W[l])Tdz[l]da^{[l-1]} = \left(W^{[l]}\right)^T dz^{[l]}da[l−1]=(W[l])Tdz[l]

因此,每一层的反向传播都可以概括为三个步骤:

  1. 计算激活函数之前的梯度 dz[l]dz^{[l]}dz[l];
  2. 计算当前层权重和偏置的梯度;
  3. 将梯度继续传递给前一层。

这个过程从输出层一直重复到输入层,最终得到所有参数对损失函数的影响程度。优化器随后使用这些梯度更新参数:

W[l]←W[l]−ηdW[l]W^{[l]} \leftarrow W^{[l]} - \eta dW^{[l]}W[l]←W[l]−ηdW[l]b[l]←b[l]−ηdb[l]b^{[l]} \leftarrow b^{[l]} - \eta db^{[l]}b[l]←b[l]−ηdb[l]

其中,η\etaη 表示学习率。
反向传播需要使用前向传播过程中计算出的 z[l]z^{[l]}z[l] 和 a[l]a^{[l]}a[l],因此训练时通常需要缓存各层的中间激活值。这也是训练相比只执行前向传播的推理过程需要更多显存的重要原因。
简单来说,前向传播负责产生预测结果和损失,反向传播负责计算误差应当如何分配到每一层参数上,梯度下降则根据这些梯度更新模型参数。

数据集划分与用途

在机器学习中,完整数据集通常被划分为训练集、验证集和测试集,三者分别承担不同的作用。

训练集(Training Set)

训练集用于模型学习和参数优化。训练过程中,模型根据输入数据生成预测结果,计算预测值与真实标签之间的损失,并通过反向传播不断更新权重和偏置。

验证集(Validation Set)

验证集用于模型开发过程中的性能评估,但不会直接参与参数更新。它通常用于:

  • 调整学习率、网络层数、隐藏层大小等超参数;
  • 比较不同模型或训练方案;
  • 判断模型是否出现过拟合;
  • 进行早停(Early Stopping)。

如果训练集上的误差持续下降,而验证集上的误差开始上升,通常说明模型正在过度拟合训练数据,此时可以提前停止训练。

3. 测试集(Test Set)

测试集用于模型训练完成后的最终性能评估。测试集不应该参与模型训练、超参数调整或模型选择,否则测试结果可能无法真实反映模型对未知数据的泛化能力。
因此,测试集原则上只应在模型和训练方案确定之后使用。

AutoGrad & AutoCast

自动微分机制 autograd

每次执行前向计算,Pytorch 的 AutoGrad 引擎会动态构建一张计算图,用户后续反向传播。初始梯度由用户在调用 .backward() 时显示传入,对标量调用则默认为 1。每个节点只保存必要的中间激活值和局部梯度函数,实现链式法则的逐层传播。

构建 Function 计算图

AutoGrad——反向微分系统,在代码执行计算的同时,AutoGrad 会构建一张由 Function 对象组成的计算图,用于反向传播:

  • 有向无环图结构:图的叶子结点对应模型的输入张量,根节点是最终输出张量。
  • 本质上是由 Function 对象组成的图:每个 Function 对象表示一个操作,通过其 apply()方法计算前向结果,并记录该操作的反向传播逻辑。
  • 梯度计算图的入口:.grad_fn 属性,每个非叶子张亮的 .grad_fn 属性指向创建该张量的 Function,是这个图的一个入口点。
  • 在反向传播阶段分析计算图自动求导:完成前向计算后,AutoGrad 会遍历计算图,基于链式法则自动计算各参数的梯度。

以平方运算为例:

y=x2y=x^2y=x2

根据链式法则,若后续损失为 LLL,则:

∂L∂x=∂L∂y∂y∂x=∂L∂y⋅2x\frac{∂L}{∂x}=\frac{∂L}{∂y} \frac{∂y}{∂x}=\frac{∂L}{∂y}\cdot 2x∂x∂L​=∂y∂L​∂x∂y​=∂y∂L​⋅2x

反向传播时需要使用 xxx 才能计算 2x2x2x,因此前向传播执行 x.pow(2) 时,Autograd 会保存输入张量 xxx。在调试时,可以通过类似 y.grad_fn._saved_self 的内部属性观察被保存的张量。
不同的运算需要保存的内容并不相同,例如:

y=exy=e^xy=ex

其导数为

dydx=ex=y\frac{dy}{dx}=e^x=ydxdy​=ex=y

此时反向传播可以直接利用前向传播的输出 yyy,不必重新计算指数。因此,指数运算对应的反向节点通常会保存计算结果,可以通过类似 x.grad_fn._saved_result 的属性观察。
需要注意,保存张量不一定表示计算图始终保存原来的 Python 张量对象,例如:

这里的两个张量数值相同,并且可能共享底层存储,但是不一定时同一个 Python 对象。Pytorch 会在部分情况下对张量进行打包和解包,以避免形成如 y → grad_fn → saved tensor → y 的循环引用。
对于反向传播需要的中间张量,PyTorch 可以使用两个钩子控制其保存和恢复方式:

  • pack_hook:前向传播保存张量时调用;其返回值会替代原始张量存入计算图。
  • unpack_hook:反向传播时需要读取该张量时调用,负责根据返回值恢复出反向传播所需的张量。

通过 torch.autograd.graph.saved_tensors_hooks,可以自定义这种保存策略。例如,前向传播时把大型中间张量从 GPU 移到 CPU,甚至保存到磁盘;反向传播时再读取回来。这样可以减少训练过程中的显存占用,但会增加数据传输、磁盘读写和运行时间,因此本质上是在用计算或 I/O 开销换取内存空间。

梯度计算规则

  • 函数在当前点可导且梯度存在——使用该梯度。
  • 函数是凸函数(或者局部凸),使用最小范数的 sub-gradient。
  • 函数是凹函数,使用最小范数的 super-gradient。
  • 如果函数在当前点定义了值,则根据连续性来定义当前点的梯度(允许 inf,例如 sqrt(0))。
  • 如果函数在当前点没有定义(例如 sqrt(-1)),可能为任意情况(抛出错误,NaN 等)。
  • 如果不是数学意义上的函数,则标记为不可微分,反向传播时如果输入张量 requires_grad=True,且没有使用 torch.no_grad() 包裹,则报错。

局部禁用 AutoGrad

使用 requires_grad 属性细粒度地控制计算操作是否被纳入 AutoGrad 的计算图中:

  • 在前向传播中,只有当某个操作的输入张量中至少有一个 requires_grad=True 时,该操作才会被记录进反向计算图中。
  • 在反向传播中,只有为叶子且 requires_grad=True 的张量,才会在它们的 .grad 字段中累加反向传过来的梯度值。

虽然所有张量都有 requires_grad 属性,但设置它只对叶子张量才有意义。
PyTorch 中还需要区分两组彼此独立的概念:

  • model.train() 与 model.eval() :控制模型中部分模块采用训练行为还是推理行为。
  • 默认梯度模式、torch.no_grad() 与 torch.inference_mode():控制 Autograd 是否记录运算并构建反向传播计算图。
model.eval() 不会关闭 Autograd

调用 model.eval() 本质上等价于 model.train(false),只是把模型切换为评估模式,并影响那些在训练和推理阶段行为不同的模块,如 Dropout 和 BatchNorm。它不会关闭梯度计算,也不会阻止计算图的建立;在 eval() 模式下,只要输入或参数需要梯度,仍然可以调用 backward()。

torch.no_grad() 局部关闭梯度记录

torch.no_grad() 会在指定代码区域内关闭反向传播计算图的构建:

在这个代码块中,PyTorch 不会保存反向传播所需的中间结果,因此通常可以减少内存占用和计算开销。退出代码块后,梯度模式会自动恢复。no_grad() 不会改变模型本身的训练或评估状态。

torch.inference_mode():更彻底的推理模式

torch.inference_mode() 与 no_grad() 类似,也不会构建反向传播计算图:

但它比 no_grad() 更严格,会进一步跳过部分与 Autograd 有关的额外记录,例如视图跟踪和版本计数,因此在纯推理场景中可能获得更低的额外开销。代价是其中创建的张量受到更多限制,不适合随后重新参与需要梯度的计算。

原地操作

原地操作是指直接修改原张量中的内容,而不是创建新张量,例如:

相比之下,下面的写法会创建新的张量:

PyTorch 允许部分原地操作,但如果它修改了反向传播所需的数据,就会报错。例如前向传播中计算 y=x2y=x^2y=x2,反向传播需要原来的 xxx 来计算:

dydx=2x\frac{dy}{dx}=2xdxdy​=2x

如果在反向传播前执行 x.add_(1),原来的 xxx 被覆盖,PyTorch 就无法确定应使用修改前还是修改后的值计算梯度。
为保证梯度正确,每个 Tensor 都维护一个版本计数器。张量每被原地修改一次,版本号就会增加;前向传播保存中间张量时,也会保存当时的版本号。反向传播读取该张量时,如果发现版本号发生变化,就会抛出错误。

混合精度训练

混合精度(Mixed Precision)训练指的是:在训练过程中同时使用 FP16 和 FP32,以提高训练速度、减少显存占用,同时尽可能保持精度不变。

torch.amp

PyTorch 的自动混合精度模块为:torch.amp,其中较老的版本中仅支持 CUDA 后端,位于 torch.cuda.amp,两大核心工具包括 autocast 与 GradScaler。

  • torch.amp.autocast:自动将某些算子用 FP16 执行,从而提高计算吞吐、降低显存占用。
    • 通常使用 with torch.amp.autocast():上下文管理器的方式调用。
    • 目标是数值稳定性较好,不易发生溢出或精度损失的算子,如 matmul/conv/relu/add/mul 等。
  • torch.amp.GradScaler:解决 FP 16 训练时梯度过小导致全为 0 的问题,其工作流程如下:
    • 缩放 loss:将 loss 乘一个很大的数,让反向传播得到的梯度也放大。
    • 恢复梯度:自动将缩放后的梯度恢复为原始大小(除以 scale 值)。
    • 动态调整 scale 大小(根据是否出现 inf/nan):如果梯度健康(没有溢出)则增大 scale;如果出现数值问题则减小 scale,确保稳定训练。

NVIDIA APEX

  • O0:不优化,前向传播、反向传播、模型权重和参数更新全部使用 FP32,这种方式数值最稳定,但训练速度较慢,显存占用也最大。
  • O1:只优化计算,模型权重仍以 FP32 保存,但前向传播和反向传播中的部分计算改用 FP16。这样能够利用 FP16 加速矩阵运算,同时保留 FP32 权重以维持参数精度,是一种相对保守的混合精度方案。
  • O2:除参数更新外使用 FP16,前向传播和反向传播使用 FP16,模型计算时也使用 FP16 权重;同时保留一份 FP32 的主权重,供优化器更新参数。其流程可以理解为使用 FP16 的权重进行前向传播与反向传播,得到梯度后更新 FP32 主权重,最后同步回 FP16 权重,这种方式可以进一步减少显存占用,同时避免直接使用 FP16 更新参数造成小数值变化丢失。
  • O3:前向传播、反向传播、权重以及参数更新都尽可能使用 FP16。它可以获得最大的速度提升和最低的显存占用,但数值稳定性最差,更容易出现下溢、溢出或训练不收敛等问题。

分层混合精度策略

神经网络中不同层的计算方式和数值范围不同,对低精度计算的敏感程度也不同。因此,不一定要让整个模型统一使用 FP16 或 FP32,而是可以为不同层选择不同的精度策略。
通常先在小模型上测试各层使用低精度后的误差和训练稳定性,再按照敏感程度进行划分:

  • 数值敏感、容易出现误差的层使用 FP32。
  • 能使用 FP16 计算,但参数更新需要较高精度的层采用混合精度。
  • 数值较稳定、计算量较大的层可以更多地使用 FP16。

这种方法可以在保证训练稳定性的同时,降低显存占用并提高计算速度。

  • Embedding 等敏感层使用 O0,即主要采用 FP32。
  • Attention 层使用 O1,即 FP16 计算、FP32 权重或参数更新。
  • FFN 层计算量大且相对适合低精度,因此使用 O3,尽可能采用 FP16。

最主要的核心思想也就是:对精度敏感的层保留高精度,对计算量大的稳定层更多使用低精度。

动态损失缩放技术

FP16 能表示的数字范围较小,反向传播中的微小梯度可能因为过小而直接变成 0,这称为梯度下溢。损失缩放会先把损失乘以一个较大的系数 SSS:

Lscaled=S⋅LL_{scaled}=S \cdot LLscaled​=S⋅L

这样反向传播得到的梯度也会被放大:

∇Lscaled=S⋅∇L\nabla L_{scaled}=S \cdot \nabla L∇Lscaled​=S⋅∇L

在优化器更新参数之前,再将梯度除以 SSS,恢复原始尺度:

∇L=∇LscaledS\nabla L = \frac{\nabla L_{scaled}}{S}∇L=S∇Lscaled​​

动态损失缩放会自动调节 SSS:

  • 如果出现 Inf 或 NaN,说明缩放过大,减小缩放系数并跳过本次更新。
  • 如果连续多轮没有溢出,可以逐渐增大缩放系数。
  • 目标是在不发生上溢的前提下,让梯度数值尽可能大,减少 FP16 下溢。

总体而言,分层混合精度负责决定不同层使用什么精度,动态损失缩放负责保护 FP16 反向传播中的微小梯度。

AI模型导论与推理基础原理

2026/07/30infinitensor, 大模型推理服务系统1317分钟阅读
z[l]z^{[l]}z[l]
σ\sigmaσ
y^\hat{y}y^​
yyy
  • a[l]a^{[l]}a[l] 是经过激活函数后的输出。
    网络最终得到预测结果:
  • python
    import torch
    
    x = torch.tensor(2.0, requires_grad=True) #requires_grad=True 表示要对该变量求导
    y = x ** 2 + 3 * x + 1 
    y.backward() # 会触发整个传播过程
    print(x.grad) # Output:7.0
    python
    x = torch.randn(5, requires_grad=True)
    y = x.exp()
    
    print(y.equal(y.grad_fn._saved_result))   # 数值相同
    print(y is y.grad_fn._saved_result)       # 可能为 False
    python
    model.eval()
    
    with torch.no_grad():
        prediction = model(x)
    python
    model.eval()
    
    with torch.inference_mode():
        prediction = model(x)
    python
    x.add_(1)
    x += 1
    x.relu_()
    python
    x = x + 1