机器人强化学习 从入门到精通

系统掌握从 Python 基础到 Sim2Real 部署的完整流水线,
用强化学习让机器人在仿真中学会技能,并迁移到真实世界。

13
章节
50+
练习题
1
完整 Sim2Real 流水线

学习路线图

阶段一 阶段二 阶段三 阶段四 阶段五 基础编程 强化学习理论 核心算法 机器人仿真 Sim2Real ┌──────┐ ┌──────────┐ ┌────────┐ ┌───────────┐ ┌──────────┐ │Python│─────▶│ MDP/贝尔曼 │────▶│PPO/SAC │────▶│MuJoCo / │────▶│域随机化 │ │PyTorch│ │ 策略梯度 │ │TD3/DQN │ │IsaacLab │ │真机部署 │ └──────┘ └──────────┘ └────────┘ └───────────┘ └──────────┘ 2 章 3 章 3 章 3 章 2 章
学习建议

建议按照路线图顺序学习。每个阶段结束后完成所有练习,再进入下一阶段。整个课程预计需要 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 管理环境:

Bash - 环境搭建
# 创建并激活 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 是一种动态类型语言,变量不需要事先声明类型。在强化学习中,你会频繁用到以下数据类型来表示状态、动作、奖励等核心概念。

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      # 整数 → 浮点数
RL 中的命名约定

在强化学习代码中,常见变量名有约定俗成的含义:obs/state(观测/状态)、action(动作)、reward(奖励)、done(回合结束标志)、info(额外信息)。Gymnasium 环境的 step() 方法正是返回这些值。

2.2 控制流

条件判断和循环是构建训练逻辑的基础。例如:判断是否结束回合、循环执行训练步数、提前终止训练等。

Python - 控制流
# === 条件判断 ===
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 项目中,你会把奖励计算、状态预处理、模型更新等逻辑封装成函数。

Python - 函数
# === 基本函数 ===
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 的所有环境都是类。你将来自己写自定义环境或封装策略网络时,必须理解面向对象编程。

Python - 类与继承
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 的前提。

为什么需要 NumPy?

Python 的原生列表运算非常慢(逐元素循环)。NumPy 使用 C 语言实现的向量化运算,速度可以快 100 倍以上。在 RL 中,状态向量、动作向量和奖励数组几乎全部使用 NumPy 表示。

Python - 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 模块导入模式

常见导入模式

强化学习项目中,你会反复看到以下导入语句。现在记住这些模式,后面的章节会更顺畅:

Python - 常见导入
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      # 绘图

练习

练习 1:折扣回报计算 简单

给定一个奖励序列 rewards = [1, 0, 1, 0, 1] 和折扣因子 gamma = 0.9,编写函数计算每个时间步的折扣回报。

提示:折扣回报公式为 Gt = rt + γ rt+1 + γ2 rt+2 + ... 从后往前计算最高效。

Python - 参考答案
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
练习 2:经验回放缓冲区 中等

实现一个 SimpleBuffer 类,要求:

  • __init__(self, max_size):初始化缓冲区
  • add(self, experience):添加经验,满了之后覆盖最旧的
  • sample(self, n):随机采样 n 条经验,如果缓冲区不够大则抛出异常
  • __len__(self):返回当前数据量
Python - 参考答案
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}")
练习 3:奖励信号分析器 困难

使用 NumPy 编写一个 RewardAnalyzer 类:

  • __init__(self):初始化,用于存储多个回合的奖励
  • add_episode(self, rewards_list):添加一个回合的奖励序列(Python 列表)
  • mean_return(self, gamma=0.99):计算所有回合的平均折扣回报
  • best_episode(self):返回总奖励最高的回合索引和总奖励
  • reward_statistics(self):返回字典,包含所有奖励的均值、标准差、最大值、最小值
Python - 参考答案
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 加速和自动求导。在强化学习中,状态、动作、奖励、网络参数全部以张量形式存储和计算。

Python - 张量创建与基本属性
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
NumPy 与 PyTorch 互转的注意事项

torch.from_numpy() 创建的张量与原始 NumPy 数组共享内存——修改一个会影响另一个。如果需要独立副本,使用 torch.tensor(np_array)(会复制数据)。在 RL 中,Gymnasium 环境返回 NumPy 数组,你需要经常进行这种转换。

3.2 张量操作

在构建策略网络和处理批量数据时,你需要灵活地变换张量形状和拼接张量。

Python - 张量形状操作
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 的核心特性。它自动计算损失函数对网络参数的梯度,是梯度下降优化和策略梯度算法的基础。

策略梯度定理的核心思想: ∇_θ J(θ) = E_π [ ∇_θ log π_θ(a|s) · Q^π(s,a) ] PyTorch 的自动求导系统帮我们自动计算 ∇_θ log π_θ(a|s) 这一项。
Python - 自动求导机制
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 的子类。

Python - 构建策略网络
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 算法的参数更新至关重要。

Python - 损失函数与优化器
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}")
训练循环的标准流程: ┌─────────────────────┐ │ optimizer.zero_grad()│ ← 清空旧梯度 └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ output = model(input)│ ← 前向传播 └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ loss = criterion( │ ← 计算损失 │ output, target) │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ loss.backward() │ ← 反向传播(计算梯度) └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ optimizer.step() │ ← 更新参数: θ ← θ - lr·∇L └─────────────────────┘

3.6 完整训练示例:MLP 策略网络

下面的例子展示了一个完整的训练循环。虽然这里用的是监督学习方式训练,但这个模式与 RL 中更新策略/价值网络的流程完全一致。

Python - 完整训练循环(可直接运行)
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()}")
model.train() vs model.eval()

PyTorch 模型有两种模式:train()(训练模式)和 eval()(评估模式)。区别在于 Dropout 和 BatchNorm 层的行为不同。在 RL 中,收集经验时使用 eval() 模式,更新参数时使用 train() 模式。简单的 MLP 没有这些层时两种模式行为相同,但养成好习惯很重要。

练习

练习 1:张量操作热身 简单

完成以下任务:

  1. 创建一个形状为 (5, 3) 的随机张量 A,和形状为 (3, 4) 的随机张量 B
  2. 计算矩阵乘法 C = A @ B
  3. 对 C 的每一行求最大值及其索引
  4. 将 C 展平(flatten)为一维张量
Python - 参考答案
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])
练习 2:实现价值网络 中等

实现一个 ValueNetwork 类(继承 nn.Module),接受状态输入,输出一个标量状态价值 V(s)。要求:

  • 至少两个隐藏层,使用 ReLU 激活
  • 输出层不加激活函数(价值可以是任意实数)
  • 实例化后对一批 16 个状态进行前向传播,输出形状应为 (16, 1)
