当前位置: 首页 > news >正文

PyTorch反向传播实战:手动推导梯度流与NaN调试指南

1. 这不是数学推导题,而是一次神经网络的“电流回溯”实操

你有没有试过在调试一个三层全连接网络时,梯度突然变成 NaN,loss 曲线像坐过山车一样直冲云霄?或者明明改了学习率、加了 BatchNorm,权重更新却纹丝不动,仿佛整个网络被冻住了?我第一次遇到这种问题时,在 Jupyter 里疯狂 print 中间层输出,盯着grad_fn=<AddBackward0>发呆——那不是抽象符号,那是神经网络内部真实流动的“信号回路”。Backpropagation(反向传播),从来就不是教科书里那个优雅的链式法则公式,而是一场对计算图结构、张量形状、内存生命周期和数值稳定性的综合压力测试。它直接决定你的模型能不能学、学得多快、学得多稳。这篇文章不讲“什么是反向传播”,而是带你亲手拆开 PyTorch 的 autograd 引擎,用最原始的手动计算验证每一步梯度流向,把loss.backward()这行代码背后发生的物理过程,一帧一帧还原出来。你会看到:为什么torch.no_grad()不是魔法开关而是内存保护罩;为什么view()reshape()更危险;为什么detach().data在梯度流中扮演完全不同的角色;甚至为什么一个没注意的.sum()就能让整个梯度归零。无论你是刚学完《深度学习入门》的研究生,还是写了三年模型但还没 debug 过nan_grad的工程师,只要你还在调 loss、改结构、换优化器,这篇内容就是你每天都在用、却从未真正看清的底层操作系统。我们不画计算图,我们直接用电压表测电流。

2. 核心设计逻辑:为什么必须手动重走一遍反向路径?

2.1 教科书推导 vs. 工程实现:两个世界的根本差异

教科书上写反向传播,永远从损失函数 $L$ 开始,一层层套用链式法则:$\frac{\partial L}{\partial w^{(l)}} = \frac{\partial L}{\partial a^{(l)}} \cdot \frac{\partial a^{(l)}}{\partial z^{(l)}} \cdot \frac{\partial z^{(l)}}{\partial w^{(l)}}$。这个公式本身没错,但它隐含了一个致命假设:所有中间变量都是标量,所有求导都是完整雅可比矩阵乘法。而现实中的 PyTorch/TensorFlow,处理的是batched tensor—— 一个 shape 为[32, 64]的激活值,它的梯度不是[32, 64]的“梯度矩阵”,而是与之形状严格一致的梯度张量,其每个元素代表 loss 对该位置输入的偏导数。这就是第一个断层:数学上的“对向量求导”在工程中被降维为“对每个元素求导”。当你写y = x @ W + b,PyTorch 并不计算 $\frac{\partial y}{\partial W} \in \mathbb{R}^{d_{in} \times d_{out}}$,而是直接生成一个 shape 为[d_in, d_out]的梯度张量,其值等于 $x^T \cdot \frac{\partial L}{\partial y}$。这个转换过程,就是手动重算的核心价值点——它强迫你面对维度匹配这个最常出错的环节。

提示:几乎所有RuntimeError: grad can be implicitly created only for scalar outputs错误,都源于你试图对非标量 loss 调用.backward(),而没意识到 PyTorch 要求 loss 必须是 0-dim tensor(即torch.tensor(1.23)而非torch.tensor([1.23]))。这不是 bug,是设计哲学:梯度必须有唯一确定的流向起点。

2.2 自动求导引擎的“黑箱”代价:我们到底放弃了什么控制权?

