ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DDPG算法实战:Python实现深度强化学习连续动作控制

DDPG算法实战:Python实现深度强化学习连续动作控制 DDPG算法实战用Python一步步实现深度强化学习中的连续动作控制如果你之前接触过深度强化学习大概率是从DQN入门的。DQN在Atari游戏上打得风生水起一套卷积网络加经验回放就能让智能体学会玩各种像素级游戏。但有一天你打开一篇论文发现任务是控制机器人关节、让无人机悬停、或者让自动驾驶车辆保持车道你手里的DQN突然就废了——因为这些任务里的动作不是“上下左右”这种离散选项而是一个连续的值比如“左轮力矩2.1牛米”“方向盘转角-0.35弧度”。这就是深度强化学习里非常有代表性的连续动作控制问题也是今天这篇实战要解决的痛点用DDPG算法在Python里一步步搭出一个能学会连续控制的智能体。DDPG的全称是Deep Deterministic Policy Gradient深度确定性策略梯度。它是DQN家族在连续动作空间上的延伸也是很多进阶算法TD3、SAC的思想源头。这篇文章不会只贴一段能跑的代码了事我会带你从“为什么DQN不行”开始把Actor-Critic结构、目标网络、经验回放、探索噪声这些模块一个一个拆开然后基于Gymnasium的Pendulum-v1环境用PyTorch从零实现一遍完整训练流程最后再分享我在实际调参中踩过的坑和排查思路。适合有一定Python基础、了解神经网络基本概念但还没系统上手过连续控制强化学习的读者。1. 为什么DQN搞不定连续动作从问题本质说起1.1 DQN的动作空间是“选择题”不是“填空题”先说个最简单的对比。DQN的核心逻辑是训练一个Q网络然后每个时刻选择Q值最大的动作[ a^* \arg\max_{a} Q(s, a) ]在Atari这类环境里动作集合是有限的比如只有上、下、左、右、开火这几个选项。哪怕动作空间有几百个离散选项把每个动作的Q值都算一遍选最大的那个计算上完全可行。这是“选择题”候选答案都在那里你只需要打分排序。但连续控制任务里动作是一个连续向量。比如Pendulum-v1环境动作是作用在摆杆上的力矩取值范围从-2到2。从数学上说是无穷多个候选动作你不可能穷举所有力矩值再去算Q值。哪怕你用很密的网格去采样一旦动作维度上升到十几个、几十个比如机械臂关节、四足机器人关节网格大小会爆炸到完全不可行。这个问题在领域里叫“维度灾难”是DQN这类基于贪心选择动作的算法根本绕不过去的坎。1.2 连续控制场景的三个硬约束我做了几个连续控制项目之后总结出这类任务常见的三个特点理解它们你才能明白为什么需要新的算法框架。第一个是动作维度往往不高但每个维度都有物理边界。比如关节力矩不能无限大转向角有最大角度。这意味着策略网络最后要能输出一个有界的实数向量而不是一个概率分布或者一个整数下标。第二个是动作对状态的影响是平滑且连续的。你稍微加大一点力矩摆杆的角度变化也是连续的不会像游戏里按一下“右”角色瞬间瞬移。这种平滑性其实是一种先验我们可以用“给定状态直接输出一个动作”的函数来拟合策略而不需要像随机策略那样对每个动作单独建模分布。第三个是任务往往对动作的“精细度”有要求。不是选个大方向就完事而是需要精确控制力度让系统稳定下来。这些任务刚好是DQN的“感知-离散决策”范式不擅长的地方。1.3 DDPG名字拆解Deep、Deterministic、Policy GradientDDPG这篇论文是2016年由Lillicrap等人发表的它的思想来自更早的Deterministic Policy Gradient算法Silver等2014再融合了DQN里经验回放和目标网络这两个稳定训练的关键技巧。名字里三个词信息量很大Deep用深度神经网络来拟合策略函数和价值函数也就是文章后面要写的Actor网络和Critic网络。Deterministic策略是确定性的。给定状态sActor网络直接输出一个确定动作a μ(s)不像PPO那样输出动作分布然后采样。这个特性让它天然适合连续控制。Policy Gradient它属于策略梯度家族直接对策略做梯度上升来优化目标函数而不是像DQN那样先把Q值学准再用Q值推导策略。下面这张表是我的一个快速对比方便你把DDPG放进整个强化学习版图里看算法动作空间策略类型训练方式典型场景DQN离散隐式策略argmax QOff-policyAtari、棋类、推荐DDPG连续确定性策略 μ(s)Off-policy机器人控制、自动驾驶PPO连续/离散随机策略 π(as)On-policySAC连续随机策略 熵正则Off-policy复杂连续控制1.4 为什么Off-policy对连续控制特别重要DDPG是off-policy算法意味着它可以用一个“行为策略”训练时加了探索噪声的策略去收集数据同时更新另一个“目标策略”当前正在优化的确定性策略。这带来的好处是样本效率高一条经验可以被反复用来学习这对真实机器人场景特别有意义——真机采样一条轨迹的成本太高了你恨不得把每条数据榨干。这一点在后面的代码实现里会有非常直观的体现。2. 拆解DDPGActor、Critic、目标网络和经验回放这四块基石DDPG的主体可以看成四个互相咬合的组件少了任何一个或者没调好任何一个训练结果都会差很多。我建议你在写代码之前先在脑子里把这四块的关系理清。2.1 Actor网络负责“做动作”的策略网络Actor的目标是学习一个确定性映射状态 s → 动作 a。输入是状态向量输出是一个连续动作向量。Pendulum-v1的输入是3维摆角余弦、正弦、角速度输出是1维力矩标量。输出层一般用tanh激活函数把输出压到[-1, 1]再乘上动作边界就能得到一个物理上可执行的动作值。为什么输出层用tanh而不是ReLU因为连续控制动作几乎都有界tanh自带[-1, 1]的边界约束且梯度平滑非常适合作为动作输出的最后一层。2.2 Critic网络负责“估价值”的价值网络Critic的目标是评估“在状态s下执行动作a之后能获得多少期望累积奖励”也就是学习Q(s, a)。关键点来了Critic的输入不仅仅是状态s还要把动作a一起拼进来。这是因为在连续控制里动作对未来的影响极大——同样的状态给一个正确力矩和给一个错误力矩未来的奖励天差地别。Q值必须依赖动作这个维度。训练BehandlungCritic输出一个标量Q值与目标网络算出来的目标Q值做均方误差回归。它本质上是在学一个回归任务。2.3 目标网络与软更新稳定训练的关键机制DDPG里有四个网络Actor、Critic以及各自对应的目标网络Target Network。目标网络初始时和主网络一样但不会每一步都跟着主网络的梯度走而是用“软更新”缓慢靠近主网络[ \theta_{target} \leftarrow \tau \theta (1 - \tau)\theta_{target} ]其中τ是软更新系数典型取值0.005。也就是说每次更新时目标网络只往主网络的方向挪5%左右。为什么需要这套机制因为在强化学习里目标Q值是自己算出来的[ y r \gamma Q_{target}(s, \mu_{target}(s)) ]如果用的是同一个网络既算目标又算当前预测目标值会随着网络更新不断跳动相当于让你去追一个一直移动的靶子训练特别容易发散。目标网络的作用是让这个“靶子”定得更慢一点给主网络一个相对稳定的学习信号。软更新的好处是比硬拷贝更平滑训练过程基本感受不到跳变。2.4 经验回放把“连续经历”变成“独立样本”经验回放是DQN时代就有的经典技巧。它用一个固定容量的缓冲区把智能体经历过的(s, a, r, s, done)存下来训练时随机采样一小批样本。之所以要随机采样而不是按顺序使用是因为强化学习的数据是强时序相关的这一帧的状态和下一帧的状态高度相似。如果按顺序训练网络会反复看到非常相似的数据导致梯度方向剧烈抖动甚至灾难性遗忘。随机采样相当于打破了这种时序相关性。DDPG用的是标准ReplayBuffer容量通常几万到几十万条经验。在Pendulum这种简单环境里我习惯设10万条。有一点需要注意神经网络使用mini-batch训练时数据分布要尽量稳定而强化学习环境的分布是动态变化的经验回放相当于给学习过程加了“减速带”让分布变化不至于太猛。3. 基于Gymnasium选个练手环境Pendulum与Python工程准备3.1 为什么是Pendulum-v1Pendulum-v1倒立摆是连续控制任务里最经典的入门环境也是DDPG论文作者们测试用的环境之一。任务是一根摆杆竖在固定轴上初始时随机倒向某个角度智能体需要对摆杆施加力矩尽快把摆杆立起来并保持稳定。这很像真实物理世界里的平衡控制问题比如火箭垂直着陆前的姿态稳定。为什么选它有四个理由状态维度低3维网络结构不需复杂观察数据也方便。动作维度只有1维力矩可以在纸上手推公式验证梯度对不对。训练速度快普通CPU上几百回合就能看到明显效果不用一开始就上GPU。它是Gymnasium自带环境不需要额外安装MuJoCo这类带版权限制的物理引擎。如果你以后想挑战更高难度的环境比如HalfCheetah、Hopper这类MuJoCo环境DDPG的结构完全不需要改变只需要改环境名和网络输入输出维度即可迁移成本很低。3.2 Python环境准备与依赖安装这次实战基于PyTorch和Gymnasium我的版本组合是这样用虚拟环境管理避免污染系统Pythonpython -m venv ddpg_env source ddpg_env/bin/activate # Windows下用 ddpg_env\Scripts\activate pip install torch gymnasium numpy matplotlib关于PyTorch安装如果你的机器没有CUDA GPU装CPU版本完全能跑Pendulum这个实验就是来回多等几分钟的差别。如果GPU可用就去PyTorch官网选对应的CUDA版本命令装。Gymnasium是新版Gym的继承者API上主要区别是env.step()返回5个值state, reward, terminated, truncated, info用terminated or truncated来判断一回合是否结束。这里有一个身边的常见坑要先提一下老项目里的gym包和gymnasium的环境名有时写法不同比如旧版是gym.make(Pendulum-v0)新版是gymnasium.make(Pendulum-v1)。如果你习惯直接抄旧代码很容易在环境创建这一步就报错推荐直接用Gymnasium。3.3 先跑通环境再谈训练任何强化学习项目我都建议先一行代码不写算法先直接把环境跑起来看看状态空间、动作空间、每个回合的长度、奖励长什么样。import gymnasium as gym env gym.make(Pendulum-v1, g9.81) print(状态空间:, env.observation_space) print(动作空间:, env.action_space) print(奖励范围:, env.reward_range) state, _ env.reset(seed0) print(初始状态:, state) # 随机策略跑一个回合 total_reward 0 done False while not done: action env.action_space.sample() # 随机动作 next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated total_reward reward state next_state print(随机策略一个回合的总奖励:, round(total_reward, 2))运行之后你会看到状态空间是Box(-inf, inf, (3,))动作空间是Box(-2.0, 2.0, (1,))。状态的三维分别是cos(theta)、sin(theta)、角速度theta_dot。为什么用cos和sin而不直接用角度theta因为直接用角度会面临角度环绕问题359度和1度只差2度但如果用数值表示就是358的gapcos和sin能把角度映射到连续光滑的二维圆环空间这是倒立摆类环境的标准做法。3.4 奖励函数的含义Pendulum的每一步奖励定义为[ r -(\theta^2 0.1 \cdot \dot{\theta}^2 0.001 \cdot a^2) ]这个公式其实是一个惩罚项摆杆越偏离竖直方向θ越大惩罚越大摆动速度越快惩罚越大用力越猛惩罚也越大。所以智能体的目标不是单纯“用最大力气把摆甩上去”而是用最小代价稳定在竖直方向。你要理解这个奖励结构后面看训练曲线时才有感觉当你不加探索噪声地运行学好的策略时总奖励会稳定在0附近且每个时刻的数值应当非常接近0而不是一个很大的负数。4. 从零写代码Actor-Critic网络、缓冲区与训练主循环这一节是核心工程落地部分。我会按模块拆开写每个模块都给出完整可运行的代码并在代码注释里说明设计意图。4.1 Actor网络实现import 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, hidden_dim256): super().__init__() self.max_action max_action self.fc1 nn.Linear(state_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.mu nn.Linear(hidden_dim, action_dim) def forward(self, state): x F.relu(self.fc1(state)) x F.relu(self.fc2(x)) return torch.tanh(self.mu(x)) * self.max_actionActor输出层是tanh把网络输出压到[-1, 1]再乘以动作边界。这里我建议不要用sigmoid因为sigmoid输出是[0, 1]无法表达负向力矩会让Pendulum这类需要正负双向控制的任务卡死。网络宽度256是我在Pendulum上试过比较中庸的配置太大容易过拟合前期噪声数据太小学不出复杂状态到动作的映射。4.2 Critic网络实现class Critic(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim256): super().__init__() self.fc1 nn.Linear(state_dim action_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.q nn.Linear(hidden_dim, 1) def forward(self, state, action): x torch.cat([state, action], dim-1) # 状态和动作拼在一起 x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.q(x)Critic的输入维度是state_dim action_dim把状态和动作拼接后过全连接网络。这里有一个工程上常见的变体把动作在第二层再拼入而不是第一层这是TD3论文里推荐的“Concatenated actions”设计对复杂任务有提升。但Pendulum这种低维环境第一层拼接已经足够代码也更简洁。4.3 经验回放缓冲区实现import random from collections import deque class ReplayBuffer: def __init__(self, capacity100000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, float(done))) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones zip(*batch) return ( torch.FloatTensor(states), torch.FloatTensor(actions), torch.FloatTensor(rewards).unsqueeze(1), torch.FloatTensor(next_states), torch.FloatTensor(dones).unsqueeze(1), ) def __len__(self): return len(self.buffer)用deque(maxlencapacity)实现的好处是存满之后新数据进来会自动覆盖最旧的数据不需要手动管理索引。我把done存成float是因为后续计算目标Q值时要乘(1 - done)把终止状态后面的“未来奖励”清零。注意这里采样的随机性很重要随机种子只影响环境不影响采样过程。4.4 DDPG Agent的更新流程接下来是Agent类它把Actor、Critic、目标网络、优化器、更新逻辑全部串起来。我建议你在理解这段代码时反复对照前面的公式看。class DDPGAgent: def __init__(self, state_dim, action_dim, max_action, actor_lr1e-4, critic_lr1e-3, gamma0.99, tau0.005): self.actor Actor(state_dim, action_dim, max_action) self.critic Critic(state_dim, action_dim) self.actor_target Actor(state_dim, action_dim, max_action) self.critic_target Critic(state_dim, action_dim) # 目标网络初始化为与主网络相同的权重 self.actor_target.load_state_dict(self.actor.state_dict()) self.critic_target.load_state_dict(self.critic.state_dict()) self.actor_optimizer torch.optim.Adam(self.actor.parameters(), lractor_lr) self.critic_optimizer torch.optim.Adam(self.critic.parameters(), lrcritic_lr) self.gamma gamma self.tau tau def select_action(self, state, noise_std0.0): state torch.FloatTensor(state).unsqueeze(0) action self.actor(state).detach().numpy().flatten() if noise_std 0: noise np.random.normal(0, noise_std, sizeaction.shape) action action noise return np.clip(action, -self.max_action, self.max_action) def update(self, replay_buffer, batch_size256): if len(replay_buffer) batch_size: return states, actions, rewards, next_states, dones replay_buffer.sample(batch_size) # 1. 计算目标Q值 with torch.no_grad(): next_actions self.actor_target(next_states) target_q self.critic_target(next_states, next_actions) y rewards self.gamma * (1 - dones) * target_q # 2. 更新Critic current_q self.critic(states, actions) critic_loss F.mse_loss(current_q, y) self.critic_optimizer.zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_(self.critic.parameters(), 1.0) self.critic_optimizer.step() # 3. 更新Actor actor_loss -self.critic(states, self.actor(states)).mean() self.actor_optimizer.zero_grad() actor_loss.backward() torch.nn.utils.clip_grad_norm_(self.actor.parameters(), 1.0) self.actor_optimizer.step() # 4. 软更新目标网络 for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()): target_param.data.copy_(self.tau * param.data (1 - self.tau) * target_param.data) for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()): target_param.data.copy_(self.tau * param.data (1 - self.tau) * target_param.data)这里重点解释几个容易被新手忽略的细节。with torch.no_grad()计算目标Q值时一定不要开梯度这是推理过程而不是训练过程开了梯度不仅浪费显存还可能不小心把梯度传播到目标网络里。Actor的损失函数是-Q(s, μ(s))的均值。原理是对于给定的状态我们想让Actor输出的动作在Critic眼里得分最高而梯度只能通过“Actor输出动作”这条路反向传播到Actor网络。这个过程相当于Actor在“迎合”Critic的打分标准。梯度裁剪我一开始写DDPG时没有加梯度裁剪训练经常到几百个回合后Q值突然炸到几千万。加一行torch.nn.utils.clip_grad_norm_(..., 1.0)本质上是在防止梯度爆炸时参数一步飞得太远。这不是DDPG论文里强调的内容但工程上几乎必备建议保留。4.5 完整的训练主循环import numpy as np env gym.make(Pendulum-v1, g9.81) state_dim env.observation_space.shape[0] action_dim env.action_space.shape[0] max_action float(env.action_space.high[0]) agent DDPGAgent(state_dim, action_dim, max_action) buffer ReplayBuffer(capacity100000) MAX_EPISODES 300 MAX_STEPS 200 BATCH_SIZE 256 NOISE_STD 0.1 TRAIN_AFTER 1000 # 经验池超过1000条才开训 episode_returns [] for episode in range(1, MAX_EPISODES 1): state, _ env.reset() episode_return 0 for step in range(MAX_STEPS): action agent.select_action(state, noise_stdNOISE_STD) next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated buffer.push(state, action, reward, next_state, done) state next_state episode_return reward if len(buffer) TRAIN_AFTER: agent.update(buffer, BATCH_SIZE) if done: break episode_returns.append(episode_return) print(fEpisode {episode:3d} | Return {episode_return:7.2f}) torch.save(agent.actor.state_dict(), actor_pendulum.pt)这个训练循环是DDPG最朴素也最标准的写法每次环境step后做一次网络更新。Pendulum每回合最多200步也就是每个回合最多更新200次网络。在Pendulum上300个回合足够看到效果CPU训练大概需要几分钟到十几分钟。4.6 一份能直接跑通的超参数配置不同任务对超参数的敏感度差别很大下面这组是我在Pendulum-v1上验证过、可以直接复现出不错效果的配置后面列出的每一项都有它存在的理由。超参数取值设计理由actor学习率1e-4Actor依赖Critic提供的梯度信号学习率太大容易震荡critic学习率1e-3Critic本质是回归任务可以比Actor学得快一些gamma0.99连续控制任务通常需要长期规划折扣因子要大tau0.005软更新系数太大目标网络不稳太小学习太慢缓冲区容量100000Pendulum状态3维、动作1维10万条足够支撑训练batch_size256对低维状态来适较大的批次能稳定梯度噪声std0.1训练期探索强度Pendulum动作范围[-2,2]0.1适度每回合最大步数200环境本身的默认设置训练起始阈值1000经验池不够时训练梯度方差太大5. 实测调参从“左右摇摆”到“立稳”的全程与避坑5.1 训练曲线怎么看你会发现前几十个回合的reward曲线几乎是一条水平线数值在-1500到-1000左右徘徊。这非常正常因为此时经验池里的样本大多来自带噪声的随机探索Critic还没形成有效的价值预估能力Actor更不可能学到什么。大概到80到150个回合曲线开始出现“抬头”迹象reward从-1000逐渐爬到-500以内。这个阶段说明Critic对动作的评估开始有区分度Actor也能接收到越来越有效的梯度。200回合之后通常就能看到一个相对稳定的结果每回合总reward在-300到0之间波动如果你用训练好的策略不加噪声测试总reward应该在-150左右甚至更高。Pendulum这个任务的难度决定了它不会像游戏分数一样刷出几千的高分因为奖励天然是负值接近0就代表策略已经很好了。建议你每训练5到10个回合做一次确定性策略的评估即select_action时噪声设为0把评估reward单独画一条曲线。这样做可以排除探索噪声对reward的干扰更真实地反映策略质量。import matplotlib.pyplot as plt plt.plot(episode_returns, labeltrain with noise) plt.xlabel(Episode) plt.ylabel(Episode Return) plt.legend() plt.show()5.2 探索噪声怎么加OU噪声 vs 高斯噪声DDPG原论文用的是Ornstein-Uhlenbeck噪声OU噪声一种时间上相关的噪声它会让连续动作更平滑不会每步都跳变当时的设计灵感来自物理系统里的随机过程。很多教程会把OU噪声实现得比较复杂。但我个人在实践中的经验是用简单的高斯噪声(\mathcal{N}(0, \sigma))就足够了。因为DDPG的Actor是确定性策略如果没有噪声训练数据永远只有一条确定性轨迹根本覆盖不了完整的状态空间。高斯噪声虽然每步独立采样但由于动作本身经过np.clip限制在[-2, 2]内实际探索的轨迹依然足够多样。TD3和SAC这些后来更稳定的算法用的也都是高斯噪声或类似的高斯平滑策略。噪声幅度需要随时间衰减吗我的做法是固定0.1不衰减因为在Pendulum这种短回合任务里训练300回合很快就结束了衰减带来的收益不明显。但在更长、更复杂的任务里建议前1/3训练阶段用较大噪声后期逐步降低噪声幅度让策略从“探索问题”切换到“利用最优解”。5.3 常见翻车现场与完整排查链路如果你照上面的代码跑大概率能顺利出结果。但如果你改动了结构或换了环境可能会碰到下面这几种情况。我把比较常见的排查思路整理成一条链路。现象一reward曲线一直是平的或者越跑越差。优先检查噪声。没有噪声或噪声过小Actor学到的策略可能从一开始就陷入一个局部坏动作之后所有经验都在这个坏动作附近采样策略永远跳不出来。解决办法是把NOISE_STD从0.1提高到0.3或0.5试试看曲线是否有波动。其次检查经验池。如果TRAIN_AFTER设置太小比如只存了几十条就开始训练抽样梯度方差极大网络基本学不到东西。我见过有人把起始训练阈值设成64训练曲线抖到天上去。建议至少在1000条以上。现象二训练中途Q值突然爆炸reward出现几千万的尖峰。这是DDPG最经典的“Q值过高估计发散”问题。常见原因是Critic的学习率太大或者目标网络更新太快tau太大导致自举误差被一步步放大。还有一个隐形原因是reward本身尺度太大比如你换了一个奖励值到几千的环境必须先把reward做归一化或者调低critic学习率。排查顺序先看是不是没有梯度裁剪加上clip_grad_norm_几乎能解决一半的发散问题再看tau是不是设到了0.01以上如果是调回0.005最后看critic学习率如果大于3e-3果断调低。现象三损失一直在降但reward不见涨。这种情况通常是Actor和Critic“一起走了歪路”Critic给了一个错误的虚高Q值Actor刚好顺应了这个错误方向两边互相配合但谁都没有学到真正的物理规律。归因往往是采样数据分布覆盖太窄策略一直在某个局部区域打转。解决办法是加大噪声强度、提高经验池容量、或者每步多做几次更新来利用已有数据。5.4 日志记录一项性价比极高的习惯我建议你从第一次跑DDPG起就养成记录日志的习惯。至少要记录五项回合序号、当前回合总reward、评估reward、actor loss、critic loss。不要只在终端打印直接把数据追加写到CSV文件里画图时才能看到更细粒度的问题。5.5 复现性和随机种子强化学习实验对随机种子高度敏感。同一套代码换一个随机种子可能从“训练成功”变成“完全学不动”。在写实验报告或对比算法时一定要在多个种子至少3个上跑多次取平均不要只跑一次就下结论。环境种子和网络权重初始化种子都要固定import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) # 如果使用CUDA # torch.cuda.manual_seed_all(seed)6. 训练之外DDPG的脾气、工程心得与进阶路线6.1 DDPG的四个“隐藏脾气”我把长期实践中总结出来的、论文不会主动告诉你的经验放在这里你如果以后要拿DDPG做真实项目迟早会碰见。第一个脾气是对奖励尺度极其敏感。DDPG的Actor依赖Critic的梯度Critic的梯度受reward绝对尺度影响很大。reward在0附近波动的环境和reward动辄上千的环境即使理论上都能收敛实际训练速度天差地别。我通常会把reward做归一化或clip到合理范围。第二个脾气是初始化非常关键。Actor初始时如果输出一个很大的错误动作前期经验池里全是负面样本Critic会快速学会“所有动作都不好”之后Actor无论怎么更新都很难翻盘。所以Actor最后几层的权重初始化要小一些或者把输出层的bias初始化为0。PyTorch默认初始化通常问题不大但如果遇到训练失败可以从这里查。第三个脾气是对探索噪声的容忍度比较窄。噪声太小学不到新东西噪声太大行为过于随机样本质量差Critic学到的是“动作和奖励基本无关”的错误结论。你需要根据动作空间的范围动态调整噪声。经验法则噪声标准差设为动作范围的5%左右再根据训练过程微调。第四个脾气是目标网络更新要“慢且稳”。不要为了追求“让目标更准确”而把tau调大实验显示0.005到0.01之间是安全区间超过0.05基本必炸。6.2 从DDPG到TD3、SAC你会踩的坑都差不多DDPG虽然是连续控制的经典算法但它在很多任务上确实不够稳定尤其是面对复杂高维环境。后来学术界和工业界的新算法基本是对DDPG缺陷的针对性修补理解这些修补能帮你建立更强的调参能力。**TD3双延迟深度确定性策略梯度**主要解决DDPG的Q值过高估计问题。它做了三件事用两个Critic取最小值来压制过高估计延迟更新ActorActor每隔几步才更新一次给目标动作加上小的高斯噪声做平滑。如果你跑DDPG在某个任务上总是发散上下抖动大换成TD3大概率直接就好很多。**SAC软演员-评论家**用随机策略加熵正则项替代确定性策略在探索能力和训练稳定性上更强。SAC在很多连续控制benchmark上是当前主流选择实现难度和DDPG接近但超参数敏感度更低。如果你打算长期做连续控制任务建议DDPG入门后下一个就学SAC。PPO则走了另一条路线它用随机策略做on-policy更新超参数相对鲁棒但对样本效率不如off-policy算法。如果你的任务里每个回合收集数据成本不高比如仿真环境PPO也是个很稳妥的选择。6.3 下一步可以怎么扩展学完DDPG之后我建议你做三件延伸实践。第一把代码从Pendulum换到MuJoCo的HalfCheetah-v4或Walker2d看看策略维度从1维变成6维后网络结构、噪声幅度、学习率这些配置是否还work。你会对DDPG的状态空间覆盖问题有更深的体会。第二写一个简单的“定时更新”版本把更新频率从每步更新改成每N步更新N次看看样本效率和训练稳定性的变化。这个设计在真实机器人场景里非常常见因为控制频率可能远高于网络更新能承受的频率。第三在DDPG基础上手动实现TD3的“双Critic取小”改进只需要改动十几行代码你会直观感受到算法稳定性带来的训练曲线变化。我个人是这么走过来的先跑通DDPG兴奋地看它学会立摆然后换HalfCheetah发现怎么都学不好开始怀疑人生接着调试无数次后终于明白DDPG对探索噪声和奖励尺度有多敏感最后接触TD3和SAC才理解DDPG作为“第一代连续控制算法”的奠基意义。我现在做连续控制项目时很少直接裸用DDPG但每当遇到训练不收敛、Q值发散、探索不足这些老熟人时脑子里第一个浮现的仍然是DDPG时期总结出的这套排查思路——理解了它你就等于掌握了连续深度强化学习的通用语言。
返回列表