Python - 参考答案
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}")
练习 3:完整训练流程 困难

编写一个完整的训练脚本,训练上面的 ValueNetwork 来逼近一个目标函数 V*(s) = s 各分量之和的平方。具体要求:

  • 随机生成 2000 个训练样本(状态维度为 4)
  • 目标值:Yi = (si1 + si2 + si3 + si4
  • 使用 MSE 损失和 Adam 优化器(lr=1e-3)
  • 训练 500 个 epoch,每 100 个 epoch 打印损失
  • 训练结束后在 10 个测试样本上展示预测值 vs 真实值
Python - 参考答案
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()

GPU 内存泄漏警告

如果你在收集经验时忘记使用 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),定义为五元组:

MDP = (S, A, P, R, γ)

其中:

  • 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):

Gₜ = rₜ₊₁ + γ·rₜ₊₂ + γ²·rₜ₊₃ + ... = Σ(k=0→∞) γᵏ · rₜ₊ₖ₊₁

奖励假说(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)

Vπ(s) = Eπ[Gₜ | sₜ = s] = Eπ[Σ(k=0→∞) γᵏ · rₜ₊ₖ₊₁ | sₜ = s]

表示在状态 s 下,遵循策略 π 所能获得的期望回报。

动作价值函数 Qπ(s,a)

Qπ(s,a) = Eπ[Gₜ | sₜ = s, aₜ = a] = Eπ[Σ(k=0→∞) γᵏ · rₜ₊ₖ₊₁ | sₜ = s, aₜ = a]

表示在状态 s 下执行动作 a,然后遵循策略 π 所能获得的期望回报。

贝尔曼期望方程

价值函数满足递归关系——贝尔曼方程(Bellman Equation),这是动态规划和 RL 的理论基石:

Vπ(s) = Σa π(a|s) · Σs' P(s'|s,a) · [R(s,a,s') + γ · Vπ(s')]

含义:当前状态的价值 = 对所有可能动作求期望 [即时奖励 + 折扣 × 下一个状态的价值]。

Qπ(s,a) = Σs' P(s'|s,a) · [R(s,a,s') + γ · Σa' π(a'|s') · Qπ(s',a')]

贝尔曼最优方程

最优价值函数对应最优策略 π*,贝尔曼最优方程为:

V*(s) = maxa Σs' P(s'|s,a) · [R(s,a,s') + γ · V*(s')]
Q*(s,a) = Σs' P(s'|s,a) · [R(s,a,s') + γ · maxa' Q*(s',a')]

区别在于:期望方程对动作取加权平均(按策略概率),最优方程对动作取最大值(选最好的动作)。

V 和 Q 的关系

两个价值函数之间的关系: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)

最简单实用的探索策略:

π(a|s) = 1-ε+ε/|A| (若 a = argmax Q(s,a)); π(a|s) = ε/|A| (其他动作)

以概率 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 = 终点, ▓ = 墙壁
    动作: ↑上 ↓下 ←左 →右
Python
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 值最大的动作即为最优策略。这是一种动态规划方法,需要已知环境模型(转移概率和奖励函数)。

练习与巩固

练习 1:概念辨析 简单

请解释状态价值函数 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) 才能计算每个动作的期望价值。

练习 2:贝尔曼方程推导 中等

已知一个简单的两状态 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),因为还有循环带来的累积收益。

练习 3:ε-贪心策略实现 中等

请实现一个带 ε 衰减的 ε-贪心策略选择函数。初始 ε=1.0,最终 ε=0.01,在 10000 步内线性衰减。函数接受 Q 值数组和当前步数,返回选择的动作索引。

Python
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() 确保 ε 不会低于最终值。

练习 4:折扣回报计算 困难

给定一个回合的奖励序列 rewards = [1, -2, 3, 5, -1, 2, 4],请编写函数计算每个时间步的折扣回报 Gₜ(使用 γ=0.95)。提示:从后向前递推更高效:Gₜ = rₜ₊₁ + γ·Gₜ₊₁。

Python
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²) 高效得多。这种技巧在策略梯度方法的实现中非常常用。

知识测验

1. 在 MDP 的五元组 (S, A, P, R, γ) 中,马尔可夫性质是指什么?
A 奖励只取决于当前动作
B 下一个状态只取决于当前状态和动作,与历史无关
C 策略必须是确定性的
D 折扣因子必须等于 1
马尔可夫性质(无后效性)是 MDP 的核心假设:P(sₜ₊₁|sₜ,aₜ) = P(sₜ₊₁|s₀,a₀,...,sₜ,aₜ)。这意味着当前状态包含了做出最优决策所需的全部信息,无需回顾历史。
2. 以下哪种方法是异策略(Off-Policy)算法?
A SARSA
B Q-Learning
C REINFORCE
D A2C
Q-Learning 是经典的异策略算法。它使用 ε-贪心策略收集数据(行为策略),但更新目标使用 max Q(对应贪心策略/目标策略)。SARSA 是同策略算法,因为它的更新使用实际执行的下一个动作。REINFORCE 和 A2C 都是同策略方法。
3. 当折扣因子 γ=0 时,智能体的行为特点是?
A 平等考虑所有未来奖励
B 只关注下一步的即时奖励,完全短视
C 无法学习任何策略
D 总是选择随机动作
当 γ=0 时,Gₜ = rₜ₊₁(仅包含即时奖励),所有未来奖励权重为零。智能体变得完全"短视"(myopic),只追求当前一步的最大奖励,不考虑长远影响。这通常不适合需要多步规划的机器人控制任务。

深度强化学习 进阶核心

本节概览

深度强化学习(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)

解决方案是用参数化的函数来近似价值函数或策略,而非逐一存储:

Q(s,a) ≈ Q(s,a; θ) 其中 θ 是神经网络参数
π(a|s) ≈ π(a|s; θ) 策略网络参数化

深度神经网络是强大的通用函数逼近器,能够从高维输入中自动提取特征,这正是深度强化学习的核心思想。

    经典 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)误差来训练网络:

损失函数 L(θ) = E[(r + γ · maxa' Q(s',a'; θ⁻) - Q(s,a; θ))²]

其中 θ⁻ 是目标网络参数(稍后解释)。

两大核心创新

经验回放 (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 实现

Python
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(θ) 对参数 θ 的梯度:

θJ(θ) = Eπθ[∇θ log πθ(aₜ|sₜ) · Gₜ]

直觉理解

  • θ log πθ(aₜ|sₜ) — 指出如何调整参数以增加动作 aₜ 在状态 sₜ 下的概率
  • Gₜ — 该动作之后获得的累积回报,作为"权重"
  • 如果 Gₜ > 0(好的回报),梯度方向增加该动作概率
  • 如果 Gₜ < 0(差的回报),梯度方向减少该动作概率

本质是:增加获得高回报的动作的概率,减少获得低回报的动作的概率

推导过程(简化版)

REINFORCE 梯度推导

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)

Aπ(s,a) = Qπ(s,a) - Vπ(s)

优势函数衡量"在状态 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 要求智能体通过左右移动小车来平衡杆子。

Python
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}")
REINFORCE 的关键技巧

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 学习可以部分缓解此问题。

练习与巩固

练习 1:DQN 原理分析 中等

请解释 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 值估计可能发散到极大值,训练完全不稳定
练习 2:策略梯度推导 困难

在 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),而策略梯度理论证明减去与动作无关的基线不改变梯度期望,但能显著降低方差。

