MXNet Gluon autograd 自动微分完全指南:从梯度原理到动态图实战
2026/9/20 11:33:14 网站建设 项目流程
  • 人工智能
  • 深度学习
  • 机器学习

【免费下载链接】mxnet

Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more

项目地址:https://gitcode.com/gh_mirrors/mxne/mxnet
点击查看免费下载

导读

本文围绕 MXNet Gluon 的autograd模块,系统讲解自动微分的原理、autograd.record()上下文的使用方法,以及冻结参数、非参数梯度、Python 控制流动态图、自定义 head gradient 等高级特性。读者学完后将能理解反向模式自动微分的底层工作机制,并掌握在 MXNet Gluon 中写出可正确计算梯度的训练代码的完整套路。

为什么需要计算梯度?

简短回答

梯度是训练神经网络的核心要素。它告诉我们应该如何调整网络的参数(例如卷积核中的权重),从而让网络的性能变得更好。

上图直观展示了梯度的几何含义:沿着负梯度方向调整参数,损失函数的值就会下降。完整的训练流程正是不断重复「计算梯度 → 更新参数」直到收敛的过程。

详细回答

神经网络在底层由各种算子(求和、乘积、卷积等)组成,其中一部分算子依赖参数完成计算。训练的目标就是为这些参数找到最优取值,而梯度正是引导我们走向最优解的信号。

梯度度量的是:当我们改变某个变量时,依赖于它的另一个变量会增加或减少多少。在训练场景中,我们关心的是每个参数对网络性能的影响,通常用一个**损失度量(loss metric)**来刻画网络预测的糟糕程度:

  • 回归任务:最小化 L2 损失(即预测值与真实值的欧氏距离)。从源码看,其数学形式为L = 1/2 * Σ |label - pred|²,内部通过np.square(label - pred)计算(见 python/mxnet/gluon/loss.py);
  • 分类任务:最小化 Softmax 交叉熵损失(定义于同一文件python/mxnet/gluon/loss.pySoftmaxCrossEntropyLoss类)。

假设已经算出了损失对每个参数的梯度,就可以借助优化器(如随机梯度下降)让参数沿梯度的相反方向微微移动,从而降低损失。关于这些优化方法,可进一步参考 Optimizers。重复「算梯度 → 更新参数」这一过程,直到参数稳定并收敛到一组好的解,模型就训练完成了。

如何计算梯度?

简短回答

