强化学习:让机器人学会做事

控制工程师设计控制器,而强化学习让控制器自己"长出来"。这不是魔法——它只是把"试错→反馈→改进"这个工程师每天都在做的事情系统化和自动化了。

4-6 周
6 个章节
8 个代码示例
5 个验收实验

本阶段目录

  1. MDP:把控制问题写成方程
  2. Q-Learning:表格法的完整实现
  3. DQN:神经网络替代Q表
  4. PPO:现代RL的主力算法
  5. Gymnasium:标准环境与自定义环境
  6. 奖励设计:机械工程师的杀手锏
  7. 离线强化学习
  8. Sim-to-Real:从仿真到真机

1. MDP:把控制问题写成方程

对照你已有的知识

从传感器读值 → 控制器计算 → 输出到执行器 → 传感器再读值。这个循环你每天都在做。MDP 只是把这个循环形式化了,并允许控制器通过数据学习而不是手写规则。

状态 s_t 关节角度/图像 策略 π(a|s) 选择动作 环境动力学 P(s_t+1|s_t,a_t) 奖励 r_t ± 信号反馈

MDP 五元组

$(S, A, P, R, \gamma)$
📷
状态 $s_t$
关节角度/图像
🤖
策略 $\pi$
选动作
🌍
环境
动力学
📊
奖励 $r_t$
± 信号
元素符号机器人例子
状态空间$S$关节角度、速度、图像、力传感器
动作空间$A$关节力矩(连续)或 上/下/左/右(离散)
转移函数$P(s'|s,a)$执行动作后机器人实际到达的状态
奖励函数$R(s,a)$越接近目标奖励越大,碰撞惩罚
折扣因子$\gamma$0.95~0.99,长远 vs 眼前

策略——你最终要得到的东西

$a_t \sim \pi(\cdot | s_t)$

策略把状态映射为动作。确定性策略 $\pi(s) = a$;随机策略输出概率分布。在深度RL中,策略就是一个神经网络。

🔧 工程直觉:PID 控制器的参数 (Kp, Ki, Kd) 就是一个简化的"策略"。RL 做的事情本质上就是用神经网络代替你手动调的这三个数——输入传感器读数,输出控制信号。

价值函数:评判"好坏"的尺度

状态价值 $V^\pi(s)$:从状态 $s$ 开始,遵循策略 $\pi$,能拿多少分。

动作价值 $Q^\pi(s,a)$:在状态 $s$ 做动作 $a$,之后遵循 $\pi$,能拿多少分。

这两个函数的核心是贝尔曼方程

$Q^*(s,a) = R(s,a) + \gamma \,\mathbb{E}_{s'}\!\left[\max_{a'} Q^*(s',a')\right]$

当前最优 = 即时奖励 + 折扣后的未来最优。这是一个自洽的递归关系——如果所有状态的最优值都满足这个方程,那么它们构成了一致的最优解。

2. Q-Learning:表格法的完整实现

算法核心

