推荐算法学习系列 6:推荐广告系统Pacing算法

1,266 阅读4分钟

Pacing是Facebook广告系统中花费预算的节奏的一个算法,Google Adwords里面有一个功能是设定预算和最高出价,Adwords就可以自动通过出价调节,让你在这个限定的预算下获取最多的点击,Pacing这个算法和Adwords的这个功能很类似,都是通过调节出价,让广告主获得最大的ROI。

目前预算控制Pacing算法有很多,这里主要说一下PID,有关强化学习Pacing算法待了解后更新。

1. 预算控制问题描述

随着信息技术的进步,互联网广告由于投放周期短、触达范围广、可精准投放等优点,近些年来得到了高速发展。在互联网广告系统中,广告主(企业/商家)通过购买供给方(媒体)提供的广告位,将广告传递给受众(消费者),进而达成广告主的商业目的。在上述过程中,针对同一广告位,往往会存在多个广告主竞争出价,最终出价高者以一定的成本,赢得广告位并获得展示机会。

image.png

然而在实际的广告投放系统中,会包含诸如广告主端的点击率预估模型、用户价值预估模型、竞价算法,媒体端的OCPA、OCPC出价模型,以及多方竞价、二价成交等不可控机制,最终的投放系统十分复杂,影响投放成本的因素过多,造成用户成交价与实际出价并不相等,实际投放成本难以契合广告主在投放初期所制定的预算。这时候就需要预算控制算法进行广告预算控制了。

在单一周期内,预算控制的目标为在既定时间单位内将预算以既定最优策略的方式分发,从长期来看,将多个周期联合起来看,预算系统的目标可以延伸为:

致力于将更长时间跨度、甚至于无穷时间的最优化控制问题分解为若干个更短时间跨度,或者有限时间跨度的最优化控制问题,在满足有限时间跨度内预算核销准确性要求的基础上,在在长周期下仍然追求策略最优解。

2. PID原理理解

一个经典的PID算法如图所示,首先有一个输入,经过比例、积分、微分的调节得到一个输入,调节系统输出进入到实际的系统里又得到了一个输出。最后利用实际系统的输出来调整下一次的输入。

image.png

PID 方法通过误差信号控制被控量,PPIIDD 分别表示比例、积分、微分误差。我们定义时刻 tt 时,系统的输入为i(t)i(t),输出为o(t)o(t),误差量为err(t)=o(t)o(t)err(t)=o^*(t)-o(t),在i(t)i(t)o(t)o^*(t)满足一定的逻辑关系时,不妨视i(t)i(t)o(t)o^*(t),此时 PID 方法的误差量可以表示为:

u(t)=kp(err(t)+1Tierr(t)dt+TDderr(t)dt)u(t)=k_p(err(t)+\frac1{T_i}\int err(t)dt+\frac{T_Dderr(t)}{dt})

简化后可得:

U(t)=kp(err(t)+kierr(t)dt+kdderr(t)dt)\mathrm{U}(t)=k_p(err(t)+k_i\int err(t)dt+k_d\frac{derr(t)}{dt})

其中,

  • kpk_p: 比例系数
  • kik_i: 积分系数
  • kdk_d: 微分系数
  • err(t)err(t): tt 时刻误差

以上是连续形式的PID公式。而显示场景一般会有一个采样间隔,为离散化形式为,这时候需要对公式进行一定的修改。假设采样间隔为TT,在第 kkTT 时刻,有:

u(k)=kp(err(k)+TTikerr(j)+TDT(err(k)err(k1)))u(k)=k_p(err(k)+\frac T{T_i}\sum^kerr(j)+\frac{T_D}T(err(k)-err(k-1)))

简化表示为:

u(k)=Kp(err(k)+kikerr(j)+kd(err(k)err(k1)))u(k)=K_p(err(k)+k_i\sum^kerr(j)+k_d(err(k)-err(k-1)))

此外,PID还有一种增量形式。在t1t-1时刻,有:

u(k1)=Kp(err(k1)+kik1err(j)+kd(err(k1)err(k2)))u(k-1)=K_p(err(k-1)+k_i\sum^{k-1}err(j)+k_d(err(k-1)-err(k-2)))

差分有:

