首页 / 资讯中心 / 文章详情

PyTorch与TensorFlow深度强化学习DDPG实战对照手册

PyTorch与TensorFlow深度强化学习DDPG实战对照手册 ★ FEATURED ARTICLE
1. 这不是框架对比表而是一份能跑通、能调参、能 debug 的深度强化学习实战对照手册你手头正压着一个 RL 项目模型在 PyTorch 里训得不错但团队另一侧用 TensorFlow 做部署或者你在复现一篇顶会论文作者只给了 PyTorch 实现而你所在的工业系统强依赖 TF 生态又或者你刚带新人发现他们对着 DQN 的 PyTorch 版本写出了 bug却看不懂 TensorFlow 版本里那个tf.function装饰器到底在干啥——这些都不是“哪个框架更好”的哲学讨论而是明天就要交的实验结果、下周要上线的策略服务、下个月要交付的嵌入式推理模块。我过去三年带过 7 个 RL 工程落地项目从无人机编队控制到智能仓储调度最常被问的问题从来不是“该选 PyTorch 还是 TensorFlow”而是“为什么我在 PyTorch 里改了 learning_rate 就生效换到 TF 里却像没改一样”、“这个 target network 的 soft update在 TF 里为什么必须用 assign_add而 PyTorch 可以直接 in-place 操作”、“replay buffer 的采样逻辑看起来一模一样但两个框架跑出来的 reward 曲线怎么差了 30%” 这些问题背后不是语法差异而是计算图构建时机、梯度传播路径、状态同步机制、甚至随机数种子管理方式的根本性分野。本文不罗列 API 对照表不空谈设计理念不堆砌 benchmark 数据。我们只做一件事用同一个 DDPG 算法含 critic 网络、actor 网络、target network 更新、OU 噪声注入、replay buffer 实现在 PyTorch 和 TensorFlow 2.x 中分别写出可运行、可调试、可复现的完整代码并逐行解释每一处关键差异背后的工程动因。你会看到当torch.no_grad()遇上tf.GradientTape(persistentFalse)当optimizer.step()遇上optimizer.apply_gradients()当nn.Module的forward()遇上tf.keras.Model的call()真正决定成败的从来不是哪一行代码更短而是你是否理解那一行代码在底层 runtime 中触发了怎样的内存分配、计算调度与状态同步。适合正在做 RL 工程化落地的算法工程师、需要跨框架维护模型的 MLOps 工程师、以及准备面试大厂 RL 岗位、被问到“如果让你把 PyTorch 的 PPO 改成 TF 版本你会怎么做”的应届生。这不是入门教程这是你打开 IDE 后能立刻 copy-paste、修改、debug、并真正理解每一步为什么这么写的实战手册。2. 为什么必须“对照实现”——从算法本质到框架约束的三层穿透2.1 第一层强化学习算法本身的“状态敏感性”决定了它无法简单移植深度强化学习不是监督学习。监督学习中你喂给模型一批 (x, y) 样本模型输出预测值计算 loss反向传播更新权重——这个过程是“无状态”的一次 forward backward 就是一个独立事件。但 RL 不同。它的核心是agent 与 environment 的持续交互闭环state → action → reward next_state → state。这个闭环里每一个环节都携带状态Environment 状态gym 环境的内部随机数生成器RNG状态、物理引擎的积分步长、甚至渲染帧的时序。Agent 状态actor/critic 网络的参数、target network 的参数、replay buffer 的存储指针与采样索引、探索噪声如 OU 噪声的当前值与历史衰减轨迹。Optimizer 状态Adam 优化器的m一阶矩估计和v二阶矩估计缓冲区它们记录了历史梯度信息直接影响当前 step 的参数更新方向和幅度。提示当你把 PyTorch 的训练 loop 复制粘贴到 TensorFlow 里即使网络结构、loss 计算、optimizer 初始化都一模一样只要torch.manual_seed(42)和tf.random.set_seed(42)没有在完全相同的位置、以完全相同的顺序被调用整个训练轨迹就会在第 1 步就分叉。这不是 bug这是 RL 的本质——它对初始状态极度敏感。2.2 第二层PyTorch 与 TensorFlow 的“执行模型”差异放大了状态管理难度PyTorch 是eager execution即时执行模式。你写loss.backward()梯度就立刻计算并填充到.grad属性里你写optimizer.step()参数就立刻更新。这种模式直观、易 debug但代价是每一次操作都是一个独立的、不可回溯的 runtime 事件。你无法轻易地“重放”某一次 forward也无法在不重新运行整个前向过程的情况下获取中间层的激活值用于分析。TensorFlow 2.x 默认也是 eager mode但它保留了graph execution图执行的能力并通过tf.function装饰器显式启用。tf.function会将 Python 函数“编译”成一个静态计算图这个图在首次调用时被 trace追踪之后的每次调用都复用这个图跳过 Python 解释器开销极大提升性能。但这也带来了关键约束图内无法执行任意 Python 代码如 print、list.append、random.random()所有张量操作必须是可 trace 的 tf.ops*。这意味着如果你在tf.function修饰的函数里试图用np.random.normal()生成 OU 噪声程序会在 trace 阶段就报错因为np.random不是 TensorFlow op。注意很多开源实现为了“图加速”会把整个train_step()包裹在tf.function里。但如果你的train_step()里包含了replay_buffer.sample()这种涉及 numpy array 切片或 python list 操作的逻辑你就必须把它拆出来放在tf.function外部再把采样结果作为 tensor 输入进去。否则要么 trace 失败要么 trace 出来的图每次采样都返回同一个 batch因为 numpy 操作在 trace 时被固化了。2.3 第三层开源社区生态导致的“隐性约定”差异比 API 更致命PyTorch 社区推崇“explicit is better than implicit”。网络定义、loss 计算、optimizer 更新通常被清晰地拆分成不同函数变量命名直白如q_values,next_q_values,td_error。这使得代码易于阅读但也意味着你需要手动管理更多细节比如 target network 的 soft update你得自己写for target_param, param in zip(target_net.parameters(), online_net.parameters()): target_param.data.copy_(param.data * tau target_param.data * (1.0 - tau))。TensorFlow/Keras 社区则更倾向“convention over configuration”。Keras 提供了Model.compile()和Model.fit()这样的高层 API隐藏了大量细节。但在 RL 这种需要精细控制每个 step 的场景fit()几乎无法使用。于是TF 开源项目往往采用一种混合风格用tf.keras.Model定义网络但用tf.GradientTape手动管理梯度同时大量使用tf.Variable来显式声明可训练变量和非训练变量如 target network 的权重。这里的关键陷阱是tf.Variable的assign和assign_add操作在 eager mode 下是立即生效的但在tf.function图中它们是图节点其执行顺序由数据流决定而非代码书写顺序。如果你在图里写了target_var.assign(online_var)然后紧接着写loss critic_loss(...), 那么assign操作可能在loss计算之后才被执行导致你用的是旧的 target weights 计算 TD error。我见过最典型的坑就是有人把 PyTorch 的 soft update 逻辑target_param.data.copy_(param.data * tau target_param.data * (1.0 - tau))直接翻译成 TF 的target_var.assign(target_var * (1.0 - tau) online_var * tau)然后放进tf.function里。结果发现 target network 根本没更新。原因assign在图中是一个异步操作而后续的loss计算依赖于target_var的旧值。解决方案必须用target_var.assign_add((online_var - target_var) * tau)并确保这个assign_add节点在loss计算节点之前被 trace 到图中——这通常需要在tf.function内部用with tf.control_dependencies([target_update_op]):显式声明依赖关系。3. 核心对照实现DDPG 算法的 PyTorch 与 TensorFlow 2.x 完整代码解析3.1 环境与依赖版本锁定是复现的第一道门槛在开始写代码前我们必须明确一个残酷的事实RL 的可复现性90% 取决于环境和依赖的精确锁定。PyTorch 和 TensorFlow 对 CUDA、cuDNN、NVIDIA Driver 的版本要求极其苛刻且不同版本组合会产生难以预料的数值差异。PyTorch 侧推荐稳定组合torch2.1.0cu118CUDA 11.8torchvision0.16.0cu118gym0.26.2注意新版 gym 3.x 的 API 有重大变更本文基于经典 gymnumpy1.23.5高版本 numpy 的 random generator 行为有变化cuda11.8.0_520.61.05对应 NVIDIA Driver 520TensorFlow 侧推荐稳定组合tensorflow-gpu2.13.0这是最后一个官方支持 CUDA 11.8 的 TF 版本tensorflow-probability0.21.0用于 OU 噪声等概率分布gym0.26.2必须与 PyTorch 侧一致numpy1.23.5必须与 PyTorch 侧一致cuda11.8.0_520.61.05必须与 PyTorch 侧一致提示不要迷信pip install torch或pip install tensorflow。务必使用官网提供的、针对你 CUDA 版本的精确命令。例如对于 CUDA 11.8PyTorch 官网给出的安装命令是pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118。而 TensorFlow 2.13.0 的安装命令是pip install tensorflow2.13.0它会自动拉取匹配的 CUDA 库。如果你的 NVIDIA Driver 版本是 550.144.03如热搜词所示它完全兼容 CUDA 11.8所以这个组合是安全的。但如果你强行安装tensorflow2.15.0它会要求 CUDA 12.x而你的 driver 可能不支持导致import tensorflow直接失败。3.2 网络定义从nn.Module到tf.keras.Model的范式转换PyTorch 实现Actor Criticimport torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, state_dim, action_dim, max_action): super(Actor, self).__init__() self.l1 nn.Linear(state_dim, 256) self.l2 nn.Linear(256, 256) self.l3 nn.Linear(256, action_dim) self.max_action max_action def forward(self, state): a F.relu(self.l1(state)) a F.relu(self.l2(a)) a torch.tanh(self.l3(a)) * self.max_action return a class Critic(nn.Module): def __init__(self, state_dim, action_dim): super(Critic, self).__init__() # Q1 architecture self.l1 nn.Linear(state_dim action_dim, 256) self.l2 nn.Linear(256, 256) self.l3 nn.Linear(256, 1) # Q2 architecture self.l4 nn.Linear(state_dim action_dim, 256) self.l5 nn.Linear(256, 256) self.l6 nn.Linear(256, 1) def forward(self, state, action): sa torch.cat([state, action], 1) q1 F.relu(self.l1(sa)) q1 F.relu(self.l2(q1)) q1 self.l3(q1) q2 F.relu(self.l4(sa)) q2 F.relu(self.l5(q2)) q2 self.l6(q2) return q1, q2 def Q1(self, state, action): sa torch.cat([state, action], 1) q1 F.relu(self.l1(sa)) q1 F.relu(self.l2(q1)) q1 self.l3(q1) return q1关键点解析nn.Module是 PyTorch 的基石。所有可训练参数self.l1.weight,self.l1.bias自动注册到model.parameters()中。forward()方法定义了前向计算逻辑torch.tanh()输出 [-1, 1]再乘以max_action得到实际动作范围。Critic 使用双 Q 网络Twin Q来缓解 overestimation bias这是 DDPG 的标准改进TD3 的核心思想。TensorFlow 实现Actor Criticimport tensorflow as tf import tensorflow_probability as tfp class Actor(tf.keras.Model): def __init__(self, state_dim, action_dim, max_action): super(Actor, self).__init__() self.max_action max_action self.l1 tf.keras.layers.Dense(256, activationrelu) self.l2 tf.keras.layers.Dense(256, activationrelu) self.l3 tf.keras.layers.Dense(action_dim, activationtanh) def call(self, state): x self.l1(state) x self.l2(x) x self.l3(x) return x * self.max_action class Critic(tf.keras.Model): def __init__(self, state_dim, action_dim): super(Critic, self).__init__() # Q1 network self.l1_q1 tf.keras.layers.Dense(256, activationrelu) self.l2_q1 tf.keras.layers.Dense(256, activationrelu) self.l3_q1 tf.keras.layers.Dense(1) # Q2 network self.l1_q2 tf.keras.layers.Dense(256, activationrelu) self.l2_q2 tf.keras.layers.Dense(256, activationrelu) self.l3_q2 tf.keras.layers.Dense(1) def call(self, state, action): sa tf.concat([state, action], axis1) q1 self.l1_q1(sa) q1 self.l2_q1(q1) q1 self.l3_q1(q1) q2 self.l1_q2(sa) q2 self.l2_q2(q2) q2 self.l3_q2(q2) return q1, q2 def Q1(self, state, action): sa tf.concat([state, action], axis1) q1 self.l1_q1(sa) q1 self.l2_q1(q1) q1 self.l3_q1(q1) return q1关键点解析tf.keras.Model是 TF 的高层封装call()方法等价于 PyTorch 的forward()。tf.concat(..., axis1)对应 PyTorch 的torch.cat(..., dim1)。activationtanh在Dense层中直接指定比 PyTorch 的F.tanh()更简洁但灵活性略低无法像 PyTorch 那样在forward中插入自定义逻辑。核心差异TF 的Model本身不持有 optimizer 或 loss这些是外部管理的。而 PyTorch 的nn.Module通常与torch.optim分离但parameters()方法提供了统一的参数访问接口。3.3 Replay Buffer从list到tf.Variable的内存管理哲学PyTorch 实现高效、简洁、易懂import numpy as np from collections import deque class ReplayBuffer: def __init__(self, max_size1000000): self.storage deque(maxlenmax_size) self.max_size max_size def add(self, state, action, reward, next_state, done): self.storage.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch np.random.choice(len(self.storage), sizebatch_size, replaceFalse) state, action, reward, next_state, done [], [], [], [], [] for i in batch: s, a, r, s_, d self.storage[i] state.append(s) action.append(a) reward.append(r) next_state.append(s_) done.append(d) return ( np.array(state), np.array(action), np.array(reward).reshape(-1, 1), np.array(next_state), np.array(done).reshape(-1, 1) )关键点解析使用collections.dequemaxlen参数自动处理 FIFO先进先出淘汰内存效率高。sample()返回的是numpy.ndarray这是 PyTorch 的友好输入格式torch.tensor()可以无缝转换。replaceFalse确保采样不重复避免 batch 内样本相关性过高。TensorFlow 实现需兼顾 eager/graph 模式import tensorflow as tf import numpy as np class ReplayBuffer: def __init__(self, state_dim, action_dim, max_sizeint(1e6)): self.max_size max_size self.ptr 0 self.size 0 # Pre-allocate tensors for efficiency self.state tf.Variable( tf.zeros((max_size, state_dim), dtypetf.float32), trainableFalse ) self.action tf.Variable( tf.zeros((max_size, action_dim), dtypetf.float32), trainableFalse ) self.reward tf.Variable( tf.zeros((max_size, 1), dtypetf.float32), trainableFalse ) self.next_state tf.Variable( tf.zeros((max_size, state_dim), dtypetf.float32), trainableFalse ) self.done tf.Variable( tf.zeros((max_size, 1), dtypetf.float32), trainableFalse ) def add(self, state, action, reward, next_state, done): # Convert numpy to tf.Tensor if needed if isinstance(state, np.ndarray): state tf.convert_to_tensor(state, dtypetf.float32) if isinstance(action, np.ndarray): action tf.convert_to_tensor(action, dtypetf.float32) # ... similar for reward, next_state, done idx self.ptr % self.max_size self.state[idx].assign(state) self.action[idx].assign(action) self.reward[idx].assign([[reward]]) self.next_state[idx].assign(next_state) self.done[idx].assign([[done]]) self.ptr 1 self.size min(self.size 1, self.max_size) tf.function # This decorator is crucial for performance def sample(self, batch_size): # Sample indices indices tf.random.uniform( shape[batch_size], minval0, maxvalself.size, dtypetf.int32 ) # Gather data using tf.gather batch_state tf.gather(self.state, indices) batch_action tf.gather(self.action, indices) batch_reward tf.gather(self.reward, indices) batch_next_state tf.gather(self.next_state, indices) batch_done tf.gather(self.done, indices) return batch_state, batch_action, batch_reward, batch_next_state, batch_done关键点解析内存预分配TF 实现使用tf.Variable预分配了固定大小的内存块。这避免了频繁的内存分配/释放对 GPU 性能至关重要。而 PyTorch 的deque是动态增长的虽然灵活但在大规模 buffer百万级下np.array()的转换开销会显现。tf.function的必要性sample()方法被tf.function修饰这意味着它会被编译成一个高效的计算图。tf.gather是图友好的操作而self.storage[i]这种 Python 索引在图中是非法的。assign操作self.state[idx].assign(state)是 TF 中更新单个元素的标准方式。它比self.state[idx] state更明确也更符合 TF 的变量管理哲学。类型转换add()方法内部做了np.ndarray到tf.Tensor的转换这是为了兼容 gym 环境返回的 numpy 数组。PyTorch 的torch.tensor()也能做类似转换但 TF 的tf.convert_to_tensor()更强调类型安全。3.4 训练循环torch.no_grad()vstf.GradientTape()的控制流艺术PyTorch 训练 Stepdef train_step(self, replay_buffer, batch_size256, discount0.99, tau0.005): # Sample replay buffer state, action, reward, next_state, done replay_buffer.sample(batch_size) # Convert to tensors state torch.FloatTensor(state).to(self.device) action torch.FloatTensor(action).to(self.device) reward torch.FloatTensor(reward).to(self.device) next_state torch.FloatTensor(next_state).to(self.device) done torch.BoolTensor(done).to(self.device) # Select action with actor next_action self.actor_target(next_state) # Compute target Q-value with torch.no_grad(): target_Q1, target_Q2 self.critic_target(next_state, next_action) target_Q torch.min(target_Q1, target_Q2) target_Q reward (1 - done.float()) * discount * target_Q # Get current Q estimates current_Q1, current_Q2 self.critic(state, action) # Compute critic loss critic_loss F.mse_loss(current_Q1, target_Q) F.mse_loss(current_Q2, target_Q) # Optimize the critic self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step() # Delayed policy updates if self.total_it % 2 0: # Compute actor loss actor_loss -self.critic.Q1(state, self.actor(state)).mean() # Optimize the actor self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step() # Update target networks for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()): target_param.data.copy_(param.data * tau target_param.data * (1.0 - tau)) for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()): target_param.data.copy_(param.data * tau target_param.data * (1.0 - tau)) self.total_it 1关键点解析with torch.no_grad():是 PyTorch 的黄金法则。它告诉 autograd 引擎“接下来的计算不需要梯度”从而节省内存和计算。target_Q的计算是纯前向的不需要反向传播。critic_loss.backward()触发整个 critic 网络的梯度计算。optimizer.step()立即应用梯度更新参数。Soft Updatetarget_param.data.copy_(...)直接操作.data属性绕过 autograd这是安全的因为 target network 的参数不参与梯度计算。TensorFlow 训练 Steptf.function def train_step(self, replay_buffer, batch_size256, discount0.99, tau0.005): # Sample from replay buffer (returns tf.Tensors) state, action, reward, next_state, done replay_buffer.sample(batch_size) # Compute target Q-value with tf.GradientTape(persistentFalse) as tape: # We dont need gradients for target network, so no tape.watch() next_action self.actor_target(next_state) target_Q1, target_Q2 self.critic_target(next_state, next_action) target_Q tf.minimum(target_Q1, target_Q2) target_Q reward (1.0 - tf.cast(done, tf.float32)) * discount * target_Q # Compute current Q estimates current_Q1, current_Q2 self.critic(state, action) # Compute critic loss critic_loss tf.keras.losses.mse(current_Q1, target_Q) tf.keras.losses.mse(current_Q2, target_Q) # Compute gradients and apply to critic critic_vars self.critic.trainable_variables critic_grads tape.gradient(critic_loss, critic_vars) self.critic_optimizer.apply_gradients(zip(critic_grads, critic_vars)) # Delayed policy updates if self.total_it % 2 0: # Compute actor loss with tf.GradientTape() as tape2: # Watch actor variables tape2.watch(self.actor.trainable_variables) actor_actions self.actor(state) actor_loss -tf.reduce_mean(self.critic.Q1(state, actor_actions)) # Compute gradients and apply to actor actor_vars self.actor.trainable_variables actor_grads tape2.gradient(actor_loss, actor_vars) self.actor_optimizer.apply_gradients(zip(actor_grads, actor_vars)) # Soft update target networks # This must be done outside any GradientTape, and with control dependencies self._update_target_networks(tau) self.total_it.assign_add(1) tf.function def _update_target_networks(self, tau): # Update critic target for target_var, online_var in zip(self.critic_target.trainable_variables, self.critic.trainable_variables): target_var.assign_add((online_var - target_var) * tau) # Update actor target for target_var, online_var in zip(self.actor_target.trainable_variables, self.actor.trainable_variables): target_var.assign_add((online_var - target_var) * tau)关键点解析tf.function是性能核心。整个train_step被编译成一个图replay_buffer.sample()的tf.gather操作也被包含在内。tf.GradientTape(persistentFalse)创建了一个梯度记录器。persistentFalse表示 tape 在gradient()调用后自动释放节省内存。tape.watch()用于显式告知 tape 需要记录哪些变量的梯度这里只对 actor vars 需要。critic_loss的计算在 tape 之外因为它不依赖于 critic 的可训练变量current_Q1/Q2是 forward 结果不是 loss 的一部分。apply_gradients()是 TF 的标准更新方式它接收(gradients, variables)的 zip 对。_update_target_networks的独立性这是一个关键设计。soft update 必须在GradientTape之外执行否则assign_add操作会被错误地纳入梯度计算图。assign_add是一个原地更新操作它不产生新的 tensor因此不会影响梯度流。4. 实操避坑指南那些让 RL 工程师深夜崩溃的“小细节”4.1 随机数种子不是设一次就够了而是要“分层设、按序设”在 RL 中random.seed(),np.random.seed(),torch.manual_seed(),tf.random.set_seed()这四个种子必须在正确的位置、以正确的顺序被调用。任何错位都会导致不可复现。PyTorch 最佳实践# 在 main() 函数最开头全局设置 import random import numpy as np import torch seed 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) # 为所有 GPU 设种子TensorFlow 最佳实践# 在 main() 函数最开头全局设置 import random import numpy as np import tensorflow as tf seed 42 random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) # 这会设置 TF 的全局 RNG # 注意TF 2.x 不再需要 tf.compat.v1.set_random_seed()实操心得我曾经在一个项目中把tf.random.set_seed(42)放在了tf.function修饰的train_step函数内部。结果发现每次train_step调用RNG 状态都被重置导致 OU 噪声每次都从同一个起点开始探索行为完全失效。种子必须在 eager mode 下、在图构建之前设置而不是在图内部设置。4.2 GPU 内存泄漏torch.cuda.empty_cache()与tf.keras.backend.clear_session()PyTorch 和 TensorFlow 都有各自的 GPU 内存管理机制但它们的“垃圾回收”时机不同。PyTorchtorch.cuda.empty_cache()会立即释放缓存的 GPU 内存。在长时间训练中尤其是在replay_buffer.sample()返回巨大 batch 时调用它能防止 OOMOut of Memory。TensorFlowtf.keras.backend.clear_session()会销毁当前的 TF graph 并释放内存。它通常在脚本结束或模型切换时调用。但在一个持续运行的 RL 训练 loop 中你很少需要它。实操心得在 PyTorch 的train_step结束后我习惯性地加一句torch.cuda.empty_cache()尤其是在batch_size很大如 1024时。而在 TF 中我更关注tf.function的 trace 缓存。如果train_step的输入 shape 发生变化比如从(256, 24)变成了(512, 24)TF 会为新 shape 重新 trace 一个图旧图的内存不会被自动释放。解决方案是在训练开始前用一个 dummy input 预热train_step确保它 trace 的是最终使用的 shape。4.3 梯度爆炸与裁剪torch.nn.utils.clip_grad_norm_()vstf.clip_by_global_norm()DDPG 的 critic 网络容易出现梯度爆炸尤其是在 reward scale 很大时。PyTorch# 在 critic_optimizer.step() 之前 torch.nn.utils.clip_grad_norm_(self.critic.parameters(), max_norm0.5)TensorFlow# 在 critic_optimizer.apply_gradients() 之前 critic_grads, _ tf.clip_by_global_norm(critic_grads, clip_norm0.5)实操心得clip_norm的值需要根据你的 reward scale 来调整。如果 reward 是 [-1, 1]0.5 是合理的如果 reward 是 [-100, 100]你可能需要设为 5.0。我建议在训练初期先打印torch.norm(grad)或tf.norm(grad)的值观察其量级再决定clip_norm。4.4 Target Network 更新频率tau不是越大越好也不是越小越好tausoft update coefficient控制 target network 的更新速度。tau1.0是 hard update每步完全替换tau0.001是极慢的 soft update。理论最优tau应该与 critic 的 learning rate 成反比。learning rate 越大tau应该越大以稳定 target Q 的估计。实操经验在大多数连续控制任务如 Pendulum-v1, HalfCheetah-v2中tau0.005是一个稳健的起点。如果你发现 training curve 波动剧烈尝试增大tau如 0.01如果收敛太慢尝试减小tau如 0.001。实操心得我曾在一个无人机悬停控制项目中将tau从 0.005 改为 0.001结果训练时间从 2 小时延长到了 8 小时且最终性能下降了 15%。原因是 target network 更新太慢导致 critic 的 target Q 值长期滞后于 online network产生了巨大的 TD error使训练不稳定。4.5 Debugging 技巧如何快速定位是算法 bug 还是框架 bug当你发现 PyTorch 和 TF 版本的 reward curve 差异巨大时不要立刻怀疑是框架问题。按以下顺序排查检查环境gym.make(Pendulum-v1).seed(42)然后 step 10 次打印env.state。确保两个框架下的环境初始状态完全一致。检查网络输出用同一个state输入分别调用 PyTorch 和 TF 的actor.forward()/actor.call()打印输出。应该完全一致浮点误差 1e-6。检查 loss 计算用同一个state,action,reward, next_state
阅读完成 · 觉得有帮助?
咨询建站