练习 3:Actor-Critic 实现 困难

请基于上面的 REINFORCE 代码,将其改造为 Actor-Critic 版本。主要修改:(1) 增加一个 Critic 网络估计 V(s);(2) 用 TD 误差 δ = r + γ·V(s') - V(s) 替代 Gₜ 作为优势估计;(3) 可以每步更新而非等完整回合。

Python
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) 自带基线减法
  • 熵正则化项防止策略过早收敛到确定性策略

知识测验

1. DQN 中目标网络的参数 θ⁻ 是如何更新的?
A 每步都与在线网络同步更新
B 使用独立的数据集单独训练
C 每隔固定步数从在线网络复制参数
D 使用随机初始化且永不更新
目标网络的参数 θ⁻ 每隔 C 步(如 1000 步或 10000 步)从在线网络硬复制:θ⁻ ← θ。另一种变体是"软更新"(Soft Update):θ⁻ ← τ·θ + (1-τ)·θ⁻,其中 τ 是很小的值(如 0.005),这种方式更平滑,被 DDPG 和 SAC 等算法采用。
2. Actor-Critic 中优势函数 A(s,a) = Q(s,a) - V(s) 的作用是什么?
A 增加所有动作的选择概率
B 衡量某个动作相对于平均水平的优劣,降低策略梯度的方差
C 计算环境的状态转移概率
D 替代折扣因子 γ 的作用
优势函数通过减去基线 V(s) 来衡量动作的相对好坏。如果 A > 0,说明该动作优于平均水平,应增加其概率;如果 A < 0,说明不如平均水平,应降低概率。这种"相对评价"机制显著降低了策略梯度的方差,使训练更稳定。从数学上说,减去与动作无关的基线不改变梯度的期望值,但减小了方差。

PPO 算法详解

Proximal Policy Optimization — 强化学习中最实用的策略梯度算法

为什么要学 PPO?

PPO 由 OpenAI 于 2017 年提出,是目前机器人强化学习最常用的算法之一。IsaacLab 中的机器人运动控制任务几乎全部使用 PPO 训练。它简单、稳定、高效,是你必须精通的核心算法。

1. 从策略梯度到 PPO 的演进

REINFORCE 基础

直接对策略做梯度上升。问题:方差大,训练不稳定,一次坏的更新可能毁掉整个策略。

TRPO 理论优美

用 KL 散度约束每次更新幅度,保证单调改进。问题:需要二阶优化(Hessian),实现极复杂

PPO 实践首选

用简单的裁剪(Clip)替代 KL 约束,只需一阶优化器(Adam),保留了 TRPO 的稳定性。

2. PPO 核心公式

2.1 重要性采样比率

PPO 利用旧策略采集的数据来更新新策略,核心是计算新旧策略的概率比:

r_t(θ) = π_θ(a_t | s_t) / π_θ_old(a_t | s_t)

当 θ = θ_old 时,r_t = 1。如果新策略更倾向于选择动作 a_t,则 r_t > 1;反之 r_t < 1。

2.2 PPO-Clip 目标函数

L^CLIP(θ) = E_t [ min( r_t(θ) · A_t, clip(r_t(θ), 1-ε, 1+ε) · A_t ) ]
裁剪机制的直觉

当 A_t > 0(好动作):我们想增大 r_t,但 clip 限制 r_t ≤ 1+ε,防止过度增大概率。

当 A_t < 0(坏动作):我们想减小 r_t,但 clip 限制 r_t ≥ 1-ε,防止过度减小概率。

ε 通常取 0.2,意味着概率比被限制在 [0.8, 1.2] 范围内。

PPO 裁剪机制示意图: A_t > 0 (好动作,应增大概率) A_t < 0 (坏动作,应减小概率) L ↑ L ↑ │ ╱ 无裁剪 │ │ ╱ │ ──────╲ 裁剪后(停止减小) │ ╱──────── 裁剪后(停止增大) │ 1-ε ╲ │ ╱ 1+ε │ ╲ 无裁剪 │╱ │ ╲ └──────────→ r_t └──────────→ r_t 0 1-ε 1 1+ε 0 1-ε 1 1+ε min() 操作保证取"悲观下界",防止策略更新过于激进

2.3 广义优势估计 (GAE)

PPO 使用 GAE 来计算优势函数,平衡偏差和方差:

δ_t = r_t + γ · V(s_{t+1}) - V(s_t) (TD 残差)

A_t^GAE = Σ_{l=0}^{T-t} (γλ)^l · δ_{t+l} (GAE 优势估计)

其中 λ ∈ [0,1] 控制偏差-方差权衡:λ=0 退化为单步 TD(低方差高偏差),λ=1 退化为蒙特卡洛(高方差低偏差)。实践中 λ=0.95 效果最好。

2.4 完整 PPO 损失函数

L(θ) = L^CLIP(θ) - c₁ · L^VF(θ) + c₂ · H[π_θ]

三个部分:策略损失(Clip)+ 价值函数损失(MSE)+ 熵奖励(鼓励探索)。c₁=0.5, c₂=0.01 是常用值。

3. PPO 算法流程

PPO 算法伪代码: ───────────────────────────────────────────────── 1. 初始化策略网络 π_θ 和价值网络 V_φ 2. for iteration = 1, 2, ... do 3. // 采集阶段 4. for t = 1, ..., T do 5. 用当前策略 π_θ_old 与环境交互 6. 存储 (s_t, a_t, r_t, s_{t+1}, log π_θ_old(a_t|s_t)) 7. end for 8. // 计算优势 9. 用 V_φ 计算 GAE 优势估计 A_t 10. 计算回报 R_t = A_t + V_φ(s_t) 11. // 更新阶段 (多个 epoch) 12. for epoch = 1, ..., K do 13. 对采集的数据做 mini-batch 随机梯度上升 14. 计算 r_t(θ) = π_θ(a_t|s_t) / π_θ_old(a_t|s_t) 15. 计算 L^CLIP 并更新 θ 16. 计算 MSE(V_φ(s_t), R_t) 并更新 φ 17. end for 18. θ_old ← θ 19. end for ─────────────────────────────────────────────────

