1. 从零开始:搭建你的第一个强化学习“游乐场”
如果你对人工智能感兴趣,想亲手试试让机器自己学习做决策,那么强化学习绝对是一个让你兴奋的起点。而OpenAI的Gym库,就是为你准备的、最棒的“游乐场”。想象一下,你不需要自己从零搭建复杂的物理引擎或游戏逻辑,就能直接让智能体(Agent)在经典的游戏环境中学习,比如平衡一根杆子(CartPole)或者开着小车爬山(MountainCar)。这就像你拿到了一个功能齐全的机器人实验平台,只需要关心“大脑”(算法)的训练,而不用操心“身体”(环境)的制造。
我刚开始接触强化学习时,就被Gym这种“开箱即用”的特性深深吸引了。它把环境交互标准化,我们只需要写几行代码,就能看到智能体在环境中探索、试错、学习。今天,我就带你从最基础的安装开始,一步步搭建环境,并深入解析两个最经典的入门问题:CartPole(平衡木)和MountainCar(小车爬山)。我会分享我踩过的坑和实测有效的技巧,保证你跟着做就能跑通第一个强化学习程序,并理解其背后的交互逻辑。无论你是学生、开发者还是算法爱好者,这篇文章都能帮你快速上手。
2. Gym库安装与环境初探
2.1 一步到位的安装指南
安装Gym库其实非常简单,但为了确保环境干净、避免后续的依赖冲突,我强烈建议你先创建一个独立的Python虚拟环境。这是我多年实践下来的经验,能省去很多麻烦。
# 使用conda创建虚拟环境(如果你用Anaconda)
conda create -n rl_gym python=3.8
conda activate rl_gym
# 或者使用venv创建虚拟环境
python -m venv rl_gym
# Windows系统激活
rl_gym\Scripts\activate
# Linux/Mac系统激活
source rl_gym/bin/activate
激活虚拟环境后,就可以安装Gym了。官方提供了几种安装方式:
# 1. 基础安装(最常用,包含经典控制、算法等基础环境)
pip install gym
# 2. 如果你想玩Atari游戏(比如吃豆人),需要安装额外的依赖
pip install gym[atari]
# 注意:在Windows上可能需要先安装一些C++编译工具
# 3. 如果你想从源码安装,以便修改环境或贡献代码
git clone https://github.com/openai/gym
cd gym
pip install -e .
我实测下来,对于新手,直接 pip install gym 就足够了。它会安装最核心的库和经典控制环境,比如我们马上要用到的CartPole-v1和MountainCar-v0。安装完成后,可以在Python中导入验证一下:
import gym
print(gym.__version__) # 查看版本,确保安装成功
如果没报错,恭喜你,强化学习的“游乐场”已经搭建好了!这里有个小坑我遇到过:如果你的网络环境导致pip安装慢或失败,可以尝试使用国内的镜像源,比如清华源:pip install gym -i https://pypi.tuna.tsinghua.edu.cn/simple。
2.2 理解环境的核心:观测、动作与空间
安装好Gym,我们得先理解智能体是如何与环境“对话”的。这就像教一个孩子玩游戏,你得告诉他:1. 你现在看到了什么(观测 Observation);2. 你能做什么(动作 Action);3. 你做得好不好(奖励 Reward)。
我们用CartPole环境来具体看看。CartPole是一个经典的控制问题:一个小车可以在一条轨道上左右移动,车上通过一个关节连接着一根杆子。你的目标是控制小车左右移动,不让杆子倒下来。
import gym
# 创建环境,'CartPole-v1'是当前更常用的版本
env = gym.make('CartPole-v1')
# 初始化环境,返回初始观测
initial_observation = env.reset()
print(f"初始观测: {initial_observation}")
print(f"观测空间: {env.observation_space}")
print(f"动作空间: {env.action_space}")
运行这段代码,你会看到类似这样的输出:
初始观测: [-0.01258566 -0.00194414 0.04207723 0.01906179]
观测空间: Box([-4.8000002e+00 -3.4028235e+38 -4.1887903e-01 -3.4028235e+38], [4.8000002e+00 3.4028235e+38 4.1887903e-01 3.4028235e+38], (4,), float32)
动作空间: Discrete(2)
这告诉我们什么?
- 观测(Observation):一个包含4个浮点数的数组。它们分别代表:小车位置、小车速度、杆子角度、杆子顶端速度。这就是智能体每一帧“看到”的世界状态。
- 观测空间(Observation Space):类型是
Box,表示一个多维连续空间。low和high数组定义了每个观测值的下限和上限。比如小车位置被限制在[-2.4, 2.4]之间(单位可理解为米),inf表示正负无穷,实际上由物理规则限制。 - 动作空间(Action Space):类型是
Discrete(2),表示离散动作空间,只有两个动作:0(向左推小车)和1(向右推小车)。
理解这些“空间”至关重要。你的智能体输出的动作必须在动作空间内,它接收到的观测也一定在观测空间内。MountainCar环境的空间又不一样:
env = gym.make('MountainCar-v0')
print(f"MountainCar 观测空间: {env.observation_space}")
print(f"MountainCar 动作空间: {env.action_space}")
print(f"观测值下限: {env.observation_space.low}")
print(f"观测值上限: {env.observation_space.high}")
输出:
观测空间: Box([-1.2 -0.07], [0.6 0.07], (2,), float32)
动作空间: Discrete(3)
这里观测只有两个值:小车位置和速度。动作有3个:0(向左加速)、1(不加速)、2(向右加速)。你看,不同的问题,环境的“接口”是不同的,但Gym用统一的step()函数把它们封装了起来。
3. 与环境的第一次交互:从随机策略开始
理解了环境的基本构成,我们就可以让智能体开始尝试了。最开始的尝试,通常是从“随机策略”开始——也就是让智能体像无头苍蝇一样随机选择动作。别小看这个随机策略,它是我们测试环境是否正常运行、理解交互流程的必经步骤。
3.1 核心交互循环:reset, step, render, close
与Gym环境交互,主要围绕四个核心方法:reset(), step(), render(), close()。我们写一个完整的随机交互循环看看:
import gym
import time
env = gym.make('CartPole-v1', render_mode='human') # 创建环境,指定渲染模式为人类可看
observation, info = env.reset(seed=42) # 重置环境,返回初始观测和信息,seed保证可复现
for step in range(200): # 最多运行200步
env.render() # 渲染出图形界面,你会看到一个小车和杆子的动画
action = env.action_space.sample() # 关键!随机从动作空间采样一个动作(0或1)
# 核心:执行动作,与环境交互一步
observation, reward, done, truncated, info = env.step(action)
print(f"步骤 {step}: 动作={action}, 观测={observation}, 奖励={reward}, 结束?{done}")
if done: # 如果done为True,代表回合结束(如杆子倒得太厉害)
print(f"回合在 {step+1} 步后结束")
break
time.sleep(0.05) # 稍微延迟,方便观察
env.close() # 关闭渲染窗口,释放资源
逐行解释一下:
gym.make(): 创建特定环境实例。render_mode='human'会弹出图形窗口。env.reset(): 重置环境到初始状态。每次开始新回合(episode)都必须调用。返回初始观测和info字典(包含额外信息,如随机数种子状态)。env.render(): 将当前环境状态以图形化方式显示出来。对于CartPole,你会看到一个经典的小车平衡杆画面。env.action_space.sample(): 这是环境提供的一个便捷方法,从动作空间中均匀随机采样一个合法动作。在智能体学会决策前,我们就用它来“瞎蒙”。env.step(action): 这是整个强化学习交互的灵魂。它接受一个动作,推进环境到下一个时间步,并返回五个值:observation(object): 新的观测。智能体根据这个决定下一步动作。reward(float): 即时奖励。在CartPole中,每存活一步奖励+1。我们的目标就是最大化累计奖励。done(bool): 回合是否终止。True表示任务失败(杆子倒了)或成功(对于CartPole-v1,坚持了500步),需要reset。truncated(bool): 回合是否被截断(例如超出最大步数限制)。与done一起决定回合结束。info(dict): 调试信息。通常包含一些对学习有帮助但智能体不能直接利用的数据(如原始状态)。
env.close(): 关闭环境,特别是关闭渲染窗口。不关闭有时会导致内存泄漏。
运行这段代码,你会看到一个窗口,小车随机左右移动,杆子很快倒下,回合结束。控制台会打印每一步的信息。这就是最基础的“智能体-环境”交互循环。奖励(reward) 在这里是+1每步,所以总奖励就是存活步数。一个完全随机的策略,在CartPole-v1上平均存活步数大概在20-40步左右。
3.2 封装交互逻辑:编写可复用的测试函数
为了后续方便测试不同策略,我们把交互逻辑封装成一个函数。这个函数能运行一个完整回合,并返回总奖励。
def run_random_episode(env_name='CartPole-v1', max_steps=500, render=False):
"""
运行一个使用完全随机策略的回合。
参数:
env_name: 环境名称
max_steps: 最大步数,防止无限循环
render: 是否渲染图形界面
返回:
total_reward: 本回合获得的总奖励
steps: 本回合持续了多少步
"""
env = gym.make(env_name)
if render:
env = gym.make(env_name, render_mode='human')
observation, info = env.reset()
total_reward = 0
steps = 0
for step in range(max_steps):
if render:
env.render()
action = env.action_space.sample() # 随机策略
observation, reward, done, truncated, info = env.step(action)
total_reward += reward
steps += 1
if done or truncated:
break
env.close()
return total_reward, steps
# 测试一下
total_reward, steps = run_random_episode('CartPole-v1', render=True)
print(f"随机策略:总奖励={total_reward}, 持续步数={steps}")
# 运行100个回合,看看平均表现
rewards = []
for i in range(100):
reward, _ = run_random_episode('CartPole-v1', render=False)
rewards.append(reward)
print(f"随机策略在100个回合中的平均奖励: {sum(rewards)/len(rewards):.1f}")
这个函数已经有点“智能体测试平台”的样子了。通过它,我们能定量评估一个策略(哪怕是随机策略)的好坏。在强化学习中,我们通常用平均回合奖励作为核心评估指标。对于CartPole-v1,官方认为连续100回合平均奖励达到475分(最大500)就算问题“解决”了。随机策略离这个目标还差得远,这就需要我们设计更聪明的策略。
4. 深入经典控制问题:CartPole与MountainCar的挑战
现在我们已经会和环境基本交互了,接下来深入看看两个经典问题到底难在哪里,以及我们如何从观测信息中寻找决策的线索。
4.1 CartPole(倒立摆):平衡的艺术
CartPole问题看似简单,但它是理解控制与稳定的绝佳例子。观测空间有4个维度,我们如何解读它们来帮助平衡?
env = gym.make('CartPole-v1')
obs, _ = env.reset()
print("观测值索引与含义解释:")
print("obs[0]: 小车位置 (单位:米)。范围 ~ [-2.4, 2.4]。越接近边界越危险。")
print("obs[1]: 小车速度 (单位:米/秒)。正数向右,负数向左。")
print("obs[2]: 杆子角度 (单位:弧度)。0表示垂直向上。正数向右倒,负数向左倒。")
print("obs[3]: 杆子顶端速度 (单位:弧度/秒)。表示杆子倒下的角速度。")
一个最直观的启发式策略是:杆子往哪边倒,小车就往哪边移动,用底盘的移动来“接住”杆子。我们可以用obs[2](角度)的正负来决定动作。写一个简单的规则试试:
def simple_policy_cartpole(observation):
"""
一个简单的基于规则的策略:杆子角度大于0(向右倒),就向右推车(动作1),反之向左(动作0)。
同时考虑角度速度,让控制更激进一些。
"""
pos, vel, angle, ang_vel = observation
# 如果角度绝对值太大,优先根据角度方向决定
if abs(angle) > 0.1:
return 1 if angle > 0 else 0
# 角度较小时,结合角度速度来预判
else:
# 角度速度为正(向右倒加快),则向右推
if ang_vel > 0:
return 1
else:
return 0
# 测试这个简单策略
env = gym.make('CartPole-v1', render_mode='human')
obs, _ = env.reset()
total_reward = 0
for step in range(500):
env.render()
action = simple_policy_cartpole(obs) # 使用我们的规则,而不是随机采样
obs, reward, done, truncated, info = env.step(action)
total_reward += reward
if done:
print(f"简单规则策略在 {step+1} 步后失败,总奖励: {total_reward}")
break
env.close()
这个策略可能比随机策略好一些,但依然很难达到满分500。因为它只考虑了当前状态,没有“记忆”和历史信息,也无法应对复杂的动态。这正好引出了强化学习的必要性:我们需要一个能自动从与环境的交互中学习最优策略的智能体。
4.2 MountainCar(小车爬山):势能与动能的博弈
MountainCar是另一个经典问题,它展示了“稀疏奖励”的挑战。环境描述是:一辆动力不足的小车位于两山之间的谷底,它的目标是冲上右侧山顶的旗子处。动作是离散的:向左推、不动、向右推。但小车的发动机力量太小,无法直接爬上山坡,必须通过左右摆动来积累动量,像荡秋千一样把自己“甩”上去。
env = gym.make('MountainCar-v0')
print("MountainCar 观测解释:")
print("obs[0]: 小车位置,范围 [-1.2, 0.6]。起点约在 -0.5,目标在 0.5。")
print("obs[1]: 小车速度,范围 [-0.07, 0.07]。")
这里的奖励设计非常“苛刻”:每走一步,奖励都是 -1,直到成功到达目标获得0奖励并结束。这意味着智能体不仅要在最终成功,还要尽可能快地成功,以减少负奖励的累积(即最大化总奖励,使其接近0)。如果一直失败,总奖励就是-200(最大步数限制)。这给学习带来了巨大挑战,因为大部分时间智能体得到的都是相同的负反馈,只有最终成功时才有信号。
我们同样可以设计一个简单的启发式规则:如果小车速度是正的(向右),并且位置还没到最右,就继续向右加速;如果速度是负的,就向左加速积累动量。
def simple_policy_mountaincar(observation):
position, velocity = observation
# 一个非常简单的策略:总是向右加速,除非在最左边且向左运动
if position > -0.5 and velocity > 0:
return 2 # 向右加速
elif velocity < 0:
return 0 # 向左加速
else:
return 1 # 不动
# 测试这个策略
env = gym.make('MountainCar-v0', render_mode='human')
obs, _ = env.reset()
total_reward = 0
for step in range(200):
env.render()
action = simple_policy_mountaincar(obs)
obs, reward, done, truncated, info = env.step(action)
total_reward += reward
if done:
print(f"简单策略在 {step+1} 步后成功!总奖励: {total_reward}")
break
if not done:
print(f"简单策略在200步内未成功,总奖励: {total_reward}")
env.close()
这个简单策略很可能无法成功,因为它没有实现“摆动积攒动量”的复杂行为。MountainCar的成功需要智能体学会先向左后退,加速获得动量,再向右冲。这恰恰是强化学习算法(如Q-learning、DQN)大显身手的地方,它们能通过试错发现这种反直觉的策略。
5. 构建你的第一个智能体类
前面我们写了些零散的策略函数,但在真正的强化学习项目中,我们通常会将策略封装成一个“智能体”(Agent)类。这个类负责根据观测做出决策(decide方法),并可能根据交互结果进行学习(learn方法)。下面我们来构建一个更结构化的、可扩展的智能体框架。
5.1 设计一个通用智能体基类
一个好的设计是从一个基类开始,定义智能体的基本接口。这样以后换不同的算法(比如从随机策略换成Q-learning),只需要继承这个基类并重写方法。
import numpy as np
class BaseAgent:
"""强化学习智能体的基类"""
def __init__(self, env):
"""
初始化智能体。
参数:
env: Gym环境实例,用于获取动作空间等信息。
"""
self.env = env
self.n_actions = env.action_space.n if hasattr(env.action_space, 'n') else None
print(f"智能体初始化完成,动作空间大小: {self.n_actions}")
def decide(self, observation):
"""
根据当前观测做出决策(选择动作)。
参数:
observation: 当前环境观测。
返回:
action: 选择的动作。
"""
# 基类中这是一个抽象方法,子类必须实现
raise NotImplementedError("子类必须实现 decide 方法")
def learn(self, observation, action, reward, next_observation, done):
"""
根据一步交互的经验进行学习。
参数:
observation: 交互前的观测。
action: 执行的动作。
reward: 获得的奖励。
next_observation: 交互后的新观测。
done: 回合是否结束。
"""
# 基类中可以选择不实现任何学习逻辑(如随机智能体)
pass
def save(self, filepath):
"""保存智能体模型(如神经网络参数)到文件。"""
pass
def load(self, filepath):
"""从文件加载智能体模型。"""
pass
5.2 实现随机智能体和规则智能体
现在,我们可以基于这个基类,快速实现前面用过的两种策略。
class RandomAgent(BaseAgent):
"""完全随机选择动作的智能体,用作基线。"""
def decide(self, observation):
# 忽略观测,完全随机选择
return self.env.action_space.sample()
class SimpleRuleBasedAgent(BaseAgent):
"""基于简单规则的智能体,针对特定环境手工设计策略。"""
def __init__(self, env):
super().__init__(env)
self.env_name = env.unwrapped.spec.id if env.spec else "Unknown"
def decide(self, observation):
if self.env_name == 'CartPole-v1':
# CartPole的简单规则
_, _, angle, ang_vel = observation
if abs(angle) > 0.1:
return 1 if angle > 0 else 0
else:
return 1 if ang_vel > 0 else 0
elif self.env_name == 'MountainCar-v0':
# MountainCar的简单规则(效果有限)
pos, vel = observation
if vel > 0 and pos < 0.4:
return 2 # 向右
elif vel < 0:
return 0 # 向左
else:
return 1 # 不动
else:
# 对于其他未知环境,退化为随机策略
print(f"警告:未知环境 {self.env_name},使用随机策略")
return self.env.action_space.sample()
5.3 编写通用的训练与测试循环
有了智能体类,我们可以编写一个更通用的函数来运行多个回合,并区分训练模式和测试模式。
def train_agent(env, agent, episodes=500, max_steps=200, train=True, render_every=100):
"""
训练或测试一个智能体。
参数:
env: Gym环境。
agent: 智能体实例。
episodes: 要运行的回合数。
max_steps: 每个回合最大步数。
train: 是否为训练模式(调用learn方法)。
render_every: 每多少回合渲染一次(用于观察进度)。
返回:
episode_rewards: 每个回合的总奖励列表。
"""
episode_rewards = []
for episode in range(episodes):
obs, info = env.reset()
total_reward = 0
done = False
truncated = False
# 偶尔渲染一下看看效果
render = (render_every > 0 and episode % render_every == 0)
for step in range(max_steps):
if render:
# 注意:这里为了简化,每次创建新环境。实际可优化。
if step == 0:
print(f"\n=== 回合 {episode+1},渲染中... ===")
env.render()
action = agent.decide(obs)
next_obs, reward, done, truncated, info = env.step(action)
total_reward += reward
if train:
agent.learn(obs, action, reward, next_obs, done)
obs = next_obs
if done or truncated:
break
episode_rewards.append(total_reward)
# 每100回合打印一次平均奖励
if (episode + 1) % 100 == 0:
avg_reward = np.mean(episode_rewards[-100:])
print(f"回合 {episode+1}/{episodes},最近100回合平均奖励: {avg_reward:.2f}")
if render:
env.close() # 关闭最后的渲染窗口
return episode_rewards
# 使用示例:测试随机智能体在CartPole上的表现
print("=== 测试随机智能体 ===")
env = gym.make('CartPole-v1')
random_agent = RandomAgent(env)
rewards = train_agent(env, random_agent, episodes=200, max_steps=500, train=False, render_every=0)
print(f"随机智能体平均奖励: {np.mean(rewards):.2f} (+/- {np.std(rewards):.2f})")
print("\n=== 测试规则智能体 ===")
env = gym.make('CartPole-v1')
rule_agent = SimpleRuleBasedAgent(env)
rewards = train_agent(env, rule_agent, episodes=200, max_steps=500, train=False, render_every=50)
print(f"规则智能体平均奖励: {np.mean(rewards):.2f} (+/- {np.std(rewards):.2f})")
通过这样的封装,我们的代码结构清晰多了。BaseAgent定义了接口,RandomAgent和SimpleRuleBasedAgent是具体实现,train_agent函数负责运行实验。这个框架很容易扩展,接下来要实现一个真正的学习算法(比如Q-learning),只需要继承BaseAgent,在learn方法中实现更新逻辑即可。
6. 实战:用Q-learning算法解决CliffWalking问题
前面我们主要关注环境交互和简单策略。现在,让我们挑战一个需要真正“学习”才能解决的问题:悬崖寻路(CliffWalking-v0)。这是一个经典的网格世界问题,智能体需要从起点S走到终点G,但中间有一片悬崖(Cliff),掉下去会受到巨大惩罚并回到起点。这个环境非常适合演示表格型强化学习算法,比如Q-learning。
6.1 理解CliffWalking环境
首先,我们看看这个环境长什么样。
import gym
import numpy as np
env = gym.make('CliffWalking-v0')
print(f"观察空间: {env.observation_space}") # Discrete(48)
print(f"动作空间: {env.action_space}") # Discrete(4)
print(f"状态数量: {env.nS}, 动作数量: {env.nA}")
print(f"网格形状 (行, 列): {env.shape}")
# 可视化一下地图(文字版)
# 状态编号规则:从左到右,从上到下。0是起点(左下角),47是终点(右下角),36-45是悬崖。
def print_grid():
grid = np.arange(48).reshape(4, 12)
for row in range(4):
for col in range(12):
state = grid[row, col]
if state == 0:
print(" S ", end='') # 起点
elif state == 47:
print(" G ", end='') # 终点
elif 36 <= state <= 45:
print(" C ", end='') # 悬崖
else:
print(" . ", end='') # 安全路径
print() # 换行
print("\n网格世界地图 (S:起点, G:终点, C:悬崖):")
print_grid()
输出会显示一个4x12的网格。智能体从左下角(S)出发,要避开中间的悬崖(C),到达右下角(G)。每走一步奖励-1,掉下悬崖奖励-100并回到起点。动作有4个:0=上,1=右,2=下,3=左。目标是找到一条最安全的路径,最大化累计奖励(即最小化步数,避免掉崖)。
6.2 实现Q-learning智能体
Q-learning是一种无模型(model-free)的强化学习算法,它通过不断更新一个Q表格(Q-table)来学习每个状态-动作对的价值。Q(s, a)代表在状态s下采取动作a所能获得的长期期望回报。
class QLearningAgent(BaseAgent):
"""使用Q-learning算法的智能体。"""
def __init__(self, env, learning_rate=0.1, discount_factor=0.95, exploration_rate=0.1):
"""
参数:
env: Gym环境。
learning_rate (alpha): 学习率,控制新信息覆盖旧信息的程度。
discount_factor (gamma): 折扣因子,衡量未来奖励的重要性。
exploration_rate (epsilon): 探索率,随机探索的概率。
"""
super().__init__(env)
self.lr = learning_rate
self.gamma = discount_factor
self.epsilon = exploration_rate
# 初始化Q表格:状态数 x 动作数,初始值可以设为0或一个小随机数
self.n_states = env.observation_space.n
self.n_actions = env.action_space.n
self.q_table = np.zeros((self.n_states, self.n_actions))
print(f"Q表格初始化: {self.n_states} 状态 x {self.n_actions} 动作")
def decide(self, observation):
# epsilon-贪婪策略:以epsilon概率探索,否则利用
if np.random.random() < self.epsilon:
# 探索:随机选择一个动作
return self.env.action_space.sample()
else:
# 利用:选择当前状态下Q值最大的动作
state = observation # 在离散环境中,观测就是状态编号
return int(np.argmax(self.q_table[state]))
def learn(self, observation, action, reward, next_observation, done):
# Q-learning更新公式: Q(s,a) <- Q(s,a) + alpha * [r + gamma * max_a' Q(s',a') - Q(s,a)]
state = observation
next_state = next_observation
current_q = self.q_table[state, action]
if done:
# 如果回合结束,没有下一个状态的最大Q值
target = reward
else:
# 下一个状态的最大Q值
max_next_q = np.max(self.q_table[next_state])
target = reward + self.gamma * max_next_q
# 更新Q值
self.q_table[state, action] += self.lr * (target - current_q)
def get_policy(self):
"""从Q表格中提取确定性策略(每个状态选择Q值最大的动作)。"""
policy = np.argmax(self.q_table, axis=1)
return policy
6.3 训练并评估Q-learning智能体
现在,让我们训练这个智能体,看看它能否学会避开悬崖,找到最优路径。
def run_qlearning_experiment(episodes=1000, max_steps=100):
env = gym.make('CliffWalking-v0')
agent = QLearningAgent(env, learning_rate=0.1, discount_factor=0.95, exploration_rate=0.1)
print("开始训练Q-learning智能体...")
rewards = train_agent(env, agent, episodes=episodes, max_steps=max_steps, train=True, render_every=0)
# 绘制学习曲线
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 5))
plt.plot(rewards, alpha=0.6, label='每回合奖励')
# 计算移动平均,使曲线更平滑
window_size = 50
moving_avg = np.convolve(rewards, np.ones(window_size)/window_size, mode='valid')
plt.plot(range(window_size-1, len(rewards)), moving_avg, 'r-', linewidth=2, label=f'{window_size}回合移动平均')
plt.xlabel('回合数')
plt.ylabel('回合总奖励')
plt.title('CliffWalking - Q-learning 学习曲线')
plt.legend()
plt.grid(True)
plt.show()
# 测试训练后的策略
print("\n=== 测试训练后的策略 ===")
test_rewards = []
for i in range(100):
obs, _ = env.reset()
total_reward = 0
done = False
while not done:
action = agent.decide(obs)
obs, reward, done, _, _ = env.step(action)
total_reward += reward
test_rewards.append(total_reward)
print(f"测试集平均奖励: {np.mean(test_rewards):.2f} (最优为 -13)")
print(f"测试集标准差: {np.std(test_rewards):.2f}")
# 查看学到的策略
policy = agent.get_policy()
print("\n学到的策略(前12个状态,对应地图第一行):")
print(policy[:12])
print("动作映射: 0=上, 1=右, 2=下, 3=左")
# 可视化策略路径
print("\n策略路径可视化 (S:起点, G:终点, C:悬崖, 箭头:动作):")
action_symbols = ['↑', '→', '↓', '←']
grid = np.arange(48).reshape(4, 12)
for row in range(4):
for col in range(12):
state = grid[row, col]
if state == 0:
print(" S ", end='')
elif state == 47:
print(" G ", end='')
elif 36 <= state <= 45:
print(" C ", end='')
else:
print(f" {action_symbols[policy[state]]} ", end='')
print()
env.close()
return agent
# 运行实验
agent = run_qlearning_experiment(episodes=2000)
运行这段代码,你会看到智能体从最初的随机乱走(奖励接近-100,经常掉崖),逐渐学习到一条沿着顶部边缘行走的安全路径,最终平均奖励接近最优值-13(即用13步从起点到终点,无掉崖)。学习曲线会显示奖励随着训练回合增加而上升的过程。最后打印的策略路径中,你会看到智能体学会了从起点一直向右走,在边缘处向下的最优策略。
6.4 调整超参数与算法分析
Q-learning的性能很大程度上依赖于超参数的选择。你可以尝试调整以下参数,观察效果:
- 学习率 (learning_rate): 太高可能导致震荡,太低则学习缓慢。通常从0.1开始尝试。
- 折扣因子 (discount_factor): 接近1表示更重视未来奖励。对于CliffWalking这种有明确终止状态的问题,0.9-0.99比较合适。
- 探索率 (exploration_rate): 平衡探索(尝试新动作)与利用(使用已知最好动作)。可以尝试随时间衰减(如
epsilon = max(0.01, 0.1 * (0.995**episode))),让智能体早期多探索,后期多利用。
Q-learning的核心思想是时间差分(Temporal Difference, TD)学习。它不需要环境的模型(即不知道状态转移概率),通过实际交互来更新价值估计。更新公式 Q(s,a) = Q(s,a) + α [r + γ max_a' Q(s',a') - Q(s,a)] 的本质是:将当前估计值 Q(s,a) 向“目标值” r + γ max_a' Q(s',a') 调整一小步(由α控制)。这个目标值结合了即时奖励r和下一状态的最大估计价值,是一种自举(bootstrapping)方法。
在实际使用中,你可能会发现Q-learning在CliffWalking上学习速度很快,但在更复杂的问题(如Atari游戏)上,由于状态空间巨大,表格法(Q-table)不再适用,这就需要使用深度Q网络(DQN)等结合神经网络的方法。但理解Q-learning是掌握所有这些高级算法的基础。
7. 总结与进阶方向
跟着上面的步骤走下来,你应该已经成功搭建了Gym环境,运行了随机策略和规则策略,甚至实现并训练了一个Q-learning智能体来解决CliffWalking问题。这已经是一个相当扎实的起点了。回顾一下我们的旅程:从安装Gym、理解环境接口,到与经典控制环境交互,再到构建智能体类并实现一个真正的学习算法。每一步我都提供了可运行的代码和实际测试结果,这些都是我实践中验证过的。
我刚开始学的时候,在MountainCar上卡了很久,因为那个简单的“总是向右”的策略永远无法成功。直到我理解了它需要“摆动积累动量”,才意识到强化学习智能体必须通过试错去发现这种反直觉的策略。而CliffWalking问题则完美展示了探索与利用的权衡:如果智能体太贪心(只利用已知的安全路径),可能永远发现不了更优的顶部路径;如果探索太多,又会频繁掉下悬崖受到惩罚。
如果你想继续深入,这里有几个明确的进阶方向:
- 尝试更多经典环境:比如
Acrobot-v1(钟摆上摆)、Pendulum-v1(连续动作控制),它们比CartPole更具挑战性。 - 实现更高级的算法:在Q-learning的基础上,可以尝试SARSA、Expected SARSA等TD学习算法,或者挑战Deep Q-Network (DQN) 来解决像素输入的Atari游戏。
- 调参实战:系统地研究学习率、折扣因子、探索率对Q-learning在CliffWalking上性能的影响,并尝试设计探索率衰减策略。
- 转向连续动作空间:CartPole的动作是离散的(左/右),但很多实际问题(如机器人控制)的动作是连续的。可以尝试
Pendulum-v1环境,并学习DDPG、PPO等适用于连续动作空间的算法。 - 使用包装器(Wrappers):Gym的Wrapper功能可以方便地修改环境,比如帧堆叠、奖励裁剪、观测归一化等,这是处理复杂环境的必备技能。
强化学习最迷人的地方在于,你写的代码真的能让智能体从零开始,通过与环境的交互学会一项技能。这种“养成”的成就感是其他编程领域难以比拟的。我建议你亲手敲一遍代码,调整参数观察变化,甚至尝试改进我上面写的简单规则策略。遇到问题多查查Gym官方文档和源码,里面藏着很多有用的细节。记住,在强化学习里,实践和试错本身就是最好的老师。

3162

被折叠的 条评论
为什么被折叠?



