
简介这份资源面向强化学习入门者与希望动手实践Q-learning的开发者聚焦经典FrozenLake冰湖游戏场景帮助读者理解模型无关算法如何通过与环境交互学习最优策略。压缩包内共1个文件为单个Python脚本整体约1KB轻量易读可直接运行调试。内容围绕Q表初始化、学习率与折扣因子设置、ε-贪婪策略、动作选择与Q值更新公式展开并涉及OpenAI Gym环境模拟、训练与测试流程以及经验回放、Double Q-learning等性能优化思路和算法局限性讨论。已有546人学习下载适合作为强化学习课程实验或自学练手素材读者可借此掌握Q-learning核心原理与代码实现细节为后续深入深度强化学习打下基础。1. 从零手写 Q-learning 跑通 FrozenLake这个标题到底在讲什么很多人第一次接触强化学习都是从FrozenLake这个环境开始的。它足够小小到你能在一张纸上画出整个状态转移图又足够典型典型到 Q-learning 的核心机制——时序差分、探索与利用、折扣回报——全都能在它身上跑一遍。标题里的q_learning_frozenlake_Qlearning说的就是这件事用 Q-learning 算法在 FrozenLake 环境里训练一个能走到终点的智能体。它解决的不是什么工业级难题而是让你真正理解「智能体怎么从零开始学会走路」这件事。适合谁适合刚学完强化学习理论、想动手写一遍但不知道从哪下手的人也适合已经调过库但没自己实现过 Q 表更新的人。这篇文章不讲抽象概念直接带你从环境理解、Q 表初始化、训练循环到参数调优一步步跑通再把踩过的坑摊开说清楚。2. FrozenLake 环境拆解与 Q-learning 的落点2.1 FrozenLake 的状态空间与奖励机制FrozenLake 是 Gymnasium原 OpenAI Gym里的一个经典环境。默认的4x4地图长这样起点在左上角S终点在右下角G中间有F表示冰面可以走H表示冰窟掉下去就结束。智能体每走一步如果踩到F或S奖励是 0踩到H奖励是 0 且回合结束踩到G奖励是 1 且回合结束。注意这里奖励是稀疏的——只有到达终点才有正向反馈中间过程没有任何引导信号。状态空间大小是 164x4 的每个格子动作空间是 4左、下、右、上。默认模式下冰面是滑的也就是说你选了「右」智能体有 1/3 概率往右1/3 概率往下1/3 概率往上。这个滑动特性是很多人第一次跑 Q-learning 时翻车的地方——你以为智能体在乱走其实是环境本身有随机性。import gymnasium as gym import numpy as np # 创建环境is_slipperyFalse 表示冰面不滑先跑通再开滑动 env gym.make(FrozenLake-v1, is_slipperyFalse) state, info env.reset() print(初始状态:, state) # 0 print(状态空间:, env.observation_space.n) # 16 print(动作空间:, env.action_space.n) # 4这段代码做了三件事创建环境、重置环境拿到初始状态、打印状态和动作空间大小。is_slipperyFalse是关键参数先关掉滑动让智能体在确定性环境里学会基本策略再开滑动增加难度。如果你一上来就开滑动Q 表收敛会慢很多容易误以为算法写错了。2.2 Q 表的形状与初始化策略Q 表本质上是一个二维数组行是状态列是动作。16 x 4的 Q 表每个元素Q[s][a]表示在状态s下采取动作a的预期回报。初始化通常用全零也可以用小的随机数。全零的好处是确定性方便调试随机数的好处是打破对称性但在 FrozenLake 这种小环境里区别不大。# Q 表初始化16 个状态4 个动作 q_table np.zeros((env.observation_space.n, env.action_space.n)) print(Q 表形状:, q_table.shape) # (16, 4)这里用np.zeros而不是np.random.rand是因为 FrozenLake 的状态和动作都很少全零初始化足够。如果你在更大的环境里比如 CliffWalking全零也没问题。真正需要随机初始化的是神经网络近似 Q 值的场景Q 表不需要。2.3 Q-learning 更新公式的代码映射Q-learning 的核心更新公式是Q(s,a) Q(s,a) α * [r γ * max(Q(s,a)) - Q(s,a)]其中α是学习率γ是折扣因子r是即时奖励max(Q(s,a))是下一个状态的最大 Q 值。这个公式的代码实现只有一行但每个参数的含义和取值直接影响收敛速度和稳定性。alpha 0.1 # 学习率 gamma 0.99 # 折扣因子 epsilon 1.0 # 初始探索率 epsilon_decay 0.995 epsilon_min 0.01 # 单步更新 def update_q(q_table, state, action, reward, next_state, alpha, gamma): best_next_q np.max(q_table[next_state]) td_target reward gamma * best_next_q td_error td_target - q_table[state, action] q_table[state, action] alpha * td_error return q_tablealpha0.1是常见起点太大容易震荡太小收敛慢。gamma0.99表示智能体比较看重长远回报FrozenLake 里到达终点的路径不长0.99 和 0.9 差别不大但 0.99 更通用。epsilon控制探索概率初始为 1.0 表示完全随机探索随着训练逐步衰减到 0.01让智能体从探索转向利用。epsilon_decay0.995表示每回合衰减 0.5%这个值需要根据总回合数调整后面会细说。3. 从零实现 Q-learning 训练循环3.1 训练循环的骨架与回合控制训练循环的结构是外层循环控制回合数内层循环控制每回合的步数。每回合开始时重置环境然后不断选择动作、执行动作、更新 Q 表直到回合结束。回合结束的条件有两个到达终点G或者掉进冰窟H或者步数超过限制。num_episodes 5000 max_steps 100 for episode in range(num_episodes): state, info env.reset() done False step_count 0 while not done and step_count max_steps: # epsilon-greedy 动作选择 if np.random.random() epsilon: action env.action_space.sample() else: action np.argmax(q_table[state]) next_state, reward, terminated, truncated, info env.step(action) done terminated or truncated # Q 表更新 q_table update_q(q_table, state, action, reward, next_state, alpha, gamma) state next_state step_count 1 # epsilon 衰减 epsilon max(epsilon_min, epsilon * epsilon_decay)这段代码里有两个容易忽略的点。第一done terminated or truncatedGymnasium 把终止和截断分开了terminated表示到达终点或掉坑truncated表示步数超限两者都要处理。第二epsilon衰减放在回合结束后不是每步衰减否则探索率下降太快智能体还没学会就停止探索了。3.2 epsilon-greedy 策略的实现细节epsilon-greedy 是 Q-learning 最常用的行为策略。以epsilon的概率随机选动作以1-epsilon的概率选当前 Q 值最大的动作。这个策略保证了智能体在训练初期充分探索后期逐渐利用学到的知识。def epsilon_greedy(q_table, state, epsilon, n_actions): if np.random.random() epsilon: return np.random.randint(n_actions) else: return np.argmax(q_table[state])np.argmax在 Q 值相同时会返回第一个最大值的索引这在全零初始化时会导致智能体总是选动作 0向左。如果你发现训练初期智能体一直往左走不是代码错了是 Q 表还没更新所有动作 Q 值都是 0argmax返回了第一个。解决办法是初始化时加一点随机噪声或者接受这个现象等 Q 表更新后自然消失。3.3 训练过程监控与收敛判断训练过程中需要监控几个指标每回合的总奖励、每 100 回合的平均奖励、epsilon 的当前值。平均奖励是最直观的收敛指标当它稳定在某个值附近不再上升时说明 Q 表基本收敛了。rewards [] for episode in range(num_episodes): # ... 训练代码 ... rewards.append(total_reward) if (episode 1) % 500 0: avg_reward np.mean(rewards[-500:]) print(f回合 {episode1}, 平均奖励: {avg_reward:.3f}, epsilon: {epsilon:.3f})在is_slipperyFalse的情况下平均奖励应该能稳定在 1.0 左右因为智能体每次都能走到终点。如果开了滑动平均奖励会低一些大概在 0.7 到 0.8 之间因为即使策略最优也有概率滑进冰窟。如果你跑出来平均奖励一直是 0检查一下max_steps是不是太小或者epsilon衰减太快导致智能体没探索到终点。4. 参数调优与常见翻车现场4.1 学习率、折扣因子与探索率的联动这三个参数不是独立的它们之间存在联动关系。学习率alpha决定 Q 表更新的幅度折扣因子gamma决定未来奖励的权重探索率epsilon决定探索和利用的平衡。一个常见的错误是alpha设得太大比如 0.9导致 Q 值震荡不收敛另一个错误是epsilon衰减太快比如epsilon_decay0.9智能体还没探索到终点就停止探索了。参数推荐范围太大后果太小后果alpha0.01 ~ 0.3Q 值震荡不收敛收敛慢训练回合多gamma0.9 ~ 0.99远期奖励权重过高方差大短视只关注即时奖励epsilon_decay0.99 ~ 0.999探索不足陷入局部最优探索过多收敛慢epsilon_min0.01 ~ 0.1后期仍随机策略不稳定过早停止探索我一般会先用alpha0.1, gamma0.99, epsilon_decay0.995跑一遍看平均奖励曲线。如果曲线震荡厉害把alpha降到 0.05如果曲线上升太慢把epsilon_decay调到 0.999 或者增加训练回合数。4.2 冰面滑动开启后的策略变化is_slipperyTrue是 FrozenLake 的默认设置也是很多人翻车的地方。开启滑动后智能体选择的动作只有 1/3 概率按预期执行另外 2/3 概率会滑向垂直方向。这意味着即使 Q 表收敛了智能体也有可能滑进冰窟。# 开启滑动 env gym.make(FrozenLake-v1, is_slipperyTrue)开启滑动后你需要调整几个地方。第一训练回合数要增加因为环境随机性大了Q 表需要更多样本才能收敛。第二epsilon_decay可以稍微慢一点让智能体多探索。第三评估策略时不能只看单次结果要跑多次取平均。我一般会跑 100 次评估看成功率。4.3 训练不收敛的排查清单训练不收敛是新手最常见的问题。现象是平均奖励一直上不去或者震荡厉害。原因可能有很多按以下顺序排查现象一平均奖励一直是 0。原因智能体从来没到达过终点Q 表没有正向更新。解决检查max_steps是否太小epsilon是否衰减太快gamma是否太小导致远期奖励被忽略。现象二平均奖励上升后突然掉下来。原因alpha太大导致 Q 值震荡或者epsilon衰减到epsilon_min后仍然太高智能体在后期还在随机探索。解决降低alpha降低epsilon_min。现象三Q 表数值越来越大不收敛。原因gamma接近或等于 1且奖励没有归一化。解决把gamma降到 0.99 以下或者对奖励做归一化。现象四训练初期智能体一直往一个方向走。原因Q 表全零初始化argmax总是返回第一个动作。解决这是正常现象等 Q 表更新后自然消失或者初始化时加随机噪声。现象五开了滑动后成功率很低。原因环境随机性导致最优策略也有失败概率。解决这是正常的FrozenLake 滑动模式下最优策略的成功率大概在 70% 到 80% 之间不要期望 100%。5. 进阶技巧从 Q 表到可视化与策略提取5.1 提取最优策略并可视化训练完成后Q 表里每个状态的最大 Q 值对应的动作就是最优策略。你可以把策略打印成箭头图直观地看到智能体在每个格子应该往哪走。def extract_policy(q_table, n_states, n_actions): policy np.zeros(n_states, dtypeint) for s in range(n_states): policy[s] np.argmax(q_table[s]) return policy def print_policy(policy, size4): arrows [, v, , ^] # 左、下、右、上 for row in range(size): line for col in range(size): state row * size col line arrows[policy[state]] print(line) policy extract_policy(q_table, env.observation_space.n, env.action_space.n) print_policy(policy)这段代码做了两件事从 Q 表中提取每个状态的最优动作然后把动作映射成箭头打印出来。在4x4不滑动的 FrozenLake 里你应该能看到一条从左上到右下的清晰路径。如果箭头图看起来乱七八糟说明 Q 表还没收敛回去增加训练回合数或者调整参数。5.2 用滑动窗口平滑奖励曲线训练过程中的奖励曲线通常很毛糙因为每回合的奖励是 0 或 1波动很大。用滑动窗口平滑后能更清楚地看到收敛趋势。def smooth_rewards(rewards, window100): smoothed [] for i in range(len(rewards)): start max(0, i - window 1) smoothed.append(np.mean(rewards[start:i1])) return smoothed smoothed smooth_rewards(rewards, window100)window100表示每 100 回合取平均这个值可以根据总回合数调整。总回合数 5000 时100 的窗口能看出趋势总回合数 1000 时窗口可以降到 50。平滑后的曲线如果还在上升说明训练不够如果已经平了说明收敛了。5.3 保存和加载 Q 表训练好的 Q 表可以保存到文件下次直接加载使用不用重新训练。这在调参时很有用你可以保存不同参数下的 Q 表对比策略差异。# 保存 np.save(q_table_frozenlake.npy, q_table) # 加载 q_table_loaded np.load(q_table_frozenlake.npy)保存格式用.npy就行NumPy 原生支持读写都快。加载后可以直接用extract_policy提取策略或者继续训练。注意加载的 Q 表要和环境的状空间大小匹配换了环境大小就不行了。5.4 从 FrozenLake 迁移到其他表格型环境FrozenLake 跑通后你可以把同样的代码迁移到其他表格型环境比如CliffWalking、Taxi。需要改的地方很少环境创建、状态空间大小、动作空间大小。Q-learning 的核心逻辑完全不变。# CliffWalking env gym.make(CliffWalking-v0) q_table np.zeros((env.observation_space.n, env.action_space.n)) # 训练循环和 FrozenLake 一样CliffWalking的状态空间是 48动作空间是 4比 FrozenLake 大一点但 Q-learning 照样能跑。Taxi的状态空间是 500动作空间是 6也能跑但收敛需要的回合数更多。如果你发现Taxi跑不动检查一下max_steps是不是太小Taxi需要更多步数才能完成任务。我自己的习惯是每学一个新算法都先在 FrozenLake 上跑通确认逻辑没问题再迁移到更大的环境。FrozenLake 就像强化学习的「Hello World」简单但完整。希望帮到你。本文还有配套的精品资源点击获取