4. PyTorch 完整实现

python
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_rate3e-4Adam 学习率,可线性衰减
gamma (γ)0.99折扣因子
lambda (λ)0.95GAE 参数
epochs (K)3-10每批数据的更新轮数
batch_size64-4096Mini-batch 大小
entropy_coef0.0-0.01熵奖励系数
num_steps2048每次采集的步数

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 终止。

练习

练习 1:理解裁剪机制 简单
假设 ε=0.2,某个状态-动作对的优势 A_t = 3.0,概率比 r_t = 1.5。请计算 PPO-Clip 目标函数的值,并解释发生了什么。

计算过程:

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。

练习 2:GAE 计算 中等
给定一个 3 步的 episode:r_0=1, r_1=2, r_2=3,V(s_0)=5, V(s_1)=4, V(s_2)=3, V(s_3)=0(终止),γ=0.99, λ=0.95。计算每一步的 TD 残差 δ_t 和 GAE 优势 A_t。

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

练习 3:实现 PPO 训练 CartPole 困难
使用上面的 ActorCritic 类和 ppo_update 函数,编写完整的训练循环来训练 CartPole-v1 环境。提示:CartPole 是离散动作空间,需要修改网络输出为分类分布。
python
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}")

测验

1. PPO 中 clip_epsilon 设为 0.1 和 0.3 相比,哪个更新更保守?
A ε = 0.1 更保守
B ε = 0.3 更保守
C 两者一样
ε = 0.1 意味着概率比被限制在 [0.9, 1.1],允许的策略变化范围更小,因此更保守。ε = 0.3 则允许 [0.7, 1.3] 的变化范围,更新幅度更大。
2. PPO 是 on-policy 还是 off-policy 算法?为什么可以对同一批数据做多轮更新?
A On-policy,但通过裁剪和重要性采样允许有限的数据复用
B Off-policy,因为它使用了经验回放
C On-policy,每个数据只能用一次
PPO 本质是 on-policy 算法——数据必须由当前策略采集。但它利用重要性采样比率 r_t(θ) 和裁剪机制,允许在不偏离太远的前提下对同一批数据做多轮(如 3-10 轮)更新。这大幅提高了数据利用效率,是 PPO 的一大优势。

SAC 算法详解

Soft Actor-Critic — 基于最大熵框架的 Off-Policy 算法

SAC 的定位

SAC 由 UC Berkeley 的 Haarnoja 等人于 2018 年提出。它是目前最高效的 off-policy 连续控制算法之一,在 MuJoCo 基准测试中表现优异,常用于机器人操作任务的训练。

1. 最大熵强化学习框架

SAC 的核心思想:除了最大化累积奖励,还要最大化策略的熵(entropy)

J(π) = Σ_t E[ r(s_t, a_t) + α · H(π(·|s_t)) ]

其中 H(π) = -E[log π(a|s)] 是策略的熵,α 是温度参数

为什么要最大化熵?

更好的探索 核心优势

高熵策略不会过早收敛到某一个动作,保持对环境的持续探索,避免陷入局部最优。

鲁棒性 Sim2Real 友好

熵正则化使策略倾向于多模态分布,对环境变化更鲁棒。这对 Sim2Real 迁移非常有利。

更好的组合能力

多模态策略可以学习多种达成目标的方式,便于在不同情境下灵活切换行为策略。

2. SAC 核心组件

SAC 网络架构: ────────────────────────────────────────────── ┌─────────────────────┐ │ Policy Network π_θ │ → 输出: μ(s), σ(s) │ (Gaussian Policy) │ → 采样: a = tanh(μ + σ·ε) └─────────────────────┘ ┌──────────────┐ ┌──────────────┐ │ Q-Network 1 │ │ Q-Network 2 │ ← Twin Q 防止过估计 │ Q_φ1 │ │ Q_φ2 │ └──────────────┘ └──────────────┘ ┌──────────────┐ ┌──────────────┐ │ Target Q1 │ │ Target Q2 │ ← 软更新: φ' ← τφ + (1-τ)φ' │ Q_φ1' │ │ Q_φ2' │ └──────────────┘ └──────────────┘ ┌──────────────┐ │ Temperature α │ ← 自动调节(可选) └──────────────┘ ──────────────────────────────────────────────

2.1 Twin Q-Networks(双 Q 网络)

SAC 使用两个独立的 Q 网络,取较小值来计算目标值,有效缓解 Q 值过高估计问题:

y = r + γ · ( min(Q_φ1'(s', a'), Q_φ2'(s', a')) - α · log π_θ(a'|s') )

2.2 Squashed Gaussian 策略

SAC 使用 Gaussian 分布采样,然后通过 tanh 将动作压缩到 [-1, 1]:

u = μ_θ(s) + σ_θ(s) · ε, ε ~ N(0, I) (重参数化技巧)
a = tanh(u) (压缩到有界范围)

log π(a|s) = log N(u|μ,σ) - Σ log(1 - tanh²(u_i)) (雅可比修正)
重参数化技巧(Reparameterization Trick)

将采样操作分离为 确定性函数 + 噪声,使得梯度可以通过采样动作反向传播到策略网络。这是 SAC 能高效训练的关键。与 PPO 使用 log_prob trick 不同,SAC 直接通过动作值传递梯度。

2.3 温度参数 α 自动调节

SAC 可以自动调节 α,使策略的熵保持在目标水平:

α* = argmin_α E[-α · log π(a|s) - α · H_target]

通常设 H_target = -dim(A) (动作空间维度的负数)

3. SAC 算法流程

SAC 算法伪代码: ───────────────────────────────────────────────── 1. 初始化 π_θ, Q_φ1, Q_φ2, Q_φ1', Q_φ2' (target = copy) 2. 初始化 Replay Buffer D 3. for each step do: 4. a ~ π_θ(·|s) // 从策略采样动作 5. s', r, done = env.step(a) 6. D.add(s, a, r, s', done) 7. 8. // 从 buffer 采样 mini-batch 9. batch = D.sample(256) 10. 11. // 更新 Q 网络 12. a' ~ π_θ(·|s') 13. target = r + γ(1-done)(min(Q_φ1'(s',a'), Q_φ2'(s',a')) - α·log π(a'|s')) 14. 更新 φ1: minimize MSE(Q_φ1(s,a), target) 15. 更新 φ2: minimize MSE(Q_φ2(s,a), target) 16. 17. // 更新策略 18. a_new ~ π_θ(·|s) // 重参数化采样 19. 更新 θ: minimize (α·log π(a_new|s) - min(Q_φ1(s,a_new), Q_φ2(s,a_new))) 20. 21. // 更新温度 (可选) 22. 更新 α: minimize (-α · (log π(a_new|s) + H_target)) 23. 24. // 软更新 target 网络 25. φ1' ← τ·φ1 + (1-τ)·φ1' 26. φ2' ← τ·φ2 + (1-τ)·φ2' 27. end for ─────────────────────────────────────────────────