Δu(k)=kp(err(k)err(k1)+kierr(k)+kd(err(k)2err(k1)+err(k2)))\Delta u(k)=k_p(err(k)-err(k-1)+k_ierr(k)+k_d(err(k)-2err(k-1)+err(k-2)))

化简后有:

Δu(k)=Aerr(k)+Berr(k1)+Cerr(k2)\Delta u(k)= Aerr(k) + Berr(k-1) + Cerr(k-2)

其中,

A=kp(1+ki+kd)A = k_p(1 + k_i + k_d)
B=kp(12kd)B = k_p(-1-2k_d)
C=kpkdC = k_pk_d

一般的使用场景下,PID 方法可以快速稳定地消除误差,使系统输出达到目标值。接下来,对比例系数、积分系数、微分系数三部分进行进一步理解:

2.1 比例单元

比例项的输出u(t)与输入err(t)成正比,直接反映了当前值与目标值得误差信号,偏差一旦产生,立即向相反方向成比例的减小偏差。比例项反应迅速,能快减小偏差,但不能消除静差。

静差指的是在系统达到稳态的过程中,稳定输入值与目标的差距。仅使用比例项,由于实际输出值与目标值间差距逐渐减小,因而比例项的输出也逐渐减小,只有存在偏差时比例项才能产生输出,当误差为零时,比例项输出也为零,因此在由非稳态逐渐向稳态逼近的过程中,必然会存在静差。

此外,比例项的输出取决于误差及比例系数,比例系数越小,输出对误差的敏感度也越小,系统响应越慢。反之比例系数越大,控制作用也越强,系统响应会越快。但值得注意的是,过大会使系统产生较大超调和振荡,导致系统稳定性变差,下图演示了在其他参数相同的条件下,不同比例系数下系统振荡曲线:

image.png

2.2 积分单元

积分项主要用于消除静差,提高系统的误差度。积分控制作用的存在与偏差err(t)的存在时间有关,只要系统存在着偏差,积分环节就会不断起作用,对输入偏差进行积分,使控制器的输出及执行器的开度不断变化,产生控制作用以减小偏差。

在积分时间足够的情况下,可以完全消除静差,这时积分控制作用将维持不变。越小,积分速度越快,积分作用越强。积分作用太强会使系统超调加大,甚至使系统出现振荡。下图所示为其他参数相同的条件下,不同积分系数下的系统振荡曲线:

image.png

2.3 微分单元

微分环节的作用能反映偏差信号的变化趋势,并能在偏差信号值变得太大之前,在系统中引入一个有效的早期修正信号,从而加快系统的动作速度,减小调节时间。在偏差刚出现或变化的瞬间,不仅根据偏差量做出及时反应,还可以根据偏差量的变化速度提前给出较大的控制作用,将偏差消灭在萌芽状态,这样可以大大减小系统的动态偏差和调节时间,使系统的动态调节品质得以改善。

微分环节有助于系统减小超调,克服振荡,加快系统的响应速度,减小调节时间,从而改善了系统的动态性能,但微分时间常数过大,会使系统出现不稳定。下图所示为其他参数相同的条件下,不同微分系数下的系统振荡曲线:

image.png

2.4 PID实现

接下来来实现一个最最简单的PID仿真,以下定义了一个PID控制器:

class PIDContorller:
    def __init__(self, kp, ki, kd, dt, pre=0.0):
        self.kp = kp
        self.ki = ki
        self.kd = kd
        self.dt = dt
        self.pre = pre
        self.p = 0.0
        self.i = 0.0
        self.d = 0.0

    def calculate(self, bid_val, history_cost):
        """
        根据history cost 更新比例项、积分项和微分项
        :param bid_val: 广告主广告费用
        :param history_cost: 历史实际广告花费
        :return: 
        """
        pre_p = self.p
        self.p = bid_val - history_cost
        self.i = self.i + self.p * self.dt
        self.d = self.p - pre_p / self.dt
        self.pre = history_cost
        return self.i

    def update(self, bid_val):
        # err(t-1)
        pre_p = self.p
        # p: 比例项,当前时刻误差 err(t)
        self.p = bid_val - self.pre
        # i: 积分项,到t时刻总误差
        self.i = self.i + self.p * self.dt
        # d:微分项,err(t) - err(t-1)
        self.d = self.p - pre_p / self.dt
        # 根据PID,输出需要调整的费用
        diff = self.kp * self.p + self.ki * self.i + self.kd * self.d
        # 根据diff进行调整,输出今天的广告花费
        self.pre = self.pre + diff
        return self.pre

    def reset(self):
        '''
        多广告商场景下,每个广告商进行calculate和update之前需要reset下原参数
        '''
        self.pre = 0.0
        self.p = 0.0
        self.i = 0.0
        self.d = 0.0

