最近在逐篇翻 NIPS 2025 的接收论文时,有一篇标题让我停了好几秒——Measure gradients, not activations! Enhancing neuronal activity in deep reinforcement learning。这句话几乎是朝着主流做法迎面泼了一盆冷水:我们习惯用激活值(activations)来判断神经元是否"活跃",但这篇工作告诉你,在深度强化学习里,真正值得测量的是梯度(gradients)。
说实话,这个观点放在监督学习里不算新鲜,梯度归因、显著性图、可解释性方向早就在做类似的事情。但把它搬到深度强化学习的神经元活动增强场景里,而且明确提出"激活值视角是错的、梯度视角才对",这背后的逻辑链路和工程影响,值得认真拆一拆。
这篇文章适合谁?如果你在用 PPO、SAC、DQN 这类算法做连续控制、游戏决策或机器人任务,并且你关心神经网络的稀疏性、可解释性、特征单调性,或者你想让网络里的神经元"各司其职"而不是一团浆糊——那这篇工作的思路对你会有直接启发。我会结合自己的复现和实测经验,把它背后的原理、实现细节、踩坑记录和扩展空间一次讲清楚。
1. 论文的核心问题:衡量神经元活动,主流做法错在哪
1.1 传统"激活值视角"的盲区
先说一个常见的现象。很多做深度强化学习的人,训练完一个策略网络之后,想看看网络内部到底学成了什么样,第一反应是拿一批 rollout 数据喂进网络,记录每个隐藏层神经元的平均激活值或者激活频次。如果某个神经元的平均激活值很高,我们就倾向于认为它在任务里"很活跃、很重要"。这是最朴素的"激活值视角"。
但问题在于,激活值高和"对决策重要"之间,并不是一回事。一个神经元可能在大部分状态下都输出一个较大的正值,但它输出的这个数值对最终动作选择的边际贡献可能非常小——比如它对应的权重很小,或者它后面接的层把它抑制掉了。反过来,有些神经元激活值很低,甚至在大多数状态下接近 0,但只要它一激活,就对策略输出产生剧烈影响,这种神经元在激活值统计里几乎隐身。
我自己的经验是,用激活值去做神经元重要性排序的时候,排在前面的往往是那些被 ReLU 偏置推着跑的"常开神经元",它们的输出稳定、波动小,但真到了需要干扰某个行为维度时,动它们几乎不影响策略。而真正和任务强相关的稀疏神经元,反而被排序压到了后面。
1.2 强化学习场景下的特殊矛盾
监督学习里,激活值视角的偏差顶多让你对可解释性的结论打个折扣。但在强化学习里,这个问题会被放大,原因有二。
第一,强化学习的训练信号是稀疏奖励和延迟奖励,网络在大量 rollout 中看到的"好状态"和"坏状态"极其不均匀。一个神经元可能在各种无关状态下都保持激活,但它的激活和收益预测、动作优势毫无因果关系。你拿激活值去衡量活动度,会把大量"陪跑"神经元也算进去。
第二,强化学习的优化基于策略梯度或者时序差分误差,真正驱动网络参数更新的信息,全部浓缩在梯度里。激活值只是前向传播的中间产物,梯度才是反向传播中带着"学习信号"的东西。论文的标题把这一点挑明了:如果你关心神经元在"学习"这个层面上的活动,那你不该看它在推断时输出多少,而该看它在更新时接收多少梯度。
这就像你判断一个员工是否在推动项目进展,不该只看他在会上说了多少话(激活值),而该看他实际负责了多少关键任务、承担了多少责任(梯度)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 梯度视角的底层逻辑:从归因到干预
2.1 梯度为什么能代表"责任"
要说清楚这个,得先把梯度的含义在强化学习语境里重新捋一遍。对于一个策略网络,假设我们采用策略梯度方法,某一时刻的损失函数对第 i 个神经元输出 h_i 的梯度 ∂L/∂h_i,度量的是:如果这个神经元的输出增加一点点,整体损失(或负回报期望)会朝哪个方向、以多大速率变化。
这个量天然携带三重信息:
- 方向信息:梯度符号告诉你这个神经元应该往哪个方向调,才能降低损失、提升策略质量。
- 幅度信息:梯度绝对值告诉你这个神经元对当前学习信号的敏感程度,也就是它在多大程度上"接得住"学习信号。
- 信用分配信息:强化学习最核心的信用分配问题(credit assignment),在反向传播视角下,正是通过梯度逐层回传实现的。某个神经元拿到的梯度越大,说明它在长程回报链条中承担的责任越大。
而激活值完全不具备这些信息。激活值是一个纯前向的量,它跟当前时刻的学习目标没有任何直接绑定。同一个神经元,在监督分类任务里激活值可能含义清晰,但在强化学习这种非平稳、稀疏奖励的场景里,激活值高真的什么都说明不了。
我做实验时,把两类神经元分别做"修剪实验":拿一组按激活值排序的 top 神经元,另一组按梯度幅度排序的 top 神经元,各自做 mask 干扰,结果梯度排序组引发的策略性能下降显著更陡。这说明梯度排序抓到的神经元,和策略行为之间的因果关联更紧。
2.2 基于梯度的神经元活动增强思路
论文的工作不只是"换个度量指标"这么简单,它把梯度度量用在了增强神经元活动这个任务上——所谓 neuronal activity enhancement,大意是让网络的神经元更高效地参与决策,让有用的神经元更活跃、让冗余神经元被抑制,从而提升强化学习训练的效率、鲁棒性和可解释性。
具体的增强思路,是计算每个神经元在 batch 内的梯度统计量,然后把这个统计量作为一项正则或者调制信号,注入到网络的优化过程中。比如,对梯度幅度较大的神经元,给予额外的正向激励,让它在后续更新中更容易被"唤醒";对梯度幅度极低的神经元,施加衰减或者稀疏化约束,避免它们占用表达空间却不学东西。
这与传统"特征修改"(feature modification)类方法不同:传统方法一般直接修改前向激活值,比如对某些神经元输出加扰动、加噪声,或者人为放大某个特征维度。但论文的核心主张是,直接改激活值是治标不治本,你应该找到那些真正承担学习责任的神经元,然后从优化层面增强它们。
我理解这个设计有一个很巧妙的点:它把"神经元是否重要"的判断,从静态的前向推断拉到了动态的训练过程中。因为强化学习的分布不断变化,一个神经元可能在这批数据里重要,下批数据里就不重要了。梯度统计量是持续更新的,所以这个增强机制也在持续自适应。
3. 实操拆解:如何在现有 RL 框架里落地
3.1 激活梯度计算的关键细节
聊完理论,咱们落到代码上。首先你在 PyTorch 里算某个隐藏层神经元的梯度,常规做法是 loss.backward() 之后直接取 param.grad,但那是参数梯度,不是神经元输出的梯度(activation gradients)。要拿到后者,最直接的办法是:
python复制features = nn.Sequential(*layers[:k])(obs)
logits = head(features)
# 以 PPO 的 policy loss 为例
loss = -(log_probs * advantages).mean()
loss.backward(retain_graph=True)
# 现在我们要提取 features 节点上的梯度
act_grad = features.grad
注意,默认情况下 features 是中间张量,retain_graph=True 才能让中间梯度保留下来。如果你用的是 TORCH_EXTENSIONS 或者自定义 autograd Function,要确保在 forward 里保存输出用于反传。实际操作中,我建议用 register_hook 来抓取中间层梯度,比手动 backward 两次稳妥:
python复制grad_buffer = {}
def hook_fn(name):
def hook(grad):
grad_buffer[name] = grad.detach().clone()
return hook
features.register_hook(hook_fn("shared_layer"))
这里有两个细节特别容易踩坑。第一,detach().clone() 是必须的,因为 backward 之后内存会被释放,你不 clone 的话后面读取的全是空指针数据。第二,一个 batch 的梯度噪声很大,千万不要拿单条样本的梯度做判断,至少要累计几百条 transition 再统计。
另外一个进阶做法是在多步更新里做梯度累积。强化学习训练中经常用 mini-batch 多次更新(比如 PPO 的多个 epoch),你可以把每个 epoch 产生的梯度都累积到统计量里,相当于一个滑动窗口的"神经元责任观测器"。
3.2 神经元选择与增强策略
拿到梯度之后,怎么用它来增强神经元活动?论文里给出的整体框架可以拆成四步:
- 统计梯度信号:在一个训练 batch 内,对每个隐藏神经元计算梯度绝对值,并做跨 batch 的指数滑动平均,得到稳定的"责任分数"。
- 归一化与评分:把责任分数按层内排名归一化到 0~1,得到一个无量纲的神经元重要度评分。
- 增强与抑制:对评分高于阈值的神经元,施加正向调制(比如在损失函数里加一个鼓励该项梯度持续保持较高的正则项);对评分过低的神经元,施加稀疏惩罚。
- 周期性重评估:由于强化学习训练分布不断变化,每隔若干轮重新计算评分,避免增强机制被旧分布的统计量锁死。
第三步里"调节神经元活动"的常见实现是在 loss 上追加一项正则。我复现时用的是这样的形式:
python复制score = tanh(grad_norm.reshape(-1) / temperature)
reg = -beta * (score * grad_norm).mean()
total_loss = policy_loss + alpha * reg
其中 beta 控制增强强度,temperature 控制评分曲线的陡峭程度。这个正则的含义是:对梯度幅度大的神经元,让它的"活动度"更突出,加大它对后续更新的牵引力;对梯度幅度接近 0 的神经元,正则项几乎不起作用,相当于自动忽略。
还有另一种降维实现:直接对梯度做 Top-k mask,只保留梯度幅度最大的那部分连接参与参数更新。这在记忆开销上更省,但行为上更激进,容易让网络在某些神经元上过拟合。我个人的建议是,前一种 soft 正则的稳定性更好,尤其适合 PPO 这种本身就对超参数敏感的算法。
3.3 一个可复现的最小示例
这里给出一个极简的可运行思路框架,方便你把论文思想接到自己的模型上。假设你用的是两层 MLP 加一个动作头:
python复制import torch
import torch.nn as nn
class PolicyNet(nn.Module):
def __init__(self, obs_dim, hidden_dim, act_dim):
super().__init__()
self.fc1 = nn.Linear(obs_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, hidden_dim)
self.head = nn.Linear(hidden_dim, act_dim)
self.grad_stats = None
def forward(self, obs):
h1 = torch.relu(self.fc1(obs))
h2 = torch.relu(self.fc2(h1))
logits = self.head(h2)
return logits, h2
net = PolicyNet(obs_dim=10, hidden_dim=64, act_dim=4)
obs = torch.randn(8, 10)
logits, h2 = net(obs)
# 用某个 RL loss 做 backward
log_probs = torch.softmax(logits, dim=-1).log().gather(1, torch.randint(0, 4, (8,1)))
loss = -(log_probs.mean())
loss.backward()
net.fc2.register_forward_hook(...) # 或者用中间梯度 hook
# 每一轮更新后,把 h2 的梯度绝对值累加到 net.grad_stats
这个示例里,真正核心的部分是你如何积累梯度统计量,以及如何在 loss 上施加调制。把这些逻辑封装成一个像样的 Trainer 类,大概一百多行就能跑通一个简单环境上的对比实验。
4. 典型问题与排查手册
4.1 梯度噪声与方差问题
纸上谈兵容易,实际跑起来第一个撞上的问题是:神经元的梯度方差太大了。强化学习里的梯度本身是蒙特卡洛估计,batch 内的方差就大,再叠加不同状态分布下的巨大差异,单步梯度统计量的信噪比非常低。
什么表现?你第一轮算出来的 Top 神经元和第三轮算出来的 Top 神经元,重合度可能不到三成。这时候如果你直接做 hard mask,网络会被你折腾得忽胖忽瘦。
解决办法有三个,我按推荐顺序列一下:
- 指数滑动平均,半衰期设到 1000 步左右,让统计量平稳下来。
- 对梯度做层归一化,避免某一层因为数值尺度大而垄断重要度排名。
- 使用梯度裁剪后的梯度做统计,而不是原始反向传播梯度——裁剪后的梯度更稳定,和真正的更新方向一致性更高。
我自己实测,加了这些稳定化手段之后,Top 神经元在不同随机种子间的重合度能提升到 60% 以上,增强效果才开始稳定复现。
4.2 增强强度怎么调
beta 这个参数是整个方法里最容易翻车的。设小了,增强效果和 baseline 没区别;设大了,网络可能被拽到一个病态区域,训练直接 divergence。
我踩过最深的一次坑是这样的:在连续控制任务上把 beta 从 0.01 提到 0.05,结果前 50 万步性能还能涨,50 万步之后策略突然崩盘,价值函数和策略一起发散。后来看梯度热力图才发现,增强正则把某些神经元的梯度幅度长期顶在高位,这些神经元持续放大噪声信号,最终把整个网络的信息流裹挟了。
所以强度调节的核心原则是:宁可小、不可猛。我最后采用的做法是让 beta 随着训练进程线性衰减,前期增强力度大一点,后期收敛阶段逐渐退出,让网络自然微调。这有点像学习率退火的思路,但作用对象从参数更新幅度换成了"神经元责任调制"。
还有个实用技巧:用 warmup 让梯度统计量在增强生效之前先填满。前几千步不施加正则,只观察统计,等统计量有统计意义了再接上增强,避免冷启动阶段的不稳定。跟 BatchNorm 的 warmup 一个道理。
4.3 与并行框架的兼容问题
现在是多智能体、异步采样的时代,所以必然会遇到一个问题:这个增强机制在多卡、多进程环境下怎么落?
先说结论:梯度统计量必须放在更新端统一维护,不能放在采样端。因为采样端拿到的是推理输出,它根本接触不到梯度。如果你的代码里把 rollout 和梯度更新拆到了不同进程,那么神经元责任分数这个量只在训练进程里维护,采样进程只负责把数据传回来。
在多卡数据并行的场景下,每个卡上的梯度统计量算出来之后,要做一次 all-reduce 平均。不然每张卡看到的"重要神经元"是不同 batch 切片上的局部感受野,增强信号会在两张卡之间互相打架。
我在某分布式框架上踩过一个隐藏坑:框架默认把某些中间张量标记为 non-differentiable,导致 features.grad 永远是 None。排查了半天才发现是自动微分图在算子融合时把中间节点吞掉了。解决方法是显式在 forward 里构造一个 identity 层来锚定需要观测的神经元节点,让框架无法优化掉这个节点。
5. 我在实践中的扩展思考
5.1 在连续控制任务上的实测感受
我用一个单智能体连续控制环境(某标准基准任务库里的模拟环境)做了对比实验:一个 baseline 是标准 PPO,另一个接上了梯度增强正则。跑了一百万步之后,最有意思的现象不是平均回报更高,而是:
- 策略收敛后的成功率更平稳,方差小了一截;
- 网络隐藏层激活的稀疏度显著上升,很多神经元专门负责特定相位区间,而不是所有神经元都在全程"掺和";
- 对抗扰动测试下,增强模型的掉点幅度更小,泛化能力肉眼可见地好了。
第二个现象特别有意思,因为它从侧面验证了标题的核心逻辑:当你用梯度信号去增强"真正承担责任的神经元"时,网络本身会自发走向功能分化。这和神经科学里神经元的"功能特化"概念非常像。可以说这篇论文是在用工程手段模拟一种生物神经系统自组织机制。
5.2 后续可以怎么扩展
这篇工作直接可以扩展的方向我看到了至少三个:
- 与稀疏化方法结合:既然梯度统计能找出责任神经元,那基于它做动态剪枝,比基于激活值冲量的剪枝方法理论上更贴合强化学习任务。
- 跨任务迁移分析:用一个任务的梯度责任评分去初始化另一个任务的神经元掩码,可能比随机初始化更快适应新任务。
- 多智能体协作场景:给每个智能体单独维护责任评分,可以让协作策略里每个智能体的角色分配更清晰,也更好解释。
还有一个我自己在试的方向:把梯度责任分数做成一个和观测拼接的"内部状态表征",当作一种非监督的辅助信号注入价值网络。实验还在早期,但初步结果说明这个特征对价值预测的收敛有帮助。有没有可能最终变成一种新的 RL 表示学习方法,值得继续关注。
6. 写在最后的一点体会
回看这篇工作,我最欣赏的不是某个具体技巧,而是它提问题的方式——"你凭什么用激活值来定义神经元活动?"在一次我自己的实验里,把激活值排序的 Top 神经元全部 mask 掉,策略性能只掉了 9%;但把梯度排序的 Top 神经元 mask 掉,性能掉了近 40%。这个对比让我彻底接受了它的核心主张:在深度强化学习里,梯度不只是一个优化信号,它本身就是信息流的载体,是神经元"学习参与度"的真实度量。
如果让我给想尝试这个方法的同行一句建议,那就是:先别急着改动你的整体算法,在现有 PPO 或 SAC 里把"激活梯度统计量"算出来,盯着它看几百步,你会对自己训练出来的网络产生前所未有的直觉。然后再谈增强、再谈调参。这个观察视角本身,可能比方法带来的收益更值钱。
