贝叶斯平滑:解决点击率预测中稀疏样本问题的经典方法
2026/9/16 1:08:46 网站建设 项目流程

做排序和广告的同学,大概率都被同一个现象折磨过:同样是点击率,展示5次点了3次的商品,算出来60%,展示1000次点了200次的商品只有20%。如果你直接按这个数排序,60%那个会冲到最前面,等流量真的灌进去,真实点击率立刻被打回2%~3%。这种“看着很美,放大就崩”的问题,本质上不是运气问题,而是小样本下的频率估计方差太大。贝叶斯平滑,就是用来解决点击率预测里这类稀疏样本问题最经典、也最实用的一套方法。我前前后后在多个业务场景里用过它,从广告创意到商品推荐都试过一轮,今天把这套方法的原理、参数估计、工程落地和踩过的坑完整过一遍,希望对正在做CTR特征、排序策略或者冷启动的同学有帮助。

1. 不解决稀疏问题,点击率预测从一开始就是错的

1.1 一个小样本直接统计点击率,结果有多离谱

先看一个具体例子。一个新上线的商品A,因为运气好,前5次曝光来了3次点击,直接统计点击率60%。商品B是老品,曝光1000次,点击200次,点击率20%。按点击率排序,A应该排在B前面。可现实是,A的60%几乎完全来自偶然——5次曝光里3次点击,置信区间跨度极大;B的20%则经过了1000次曝光的检验,五个点以内的波动都不大。

这就是点击率预测里最典型的场景:越是需要判断的新品、长尾品,数据越少;数据越少,直接统计的点击率越不可靠。你把一个统计量当成真实点击率去排序,等于把大量噪声当成了信号。

1.2 大数定律的“另一半”被忽略了

很多人知道大数定律,频率会随着样本量增大趋于真实概率。但现实里我们面对的都是有限样本,尤其是冷启动阶段,展示量可能只有个位数。此时频率估计(MLE)的期望虽然是无偏的,方差却大得吓人。在点击率预测中,你排序用的不是“真实点击率”,而是“点击率的估计值”,这个估计值本身波动越剧烈,排序就越不稳定。

反过来说,哪怕给你100万次曝光,如果点击率本身只有0.1%,估计的绝对误差依然不可忽视。所以点击率预测的前置工作,就是先把“统计点击率”变成“平滑点击率”,让估计的方差降下来,让数值更接近真实概率。

1.3 直接排序造成的恶性循环

没有做平滑的线上系统,经常会出现一个循环:

  • 小样本商品因为偶尔几次点击,拿到很高点击率,排到前面;
  • 排序靠前获得大量曝光;
  • 曝光一上来,真实点击率回归正常水平,排名迅速下降;
  • 系统又重新寻找下一批“高点击率”的小样本,继续循环。

这个循环不仅浪费流量,还会让模型的训练标签充满噪声。因为你在某个时刻采集到的“曝光后点击率”,可能根本不是这个商品稳定状态下的点击率。这也是为什么我说,不解决稀疏问题,点击率预测从一开始就是错的。

2. 贝叶斯平滑的数学内核:从Beta分布到后验均值

2.1 为什么偏偏选Beta分布

贝叶斯平滑的核心思想,是给点击率p一个先验分布。点击行为本身很好建模:n次曝光,c次点击,可以看成二项分布Binom(n, p)。问题在于p未知,我们需要对p做估计。

如果直接做贝叶斯推断,最省事的做法是选一个跟二项分布“共轭”的先验——共轭的意思是,先验和后验的分布形式相同,计算起来非常方便。二项分布的共轭先验正是Beta分布,记为p ~ Beta(α, β)。其中α和β可以直观理解为“先验点击次数”和“先验未点击次数”。

Beta分布的表达能力也不弱:α和β取不同的值,可以拟合均匀分布、集中在某一均值附近的尖峰分布、甚至偏向两端的分布。这让它非常适合表达先验的“强弱”和“中心位置”。

2.2 后验公式:贝叶斯平滑最核心的一行

假设一个item展示了n次,点击了c次,那么似然函数是:

L(p) = p^c * (1-p)^(n-c)

先验是:

P(p) ∝ p^(α-1) * (1-p)^(β-1)

两者相乘后,后验依然是Beta分布:

p | c, n ~ Beta(α+c, β+n-c)

用后验均值作为点估计,就得到了贝叶斯平滑最经典的公式:

smooth_ctr = (c + α) / (n + α + β)

