Sutton强化学习第二章:手把手教你用Python复现多臂赌博机实验(附完整代码)
从零实现多臂赌博机Python代码详解与算法对比实验在强化学习的入门阶段多臂赌博机问题就像Hello World一样经典。这个看似简单的模型却包含了探索与利用(exploration-exploitation)这一核心矛盾。本文将带你用Python从零实现Sutton教材第二章的所有关键算法并通过可视化对比不同策略的表现差异。1. 环境搭建与基础实现首先我们需要建立一个模拟的多臂赌博机环境。每个臂可以看作是一个独立的动作其奖励服从特定的概率分布import numpy as np import matplotlib.pyplot as plt class BanditEnv: def __init__(self, k10): self.k k # 老虎机臂数 self.q_star np.random.randn(k) # 每个臂的真实奖励均值 self.best_action np.argmax(self.q_star) def step(self, action): # 返回奖励奖励真实均值随机噪声 return self.q_star[action] np.random.randn()关键参数说明k: 老虎机的臂数默认为10q_star: 每个臂的真实奖励均值初始化为标准正态分布best_action: 当前最优动作的索引注意在实际问题中我们不知道q_star的真实值这正是需要通过不断尝试来估计的。2. 核心算法实现2.1 ε-贪心算法ε-贪心是最基础的策略以1-ε的概率选择当前认为最优的动作以ε的概率随机探索class EpsilonGreedyAgent: def __init__(self, k, epsilon0.1): self.k k self.epsilon epsilon self.Q np.zeros(k) # 动作价值估计 self.N np.zeros(k) # 动作选择次数 def act(self): if np.random.rand() self.epsilon: return np.random.randint(self.k) # 探索 return np.argmax(self.Q) # 利用 def update(self, action, reward): self.N[action] 1 # 增量式更新动作价值估计 self.Q[action] (reward - self.Q[action]) / self.N[action]参数调优建议ε0.1 通常是一个不错的起点对于静态问题可以随时间衰减ε值高ε值有利于探索但会降低最终表现2.2 UCB算法置信区间上界(UCB)算法通过量化不确定性来平衡探索与利用class UCBAgent: def __init__(self, k, c2): self.k k self.c c self.Q np.zeros(k) self.N np.zeros(k) self.t 0 def act(self): self.t 1 if (self.N 0).any(): return np.where(self.N 0)[0][0] ucb self.Q self.c * np.sqrt(np.log(self.t) / (self.N 1e-5)) return np.argmax(ucb) def update(self, action, reward): self.N[action] 1 self.Q[action] (reward - self.Q[action]) / self.N[action]UCB公式解析第一项Q(a)代表当前价值估计利用第二项衡量不确定性探索c参数控制探索强度2.3 梯度赌博机算法基于softmax的策略梯度方法直接学习动作偏好class GradientBanditAgent: def __init__(self, k, alpha0.1, baselineTrue): self.k k self.alpha alpha self.H np.zeros(k) # 动作偏好 self.baseline baseline self.avg_reward 0 self.t 0 def act(self): exp_H np.exp(self.H - np.max(self.H)) # 数值稳定性处理 pi exp_H / np.sum(exp_H) return np.random.choice(self.k, ppi) def update(self, action, reward): self.t 1 if self.baseline: self.avg_reward (reward - self.avg_reward) / self.t exp_H np.exp(self.H - np.max(self.H)) pi exp_H / np.sum(exp_H) one_hot np.zeros(self.k) one_hot[action] 1 baseline self.avg_reward if self.baseline else 0 self.H self.alpha * (reward - baseline) * (one_hot - pi)算法特点直接学习动作偏好而非价值估计使用baseline可以减少方差加速收敛α是学习率影响更新步长3. 实验设计与结果分析3.1 不同ε值的对比实验我们固定2000次独立运行每次1000步比较ε0, 0.01, 0.1的表现def compare_epsilons(): k 10 runs 2000 steps 1000 epsilons [0, 0.01, 0.1] rewards np.zeros((len(epsilons), runs, steps)) optimal_actions np.zeros_like(rewards) for i, eps in enumerate(epsilons): for run in range(runs): env BanditEnv(k) agent EpsilonGreedyAgent(k, epsiloneps) for step in range(steps): action agent.act() reward env.step(action) agent.update(action, reward) rewards[i, run, step] reward optimal_actions[i, run, step] action env.best_action # 计算平均奖励和最优动作比例 avg_rewards rewards.mean(axis1) opt_actions optimal_actions.mean(axis1) # 绘制结果 plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) for eps, rewards in zip(epsilons, avg_rewards): plt.plot(rewards, labelfε{eps}) plt.xlabel(Steps) plt.ylabel(Average Reward) plt.legend() plt.subplot(1, 2, 2) for eps, opts in zip(epsilons, opt_actions): plt.plot(opts, labelfε{eps}) plt.xlabel(Steps) plt.ylabel(% Optimal Action) plt.legend() plt.tight_layout() plt.show()典型实验结果分析ε值初期表现长期表现收敛速度0差差快0.01中等优慢0.1优中等快3.2 算法综合对比将ε-贪心、UCB、梯度算法放在一起比较def compare_algorithms(): k 10 runs 2000 steps 1000 agents [ EpsilonGreedyAgent(k, epsilon0.1), UCBAgent(k, c2), GradientBanditAgent(k, alpha0.1) ] rewards np.zeros((len(agents), runs, steps)) for i, agent in enumerate(agents): for run in range(runs): env BanditEnv(k) agent.__init__(k) # 重置agent for step in range(steps): action agent.act() reward env.step(action) agent.update(action, reward) rewards[i, run, step] reward avg_rewards rewards.mean(axis1) plt.figure(figsize(8, 5)) labels [ε-greedy (ε0.1), UCB (c2), Gradient (α0.1)] for i in range(len(agents)): plt.plot(avg_rewards[i], labellabels[i]) plt.xlabel(Steps) plt.ylabel(Average Reward) plt.legend() plt.show()算法选择指南对于简单问题ε-贪心通常足够需要系统探索时UCB表现更好非平稳环境下梯度方法更鲁棒4. 高级话题与实用技巧4.1 非平稳问题处理真实环境中的奖励分布往往会随时间变化。我们可以使用指数加权移动平均来适应变化class NonStationaryAgent(EpsilonGreedyAgent): def __init__(self, k, epsilon0.1, alpha0.1): super().__init__(k, epsilon) self.alpha alpha # 固定步长参数 def update(self, action, reward): self.N[action] 1 # 使用固定步长而非样本平均 self.Q[action] self.alpha * (reward - self.Q[action])步长选择经验较大的α对变化反应更快但估计更波动可以设计随时间衰减的α序列4.2 参数敏感性分析通过网格搜索观察参数对算法表现的影响def parameter_sensitivity(): k 10 runs 1000 steps 1000 # 测试不同ε值 epsilons np.logspace(-3, 0, 10) eps_rewards [] for eps in epsilons: agent EpsilonGreedyAgent(k, epsiloneps) total_reward 0 for _ in range(runs): env BanditEnv(k) agent.__init__(k, epsiloneps) for _ in range(steps): action agent.act() reward env.step(action) agent.update(action, reward) total_reward reward eps_rewards.append(total_reward / (runs * steps)) plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.semilogx(epsilons, eps_rewards) plt.xlabel(ε value) plt.ylabel(Average Reward) # 类似地可以测试UCB的c参数和梯度的α参数4.3 实际应用建议初始化技巧乐观初始化可以鼓励早期探索结合领域知识设置合理的初始值监控与调试def debug_agent(agent, steps100): actions [] for _ in range(steps): action agent.act() actions.append(action) plt.hist(actions, binsagent.k) plt.xlabel(Action) plt.ylabel(Frequency) plt.show()多臂赌博机的扩展应用A/B测试推荐系统在线广告投放临床治疗方案选择