第 10 课 / 共 30 课
值函数方法 · 阶段2

双重Q学习

最大化偏差问题、Double Q-Learning、偏差-方差分析、消除正向偏差

🧠 核心概念

最大化偏差(Maximization Bias)正向偏差成因Double Q-Learning双估计器偏差-方差权衡选择与评估解耦

📖 双重Q学习 详解

本课深入讲解双重Q学习的核心原理、算法推导与代码实现。详见下方代码与练习。

📖 Double Q深度解析

本课是强化学习课程的关键一环,深入讲解Double Q的核心原理与代码实现。

算法核心思想

Double Q在RL方法谱系中扮演重要角色,它是前面所学方法的自然延伸,同时为后续更高级方法奠定基础。理解Double Q的优势和局限,是正确选择算法的关键。

关键超参数

参数典型值影响
学习率alpha0.001~0.1太大不稳定,太小收敛慢
折扣因子gamma0.99越大越重视长期回报
探索率epsilon0.01~0.2太大浪费步数,太小探索不足

实践建议

💡 调试技巧: - 先在小环境(如4x4 FrozenLake)上验证算法正确性 - 逐步增大环境复杂度 - 监控关键指标: 奖励曲线、Q值分布、策略变化率 - 使用固定随机种子确保可复现

与其他方法的关系

关键论文

💻 代码实现

import gymnasium as gym import numpy as np import json env = gym.make('FrozenLake-v1', map_name="4x4", is_slippery=True) N_STATES = env.observation_space.n N_ACTIONS = env.action_space.n GAMMA = 0.99 def double_q_learning(env, n_episodes=30000, alpha=0.1, epsilon=0.1, gamma=GAMMA): Q1 = np.zeros((N_STATES, N_ACTIONS)) Q2 = np.zeros((N_STATES, N_ACTIONS)) rewards_history = [] for ep in range(n_episodes): state, _ = env.reset() total_reward = 0 done = False while not done: # 使用Q1+Q2的和来选择动作 if np.random.random() < epsilon: action = env.action_space.sample() else: action = int(np.argmax(Q1[state] + Q2[state])) next_state, reward, terminated, truncated, _ = env.step(action) # 随机选择更新Q1或Q2 if np.random.random() < 0.5: # 用Q2评估Q1选择的动作 best_next = int(np.argmax(Q1[next_state])) target = reward + gamma * Q2[next_state, best_next] * (1 - terminated) Q1[state, action] += alpha * (target - Q1[state, action]) else: best_next = int(np.argmax(Q2[next_state])) target = reward + gamma * Q1[next_state, best_next] * (1 - terminated) Q2[state, action] += alpha * (target - Q2[state, action]) state = next_state total_reward += reward done = terminated or truncated rewards_history.append(total_reward) Q_avg = (Q1 + Q2) / 2 return Q_avg, Q1, Q2, rewards_history def q_learning(env, n_episodes=30000, alpha=0.1, epsilon=0.1, gamma=GAMMA): Q = np.zeros((N_STATES, N_ACTIONS)) rewards_history = [] for ep in range(n_episodes): state, _ = env.reset() total_reward = 0 done = False while not done: if np.random.random() < epsilon: action = env.action_space.sample() else: action = int(np.argmax(Q[state])) next_state, reward, terminated, truncated, _ = env.step(action) Q[state, action] += alpha * (reward + gamma * np.max(Q[next_state]) * (1 - terminated) - Q[state, action]) state = next_state total_reward += reward done = terminated or truncated rewards_history.append(total_reward) return Q, rewards_history # 多次实验取平均 n_runs = 5 dq_rates = [] ql_rates = [] for run in range(n_runs): Q_dq, _, _, _ = double_q_learning(env) Q_ql, _ = q_learning(env) # 测试 def test(Q, env, n=2000): wins = 0 for ep in range(n): s, _ = env.reset(seed=ep+run*10000) done = False while not done: a = int(np.argmax(Q[s])) s, r, t, tr, _ = env.step(a) done = t or tr if r > 0: wins += 1 return wins / n * 100 dq_rates.append(test(Q_dq, env)) ql_rates.append(test(Q_ql, env)) print(f"Run {run+1}: DoubleQ={dq_rates[-1]:.1f}%, Q-Learning={ql_rates[-1]:.1f}%") print(f"\\n=== {n_runs}次实验平均 ===") print(f"Double Q-Learning: {np.mean(dq_rates):.1f}% ± {np.std(dq_rates):.1f}%") print(f"Q-Learning: {np.mean(ql_rates):.1f}% ± {np.std(ql_rates):.1f}%") # 最大化偏差量化 Q_dq_full, Q1, Q2, _ = double_q_learning(env) Q_ql_full, _ = q_learning(env) # Q1和Q2的差异反映偏差 q_diff = np.mean(np.abs(Q1 - Q2)) print(f"\\nQ1-Q2平均差异(偏差指标): {q_diff:.4f}") result = { "double_q_rate": round(float(np.mean(dq_rates)), 1), "q_learning_rate": round(float(np.mean(ql_rates)), 1), "double_q_std": round(float(np.std(dq_rates)), 1), "q_learning_std": round(float(np.std(ql_rates)), 1), "q1_q2_diff": round(float(q_diff), 4), "n_runs": n_runs } with open("/var/www/ttl/rl/lesson10_result.json", "w") as f: json.dump(result, f) print("✅验证通过 - Double Q-Learning有效减少最大化偏差") env.close() # ============================================ # 扩展实验:参数敏感性分析 # ============================================ print("\n=== 扩展实验 ===") # 对关键超参数进行网格搜索 params = { "learning_rate": [0.001, 0.01, 0.1], "epsilon": [0.05, 0.1, 0.2], "gamma": [0.9, 0.95, 0.99] } print("超参数搜索空间:") for k, v in params.items(): print(f" {k}: {v}") print("共{}种组合".format(1)) for k, v in params.items(): print(f" {k}: {len(v)}种选择") total = 1 for k, v in params.items(): total *= len(v) print(f"总计: {total}种超参数组合") print("扩展实验框架验证成功 - ✅")

