机器人强化学习 从入门到精通
系统掌握从 Python 基础到 Sim2Real 部署的完整流水线,
用强化学习让机器人在仿真中学会技能,并迁移到真实世界。
章节
练习题
完整 Sim2Real 流水线
学习路线图
建议按照路线图顺序学习。每个阶段结束后完成所有练习,再进入下一阶段。整个课程预计需要 8-12 周(每周 10 小时)。
课程内容概览
阶段一 Python & PyTorch
从 Python 基本语法到 PyTorch 张量运算与自动求导,为后续算法实现打下坚实基础。涵盖 NumPy、面向对象编程、神经网络搭建与训练循环。
阶段二 强化学习理论
系统学习马尔可夫决策过程(MDP)、贝尔曼方程、策略梯度定理、价值函数与优势函数。理解探索与利用的权衡,以及折扣回报的数学本质。
阶段三 核心算法:PPO / SAC / TD3
深入理解并手动实现 Proximal Policy Optimization、Soft Actor-Critic 和 Twin Delayed DDPG。掌握连续动作空间与离散动作空间中的经典方法。
阶段四 MuJoCo & IsaacLab 仿真
学习使用 MuJoCo 物理引擎搭建机器人仿真环境,利用 NVIDIA IsaacLab 进行大规模 GPU 并行训练。掌握 URDF/MJCF 模型导入与 Gymnasium 接口。
阶段五 Sim2Real 迁移
学习域随机化(Domain Randomization)、系统辨识、真实世界部署策略。将仿真中训练好的策略迁移到真实机器人上,完成从仿真到现实的闭环。
前置要求
你需要具备的基础
- 基础数学:高中水平的线性代数(矩阵乘法、转置)、微积分(求导、链式法则)和概率论(条件概率、期望)。不需要很深入,课程中会在用到时复习。
- 编程入门:了解任意一门编程语言的基本概念(变量、循环、函数)。即使你只看过代码但没有系统写过,也可以跟上——第一章会从零开始讲 Python。
- 英文阅读能力:部分库的文档和错误信息是英文的,具备基本英文阅读能力会有帮助。
开始学习前,请安装 Python 3.9+、PyTorch 2.0+ 和 MuJoCo 3.x。推荐使用 Conda 管理环境:
# 创建并激活 conda 环境 conda create -n robot-rl python=3.10 -y conda activate robot-rl # 安装核心依赖 pip install torch numpy gymnasium mujoco pip install matplotlib tensorboard
Python 基础
掌握强化学习所需的 Python 核心语法与编程范式
入门2.1 数据类型与变量
Python 是一种动态类型语言,变量不需要事先声明类型。在强化学习中,你会频繁用到以下数据类型来表示状态、动作、奖励等核心概念。
# === 数值类型 === reward = 1.5 # float: 奖励值通常是浮点数 episode = 100 # int: 回合数是整数 done = True # bool: 回合是否结束 # === 字符串 === env_name = "CartPole-v1" # str: 环境名称 print(f"环境: {env_name}, 回合: {episode}") # f-string 格式化 # === 列表 (List) - 有序可变序列 === rewards = [0.5, 1.0, -0.3, 2.0] # 存储每步的奖励 rewards.append(1.5) # 添加新奖励 total = sum(rewards) # 计算总奖励 # === 字典 (Dict) - 键值对映射 === config = { "learning_rate": 3e-4, # 学习率 "gamma": 0.99, # 折扣因子 "batch_size": 64, # 批大小 "hidden_dim": 256, # 隐藏层维度 } # === 元组 (Tuple) - 有序不可变序列 === state_shape = (4,) # 状态空间维度 action_range = (-1.0, 1.0) # 动作取值范围 # === 类型转换 === steps_str = "1000" steps_int = int(steps_str) # 字符串 → 整数 ratio = float(steps_int) / 500 # 整数 → 浮点数
在强化学习代码中,常见变量名有约定俗成的含义:obs/state(观测/状态)、action(动作)、reward(奖励)、done(回合结束标志)、info(额外信息)。Gymnasium 环境的 step() 方法正是返回这些值。
2.2 控制流
条件判断和循环是构建训练逻辑的基础。例如:判断是否结束回合、循环执行训练步数、提前终止训练等。
# === 条件判断 === reward = -0.5 if reward > 0: print("正奖励,智能体做了好的决策") elif reward == 0: print("零奖励,中性结果") else: print("负奖励,智能体需要改进策略") # === for 循环 - 训练回合 === num_episodes = 5 for ep in range(num_episodes): episode_reward = 0 # 模拟一个回合内的多个步骤 for step in range(100): r = 1.0 # 假设每步奖励为 1 episode_reward += r print(f"回合 {ep}: 总奖励 = {episode_reward}") # === while 循环 + break === step_count = 0 max_steps = 200 while True: step_count += 1 done = step_count >= max_steps if done: print(f"回合在第 {step_count} 步结束") break # === continue - 跳过无效数据 === rewards_list = [1.0, None, 0.5, None, 2.0] valid_rewards = [] for r in rewards_list: if r is None: continue # 跳过空值 valid_rewards.append(r)
2.3 函数定义
函数是代码复用的基本单位。在 RL 项目中,你会把奖励计算、状态预处理、模型更新等逻辑封装成函数。
# === 基本函数 === def compute_return(rewards, gamma=0.99): """计算折扣回报 G_t = r_t + gamma * r_{t+1} + gamma^2 * r_{t+2} + ...""" g = 0 returns = [] for r in reversed(rewards): g = r + gamma * g returns.insert(0, g) return returns rewards = [1.0, 0.0, 1.0, 0.5] G = compute_return(rewards, gamma=0.99) print(f"折扣回报: {G}") # === *args 和 **kwargs === def create_env(env_id, *wrappers, **kwargs): """灵活创建环境,支持任意多个包装器和配置参数""" print(f"创建环境: {env_id}") for w in wrappers: print(f" 应用包装器: {w}") for k, v in kwargs.items(): print(f" 配置: {k} = {v}") create_env("Hopper-v4", "NormalizeObservation", "ClipAction", max_episode_steps=1000, render_mode="human") # === lambda 表达式 === # 常用于简短的奖励变换或回调函数 clip_reward = lambda r: max(-1.0, min(1.0, r)) print(clip_reward(5.0)) # 输出 1.0 print(clip_reward(-3.0)) # 输出 -1.0 # === 列表推导式 === # 快速生成数据,在 RL 中用于批量处理非常常见 squared = [x ** 2 for x in range(10)] positive_rewards = [r for r in rewards if r > 0] # === 生成器 - 节省内存 === def episode_generator(num_episodes): """逐个生成回合数据,无需一次性加载所有数据到内存""" for i in range(num_episodes): # 模拟生成一个回合的奖励序列 ep_rewards = [1.0] * 50 yield i, ep_rewards for ep_id, ep_r in episode_generator(3): print(f"回合 {ep_id}: 奖励步数 = {len(ep_r)}")
2.4 类与面向对象编程
Gymnasium 的所有环境都是类。你将来自己写自定义环境或封装策略网络时,必须理解面向对象编程。
import random class ReplayBuffer: """经验回放缓冲区 - 存储并采样训练数据""" def __init__(self, capacity): self.capacity = capacity # 最大容量 self.buffer = [] # 存储列表 self.position = 0 # 当前写入位置 def push(self, state, action, reward, next_state, done): """存储一条经验""" experience = (state, action, reward, next_state, done) if len(self.buffer) < self.capacity: self.buffer.append(experience) else: self.buffer[self.position] = experience self.position = (self.position + 1) % self.capacity def sample(self, batch_size): """随机采样一批经验""" return random.sample(self.buffer, batch_size) def __len__(self): return len(self.buffer) # === 继承 - 创建特殊版本的缓冲区 === class PrioritizedReplayBuffer(ReplayBuffer): """优先经验回放 - 按 TD 误差优先级采样""" def __init__(self, capacity, alpha=0.6): super().__init__(capacity) # 调用父类初始化 self.alpha = alpha # 优先级指数 self.priorities = [] # 优先级列表 def push(self, state, action, reward, next_state, done, td_error=1.0): super().push(state, action, reward, next_state, done) priority = abs(td_error) ** self.alpha if len(self.priorities) < self.capacity: self.priorities.append(priority) else: self.priorities[self.position - 1] = priority # 使用示例 buf = ReplayBuffer(capacity=10000) buf.push([1.0, 0.5], 0, 1.0, [1.1, 0.4], False) print(f"缓冲区大小: {len(buf)}")
2.5 NumPy 基础
NumPy 是科学计算的核心库。Gymnasium 环境返回的观测和动作通常都是 NumPy 数组。掌握 NumPy 是后续学习 PyTorch 的前提。
Python 的原生列表运算非常慢(逐元素循环)。NumPy 使用 C 语言实现的向量化运算,速度可以快 100 倍以上。在 RL 中,状态向量、动作向量和奖励数组几乎全部使用 NumPy 表示。
import numpy as np # === 创建数组 === state = np.array([0.5, -0.3, 1.2, 0.0]) # 从列表创建 zeros = np.zeros((3, 4)) # 3x4 零矩阵 ones = np.ones(5) # 全 1 向量 random_actions = np.random.randn(10, 2) # 10 个 2 维随机动作 indices = np.arange(0, 100, 10) # [0, 10, 20, ..., 90] # === 索引与切片 === rewards = np.array([1.0, 0.5, -0.3, 2.0, 0.8, -1.0]) print(rewards[0]) # 第一个: 1.0 print(rewards[-1]) # 最后一个: -1.0 print(rewards[1:4]) # 切片: [0.5, -0.3, 2.0] print(rewards[rewards > 0]) # 布尔索引: [1.0, 0.5, 2.0, 0.8] # === 广播 (Broadcasting) === # 标量与数组运算:对每个元素应用 normalized = (rewards - rewards.mean()) / (rewards.std() + 1e-8) print(f"归一化后: {normalized}") # === 矩阵运算 === # 模拟一个简单的线性策略: action = W @ state + b W = np.random.randn(2, 4) # 权重矩阵 (2 个动作, 4 维状态) b = np.zeros(2) # 偏置 action = W @ state + b # 矩阵乘法 + 广播加法 print(f"动作: {action}") # === 常用统计 === print(f"平均奖励: {rewards.mean()}") print(f"最大奖励: {rewards.max()}") print(f"标准差: {rewards.std()}") print(f"形状: {random_actions.shape}") # (10, 2)
2.6 模块导入模式
强化学习项目中,你会反复看到以下导入语句。现在记住这些模式,后面的章节会更顺畅:
import numpy as np # 数值计算 import torch # PyTorch 深度学习框架 import torch.nn as nn # 神经网络模块 import torch.optim as optim # 优化器 from torch.distributions import Normal # 概率分布(用于随机策略) import gymnasium as gym # RL 环境接口 from collections import deque # 高效的双端队列 import matplotlib.pyplot as plt # 绘图
练习
给定一个奖励序列 rewards = [1, 0, 1, 0, 1] 和折扣因子 gamma = 0.9,编写函数计算每个时间步的折扣回报。
提示:折扣回报公式为 Gt = rt + γ rt+1 + γ2 rt+2 + ... 从后往前计算最高效。
def compute_discounted_returns(rewards, gamma): """ 从后向前计算折扣回报。 G_t = r_t + gamma * G_{t+1} """ returns = [] g = 0 for r in reversed(rewards): g = r + gamma * g returns.insert(0, g) return returns rewards = [1, 0, 1, 0, 1] gamma = 0.9 result = compute_discounted_returns(rewards, gamma) print(f"折扣回报: {[round(x, 4) for x in result]}") # 输出: [2.4481, 1.6090, 1.81, 0.9, 1] # 解释: G_4=1, G_3=0+0.9*1=0.9, G_2=1+0.9*0.9=1.81, # G_1=0+0.9*1.81=1.629, G_0=1+0.9*1.629=2.4661
实现一个 SimpleBuffer 类,要求:
__init__(self, max_size):初始化缓冲区add(self, experience):添加经验,满了之后覆盖最旧的sample(self, n):随机采样 n 条经验,如果缓冲区不够大则抛出异常__len__(self):返回当前数据量
import random class SimpleBuffer: def __init__(self, max_size): self.max_size = max_size self.data = [] self.index = 0 # 环形写入指针 def add(self, experience): if len(self.data) < self.max_size: self.data.append(experience) else: self.data[self.index] = experience self.index = (self.index + 1) % self.max_size def sample(self, n): if n > len(self.data): raise ValueError( f"采样数 {n} 大于缓冲区大小 {len(self.data)}" ) return random.sample(self.data, n) def __len__(self): return len(self.data) # 测试 buf = SimpleBuffer(max_size=5) for i in range(8): buf.add({"state": i, "reward": i * 0.1}) print(f"缓冲区大小: {len(buf)}") # 5(最多 5 条) batch = buf.sample(3) print(f"采样结果: {batch}")
使用 NumPy 编写一个 RewardAnalyzer 类:
__init__(self):初始化,用于存储多个回合的奖励add_episode(self, rewards_list):添加一个回合的奖励序列(Python 列表)mean_return(self, gamma=0.99):计算所有回合的平均折扣回报best_episode(self):返回总奖励最高的回合索引和总奖励reward_statistics(self):返回字典,包含所有奖励的均值、标准差、最大值、最小值
import numpy as np class RewardAnalyzer: def __init__(self): self.episodes = [] # 每个元素是一个回合的奖励列表 def add_episode(self, rewards_list): self.episodes.append(np.array(rewards_list, dtype=np.float64)) def _discounted_return(self, rewards, gamma): """计算单个回合的折扣回报 G_0""" g = 0.0 for r in reversed(rewards): g = r + gamma * g return g def mean_return(self, gamma=0.99): if not self.episodes: return 0.0 returns = [self._discounted_return(ep, gamma) for ep in self.episodes] return np.mean(returns) def best_episode(self): if not self.episodes: return None, 0.0 totals = [ep.sum() for ep in self.episodes] best_idx = int(np.argmax(totals)) return best_idx, totals[best_idx] def reward_statistics(self): if not self.episodes: return {} all_rewards = np.concatenate(self.episodes) return { "mean": float(all_rewards.mean()), "std": float(all_rewards.std()), "max": float(all_rewards.max()), "min": float(all_rewards.min()), } # 测试 analyzer = RewardAnalyzer() analyzer.add_episode([1, 1, 1, 0, 0]) analyzer.add_episode([0, 0, 1, 1, 1, 1]) analyzer.add_episode([2, 2, -1, -1]) print(f"平均折扣回报: {analyzer.mean_return(gamma=0.99):.4f}") idx, total = analyzer.best_episode() print(f"最佳回合: 第 {idx} 个, 总奖励 = {total}") print(f"统计: {analyzer.reward_statistics()}")
知识检测
问题 1:在 Python 中,以下哪个数据结构最适合用作 RL 超参数配置?
问题 2:下面哪段 NumPy 代码可以正确计算奖励数组的归一化(零均值、单位方差)?
归一化时务必在标准差上加一个小常数(如 1e-8),防止除以零。当所有奖励相同时,标准差为 0,不加小常数会导致 NaN,进而让整个训练崩溃。
PyTorch 基础
掌握深度强化学习的核心工具——从张量运算到训练循环
中等3.1 张量(Tensor)基础
张量是 PyTorch 中的基本数据结构,类似于 NumPy 的 ndarray,但支持 GPU 加速和自动求导。在强化学习中,状态、动作、奖励、网络参数全部以张量形式存储和计算。
import torch import numpy as np # === 创建张量 === # 从 Python 列表创建 state = torch.tensor([0.5, -0.3, 1.2, 0.0]) print(f"状态张量: {state}") print(f"形状: {state.shape}, 数据类型: {state.dtype}") # 常用创建函数 zeros = torch.zeros(3, 4) # 3x4 零矩阵 ones = torch.ones(2, 3) # 2x3 全 1 矩阵 rand = torch.rand(5, 2) # 5x2 均匀分布 [0,1) randn = torch.randn(64, 4) # 64x4 标准正态分布(常用于初始化) arange = torch.arange(0, 10) # [0, 1, 2, ..., 9] eye = torch.eye(3) # 3x3 单位矩阵 # === 指定数据类型 === int_tensor = torch.tensor([1, 0, 1], dtype=torch.long) # 离散动作索引 float_tensor = torch.tensor([0.5], dtype=torch.float32) # 连续值 bool_tensor = torch.tensor([True, False]) # 掩码 # === 设备管理 (CPU / GPU) === device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"当前设备: {device}") gpu_tensor = torch.randn(3, 4).to(device) # 将张量移动到指定设备 # === NumPy 互转 === np_array = np.array([1.0, 2.0, 3.0]) from_np = torch.from_numpy(np_array) # NumPy → Tensor(共享内存) back_to_np = from_np.numpy() # Tensor → NumPy
torch.from_numpy() 创建的张量与原始 NumPy 数组共享内存——修改一个会影响另一个。如果需要独立副本,使用 torch.tensor(np_array)(会复制数据)。在 RL 中,Gymnasium 环境返回 NumPy 数组,你需要经常进行这种转换。
3.2 张量操作
在构建策略网络和处理批量数据时,你需要灵活地变换张量形状和拼接张量。
import torch # === reshape 和 view === # 假设我们有一批展平的状态向量,需要恢复为 (batch, channels, height, width) flat = torch.randn(8, 84 * 84 * 3) # 8 张展平的 84x84 RGB 图像 images = flat.view(8, 3, 84, 84) # 恢复为 4D 张量 print(f"恢复后形状: {images.shape}") # torch.Size([8, 3, 84, 84]) # === squeeze 和 unsqueeze === # squeeze: 移除大小为 1 的维度 action = torch.tensor([[0.5]]) # 形状 (1, 1) print(action.squeeze().shape) # 形状 () - 变成标量 # unsqueeze: 添加大小为 1 的维度 state = torch.randn(4) # 形状 (4,) batched = state.unsqueeze(0) # 形状 (1, 4) - 添加 batch 维度 print(f"添加 batch 维度: {batched.shape}") # torch.Size([1, 4]) # === cat 和 stack === # cat: 沿现有维度拼接 states_a = torch.randn(32, 4) # 第一批 32 个状态 states_b = torch.randn(16, 4) # 第二批 16 个状态 combined = torch.cat([states_a, states_b], dim=0) print(f"拼接后: {combined.shape}") # torch.Size([48, 4]) # stack: 沿新维度堆叠 episode_returns = [torch.tensor(100.0), torch.tensor(150.0), torch.tensor(120.0)] stacked = torch.stack(episode_returns) print(f"堆叠后: {stacked}") # tensor([100., 150., 120.]) # === 高级索引 === q_values = torch.randn(4, 3) # 4 个状态,3 个动作的 Q 值 actions = torch.tensor([0, 2, 1, 0]) # 每个状态选择的动作索引 # 取出每个状态对应动作的 Q 值 selected_q = q_values.gather(1, actions.unsqueeze(1)).squeeze(1) print(f"选中的 Q 值: {selected_q}")
3.3 自动求导(Autograd)
自动求导是 PyTorch 的核心特性。它自动计算损失函数对网络参数的梯度,是梯度下降优化和策略梯度算法的基础。
import torch # === 基本梯度计算 === # requires_grad=True 告诉 PyTorch 追踪这个张量的所有运算 w = torch.tensor([2.0, 3.0], requires_grad=True) x = torch.tensor([1.0, 1.0]) # 前向传播: y = w · x y = (w * x).sum() # y = 2*1 + 3*1 = 5 # 反向传播: 计算 dy/dw y.backward() print(f"梯度 dy/dw: {w.grad}") # tensor([1., 1.]) — 因为 dy/dw_i = x_i # === 计算图与 detach === a = torch.tensor([1.0], requires_grad=True) b = a * 2 # b 在计算图中 c = b.detach() # c 脱离计算图,梯度不再回传到 a print(f"b 需要梯度: {b.requires_grad}") # True print(f"c 需要梯度: {c.requires_grad}") # False # === torch.no_grad() — 推理时关闭梯度追踪 === # 在 RL 中,收集经验时不需要计算梯度,这样可以节省内存和加速 model_params = torch.randn(4, 2, requires_grad=True) state = torch.randn(4) with torch.no_grad(): # 这个块里的运算不会建立计算图 action = state @ model_params print(f"推理动作: {action}") print(f"需要梯度: {action.requires_grad}") # False
PyTorch 默认会累积梯度,而不是覆盖。每次调用 backward() 后梯度会叠加。因此在训练循环中,必须在每次反向传播前调用 optimizer.zero_grad() 清除旧梯度,否则梯度会越来越大,训练直接发散。
3.4 神经网络模块(nn.Module)
所有 PyTorch 神经网络都继承自 nn.Module。在 RL 中,策略网络(Actor)和价值网络(Critic)都是 nn.Module 的子类。
import torch import torch.nn as nn # === 方式一:使用 Sequential 快速搭建 === simple_policy = nn.Sequential( nn.Linear(4, 128), # 输入层: 4 维状态 → 128 维隐藏层 nn.ReLU(), # 激活函数 nn.Linear(128, 128), # 隐藏层 nn.ReLU(), nn.Linear(128, 2), # 输出层: 2 维动作 nn.Tanh(), # 将动作限制在 [-1, 1] ) # 测试前向传播 dummy_state = torch.randn(1, 4) # batch_size=1, state_dim=4 action = simple_policy(dummy_state) print(f"策略输出动作: {action}") # === 方式二:自定义 Module(更灵活,推荐用于 RL)=== class PolicyNetwork(nn.Module): """高斯策略网络 - 输出动作的均值和标准差""" def __init__(self, state_dim, action_dim, hidden_dim=256): super().__init__() # 共享特征提取层 self.shared = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), ) # 均值头 self.mean_head = nn.Linear(hidden_dim, action_dim) # 对数标准差(可学习参数) self.log_std = nn.Parameter(torch.zeros(action_dim)) def forward(self, state): """前向传播: 状态 → 动作分布参数""" features = self.shared(state) mean = self.mean_head(features) std = self.log_std.exp() # 确保标准差为正数 return mean, std # 实例化并测试 policy = PolicyNetwork(state_dim=4, action_dim=2) state = torch.randn(8, 4) # 一批 8 个状态 mean, std = policy(state) print(f"均值形状: {mean.shape}") # torch.Size([8, 2]) print(f"标准差: {std}") # 查看网络参数数量 total_params = sum(p.numel() for p in policy.parameters()) print(f"总参数量: {total_params}")
3.5 损失函数与优化器
损失函数衡量网络预测值与目标值的差距;优化器根据梯度更新网络参数。理解这套机制对理解 RL 算法的参数更新至关重要。
import torch import torch.nn as nn import torch.optim as optim # === 常用损失函数 === mse_loss = nn.MSELoss() # 均方误差 — 用于价值函数回归 ce_loss = nn.CrossEntropyLoss() # 交叉熵 — 用于离散动作分类 # MSE 示例: 预测 Q 值 vs 目标 Q 值 predicted_q = torch.tensor([1.5, 2.3, 0.8]) target_q = torch.tensor([1.0, 2.0, 1.0]) loss = mse_loss(predicted_q, target_q) print(f"MSE 损失: {loss.item():.4f}") # === 自定义损失 — 策略梯度损失 === def policy_gradient_loss(log_probs, advantages): """策略梯度损失: L = -E[log π(a|s) · A(s,a)]""" return -(log_probs * advantages).mean() # === 优化器与训练步骤 === # 构建一个简单的 Q 网络 q_network = nn.Sequential( nn.Linear(4, 64), nn.ReLU(), nn.Linear(64, 2), # 2 个动作的 Q 值 ) # Adam 优化器 — RL 中最常用的优化器 optimizer = optim.Adam(q_network.parameters(), lr=3e-4) # === 标准训练三步曲 === # 1. 清空梯度 optimizer.zero_grad() # 2. 前向 + 计算损失 + 反向传播 states = torch.randn(32, 4) q_pred = q_network(states)[:, 0] # 取动作 0 的 Q 值 q_target = torch.randn(32) # 目标 Q 值(实际中用贝尔曼方程计算) loss = mse_loss(q_pred, q_target) loss.backward() # 计算梯度 # 3. 更新参数 optimizer.step() print(f"损失: {loss.item():.4f}")
3.6 完整训练示例:MLP 策略网络
下面的例子展示了一个完整的训练循环。虽然这里用的是监督学习方式训练,但这个模式与 RL 中更新策略/价值网络的流程完全一致。
import torch import torch.nn as nn import torch.optim as optim # ── 1. 定义网络 ── class SimpleMLP(nn.Module): """简单多层感知机,可作为策略网络或价值网络的基础结构""" def __init__(self, input_dim, output_dim, hidden_dim=128): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, output_dim), ) def forward(self, x): return self.net(x) # ── 2. 准备数据(模拟"状态→Q值"映射)── torch.manual_seed(42) num_samples = 1000 state_dim = 4 action_dim = 2 # 生成假数据:模拟 Q 值标签 X = torch.randn(num_samples, state_dim) # 假设真实 Q 值是状态特征的某种线性组合 + 噪声 W_true = torch.randn(state_dim, action_dim) Y = X @ W_true + 0.1 * torch.randn(num_samples, action_dim) # ── 3. 初始化模型和优化器 ── model = SimpleMLP(input_dim=state_dim, output_dim=action_dim) optimizer = optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() # ── 4. 训练循环 ── num_epochs = 200 batch_size = 64 for epoch in range(num_epochs): # 随机采样一个 mini-batch indices = torch.randint(0, num_samples, (batch_size,)) batch_x = X[indices] batch_y = Y[indices] # 前向传播 predictions = model(batch_x) # 计算损失 loss = loss_fn(predictions, batch_y) # 反向传播与参数更新 optimizer.zero_grad() loss.backward() optimizer.step() # 每 50 个 epoch 打印一次 if (epoch + 1) % 50 == 0: print(f"Epoch {epoch+1}/{num_epochs}, 损失: {loss.item():.4f}") # ── 5. 推理(模拟在环境中使用策略)── model.eval() # 切换到评估模式 with torch.no_grad(): test_state = torch.randn(1, state_dim) q_values = model(test_state) best_action = q_values.argmax(dim=1) print(f"测试状态的 Q 值: {q_values}") print(f"选择动作: {best_action.item()}")
PyTorch 模型有两种模式:train()(训练模式)和 eval()(评估模式)。区别在于 Dropout 和 BatchNorm 层的行为不同。在 RL 中,收集经验时使用 eval() 模式,更新参数时使用 train() 模式。简单的 MLP 没有这些层时两种模式行为相同,但养成好习惯很重要。
练习
完成以下任务:
- 创建一个形状为 (5, 3) 的随机张量 A,和形状为 (3, 4) 的随机张量 B
- 计算矩阵乘法 C = A @ B
- 对 C 的每一行求最大值及其索引
- 将 C 展平(flatten)为一维张量
import torch # 1. 创建随机张量 A = torch.randn(5, 3) B = torch.randn(3, 4) print(f"A 形状: {A.shape}, B 形状: {B.shape}") # 2. 矩阵乘法 C = A @ B # 等价于 torch.matmul(A, B) print(f"C 形状: {C.shape}") # torch.Size([5, 4]) # 3. 每行最大值 max_values, max_indices = C.max(dim=1) print(f"每行最大值: {max_values}") print(f"最大值索引: {max_indices}") # 4. 展平 flat = C.flatten() # 或 C.view(-1) 或 C.reshape(-1) print(f"展平后形状: {flat.shape}") # torch.Size([20])
实现一个 ValueNetwork 类(继承 nn.Module),接受状态输入,输出一个标量状态价值 V(s)。要求:
- 至少两个隐藏层,使用 ReLU 激活
- 输出层不加激活函数(价值可以是任意实数)
- 实例化后对一批 16 个状态进行前向传播,输出形状应为 (16, 1)
import torch import torch.nn as nn class ValueNetwork(nn.Module): """ 状态价值网络 V(s) 输入: 状态向量 (batch_size, state_dim) 输出: 标量价值 (batch_size, 1) """ def __init__(self, state_dim, hidden_dim=128): super().__init__() self.network = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), # 输出单一标量,无激活 ) def forward(self, state): return self.network(state) # 测试 state_dim = 8 v_net = ValueNetwork(state_dim=state_dim) test_states = torch.randn(16, state_dim) values = v_net(test_states) print(f"价值输出形状: {values.shape}") # torch.Size([16, 1]) print(f"前 5 个价值: {values[:5].squeeze().tolist()}") # 查看参数量 params = sum(p.numel() for p in v_net.parameters()) print(f"参数总量: {params}")
编写一个完整的训练脚本,训练上面的 ValueNetwork 来逼近一个目标函数 V*(s) = s 各分量之和的平方。具体要求:
- 随机生成 2000 个训练样本(状态维度为 4)
- 目标值:Yi = (si1 + si2 + si3 + si4)²
- 使用 MSE 损失和 Adam 优化器(lr=1e-3)
- 训练 500 个 epoch,每 100 个 epoch 打印损失
- 训练结束后在 10 个测试样本上展示预测值 vs 真实值
import torch import torch.nn as nn import torch.optim as optim class ValueNetwork(nn.Module): def __init__(self, state_dim, hidden_dim=128): super().__init__() self.network = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) def forward(self, state): return self.network(state) # 生成数据 torch.manual_seed(0) state_dim = 4 N = 2000 X = torch.randn(N, state_dim) Y = X.sum(dim=1, keepdim=True) ** 2 # 目标: (Σs_i)^2 # 初始化 model = ValueNetwork(state_dim) optimizer = optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() # 训练 batch_size = 128 for epoch in range(500): idx = torch.randint(0, N, (batch_size,)) pred = model(X[idx]) loss = loss_fn(pred, Y[idx]) optimizer.zero_grad() loss.backward() optimizer.step() if (epoch + 1) % 100 == 0: print(f"Epoch {epoch+1}/500, 损失: {loss.item():.4f}") # 测试 model.eval() with torch.no_grad(): test_X = torch.randn(10, state_dim) test_Y = test_X.sum(dim=1, keepdim=True) ** 2 pred_Y = model(test_X) for i in range(10): print(f" 真实: {test_Y[i].item():7.3f} 预测: {pred_Y[i].item():7.3f}")
知识检测
问题 1:在训练循环中,以下步骤的正确顺序是什么?
问题 2:在 RL 中收集经验(与环境交互)时,为什么推荐使用 torch.no_grad()?
如果你在收集经验时忘记使用 torch.no_grad() 或 .detach(),PyTorch 会持续构建计算图并占用 GPU 内存。在长时间训练中,这会导致 OOM(Out of Memory)错误。这是 RL 新手最常犯的错误之一。
强化学习基础理论 核心基础
强化学习(Reinforcement Learning, RL)是机器学习的三大范式之一,与监督学习和无监督学习并列。在机器人领域,强化学习让智能体通过与环境交互来学习最优行为策略。本节将系统讲解强化学习的核心理论基础。
什么是强化学习?
强化学习是一种试错学习(Trial-and-Error Learning)方法。智能体(Agent)在环境(Environment)中采取动作(Action),环境返回新的状态(State)和奖励信号(Reward),智能体通过最大化累积奖励来学习最优策略。
与监督学习不同,强化学习没有标注数据,智能体必须自己探索哪些动作是好的;与无监督学习不同,强化学习有明确的目标信号——奖励。
┌─────────────────────────────────────────────┐
│ 强化学习交互循环 │
│ │
│ ┌──────────┐ 动作 aₜ ┌──────────┐
│ │ │ ──────────────────▶ │ │
│ │ 智能体 │ │ 环境 │
│ │ (Agent) │ ◀────────────────── │(Environ) │
│ │ │ 状态 sₜ₊₁, 奖励 rₜ₊₁│ │
│ └──────────┘ └──────────┘
│ │
│ 流程: s₀ → a₀ → r₁,s₁ → a₁ → r₂,s₂ → ...│
└─────────────────────────────────────────────┘
在每个时间步 t,智能体观察当前状态 sₜ,根据策略 π 选择动作 aₜ,环境转移到新状态 sₜ₊₁ 并给出奖励 rₜ₊₁。这个循环不断重复,直到达到终止条件。
核心概念详解
状态 (State, S)
描述环境在某一时刻的完整信息。例如:机器人的关节角度、位置、速度等。状态空间可以是离散的(如棋盘格局)或连续的(如机械臂关节角)。
动作 (Action, A)
智能体可以执行的操作。例如:向左移动、施加力矩、抓取物体等。动作空间同样可以是离散的或连续的。
奖励 (Reward, R)
环境对智能体动作的即时反馈信号,是一个标量值。正奖励鼓励行为,负奖励(惩罚)抑制行为。奖励函数的设计对学习效果至关重要。
策略 (Policy, π)
从状态到动作的映射,是智能体的行为准则。可以是确定性的 π(s)=a,或随机性的 π(a|s)=P(aₜ=a|sₜ=s)。学习最优策略 π* 是RL的核心目标。
状态转移 (Transition, P)
环境的动态模型,描述在状态 s 下执行动作 a 后转移到状态 s' 的概率:P(s'|s,a)。在模型无关方法中,我们不需要显式知道此函数。
折扣因子 (Discount, γ)
取值范围 [0,1],决定未来奖励的重要程度。γ=0 表示只关注即时奖励(短视),γ→1 表示远期奖励同样重要(远见)。通常取 0.99 或 0.995。
马尔可夫决策过程 (MDP)
强化学习问题的数学框架是马尔可夫决策过程(Markov Decision Process),定义为五元组:
其中:
- S — 状态空间(所有可能状态的集合)
- A — 动作空间(所有可能动作的集合)
- P(s'|s,a) — 状态转移概率函数
- R(s,a,s') — 奖励函数
- γ ∈ [0,1] — 折扣因子
MDP 的核心假设是马尔可夫性质:未来状态只依赖于当前状态和动作,与历史无关。数学表达为:P(sₜ₊₁|sₜ,aₜ) = P(sₜ₊₁|s₀,a₀,...,sₜ,aₜ)。这大大简化了问题建模,但在实际应用中,我们可能需要使用状态堆叠或循环网络来近似满足此条件。
累积回报与折扣因子
强化学习的目标不是最大化即时奖励,而是最大化从当前时刻起的折扣累积回报(Discounted Return):
奖励假说(Reward Hypothesis):所有目标都可以被描述为最大化累积奖励信号的期望值。这是强化学习的基本假设。
折扣因子 γ 的直觉理解:
- γ = 0:Gₜ = rₜ₊₁,只关心下一步的即时奖励,极度短视
- γ = 0.5:未来奖励价值快速衰减,10步后的奖励仅值原来的 0.5¹⁰ ≈ 0.001
- γ = 0.9:适中,20步后的奖励值约 0.9²⁰ ≈ 0.12
- γ = 0.99:常用值,100步后的奖励仍值约 0.99¹⁰⁰ ≈ 0.37
- γ = 1:不折扣,所有未来奖励等同重要(仅适用于有限长度的回合)
在机器人控制中,γ 通常取较大值(0.99~0.999),因为机器人需要规划长期动作序列。若 γ 过小,机器人可能只学会贪心的短期行为,无法完成需要多步规划的复杂任务。
价值函数与贝尔曼方程
价值函数衡量某个状态(或状态-动作对)的"好坏程度",即从该状态出发能获得的期望累积回报。
状态价值函数 Vπ(s)
表示在状态 s 下,遵循策略 π 所能获得的期望回报。
动作价值函数 Qπ(s,a)
表示在状态 s 下执行动作 a,然后遵循策略 π 所能获得的期望回报。
贝尔曼期望方程
价值函数满足递归关系——贝尔曼方程(Bellman Equation),这是动态规划和 RL 的理论基石:
含义:当前状态的价值 = 对所有可能动作求期望 [即时奖励 + 折扣 × 下一个状态的价值]。
贝尔曼最优方程
最优价值函数对应最优策略 π*,贝尔曼最优方程为:
区别在于:期望方程对动作取加权平均(按策略概率),最优方程对动作取最大值(选最好的动作)。
两个价值函数之间的关系:Vπ(s) = Σa π(a|s) · Qπ(s,a)。在最优策略下:V*(s) = maxa Q*(s,a)。如果我们知道 Q*,最优策略就是在每个状态选择 Q 值最大的动作:π*(s) = argmaxa Q*(s,a)。
策略类型
确定性策略 (Deterministic)
给定状态,策略输出唯一确定的动作:a = π(s)。简单直接,但缺乏探索能力。适用于已经学好策略后的部署阶段。
随机性策略 (Stochastic)
给定状态,策略输出动作的概率分布:π(a|s) = P(aₜ=a|sₜ=s)。自带探索能力,是大多数策略梯度方法的基础。对于连续动作,常用高斯分布。
探索与利用困境
探索(Exploration):尝试未知或较少访问的动作,以发现可能更好的策略。
利用(Exploitation):根据当前已知信息,选择目前看来最优的动作。
过度探索会浪费时间在差的动作上;过度利用会陷入局部最优。如何平衡两者是 RL 的核心难题。
ε-贪心策略 (ε-greedy)
最简单实用的探索策略:
以概率 1-ε 选择当前最优动作(利用),以概率 ε 随机选择动作(探索)。ε 通常从较大值(如 1.0)逐渐衰减到较小值(如 0.01)。
强化学习方法分类
基于模型 vs 无模型
基于模型 (Model-Based) 需要环境模型
智能体学习或已知环境的状态转移模型 P(s'|s,a) 和奖励函数 R。可以通过模型进行"想象中的规划",样本效率高,但模型误差可能导致策略偏差。例如:Dyna-Q、MBPO、World Models。
无模型 (Model-Free) 直接从交互学习
不学习环境模型,直接从交互经验中学习价值函数或策略。更简单通用,但样本效率较低。大多数深度 RL 方法属于此类。例如:Q-Learning、SARSA、PPO、SAC。
同策略 vs 异策略
同策略 (On-Policy) 用自己的数据学习
用于学习的数据必须来自当前策略生成。策略更新后,旧数据必须丢弃。样本效率低但更稳定。例如:SARSA、PPO、A2C。
异策略 (Off-Policy) 可用他人的数据学习
可以使用任何策略(包括旧策略、专家策略)生成的数据学习。可配合经验回放缓冲区,样本效率更高。例如:Q-Learning、DQN、SAC、TD3。
实例:GridWorld 价值迭代
我们用一个 4×4 的网格世界来演示价值迭代算法。智能体从起点 (0,0) 出发,目标是到达终点 (3,3),每走一步奖励为 -1,到达终点奖励为 0。
GridWorld (4×4)
┌─────┬─────┬─────┬─────┐
│ S │ │ │ │
│(0,0)│(0,1)│(0,2)│(0,3)│
├─────┼─────┼─────┼─────┤
│ │ ▓ │ │ │
│(1,0)│ 墙壁│(1,2)│(1,3)│
├─────┼─────┼─────┼─────┤
│ │ │ │ │
│(2,0)│(2,1)│(2,2)│(2,3)│
├─────┼─────┼─────┼─────┤
│ │ │ │ G │
│(3,0)│(3,1)│(3,2)│(3,3)│
└─────┴─────┴─────┴─────┘
S = 起点, G = 终点, ▓ = 墙壁
动作: ↑上 ↓下 ←左 →右
import numpy as np class GridWorld: """4×4 网格世界环境""" def __init__(self, size=4): self.size = size self.wall = (1, 1) # 墙壁位置 self.goal = (size-1, size-1) # 终点位置 # 四个动作:上、下、左、右 self.actions = [(-1,0), (1,0), (0,-1), (0,1)] self.n_actions = 4 def step(self, state, action_idx): """执行动作,返回 (下一状态, 奖励)""" if state == self.goal: return state, 0 # 终点状态,奖励为 0 action = self.actions[action_idx] next_row = state[0] + action[0] next_col = state[1] + action[1] next_state = (next_row, next_col) # 越界或撞墙则停留原地 if (next_row < 0 or next_row >= self.size or next_col < 0 or next_col >= self.size or next_state == self.wall): next_state = state return next_state, -1 # 每步奖励 -1 def value_iteration(env, gamma=0.99, theta=1e-6): """ 价值迭代算法 反复应用贝尔曼最优方程直到价值函数收敛 参数: env: 环境对象 gamma: 折扣因子 theta: 收敛阈值 返回: V: 最优状态价值函数 policy: 最优策略 """ # 初始化价值函数为零 V = np.zeros((env.size, env.size)) iteration = 0 while True: delta = 0 # 记录最大变化量 iteration += 1 for i in range(env.size): for j in range(env.size): state = (i, j) if state == env.goal or state == env.wall: continue old_v = V[i, j] # 贝尔曼最优方程:V(s) = max_a [R + γ·V(s')] action_values = [] for a in range(env.n_actions): next_state, reward = env.step(state, a) q_val = reward + gamma * V[next_state[0], next_state[1]] action_values.append(q_val) V[i, j] = max(action_values) delta = max(delta, abs(old_v - V[i, j])) # 检查是否收敛 if delta < theta: print(f"价值迭代在第 {iteration} 轮收敛 (delta={delta:.8f})") break # 从最优价值函数提取最优策略 policy = np.zeros((env.size, env.size), dtype=int) action_symbols = ['↑', '↓', '←', '→'] for i in range(env.size): for j in range(env.size): state = (i, j) if state == env.goal or state == env.wall: continue action_values = [] for a in range(env.n_actions): ns, r = env.step(state, a) action_values.append(r + gamma * V[ns[0], ns[1]]) policy[i, j] = np.argmax(action_values) return V, policy, action_symbols # 运行价值迭代 env = GridWorld(size=4) V, policy, symbols = value_iteration(env, gamma=0.99) # 打印最优价值函数 print("\n最优状态价值函数 V*:") for i in range(env.size): row = "" for j in range(env.size): if (i,j) == env.wall: row += " ▓▓▓ " else: row += f" {V[i,j]:6.2f} " print(row) # 打印最优策略 print("\n最优策略 π*:") for i in range(env.size): row = "" for j in range(env.size): if (i,j) == env.goal: row += " G " elif (i,j) == env.wall: row += " ▓ " else: row += f" {symbols[policy[i,j]]} " print(row)
价值迭代的核心思想:反复对每个状态应用贝尔曼最优方程 V(s) ← max_a [R(s,a) + γ·V(s')],直到所有状态的价值变化小于阈值 θ。收敛后,对每个状态选 Q 值最大的动作即为最优策略。这是一种动态规划方法,需要已知环境模型(转移概率和奖励函数)。
练习与巩固
请解释状态价值函数 Vπ(s) 和动作价值函数 Qπ(s,a) 的区别和联系。为什么在实际应用中,Q 函数通常比 V 函数更有用?
区别:
- Vπ(s) 评估的是"处于某个状态有多好"——在状态 s 下遵循策略 π 的期望回报
- Qπ(s,a) 评估的是"在某个状态执行某个动作有多好"——在状态 s 下执行动作 a 后遵循策略 π 的期望回报
联系:Vπ(s) = Σa π(a|s) · Qπ(s,a),即状态价值等于所有动作价值按策略概率的加权平均。
Q 函数更实用的原因:知道 Q*(s,a) 后,可以直接通过 π*(s) = argmaxa Q*(s,a) 得到最优策略,无需知道环境的转移模型。而仅知道 V*(s) 的话,选择动作时还需要知道 P(s'|s,a) 才能计算每个动作的期望价值。
已知一个简单的两状态 MDP:状态 A 执行动作后以概率 0.7 留在 A(奖励 +2),以概率 0.3 转到 B(奖励 +5);状态 B 是终止状态。折扣因子 γ=0.9。请用贝尔曼方程计算 V(A)。
状态 B 是终止状态,所以 V(B) = 0。
对状态 A 应用贝尔曼方程:
V(A) = 0.7 × [2 + γ × V(A)] + 0.3 × [5 + γ × V(B)]
V(A) = 0.7 × [2 + 0.9 × V(A)] + 0.3 × [5 + 0.9 × 0]
V(A) = 1.4 + 0.63 × V(A) + 1.5
V(A) - 0.63 × V(A) = 2.9
0.37 × V(A) = 2.9
V(A) ≈ 7.84
直觉验证:智能体在 A 状态有 70% 概率循环获得奖励 2,30% 概率获得奖励 5 后结束,所以 V(A) 应该大于单步奖励的期望(0.7×2+0.3×5=2.9),因为还有循环带来的累积收益。
请实现一个带 ε 衰减的 ε-贪心策略选择函数。初始 ε=1.0,最终 ε=0.01,在 10000 步内线性衰减。函数接受 Q 值数组和当前步数,返回选择的动作索引。
import numpy as np def epsilon_greedy(q_values, step, eps_start=1.0, eps_end=0.01, eps_decay_steps=10000): """ 带线性衰减的 ε-贪心动作选择 参数: q_values: 当前状态下各动作的 Q 值数组 step: 当前训练步数 eps_start: 初始探索率 eps_end: 最终探索率 eps_decay_steps: 衰减总步数 返回: action: 选择的动作索引 """ # 计算当前 ε(线性衰减) epsilon = max(eps_end, eps_start - (eps_start - eps_end) * step / eps_decay_steps) if np.random.random() < epsilon: # 探索:随机选择动作 action = np.random.randint(len(q_values)) else: # 利用:选择 Q 值最大的动作 action = np.argmax(q_values) return action # 测试 q = np.array([1.5, 3.2, 0.8, 2.1]) for s in [0, 2000, 5000, 10000, 20000]: eps = max(0.01, 1.0 - 0.99 * s / 10000) print(f"步数={s:>5}, ε={eps:.3f}, " f"选择动作={epsilon_greedy(q, s)}")
关键点:ε 从 1.0 线性下降到 0.01,在训练初期以高探索率广泛探索,后期以低探索率主要利用已有知识。max() 确保 ε 不会低于最终值。
给定一个回合的奖励序列 rewards = [1, -2, 3, 5, -1, 2, 4],请编写函数计算每个时间步的折扣回报 Gₜ(使用 γ=0.95)。提示:从后向前递推更高效:Gₜ = rₜ₊₁ + γ·Gₜ₊₁。
import numpy as np def compute_returns(rewards, gamma=0.95): """ 计算每个时间步的折扣回报(从后向前递推) Gₜ = rₜ₊₁ + γ·Gₜ₊₁ 从最后一步开始:G_{T-1} = r_T, G_{T-2} = r_{T-1} + γ·G_{T-1}, ... 参数: rewards: 奖励序列列表 gamma: 折扣因子 返回: returns: 每个时间步的折扣回报列表 """ T = len(rewards) returns = np.zeros(T) # 从后向前递推(高效 O(T) 算法) returns[T-1] = rewards[T-1] for t in range(T-2, -1, -1): returns[t] = rewards[t] + gamma * returns[t+1] return returns # 计算并展示结果 rewards = [1, -2, 3, 5, -1, 2, 4] gamma = 0.95 G = compute_returns(rewards, gamma) print("时间步 | 即时奖励 | 折扣回报 Gₜ") print("-" * 35) for t in range(len(rewards)): print(f" t={t} | r={rewards[t]:>3} | G={G[t]:.4f}") # 手动验证 G₀: # G₀ = 1 + 0.95×(-2) + 0.95²×3 + 0.95³×5 # + 0.95⁴×(-1) + 0.95⁵×2 + 0.95⁶×4 g0_manual = sum(gamma**k * rewards[k] for k in range(len(rewards))) print(f"\n手动验证 G₀ = {g0_manual:.4f}")
从后向前递推的优点:时间复杂度 O(T),比对每个 t 分别从前向后计算的 O(T²) 高效得多。这种技巧在策略梯度方法的实现中非常常用。
知识测验
深度强化学习 进阶核心
深度强化学习(Deep Reinforcement Learning, DRL)将深度神经网络与强化学习结合,使智能体能够在高维、复杂的状态空间中学习策略。本节从经典 RL 的局限性出发,依次讲解 DQN、策略梯度、Actor-Critic 三大方法族,并包含完整的 PyTorch 实现代码。
为什么需要深度学习?
在上一节中,我们用表格(Table)存储每个状态(或状态-动作对)的价值。这种方法在小规模离散问题(如 GridWorld)中表现良好,但面临以下根本性挑战:
维度灾难 (Curse of Dimensionality)
现实世界的状态空间通常是高维且连续的:
- Atari 游戏:210×160×3 的像素图像,状态空间约 256^(210×160×3) — 天文数字
- 机器人控制:多个关节的角度、角速度、力矩等连续值组合
- 围棋:19×19 棋盘约有 3^361 ≈ 10^172 种可能状态
表格方法无法存储和访问如此大的状态空间。
函数逼近 (Function Approximation)
解决方案是用参数化的函数来近似价值函数或策略,而非逐一存储:
深度神经网络是强大的通用函数逼近器,能够从高维输入中自动提取特征,这正是深度强化学习的核心思想。
经典 RL(表格法) 深度 RL(函数逼近)
┌─────────────┐ ┌──────────────────┐
│ Q-Table │ │ 神经网络 │
│ │ │ ┌──┐ ┌──┐ ┌──┐ │
│ s\a a1 a2 │ │ │输│→│隐│→│输│ │
│ s1 0.5 0.3 │ →→→ │ │入│ │藏│ │出│ │
│ s2 0.8 0.1 │ │ │层│→│层│→│层│ │
│ ... │ │ └──┘ └──┘ └──┘ │
│ 无法处理 │ │ 可处理高维连续 │
│ 高维连续空间 │ │ 状态空间 │
└─────────────┘ └──────────────────┘
DQN:深度 Q 网络 价值方法
DQN(Deep Q-Network)由 DeepMind 于 2013 年提出,是深度强化学习的里程碑式工作。它首次证明了深度网络可以直接从像素输入学会玩 Atari 游戏。
核心思想
用深度神经网络参数化 Q 函数:Q(s,a;θ),通过最小化时序差分(TD)误差来训练网络:
其中 θ⁻ 是目标网络参数(稍后解释)。
两大核心创新
经验回放 (Experience Replay)
问题:连续交互产生的数据高度相关,违反了随机梯度下降的独立同分布假设,导致训练不稳定。
解决:将经验 (s, a, r, s') 存入回放缓冲区(Replay Buffer),训练时随机抽取小批量(mini-batch)数据。这打破了数据相关性,提高了数据利用效率(一条经验可被多次使用)。
目标网络 (Target Network)
问题:用同一个网络既计算 Q 值又计算目标值,目标不断变化(移动靶子),导致训练发散。
解决:维护两个网络——在线网络 θ(不断更新)和目标网络 θ⁻(定期从在线网络复制参数)。目标网络提供稳定的学习目标,通常每隔 C 步复制一次参数。
DQN 算法伪代码
算法:DQN (Deep Q-Network)
─────────────────────────────────────────────
初始化回放缓冲区 D,容量为 N
初始化 Q 网络参数 θ(随机权重)
初始化目标网络参数 θ⁻ ← θ
FOR 回合 episode = 1 到 M:
获取初始状态 s₁
FOR 时间步 t = 1 到 T:
用 ε-贪心策略选择动作:
以概率 ε 选择随机动作 aₜ
否则 aₜ = argmax_a Q(sₜ, a; θ)
执行动作 aₜ,观察奖励 rₜ 和新状态 sₜ₊₁
将 (sₜ, aₜ, rₜ, sₜ₊₁) 存入 D
从 D 中随机采样 mini-batch {(sⱼ,aⱼ,rⱼ,sⱼ₊₁)}
计算目标: yⱼ = rⱼ + γ · max_a' Q(sⱼ₊₁, a'; θ⁻)
最小化损失: L = (1/B)·Σ(yⱼ - Q(sⱼ,aⱼ;θ))²
梯度下降更新 θ
每 C 步: θ⁻ ← θ (更新目标网络)
─────────────────────────────────────────────
DQN PyTorch 实现
import torch import torch.nn as nn import torch.optim as optim import numpy as np from collections import deque import random class QNetwork(nn.Module): """Q 值网络:输入状态,输出每个动作的 Q 值""" def __init__(self, state_dim, action_dim, hidden_dim=128): super().__init__() self.network = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim) ) def forward(self, state): return self.network(state) class ReplayBuffer: """经验回放缓冲区""" def __init__(self, capacity=100000): 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): """随机采样一个批次""" batch = random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones = zip(*batch) return (np.array(states), np.array(actions), np.array(rewards, dtype=np.float32), np.array(next_states), np.array(dones, dtype=np.float32)) def __len__(self): return len(self.buffer) class DQNAgent: """DQN 智能体""" def __init__(self, state_dim, action_dim, lr=1e-3, gamma=0.99, epsilon_start=1.0, epsilon_end=0.01, epsilon_decay=10000, target_update_freq=1000, buffer_size=100000, batch_size=64): self.action_dim = action_dim self.gamma = gamma self.batch_size = batch_size self.target_update_freq = target_update_freq self.epsilon_start = epsilon_start self.epsilon_end = epsilon_end self.epsilon_decay = epsilon_decay self.step_count = 0 self.device = torch.device( "cuda" if torch.cuda.is_available() else "cpu" ) # 在线网络和目标网络 self.q_network = QNetwork(state_dim, action_dim).to(self.device) self.target_network = QNetwork(state_dim, action_dim).to(self.device) self.target_network.load_state_dict( self.q_network.state_dict() ) self.optimizer = optim.Adam(self.q_network.parameters(), lr=lr) self.replay_buffer = ReplayBuffer(buffer_size) def get_epsilon(self): """计算当前 ε(线性衰减)""" return max(self.epsilon_end, self.epsilon_start - (self.epsilon_start - self.epsilon_end) * self.step_count / self.epsilon_decay) def select_action(self, state): """ε-贪心动作选择""" if random.random() < self.get_epsilon(): return random.randint(0, self.action_dim - 1) state_t = torch.FloatTensor(state).unsqueeze(0).to(self.device) with torch.no_grad(): q_values = self.q_network(state_t) return q_values.argmax(dim=1).item() def update(self): """从经验回放中采样并更新网络""" if len(self.replay_buffer) < self.batch_size: return # 采样一个批次的经验 states, actions, rewards, next_states, dones = \ self.replay_buffer.sample(self.batch_size) states = torch.FloatTensor(states).to(self.device) actions = torch.LongTensor(actions).to(self.device) rewards = torch.FloatTensor(rewards).to(self.device) next_states = torch.FloatTensor(next_states).to(self.device) dones = torch.FloatTensor(dones).to(self.device) # 计算当前 Q 值: Q(s, a; θ) current_q = self.q_network(states).gather( 1, actions.unsqueeze(1) ).squeeze(1) # 计算目标 Q 值: r + γ * max_a' Q(s', a'; θ⁻) with torch.no_grad(): next_q = self.target_network(next_states).max(dim=1)[0] target_q = rewards + self.gamma * next_q * (1 - dones) # 计算均方误差损失并反向传播 loss = nn.MSELoss()(current_q, target_q) self.optimizer.zero_grad() loss.backward() # 梯度裁剪,防止梯度爆炸 nn.utils.clip_grad_norm_(self.q_network.parameters(), 1.0) self.optimizer.step() # 定期更新目标网络 self.step_count += 1 if self.step_count % self.target_update_freq == 0: self.target_network.load_state_dict( self.q_network.state_dict() ) return loss.item()
从价值方法到策略方法
DQN 等价值方法(Value-Based)通过学习 Q 函数间接得到策略。然而这类方法存在局限性:
- 只能处理离散动作空间:argmax 操作在连续动作空间中不可行
- 策略是确定性的:无法表达随机策略(某些环境中最优策略本身就是随机的)
- 微小的 Q 值变化可能导致策略剧烈变化:不稳定
策略方法(Policy-Based)直接参数化策略 π(a|s;θ) 并优化策略参数,可以自然地处理连续动作空间和随机策略。
价值方法:间接法,先学 Q 再导出策略。优点是样本效率较高(Off-Policy),缺点是只能处理离散动作。
策略方法:直接法,直接优化策略参数。优点是能处理连续动作和随机策略,缺点是方差大、样本效率低(On-Policy)。
策略梯度定理与 REINFORCE
策略梯度定理 (Policy Gradient Theorem)
定义目标函数为期望累积回报:J(θ) = Eτ~πθ[Σ rₜ],其中 τ 表示一条完整轨迹。
策略梯度定理给出了 J(θ) 对参数 θ 的梯度:
直觉理解:
- ∇θ log πθ(aₜ|sₜ) — 指出如何调整参数以增加动作 aₜ 在状态 sₜ 下的概率
- Gₜ — 该动作之后获得的累积回报,作为"权重"
- 如果 Gₜ > 0(好的回报),梯度方向增加该动作概率
- 如果 Gₜ < 0(差的回报),梯度方向减少该动作概率
本质是:增加获得高回报的动作的概率,减少获得低回报的动作的概率。
推导过程(简化版)
1. 目标:最大化 J(θ) = Eτ~πθ[R(τ)],其中 R(τ) 是轨迹总回报。
2. J(θ) = ∫ πθ(τ) · R(τ) dτ
3. ∇J(θ) = ∫ ∇πθ(τ) · R(τ) dτ
4. 利用对数导数技巧(Log-Derivative Trick):∇πθ(τ) = πθ(τ) · ∇log πθ(τ)
5. ∇J(θ) = ∫ πθ(τ) · ∇log πθ(τ) · R(τ) dτ = Eτ~πθ[∇log πθ(τ) · R(τ)]
6. 由于 log πθ(τ) = Σₜ log πθ(aₜ|sₜ) + 与 θ 无关的项(环境动态项)
7. 最终:∇J(θ) = E[Σₜ ∇log πθ(aₜ|sₜ) · Gₜ]
REINFORCE 算法
算法:REINFORCE(蒙特卡罗策略梯度)
─────────────────────────────────────────────
初始化策略网络参数 θ
FOR 回合 episode = 1 到 M:
用当前策略 π_θ 采集一条完整轨迹:
τ = (s₀, a₀, r₁, s₁, a₁, r₂, ..., sₜ)
FOR 每个时间步 t = 0 到 T-1:
计算折扣回报: Gₜ = Σ(k=0→T-t-1) γᵏ · rₜ₊ₖ₊₁
计算策略梯度:
∇J ≈ Σₜ ∇_θ log π_θ(aₜ|sₜ) · Gₜ
更新参数: θ ← θ + α · ∇J
─────────────────────────────────────────────
特点: 必须完成整个回合后才能更新(蒙特卡罗)
缺点: 梯度方差大,样本效率低
Actor-Critic 方法 结合两者优势
REINFORCE 使用完整回合的回报 Gₜ 作为梯度的权重,方差很大(因为 Gₜ 是对一条完整轨迹的蒙特卡罗估计)。Actor-Critic 方法引入值函数作为基线来降低方差。
优势函数 (Advantage Function)
优势函数衡量"在状态 s 下执行动作 a 比遵循策略 π 的平均表现好多少":
- A > 0:动作 a 比平均水平好,应该增加其概率
- A < 0:动作 a 比平均水平差,应该降低其概率
- A = 0:动作 a 与平均水平持平
Actor-Critic 架构
┌──────────────────────────────────────────┐
│ Actor-Critic 架构 │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Actor │ │ Critic │ │
│ │ (策略网络) │ │ (价值网络) │ │
│ │ π(a|s;θ) │ │ V(s;w) │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ │ 动作 a │ V(s) │
│ ▼ ▼ │
│ ┌──────────────────────────┐ │
│ │ 环境 Environment │ │
│ │ 返回: r, s' │ │
│ └──────────────────────────┘ │
│ │
│ TD 误差: δ = r + γ·V(s') - V(s) │
│ Actor 更新: θ ← θ + α·∇log π(a|s)·δ │
│ Critic 更新: w ← w - β·∇(δ²) │
└──────────────────────────────────────────┘
A2C(Advantage Actor-Critic)
A2C 是 Actor-Critic 的同步版本。它使用 TD 误差 δ = r + γ·V(s';w) - V(s;w) 作为优势函数的估计:
- Actor(策略网络):输出动作概率分布,使用优势函数加权的策略梯度更新
- Critic(价值网络):估计状态价值 V(s),使用 TD 误差更新
实际中常加入熵正则化项 H(π) 来鼓励探索,防止策略过早收敛。
三大方法族对比
| 特性 | DQN(价值方法) | 策略梯度 | Actor-Critic |
|---|---|---|---|
| 学习对象 | Q(s,a) 价值函数 | π(a|s) 策略 | 策略 + 价值函数 |
| 动作空间 | 仅离散 | 离散 + 连续 | 离散 + 连续 |
| 策略类型 | Off-Policy | On-Policy | On/Off-Policy |
| 样本效率 | 较高(可回放) | 低(用完即弃) | 中等 |
| 梯度方差 | 低 | 高 | 中等(有基线) |
| 训练稳定性 | 需要技巧 | 较不稳定 | 较稳定 |
| 代表算法 | DQN, DDQN | REINFORCE | A2C, A3C, PPO, SAC |
完整实现:REINFORCE 解决 CartPole
以下是使用 REINFORCE 算法训练策略网络解决 CartPole-v1 环境的完整 PyTorch 代码。CartPole 要求智能体通过左右移动小车来平衡杆子。
import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torch.distributions import Categorical import gymnasium as gym import numpy as np class PolicyNetwork(nn.Module): """ 策略网络:输入状态,输出动作概率分布 CartPole 状态维度=4,动作维度=2(左/右) """ def __init__(self, state_dim, action_dim, hidden_dim=128): super().__init__() self.fc1 = nn.Linear(state_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, action_dim) def forward(self, x): x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) # Softmax 输出动作概率 action_probs = F.softmax(self.fc3(x), dim=-1) return action_probs def compute_returns(rewards, gamma=0.99): """从后向前计算每步的折扣回报""" returns = [] G = 0 for r in reversed(rewards): G = r + gamma * G returns.insert(0, G) returns = torch.tensor(returns, dtype=torch.float32) # 标准化回报以降低方差(重要技巧) if len(returns) > 1: returns = (returns - returns.mean()) / (returns.std() + 1e-8) return returns def train_reinforce(num_episodes=1000, gamma=0.99, lr=1e-3): """REINFORCE 算法主训练循环""" # 创建环境 env = gym.make("CartPole-v1") state_dim = env.observation_space.shape[0] # 4 action_dim = env.action_space.n # 2 # 初始化策略网络和优化器 policy = PolicyNetwork(state_dim, action_dim) optimizer = optim.Adam(policy.parameters(), lr=lr) reward_history = [] # 记录每回合总奖励 for episode in range(num_episodes): state, _ = env.reset() log_probs = [] # 存储 log π(aₜ|sₜ) rewards = [] # 存储每步奖励 # ===== 第一步:用当前策略采集完整轨迹 ===== done = False while not done: state_t = torch.FloatTensor(state) action_probs = policy(state_t) # 按概率分布采样动作 dist = Categorical(action_probs) action = dist.sample() log_prob = dist.log_prob(action) # 执行动作 next_state, reward, terminated, truncated, _ = \ env.step(action.item()) done = terminated or truncated log_probs.append(log_prob) rewards.append(reward) state = next_state # ===== 第二步:计算折扣回报 ===== returns = compute_returns(rewards, gamma) # ===== 第三步:计算策略梯度并更新 ===== # 损失 = -Σ log π(aₜ|sₜ) · Gₜ (取负号因为优化器做梯度下降) policy_loss = [] for log_prob, G in zip(log_probs, returns): policy_loss.append(-log_prob * G) loss = torch.stack(policy_loss).sum() optimizer.zero_grad() loss.backward() optimizer.step() # 记录并打印训练进度 total_reward = sum(rewards) reward_history.append(total_reward) if (episode + 1) % 50 == 0: avg_reward = np.mean(reward_history[-50:]) print(f"回合 {episode+1:>4d} | " f"平均奖励(50回合): {avg_reward:.1f} | " f"本回合奖励: {total_reward:.0f}") # CartPole-v1 最大奖励 500,达到 475 视为解决 if avg_reward >= 475: print(f"\n环境已解决!在第 {episode+1} 回合") break env.close() return policy, reward_history # 运行训练 if __name__ == "__main__": policy, history = train_reinforce( num_episodes=1000, gamma=0.99, lr=1e-3 ) print(f"\n训练完成!最终50回合平均奖励: " f"{np.mean(history[-50:]):.1f}")
1. 回报标准化:对 Gₜ 做标准化处理(减均值除标准差),可以显著降低梯度方差,加速收敛。这是因为标准化后正负回报大致均衡,避免了所有梯度都朝一个方向推。
2. 学习率选择:策略梯度对学习率敏感。太大会导致策略剧烈变化甚至崩溃,太小则收敛缓慢。通常取 1e-3 到 1e-4。
3. 必须等完整回合:REINFORCE 是蒙特卡罗方法,需要完整轨迹来计算 Gₜ,无法像 DQN 那样每步更新。
深度强化学习的关键挑战
样本效率 核心瓶颈
深度 RL 通常需要数百万甚至数十亿次环境交互。在真实机器人上,每次交互都需要实际时间和成本,这使得样本效率成为机器人 RL 的核心挑战。解决方案包括:Off-Policy 方法、Sim-to-Real 迁移、模型学习等。
训练稳定性 常见问题
深度 RL 训练常常不稳定:奖励曲线可能突然崩溃、策略可能发散。原因包括:非平稳数据分布、自举(bootstrapping)误差累积、策略更新过大等。PPO 和 SAC 等现代算法通过各种技巧改善了这一问题。
信用分配 理论难题
某个时刻获得的奖励可能是之前很多步决策的结果,如何将功劳正确分配给每个时间步的动作?这就是"信用分配问题"(Credit Assignment Problem)。在长序列任务中尤为困难。优势函数和 TD 学习可以部分缓解此问题。
练习与巩固
请解释 DQN 中经验回放(Experience Replay)和目标网络(Target Network)分别解决了什么问题。如果去掉其中一个,训练会出现什么情况?
经验回放解决的问题:数据相关性
智能体连续交互产生的数据 (s₁,a₁,r₁,s₂), (s₂,a₂,r₂,s₃), ... 高度相关(相邻状态相似)。神经网络的随机梯度下降(SGD)假设数据是独立同分布的(i.i.d.),违反此假设会导致训练过拟合到最近的经验,产生灾难性遗忘。经验回放通过随机采样打破了时间相关性。
目标网络解决的问题:训练不稳定(移动靶子)
在 Q-Learning 更新中,TD 目标 y = r + γ·max Q(s',a';θ) 依赖于同一个网络 θ。每次更新 θ,目标值也跟着变化,形成"追逐移动靶子"的局面,容易导致振荡甚至发散。目标网络 θ⁻ 保持固定一段时间,提供稳定的学习目标。
去掉的影响:
- 去掉经验回放:网络会过拟合到最近经验,在不同环境区域间来回振荡,忘记之前学过的知识
- 去掉目标网络:Q 值估计可能发散到极大值,训练完全不稳定
在 REINFORCE 实现中,损失函数写为 loss = -Σ log π(aₜ|sₜ) · Gₜ。请解释为什么要取负号,以及为什么对回报做标准化(减均值、除标准差)可以降低梯度方差?
为什么取负号:
策略梯度定理给出:∇J(θ) = E[∇log π(aₜ|sₜ) · Gₜ]。我们的目标是最大化 J(θ),即沿梯度上升方向更新:θ ← θ + α·∇J。但 PyTorch 的优化器默认执行梯度下降(最小化损失),所以我们定义损失为 -J 的近似:L = -Σ log π(aₜ|sₜ) · Gₜ。最小化 L 等价于最大化 J。
为什么标准化降低方差:
假设所有回报都为正(比如 CartPole 中回报范围是 [0, 500]),那么梯度 ∇log π · Gₜ 总是正的,意味着所有被采样到的动作概率都会增加——虽然好动作增加得多、差动作增加得少,但这种"全部增加"的方式效率很低。
标准化后,回报有正有负(均值为0),好于平均的动作被增强(Gₜ > 0),差于平均的动作被抑制(Gₜ < 0)。这等价于引入了一个基线 b = mean(G),而策略梯度理论证明减去与动作无关的基线不改变梯度期望,但能显著降低方差。
请基于上面的 REINFORCE 代码,将其改造为 Actor-Critic 版本。主要修改:(1) 增加一个 Critic 网络估计 V(s);(2) 用 TD 误差 δ = r + γ·V(s') - V(s) 替代 Gₜ 作为优势估计;(3) 可以每步更新而非等完整回合。
import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torch.distributions import Categorical import gymnasium as gym import numpy as np class ActorCritic(nn.Module): """Actor-Critic 网络:共享底层特征,分别输出策略和价值""" def __init__(self, state_dim, action_dim, hidden=128): super().__init__() # 共享特征提取层 self.shared = nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU() ) # Actor 头:输出动作概率 self.actor = nn.Sequential( nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Softmax(dim=-1) ) # Critic 头:输出状态价值标量 self.critic = nn.Sequential( nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, x): features = self.shared(x) action_probs = self.actor(features) state_value = self.critic(features) return action_probs, state_value def train_actor_critic(num_episodes=1000, gamma=0.99, lr=3e-4): """Actor-Critic 训练循环(每步更新版本)""" env = gym.make("CartPole-v1") state_dim = env.observation_space.shape[0] action_dim = env.action_space.n model = ActorCritic(state_dim, action_dim) optimizer = optim.Adam(model.parameters(), lr=lr) reward_history = [] for episode in range(num_episodes): state, _ = env.reset() total_reward = 0 done = False while not done: state_t = torch.FloatTensor(state) action_probs, value = model(state_t) # 采样动作 dist = Categorical(action_probs) action = dist.sample() log_prob = dist.log_prob(action) # 执行动作 next_state, reward, terminated, truncated, _ = \ env.step(action.item()) done = terminated or truncated total_reward += reward # 计算 TD 误差(优势估计) next_state_t = torch.FloatTensor(next_state) with torch.no_grad(): _, next_value = model(next_state_t) # 如果终止,下一状态价值为 0 td_target = reward + gamma * next_value * (1 - float(done)) advantage = td_target - value # Actor 损失:策略梯度(用优势加权) actor_loss = -log_prob * advantage.detach() # Critic 损失:TD 误差的均方 critic_loss = advantage.pow(2) # 熵正则化:鼓励探索 entropy = dist.entropy() # 总损失 = Actor损失 + 0.5×Critic损失 - 0.01×熵 loss = actor_loss + 0.5 * critic_loss - 0.01 * entropy optimizer.zero_grad() loss.backward() optimizer.step() state = next_state reward_history.append(total_reward) if (episode + 1) % 50 == 0: avg = np.mean(reward_history[-50:]) print(f"回合 {episode+1:>4d} | 平均奖励: {avg:.1f}") env.close() return model, reward_history
与 REINFORCE 的关键区别:
- 不需要等完整回合,每一步都可以更新(在线学习)
- 用 TD 误差 δ 替代蒙特卡罗回报 Gₜ,方差更低但引入了偏差(bias)
- Critic 网络提供了基线 V(s),优势 A = δ = r + γV(s') - V(s) 自带基线减法
- 熵正则化项防止策略过早收敛到确定性策略
知识测验
PPO 算法详解
Proximal Policy Optimization — 强化学习中最实用的策略梯度算法
PPO 由 OpenAI 于 2017 年提出,是目前机器人强化学习最常用的算法之一。IsaacLab 中的机器人运动控制任务几乎全部使用 PPO 训练。它简单、稳定、高效,是你必须精通的核心算法。
1. 从策略梯度到 PPO 的演进
REINFORCE 基础
直接对策略做梯度上升。问题:方差大,训练不稳定,一次坏的更新可能毁掉整个策略。
TRPO 理论优美
用 KL 散度约束每次更新幅度,保证单调改进。问题:需要二阶优化(Hessian),实现极复杂。
PPO 实践首选
用简单的裁剪(Clip)替代 KL 约束,只需一阶优化器(Adam),保留了 TRPO 的稳定性。
2. PPO 核心公式
2.1 重要性采样比率
PPO 利用旧策略采集的数据来更新新策略,核心是计算新旧策略的概率比:
当 θ = θ_old 时,r_t = 1。如果新策略更倾向于选择动作 a_t,则 r_t > 1;反之 r_t < 1。
2.2 PPO-Clip 目标函数
当 A_t > 0(好动作):我们想增大 r_t,但 clip 限制 r_t ≤ 1+ε,防止过度增大概率。
当 A_t < 0(坏动作):我们想减小 r_t,但 clip 限制 r_t ≥ 1-ε,防止过度减小概率。
ε 通常取 0.2,意味着概率比被限制在 [0.8, 1.2] 范围内。
2.3 广义优势估计 (GAE)
PPO 使用 GAE 来计算优势函数,平衡偏差和方差:
A_t^GAE = Σ_{l=0}^{T-t} (γλ)^l · δ_{t+l} (GAE 优势估计)
其中 λ ∈ [0,1] 控制偏差-方差权衡:λ=0 退化为单步 TD(低方差高偏差),λ=1 退化为蒙特卡洛(高方差低偏差)。实践中 λ=0.95 效果最好。
2.4 完整 PPO 损失函数
三个部分:策略损失(Clip)+ 价值函数损失(MSE)+ 熵奖励(鼓励探索)。c₁=0.5, c₂=0.01 是常用值。
3. PPO 算法流程
4. PyTorch 完整实现
import torch import torch.nn as nn import torch.optim as optim import numpy as np class ActorCritic(nn.Module): """Actor-Critic 网络:策略和价值共享底层特征""" def __init__(self, obs_dim, act_dim, hidden=256): super().__init__() # 共享特征提取层 self.shared = nn.Sequential( nn.Linear(obs_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), ) # Actor 头:输出动作均值 self.actor_mean = nn.Linear(hidden, act_dim) # 可学习的对数标准差 self.actor_log_std = nn.Parameter(torch.zeros(act_dim)) # Critic 头:输出状态价值 self.critic = nn.Linear(hidden, 1) def forward(self, obs): features = self.shared(obs) return self.actor_mean(features), self.critic(features) def get_action(self, obs): """采样动作并返回 log_prob 和 value""" mean, value = self.forward(obs) std = self.actor_log_std.exp() dist = torch.distributions.Normal(mean, std) action = dist.sample() log_prob = dist.log_prob(action).sum(-1) return action, log_prob, value.squeeze(-1) def evaluate(self, obs, actions): """计算给定 obs-action 对的 log_prob, value, entropy""" mean, value = self.forward(obs) std = self.actor_log_std.exp() dist = torch.distributions.Normal(mean, std) log_prob = dist.log_prob(actions).sum(-1) entropy = dist.entropy().sum(-1) return log_prob, value.squeeze(-1), entropy def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95): """计算广义优势估计 (GAE)""" advantages = [] gae = 0 # 从后往前计算 for t in reversed(range(len(rewards))): if t == len(rewards) - 1: next_value = 0 # 终止状态 else: next_value = values[t + 1] delta = rewards[t] + gamma * next_value * (1 - dones[t]) - values[t] gae = delta + gamma * lam * (1 - dones[t]) * gae advantages.insert(0, gae) return torch.tensor(advantages, dtype=torch.float32) def ppo_update(model, optimizer, obs, actions, old_log_probs, returns, advantages, clip_eps=0.2, epochs=10, batch_size=64, ent_coef=0.01, vf_coef=0.5): """PPO 策略更新""" dataset_size = obs.shape[0] for _ in range(epochs): # 随机打乱数据 indices = np.random.permutation(dataset_size) for start in range(0, dataset_size, batch_size): end = start + batch_size idx = indices[start:end] # 取 mini-batch mb_obs = obs[idx] mb_actions = actions[idx] mb_old_log = old_log_probs[idx] mb_returns = returns[idx] mb_adv = advantages[idx] # 优势标准化(关键trick!) mb_adv = (mb_adv - mb_adv.mean()) / (mb_adv.std() + 1e-8) # 计算新策略下的 log_prob new_log_prob, values, entropy = model.evaluate(mb_obs, mb_actions) # 概率比 r_t(θ) ratio = (new_log_prob - mb_old_log).exp() # PPO-Clip 目标 surr1 = ratio * mb_adv surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * mb_adv policy_loss = -torch.min(surr1, surr2).mean() # 价值函数损失 value_loss = nn.functional.mse_loss(values, mb_returns) # 总损失 = 策略损失 + 价值损失 - 熵奖励 loss = policy_loss + vf_coef * value_loss - ent_coef * entropy.mean() optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 0.5) # 梯度裁剪 optimizer.step()
5. 超参数指南
| 参数 | 推荐值 | 说明 |
|---|---|---|
clip_epsilon (ε) | 0.2 | 裁剪范围,越小越保守 |
learning_rate | 3e-4 | Adam 学习率,可线性衰减 |
gamma (γ) | 0.99 | 折扣因子 |
lambda (λ) | 0.95 | GAE 参数 |
epochs (K) | 3-10 | 每批数据的更新轮数 |
batch_size | 64-4096 | Mini-batch 大小 |
entropy_coef | 0.0-0.01 | 熵奖励系数 |
num_steps | 2048 | 每次采集的步数 |
6. PPO 实践技巧
优势标准化
对每个 mini-batch 的优势做标准化(减均值除标准差)。这是最重要的 trick,可以显著提高训练稳定性。
梯度裁剪
使用 clip_grad_norm_(params, 0.5) 防止梯度爆炸。对机器人任务特别重要。
学习率衰减
线性衰减学习率到 0,可以在训练后期精细调整策略。IsaacLab 默认使用此技巧。
观测标准化
用 Running Mean/Std 标准化观测值,保持网络输入在合理范围。对高维观测空间非常有效。
1. 忘记 detach 旧策略的 log_prob:采集数据时的 log_prob 不能参与梯度计算。
2. 数据复用过多:epochs 设太大(>15)会导致过拟合采集的数据。
3. 忽略 done 信号:计算 GAE 时必须正确处理 episode 终止。
练习
计算过程:
surr1 = r_t × A_t = 1.5 × 3.0 = 4.5
surr2 = clip(1.5, 0.8, 1.2) × 3.0 = 1.2 × 3.0 = 3.6
L^CLIP = min(4.5, 3.6) = 3.6
解释:由于 A_t > 0 且 r_t > 1+ε,概率比被裁剪到 1.2。这意味着虽然新策略大幅增加了该动作的概率(r_t=1.5),但 PPO 只"认可"到 1.2,阻止了过激的策略更新。目标值为 3.6 而非 4.5。
Step 1: 计算 TD 残差
δ_0 = r_0 + γ·V(s_1) - V(s_0) = 1 + 0.99×4 - 5 = -0.04
δ_1 = r_1 + γ·V(s_2) - V(s_1) = 2 + 0.99×3 - 4 = 0.97
δ_2 = r_2 + γ·V(s_3) - V(s_2) = 3 + 0.99×0 - 3 = 0.00
Step 2: 从后往前计算 GAE
A_2 = δ_2 = 0.00
A_1 = δ_1 + γλ·A_2 = 0.97 + 0.99×0.95×0.00 = 0.97
A_0 = δ_0 + γλ·A_1 = -0.04 + 0.99×0.95×0.97 = -0.04 + 0.912 = 0.872
import gymnasium as gym # CartPole 离散版 Actor-Critic class DiscreteAC(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.shared = nn.Sequential( nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh()) self.actor = nn.Linear(64, act_dim) self.critic = nn.Linear(64, 1) def get_action(self, obs): feat = self.shared(obs) dist = torch.distributions.Categorical( logits=self.actor(feat)) action = dist.sample() return action, dist.log_prob(action), self.critic(feat).squeeze() def evaluate(self, obs, actions): feat = self.shared(obs) dist = torch.distributions.Categorical( logits=self.actor(feat)) return (dist.log_prob(actions), self.critic(feat).squeeze(), dist.entropy()) # 训练循环 env = gym.make('CartPole-v1') model = DiscreteAC(4, 2) optimizer = optim.Adam(model.parameters(), lr=3e-4) for iteration in range(200): obs_buf, act_buf, rew_buf = [], [], [] logp_buf, val_buf, done_buf = [], [], [] obs, _ = env.reset() ep_ret, ep_count = 0, 0 for step in range(2048): obs_t = torch.FloatTensor(obs) with torch.no_grad(): action, log_prob, value = model.get_action(obs_t) next_obs, reward, term, trunc, _ = env.step(action.item()) obs_buf.append(obs_t) act_buf.append(action) rew_buf.append(reward) logp_buf.append(log_prob) val_buf.append(value) done = term or trunc done_buf.append(float(done)) ep_ret += reward obs = next_obs if done: obs, _ = env.reset() ep_count += 1 # 计算 GAE 和 returns advantages = compute_gae(rew_buf, val_buf, done_buf) returns = advantages + torch.stack(val_buf) # PPO 更新 ppo_update(model, optimizer, torch.stack(obs_buf), torch.stack(act_buf), torch.stack(logp_buf).detach(), returns, advantages) if iteration % 10 == 0: print(f"Iter {iteration}, AvgReturn: {ep_ret/max(ep_count,1):.0f}")
测验
SAC 算法详解
Soft Actor-Critic — 基于最大熵框架的 Off-Policy 算法
SAC 由 UC Berkeley 的 Haarnoja 等人于 2018 年提出。它是目前最高效的 off-policy 连续控制算法之一,在 MuJoCo 基准测试中表现优异,常用于机器人操作任务的训练。
1. 最大熵强化学习框架
SAC 的核心思想:除了最大化累积奖励,还要最大化策略的熵(entropy):
其中 H(π) = -E[log π(a|s)] 是策略的熵,α 是温度参数
为什么要最大化熵?
更好的探索 核心优势
高熵策略不会过早收敛到某一个动作,保持对环境的持续探索,避免陷入局部最优。
鲁棒性 Sim2Real 友好
熵正则化使策略倾向于多模态分布,对环境变化更鲁棒。这对 Sim2Real 迁移非常有利。
更好的组合能力
多模态策略可以学习多种达成目标的方式,便于在不同情境下灵活切换行为策略。
2. SAC 核心组件
2.1 Twin Q-Networks(双 Q 网络)
SAC 使用两个独立的 Q 网络,取较小值来计算目标值,有效缓解 Q 值过高估计问题:
2.2 Squashed Gaussian 策略
SAC 使用 Gaussian 分布采样,然后通过 tanh 将动作压缩到 [-1, 1]:
a = tanh(u) (压缩到有界范围)
log π(a|s) = log N(u|μ,σ) - Σ log(1 - tanh²(u_i)) (雅可比修正)
将采样操作分离为 确定性函数 + 噪声,使得梯度可以通过采样动作反向传播到策略网络。这是 SAC 能高效训练的关键。与 PPO 使用 log_prob trick 不同,SAC 直接通过动作值传递梯度。
2.3 温度参数 α 自动调节
SAC 可以自动调节 α,使策略的熵保持在目标水平:
通常设 H_target = -dim(A) (动作空间维度的负数)
3. SAC 算法流程
4. PyTorch 完整实现
import torch import torch.nn as nn import torch.optim as optim import numpy as np from collections import deque import random # ---- 经验回放缓冲区 ---- class ReplayBuffer: def __init__(self, capacity=1_000_000): self.buffer = deque(maxlen=capacity) def add(self, s, a, r, s_next, done): self.buffer.append((s, a, r, s_next, done)) def sample(self, batch_size=256): batch = random.sample(self.buffer, batch_size) s, a, r, s2, d = zip(*batch) return (torch.FloatTensor(np.array(s)), torch.FloatTensor(np.array(a)), torch.FloatTensor(r).unsqueeze(1), torch.FloatTensor(np.array(s2)), torch.FloatTensor(d).unsqueeze(1)) # ---- Q 网络 ---- class QNetwork(nn.Module): def __init__(self, obs_dim, act_dim, hidden=256): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim + act_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1)) def forward(self, obs, action): return self.net(torch.cat([obs, action], dim=-1)) # ---- Squashed Gaussian 策略 ---- class GaussianPolicy(nn.Module): def __init__(self, obs_dim, act_dim, hidden=256): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU()) self.mean_head = nn.Linear(hidden, act_dim) self.log_std_head = nn.Linear(hidden, act_dim) def forward(self, obs): feat = self.net(obs) mean = self.mean_head(feat) log_std = self.log_std_head(feat).clamp(-20, 2) return mean, log_std def sample(self, obs): """重参数化采样 + tanh 压缩""" mean, log_std = self.forward(obs) std = log_std.exp() dist = torch.distributions.Normal(mean, std) # 重参数化: u = μ + σ·ε u = dist.rsample() # tanh 压缩到 [-1, 1] action = torch.tanh(u) # 计算 log_prob(含雅可比修正) log_prob = dist.log_prob(u) - torch.log(1 - action.pow(2) + 1e-6) log_prob = log_prob.sum(-1, keepdim=True) return action, log_prob # ---- SAC Agent ---- class SACAgent: def __init__(self, obs_dim, act_dim, lr=3e-4, gamma=0.99, tau=0.005): self.gamma = gamma self.tau = tau # 网络 self.policy = GaussianPolicy(obs_dim, act_dim) self.q1 = QNetwork(obs_dim, act_dim) self.q2 = QNetwork(obs_dim, act_dim) self.q1_target = QNetwork(obs_dim, act_dim) self.q2_target = QNetwork(obs_dim, act_dim) # 初始化 target = 原网络 self.q1_target.load_state_dict(self.q1.state_dict()) self.q2_target.load_state_dict(self.q2.state_dict()) # 自动温度调节 self.target_entropy = -act_dim self.log_alpha = torch.zeros(1, requires_grad=True) # 优化器 self.policy_opt = optim.Adam(self.policy.parameters(), lr=lr) self.q1_opt = optim.Adam(self.q1.parameters(), lr=lr) self.q2_opt = optim.Adam(self.q2.parameters(), lr=lr) self.alpha_opt = optim.Adam([self.log_alpha], lr=lr) @property def alpha(self): return self.log_alpha.exp().detach() def select_action(self, obs): obs_t = torch.FloatTensor(obs).unsqueeze(0) with torch.no_grad(): action, _ = self.policy.sample(obs_t) return action.squeeze(0).numpy() def update(self, batch): s, a, r, s2, done = batch # ---- 更新 Q 网络 ---- with torch.no_grad(): a2, log_prob2 = self.policy.sample(s2) q1_target = self.q1_target(s2, a2) q2_target = self.q2_target(s2, a2) q_target = torch.min(q1_target, q2_target) - self.alpha * log_prob2 y = r + self.gamma * (1 - done) * q_target q1_loss = nn.functional.mse_loss(self.q1(s, a), y) q2_loss = nn.functional.mse_loss(self.q2(s, a), y) self.q1_opt.zero_grad(); q1_loss.backward(); self.q1_opt.step() self.q2_opt.zero_grad(); q2_loss.backward(); self.q2_opt.step() # ---- 更新策略 ---- a_new, log_prob = self.policy.sample(s) q_new = torch.min(self.q1(s, a_new), self.q2(s, a_new)) policy_loss = (self.alpha * log_prob - q_new).mean() self.policy_opt.zero_grad(); policy_loss.backward(); self.policy_opt.step() # ---- 更新温度 α ---- alpha_loss = -(self.log_alpha * (log_prob.detach() + self.target_entropy)).mean() self.alpha_opt.zero_grad(); alpha_loss.backward(); self.alpha_opt.step() # ---- 软更新 target 网络 ---- for p, pt in zip(self.q1.parameters(), self.q1_target.parameters()): pt.data.copy_(self.tau * p.data + (1 - self.tau) * pt.data) for p, pt in zip(self.q2.parameters(), self.q2_target.parameters()): pt.data.copy_(self.tau * p.data + (1 - self.tau) * pt.data)
5. SAC vs PPO 对比
| 特性 | PPO | SAC |
|---|---|---|
| 学习类型 | On-Policy | Off-Policy |
| 样本效率 | 较低(数据不能重用) | 较高(经验回放) |
| 训练稳定性 | 非常稳定 | 稳定(Twin Q + 熵正则) |
| 并行化 | 天然支持大规模并行 | 不适合 GPU 并行 |
| 适用场景 | 大规模并行仿真(IsaacLab) | 单环境高样本效率场景 |
| 动作空间 | 离散/连续均可 | 主要连续 |
| 超参数敏感度 | 较低 | 中等 |
| 典型应用 | 四足行走、人形运动 | 机械臂操作、灵巧手 |
选 PPO:当你可以并行运行成百上千个仿真环境时(IsaacLab + GPU),PPO 的总体训练速度更快。四足/人形运动控制几乎都用 PPO。
选 SAC:当只有单个或少量仿真环境时(MuJoCo),SAC 的样本效率更高。机械臂操作任务常用 SAC。
6. 超参数指南
| 参数 | 推荐值 | 说明 |
|---|---|---|
learning_rate | 3e-4 | 所有网络统一学习率 |
tau | 0.005 | target 网络软更新系数 |
gamma | 0.99 | 折扣因子 |
buffer_size | 1,000,000 | 经验回放缓冲区大小 |
batch_size | 256 | Mini-batch 大小 |
alpha | 自动调节 | 温度参数,建议用自动调节 |
hidden_dim | 256 | 隐藏层大小 |
warmup_steps | 10,000 | 随机采集初始数据量 |
练习
高熵策略在 Sim2Real 中更有优势,原因如下:
- 高熵策略学到了多种达成目标的方式,当仿真与现实的某些差异导致一种方式失效时,策略可以切换到其他方式
- 确定性策略高度依赖仿真环境的精确性,真实世界的微小差异可能导致完全失败
- 高熵策略的动作分布更平滑,对扰动和噪声有更好的容忍度
- 熵正则化相当于隐式地进行了一种 "domain randomization"
这是概率密度变量替换公式的应用。
当 u ~ N(μ,σ) 且 a = tanh(u) 时,由于 tanh 是非线性变换,概率密度会发生变化:
因为 da/du = 1 - tanh²(u),所以:
= log N(u|μ,σ) - Σ_i log(1 - tanh²(u_i))
如果忽略这个修正项,log_prob 会不准确,导致策略梯度估计偏差,温度自动调节也会失效。
Pendulum-v1 环境。编写完整的训练循环,包括 warmup 阶段和训练日志。
import gymnasium as gym env = gym.make('Pendulum-v1') obs_dim = env.observation_space.shape[0] # 3 act_dim = env.action_space.shape[0] # 1 agent = SACAgent(obs_dim, act_dim) buffer = ReplayBuffer(capacity=100_000) # 训练参数 total_steps = 50_000 warmup = 1_000 # 随机探索阶段 batch_size = 256 update_every = 1 # 每步更新一次 obs, _ = env.reset() ep_return = 0 ep_count = 0 for step in range(total_steps): # 选择动作 if step < warmup: action = env.action_space.sample() # 随机探索 else: action = agent.select_action(obs) # 缩放到环境动作范围 action = action * 2.0 # Pendulum 范围 [-2, 2] next_obs, reward, term, trunc, _ = env.step(action) done = term or trunc buffer.add(obs, action, reward, next_obs, float(done)) ep_return += reward obs = next_obs if done: obs, _ = env.reset() ep_count += 1 if ep_count % 10 == 0: print(f"Episode {ep_count}, Return: {ep_return:.1f}, α: {agent.alpha.item():.3f}") ep_return = 0 # 更新网络 if step >= warmup and len(buffer.buffer) >= batch_size: batch = buffer.sample(batch_size) agent.update(batch)
测验
机器人动力学基础
在将强化学习应用于机器人控制之前,我们需要理解机器人的物理运动规律。本节将介绍刚体运动学与动力学的核心概念,为后续仿真环境搭建和策略训练奠定基础。
刚体运动基础
机器人本质上由多个刚体(Rigid Body)通过关节连接而成。描述刚体状态需要以下物理量:
位置与姿态
位置(Position):三维空间坐标 (x, y, z),描述刚体质心在世界坐标系中的位置。
姿态(Orientation):描述刚体的旋转状态,常用表示方法有欧拉角、旋转矩阵和四元数。
速度与角速度
线速度(Linear Velocity):位置的时间导数 v = dx/dt,单位 m/s。
角速度(Angular Velocity):姿态的时间变化率 ω,描述刚体绕某轴的旋转速率,单位 rad/s。
力与力矩
力(Force):改变刚体线速度的原因,F = ma。
力矩(Torque):改变刚体角速度的原因,τ = Iα,其中 I 为转动惯量,α 为角加速度。
坐标系与变换
机器人系统中存在多个坐标系:世界坐标系、基座坐标系、各连杆坐标系、末端执行器坐标系等。我们需要在它们之间进行坐标变换。
旋转矩阵
旋转矩阵 R 是一个 3×3 正交矩阵,满足 R^T R = I 且 det(R) = 1。绕三个坐标轴的基本旋转矩阵为:
齐次变换矩阵
齐次变换矩阵 T 是 4×4 矩阵,同时包含旋转和平移信息:
import numpy as np def rotation_z(theta): """绕 Z 轴旋转的旋转矩阵""" c, s = np.cos(theta), np.sin(theta) return np.array([ [c, -s, 0], [s, c, 0], [0, 0, 1] ]) def homogeneous_transform(R, p): """构造 4×4 齐次变换矩阵""" T = np.eye(4) T[:3, :3] = R T[:3, 3] = p return T # 示例:绕 Z 轴旋转 90 度,平移 (1, 0, 0.5) R = rotation_z(np.pi / 2) p = np.array([1.0, 0.0, 0.5]) T = homogeneous_transform(R, p) print("齐次变换矩阵 T:") print(np.round(T, 3))
自由度与关节类型
自由度(Degrees of Freedom, DoF)描述机器人可独立运动的维度数。一个在三维空间中自由运动的刚体有 6 个自由度(3 个平移 + 3 个旋转)。
| 关节类型 | 英文名 | 自由度 | 运动描述 | 典型应用 |
|---|---|---|---|---|
| 旋转关节 | Revolute | 1 | 绕固定轴旋转,有角度限制 | 机械臂肘关节 |
| 移动关节 | Prismatic | 1 | 沿固定轴平移 | 升降平台、线性导轨 |
| 固定关节 | Fixed | 0 | 无相对运动 | 传感器安装座 |
| 球面关节 | Spherical | 3 | 绕三个轴旋转 | 肩关节、髋关节 |
正运动学与逆运动学
正运动学(Forward Kinematics)
正运动学解决的问题是:已知各关节角度 q = [q1, q2, ..., qn],求末端执行器在笛卡尔空间中的位姿。
DH 参数(Denavit-Hartenberg)是描述相邻连杆之间几何关系的标准方法。每个关节用 4 个参数描述:
- θ(theta):绕 z 轴的旋转角度(旋转关节的变量)
- d:沿 z 轴的偏移距离(移动关节的变量)
- a:沿 x 轴的连杆长度
- α(alpha):绕 x 轴的扭转角度
逆运动学(Inverse Kinematics)
逆运动学是正运动学的反问题:已知末端执行器期望位姿,求对应的关节角度。逆运动学通常更难求解,可能有多解、无解或无穷解。常用方法包括解析法、雅可比迭代法和数值优化法。
在 RL 控制中,策略网络可以直接输出关节角度或力矩,从而绕过传统逆运动学求解。这是 RL 在机器人控制中的一大优势——可以端到端学习从目标到动作的映射。
二连杆机械臂示例
import numpy as np def forward_kinematics_2link(theta1, theta2, L1=1.0, L2=0.8): """二连杆机械臂正运动学 参数: theta1: 关节1角度 (rad) theta2: 关节2角度 (rad) L1: 连杆1长度 L2: 连杆2长度 返回: (x, y) 末端执行器位置 """ # 关节1位置(肘部) x1 = L1 * np.cos(theta1) y1 = L1 * np.sin(theta1) # 末端执行器位置 x2 = x1 + L2 * np.cos(theta1 + theta2) y2 = y1 + L2 * np.sin(theta1 + theta2) return x2, y2 # 测试不同关节角度 angles = [ (0, 0), # 完全伸展 (np.pi/4, 0), # 第一关节旋转45度 (np.pi/4, -np.pi/2), # 肘部弯曲90度 ] for t1, t2 in angles: x, y = forward_kinematics_2link(t1, t2) print(f"θ1={np.degrees(t1):.0f}°, θ2={np.degrees(t2):.0f}° → x={x:.3f}, y={y:.3f}")
牛顿-欧拉动力学
机器人动力学描述力/力矩与运动之间的关系。经典的机器人动力学方程为:
在仿真环境(MuJoCo、IsaacSim)中,物理引擎会自动计算这些动力学方程。我们只需设定控制输入 τ(或目标位置/速度),引擎会积分求解出下一时刻的状态。理解动力学方程有助于我们设计更好的观测空间和奖励函数。
关节空间与任务空间
关节空间(Joint Space)
以关节角度 q 和关节角速度 q̇ 描述机器人状态。维度等于机器人自由度数。
优点:直接对应电机控制量,没有奇异性问题。
缺点:不直观,难以直接描述末端执行器的运动。
任务空间(Task Space)
以末端执行器的笛卡尔坐标和姿态描述机器人状态。通常为 6 维(3 平移 + 3 旋转)。
优点:直观,与任务目标直接相关。
缺点:存在奇异性,需要逆运动学求解。
控制模式
| 控制模式 | 控制量 | 特点 | RL 中的应用 |
|---|---|---|---|
| 力矩控制 | 关节力矩 τ | 最底层、最灵活,但需要精确的动力学模型 | 连续动作空间直接输出力矩 |
| 位置控制 | 目标关节角度 q_target | 底层 PD 控制器跟踪目标位置,更稳定 | RL 输出目标角度,PD 控制器执行 |
| 速度控制 | 目标关节速度 q̇_target | 介于力矩和位置控制之间 | RL 输出期望速度增量 |
PD 控制器
位置控制模式下,底层通常使用 PD(比例-微分)控制器将目标位置转换为力矩:
import numpy as np class PDController: """简单的 PD 控制器""" def __init__(self, kp, kd, num_joints): self.kp = np.array(kp) # 比例增益 self.kd = np.array(kd) # 微分增益 self.num_joints = num_joints def compute_torque(self, q_desired, q_current, dq_desired, dq_current): """计算 PD 控制力矩 参数: q_desired: 目标关节角度 q_current: 当前关节角度 dq_desired: 目标关节速度(通常为0) dq_current: 当前关节速度 返回: tau: 关节力矩 """ pos_error = q_desired - q_current vel_error = dq_desired - dq_current tau = self.kp * pos_error + self.kd * vel_error return tau # 示例:6自由度机械臂的 PD 控制器 controller = PDController( kp=[100, 100, 80, 50, 50, 30], # 各关节刚度 kd=[10, 10, 8, 5, 5, 3], # 各关节阻尼 num_joints=6 ) # 当前状态 q_current = np.array([0.5, -0.3, 0.8, 0.1, -0.2, 0.0]) dq_current = np.array([0.01, -0.02, 0.0, 0.03, 0.0, -0.01]) # 目标位置 q_desired = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) dq_desired = np.zeros(6) tau = controller.compute_torque(q_desired, q_current, dq_desired, dq_current) print(f"控制力矩: {tau}")
动力学对强化学习的意义
理解动力学直接决定了 RL 问题的建模质量:观测空间的选择、动作空间的设计、奖励函数的构造都依赖于对机器人物理特性的理解。
观测空间与动作空间设计
| 机器人类型 | 典型观测空间 | 典型动作空间 | 代表环境 |
|---|---|---|---|
| 机械臂 | 关节角度、角速度、末端位置、目标位置 | 关节力矩 / 目标角度 | FetchReach, FrankaKitchen |
| 四足机器人 | 躯干姿态、关节角度/速度、足底接触力 | 12个关节目标角度 | Ant-v4, Unitree Go1 |
| 人形机器人 | 全身关节状态、质心速度、IMU 数据 | 全关节力矩/目标位置 | Humanoid-v4 |
| 无人机 | 位置、速度、姿态四元数、角速度 | 四个旋翼推力 | Quadrotor 自定义环境 |
RL 中常见的机器人类型
机械臂(Manipulator)
通常为 6-7 自由度串联机构。任务包括抓取、放置、装配等。代表型号:Franka Panda(7DoF)、UR5(6DoF)。
四足机器人(Quadruped)
4条腿各 3 个关节,共 12 自由度。任务包括行走、奔跑、翻越障碍。代表型号:Unitree Go1/Go2、ANYmal。
人形机器人(Humanoid)
20-30+ 自由度,最接近人体结构。任务包括双足行走、全身操作。代表型号:Atlas、Unitree H1。
无人机(Drone / UAV)
通常 4 个旋翼(6DoF 欠驱动系统)。任务包括悬停、轨迹跟踪、穿越障碍。代表型号:Crazyflie。
练习
使用正运动学公式:
力矩计算:
负号表示力矩方向与正方向相反,即驱动关节回到目标位置。
增大 Kd 的影响:增大微分增益(Kd=50)会增加阻尼效果,使系统更快地消耗动能。关节到达目标位置时超调减少,但响应速度变慢。Kd 过大可能导致系统响应迟钝(过阻尼)。
推荐的观测空间设计(约 25 维):
- 7 个关节角度 q(归一化到 [-1, 1])
- 7 个关节角速度 dq
- 3 个末端执行器位置 (x, y, z)
- 3 个目标物体位置 (x, y, z)
- 3 个目标到末端的相对位置差
- 1 个夹爪开合状态
- 1 个是否抓住物体的布尔信号
推荐的动作空间(8 维,连续):
- 7 个目标关节角度增量 Δq(位置控制模式)
- 1 个夹爪开合控制
理由:位置控制模式比力矩控制更稳定,底层 PD 控制器保证了运动的平滑性。输出增量(而非绝对位置)使动作空间在零附近居中,有利于策略网络训练。包含相对位置差作为观测可以加速学习目标导向行为。
测验
URDF 与 MJCF 机器人描述文件
用 XML 定义你的虚拟机器人 — 仿真训练的第一步
在 MuJoCo 或 IsaacLab 中训练机器人之前,你需要用描述文件定义机器人的形状、关节、质量等物理属性。URDF 和 MJCF 是两种最常用的格式。
1. URDF(Unified Robot Description Format)
URDF 是 ROS 生态中的标准格式,结构为 XML。核心元素:
<link> 连杆
定义刚体:视觉外观 (visual)、碰撞体 (collision)、惯性参数 (inertial: 质量 + 惯性张量)
<joint> 关节
连接两个 link。类型:revolute(有限旋转), continuous(无限旋转), prismatic(平移), fixed(固定)
<robot> 根元素
包含所有 link 和 joint。URDF 只能描述树形结构(无闭链),一个 parent 对应多个 child。
URDF 示例:二连杆机械臂
<?xml version="1.0"?> <robot name="two_link_arm"> <!-- 基座(固定在世界中) --> <link name="base_link"> <visual> <geometry><cylinder radius="0.05" length="0.1"/></geometry> </visual> <inertial> <mass value="5.0"/> <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/> </inertial> </link> <!-- 连杆1:长0.5m --> <link name="link1"> <visual> <origin xyz="0 0 0.25"/> <geometry><cylinder radius="0.03" length="0.5"/></geometry> </visual> <inertial> <mass value="2.0"/> <origin xyz="0 0 0.25"/> <inertia ixx="0.042" ixy="0" ixz="0" iyy="0.042" iyz="0" izz="0.001"/> </inertial> </link> <!-- 肩关节:绕 Z 轴旋转 --> <joint name="shoulder" type="revolute"> <parent link="base_link"/> <child link="link1"/> <axis xyz="0 0 1"/> <limit lower="-3.14" upper="3.14" effort="50" velocity="3.0"/> </joint> <!-- 连杆2 和 肘关节 省略,结构类似 --> </robot>
2. MJCF(MuJoCo XML Format)
MuJoCo 的原生格式,比 URDF 功能更丰富。核心区别:
| 特性 | URDF | MJCF |
|---|---|---|
| 执行器 | 不支持 | 支持 motor/position/velocity |
| 传感器 | 不支持 | 支持关节位置/速度/力等 |
| 仿真参数 | 不支持 | 可配置时间步、积分器等 |
| 接触模型 | 有限 | 详细的摩擦、弹性配置 |
| 默认值继承 | 不支持 | 支持 default class |
| 生态 | ROS, Gazebo, PyBullet | MuJoCo 专用 |
MJCF 示例
<mujoco model="two_link_arm"> <option timestep="0.002" gravity="0 0 -9.81"/> <worldbody> <light pos="0 0 3"/> <geom type="plane" size="5 5 0.1"/> <!-- 基座固定在世界 --> <body name="base" pos="0 0 1"> <geom type="cylinder" size="0.05 0.05" rgba="0.5 0.5 0.5 1"/> <!-- 连杆1: body 嵌套代替 parent/child --> <body name="link1"> <joint name="shoulder" type="hinge" axis="0 0 1" range="-3.14 3.14"/> <geom type="capsule" fromto="0 0 0 0 0 0.5" size="0.03" mass="2.0"/> <body name="link2" pos="0 0 0.5"> <joint name="elbow" type="hinge" axis="0 1 0" range="-2.0 2.0"/> <geom type="capsule" fromto="0 0 0 0 0 0.4" size="0.025" mass="1.5"/> </body> </body> </body> </worldbody> <!-- 执行器(URDF 中没有!) --> <actuator> <motor joint="shoulder" ctrlrange="-50 50"/> <motor joint="elbow" ctrlrange="-30 30"/> </actuator> <!-- 传感器(URDF 中没有!) --> <sensor> <jointpos joint="shoulder"/> <jointvel joint="shoulder"/> <jointpos joint="elbow"/> <jointvel joint="elbow"/> </sensor> </mujoco>
MuJoCo 自带编译工具:python -m mujoco.compile robot.urdf robot.xml。也可以在 Python 中用 mujoco.MjModel.from_xml_path("robot.urdf") 直接加载 URDF。
3. 去哪里找机器人模型?
MuJoCo Menagerie
DeepMind 维护的高质量 MJCF 模型库:Franka、UR5e、Unitree Go1/H1、Shadow Hand 等。github.com/google-deepmind/mujoco_menagerie
robot_descriptions
Python 包,整合了数十种机器人模型:pip install robot_descriptions,一行代码加载。
厂商官方 GitHub
Unitree、Franka、UR 等厂商通常在 GitHub 提供官方 URDF/MJCF 文件。
练习
(1) 1个自由度(只展示了 shoulder 关节,完整版2个)
(2) Z轴:axis xyz="0 0 1"
(3) 2.0 kg:mass value="2.0"
<mujoco model="pendulum"> <option timestep="0.002" gravity="0 0 -9.81"/> <worldbody> <light pos="0 0 3"/> <body name="base" pos="0 0 1.5"> <geom type="sphere" size="0.05"/> <body name="pendulum"> <joint name="pivot" type="hinge" axis="0 1 0"/> <geom type="capsule" fromto="0 0 0 0 0 -1.0" size="0.02" mass="1.0"/> </body> </body> </worldbody> <actuator> <motor joint="pivot" ctrlrange="-10 10"/> </actuator> <sensor> <jointpos joint="pivot"/> <jointvel joint="pivot"/> </sensor> </mujoco>
<actuator> 的 motor 和 position 类型有什么区别?MuJoCo 仿真环境
Multi-Joint dynamics with Contact — 机器人 RL 的标准仿真引擎
MuJoCo 是由 DeepMind 维护的高性能物理引擎,专为机器人控制和强化学习设计。2022 年开源后成为 RL 研究的标准工具。安装:pip install mujoco
1. 核心概念
MjModel
静态模型:从 XML 加载的机器人描述(形状、质量、关节等)。不会在仿真过程中改变。
MjData
动态状态:关节位置 qpos、速度 qvel、控制信号 ctrl、传感器数据等。每步仿真后更新。
mj_step
推进一步仿真。根据当前控制信号和物理定律更新所有状态。
2. 基本仿真循环
import mujoco import numpy as np # 加载模型 model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) # 仿真循环 for step in range(1000): # 设置控制信号(关节力矩) data.ctrl[:] = np.array([1.0, -0.5]) # 推进一步仿真 mujoco.mj_step(model, data) # 读取状态 qpos = data.qpos.copy() # 关节位置 qvel = data.qvel.copy() # 关节速度 sensor = data.sensordata # 传感器数据 print(f"Step {step}: qpos={qpos}, qvel={qvel}") # 可视化(打开交互窗口) import mujoco.viewer mujoco.viewer.launch(model, data)
3. Gymnasium 集成
Gymnasium(原 OpenAI Gym)提供了标准的 MuJoCo 环境接口:
import gymnasium as gym # 使用预置的 MuJoCo 环境 env = gym.make("Ant-v4", render_mode="human") obs, info = env.reset() for _ in range(1000): action = env.action_space.sample() # 随机动作 obs, reward, terminated, truncated, info = env.step(action) if terminated or truncated: obs, info = env.reset() env.close()
入门级:Pendulum-v1, CartPole-v1(非 MuJoCo 但接口相同)
中级:HalfCheetah-v4, Hopper-v4, Walker2d-v4
高级:Ant-v4, Humanoid-v4, HumanoidStandup-v4
4. 自定义 Gym 环境
这是 RL 工程中最核心的技能之一——把你的 MuJoCo 模型封装为 Gym 环境:
import gymnasium as gym from gymnasium import spaces import mujoco import numpy as np class PendulumSwingUp(gym.Env): """自定义摆锤 Swing-up 环境""" def __init__(self): self.model = mujoco.MjModel.from_xml_path("pendulum.xml") self.data = mujoco.MjData(self.model) self.dt = self.model.opt.timestep # 定义观测空间:[cos(θ), sin(θ), dθ] self.observation_space = spaces.Box( low=-np.inf, high=np.inf, shape=(3,), dtype=np.float32) # 定义动作空间:力矩 [-10, 10] self.action_space = spaces.Box( low=-10.0, high=10.0, shape=(1,), dtype=np.float32) self.step_count = 0 def _get_obs(self): theta = self.data.qpos[0] dtheta = self.data.qvel[0] return np.array([np.cos(theta), np.sin(theta), dtheta], dtype=np.float32) def reset(self, seed=None, **kwargs): super().reset(seed=seed) mujoco.mj_resetData(self.model, self.data) # 随机初始角度(倒垂状态附近) self.data.qpos[0] = np.pi + np.random.uniform(-0.1, 0.1) self.step_count = 0 return self._get_obs(), {} def step(self, action): self.data.ctrl[:] = action # 执行多个仿真步(降低控制频率) for _ in range(5): mujoco.mj_step(self.model, self.data) self.step_count += 1 obs = self._get_obs() # 奖励:cos(θ) 在竖直向上时为 1 theta = self.data.qpos[0] reward = np.cos(theta) - 0.01 * action[0]**2 terminated = False truncated = self.step_count >= 200 return obs, reward, terminated, truncated, {}
控制频率:MuJoCo 仿真步长通常 0.002s,但 RL 策略不需要那么高的频率。常见做法是每个 step() 执行 5-20 个仿真步,使策略控制频率为 50-100Hz。
观测设计:角度用 (cos,sin) 而非原始角度,避免 2π 不连续性。关节速度需要包含。
奖励设计:奖励要连续可微,避免稀疏奖励。加入动作惩罚项鼓励平滑控制。
练习
import gymnasium as gym env = gym.make("Ant-v4") print(f"观测空间: {env.observation_space.shape}") # (27,) print(f"动作空间: {env.action_space.shape}") # (8,) obs, _ = env.reset() for _ in range(1000): obs, r, term, trunc, _ = env.step(env.action_space.sample()) if term or trunc: obs, _ = env.reset() print("完成!Ant 有 27 维观测,8 维动作(8个关节力矩)")
# 改进版奖励函数 theta = self.data.qpos[0] dtheta = self.data.qvel[0] # 位置奖励:竖直向上(θ=0)时最大 pos_reward = np.cos(theta) # 速度惩罚:鼓励稳定(到达顶部后减速) vel_penalty = -0.01 * dtheta**2 # 动作惩罚:鼓励能量效率 act_penalty = -0.005 * action[0]**2 # 存活奖励:鼓励长时间运行 alive_bonus = 0.1 reward = pos_reward + vel_penalty + act_penalty + alive_bonus
设计原则:位置奖励为主,速度和动作惩罚为辅,加上存活奖励避免过早终止。
IsaacLab 训练框架
什么是 IsaacLab?
IsaacLab(前身为 Isaac Gym / Orbit)是 NVIDIA 推出的 GPU 加速机器人仿真训练框架。它建立在 Isaac Sim 之上,为强化学习研究者和工程师提供了一套完整的机器人学习工具链。
IsaacLab 的核心优势在于能够在单块 GPU 上同时运行 数千个并行环境,将传统需要数天的训练缩短到数小时甚至数分钟。
- GPU 并行化:所有物理仿真和策略推理都在 GPU 上完成,避免 CPU-GPU 数据传输瓶颈
- 基于 USD:使用 Universal Scene Description 格式,场景描述标准化且可复用
- 模块化设计:观测、动作、奖励、终止条件均可独立配置
- 多 RL 库支持:兼容 RSL-RL、rl_games、skrl、Stable-Baselines3 等主流库
IsaacLab vs MuJoCo 对比
| 特性 | IsaacLab | MuJoCo |
|---|---|---|
| 并行环境数 | 数千~数万(GPU 并行) | 通常数十个(CPU 多进程) |
| 物理引擎 | PhysX 5(GPU 加速) | MuJoCo 引擎(CPU) |
| 训练速度 | 极快(分钟级完成简单任务) | 较慢(需要更多墙钟时间) |
| 渲染 | 光线追踪(RTX) | OpenGL 渲染 |
| 接触模型 | GPU 加速软接触 | 高精度凸优化接触 |
| 学习曲线 | 较陡(依赖 Isaac Sim 生态) | 较平缓(API 简洁) |
| 适用场景 | 大规模并行训练、Sim2Real | 研究原型、精确物理仿真 |
如果你需要快速大规模训练并最终部署到真实机器人,优先选择 IsaacLab;如果你在进行算法研究或需要精确的物理仿真对比,MuJoCo 仍然是优秀选择。
系统要求与安装
硬件要求
- GPU:NVIDIA RTX 2070 及以上(推荐 RTX 3080/4080 或更高)
- 显存:至少 8GB(推荐 12GB+,环境数量与显存成正比)
- 内存:32GB+ 推荐
- 系统:Ubuntu 22.04(官方支持),Python 3.10+
安装概览
# 第一步:安装 Isaac Sim(通过 pip 安装,推荐方式) # 创建 conda 环境 conda create -n isaaclab python=3.10 -y conda activate isaaclab # 安装 Isaac Sim pip install isaacsim-rl # 第二步:克隆并安装 IsaacLab git clone https://github.com/isaac-sim/IsaacLab.git cd IsaacLab # 安装 IsaacLab 及其依赖 ./isaaclab.sh --install # 第三步:验证安装 python -c "import isaaclab; print(isaaclab.__version__)" # 运行示例以确认一切正常 python scripts/tutorials/00_sim/create_empty.py
IsaacLab 对驱动版本有严格要求。请确保 NVIDIA 驱动版本 >= 535,CUDA >= 12.0。安装前请查阅官方文档确认兼容性。
架构总览
┌─────────────────────────────────────────────────────────┐ │ RL 训练库层 │ │ ┌──────────┐ ┌──────────┐ ┌──────┐ ┌───────────────┐ │ │ │ RSL-RL │ │ rl_games │ │ skrl │ │ Stable-Bases3 │ │ │ └─────┬────┘ └─────┬────┘ └──┬───┘ └───────┬───────┘ │ │ └────────────┬┴────────┘ │ │ │ ▼ ▼ │ ├─────────────────────────────────────────────────────────┤ │ IsaacLab 层 │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 任务(Tasks) / 环境(Envs) / 管理器(Managers) │ │ │ │ ├── ObservationManager (观测管理) │ │ │ │ ├── ActionManager (动作管理) │ │ │ │ ├── RewardManager (奖励管理) │ │ │ │ └── TerminationManager (终止管理) │ │ │ └──────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ InteractiveScene (交互式场景) │ │ │ │ ├── ArticulationCfg (关节体配置) │ │ │ │ ├── RigidObjectCfg (刚体配置) │ │ │ │ └── SensorCfg (传感器配置) │ │ │ └──────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ Isaac Sim 层 │ │ ┌──────────────────────────────────────────────────┐ │ │ │ PhysX 5 (GPU物理) │ USD 场景 │ RTX 渲染 │ │ │ └──────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ NVIDIA Omniverse 平台 │ └─────────────────────────────────────────────────────────┘
核心概念详解
1. 环境类型:ManagerBasedRLEnv vs DirectRLEnv
IsaacLab 提供两种环境编写方式:
| 类型 | ManagerBasedRLEnv | DirectRLEnv |
|---|---|---|
| 设计理念 | 配置驱动,声明式 | 代码驱动,命令式 |
| 适用场景 | 标准任务、快速原型 | 高度自定义任务 |
| 灵活性 | 通过 Manager 组合 | 完全自由控制 |
| 推荐 | 初学者首选 | 高级用户 |
2. InteractiveScene(交互式场景)
场景是所有仿真对象的容器,使用配置类来定义:
import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.scene import InteractiveSceneCfg @configclass class MySceneCfg(InteractiveSceneCfg): """自定义场景配置""" # 地面平面 ground = AssetBaseCfg( prim_path="/World/ground", spawn=sim_utils.GroundPlaneCfg(), ) # 机械臂(关节体) robot = ArticulationCfg( prim_path="/World/envs/env_.*/Robot", spawn=sim_utils.UsdFileCfg( usd_path="path/to/robot.usd", ), actuators={ "arm": ImplicitActuatorCfg( joint_names_expr=["joint_.*"], stiffness=400.0, damping=80.0, ), }, )
3. Manager 管理器系统
IsaacLab 的核心设计是将环境的各个方面拆分为独立的管理器:
- ObservationManager:定义智能体能观察到的状态信息(关节角度、末端位置等)
- ActionManager:定义动作空间和如何将网络输出映射到执行器命令
- RewardManager:定义多个奖励项及其权重
- TerminationManager:定义回合终止条件(超时、跌倒等)
- CurriculumManager:定义课程学习策略(逐步增加难度)
4. 向量化环境
在 IsaacLab 中,所有环境实例共享同一个 GPU 物理世界。4096 个环境的 step() 只需一次 GPU kernel 调用,而非 4096 次独立仿真。这使得数据吞吐量达到每秒数百万步。
创建自定义任务
以机械臂末端到达目标位置(Reach Task)为例,展示完整的任务创建流程:
from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.managers import ( ObservationGroupCfg, ObservationTermCfg, RewardTermCfg, SceneEntityCfg, TerminationTermCfg, ) import isaaclab.envs.mdp as mdp ## ---- 观测配置 ---- ## @configclass class ObservationsCfg: """观测空间配置""" @configclass class PolicyCfg(ObservationGroupCfg): # 关节位置(归一化到 [-1, 1]) joint_pos = ObservationTermCfg( func=mdp.joint_pos_rel ) # 关节速度 joint_vel = ObservationTermCfg( func=mdp.joint_vel_rel ) # 目标位置(相对于末端执行器) target_pos = ObservationTermCfg( func=mdp.generated_commands, params={"command_name": "ee_pose"}, ) policy: PolicyCfg = PolicyCfg() ## ---- 奖励配置 ---- ## @configclass class RewardsCfg: """奖励函数配置""" # 主要奖励:末端接近目标 reaching_target = RewardTermCfg( func=mdp.rew_reaching_target, weight=1.0, ) # 惩罚项:动作幅度过大 action_penalty = RewardTermCfg( func=mdp.rew_action_rate_l2, weight=-0.01, ) # 惩罚项:关节速度过快 joint_vel_penalty = RewardTermCfg( func=mdp.rew_joint_vel_l2, weight=-0.001, ) ## ---- 终止条件配置 ---- ## @configclass class TerminationsCfg: # 超时终止 time_out = TerminationTermCfg( func=mdp.time_out, time_out=True )
使用 RSL-RL 训练
RSL-RL 是苏黎世联邦理工学院机器人系统实验室开发的轻量级 RL 库,是 IsaacLab 中四足运动训练的标准选择。
# RSL-RL 的 PPO 配置示例 from rsl_rl.runners import OnPolicyRunner from rsl_rl.algorithms import PPO from rsl_rl.modules import ActorCritic # PPO 训练参数配置 ppo_cfg = { "value_loss_coef": 1.0, "use_clipped_value_loss": True, "clip_param": 0.2, # PPO 裁剪范围 "entropy_coef": 0.01, # 熵正则化系数 "num_learning_epochs": 5, # 每次更新的 epoch 数 "num_mini_batches": 4, # mini-batch 数量 "learning_rate": 1e-3, # 学习率 "gamma": 0.99, # 折扣因子 "lam": 0.95, # GAE lambda 参数 "max_grad_norm": 1.0, # 梯度裁剪 } # 网络结构配置 policy_cfg = { "actor_hidden_dims": [256, 256, 128], "critic_hidden_dims": [256, 256, 128], "activation": "elu", }
# 启动训练(机械臂到达任务) python source/standalone/workflows/rsl_rl/train.py \ --task Isaac-Reach-Franka-v0 \ --num_envs 4096 \ --headless # 使用 TensorBoard 查看训练曲线 tensorboard --logdir logs/rsl_rl/Isaac-Reach-Franka-v0 # 评估训练好的策略(带可视化) python source/standalone/workflows/rsl_rl/play.py \ --task Isaac-Reach-Franka-v0 \ --num_envs 32
四足运动训练简介
IsaacLab 最经典的应用场景之一是四足机器人运动控制。典型流程如下:
第一步:选择机器人模型
从 IsaacLab 内置资产或 URDF/USD 导入(如 Unitree Go1/Go2、ANYmal 等)
第二步:定义运动任务
配置速度跟踪命令(前进/转弯速度),定义观测空间(IMU、关节角、关节速度、指令)
第三步:设计奖励函数
线速度跟踪 + 角速度跟踪 + 步态惩罚 + 能量惩罚 + 平滑性惩罚
第四步:域随机化
随机化摩擦、质量、电机力矩、地形等参数,为 Sim2Real 做准备
第五步:训练与评估
使用 RSL-RL + PPO 在 4096 个并行环境中训练,约 1000 轮迭代收敛
实用技巧
将复杂目标拆分为多个小的奖励项,并仔细调节权重。例如,四足运动可能包含 10-15 个奖励项。使用 TensorBoard 单独监控每个奖励项的贡献。
从简单任务开始逐步增加难度。例如:先在平坦地面训练行走,再引入不平坦地形、台阶、斜坡。
在 IsaacLab 中通过 EventTermCfg 配置随机化参数。建议从小范围开始,逐步扩大随机化范围,观察训练曲线变化。
请解释 IsaacLab 中 ManagerBasedRLEnv 和 DirectRLEnv 的区别。在什么场景下你会选择使用 DirectRLEnv?
ManagerBasedRLEnv 采用配置驱动的方式,通过定义各种 Manager(观测、动作、奖励、终止)的配置类来构建环境。它更加模块化、易于维护。
DirectRLEnv 采用代码驱动的方式,你需要直接重写 _pre_physics_step()、_get_observations()、_get_rewards() 等方法。
选择 DirectRLEnv 的场景:
- 需要在 step 中执行复杂的自定义逻辑
- 观测/奖励之间有复杂依赖关系,难以用独立 Manager 表达
- 需要完全控制仿真循环的执行顺序
- 移植现有的非 IsaacLab 环境代码
编写一个 IsaacLab 的 RewardsCfg 配置类,为四足运动任务定义以下奖励项:线速度跟踪(权重 1.0)、角速度跟踪(权重 0.5)、关节加速度惩罚(权重 -2.5e-7)、动作变化率惩罚(权重 -0.01)。
@configclass class RewardsCfg: """四足运动奖励配置""" # 线速度跟踪奖励 track_lin_vel_xy = RewardTermCfg( func=mdp.track_lin_vel_xy_exp, weight=1.0, params={"std": 0.25}, ) # 角速度跟踪奖励 track_ang_vel_z = RewardTermCfg( func=mdp.track_ang_vel_z_exp, weight=0.5, params={"std": 0.25}, ) # 关节加速度惩罚(防止抖动) joint_accel = RewardTermCfg( func=mdp.rew_joint_acc_l2, weight=-2.5e-7, ) # 动作变化率惩罚(保证平滑) action_rate = RewardTermCfg( func=mdp.rew_action_rate_l2, weight=-0.01, )
如果在 IsaacLab 训练中发现策略收敛很慢(1000 轮迭代后奖励仍在低水平),请列出至少 5 个可能的排查方向和对应的解决策略。
排查方向与解决策略:
- 奖励函数设计:检查各奖励项是否冲突、权重是否合理。使用 TensorBoard 分别查看每个奖励项的曲线。尝试简化奖励函数,先只保留核心项。
- 观测空间:检查观测是否包含足够信息。确认观测值是否被正确归一化。添加观测历史(observation history)可能帮助策略学习动态特征。
- 超参数调节:降低学习率(如 1e-3 → 3e-4),增加 mini-batch 数量,调整 GAE lambda 值,增大 entropy 系数鼓励探索。
- 动作空间:确认动作范围是否合理。位置目标空间通常比力矩空间更容易学习。检查动作缩放因子。
- 环境参数:检查仿真步长是否合适(推荐 decimation=4,dt=1/200s)。确认初始状态是否合理。域随机化范围过大会导致早期训练困难。
- 网络结构:尝试不同的网络大小([256,128,64] 或 [512,256,128])。确认激活函数适合任务。
- 课程学习:引入课程学习,从简单版本开始训练,逐步增加难度。
测验 1
IsaacLab 相比传统 CPU 仿真(如 MuJoCo + 多进程)的核心加速原理是什么?
解析:IsaacLab 的核心加速来源于 GPU 并行化。所有环境实例共享同一个物理世界,仿真和策略推理均在 GPU 上完成。这避免了传统方案中 CPU 仿真 → GPU 推理 → CPU 仿真的数据传输瓶颈,实现了端到端的 GPU 加速。
测验 2
在 IsaacLab 的 ManagerBasedRLEnv 中,如果要为四足机器人添加"脚部滑动惩罚",应该在哪个配置类中定义?
解析:脚部滑动惩罚是一个奖励项(负奖励即惩罚),应在 RewardsCfg 中定义。通过 RewardTermCfg 设置对应的函数和负权重即可。例如 foot_slip = RewardTermCfg(func=mdp.rew_foot_slip, weight=-0.05)。
Sim2Real 全流程
什么是 Sim2Real 差距?
Sim2Real(仿真到真实)是将在仿真环境中训练好的策略部署到真实机器人上的过程。由于仿真与现实之间存在不可避免的差距,直接迁移往往导致策略失效。
差距来源
| 差距类型 | 具体表现 | 影响程度 |
|---|---|---|
| 物理参数不匹配 | 摩擦系数、质量分布、关节阻尼与真实不同 | 高 |
| 执行器延迟 | 电机响应时间、通信延迟(1-10ms) | 高 |
| 传感器噪声 | IMU 漂移、编码器精度、力传感器噪声 | 中 |
| 接触模型差异 | 仿真中的软接触 vs 真实刚性碰撞 | 中 |
| 环境差异 | 地面不平整、风力干扰等未建模因素 | 低-中 |
Sim2Real 流程总览
┌──────────────────────────────────────────────────────────────────────┐ │ Sim2Real 完整流程 │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │ │ │ 系统辨识 │───▶│ 仿真环境构建 │───▶│ 策略训练(含域随机化) │ │ │ │(System ID) │ │ (IsaacLab) │ │ (PPO / SAC) │ │ │ └─────────────┘ └──────────────┘ └───────────┬─────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │ │ │ 真机部署 │◀───│ 策略导出 │◀───│ 仿真评估与验证 │ │ │ │(ROS2/实时) │ │(JIT / ONNX) │ │ (多种条件下测试) │ │ │ └──────┬──────┘ └──────────────┘ └─────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────┐ │ │ │ 反馈迭代:收集真机数据 │ │ │ │ 调整仿真参数 → 重新训练 │ │ │ └─────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────┘
域随机化(Domain Randomization)
域随机化是目前最广泛使用的 Sim2Real 方法。核心思想:如果策略能在大范围随机化的仿真参数下都表现良好,那么真实世界的参数大概率落在这个范围内。
需要随机化的参数
物理参数
- 摩擦系数:0.5 - 1.5
- 机身质量:±15%
- 连杆质量:±10%
- 质心偏移:±2cm
- 关节阻尼:±30%
执行器参数
- 电机力矩限制:±20%
- PD 增益:±25%
- 动作延迟:0-20ms
- 电机摩擦:±30%
传感器噪声
- 关节位置噪声:±0.01rad
- 关节速度噪声:±1.5rad/s
- IMU 角速度偏置
- IMU 加速度噪声
在 IsaacLab 中实现域随机化
from isaaclab.managers import EventTermCfg import isaaclab.envs.mdp as mdp @configclass class RandomizationCfg: """域随机化配置""" # 每次重置时随机化物理参数 randomize_friction = EventTermCfg( func=mdp.randomize_rigid_body_material, mode="reset", params={ "asset_cfg": SceneEntityCfg("robot"), "static_friction_range": (0.5, 1.5), "dynamic_friction_range": (0.4, 1.2), }, ) # 随机化机体质量 randomize_mass = EventTermCfg( func=mdp.randomize_rigid_body_mass, mode="reset", params={ "asset_cfg": SceneEntityCfg("robot"), "mass_distribution_params": (-0.5, 0.5), "operation": "add", }, ) # 每步添加动作噪声 add_action_noise = EventTermCfg( func=mdp.add_noise_to_action, mode="interval", params={ "noise_range": (-0.02, 0.02), }, )
范围太小无法覆盖真实世界的变化;范围太大会让训练变得极其困难甚至无法收敛。建议从小范围开始,逐步扩大,同时监控训练曲线。课程学习策略也可以应用于域随机化的范围。
系统辨识(System Identification)
系统辨识的目标是让仿真尽可能精确地匹配真实硬件,从而缩小需要域随机化弥补的差距。
步骤 1:测量硬件参数
使用电子秤测量各连杆质量,用 CAD 模型计算惯量,测量关节摩擦和阻尼系数
步骤 2:执行器建模
测量电机的力矩-速度曲线、阶跃响应延迟、PD 控制器的实际增益
步骤 3:对比验证
在真机上执行固定轨迹(如正弦波运动),对比仿真和真实的关节角度/力矩曲线
步骤 4:迭代调整
根据差异调整仿真参数,重复步骤 3,直到仿真与真实数据高度匹配
观测设计与可迁移性
观测空间的设计直接影响策略能否从仿真迁移到真实世界。
推荐的本体感知观测
- 关节位置(相对于默认姿态的偏移)
- 关节速度(可在真机上稳定获取)
- 机身角速度(来自 IMU 陀螺仪)
- 重力投影向量(代替姿态角,避免万向锁)
- 上一步动作(帮助策略学习动态特性)
- 速度指令(目标前进/转弯速度)
避免使用在真机上难以获取或噪声很大的信号,如:线速度(需要状态估计器)、绝对位置(需要外部定位)、接触力(传感器昂贵且不精确)。
观测历史堆叠
将最近 N 步的观测拼接在一起,帮助策略推断系统动态特性(如延迟、惯性):
常见设置为 N = 3~10,这能显著提升 Sim2Real 的成功率,因为策略能隐式地估计不可观测的状态。
观测设计原则
- 对称性:确保观测空间在机器人左右对称的情况下也是对称的
- 归一化:将所有观测值归一化到 [-1, 1] 范围
- 可获取性:仿真中使用的每一个观测信号都必须能从真实传感器获取
- 低噪声:优先选择噪声小、精度高的传感信号
- 观测延迟模拟:在仿真中添加 1-2 步的观测延迟,模拟真机通信延迟
奖励函数设计(面向真实部署)
为了使训练出的策略适合真实部署,奖励函数需要特别关注运动质量:
# 面向 Sim2Real 的奖励函数设计示例 rewards = { # === 主任务奖励 === "lin_vel_tracking": 1.0, # 线速度跟踪 "ang_vel_tracking": 0.5, # 角速度跟踪 # === 运动质量惩罚(关键!) === "action_rate": -0.01, # 动作变化率 - 防抖动 "joint_accel": -2.5e-7, # 关节加速度 - 防抖动 "torque_penalty": -1e-5, # 力矩惩罚 - 节省电机 "foot_slip": -0.05, # 脚部滑动 - 防打滑 # === 姿态稳定性 === "base_height": -1.0, # 躯干高度偏离期望值 "orientation": -1.0, # 躯干姿态偏离水平 # === 能量效率 === "energy": -1e-4, # 总能量消耗 }
真实电机无法执行仿真中的高频抖动动作。action_rate 和 joint_accel 惩罚项是 Sim2Real 成功的关键因素之一。如果缺少这些惩罚,策略通常会学到高频振动行为,在真机上立刻失效。
动作空间设计
| 动作类型 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 位置目标 | 简单稳定,低层 PD 控制器保证安全 | 动态性能受 PD 限制 | 大多数 Sim2Real 应用(推荐) |
| 位置增量 | 相对控制,更平滑 | 可能积累误差 | 需要精细控制时 |
| 力矩命令 | 最大灵活性和动态性能 | 迁移困难,需精确电机模型 | 高性能需求且有精确模型时 |
部署到真实机器人
策略导出
import torch # 加载训练好的策略 policy = torch.load("logs/model_5000.pt") policy.eval() # 方法一:TorchScript 导出(推荐,保留 PyTorch 生态) dummy_obs = torch.zeros(1, obs_dim) traced_policy = torch.jit.trace(policy, dummy_obs) traced_policy.save("policy_deployed.pt") # 方法二:ONNX 导出(跨平台部署) torch.onnx.export( policy, dummy_obs, "policy_deployed.onnx", input_names=["observation"], output_names=["action"], dynamic_axes={"observation": {0: "batch"}}, )
实时控制回路
import torch import time class RealRobotController: """真实机器人控制器""" def __init__(self, policy_path, control_freq=50): self.policy = torch.jit.load(policy_path) self.dt = 1.0 / control_freq self.obs_history = [] self.last_action = None def get_observation(self): """从真实传感器获取观测""" joint_pos = self.robot.get_joint_positions() joint_vel = self.robot.get_joint_velocities() imu_data = self.imu.get_angular_velocity() gravity_vec = self.imu.get_projected_gravity() command = self.joystick.get_velocity_command() obs = torch.tensor([ *joint_pos, *joint_vel, *imu_data, *gravity_vec, *command, *self.last_action, ]) return obs def run(self): """主控制循环""" while True: t_start = time.time() # 1. 读取观测 obs = self.get_observation() # 2. 策略推理 with torch.no_grad(): action = self.policy(obs.unsqueeze(0)).squeeze() # 3. 安全检查 action = self.safety_check(action) # 4. 发送指令到电机 self.robot.set_joint_targets(action) self.last_action = action # 5. 控制频率同步 elapsed = time.time() - t_start if elapsed < self.dt: time.sleep(self.dt - elapsed)
- 关节限位保护:在发送指令前必须检查关节角度是否在安全范围内
- 力矩限制:限制最大输出力矩,防止电机过载
- 急停机制:配备硬件急停按钮,确保任何时候都能立即切断电机
- 低速启动:首次测试时从极低速度指令开始,逐步增加
- 软件看门狗:如果控制循环卡死或延迟过大,自动进入安全姿态
案例:四足运动 Sim2Real 流程
阶段 1:仿真环境搭建
在 IsaacLab 中导入机器人 URDF/USD,配置执行器参数、物理参数。先在无随机化条件下验证仿真行为合理。
阶段 2:策略训练
设计观测/动作/奖励空间,添加域随机化,使用 RSL-RL + PPO 训练。4096 并行环境约 30-60 分钟收敛。
阶段 3:仿真验证
在多种随机化参数下测试策略鲁棒性,测试不同速度指令、推力干扰、不平地面等。
阶段 4:真机测试
导出策略到 TorchScript,部署到机载计算机(如 Jetson Orin)。从站立开始,先测试站稳,再尝试行走。
阶段 5:迭代优化
收集真机数据,对比仿真差异,调整物理参数和域随机化范围。通常需 2-5 轮迭代。
常见失败模式与解决方案
| 失败现象 | 可能原因 | 解决方案 |
|---|---|---|
| 真机上关节剧烈抖动 | 缺少动作平滑惩罚 | 增大 action_rate 惩罚权重,添加低通滤波器 |
| 机器人站起后立即摔倒 | 物理参数差距过大 | 重新进行系统辨识,扩大域随机化范围 |
| 行走方向偏移 | IMU 偏置未校准 | 校准 IMU,训练中添加质心偏移随机化 |
| 地面打滑无法前进 | 仿真摩擦系数偏高 | 降低训练摩擦系数下界,添加脚部滑动惩罚 |
| 控制延迟导致不稳定 | 真机延迟大于仿真 | 增大动作延迟随机化范围(如 10-40ms) |
列出 Sim2Real 差距的三个主要来源,并解释为什么域随机化能帮助缓解这些差距。
三个主要来源:
- 物理参数不匹配:仿真中的摩擦系数、质量、阻尼等参数与真实值不完全一致。
- 执行器模型不精确:真实电机有延迟、摩擦和非线性特性,仿真难以完美建模。
- 传感器噪声:真实传感器有噪声、漂移和量化误差,仿真中数据通常是"完美"的。
域随机化的作用:通过在训练时大范围随机化这些参数,策略被迫学习对参数变化具有鲁棒性的行为。当部署到真机时,真实参数大概率落在随机化范围内,策略因此能够正常工作。本质上是用"宽泛的仿真"代替"精确的仿真"。
设计一个四足运动策略的观测空间,列出所有观测分量,说明每个分量的维度和选择理由。要求适合 Sim2Real 迁移。
以 12 自由度四足机器人(每条腿 3 个关节)为例:
| 观测分量 | 维度 | 选择理由 |
|---|---|---|
| 关节位置(相对默认) | 12 | 本体感知核心,编码器直接获取 |
| 关节速度 | 12 | 动态信息,编码器差分可得 |
| 机身角速度 | 3 | IMU 陀螺仪,高精度低延迟 |
| 投影重力向量 | 3 | 代替欧拉角,避免万向锁 |
| 速度指令 | 3 | vx, vy, yaw_rate 三维指令 |
| 上一步动作 | 12 | 帮助推断执行器延迟特性 |
总维度:45。如果添加 3 步历史堆叠,最终观测维度为 45 x 3 = 135。所有信号在真机上均可通过板载传感器直接获取。
假设你将四足运动策略部署到真实机器人后,发现机器人行走时腿部出现高频抖动。请分析可能的原因,并提出至少 3 种解决方案。
可能原因:
- 奖励函数中缺少足够的动作平滑惩罚,策略学到了高频动作
- 仿真中的执行器模型过于理想化,与真实电机延迟不匹配
- PD 控制器增益在仿真和真机上不一致
解决方案:
- 训练端 - 增加平滑惩罚:提高
action_rate和joint_accel的惩罚权重 - 训练端 - 添加动作延迟:在仿真中引入 1-3 步的随机动作延迟
- 部署端 - 低通滤波:在策略输出后加一阶滤波器:
a_filtered = alpha * a_new + (1-alpha) * a_prev - 部署端 - 降低控制频率:将控制频率从 100Hz 降至 50Hz
- 训练端 - 限制动作范围:缩小 action_scale,限制每步关节角度变化幅度
测验 1
以下哪种域随机化策略对纯本体感知的四足运动 Sim2Real 迁移最不重要?
解析:对于纯本体感知(不使用视觉)的策略,光照和材质颜色的随机化毫无作用,因为策略不依赖视觉输入。物理参数(摩擦、力矩、质量)的随机化才是关键。
测验 2
在将 RL 策略部署到真实机器人时,以下哪项安全措施最为关键?
解析:安全性是真机部署的首要考虑。关节限位保护防止电机过转导致硬件损坏,硬件急停机制确保在策略异常时能立即切断电源保护设备和人员。
实战项目
本节包含三个难度递增的实战项目,从经典控制问题到工业级机器人训练,帮助你将前面章节的知识融会贯通。
项目 1:CartPole + PPO 从零实现
入门级项目目标
- 从零实现 PPO 算法(不依赖第三方 RL 库)
- 在 Gymnasium 的 CartPole-v1 环境上训练并解决任务
- 理解 PPO 每一步的实现细节
预期成果
策略在约 300 个 episode 内达到 500 分(满分),训练总时间 < 2 分钟。
第一步:环境与网络定义
import torch import torch.nn as nn import torch.optim as optim import numpy as np import gymnasium as gym class ActorCritic(nn.Module): """Actor-Critic 网络:共享特征提取层""" def __init__(self, obs_dim, act_dim): super().__init__() # 共享特征提取层 self.shared = nn.Sequential( nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), ) # Actor 头:输出动作概率 self.actor = nn.Linear(64, act_dim) # Critic 头:输出状态价值 self.critic = nn.Linear(64, 1) def forward(self, x): features = self.shared(x) return self.actor(features), self.critic(features) def get_action(self, obs): """采样动作并返回 log 概率和价值估计""" logits, value = self.forward(obs) dist = torch.distributions.Categorical(logits=logits) action = dist.sample() return action, dist.log_prob(action), value.squeeze()
第二步:PPO 核心更新
def ppo_update(policy, optimizer, states, actions, old_log_probs, returns, advantages, clip_eps=0.2, epochs=4): """PPO 策略更新""" for _ in range(epochs): logits, values = policy(states) dist = torch.distributions.Categorical(logits=logits) new_log_probs = dist.log_prob(actions) # 计算概率比 ratio = torch.exp(new_log_probs - old_log_probs) # PPO 裁剪目标 surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages actor_loss = -torch.min(surr1, surr2).mean() # 价值函数损失 critic_loss = (returns - values.squeeze()).pow(2).mean() # 熵奖励(鼓励探索) entropy = dist.entropy().mean() # 总损失 loss = actor_loss + 0.5 * critic_loss - 0.01 * entropy optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(policy.parameters(), 0.5) optimizer.step()
第三步:训练循环
# 初始化 env = gym.make("CartPole-v1") policy = ActorCritic(obs_dim=4, act_dim=2) optimizer = optim.Adam(policy.parameters(), lr=3e-4) # 训练主循环 for episode in range(500): states, actions, rewards_list = [], [], [] log_probs, values = [], [] obs, _ = env.reset() done = False ep_reward = 0 while not done: obs_t = torch.FloatTensor(obs) action, log_prob, value = policy.get_action(obs_t) next_obs, reward, terminated, truncated, _ = env.step(action.item()) done = terminated or truncated states.append(obs_t) actions.append(action) rewards_list.append(reward) log_probs.append(log_prob) values.append(value) obs = next_obs ep_reward += reward # 计算 GAE 优势和回报后执行 PPO 更新 # ...(此处省略 GAE 计算,参见完整代码) if episode % 20 == 0: print(f"Episode {episode}: 奖励 = {ep_reward}")
如果训练不收敛:(1) 检查优势函数是否正确归一化;(2) 确认 clip_eps 设为 0.2;(3) 尝试降低学习率到 1e-4。CartPole 是最简单的测试环境,如果这里不工作,代码一定有 bug。
项目 2:MuJoCo Ant 运动控制
进阶级项目目标
- 使用 SAC 算法训练 Ant-v4(四足蚂蚁)学会行走
- 掌握连续动作空间的 RL 训练
- 学会超参数调优和训练曲线分析
预期成果
Ant-v4 在 100 万步后平均回报达到 3000+,蚂蚁能稳定向前行走。
第一步:环境配置与 SAC 训练
import gymnasium as gym from stable_baselines3 import SAC from stable_baselines3.common.callbacks import EvalCallback # 创建环境 env = gym.make("Ant-v4") # 配置 SAC 算法 model = SAC( "MlpPolicy", env, learning_rate=3e-4, buffer_size=1_000_000, # 经验池大小 batch_size=256, # 批量大小 tau=0.005, # 目标网络软更新系数 gamma=0.99, # 折扣因子 train_freq=1, # 每步都训练 gradient_steps=1, # 每步梯度更新次数 ent_coef="auto", # 自动调节熵系数 policy_kwargs=dict( net_arch=[256, 256], # 网络结构 ), verbose=1, tensorboard_log="./logs/ant_sac/", ) # 设置评估回调 eval_callback = EvalCallback( gym.make("Ant-v4"), eval_freq=10000, n_eval_episodes=5, best_model_save_path="./logs/ant_sac/best/", ) # 开始训练 model.learn( total_timesteps=1_000_000, callback=eval_callback, )
第二步:超参数调优指南
| 超参数 | 推荐值 | 调优建议 |
|---|---|---|
| learning_rate | 3e-4 | 如果训练不稳定可降至 1e-4 |
| batch_size | 256 | 越大越稳定,但速度变慢 |
| buffer_size | 1e6 | 确保足够存储多样化经验 |
| tau | 0.005 | 太大导致不稳定,太小收敛慢 |
| net_arch | [256, 256] | Ant 任务不需要太大网络 |
第三步:训练曲线分析
# 查看训练曲线 tensorboard --logdir ./logs/ant_sac/ # 评估最佳模型 python -c " from stable_baselines3 import SAC import gymnasium as gym model = SAC.load('./logs/ant_sac/best/best_model') env = gym.make('Ant-v4', render_mode='human') obs, _ = env.reset() for _ in range(1000): action, _ = model.predict(obs, deterministic=True) obs, reward, done, truncated, _ = env.step(action) if done or truncated: obs, _ = env.reset() "
Ant-v4 的奖励由多项组成:前进速度奖励、存活奖励、控制代价惩罚。如果蚂蚁学会站着不动(只拿存活奖励),说明前进奖励的权重可能不够,或探索不足。可以尝试增大熵系数的初始值。
项目 3:IsaacLab 四足运动
实战级项目目标
- 在 IsaacLab 中训练四足机器人在平坦地面上行走
- 使用 RSL-RL + PPO 进行大规模并行训练
- 实践完整的奖励设计、域随机化配置
预期成果
四足机器人能跟踪速度指令稳定行走,训练约 1000 轮迭代(GPU 上约 20-40 分钟)。
第一步:任务配置
from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.managers import ( ObservationGroupCfg, ObservationTermCfg, RewardTermCfg, TerminationTermCfg, EventTermCfg, SceneEntityCfg, ) from isaaclab.assets import ArticulationCfg import isaaclab.envs.mdp as mdp # ---- 场景配置 ---- @configclass class QuadrupedSceneCfg(InteractiveSceneCfg): # 地面 ground = AssetBaseCfg( prim_path="/World/ground", spawn=sim_utils.GroundPlaneCfg(), ) # 四足机器人(以 Unitree Go2 为例) robot = ArticulationCfg( prim_path="/World/envs/env_.*/Robot", spawn=sim_utils.UsdFileCfg( usd_path="datasets/robots/unitree/go2/go2.usd", ), actuators={ "legs": ImplicitActuatorCfg( joint_names_expr=[".*_hip_joint", ".*_thigh_joint", ".*_calf_joint"], stiffness=25.0, damping=0.5, ), }, )
第二步:奖励函数设计
@configclass class RewardsCfg: """四足运动完整奖励配置""" # 主要目标:跟踪速度指令 track_lin_vel = RewardTermCfg( func=mdp.track_lin_vel_xy_exp, weight=1.5, params={"std": 0.25}, ) track_ang_vel = RewardTermCfg( func=mdp.track_ang_vel_z_exp, weight=0.75, params={"std": 0.25}, ) # 运动平滑性惩罚 action_rate = RewardTermCfg(func=mdp.rew_action_rate_l2, weight=-0.01) joint_accel = RewardTermCfg(func=mdp.rew_joint_acc_l2, weight=-2.5e-7) # 能量和安全惩罚 torques = RewardTermCfg(func=mdp.rew_joint_torques_l2, weight=-1e-5) feet_slip = RewardTermCfg(func=mdp.rew_feet_slip, weight=-0.05) # 姿态惩罚 lin_vel_z = RewardTermCfg(func=mdp.rew_lin_vel_z_l2, weight=-2.0) ang_vel_xy = RewardTermCfg(func=mdp.rew_ang_vel_xy_l2, weight=-0.05)
第三步:训练与评估
# 启动四足运动训练 python source/standalone/workflows/rsl_rl/train.py \ --task Isaac-Velocity-Flat-Unitree-Go2-v0 \ --num_envs 4096 \ --max_iterations 1500 \ --headless # 可视化评估 python source/standalone/workflows/rsl_rl/play.py \ --task Isaac-Velocity-Flat-Unitree-Go2-v0 \ --num_envs 64 # 查看训练日志 tensorboard --logdir logs/rsl_rl/
- 机器人起飞/穿模:检查 USD 模型的碰撞体设置是否正确
- 原地不动:增大速度跟踪奖励权重,检查动作缩放是否合理
- 剧烈抖动:增大 action_rate 和 joint_accel 惩罚
- 摔倒后无法恢复:检查终止条件和重置逻辑
项目总结与进阶
项目 1 完成后
你已理解 PPO 的核心实现。尝试将其扩展到连续动作空间(Pendulum-v1)。
项目 2 完成后
你已掌握 MuJoCo 连续控制训练。尝试对比 SAC 和 PPO 在 Ant 上的表现差异。
项目 3 完成后
你已具备工业级训练能力。下一步是添加域随机化并尝试 Sim2Real 迁移。
资源与参考
本节汇总了机器人强化学习领域最有价值的学习资源,按类别整理并附带推荐理由,帮助你高效地深入学习。
经典教材
| 书名 | 作者 | 推荐理由 | 难度 |
|---|---|---|---|
| Reinforcement Learning: An Introduction (2nd Ed.) | Sutton & Barto | RL 领域的"圣经",理论基础必读。免费在线版本可获取 | 入门-中级 |
| Spinning Up in Deep RL | OpenAI (Josh Achiam) | 最佳实践入门指南,理论与代码并重,含完整算法实现 | 入门 |
| Decision Making Under Uncertainty | Mykel Kochenderfer | 从决策论角度理解 RL,数学推导严谨 | 中级-高级 |
| Robotics: Modelling, Planning and Control | Siciliano et al. | 机器人学基础教材,理解机器人动力学的必备参考 | 中级 |
在线课程
David Silver RL 课程
机构:UCL / DeepMind
经典 RL 入门课程,涵盖 MDP、动态规划、MC、TD、策略梯度等核心概念。10 节课视频,每节约 1.5 小时。
推荐指数:必看
CS285: Deep RL
机构:UC Berkeley (Sergey Levine)
深度强化学习前沿课程,涵盖 model-based RL、离线 RL、多任务 RL 等高级主题。有完整的课程视频和作业。
推荐指数:进阶必看
Hugging Face Deep RL 课程
机构:Hugging Face
免费在线课程,动手实践为主,使用 Stable-Baselines3 和 Unity ML-Agents。适合喜欢边做边学的同学。
推荐指数:实践入门首选
CS236: Robot Learning
机构:Stanford
专注于机器人学习的课程,包括模仿学习、Sim2Real 等主题,与本教程高度相关。
推荐指数:机器人方向推荐
核心代码仓库
| 仓库 | 用途 | 特点 |
|---|---|---|
| CleanRL | 单文件 RL 算法实现 | 每个算法一个文件,极易阅读和理解。学习算法实现的最佳起点 |
| Stable-Baselines3 | 生产级 RL 库 | API 规范、文档完善、社区活跃。适合快速实验和基准测试 |
| RSL-RL | 机器人运动 RL | ETH 开发,专为四足运动优化,IsaacLab 的标准训练库 |
| IsaacLab | GPU 并行仿真训练 | NVIDIA 官方框架,大规模并行训练的首选 |
| rl_games | 高性能 RL 训练 | 针对 GPU 仿真优化的 RL 库,速度极快 |
| skrl | 模块化 RL 库 | 支持多种仿真器后端(Isaac、Omniverse、Gymnasium) |
| MuJoCo Menagerie | 机器人模型集合 | 高质量 MuJoCo 模型库(含四足、双足、机械臂等) |
重要论文
算法基础
- PPO: "Proximal Policy Optimization Algorithms" (Schulman et al., 2017) -- 最广泛使用的策略梯度算法
- SAC: "Soft Actor-Critic: Off-Policy Maximum Entropy Deep RL" (Haarnoja et al., 2018) -- 最优秀的 off-policy 连续控制算法
- GAE: "High-Dimensional Continuous Control Using Generalized Advantage Estimation" (Schulman et al., 2016) -- PPO 的核心组件
Sim2Real 与机器人
- Domain Randomization: "Domain Randomization for Transferring Deep Neural Networks from Simulation to the Real World" (Tobin et al., 2017)
- 四足运动: "Learning Agile and Dynamic Motor Skills for Legged Robots" (Hwangbo et al., 2019) -- ANYmal 运动训练经典论文
- 敏捷运动: "Learning Robust Perceptive Locomotion for Quadrupedal Robots in the Wild" (Miki et al., 2022)
- 灵巧手: "Learning Dexterous In-Hand Manipulation" (OpenAI, 2020) -- 魔方求解,Sim2Real 里程碑
- 跑酷: "Robot Parkour Learning" (Zhuang et al., 2023) -- 四足机器人跑酷
建议按以下顺序阅读:PPO → GAE → SAC → Domain Randomization → Hwangbo 2019。先理解算法基础,再进入应用层面。每篇论文至少读两遍:第一遍把握全局,第二遍理解细节。
开发工具
TensorBoard
训练曲线可视化的标准工具。监控奖励、损失、学习率等关键指标。所有主流 RL 库都支持。
Weights & Biases (W&B)
云端实验管理平台。自动记录超参数、训练曲线、模型文件。支持团队协作和实验对比。
MuJoCo Viewer
MuJoCo 内置的可视化工具,支持实时交互、施加外力、查看接触力等,调试物理仿真的利器。
Isaac Sim GUI
IsaacLab 的可视化界面,基于 Omniverse。可实时观察训练过程,调试场景配置。
中文学习资源
| 平台 | 资源类型 | 推荐内容 |
|---|---|---|
| Bilibili | 视频教程 | 搜索"强化学习"或"机器人RL",有大量中文讲解视频。推荐关注高质量 UP 主的系列教程 |
| 知乎 | 专栏文章 | "强化学习"专栏有很多深入浅出的算法解读。搜索"IsaacGym"或"Sim2Real"可找到实战经验 |
| GitHub | 中文项目 | 搜索"reinforcement-learning-chinese"可找到中文注释的 RL 代码实现 |
| 微信公众号 | 技术文章 | 关注"机器之心"、"量子位"等公众号,跟踪最新 RL 和机器人研究进展 |
| CSDN/博客园 | 技术博客 | 有大量中文 RL 教程和实战笔记,适合查阅具体问题的解决方案 |
社区与交流
- Reddit r/reinforcementlearning:最活跃的 RL 英文社区,讨论算法、论文和实践问题
- Reddit r/robotics:机器人技术社区,Sim2Real 相关讨论较多
- NVIDIA Isaac Forum:IsaacLab / Isaac Sim 官方论坛,技术问题可直接获得开发者回复
- GitHub Discussions:各开源项目的 Discussions 板块是提问和交流的好地方
- Discord:很多 RL 开源项目有 Discord 服务器(如 Stable-Baselines3、IsaacLab)
推荐学习路线
第 1-2 周:基础理论
阅读 Sutton & Barto 前 6 章,观看 David Silver 课程前 5 节。理解 MDP、价值函数、策略梯度。
第 3-4 周:核心算法
学习 PPO 和 SAC 算法细节。阅读 CleanRL 的实现代码。完成项目 1(CartPole + PPO)。
第 5-6 周:连续控制
学习 MuJoCo 环境。完成项目 2(Ant 运动控制)。阅读 GAE 论文。
第 7-8 周:机器人仿真
安装并学习 IsaacLab。理解 GPU 并行训练原理。阅读 Hwangbo 2019 论文。
第 9-10 周:四足运动
完成项目 3(IsaacLab 四足运动)。学习奖励设计和域随机化技巧。
第 11-12 周:Sim2Real
学习 Sim2Real 完整流程。阅读 Domain Randomization 论文。如果有真机条件,尝试部署。
理论和实践并重是最有效的学习方式。每学完一个算法,立刻动手实现或使用它训练一个任务。不要试图一次性掌握所有内容,按照路线图循序渐进。遇到问题时善用社区资源和论文检索。
常用速查表
环境安装速查
# Gymnasium + MuJoCo(基础环境) pip install gymnasium[mujoco] # Stable-Baselines3(RL 算法库) pip install stable-baselines3[extra] # CleanRL(单文件 RL 实现) pip install cleanrl # TensorBoard(训练可视化) pip install tensorboard # Weights & Biases(实验管理) pip install wandb
训练命令速查
# CleanRL 训练 PPO python cleanrl/ppo_continuous_action.py \ --env-id Ant-v4 --total-timesteps 1000000 # IsaacLab + RSL-RL 训练 python source/standalone/workflows/rsl_rl/train.py \ --task Isaac-Velocity-Flat-Unitree-Go2-v0 \ --num_envs 4096 --headless # TensorBoard 查看日志 tensorboard --logdir ./logs/