我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

MXNet autograd 自动微分实战指南:从梯度计算到自定义反向传播

MXNet autograd 自动微分实战指南:从梯度计算到自定义反向传播 深度学习人工智能机器学习分布式训练【免费下载链接】mxnetLightweight, 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/mx/mxnet点击查看免费下载本教程是 MXNet Crash Course 快速入门系列的第三步围绕mxnet.autograd包讲解如何在 Gluon 训练流程中完成梯度计算。你将掌握attach_grad()为 NDArray 挂载梯度、autograd.record()记录计算图、backward()触发反向传播、retain_graph控制计算图生命周期以及通过autograd.Function自定义前向/反向传播实现定制梯度。读完本节你可以无缝衔接本系列的 Step 4训练神经网络的必要组件。基本用法三步完成一次反向传播自动微分Autograd是训练神经网络的核心机制。MXNet 的autograd包会记录你在record()作用域内执行的运算并在调用backward()时按照链式法则自动计算梯度。使用前需要导入相关包并调用npx.set_np()启用 NumPy 兼容的 NDArray 接口from mxnet import np, npx from mxnet import autograd npx.set_np()以一个简单的标量函数 $f(x) 2x^2$ 为例推导可得 $\frac{dy}{dx} 4x$。首先为参数 $x$ 赋一个初始值x np.array([[1, 2], [3, 4]]) x接下来通过三步完成自动微分第一步调用attach_grad()申请梯度存储空间。在计算梯度之前需要告诉 MXNet 你打算为这个 NDArray 保存梯度x.attach_grad()从源码看attach_grad()会先为梯度分配一个与x同形状、同 dtype 的全零数组然后调用 C APIMXAutogradMarkVariables将x标记为需要计算梯度的变量见 ndarray.py 与 c_api_ndarray.cc。它的完整签名是attach_grad(grad_reqwrite, stypeNone)grad_req梯度累积方式可选write每次 backward 覆盖旧梯度默认、add每次 backward 累加到已有梯度上常用于梯度累积或null不计算梯度。内部通过_GRAD_REQ_MAP {null: 0, write: 1, add: 3}映射为整型传给底层引擎见 ndarray.pystype梯度数组的存储类型默认与x的存储类型一致可用于稀疏梯度场景。第二步在autograd.record()作用域内定义函数。把 $yf(x)$ 的表达式放在record()上下文中MXNet 就会记录下这段前向计算的执行轨迹以便后续求导with autograd.record(): y 2 * x * x第三步调用y.backward()触发反向传播y.backward()此时x.grad中保存的就是自动计算出的梯度。由于 $y2x^2$、$\frac{dy}{dx}4x$预期结果为[[4, 8], [12, 16]]验证如下x.gradbackward()的底层实现对应 C APIMXAutogradBackwardEx见 c_api_ndarray.ccPython 层的NDArray.backward(out_gradNone, retain_graphFalse, train_modeTrue)还额外支持传入out_grad指定头节点的梯度、通过retain_graph保留计算图、通过train_mode控制求导时的训练/推理模式见 ndarray.py。深入y.backward()等价于对和求导当 $y$ 包含多个元素时y.backward()等价于y.sum().backward()即先对输出求和再反向传播。下面这段代码与上面的结果完全一致with autograd.record(): y np.sum(2 * x * x) y.backward() x.grad这一设计保证了梯度可以顺利传播标量化的损失函数是训练中最常见的反向传播入口。retain_graph计算图默认只能用一次MXNet 默认在一次backward()之后就会释放计算图。若需要对同一计算图执行多次反向传播必须在调用时传入retain_graphTrue保留图结构。下面的示例演示了两种写法的差异with autograd.record(): y np.sum(2 * x * x) y.backward(retain_graphTrue) print(x.grad) print(Since you have retained your previous graph you can run backward again) y.backward() print(x.grad) try: y.backward() except: print(However, you cant do backward twice unless you retain the graph.)这段代码的执行流程是第一次backward(retain_graphTrue)保留计算图之后可以再次调用backward()第二次backward()未传retain_graph执行后计算图被释放第三次直接调用backward()会因计算图已不存在而抛出异常被try/except捕获。对应地在测试用例 test_autograd.py 中test_retain_graph也验证了「同一计算图不传retain_graph连续反向传播两次会失败而传retain_graphTrue后可以继续传播」的行为。此外NDArray还提供了drop_grad()主动释放已标记数组的梯度内存见 ndarray.py。自定义 MXNet ndarray 运算用autograd.Function改写梯度要真正理解backward()最好先弄清楚如何创建自定义运算。MXNet 的算子本质上是带有forward和backward两个方法的类其契约如下backward()的入参数目必须等于forward()的返回项数目即反向收到的梯度个数 前向的输出个数forward()的入参数目必须等于backward()的返回项数目即前向的输入个数 反向返回的梯度个数。因此你完全可以在backward()中修改梯度返回与真实导数不同的自定义梯度。下面的例子定义了一个三输入三输出的自定义算子并在反向中返回了「自定义」的梯度class MyFirstCustomOperation(autograd.Function): def __init__(self): super().__init__() def forward(self,x,y): return 2 * x, 2 * x * y, 2 * y def backward(self, dx, dxy, dy): The input number of arguments must match the number of outputs from forward. Furthermore, the number of output arguments must match the number of inputs from forward. return x, y使用这个自定义算子的方式与内置算子完全一致x np.random.uniform(-1, 1, (2, 3)) y np.random.uniform(-1, 1, (2, 3)) x.attach_grad() y.attach_grad() with autograd.record(): z MyFirstCustomOperation() z1, z2, z3 z(x, y) out z1 z2 z3 out.backward() print(np.array_equiv(x.asnumpy(), x.asnumpy())) print(np.array_equiv(y.asnumpy(), y.asnumpy()))从实现细节看autograd.Function.__call__会先临时关闭记录状态执行forward()再将自定义的反向回调通过MXCustomFunctionRecord注册进计算图反向传播时backward_entry会按grad_reqwrite/add等把自定义 backward 的返回值写入梯度缓冲区并严格校验「返回的 NDArray 个数必须等于 forward 的输入个数」见 autograd.py。每个Function实例只能被调用一次重复调用会触发断言需要每次新建实例。类中还提供了save_for_backward(*args)在forward中暂存中间张量供backward通过saved_tensors读取——这是实现数值稳定版 sigmoid 等自定义算子的常用手法见 autograd.py 中的完整示例。训练/推理模式is_training()与train_mode有时你希望函数在训练阶段和推理阶段表现不同例如 Dropout 随机丢弃、BatchNorm 使用批统计量。通过autograd.is_training()可以查询当前是否处于训练模式并据此分支def my_first_function(x): if autograd.is_training(): # Return something else when training return(4 * x) else: return(x)autograd.record()的默认行为是train_modeTrue你也可以显式指定y my_first_function(x) print(np.array_equiv(y.asnumpy(), x.asnumpy())) with autograd.record(train_modeFalse): y my_first_function(x) y.backward() print(x.grad) with autograd.record(train_modeTrue): # train_mode True by default y my_first_function(x) y.backward() print(x.grad)record(train_modeTrue)返回一个_RecordingStateScope上下文它会同时设置「记录状态」和「训练状态」两个全局开关对应的set_recording/set_training分别调用MXAutogradSetIsRecording/MXAutogradSetIsTraining并返回切换前的旧值退出with块时自动恢复见 autograd.py。除了recordautograd 模块还提供pause()在记录过程中临时停止记录常用于测试、IO 或参数更新等无需梯度的代码段train_mode()/predict_mode()只切换训练/推理状态而不改变记录状态例如在record()内部对某个子网络强制以推理模式前向。train_mode的语义在测试 test_is_train 中有完整覆盖其中还演示了backward(train_modeFalse)与record(train_modeFalse)必须配对使用否则梯度未定义。记录执行轨迹面向控制流的自动微分与静态图框架不同MXNet 的 autograd 是动态的它记录的是代码实际执行的计算轨迹因此天然支持while循环、if/else分支等控制流。下面的函数把输入a反复翻倍直到其norm这里用绝对值之和近似超过 1000再根据元素和的正负从b中挑选一个元素返回def f(a): b a * 2 while np.abs(b).sum() 1000: b b * 2 if b.sum() 0: c b[0] else: c b[1] return c记录执行轨迹并喂入一个随机初始值a np.random.uniform(size2) a.attach_grad() with autograd.record(): c f(a) c.backward()由于b是a的线性函数而c是从b中选出的某个元素因此a的梯度要么是[c/a[0], 0]要么是[0, c/a[1]]取决于最终选中了b的哪个元素。可以用下面的代码验证a.grad c / a如文档所述这里沿维度 0 存在 3 个值沿该轴取mean等价于先求和再乘以1/3这是读者在验证时需要注意的广播细节。这个例子直观展示了 MXNet 自动微分「按实际执行路径求导」的动态特性——记录的是运行时真实发生的运算序列而非静态展开的公式。进阶attach_grad()会切断梯度链路你可以针对不同的 ndarray 运算精细控制梯度传播。一个容易踩坑的点是attach_grad()会自动把自身从计算图中「分离」。这意味着在record()作用域内一旦对某个中间结果y调用了attach_grad()从该节点往前的输入x便不再出现在y的梯度回传路径上。对比下面两段代码即可看出差异。第一段代码对中间结果y调用了attach_grad()with autograd.record(): y 3 * x y.attach_grad() z 4 * y 2 * x z.backward() print(x.grad) print(y.grad)第二段代码没有调用with autograd.record(): y 3 * x z 4 * y 2 * x z.backward() print(x.grad) print(y.grad)两段代码中x.grad与y.grad的结果并不相同第一段里y被attach_grad()标记为新的变量起点梯度只回传到y为止第二段里梯度会继续穿过y回传到x。在源码层面attach_grad()通过MXAutogradMarkVariables把该数组登记为一个独立的变量节点从而切断了它与更早计算图的关联若想显式切断某一支路且不申请梯度缓冲也可以使用NDArray.detach()见 ndarray.py测试 test_detach_updated_grad 对该行为做了专门验证。组合使用把record()封装进函数autograd.record()完全可以封装进普通函数中这样调用方无需关心记录逻辑def my_second_function(x): with autograd.record(): return(2 * x)y my_second_function(x) y.backward() print(x.grad)多个这样的函数还可以互相组合梯度会沿着嵌套的记录作用域正确传播y my_second_function(x) with autograd.record(): z my_second_function(y) 2 z.backward() print(x.grad)这一模式正是 Gluon 模块的底层工作方式nn.Block的前向计算内部自动管理记录作用域用户在训练循环中只需写出with autograd.record(): output net(data)的范式。小结与下一步本节覆盖了 MXNet autograd 的完整使用路径基础流程attach_grad()申请梯度 →record()记录前向 →backward()反向传播 →x.grad读取梯度关键参数grad_req控制梯度写入/累加方式retain_graph控制计算图是否可复用train_mode控制训练/推理语义自定义算子继承autograd.Function重写forward/backward可注入自定义梯度契约是「backward 入参 forward 输出数backward 返回 forward 输入数」动态图特性autograd 按实际执行轨迹求导天然支持循环与分支控制流梯度切断attach_grad()与detach()会在指定节点截断梯度回传。本系列的后续内容将在此基础上展开接下来学习如何初始化权重、选择损失函数、评估指标与优化器请继续阅读 Step 4: Necessary components to train the neural network。若想复习本教程的前置知识可回顾 Step 1NDArray 与 NumPy 兼容接口 与 Step 2创建神经网络。完整的快速入门课程目录见 Crash Course 索引本系列各小节可独立阅读也可按顺序通读。赞分享深度学习人工智能机器学习分布式训练【免费下载链接】mxnetLightweight, 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/mx/mxnet点击查看免费下载相关推荐MXNet 自动微分实战指南mxnet.autograd 记录式计算图、反向传播与自定义梯度MXNet 自动微分实战指南mxnet.autograd 记录式计算图、反向传播与自定义梯度 导读 mxnet.autograd 是 MXNet 的命令式自动深度学习人工智能机器学习分布式训练MXNet autograd 自动微分实战指南从 NDArray 梯度计算到动态控制流MXNet autograd 自动微分实战指南从 NDArray 梯度计算到动态控制流 autograd 是 MXNet 的自动微分引擎它让求损失函数对权深度学习机器学习人工智能Warp 自动微分完全指南Tape 反向传播、Jacobian 计算与自定义梯度Warp 自动微分完全指南Tape 反向传播、Jacobian 计算与自定义梯度 本篇技术指南围绕 NVIDIA WarpGPU 加速的仿真、机器人与机器学高性能计算物理引擎图形学机器人创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表