ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Thompson采样在推荐系统冷启动与探索利用中的应用实战

Thompson采样在推荐系统冷启动与探索利用中的应用实战 做推荐的同学估计都听过Thompson采样这个名字。它不是什么新东西1917年就被提出来了但这些年做推荐、广告、增长的过程中我越来越觉得它是个被低估的算法。尤其是做内容冷启动、新物料探索这类场景它比很多花里胡哨的方法都稳代码量也小得感人。这篇文章不聊复杂的数学推导就聊聊我在实际推荐项目里怎么理解Thompson采样、怎么用它解决线上问题以及踩过哪些坑。如果你是刚接触推荐算法或者正在选型探索策略这篇文章应该能帮你省下不少调研时间。1. Thompson采样到底在解决什么问题1.1 多臂老虎机与EE困境先聊一个经典问题假设你面前有十台老虎机每台的中奖概率不同但你不知道哪个更高。你手里的预算有限怎么才能在花最少钱的前提下找到中奖率最高那台并且尽可能多地赢钱这个问题在多臂老虎机Multi-Armed BanditMAB里有个专门的叫法exploration和exploitation的权衡。exploitation是抓住当前看起来最好的机会拼命赚exploration是花成本去试那些可能更好的机会。两件事天然冲突——一直赚可能错过真正的黑马一直试又浪费了大量本该收割的展示。推荐系统里几乎天天碰到这个问题。新上架的文章、新注册的作者、新创建的广告创意这些物品没有任何历史行为数据。如果策略端只看历史点击率它们永远拿不到曝光永远没有机会变成爆款。但如果你无脑均匀分发又会牺牲整体的点击率指标做得太激进老板那边交不了差。Thompson采样就是用来做这个平衡的。它的做法极其简单给每个物品维护一个收益分布每次决策时从这个分布里抽一个随机数谁的随机数大谁就上。听起来像儿戏但背后有非常扎实的贝叶斯理论支撑。1.2 点击率优化场景下的建模思路在推荐场景里我们把问题抽象成每次请求过来要从候选池里挑一个物品展示给用户用户反馈要么是点击记为1要么是没点击记为0。每个物品的点击行为可以看作一轮独立的伯努利试验物品本身有一个真实的点击概率p而我们不知道p是多少只能通过一次次观察去估计它。Thompson采样的核心思路是不要只用一个点估计值比如历史点击率点击数/展示数来决定展示什么而是把每个物品的点击率用一整个概率分布来表示。展示少的物品分布宽说明不确定性大需要多探索展示多的物品分布窄点击率估计比较准基本可以放心利用。具体实现上假设每个物品的点击率服从Beta分布先验参数是Beta(1,1)。每得到一次观察结果就更新这个分布。每个请求到来时从每个物品的后验Beta分布里各抽一个样本选样本值最大的那个物品展示。这样设计的好处是分布宽且有潜力的物品有机会抽到很大的值所以偶尔能得到展示机会而分布已经很窄的高点击率物品抽出来的值稳定在高位会持续被选中。2. 从Beta分布到采样决策核心原理拆解2.1 Beta分布为什么是天然选择Beta分布是定义在[0,1]区间上的连续概率分布非常适合描述概率值本身。它的概率密度函数长这样f(x; a, b) x^(a-1) * (1-x)^(b-1) / B(a, b)其中B(a, b)是Beta函数起归一化作用。a和b是两个形状参数决定了分布的长相。a和b都等于1时Beta(1,1)退化为[0,1]上的均匀分布相当于没有任何先验知识所有点击率水平等可能。Beta分布有个特别好的性质叫共轭先验如果先验是Beta(a,b)观察到一次伯努利试验的结果后后验仍然是Beta分布只是参数变成了Beta(a1,b)或Beta(a,b1)。也就是说你不需要重新拟合任何参数只靠简单的加减法就能完成在线更新。具体到推荐场景每次物品被展示了但没被点击就把b加1被点击了就把a加1。所以一段时间后某个物品的Beta参数就是Beta(点击数1, 展示数-点击数1)。后验均值是a/(ab)也就是我们熟悉的平滑点击率估计。这个过程的巧妙之处在于它天然带有不确定性度量展示次数少时分布非常宽哪怕均值不大也可能抽出很大的值展示次数多了分布变尖采样结果趋近于真实的点击率。2.2 采样-选择-反馈-更新的完整闭环完整跑一轮Thompson采样是这个流程初始化每个物品都对应一个Beta分布参数往往是Beta(1,1)代表“什么都不了解”。采样对每个物品从它的当前Beta分布里抽一个样本。选择挑样本值最大的物品进行展示。反馈收集用户是否点击的结果。更新如果点击了该物品的a加1没点击b加1。然后进入下一轮。整个过程不需要任何全局优化器也不用算复杂的梯度每个物品的参数是独立的。这个特性在分布式场景下特别友好不同物品可以分到不同机器维护自己的计数简单做个合并就能用。2.3 一个数值例子辅助理解假设目前只有两个候选物品A和B。物品A已经展示过1000次点击100次所以A的后验是Beta(101, 901)均值约0.10。物品B是刚加入的新品展示过1次点击1次所以B的后验是Beta(2, 1)均值约0.67。如果按点估计来选B的均值0.67远高于A的0.10看起来应该一直推B。但B只有1次展示这个0.67完全不可信。Thompson采样的做法是从A的Beta(101,901)里抽一个样本大概率在0.08到0.12之间从B的Beta(2,1)里抽一个样本分布很宽经常能抽出0.2、0.5甚至0.9。所以B虽然不一定每次都比A大但会经常获得展示机会。随着B的展示数据增加它的分布会逐渐变窄最终如果真实点击率只有0.05那么它的分布会慢慢移到低位A会重新夺回主导权。这个过程完全自动不需要人工去判断“探索够了没有”。3. 用代码实现一个可用的Thompson采样3.1 基础版实现动手实现一个基础版本很简单核心代码也就几十行。我用Python写了个能直接跑的例子假设候选池有5个物品使用Beta(1,1)作为初始先验。import numpy as np class BetaBandit: def __init__(self, n_arms, alpha1.0, beta1.0): n_arms: 候选物品数量 alpha, beta: Beta分布初始参数 self.n_arms n_arms self.alpha np.full(n_arms, alpha) self.beta np.full(n_arms, beta) def choose(self): 从每个物品的Beta分布中采样返回采样值最大的物品下标 samples np.random.beta(self.alpha, self.beta) return int(np.argmax(samples)) def update(self, arm_idx, reward): 根据反馈更新对应物品的Beta参数 arm_idx: 被展示的物品下标 reward: 用户反馈点击为1未点击为0 if reward 1: self.alpha[arm_idx] 1 else: self.beta[arm_idx] 1 # 模拟5个物品真实点击率分别为0.05, 0.10, 0.15, 0.20, 0.30 true_p np.array([0.05, 0.10, 0.15, 0.20, 0.30]) n_rounds 20000 bandit BetaBandit(n_arms5) total_reward 0 for i in range(n_rounds): arm bandit.choose() reward 1 if np.random.rand() true_p[arm] else 0 bandit.update(arm, reward) total_reward reward print(总收益:, total_reward) print(每个物品的Beta参数:, bandit.alpha, bandit.beta) print(每个物品的后验均值:, bandit.alpha / (bandit.alpha bandit.beta))跑完这段代码你会看到后验均值会逐渐逼近真实的点击率而且高点击率的物品拿到的展示次数会远多于低点击率的物品。注意numpy的np.random.beta里传的alpha和beta必须是正数这正好呼应了初始先验从Beta(1,1)开始的设定。3.2 参数选择和初始化问题虽然Beta(1,1)是最常用的先验但实际项目中完全可以更精细地设置。比如你知道这个位置的平均点击率在0.08左右那可以用Beta(2,23)之类参数来初始化让分布一开始就集中在0.08附近而不是完全均匀分布。这里有个关键点先验的强度会影响探索速度。如果初始a和b都很大比如Beta(80,920)那么前期的采样结果会比较稳定探索幅度很小反过来如果把a和b都设成接近0采样出来的值就会非常极端可能在0.001和0.999之间横跳导致物品一会儿被疯狂推荐一会儿被彻底冷落。我在实际项目中习惯用Beta(1,1)起步然后跑一轮离线仿真看看探索节奏是否符合预期。如果策略偏保守再适当加大参数值。还有一个容易踩的坑不要在线上用Beta(0,0)或负参数。有些同学觉得“没有任何观察记录就用0嘛”这是不对的。Beta分布要求a和b严格为正否则采样时会出现0或1的边界值导致argmax结果不可控。3.3 给线上推送增加一点平滑和约束纯bandit版本在真实推荐系统里往往是跑不通的因为它只看点击率这一个目标会忽略很多业务约束。比如用户已经看过同一个物品很多次了你还一直推用户会烦或者有些物品点击率高但内容违规也不能推。所以工程落地时Thompson采样通常只作为一个探索信号和主排序分数融合使用。我在项目里常用的做法是给每个候选物品算一个融合分score w1 * 主模型预估ctr w2 * thompson_sample其中thompson_sample就是从该物品的Beta后验分布里抽出来的样本。w1和w2用于控制探索力度。当w2较大时物品更容易因为探索信号而上位w2较小时策略偏向保守利用。另一种做法是分层大部分流量走主排序逻辑小部分流量专门用Thompson采样探索。比如95%的请求走主模型5%的请求走bandit策略。两者并行互不干扰。这个方案的好处是风险可控即使bandit策略出一两个奇怪的物品也不会影响大盘指标。坏处是探索效率会低一些因为探索流量本身也贡献了转化。还可以结合业务规则做约束设置最小展示次数门槛展示次数低于该门槛的物品额外乘一个boosting系数或者使用滑动窗口只统计最近7天或14天的数据让点击率估计更快适应时效性强的内容。如果是电商场景还要考虑库存、毛利、复购率等因素直接把bandit的分数当硬排序是不现实的。4. 与UCB、Epsilon-Greedy对比什么时候选Thompson4.1 三者的核心机制对比推荐系统里做探索最常见的就是这三套Epsilon-Greedy、UCBUpper Confidence Bound、Thompson采样。我以前上学时先学的Epsilon-Greedy感觉太简单粗暴后来用了UCB觉得公式漂亮真正上手做项目后反而越来越喜欢Thompson。我整理了个对比表格方便你参考策略核心机制不确定性处理参数敏感度适用场景Epsilon-Greedy以概率ε随机探索1-ε选择当前最优不感知不确定性的方向纯随机ε需要调参调不好很浪费流量简单场景、基线策略UCB选择均值置信区间上界最大的物品用统计区间显式刻画不确定性置信系数直接决定探索强度展示量适中、静态环境Thompson采样从后验分布中采样按采样值最大选择后验分布天然包含不确定性先验参数影响初期行为后期鲁棒冷启动、非平稳环境、并发场景Epsilon-Greedy的问题在于探索是盲目的。ε设为0.1就意味着一律有10%的流量在随机乱试哪怕某个物品已经很确定点击率极低它也有机会被选中。这在流量紧张的推荐场景里属于明显浪费。UCB的问题在于它假设奖励分布是对称的实际点击行为是偏态的且UCB公式里那个置信区间宽度对展示次数很少的物品会算得非常大容易导致新物品上来就被猛烈探索一轮发现效果不好之后又被瞬间打入冷宫。这种忽上忽下的表现让运营同学很头疼。Thompson采样因为是从后验分布里抽样本天然平滑地处理了不确定性展示少的物品分布宽但也可能抽出低值展示多的物品分布窄但偶尔也会被边缘样本挤下去。整体exploration曲线看上去更加自然。4.2 场景建议与融合策略纯看公式选型的话我的建议是这样的如果整个推荐池只有几十个物品且内容更新频率很低UCB就够了因为它的计算开销小解释性也强给老板讲起来方便。如果候选池上千、每天都有新物品进来或者用户反馈存在明显的时间衰减效应那Thompson采样更合适因为它对非平稳环境的适应能力更强——旧的点击记录会通过增加a和b把分布形状固定住但如果我们对参数做时间衰减就能让模型更关注近期行为。如果业务侧还希望引入更多特征比如用户偏好、内容类别、时段等单纯靠bandit就不够了需要换成Contextual Bandit或者把Thompson的采样分数作为一个特征喂给上层模型。这种用法在广告平台里非常常见拿历史探索数据训练一个点击率预估模型模型上线后继续小规模探索积累样本两者形成正循环。我踩过的一个坑是把Thompson采样分数和主模型分数直接相加前没做min-max归一化导致某个物品的采样值天然比主模型分数高一个量级融进去后排序结果完全被bandit主导。正确的做法是先把两者都做标准化或者用指数合成score (主模型ctr)^k * (thompson_sample)^(1-k)k通过验证集调。5. 工程落地中的坑和排查实录5.1 常见问题速查表实际做工程时遇到的问题往往是数据结构或并发上的小事但搞不定会让策略完全失效。这里列一个我整理的问题速查表现象可能原因排查思路与处理办法物品点击率稳定但长期不被选中更新逻辑没有生效a和b一直没有变化检查日志确认反馈数据真的回流到策略服务新品上线后被大量展示但很快消失初始先验设置太宽采样值极端把先验从Beta(1,1)改成更紧的先验或增加最小展示门槛大盘点击率下降探索流量占比过高调低w2或探索流量百分比检查是否部分物品被重复展示导致疲劳某些物品的展示次数始终为0样本值被数值下限卡住参数更新有bug检查Alpha和Beta是否为正数确认上线时是否加载了正确的计数离线评估效果很好线上却不行离线没有模拟用户反馈的延迟线下评估要加入反馈延迟和时间衰减不能做即时反馈模拟多副本部署时各机器的参数不一致每个副本独立维护参数没有做中心化同步统一使用Redis或数据库做计数器或定期合并计数到全局参数5.2 实操中的几个经验技巧第一更新粒度是个容易被忽略的问题。如果每个请求都立刻更新Beta参数那么短时间内的随机波动会被放大导致物品排序频繁抖动。我在项目里通常采用小批量更新比如每分钟合一次反馈数据批量更新参数。这样既保证了一定的实时性又不会让策略由于个别噪声样本而剧烈波动。第二日志和监控要设计好。上线Thompson采样策略时我会记录每个物品每天的展示数、点击数、平均点击率、被选中次数以及Beta分布的a和b值。有了这些指标就能快速判断策略是否偏离预期。比如某个物品的展示数大量集中在第一天后面基本没有曝光说明初始先验过强需要调低初始参数或增加探索力度。第三时间衰减很值得做。推荐场景里内容热度变化快用户对旧内容疲劳。如果某个物品在一个月前很受欢迎但最近表现很差完全靠累计的a和b来估计会拖慢反应速度。可以给Beta统计加上衰减窗口只统计最近N天的行为或者用滑动加权的方式给近期行为更高权重。简单做法是定时任务每天把每个物品的a和b乘以一个衰减系数比如0.95然后再补充当天的点击反馈。第四对照实验要做但不是只观察点击率。探索策略的收益往往体现在长期上比如新物品被发现成为爆款后带来的增量点击。我一般同时关注两个指标短期指标是整体点击率或转化率是否下降长期指标是新物品在7天内累计展示量和30天内成为爆款的数量。如果短期指标基本不跌长期指标有明显提升说明探索策略带来了真实价值。最后我在实际项目里用Thompson采样最大的感受是它的调参成本低下限高。不需要像Epsilon-Greedy那样反复调整ε也不用像UCB那样担心置信系数的设置只要先验参数别太离谱策略自身能在探索与利用之间走出一个合理平衡。而且Beta分布的参数更新本身就是计数操作和统计点击率用的是同一份数据逻辑简单到后端同学一听就懂不容易在跨团队沟通时解释不清。最后再分享一个小技巧上线初期不用追求“最优策略参数”先快速跑一周看日志里每个臂的展示数是否大体符合预期。如果某个臂几乎没被展示过不要急着加探索权重先查它的a和b数值是否正确——这种低级问题我至少遇到过两次。
返回列表