
做推荐系统的同学几乎没有人没为“探索exploration”挠过头。新内容一上线没有点击数据算法不知道用户喜不喜欢可不给曝光永远不知道给了曝光又可能牺牲线上收益。今天聊的Thompson采样是解决这个矛盾的一个非常优雅的贝叶斯策略。它在推荐算法里常被称为汤普森采样核心思想用一句话说就是每个候选物品都维护一个带记忆的概率分布每次推荐前先从所有物品的分布中抽一次签谁抽到的值最大谁就上屏。这套方法不是新东西1933年就有人提出但现在很多推荐系统、广告系统、AB实验平台里依然在用。我这个团队里凡是做冷启动、做内容探索、做流量分配的算法同学我都会让他们先把Thompson采样跑明白因为它简单到十几行代码能写完效果却不输一堆花哨策略。这篇文章我会从原理讲到工程细节再讲几个踩过的坑适合正在做推荐却没系统研究过探索策略的同学也适合刚入门想找个低成本方案练手的读者。1. 为什么推荐系统需要Thompson采样1.1 探索与利用是推荐系统的一对矛盾推荐系统本质上就是一个“多臂老虎机”问题。想象面前有一排老虎机每台机器吐钱的概率不一样你手里的硬币有限怎么分配尝试次数才能赚得最多推荐系统同理面前是一堆候选物品用户对每个物品有真实的点击概率但我们不知道这个概率只能通过一次次的曝光去估计它。矛盾就在这里。如果只推荐目前看起来最好的物品这是“利用”收益稳定但信息永远不够如果拿出一定的流量去试那些没怎么曝光过的物品这是“探索”有希望发现黑马但也可能浪费流量、拉低点击率。传统的经验做法是给探索留一个固定比例比如10%的流量随机分发也就是epsilon-greedy。这个方法简单问题也明显10%的流量在任何场景下都是刚性的哪怕你已经把物品的点击率摸得很清楚了它依然要浪费10%的探索预算而真正有潜力的新物品也不一定能在这10%里得到足够曝光。Thompson采样的聪明之处在于它把“该不该探索”的决策完全交给数据本身。物品的数据越少分布越模糊被探索的概率天然就高物品的数据越多分布越集中被推荐的概率就由它的真实水平决定。这个自适应过程不需要人为设定探索比例所以它在工程上非常省心。1.2 概率匹配Thompson的灵魂Thompson采样有个更学术的叫法叫“概率匹配probability matching”。什么意思呢一个物品被选中的概率应该等于“它是当前最优物品”的后验概率。如果物品A有70%的可能会比物品B表现好那么系统在70%的请求里应该选择A剩下30%请求选择B。听起来像一句废话但大部分策略做不到这一点。用一种形象点的类比。老板要在一批新员工里挑一个人当项目负责人但没有历史数据。Thompson的做法是先给每位员工画一条“能力曲线”老员工因为有业绩曲线很尖新员工没业绩曲线又宽又平。做决策时从每个人的曲线里随机抽一个数谁抽的数字大谁就带这个项目。老员工通常赢但也保不齐某个新人某次抽到很高的数字于是他就有机会被派去试一次。试完了再根据结果更新他的能力曲线。这样既保证了整体效率又给新人留了曝光机会。这种“让随机性去承担探索”的思路跟我们平时在推荐系统里见到的“按置信区间排序”完全不一样。置信区间本质是悲观中带乐观而概率匹配本质是“我不猜你是多少我按你的后验分布随机抽”。实践下来它最大的好处是实现简单不需要推导复杂的置信界后验更新也就是加加减减的事。1.3 这套方法适合什么场景不是所有推荐场景都需要Thompson采样。我个人的判断标准有三个第一反馈信号是“点击/不点击”这类二值信号最贴合它天生的数学模型第二候选集规模在几十到几十万这个量级尤其适合冷启动阶段第三你希望的是“在现有排序模型之上增加探索能力”而不是重新做一套深度学习排序。典型场景包括资讯App冷启动时决定给新文章多少曝光电商平台对没有销量记录的新品做流量试探广告系统中广告计划缺乏转化数据时的预算分配。这些场景的共同特征是收益即时反馈、候选多且有明显的冷启动需求。如果你面对的是长周期转化目标比如“注册后7天内是否付费”那就要在反馈更新环节多做一层处理这一点后面我会专门讲。2. Thompson采样的数学原理2.1 Beta分布一个“带记忆的骰子”Thompson采样的核心工具是Beta分布。很多人在概率论课上学过Beta分布但学完就忘了因为它看起来像一堆不解其意的公式。其实你完全可以把它理解成一个“带记忆的骰子”它有两个形状参数α和β分别代表“成功次数”和“失败次数”。一个骰子如果经历过多次成功它的重心就会往成功方向偏如果经历过多次失败重心就往失败方向偏。骰子本身的取值范围是0到1正好可以用来表示“用户点击这个物品的概率”。Beta分布的期望值有一个非常漂亮的形式α/(αβ)。假设一个物品被曝光了10次点击了3次用初始参数α1、β1去算更新的后验就是Beta(4, 8)期望是4/12≈0.33。这个期望值本质上就是我们在用平滑后的点击率作为“这个物品有多好”的估计。和普通统计里直接用点击率不同Beta分布还会告诉你估计的置信度——曝光次数越多分布越尖曝光次数越少分布越平。这个“置信度”就是Thompson采样做探索时手里捏的那张牌。那为什么用Beta分布而不用正态分布或者别的分布因为Beta分布是伯努利试验的共轭先验。通俗点说“点击/不点击”这个二值结果配上Beta先验算出来的后验仍然是Beta分布更新规则就简化成一个加一操作点击了α加1没点β加1。这种数学上的“闭合性”决定了它写代码时极其方便不用跑马尔可夫链蒙特卡洛也不用算复杂的积分一个采样函数就够了。2.2 采样与更新的完整流程我们把流程拆开实际上就四步虽然朴素但每一步都有它的道理。第一步是初始化。对每个候选物品设定初始后验分布Beta(α0, β0)。最常用的选择是Beta(1,1)它等价于均匀分布意思是我们对这个物品的好坏完全没有任何先验判断。也可以根据业务经验改成别的值比如你觉得新物品整体质量一般可以把先验均值拉低这个我在工程踩坑部分再展开讲。第二步是采样。每次用户发起请求从候选集合里每个物品的Beta分布中采样一个随机数θ。采样这个动作天然地把“不确定性”转化为“探索概率”分布宽、方差大的新物品采样值忽高忽低容易抽到很高的数字从而获得曝光分布尖、方差小的老物品采样值围绕均值波动能不能被选中完全看它真实水平。第三步是决策。选择θ最大的那个物品展示给用户。注意这里不是选择期望最高的物品而是选择“本次采样值最高”的物品这是理解Thompson采样的关键。正因为随机抽样小概率事件仍有可能发生所以低期望物品偶尔也能上位这就完成了探索。第四步是更新。拿到用户反馈后如果点击了就把它对应的α加1如果没有点击就把β加1。下一次请求时它的分布就稍微向右或向左偏移一点采样结果也随之改变。整个闭环没有任何复杂的调参只有“加一”所以工程实现成本极低。2.3 和ε-greedy、UCB摆在一起看要理解Thompson采样在探索策略里的位置最好的办法是拉出来和另外两个常用策略做对比。我把它们画成一个表格你在落地选型时可以直接参考。策略核心思想探索方式需要维护参数实现难度典型问题ε-greedy大多数选择当前最优小部分随机固定比例随机每个物品的点击率估计很低探索比例固定浪费流量随机探索没有方向UCB选“均值置信上界”最大的物品基于置信区间宽度均值、样本数、方差中需要推导置信界对非平稳场景不够稳Thompson从后验分布采样选采样值最大概率自适应每个物品的α和β低需要假设后验为Beta分布无法直接用连续反馈实际跑实验时我最直观的感受是ε-greedy在探索比例定得好的时候效果尚可但定得再好也扛不住数据分布变化。UCB在高方差场景下边界计算容易抖调试成本高。Thompson采样则总是表现得“脾气很好”它对异常反馈的容忍度高收敛也快。如果用一句话总结我的选型经验纯点击率冷启动场景闭眼选Thompson有上下文特征、需要建模用户个性化时再用特征化变体。3. 手把手实现一个Thompson推荐模块3.1 先搭一个离线模拟实验在动线上流量之前我强烈建议先在离线环境跑一遍模拟。这件事花不了多少时间但能帮你想清楚策略的收敛行为。这里我们构造一个最简单的场景假设系统里有10个物品它们的真实点击率分别是0.1到0.55之间线性分布。这个真实点击率是“上帝视角”我们的算法看不到它只能通过用户行为去猜。用户行为用伯努利模拟调用一次随机数小于真实点击率就算用户点了否则就是没点。我每次带新人做这个实验的时候都会强调一件事别急着上你的业务数据先把这个合成环境跑通。因为它可以直接观察“策略能不能找到最优物品”“找到之后会不会稳定下来”“探索过程会不会反复无常”。这些问题在真实系统里会被各种噪声掩盖离线环境反而看得清清楚楚。3.2 核心代码实现下面给出一段完整可运行的Python代码。它只有三个对象一个Thompson采样器、一组真实点击率、一个模拟主循环。import numpy as np class ThompsonSampler: def __init__(self, n_items, alpha_01.0, beta_01.0): self.alpha np.ones(n_items) * alpha_0 self.beta np.ones(n_items) * beta_0 def recommend(self): samples np.random.beta(self.alpha, self.beta) return int(np.argmax(samples)) def update(self, item_id, reward): self.alpha[item_id] reward self.beta[item_id] 1 - reward然后是主循环np.random.seed(42) true_ctr np.array([0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55]) n_items len(true_ctr) n_rounds 5000 ts ThompsonSampler(n_items) total_clicks 0 total_regret 0 best_ctr true_ctr.max() show_count np.zeros(n_items) for t in range(n_rounds): item ts.recommend() reward 1 if np.random.rand() true_ctr[item] else 0 ts.update(item, reward) total_clicks reward total_regret best_ctr - true_ctr[item] show_count[item] 1 print(累计点击量:, total_clicks) print(平均点击率:, total_clicks / n_rounds) print(理论最优点击量:, n_rounds * best_ctr) print(累计遗憾:, total_regret) print(各物品曝光次数:, show_count.astype(int))这段代码里有一个细节值得注意update函数里把reward和1 - reward直接加到α和β上。当reward是1时α加1、β不变当reward是0时α不变、β加1。这个写法紧凑但新手容易看懵。展开写其实就是点击了alpha[item] 1没点beta[item] 1。3.3 如何解读实验结果跑完这段模拟你会看到几个很有意思的现象。第一平均点击率会稳定在0.5上下而不是接近0.55。原因在于策略前几十轮或几百轮需要摸索最优物品虽然大概率被选中但不是每次都选它探索过程必然拉低一点均值。第二曝光次数分布不是均匀的点击率高的物品会被反复选中新物品或低点击率物品则很少出现这正是“利用”的体现。第三累计遗憾在前期涨得快、后期涨得慢因为前期未知多经常会选到差物品后期分布集中选择基本逼近最优。我自己一般还会加一个尾随统计指标“最优物品占据的曝光比例”。如果这个比例在5000轮后超过80%说明策略对好物品的识别是有效的如果长期低于50%那要考虑是不是先验设得太宽或者真实点击率差异太小。这个比例比累计遗憾更直观也和线上“优质内容占据主流量”的业务指标直接挂钩。4. 工程落地会踩的坑4.1 高频踩坑速查表离线实验跑得挺好一到线上就各种问题这是推荐算法同学的日常。我把这几年在生产环境里积累的坑整理成一张速查表基本覆盖了Thompson采样落地时最容易被咬一口的地方。现象原因解决办法新物品死活没有曝光后验分布很快变得很尖采样值几乎稳定压低初始α、β或者给采样结果加随机扰动曝光量很高但点击率持续上不去曝光不等于负反馈更新逻辑错了明确归因窗口避免重复计为负样本物品池几十万每次请求采样太慢每次请求对所有物品做Beta采样复杂度过高改离线预采样缓存或先粗排再做Thompson高频物品参数过大后几乎不变α、β累计到几万后验变成确定性加时间衰减定期对齐初始先验线上点击率提升但不涨GMV只用点击做奖励和目标不一致换成点击后转化目标或多目标加权这张表我建议贴在显示器边上。它不是常规文档里的概念而是从真实的线上事故里一条条抠出来的。下面我挑几个重点展开。4.2 冷启动先验怎么设很多同学一开始都用Beta(1,1)这里头其实有个陷阱。Beta(1,1)是均匀分布意味着新物品的采样值从0到1都有可能期望是0.5。但在大多数推荐场景里新物品的真实点击率可能不到2%。你用Beta(1,1)做先验相当于给新物品发了一张“期望点击率50%”的入场券探索量太大会白扔不少流量。我的习惯是先用一段历史日志统计新物品在新客上的平均点击率比如0.02然后反推一个比较合理的Beta参数。因为Beta分布的期望是α/(αβ)如果想让期望是0.02同时又有一定的探索宽度可以用α1、β49。这样初始采样值集中在2%附近偶尔也会冒出高值既不会过度探索也不会完全不给机会。如果你发现业务上新物品普遍表现不错可以把α调成2、β调成98让分布更集中减少无谓试探。这个参数看起来事小但直接决定你单位流量的变现效率。4.3 延迟反馈怎么算Thompson采样天生假设“曝光后马上知道点击与否”但真实业务里点击往往会延迟几分钟甚至几天转化就更晚。这个延迟反馈问题如果不处理会把算法带偏。比如一个物品在下午3点被曝光用户下午4点才点击如果你在3点整就把它记成“未点击”β就会错误地加1物品优点被埋没流量就探不出来了。我采用的方案分两层。第一层是设置归因窗口比如曝光后5分钟内有统一点击事件到达就算作这次曝光的正样本超过窗口仍未收到点击才把它算成负样本。这里窗口长度要按业务属性调资讯类可以短一点电商类因为有浏览加购的过程窗口适当拉长。第二层是收到延迟点击之后再修正把之前误记的负样本撤销正样本真正地加回去。虽然实现多一层状态管理但线上效果非常显著尤其对长决策链路业务。4.4 物品池很大怎么办Thompson采样的复杂度是O(n)n是候选物品数。如果n只有1000完全没问题但真实推荐池动辄上百万每次请求对每个物品跑一次Beta采样延迟和成本根本扛不住。我实践下来比较稳的方案是“离线预采样在线查表”。离线每隔5分钟或10分钟用当前最新的α、β参数对每个物品做几百次采样把采样值的分位数缓存到Redis或本地内存缓存里。在线请求过来时直接读取缓存中的中位数或者随机选取一条采样结果再结合业务规则产出最终物品。这样做把计算压力转到了离线侧在线只做一次查表复杂度立刻降下来。如果不想引入太多新组件跑一个稀疏化版本也可以只对最近7天曝光过或新入库的物品做在线采样其余历史老物品保持上一个周期算好的值。4.5 业务约束与预算约束纯粹的Thompson采样只管点击率它不知道什么叫“品牌广告主必须获得保量曝光”也不知道什么叫“每个用户每天最多看到某类内容3次”。这些业务约束一定要落到采样决策之外。我常用的做法是分层控制先由业务规则决定哪些物品属于保量池保量池内的物品按协议流量直接注入剩余流量再交给Thompson采样采样出来的结果最后还要过一次去重和频控保证用户体验。对预算型场景比如广告计划每日预算有限我需要在Thompson的采样顺序上做一个小改动每天初始化时将每条计划的剩余预算归一化成权重采样值乘以权重后再比较。这样接近预算消耗完的计划采样值会快速下降系统自动把流量分配给还有预算的计划。这个改动只有一行乘法但带来的预算消耗平滑度提升很明显。5. 进阶从Beta分布走向特征化5.1 为什么纯Beta不够用纯Beta分布版的Thompson采样有个隐含前提各个物品之间是独立的物品对所有用户的吸引力是同一个未知常数。但在真实推荐系统中物品和用户之间有复杂交互。一件羽绒服在北方的冬天点击率很高在广州的夏天几乎没人点同一个物品对老用户和新用户的吸引力也完全不一样。Beta分布没有办法表达这种“用户特征带来的差异”。所以当你有用户画像、场景特征、物品属性时就需要把Thompson采样从“估计一个概率”升级成“估计一个概率函数”。常见做法是把奖励建模成逻辑回归参数先验设成正态分布每次推荐时对参数做后验采样再用采样出来的参数对每个候选计算点击概率。这类方法在文献里叫线性Thompson采样或贝叶斯逻辑回归。工程上会用变分推断等手段近似后验实现难度比Beta版高一个量级但在个性化推荐场景里收益也大一个量级。5.2 Thompson采样在推荐链路里的真实位置可能有人会问现在已经有大模型、深排序了一个1933年的算法还有位置吗我的回答是位置不仅还在而且挺关键。推荐链路通常是召回、粗排、精排、重排精排模型负责把几十个候选按预估分排序但精排模型的目标函数通常是最小化预测误差它天然偏向“利用”不负责“探索”。如果没有一个单独的探索机制新内容很快会在冷启动阶段被精排模型压死系统陷入“越新越没数据越没数据越不推荐”的死循环。Thompson采样在链路里最常见的放法有两个。一是加在召回层让它负责从全网新内容里捞出值得二次排序的候选解决纯召回对长尾和新鲜内容不友好的问题。二是加在重排层之后作为“随机探索分支”对已排序列表里的第5到第10位做小概率的Thompson替换。这两个位置都有个共同特点它们不影响主排序模型的稳定性但会持续往系统里注入新鲜样本。5.3 值得关注的两个改进方向如果看完上面的内容你想再进一步研究我给你指两个方向。第一个是神经网络版Thompson采样业内也叫Neural Thompson Sampling核心思路是让一个神经网络去建模奖励函数并对网络的输出分布做后验采样。它解决了线性模型表达能力不足的问题也能用于图像、文本等复杂特征的推荐场景但训练和推理成本都更高。第二个是带约束的多目标Thompson采样。真实业务经常同时关心点击率、转化率、停留时长和商家生态你可以给每个目标配一个Beta分布采样时用加权组合代替单一采样值这样就不会为了一个指标牺牲另一个指标。这两个方向我目前都只在线下实验里验证过一部分还没有完全替代经典版本。但可以确定的是Thompson采样这套“用后验不确定性驱动探索”的思想不会过时。哪怕有一天算法框架变了只要你还需要面对未知和不确定性这套思想都会以某种形式继续存在。最后分享一点我自己的经验。我从最开始觉得Thompson采样太简单、没啥好学的到后来被线上冷启动折磨得回头研究它前后折腾了好几个月。现在我的建议是不要一上来就搞那些复杂的贝叶斯模型先用最朴素的Beta分布版本把离线和线上链路跑通亲手感受一下概率匹配带来的平滑探索再根据业务痛点往特征化方向迭代。工具再好也得先看得懂结果用得上手。