1. 从赌场到代码多臂老虎机到底是什么如果你第一次听到“多臂老虎机”这个词可能会觉得它离我们很远好像只存在于拉斯维加斯的赌场里。但说实话它可能是你理解整个强化学习世界最好的一块敲门砖。我刚开始接触的时候也觉得这个概念有点抽象但后来发现它其实是我们每天都会遇到的决策问题的完美抽象。想象一下你面前有10台老虎机每台机器在你拉动它的摇臂时都会给你一个随机的奖励比如一些游戏币。但问题是每台机器的“中奖”概率和平均奖励金额都不一样而且你事先完全不知道哪台机器最“大方”。你的目标很简单在有限的拉杆次数内尽可能拿到最多的总奖励。这就是多臂老虎机问题的核心。为什么这个问题如此重要因为它抓住了智能决策中的一个根本矛盾探索Exploration与利用Exploitation。你是应该把时间和金钱都花在目前看起来“最慷慨”的那台机器上利用已知最佳还是应该分一些机会去试试其他机器看看有没有隐藏的“大奖机”探索未知如果只利用你可能会错过真正的最佳选择如果只探索你又会浪费很多机会在很差的机器上。这个平衡的艺术就是强化学习的灵魂。在实际生活中这种场景无处不在。比如你每天中午要点外卖有10家你常点的店。你是每次都点那家你觉得最好吃的利用还是偶尔试试新开的店或者很久没点的老店探索再比如一个电商APP给用户推荐商品是每次都推点击率最高的那几个爆款利用还是偶尔插入一些新的、潜在的黑马商品探索理解了多臂老虎机你就理解了这些系统背后做决策的基本逻辑。所以别把它想得太复杂。接下来我们就用最直观的代码亲手搭建一个多臂老虎机的模拟环境并请出我们今天的主角——Epsilon-greedy算法来看看一个智能体是如何在这个充满未知的世界里一步步学会“赚钱”的。2. Epsilon-greedy用最简单的方式搞定探索与利用好了现在我们知道了问题是什么也感受到了“探索”和“利用”这对欢喜冤家的矛盾。那么有没有一种既简单又有效的方法来调和它们呢答案是肯定的这就是Epsilon-greedy策略。我敢说这是你强化学习生涯中会遇到的第一个也可能是最常用的策略之一。它的思想简单到令人发指但效果却出奇地好。Epsilon-greedy到底在干什么它的核心规则只有一句话大部分时间选择当前认为最好的但偶尔也随机选一个试试。就这么简单。我们来拆解一下这个名字和它的工作流程Epsilon (ε) 这是一个介于0和1之间的小数比如0.1。它代表了你“心血来潮”去随机探索的概率。Greedy (贪婪) 这代表了你“精明”的一面即选择当前估值最高的那个选项。所以Epsilon-greedy策略在每一次做选择时心里会默默进行一场“掷骰子”游戏生成一个0到1之间的随机数。如果这个随机数小于 ε比如0.1那么这次就“任性”一下完全随机地从所有选项中挑一个。这一步就是探索目的是收集新信息看看有没有被低估的“潜力股”。如果这个随机数大于等于 ε比如0.9的概率那么就“精明”一点选择当前记录中平均奖励最高的那个选项。这一步就是利用目的是把已知的好成果最大化。你可以把 ε 想象成一个控制探索欲望的旋钮ε 0 极端保守派。永远只选当前最好的绝不尝试新东西。如果一开始运气不好选了个差的就可能一条路走到黑永远发现不了真正的最佳选项。ε 1 极端冒险家。永远在随机乱选完全不在乎历史经验。这样虽然能探索得很充分但无法积累和利用知识长期表现会很差。ε 0.1 理智的平衡者。90%的时间利用已知最佳10%的时间去探索未知。这是我们最常用的一个起点值。我刚开始用的时候觉得这方法也太“糙”了吧探索的时候完全随机岂不是浪费了很多机会在明显很差的选项上但实测下来在像多臂老虎机这样选项不多、环境相对稳定的问题中它非常“稳”。因为它的探索是均匀的、持续的能保证即使某个选项一开始表现很差也有机会被重新评估避免被永远遗忘。注意Epsilon-greedy的探索是“无脑”的它不会因为某个选项历史表现差就减少探索也不会因为某个选项看起来有潜力就增加探索。这是它的优点简单也是它的缺点不够高效。但对于入门来说先掌握这个“傻瓜式”平衡术绝对足够了。3. 手把手搭建从零实现多臂老虎机模拟光说不练假把式咱们直接上代码。我会带你一行行地理解并分享我在写这类代码时踩过的一些坑。我们使用Python因为它有强大的科学计算库写起来就像搭积木一样简单。首先我们需要创建“赌场”也就是多臂老虎机环境。import numpy as np import matplotlib.pyplot as plt class MultiArmedBandit: 多臂老虎机类。 你可以把它想象成一个有n个摇臂的老虎机每个摇臂背后都有一个固定的“真实价值” 但每次拉动它你得到的奖励会在这个价值周围波动。 def __init__(self, num_arms, mu0, sigma1): 初始化老虎机。 :param num_arms: 老虎机的臂数选项数量。 :param mu: 用于生成每个臂真实价值的正态分布均值。默认0。 :param sigma: 用于生成每个臂真实价值的正态分布标准差。默认1。 self.num_arms num_arms # 核心每个臂的“真实”期望奖励。这是我们不知道的需要智能体去学习。 # 我们从均值为mu标准差为sigma的正态分布中随机采样为每个臂生成一个真实价值。 # 比如10个臂可能生成 [1.2, -0.3, 0.7, 2.1, ...] self.action_values np.random.normal(mu, sigma, num_arms) # 这个sigma也用于后续生成奖励的噪声。 self.sigma sigma def get_reward(self, action): 拉动指定的摇臂获得奖励。 :param action: 要拉动的摇臂编号0到num_arms-1。 :return: 一个奖励值浮点数。 # 奖励 真实价值 噪声。噪声也来自正态分布均值为0标准差为sigma。 # 这模拟了现实世界你选择同一个选项每次结果也可能不同。 reward np.random.normal(self.action_values[action], self.sigma) return reward这里有个关键点self.action_values是环境的绝对真理是每个臂的“真实”平均回报。但智能体是看不到这个数组的它只能通过一次次地拉杆、获得有噪声的奖励来估计每个臂的价值。这就好比你不知道一家餐馆的真实水平只能通过多次就餐的体验来猜测。接下来我们创建智能体它使用Epsilon-greedy策略来做决策。class EpsilonGreedyAgent: 使用Epsilon-greedy策略的智能体。 它的任务是在不了解老虎机内部构造的情况下通过尝试学会选择最好的摇臂。 def __init__(self, num_actions, epsilon0.1, initial_value0.0): 初始化智能体。 :param num_actions: 可选动作的数量应等于老虎机的臂数。 :param epsilon: 探索概率ε。默认0.1。 :param initial_value: 对每个动作价值的初始估计。默认乐观地设为0。 self.num_actions num_actions self.epsilon epsilon # Q值智能体对每个动作摇臂价值的当前估计。开始时间一无所知全设为初始值。 self.q_values np.full(num_actions, initial_value) # 记录每个动作被选择的次数用于计算增量式平均值。 self.action_counts np.zeros(num_actions) def select_action(self): 根据Epsilon-greedy策略选择一个动作。 # 生成一个[0, 1)之间的随机数 if np.random.uniform() self.epsilon: # 探索随机选择一个动作 action np.random.randint(self.num_actions) else: # 利用选择当前Q值估计最高的动作。 # np.argmax 返回最大值的索引如果有多个相同的最大值返回第一个。 action np.argmax(self.q_values) return action def update(self, action, reward): 根据执行动作后获得的奖励更新对该动作的价值估计。 :param action: 执行的动作。 :param reward: 获得的奖励。 # 增加该动作的选择次数 self.action_counts[action] 1 # 核心更新公式新的估计 旧的估计 步长 * (目标 - 旧的估计) # 这里步长是 1/N即用样本平均值来估计期望值。 # 这个公式等价于新的平均值 (旧的总和 新奖励) / 新的总次数 self.q_values[action] (reward - self.q_values[action]) / self.action_counts[action]这个update函数是学习发生的地方。它采用了一种叫增量式更新的方法非常巧妙。你不需要存储所有历史奖励只需要记住当前的估计值Q和被选择的次数N。每次获得新奖励R就用Q (1/N)*(R - Q)来更新。(R - Q)可以理解为“惊喜度”——实际奖励比预期高就是正惊喜要调高估计比预期低就是负惊喜要调低估计。1/N是步长随着尝试次数变多我们对这个动作的估计越来越准每次更新幅度就越来越小。4. 运行实验与可视化看看智能体如何学习环境有了智能体也有了现在让我们把它们放到一起跑一个完整的实验并用图表直观地看看学习过程。def run_experiment(num_arms10, steps1000, trials2000, epsilon0.1): 运行多臂老虎机实验。 :param num_arms: 老虎机臂数。 :param steps: 每次试验的步数拉杆次数。 :param trials: 独立试验的轮数为了结果更稳定取多次试验的平均。 :param epsilon: Epsilon-greedy算法的探索率。 :return: 每一步的平均奖励数组。 # 初始化一个数组用来累积所有试验、每一步的平均奖励 avg_rewards_over_time np.zeros(steps) print(f开始实验{trials}轮试验每轮{steps}步ε{epsilon}) for trial in range(trials): # 每一轮试验都生成一个全新的、随机老虎机环境 bandit MultiArmedBandit(num_arms) # 每一轮试验智能体都“从零开始”学习 agent EpsilonGreedyAgent(num_arms, epsilonepsilon) for step in range(steps): # 智能体根据当前策略选择动作 chosen_arm agent.select_action() # 环境给出奖励 reward bandit.get_reward(chosen_arm) # 智能体根据奖励更新知识 agent.update(chosen_arm, reward) # 增量式更新平均奖励一种高效的在线平均计算法 avg_rewards_over_time[step] (reward - avg_rewards_over_time[step]) / (trial 1) # 每完成一定比例的试验打印进度方便观察 if (trial 1) % 500 0: print(f 已完成 {trial 1} / {trials} 轮试验) return avg_rewards_over_time # 设置参数并运行实验 num_arms 10 steps 2000 trials 2000 epsilon 0.1 avg_rewards run_experiment(num_arms, steps, trials, epsilon) # 可视化结果 plt.figure(figsize(10, 6)) plt.plot(avg_rewards, linewidth2) plt.xlabel(拉杆次数 (Steps), fontsize12) plt.ylabel(平均奖励 (Average Reward), fontsize12) plt.title(fEpsilon-greedy 策略在多臂老虎机上的表现 (ε{epsilon}), fontsize14) plt.grid(True, linestyle--, alpha0.7) plt.tight_layout() plt.show()运行这段代码你会得到一张图。这张图讲述了一个完整的故事曲线起始阶段前几十到几百步 平均奖励快速上升。这个阶段是探索主导期。智能体在疯狂地尝试各个摇臂因为它一开始的Q值估计都是一样的初始值所以利用阶段其实也是在随机选。随着它尝试它开始分辨出哪些臂好哪些臂差平均奖励自然就上去了。曲线中后期 平均奖励逐渐稳定在一个较高的水平并缓慢上升或轻微波动。这个阶段是利用主导期。智能体已经大致摸清了各个臂的“底细”大部分时间1-ε都在拉它认为最好的那个臂所以平均奖励接近最佳臂的真实价值。但由于仍有ε概率的随机探索它偶尔会拉到差臂导致奖励偶尔下降所以曲线会有微小波动。缓慢上升是因为随着探索的持续它对最佳臂的估计越来越精确。提示我们做了2000轮独立试验并取平均是为了消除单次试验中老虎机生成好坏action_values的随机性和奖励噪声带来的偶然性让学习曲线更平滑反映算法的平均性能。这是评估强化学习算法的标准做法。5. 深入调参ε值如何影响学习曲线我们上面用了 ε0.1那如果用别的值会怎样呢是探索多点好还是少点好我们来做个对比实验这是理解算法行为的关键一步。# 测试不同的epsilon值 epsilon_list [0.0, 0.01, 0.1, 0.3] results {} for eps in epsilon_list: print(f\n正在运行 ε {eps} 的实验...) avg_rewards run_experiment(num_arms10, steps2000, trials1000, epsiloneps) results[eps] avg_rewards # 绘制对比图 plt.figure(figsize(12, 7)) colors [red, blue, green, orange] for idx, (eps, rewards) in enumerate(results.items()): plt.plot(rewards, labelfε {eps}, colorcolors[idx], linewidth2) plt.xlabel(拉杆次数 (Steps), fontsize12) plt.ylabel(平均奖励 (Average Reward), fontsize12) plt.title(不同探索率(ε)对Epsilon-greedy算法性能的影响, fontsize14) plt.legend(fontsize11) plt.grid(True, linestyle--, alpha0.7) plt.tight_layout() plt.show()运行后你会看到几条走势迥异的曲线ε 值别名学习曲线特点长期表现适用场景0.0 (纯贪婪)极端保守派初期上升后很快稳定在一个平台但平台高度可能很低。不稳定完全取决于初始运气。如果一开始碰巧探索到最佳臂表现就好否则永远困在次优臂。几乎不适用风险太高。0.01轻度探索者初期上升较慢因为探索不足需要更长时间发现最佳臂。但一旦发现曲线会稳步缓慢上升至接近最优。长期表现最好之一因为后期探索干扰极少几乎纯利用。环境稳定、训练步数非常长的场景。0.1标准平衡者初期上升快能较快找到不错的手臂。后期稳定在一个较高水平但有轻微波动。稳定且优秀是大多数情况下的默认首选。通用场景平衡了学习速度和最终性能。0.3重度探索者初期上升可能最快因为探索猛但后期曲线在较高位置持续大幅波动无法稳定。长期平均奖励较低因为即使后期仍有30%的时间在随机试错浪费机会。环境可能剧烈变化、需要持续追踪最优解的场景。从对比中我们能得出几个非常实用的经验ε0纯贪婪要尽量避免除非你能保证智能体初始估计非常准否则它很容易陷入局部最优。我在早期项目中用过一次结果模型表现时好时坏排查了半天才发现是这个原因。小ε值需要耐心如果你有充足的时间训练步数比如训练一个游戏AI玩几百万局那么用很小的ε如0.01可能最终效果最好因为它后期几乎不“分心”。ε0.1是黄金起点当你面对一个新问题不知道如何设置时先用0.1。它在大多数问题上都能提供一个可靠且不错的基准性能。大ε值用于动态环境如果你的环境中的最佳选项会随着时间变化比如用户的兴趣在迁移那么你需要保持较高的探索率如0.2、0.3以便持续发现新的最佳选项。6. 算法优化与实战思考让Epsilon-greedy更强大基础的Epsilon-greedy已经能解决很多问题了但如果你想让它更上一层楼这里有几个我实践中觉得非常有效的优化思路和实战技巧。1. 衰减的Epsilon (ε-decay)从探索者到利用者的优雅过渡一个很自然的想法是我们一开始啥也不知道应该多探索后来知道的多了就应该多利用。我们可以让ε随着时间步数衰减。class EpsilonDecayAgent(EpsilonGreedyAgent): 带有epsilon衰减的智能体 def __init__(self, num_actions, epsilon_start1.0, epsilon_end0.01, decay_steps1000): super().__init__(num_actions, epsilonepsilon_start) # 初始epsilon很高 self.epsilon_start epsilon_start self.epsilon_end epsilon_end self.decay_steps decay_steps self.step_count 0 def select_action(self): # 计算当前衰减后的epsilon self.epsilon self.epsilon_end (self.epsilon_start - self.epsilon_end) * \ max(0, (self.decay_steps - self.step_count) / self.decay_steps) self.step_count 1 # 调用父类的选择动作方法 return super().select_action()这种线性衰减策略非常常用。比如设置epsilon_start1.0epsilon_end0.01decay_steps1000。意味着前1000步ε从1.0完全随机探索线性降到0.01几乎纯利用。这模拟了一个更自然的学习过程早期广泛尝试后期专注深耕。2. 乐观初始值给智能体一点“积极的暗示”还记得我们初始化q_values时用的initial_value0吗如果我们把它设成一个比较大的正数比如5.0会发生什么这叫做乐观初始值。因为智能体一开始对所有动作的期望都很高所以每次选择后得到的实际奖励可能接近0或负数都会让它“失望”从而调低该动作的估值。这样未被尝试过的动作估值始终很高会不断被选择从而自然地鼓励了探索。这种方法甚至可以和ε0的纯贪婪策略结合实现一种隐式的、无需参数的探索。在有些问题上效果惊人。3. 处理非平稳环境给旧经验降权我们之前用的更新公式步长 1/N意味着越旧的样本和越新的样本权重一样。这在环境是静止臂的奖励分布不变时是合理的。但如果老虎机的奖励分布会随时间缓慢变化非平稳环境我们就应该更看重近期的经验。一个简单的办法是使用一个固定的小步长α如0.1来代替1/Ndef update_with_alpha(self, action, reward, alpha0.1): self.q_values[action] alpha * (reward - self.q_values[action])这样每次更新都只向新奖励调整一小步旧的信息会逐渐被遗忘让智能体能够跟踪环境的变化。4. 代码实现的坑与最佳实践随机种子在调试和对比不同算法时务必使用np.random.seed(你的种子)来固定随机数生成器。这样才能确保环境老虎机真实价值和智能体的随机选择是可复现的对比才是公平的。性能考量当臂数非常多成千上万时np.argmax(self.q_values)可能会成为瓶颈。虽然对于入门问题这不是问题但要知道这一点。可视化不止平均奖励除了看平均奖励曲线还可以绘制“选择最佳臂的百分比”曲线。这能更直接地反映智能体是否学会了找到真正最好的那个选项。Epsilon-greedy就像一把瑞士军刀虽然简单但通过不同的初始化、步长和衰减策略它能适应各种各样的场景。理解它不仅是学习一个算法更是理解“探索-利用”这一核心权衡的思维方式。当你以后遇到更复杂的算法如UCB、Thompson Sampling甚至深度Q网络时你会发现它们本质上都是在用更聪明的方式解决同一个问题。