☰
昇思25天学习打卡营第6天 | 函数式自动微分:grad与stop_gradient配置实战
2026/9/29 4:02:07 网站建设 项目流程

1. 从一次梯度对不上的调试说起

如果你正在跟昇思25天学习打卡营,第6天大概率会卡在同一个地方:明明照着示例写了mindspore.grad,可一旦函数多返回了一个z,求出来的w、b梯度就跟预期对不上,数值直接翻倍。这不是你抄错了,而是函数式自动微分里一个非常典型的现象——多输出函数的梯度会累加所有输出项对参数的贡献。

MindSpore 的函数式自动微分(Functional AutoDiff)核心就两个接口:grad和value_and_grad。它们把求导这件事做成了一种“函数变换”:你给它一个函数,它还你一个求导后的函数。这跟数学里d/dx算子的语义几乎一致,所以叫“函数式”。而stop_gradient则是配套的“截断开关”,用来告诉框架:这个张量在反向传播时当作常数,梯度到这儿就停。

这篇内容面向两类人:一是打卡营里想把第6天作业真正跑通的学员,二是刚接触深度学习、对“梯度到底怎么流”还没建立直觉的入门者。我会用一个单层线性变换 + 二值交叉熵的最小例子,把grad的grad_position参数、stop_gradient的截断效果、以及has_aux这个更省事的替代方案串起来。每一步都给可复制的代码和预期输出,你照着敲一遍,梯度控制逻辑基本就通了。

需要说明的是,下面所有实验都在 CPU 上跑,MindSpore 版本建议 2.x。如果你还没装环境,先pip install mindspore即可,不需要 GPU 也能验证全部结论。

2. TaoToken 前置:把模型对话和接入文档放在手边

打卡营的代码本身不依赖外部服务,但学习过程中有两类问题很容易卡住:一是 MindSpore 报错信息看不懂,二是想对照某个 API 的官方语义。这时候我会开一个模型对话窗口,把报错原文贴进去让它解释,比翻文档快很多。

如果你也想用这种方式辅助学习,可以先把入口准备好。模型对话入口在https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite,适合贴报错、问概念、让它帮你逐行解释梯度代码。接入相关的文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面能查到接口调用的基本约定。

真正要写代码调 API 的时候,Key 在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite生成,API 基地址是https://taotoken.net/api。注意这个/api地址后面不加任何查询参数,直接作为 base_url 用就行。如果你后面要长期做编码类任务或者搭 Agent,可以看下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite,它更适合高频调用场景。

这一节不是必须的,但把对话和文档两个页面开着,遇到grad_position该填几、has_aux返回结构是什么这类问题时,能省不少来回搜索的时间。

3. 可复制配置:grad 与 stop_gradient 的最小骨架

先把依赖和计算图搭起来。这个例子里x是 5 维输入,w是 5×3 权重,b是 3 维偏置,y是 3 维标签。损失用binary_cross_entropy_with_logits,它内部自带 sigmoid,所以z不需要再过激活函数。

import numpy as np import mindspore from mindspore import nn, ops from mindspore import Tensor, Parameter # 输入与标签 x = ops.ones(5, mindspore.float32) y = ops.zeros(3, mindspore.float32) # 可训练参数 w = Parameter(Tensor(np.random.randn(5, 3), mindspore.float32), name='w') b = Parameter(Tensor(np.random.randn(3,), mindspore.float32), name='b') def function(x, y, w, b): z = ops.matmul(x, w) + b loss = ops.binary_cross_entropy_with_logits( z, y, ops.ones_like(z), ops.ones_like(z) ) return loss loss = function(x, y, w, b) print(loss)

跑出来是一个标量,类似0.17323041。到这里只是前向计算,还没有求导。

关键在下一步。mindspore.grad的第一个参数是待求导函数,第二个参数grad_position指定对第几个入参求导。注意它是从 0 开始数的:x是 0,y是 1,w是 2,b是 3。所以对w、b求导就填(2, 3)。

grad_fn = mindspore.grad(function, (2, 3)) grads = grad_fn(x, y, w, b) print(grads)

输出是两个 Tensor:一个形状[5, 3]对应w的梯度,一个形状[3]对应b的梯度。因为x全是 1,所以w梯度矩阵的每一行都相同,这是正常现象,不是 bug。

现在引入多输出的坑。把函数改成同时返回loss和z:

def function_with_logits(x, y, w, b): z = ops.matmul(x, w) + b loss = ops.binary_cross_entropy_with_logits( z, y, ops.ones_like(z), ops.ones_like(z) ) return loss, z grad_fn = mindspore.grad(function_with_logits, (2, 3)) grads = grad_fn(x, y, w, b) print(grads)

