强化学习:让机器人学会做事
控制工程师设计控制器,而强化学习让控制器自己"长出来"。这不是魔法——它只是把"试错→反馈→改进"这个工程师每天都在做的事情系统化和自动化了。
本阶段目录
1. MDP:把控制问题写成方程
对照你已有的知识
从传感器读值 → 控制器计算 → 输出到执行器 → 传感器再读值。这个循环你每天都在做。MDP 只是把这个循环形式化了,并允许控制器通过数据学习而不是手写规则。
MDP 五元组
关节角度/图像
选动作
动力学
± 信号
| 元素 | 符号 | 机器人例子 |
|---|---|---|
| 状态空间 | $S$ | 关节角度、速度、图像、力传感器 |
| 动作空间 | $A$ | 关节力矩(连续)或 上/下/左/右(离散) |
| 转移函数 | $P(s'|s,a)$ | 执行动作后机器人实际到达的状态 |
| 奖励函数 | $R(s,a)$ | 越接近目标奖励越大,碰撞惩罚 |
| 折扣因子 | $\gamma$ | 0.95~0.99,长远 vs 眼前 |
策略——你最终要得到的东西
策略把状态映射为动作。确定性策略 $\pi(s) = a$;随机策略输出概率分布。在深度RL中,策略就是一个神经网络。
价值函数:评判"好坏"的尺度
状态价值 $V^\pi(s)$:从状态 $s$ 开始,遵循策略 $\pi$,能拿多少分。
动作价值 $Q^\pi(s,a)$:在状态 $s$ 做动作 $a$,之后遵循 $\pi$,能拿多少分。
这两个函数的核心是贝尔曼方程:
当前最优 = 即时奖励 + 折扣后的未来最优。这是一个自洽的递归关系——如果所有状态的最优值都满足这个方程,那么它们构成了一致的最优解。
2. Q-Learning:表格法的完整实现
算法核心
三个超参数:$\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
点击网格设置障碍物,然后点"开始训练"观察智能体如何学会避开障碍到达目标(绿色格)。下方显示实时奖励曲线和策略箭头。
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-Critic | Actor(策略网络)输出动作,Critic(价值网络)评价好坏 |
| 裁剪目标 | $\min(r_t A_t, \text{clip}(r_t, 1-\epsilon, 1+\epsilon) A_t)$,防止更新过大 |
| GAE优势估计 | 平衡偏差和方差,比纯蒙特卡洛或纯TD更稳定 |
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)
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与大模型 | 下一阶段:机器人控制与仿真 →