用Python实战MDP:从零实现一个强化学习智能体(附完整代码)
用Python实战MDP:从零实现一个强化学习智能体(附完整代码)
如果你对强化学习(Reinforcement Learning)的理论概念感到抽象,觉得那些关于状态、动作、奖励的公式离实际代码很远,那么这篇文章正是为你准备的。我们绕开冗长的纯理论推导,直接进入实战环节。想象一下,你正在构建一个能玩简单游戏的AI,或者一个能自动优化某个流程的智能体。它的核心大脑,就是一个对马尔科夫决策过程(Markov Decision Process, MDP)有深刻理解的算法。今天,我们就用Python,从最基础的MDP组件开始,亲手搭建这个“大脑”,并看着它在模拟环境中学习成长。这篇文章面向的是已经掌握Python基础,渴望通过动手编码来理解强化学习内核的开发者。我们将构建一个完整的、可运行的智能体,过程中你会遇到并解决那些教科书上不会讲的报错和调试难题。
1. 环境搭建与MDP核心组件手写
在开始训练智能体之前,我们必须先定义它所在的世界。一个MDP世界由五个核心要素构成:状态集(S)、动作集(A)、状态转移概率(P)、奖励函数(R)和折扣因子(γ)。我们将用纯粹的Python代码来定义它们,而不是直接调用某个库的现成接口。这样做的目的是让你透彻理解每一个矩阵和函数背后的意义。
首先,我们创建一个简单的网格世界(Grid World)环境。假设有一个4x4的网格,智能体从左上角(0,0)出发,目标是到达右下角(3,3)。碰到网格边界则保持原地不动。这是一个经典的 episodic MDP(分幕式MDP)。
import numpy as np
from typing import Tuple, Dict, List
class GridWorldMDP:
"""
一个简单的4x4网格世界MDP实现。
"""
def __init__(self, size=4, gamma=0.99):
self.size = size
self.n_states = size * size # 状态总数
self.n_actions = 4 # 动作:0:上, 1:右, 2:下, 3:左
self.gamma = gamma # 折扣因子
# 定义状态空间和动作空间
self.states = [i for i in range(self.n_states)]
self.actions = [i for i in range(self.n_actions)]
# 构建状态转移矩阵 P(s' | s, a) 和奖励函数 R(s, a, s')
# 我们将用字典来存储,键为(s, a),值为列表,列表元素为(概率, 下一状态, 奖励, 是否终止)
self.P = {}
self._build_dynamics()
def _state_to_coord(self, s: int) -> Tuple[int, int]:
"""将状态编号转换为二维坐标。"""
return (s // self.size, s % self.size)
def _coord_to_state(self, row: int, col: int) -> int:
"""将二维坐标转换为状态编号。"""
return row * self.size + col
def _is_terminal(self, s: int) -> bool:
"""判断状态是否为终止状态(目标或陷阱)。"""
# 在我们的简单示例中,只有右下角(3,3)是终止状态
return s == self.n_states - 1
def _build_dynamics(self):
"""手动构建MDP的动态特性(转移概率和奖励)。"""
for s in range(self.n_states):
if self._is_terminal(s):
# 终止状态,任何动作都转移到自身,奖励为0
for a in self.actions:
self.P[(s, a)] = [(1.0, s, 0.0, True)]
continue
row, col = self._state_to_coord(s)
for a in self.actions:
next_row, next_col = row, col
if a == 0: # 上
next_row = max(row - 1, 0)
elif a == 1: # 右
next_col = min(col + 1, self.size - 1)
elif a == 2: # 下
next_row = min(row + 1, self.size - 1)
elif a == 3: # 左
next_col = max(col - 1, 0)
next_s = self._coord_to_state(next_row, next_col)
# 简单奖励设定:到达目标状态奖励+1,其他转移奖励为-0.01(鼓励尽快到达目标)
reward = 1.0 if self._is_terminal(next_s) else -0.01
done = self._is_terminal(next_s)
# 确定性环境:概率为1.0
self.P[(s, a)] = [(1.0, next_s, reward, done)]
def step(self, s: int, a: int) -> Tuple[int, float, bool]:
"""
执行一步动作。
返回: (下一状态, 奖励, 是否终止)
"""
transitions = self.P.get((s, a), [])
# 在实际随机环境中,这里需要根据概率采样。我们这里是确定性的。
prob, next_s, reward, done = transitions[0]
return next_s, reward, done
def reset(self) -> int:
"""重置环境到起始状态。"""
return 0 # 状态0,即(0,0)
注意:上面的
_build_dynamics函数是理解MDP“动力学”的关键。它明确定义了环境对智能体动作的响应规则。在更复杂的随机环境中,self.P[(s, a)]列表里会包含多个(概率, 下一状态, 奖励)元组。
现在,我们已经有了一个完全由代码定义的MDP环境。接下来,我们需要一个能在这个环境中做决策的智能体。智能体的核心是策略(Policy) 和价值函数(Value Function)。我们先来实现一个最简单的智能体:它有一个策略(可能是随机的),并能评估在当前策略下每个状态的价值。
2. 策略评估与价值迭代算法实现
智能体要学习,首先得知道自己在当前策略下表现得“好不好”。这就是策略评估(Policy Evaluation)要解决的问题:计算给定策略π下的状态价值函数 Vπ(s)。我们采用经典的动态规划方法——迭代策略评估。
class MDPAgent:
def __init__(self, mdp: GridWorldMDP):
self.mdp = mdp
self.V = np.zeros(mdp.n_states) # 状态价值函数
self.Q = np.zeros((mdp.n_states, mdp.n_actions)) # 动作价值函数
self.policy = np.ones((mdp.n_states, mdp.n_actions)) / mdp.n_actions # 初始随机策略
def policy_evaluation(self, theta=1e-6, max_iter=1000):
"""
迭代策略评估。
theta: 价值函数收敛阈值
max_iter: 最大迭代次数
"""
for i in range(max_iter):
delta = 0.0
V_new = self.V.copy()
for s in range(self.mdp.n_states):
if self.mdp._is_terminal(s):
continue
v = 0
# 对当前策略下每个可能的动作进行求和
for a in range(self.mdp.n_actions):
prob_a = self.policy[s, a] # 策略π(a|s)
# 对环境的可能转移进行求和 (贝尔曼期望方程)
for (trans_prob, next_s, reward, _) in self.mdp.P[(s, a)]:
v += prob_a * trans_prob * (reward + self.mdp.gamma * self.V[next_s])
V_new[s] = v
delta = max(delta, abs(v - self.V[s]))
self.V = V_new
if delta < theta:
print(f"策略评估在 {i+1} 次迭代后收敛。")
break
return self.V
仅仅评估策略还不够,我们的目标是找到最优策略。价值迭代(Value Iteration)算法将策略改进和策略评估结合,直接寻找最优价值函数 V*,进而导出最优策略。
def value_iteration(self, theta=1e-6, max_iter=1000):
"""
价值迭代算法,直接求解最优价值函数V*和最优策略π*。
"""
for i in range(max_iter):
delta = 0.0
V_new = self.V.copy()
for s in range(self.mdp.n_states):
if self.mdp._is_terminal(s):
continue
# 贝尔曼最优方程:V*(s) = max_a Σ P(s'|s,a)[R + γV*(s')]
action_values = []
for a in range(self.mdp.n_actions):
q = 0
for (trans_prob, next_s, reward, _) in self.mdp.P[(s, a)]:
q += trans_prob * (reward + self.mdp.gamma * self.V[next_s])
action_values.append(q)
# 取能获得最大价值的行为对应的价值
V_new[s] = max(action_values)
delta = max(delta, abs(V_new[s] - self.V[s]))
self.V = V_new
if delta < theta:
print(f"价值迭代在 {i+1} 次迭代后收敛。")
break
# 根据最优价值函数提取确定性最优策略
self._extract_optimal_policy()
return self.V
def _extract_optimal_policy(self):
"""从最优价值函数V*中提取确定性最优策略π*。"""
for s in range(self.mdp.n_states):
if self.mdp._is_terminal(s):
continue
action_values = []
for a in range(self.mdp.n_actions):
q = 0
for (trans_prob, next_s, reward, _) in self.mdp.P[(s, a)]:
q += trans_prob * (reward + self.mdp.gamma * self.V[next_s])
action_values.append(q)
# 找到价值最大的动作
best_a = np.argmax(action_values)
# 将策略设置为确定性策略
self.policy[s, :] = 0.0
self.policy[s, best_a] = 1.0
运行上述代码,你将看到智能体通过计算,自己“规划”出了一条从起点到终点的最优路径。价值迭代的本质是离线规划,它要求智能体完全知晓环境的动态模型(即self.P)。这在很多实际问题中是一个强假设。
3. 与OpenAI Gym集成与智能体训练
为了让我们手写的智能体在一个更标准、更丰富的环境中进行测试和学习,我们将其与OpenAI Gym集成。Gym提供了大量标准化的环境,但我们这里选择将其“包装”一下,以适配我们自定义的MDP逻辑,同时展示如何与Gym API交互。
首先,确保你已安装gym库:pip install gym。我们将创建一个Gym风格的包装器。
import gym
from gym import spaces
class GridWorldGymEnv(gym.Env):
"""将我们自定义的GridWorldMDP包装成Gym环境。"""
metadata = {'render.modes': ['human']}
def __init__(self, size=4):
super(GridWorldGymEnv, self).__init__()
self.mdp = GridWorldMDP(size=size)
# 定义Gym规范的动作和观察空间
self.action_space = spaces.Discrete(self.mdp.n_actions)
self.observation_space = spaces.Discrete(self.mdp.n_states)
self.state = None
self.size = size
def reset(self):
self.state = self.mdp.reset()
return self.state
def step(self, action):
next_state, reward, done = self.mdp.step(self.state, action)
self.state = next_state
# Gym的step返回: observation, reward, done, info
return next_state, reward, done, {}
def render(self, mode='human'):
grid = np.arange(self.mdp.n_states).reshape(self.size, self.size)
for i in range(self.size):
row_str = ''
for j in range(self.size):
s = self._coord_to_state(i, j)
if s == self.state:
row_str += ' A ' # 智能体位置
elif self.mdp._is_terminal(s):
row_str += ' G ' # 目标位置
else:
row_str += ' . '
print(row_str)
print('---')
def _coord_to_state(self, row, col):
return row * self.size + col
现在,我们可以用这个环境来训练一个不需要环境模型的智能体,例如Q-learning算法。这是一种经典的时序差分(Temporal Difference, TD)学习算法,属于模型无关的强化学习方法。
class QLearningAgent:
def __init__(self, env, alpha=0.1, gamma=0.99, epsilon=0.1):
self.env = env
self.alpha = alpha # 学习率
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # 探索率
self.n_actions = env.action_space.n
self.n_states = env.observation_space.n
self.Q = np.zeros((self.n_states, self.n_actions))
def choose_action(self, state):
"""ε-greedy策略选择动作。"""
if np.random.random() < self.epsilon:
return self.env.action_space.sample() # 探索
else:
return np.argmax(self.Q[state]) # 利用
def learn(self, state, action, reward, next_state, done):
"""Q-learning更新规则。"""
best_next_action = np.argmax(self.Q[next_state])
td_target = reward + self.gamma * self.Q[next_state, best_next_action] * (not done)
td_error = td_target - self.Q[state, action]
self.Q[state, action] += self.alpha * td_error
def train(self, episodes=1000):
"""训练智能体。"""
rewards_per_episode = []
for episode in range(episodes):
state = self.env.reset()
total_reward = 0
done = False
while not done:
action = self.choose_action(state)
next_state, reward, done, _ = self.env.step(action)
self.learn(state, action, reward, next_state, done)
state = next_state
total_reward += reward
rewards_per_episode.append(total_reward)
if (episode + 1) % 100 == 0:
avg_reward = np.mean(rewards_per_episode[-100:])
print(f"Episode {episode+1}, 最近100轮平均奖励: {avg_reward:.2f}")
return rewards_per_episode
运行训练过程,你会看到智能体从随机探索开始,平均奖励逐渐提高,最终学会快速找到目标。这个过程完美诠释了强化学习的核心:通过试错和与环境的交互来学习最优行为策略。
4. 实战调试与性能优化技巧
在亲手实现上述代码的过程中,你几乎一定会遇到各种问题。下面是一些常见的坑和解决方案,这些是纯理论教程里很少涉及的实战经验。
常见报错1:维度不匹配或索引越界
- 场景:在计算
self.Q[state, action]时,state可能是一个由(row, col)组成的元组,而不是一个整数索引。 - 解决方案:确保你的状态表示是离散且可哈希的。如果使用原始坐标,最好像我们之前做的那样,将其线性映射为一个整数。在Gym的
Discrete观察空间中,状态已经是整数。
常见报错2:算法不收敛,奖励曲线震荡或没有提升
- 可能原因与调试步骤:
- 学习率(α)过高或过低:学习率太大可能导致Q值震荡,太小则学习缓慢。可以尝试设置一个衰减的学习率,例如
alpha = 0.1 / (1 + episode * 0.01)。 - 探索率(ε)设置不当:始终高探索率(如ε=0.5)会导致智能体无法充分利用学到的知识;始终低探索率(如ε=0.01)则可能陷入局部最优。实现ε衰减策略是常用技巧。
self.epsilon = max(0.01, self.epsilon * 0.995) # 指数衰减 - 奖励函数设计不合理:这是最核心也最容易出错的地方。如果每一步的奖励都是-1,而目标奖励是0,智能体可能会发现“原地不动”和“走向目标”的长期收益没有区别。我们的设计(到达目标+1,其他步-0.01)给予了明确的优化方向。
- 折扣因子(γ)接近1:在分幕式任务中,γ可以设为1。但在持续任务或非常长的分幕任务中,γ需要小于1(如0.99)以确保回报是有限的。
- 学习率(α)过高或过低:学习率太大可能导致Q值震荡,太小则学习缓慢。可以尝试设置一个衰减的学习率,例如
性能优化与可视化 为了更直观地理解智能体的学习过程,我们可以将策略和价值函数可视化。
import matplotlib.pyplot as plt
def plot_learning_curve(rewards_history, window=50):
"""绘制平滑后的奖励曲线。"""
smoothed_rewards = np.convolve(rewards_history, np.ones(window)/window, mode='valid')
plt.figure(figsize=(10, 6))
plt.plot(rewards_history, alpha=0.3, label='原始奖励')
plt.plot(range(window-1, len(rewards_history)), smoothed_rewards, label=f'{window}轮移动平均', linewidth=2)
plt.xlabel('训练轮次 (Episode)')
plt.ylabel('每轮总奖励')
plt.title('智能体训练学习曲线')
plt.legend()
plt.grid(True)
plt.show()
def visualize_policy(agent, env):
"""在网格中可视化最终学到的策略。"""
policy_arrows = ['↑', '→', '↓', '←']
grid_policy = []
for s in range(env.mdp.n_states):
if env.mdp._is_terminal(s):
grid_policy.append('G')
else:
# 对于Q-learning,策略是贪婪策略
best_action = np.argmax(agent.Q[s])
grid_policy.append(policy_arrows[best_action])
grid_policy = np.array(grid_policy).reshape(env.size, env.size)
print("学到的策略(箭头表示最优动作方向):")
for row in grid_policy:
print(' '.join(row))
将训练后的智能体进行可视化,你会看到一个清晰的箭头网格,指向从每个状态出发的最佳方向。这比任何数字表格都更能证明你的智能体真正“学会”了。
从玩具环境到更复杂挑战 当你成功运行了4x4网格世界后,可以尝试以下进阶挑战,这些都是强化学习从业者的日常:
- 增加随机性:修改
GridWorldMDP.step函数,让动作有10%的概率执行失败(随机方向)。观察Q-learning算法能否处理这种随机动态。 - 引入“陷阱”:在网格中设置几个状态,进入后获得大额负奖励(如-1)。这模拟了现实任务中的风险。
- 更换环境:尝试用Gym中的
FrozenLake-v1(自带随机冰面)或CliffWalking-v0环境替换我们的自定义环境。这些环境有更复杂的动态,测试你的智能体代码的通用性。 - 实现更高级算法:如Double DQN、Policy Gradient等。这需要你将状态从离散扩展到连续(或高维离散),并引入神经网络作为函数近似器。
手写代码实现MDP和基础RL算法的过程,就像亲手搭建了一台机器的内部齿轮。你不再只是调用agent.learn()的黑盒,而是清楚地知道每一次价值更新、每一次策略改进背后的数学原理和代码逻辑。当你的智能体在屏幕上第一次成功地、稳定地找到目标时,那种对agent-environment interface和形式化框架的理解,会比阅读十篇理论文章更加深刻。
更多推荐
所有评论(0)