【论文笔记】004e Feedback Control of Real-Time Display Advertising 代码解析

网上的文章要么偏工程,要么偏学术,如何将论文中的成果应用到工程上的文章相对偏少,本文将通过介绍分析论文中的试验代码,将工程和学术连接起来。原始论文解读


更新历史

  • 2020.02.14: 完成初稿

试验准备

这次选择的论文刚好有开源对应的试验代码,我们可以在 这里 查看下载,就省去了自己编写代码复现论文结果的麻烦。(针对广告投放相关的试验一般数据集都比较大,如果没有开源代码的话,要复现论文结果还是要花不少时间的)

代码作者也很贴心地在 README 中说明了如何获取完整数据集进行试验,不过目前该链接已失效,感兴趣的同学可能需要花一点时间自行寻找了。

运行代码

因为代码已经写好,所以我们先尝试运行一次,根据 README.md 中的说明,我们找到 scripts/run_demo_example.sh 文件,查看内容后发现代码在 python/control-ecpc-pid-example.py 中。根据代码中的 import 安装好指定的包,就可以直接 python control-ecpc-pid-example.py 执行了。因为是测试数据,执行结果非常快,不过我们想要弄清楚的是代码的具体逻辑,能够快速执行其实更方便。结果如下(选取前几行)

1
2
3
4
5
6
7
8
9
10
11
Example of PID control eCPC.
Data sample from campaign 1458 from iPinYou dataset.
Reference eCPC: 40000
test performance:
round ecpc phi total_click click_ratio win_ratio total_cost ref
0 54062.0000 0.0000 2 0.0308 0.0250 162186.0000 40000.0
1 54230.3333 -2.0000 2 0.0308 0.0251 162691.0000 40000.0
2 40755.2500 -2.0000 3 0.0462 0.0254 163021.0000 40000.0
3 51294.7500 0.9408 3 0.0462 0.0343 205179.0000 40000.0
4 51399.7500 -2.0000 3 0.0462 0.0347 205599.0000 40000.0
5 51478.0000 -2.0000 3 0.0462 0.0349 205912.0000 40000.0

简单来说,我们希望看到的结果是第二列 ecpc 项随着轮数增加,稳定在 40000 这个预设值附近。

数据集

想要最快速度了解代码逻辑,第一步并不是看代码,而是看数据。我们打开 exp-data/test.txt,就可以看到如下数据(选取几行)

1
2
3
4
5
6
7
0	20	0.000070	3
0 20 0.000049 3
0 106 0.001190 3
0 64 0.000158 3
0 20 0.000120 3
0 50 0.000098 3
0 70 0.000510 3

具体每一列的含义文档里没有说明,通过阅读相关代码可知具体含义如下:

  • 列 1: 是否点击,点击为 1,没有点击为 0
  • 列 2: 获胜价格,如果最终预测值大于该价格,则认为竞价成功
  • 列 3: Bid Request 通过 LR 模型的预测值,即 PCTR
  • 列 4: 没有使用

注:因为不同人预处理数据习惯不同,这部分代码比较繁杂,感兴趣的同学可以自行阅读 python/lryzx.pypython/mak-yzpc.py 的内容。

代码逻辑

了解了数据的含义,再来看代码就比较轻松了,这里我们从主逻辑入手,具体函数在最后统一说明。这里我们主要关注以下三个问题:

  1. PID 控制器的参数是如何得到的?需要哪些数据参与计算?
  2. PID 控制器的评测方法
  3. PID 控制器是否有效

数据读取

因为是直接读取已经预处理好的数据,这部分非常简单,具体流程如下:

  1. 设定基础变量
    1. 参考值 ref 40000(单位是千次分,也就是 1000 次展示 400 元,一次 0.4 元)
    2. 训练(advs_test_bids)和测试(advs_test_bids)的 bid request 各 100000 条
    3. 训练(advs_train_clicks)和测试(advs_test_clicks)的点击分别为 79 和 65 条
    4. 基础出价 basebid 69
  2. 设定参数
    1. 最低出价 minbid 5
    2. 总轮数 cntr_rounds 40
    3. P 系数 para_p 0.0005,范围 para_psrange(0, 40, 5)
    4. I 系数 para_i 0.000001,范围 para_isrange(0, 25, 5)
    5. D 系数 para_d 0.0001,范围 para_dsrange(0, 25, 5)
    6. 范围间隔系数 div 1e-6
    7. Settle 条件 settle_con 0.1,就是误差在 10% 算 settle
    8. Rise 条件 rise_con 0.9,和 Settle 一起使用的
    9. 控制信号的下限(min_phi)和上限(max_phi)分别为 -2 和 5
  3. 设定随机数种子为固定值,便于复现结果
  4. 将训练和测试数据从文本文件中读取出来,保存到数组中:
    1. 训练集真实的 y 保存在 y_train 中(即有没有点击);测试集保存在 y
    2. 训练集真实的获胜价格保存在 mplist_train 中;测试集保存在 mplist
    3. 训练集的 pctr 保存在 yp_train 中;测试集保存在 yp
  5. 计算训练集的点击率 basectr
  6. 声明其他用于记录训练和测试指标的变量

PID 控制器

