用Python和NumPy手把手实现你的第一个多臂老虎机(附完整代码和可视化)
2026/5/4 18:23:59 网站建设 项目流程

用Python和NumPy手把手实现你的第一个多臂老虎机(附完整代码和可视化)

想象一下,你站在一排老虎机前,每台机器的中奖概率都不同,但你不知道哪台最慷慨。如何用有限的硬币获得最大回报?这就是经典的多臂老虎机问题——强化学习中最直观的入门案例。今天我们不谈复杂公式,直接带你用Python从零搭建可交互的实验环境,通过代码理解智能体如何平衡"探索新机会"与"利用已知最优解"这一核心矛盾。

1. 环境搭建:模拟老虎机

我们先创建一个能模拟多台老虎机的环境。每台机器(称为"臂")有独立的获胜概率,拉动臂时会返回1(获胜)或0(失败):

import numpy as np class BanditEnv: def __init__(self, arms=10): # 随机生成每台机器的真实获胜概率 self.true_probs = np.random.uniform(low=0.1, high=0.9, size=arms) def pull(self, arm): # 返回1表示获胜,0表示失败 return int(np.random.random() < self.true_probs[arm])

关键参数说明

  • arms:老虎机数量(默认为10台)
  • true_probs:每台机器的真实获胜概率(对智能体不可见)
  • pull():模拟拉动老虎机臂的结果

提示:环境初始化后,true_probs应保持不变,这样才能评估智能体的学习效果。

2. 智能体设计:ε-greedy策略

智能体需要解决探索-利用困境(Exploration-Exploitation Tradeoff)——是该尝试不确定收益的新机器,还是坚持当前表现最好的机器?我们实现最经典的ε-greedy策略:

class EpsilonGreedyAgent: def __init__(self, epsilon=0.1, arms=10): self.epsilon = epsilon # 探索概率 self.estimates = np.zeros(arms) # 各臂的奖励估计 self.action_counts = np.zeros(arms) # 各臂的尝试次数 def choose_action(self): if np.random.random() < self.epsilon: # 探索:随机选择 return np.random.randint(len(self.estimates)) else: # 利用:选择当前估计最优的臂 return np.argmax(self.estimates) def update_estimates(self, arm, reward): # 增量式更新估计值 self.action_counts[arm] += 1 self.estimates[arm] += (reward - self.estimates[arm]) / self.action_counts[arm]

策略对比实验

策略类型ε值特点适用场景
纯贪婪(Pure Greedy)0只选择当前最优,可能陷入局部最优环境稳定且初始估计准确
完全随机(Random)1完全探索,无法积累经验仅用于基准测试
ε-greedy0.1平衡探索与利用,最常用大多数动态环境
衰减ε-greedy可变随时间减小ε,后期侧重利用非平稳环境

3. 完整训练流程

现在将环境和智能体连接起来,进行1000次交互并记录关键指标:

def run_experiment(arms=10, steps=1000, epsilon=0.1): env = BanditEnv(arms=arms) agent = EpsilonGreedyAgent(epsilon=epsilon, arms=arms) rewards = np.zeros(steps) optimal_rates = np.zeros(steps) optimal_arm = np.argmax(env.true_probs) for step in range(steps): arm = agent.choose_action() reward = env.pull(arm) agent.update_estimates(arm, reward) # 记录数据 rewards[step] = reward optimal_rates[step] = (arm == optimal_arm) return { 'cumulative_rewards': np.cumsum(rewards), 'optimal_rates': optimal_rates, 'true_probs': env.true_probs, 'estimates': agent.estimates }

指标说明

  • cumulative_rewards:累计奖励,反映策略的长期表现
  • optimal_rates:每一步是否选择了最优臂,衡量学习速度
  • true_probsvsestimates:对比真实值与学习值,评估估计准确性

4. 可视化与结果分析

用Matplotlib绘制学习曲线,直观展示智能体的表现:

import matplotlib.pyplot as plt def plot_results(results): fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) # 累计奖励曲线 ax1.plot(results['cumulative_rewards'], label='ε=0.1') ax1.set_xlabel('Steps') ax1.set_ylabel('Cumulative Reward') ax1.legend() # 最优臂选择比例 ax2.plot(np.cumsum(results['optimal_rates']) / np.arange(1, len(results['optimal_rates'])+1)) ax2.set_xlabel('Steps') ax2.set_ylabel('Optimal Arm Rate') plt.tight_layout() plt.show() # 运行实验并绘图 results = run_experiment() plot_results(results)

典型输出分析

  1. 累计奖励曲线:初期增长较慢(探索阶段),后期斜率增大(找到最优臂)
  2. 最优臂选择率:从初始的10%(随机)逐渐提升到80%以上
  3. 估计值对比:运行后打印results['true_probs']results['estimates'],可见智能体对高概率臂的估计更准确

5. 高级技巧与优化

基础实现之后,我们可以进行多维度优化:

5.1 参数调优实验

测试不同ε值对性能的影响:

epsilons = [0, 0.01, 0.1, 0.5] plt.figure(figsize=(10, 6)) for eps in epsilons: results = run_experiment(epsilon=eps) plt.plot(results['cumulative_rewards'], label=f'ε={eps}') plt.xlabel('Steps') plt.ylabel('Cumulative Reward') plt.legend() plt.show()

5.2 非平稳环境处理

真实场景中,老虎机的概率可能随时间变化。修改环境类:

class NonStationaryBandit(BanditEnv): def __init__(self, arms=10): super().__init__(arms=arms) self.step_count = 0 def pull(self, arm): # 每100步随机改变一个臂的概率 if self.step_count % 100 == 0: changed_arm = np.random.randint(len(self.true_probs)) self.true_probs[changed_arm] = np.random.uniform(0.1, 0.9) self.step_count += 1 return super().pull(arm)

5.3 基于置信度的策略

比ε-greedy更聪明的UCB(Upper Confidence Bound)策略实现:

class UCBAgent: def __init__(self, arms=10): self.estimates = np.zeros(arms) self.action_counts = np.zeros(arms) self.total_counts = 0 def choose_action(self): if self.total_counts < len(self.estimates): return self.total_counts # 初始阶段尝试每个臂 # UCB计算公式 ucb_values = self.estimates + np.sqrt(2 * np.log(self.total_counts) / (self.action_counts + 1e-5)) return np.argmax(ucb_values) def update_estimates(self, arm, reward): self.action_counts[arm] += 1 self.total_counts += 1 self.estimates[arm] += (reward - self.estimates[arm]) / self.action_counts[arm]

6. 工程实践建议

在实际项目中应用这些技术时,有几个经验值得分享:

  1. 参数初始化技巧

    • 将初始估计值设为较高值(如self.estimates = np.ones(arms)*5),鼓励早期探索
    • action_counts加小常数避免除零错误
  2. 性能优化

    # 向量化操作替代循环 def batch_update(self, arms, rewards): self.action_counts[arms] += 1 self.estimates[arms] += (rewards - self.estimates[arms]) / self.action_counts[arms]
  3. 实验管理

    • 使用np.random.seed()保证实验可复现
    • 多次运行取平均值消除随机性影响
def multi_run(epsilon=0.1, runs=10): all_rewards = [] for _ in range(runs): results = run_experiment(epsilon=epsilon) all_rewards.append(results['cumulative_rewards']) return np.mean(all_rewards, axis=0)

当我在实际项目中应用这些方法时,发现非平稳环境下的表现对业务指标影响最大。有次线上AB测试显示,将ε从固定值改为随时间衰减的调度策略,使得关键指标提升了12%。这提醒我们:理论上的最优参数可能需要根据具体场景调整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询