PyTorch 的autograd是基于动态计算图(Dynamic Computation Graph)构建的。每次前向执行,它就在内存里实时记录下所有操作节点(AddBackward,MulBackward,ViewBackward),构成一张 DAG(有向无环图)。反向传播时,它按拓扑逆序遍历这张图,调用每个节点注册的backward()方法,把上游梯度“分发”给下游输入。这个机制带来了极致的灵活性(支持 if/while 控制流),但也埋下了三个隐形地雷:

  1. 内存泄漏风险:计算图节点会强引用所有参与运算的 tensor,只要图没被释放,这些 tensor 就无法被 GC 回收。torch.no_grad()的本质,是彻底不构建计算图,而非“不计算梯度”——它连grad_fn都不设,从源头切断内存绑定。
  2. 梯度覆盖陷阱tensor.backward(grad)中的grad参数,是累加到tensor.grad上的,不是赋值。如果你在循环中反复调用loss.backward()而不optimizer.zero_grad(),梯度会越积越大,最终爆炸。这和数学上“求导结果唯一”完全相悖。
  3. 视图操作的“幽灵梯度”x.view(-1, 10)创建的是x的视图(view),共享底层存储。当对 view 求导时,梯度会自动“折叠”回原 tensor。但若你在 view 后又做了x_copy = x.clone(),再对x_copy操作,梯度就只流向x_copyx本身梯度为 0——这种细微差别,只有手动推一遍才能刻进肌肉记忆。

所以,手动重走反向路径,不是为了取代backward(),而是为了建立一套工程直觉:看到y = torch.relu(x),立刻反应出dy/dx是一个与x同 shape 的 mask(x > 0为 1,否则为 0);看到z = y.mean(),立刻知道dz/dy是一个全1/len(y)的张量;看到loss = F.cross_entropy(logit, target),立刻明白它内部已完成了 softmax + log + one-hot 索引的整套梯度合成。这种直觉,是 debugnan_grad时最快的定位器。

2.3 手动计算的边界在哪里?哪些必须交还给 autograd?

必须明确:手动计算反向传播,只适用于教学验证和极端 debug 场景。你永远不会、也不应该在生产代码中手动写dW = x.T @ dy。它的价值在于划定能力边界:

  • 可以且应该手动验证的部分:单层线性变换、激活函数、loss 函数的梯度公式、shape 变换(如view,permute)对梯度的影响。这些是“原子操作”,错误模式高度可复现。
  • 必须交给 autograd 的部分:嵌套函数组合(如F.dropout(F.relu(x @ W + b)))、高阶导数(.backward(retain_graph=True))、自定义 C++ 扩展的梯度逻辑。这些涉及图节点间的复杂依赖,手动推导极易出错。
  • 绝对禁止手动干预的部分optimizer.step()的参数更新逻辑、torch.compile()的图融合优化、分布式训练中的梯度同步(DistributedDataParallel)。这些已脱离单机计算范畴,属于系统级抽象。

我的经验是:每当新引入一个自定义 layer(比如自己写的 attention mask 或 positional encoding),第一件事就是在 CPU 上用小 batch(batch_size=1)手动跑通前向+反向,确认param.grad的 shape 和数值范围符合预期,再扔进大集群训练。这5分钟,能省掉后续两小时的all-reduce timeout排查。

3. 核心细节解析:从最简网络开始,逐层解剖梯度流

3.1 构建最小可验证单元:一个带 ReLU 的单层感知机

我们从最简结构出发,剥离所有干扰项。目标网络:输入x ∈ R^{2},权重W ∈ R^{2×1},偏置b ∈ R^{1},激活函数ReLU,loss 用MSE。前向过程:

import torch import torch.nn.functional as F x = torch.tensor([2.0, 3.0], requires_grad=False) # input, no grad needed W = torch.tensor([[1.0], [2.0]], requires_grad=True) # weight, need grad b = torch.tensor([0.5], requires_grad=True) # bias, need grad z = x @ W + b # linear: z = xW + b, shape [1] a = F.relu(z) # activation: a = relu(z), shape [1] y_true = torch.tensor([5.0]) # target loss = F.mse_loss(a, y_true) # loss = (a - y_true)^2 / 2

此时loss是一个标量 tensor,loss.grad_fn指向MseLossBackward。现在,我们暂停loss.backward(),手动计算每一步梯度。

3.2 第一步:Loss 对激活值 a 的梯度(dL/da)

MSE Loss 定义为 $L = \frac{1}{2}(a - y_{true})^2$。对其求导: $$\frac{dL}{da} = (a - y_{true})$$ 代入数值:a = relu(2*1 + 3*2 + 0.5) = relu(8.5) = 8.5y_true = 5.0,所以dL/da = 8.5 - 5.0 = 3.5。这是一个标量,shape 为[](0-dim tensor)。这步看似简单,却是整个链条的“电压源”——所有后续梯度都源于此。关键洞察dL/da的 shape 必须与a完全一致。如果a[32, 1]dL/da也必须是[32, 1],其每个元素对应 batch 中每个样本的(a_i - y_i)。PyTorch 的F.mse_loss默认对 batch 维度取均值,所以dL/da[32, 1],而非[1]。这是新手最常混淆的点。

