简介:这份资源面向强化学习入门者与希望动手实践Q-learning的开发者,围绕经典FrozenLake冰湖游戏,提供一份可直接运行的Python实现,帮助理解模型无关强化学习的核心流程。压缩包内共1个文件,为单个py脚本,整体约1KB,轻量便于快速阅读与调试。脚本涵盖Q表初始化、学习率与折扣因子设置、ε-贪婪策略选择动作、环境交互与Q值迭代更新,并包含训练与测试环节,可观察智能体从随机探索到逐步找到安全路径的过程。目前已有546人学习下载,适合作为强化学习课程作业、算法复现或教学演示的参考。读者可借此掌握Q-learning更新公式的代码落地方式,理解探索与利用的平衡,并在此基础上尝试经验回放、Double Q-learning等改进思路,为深入深度强化学习打下基础。
1. 从冰湖到Q表:一份能跑通的 Qlearning 实战资源
FrozenLake 这个环境,第一次跑的人多半会骂街:明明看着快到终点了,下一步直接掉冰窟窿,训练几百轮成功率还是 0。这不是代码写错了,而是 Qlearning 在稀疏奖励下的典型表现。这次拆的q_learning_frozenlake.zip里就一个核心文件q_learning_frozenlake.py,配套 OpenAI Gym 的FrozenLake-v0环境,把 Q 表初始化、ε-贪婪策略、时序差分更新、训练与测试整条链路都串起来了。它适合两类人:刚接触强化学习、想找一个能跑通的最小闭环练手的新手;以及想拿它当基线,往经验回放、Double Q-learning 上改的熟手。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序,把这份代码拆开讲。
2. Qlearning 与 FrozenLake:先搞懂 Q 表在更新什么
2.1 冰湖环境的状态、动作与奖励结构
FrozenLake 是一个网格世界,默认4x4版本共 16 个格子,其中 S 是起点、F 是冰面、H 是洞、G 是终点。智能体每走一步,环境返回(next_state, reward, done, info)四元组。奖励设计非常克制:只有踩到 G 才给1.0,踩到 H 或普通冰面都是0.0,掉洞里done=True提前结束。这种稀疏奖励就是训练初期成功率上不去的根源——智能体在绝大多数回合里拿不到任何正向信号,Q 表更新几乎全靠折扣因子 γ 把终点那点奖励往回传。
动作空间是 4 个离散动作:0=左, 1=下, 2=右, 3=上。注意这里有个反直觉的点:冰面是滑的,执行某个动作后不一定往那个方向走,而是按一定概率滑向相邻格子。默认is_slippery=True,也就是每次动作有约 1/3 概率走对方向,另外 2/3 滑向两侧。很多人第一次跑发现策略学出来是「绕远路」,就是因为这个随机性让最短路径的期望回报反而不如一条更稳的路径。
状态空间 16、动作空间 4,意味着 Q 表就是一个16x4的二维数组,用 numpy 初始化成全零即可。这个规模小到可以直接打印出来肉眼观察收敛过程,这也是它适合入门的原因。
2.2 Qlearning 更新公式与 ε-贪婪的取舍
Qlearning 是 off-policy 的时序差分算法,核心更新式:
Q(s, a) <- Q(s, a) + α * [r + γ * max(Q(s', a')) - Q(s, a)]拆开看每一项:α是学习率,控制新信息覆盖旧估计的幅度;r是即时奖励;γ是折扣因子,决定未来奖励的权重;max(Q(s', a'))是对下一状态所有动作取最大 Q 值,这一步是 off-policy 的关键——它用的是「最优动作」的估计,而不是实际执行的动作,所以行为策略和目标策略可以分离。
ε-贪婪负责探索与利用的平衡:以概率 ε 随机选动作,以1-ε选当前 Q 值最大的动作。常见做法是让 ε 从1.0线性衰减到0.01,前期多探索、后期多利用。如果 ε 固定不衰减,训练后期会一直在最优策略附近抖动,成功率上不去;如果 ε 衰减太快,又容易过早收敛到次优路径。这个参数是整份代码里最需要动手调的地方。
提示:FrozenLake 的奖励稀疏,γ 设太小(比如 0.8)会导致终点奖励传不回起点,Q 表长期全零;一般从 0.95 或 0.99 起步。
3. 把 q_learning_frozenlake.py 跑起来:环境、参数与训练循环
3.1 环境安装与最小可运行骨架
先确认依赖。这份代码依赖gym和numpy,Gym 新版本把 FrozenLake 挪到了gymnasium,如果 import 报错,按下面方式装:
pip install numpy pip install gym # 如果 gym 装完 import 报 FrozenLake 相关错误,改用 gymnasium pip install gymnasium对应的 import 也要跟着改:
# 老版本 gym import gym env = gym.make("FrozenLake-v0") # 新版本 gymnasium import gymnasium as gym env = gym.make("FrozenLake-v1", is_slippery=True)逻辑说明:FrozenLake-v0和FrozenLake-v1在奖励结构和状态编码上基本一致,主要差异在 API 返回值。gymnasium 的reset()返回(obs, info)而不是单个 obs,step()返回五元组(obs, reward, terminated, truncated, info)。如果你的代码是从老教程抄的,这两处不改会直接报解包错误。参数is_slippery控制冰面是否打滑,想先跑通逻辑可以设成False,但那样学出来的策略不具备泛化意义,正式训练建议保持True。
3.2 Q 表初始化与超参数设置
import numpy as np # 状态数 16,动作数 4 n_states = env.observation_space.n n_actions = env.action_space.n # Q 表初始化为全零 Q = np.zeros((n_states, n_actions)) # 超参数 alpha = 0.1 # 学习率 gamma = 0.99 # 折扣因子 epsilon = 1.0 # 初始探索率 epsilon_min = 0.01 epsilon_decay = 0.995 n_episodes = 10000 # 训练回合数 max_steps = 100 # 单回合最大步数逻辑说明:Q 表用np.zeros初始化是最省事的做法,也可以加一点小随机数打破对称,但在 16 状态规模下没必要。alpha=0.1是这类小环境的稳妥值,太大(0.5 以上)会让 Q 值震荡,太小(0.01)收敛慢。gamma=0.99保证终点奖励能有效回传。epsilon_decay=0.995配合 10000 回合,大约在 1000 回合左右 ε 降到 0.01 附近,这个节奏和 FrozenLake 的收敛速度比较匹配。
3.3 训练循环:选动作、执行、更新 Q 表
for episode in range(n_episodes): state, _ = env.reset() done = False for step in range(max_steps): # ε-贪婪选动作 if np.random.rand() < epsilon: action = env.action_space.sample() # 探索 else: action = np.argmax(Q[state, :]) # 利用 # 执行动作 next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated # Q 表更新 best_next = np.max(Q[next_state, :]) Q[state, action] += alpha * (reward + gamma * best_next - Q[state, action]) state = next_state if done: break # ε 衰减 epsilon = max(epsilon_min, epsilon * epsilon_decay)逻辑说明:这段是整个算法的骨架。选动作部分用np.random.rand()和 ε 比较,比random.random()更统一,因为后面 Q 表操作都是 numpy。更新那行严格对应公式,best_next取的是下一状态的最大 Q 值,注意即使done=True也要走这一步更新,因为终点状态的 Q 值全零,best_next自然为 0,公式退化成Q += alpha * (reward - Q),正好把终点奖励写进去。max_steps=100是防止智能体在冰面上无限打转,4x4 网格里 100 步足够走完任何合理路径。
3.4 测试与策略可视化
训练完不能只看训练成功率,要单独跑测试回合,并且关掉探索:
def evaluate(Q, n_test=100): wins = 0 for _ in range(n_test): state, _ = env.reset() done = False while not done: action = np.argmax(Q[state, :]) # 纯贪婪,不探索 state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated if reward == 1.0: wins += 1 break return wins / n_test print("测试成功率:", evaluate(Q))逻辑说明:测试阶段必须用argmax纯贪婪,如果还带 ε 探索,成功率会被随机动作拉低,误判模型没学好。n_test=100是经验值,太少方差大,太多没必要。正常训练 10000 回合后,is_slippery=True下成功率能到 0.7 左右,is_slippery=False下能到 1.0。如果只有 0.2 以下,基本可以判定是 ε 衰减或 γ 设置出了问题。
还可以把 Q 表打印出来看策略:
policy = np.argmax(Q, axis=1).reshape(4, 4) print(policy) # 0左 1下 2右 3上对照网格图看每个格子建议往哪走,能直观判断策略是否合理,比如起点附近应该指向能绕开洞的方向。
4. 避坑与排查:训练不收敛时先查这几处
4.1 成功率长期为 0
现象:训练几千回合,测试成功率始终是 0,Q 表打印出来几乎全零或数值极小。
原因:最常见的是 γ 设得太小,终点奖励1.0经过多步折扣后趋近于 0,传不回起点;其次是max_steps太小,智能体还没走到终点回合就结束了,永远拿不到奖励。
解决:把 γ 提到 0.95 以上,max_steps至少给到 100。另外确认is_slippery没有误设成导致环境异常的值。
4.2 训练成功率高但测试成功率低
现象:训练过程中偶尔能赢,但测试阶段成功率明显掉一截。
原因:测试代码里忘了关探索,还在用 ε-贪婪选动作;或者训练回合数不够,Q 表还没稳定。
解决:测试统一用np.argmax(Q[state, :]),并适当增加训练回合。如果两者差距依然大,说明 Q 表过拟合到某条特定路径,可以调低alpha或放慢 ε 衰减。
4.3 import gym 报错或 step 返回值解包失败
现象:import gym报模块找不到,或者state, reward, done, info = env.step(action)抛ValueError: too many values to unpack。
原因:装的是新版 gymnasium,API 和老版 gym 不兼容。
解决:要么降级到老版 gym,要么按 3.1 节改成 gymnasium 的调用方式,reset()接两个返回值,step()接五个返回值。
4.4 Q 值更新后不收敛、来回震荡
现象:Q 表数值忽大忽小,策略每轮都在变。
原因:alpha设得过大,新样本把旧估计冲得太狠;或者 ε 衰减太快,探索不足导致 Q 值估计偏差大。
解决:alpha降到 0.1 甚至 0.05,epsilon_decay调到 0.999 让探索更充分。FrozenLake 这种小环境,稳比快重要。
4.5 换了 8x8 地图后效果骤降
现象:把FrozenLake-v1的map_name改成8x8,成功率断崖式下跌。
原因:状态数从 16 涨到 64,奖励更稀疏,同样的回合数和 ε 衰减节奏不够用了。
解决:训练回合数至少翻几倍,ε 衰减放慢,max_steps提到 200。如果还是不行,说明表格型 Qlearning 在这个规模上已经吃力,该考虑函数逼近方法了。
5. 从表格型 Qlearning 往外走:验证、调参与升级路径
把这份代码跑通只是起点,真正有价值的是拿它当基线做对照实验。我一般会固定随机种子,跑三组参数对比:一组gamma=0.9、一组0.99、一组0.999,各跑 10000 回合,记录每 500 回合的测试成功率,画成曲线看收敛速度。这样能直观看到 γ 对稀疏奖励传播的影响,比看公式印象深得多。
验证策略是否真的学到东西,除了看成功率,还可以把 Q 表导出来做策略可视化。下面这段把每个格子的最优动作转成箭头,直接对照网格图检查:
import numpy as np arrows = {0: "<", 1: "v", 2: ">", 3: "^"} policy = np.argmax(Q, axis=1) # 4x4 网格,H 是洞,G 是终点 grid = ["SFFF", "FHFH", "FFFH", "HFFG"] for r in range(4): row = "" for c in range(4): idx = r * 4 + c cell = grid[r][c] if cell in ("H", "G"): row += f" {cell} " else: row += f" {arrows[policy[idx]]} " print(row)逻辑说明:policy是长度 16 的数组,每个元素是 0 到 3 的动作编号,映射成箭头后按 4x4 排布。洞和终点直接显示字母,其余显示建议动作。如果看到某个箭头指向洞,说明该状态的 Q 值还没学好,需要继续训练或调参。这个可视化在调参时比看数字快得多。
调参上有个血泪经验:不要同时动alpha、gamma、epsilon_decay三个参数,否则出了问题根本不知道是哪个引起的。固定两个、只动一个,记录结果,再换下一个。FrozenLake 规模小,单次训练几秒钟,完全有条件做这种笨功夫。
再往上走,表格型 Qlearning 的边界很明显:状态一多,Q 表就爆炸。想继续深入,常见路径是经验回放加目标网络的 DQN,把 Q 表换成神经网络;或者 Double Q-learning 解决max操作带来的 Q 值高估问题。这份q_learning_frozenlake.py的价值就在于它把最核心的更新逻辑裸露出来了,改 DQN 的时候,你会清楚哪一行被换成了网络前向传播、哪一行被换成了从回放缓冲区采样。从那以后我每次改强化学习代码,都强制先把基线在 FrozenLake 上跑一遍确认环境没问题,再上复杂环境——这个习惯帮我省了无数次排查环境 API 的时间。希望帮到你。
本文还有配套的精品资源,点击获取