深度强化学习实战:基于PyTorch 2.0与Gymnasium实现DDPG算法控制倒立摆
深度强化学习实战:基于PyTorch 2.0与Gymnasium实现DDPG算法控制倒立摆
深度强化学习(Deep Reinforcement Learning, DRL)作为人工智能领域的前沿技术,正在重塑我们解决复杂决策问题的方式。想象一下,一个智能体能够通过不断试错学会骑自行车、玩电子游戏甚至控制工业机器人——这正是深度强化学习赋予机器的能力。本文将带您从零开始构建一个完整的DDPG(Deep Deterministic Policy Gradient)算法实现,使用PyTorch 2.0和Gymnasium环境来解决经典的倒立摆控制问题。
1. 环境配置与工具链准备
在开始我们的深度强化学习之旅前,需要搭建一个高效的开发环境。PyTorch 2.0带来了显著的性能提升和新特性,如编译优化和更快的GPU运算,而Gymnasium作为OpenAI Gym的进化版,提供了更多样化的环境和更稳定的API。
1.1 安装核心依赖
首先确保您的Python版本在3.8以上,然后安装必要的包:
pip install torch==2.0.0 gymnasium==0.28.1 numpy matplotlib对于需要GPU加速的用户,请安装对应CUDA版本的PyTorch。可以通过以下命令检查PyTorch是否正确识别了您的GPU:
import torch print(f"PyTorch版本: {torch.__version__}") print(f"CUDA可用: {torch.cuda.is_available()}") print(f"GPU数量: {torch.cuda.device_count()}")1.2 倒立摆环境解析
Gymnasium的Pendulum-v1环境是一个经典的连续控制问题。在这个环境中:
- 状态空间:3维向量[cosθ, sinθ, θ̇],其中θ是摆杆与垂直方向的夹角
- 动作空间:1维连续值[-2.0, 2.0],表示施加在摆杆上的扭矩
- 奖励函数:-(θ² + 0.1θ̇² + 0.001τ²),目标是保持摆杆直立且消耗最小能量
让我们先直观了解这个环境:
import gymnasium as gym env = gym.make("Pendulum-v1", render_mode="human") observation, info = env.reset() for _ in range(100): action = env.action_space.sample() # 随机动作 observation, reward, terminated, truncated, info = env.step(action) if terminated or truncated: observation, info = env.reset() env.close()1.3 项目结构规划
良好的代码组织能显著提高开发效率。建议采用如下目录结构:
ddpg_pendulum/ ├── models/ # 神经网络定义 │ ├── actor.py │ └── critic.py ├── utils/ # 辅助工具 │ ├── replay_buffer.py │ └── noise.py ├── config.py # 超参数配置 ├── train.py # 训练脚本 └── evaluate.py # 评估脚本2. DDPG算法核心实现
DDPG作为解决连续控制问题的经典算法,结合了DQN的思想和策略梯度方法。其核心架构包含四个神经网络:Actor网络、Critic网络以及它们对应的目标网络。
2.1 Actor-Critic网络设计
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, hidden_size=256): super(Actor, self).__init__() self.fc1 = nn.Linear(state_dim, hidden_size) self.fc2 = nn.Linear(hidden_size, hidden_size) self.fc3 = nn.Linear(hidden_size, action_dim) # 初始化权重 nn.init.xavier_uniform_(self.fc1.weight) nn.init.xavier_uniform_(self.fc2.weight) nn.init.xavier_uniform_(self.fc3.weight) def forward(self, state): x = F.relu(self.fc1(state)) x = F.relu(self.fc2(x)) # 使用tanh将输出限制在[-1,1]范围内,再根据环境缩放 return torch.tanh(self.fc3(x))Critic网络评估状态-动作对的Q值,采用双输入结构:
class Critic(nn.Module): def __init__(self, state_dim, action_dim, hidden_size=256): super(Critic, self).__init__() self.fc1 = nn.Linear(state_dim + action_dim, hidden_size) self.fc2 = nn.Linear(hidden_size, hidden_size) self.fc3 = nn.Linear(hidden_size, 1) # 初始化权重 nn.init.xavier_uniform_(self.fc1.weight) nn.init.xavier_uniform_(self.fc2.weight) nn.init.xavier_uniform_(self.fc3.weight) 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.fc3(x)2.2 经验回放缓冲区
经验回放(Experience Replay)是稳定训练的关键组件,它通过存储和随机采样转移样本打破数据相关性:
import numpy as np import random from collections import deque class ReplayBuffer: def __init__(self, capacity): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): state, action, reward, next_state, done = zip(*random.sample(self.buffer, batch_size)) return ( np.array(state, dtype=np.float32), np.array(action, dtype=np.float32), np.array(reward, dtype=np.float32), np.array(next_state, dtype=np.float32), np.array(done, dtype=np.float32) ) def __len__(self): return len(self.buffer)2.3 噪声生成器
为了鼓励探索,我们需要在Actor输出的动作上添加噪声。Ornstein-Uhlenbeck过程噪声特别适合连续控制问题:
import numpy as np class OUNoise: def __init__(self, action_dim, mu=0, theta=0.15, sigma=0.2): self.action_dim = action_dim self.mu = mu self.theta = theta self.sigma = sigma self.state = np.ones(self.action_dim) * self.mu self.reset() def reset(self): self.state = np.ones(self.action_dim) * self.mu def sample(self): dx = self.theta * (self.mu - self.state) dx += self.sigma * np.random.randn(self.action_dim) self.state += dx return self.state3. DDPG训练流程实现
有了上述组件,我们现在可以组装完整的DDPG算法。以下是训练循环的核心代码:
3.1 算法初始化
import copy import torch.optim as optim class DDPG: def __init__(self, state_dim, action_dim): # 初始化网络 self.actor = Actor(state_dim, action_dim).to(device) self.actor_target = copy.deepcopy(self.actor) self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=1e-4) self.critic = Critic(state_dim, action_dim).to(device) self.critic_target = copy.deepcopy(self.critic) self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=1e-3) # 初始化回放缓冲区 self.replay_buffer = ReplayBuffer(capacity=100000) # 初始化噪声过程 self.noise = OUNoise(action_dim) # 超参数 self.gamma = 0.99 # 折扣因子 self.tau = 0.005 # 目标网络软更新系数 self.batch_size = 128 # 批大小3.2 网络更新逻辑
DDPG的核心在于如何更新Actor和Critic网络:
def update(self): if len(self.replay_buffer) < self.batch_size: return # 从回放缓冲区采样 state, action, reward, next_state, done = self.replay_buffer.sample(self.batch_size) state = torch.FloatTensor(state).to(device) action = torch.FloatTensor(action).to(device) reward = torch.FloatTensor(reward).unsqueeze(1).to(device) next_state = torch.FloatTensor(next_state).to(device) done = torch.FloatTensor(done).unsqueeze(1).to(device) # Critic损失计算 with torch.no_grad(): next_action = self.actor_target(next_state) target_Q = self.critic_target(next_state, next_action) target_Q = reward + (1 - done) * self.gamma * target_Q current_Q = self.critic(state, action) critic_loss = F.mse_loss(current_Q, target_Q) # Critic优化 self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step() # Actor优化 actor_loss = -self.critic(state, self.actor(state)).mean() self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step() # 目标网络软更新 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) 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)3.3 训练主循环
完整的训练流程包含探索、经验存储和学习三个阶段:
def train(self, env, episodes=1000): rewards = [] for episode in range(episodes): state, _ = env.reset() self.noise.reset() episode_reward = 0 done = False while not done: # 选择动作并添加探索噪声 action = self.actor(torch.FloatTensor(state).unsqueeze(0).to(device)) action = action.detach().cpu().numpy()[0] + self.noise.sample() action = np.clip(action, -1, 1) # 根据环境动作范围裁剪 # 执行动作 next_state, reward, terminated, truncated, _ = env.step(action * 2) # 缩放至[-2,2] done = terminated or truncated # 存储转移 self.replay_buffer.push(state, action, reward, next_state, done) # 更新网络 self.update() state = next_state episode_reward += reward rewards.append(episode_reward) print(f"Episode {episode+1}/{episodes}, Reward: {episode_reward:.2f}") return rewards4. 训练优化与调试技巧
深度强化学习的训练过程往往不稳定,需要精心调整超参数和采用适当的优化策略。
4.1 关键超参数设置
以下表格总结了DDPG算法中的关键超参数及其典型取值范围:
| 参数 | 描述 | 典型值 | 调整建议 |
|---|---|---|---|
| γ (gamma) | 折扣因子 | 0.95-0.99 | 越高表示越重视长期回报 |
| τ (tau) | 目标网络更新系数 | 0.001-0.01 | 越小目标网络更新越平滑 |
| actor_lr | Actor学习率 | 1e-5-1e-4 | 通常比Critic学习率小 |
| critic_lr | Critic学习率 | 1e-4-1e-3 | 需要稳定训练 |
| batch_size | 批大小 | 64-256 | 取决于可用内存 |
| buffer_size | 回放缓冲区大小 | 1e5-1e6 | 越大样本多样性越好 |
| OU_θ | OU噪声参数θ | 0.1-0.2 | 控制噪声回归速度 |
| OU_σ | OU噪声参数σ | 0.1-0.3 | 控制噪声波动幅度 |
4.2 训练监控与可视化
实时监控训练过程有助于及时发现问题。我们可以使用Matplotlib绘制奖励曲线:
import matplotlib.pyplot as plt def plot_rewards(rewards, window=100): """绘制奖励曲线并计算滑动平均""" plt.figure(figsize=(12, 6)) # 原始奖励 plt.subplot(1, 2, 1) plt.plot(rewards, alpha=0.3, label='Raw') plt.xlabel('Episode') plt.ylabel('Reward') plt.title('Training Rewards') # 滑动平均 plt.subplot(1, 2, 2) moving_avg = np.convolve(rewards, np.ones(window)/window, mode='valid') plt.plot(moving_avg) plt.xlabel('Episode') plt.ylabel(f'Avg Reward (last {window})') plt.title(f'Moving Average (window={window})') plt.tight_layout() plt.show()4.3 常见问题与解决方案
在训练DDPG时,您可能会遇到以下典型问题:
奖励不增长
- 检查探索噪声是否合适,尝试增大OU噪声的σ
- 验证Critic损失是否在下降,如果Critic学习失败,Actor也无法改进
- 确保奖励函数设计合理,能够提供足够的梯度信号
训练不稳定
- 降低学习率,特别是Critic的学习率
- 增加目标网络更新频率(减小τ)
- 增大回放缓冲区大小,确保样本多样性
过拟合
- 在Critic网络中加入Dropout层
- 使用L2正则化
- 增加批归一化层
提示:在PyTorch 2.0中,可以使用
torch.compile()对模型进行优化,通常能获得10-30%的训练加速:self.actor = torch.compile(self.actor) self.critic = torch.compile(self.critic)
5. 结果评估与部署
训练完成后,我们需要评估智能体的性能并将其部署为可用的控制器。
5.1 性能评估指标
除了累计奖励外,还应考虑以下指标:
- 稳定时间:摆杆保持在垂直位置(|θ|<10°)的时间比例
- 能量效率:平均每次动作的扭矩绝对值
- 收敛速度:达到90%最大性能所需的训练步数
def evaluate(env, agent, episodes=10): total_reward = 0 stable_time = 0 total_steps = 0 energy_consumption = 0 for _ in range(episodes): state, _ = env.reset() episode_reward = 0 episode_stable = 0 episode_energy = 0 steps = 0 done = False while not done: action = agent.actor(torch.FloatTensor(state).unsqueeze(0).to(device)) action = action.detach().cpu().numpy()[0] next_state, reward, terminated, truncated, _ = env.step(action * 2) done = terminated or truncated # 计算稳定时间 theta = np.arctan2(state[1], state[0]) # 从状态恢复角度 if abs(theta) < np.pi/18: # 10度以内 episode_stable += 1 # 计算能量消耗 episode_energy += abs(action[0]) state = next_state episode_reward += reward steps += 1 total_reward += episode_reward stable_time += episode_stable / steps energy_consumption += episode_energy / steps total_steps += steps metrics = { 'avg_reward': total_reward / episodes, 'avg_stable_time': stable_time / episodes, 'avg_energy': energy_consumption / episodes, 'avg_steps': total_steps / episodes } return metrics5.2 可视化策略行为
为了直观理解学习到的策略,我们可以渲染智能体的控制过程:
def render_policy(env, agent, episodes=1): for _ in range(episodes): state, _ = env.reset() done = False while not done: action = agent.actor(torch.FloatTensor(state).unsqueeze(0).to(device)) action = action.detach().cpu().numpy()[0] state, _, terminated, truncated, _ = env.step(action * 2) done = terminated or truncated env.render() env.close()5.3 模型保存与加载
训练好的模型可以保存供后续使用:
def save_checkpoint(agent, filename): torch.save({ 'actor_state_dict': agent.actor.state_dict(), 'critic_state_dict': agent.critic.state_dict(), 'actor_target_state_dict': agent.actor_target.state_dict(), 'critic_target_state_dict': agent.critic_target.state_dict(), 'actor_optimizer_state_dict': agent.actor_optimizer.state_dict(), 'critic_optimizer_state_dict': agent.critic_optimizer.state_dict(), }, filename) def load_checkpoint(agent, filename): checkpoint = torch.load(filename) agent.actor.load_state_dict(checkpoint['actor_state_dict']) agent.critic.load_state_dict(checkpoint['critic_state_dict']) agent.actor_target.load_state_dict(checkpoint['actor_target_state_dict']) agent.critic_target.load_state_dict(checkpoint['critic_target_state_dict']) agent.actor_optimizer.load_state_dict(checkpoint['actor_optimizer_state_dict']) agent.critic_optimizer.load_state_dict(checkpoint['critic_optimizer_state_dict'])6. 进阶优化与扩展
基础DDPG实现完成后,我们可以考虑以下进阶优化方向提升算法性能。
6.1 prioritized Experience Replay
优先经验回放通过给重要的转移样本更高采样概率来提升学习效率:
class PrioritizedReplayBuffer: def __init__(self, capacity, alpha=0.6): self.capacity = capacity self.alpha = alpha self.buffer = [] self.pos = 0 self.priorities = np.zeros((capacity,), dtype=np.float32) def push(self, state, action, reward, next_state, done): max_prio = self.priorities.max() if self.buffer else 1.0 if len(self.buffer) < self.capacity: self.buffer.append((state, action, reward, next_state, done)) else: self.buffer[self.pos] = (state, action, reward, next_state, done) self.priorities[self.pos] = max_prio self.pos = (self.pos + 1) % self.capacity def sample(self, batch_size, beta=0.4): if len(self.buffer) == self.capacity: prios = self.priorities else: prios = self.priorities[:self.pos] probs = prios ** self.alpha probs /= probs.sum() indices = np.random.choice(len(self.buffer), batch_size, p=probs) samples = [self.buffer[idx] for idx in indices] total = len(self.buffer) weights = (total * probs[indices]) ** (-beta) weights /= weights.max() weights = np.array(weights, dtype=np.float32) batch = list(zip(*samples)) states = np.array(batch[0], dtype=np.float32) actions = np.array(batch[1], dtype=np.float32) rewards = np.array(batch[2], dtype=np.float32) next_states = np.array(batch[3], dtype=np.float32) dones = np.array(batch[4], dtype=np.float32) return states, actions, rewards, next_states, dones, indices, weights def update_priorities(self, batch_indices, batch_priorities): for idx, prio in zip(batch_indices, batch_priorities): self.priorities[idx] = prio def __len__(self): return len(self.buffer)6.2 使用N-step Returns
N-step returns通过结合多步奖励来平衡偏差和方差:
class NStepReplayBuffer: def __init__(self, capacity, n_step=3, gamma=0.99): self.capacity = capacity self.n_step = n_step self.gamma = gamma self.buffer = deque(maxlen=capacity) self.n_step_buffer = deque(maxlen=n_step) def push(self, state, action, reward, next_state, done): self.n_step_buffer.append((state, action, reward, next_state, done)) if len(self.n_step_buffer) == self.n_step: state, action, _, _, _ = self.n_step_buffer[0] _, _, _, next_state, done = self.n_step_buffer[-1] # 计算n步回报 reward = 0 for i in range(self.n_step): r = self.n_step_buffer[i][2] reward += r * (self.gamma ** i) self.buffer.append((state, action, reward, next_state, done)) if done: self.n_step_buffer.clear() def sample(self, batch_size): state, action, reward, next_state, done = zip(*random.sample(self.buffer, batch_size)) return ( np.array(state, dtype=np.float32), np.array(action, dtype=np.float32), np.array(reward, dtype=np.float32), np.array(next_state, dtype=np.float32), np.array(done, dtype=np.float32) ) def __len__(self): return len(self.buffer)6.3 扩展到其他环境
DDPG算法可以轻松扩展到其他连续控制环境。以下是几个流行的Gymnasium环境及其特点:
| 环境名称 | 状态维度 | 动作维度 | 挑战点 |
|---|---|---|---|
| Pendulum-v1 | 3 | 1 | 简单的基准测试 |
| MountainCarContinuous-v0 | 2 | 1 | 稀疏奖励 |
| BipedalWalker-v3 | 24 | 4 | 高维状态空间 |
| HalfCheetah-v4 | 17 | 6 | 复杂动力学 |
对于更复杂的任务,可以考虑以下改进:
- 分层DDPG:将任务分解为子任务,每个子任务使用独立的DDPG
- 多智能体DDPG:扩展MADDPG解决多智能体协作问题
- 结合模仿学习:使用专家演示数据预训练网络
7. 实际应用案例与展望
深度强化学习在实际工业应用中展现出巨大潜力。以下是一些成功案例:
- 机器人控制:波士顿动力使用DRL优化机器人运动策略
- 能源管理:谷歌DeepMind应用DRL降低数据中心冷却能耗40%
- 金融交易:多家对冲基金使用DRL开发自动化交易策略
- 医疗诊断:DRL用于优化个性化治疗方案的剂量调整
在倒立摆控制基础上,我们可以进一步探索:
- 双倒立摆:更复杂的欠驱动系统
- 基于视觉的控制:使用CNN处理原始像素输入
- 真实物理系统部署:考虑延迟、噪声等现实因素
# 示例:基于视觉的DDPG扩展 class VisualActor(nn.Module): def __init__(self, action_dim): super(VisualActor, self).__init__() self.conv1 = nn.Conv2d(3, 32, kernel_size=8, stride=4) self.conv2 = nn.Conv2d(32, 64, kernel_size=4, stride=2) self.conv3 = nn.Conv2d(64, 64, kernel_size=3, stride=1) self.fc1 = nn.Linear(64*7*7, 512) self.fc2 = nn.Linear(512, action_dim) def forward(self, x): x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)) return torch.tanh(self.fc2(x))深度强化学习仍在快速发展中,未来趋势包括:
- 更高效的样本利用:如隐空间模型、世界模型等方法
- 多任务学习:单一智能体掌握多种技能
- 安全强化学习:确保策略的安全性约束
- 分布式训练:大规模并行化加速学习
通过本教程,您已经掌握了DDPG算法的核心实现和应用技巧。实际项目中,建议从简单环境开始,逐步增加复杂度,并持续监控训练过程。深度强化学习需要耐心和系统化的调试,但当看到智能体最终学会解决复杂任务时,所有的努力都将得到回报。