注意:F.mse_loss(input, target, reduction='none')会返回[N]形状的 loss tensor,此时dL/da也是[N],需手动mean()sum()后再 backward。不指定reduction时,默认'mean',梯度已自动除以 N。

3.3 第二步:激活值 a 对线性输出 z 的梯度(da/dz)

ReLU 函数定义为 $a = \max(0, z)$,其导数是 Heaviside 阶跃函数: $$\frac{da}{dz} = \begin{cases} 1 & \text{if } z > 0 \ 0 & \text{if } z < 0 \ \text{undefined} & \text{if } z = 0 \end{cases}$$ PyTorch 在z=0处约定导数为 0(实际实现中为 subgradient)。当前z = 8.5 > 0,所以da/dz = 1。这是一个标量,但要注意:在 tensor 计算中,它表现为一个与z同 shape 的 broadcastable tensor。即da/dz的 shape 是[1],值为1.0。这步的物理意义是:激活值的变化,100% 传递给线性输出的变化。如果z是负数(比如-2.0),da/dz = 0,梯度在此处彻底中断——这就是著名的 “dead ReLU” 现象,手动计算时一眼就能看出问题根源。

3.4 第三步:线性输出 z 对权重 W 和偏置 b 的梯度(dz/dW, dz/db)

这是最关键的维度匹配环节。z = x @ W + b,其中x[2]W[2, 1]b[1]z[1]。根据矩阵微积分规则:

  • $\frac{\partial z}{\partial W} = x^T$,shape 为[1, 2](因为W[2, 1],导数应是[1, 2]
  • $\frac{\partial z}{\partial b} = 1$,shape 为[1]

但 PyTorch 的梯度存储方式是:W.grad的 shape 必须与W完全一致,即[2, 1]。所以x^T需要被 reshape 为[2, 1]。计算过程:

  • x^T = [2.0, 3.0].T = [[2.0, 3.0]](shape[1, 2]
  • 转置后变为[[2.0], [3.0]](shape[2, 1]),即x.unsqueeze(1)
  • b.grad = 1.0(标量),broadcast 到[1]

现在,应用链式法则:

  • dL/dW = (dL/da) * (da/dz) * (dz/dW) = 3.5 * 1.0 * [[2.0], [3.0]] = [[7.0], [10.5]]
  • dL/db = (dL/da) * (da/dz) * (dz/db) = 3.5 * 1.0 * 1.0 = 3.5

验证:运行loss.backward()后,检查W.gradb.grad

loss.backward() print("W.grad:", W.grad) # tensor([[7.0000], [10.5000]]) print("b.grad:", b.grad) # tensor([3.5000])

完全匹配!这证明我们的手动推导没有维度错误。实操心得:每次写自定义 layer,我都会在forward返回前加一句print(f"forward out shape: {out.shape}"),在backward函数里第一行加print(f"grad_output shape: {grad_output.shape}"),确保输入输出 shape 对得上。90% 的RuntimeError都发生在这两行之间。

3.5 第四步:引入 Batch 维度——从标量到张量的质变

将输入扩展为 batch:x ∈ R^{3×2}W ∈ R^{2×1}b ∈ R^{1},则z ∈ R^{3×1}a ∈ R^{3×1}loss = MSE(a, y_true)y_true ∈ R^{3×1}。前向:

x = torch.tensor([[2.0, 3.0], [1.0, 4.0], [0.5, 2.5]], requires_grad=False) # [3,2] W = torch.tensor([[1.0], [2.0]], requires_grad=True) # [2,1] b = torch.tensor([0.5], requires_grad=True) # [1] y_true = torch.tensor([[5.0], [6.0], [4.0]]) # [3,1] z = x @ W + b # [3,2] @ [2,1] + [1] -> [3,1] a = F.relu(z) # [3,1] loss = F.mse_loss(a, y_true) # default reduction='mean'

现在手动计算dL/daF.mse_loss对 batch 取均值,所以dL/da = (a - y_true) / N,其中N=3。计算a

  • z[0] = 2*1 + 3*2 + 0.5 = 8.5 → a[0] = 8.5
  • z[1] = 1*1 + 4*2 + 0.5 = 9.5 → a[1] = 9.5
  • z[2] = 0.5*1 + 2.5*2 + 0.5 = 5.5 → a[2] = 5.5所以a - y_true = [[3.5], [3.5], [1.5]],除以 3 得dL/da = [[1.1667], [1.1667], [0.5]]

da/dz仍是[3,1]的 mask:[[1], [1], [1]](因所有z>0)。

dz/dWx[3,2]W[2,1]dz/dW = x^T应为[1,3,2]?不对!矩阵求导规则在此升级:对于z = x @ W∂z/∂W是一个三阶张量,但 PyTorch 使用Einstein summation convention简化:dL/dW = x.T @ dL/dzx.T[2,3]dL/dz[3,1],结果是[2,1],完美匹配W的 shape。计算:

  • x.T = [[2,1,0.5], [3,4,2.5]]([2,3])
  • dL/dz = dL/da * da/dz = [[1.1667], [1.1667], [0.5]]([3,1])
  • x.T @ dL/dz = [[2*1.1667 + 1*1.1667 + 0.5*0.5], [3*1.1667 + 4*1.1667 + 2.5*0.5]] = [[3.5834], [10.0834]]

运行loss.backward()验证:

loss.backward() print("W.grad:", W.grad) # tensor([[3.5833], [10.0833]])

误差来自浮点精度,完全一致。这个计算揭示了核心规律:dL/dW总是input.T @ dL/doutputdL/db总是dL/doutput.sum(0)(对 batch 维度求和)。记住这个口诀,比背公式管用十倍。

4. 实操过程:用 PyTorch 原生工具链完成端到端验证

4.1 构建可插拔的梯度验证器(Gradient Verifier)

手动计算易错,我们需要一个自动化校验框架。核心思想:对任意nn.Module,用torch.autograd.grad()获取其输出对参数的梯度,再与module.parameters().grad属性对比。以下是一个健壮的验证器:

import torch import torch.nn as nn import torch.nn.functional as F class GradientVerifier: def __init__(self, module: nn.Module, input_shape: tuple, device='cpu'): self.module = module.to(device) self.device = device self.input_shape = input_shape def _generate_input(self, batch_size=1): """生成随机但可控的输入,避免梯度为0""" # 使用 small random values to avoid dead ReLU, but not too small for numeric stability x = torch.randn(batch_size, *self.input_shape, device=self.device) * 0.1 + 0.5 return x.requires_grad_(True) def _compute_autograd_grads(self, x, y_true=None): """用 autograd.grad 计算梯度,不污染原参数""" # 前向 y_pred = self.module(x) # 构造 loss:若提供 y_true 用 MSE,否则用 y_pred.sum()(确保标量) if y_true is not None: loss = F.mse_loss(y_pred, y_true) else: loss = y_pred.sum() # 获取所有可训练参数的梯度 params = list(self.module.parameters()) grads = torch.autograd.grad(loss, params, retain_graph=True, allow_unused=True) return grads def _compute_manual_grads(self, x, y_true=None): """手动计算梯度(此处为占位,实际需按网络结构编写)""" # 示例:对单层线性层手动计算 if isinstance(self.module, nn.Linear): y_pred = self.module(x) if y_true is not None: dL_dy = (y_pred - y_true) / y_pred.numel() # mean reduction else: dL_dy = torch.ones_like(y_pred) # dL/dW = x.T @ dL/dy dL_dW = x.t() @ dL_dy # dL/db = dL/dy.sum(0) dL_db = dL_dy.sum(0) return [dL_dW, dL_db] else: raise NotImplementedError("Manual grad for this module not implemented") def verify(self, batch_size=1, y_true=None, tol=1e-5): """主验证函数""" x = self._generate_input(batch_size) if y_true is not None: y_true = y_true.to(self.device) # 获取 autograd 梯度 auto_grads = self._compute_autograd_grads(x, y_true) # 获取手动梯度(或使用其他方法) try: manual_grads = self._compute_manual_grads(x, y_true) except NotImplementedError: # fallback: use autograd.grad on each param separately for debugging print("Manual grad not implemented; using autograd.grad per param for inspection") manual_grads = auto_grads # 逐个比较 for i, (auto_g, manual_g) in enumerate(zip(auto_grads, manual_grads)): if auto_g is None or manual_g is None: print(f"Param {i}: grad is None, skip") continue diff = torch.abs(auto_g - manual_g).max().item() if diff > tol: print(f"❌ Param {i} FAILED: max diff = {diff:.6f} > {tol}") print(f" auto: {auto_g.flatten()[:3]}...") print(f" manu: {manual_g.flatten()[:3]}...") return False else: print(f"✅ Param {i} PASSED: max diff = {diff:.6f}") return True # 使用示例 model = nn.Sequential( nn.Linear(2, 3), nn.ReLU(), nn.Linear(3, 1) ) verifier = GradientVerifier(model, input_shape=(2,)) verifier.verify(batch_size=2)

这个验证器的价值在于:它把抽象的“梯度是否正确”转化为具体的max_diff < 1e-5数值判断。我在调试 Transformer 的MultiheadAttention时,就是靠它发现attn_weights的 softmax 梯度在mask处理时漏掉了inf的梯度屏蔽,导致nan

4.2 关键环节:ReLU 梯度的“硬截断”效应实测

ReLU 的梯度在z<=0时为 0,这会导致梯度消失。我们用实验量化其影响:

import matplotlib.pyplot as plt # 测试不同初始化对 ReLU 梯度死亡率的影响 def test_relu_dead_ratio(init_method='kaiming', n_samples=10000): if init_method == 'kaiming': # Kaiming 初始化:variance = 2/n_in w = torch.randn(100, 100) * (2/100)**0.5 elif init_method == 'xavier': w = torch.randn(100, 100) * (2/(100+100))**0.5 else: w = torch.randn(100, 100) * 0.1 x = torch.randn(n_samples, 100) # random input z = x @ w # linear output a = F.relu(z) # 计算 dead neuron ratio: neurons with z <= 0 dead_ratio = (z <= 0).float().mean().item() active_ratio = (a > 0).float().mean().item() return dead_ratio, active_ratio methods = ['kaiming', 'xavier', 'normal_0.1'] results = [test_relu_dead_ratio(m) for m in methods] plt.figure(figsize=(10,4)) plt.subplot(1,2,1) plt.bar(methods, [r[0] for r in results]) plt.title('Dead Neuron Ratio (z <= 0)') plt.ylabel('Ratio') plt.subplot(1,2,2) plt.bar(methods, [r[1] for r in results]) plt.title('Active Neuron Ratio (a > 0)') plt.ylabel('Ratio') plt.show()

实测结果:Kaiming 初始化下dead_ratio ≈ 0.5,Xavier 约0.45,普通正态分布0.65。这解释了为什么 Kaiming 是 ReLU 的黄金搭档——它让输入z的分布中心在 0 附近,恰好一半激活一半抑制,最大化信息流。这个实验不能只看结论,要亲手跑一遍。当你看到柱状图上 Kaiming 的蓝色柱子明显低于其他两个,那种“原来如此”的顿悟感,是读十篇论文都换不来的。

4.3 终极挑战:Cross-Entropy Loss 的梯度合成全过程

分类任务中,F.cross_entropy是最常用的 loss,但它内部封装了 softmax + log + one-hot 索引三步。手动拆解它,是理解分类梯度的关键:

# 假设 logits = [z0, z1, z2], target = 1 (class index) logits = torch.tensor([[2.0, 5.0, 1.0]], requires_grad=True) # [1,3] target = torch.tensor([1]) # class 1 # Step 1: Softmax exp_logits = torch.exp(logits) # [1,3] softmax = exp_logits / exp_logits.sum(dim=1, keepdim=True) # [1,3] # Step 2: Log log_softmax = torch.log(softmax) # [1,3] # Step 3: Negative log-likelihood: -log_softmax[0, target] nll_loss = -log_softmax[0, target] # scalar # 手动计算梯度:d(nll)/d(logits) # 结论:d(nll)/d(z_i) = softmax[i] - (1 if i==target else 0) # 即:对 target 类,梯度 = softmax[target] - 1;对其他类,梯度 = softmax[i] softmax_val = softmax.detach().numpy()[0] target_grad = softmax_val.copy() target_grad[target] -= 1.0 print("Softmax:", softmax_val) # [0.042, 0.912, 0.046] print("Target grad:", target_grad) # [0.042, -0.088, 0.046] # 验证 autograd nll_loss.backward() print("Autograd grad:", logits.grad) # tensor([[0.042, -0.088, 0.046]])

这个结果极具启发性:Cross-Entropy 的梯度,本质上是预测概率分布与真实 one-hot 分布的差值-0.088的负梯度,意味着模型对正确类别的置信度过高(0.912),需要略微降低其 logits;而0.042的正梯度,意味着对错误类别的置信度虽低,但仍需进一步压制。这正是分类任务的优化本质——拉大正确类与错误类的 logits 差距。把这个公式刻在脑子里,下次看到logits.grad的数值,你就能立刻判断模型是在“过度自信”还是“犹豫不决”。

5. 常见问题与排查技巧实录:那些让工程师熬夜的梯度陷阱

5.1 问题速查表:高频梯度异常现象与根因

现象典型报错/表现根本原因快速定位技巧
梯度全为零param.grad全 0,loss不下降1.requires_grad=False未设
2. 使用了tensor.data.detach()断开了图
3. ReLU 输入全 ≤0(dead ReLU)
print(param.requires_grad)
print((z <= 0).all())
梯度爆炸(NaN)loss突然变infnanparam.gradnan1. 学习率过大
2. 权重初始化方差过大
3. 梯度裁剪未启用
4.log(0)1/0在自定义 loss 中
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
print(torch.isnan(loss).any())
梯度不匹配(Shape Error)RuntimeError: Expected object of scalar type...1.loss不是标量(如loss = F.mse_loss(..., reduction='none')
2.targetpredshape 不一致(如[32,10]vs[32]
print(loss.shape, loss.requires_grad)
print(pred.shape, target.shape)
梯度累加(Accumulation)loss下降极慢,param.grad越来越大optimizer.zero_grad()忘记调用,梯度在多次backward()中累加step()前加print(param.grad.norm()),观察是否单调增长
梯度消失(Vanishing)浅层param.grad极小(1e-10),深层正常1. Sigmoid/Tanh 激活函数饱和
2. 网络过深,梯度连乘衰减
3. 初始化不当(如std=0.01
print([p.grad.norm().item() for p in model.parameters()])

5.2 实战排障:一次真实的nan_grad追踪全过程

上周,一个同事的 BERT 微调任务在第 123 步突然loss=nan。我们按以下步骤 15 分钟内定位:

  1. 冻结上游,聚焦下游:注释掉model.bert,只保留model.classifier,重新训练。loss正常 → 问题在 BERT 内部。
  2. 插入梯度探针:在BertLayer.forward结尾添加:
    print(f"Layer {layer_idx} output norm: {hidden_states.norm().item():.4f}") print(f"Layer {layer_idx} grad norm: {hidden_states.grad.norm().item():.4f}")
    发现第 11 层输出norm=120.5,但梯度norm=inf→ 梯度在第 11 层爆炸。
  3. 检查该层组件BertLayer包含attention+intermediate+output。分别在attention.forwardintermediate.forward后打印norm。发现intermediate.dense_act(GELU)输出norm=350.2,远超其他层。
  4. 溯源 GELU 输入:GELU 公式为x * Φ(x),其中Φ是标准正态 CDF。当x很大时,Φ(x)≈1,但计算torch.erf(x/sqrt(2))会溢出。查看输入x,发现其max=15.8erf(15.8/sqrt(2))超出 float32 表示范围。
  5. 修复方案:在 GELU 实现中加入截断:
    def gelu(x): # Clip input to prevent erf overflow x = torch.clamp(x, min=-10.0, max=10.0) return x * 0.5 * (1.0 + torch.erf(x / 1.414213562))

这个案例说明:nan_grad很少是单一原因,而是多个脆弱环节(初始化+激活函数+数值范围)的连锁失效。手动反向传播训练出的直觉,让你能在看到hidden_states.norm()=120.5时,立刻联想到 “GELU 输入可能溢出”,而不是盲目调 learning rate。

5.3 高级技巧:用torch.autograd.functional.jacobian检查非标量输出

当你的模型输出不是标量(如强化学习的 policy logits),无法直接backward(),需要用jacobian

def policy_forward(state): # state: [4] -> logits: [2] (left/right action) return model(state) state = torch.tensor([1.0, 0.5, -0.2, 0.8], requires_grad=True) logits = policy_forward(state) # Compute Jacobian: ∂logits/∂state, shape [2, 4] jacobian = torch.autograd.functional.jacobian(policy_forward, state) print("Jacobian shape:", jacobian.shape) # [2,4] print("Jacobian:\n", jacobian)

jacobian返回一个二维张量,每一行

http://www.rkmt.cn/news/1509305.html

相关文章:

  • 温州卫生间漏水不用砸砖?微创补漏靠谱方案 - 苏易修缮
  • reductstore 高性能面向机器人以及IOT场景的存储以及流数据基石
  • 数据库连接报错问题
  • 2026免费证件照制作工具合集,手把手教你自制标准证件照 - 办公小帮手
  • 心衰越治越重、频繁复发?精准诊疗给患者新生希望
  • 景区数字化AR公司有哪些在做深度落地?从试点项目到规模化运营的能力差异对比 - 品牌排行榜
  • Day11|精神焦虑人群专属:AI情绪树洞,如何悄悄抚平日常无名烦躁与焦虑?
  • 国产贴片机和进口机的差距,根源在哪?
  • AIStarter 即将重大升级!PanelAI 9月正式版上线,一键部署本地AI应用闭环生态详解
  • 别被200年数据保存忽悠了!聊聊EEPROM寿命测试里的‘高温催熟’与‘擦写计数’那些坑
  • 进口滚珠丝杠代理哪家值得合作?一级授权、现货库存与技术服务能力是关键门槛 - 品牌排行榜
  • 2026 东莞卫生间漏水不用砸砖?微创补漏靠谱方案 - 苏易修缮
  • 【Springboot毕设全套源码+文档】springboot人脸识别系统研究及其在社区门禁系统中的应用(丰富项目+远程调试+讲解+定制)
  • 大数据平台项目投标技术方案参考文档(Word300页)
  • Strands Agents A2A 协议实战:让多个 AI Agent 互相对话
  • 从Console.WriteLine到你的代码:深入理解C# params关键字的‘前世今生’与设计哲学
  • FLV 如何转换成MP3,一招搞定
  • 1039市场采购和买单出口有什么区别?哪个更合规?| 性质与合规全面对比 - 欢欢在创业
  • Claude Code 主创放弃写 Prompt 了:他改写循环。Prompt Engineer 这个岗位还活得下去吗?
  • 别让栅极电阻毁了你的MOS管!手把手教你选对Rg值(附计算实例)
  • 【毕业设计】基于 SpringBoot 与 Android 的个人健康管理系统设计与实现基于springboot+Android的健康管理应用的设计与实现(源码+文档+远程调试,全bao定制等)
  • 【海斗小助手】0.9.1 版本更新公告:同步官方 26.12 最新版本变动
  • 【Springboot毕设全套源码+文档】基于spring boot的图书交易平台设计与实现(丰富项目+远程调试+讲解+定制)
  • 为什么Sunshine能帮你实现零延迟游戏串流:3个实战秘诀
  • WPF 自定义容器控件的布局
  • 给嵌入式工程师的CSI-2协议实战拆解:从PHY层到Packet,手把手分析图像数据流
  • 百度网盘直链解析终极指南:告别龟速下载,重获下载自由
  • Vivado资源报告怎么看?从Utilization报告里揪出LUTRAM浪费和DSP使用不足的‘元凶’
  • 太原市黄金回收白银回收铂金回收彩金回收靠谱门店TOP排行榜及联系方式地址电话+诚信店铺推荐 - 大熊猫898989
  • 铜川市黄金回收白银回收铂金回收彩金回收靠谱门店TOP排行榜及联系方式地址电话+诚信店铺推荐 - 大熊猫898989