此外还有一种增量形式,也很简单,只要对update函数进行一些小修改:

def update(self, total_exp):
    """
    根据历史数据更新 t + 1 时刻的 coef
    使用增量计算公式 Pout= Kp * [e(t) - e(t-1)] + Ki * e(t) + Kd * [e(t) - 2*e(t-1) +e(t-2)]
    :param total_exp: t 时刻的总流量
    :param coef: t 时刻线上的调节系数
    :return: t + 1 时刻调节系数 coef
    """
    pre_p = self.p
    self.p = self.bid_val - total_exp * self.pre_coef
    diff = self.kp * (self.p - pre_p) + self.ki * self.p + self.kd * (self.p - 2 * pre_p + self.p_pre)
    self.p_pre = pre_p

    cur = total_exp * self.pre_coef + diff
    self.pre_coef = cur / total_exp
    return self.pre_coef

此外,还有傅立叶滤波转换的方法,具体来讲就是:在计算微分项的时候,不直接采用当前时刻的误差err(t) 进行计算,而是采用经过滤波之后的滤波值。

from scipy.fftpack import fft, ifft

def update(self, total_exp):
    """
    根据历史数据更新 t + 1 时刻的 coef
    :param total_exp: t 时刻的总流量
    :param coef: t 时刻线上的调节系数
    :return: t + 1 时刻调节系数 coef
    """
    pre_p = self.p
    self.p = self.bid_val - total_exp * self.pre_coef
    self.i = self.i + self.p * self.dt

    if len(self.stack) >= self.preiod:
        self.stack.pop(0)
    self.stack.append(self.p)

    if len(self.stack) < 2:
        self.d = self.p - pre_p
    else:
        fft_values = fft(self.stack)
        frequencies = np.fft.fftfreq(len(fft_values))
        filtered_fft = np.zeros_like(fft_values)
        dominant_frequencies_index = np.argmax(np.abs(fft_values))
        filtered_fft[dominant_frequencies_index] = fft_values[dominant_frequencies_index]
        filtered_fft[-dominant_frequencies_index] = fft_values[-dominant_frequencies_index]
        period_component = np.real(ifft(filtered_fft))
        self.d = period_component[-1]

    diff = self.kp * self.p + self.ki * self.i + self.kd * self.d
    self.p_pre = pre_p

    cur = total_exp * self.pre_coef + diff
    self.pre_coef = cur / total_exp
    return self.pre_coef

PID仿真的结果如图:

image.png 在这里设定了前七天的波动,从第1天开始进行了PID控制,最后使得总cost趋近于目标bid,且累积diff逐渐趋近于0。

pid参数如何选择呢,一般来说需要线下不断调试寻参,这里给出一种最简单的网格搜索方法:

class pid_grid_search:
    def __init__(self, dt, bid_val, bid2exp, history_day, tend_coef):
        self.dt = dt
        self.bid_val = bid_val
        self.bid2exp = bid2exp
        self.history_day = history_day
        self.tend_coef = tend_coef

    def pid_task(self, conf, data):
        kp, ki, kd, kv = conf
        pid = PIDContorller(kp, ki, kd, self.dt, self.bid2exp(self.bid_val), self.tend_coef)
        sum_diff, statis_list = self.run_pid_parallel(data, pid, kv)
        return (kp, ki, kd, kv), sum_diff, statis_list


    def grid_search(self, data, parallel=True):
        confs = self.pid_configs()
        if parallel:
            # 定义分布式计算
            executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
            tasks = (delayed(self.pid_task)(conf, data) for conf in tqdm(confs))
            res = executor(tasks)
        else:
            res = [self.pid_task(conf, data) for conf in tqdm(confs)]
            res.sort(key=lambda x: abs(x[1]))
        return res

    def run_pid_parallel(self, data, pid, kv):
        # some process
        return sum_diff, [sum_diff_list, flow_list, cost_list, coef_list]

    def pid_configs(self):
        confs = list()
        p_params = np.arange(0, 1, 0.01)
        i_params = np.arange(0, 1, 0.01)
        d_params = np.arange(0, 1, 0.01)
        v_params = ['v1']
        for p in p_params:
            for i in i_params:
                for d in d_params:
                    for v in v_params:
                        confs.append([p, i, d, v])
        return confs

