ML-For-Beginners 强化学习实战:把 Q-Learning 从 CartPole 迁移到 Mountain Car 环境(Train Mountain Car 任务解析)
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
本文基于 ML-For-Beginners 课程第 8 周强化学习部分的德语任务文档 assignment.md(“Train Mountain Car”)展开,围绕 OpenAI Gym 的统一环境 API,讲清 Mountain Car 环境的动作空间、观测空间与奖励机制,并说明如何以最小代码改动把课程中已实现的 Q-Learning 算法从 CartPole 迁移到 Mountain Car 上完成训练,最终帮助读者掌握“同一算法跨 Gym 环境复用”这一强化学习的核心工作流。
任务背景:Gym 统一 API 让算法可以“换环境不换代码”
这份任务文档的开篇就点出了 OpenAI Gym 的设计前提:所有 Gym 环境都共享同一套 API——即相同的reset、step、render方法,以及相同的Action Space(动作空间)与Observation Space(观测空间)抽象。德语原文的表述是:
„[OpenAI Gym] wurde so konzipiert, dass alle Umgebungen dieselbe API bereitstellen – d.h. dieselben Methoden
reset,stepundrendersowie dieselben Abstraktionen vonAction SpaceundObservation Space. Daher sollte es möglich sein, dieselben Reinforcement-Learning-Algorithmen mit minimalen Codeänderungen an verschiedene Umgebungen anzupassen.“
(译注:Gym 被设计为让所有环境提供相同 API,因此相同的强化学习算法只需极少的代码修改即可适配不同环境。)
这也是本任务在课程中的位置所在:在第 8 周前一课(CartPole Skating,见 8-Reinforcement/2-Gym/README.md 与配套 notebook.ipynb)中,课程用 Gym 的 CartPole 环境完整实现了一套 Q-Learning;而本任务要求学员把这套现成算法搬到 Mountain Car 环境上。前一课之所以值得先读透,是因为任务中“最小改动”的起点代码全部来自它。
从文档目录结构看,德语版课程资料(translations/de/8-Reinforcement/2-Gym/下的 README.md、notebook.ipynb)与英文原版一一对应,文档末尾的免责声明(Haftungsausschluss)也说明该德语文档由 AI 翻译服务自动生成,英文原文才是权威来源(对应 8-Reinforcement/2-Gym/assignment.md)。
Mountain Car 环境:动作、观测与奖励机制
Mountain Car(MountainCar-v0)模拟一辆被困在山谷中的车,目标是冲出山谷、登上山顶拿到旗帜。它的规则要点在任务文档中全部给出,完整整理如下:
动作空间(Action Space)
每一步从 3 个离散动作中选一个:
| 值 | 含义 |
|---|---|
| 0 | 向左加速(Nach links beschleunigen) |
| 1 | 不加速(Nicht beschleunigen) |
| 2 | 向右加速(Nach rechts beschleunigen) |
观测空间(Observation Space)
观测向量只有两个连续值:
| 序号 | 观测 | 最小值 | 最大值 |
|---|---|---|---|
| 0 | 车的位置(Position des Autos) | -1.2 | 0.6 |
| 1 | 车的速度(Geschwindigkeit des Autos) | -0.07 | 0.07 |
与 CartPole 的 4 维观测(小车位置、小车速度、杆角度、杆角速度)相比,Mountain Car 只有 2 维,状态离散化的工作量更小,这也是它适合作为 CartPole 之后的下一个练习环境的原因。
奖励机制与回合终止条件
Mountain Car 的奖励系统被文档形容为“相当棘手”(ziemlich knifflig),规则是:
- 当代理到达山顶旗帜(位置 = 0.5)时,奖励为0;
- 当代理位置小于 0.5时,每步奖励为-1。
回合(episode)在以下任一条件满足时结束:
- 车的位置大于 0.5(成功登顶);
- 回合长度超过 200 步(超时失败)。
核心难点:动力不足,必须靠摆荡积累动能
文档特别强调了这个问题最关键的“trick”:
汽车的动力不足以一次性爬上山坡,因此唯一成功的方式是左右来回行驶以积累动能(Schwung aufzubauen)。
这意味着策略不能是“始终朝山顶方向加速”的贪心行为——那只会让车在坡上反复打滑。代理必须学会先向山谷另一侧倒退,换取势能,再冲上坡顶,即学会一种延迟满足的多步规划能力。这也正是 Mountain Car 相比 CartPole(奖励恒为 +1、目标是尽量存活)在奖励设计上更“反直觉”的地方:它的总回报永远是负数,训练目标是让累计 -1 惩罚尽量少,并尽快触发“位置 > 0.5”的终止条件。
任务指令:用最小改动迁移 Q-Learning 算法
任务文档的“Anweisungen”(指令)部分原文如下(德→中):
把你的强化学习算法改造到能解决 Mountain Car 问题。从 notebook.ipynb 中的现有代码出发,替换环境、修改状态离散化函数,并尝试以最少的代码改动训练现有算法。然后通过调整超参数来优化结果。
注意:很可能需要调整超参数,算法才能收敛。
对照课程提供的代码资产,可以拆解出明确的改动清单:
| 改动点 | 起点(CartPole 课中代码) | 迁移到 Mountain Car |
|---|---|---|
| 环境创建 | gym.make("CartPole-v1") | gym.make("MountainCar-v0") |
| 动作空间 | 2 个动作(actions = (0, 1)) | 3 个动作(actions = (0, 1, 2)) |
| 状态离散化 | 4 维观测的discretize/discretize_bins | 2 维观测(位置、速度),需重新设定区间与分箱数 |
| 随机探索 | np.random.randint(env.action_space.n) | 无需改动,action_space.n自动变为 3 |
训练主循环、Q-Table、Qbest保存逻辑 | 保持不变 | 保持不变 |
| 超参数 | alpha、gamma、epsilon | 需要重新调参使算法收敛 |
值得注意的是,env.action_space.n、env.reset()、env.step(a)、done标志等调用都与具体环境无关,这是 Gym 统一 API 带来的直接收益——真正需要改的只有环境名、离散化函数和动作数。
起点代码解析:CartPole 课里的 Q-Learning 是怎么写的
任务要求“从现有代码出发”,而这份代码在 8-Reinforcement/2-Gym/README.md 中逐块讲解(配套可运行 notebook 为 8-Reinforcement/2-Gym/notebook.ipynb)。下面按源码逐段梳理,作为迁移的参照系。
1. 环境初始化与观测范围
import sys !{sys.executable} -m pip install gym import gym import matplotlib.pyplot as plt import numpy as np import randomenv = gym.make("CartPole-v1") print(env.action_space) print(env.observation_space) print(env.action_space.sample())用env.observation_space.low/env.observation_space.high可以打印出观测各维度的上下界——这一步对 Mountain Car 迁移尤其重要,因为 Mountain Car 的位置界是 (-1.2, 0.6)、速度界是 (-0.07, 0.07),离散化分箱必须依据这些真实区间来设置。
2. 状态离散化:两种方法
Q-Learning 要求状态是有限离散值集,而 Gym 的观测是连续实数,因此需要离散化。课程给了两种实现:
方法一:线性缩放取整
def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))方法二:分箱(bins)
def create_bins(i, num): return np.arange(num+1)*(i[1]-i[0])/num + i[0] ints = [(-5, 5), (-2, 2), (-0.5, 0.5), (-2, 2)] # 各参数取值区间 nbins = [20, 20, 10, 10] # 各参数分箱数 bins = [create_bins(ints[i], nbins[i]) for i in range(4)] def discretize_bins(x): return tuple(np.digitize(x[i], bins[i]) for i in range(4))课程明确指出:discretize的缩放取整方式下,状态取值可能无界,状态总数不可知;discretize_bins则状态总数完全可控(各维度分箱数的乘积)。对于 Mountain Car,推荐采用分箱法:位置区间 (-1.2, 0.6) 和速度区间 (-0.07, 0.07) 都是已知有界的,正好满足“若已知某值的区间,可将其划分为若干个 bins”这一前提。一个符合课程思路的参考写法是:
# Mountain Car 参考离散化(位置 12 箱、速度 10 箱;箱数可再调) pos_bins = create_bins((-1.2, 0.6), 12) vel_bins = create_bins((-0.07, 0.07), 10) def discretize_mc(x): return (np.digitize(x[0], pos_bins), np.digitize(x[1], vel_bins))该示例是按课程 README 中discretize_bins的模式改写、供任务作答参考的,并非仓库自带代码;分箱数量属于可自由实验的超参数(箱数越多状态越精细,但 Q-Table 越大、收敛越慢)。
3. Q-Table 结构与qvalues辅助函数
因为discretize方式下状态维度无法精确预知,课程没有使用 20x20x10x10x2 的张量,而是用字典表示 Q-Table,以 (状态, 动作) 对为键:
Q = {} actions = (0, 1) # Mountain Car 中应改为 (0, 1, 2) def qvalues(state): return [Q.get((state, a), 0) for a in actions]qvalues(state)返回该状态下所有动作的 Q 值列表,未访问过的 (状态, 动作) 对默认取 0。迁移到 Mountain Car 时这段逻辑完全不用动,只需把actions扩到 3 元组。
4. 超参数与训练主循环
课程设定的超参数(code block 10)及其含义:
# hyperparameters alpha = 0.3 # 学习率:每步按多大比例更新 Q-Table 当前值 gamma = 0.9 # 折扣因子:未来奖励相对于当前奖励的权重 epsilon = 0.90 # 探索/利用因子:90% 的情况按 Q-Table 概率采样动作,10% 随机动作训练主循环(code block 11,含课程提到的两处改进——按 5000 次迭代打印平均累计奖励、保存历史最优 Q-TableQbest):
def probs(v, eps=1e-4): v = v - v.min() + eps v = v / v.sum() return v Qmax = 0 cum_rewards = [] rewards = [] for epoch in range(100000): obs = env.reset() done = False cum_reward = 0 while not done: s = discretize(obs) if random.random() < epsilon: # 利用:按 Q-Table 概率分布选动作 v = probs(np.array(qvalues(s))) a = random.choices(actions, weights=v)[0] else: # 探索:随机选动作 a = np.random.randint(env.action_space.n) obs, rew, done, info = env.step(a) cum_reward += rew ns = discretize(obs) Q[(s, a)] = (1 - alpha) * Q.get((s, a), 0) + alpha * (rew + gamma * max(qvalues(ns))) cum_rewards.append(cum_reward) rewards.append(cum_reward) if epoch % 5000 == 0: print(f"{epoch}: {np.average(cum_rewards)}, alpha={alpha}, epsilon={epsilon}") if np.average(cum_rewards) > Qmax: Qmax = np.average(cum_rewards) Qbest = Q cum_rewards = []其中 Q 值更新就是标准的 Q-Learning 更新式Q(s,a) ← (1-α)Q(s,a) + α(rew + γ·max_a' Q(s',a'))。迁移时注意两点差异会直接影响行为:
- 奖励语义不同:Mountain Car 的
rew除终止步外恒为 -1,cum_reward是负值且随步数增长越来越负。因此“平均累计奖励越高越好”的监控指标依然成立(越接近 0 越好),但数值量级与 CartPole 的 195 分标准完全不同——Mountain Car 的成功判据应是平均用步数显著下降,或在 200 步上限内登顶的比例上升。 - 训练步数放大:CartPole 单回合最多 500 步;Mountain Car 单回合最长 200 步,但每个 episode 都需要 20~60 步摆荡才可能成功,样本效率更低,
epoch总量与epsilon的退火策略需要重新实验(文档已明确提示“很可能需要调整超参数才能收敛”)。
5. 训练进度可视化
课程用移动平均来平滑随机训练的波动(code block 12),这段代码可直接复用:
def running_average(x, window): return np.convolve(x, np.ones(window) / window, mode='valid') plt.plot(running_average(rewards, 100))原始rewards曲线(每次 episode 的累计奖励)噪声很大,取 100 的滑动平均后才能看出收敛趋势(对应课程插图 train_progress_runav.png)。
6. 部署验证:按概率分布选动作
训练完成后,用与训练时一致的策略运行仿真并render()观察行为:
obs = env.reset() done = False while not done: s = discretize(obs) env.render() v = probs(np.array(qvalues(s))) a = random.choices(actions, weights=v)[0] obs, _, done, _ = env.step(a) env.close()课程还留了两个进阶挑战,迁移到 Mountain Car 同样适用:
- Task 3:课程保存了历史最优
Qbest,用Qbest覆盖Q再演示,对比行为差异; - Task 4:把“按概率分布采样动作”改为
np.argmax直接选 Q 值最大的动作,观察平衡/登顶表现是否更稳定。
评分标准(Rubric)
任务文档给出的三级评分标准完整保留如下(德译中):
| 标准 | 优秀(Vorbildlich) | 合格(Angemessen) | 待改进(Verbesserungswürdig) |
|---|---|---|---|
| Q-Learning 算法成功从 CartPole 示例迁移而来,代码改动最小,且能在200 步以内解决登顶取旗问题 | 从网络获取了新的 Q-Learning 算法但文档良好;或迁移了现有算法但未达到预期效果 | 未能成功迁移任何算法,但采取了实质性解题步骤(如实现了状态离散化、Q-Table 数据结构等) |
“优秀”档实际上给出了可量化的验收指标:< 200 步登顶——这恰好与环境的回合上限(200 步)呼应,意味着策略必须接近最优效率地摆荡。
相关资源与作答路径
完成本任务时,仓库中可直接引用的材料有:
- 德语文档本身:translations/de/8-Reinforcement/2-Gym/assignment.md,及其英文权威原文 8-Reinforcement/2-Gym/assignment.md;
- 起点代码(CartPole Q-Learning 全量实现):8-Reinforcement/2-Gym/notebook.ipynb、逐块讲解见 8-Reinforcement/2-Gym/README.md(德语版 translations/de/8-Reinforcement/2-Gym/README.md);
- 前序课程(离散棋盘上的 Q-Learning 原型,本课程的算法思想源头):8-Reinforcement/1-QLearning/README.md 与 8-Reinforcement/1-QLearning/notebook.ipynb;
- 参考实现目录:8-Reinforcement/2-Gym/solution/ 下提供了 notebook.ipynb 解答,另有 R(solution/R)与 Julia(solution/Julia)语言的解答说明。
小结
这份 “Train Mountain Car” 任务的价值在于它用最精炼的方式演示了 Gym 统一 API 的红利:算法骨架(Q-Table 字典、qvalues、ε-策略、Q 值更新式、Qbest保存)一行不改,只需替换环境名、重写 2 维观测的离散化函数、把动作集从 2 扩到 3,再针对 Mountain Car 的负奖励与 200 步上限重新调参,就能把 CartPole 的平衡策略训练流程复用到“摆荡登顶”这一需要延迟满足的新问题上。掌握这一迁移流程后,面对 Gym 中其他经典控制环境(文档也提到 Gym 的环境从 CartPole 一直到 Atari 游戏)时,都可以沿用同样的思路起步。
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考