别再死记硬背了!用Python代码实战带你搞懂多臂老虎机的‘探索与利用’
用Python代码实战理解多臂老虎机的探索与利用平衡打开你的Jupyter Notebook我们今天不聊枯燥的数学公式而是用代码和可视化来直观感受强化学习中最经典的探索与利用困境。想象你站在一排老虎机前每台机器的中奖概率各不相同但未知——这就是著名的多臂老虎机问题。1. 环境搭建与问题建模我们先构建一个10臂老虎机模拟环境每台机器的中奖概率随机生成import numpy as np import matplotlib.pyplot as plt class BernoulliBandit: def __init__(self, K): self.probs np.random.uniform(0.1, 0.9, K) # 各臂中奖概率 self.best_idx np.argmax(self.probs) self.best_prob self.probs[self.best_idx] self.K K def pull(self, k): return 1 if np.random.rand() self.probs[k] else 0关键参数说明K: 老虎机臂数probs: 各臂真实中奖概率对算法不可见best_prob: 最大中奖概率用于计算懊悔2. 基础算法实现与比较2.1 ε-贪婪算法最简单的平衡策略class EpsilonGreedy: def __init__(self, bandit, epsilon0.1): self.bandit bandit self.epsilon epsilon self.estimates np.zeros(bandit.K) # 各臂奖励估计 self.counts np.zeros(bandit.K) # 各臂尝试次数 def choose_action(self): if np.random.rand() self.epsilon: return np.random.randint(self.bandit.K) # 探索 return np.argmax(self.estimates) # 利用 def update(self, k, reward): self.counts[k] 1 self.estimates[k] (reward - self.estimates[k]) / self.counts[k] # 增量更新运行测试np.random.seed(42) bandit BernoulliBandit(10) eps_greedy EpsilonGreedy(bandit, epsilon0.1) rewards [] for _ in range(1000): k eps_greedy.choose_action() reward bandit.pull(k) eps_greedy.update(k, reward) rewards.append(reward)2.2 衰减ε策略动态调整探索率固定ε值会导致长期不必要的探索改进方案是让ε随时间衰减class DecayingEpsilonGreedy(EpsilonGreedy): def choose_action(self): epsilon 1 / (1 np.sum(self.counts)) # 衰减公式 if np.random.rand() epsilon: return np.random.randint(self.bandit.K) return np.argmax(self.estimates)2.3 UCB算法基于不确定性的智能探索上置信界(UCB)算法为每个动作计算奖励上界class UCB: def __init__(self, bandit, c2): self.bandit bandit self.c c # 探索系数 self.estimates np.zeros(bandit.K) self.counts np.zeros(bandit.K) self.total 0 def choose_action(self): self.total 1 ucb self.estimates self.c * np.sqrt(np.log(self.total) / (self.counts 1e-5)) return np.argmax(ucb) def update(self, k, reward): self.counts[k] 1 self.estimates[k] (reward - self.estimates[k]) / self.counts[k]3. 可视化分析与算法对比让我们用matplotlib绘制三种算法的累积懊悔曲线def compare_algorithms(bandit, n_steps2000): algos { ε-greedy(0.1): EpsilonGreedy(bandit, 0.1), Decaying ε-greedy: DecayingEpsilonGreedy(bandit), UCB(c2): UCB(bandit, 2) } results {name: [] for name in algos} for _ in range(n_steps): for name, algo in algos.items(): k algo.choose_action() reward bandit.pull(k) algo.update(k, reward) regret bandit.best_prob - bandit.probs[k] results[name].append(regret if not results[name] else results[name][-1] regret) plt.figure(figsize(10,6)) for name, regrets in results.items(): plt.plot(np.cumsum(regrets), labelname) plt.xlabel(Steps) plt.ylabel(Cumulative Regret) plt.legend() plt.show() compare_algorithms(BernoulliBandit(10))典型输出分析ε-greedy懊悔线性增长持续付出探索成本Decaying ε-greedy前期快速增长后期趋缓UCB最优的次线性增长对数级别4. 高级话题非平稳环境处理现实场景中老虎机的奖励概率可能随时间变化。我们需要修改奖励更新策略class NonStationaryBandit(BernoulliBandit): def __init__(self, K, drift0.01): super().__init__(K) self.drift drift def pull(self, k): self.probs np.random.normal(0, self.drift, self.K) # 概率漂移 self.probs np.clip(self.probs, 0.1, 0.9) self.best_prob np.max(self.probs) return super().pull(k) class WeightedUCB(UCB): def __init__(self, bandit, c2, alpha0.1): super().__init__(bandit, c) self.alpha alpha # 遗忘因子 def update(self, k, reward): self.counts[k] 1 self.estimates[k] self.alpha * (reward - self.estimates[k]) # 指数加权更新关键改进点使用指数加权平均替代算术平均更大的探索系数应对环境变化定期强制探索机制5. 实用技巧与工程优化在实际应用中我们还需要考虑以下优化点内存优化# 使用生成器替代完整记录 def run_algorithm(algo, bandit, n_steps): total_reward 0 for _ in range(n_steps): k algo.choose_action() reward bandit.pull(k) algo.update(k, reward) total_reward reward yield total_reward并行化探索from concurrent.futures import ThreadPoolExecutor def parallel_bandit_test(n_bandits5, n_steps1000): with ThreadPoolExecutor() as executor: futures [] for _ in range(n_bandits): bandit BernoulliBandit(10) algo UCB(bandit) futures.append(executor.submit(run_algorithm, algo, bandit, n_steps)) results [f.result() for f in futures] return np.mean(results, axis0)常见陷阱与解决方案问题现象可能原因解决方案懊悔持续线性增长探索不足或固定ε值使用衰减ε或UCB收敛到次优解早期探索不充分增加初始探索率性能波动大非平稳环境引入遗忘机制计算资源消耗高完全记录历史改用增量更新6. 扩展应用场景多臂老虎机思想可应用于诸多领域推荐系统案例class NewsRecommender: def __init__(self, article_topics): self.bandit BernoulliBandit(len(article_topics)) self.topics article_topics def recommend(self, user_profile): # 结合用户画像和UCB选择文章 ucb_scores self.bandit.estimates 1.5 * np.sqrt(np.log(self.bandit.total) / (self.bandit.counts 1)) topic_idx np.argmax(ucb_scores * user_profile) # 个性化权重 return self.topics[topic_idx]A/B测试优化def adaptive_ab_test(variants, n_steps10000): bandit BernoulliBandit(len(variants)) algo ThompsonSampling(bandit) for _ in range(n_steps): var_idx algo.choose_action() conversion show_variant_and_track(variants[var_idx]) algo.update(var_idx, conversion) return variants[np.argmax(algo.estimates)]在真实项目中我发现Thompson采样对初始参数非常敏感特别是在小样本场景下。一个实用的技巧是为每个臂设置合理的先验分布而不是简单的均匀分布。例如如果某些广告的历史CTR约为2%可以将Beta分布的参数初始化为α2β98这样能加速初期收敛。