CTR 平滑方法

2021-05-15 机器学习 推荐系统

# 一、背景

在电商领域中,经常要计算 CTR 或者 CVR,以点击率 CTR 为例,CTR 的值等于点击量(Click)除以曝光量(Exposure)。以 rr 表示点击率,

r=CEr=\frac{C}{E}

然而实际应用中,会遇到两个问题:

1. 「新品问题」新商品点击率的预测和计算问题

若曝光 1 次,点击 1 次,则 r=CEr=\frac{C}{E} 的值为 1;若曝光 1 次,点击 0 次,则 r=CEr=\frac{C}{E} 的值为 0。可以看到在曝光过少时,CTR 的波动较大,不够准确。

2. 「数据不可信问题」不同商品点击率之间的比较

假设有两件商品 A 和 B,rA=510r_A=\frac{5}{10}rB=50100rA=rBr_B=\frac{50}{100} \Rightarrow r_A=r_B,但这样计算不够合理,从置信度的角度来说,明显 rBr_B 更加可信。

解决以上两个问题可以使用平滑的技术解决。最简单的方法是在计算 CTR 的公式中分子分母同时加上一个数,加上之后可避免这两个问题。

r=C+aE+br=\frac{C+a}{E+b}

aabb 的值如何确定呢?

首先,这两个数可以人为设定,或者使用历史数据的平均值等,这样可以在一定程度上解决第一个问题。但第二个问题表明,a,ba,b 的值需要根据曝光量、点击量进行动态变化,不能简单地给出一个定值。

即解决的思路为:

引入先验 a,ba,b(解决问题 1:r=C+aE+babr=\frac{C+a}{E+b}\approx\frac{a}{b}),而且 a,ba,b 的取值可以根据曝光量大小变化(似然)而改变(后验)(解决问题 2)。

本文将介绍如何通过历史数据来计算有统计意义的 aabb,即贝叶斯平滑。

# 二、理论基础

对于一件商品或者或者一个广告,对于某次曝光,用户要么点击,要么不点击,符合二项分布。因此下文对于点击率的贝叶斯平滑,都是基于以下假设:

  • 对于某件商品或者广告 xx,其是否点击是一个伯努利分布(Bernoulli): xBer(r)x \sim Ber(r)
  • xx 表示是否点击,当 x=1x=1 时表示点击,当 x=0x=0 时表示未点击,rr 表示某件商品被点击的概率,即点击率。

# 三、贝叶斯平滑

首先通过极大似然估计得到初始 a,ba,b 的值,作为先验的初始化。

根据贝叶斯公式可知:P(rx)P(r)P(xr)P(r|x) \propto P(r) \cdot P(x|r),即 后验先验似然\text{后验} \propto \text{先验} \cdot \text{似然}

为了让参数 a,ba,b 能够随着数据的变化而动态改变,我们需要让后验是先验的共轭分布,这样才能将上一次的后验结果作为本次的先验输入,不断对后验结果进行修正。

由于先验分布 P(r)Ber(r)P(r) \sim Ber(r),因此我们假设后验分布 P(rx)Beta(α,β)P(r|x) \sim Beta(\alpha,\beta),而贝叶斯平滑给出的的公式为:

r=C+αE+α+βr=\frac{C+\alpha}{E+\alpha+\beta}

那么接下来我们的目标就是估计出 α\alphaβ\beta

通过二阶矩估计得到,

α=X(X(1X)S21)\alpha = \overline X(\frac{\overline X(1-\overline X)}{S^2}-1)
β=(1X)(X(1X)S21)\beta = (1-\overline X)(\frac{\overline X(1-\overline X)}{S^2}-1)

在工程实现上,需要取连续一段时间的数据,比如一周,然后在每天的数据中计算每件商品或者广告的点击率,之后求这些点击率的均值和方差,然后带入到公式中。

# 四、威尔逊置信区间

置信区间的实质,就是进行可信度的修正,弥补样本量过小的影响。

  • 如果样本多,就说明比较可信,不需要很大的修正,所以置信区间会比较窄,下限值会比较大;
  • 如果样本少,就说明不一定可信,必须进行较大的修正,所以置信区间会比较宽,下限值会比较小。

二项分布的置信区间有多种计算公式,最常见的是“正态区间”。但是,它只适用于样本较多的情况(np>5np > 5n(1p)>5n(1 − p) > 5),对于小样本,它的准确性很差。