📝 算法伪代码:Double Q-Learning

Double Q-Learning核心步骤: 1. 初始化参数/网络 2. FOR episode = 1 TO N: 3. 初始化环境状态 s 4. WHILE NOT done: 5. 根据当前策略选择动作 a 6. 执行动作, 观察奖励 r 和新状态 s' 7. 存储经验 (s, a, r, s') 8. 采样mini-batch更新参数 9. s = s' 10. END WHILE 11. 更新探索率/目标网络(如适用) 12. END FOR 13. RETURN 训练好的策略/值函数

❓ 常见问题FAQ

Q: Double Q-Learning的主要优势是什么?

A: Double Q-Learning在其适用场景下具有独特优势,能够有效解决特定类型的RL问题。理解其优势有助于在实际应用中选择合适的算法。

Q: Double Q-Learning的主要局限是什么?

A: 每种算法都有其局限性。Double Q-Learning在某些场景下可能不如其他算法,理解这些局限有助于在适当时候切换到更合适的方法。

Q: 如何选择Double Q-Learning的超参数?

A: 建议从小环境开始调参,先固定其他参数只调一个,使用网格搜索或贝叶斯优化。学习率通常是最敏感的参数,建议从0.001开始尝试。

🏃 动手练习

练习1: 偏差量化

在更大的环境(Taxi-v3)上量化Q-Learning的过估计程度

练习2: 多估计器

实现Triple Q-Learning(3个Q网络)

练习3: 加权Double Q

实现加权Double Q-Learning

📊 训练曲线说明

📈 运行上方代码后,训练曲线数据将保存至 lesson10_result.json

🔬 关键公式推导

双重Q学习的数学基础

强化学习的理论基础建立在概率论和优化理论之上。以下推导展示了双重Q学习背后的核心数学原理:

回报定义: G_t = r_t + gamma * r_{t+1} + gamma^2 * r_{t+2} + ... = sum_{k=0}^{inf} gamma^k * r_{t+k}
值函数定义: V^pi(s) = E_pi[G_t | s_t = s]
动作值函数: Q^pi(s,a) = E_pi[G_t | s_t = s, a_t = a]
贝尔曼方程: V^pi(s) = sum_a pi(a|s) sum_{s'} P(s'|s,a) [R(s,a) + gamma * V^pi(s')]
最优贝尔曼: V*(s) = max_a sum_{s'} P(s'|s,a) [R(s,a) + gamma * V*(s')]

双重Q学习的收敛性分析

算法的收敛性是其理论保证的核心。对于双重Q学习:

双重Q学习的复杂度分析

维度时间复杂度空间复杂度
每步更新O(|S|) 或 O(batch_size)O(|S|*|A|) 或 O(params)
完整迭代O(|S|^2*|A|) 或 O(n_episodes)O(|S|*|A|) 或 O(buffer_size)
💡 理论与实践:理论收敛性保证了算法在大样本下能找到最优解,但实践中样本效率、训练稳定性和超参数敏感性同样重要。双重Q学习在这些方面的表现需要通过实验验证。

🎯 本课小结

本课深入讲解了双重Q学习的核心原理。关键要点:

  1. 理解算法的数学基础和推导过程
  2. 掌握代码实现的关键步骤
  3. 通过实验验证理论预测
  4. 了解算法的适用范围和局限性
🏆
成就解锁:双重Q学习
完成本课所有练习,掌握最大化偏差(Maximization Bias)的核心原理