十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从FrozenLake入门Q-learning:稀疏奖励下的Q-table训练实战

从FrozenLake入门Q-learning:稀疏奖励下的Q-table训练实战 简介面向强化学习零基础或入门阶段的开发者提供了一份基于Q学习解决冰湖游戏FrozenLake的Python实现脚本用于演示模型无关的强化学习算法如何在未知环境中通过试错逼近最优策略。压缩包内仅有1个Python源文件大小约1KB代码紧凑但结构清晰从Q表初始化、ε-贪婪策略选择动作到依据奖励与折扣因子更新Q值再到与OpenAI Gym的FrozenLake-v0环境交互完成训练循环均在一份脚本内完整呈现。由于体积小巧读者无需繁琐配置即可快速运行尤其适合作为教学演示或算法对比实验的基线代码也适合初学者逐行对照Q-learning更新公式理解模型无关学习的核心思想。目前已有545人学习。通过运行和阅读这份实现能够直观理解学习率、折扣因子、探索率等参数对策略收敛的影响看清智能体如何避开冰面陷阱并抵达终点同时为后续转向Double Q-learning、DQN等更高级的深度强化学习方法打下基础。1. FrozenLake 训练 Q-learning 时真正的入门门槛是奖励稀疏输入 OpenAI Gym 教育环境清单时FrozenLake 往往是第一个让人意识到强化学习不是监督学习的关卡。它只有 16 个离散状态、4 个动作奖励只在踩到目标格时返回 1其余每一步都是 0。这种奖励设计让 Q-table 在做贝尔曼更新时长期没有真值回传如果你直接把教程里的 Q-learning 代码搬过来会发现成功率长期停在 0 附近。这篇博文围绕 gym 的 FrozenLake-v1 环境把 Q-table 初始化、ε-greedy 策略、Q 值迭代更新和成功的判定方式全部过一遍并给出可以实际跑通的小型 Python 实现。适合刚接触 Q-learning 的工程师也适合想快速弄清稀疏奖励下训练怎么收敛的从业者。2. 用 Gym 构造 FrozenLake 环境与 Q-table 的初始化标准2.1 FrozenLake 的离散状态空间与动作空间FrozenLake 的经典地图是 4×4 网格起点在左上角状态 0终点在右下角状态 15。环境用 0 到 15 的整数编码所有格子游戏一次最多走 6 步大部分实现不限制步数只限制到不了终点时 episode 自然结束。动作空间是 4 个离散动作0 表示向左1 表示向下2 表示向右3 表示向上。这里有一个和确定性环境最大的差别创建环境时传入的是 is_slipperyFalse 还是 True直接决定训练难度。False 版本里你选哪个方向就走哪个方向2000 个 episode 就能稳定收敛True 版本里冰面是滑的实际移动方向在所选方向的垂直方向里随机偏移比如向右走时可能滑向上下。这种情况如果不调整超参数成功率会从 0.7 掉到 0.3 以下。import gym import numpy as np # 创建 FrozenLake 环境 env gym.make(FrozenLake-v1, map_name4x4, is_slipperyTrue) # 初始化 Q-table16 个状态 x 4 个动作 q_table np.zeros((env.observation_space.n, env.action_space.n)) print(状态数:, env.observation_space.n, 动作数:, env.action_space.n) # 验证一下具体含义 print(状态 0 表示起点状态 15 表示目标格)这段代码里的 observation_space.n 返回离散状态总数action_space.n 返回动作总数。Q-table 用零矩阵初始化表示所有动作的初始价值均为 0学习过程中会被逐步更新为实际期望回报。2.2 奖励稀疏对 Q-learning 收敛的直接影响Q-learning 的更新公式里一次经验的回报只来自立即奖励加上下一状态的最大 Q 值。因为 FrozenLake 中除了目标格以外所有转移的奖励都是 0初始 Q-table 又全是 0导致前几百轮训练里 max(Q[s][a]) 也是 0。这就形成一个死循环没有正信号Q 值不更新Q 值不更新智能体行动完全随机。打破死循环靠的是探索足够久。常见做法是设置一个较大的 epsilon 初始值比如 1.0让智能体在前期完全随机探索一旦某次 episode 偶然走到目标格reward1 会沿着状态路径一点点回传。你可以把这个过程理解成奖励是沿着一步的相邻状态逐步倒灌回来的而不是瞬间传遍所有格子。提示如果跑 5000 个 episode 后 Q-table 仍然全是 0优先检查环境是否成功创建为 is_slipperyTrue 的随机版本以及 epsilon 是否衰减过慢导致后期仍然随机乱走。2.3 Q-table 的维度检查与常见初始化错误一个最容易被忽略的问题是 Q-table 的形状。FrozenLake-v1 的 observation_space 是 Discrete(16)action_space 是 Discrete(4)所以 Q-table 一定是 (16, 4)。有些老版本环境没有 map_name 参数直接取 gym.make(FrozenLake-v0)状态空间默认是 16但如果你改过地图比如 8×8shape 就变成 (64, 4) 了。我一般会在训练前加一个断言避免训练中途崩掉assert q_table.shape (env.observation_space.n, env.action_space.n) # 防止把 Q-table 初始化成 (4, 16) 这种行列互换的形式 # 后续所有索引操作都依赖这个形状初始化经常犯的另一个错误是用 np.random.rand 来填 Q-table。Q-learning 的收敛性证明默认初始 Q 值是任意有限值随机初始化不会破坏收敛性但会拖慢速度。初始值全 0 是 FrozenLake 这类稀疏奖励环境下最不引入偏差的做法因为它不会给某个未探索动作凭空加分。3. Q-learning 更新公式与完整训练循环的可执行实现3.1 贝尔曼更新公式的逐项拆解Q-learning 属于时间差分Temporal Difference家族它不需要等到 episode 结束才拿到完整回报。每一步转移 (s, a, r, s) 都会执行这样一次更新Q[s][a] Q[s][a] alpha * (r gamma * max(Q[s][a]) - Q[s][a])其中括号里的部分叫作 TD 误差r gamma * max(Q[s][a]) 是当前估计的目标值减去当前的 Q[s][a] 得到误差。alpha 决定这个误差有多大比例被写入新 Q 值gamma 决定未来奖励折算成当前价值的比例。在 FrozenLake 里r 是 0 或 1max(Q[s][a]) 是下一个状态所有动作的最大 Q 值。注意这里选动作和更新 Q 值用的是同一张表但与策略梯度不同Q-learning 更新时的下一状态价值不看实际执行哪个动作而是直接取最大值这就是 off-policy 的含义。3.2 ε-greedy 探索策略与衰减设计训练过程中动作选择最常见的是 ε-greedy以 epsilon 的概率从所有动作里等概率随机选择以 1-epsilon 的概率选择当前 Q 值最大的动作。这份代码直接照抄就能运行import random import numpy as np alpha 0.1 # 学习率决定新信息覆盖旧信息的程度 gamma 0.99 # 折扣因子接近 1 表示重视长期回报 epsilon 1.0 # 初始探索率100% 随机动作 epsilon_min 0.01 # 探索率下限防止后期无法收敛 epsilon_decay 0.999 # 每个 episode 后衰减 q_table np.zeros((env.observation_space.n, env.action_space.n)) episode_rewards [] # 记录每个 episode 是否成功 max_steps 100 # 单局最大步数 for episode in range(20000): state env.reset()[0] # Gym 新版返回 (observation, info) done False total_reward 0 for step in range(max_steps): if random.uniform(0, 1) epsilon: action env.action_space.sample() # 随机探索 else: action np.argmax(q_table[state]) # 利用当前策略 result env.step(action) new_state, reward, done, truncated, info result[:5] # Q-learning 更新公式 q_table[state, action] q_table[state, action] alpha * ( reward gamma * np.max(q_table[new_state]) - q_table[state, action] ) state new_state total_reward reward if done or truncated: break epsilon max(epsilon_min, epsilon * epsilon_decay) episode_rewards.append(total_reward)代码里值得注意的细节是 env.reset() 和 env.step() 在 Gym 0.26 之后都返回五元组或二元组直接用 state env.reset() 在部分环境会报错。上面代码里 reset()[0] 和 step 解包成五个变量兼容新版本 API。total_reward 只记录了本次 episode 的累计奖励对于 FrozenLake 来说它的值只能是 0 或 1所以把它当成是否成功的布尔指标也能用。表格列出主要超参数在 FrozenLake 上的参考区间参数名含义FrozenLake 推荐值调大后的效果调小后的效果alpha学习率0.1 ~ 0.3收敛更快但震荡加剧收敛慢但更稳定gamma折扣因子0.95 ~ 0.99更重视远期目标只看短期奖励丢目标epsilon_decay探索衰减0.998 ~ 0.9995探索时间长收敛慢探索过短易陷入局部max_steps单局步数上限50 ~ 200增加随机游走时间过早截断成功路径3.3 用滑动窗口成功率评估是否真的学会了只打印 episode_rewards 的平均值看不出来效果因为 FrozenLake 的每局奖励是 0 或 1偶尔成功一次会把 20 局的均值顶上去不少。通常我计算最近 100 个 episode 的成功率def success_rate(rewards, window100): if len(rewards) window: return 0.0 recent rewards[-window:] return sum(recent) / window # 训练过程中每个 episode 结束后打印 if episode % 1000 0: rate success_rate(episode_rewards) print(fepisode {episode}, 成功率 {rate:.2f}, epsilon {epsilon:.3f})一个判断训练是否正常的经验数值is_slipperyFalse 时20000 步训练后成功率能到 0.95 以上is_slipperyTrue 时同一套超参数大约是 0.6 到 0.8。如果两者都低于 0.3问题大概率出在 epsilon 衰减速度或 alpha 上。4. FrozenLake 不收敛的常见原因与针对性调参方案4.1 奖励稀疏导致的 credit assignment 问题FrozenLake 每个 episode 只有一个非零奖励而且这个奖励出现在最后一步。从 Q-learning 的回传机制看只有在最终转移到目标格的那一步Q[15 的上一状态][对应动作] 才会被直接更新。更早的状态需要多次 episode 才能通过 max(Q[s]) 间接获得奖励信号。这意味着训练曲线会有很长的平台期。前几千个 episode 里成功率可能是 0因为智能体还没随机走到过目标格。常见的错误是看到曲线没变化就认为算法错了提前调整超参数或停掉训练。我一般会在随机策略下先做 200 个 episode 的 baseline 统计看随机游走本身有多少概率能到达目标。对于 slipperyTrue 的 4×4 地图这个值大约在 0.05 到 0.1 之间如果你的 Q-learning 训练后期还不如这个数字才说明真有问题。4.2 epsilon 衰减策略影响训练快慢的判断方法epsilon 衰减是一个容易被误解的参数。很多人以为 epsilon 越小越好但实际在 FrozenLake 中epsilon 衰减太快会导致前期探索不足Q-table 学不全衰减太慢会导致后期徘徊在随机动作上无法充分利用学到的策略。可以这样判断衰减是否合适打印每一步的平均回报如果训练后期 success_rate 一直在 0.5 附近波动且 epsilon 还大于 0.3说明探索太凶需要把 epsilon_decay 调小比如从 0.999 调到 0.995以便更快进入利用阶段。反过来如果成功率前 2000 局就上去了但后面又掉下来说明衰减太快导致过度利用局部最优。# 一个常用的分段衰减实现训练中期强制压低 epsilon def get_epsilon(episode, base1.0, decay0.998): eps base * (decay ** episode) return max(0.01, eps) # 对比固定衰减与分段衰减的训练曲线观察成功率变化 # 通常分段衰减在 FrozenLake 上能提升 5~10 个百分点注意不要同时调整多个超参数。先固定 alpha0.1, gamma0.99单独调 epsilon_decay确认成功率稳定后再调 alpha。4.3 reward shaping 是否适合 FrozenLake面对奖励稀疏很多工程师想到给中间状态设计势能奖励离目标近奖励大离目标远奖励小。在 FrozenLake 这种离散小状态空间里reward shaping 确实能加速前期的探索但要注意不能改变最优策略。基于势能的 shaping 函数使用公式 F(s, s) gamma * phi(s) - phi(s)且 phi 必须是有界函数。在 FrozenLake 上常见做法是用曼哈顿距离的负数作为势能# 状态坐标映射 state_to_pos {s: (s // 4, s % 4) for s in range(16)} goal_pos (3, 3) def potential(state): x, y state_to_pos[state] return - (abs(x - 3) abs(y - 3)) # 距终点越近pot 越大 # 在 Q 值更新时改写 target 值 # sigma gamma * potential(new_state) - potential(state) # target reward sigma gamma * np.max(q_table[new_state])实际测试中上面这种改写能让第一个成功 episode 的出现时间提前 30% 到 50%。但你别忘了一件事FrozenLake 的冰面滑动会破坏曼哈顿距离和真实可达性的关系某些地图上最近的格子在滑动机制下反而不容易到达目标势能函数可能引入偏差。如果你训练的目标只是验证 Q-learning 流程建议先不加 shaping把它作为后续优化方向。4.4 避免评估指标污染每次训练后单独跑测试训练过程中算滑动窗口成功率其实带有探索噪声因为 epsilon 还没降到最低。更准确的评估办法是训练结束后冻结策略把 epsilon 设为 0重新跑 1000 局新 episode 来计算成功率。# 测试阶段不更新 Q 表 def evaluate(q_table, env, episodes1000): success 0 for _ in range(episodes): state env.reset()[0] done False while not done: action np.argmax(q_table[state]) # greedy 策略无探索 state, reward, done, truncated, _ env.step(action)[:5] if done or truncated: if reward 0: success 1 break return success / episodes这个成功率才是可以拿来做横向对比的数字。上面函数中每次 env.step 都重新从观察值里取状态避免复用训练时已经过期的引用测试时不传 epsilon直接取 argmax反映真实部署时的决策表现。5. 从 FrozenLake 带走的三个 Q-learning 实战技巧第一个技巧是中间状态价值的热力图可视化。Q-learning 的 Q-table 不是黑盒训练结束后把每个状态下的最大 Q 值画出来能直观看到智能体对地图的认知。FrozenLake 只有 16 个状态直接用 matplotlib 画 4×4 网格即可。接近终点的格子应该 Q 值最高起点最低如果热力图里中间有个别格子异常突出多半是探索不够把 epsilon_decay 调小再训一次。import matplotlib.pyplot as plt max_q np.max(q_table, axis1).reshape(4, 4) plt.imshow(max_q, cmaphot) plt.colorbar(labelmax Q-value) plt.title(FrozenLake Q-value heatmap) plt.show()第二个技巧是双 Q-learning 在类似小环境上的试水。FrozenLake 的 Q 值更新里 max 操作会高估最优动作的价值在随机转移环境下高估问题更明显。双 Q-learning 维护两张 Q 表用一张表选动作、另一张表评估价值能缓解偏差。对这个 16 状态的小环境来说实现成本很低但收益在 is_slippery 环境下非常明显成功率往往能再提高 0.1 左右。第三个技巧是确定性的 offline 评估。不要因为训练时成功率达到 0.8 就觉得算法已经搞定要在不同的随机种子下各训一次计算平均值和方差。常见做法是固定 env.reset(seedvalue)训练 5 次后对比最好与最差结果。如果两次训练的成功率相差超过 0.2说明超参数对初始随机状态敏感需要增加训练局数或者调大 epsilon 初始值。把这三个技巧应用到实际项目里的工作流是先用热力图确认 Q-table 学到了合理的空间结构再用双 Q-learning 消除高估偏差最后用多种子评估得到可信的指标数字。这一套流程不限于 FrozenLake换成任何小型离散状态空间环境都能复用。本文还有配套的精品资源点击获取
返回列表