在工业界,PID还有许多变体,以适应线上真实场景。

参考文献

附录:广告相关术语记录

  1. Ad
  • 含义:广告。一个广告系统包含四大基本角色:
    • 广告主(advertisers) :指想为自己的品牌或者产品做广告的人,例如宝马、Intel、蒙牛,游戏等
    • 媒体(publisers): 提供广告位置的载体。例如今日头条、QQ浏览器、抖音等等。
    • 广告商(agency): 本质上其实就是中介,帮广告主找媒体广告位,帮媒体找广告主
    • 受众(audience): “消费”广告的人,即消费者、用户。
  1. AOV
  • 含义:Average Order Value(AOV),指客单价。
  1. ARPU
  • 含义:ARPU-AverageRevenuePerUser,就是每用户平均收入。ARPU值高说明平均每个用户贡献的收入高,这段时间业务在上升。
  • 计算:ARPU值(元/月)=总收入/用户数ARPU 值(元/月)= 总收入 / 用户数
  1. Bid
  • 含义: 广告主出价,每个广告都有一个出价bid
  1. Bounce Rate
  • 含义:中文翻译为“跳出率”,是网站分析中一个度量,跳出率定义了只浏览了单个页面的访问量占总访问量的比率。
  • 计算:Bounce Rate的极限值是1,也就是100%,数字越高说明网站存在的问题越多。
  1. B2B、B2C与B2W
  • B2B:Business-to-Business的缩写,是指企业与企业之间通过专用网络,进行数据信息的交换、传递,开展交易活动的商业模式。
  • B2C:Business-to-Consumer的缩写,指电子商务的一种模式,也是直接面向消费者销售产品和服务商业零售模式。
  • B2W:Business-to-Wholesale的缩写,商家对小额批发主,区别与传统的大额贸易和终端零售。
  1. CAC
  • 含义:Customer Acquisition Cost,用户获取成本,即你花多少钱获取了一个新用户。
  • 计算:CAC=(N个月的营销费用+N个月的销售成本)/N个月的新客数量CAC = (N个月的营销费用 + N个月的销售成本)/ N个月的新客数量
  1. CPL
  • 含义:Cost Per Leads,以搜集潜在客户名单多少来收费,对每一条销售线索进行成本衡量,即常见的表单广告。
  • 计算:每条销售线索成本衡量=总费用/线索量每条销售线索成本衡量=总费用/线索量
  1. CPA
  • 含义:Cost Per Action,简称CPA,按成果数计费,获客成本。一般指消费量/转化量。
  • 计算:CPA=广告花费金额/转化数量CPA=广告花费金额/转化数量
  1. CPC
  • 含义:Cost Per Click,每次点击付费广告的成本(平均点击价格)。
  • 计算:CPC=广告费/点击量CPC=广告费/点击量
  1. CPM
  • 含义:Cost Per Mille,按千次展现计费(千次展现价格)。
  • 计算:CPM=广告费/展现量1000CPM=广告费/展现量 *1000
  1. CPS
  • 含义:Cost Per Sale,以实际销售产品数量来换算广告金额,通俗的说就是以佣金提成作为广告费。
  • CPA与CPS的区别:CPA是按照广告目标用户行为(Action)计算广告费,而用户行为(Action)需要预先定义,是点击还是注册或者互动或者转化等等;而CPS只按照销售量计算广告费。
  1. CPP
  • Cost Per Purchase, 每购买成本,指按交易笔数来结算。只有在用户点击广告并进行交易后,按交易笔数付给广告站点费用。与cps不同的是,cpp是按订单数量,cps是订单金额,一般以采用cps居多
  1. CPV
  • 含义:Cost Per View,按照显示效果付费。CPV广告就是展示广告,就是广告商通过实际的广告显示数量来计费。也就是说,独立IP显示一次就计费一次,计费方式非常简单。
  • CPM与CPV的区别:CPV是展示计费,即只要广告展示了那么就需要收取费用,而cpm是曝光计费,就是广告显示在用户可见的网页屏幕内,则记为有效曝光,也就是cpm用户是会看到广告的。
  1. CPT
  • Cost Per Time, 即按照一个时间段进行展示来费用,一般为1天,1周,1月。以一个固定价格去买断一段时间内的广告位展示,被称作最省心的投放方式。大多数平台方通过CPT是最快速也是最有效赚钱的合作方式。