你会发现梯度值变了,比之前大了一截。原因是grad默认对所有输出求和后再求导,z也参与了梯度贡献。如果你只想让loss影响梯度,就得用stop_gradient把z截断:

def function_stop_gradient(x, y, w, b): z = ops.matmul(x, w) + b loss = ops.binary_cross_entropy_with_logits( z, y, ops.ones_like(z), ops.ones_like(z) ) return loss, ops.stop_gradient(z) grad_fn = mindspore.grad(function_stop_gradient, (2, 3)) grads = grad_fn(x, y, w, b) print(grads)

这次梯度值就回到和最初function一致了。ops.stop_gradient(z)的作用是:前向传播时z的值照常返回,但反向传播时把它当常数,梯度不往它身上流。

还有一个更省事的写法是has_aux=True。它等价于自动帮你对辅助输出做stop_gradient:

grad_fn = mindspore.grad(function_with_logits, (2, 3), has_aux=True) grads, (z,) = grad_fn(x, y, w, b) print(grads, z)

注意返回结构变成了(梯度元组, 辅助输出元组),所以解包时要写成grads, (z,)。梯度值和stop_gradient版本一致,同时z还能拿到。

4. 验证请求与成功结果:用 value_and_grad 跑通神经网络反向传播

前面都是纯函数,实际训练里模型是nn.Cell的子类。把同样的逻辑搬到 Cell 上,用value_and_grad一次拿到 loss 和梯度。

class Network(nn.Cell): def __init__(self): super().__init__() self.w = w self.b = b def construct(self, x): return ops.matmul(x, self.w) + self.b model = Network() loss_fn = nn.BCEWithLogitsLoss() def forward_fn(x, y): z = model(x) return loss_fn(z, y) grad_fn = mindspore.value_and_grad( forward_fn, None, weights=model.trainable_params() ) loss, grads = grad_fn(x, y) print(loss) print(grads)

这里value_and_grad的第二个参数传None,表示不对输入求导,只对weights指定的可训练参数求导。跑出来的梯度应该和前面function版本的w、b梯度完全一致。如果一致,说明你已经把函数式自动微分和面向对象的 Cell 模型打通了。

验证成功的标志有三个:一是loss是标量且数值合理;二是grads里w梯度形状[5, 3]、b梯度形状[3];三是数值和纯函数版本对得上。三个都满足,这一天的核心目标就达成了。

5. 本篇常见错排查

报错一:grad_position填错导致梯度形状不对。最常见的是把(2, 3)写成(1, 2),结果对y和w求了导,y是常量标签,梯度自然不对。记住入参顺序:x=0, y=1, w=2, b=3。如果你改了函数签名,位置要重新数。

报错二:多输出时梯度翻倍却找不到原因。只要函数返回了多个 Tensor,grad就会对所有输出求和求导。解决办法二选一:要么在返回前对辅助输出套ops.stop_gradient,要么在grad里加has_aux=True。后者更推荐,因为不用改函数体。

报错三:has_aux=True后解包报too many values to unpack。这是因为返回结构从grads变成了(grads, aux)。正确写法是grads, (z,) = grad_fn(...),注意z外面那层括号不能少,因为辅助输出本身是个元组。

报错四:value_and_grad里weights传了空列表。如果model.trainable_params()返回空,说明参数没有用Parameter包装,或者没挂到 Cell 属性上。检查self.w = w里的w是不是Parameter类型。

报错五:CPU 上跑ops.ones_like报 dtype 不匹配。binary_cross_entropy_with_logits的权重参数要求 float32,如果你前面用了 float64 建张量,这里会报错。统一用mindspore.float32就行。

6. 继续往下走:把梯度控制用起来

函数式自动微分的价值不在于会调grad,而在于你能精确控制“哪些输出影响梯度、哪些不影响”。stop_gradient和has_aux就是两个最常用的控制手段:前者用于手动截断,后者用于多输出场景的自动处理。

如果你在打卡营后续要搭更复杂的模型,比如多任务学习里只想让某个分支的 loss 回传,或者预训练层冻结只训新加层,这两个接口会反复出现。建议把上面的代码存成一个.py文件,改改grad_position和has_aux多跑几组,观察梯度数值的变化,比只看文档印象深得多。

遇到 MindSpore 的报错拿不准时,把完整 traceback 贴到模型对话里问一下,通常能快速定位到是grad_position还是has_aux的问题。接入文档里也有grad和value_and_grad的参数说明,配合着看效率更高。

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

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

立即咨询