用Python和NumPy手把手教你实现10臂老虎机(附完整代码与可视化分析)
在强化学习的入门阶段,很多学习者都会被各种数学公式和抽象概念所困扰。今天,我们将通过一个经典的10臂老虎机问题,用Python代码带你直观理解强化学习的核心机制。不同于传统的理论讲解,我们将从零开始编写每一行代码,并通过实时可视化观察学习过程。
1. 理解多臂老虎机问题
多臂老虎机(Multi-armed Bandit)是强化学习中最基础的决策问题模型。想象你站在一个赌场里,面前有10台老虎机(slot machine),每台机器的中奖概率各不相同。你的目标是通过有限的尝试次数,找到最优的老虎机并获得最大累积奖励。
这个问题的核心在于探索-利用困境(Exploration-Exploitation Dilemma):
- 探索:尝试不同的老虎机以收集信息
- 利用:根据已有知识选择当前表现最好的老虎机
import numpy as np import matplotlib.pyplot as plt # 设置随机种子确保结果可复现 np.random.seed(42)2. 构建老虎机环境
首先我们需要创建一个老虎机环境类。每个老虎机都有一个固定的中奖概率,但每次拉动臂杆的结果仍然是随机的。
class Bandit: def __init__(self, arms=10): # 随机生成10个老虎机的中奖概率 self.true_rates = np.random.rand(arms) def play(self, arm): # 模拟拉动指定老虎机臂杆的结果 rate = self.true_rates[arm] return 1 if rate > np.random.rand() else 0提示:这里我们使用0和1作为奖励信号,1表示中奖,0表示未中奖。在实际应用中,奖励可以是任意数值。
3. 实现智能体决策逻辑
智能体需要维护两个关键信息:
- 每个动作的价值估计(Q值)
- 每个动作的尝试次数
class Agent: def __init__(self, epsilon, action_size=10): self.epsilon = epsilon # 探索率 self.Q = np.zeros(action_size) # 动作价值估计 self.counts = np.zeros(action_size) # 动作尝试次数 def update(self, action, reward): """根据新获得的奖励更新价值估计""" self.counts[action] += 1 # 增量式更新Q值 self.Q[action] += (reward - self.Q[action]) / self.counts[action] def get_action(self): """基于ε-greedy策略选择动作""" if np.random.rand() < self.epsilon: # 探索:随机选择动作 return np.random.randint(len(self.Q)) # 利用:选择当前估计价值最高的动作 return np.argmax(self.Q)4. 完整训练流程与可视化
现在我们将环境、智能体和训练循环组合起来,并实时可视化学习过程。
def run_experiment(epsilon, steps=1000): bandit = Bandit() agent = Agent(epsilon) rewards = [] optimal_rates = [] optimal = np.argmax(bandit.true_rates) # 真实最优老虎机 for step in range(steps): action = agent.get_action() reward = bandit.play(action) agent.update(action, reward) # 记录数据用于可视化 rewards.append(reward) optimal_rates.append(action == optimal) return rewards, optimal_rates # 运行不同探索率的实验 epsilons = [0, 0.01, 0.1] results = {eps: run_experiment(eps) for eps in epsilons} # 可视化结果 plt.figure(figsize=(12, 8)) # 绘制累积奖励曲线 plt.subplot(2, 1, 1) for eps, (rewards, _) in results.items(): plt.plot(np.cumsum(rewards), label=f'ε={eps}') plt.ylabel('累积奖励') plt.xlabel('步数') plt.legend() # 绘制最优动作选择率 plt.subplot(2, 1, 2) for eps, (_, optimal_rates) in results.items(): plt.plot(np.cumsum(optimal_rates) / (np.arange(len(optimal_rates)) + 1), label=f'ε={eps}') plt.ylabel('最优动作选择率') plt.xlabel('步数') plt.legend() plt.tight_layout() plt.show()5. 关键参数分析与调优
从实验结果可以看出,探索率ε对学习效果有显著影响:
| ε值 | 累积奖励 | 收敛速度 | 最优动作发现能力 |
|---|---|---|---|
| 0 | 最低 | 最快 | 差 |
| 0.01 | 中等 | 慢 | 中等 |
| 0.1 | 最高 | 中等 | 优 |
实际应用建议:
- 初期可以设置较高的ε值(如0.1-0.3)以充分探索
- 随着尝试次数增加,可以逐渐降低ε值(退火策略)
- 对于确定性环境,最终可以将ε降至0
# 退火ε-greedy策略实现 class AnnealingAgent(Agent): def __init__(self, action_size=10): super().__init__(1.0, action_size) # 初始ε=1.0 self.steps = 0 def get_action(self): self.epsilon = 1.0 / (self.steps + 1) self.steps += 1 return super().get_action()6. 高级改进与扩展思路
基础版本实现后,我们可以考虑以下改进方向:
- 置信区间上界(UCB)算法:
- 不仅考虑Q值,还考虑动作的不确定性
- 平衡探索和利用的更优方式
class UCBAgent: def __init__(self, c=2, action_size=10): self.Q = np.zeros(action_size) self.counts = np.zeros(action_size) self.c = c # 探索系数 self.total_counts = 0 def get_action(self): if self.total_counts == 0: return np.random.randint(len(self.Q)) # UCB计算公式 ucb_values = self.Q + self.c * np.sqrt(np.log(self.total_counts) / (self.counts + 1e-5)) return np.argmax(ucb_values) def update(self, action, reward): self.counts[action] += 1 self.total_counts += 1 self.Q[action] += (reward - self.Q[action]) / self.counts[action]- 非平稳环境处理:
- 真实场景中老虎机的中奖概率可能随时间变化
- 可以使用加权平均替代算术平均
class NonStationaryAgent(Agent): def __init__(self, epsilon=0.1, alpha=0.1, action_size=10): super().__init__(epsilon, action_size) self.alpha = alpha # 固定学习率 def update(self, action, reward): self.Q[action] += self.alpha * (reward - self.Q[action])7. 实战技巧与常见问题
在实现过程中,可能会遇到以下典型问题及解决方案:
- 初始Q值设置:
- 全零初始化可能导致智能体过于保守
- 解决方案:使用乐观初始值(Optimistic Initial Values)
class OptimisticAgent(Agent): def __init__(self, epsilon=0, initial_value=5, action_size=10): super().__init__(epsilon, action_size) self.Q[:] = initial_value # 设置乐观初始值- 随机性处理:
- 确保实验可重复性
- 解决方案:固定随机种子
np.random.seed(42) # 在实验开始前设置- 性能优化:
- 对于大规模实验,使用向量化操作
- 避免Python循环,尽量使用NumPy
# 向量化实现示例 def vectorized_update(Q, counts, actions, rewards): counts[actions] += 1 Q[actions] += (rewards - Q[actions]) / counts[actions]在完成基础实现后,我通常会先测试极端参数情况(如ε=0和ε=1)来验证代码逻辑是否正确。一个常见错误是忘记在ε-greedy策略中处理探索和利用的边界条件,这会导致智能体无法学习到最优策略。