4. PyTorch 完整实现

python
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 对比

特性PPOSAC
学习类型On-PolicyOff-Policy
样本效率较低(数据不能重用)较高(经验回放)
训练稳定性非常稳定稳定(Twin Q + 熵正则)
并行化天然支持大规模并行不适合 GPU 并行
适用场景大规模并行仿真(IsaacLab)单环境高样本效率场景
动作空间离散/连续均可主要连续
超参数敏感度较低中等
典型应用四足行走、人形运动机械臂操作、灵巧手
机器人领域如何选择?

选 PPO:当你可以并行运行成百上千个仿真环境时(IsaacLab + GPU),PPO 的总体训练速度更快。四足/人形运动控制几乎都用 PPO。

选 SAC:当只有单个或少量仿真环境时(MuJoCo),SAC 的样本效率更高。机械臂操作任务常用 SAC。

6. 超参数指南

参数推荐值说明
learning_rate3e-4所有网络统一学习率
tau0.005target 网络软更新系数
gamma0.99折扣因子
buffer_size1,000,000经验回放缓冲区大小
batch_size256Mini-batch 大小
alpha自动调节温度参数,建议用自动调节
hidden_dim256隐藏层大小
warmup_steps10,000随机采集初始数据量

练习

练习 1:理解最大熵 简单
解释为什么最大熵策略在 Sim2Real 中有优势?如果训练出一个确定性策略和一个高熵策略,哪个在真实机器人上更可能成功?

高熵策略在 Sim2Real 中更有优势,原因如下:

  • 高熵策略学到了多种达成目标的方式,当仿真与现实的某些差异导致一种方式失效时,策略可以切换到其他方式
  • 确定性策略高度依赖仿真环境的精确性,真实世界的微小差异可能导致完全失败
  • 高熵策略的动作分布更平滑,对扰动和噪声有更好的容忍度
  • 熵正则化相当于隐式地进行了一种 "domain randomization"
练习 2:tanh 修正项推导 中等
在 SAC 中,动作 a = tanh(u)。解释为什么计算 log π(a|s) 时需要减去 Σ log(1 - tanh²(u_i))?这和概率密度的变量替换有什么关系?

这是概率密度变量替换公式的应用。

当 u ~ N(μ,σ) 且 a = tanh(u) 时,由于 tanh 是非线性变换,概率密度会发生变化:

p(a) = p(u) · |du/da| = p(u) / |da/du|

因为 da/du = 1 - tanh²(u),所以:

log p(a) = log p(u) - log|da/du|
= log N(u|μ,σ) - Σ_i log(1 - tanh²(u_i))

如果忽略这个修正项,log_prob 会不准确,导致策略梯度估计偏差,温度自动调节也会失效。

练习 3:用 SAC 训练 MuJoCo 环境 困难
使用上面的 SACAgent 类训练 Pendulum-v1 环境。编写完整的训练循环,包括 warmup 阶段和训练日志。
python
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)

测验

1. SAC 使用 Twin Q-Network 的主要目的是什么?
A 增加网络容量
B 缓解 Q 值过高估计
C 加速训练速度
D 处理多任务学习
取两个 Q 网络的较小值作为目标,可以有效缓解 Q 值的过高估计(overestimation bias)。这一技术源自 TD3 (Twin Delayed DDPG)。过高估计会导致策略追逐虚假的高价值动作,使训练发散。
2. 以下哪个不是 SAC 相比 PPO 的优势?
A 更高的样本效率
B 更适合大规模 GPU 并行仿真
C 内置探索机制(熵正则化)
SAC 是 off-policy 算法,依赖经验回放缓冲区,不能直接利用大规模 GPU 并行仿真的优势。PPO 作为 on-policy 算法,可以同时运行数千个并行环境来高效采集数据,这正是 IsaacLab 选择 PPO 的原因。

机器人动力学基础

在将强化学习应用于机器人控制之前,我们需要理解机器人的物理运动规律。本节将介绍刚体运动学与动力学的核心概念,为后续仿真环境搭建和策略训练奠定基础。

刚体运动基础

机器人本质上由多个刚体(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。绕三个坐标轴的基本旋转矩阵为:

绕 X 轴旋转角度 θ: Rx(θ) = | 1 0 0 | | 0 cos(θ) -sin(θ)| | 0 sin(θ) cos(θ)| 绕 Z 轴旋转角度 θ: Rz(θ) = | cos(θ) -sin(θ) 0 | | sin(θ) cos(θ) 0 | | 0 0 1 |

齐次变换矩阵

齐次变换矩阵 T 是 4×4 矩阵,同时包含旋转和平移信息:

T = | R p | 其中 R 为 3×3 旋转矩阵 | 0 1 | p 为 3×1 平移向量
Python
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 个参数描述:

正运动学变换链: T_0n = T_01(q1) · T_12(q2) · ... · T_(n-1)n(qn) 其中每个变换矩阵 T_i 由 DH 参数决定

逆运动学(Inverse Kinematics)

逆运动学是正运动学的反问题:已知末端执行器期望位姿,求对应的关节角度。逆运动学通常更难求解,可能有多解、无解或无穷解。常用方法包括解析法、雅可比迭代法和数值优化法。

RL 与逆运动学

在 RL 控制中,策略网络可以直接输出关节角度或力矩,从而绕过传统逆运动学求解。这是 RL 在机器人控制中的一大优势——可以端到端学习从目标到动作的映射。

二连杆机械臂示例

Y ^ | *(末端执行器) | / | / L2 (连杆2) | / | * 关节2 (θ2) | / | / L1 (连杆1) | / |/ θ1 ------*---------> X 基座 (关节1) 正运动学公式: x = L1·cos(θ1) + L2·cos(θ1 + θ2) y = L1·sin(θ1) + L2·sin(θ1 + θ2)
Python
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}")

牛顿-欧拉动力学

机器人动力学描述力/力矩与运动之间的关系。经典的机器人动力学方程为:

M(q)q̈ + C(q, q̇)q̇ + g(q) = τ M(q): 质量矩阵(惯性矩阵),n×n 正定对称矩阵 C(q, q̇): 科氏力和离心力矩阵 g(q): 重力项 τ: 关节力矩(控制输入) q, q̇, q̈: 关节位置、速度、加速度
仿真引擎的作用

在仿真环境(MuJoCo、IsaacSim)中,物理引擎会自动计算这些动力学方程。我们只需设定控制输入 τ(或目标位置/速度),引擎会积分求解出下一时刻的状态。理解动力学方程有助于我们设计更好的观测空间和奖励函数。