$Q(s,a) \leftarrow Q(s,a) + \alpha\!\left[r + \gamma\max_{a'}Q(s',a') - Q(s,a)\right]$

三个超参数:$\alpha$(学习率)、$\gamma$(折扣因子)、$\epsilon$(探索概率)。

GridWorld 完整实现

import numpy as np
import matplotlib.pyplot as plt

class GridWorld:
    """机器人导航网格环境"""
    def __init__(self, size=10):
        self.size = size
        self.actions = [(0,1),(1,0),(0,-1),(-1,0)]
        self.n_actions = 4
        self._place_obstacles()

    def _place_obstacles(self):
        self.obstacles = set()
        for i in range(3,7):
            for j in range(1,9): self.obstacles.add((i,j))
        for j in range(2,9): self.obstacles.add((j,5))

    def reset(self):
        self.agent = np.array([0,0]); self.goal = np.array([9,9])
        return self.agent[0]*self.size + self.agent[1]

    def step(self, action):
        d = self.actions[action]; nxt = self.agent + d
        if 0<=nxt[0]<self.size and 0<=nxt[1]<self.size and tuple(nxt) not in self.obstacles:
            self.agent = nxt
        done = np.array_equal(self.agent, self.goal)
        reward = 100 if done else -1  # 稀疏奖励
        return self.agent[0]*self.size+self.agent[1], reward, done

class QLearning:
    def __init__(self, s_dim, a_dim, lr=0.1, gamma=0.99, eps=1.0):
        self.Q = np.zeros((s_dim, a_dim))
        self.lr, self.gamma, self.eps = lr, gamma, eps

    def act(self, s):
        if np.random.random() < self.eps: return np.random.randint(self.Q.shape[1])
        return np.argmax(self.Q[s])

    def learn(self, s, a, r, ns, done):
        target = r if done else r + self.gamma * np.max(self.Q[ns])
        self.Q[s,a] += self.lr * (target - self.Q[s,a])

env = GridWorld(10)
agent = QLearning(100, 4)
rewards_hist = []

for ep in range(2000):
    s, total_r, done = env.reset(), 0, False
    while not done:
        a = agent.act(s); ns, r, done = env.step(a)
        agent.learn(s, a, r, ns, done)
        s, total_r = ns, total_r + r
    agent.eps = max(0.01, agent.eps * 0.995)
    rewards_hist.append(total_r)
    if ep % 200 == 0:
        print(f"Ep {ep} | Avg Reward: {np.mean(rewards_hist[-100:]):.1f}")

# 绘制最优策略
policy = np.argmax(agent.Q, axis=1).reshape(10,10)
arrows = {0:'→',1:'↓',2:'←',3:'↑'}
for i in range(10):
    print(''.join(arrows.get(policy[i,j],'·') for j in range(10)))

常见坑

Q表初始化为全零→智能体永远选第一个动作→永远不探索→永远学不会。务必设置 $\epsilon > 0$,初始值至少 0.1-1.0。

🎮 交互演示:亲眼见证Q-Learning

点击网格设置障碍物,然后点"开始训练"观察智能体如何学会避开障碍到达目标(绿色格)。下方显示实时奖励曲线和策略箭头。

Episode
0
步数
0
奖励
0
ε-探索率
1.00
目标 智能体 障碍 Q值路径
class="animate-in">

3. DQN:神经网络替代Q表

为什么需要DQN

Q表能存 $10 \times 10 = 100$ 个状态,但机器人有连续关节角度(每个值有无穷多种)、摄像头图像($224 \times 224 \times 3$ 维)。神经网络可以泛化——相似状态给相似Q值。

两个关键创新

技术解决的问题
经验回放打破样本的时间相关性,每个样本可以用多次
目标网络让TD目标计算更稳定,避免"追着自己的尾巴跑"
import torch
import torch.nn as nn
from collections import deque
import random

class DQN(nn.Module):
    def __init__(self, s_dim, a_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(s_dim, 256), nn.ReLU(),
            nn.Linear(256, 256), nn.ReLU(),
            nn.Linear(256, a_dim)
        )
    def forward(self, x): return self.net(x)

class DQNAgent:
    def __init__(self, s_dim, a_dim):
        self.q_net = DQN(s_dim, a_dim)
        self.target_net = DQN(s_dim, a_dim)
        self.target_net.load_state_dict(self.q_net.state_dict())
        self.opt = torch.optim.Adam(self.q_net.parameters(), lr=1e-3)
        self.buffer = deque(maxlen=100000)
        self.gamma, self.eps = 0.99, 1.0
        self.a_dim = a_dim; self.updates = 0

    def act(self, s):
        if random.random() < self.eps: return random.randrange(self.a_dim)
        with torch.no_grad():
            return self.q_net(torch.FloatTensor(s)).argmax().item()

    def update(self):
        if len(self.buffer) < 64: return
        batch = random.sample(self.buffer, 64)
        s, a, r, ns, d = zip(*batch)
        s = torch.FloatTensor(np.array(s))
        a = torch.LongTensor(a).unsqueeze(1)
        r = torch.FloatTensor(r); ns = torch.FloatTensor(np.array(ns))
        d = torch.FloatTensor(d)

        q = self.q_net(s).gather(1, a).squeeze()
        with torch.no_grad():
            target = r + self.gamma * self.target_net(ns).max(1)[0] * (1-d)
        loss = nn.MSELoss()(q, target)
        self.opt.zero_grad(); loss.backward()
        torch.nn.utils.clip_grad_norm_(self.q_net.parameters(), 10)
        self.opt.step()

        self.eps = max(0.01, self.eps * 0.995)
        self.updates += 1
        if self.updates % 100 == 0:
            self.target_net.load_state_dict(self.q_net.state_dict())

典型失败模式

无目标网络 → Q值爆炸。无经验回放 → 样本高度相关,网络只会记住最近的经验。无梯度裁剪 → 训练不稳定,loss剧烈震荡。batch size太小 → 梯度噪声过大。

4. PPO:现代RL的主力算法

为什么DQN不够

DQN只能处理离散动作(上下左右、抓/放)。机器人需要连续动作——关节力矩、末端速度、夹爪开合度。PPO用策略梯度方法直接输出连续动作的均值和方差。

PPO的三个核心思想

组件作用
Actor-CriticActor(策略网络)输出动作,Critic(价值网络)评价好坏
裁剪目标$\min(r_t A_t, \text{clip}(r_t, 1-\epsilon, 1+\epsilon) A_t)$,防止更新过大
GAE优势估计平衡偏差和方差,比纯蒙特卡洛或纯TD更稳定
$L^{CLIP}(\theta) = \mathbb{E}\left[\min\left(r_t(\theta)\hat{A}_t,\; \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon)\hat{A}_t\right)\right]$
class Actor(nn.Module):
    """连续动作策略网络"""
    def __init__(self, s_dim, a_dim):
        super().__init__()
        self.body = nn.Sequential(
            nn.Linear(s_dim, 256), nn.ReLU(),
            nn.Linear(256, 256), nn.ReLU()
        )
        self.mean = nn.Linear(256, a_dim)
        self.log_std = nn.Parameter(torch.zeros(a_dim))

    def forward(self, s):
        x = self.body(s)
        return self.mean(x), self.log_std.exp()

    def sample(self, s):
        mean, std = self(s)
        dist = torch.distributions.Normal(mean, std)
        a = dist.sample()
        return a, dist.log_prob(a).sum(-1)

class Critic(nn.Module):
    """价值网络"""
    def __init__(self, s_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(s_dim, 256), nn.ReLU(),
            nn.Linear(256, 256), nn.ReLU(),
            nn.Linear(256, 1)
        )
    def forward(self, s): return self.net(s)
🔧 工程直觉:Actor 就像你的 PID 控制器——输入误差输出控制量。Critic 就像你做仿真验证——预测这个控制策略好不好。两者同时训练,Actor 学会更好的控制,Critic 学会更准的预测。

5. Gymnasium:标准环境与自定义环境

标准环境速查

import gymnasium as gym

# 离散动作:CartPole(入门),LunarLander(中等)
env = gym.make('CartPole-v1')
# 连续动作:Pendulum, HalfCheetah, Ant
env = gym.make('HalfCheetah-v4')
# 机器人:FetchReach, FetchPickAndPlace
env = gym.make('FetchReach-v2')

s, _ = env.reset()
for _ in range(1000):
    a = env.action_space.sample()  # 随机动作
    ns, r, done, trunc, _ = env.step(a)
    if done or trunc: s, _ = env.reset()
env.close()

自定义机器人环境

class RobotReachEnv(gym.Env):
    """2-DOF机械臂到达任务"""
    def __init__(self):
        super().__init__()
        self.action_space = gym.spaces.Box(-np.pi/4, np.pi/4, (2,))
        self.observation_space = gym.spaces.Box(-np.inf, np.inf, (6,))
        self.l1, self.l2 = 1.0, 0.8

    def reset(self, seed=None):
        super().reset(seed=seed)
        self.q = np.random.uniform(-np.pi, np.pi, 2)
        self.target = np.random.uniform(-1.5, 1.5, 2)
        return self._obs(), {}

    def _fk(self, q):
        x = self.l1*np.cos(q[0]) + self.l2*np.cos(q[0]+q[1])
        y = self.l1*np.sin(q[0]) + self.l2*np.sin(q[0]+q[1])
        return np.array([x, y])

    def _obs(self):
        ee = self._fk(self.q)
        return np.concatenate([self.q, ee, self.target]).astype(np.float32)

    def step(self, a):
        self.q = np.clip(self.q + a * 0.05, -np.pi, np.pi)
        ee = self._fk(self.q)
        dist = np.linalg.norm(ee - self.target)
        return self._obs(), -dist, dist < 0.03, False, {}

6. 奖励设计:机械工程师的杀手锏

奖励设计是RL中最需要工程直觉的环节

一个好的奖励函数比一个复杂的网络架构更重要。你的机械工程背景让你天然理解"什么行为值得鼓励"——把这种直觉编码进奖励函数。

奖励设计策略对比

策略形式优点风险
稀疏奖励到达+100,其他0无偏差,最优解明确极难学习,需海量探索
稠密奖励$-\text{dist}$ 每步学习快可能陷入局部最优
势能奖励$\Phi(s')-\Phi(s)$不改变最优策略需设计势能函数
课程学习先简单后困难稳定训练需设计课程

奖励塑形实战

def shaped_reward(state, next_state, goal, collision):
    """多目标奖励:距离开销 + 碰撞惩罚 + 到达奖励"""
    d_before = np.linalg.norm(state - goal)
    d_after = np.linalg.norm(next_state - goal)

    # 势能差(鼓励接近)
    progress = (d_before - d_after) * 10

    # 到达奖励
    goal_bonus = 100 if d_after < 0.03 else 0

    # 碰撞惩罚(安全硬约束)
    collision_penalty = -50 if collision else 0

    # 步数惩罚(鼓励快速完成任务)
    step_cost = -0.1

    return progress + goal_bonus + collision_penalty + step_cost

验收实验

  • 在 10×10 GridWorld 上用 Q-Learning 训练导航智能体,连续 100 轮成功率 > 95%
  • 用 DQN 解决 CartPole-v1,单次跑 500 步不倒(对比表格 Q-Learning 的差异)
  • 实现 PPO 在自定义 RobotReach 环境中训练,末端误差 < 3cm
  • 对比稀疏/稠密/势能奖励在同一个任务上的训练速度和最终性能
  • 写一份实验报告,分析 $\gamma$(折扣)、$\alpha$(学习率)、$\epsilon$(探索)对收敛的影响

上一阶段:← NLP与大模型   |   下一阶段:机器人控制与仿真 →

拓展资源:强化学习

GitHub 仓库

视频课程

必读论文

离线强化学习:从固定数据集学习

为什么离线RL在机器人学中至关重要

在线RL需要机器人反复试错——这在真实世界中太危险也太慢了。离线RL让你从已有的演示数据中学习策略,无需与环境交互。这对转行者特别友好:你可以在仿真中大量采数据,离线训练,然后部署。

离线RL的核心挑战:分布偏移

你只有一个固定的数据集,策略可能想尝试数据中没有覆盖的动作 → 高估那个动作的Q值 → 灾难性失败。这就是离线RL的核心难点。

算法核心思想适合场景
CQL对数据集外的动作Q值加惩罚操作任务、导航
IQL用期望回归替代max操作高维动作空间
Decision Transformer把RL转化为序列建模问题多任务、元学习
# 离线RL最小示例:用d4rl数据集训练
import gymnasium as gym
import d4rl  # 离线RL数据集

env = gym.make('halfcheetah-medium-v2')
dataset = env.get_dataset()  # 直接获取预采集数据
print(f"数据集: {len(dataset['observations'])} 条轨迹")

# 使用CQL训练(无需与环境交互!)
# from d3rlpy.dataset import MDPDataset
# from d3rlpy.algos import CQL
# cql = CQL(); cql.fit(dataset, n_steps=100000)
🔧 转行者优势:你的机械背景让你天然理解"什么是好数据"——你知道哪些操作姿态是物理可行的,哪些演示是无效的。数据质量判断能力在离线RL中极其宝贵。

Sim-to-Real:从仿真到真机

仿真中跑得完美 ≠ 真机上能用

仿真和现实的差距(Sim-to-Real Gap)是具身智能最大的工程挑战。仿真物理参数、视觉渲染、传感器噪声——都和真实世界不同。你的策略必须对这些差异鲁棒。

域随机化:让策略见过"一切可能的世界"

def randomize_physics():
    """每次重置环境时随机扰动物理参数"""
    return {
        'gravity': np.random.uniform(-9.81*0.9, -9.81*1.1),
        'friction': np.random.uniform(0.4, 1.6),
        'mass_scale': np.random.uniform(0.85, 1.15),
        'joint_damping': np.random.uniform(0.1, 3.0),
        'obs_noise': np.random.uniform(0, 0.02),
        'action_delay': np.random.randint(0, 3),  # 0-3步延迟
    }

# 在仿真中使用 domain randomization 训练
# 策略学会了在各种物理参数下完成任务
# → 真机部署时自然更鲁棒

Sim-to-Real 策略对比

策略原理成本效果
域随机化训练时大幅随机环境参数低(计算)中等
系统辨识测真机参数→校准仿真中(需真机)
域自适应用GAN/adversarial对齐特征
Sim+Real混合少量真机数据+大量仿真数据中高最好