这个公式看起来简单,含义却很深。α+β可以理解为“等效曝光量”,α是其中的“等效点击数”。当真实曝光n很小时,平滑结果主要由先验决定;当n很大时,先验的影响逐渐被稀释,平滑结果会逼近真实统计点击率c/n。

我用一个直观例子说明:假设全局估计α=20,β=580(均值3.3%,先验强度600)。商品A展示5次点击3次,直接统计60%,平滑后是(3+20)/(5+600)=3.8%,几乎被拉回到先验附近;商品B展示1000次点击200次,直接统计20%,平滑后是(200+20)/(1000+600)=13.75%,虽然仍高于先验,但也没有疯狂到20%。这个收缩效果,正是我们想要的。

2.3 点估计为什么取均值而不是众数

贝叶斯推断里,点估计可以取后验均值、后验中位数或最大后验概率(MAP,也就是众数)。对Beta分布来说,众数的公式是:

mode = (α+c-1) / (α+β+n-2)

分子分母都要减1,会带来一个隐蔽的问题:当α+c和β+n-c恰好小于1时,众数可能跑到0或1的边界,导致极不合理的估计。比如一个商品展示1次点击0次,如果先验α=β=0.5,MAP直接是0,相当于完全不给他机会;后验均值却会给出一个温和的平滑值。

取均值的好处是,它在“完全相信数据”和“完全相信先验”之间做了软折中,既不会无限偏向0/1,也不会让小样本特征主导排序。这在点击率预测这种需要稳健性的场景里,比MAP更实用。

3. 超参数α和β的估计:工业级落地最关键的环节

3.1 矩估计法:从样本均值和方差反推参数

公式给出后,下一个问题是α、β从哪来。一种偷懒的做法是拍脑袋,比如设α=1、β=1(均匀先验),或者设α=5、β=95(假设先验均值5%)。但每个业务的点击率水平完全不一样,拍脑袋容易出错,所以我们希望从历史数据中估计出合理的α、β。

工业界最常用的方式是矩估计,思路是让理论均值和方差等于样本均值和方差,然后反解参数。Beta分布的均值和方差公式如下:

E[p] = α / (α+β) Var[p] = αβ / ((α+β)^2 * (α+β+1))

实际操作时:

  1. 统计所有item的总曝光和总点击,计算出加权平均点击率μ;
  2. 用曝光数加权的方差,估计点击率的样本方差σ²;
  3. 令M = α+β,根据公式σ² = μ(1-μ)/(M+1),解出M;
  4. 再算出α = μM,β = (1-μ)M。

写成Python代码如下:

import numpy as np def estimate_beta_params(impressions, clicks): impressions = np.asarray(impressions, dtype=np.float64) clicks = np.asarray(clicks, dtype=np.float64) # 过滤掉无效曝光 mask = impressions > 0 impressions, clicks = impressions[mask], clicks[mask] # 加权平均点击率 mu = clicks.sum() / impressions.sum() # 曝光数加权的点击率方差 p_i = clicks / impressions sigma2 = (impressions * (p_i - mu) ** 2).sum() / impressions.sum() # 方差边界保护:Beta分布的理论方差不会超过 mu*(1-mu) max_sigma2 = mu * (1 - mu) if sigma2 >= max_sigma2: sigma2 = max_sigma2 * 0.999 # 反解参数 m = mu * (1 - mu) / sigma2 - 1 if m <= 0: # 兜底:样本方差过大时,退化为一个很弱的先验 m = 1.0 alpha = mu * m beta = (1 - mu) * m return alpha, beta

这段代码里的边界保护很重要。为什么?因为样本点击率经常会有大量0和少量1,直接计算方差,很容易超过μ(1-μ),导致M为负数,α、β也变成负数,那后面所有平滑结果都不可用了。经验做法是:先过滤掉展示量极低的样本,再做方差估计;如果方差还是异常,就做截断,或者改用后面说的极大似然法。

3.2 矩估计的失效场景

矩估计虽然快,但确实有失效场景。最典型的是数据里混入了极端离群值,比如某个无效流量包拉高单个item的点击率。因为方差对离群值非常敏感,算出来的σ²会偏大,进而导致α+β偏小,平滑力度不够,整个模型对小样本噪声的抑制能力大打折扣。

更麻烦的是,矩估计对“点击率服从Beta分布”这个假设比较依赖。实际业务中,不同类目、不同位置的点击率差异非常大,如果全部混在一起估计,得到的α、β就会是一个“平均”的结果:热门品被压得过狠,冷门品又平滑得不够。这种情况下,我建议至少按类目、按广告位、按新老品分层,各估一套α、β,再应用到对应层级的item上。

