网上的文章要么偏工程,要么偏学术,如何将论文中的成果应用到工程上的文章相对偏少,本文将通过介绍分析论文中的试验代码,将工程和学术连接起来。原始论文解读
更新历史
- 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 | Example of PID control eCPC. |
简单来说,我们希望看到的结果是第二列 ecpc 项随着轮数增加,稳定在 40000 这个预设值附近。
数据集
想要最快速度了解代码逻辑,第一步并不是看代码,而是看数据。我们打开 exp-data/test.txt
,就可以看到如下数据(选取几行)
1 | 0 20 0.000070 3 |
具体每一列的含义文档里没有说明,通过阅读相关代码可知具体含义如下:
- 列 1: 是否点击,点击为 1,没有点击为 0
- 列 2: 获胜价格,如果最终预测值大于该价格,则认为竞价成功
- 列 3: Bid Request 通过 LR 模型的预测值,即 PCTR
- 列 4: 没有使用
注:因为不同人预处理数据习惯不同,这部分代码比较繁杂,感兴趣的同学可以自行阅读 python/lryzx.py
和 python/mak-yzpc.py
的内容。
代码逻辑
了解了数据的含义,再来看代码就比较轻松了,这里我们从主逻辑入手,具体函数在最后统一说明。这里我们主要关注以下三个问题:
- PID 控制器的参数是如何得到的?需要哪些数据参与计算?
- PID 控制器的评测方法
- PID 控制器是否有效
数据读取
因为是直接读取已经预处理好的数据,这部分非常简单,具体流程如下:
- 设定基础变量
- 参考值
ref
40000(单位是千次分,也就是 1000 次展示 400 元,一次 0.4 元) - 训练(
advs_test_bids
)和测试(advs_test_bids
)的 bid request 各 100000 条 - 训练(
advs_train_clicks
)和测试(advs_test_clicks
)的点击分别为 79 和 65 条 - 基础出价
basebid
69
- 参考值
- 设定参数
- 最低出价
minbid
5 - 总轮数
cntr_rounds
40 - P 系数
para_p
0.0005,范围para_ps
是range(0, 40, 5)
- I 系数
para_i
0.000001,范围para_is
是range(0, 25, 5)
- D 系数
para_d
0.0001,范围para_ds
是range(0, 25, 5)
- 范围间隔系数
div
1e-6 - Settle 条件
settle_con
0.1,就是误差在 10% 算 settle - Rise 条件
rise_con
0.9,和 Settle 一起使用的 - 控制信号的下限(
min_phi
)和上限(max_phi
)分别为 -2 和 5
- 最低出价
- 设定随机数种子为固定值,便于复现结果
- 将训练和测试数据从文本文件中读取出来,保存到数组中:
- 训练集真实的 y 保存在
y_train
中(即有没有点击);测试集保存在y
中 - 训练集真实的获胜价格保存在
mplist_train
中;测试集保存在mplist
中 - 训练集的 pctr 保存在
yp_train
中;测试集保存在yp
中
- 训练集真实的 y 保存在
- 计算训练集的点击率
basectr
- 声明其他用于记录训练和测试指标的变量
PID 控制器
接下来的代码可以通过修改第 9 行的 mode
变量来切换不同的执行逻辑(三种选择:test
, batch
, single
)。具体的差别如下:
test
使用测试数据集和初始的参数设定运行 PID 控制器batch
使用训练数据集对遍历范围内的参数组合运行 PID 控制器single
使用训练数据集和初始的参数设定运行 PID 控制器
这里可以看到核心的流程是一样的,只是参数有不同,所以我们以 single
模式为例子进行说明,对应的是 control
函数:
- 打开文件用于写入结果,声明一些变量
- 记录每轮 ecpc 变化
ecpcs
- 记录累计误差
error_sum
- 是否第一轮
first_round
以及是否是第二轮sec_round
- 每一轮处理的竞价数量
cntr_size
- 总花费
total_cost
,总点击total_clks
,总获胜total_wins
,每轮花费记录tc
- 记录每轮 ecpc 变化
- 开始循环
- 确定本轮的控制信号
phi
,分两种情况- 第一轮:
phi
为 0 - 第二轮:
- 计算当前 ecpc 与参考值的误差
error
并累加到error_sum
中 - 根据 PID 方程确定
phi
为para_p*error + para_i*error_sum
(这里没有 D 的部分)
- 计算当前 ecpc 与参考值的误差
- 第三轮:
- 计算当前 ecpc 与参考值的误差
error
并累加到error_sum
中 - 根据 PID 方程确定
phi
为para_p*error + para_i*error_sum + para_d*(ecpcs[round-2]-ecpcs[round-1])
- 计算当前 ecpc 与参考值的误差
- 第一轮:
- 循环变量初始化
- 本轮花费
cost
重置为 0 - 本轮点击
clks
重置为 0 - 本轮结束的索引
imp_index
(最后一轮特殊处理)
- 本轮花费
- 对控制信号
phi
进行上下限约束 - 针对本轮的所有 bid 进行模拟
- 取得当前 bid 是否点击,pctr,获胜价格信息
- 根据 pctr 计算出经 PID 控制器调整的出价,公式为
lin(pctr, basectr, basebid) * (math.exp(phi)
。注:如果是第 1 轮,则出价直接为 1000 - 如果出价大于获胜价格,获胜数量
total_wins
、点击数量clks
、总点击数量total_clks
、花费cost
、总花费total_cost
这几个统计值都进行累加
- 记录截止本轮的累计花费
tc[round]
、本轮的 ecpc 值ecpcs[round]
、点击占比click_ratio
和获胜占比win_ratio
。注:最后的两个占比都是与真实值的对比 - 写入本轮的信息到文件中
- 确定本轮的控制信号
- 全部循环结束后,根据每轮的数据,计算以下评测指标
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
看到结果。这里选取几个参数组合的结果:
每一列的结果及分析如下:
- campaign: 表示选取的广告投放计划 ID,均为 1458
- total-rounds: 模拟总轮数,均为 40 轮
- base-bid: 用来计算出价的基础值,均为 69
- ref: 设定的 ecpc 参考值,均为 40000(CPM 单位人民币分)
- p: PID 控制器的 P 系数,遍历尝试
- i: PID 控制器的 I 系数,遍历尝试
- d: PID 控制器的 D 系数,遍历尝试
- settle-con: 进入稳定状态的条件,均为 0.1,表示当前轮 ecpc 在参考值的 10% 误差范围内
- rise-con: 完成 rise 的条件,均为 0.9,表示当前轮 ecpc 在参考值的 10% 误差范围内
- rise-time: 指的是第一个满足 rise-con 的轮次编号,越小说明越快接近参考值
- settling-time: 指的是在此轮之后,ecpc 均满足 settle-con 的轮次,越小说明越快稳定。如果值为 40,说明一直没有达到稳定状态(即可能第 N 轮满足 10% 误差,但第 N+1 轮又不满足)
- overshoot: 指的是偏离参考值最大的百分比,超过和不满足都算,越小说明控制越稳定
- rmse-ss: 进入稳定状态前的 ecpc 的均方根误差,越小说明波动越小
- sd-ss: 进入稳定状态前 ecpc 的标准差,越小说明波动越小。注:如果一直没进入稳定状态,此项为 0
简单来说,我们就是要找到一组 PID 参数,使得 rise-time, settling-time, overshoot, rmse-ss, sd-ss 这几个指标都尽量小。
至此,前面提到的三个问题都已经得到了解答,接下来就是根据实际的需求,进行工程设计了。