多臂赌机问题实战:MATLAB程序包从策略原理到调参避坑全解析 简介针对多臂赌机问题的MATLAB强化学习程序包面向机器学习与强化学习入门者以及需要动手验证经典探索-利用策略的算法爱好者。程序包围绕三种常用策略展开e-greedy、softmax以及时变epsilon的e-greedy配合老虎机环境模拟脚本便于在不同参数下观察收益收敛行为。压缩包内共9个m文件体积仅4KB代码精简、结构清晰包含核心策略函数、辅助寻优函数与仿真主脚本适合直接运行或在此基础上二次改造。已有2997人学习下载热度较高足见该主题受关注程度。通过本资源读者可快速搭建多臂赌机实验环境对比固定探索率与衰减探索率、概率型策略之间的差异理解强化学习中“探索与利用”的权衡思想也可作为课程设计或论文复现的参考基础能够有效帮助学习者巩固强化学习基础。 刚从机场出来就碰到这个题目还挺有感触的。做强化学习的人十个里有八个拿多臂赌机Multi-Armed BanditMAB当入门第一课。但网上能找到的MATLAB实现要么是教学用的玩具代码要么就是一堆英文函数拼接拿过来根本跑不通。这次把我自己整理的一套程序包从头到尾拆开讲一遍从概率模型到几大经典策略的MATLAB实现再到调参和避坑一次性说清楚。这个程序包能做什么简单说就是你在MATLAB里定义好K台老虎机每个臂有自己的中奖概率分布然后用epsilon-greedy、UCB、Thompson Sampling等策略去玩程序会自动统计累计收益、最优臂选择率画出收敛曲线。对做科研、做课程作业、或者想快速验证某个场景下该选哪种算法的人来说这套代码可以直接当基线用。1. 问题模型与整体设计思路1.1 多臂赌机问题的数学模型先把问题抽象出来。假设你面前有K台老虎机第i台机器的期望收益是μ_i。每玩一次你可以选择一台机器得到一个随机收益r这个r通常服从伯努利分布中奖1没中0或者正态分布。目标是最大化T轮游戏的总收益。从数学上讲这个问题有个关键指标叫遗憾值Regret[ R_T T \cdot \mu^* - \sum_{t1}^{T} \mu_{a_t} ]其中μ^*是最优臂的期望收益a_t是第t轮选择的臂。遗憾值越低说明你的策略越接近“每次都选最优臂”的理想状态。这个公式我一定要强调一下很多初学者只看累计收益忽略遗憾值。但累计收益会受具体奖励分布影响而遗憾值直接反映算法本身的“后悔程度”在不同场景下更可比较。1.2 为什么选MATLAB而不是Python现在强化学习的主流实现都在Python生态但MATLAB在学术场景里依然有不可替代的优势第一矩阵运算和可视化天然一体化画收敛曲线不需要额外引包第二如果你的课题还要做系统辨识、PID控制或者跟Simulink联动MATLAB显然是更省事的平台第三MATLAB的代码阅读门槛低对非计算机专业的学生很友好。当然MATLAB做RL也有明显的短板——循环效率低跑大规模仿真会很痛苦。所以我在设计程序包时尽量把能向量化的操作都向量化了K臂数量控制在10以内、轮次在5000轮以内时运行时间完全可以接受。1.3 程序包的整体架构这套程序包遵循一个非常朴素的分层结构mab_bandit/ ├── environments/ # 环境定义伯努利、高斯、随机游走 ├── agents/ # 策略算法epsilon-greedy、UCB、Thompson等 ├── experiments/ # 主脚本和批量实验配置 ├── utils/ # 可视化、指标计算、结果导出 └── config/ # 参数配置文件环境层负责生成奖励策略层负责决策实验层负责把两者组合起来跑工具层负责记录数据。这样设计的好处是新增一个策略只需在agents目录加一个文件不用动其他代码。2. 核心策略的原理与MATLAB实现要点2.1 epsilon-greedy最直觉的方法epsilon-greedy的核心思想是以ε的概率随机探索以1-ε的概率选择当前平均收益最高的臂。这个策略简单到让人怀疑它到底能不能用——但它确实有效。MATLAB里的实现核心在于平均收益的增量更新这一点我见过太多人写错。不要每次把所有历史数据重新求均值用如下递推式function [Q, count] updateAverage(Q, count, idx, reward) count(idx) count(idx) 1; Q(idx) Q(idx) (reward - Q(idx)) / count(idx); end这里面还藏着一个数学细节为什么不是Q(idx) (Q(idx) * (count(idx)-1) reward) / count(idx)两种写法数学上等价但迭代式在数值上更稳定避免了大数相乘再除回来的精度损失。实现上会让epsilon随时间衰减从0.3降到0.05因为前期需要更多探索来估计各个臂的收益后期则应更多利用。2.2 UCB1用置信区间消除不确定性UCBUpper Confidence Bound系列算法是在“利用”和“探索”之间做更系统化权衡的方式。它的选择准则是对每个臂计算一个上置信界UCB1(t) Q(a) sqrt(2 * log(t) / N(a))直观理解就是如果某个臂被尝试的次数N(a)很少它的置信区间上限就很高算法会倾向于去多试它几次。随着实验次数增加对数项增长放缓最终算法会收敛到最优臂。UCB1的实现需要留意的几个变量是全局时间步t、各臂被选中次数N(a)以及各臂平均收益Q(a)。有个容易踩的坑是初始阶段每个臂至少要选一次否则N(a)0会导致除零错误。我会在初始化时让每个臂先各玩一次。2.3 Thompson Sampling贝叶斯视角下的优雅解法Thompson Sampling是贝叶斯学派的核心算法。它假设每个臂的收益参数服从某个先验分布每轮根据当前后验分布采样一个参数然后选择采样值最大的臂。以伯努利收益为例假设第i个臂的中奖率p_i的先验分布为Beta(1,1)即均匀分布每轮之后根据中奖与否更新为Beta(α_i成功次数, β_i失败次数)。MATLAB里生成Beta分布随机数直接用betarnd函数theta betarnd(alpha(idx), beta(idx));这种方式比前面两者都更“自然”——不需要手动调ε也不需要设计置信区间公式。每个臂被探索的频率自动匹配其不确定性不确定性越大越容易被尝试。2.4 三种策略的对比速查表策略核心思想参数适用场景计算开销epsilon-greedy以概率ε随机探索ε, 衰减率收益方差小、臂数少极低UCB1置信区间上界无臂数适中、静态环境低Thompson Sampling贝叶斯后验采样先验参数奖励非平稳、臂数多中选择策略没有绝对的优劣关键看场景。如果追求“开箱即用”UCB1是第一选择如果环境是动态变化的Thompson Sampling适应性更强如果对计算资源极度敏感epsilon-greedy最靠谱。3. 实操过程从零搭建完整程序包3.1 环境定义构建可配置的赌机先把环境层的代码说清楚。一个标准的伯努利赌机环境包括臂的真实中奖概率、轮次上限、奖励生成逻辑还要能记录每一步的真实最优臂——这个稍后是评估策略的核心指标。classdef BernoulliBandit handle % 多臂老虎机环境伯努利奖励 properties K % 臂的数量 probs % 每个臂的真实中奖概率 bestArm % 最优臂索引 bestProb % 最优概率 end methods function obj BernoulliBandit(k, probs) if nargin 2 probs 0.3 0.5 * rand(1, k); end obj.K k; obj.probs probs; [obj.bestProb, obj.bestArm] max(probs); end function r getReward(obj, action) r rand() obj.probs(action); end end end3.2 策略层实现三个核心Agent策略层的基类我统一命名为Agent所有策略都继承这个基类并重写act和update两个方法。act负责决策update负责在拿到奖励后更新内部状态。这里给出UCB1的完整实现classdef UCB1Agent handle properties K Q % 各臂平均收益 counts % 各臂被选次数 t % 时间步 end methods function obj UCB1Agent(k) obj.K k; obj.Q zeros(1, k); obj.counts zeros(1, k); obj.t 0; end function action act(obj) obj.t obj.t 1; % 每个臂至少尝试一次 unexplored find(obj.counts 0); if ~isempty(unexplored) action unexplored(randi(length(unexplored))); return; end ucb obj.Q sqrt(2 * log(obj.t) ./ obj.counts); [~, action] max(ucb); end function update(obj, action, reward) obj.counts(action) obj.counts(action) 1; obj.Q(action) obj.Q(action) ... (reward - obj.Q(action)) / obj.counts(action); end end end注意在exploration阶段建议随机选择一个未探索的臂而不是按索引顺序。这样可以避免系统性地偏向编号靠前的臂虽然影响不大但也是一种严谨性的体现。3.3 实验主脚本跑通全流程主脚本的核心职责是创建环境、实例化策略、循环交互、记录数据、最后可视化。% config numArms 5; numRounds 2000; probs [0.2, 0.5, 0.1, 0.4, 0.3]; env BernoulliBandit(numArms, probs); agent UCB1Agent(numArms); % 记录 R_history zeros(1, numRounds); A_history zeros(1, numRounds); regret zeros(1, numRounds); for t 1:numRounds action agent.act(); reward env.getReward(action); agent.update(action, reward); R_history(t) reward; A_history(t) (action env.bestArm); regret(t) t * env.bestProb - sum(R_history(1:t)); end % visualization figure(Color, w); subplot(2,1,1); plot(cumsum(R_history), LineWidth, 1.5); title(Cumulative Reward); subplot(2,1,2); plot(regret, LineWidth, 1.5); title(Cumulative Regret);运行完之后你会看到累计收益曲线是一个上升的斜坡斜率会逐渐变大而遗憾值的增长率也在逐渐下降——这些都是策略在“学习”的信号。3.4 多策略一键对比实验单跑一个策略意义有限我做了一个批量对比脚本让epsilon-greedy、UCB1和Thompson Sampling在完全相同的环境配置下跑每次实验执行多次比如10次取平均减少随机性影响。这就是Monte Carlo平均的思想。具体做法是把环境构建和策略实例化放进一个循环每次记录累计遗憾最后对不同实验取均值并画标准差带。有个小技巧MATLAB的rng设置一定要放在实验开始的循环之前否则“对照组”用的随机数序列不一致对比就没有意义了。比如rng(42)固定随机种子再来跑所有策略。3.5 关键参数调优心得经过大量实验我总结出几个调整要点第一epsilon-greedy的衰减。初始epsilon0.3时探索多收敛慢初始0.1收敛快但要承受选错臂的风险。推荐的衰减方式是epsilon max(0.05, epsilon * 0.999)指数衰减。0.05是保底探索率防止环境变化时彻底失去探索能力。第二UCB1的常数项。标准UCB1是sqrt(2 * log(t) / N)实际中可以把“2”改成1或0.5。调小常数意味着更激进地利用当前最优调大则是更保守。如果奖励方差较大建议常数项调大一些。第三Thompson Sampling的先验。Beta先验预置为(1,1)是均匀分布代表了“完全未知”的信任状态。如果你对某个臂有先验知识比如知道它大概有0.7的中奖率可以把先验设为Beta(7,3)这样一开始就会偏向该臂。4. 常见问题与调试技巧实录4.1 结果抖动很大哪一次实验都跟哪一次不一样这太正常了。多臂赌机每次运行都是一次随机过程如果不做多次实验取平均曲线奇形怪状是必然现象。解决思路是要么增加重复次数比如30次再画均值±标准差要么固定随机种子让结果可复现。做实验展示时两者都需要。4.2 所有策略跑了5000轮表现都差不多出现这种情况大概率是臂之间的期望收益差异太小。比如五台老虎机的真实概率分别是0.24、0.25、0.23、0.26、0.25这种环境下最优臂和次优臂几乎无差遗憾值天然很小各种算法的差距也小。可以设置区分度高的概率组合比如0.3~0.9之间间隔拉开这样不同策略在前期探索阶段的差异会非常明显。4.3 为什么epsilon-greedy前期反而比UCB好因为epsilon-greedy在前期有固定比例的时间在随机探索会以一定概率选中较优的臂而UCB前期主要花时间在系统性地排除每个臂上探索阶段收益偏低。但从中后期开始UCB的优势会逐渐显现。如果只看前1000轮epsilon-greedy胜出并不奇怪——这也是我建议看“长程表现”的原因。4.4 “程序包”里的函数出现维数不匹配错误这个问题很常见根源通常在于更新状态向量时索引不对。比如counts(idx)和Q(idx)长度不一致或者传入的action是逻辑值而非整数。我的调试习惯是在每个策略的update方法入口处加一句断言assert(action 1 action obj.K, action out of range);这个方法能快速定位问题。再一个常见坑是classdef函数同名冲突文件名和class名不一致导致调用失败检查一下大小写就好。4.5 MATLAB有没有完全对口的官方工具箱严格来说MATLAB没有专门为MAB问题封装的工具箱。但如果你有Reinforcement Learning Toolbox里面的rlQAgent、rlSARSAAgent适合更复杂的Q-learning类问题单纯做MAB实验自己写这几个策略代码量不大也不容易出错。如果确实不想写File Exchange上有一些第三方包但代码风格和数据结构不一定能直接匹配你的场景反而不如自己封装这3个策略文件省心。4.6 程序包如何扩展到更复杂的强化学习问题多臂赌机是“单状态”的强化学习问题没有环境状态迁移。扩展到完整MDP问题时最核心的变化在于Q值不再是向量而是多维表格更新从即时均值变成时序差分也就是Q(s,a) Q(s,a) alpha * (r gamma * max Q(s, a) - Q(s,a))。好消息是当前程序包的分层结构不需要大改只需把环境层增加state属性策略层更新逻辑替换成Q-learning等即可。5. 从多臂赌机到真实场景的迁移思考多臂赌机虽然看起来像个“玩具”但实际应用极其广泛广告推荐系统里选择最优投放策略、临床试验中快速找到最佳治疗方案、自适应A/B测试、甚至游戏AI里的动作选择。当你在MATLAB里调通这套代码后脑子里要建立的是“探索-利用”这个框架而不只是几行算法。我在实际做项目时发现一个特别实用的场景是把它用到PID参数自整定上——把一组PID参数当成一个“臂”用MAB策略在线挑选参数组合。这个思路比遍历网格快得多尤其在参数空间大且目标函数有噪声的情况下。程序包里的策略层可以直接复用只需替换环境和奖励定义。再说一个不是所有人都知道的细节MAB问题在非平稳环境比如广告点击率随时间变化下UCB1和epsilon-greedy的表现会明显下降而带遗忘因子的Thompson Sampling依然能持续跟踪最优臂。如果你打算把MAB用于真实业务场景一定要考虑非平稳性。每次我在MATLAB里跑完一套对比实验都会习惯性地检查一下每个策略选择最优臂的比例曲线——这个指标比累计收益更能直观反映算法是否真正学会了。在程序包里我额外加了一个“最优臂选择率”绘图功能横轴是轮数纵轴是最近50轮中选择最优臂的百分比看这种图比看累计收益曲线更敏感。5.1 一个不算技巧的技巧代码里多用struct做配置程序包用了一段时间后你会发现策略参数多起来后直接在构造函数里传参很混乱。我后来统一改成传一个config结构体cfg struct(); cfg.eps 0.3; cfg.decay 0.999; cfg.minEps 0.05; agent EpsilonGreedyAgent(numArms, cfg);这样新增参数不需要改动函数签名实验时调参也直观很多。虽然是小改动但对后续维护帮助很大。5.2 关于运行效率的实测记录我拿五臂伯努利环境、2000轮、每种策略重复10次完整跑在普通笔记本上用MATLAB R2023b版本全程跑完不到30秒。如果推向10臂、10000轮运行时间会明显上升主要瓶颈是MATLAB的循环嵌套。我的建议是如果实验规模超过万级别可以考虑把内层循环函数化并用arrayfun或并行计算替代不过这个话题太深以后有精力再单独写。我个人的习惯是复制程序包之前先想清楚“我最终要画什么图、加什么策略”而不是桌面上十几份m文件堆着。一次性设计好目录和接口后面做实验的时间能省一半。这个程序包后面我计划再加两个策略EXP3针对对抗性环境和SW-UCB滑动窗口UCB针对非平稳环境然后给Thompson Sampling加一个支持高斯奖励的版本。如果你们在跑的过程中遇到奇怪的报错或者对代码结构有更好的想法欢迎来交流。本文还有配套的精品资源点击获取