3.3 想要更稳定,就最大化边际似然

矩估计的另一个替代方案,是直接最大化边际似然。把每个item的曝光n_i、点击c_i带入,边际似然可以表达成一组Beta函数的乘积。对大样本业务,可以用不动点迭代,也可以用梯度优化直接求解。

最简单的方式是写一个负对数似然函数,丢给SciPy的L-BFGS-B优化器:

import numpy as np from scipy.special import gammaln from scipy.optimize import minimize def beta_binomial_loss(theta, impressions, clicks): alpha = np.exp(theta[0]) beta = np.exp(theta[1]) res = 0.0 for n, c in zip(impressions, clicks): res += gammaln(n + 1) - gammaln(c + 1) - gammaln(n - c + 1) res += gammaln(alpha + c) + gammaln(beta + n - c) - gammaln(alpha + beta + n) res -= gammaln(alpha) + gammaln(beta) - gammaln(alpha + beta) return -res def estimate_beta_mle(impressions, clicks): impressions = np.asarray(impressions, dtype=np.float64) clicks = np.asarray(clicks, dtype=np.float64) mu = clicks.sum() / impressions.sum() result = minimize( beta_binomial_loss, x0=[np.log(mu), np.log(1 - mu)], args=(impressions, clicks), method='L-BFGS-B' ) alpha = np.exp(result.x[0]) beta = np.exp(result.x[1]) return alpha, beta

实际项目中,我只在数据量不算特别大、且对精度要求高的场景用极大似然。数据量超过百万级item时,跑一遍L-BFGS-B也不便宜,矩估计加上清洗流程已经足够,没必要为了完美而牺牲效率。

4. 在真实点击率预测管线里的落地姿势

4.1 离线场景:每日全量CTR基准表

最经典的落地路径,是每天离线跑一遍全量item的平滑点击率,产出一张多维度的宽表,供给线上特征和离线训练。

表结构通常长这样:

字段说明
item_id商品、创意或广告的唯一ID
exposure_count近N天累计曝光数
click_count近N天累计点击数
raw_ctr直接统计点击率
smooth_ctr贝叶斯平滑后的点击率(α, β来自对应分层)
confidence_score可根据展示量、平滑后分母计算的置信度

这张表的作用不止是提供一个“更准的CTR”,它还能做排序候选过滤、做冷启动预估基值、做特征工程的原料。平滑后的smooth_ctr,相比raw_ctr,在排序场景里更容易让新老品站在同一条起跑线上。

4.2 在线场景:参数滚动更新

线上实时点击率预测,不建议直接把α、β固定死就不再更新。我的经验是:取最近14天或者30天的数据,每天或者每周重新估计一次α、β,更新到配置中心。这样先验会跟着大盘点击率的季节性变化走,不会出现“大促前后先用同一套参数”的尴尬。

滑动窗口还会带来另一个细节:新旧参数的切换要平滑,不要直接重灌。比如大促刚结束,整体点击率变化剧烈,如果用14天窗口,可能某一天突然把先验均值拉高一大截,导致全量item的平滑CTR集体跳动,排序波动变大。我习惯做一个参数混合:新参数计算完成后,和线上正在用的参数按0.1~0.2的权重做加权平均,再发布上线。这样既跟得上趋势,又不会让线上指标突然抖动。

4.3 平滑CTR在模型里的用法

很多同学以为平滑完成后,把smooth_ctr当一个特征喂给GBDT就结束了。实际上还有几个更好用的姿势:

  • 同时输入曝光数或log(曝光数)。树模型可以从中学习到“这个特征值是否可信”;
  • 把(α+β+n)这个等效总样本量作为权重或者辅助特征,让模型在样本充足时对smooth_ctr有更高的置信度;
  • 对分层先验做个衍生特征,比如“item平滑CTR减去所属类目平滑均值的差”,能帮助模型捕捉不同层级上的相对竞争力。

在线排序模型如果用LR或者深度模型,smooth_ctr直接做连续特征也完全没问题,只要做好分箱或归一化。树模型对数值尺度不敏感,所以直接给原值就够了。

4.4 分层平滑:不同业务场景别共用一套先验

不同类目、不同广告位的点击率天然有差异:首页banner的点击率可能5%,详情页商品的点击率可能0.5%,如果你把它们放在一起估计全局α、β,两边的结果都会被扭曲。