接下来的代码可以通过修改第 9 行的 mode 变量来切换不同的执行逻辑(三种选择:test, batch, single)。具体的差别如下:

  • test 使用测试数据集和初始的参数设定运行 PID 控制器
  • batch 使用训练数据集对遍历范围内的参数组合运行 PID 控制器
  • single 使用训练数据集和初始的参数设定运行 PID 控制器

这里可以看到核心的流程是一样的,只是参数有不同,所以我们以 single 模式为例子进行说明,对应的是 control 函数:

  1. 打开文件用于写入结果,声明一些变量
    1. 记录每轮 ecpc 变化 ecpcs
    2. 记录累计误差 error_sum
    3. 是否第一轮 first_round 以及是否是第二轮 sec_round
    4. 每一轮处理的竞价数量 cntr_size
    5. 总花费 total_cost,总点击 total_clks,总获胜 total_wins,每轮花费记录 tc
  2. 开始循环
    1. 确定本轮的控制信号 phi,分两种情况
      1. 第一轮:phi 为 0
      2. 第二轮:
        1. 计算当前 ecpc 与参考值的误差 error 并累加到 error_sum
        2. 根据 PID 方程确定 phipara_p*error + para_i*error_sum (这里没有 D 的部分)
      3. 第三轮:
        1. 计算当前 ecpc 与参考值的误差 error 并累加到 error_sum
        2. 根据 PID 方程确定 phipara_p*error + para_i*error_sum + para_d*(ecpcs[round-2]-ecpcs[round-1])
    2. 循环变量初始化
      1. 本轮花费 cost 重置为 0
      2. 本轮点击 clks 重置为 0
      3. 本轮结束的索引 imp_index(最后一轮特殊处理)
    3. 对控制信号 phi 进行上下限约束
    4. 针对本轮的所有 bid 进行模拟
      1. 取得当前 bid 是否点击,pctr,获胜价格信息
      2. 根据 pctr 计算出经 PID 控制器调整的出价,公式为 lin(pctr, basectr, basebid) * (math.exp(phi)。注:如果是第 1 轮,则出价直接为 1000
      3. 如果出价大于获胜价格,获胜数量 total_wins、点击数量 clks、总点击数量 total_clks、花费 cost、总花费 total_cost 这几个统计值都进行累加
    5. 记录截止本轮的累计花费 tc[round] 、本轮的 ecpc 值 ecpcs[round]、点击占比 click_ratio 和获胜占比 win_ratio。注:最后的两个占比都是与真实值的对比
    6. 写入本轮的信息到文件中
  3. 全部循环结束后,根据每轮的数据,计算以下评测指标 overshoot, settling_time, rise_time, rmse_ss, sd_ss

执行结果可以在 exp-data 文件夹中看到,总结一下就是 PID 控制器可以将 ecpc 控制在指定的参考值范围内。

注,对于 test 模式,对应的函数是 control_test,和前面流程的差别只是在于数据集,这里不再赘述。

寻找最佳参数

从前面的试验我们可以验证 PID 控制器的有效性,但是对于最开始提到的前两个问题,即如何找到最佳的 PID 控制器参数,依然没有答案。这部分内容,只要我们把目光转移到 mode=batch 的逻辑,PID 参数的逻辑就迎刃而解了。

论文中提到的搜索方法是 adaptive coordinate search,但是在代码中的实现方式就是简单的三个嵌套循环(不知道是不是开源的版本是较早期的代码)。执行完成之后可以在 report/report-batch.tsv 看到结果。这里选取几个参数组合的结果:

每一列的结果及分析如下:

  1. campaign: 表示选取的广告投放计划 ID,均为 1458
  2. total-rounds: 模拟总轮数,均为 40 轮
  3. base-bid: 用来计算出价的基础值,均为 69
  4. ref: 设定的 ecpc 参考值,均为 40000(CPM 单位人民币分)
  5. p: PID 控制器的 P 系数,遍历尝试
  6. i: PID 控制器的 I 系数,遍历尝试
  7. d: PID 控制器的 D 系数,遍历尝试
  8. settle-con: 进入稳定状态的条件,均为 0.1,表示当前轮 ecpc 在参考值的 10% 误差范围内
  9. rise-con: 完成 rise 的条件,均为 0.9,表示当前轮 ecpc 在参考值的 10% 误差范围内
  10. rise-time: 指的是第一个满足 rise-con 的轮次编号,越小说明越快接近参考值
  11. settling-time: 指的是在此轮之后,ecpc 均满足 settle-con 的轮次,越小说明越快稳定。如果值为 40,说明一直没有达到稳定状态(即可能第 N 轮满足 10% 误差,但第 N+1 轮又不满足)
  12. overshoot: 指的是偏离参考值最大的百分比,超过和不满足都算,越小说明控制越稳定
  13. rmse-ss: 进入稳定状态前的 ecpc 的均方根误差,越小说明波动越小
  14. sd-ss: 进入稳定状态前 ecpc 的标准差,越小说明波动越小。注:如果一直没进入稳定状态,此项为 0

简单来说,我们就是要找到一组 PID 参数,使得 rise-time, settling-time, overshoot, rmse-ss, sd-ss 这几个指标都尽量小。

至此,前面提到的三个问题都已经得到了解答,接下来就是根据实际的需求,进行工程设计了。

一点打赏,十分感谢,百分动力