SERL:让真机强化学习从“难用”走向“可复现”的强化学习框架 ---- 算法篇(SAC)
统的 RL 让智能体像个做题家只盯着分数奖励 R看。SAC 则引入了熵Entropy的概念。简单来说熵代表了智能体动作的随机性或多样性。SAC 的目标不只是最大化奖励而是Maximize E [ 奖励 α × 熵 ]奖励告诉智能体什么是对的。熵告诉智能体不要死脑筋多试试不同的动作。αTemperature决定了智能体有多爱折腾。1.2 熵解决的问题SAC 引入熵Entropy解决的是强化学习里最核心的矛盾探索Exploration与利用Exploitation的矛盾。传统算法的困境智能体一旦发现往左走拿高分就会迅速收缩策略只往左走过度利用这会导致环境稍微改变就彻底抓瞎。SAC 的方案通过增加熵项它告诉智能体在保证能拿到高分的同时你的动作要尽可能随机。核心矛盾的解决SAC 让智能体不仅学习最优动作还学习了所有可能成功的动作。这带来了两个好处极强的抗干扰能力即便路被堵了一半它也知道其他走法。极快的训练速度因为它在探索时更广不容易掉进死胡同。直观理解如果智能体发现有两条路都能到达终点传统 RL 可能会死磕其中一条而 SAC 尽量保持一种两条路都能走的状态这让它在环境发生变化时更具鲁棒性。0x02 演化脉络我们可以把强化学习看作是教一个小孩智能体在不同的房间状态里做不同的动作最终为了拿到最多的糖果奖励。接下来我们看看算法如何演进。Q-Learning: 查表求最大, 适合离散动作。Actor-Critic: 向导 地图, 适合连续动作。SAC: 带灵魂的向导, 追求奖励与随机性的平衡。2.1 Q-Learning价值的推演2.1.1 核心思想核心思想我不教小孩怎么走我只告诉他每个房间里每个动作值多少分。Q 值 Q(s,a)在状态 s 采取动作 a 后直到游戏结束你预期能拿到的总分。怎么学小孩在房间里试。他看到我在房间 A 往右拿走了 1 分到了房间 B。他想房间 A 往右的价值 现在的 1 分 房间 B 里最值钱的那个动作的分。公式贝尔曼方程Q(s,a)Rγmaxa′Q(s′,a′)。我现在的身价 我现在挣的钱 我到了新环境后最值钱的那个可能性的折现值。特点它是 Value-based基于价值。小孩做决策很简单看到哪个动作 Q 值大就选哪个。Q 值不是当前的分数也不是结束时的总分而是从现在开始到未来的累积预期。当前分数Reward你这一步踩下去拿到的即时反馈 R。Q 值 Q(s,a)现在的奖励 未来的奖励打个折 γ。类比你现在决定去大厂加班动作 a当前奖励 R 是高工资但 Q 值还要考虑这之后带给你的职业晋升空间和未来的总收入。2.1.2 Q-Learning 的问题在 Q-Learning 中我们需要找到让 Q 最大的动作 。Q-learning 的困难如果动作是一个连续的数字比如 0.1234… 到 1.0 之间的任何数这代表这机器人下一步可能有无数个动作。你没办法一个一个带入 Q 网络去算一遍谁的分最高。这叫搜索难题。具体而言使用 Q 网络当你真的要决定该做什么动作时问题来了。Q 网络就像一个黑盒函数 。在离散空间比如左、右、上、下你把 4 个动作分别塞进去看谁高分。这很简单。在连续空间比如转向 15.342 度你需要找到一个动作使得分数最大。难点虽然你可以求导但函数图像可能像崇山峻岭一样复杂有很多局部最大值。如果你只靠梯度微调去在大海捞针一样寻找那个 每一轮预测都要做一次耗时的优化过程这在实时控制里太慢了2.2 Actor-CriticAC分工协作2.2.1 核心思路核心思想与其让小孩自己记 Q 值不如给小孩配一个教练。Actor演员/小孩负责做动作。它不看分数它只学一套秘籍策略 π在房间 A 往左走的概率是 80%往右是 20%。Critic评论家/教练负责打分。它不亲自下场它只学 Q 值评价小孩做得好不好。怎么学Actor 做个动作。Critic 看一眼结果说这个动作比我预期的好或者差。Actor 根据教练的反馈调整自己的秘籍好的动作以后多做差的少做。特点结合了 Policy-based基于策略和 Value-based 的优点。它能处理连续动作比如角度控制这是纯 Q-Learning 很难做到的。2.2.2 输出分布在机器人控制中动作通常是连续的比如电机的电压、关节的角度。Actor 输出一个概率分布如正态分布而不是单一数值。输出分布对机器人的意义很大想象你要控制机械臂抓一个杯子左边抓可以右边抓也可以。如果输出一个确定数值机械臂必须在左右之间死选一个。如果传感器有一点噪声它可能就在左右之间疯狂抖动这就是不稳定的来源。如果输出一个分布机械臂就知道这两个动作都不错。在实际操作中这种模糊性反而能让动作更平滑因为它允许系统在遇到微小阻力时有自然的调节空间。2.2.3 Actor-Critic 如何解决Actor 为什么可以解决 Q-Learning 的问题Actor 的思路是我不用临时去找最佳动作我直接养一个专门输出最佳动作的函数。Actor 的参数 θ 决定了在状态 s 下我倾向于输出哪个动作 a。链式法则核心秘籍如下我们要最大化 Q(s,Actor(s))更新 Actor 的参数。∂Q∂(Actor参数)(∂Q∂(动作 a))Critic告诉Actor动作往哪改×(∂Actor∂(Actor参数))Actor自己调整内部参数Actor 的巧妙之处Actor 也是一个神经网络。我们不搜索动作我们直接优化参数。我们问 Critic按照 Actor 现在输出的动作分高吗Critic 说不高往左偏一点更高。Actor 就通过梯度下降计算导数把自己的参数往左边挪一点。结论Actor 不需要遍历动作空间。它直接通过 Critic 给出的梯度信号把自己的整个输出拉向高分区域。2.2.4 形象的类比想象你在漆黑的深夜要在山上找最高点Q-Learning你买了一份地图Q网络。你要在地图上找最高点。如果地图很大很细连续空间你得拿着放大镜一点点找半天才能找到坐标。Actor-Critic你不仅有地图Critic你还训练了一个向导Actor。每当你站在一个地方地图告诉你往北走海拔升高最快。向导立刻记住了下次遇到这种情况直接往北走。下次你再来你不需要看地图直接问向导他一秒钟就能指出方向。2.2.5 结论Q-Learning目标是练出一本完美的地图。Actor-Critic目标是练出一个完美的向导。而为了练好向导我们不得不先画一份还凑合的地图来指导他。Q-learning 的 Q 网络它像一张估值表。它不直接告诉你怎么走你要自己查表找最高的。Actor它相当于直觉/本能。它不是表它是一个函数。输入状态 s它直接喷出动作 a。在 Actor-Critic 中Actor 取代了 Q-learning 中查表求最大值的那个过程。2.3 SACSERL 框架的底层动力引擎是 SAC。之所以选择 SAC是因为它是处理连续动作空间机器人关节或末端位移最稳定、性能最强的算法之一。2.3.1 核心思想做一个爱探索的聪明人传统的强化学习算法只追求分数最高。但 SAC 多了一个追求最大化熵Entropy Maximization。它的公式可以表示为目标 奖励 (Reward) α × 熵 (Entropy)。直白地说SAC 不仅想拿高分它还希望自己的动作尽可能地多样化、不呆板。这对于真实机器人非常重要如果策略过早变得确定一旦陷入错误的动作模式就很难恢复而熵正则让机器人保留了探索能力。好处有两点强力探索它能尝试出各种不同的方法来完成任务极强鲁棒性如果环境发生微小变化因为它学过很多种姿势能快速适应不容易在死胡同里卡死。具体算法如下2.3.2 SAC 的三驾马车架构SERL 里的 SAC 实际上训练了三种网络Actor策略网络 π负责出动作。它输出的是一个概率分布比如均值和方差这意味着机器人每次做动作都会带有一点点随机性。Critic两个 Q 网络 Q₁、Q₂负责打分。为了解决过估计问题SAC 永远训练两个 Q 函数并取其中的最小值。Actor 网络任务根据当前状态 s决定该做什么动作 a。实现它输出的不是一个固定动作而是一个分布比如高斯分布的均值 μ 和标准差 σ。SAC 要求 Actor 的动作既要让 Q 值大又要保持随机熵大这就像是要求一个短跑运动员你要跑得尽量快Q值但跑姿还要尽量花哨多变熵。Critic 网络Critic就是在做一个带熵修正的Q-learning。任务估算在状态 s 采取动作 a 之后未来能拿多少分奖励熵。实现通常用神经网络 Q(s,a) 表示。为了稳健SAC 通常用两个 Q 网络每次取最小值防止高估。SAC 的 Critic 在算未来的价值时不只看 maxQ还要加上一句而且我希望未来的动作选择越丰富越好。普通 Q-learningQRγmaxQ′SAC 的 CriticQRγE[未来能拿的分未来动作的熵]Temperatureα网络这是一个自动调节的参数控制熵的权重。如果探索得不够α 会变大逼着智能体去随机尝试。或者说Temperature用来控制机器人什么时候该浪一点多探索什么时候该稳一点多拿分。2.3.3 SAC vs. 普通 Actor-CriticAC公式对比普通 AC 的目标函数J(π)∑tE[rt] 只看奖励SAC 的目标函数J(π)∑tE[rtαH(π(⋅|st))] 奖励 α × 熵。其中 H 就是熵。如果 α0SAC 就变成了普通的连续空间 AC 算法。通俗对比普通 AC智能体像个死记硬背的学生。如果它发现往左能拿 10 分往右拿 9 分它会永远、固执地只往左走。即便左边的路偶尔塌陷它也不管。SAC智能体像个富有探索精神的探险家。它发现往左拿 10 分往右拿 9 分它会想虽然左边高分但右边也挺有意思的我也得经常去转转。它追求的是条条大路通罗马而不是死磕一条路。2.4. 三者的联系与进化我们可以把这三者的演进看作是Q-Learning只有大脑记录分数的表没有身体。在连续空间动作有无穷多种可能里它没法找最大值 maxQ。Actor-Critic给大脑配了身体。大脑Critic评估价值身体Actor直接输出动作。不用再费劲去求 max 了Actor 直接告诉你该做什么。SACSoft Actor-Critic给这个组合加了灵魂熵。不仅要拿分还要动作多样化不要死板。用一个类比总结Q-Learning就像你在玩扫雷。你学习每一格如果点开大概率有多少分数。你最后选分数最高的那格点。Actor-Critic就像导演和演员。演员Actor练习表演导演Critic在旁边说这一段演得好多保持那一段太浮夸少来。演员不看剧本的分数只听导演的。SAC导演Critic跟演员Actor说你不仅要演得好还得有自己的风格熵别老是演得跟模板一模一样多尝试点即兴发挥。0x03 网络结构本节我们来看看SAC 的特色细节。3.1 SAC 到底有几个网络在一个标准的 SAC 实现中通常有以下几个神经网络Actor 网络1 个输出动作的分布μ,σ。Critic 网络2 个即 Q1 和 Q2。为什么要两个为了解决过度估计问题。如果只有一个 Q它会像个爱吹牛的人把得分估得太高。两个 Q 取最小值就能压住这种吹牛。Target Critic 网络2 个即 Qtarget1 和 Qtarget2。它们是 Q1Q2 的影子更新得非常慢平滑更新。这是为了让训练目标更稳定。Temperature (自动熵调节) 网络1个自动调节策略熵的目标值确保温度参数 ≥ 目标熵。3.2 目标函数 J(π) 与损失函数在 RL 中我们确实希望最大化 J(π)即累积奖励。但神经网络优化工具如 PyTorch/TensorFlow通常只能最小化一个损失函数Loss。因此会设置 Loss −J(π)。3.2.1 普通 AC 的损失函数Critic最小化均方误差 MSE(Q(s,a), Target)。Actor通常使用策略梯度Policy Gradient让能够获得高 Q 值的动作出现的概率变大。3.2.2 SAC 的损失函数Critic LossMSE(Q(s,a),rγ(minQtarget(s,a)−αlogπ(a∣s)))Actor LossEa∼π[αlogπ(a∣s)−minQ(s,a)]Alpha Loss−α⋅E[logπ(a∣s)Htarget]3.3 Critic评论家Critic 的目标是预测未来的总收益。在 SAC 的实现中通常会维护两个 Q 网络Clipped Double-Q。SAC 的 CriticQRγE[未来能拿的分未来动作的熵]Q(s,a)←rγEa′∼π(⋅|s′)[Q(s′,a′)−αlogπ(a′|s′)]3.3.1 Critic 公式里的「Soft Value软价值」[Q(s′,a′)−αlogπ(a′|s′)] 这个括号里的东西我们称之为 Soft Value软价值。Q(s′,a′)下一步能拿到的奖励预期。−αlogπ(a′|s′)下一步动作的随机性熵奖励。−logP 在信息论里就是「惊奇度」期望的惊奇度就是熵。含义Critic 现在不只是在预测钱奖励它还在预测 钱 自由度熵。它告诉智能体「去那个奖励又高、选择又丰富的地方」。3.3.2 Critic 到底该不该考虑熵既然提到了熵我们就看看Critic 为何要考虑熵。如果 Critic 只预测奖励而 Actor 却在追求奖励熵。这会导致什么结果Critic 会对 Actor 说你刚才那个动作太随机了虽然奖励高但我不看好你。Actor 会说可我的目标就是要随机啊这会导致驴唇不对马嘴两者无法协作。所以SAC 的 Critic 更新公式是 Q(s,a)←rγEa′∼π(⋅|s′)[Q(s′,a′)−αlogπ(a′|s′)]。这里的 −αlogπ(a′|s′) 其实就是熵的体现。如果智能体在下一步动作 a′ 的概率非常高非常确定−logπ 会变得很小如果动作很随机−logπ 会变大。这意味着SAC 的 Critic 实际上是在评估这步动作不仅现在好而且能保证以后有更多的选择余地。3.3.3 训练 Critic 时要不要更新 Actor实际上在代码实现中训练 Critic 时Actor 是禁止动弹的。原因Critic 的目标是「预测准确」。它要预测的是当前 Actor 表现如何。如果 Critic 一边在学预测Actor 一边在变Critic 就会像在追一个移动的靶子永远练不准。做法我们计算 Target 时会用到 Actor 输出的概率 π但我们只传导梯度给 Critic 的参数。这叫解耦。在每一轮训练中我们其实是分两步走的第一步练 CriticActor 站着不动我们要让 Critic 学会评价当前这个 Actor 的水平。计算 Loss 时我们会用到 Actor 的输出 π但我们设置actor.requires_grad False或者只是不把 Actor 的参数放进优化器。结果只有 Critic 的权重变了Actor 没变。第二步练 ActorCritic 坐着当评委现在 Critic 已经练好了它能准确判断动作的好坏了。我们让 Actor 跑一遍计算 Lossαlogπ−Q。此时计算梯度并且只更新 Actor 的参数。结果Actor 变聪明了它学会了如何让评委Critic给自己打高分。总结在整个大循环里Actor 当然要更新但在训练 Critic那个具体的子步骤里Actor 是不动的。3.4 Actor演员Actor Loss 如下$$\text{Loss}{{\text{Actor}}} \mathbb{E}\left[ \alpha \log \pi(a|s) - Q(s, a) \right]$$这个 Loss 的两部分为−Q(s,a)最小化这个就是在最大化 Q优化奖励。αlogπ(a|s)最小化这个就是在让 π(a|s) 变小因为 log 是增函数。π 越小分布就越平、越随机即最大化熵。这就好比一个教练Loss同时盯着运动员的「速度」和「花哨程度」。如果速度慢了教练扣分如果动作单一了教练也扣分。当调用loss.backward()时梯度会穿过 Q 网络但 Q 的参数被冻结不更新一直回传到 Actor 网络输出 μ 和 σ 的那一层。在这个 Loss 里Q(s,a) 的值决定了梯度的大小和方向但我们只用它来告诉 Actor往这边调整你的 μ 和 σ能让 Q 变得更大。3.4.1 Actor Loss 的直观平衡LossaE[−a变量⋅(logπ(a|s)¯H误差)]这是一个标量损失函数Scalar Loss。¯H 是你的目标。比如你希望动作保持一定的随机性。当 Loss 减小时αlogπ(a|s) 减小 → logπ 趋向更负的值 → π 变小 → 分布变宽、越随机熵优化。博弈平衡如果 π 缩得太小太随机Q 值可能会下降如果 π 太集中熵损失会变大。α 这个权重决定了最终平衡点在哪里。场景 A: 太确定了。log π 很大 (接近 0), 导致 (log π \bar{H}) 变成正数。为了让 Loss 减小, a 必须增大。后果: a 变大后, 在 Actor 的 Loss 中, 熵的权重增加了。Actor 会被教导: 别管奖励了, 先给我变随机点! 场景 B: 太乱了。a 会减小, 让 Actor 专心去拿奖励。3.4.2 特色目标函数Maximize E [ 奖励 α × 熵 ]。如果 α0智能体会陷入死磕一条路的死胡同。熵确保了智能体在追求高分的同时保持条条大路通罗马的鲁棒性。为什么 σ→0熵就没了直观理解σ 代表不确定性。如果 σ0意味着智能体 100% 确定只做一个动作。既然完全确定就没有随机性不确定性熵自然就是 0甚至在连续空间定义下趋向负无穷。数学公式高斯分布的微分熵公式是 (1/2)ln(2πeσ2)。当 σ→0 时这个值趋向 −∞。为什么 log π 越大, 熵就越小? 我们要先搞清楚概率 π 的范围: 它在 [0, 1] 之间。动作非常确定比如智能体 99% 的概率选动作 A。此时 π(A|s) ≈ 1, 那么 log π ≈ log 1 0。动作非常随机比如有 100 个动作, 智能体每个都选, 概率 π ≈ 0.01。此时 log π ≈ log 0.01 -4.6。结论: 在负数世界里, 0 是最大的。所以 log π 越接近 0 (越大), 说明概率越集中, 熵 (即 -log π 的平均值) 就越小。3.5 小结Actor和Critic使用高度相似但不完全相同的网络架构。主要区别在于Critic需要额外输入动作信息这符合Actor-Critic算法的理论设计。相似点都使用相同的编码器视觉编码器可共享都使用MLP主干网络hidden_dims配置相同默认[256, 256]都支持多设备并行通过ensemble机制关键差异特性ActorCritic输入仅观测observations观测 动作[obs_enc, actions]输出动作分布参数mean, std标量Q值网络结构独立输出均值和标准差单一输出层激活函数最后一层通常激活通常线性输出0x04 实现SERL 的网络设计选择如下4.1 异同Actor和Critic使用高度相似但不完全相同的网络架构。主要区别在于Critic需要额外输入动作信息这符合Actor-Critic算法的理论设计。相似点都使用相同的编码器视觉编码器可共享都使用MLP主干网络hidden_dims配置相同默认[256, 256]都支持多设备并行通过ensemble机制关键差异特性ActorCritic输入仅观测observations观测 动作[obs_enc, actions]输出动作分布参数mean, std标量Q值网络结构独立输出均值和标准差单一输出层激活函数最后一层通常激活通常线性输出4.2 网络定义Actor 网络如下class Policy(nn.Module): encoder: Optional[nn.Module] # 视觉编码器 network: nn.Module # MLP主干网络 action_dim: int def __call__(self, observations, temperature1.0): if self.encoder is None: obs_enc observations else: obs_enc self.encoder(observations, traintrain, stop_gradientTrue) outputs self.network(obs_enc, traintrain) means nn.Dense(self.action_dim)(outputs) stds nn.Dense(self.action_dim)(outputs) # 标准差参数 return TanhMultivariateNormalDiag(locmeans, scale_diagstds)Critic 网络定义如下class Critic(nn.Module): encoder: Optional[nn.Module] # 视觉编码器 network: nn.Module # MLP主干网络 def __call__(self, observations, actions, trainFalse): if self.encoder is None: obs_enc observations else: obs_enc self.encoder(observations) inputs jnp.concatenate([obs_enc, actions], -1) # 关键差异 outputs self.network(inputs, traintrain) value nn.Dense(1)(outputs) # 输出Q值 return jnp.squeeze(value, -1)4.3 SAC 网络架构SACAgent 算是 SERL Agent 系统的基础所以我们从它看起。class SACAgent(flax.struct.PyTreeNode):其总体信息如下组件输入网络结构输出参数共享Actor图像观测编码器MLP[256,256]动作分布(μ,σ)编码器可共享Critic图像动作编码器Ensemble MLP[256,256]×2Q值编码器可共享Temperature无Lagrange乘数标量温度独立参数4.3.1 核心组件Actor (Policy) 网络内部结构编码器将图像编码为特征向量MLP主干[256, 256]全连接层输出层均值和标准差各一个全连接层分布TanhMultivariateNormalDiagpolicy_def Policy( encoderencoders[actor], # 视觉编码器 networkMLP(**policy_network_kwargs), # 默认 [256, 256] action_dimactions.shape[-1], tanh_squash_distributionTrue, std_parameterizationuniform, )Critic内部结构编码器与Actor相同或独立Ensemble MLP默认2个独立的Critic网络输入拼接编码特征和动作[obs_enc, actions]输出标量Q值critic_backbone partial(MLP, **critic_network_kwargs) # [256, 256] critic_backbone ensemblize(critic_backbone, critic_ensemble_size)( namecritic_ensemble ) critic_def partial( Critic, encoderencoders[critic], # 可与Actor共享编码器 networkcritic_backbone )Temperature (自动熵调节) 网络作用自动调节策略熵的目标值约束确保温度参数 ≥ 目标熵更新通过拉格朗日乘数法优化temperature_def GeqLagrangeMultiplier( init_valuetemperature_init, # 默认1.0 constraint_shape(), constraint_typegeq, )4.3.2 编码器架构small 编码器encoders { image_key: SmallEncoder( features(32, 64, 128, 256), kernel_sizes(3, 3, 3, 3), strides(2, 2, 2, 2), paddingVALID, pool_methodavg, bottleneck_dim256, spatial_block_size8, ) }resnet 编码器encoders { image_key: resnetv1_configs[resnetv1-10]( pooling_methodspatial_learned_embeddings, num_spatial_blocks8, bottleneck_dim256, ) }resnet-pretrained 编码器pretrained_encoder resnetv1_configs[resnetv1-10-frozen]( pre_poolingTrue, ) encoders { image_key: PreTrainedResNetEncoder( pooling_methodspatial_learned_embeddings, num_spatial_blocks8, bottleneck_dim256, pretrained_encoderpretrained_encoder, # 冻结的预训练权重 ) }4.3.3 损失函数SAC 的熵正则化保证了探索性双Critic的ensemble提供了稳定的价值估计自动温度调节实现了探索-利用的平衡。Critic损失def critic_loss_fn(self, batch, params, rng): # 计算目标Q值 target_next_qs self.forward_target_critic(batch[next_observations], next_actions, rng) target_next_min_q target_next_qs.min(axis0) # 最小Q值保守估计 # TD误差 predicted_qs self.forward_critic(batch[observations], batch[actions], rng, grad_paramsparams) critic_loss jnp.mean((predicted_qs - target_qs) ** 2)Actor损失def policy_loss_fn(self, batch, params, rng): # 最大化Q值-熵 predicted_q predicted_qs.mean(axis0) actor_objective predicted_q - temperature * log_probs actor_loss -jnp.mean(actor_objective)0x05 特色功能5.1 重参数化重参数化Reparameterization Trick直接从分布采样是不可导的。通过 a μ σ · εε 是固定噪声我们把随机性剥离出来让梯度能顺着加法和乘法回传。5.1.1 问题在 SAC 中Actor 输出的是一个概率分布通常是高斯分布。由于我们需要对这个分布进行采样才能得到动作 a但采样这个动作是不可导的这就导致梯度无法直接回传给生成分布的神经网络。为什么采样不能传导梯度这是深度学习中最经典的问题之一。场景神经网络输出 μ10σ2。采样你从这个分布里「随机」抽了一个数 a11。断裂点当你计算 Loss 后你想问「如果我把 μ 从 10 改成 10.1对 a 有什么影响」结论无法回传。因为「采样」这个动作在计算机里是调用了random()。随机数发生器就像一个黑盒梯度传到这里就断了。5.1.2 方案那么在复现 Actor 的更新过程时我们该如何让梯度通过这个采样步骤传回神经网络的参数中重参数化Reparameterization Trick其实就是为了解决 Actor 怎么根据这个带熵的 Q 值更新梯度的问题。目前问题就是我们该怎么把抽样这个动作变成一个加减乘除的公式SERL 不直接采样 a∼N(μ,σ)而是写成aμσ⋅ε,ε∼N(0,1)这里 ε 是一个固定的随机噪声。现在a 就变成了一个关于 μ 和 σ 的确定性函数加法和乘法梯度就可以顺着 a→μ 和 a→σ 传回神经网络了。5.2 输出控制重参数化使用了 aμσε。但在机器人控制中动作通常是有范围的比如 -1 到 1。直接加减可能会超出范围。SAC 论文里用了Tanh 激活函数来把这个 a 限制在 (−1,1)。做法Actor 输出一个原始值 u∼N(μ,σ)然后计算 atanh(u)。用了 Tanh 之后动作就不再是纯粹的高斯分布了。为了计算准确的熵我们需要用到雅可比行列式Jacobian来对概率密度进行修正。在代码里这通常表现为一个修正项loss−logp(u)−log(1−tanh(u)2)。5.2.1 Tanh 挤压: 气球与盒子的数学当你把一个高斯分布的 u 通过 a tanh(u) 映射到 (-1, 1) 时, 概率密度会发生变化。为什么不能直接用高斯公式? 因为 tanh 在靠近 1和-1 的时候非常平。很多个不同的 u 可能会被挤压到极其接近的 a。代码怎么写? 我们需要用到雅可比修正 (Jacobian Correction)。公式如下logπ(a|s)logμ(u|s)−ΣDi1log(1−tanh²(ui))注: μ(u|s) 是原始高斯分布的概率。在代码中, 这通常写成: log_prob dist.log_prob(u) - torch.log(1 - a.pow(2) 1e-6).sum(dim-1)。1e-6 是为了防止数值溢出。5.2.2 Clip 的灾难梯度消失如果我们不使用 Tanh 修正直接强制把超出范围的动作 clip 掉这会给梯度回传带来什么灾难如果用clip(a, -1, 1)Actor 输出 a1.5被clip成了1.0。在反向传播时clip 函数在 1.5 这里的导数是 0。后果梯度传到这里就断了神经网络接收不到任何信号告诉它其实你应该减小输出。Tanh 的好处它是平滑的即便输出很大梯度依然存在虽然很小能指引网络回来。0x06 SAC 的工作流程6.1 工作流程极简版工作流程如下收集数据在环境里跑趟把 (s,a,r,s′,done) 存进经验回放池Replay Buffer。训练 Critic从池子里抓一批数据告诉 Q 网络根据你看到的奖励和下一步的预测修正你对当前状态动作价值的评估。训练 Actor告诉 Actor调整你的参数使得你输出的动作能让 Q 值最大同时熵也要足够大。下面是 SAC 算法的高层结构伪代码。它清晰地展示了 数据流 是如何在 Actor (演员)、Critic (评论家) 和 Buffer (经验池) 之间流动的。class SACAgent: def __init__(self): # 1. 初始化 5 个核心网络 self.actor ActorNetwork() # 策略函数: s - (mu, sigma) self.critic1 CriticNetwork() # Q1函数: (s, a) - q1 self.critic2 CriticNetwork() # Q2函数: (s, a) - q2 self.target_critic1 Target() # Q1的稳定副本 self.target_critic2 Target() # Q2的稳定副本 # 2. 熵自动调节参数 (Temperature) self.log_alpha log(initial_alpha) # 3. 经验回放池 self.replay_buffer ReplayBuffer(capacity1000000) def step(self, state): 与环境交互: 根据当前状态, 喷出一个动作 action self.actor.sample(state) return action def train_step(self): 核心训练逻辑: SAC 的三步走 # 从池子里抓一把数据 batch self.replay_buffer.sample(batch_size256) # --- 第一步: 更新 Critic (练地图) --- self.update_critic(batch) # --- 第二步: 更新 Actor (练向导) --- # 顺着 Critic 指出的梯度方向, 让 Actor 变得更好 self.update_actor(batch) # --- 第三步: 自动调节 Alpha (练灵魂) --- # 如果熵太小, 调大 Alpha 增加探索; 反之调小 self.update_alpha(batch) # --- 最后: 平滑更新 Target 网络 --- self.soft_update_targets() def update_critic(self, batch): 计算带熵的 Bellman 目标 # 核心公式: Target R gamma * (min(Q1_target, Q2_target) - alpha * log_prob) target_q self.calculate_target_q(batch) # 最小化 MSE 误差 loss1 MeanSquaredError(self.critic1(s, a), target_q) loss2 MeanSquaredError(self.critic2(s, a), target_q) # 执行梯度下降...6.2 sac.py SERL我们接下来看看 SERL 开源代码的实现看看其对 SAC 做了什么改变。6.2.1 RLPD 预适配在 SERL 的代码库中sac.py 扮演的是通用底座的角色。原生 SAC 在真机上其实很慢。为了让它起飞SERL 做了若干增强sac.py 其实是一个全能型 SAC。虽然这个文件叫 sac.py但它已经为 RLPD 做好了全部基础准备High UTD 支持update_high_utd 函数把一个大的 Batch 拆成 20 份连续更新 20 次 Critic这是 RLPD 能跑通的前提。LayerNorm 的隐形支持它调用了 MLP 网络。只要在创建时传入 value_layer_normTrue它就会自动在内部插入归一化层。Ensemble Q它支持 critic_ensemble_size10这是 RLPD 抑制 Q 值发散的手段。即在计算 Target 时, 它不是取最小值, 而是计算这 10 个 Q 的均值减去标准差TargetQmean(Q1...10)−std(Q1...10)×ρ这叫悲观备份。在不确定的地方, Q 值会因为标准差大而被拉低。这强迫智能体只信任那些所有 Q 网络都达成共识的高分区域。自动调节 AlphaLagrange它使用了拉格朗日乘子法GeqLagrangeMultiplier来自动调节熵比我们手写的手动更新公式更数学化、更稳定。JAX 异步更新利用 JAXSAC 的 10 个 Critic 可以在不同显卡上并行更新极大地提升了训练吞吐量。缺少的内容如下缺少 50/50 采样逻辑在 sac.py 的 update 函数中它只接收一个 batch。真正的 RLPD 逻辑从两个池子各抽 128 个数据通常是在外部的训练循环中完成的或者是通过更高层的封装实现的。缺少 BC Losssac.py 的 policy_loss_fn中只有 predicted_q - temperature * log_probs。它没有我们之前在 rlpd.py 里看到的那个关键的 bc_alpha * log_prob(batch_actions)。这意味着这个 sac.py 并不具备模仿演示数据的能力。6.2.2 逻辑流程图特色功能 (Special Features)如下Ensemble Support: 通过 jax.vmap 实现的 Q 集成训练速度极快天生支持 REDQ 算法。High UTD Dispatch: 专门的 update_high_utd 逻辑大幅提升采样效率。Modular Encoders: 支持 Shared Encoder (ResNet)节省显存并加速表征学习。Action Chunking: 支持一次输出一串动作适合高频机器人控制场景。6.2.3 四大特色深度解释极致的集成Ensemble与向量化。sac.py 使用了 ensemblize 技巧。黑科技它利用 JAX 的 vmap 将 Q 网络变成了一个并行张量。优势无论你是想要 2 个 Q 还是 10 个 Q在底层计算上几乎一样快。这让算法在保持悲观评估防止高估的同时不会拖累机器人的实时响应。重 Critic、轻 Actor 的高 UTD 架构。SERL 中有一个非常显著的策略在 update_high_utd 里Critic 更新 20 次Actor 才更新 1 次。解释Critic 是 Actor 的导师。如果导师自己都还没把图画清楚Q 值没收敛让 Actor 拼命改参数只会让它学废了。先刷 20 次再更新一次是 SERL 实现 20 分钟学会抓取的硬件级优化。灵活的视觉编码器架构create_pixels。源码中通过 shared_encoder 参数决定了 Actor 和 Critic 是否共用一个视觉大脑。解释在机器人任务中处理像素是最累的活。共用 ResNet 不仅显存省更重要的是能强迫网络去学习那些任务通用的物理特征比如杯子的边缘在哪里、桌子的高度是多少而不是只学习针对自己有用的特征。拉格朗日温度控制GeqLagrangeMultiplier。源码中引入了拉格朗日约束并非简单的梯度下降来更新 α。解释这是一种更稳健的数学方法它能确保熵被强制约束在一个区间内。当熵太低时α 会像踩刹车一样迅速反弹防止智能体陷入死胡同。6.2.4 损失函数在 SAC 中训练目标通常拆成三个部分损失函数更新对象核心目标critic_loss_fncritic / Q 网络学习 Bellman backup让 Q 逼近 TD targetpolicy_loss_fnactor / policy 网络最大化 Q同时最大化熵temperature_loss_fntemperature / α自动调节熵权重使策略熵接近目标熵在这份代码里这三个 loss 会被包装成一个字典def loss_fns(self, batch): return { critic: partial(self.critic_loss_fn, batch), actor: partial(self.policy_loss_fn, batch), temperature: partial(self.temperature_loss_fn, batch), }这意味着critic_loss_fn → 更新 params[critic] policy_loss_fn → 更新 params[actor] temperature_loss_fn → 更新 params[temperature]不过从实现上看apply_loss_fns会对全量self.params求梯度然后通过不同 optimizer 分支把对应梯度应用到参数树上。6.2.5 训练调度器update则是训练调度器它先整理 batch再构造三个 loss按networks_to_update决定本轮更新哪些网络最后统一调用apply_loss_fns计算梯度并应用 optimizer。可以把整个 update 理解成一个三方协作系统critic_loss_fn: 学会评价 replay buffer 中的动作。 目标来自 r γ * target_Q(s, π(s))。 policy_loss_fn: 利用 critic 的评价来改进 actor。 让 actor 选择 Q 更高且保持一定熵的动作。 temperature_loss_fn: 自动调节 α。 如果策略太确定就提高熵权重 如果策略太随机就降低熵权重。对应到真实机器人训练场景中我们可以这样理解critic像评分器判断某个状态下某个动作未来是否有价值让打分更准actor像执行策略根据评分器的反馈学习更好的动作让动作更像高分动作且多样化temperature像探索旋钮控制机器人是更大胆探索还是更稳定执行。6.2.6 三个 loss 与 update 的总流程图