求导。MXNet Gluon 使用反向模式自动微分(Reverse Mode Automatic Differentiation,即autograd,把梯度从损失度量反向传播回网络参数。

上图展示了前向传播(实线,从左到右计算输出)与反向传播(虚线,从右到左传播梯度)的对应关系。

详细回答

计算梯度有哪些可行方案?逐一分析其缺陷:

方案思路缺陷
手算求导翻出微积分教材手工推导耗时且极易出错
符号微分(Symbolic Differentiation)为每个梯度推导出公式网络越深、算子越复杂,公式急剧膨胀到无法处理
有限差分(Finite Differencing)对每个参数施加微小扰动,观察损失变化计算昂贵,且数值精度差

正确答案是自动微分。通过反向传播(backpropagation),用一种动态规划的方式高效计算梯度,这也被称为反向模式自动微分。它在「扇入(fan-in)」场景下极其高效——即众多参数共同影响单个损失度量,这正是神经网络训练的情形。虽然也存在前向模式自动微分,但它更适合「扇出(fan-out)」场景(少数参数影响多个度量),并不适用于神经网络训练。

autograd 是如何工作的?

简短回答

自动微分分为两个阶段:

  • 阶段 1(前向传播):记录网络为做出预测并计算损失所使用的所有算子,形成一张计算图;
  • 阶段 2(反向传播):沿着这张记录反向遍历,逐个算子求偏导数,一直回溯到网络参数。

详细回答

MXNet 中的每个算子都定义了两个方法:forward方法按预期执行算子,backward方法返回偏导数(即输出对输入的导数)。即便是在极少数需要自定义算子的场景下,你定义的也是同样的两个方法——这与 python/mxnet/autograd.py 中Function类的设计完全一致:子类实现forwardbackward,autograd 在求梯度时就会调用用户自定义的backward而非默认链式法则。

前向传播时,autograd 记录下网络用到的算子(即forward调用序列),并用图结构保存每个算子的输入(含取值)与输出,以及算子之间的依赖关系。反向传播时,autograd 从损失度量(也可以从任意输出)出发,沿图反向调用每个算子的backward,计算损失对该算子输入的梯度(输入可能是参数)。

从 C++ 实现层面看,MarkVariables会为每个需要梯度的变量创建一个变量节点(variable node),并把梯度缓冲与grad_req(梯度需求)关联到该节点的AGInfo上(见 src/imperative/imperative.cc);反向过程则通过算子注册的FGradient属性推导输入梯度(见同文件GetBackwardDependency的实现,src/imperative/imperative.cc#L190 起)。

autograd 的优势是什么?

简短回答

灵活、自动、高效。你可以在网络中使用原生 Python 控制流,例如if条件与while循环,autograd 依然能够正确反向传播梯度。

详细回答

使用autograd的巨大收益在于定义网络时的灵活性:你可以在每一次迭代中改变运算,autograd 仍能正确反向传播梯度。这种能力常被称为「动态图」,在要求静态图的框架(如 TensorFlow)中实现起来要复杂得多。

正如其名,autograd 是自动的,反向传播的复杂细节都被封装好了。你要做的仅仅是:在需要记录梯度时告知 autograd(record上下文),并指明想计算哪个量的梯度——绝大多数情况下就是损失度量。而这些梯度计算也会被高效地执行。

如何在 MXNet Gluon 中使用 autograd?

第一步:导入并搭建网络

第一步是导入autograd包:

from mxnet import autograd

下面用一个简单的回归模型(对应前文图示)演示完整流程,稍后用 autograd 自动计算损失对每个权重参数的梯度:

import mxnet as mx from mxnet.gluon.nn import HybridSequential, Dense from mxnet.gluon.loss import L2Loss # 定义网络 net = HybridSequential() net.add(Dense(units=3)) net.add(Dense(units=1)) net.initialize() # 定义损失 loss_fn = L2Loss() # 创建模拟数据 x = mx.np.array([[0.3, 0.5]]) y = mx.np.array([[1.5]])

第二步:在 record 上下文中完成前向传播

接下来进行第一次前向传播,并让 autograd 记录计算图以便求梯度。最简单的方式就是把网络(和损失)代码放进autograd.record上下文的作用域内:

with autograd.record(): y_hat = net(x) loss = loss_fn(y_hat, y)

注意:只有我们确实想记录的操作才应放进record上下文(因为记录有计算开销)。现在 autograd 已经为这些操作构建好了一张计算图,随时可以进行反向传播。

第三步:调用 backward 启动反向传播

调用目标量的backward方法即可开始反向传播,这里目标量是loss,因为我们想求损失对参数的梯度:

loss.backward()

重要提醒:如果loss不是单个标量值(例如它是每个样本的损失,而非整批的平均损失),backward启动反向传播前会隐式地先做一次sum求和,最终算出的梯度是「损失之和」对参数的梯度。

第四步:读取梯度

至此所有 autograd 的「魔法」已完成,网络每个参数都有了梯度,可供优化器更新参数值。例如查看第一层的权重梯度:

net[0].weight.grad()

底层上,loss.backward()会调用MXAutogradBackwardExC API(见 python/mxnet/ndarray/ndarray.py),由 C++ 端逐节点反向求导并把结果写入参数对应的梯度缓冲。

高级:训练模式与推理模式的切换

部分神经网络层在训练与推理时的行为不同。典型例子:

  • Dropout:训练时随机将激活置 0,推理时保持不变;
  • BatchNorm:训练时用当前批次的统计量做归一化,推理时使用全局统计量。

在 MXNet Gluon 中,autograd对训练/推理模式的切换至关重要。默认情况下网络运行在推理模式;一旦 autograd 开始记录(例如处于autograd.record()上下文作用域内),网络就运行在训练模式

用一个单独的Dropout块来演示:

dropout = mx.gluon.nn.Dropout(rate=0.5) data = mx.np.ones(shape=(3,3)) output = dropout(data) is_training = autograd.is_training() print('is_training:', is_training, output)

上面这段代码在 autograd未记录时调用dropout,网络处于推理模式,因此输入没有任何 dropout(输出仍全是 1)。可以用autograd.is_training()确认当前模式。

with autograd.record(): output = dropout(data) print('is_training:', is_training, output)

这次在 autograd记录期间调用dropout,网络处于训练模式,输入发生了 dropout。由于 dropout 概率为 50%,输出会被自动乘以1/0.5 = 2,以保持激活的平均值不变。

也可以强制某些算子在推理模式下仍然表现得像训练时一样,例如给 Dropout 算子设置mode='always',但这种用法并不常见。

从实现看,record(train_mode=True)返回一个_RecordingStateScope(True, True)作用域(见 python/mxnet/autograd.py),进入上下文时会把「记录」与「训练」两个状态位同时置位(内部调用MXAutogradSetIsRecording/MXAutogradSetIsTraining,见 python/mxnet/autograd.py),退出时自动恢复。配套的上下文还有pause()train_mode()predict_mode(),分别用于「不记录但切换模式」等更细粒度的控制。仓库测试 tests/python/unittest/test_autograd.py 也验证了这一点:record()内 Dropout 生效,pause()内 Dropout 透传。

高级:跳过参数梯度的计算

用 MXNet Gluon 创建网络时,默认假设你需要损失对每个网络参数的梯度——因为通常要训练整个网络。调用net.initialize()后,网络参数被(惰性)初始化,同时也会为梯度分配内存,这相当于让每个参数占用的空间翻倍。完成一次前向 + 反向传播后,所有参数都会有梯度。

但有时我们并不需要全部参数的梯度,一个典型场景是**「冻结」某些层的参数**:既然不需要更新它们的值,也就不需要梯度。把参数的grad_req属性设为'null',即可告知 autograd 跳过这些梯度,节省计算时间与内存:

net[0].weight.grad_req = 'null'

grad_req的取值定义在 python/mxnet/ndarray/ndarray.py 的_GRAD_REQ_MAP中:

grad_req 取值底层编码含义
'null'0不计算该参数的梯度
'write'1每次 backward 直接覆盖写入梯度(默认值)
'add'3每次 backward 把梯度累加到已有值上

其中'add'在分布式训练等需要累积梯度的场景非常有用。Parameter.grad_req的 setter 会对取值做合法性校验(见 python/mxnet/gluon/parameter.py),并将'null'下的梯度缓冲释放、数据节点与计算图脱离(detach),从而真正省下内存。

高级:计算非参数的梯度

虽然最常见的做法是处理网络参数(Parameter是 MXNet Gluon 对可训练参数的一层抽象),但有些场景需要对非参数的量求梯度——例如生成对抗样本时,需要计算损失对输入数据的梯度。

用 autograd 实现很简单,但与参数相比有一个关键区别:参数默认就要求梯度,非参数不会。你需要显式调用.attach_grad()来声明需要梯度,然后在backward之后通过.grad访问梯度。

来看一个简单例子:设y = 2x²,用 autograd 计算yx在三个不同取值处的梯度。手算可知dy/dx = 4x,正好用来检验 autograd。由于xndarray而非Parameter,必须先调用x.attach_grad()

x = mx.np.array([1, 2, 3]) x.attach_grad() with autograd.record(): y = 2 * x ** 2 y.backward() print(x.grad)

期望输出[ 4. 8. 12.],与dy/dx = 4xx = 1, 2, 3处的取值完全一致。

从实现看,attach_grad会创建一个与x同 shape、初始化为 0 的梯度缓冲,并通过MXAutogradMarkVariables把该数组标记为需要梯度的变量(见 python/mxnet/ndarray/ndarray.py)。x.grad属性则通过MXNDArrayGetGrad取出这个缓冲(python/mxnet/ndarray/ndarray.py#L2925-L2933)。如果你想更精细地控制非参数梯度,autograd.mark_variables(variables, gradients, grad_reqs='write')还可以在标记时自定义梯度缓冲的初始值与grad_req(见 python/mxnet/autograd.py)。

高级:使用 Python 控制流构建动态图

如前所述,autograd 的一大优势是能自动计算动态图的梯度——即每次前向传播的算子都可能不同的图。一个实际例子是用树结构循环网络结合句子的解析树来解析句子;我们可以用 Python 控制流算子来构造这种依赖数据的动态流程,而不必使用 MXNet 自己的控制流算子。

下面写一个动态网络的玩具函数:加入一个if条件和一个迭代次数可变的循环,两者都依赖输入数据。虽然这些现在也能用条件算子放进静态图,但用原生控制流要自然得多:

import math def f(x): y = x # 稍后会修改 y,但仍需保留 x if x < 0.75: # num_loops 可变,因为它依赖 x num_loops = math.floor(1/(1-x.item())) for i in range(num_loops): y = y * x # 提高多项式次数 else: # 否则水平直线 y = y * 0 return y

将函数在x ∈ [0, 1]上绘制出来,可以识别出若干分段函数:[0, 1/2]是二次曲线,[1/2, 2/3]是三次曲线,[2/3, 3/4]是四次曲线,之后是水平直线。

由于存在控制流,这个函数没有向量化形式,我们另外写一个用 autograd 求梯度的辅助函数:

def get_grad(f, x): x.attach_grad() with autograd.record(): y = f(x) y.backward() return x.grad xs = mx.np.arange(0.0, 1.0, step=0.1) grads = [get_grad(f, x).item() for x in xs] print(grads)

这个玩具例子可以手算验证:对前面讨论的四个分段,期望梯度分别是2x3x²4x³0。抽查x = 0.6时手算梯度3x² = 1.08,与 autograd 计算出的1.08完全一致。

高级:自定义 head gradient

大多数情况下 autograd 了解完整的计算图并能自动算出梯度。但在少数场景,你可能有 MXNet Gluon 之外的外部后处理组件,却仍想计算对 MXNet Gluon 网络参数的梯度。

autograd 通过在.backward()中传入自定义 head gradient 来支持这一功能。当不传任何东西时(绝大多数情况),autograd 默认使用全 1 作为 head gradient。假设我们想算dz/dx,但只通过 MXNet Gluon 计算了中间变量y,那就需要先(手工或借助其他工具)算出 head gradientdz/dy,再传给.backward();autograd 会按照链式法则用它计算出dz/dx

举个例子:设y = x³(用mxnet计算)、z = y²(用numpy计算)。手算dz/dy = 2y(仍用numpy),把它作为 head gradient 交给 autograd,让它自动算dz/dx。按链式法则手算dz/dx = 6x⁵,当x = 2时期望dz/dx = 192。验证 autograd 是否给出相同结果:

x = mx.np.array([2,]) x.attach_grad() # 在 mxnet 中(在 autograd 下)计算 y with autograd.record(): y = x**3 # 在 mxnet 外部计算 dz/dy y_np = y.asnumpy() z_np = y_np**2 dzdy_np = 2*y_np # 在 mxnet 内部计算 dz/dx(给定 dz/dy) dzdy = mx.np.array(dzdy_np) y.backward(dzdy) print(x.grad)

如预期所示,x的梯度为192

需要说明的是,.backward(head_grads)的完整签名还支持retain_graphtrain_mode参数:retain_graph=True可以保留计算图以支持对同一张图再次反向传播,train_mode则控制反向传播时按训练还是推理模式处理(见 python/mxnet/autograd.py)。如果你希望梯度以新数组返回而不是写回variable.grad,也可以使用autograd.grad(heads, variables, ...)接口,它支持create_graph=True以记录梯度图、进一步计算高阶梯度(注意目前仅有限的一小部分算子支持高阶梯度,见python/mxnet/autograd.py#L272-L346)。

总结

围绕 MXNet Gluon 的autograd,本文完整覆盖了:梯度为什么重要(损失函数与参数优化)、反向模式自动微分相比手算/符号微分/有限差分在效率与精度上的优势、record+backward的完整使用范式,以及训练/推理模式切换、grad_req='null'冻结参数、attach_grad()计算非参数梯度、Python 控制流动态图、自定义 head gradient 等高级用法。

核心要点可浓缩为四句话:

  1. with autograd.record():包住前向传播,autograd 就会记录计算图;
  2. 对损失(或任意输出)调用.backward(),梯度会写回已标记变量的.grad
  3. 参数默认需要梯度,普通ndarray需先.attach_grad()
  4. grad_req='null'冻结层、用head_grads打通外部组件,可应对各类特殊训练需求。

更多 API 细节可查阅 autograd 官方 API 文档,完整的训练闭环(结合优化器与评估指标)可参考 Gluon 训练教程 与 Optimizers 文档。

  • 人工智能
  • 深度学习
  • 机器学习

【免费下载链接】mxnet

Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more

项目地址:https://gitcode.com/gh_mirrors/mxne/mxnet
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询