总结:

  • CPC=CPM/CTRCPC = CPM / CTR
  • CPA=CPC/CVRCPA = CPC / CVR
  • ROAS=AOV/CPA=AOVCVR/CPC=AOVCVRCTR/CPMROAS = AOV / CPA = AOV * CVR / CPC = AOV * CVR * CTR / CPM

image.png

  1. CTR
  • 含义:Click Through Rate,即点击率,衡量用户从看到广告到点击广告的比例
  • 计算:CTR=点击量/展现量100%CTR=点击量/展现量 *100\%
  • 预估点击率 (predict CTR, pCTR) 是指对某个广告将要在某个情形下展现前,系统预估其可能的点击概率
  1. CVR
  • 含义:Conversion Rate,转化率,衡量广告流量转化效果的指标。
  • 计算:CVR=转化量/访问量100%CVR= 转化量/访问量 *100\%
  • 预估转化率 (predict CVR, pCVR) 是指对某个广告将要在某个情形下展现前,系统预估其可能的转化概率
  1. eCPM
  • 含义:effective cost per mile,千次展示收益,是媒体衡量自己广告投产效率的指标,它是指每千次广告的曝光,能够给媒体带来多少的广告收入。这个值对媒体越大越好。
  • 计算:eCPM=CPC×CTR×1000eCPM=CPC×CTR×1000
  1. LTV
  • 含义:Life Time Value,客户终生价值。是公司从用户所有的互动中所得到的全部经济收益的总和。
  • 计算:LTV的计算涉及到顾客保持率、顾客消费率、变动成本、获得成本、贴现率等信息的正确取得。
    • 顾客保留率(retention rate,RR)= 本年度的顾客总数 / 上年度的顾客总数;
    • 顾客消费率(spending rate,SR)= 顾客总消费额 / 顾客总数;
    • 变动成本(variable cost,VC)= 产品成本 + 服务管理费用 + 信用卡成本等;
    • 获得成本(acquisition cost,AC)= 本年度广告、促销费用 / 本年度顾客总数;
    • 净利润(gross profit,GP)= 总收入 – 总成本;
    • 贴现率(discount rate,DR)= [1 +(风险系数*银行利率)]n ;
    • 利润净现值(net present value profit,NPV)= GP / DR ;
    • 累积NPV = 特定时间内每年NPV 的总和;
    • 顾客终身价值(LTV)= 累积NPV / 顾客总数。
  1. PV
  • 含义:Page View,页面浏览量。
  • 计算:用户每1次对网站中的每个网页访问均被记录1次。用户对同一页面的多次访问,访问量累计。
  1. ROI
  • 含义:Return on investment, 指投入产出比,即一定周期内,广告主通过广告投放收回的价值占广告投入的百分比。数值大于1为盈利。
  • 计算:总销售收入/总消耗量(即广告费用)=LTV/cost=平均注册用户的付费金额/CPA总销售收入/总消耗量(即广告费用)=LTV/总cost=平均注册用户的付费金额/CPA
  1. UV
  • 含义:Unique Visitor,独立访客。指访问某个站点或点击某条新闻的不同IP地址的人数。
  • 计算:同一页面,客户端多次点击只计算一次,访问量不累计。
  1. GMV
  • 含义:GMV代指网站的成交金额,主要包括付款金额和未付款的。
  • 计算:GMV=销售额+取消订单金额+拒收订单金额+退货订单金额,即GMV为已付款订单和未付款订单两者之和
  1. 广告计费
  • 一价计费 (First-Price, FP): 即广告出价多少则一次点击计费多少
  • 二价计费 (Second-Price, SP): 即广告按下一位出价来支付点击价格
  • 广义第一价格 (Generalized First Price, GFP): 和传统第一密封竞价类似,出价高者得,需要支付自己提出的报价。
  • 广义第二价格 (Generalized Second Price, GSP): 出价高者得,需要支付出价第二高者提出的报价再加上一个最小值