关节空间与任务空间

关节空间(Joint Space)

以关节角度 q 和关节角速度 q̇ 描述机器人状态。维度等于机器人自由度数。

优点:直接对应电机控制量,没有奇异性问题。

缺点:不直观,难以直接描述末端执行器的运动。

任务空间(Task Space)

以末端执行器的笛卡尔坐标和姿态描述机器人状态。通常为 6 维(3 平移 + 3 旋转)。

优点:直观,与任务目标直接相关。

缺点:存在奇异性,需要逆运动学求解。

控制模式

控制模式 控制量 特点 RL 中的应用
力矩控制 关节力矩 τ 最底层、最灵活,但需要精确的动力学模型 连续动作空间直接输出力矩
位置控制 目标关节角度 q_target 底层 PD 控制器跟踪目标位置,更稳定 RL 输出目标角度,PD 控制器执行
速度控制 目标关节速度 q̇_target 介于力矩和位置控制之间 RL 输出期望速度增量

PD 控制器

位置控制模式下,底层通常使用 PD(比例-微分)控制器将目标位置转换为力矩:

τ = Kp · (q_desired - q_current) + Kd · (q̇_desired - q̇_current) Kp: 比例增益(位置刚度),控制跟踪精度 Kd: 微分增益(阻尼),控制运动平滑度
Python
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。

练习

练习 1:正运动学计算 简单
对于二连杆平面机械臂,连杆长度 L1 = 1.0m,L2 = 0.5m。当 θ1 = 45°,θ2 = -90° 时,计算末端执行器的 (x, y) 坐标。

使用正运动学公式:

x = L1·cos(θ1) + L2·cos(θ1 + θ2) = 1.0·cos(45°) + 0.5·cos(45° + (-90°)) = 1.0·cos(45°) + 0.5·cos(-45°) = 0.707 + 0.354 = 1.061 y = L1·sin(θ1) + L2·sin(θ1 + θ2) = 1.0·sin(45°) + 0.5·sin(-45°) = 0.707 + (-0.354) = 0.354 末端执行器位置:(1.061, 0.354)
练习 2:PD 控制器调参 中等
一个单关节机器人,当前角度 q = 0.5 rad,目标角度 q_d = 0 rad,当前角速度 dq = 0.1 rad/s,目标角速度 dq_d = 0 rad/s。若 Kp = 200,Kd = 20,计算输出力矩 τ。如果将 Kd 增大到 50,对系统行为有什么影响?

力矩计算:

τ = Kp·(q_d - q) + Kd·(dq_d - dq) = 200·(0 - 0.5) + 20·(0 - 0.1) = 200·(-0.5) + 20·(-0.1) = -100 + (-2) = -102 Nm

负号表示力矩方向与正方向相反,即驱动关节回到目标位置。

增大 Kd 的影响:增大微分增益(Kd=50)会增加阻尼效果,使系统更快地消耗动能。关节到达目标位置时超调减少,但响应速度变慢。Kd 过大可能导致系统响应迟钝(过阻尼)。

练习 3:观测空间设计 困难
你需要训练一个 RL 策略来控制一个 7 自由度机械臂完成桌面物体抓取任务。请设计观测空间和动作空间,并说明理由。考虑以下因素:需要哪些状态信息?用关节空间还是任务空间表示?动作空间用力矩控制还是位置控制?

推荐的观测空间设计(约 25 维):

  • 7 个关节角度 q(归一化到 [-1, 1])
  • 7 个关节角速度 dq
  • 3 个末端执行器位置 (x, y, z)
  • 3 个目标物体位置 (x, y, z)
  • 3 个目标到末端的相对位置差
  • 1 个夹爪开合状态
  • 1 个是否抓住物体的布尔信号

推荐的动作空间(8 维,连续):

  • 7 个目标关节角度增量 Δq(位置控制模式)
  • 1 个夹爪开合控制

理由:位置控制模式比力矩控制更稳定,底层 PD 控制器保证了运动的平滑性。输出增量(而非绝对位置)使动作空间在零附近居中,有利于策略网络训练。包含相对位置差作为观测可以加速学习目标导向行为。

测验

1. 在强化学习中使用位置控制而非力矩控制的主要优势是什么?
A 位置控制的动作空间维度更低
B 底层 PD 控制器提供了稳定性,降低了策略学习难度
C 位置控制不需要知道机器人的动力学模型
D 位置控制可以实现更快的关节运动速度
位置控制模式下,底层 PD 控制器将 RL 策略输出的目标位置转换为力矩,相当于提供了一层"安全网"。策略只需学习运动规划,而不用直接处理复杂的动力学。这大大降低了学习难度,也使训练出的策略更容易迁移到真实机器人(Sim2Real)。
2. 一个四足机器人有 4 条腿,每条腿 3 个旋转关节(髋侧摆、髋前摆、膝关节),该机器人在关节空间中的自由度是多少?
A 4
B 6
C 12
D 18
每条腿有 3 个旋转关节,4 条腿共 4 × 3 = 12 个自由度。这意味着 RL 策略的动作空间为 12 维(如 Unitree Go1)。注意这里不计算浮动基座(机器人躯干)的 6 个自由度,因为那些不是可以直接驱动的关节。

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
<?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 功能更丰富。核心区别:

特性URDFMJCF
执行器不支持支持 motor/position/velocity
传感器不支持支持关节位置/速度/力等
仿真参数不支持可配置时间步、积分器等
接触模型有限详细的摩擦、弹性配置
默认值继承不支持支持 default class
生态ROS, Gazebo, PyBulletMuJoCo 专用

MJCF 示例

xml
<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>
URDF 转 MJCF

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:阅读 URDF 简单
阅读上面的二连杆 URDF:(1) 有几个自由度?(2) 肩关节的旋转轴是哪个方向?(3) link1 的质量?

(1) 1个自由度(只展示了 shoulder 关节,完整版2个)

(2) Z轴:axis xyz="0 0 1"

(3) 2.0 kg:mass value="2.0"

练习 2:编写 MJCF 摆锤 中等
编写一个单关节摆锤的 MJCF 文件。要求:绕 Y 轴旋转,摆杆长1m,质量1kg,力矩电机控制范围 [-10, 10],包含关节位置和速度传感器。
xml
<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>
MJCF 中 <actuator> 的 motor 和 position 类型有什么区别?
A motor 输出力矩,position 输出目标位置(内置 PD 控制)
B motor 更精确,position 更快
C 它们完全相同,只是名字不同
motor 类型直接将控制信号映射为关节力矩(τ = ctrl × gear)。position 类型则内置了一个 PD 控制器,控制信号是目标位置,执行器自动计算力矩 τ = kp×(target - q) - kd×dq。在 RL 中,position 类型更容易训练。