1927 年,美国数学家 Edwin Bidwell Wilson 提出了一个修正公式,被称为“威尔逊区间”,很好地解决了小样本的准确性问题。

p^+12nz1α22±z1α2p^(1p^)n+z1α224n21+1nz1α22\frac{\hat{p}+\frac{1}{2n}z^{2}_{1-\frac{\alpha}{2}}\pm z_{1-\frac{\alpha}{2}}\sqrt{\frac{\hat{p}(1-\hat{p})}{n}+\frac{z^{2}_{1-\frac{\alpha}{2}}}{4n^{2}}}}{1+\frac{1}{n}z^{2}_{1-\frac{\alpha}{2}}}

在上面的公式中,p^\hat{p} 表示样本的 CTR,nn 表示样本的大小,z1α2z_{1-\frac{\alpha}{2}} 表示对应某个置信水平的 zz 统计量,这是一个常数,可以通过查表得到。一般情况下,在 9595% 的置信水平下,zz 统计量的值为 1.961.96

我们一般选取置信区间得下限作为估计值,可以看到,当 nn 的值足够大时,这个下限值会趋向 p^\hat{p}。如果 nn 非常小,这个下限值会大大小于 p^\hat{p}

# 五、代码实战

import numpy as np


class BayesianSmoothing(object):
    def __init__(self, alpha, beta):
        self.alpha = alpha
        self.beta = beta

    def update_from_data_by_moment(self, pos, n):
        """
        使用二阶矩估计,计算 alpha、beta
        :param pos: 正例数
        :param n: 总数
        """

        def __compute_moment(_pos, _n):
            _ctr_list = []
            _var = 0.0
            for _ in range(len(_n)):
                if not _n[_]:
                    continue
                _ctr_list.append(float(_pos[_]) / _n[_])
            _mean = sum(_ctr_list) / len(_ctr_list)
            for _ctr in _ctr_list:
                _var += pow(_ctr - _mean, 2)

            return _mean, _var / (len(_ctr_list) - 1)

        mean, var = __compute_moment(pos, n)
        self.alpha = mean * (mean * (1 - mean) / (var + 1e-6) - 1)
        self.beta = (1 - mean) * (mean * (1 - mean) / (var + 1e-6) - 1)


class Wilson(object):
    @classmethod
    def wilsonCI(cls, pos, n, z=1.96):
        """
        威尔逊置信区间下限计算函数
        :param pos: 正例数
        :param n: 总数
        :param z: 正态分布的分位数(查表 0.95 的置信区间约等于 1.96)
        :return: 威尔逊置信区间下限
        """
        p = pos * 1. / n * 1.
        return (p + (np.square(z) / (2. * n))
                - ((z / (2. * n)) * np.sqrt(4. * n * (1. - p) * p + np.square(z)))) / \
               (1. + np.square(z) / n)


if __name__ == '__main__':
    clk = [0, 2, 30, 100]
    exp = [3, 5, 100, 500]

    bs = BayesianSmoothing(1, 1)
    bs.update_from_data_by_moment(clk, exp)
    print('Bayes 平滑先验分布参数:', bs.alpha, bs.beta)

    for i in range(len(clk)):
        origin_ctr = clk[i] / exp[i]
        bayes_ctr = (clk[i] + bs.alpha) / (exp[i] + bs.alpha + bs.beta)
        wilson_ctr = Wilson.wilsonCI(clk[i], exp[i])
        print('修正前{},Bayes 修正后{},Wilson 修正后{},合并修正后{}'.format(
            round(origin_ctr, 3),
            round(bayes_ctr, 3),
            round(wilson_ctr, 3),
            round((bayes_ctr + wilson_ctr) / 2, 3)
        ))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

测试结果

Bayes 平滑先验分布参数: 1.1201324526016245 3.858234003405596
修正前0.0,Bayes 修正后0.14,Wilson 修正后0.0,合并修正后0.07
修正前0.4,Bayes 修正后0.313,Wilson 修正后0.118,合并修正后0.215
修正前0.3,Bayes 修正后0.296,Wilson 修正后0.219,合并修正后0.258
修正前0.2,Bayes 修正后0.2,Wilson 修正后0.167,合并修正后0.184
1
2
3
4
5

# 六、参考资料

Last Updated: 2023-01-28 4:31:25