- 人工智能
- 深度学习
- 机器学习
【免费下载链接】mxnet
Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more
导读
本文围绕 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.py的SoftmaxCrossEntropyLoss类)。
假设已经算出了损失对每个参数的梯度,就可以借助优化器(如随机梯度下降)让参数沿梯度的相反方向微微移动,从而降低损失。关于这些优化方法,可进一步参考 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类的设计完全一致:子类实现forward与backward,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 计算y对x在三个不同取值处的梯度。手算可知dy/dx = 4x,正好用来检验 autograd。由于x是ndarray而非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 = 4x在x = 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)这个玩具例子可以手算验证:对前面讨论的四个分段,期望梯度分别是2x、3x²、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_graph与train_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 等高级用法。
核心要点可浓缩为四句话:
- 用
with autograd.record():包住前向传播,autograd 就会记录计算图; - 对损失(或任意输出)调用
.backward(),梯度会写回已标记变量的.grad; - 参数默认需要梯度,普通
ndarray需先.attach_grad(); - 用
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
相关推荐
MXNet Autograd 自动微分 API 完全指南:record、backward 与自定义梯度 Function
MXNet Autograd 自动微分 API 完全指南:record、backward 与自定义梯度 Function 本文以 MXNet Python 包中
深度学习机器学习人工智能Ring-2.6-1T长文本处理突破:256K上下文窗口的技术实现
Ring 2.6 1T长文本处理突破:256K上下文窗口的技术实现 Ring 2.6 1T是一款万亿参数级旗舰推理模型,专为复杂任务场景设计,其核心突破在于通过
计算化学新突破:AIMNet2-wb97m-d3如何通过torch.compile实现5倍GPU加速
计算化学新突破:AIMNet2 wb97m d3如何通过torch.compile实现5倍GPU加速 AIMNet2 wb97m d3作为计算化学领域的创新工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考