WARNING
🧪 Beta公测版本提示:教程主体已完成,正在优化细节,欢迎大家提Issue反馈问题或建议。
s19 强化学习入门:MDP 与 Q-Learning — demo.py 代码详解
运行方式
cd docs/nn-decision/rl/qlearning/code
python demo.py依赖:numpy, matplotlib
代码逐段详解
第1步:GridWorld 环境 — 强化学习的三要素
GridWorld(网格世界)是强化学习中最经典的测试环境。Agent 在一个二维网格中移动,目标是到达终点并获得正向奖励,同时避免陷阱带来的负向惩罚。
class GridWorld:
def __init__(self, size=10, start=(0,0), goal=(9,9),
traps=[(3,3), (5,5), (7,7)],
step_reward=-0.1, goal_reward=100.0, trap_reward=-50.0):
self.action_deltas = [(-1,0), (1,0), (0,-1), (0,1)] # 上/下/左/右环境配置对应 MDP 五元组:
| MDP 元素 | GridWorld 实现 | 设计原因 |
|---|---|---|
| 100 个离散状态(10x10 网格), | 离散状态使 Q-Table 可行 | |
| 4 个离散动作: | 最简单的导航动作集 | |
| $P(s' | s, a)$(状态转移) | 确定性转移(只有边界检查) |
| 终点 +100, 陷阱 -50, 每步 -0.1 | 引导 Agent 学习最短安全路径 | |
| 0.95 | 对未来奖励适度打折 |
奖励设计的关键:
- 步数惩罚 -0.1:鼓励 Agent 走最短路径。如果没有步数惩罚,Agent 可以任意绕路,只要最终到达终点。
- 终点奖励 +100:正向信号,远大于步数惩罚的累计值(最优路径约 18 步 = -1.8),确保到达终点的策略优于中途徘徊。
- 陷阱惩罚 -50:足够强烈,让 Agent 宁可绕远路也不要冒险。这比步数惩罚大三数量级,确保了安全性优先。
1.1 step() 方法
def step(self, action):
dr, dc = self.action_deltas[action] # 获取偏移量
new_r = self.state[0] + dr
new_c = self.state[1] + dc
# 边界检查:如果移出网格,留在原地
if 0 <= new_r < self.size and 0 <= new_c < self.size:
self.state = (new_r, new_c)
# 判断奖励和终止条件
if self.state == self.goal:
reward = self.goal_reward # +100
done = True # episode 结束
elif self.state in self.traps:
reward = self.trap_reward # -50
done = True
else:
reward = self.step_reward # -0.1
done = False
return self.state, reward, doneself.action_deltas 的设计:四个动作的偏移量 [(-1,0), (1,0), (0,-1), (0,1)] 对应上/下/左/右。这种编码方式简单高效,通过索引直接获取行和列的增量。
边界处理:如果 Agent 试图移出网格(如从 (0,0) 向上移),行为无效且 Agent 留在原地。这在强化学习中称为"absorbing boundary"——尝试无效动作不会导致 episode 终止,但会产生步数惩罚(因为 step 数增加了),从而让 Agent 学会不要撞墙。
1.2 get_state_index() — 状态编码
def get_state_index(self, state):
return state[0] * self.size + state[1] # 行优先编码: index = row * 10 + col将二维坐标
第2步:Q-Learning Agent — "试错学习"的核心
2.1 Q-Table 初始化
class QLearningAgent:
def __init__(self, n_states, n_actions, alpha=0.1, gamma=0.95,
epsilon_init=1.0, epsilon_min=0.01, epsilon_decay=0.995):
self.q_table = np.zeros((n_states, n_actions)) # Q 表全零初始化
self.epsilon = epsilon_init # 初始 100% 探索Q-Table 的形状:对于 10x10 网格(100 个状态)和 4 个动作,Q-Table 是
q_table[0]:状态 0(即 (0,0))下 4 个动作的 Q 值q_table[0, 3]:在起点执行"右"动作的 Q 值
初始化为零:Q-Learning 表格方法通常将 Q 表初始化为零(或小的随机值)。初始化为零意味着 Agent 对环境的估值一开始是"中性"的——它认为任何动作在任何状态下的期望奖励都是 0。随着交互进行,Q 值逐渐收敛到真实值。
2.2 -贪婪动作选择
这是强化学习探索 vs 利用(Exploration vs Exploitation)权衡的最佳体现:
def choose_action(self, state_idx):
if np.random.random() < self.epsilon:
action = np.random.randint(self.n_actions) # 探索:随机选
else:
action = np.argmax(self.q_table[state_idx]) # 利用:选 Q 值最大的
return action为什么 argmax 在平局时返回第一个索引),永远无法探索其他动作。
argmax 在平局时的行为:当多个动作的 Q 值相同时(如初始化时全为零),np.argmax 返回第一个最大值的索引。这意味着纯利用(
2.3 Q-Learning 的 TD 更新 — 算法的核心
这是整个 demo 中最重要的一行代码:
def update(self, state_idx, action, reward, next_state_idx, done):
current_q = self.q_table[state_idx, action] # Q(s,a)
if done:
td_target = reward # 终止状态:未来价值为 0
else:
max_next_q = np.max(self.q_table[next_state_idx]) # max_{a'} Q(s', a')
td_target = reward + self.gamma * max_next_q # r + γ·max Q(s',a')
td_error = td_target - current_q # TD 误差 δ
self.q_table[state_idx, action] += self.alpha * td_error # Q += α·δ逐项解释:
current_q:当前对的价值估计。这是更新前的"旧认识"。 td_target(TD 目标):我们认为应该值多少。这是由即时奖励 加上打折后的未来最佳价值组成。注意这里使用了 而非实际执行的下一动作的 Q 值——这是 off-policy 性质的核心体现:我们用最优策略的价值来更新当前策略。 td_error(TD 误差):TD 目标与当前估计的差距。正值表示"之前低估了这个动作",负值表示"之前高估了"。 q_table += alpha * td_error:朝着 TD 目标的方向走一小步(步长由控制)。
终止状态的特殊处理:当
Off-Policy 的含义:在更新时,我们用
2.4 衰减
def decay_epsilon(self):
self.epsilon = max(self.epsilon_min, # 不低于最小探索率
self.epsilon * self.epsilon_decay) # 指数衰减指数衰减:
- Episode 0:
(完全探索) - Episode 100:
- Episode 500:
- Episode 2000:
(几乎纯利用)
epsilon_min 保底:即使训练到最后,也保留 1% 的探索概率。这是为了防止 Agent 陷入局部最优——万一学到的"最优"策略其实不是全局最优,保留少量探索机会能让 Agent 有机会发现更好的策略。
第3步:训练循环
def train_agent(env, agent, n_episodes=2000, max_steps=500):
for ep in range(n_episodes):
state = env.reset() # 回到起点
state_idx = env.get_state_index(state)
for step in range(max_steps):
action = agent.choose_action(state_idx) # ε-贪婪选动作
next_state, reward, done = env.step(action) # 与环境交互
next_state_idx = env.get_state_index(next_state)
agent.update(state_idx, action, reward, # TD 更新
next_state_idx, done)
state_idx = next_state_idx
if done:
break # 到达终点或陷阱
agent.decay_epsilon() # 衰减探索率一个 episode 的生命周期:
- 从起点出发
- 每一步:选择动作 → 执行 → 获得奖励 → 更新 Q 表
- 直到:到达终点、踩到陷阱或步数超过 max_steps
- Episode 结束,衰减
max_steps=500 作为安全网:如果 Agent 陷入循环或永远找不到终点,episode 不会无限进行。
3.1 收敛检测
if (converged_episode is None
and len(recent_rewards) >= window_size
and np.mean(recent_rewards) > 0
and ep > 500):
converged_episode = ep当最近 100 个 episode 的平均奖励首次大于 0 时,认为 Agent 已收敛。奖励大于 0 意味着 Agent 找到了终点(+100 的终点奖励超过了步数惩罚和可能的陷阱惩罚)。
第4步:提取最优策略
训练完成后,Agent 的 Q-Table 已学会每个状态的最优动作。最优策略的提取方式很简单——在每个状态选择 Q 值最大的动作:
def extract_optimal_path(env, agent):
state = env.start
path = [state]
for _ in range(max_steps):
state_idx = env.get_state_index(state)
action = np.argmax(agent.q_table[state_idx]) # 纯利用,ε=0
dr, dc = env.action_deltas[action]
state = (state[0]+dr, state[1]+dc)
path.append(state)
if state == env.goal or state in env.traps:
break
return path注意这里用 argmax 而非
价值传播的直观理解:在训练初期,只有终点附近状态的 Q 值被更新(因为只有它们能直接获得终点奖励)。随着更多 episode 的进行,这些状态的 Q 值通过贝尔曼备份逐渐传播回更早的状态——这就是 Q-Learning 中"奖励信号像涟漪一样从终点扩散回起点"的直觉。
第5步:可视化
5.1 Q 值热力图演化
绘制不同 episode 的 Q 值热力图,展示学习过程:
- Episode 0:全零(初始状态)
- Episode 50:终点附近开始出现高 Q 值(但范围有限)
- Episode 200:价值传播到网格中部
- Episode 500:几乎所有状态都有了合理的 Q 值
- Episode 1999:最优 Q 值趋于稳定
5.2 最优策略可视化
在每个格子上用箭头标明最优动作方向,箭头的颜色和透明度由 Q 值大小决定:
- 绿色箭头:正向 Q 值(这个动作朝向奖励)
- 红色箭头:负向 Q 值(这个动作应避免)
- 箭头透明度:Q 值的绝对值越大,箭头越不透明
5.3 训练奖励曲线
def plot_training_rewards(episode_rewards, window_size=50):
smoothed = np.convolve(rewards, np.ones(window_size)/window_size,
mode='valid')
ax.plot(smooth_episodes, smoothed, 'b-', linewidth=2) # 滑动平均
ax.plot(episodes, rewards, 'lightblue', alpha=0.3) # 原始奖励np.convolve 实现滑动平均:将原始奖励序列与一个全为 1/window_size 的窗口做卷积,等价于每 window_size 个值的算术平均。滑动平均值更平滑,便于观察训练趋势——理论上应该从负值逐渐上升并趋于平稳。
第6步:消融实验 — 衰减策略对比
epsilon_configs = {
"快速衰减 (decay=0.99)": {"decay": 0.99},
"中等衰减 (decay=0.995)": {"decay": 0.995},
"慢速衰减 (decay=0.999)": {"decay": 0.999},
}| 策略 | Episode 1000 时的 | 效果 | |
|---|---|---|---|
| 快速衰减 | 快 | 早期转向利用,但可能陷入次优策略 | |
| 中等衰减 | 中 | 平衡探索与利用 | |
| 慢速衰减 | 慢 | 长时间探索,收敛慢但更可能找到全局最优 |
为什么快速衰减可能不好? 如果探索不足,Agent 可能在学习初期偶然发现一条"还不错"的路径后就停止探索,永远无法发现更优的路径。这就是探索-利用困境的具体表现。
第7步:消融实验 — 学习率 对比
alpha_configs = {"0.05": 0.05, "0.1": 0.1, "0.3": 0.3, "0.5": 0.5}| 学习率 | 更新幅度 | 效果 |
|---|---|---|
| 小步 | 学习慢但稳定,不易震荡 | |
| 适步(默认) | 平衡 | |
| 大步 | 学习快但可能不稳定 | |
| 非常大 | 容易震荡,Q 值可能在最优值附近剧烈波动 |
关键概念速查表
| 概念 | 公式 | 一句话 |
|---|---|---|
| MDP | 五元组形式化决策问题 | |
| Q 函数 | $Q^{\pi}(s,a) = \mathbb{E}{\pi}[\sum \gamma^k r | s_t=s, a_t=a]$ |
| TD 更新 | Q-Learning 的核心更新规则 | |
| TD 目标 | 理想情况下 | |
| TD 误差 | 目标与实际估计的差距 | |
| 学习率 | 控制每次更新步长 | |
| 折扣因子 | 控制对未来的重视程度 | |
| 以概率 | 最简单的探索策略 | |
| 从探索逐渐转向利用 | ||
| Off-Policy | 用 | 学习最优策略,用任意策略收集数据 |
| Model-Free | 不需要知道 $P(s' | s,a)$ |
| Q-Table | np.zeros((n_states, n_actions)) | 存储每个状态-动作对的估计价值 |
| 价值传播 | 奖励从终点向起点反向传播 | Q-Learning 的收敛机制 |
源码位置
clone 后打开(相对仓库根目录):
docs/nn-decision/rl/qlearning/code/demo.py