# Gymnasium 上手：CartPole 与 FrozenLake

> 来源：https://dqn.cn/basics/gymnasium ｜ 作者：DQN.CN 编辑部 ｜ 发布：2026-10-29 ｜ 更新：2026-10-30 ｜ 许可：CC BY-NC-SA 4.0（文字）/ MIT（代码）

> **要点速览**
>
> - Gymnasium 是 OpenAI Gym 的继任者，由 Farama 基金会维护，是强化学习环境的事实标准接口。
> - 核心循环是 obs, info = env.reset(seed=…) 与 obs, reward, terminated, truncated, info = env.step(action)。
> - terminated 表示进入终止状态（如杆倒下），truncated 表示因时间上限等原因被截断；两者在计算目标值时必须区别对待。
> - Wrapper 可以在不修改环境的情况下改变观测、奖励或动作；自定义环境继承 gym.Env 并用 check_env 检查接口。

[Gymnasium](https://gymnasium.farama.org/) 是由 Farama 基金会维护的强化学习环境标准接口，是 OpenAI Gym 的官方继任者。它提供统一的 `reset()` / `step()` API 和大量经典环境，几乎所有主流强化学习库（Stable-Baselines3、CleanRL、RLlib 等）都以它为标准。本节带你安装 Gymnasium，理解它的核心接口，在 FrozenLake 上训练表格型 Q-learning，在 CartPole 上测出随机策略基线，并学会编写自定义环境和使用 Wrapper。

本站所有代码示例都在 Gymnasium 1.4.0、Python 3.13、PyTorch 2.x（CPU）下实际运行过。

## 安装与环境检查

### pip 安装

```bash
pip install gymnasium                 # 核心库 + 经典控制、Toy Text 环境
pip install "gymnasium[box2d]"        # LunarLander、BipedalWalker 等（需要 swig）
pip install "gymnasium[mujoco]"       # MuJoCo 连续控制环境
pip install "gymnasium[atari]"        # Atari 游戏（ale-py）
```

本站入门教程只需要第一行。验证安装：

```python
import gymnasium as gym
env = gym.make("CartPole-v1")
print(env.observation_space, env.action_space)
```

### 常见安装问题

- **`box2d` 安装失败**：先安装 `swig`（`pip install swig` 或系统包管理器），再装 `gymnasium[box2d]`。
- **从旧版 Gym 迁移**：`import gym` 改为 `import gymnasium as gym`；`step()` 现在返回 5 个值而不是 4 个；`reset()` 返回 `(obs, info)`；随机种子通过 `env.reset(seed=...)` 设置。
- **环境版本过时**：例如 `CliffWalking-v0` 在新版本中已弃用，按提示改用 `CliffWalking-v1`。

## 核心 API

### reset 与 step

```python
obs, info = env.reset(seed=42)                                   # 开始新回合
obs, reward, terminated, truncated, info = env.step(action)      # 执行一步
env.close()                                                      # 释放资源
```

这正是 [智能体与环境](https://dqn.cn/basics/agent-environment) 中交互循环的代码表达：`step(action)` 返回下一观测 $S_{t+1}$、奖励 $R_{t+1}$ 和回合是否结束的信息。

### observation_space 与 action_space

每个环境都声明了观测空间和动作空间：

| 环境 | 观测空间 | 动作空间 | 回合步数上限 |
| --- | --- | --- | --- |
| `FrozenLake-v1` | `Discrete(16)`：格子编号 | `Discrete(4)`：左、下、右、上 | 100 |
| `CliffWalking-v1` | `Discrete(48)` | `Discrete(4)`：上、右、下、左 | 无 |
| `CartPole-v1` | `Box(4,)`：小车位置、速度、杆角度、角速度 | `Discrete(2)`：向左推、向右推 | 500 |
| `Pendulum-v1` | `Box(3,)`：$\cos\theta, \sin\theta, \dot\theta$ | `Box(1,)`：力矩 $\in [-2, 2]$ | 200 |

常用操作：`env.action_space.sample()` 随机采样动作；`env.observation_space.shape` 获取维度；`env.action_space.n` 获取离散动作数。

### terminated 与 truncated

这是新版 API 最重要的改动：

- `terminated=True`：到达 MDP 的**真正终止状态**（杆倒了、掉进冰洞、到达终点），此后没有未来奖励，TD 目标中下一状态价值取 0；
- `truncated=True`：因**外部原因被截断**（最常见的是 `TimeLimit` 步数上限），下一状态并非终止状态，TD 目标中**仍应自举**下一状态的价值。

两者任一为真都要调用 `reset()` 开始新回合，但计算目标值时必须区分。本站 [DQN](https://dqn.cn/advanced/dqn)、[PPO](https://dqn.cn/advanced/ppo) 等代码都按此处理。

## FrozenLake：表格型 Q-learning

`FrozenLake-v1`（4×4，`is_slippery=True`）中，智能体要从左上角走到右下角，避开冰洞；地面打滑，执行的动作只有 1/3 概率按预期方向移动。这里用 [Q-learning](https://dqn.cn/basics/q-learning-sarsa) 训练 2 万回合，ε 从 1.0 指数衰减到 0.05：

```python
"""Gymnasium 上手：FrozenLake 表格型 Q-learning，以及 CartPole 随机策略基线。"""
import gymnasium as gym
import numpy as np

# ---------- 1. FrozenLake：表格型 Q-learning ----------
env = gym.make("FrozenLake-v1", is_slippery=True)
nS, nA = env.observation_space.n, env.action_space.n
Q = np.zeros((nS, nA))
alpha, gamma = 0.1, 0.99
eps, eps_min, eps_decay = 1.0, 0.05, 0.9995
rng = np.random.default_rng(0)

for ep in range(20_000):
    s, _ = env.reset(seed=ep)
    done = False
    while not done:
        a = int(rng.integers(nA)) if rng.random() < eps else int(np.argmax(Q[s]))
        s2, r, terminated, truncated, _ = env.step(a)
        target = r + gamma * np.max(Q[s2]) * (not terminated)
        Q[s, a] += alpha * (target - Q[s, a])
        s, done = s2, terminated or truncated
    eps = max(eps_min, eps * eps_decay)

wins = 0
for ep in range(1000):
    s, _ = env.reset(seed=100_000 + ep)
    done = False
    while not done:
        s, r, terminated, truncated, _ = env.step(int(np.argmax(Q[s])))
        done = terminated or truncated
    wins += r
print(f"FrozenLake 贪心策略成功率（1000 回合）: {wins / 1000:.3f}")

# ---------- 2. CartPole：随机策略基线 ----------
env = gym.make("CartPole-v1")
env.action_space.seed(0)                     # action_space 有独立的随机数生成器，需单独设种子
returns = []
for ep in range(100):
    obs, _ = env.reset(seed=ep)
    G, done = 0.0, False
    while not done:
        obs, r, terminated, truncated, _ = env.step(env.action_space.sample())
        G += r
        done = terminated or truncated
    returns.append(G)
print(f"CartPole 随机策略：平均回报 {np.mean(returns):.1f} ± {np.std(returns):.1f}（100 回合）")
```
实际运行输出（约 5 秒）：

```text
FrozenLake 贪心策略成功率（1000 回合）: 0.728
CartPole 随机策略：平均回报 23.7 ± 12.2（100 回合）
```

作为对照，[动态规划](https://dqn.cn/basics/dynamic-programming) 用完整环境模型求出的最优策略实测成功率约为 0.737。Q-learning **完全不知道环境模型**，只靠试错就达到了接近最优的水平。

## CartPole：随机策略与回报统计

`CartPole-v1` 中，小车上竖着一根杆，每一步向左或向右推小车，只要杆不倒（角度不超过约 12°、小车不出界）每步就得 +1，最多 500 步。上面的输出显示随机策略平均只能坚持约 24 步。这是一个很好的**基线**：之后训练的算法至少要显著超过它，满分是 500。

CartPole 的观测是连续向量，表格方法无法直接使用（除非离散化），这正是 [DQN](https://dqn.cn/advanced/dqn)、[REINFORCE](https://dqn.cn/advanced/reinforce)、[PPO](https://dqn.cn/advanced/ppo) 等使用神经网络的算法登场的地方——本站进阶教程中它们都在 CartPole 上达到了满分 500。

## 渲染与录制视频

```python
# 实时窗口渲染（需要图形界面，需额外安装 pygame）
env = gym.make("CartPole-v1", render_mode="human")

# 获取 RGB 图像数组，便于自己保存或显示
env = gym.make("CartPole-v1", render_mode="rgb_array")
frame = env.render()            # shape (400, 600, 3)

# 录制视频（需要 moviepy）
env = gym.wrappers.RecordVideo(gym.make("CartPole-v1", render_mode="rgb_array"),
                               video_folder="videos", episode_trigger=lambda ep: ep % 100 == 0)
```

在服务器或 Colab 等无显示器环境中，请使用 `rgb_array` 模式，不要使用 `human`。

## 常用 Wrapper

Wrapper 可以在不修改环境源码的情况下改变观测、动作或奖励：

| Wrapper | 作用 |
| --- | --- |
| `TimeLimit` | 限制回合最大步数（`gym.make` 默认已按环境注册信息添加） |
| `RecordEpisodeStatistics` | 在回合结束时把回报、长度写入 `info["episode"]` |
| `NormalizeObservation` / `NormalizeReward` | 观测、奖励的在线标准化（连续控制中常用） |
| `TransformReward` / `ClipReward` | 变换、裁剪奖励 |
| `RescaleAction` / `ClipAction` | 缩放、裁剪连续动作 |
| `FrameStackObservation` | 堆叠最近 k 帧观测 |
| `AtariPreprocessing` | Atari 标准预处理（灰度、缩放、跳帧等），见 [DQN](https://dqn.cn/advanced/dqn) |

并行环境（向量化环境）可以同时运行多个环境副本，显著加快同策略算法的采样：

```python
envs = gym.vector.SyncVectorEnv([lambda: gym.make("CartPole-v1") for _ in range(8)])
obs, info = envs.reset(seed=0)        # obs.shape == (8, 4)
```

## 自定义环境

把自己的问题封装成 Gymnasium 环境，就能直接使用现成的算法库。只需继承 `gym.Env`，声明两个空间，实现 `reset` 和 `step`：

```python
"""自定义 Gymnasium 环境：一维走廊。智能体从位置 0 出发，走到位置 N-1 得到奖励 1。"""
import gymnasium as gym
import numpy as np
from gymnasium import spaces
from gymnasium.utils.env_checker import check_env

class Corridor(gym.Env):
    metadata = {"render_modes": ["ansi"], "render_fps": 4}

    def __init__(self, length=8, render_mode=None):
        self.length = length
        self.observation_space = spaces.Discrete(length)
        self.action_space = spaces.Discrete(2)          # 0 = 向左，1 = 向右
        self.render_mode = render_mode

    def reset(self, *, seed=None, options=None):
        super().reset(seed=seed)                        # 初始化 self.np_random
        self.pos = 0
        return self.pos, {}

    def step(self, action):
        self.pos = int(np.clip(self.pos + (1 if action == 1 else -1), 0, self.length - 1))
        terminated = self.pos == self.length - 1
        reward = 1.0 if terminated else 0.0
        return self.pos, reward, terminated, False, {}

    def render(self):
        return "".join("A" if i == self.pos else "." for i in range(self.length))

check_env(Corridor())                                   # 检查是否符合 Gymnasium API 规范
gym.register(id="Corridor-v0", entry_point=Corridor, max_episode_steps=50)
env = gym.make("Corridor-v0", render_mode="ansi")

# 常用 Wrapper：记录回合统计 + 奖励变换
env = gym.wrappers.RecordEpisodeStatistics(env)
env = gym.wrappers.TransformReward(env, lambda r: 10 * r)

obs, info = env.reset(seed=0)
done = False
while not done:
    obs, r, terminated, truncated, info = env.step(1)  # 一直向右
    done = terminated or truncated
print(env.render())
print("回合统计:", {k: float(v) for k, v in info["episode"].items()})
```
实际运行输出：

```text
.......A
回合统计: {'r': 1.0, 'l': 7.0, 't': 7.6e-05}
```

注意 Wrapper 的嵌套顺序：`RecordEpisodeStatistics` 在 `TransformReward` 内层，所以记录的是变换**之前**的原始回报 1.0。`check_env` 会检查空间声明、返回值类型等是否符合规范（运行时会提示该环境没有通过 `gym.make` 创建、无法测试其他渲染模式，可以忽略）。

## 常见坑

- **忘记设置随机种子**：调用 `env.reset(seed=...)` 才能让实验可复现；`action_space.sample()` 的种子需要单独用 `env.action_space.seed(...)` 设置。
- **把 truncated 当作 terminated**：会让智能体误以为“时间到了”是一种失败，导致价值估计偏低。
- **旧教程的 4 返回值写法**：`obs, r, done, info = env.step(a)` 在新版中会报错。
- **向量化环境的自动重置**：回合结束时会自动 reset，返回的观测是新回合的第一个观测，真正的最终观测在 `info` 中（具体行为由 `autoreset_mode` 决定，本站 [A2C](https://dqn.cn/advanced/actor-critic) 与 [PPO](https://dqn.cn/advanced/ppo) 代码使用 `SAME_STEP` 模式并从 `info["final_obs"]` 读取）。

## 小结

- Gymnasium 用 `reset()` / `step()` 统一了环境接口，`step` 返回 `obs, reward, terminated, truncated, info`。
- 正确区分 `terminated` 和 `truncated` 是写对强化学习代码的关键。
- 表格型 Q-learning 在 FrozenLake 上达到了接近 DP 最优解的成功率；CartPole 的随机基线约 24 分，满分 500。
- Wrapper 和向量化环境是实际项目中的常用工具。

## 练习题

1. 把 FrozenLake 换成 `map_name="8x8"`，需要多少回合 Q-learning 才能达到 50% 以上的成功率？
2. 用 `gym.wrappers.DiscretizeObservation` 或自己写的分箱函数把 CartPole 观测离散化，再用表格型 Q-learning 训练，能得到多少分？
3. 给自定义的走廊环境加上“每步 −0.01”的奖励，用 Q-learning 训练并观察学到的策略。
4. 用 `RecordVideo` 录制一段随机策略玩 CartPole 的视频。
5. 查阅 Gymnasium 文档，列出 `Pendulum-v1` 的奖励函数公式。

## 参考文献

- Towers, M. et al. (2024). Gymnasium: A Standard Interface for Reinforcement Learning Environments. [arXiv:2407.17032](https://arxiv.org/abs/2407.17032)
- Brockman, G. et al. (2016). OpenAI Gym. [arXiv:1606.01540](https://arxiv.org/abs/1606.01540)
- Barto, A. G., Sutton, R. S., & Anderson, C. W. (1983). Neuronlike adaptive elements that can solve difficult learning control problems. *IEEE Transactions on Systems, Man, and Cybernetics*, SMC-13(5), 834–846.（CartPole 问题的经典来源）

## 下一步

入门教程到此结束！继续阅读：[进阶教程：DQN](https://dqn.cn/advanced/dqn)——用神经网络把 Q-learning 扩展到 CartPole 这样的连续状态空间。

## 常见问题速查（FAQ）

**问：Gym 和 Gymnasium 有什么区别？**

答：Gymnasium 是 OpenAI Gym 停止维护后由社区接手的版本，API 基本兼容，但 step 返回 terminated 和 truncated 两个标志（旧版 Gym 只有 done），新项目应使用 Gymnasium。

**问：terminated 和 truncated 有什么区别？**

答：terminated 表示到达真正的终止状态，之后价值为 0；truncated 表示回合因时间限制等外部原因被截断，此时下一状态仍有价值，计算目标时应该用 V(s') 自举。

**问：CartPole 随机策略能得多少分？**

答：在本站实测中，CartPole-v1 上随机策略 100 回合的平均回报约为 23.7，满分为 500。

## 如何引用本文

DQN.CN 编辑部. Gymnasium 上手：CartPole 与 FrozenLake[EB/OL]. DQN.CN, 2026-10-29（更新于 2026-10-30）. https://dqn.cn/basics/gymnasium.