MuJoCo 仿真环境

Multi-Joint dynamics with Contact — 机器人 RL 的标准仿真引擎

MuJoCo 简介

MuJoCo 是由 DeepMind 维护的高性能物理引擎,专为机器人控制和强化学习设计。2022 年开源后成为 RL 研究的标准工具。安装:pip install mujoco

1. 核心概念

MjModel

静态模型:从 XML 加载的机器人描述(形状、质量、关节等)。不会在仿真过程中改变。

MjData

动态状态:关节位置 qpos、速度 qvel、控制信号 ctrl、传感器数据等。每步仿真后更新。

mj_step

推进一步仿真。根据当前控制信号和物理定律更新所有状态。

2. 基本仿真循环

python
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 环境接口:

python
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()
常用 MuJoCo 环境

入门级:Pendulum-v1, CartPole-v1(非 MuJoCo 但接口相同)

中级:HalfCheetah-v4, Hopper-v4, Walker2d-v4

高级:Ant-v4, Humanoid-v4, HumanoidStandup-v4

4. 自定义 Gym 环境

这是 RL 工程中最核心的技能之一——把你的 MuJoCo 模型封装为 Gym 环境:

python
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π 不连续性。关节速度需要包含。

奖励设计:奖励要连续可微,避免稀疏奖励。加入动作惩罚项鼓励平滑控制。

练习

练习 1:运行 MuJoCo 环境 简单
安装 mujoco 和 gymnasium[mujoco],运行 Ant-v4 环境 1000 步,输出观测空间和动作空间的维度。
python
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个关节力矩)")
练习 2:自定义奖励函数 中等
为上面的 PendulumSwingUp 设计一个更好的奖励函数,鼓励摆锤快速到达竖直向上位置并稳定保持。
python
# 改进版奖励函数
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

设计原则:位置奖励为主,速度和动作惩罚为辅,加上存活奖励避免过早终止。

MuJoCo 中 data.qpos 和 data.ctrl 的区别是什么?
A qpos 是当前关节位置(状态),ctrl 是输入的控制信号(动作)
B qpos 是目标位置,ctrl 是当前位置
C 它们是同一个东西的不同名字
qpos 存储所有关节的广义坐标(generalized positions),是仿真状态的一部分,由物理引擎更新。ctrl 是用户输入的控制信号,会被映射到对应的执行器上。在 RL 中,qpos 是观测的一部分,ctrl 是动作的输出目标。

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+

安装概览

Bash
# 第一步:安装 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(交互式场景)

场景是所有仿真对象的容器,使用配置类来定义:

Python
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. 向量化环境

GPU 并行的威力

在 IsaacLab 中,所有环境实例共享同一个 GPU 物理世界。4096 个环境的 step() 只需一次 GPU kernel 调用,而非 4096 次独立仿真。这使得数据吞吐量达到每秒数百万步。

创建自定义任务

以机械臂末端到达目标位置(Reach Task)为例,展示完整的任务创建流程:

Python
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 中四足运动训练的标准选择。

Python
# 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",
}
Bash
# 启动训练(机械臂到达任务)
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 轮迭代收敛

实用技巧

奖励塑形(Reward Shaping)

将复杂目标拆分为多个小的奖励项,并仔细调节权重。例如,四足运动可能包含 10-15 个奖励项。使用 TensorBoard 单独监控每个奖励项的贡献。

课程学习(Curriculum Learning)

从简单任务开始逐步增加难度。例如:先在平坦地面训练行走,再引入不平坦地形、台阶、斜坡。

域随机化(Domain Randomization)

在 IsaacLab 中通过 EventTermCfg 配置随机化参数。建议从小范围开始,逐步扩大随机化范围,观察训练曲线变化。

练习 1 基础

请解释 IsaacLab 中 ManagerBasedRLEnvDirectRLEnv 的区别。在什么场景下你会选择使用 DirectRLEnv?

ManagerBasedRLEnv 采用配置驱动的方式,通过定义各种 Manager(观测、动作、奖励、终止)的配置类来构建环境。它更加模块化、易于维护。

DirectRLEnv 采用代码驱动的方式,你需要直接重写 _pre_physics_step()_get_observations()_get_rewards() 等方法。

选择 DirectRLEnv 的场景:

  • 需要在 step 中执行复杂的自定义逻辑
  • 观测/奖励之间有复杂依赖关系,难以用独立 Manager 表达
  • 需要完全控制仿真循环的执行顺序
  • 移植现有的非 IsaacLab 环境代码
练习 2 中级

编写一个 IsaacLab 的 RewardsCfg 配置类,为四足运动任务定义以下奖励项:线速度跟踪(权重 1.0)、角速度跟踪(权重 0.5)、关节加速度惩罚(权重 -2.5e-7)、动作变化率惩罚(权重 -0.01)。

Python
@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,
    )
练习 3 高级

如果在 IsaacLab 训练中发现策略收敛很慢(1000 轮迭代后奖励仍在低水平),请列出至少 5 个可能的排查方向和对应的解决策略。

排查方向与解决策略:

  1. 奖励函数设计:检查各奖励项是否冲突、权重是否合理。使用 TensorBoard 分别查看每个奖励项的曲线。尝试简化奖励函数,先只保留核心项。
  2. 观测空间:检查观测是否包含足够信息。确认观测值是否被正确归一化。添加观测历史(observation history)可能帮助策略学习动态特征。
  3. 超参数调节:降低学习率(如 1e-3 → 3e-4),增加 mini-batch 数量,调整 GAE lambda 值,增大 entropy 系数鼓励探索。
  4. 动作空间:确认动作范围是否合理。位置目标空间通常比力矩空间更容易学习。检查动作缩放因子。
  5. 环境参数:检查仿真步长是否合适(推荐 decimation=4,dt=1/200s)。确认初始状态是否合理。域随机化范围过大会导致早期训练困难。
  6. 网络结构:尝试不同的网络大小([256,128,64] 或 [512,256,128])。确认激活函数适合任务。
  7. 课程学习:引入课程学习,从简单版本开始训练,逐步增加难度。

测验 1

IsaacLab 相比传统 CPU 仿真(如 MuJoCo + 多进程)的核心加速原理是什么?

A 使用更高效的物理引擎算法
B 所有环境在同一 GPU 上并行仿真,数据无需在 CPU-GPU 间传输
C 使用更大的网络和更多训练数据
D 跳过部分物理仿真步骤以加快速度

解析:IsaacLab 的核心加速来源于 GPU 并行化。所有环境实例共享同一个物理世界,仿真和策略推理均在 GPU 上完成。这避免了传统方案中 CPU 仿真 → GPU 推理 → CPU 仿真的数据传输瓶颈,实现了端到端的 GPU 加速。