我推荐的做法是两层甚至三层结构:全局一套α、β作为底层;每个类目/广告位单独算属于自己的α_c、β_c;item自己的平滑,优先用类目参数。当某个类目样本太少时,再向全局参数收缩。这个“向高层收缩”的思想,其实就是贝叶斯层级模型的简化版本,落地起来并不复杂——只需要在参数表里多维护一个level字段。

5. 同类平滑手段怎么选,以及我踩过的几个坑

5.1 拉普拉斯平滑、Wilson区间、贝叶斯平滑的对比

很多人会问,平滑点击率是不是只有贝叶斯平滑一种办法。实际上常用的还有拉普拉斯平滑和Wilson分数区间。我单独用一张表总结它们的使用场景:

方法计算公式优点缺点适用场景
拉普拉斯平滑(c+1)/(n+2)实现简单,零成本先验不自适应,接近均值时偏差大快速验证,小规模特征
Wilson区间区间下界或中点自带置信度,边缘解释清晰不是点估计,需要二次处理低曝光过滤、类目基准
贝叶斯平滑(c+α)/(n+α+β)先验从数据中学习,收缩自适应需要估计α、β,有一定实现成本大规模CTR/全链路排序

如果你只需要粗粒度过滤低质量item,Wilson区间的下界很好用;但如果你希望输出一个稳定、可解释、能进排序模型的特征,贝叶斯平滑是更合适的方案。

5.2 坑一:矩估计的样本方差被高估,平滑形同虚设

这是我第一次做贝叶斯平滑时踩得最深的坑。当时我以为只要套公式就能拿到α、β,结果算出来的α+β只有十几,平滑力度弱得可怜。查了半天发现,是因为我没有过滤展示量极少的item,大量曝光只有个位数的样本把点击率方差拉高了,M被压缩到很小。

后来我把估计α、β的样本限制在“展示量大于等于某个阈值”(比如n≥50)的范围里,方差才回归正常。这个步骤不能省,阈值根据业务点击率整体水平来定,我的经验是让阈值内的样本覆盖全量曝光的80%以上即可。

5.3 坑二:点击和曝光口径混在一起估计

点击率的口径远没有想象中统一。raw click、valid click、filtered click(过滤掉机器人流量)差异巨大;曝光也有“展示即算”和“可见曝光”两种口径。如果训练数据里用的是valid click,而统计α、β时用的是raw click,那整体点击率会被高估,平滑结果整体偏大,排序时会有系统性偏差。

更隐蔽的问题是:不同位置的曝光质量不同,混在一起估计会让先验丧失业务意义。我现在的做法是,每个业务线独立清洗流量,再按口径分别估计α、β,宁可多维护几组参数,也不要“一刀切”共用一组。

5.4 坑三:先验强度过大,把真正爆款给压制了

贝叶斯平滑引入先验后,天然会让估计值向均值收缩。但如果α+β设得非常大(比如根据全量数据算出M=5000以上),而某个新品的曝光已经达到数千次、真实点击率确实很高,平滑后的值会被严重拉低。你不是在去噪,而是在抹杀真实信号。

这个问题的本质,是样本量增加后,应该让数据自己说话。解决方式有两种:一是随着item曝光量增加,逐渐降低先验的权重,比如用权重因子 V/(V+M) 加权原始频率估计和平滑估计;二是分新老品各自估计先验,新品用较弱的先验强度,老品用较强但稳定的先验强度。第一种方式效果直接,第二种方式更细腻,实际使用中可以结合。

5.5 一个经常被忽略的进阶操作:把置信度交给下游

平滑之后得到的其实不只是点估计,还有后验分布。用后验均值做特征只是最基础的用法,更进阶的做法是,把每个item置信度的高低也作为一个维度传给下游排序模型。比如smooth_ctr差异不大时,模型更倾向选择曝光量更大、置信度更高的item;或者反向选择,故意试探置信度低的潜力品。

这种用法在一些探索性排序策略里效果好到出奇,也是贝叶斯平滑相比纯拉普拉斯平滑更强的地方——你不是只有一个数字,你有一个分布,分布能告诉你一个item当前被验证得够不够充分。

回到我自己的经验,如果你是从零开始搭建这套能力,第一版建议直接走“分层的矩估计+每日更新参数+平滑CTR宽表”这三板斧,跑通拿到收益后,再去考虑极大似然、在线更新或置信度特征。克制很关键,因为贝叶斯平滑解决的只是“小样本方差大”这一个问题,它不解决所有排序问题。先把噪声摁住了,再来谈调优,顺序不能反。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询