资讯动态

强化学习入门:多臂老虎机翻译与Python实现详解

发布时间:2026/10/17 22:32:09 来源:尧图企业网站定制
1. 为什么我要动手翻译这一章动手翻译《Reinforcement Learning: An Introduction》第二章起因很简单我在带几个刚入门强化学习的朋友时发现他们卡在“多臂老虎机”这一章的时间远超预期。英文原版写得极好但大量统计学术语和微妙的措辞对非英语母语的初学者来说理解成本很高。市面上虽然已有一些中文资料但要么过于简略要么跳过了关键的推导细节读完之后脑子里只剩一个模糊的“探索与利用”概念真到写代码时完全不知道从哪下手。第二章“Multi-armed Bandits”在整本书里的地位非常特殊。它不是一个可以跳过的基础章节而是整个强化学习核心矛盾的微缩模型——你只有有限的信息必须在“利用已知的好选项”和“探索可能更好的选项”之间做取舍。这个矛盾在后面所有章节里反复出现只是形式越来越复杂。如果第二章没吃透后面看TD学习、策略梯度时遇到“探索策略”“乐观初始值”“置信上界”这些概念就只能死记硬背没法真正理解它们为什么被设计成那样。所以我决定自己动手翻译这一章目标不是逐字逐句的直译而是让一个只学过概率论和基础Python的人读完中文版之后能直接上手写出一个可运行的测试环境并且能解释清楚每个算法背后的直觉。翻译过程中我做了大量注释和补充把原文里一笔带过的推导展开把容易混淆的符号统一把代码示例改写成更贴近实际工程习惯的写法。这篇博文就是整个翻译项目的完整记录包括我的翻译策略、核心概念的拆解方式、代码实现细节以及踩过的坑。适合读这篇内容的人正在自学强化学习但被英文教材卡住的开发者想快速回顾多臂老虎机核心算法和实现细节的从业者以及任何对“探索与利用”这个经典问题感兴趣、想看看一个具体项目是怎么落地的人。不需要你有强化学习基础但最好懂一点Python和基础概率。2. 翻译策略与整体设计思路2.1 为什么选择“注释式翻译”而不是直译直译最大的问题是英文教材里很多句子是“作者在跟你聊天”的语气直译成中文会变得非常生硬。比如原文里有一句“We call this the exploration-exploitation dilemma”直译是“我们称之为探索-利用困境”读起来像在背定义。我的处理方式是保留术语的准确性但把解释性的句子改写成更自然的中文表达同时在旁边加注释说明原文的措辞意图。另一个原因是原书第二版相比第一版增加了不少内容有些段落之间的逻辑跳跃比较大。比如从“贪心策略”过渡到“ε-贪心策略”时原文只用了一小段话说明动机但初学者往往在这里卡住为什么不能一直贪心我的做法是在翻译之后补一段“为什么需要随机探索”的说明用具体的数值例子展示贪心策略在什么情况下会锁死在次优选项上。注释式翻译的另一个好处是我可以把原文里分散在不同段落的相关概念集中到一起解释。比如“乐观初始值”和“置信上界”这两个方法原文是分开讲的但它们背后的核心思想有相通之处——都是通过某种方式给不确定性高的选项更高的优先级。我在翻译时加了一个对比表格把两种方法的适用场景、参数敏感度、实现复杂度放在一起方便读者建立整体认知。2.2 符号系统的统一与简化原书第二章的符号系统对初学者不太友好。同一个概念在不同段落里用了不同的符号比如动作价值有时写成 ( q_*(a) )有时写成 ( Q_t(a) )还有 ( q_t(a) ) 这种估计值。如果不做处理读者很容易混淆“真实值”和“估计值”。我的处理方式是在翻译正文之前先加一个“符号约定”小节把所有会用到的符号列出来明确区分真实值带星号、估计值带下标t、以及采样得到的奖励值。然后在正文翻译中严格遵循这套约定如果原文用了不一致的符号我会统一替换并在注释里说明。具体来说我定义了以下核心符号符号含义备注( A_t )第t步选择的动作随机变量( R_t )第t步获得的奖励随机变量( q_*(a) )动作a的真实价值未知需要估计( Q_t(a) )动作a在t时刻的估计价值算法维护的值( N_t(a) )动作a在t时刻之前被选择的次数用于计算平均值( \epsilon )探索概率ε-贪心算法的参数( c )置信度参数UCB算法的参数这套符号约定贯穿整个翻译读者只要记住这一张表后面读起来就不会乱。2.3 代码示例的改写原则原书第二章的代码示例是用伪代码和简单的Python片段给出的风格偏学术。我在翻译时做了两处调整第一把所有伪代码改写成可运行的Python函数加上类型提示和文档字符串第二把原书里为了简化而省略的边界条件补上比如当某个动作从未被选择过时平均值计算需要特殊处理。改写后的代码遵循几个原则每个算法独立成一个类方便对比和替换所有随机性都通过显式的随机种子控制保证结果可复现测试环境多臂老虎机和算法逻辑分离这样读者可以自己替换不同的环境来测试同一个算法。举个例子原书在讲ε-贪心算法时给出的伪代码大致是“以概率ε随机选择否则选择当前估计值最高的动作”。这个描述在实现时会遇到一个问题如果有多个动作的估计值相同且都是最高应该选哪个原书没有明确说明。我的处理是默认选择第一个遇到的最高值动作并在注释里说明这是一个实现选择不同的选择策略可能会影响早期表现。3. 核心概念拆解与翻译要点3.1 多臂老虎机问题的形式化定义翻译这一节时我花了最多时间在“如何让读者理解为什么需要形式化定义”上。原书直接给出了动作价值、奖励分布、最优价值等定义但没有解释这些定义是为了解决什么问题。我在翻译之后补了一段说明形式化定义的目的不是增加理解难度而是为了让我们能够精确地描述“什么是好的决策”从而设计出可以衡量好坏的算法。具体来说多臂老虎机问题可以这样理解你面前有k个选项动作每个选项的奖励来自一个固定的概率分布但你不知道这些分布的具体参数。你每一步选择一个动作获得一个奖励目标是让一段时间内获得的总奖励尽可能大。这个问题的核心难点在于你需要在“利用”当前认为最好的动作和“探索”可能更好的动作之间做权衡。翻译时我特别注意区分了两个概念动作的真实价值和动作的估计价值。真实价值是固定的但未知的估计价值是算法根据已有数据计算出来的。很多初学者会混淆这两个概念导致在分析算法表现时搞不清楚“误差”到底来自哪里。我在注释里用了一个比喻真实价值像是每个选项的“真实水平”估计价值像是你根据有限次尝试形成的“印象”。你的目标是让印象尽可能接近真实水平但永远无法完全确定。3.2 动作价值估计的增量式实现原书在讲动作价值估计时先给出了最简单的样本平均法然后推导了增量式更新公式。这个推导是第二章最重要的数学内容之一但原文写得比较紧凑初学者容易在符号变换上卡住。我的翻译策略是把推导过程拆成三步每一步都加上文字说明。第一步写出样本平均的定义式第二步把第n次观测从求和式中分离出来第三步把分离出来的部分用前n-1次的平均值表示得到增量式更新公式。每一步之间用“这一步的目的是……”来过渡让读者知道为什么要做这个变换。增量式更新公式的核心形式是[ Q_{n1} Q_n \frac{1}{n}(R_n - Q_n) ]这个公式的直觉是新的估计值等于旧的估计值加上一个修正项修正项的大小取决于“实际奖励与旧估计的差距”以及“学习率”。学习率是1/n随着n增大而减小意味着早期观测的影响更大后期观测的影响逐渐减小。这个特性在平稳环境下是合理的因为随着数据增多估计值应该趋于稳定。我在翻译时加了一个数值例子假设前三次奖励分别是1、3、2用增量式公式一步步计算估计值的变化过程。这个例子在原文中没有但我觉得对理解公式非常有帮助。计算过程如下初始 ( Q_1 0 )( n1 )( Q_2 0 \frac{1}{1}(1-0) 1 )( n2 )( Q_3 1 \frac{1}{2}(3-1) 2 )( n3 )( Q_4 2 \frac{1}{3}(2-2) 2 )最终估计值是2正好是三个奖励的平均值。这个例子说明增量式更新和直接求平均是等价的但增量式的好处是不需要保存所有历史奖励只需要维护当前估计值和计数。3.3 探索与利用的权衡从贪心到ε-贪心这一节是第二章的核心也是翻译时最需要小心处理的部分。原书先介绍了贪心策略然后指出它的局限性再引入ε-贪心策略。这个逻辑链条很清晰但初学者容易产生一个误解以为ε-贪心只是“偶尔随机一下”没有理解它背后的理论保证。我在翻译时加了一段说明贪心策略的问题不是“不够好”而是“可能永远发现不了更好的选项”。如果初始估计值恰好让某个次优动作排在第一位贪心策略会一直选择它永远不会去尝试其他动作也就永远无法修正这个错误的估计。ε-贪心通过以一定概率随机选择动作保证了每个动作都有机会被选中从而保证了估计值最终会收敛到真实值。翻译ε-贪心算法时我特别强调了ε的选择是一个权衡ε越大探索越多估计值收敛越快但短期奖励损失越大ε越小利用越多短期奖励越高但可能收敛到次优动作。原书给出了一个经验法则在10臂测试平台上ε取0.1左右通常表现不错。我在注释里补充说这个值不是绝对的实际应用中需要根据任务的时间跨度和奖励尺度来调整。为了帮助读者直观理解ε的影响我在翻译之后加了一个对比实验的描述用同一个随机种子分别运行ε0、ε0.01、ε0.1的ε-贪心算法记录平均奖励随步数的变化。实验结果显示ε0的曲线在早期上升很快但很快趋于平缓最终平均奖励明显低于其他两条曲线ε0.1的曲线早期上升较慢但最终平均奖励最高ε0.01的曲线介于两者之间。这个实验在原文中没有具体数据但结论与原文一致我用自己的实现验证了这一点。3.4 乐观初始值与不确定性驱动探索乐观初始值方法是我在翻译时觉得最巧妙的部分。它的核心思想是把初始估计值设得比真实值高这样每个动作在第一次被选择时获得的奖励都会低于初始估计导致估计值下降。由于算法总是选择估计值最高的动作那些还没被选择过的动作会因为初始值高而被优先选择从而实现了一种“自动探索”。翻译这一节时我重点解释了“为什么乐观初始值能驱动探索”。关键在于初始估计值是一个先验信念算法会根据实际观测不断修正这个信念。如果初始信念过于乐观算法会倾向于尝试那些还没有被“失望”过的动作。这种探索是确定性的不需要随机性因此在某些场景下比ε-贪心更高效。但乐观初始值有一个明显的局限它只适用于平稳环境。如果环境是非平稳的真实价值会随时间变化那么早期形成的乐观信念可能已经过时继续依赖它会导致错误的决策。原书在讲非平稳环境时提到了这个问题我在翻译时加了一个注释说明乐观初始值方法在非平稳环境下的表现会显著下降需要配合其他机制使用。3.5 置信上界与梯度算法置信上界UCB算法的翻译是第二章技术含量最高的部分之一。原书给出了UCB的公式[ A_t \arg\max_a \left[ Q_t(a) c \sqrt{\frac{\ln t}{N_t(a)}} \right] ]这个公式的直觉是在选择动作时不仅考虑估计价值还考虑估计的不确定性。不确定性用 ( \sqrt{\ln t / N_t(a)} ) 来衡量其中 ( N_t(a) ) 是动作a被选择的次数。选择次数越少不确定性越大这个项就越大从而鼓励算法去探索那些被选择次数少的动作。翻译时我重点解释了两个问题第一为什么不确定性项里有一个 ( \ln t )这是因为随着总步数t增加我们对所有动作的估计都在变得更准确但准确度的提升速度是对数级的所以不确定性项的增长也是对数级的。第二参数c控制探索的程度c越大探索越多。原书建议c取2左右但实际应用中需要根据奖励的尺度来调整。梯度算法部分原书介绍了基于softmax的偏好函数方法。这个方法与前面几种方法有本质区别前面几种方法都是直接估计动作价值然后根据价值选择动作梯度算法则是直接学习一个动作偏好函数通过梯度上升来更新偏好使得获得高奖励的动作被选择的概率增加。翻译这一节时我加了一个对比表格把ε-贪心、UCB、梯度算法在几个维度上做了对比方法探索机制参数适用场景实现复杂度ε-贪心随机探索ε通用低乐观初始值确定性探索初始值平稳环境低UCB不确定性驱动c平稳环境中梯度算法偏好学习α通用中这个表格在原文中没有但我觉得对读者建立整体认知非常有帮助。4. 实操过程与核心环节实现4.1 测试环境搭建10臂测试平台翻译完理论部分之后我动手实现了一个完整的10臂测试平台用来验证书中的结论。这个平台的设计目标是可复现、可扩展、可对比。可复现意味着所有随机性都通过种子控制可扩展意味着可以方便地替换成其他臂数或奖励分布可对比意味着不同算法的运行条件完全一致。环境的核心是一个类初始化时接收臂数k、奖励分布的均值和方差、随机种子。每个臂的真实价值从标准正态分布中采样得到奖励在真实价值的基础上加上标准正态噪声。这个设计对应原书中的“10臂测试平台”10个臂真实价值服从均值为0、方差为1的正态分布奖励噪声服从均值为0、方差为1的正态分布。import numpy as np class BanditEnv: def __init__(self, k10, mean0.0, std1.0, seedNone): self.k k self.rng np.random.default_rng(seed) self.true_values self.rng.normal(mean, std, sizek) self.optimal_action np.argmax(self.true_values) def step(self, action): reward self.rng.normal(self.true_values[action], 1.0) return reward def reset(self, seedNone): if seed is not None: self.rng np.random.default_rng(seed) self.true_values self.rng.normal(0, 1, sizeself.k) self.optimal_action np.argmax(self.true_values)这个环境类的设计有一个细节需要注意reset方法允许重新采样真实价值这在做多次独立实验时很有用。每次实验用不同的种子可以得到不同的真实价值配置从而评估算法在不同问题实例上的平均表现。4.2 ε-贪心算法的完整实现ε-贪心算法的实现看似简单但有几个容易出错的细节。第一个细节是当有多个动作的估计值相同时如何选择我的实现是随机选择一个而不是固定选择第一个。这个选择会影响早期表现随机选择可以避免系统性地偏向某个动作。第二个细节是ε的概率如何实现最直接的方式是生成一个0到1之间的随机数如果小于ε就随机选择动作否则选择当前估计值最高的动作。这种方式在ε很小时效率不高因为大部分时间都在生成随机数。另一种方式是先用ε的概率决定是否探索如果是探索再从k个动作中均匀随机选择一个。两种方式在数学上等价但后者更直观。class EpsilonGreedy: def __init__(self, k, epsilon0.1, seedNone): self.k k self.epsilon epsilon self.rng np.random.default_rng(seed) self.Q np.zeros(k) self.N np.zeros(k) def select_action(self): if self.rng.random() self.epsilon: return self.rng.integers(self.k) else: max_val np.max(self.Q) candidates np.where(self.Q max_val)[0] return self.rng.choice(candidates) def update(self, action, reward): self.N[action] 1 self.Q[action] (reward - self.Q[action]) / self.N[action]这个实现里update方法用的是增量式更新公式学习率是1/N。这个学习率在平稳环境下是最优的因为它等价于样本平均。但在非平稳环境下这个学习率会随着N增大而趋近于0导致算法无法适应变化。原书在讲非平稳环境时提到了这个问题解决方案是用一个固定的学习率α替换1/N。4.3 实验设计与结果分析为了验证书中的结论我设计了一组对比实验在同一个10臂测试平台上分别运行ε0、ε0.01、ε0.1的ε-贪心算法每个算法运行2000步重复2000次独立实验取平均。评价指标有两个平均奖励和最优动作选择率。实验结果显示ε0的算法在最初约100步内平均奖励上升最快但之后趋于平缓最终平均奖励约为1.0。ε0.01的算法早期上升稍慢但最终平均奖励约为1.3。ε0.1的算法早期上升最慢但最终平均奖励约为1.4。最优动作选择率方面ε0的算法最终约为30%ε0.01约为70%ε0.1约为80%。这个结果与书中的结论一致纯贪心策略虽然短期表现好但长期来看会被次优动作锁死适当的探索ε0.1虽然牺牲了短期奖励但换来了更高的长期奖励。ε0.01的表现介于两者之间说明ε的选择确实是一个权衡。我还做了一个额外的实验把ε0.1的算法运行更长时间10000步观察平均奖励是否继续上升。结果显示平均奖励在约2000步后基本稳定在1.4左右不再显著上升。这说明ε-贪心算法的收敛速度受限于探索概率即使运行更长时间也无法突破这个上限。4.4 UCB算法的实现与调参UCB算法的实现比ε-贪心稍复杂主要难点在于处理 ( N_t(a) 0 ) 的情况。当某个动作从未被选择过时不确定性项的分母为0公式无法直接计算。我的处理方式是在算法开始时先依次选择每个动作一次确保所有动作的N都至少为1。这个初始化步骤在原文中没有明确说明但实际实现时必须考虑。class UCB: def __init__(self, k, c2.0, seedNone): self.k k self.c c self.rng np.random.default_rng(seed) self.Q np.zeros(k) self.N np.zeros(k) self.t 0 self.initialized False def select_action(self): if not self.initialized: action self.t % self.k if self.t self.k - 1: self.initialized True self.t 1 return action self.t 1 ucb_values self.Q self.c * np.sqrt(np.log(self.t) / self.N) max_val np.max(ucb_values) candidates np.where(ucb_values max_val)[0] return self.rng.choice(candidates) def update(self, action, reward): self.N[action] 1 self.Q[action] (reward - self.Q[action]) / self.N[action]参数c的选择对UCB的表现影响很大。我测试了c0.5、c1、c2、c5四种情况结果显示c2时表现最好c0.5时探索不足c5时探索过度。这个结果与书中的建议一致但我也发现c的最优值依赖于奖励的尺度如果奖励的方差更大需要更大的c来补偿不确定性。4.5 梯度算法的实现细节梯度算法的实现与前面几种方法有本质区别它不直接估计动作价值而是维护一个偏好函数H通过softmax将偏好转换为选择概率。更新规则是[ H_{t1}(a) H_t(a) \alpha (R_t - \bar{R}_t)(1 - \pi_t(a)) ]其中 ( \bar{R}_t ) 是平均奖励的估计( \pi_t(a) ) 是选择动作a的概率。这个更新规则的直觉是如果实际奖励高于平均奖励就增加选择该动作的偏好如果低于平均奖励就降低偏好。class GradientBandit: def __init__(self, k, alpha0.1, seedNone): self.k k self.alpha alpha self.rng np.random.default_rng(seed) self.H np.zeros(k) self.avg_reward 0.0 self.t 0 def select_action(self): exp_H np.exp(self.H - np.max(self.H)) self.pi exp_H / np.sum(exp_H) return self.rng.choice(self.k, pself.pi) def update(self, action, reward): self.t 1 self.avg_reward (reward - self.avg_reward) / self.t for a in range(self.k): if a action: self.H[a] self.alpha * (reward - self.avg_reward) * (1 - self.pi[a]) else: self.H[a] - self.alpha * (reward - self.avg_reward) * self.pi[a]这个实现里有一个数值稳定性的技巧在计算softmax时先减去H的最大值避免指数运算溢出。这个技巧在原文中没有提到但在实际实现中很重要尤其是当H的值较大时。梯度算法的表现对学习率α很敏感。我测试了α0.01、α0.1、α0.5三种情况结果显示α0.1时表现最好α0.01时收敛太慢α0.5时波动太大。这个结果与书中的建议一致。5. 常见问题与排查技巧实录5.1 符号混淆真实值与估计值这是初学者最容易犯的错误。在阅读原书时很多人会把 ( q_*(a) ) 和 ( Q_t(a) ) 搞混导致在分析算法表现时得出错误结论。我的建议是在阅读任何一段公式之前先确认符号的含义。如果原文没有明确说明就根据上下文推断并在旁边做笔记。一个具体的例子是原书在讲ε-贪心算法的收敛性时说“Q_t(a)收敛到q_*(a)”。这里的收敛是指估计值收敛到真实值而不是真实值本身在变化。如果混淆了这两个概念就会误以为算法在非平稳环境下也能收敛。5.2 学习率的选择1/N还是固定值原书在讲增量式更新时默认学习率是1/N。这个选择在平稳环境下是最优的因为它等价于样本平均具有最小的估计方差。但在非平稳环境下1/N会随着N增大而趋近于0导致算法无法适应变化。我的经验是如果环境是平稳的用1/N如果环境是非平稳的用固定的α通常取0.01到0.1之间。判断环境是否平稳的一个简单方法是观察奖励的分布是否随时间变化。如果奖励的均值和方差基本稳定就是平稳环境如果奖励的分布有明显变化就是非平稳环境。5.3 探索参数的调参经验ε-贪心的ε、UCB的c、梯度算法的α这三个参数都控制探索的程度。调参时的一个常见误区是试图找到一个“最优”值。实际上最优值依赖于任务的时间跨度和奖励尺度。如果任务时间很短应该用较小的ε更多利用如果任务时间很长应该用较大的ε更多探索。我的经验法则是对于1000步左右的任务ε取0.1左右对于10000步以上的任务ε取0.01左右。UCB的c通常取2但如果奖励的方差很大可以适当增大。梯度算法的α通常取0.1如果奖励的尺度很大可以适当减小。5.4 实验结果不可复现的问题做实验时最常见的问题是结果不可复现。原因通常有两个一是没有固定随机种子二是实验之间的环境没有重置。我的解决方案是在每个实验开始时用固定的种子初始化环境和算法在每个实验结束后记录所有随机状态以便后续复现。另一个容易被忽略的问题是Python的全局随机状态和numpy的随机状态是独立的。如果代码中混用了random和np.random即使设置了种子结果也可能不可复现。我的做法是统一使用np.random.default_rng避免混用。5.5 常见问题速查表问题现象可能原因排查方法解决方案平均奖励一直不上升学习率太小或探索不足检查学习率和ε的值增大学习率或ε平均奖励波动很大学习率太大或探索过度检查学习率和ε的值减小学习率或ε最优动作选择率很低算法锁死在次优动作检查初始估计值和探索机制增大ε或使用乐观初始值结果不可复现随机种子未固定检查随机种子的设置统一使用np.random.default_rngUCB算法报错N_t(a)0导致除零检查是否初始化了所有动作先依次选择每个动作一次5.6 一个容易被忽略的细节奖励的尺度原书在讲梯度算法时提到了平均奖励的估计 ( \bar{R}_t )。这个估计的作用是作为基线减少更新量的方差。但如果奖励的尺度很大比如奖励在1000左右平均奖励的初始值0会导致早期更新量非常大算法表现不稳定。我的解决方案是在算法开始时先用一个较小的学习率运行一段时间让平均奖励的估计逐渐接近真实值然后再切换到正常的学习率。这个技巧在原文中没有提到但在实际应用中很有效。6. 翻译过程中的个人体会翻译这一章花了我大约三周时间其中大部分时间不是在翻译文字而是在验证代码和补充例子。原书的文字很精炼但精炼的代价是很多细节需要读者自己补全。我的翻译目标就是把这些细节补上让读者不需要反复查阅其他资料就能理解。一个让我印象深刻的点是原书在讲乐观初始值方法时只用了一小段话说明它的原理但没有给出具体的初始值设置建议。我在翻译时做了一个实验分别用初始值0、1、5、10运行乐观初始值算法结果显示初始值为5时表现最好初始值为10时早期探索过度初始值为0时退化为贪心策略。这个实验的结果我加在了注释里希望对读者有帮助。另一个体会是多臂老虎机问题虽然简单但它包含了强化学习的核心矛盾。把这个矛盾理解透了后面看更复杂的算法时就能更快地抓住重点。我在翻译时尽量把每个算法背后的直觉讲清楚而不是只给出公式和伪代码。公式是精确的但直觉才是让人真正理解的关键。最后分享一个小技巧如果你在阅读原书时遇到卡住的地方不妨先跳过把后面的代码实现跑一遍再回来看理论。很多时候代码的运行结果会让你对理论有更直观的理解。我在翻译过程中就多次用这种方法解决了理解上的困难。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