测验 2

在 IsaacLab 的 ManagerBasedRLEnv 中,如果要为四足机器人添加"脚部滑动惩罚",应该在哪个配置类中定义?

A ObservationsCfg
B ActionsCfg
C RewardsCfg
D TerminationsCfg

解析:脚部滑动惩罚是一个奖励项(负奖励即惩罚),应在 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 中实现域随机化

Python
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 步的观测拼接在一起,帮助策略推断系统动态特性(如延迟、惯性):

o_t = [s_t, s_{t-1}, s_{t-2}, ..., s_{t-N+1}]

常见设置为 N = 3~10,这能显著提升 Sim2Real 的成功率,因为策略能隐式地估计不可观测的状态。

观测设计原则

  1. 对称性:确保观测空间在机器人左右对称的情况下也是对称的
  2. 归一化:将所有观测值归一化到 [-1, 1] 范围
  3. 可获取性:仿真中使用的每一个观测信号都必须能从真实传感器获取
  4. 低噪声:优先选择噪声小、精度高的传感信号
  5. 观测延迟模拟:在仿真中添加 1-2 步的观测延迟,模拟真机通信延迟

奖励函数设计(面向真实部署)

为了使训练出的策略适合真实部署,奖励函数需要特别关注运动质量:

Python
# 面向 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_ratejoint_accel 惩罚项是 Sim2Real 成功的关键因素之一。如果缺少这些惩罚,策略通常会学到高频振动行为,在真机上立刻失效。

动作空间设计

动作类型 优点 缺点 推荐场景
位置目标 简单稳定,低层 PD 控制器保证安全 动态性能受 PD 限制 大多数 Sim2Real 应用(推荐)
位置增量 相对控制,更平滑 可能积累误差 需要精细控制时
力矩命令 最大灵活性和动态性能 迁移困难,需精确电机模型 高性能需求且有精确模型时

部署到真实机器人

策略导出

Python
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"}},
)

实时控制回路

Python
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)
练习 1 基础

列出 Sim2Real 差距的三个主要来源,并解释为什么域随机化能帮助缓解这些差距。

三个主要来源:

  1. 物理参数不匹配:仿真中的摩擦系数、质量、阻尼等参数与真实值不完全一致。
  2. 执行器模型不精确:真实电机有延迟、摩擦和非线性特性,仿真难以完美建模。
  3. 传感器噪声:真实传感器有噪声、漂移和量化误差,仿真中数据通常是"完美"的。

域随机化的作用:通过在训练时大范围随机化这些参数,策略被迫学习对参数变化具有鲁棒性的行为。当部署到真机时,真实参数大概率落在随机化范围内,策略因此能够正常工作。本质上是用"宽泛的仿真"代替"精确的仿真"。

练习 2 中级

设计一个四足运动策略的观测空间,列出所有观测分量,说明每个分量的维度和选择理由。要求适合 Sim2Real 迁移。

以 12 自由度四足机器人(每条腿 3 个关节)为例:

观测分量维度选择理由
关节位置(相对默认)12本体感知核心,编码器直接获取
关节速度12动态信息,编码器差分可得
机身角速度3IMU 陀螺仪,高精度低延迟
投影重力向量3代替欧拉角,避免万向锁
速度指令3vx, vy, yaw_rate 三维指令
上一步动作12帮助推断执行器延迟特性

总维度:45。如果添加 3 步历史堆叠,最终观测维度为 45 x 3 = 135。所有信号在真机上均可通过板载传感器直接获取。

练习 3 高级

假设你将四足运动策略部署到真实机器人后,发现机器人行走时腿部出现高频抖动。请分析可能的原因,并提出至少 3 种解决方案。

可能原因:

  • 奖励函数中缺少足够的动作平滑惩罚,策略学到了高频动作
  • 仿真中的执行器模型过于理想化,与真实电机延迟不匹配
  • PD 控制器增益在仿真和真机上不一致

解决方案:

  1. 训练端 - 增加平滑惩罚:提高 action_ratejoint_accel 的惩罚权重
  2. 训练端 - 添加动作延迟:在仿真中引入 1-3 步的随机动作延迟
  3. 部署端 - 低通滤波:在策略输出后加一阶滤波器:a_filtered = alpha * a_new + (1-alpha) * a_prev
  4. 部署端 - 降低控制频率:将控制频率从 100Hz 降至 50Hz
  5. 训练端 - 限制动作范围:缩小 action_scale,限制每步关节角度变化幅度

测验 1

以下哪种域随机化策略对纯本体感知的四足运动 Sim2Real 迁移最不重要?

A 随机化地面摩擦系数
B 随机化电机力矩和 PD 增益
C 随机化场景中的光照条件和材质颜色
D 随机化机体质量和质心位置

解析:对于纯本体感知(不使用视觉)的策略,光照和材质颜色的随机化毫无作用,因为策略不依赖视觉输入。物理参数(摩擦、力矩、质量)的随机化才是关键。

测验 2

在将 RL 策略部署到真实机器人时,以下哪项安全措施最为关键?

A 使用更大的神经网络以提高精度
B 关节限位保护和硬件急停机制
C 使用更多的域随机化参数
D 提高控制频率到 1000Hz

解析:安全性是真机部署的首要考虑。关节限位保护防止电机过转导致硬件损坏,硬件急停机制确保在策略异常时能立即切断电源保护设备和人员。

实战项目

本节包含三个难度递增的实战项目,从经典控制问题到工业级机器人训练,帮助你将前面章节的知识融会贯通。

项目 1:CartPole + PPO 从零实现

入门级

项目目标

  • 从零实现 PPO 算法(不依赖第三方 RL 库)
  • 在 Gymnasium 的 CartPole-v1 环境上训练并解决任务
  • 理解 PPO 每一步的实现细节

预期成果

策略在约 300 个 episode 内达到 500 分(满分),训练总时间 < 2 分钟。

第一步:环境与网络定义

Python
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 核心更新

Python
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()

第三步:训练循环

Python
# 初始化
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 训练

Python
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 任务不需要太大网络

第三步:训练曲线分析

Bash
# 查看训练曲线
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 分钟)。

第一步:任务配置

Python
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,
            ),
        },
    )

第二步:奖励函数设计

Python
@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)

第三步:训练与评估

Bash
# 启动四足运动训练
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 论文。如果有真机条件,尝试部署。

学习建议

理论和实践并重是最有效的学习方式。每学完一个算法,立刻动手实现或使用它训练一个任务。不要试图一次性掌握所有内容,按照路线图循序渐进。遇到问题时善用社区资源和论文检索。

常用速查表

环境安装速查

Bash
# 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

训练命令速查

Bash
# 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/