← 返回个人主页
READING NOTE / BLOG个人博客

GAVE:让离线自动出价不只会模仿历史

Castle Zeng个人博客

基于强化学习的广告自动出价有一个很现实的矛盾:线上试错成本和风险较高,离线学习又容易被历史策略限制住。

具体来讲,直接上在线强化学习,等于让系统拿预算去验证一个坏动作,代价太大,给广告主的解释成本也很高;只拿历史日志训练倒是安全,可模型很容易变成“历史策略复读机”,哪怕有更好的动作,它也没什么理由往外迈那一步,起不到优化策略的作用。

GAVE 想解决的,正是这个夹缝里的问题。它以 Decision Transformer 为骨架,用业务 Score 重写 RTG,在历史动作附近生成候选动作,再用一个偏向高收益区域的 Value Function 给探索指路。如果只用一句话概括:GAVE 学的是“历史动作附近有没有预测价值更高的出价方式”。

符号说明

本文统一采用下面的记号。带“帽子”与“波浪线”的变量尤其容易混淆:x^\hat{x} 表示模型预测的量,x~\tilde{x} 表示基于历史数据生成的探索候选,x∗x^* 表示在当前公式条件下的最优量。

类别 符号 汉语意义
时间与序列 tt、TT tt 是当前决策时间步;TT 是一天或一条投放轨迹的最后时间步
时间与序列 MM、KK MM 是论文公式中向前回看的步数,输入共含 M+1M+1 步;KK 是参考代码中的窗口长度,默认 K=20K=20
时间与序列 ii、ItI_t ii 是单个广告流量(Impression)的编号;ItI_t 是从投放开始到时间步 tt 为止到达的流量总数
轨迹 T\mathcal{T}、hth_t T\mathcal{T} 是一条完整投放轨迹;hth_t 是预测第 tt 步时可见的历史上下文
状态与动作 sts_t、ata_t sts_t 是第 tt 步的投放状态;ata_t 是日志中真实执行的历史动作
状态与动作 a^t\hat a_t、a~t\tilde a_t、at∗a_t^* 分别是模型预测动作、探索候选动作,以及给定损失下的最优预测动作
出价 viv_i、λt\lambda_t、bib_i 分别是流量价值、当前时间段的出价系数,以及最终竞价价格
竞价结果 xix_i、cic_i xi∈{0,1}x_i\in\{0,1\} 表示是否赢得流量 ii;cic_i 是赢得该流量后支付的成本
约束 BB、CC、CPAtCPA_t 分别是总预算、CPA 上限,以及截至时间步 tt 的累计 CPA
收益 rwtrw_t、StS_t、STS_T 分别是第 tt 步获得的即时收益、截至 tt 的累计业务 Score,以及整条轨迹的最终 Score
RTG GG、rtr_t、EtconvE_t^{conv} GG 是线上事先设定的整日目标;rtr_t 是第 tt 步开始时尚未完成的目标;EtconvE_t^{conv} 是截至 tt 的累计预期转化
业务目标 P(CPAt;C)P(CPA_t;C)、γ\gamma 分别是 CPA 超限惩罚函数,以及控制惩罚强度的指数
探索 β^t\hat\beta_t、wtw_t 分别是模型预测的动作缩放系数,以及历史动作与探索动作之间的学习权重
价值学习 r^t+1\hat r_{t+1}、r~t+1\tilde r_{t+1}、V^t+1\hat V_{t+1} 分别是历史动作的预测后续 RTG、探索动作的预测后续 RTG,以及高收益 Value 目标
损失函数 LBCL_{BC}、LrL_r、LaL_a、LeL_e、LvL_v、LoL_o 分别是行为克隆、RTG 预测、动作、Expectile、价值引导和总损失
模型参数 θ\theta、αr\alpha_r、α1…α4\alpha_1\ldots\alpha_4 θ\theta 是模型参数;αr\alpha_r 控制价值差对动作权重的影响;α1…α4\alpha_1\ldots\alpha_4 是各损失的组合权重
Expectile τe\tau_e、E\mathbb E、I\mathbb I τe\tau_e 是 Expectile 水平;E\mathbb E 表示求期望;I\mathbb I 是条件成立取 1、否则取 0 的指示函数
梯度 sg⁡(x)\operatorname{sg}(x) 使用 xx 的前向数值,但在该位置停止反向传播
价值评估 Q(h,a)Q(h,a) Critic 根据历史上下文 hh 与候选动作 aa 估计的完整未来价值

下标 t+1t+1 表示下一个时间步,≤t\le t 表示“截至并包含 tt”,<t<t 表示“严格早于 tt”。后文会在每个公式附近再次解释当时真正用到的符号,这张表主要是方便随时回查,读的时候不必把它背下来。

1 GAVE 与 Decision Transformer

在自动出价里,一天可以被切成若干决策时间步。论文的离线实验把 24 小时分成 48 步,每一步的做法是先给出一个统一的出价系数,再由它换算成每个流量的报价:

bi=λtvib_i=\lambda_t v_i

其中,viv_i 是第 ii 个流量的价值,例如预估转化率 pCVR;λt\lambda_t 是当前时间段的出价系数;bib_i 才是最终参与竞价的价格。

Decision Transformer 会把一次投放写成序列:

T=(r1,s1,a1,…,rT,sT,aT)\mathcal{T}=(r_1,s_1,a_1,\ldots,r_T,s_T,a_T)
  • T\mathcal{T}:一天或一个投放周期形成的完整轨迹。
  • sts_t:预算、剩余时间、历史消耗、流量和成本速度等状态。
  • ata_t:当时采用的出价系数,离线问题定义中就是 λt\lambda_t。
  • rtr_t:Return-to-Go(RTG),表示从当前位置往后还希望拿到多少收益。

模型用因果注意力读取历史,在当前位置预测动作:

a^t=fθ(r≤t,s≤t,a<t)\hat a_t=f_\theta(r_{\le t},s_{\le t},a_{<t})

这里,fθf_\theta 表示参数为 θ\theta 的因果 Transformer;r≤tr_{\le t} 和 s≤ts_{\le t} 分别表示截至当前时刻的全部 RTG 与状态;a<ta_{<t} 只包含当前时刻之前的动作,因为 ata_t 正是此时要预测的量。a^t\hat a_t 上方的“帽子”表示它是模型预测值。

更严格地说,Decision Transformer 是一种将决策轨迹表示成“收益目标—状态—动作”序列,并使用因果 Transformer,在目标收益与历史轨迹条件下自回归预测动作的决策模型。 判断一个模型是不是 DT,关键不只是看它有没有使用 Transformer,还要看它是否把轨迹当作序列、是否用 RTG 或类似目标作为条件,以及是否据此预测下一步动作。

DT 处理的仍是强化学习里的序列决策问题,所以通常被归入离线强化学习;它绕开了传统 RL 常见的 Bellman 迭代和策略梯度,把离线轨迹改写成带收益条件的监督式序列建模问题。

普通 DT 的动作损失接近行为克隆:

LBC=(a^t−at)2L_{BC}=(\hat a_t-a_t)^2

LBCL_{BC} 是行为克隆损失:模型预测动作 a^t\hat a_t 与日志动作 ata_t 相差越大,平方误差越大。

这套做法很稳,前提是默认历史标签值得模仿。可日志里的动作只代表“发生过”,离“最优”还差一步。GAVE 的三项改造,正好对应三个缺口:

  1. Score-based RTG:先把转化、CPA 等业务要求写进同一个目标,避免训练指标和评测指标各说各话。
  2. Action Exploration:在历史动作附近生成候选动作,给模型多几个答案可选。
  3. Learnable Value Function:给候选动作一个较高但仍来自数据的价值锚点,减少盲目探索。

所以,GAVE 更像是长在 DT 上的一套机制。它以 Decision Transformer 为策略骨架,加入价值引导的策略改进机制,是一个 DT 变体。它保留了 DT-based model 的底子:RTG、State、Action 的条件序列和自回归动作预测原样不动,新增模块主要负责改进训练阶段的目标与监督信号。

三者和训练、推理的关系可以放在一张图里看:

图 1|GAVE 总体结构。上半部分展示 Causal Transformer、Score-based RTG、Action Exploration 与 Learnable Value Function;左下是离线训练和权重生成,右下是离线竞价评测环境。原图来源:GAVE 论文,第 4 页。

原图信息很完整,只是第一次读时不太容易分清哪些是训练辅助模块、哪些是线上输出。下面这张 Mermaid 图是我根据论文里的关系重新整理的简化示意:

flowchart TD
    A[完整离线投放轨迹] -->|计算业务 Score| B[Score-based RTG]
    B --> C[RTG / State / Action 序列]
    C --> D[Causal Transformer]
    D -->|线上保留| E[Action Head: 输出出价系数]
    D -->|训练辅助| F[Beta Head: 生成局部探索动作]
    D -->|训练辅助| G[Value Head: 学高收益 Expectile]
    F --> H[Return Predictor 比较历史动作与探索动作]
    G -->|高价值目标| H
    H -->|价值差决定权重| I[Action Loss]
    I -->|把较优候选学入策略| E
    E --> J[最终出价 = 系数 × 流量价值]

2 GAVE 的输入与输出

GAVE 最容易让人绕晕的地方,就是论文里那一堆预测头。看久了会发现,线上接口比训练图简单得多。

【线上推理输入】

预测当前动作时,输入是最近一段历史,加上当前 RTG 和 State:

(rt−M,st−M,at−M,…,rt,st)(r_{t-M},s_{t-M},a_{t-M},\ldots,r_t,s_t)

其中,MM 表示向前回看的时间步数。早于当前时刻的每一步都包含“当时的 RTG—状态—动作”,而当前的 ata_t 还没有产生,所以序列停在 (rt,st)(r_t,s_t);历史 Action 则已经存在。

【线上推理输出】

最终真正用于竞价的是一个动作:

a^t=λt\hat a_t=\lambda_t

这里输出的就是一个出价系数:pCVR 由外部模型估算,CPA 是约束,每个 Impression 的最终价格还要再乘一次。流量价值来自外部预估模型,GAVE 只根据投放进度调节价值的“放大倍数”。

例如,预估模型给出:

vi=pCVRi=0.02v_i=pCVR_i=0.02

pCVRipCVR_i 是外部转化预估模型给出的“流量 ii 发生转化的概率”,这里把它作为流量价值 viv_i。

GAVE 给出:

λt=100\lambda_t=100

最终出价就是:

bi=100×0.02=2b_i=100\times0.02=2

【离线训练输出】

训练阶段还会出现 β^t\hat\beta_t、r^t+1\hat r_{t+1}、r~t+1\tilde r_{t+1} 和 V^t+1\hat V_{t+1}。它们都是为了让策略训练得更扎实:探索头提出候选动作,Return Predictor 给两个动作估价,Value Head 提供高收益锚点。到了部署阶段,这些分支都用不上,不用在线搜一批动作,也不用每一步都跑 Value Head。

论文的线上系统还有一层稳定化处理:模型输出的是相对调整量 ata_t,最终 λt\lambda_t 会叠加过去两小时的窗口平均出价系数。换句话说,论文离线问题定义里把 ata_t 直接当成 λt\lambda_t,那是最简抽象;实际线上部署还会再做一层平滑。

3 核心技术创新

3.1 Score-based RTG:融入CPA约束

如果 RTG 只代表未来转化量,模型天然偏爱“转化更多”的轨迹,却不一定在乎 CPA 有没有超限。

考虑两种策略:

指标 策略 A 策略 B
转化数 100 90
总消耗 1200 900
CPA 12 10
CPA 上限 10 10

只看转化,策略 A 更好;站在广告主角度,A 已经违反 CPA 约束,策略 B 反而更可用。

GAVE 采用带惩罚的 Score:

S=P(CPA;C)⋅∑ixiviS=P(CPA;C)\cdot\sum_i x_i v_i

这里,SS 是整段投放的业务得分;xix_i 表示是否赢得第 ii 个流量;viv_i 是该流量的价值,因此 ∑ixivi\sum_i x_i v_i 是所有已赢流量的总价值。P(CPA;C)P(CPA;C) 是根据实际 CPA 和上限 CC 计算的惩罚系数。

实际 CPA 由总消耗除以总价值计算:

CPA=∑ixici∑ixiviCPA=\frac{\sum_i x_i c_i}{\sum_i x_i v_i}

其中,cic_i 是赢得流量 ii 后支付的成本,所以分子是总消耗,分母是总转化价值。

P(CPA;C)=min⁡{(CCPA)γ,1},γ=2P(CPA;C)=\min\left\{\left(\frac{C}{CPA}\right)^\gamma,1\right\},\qquad \gamma=2

γ\gamma 控制超限惩罚的强度,论文取 γ=2\gamma=2。min⁡{⋅,1}\min\{\cdot,1\} 把惩罚系数的上限截在 1:CPA 达标时不额外奖励,CPA 超限时才降低 Score。

当 CPA≤CCPA\le C 时没有惩罚;超过约束后,Score 按平方比例下降。代入上面的例子:

SA=100×(1012)2≈69.4S_A=100\times\left(\frac{10}{12}\right)^2\approx69.4
SB=90×1=90S_B=90\times1=90

目标经过这次改写以后,模型才会把 B 视为更好的轨迹。这里我想提醒一句,这是一种软约束:它的作用是把超限的代价抬高,属于软约束;真要每次都守住 CPA,还得看投放本身。

离线训练时的 RTG 可以根据完整轨迹回算;线上推理时的 RTG 则由系统事先设置并逐步维护。输入模型的 rtr_t 来自外部目标条件,模型对未来收益的预测是后面会看到的 r^t+1\hat r_{t+1}。

【离线训练:用整条轨迹的最终结果回算】

论文先计算截至时间步 tt 的累计 Score:

St=P(CPAt;C)⋅∑i=1ItxiviS_t=P(CPA_t;C)\cdot\sum_{i=1}^{I_t}x_i v_i

公式中,StS_t 是截至时间步 tt 的累计业务得分;CPAtCPA_t 是截至该时刻的累计 CPA;CC 是 CPA 上限;ItI_t 是截至 tt 已经到达的流量数量;xivix_i v_i 是第 ii 个流量被赢得后贡献的价值。

一条离线轨迹已经完整结束,因此最终 Score STS_T 是已知的。训练标签可以回算为:

rttrain=ST−St−1r_t^{train}=S_T-S_{t-1}

其中,rttrainr_t^{train} 是训练数据在第 tt 步使用的 RTG;STS_T 是整条历史轨迹最终真实取得的 Score;St−1S_{t-1} 是执行第 tt 步动作之前已经取得的 Score。它描述的是“这条历史轨迹从当前位置开始,后来实际又获得了多少”。

假设一轮历史投放的累计 Score 依次是 20、45、70、100,最终 ST=100S_T=100。第 3 步开始之前已经获得 45,因此:

r3train=100−45=55r_3^{train}=100-45=55

【线上推理:用事先设置的目标代替未知的最终结果】

线上的麻烦在于,今天最终能拿多少 Score 要等结束才知道,因此不能使用未知的 所以提前拿不到 STS_T。系统需要在一天开始时给定目标 GG,再根据已经完成的 Score 更新剩余目标:

rtonline=G−St−1r_t^{online}=G-S_{t-1}

这里,GG 是当天事先设定的目标 Score;rtonliner_t^{online} 是第 tt 步开始时仍未完成的目标;St−1S_{t-1} 是前 t−1t-1 步已经取得的累计 Score。

因此,线上输入序列中的历史 rr,就是模型在那些时间步真正使用过的“剩余目标快照”。以 G=100G=100 为例:

时间步开始时 此前累计 Score 当前输入的 RTG
第 1 步 S0=0S_0=0 r1=100r_1=100
第 2 步 S1=12S_1=12 r2=100−12=88r_2=100-12=88
第 3 步 S2=30S_2=30 r3=100−30=70r_3=100-30=70
第 4 步 S3=45S_3=45 r4=100−45=55r_4=100-45=55

到了第 3 步,模型看到的序列就是:

(100,s1,a1,  88,s2,a2,  70,s3)(100,s_1,a_1,\;88,s_2,a_2,\;70,s_3)

s1,s2,s3s_1,s_2,s_3 分别是前三个时间步的投放状态,a1,a2a_1,a_2 是已经执行过的动作。当前动作 a3a_3 还没有产生,所以序列停在 (r3,s3)(r_3,s_3),随后由模型预测 a^3\hat a_3。

整个更新循环可以写成四步:

  1. 第 tt 步开始前,根据已完成的 St−1S_{t-1} 计算 rt=G−St−1r_t=G-S_{t-1}。
  2. 把历史序列和当前 (rt,st)(r_t,s_t) 输入 GAVE,得到预测动作 a^t\hat a_t。
  3. 执行动作后观察新的消耗、转化和流量价值,更新累计 Score 为 StS_t。
  4. 下一步使用 rt+1=G−Str_{t+1}=G-S_t。

如果业务收益是可以逐步相加的即时奖励 rwtrw_t,那么更新也可以写成:

rt+1=rt−rwtr_{t+1}=r_t-rw_t

其中,rwtrw_t 是第 tt 步刚获得的即时收益。但 GAVE 的 Score 同时依赖累计 CPA,未必是简单的逐步相加量,因此更准确的写法仍然是 rt+1=G−Str_{t+1}=G-S_t。

3.2 Beta Head:动作可控探索

GAVE 额外预测一个探索系数:

β^t=Sigmoid(FCβ(ht))+0.5\hat\beta_t=Sigmoid(FC_\beta(h_t))+0.5

其中,hth_t 是预测第 tt 步动作时已经可见的历史上下文;FCβFC_\beta 是 Beta Head 中的全连接网络;SigmoidSigmoid 把网络输出压缩到 (0,1)(0,1);再加 0.5 后得到探索缩放系数 β^t\hat\beta_t。

因此:

0.5<β^t<1.50.5<\hat\beta_t<1.5

候选动作由历史动作缩放得到:

a~t=β^tat\tilde a_t=\hat\beta_t a_t

ata_t 是日志中的真实历史动作,a~t\tilde a_t 是将它乘以缩放系数后得到的探索候选动作。假设日志动作是 2.0,β^t=1.2\hat\beta_t=1.2,那么候选动作就是 2.4。这里特意拿真实历史动作 ata_t 做起点(换成模型自己的预测 a^t\hat a_t 再放大就心里没底了),目的就是让探索尽量贴着数据分布走。

随后,模型在同一个历史上下文里分别估计历史动作和探索动作对应的下一时刻 RTG:

r^t+1=GAVE(ht,at)\hat r_{t+1}=GAVE(h_t,a_t)
r~t+1=GAVE(ht,a~t)\tilde r_{t+1}=GAVE(h_t,\tilde a_t)

r^t+1\hat r_{t+1} 表示模型对“执行历史动作 ata_t 后还剩多少 RTG”的预测;r~t+1\tilde r_{t+1} 表示模型对“执行探索动作 a~t\tilde a_t 后还剩多少 RTG”的预测。两者都属于模型估值,环境里还没有真实结果来对照。

两个估值的差决定学习权重:

wt=Sigmoid(αr(r~t+1−r^t+1))w_t=Sigmoid\left(\alpha_r(\tilde r_{t+1}-\hat r_{t+1})\right)

wt∈(0,1)w_t\in(0,1) 是两个监督动作之间的学习权重,αr\alpha_r 控制它对预测 RTG 差值的敏感程度。探索动作的预测 RTG 越高,wtw_t 越接近 1。

Action Loss 同时参考历史动作和候选动作:

La=(1−wt)(a^t−at)2+wt(a^t−a~t)2L_a=(1-w_t)(\hat a_t-a_t)^2+w_t(\hat a_t-\tilde a_t)^2

LaL_a 是动作损失:第一项让预测动作 a^t\hat a_t 靠近历史动作 ata_t,第二项让它靠近探索动作 a~t\tilde a_t,wtw_t 决定两者各占多少权重。

如果把 wtw_t、ata_t 和 a~t\tilde a_t 暂时当作常数,最优预测动作其实就是两者的加权平均:

a^t∗=(1−wt)at+wta~t\hat a_t^*=(1-w_t)a_t+w_t\tilde a_t

a^t∗\hat a_t^* 上标的星号表示:当 ata_t、a~t\tilde a_t 和 wtw_t 固定时,使当前动作损失最小的预测动作。

例如,历史动作 2.0、候选动作 2.4,预测 RTG 分别是 100 和 110,取 αr=0.2\alpha_r=0.2 时有 wt≈0.88w_t\approx0.88,最后的学习目标约为:

a^t∗≈0.12×2.0+0.88×2.4=2.35\hat a_t^*\approx0.12\times2.0+0.88\times2.4=2.35

这就是 GAVE 超越纯行为克隆的关键:它没有直接把 2.4 当成绝对真相,而是根据价值差,把策略从 2.0 平滑地推向 2.4。

3.3 Value Function:指引探索方向

光有 Beta Head 还不够:它能产生新动作,却不知道 但往大调还是往小调,它心里没数。GAVE 为此引入一个序列 Value Function,希望学习当前历史条件下“较高的未来收益水平”。

论文用期望回归(Expectile Regression)训练它:

Le=E[∣τe−I(rt+1−V^t+1<0)∣(rt+1−V^t+1)2]L_e= \mathbb E\left[ |\tau_e-\mathbb I(r_{t+1}-\hat V_{t+1}<0)| (r_{t+1}-\hat V_{t+1})^2 \right]

其中,LeL_e 是 Expectile 损失;rt+1r_{t+1} 是离线轨迹中的真实后续 RTG;V^t+1\hat V_{t+1} 是 Value Head 预测的高收益水平;E\mathbb E 表示对训练样本求平均;I(⋅)\mathbb I(\cdot) 是指示函数。本文将 Expectile 水平写成 τe=0.99\tau_e=0.99,以免与前文的轨迹符号混淆。当真实 RTG 高于预测时,低估会乘上 0.99;当真实 RTG 低于预测时,高估只乘上 0.01。相同误差幅度下,模型会更认真地追逐高收益样本,因此学到的不是平均回报,而是历史条件分布中偏高的 Expectile。

它不是严格意义上的“所有动作最优值”。论文把 VV 写成 arg⁡max⁡ar\arg\max_a r,从数学符号上说也不够严谨:arg⁡max⁡\arg\max 返回的是动作,Value 应该对应最大收益。我更愿意把 V^t+1\hat V_{t+1} 理解成一个锚点,来自离线数据的高收益区域。

探索动作的预测 RTG 再向这个锚点靠拢:

Lv=(r~t+1−sg⁡(V^t+1))2L_v=(\tilde r_{t+1}-\operatorname{sg}(\hat V_{t+1}))^2

LvL_v 是价值引导损失;r~t+1\tilde r_{t+1} 是探索动作对应的预测后续 RTG;sg⁡(V^t+1)\operatorname{sg}(\hat V_{t+1}) 表示把 Value 预测当作固定目标,不让这条损失路径反向修改该目标。

梯度路径是:

Lv→r~t+1→a~t→β^tL_v\rightarrow\tilde r_{t+1}\rightarrow\tilde a_t\rightarrow\hat\beta_t

于是 Beta Head 就有了方向,沿着 Return Predictor 认为更接近高价值的那一侧调整。

3.4 总结四个 Loss

完整目标是:

Lo=α1Lr+α2La+α3Le+α4LvL_o=\alpha_1L_r+\alpha_2L_a+\alpha_3L_e+\alpha_4L_v

LoL_o 是最终联合优化的总损失;LrL_r、LaL_a、LeL_e、LvL_v 分别负责 RTG 预测、动作学习、Expectile Value 学习和价值引导探索;α1\alpha_1 到 α4\alpha_4 是控制四项相对重要性的超参数。

Loss 主要作用 直观理解
LrL_r 拟合历史动作对应的真实 RTG 先学会给动作估价
LaL_a 在历史动作与探索动作之间加权学习 把较优候选吸收到策略里
LeL_e 用 Expectile Regression 学高收益 Value 给探索一个有数据依据的高目标
LvL_v 让探索动作的预测 RTG 接近 Value 训练 Beta Head 找方向

算法 1|GAVE 的完整优化流程:采样一个训练 Batch,得到动作、Beta、Value 与 RTG 预测,计算四项损失,再联合更新模型参数。原图来源:GAVE 论文,第 6 页。

这张伪代码把前面的模块串成了一次参数更新:LrL_r 先提供动作估值能力,LeL_e 学习高收益锚点,LvL_v 把锚点传给探索动作,最后 LaL_a 把历史动作和探索动作的比较结果写回策略。

一次训练迭代的顺序大致是:从离线轨迹采样一个 Mini-batch;Transformer 同时输出动作、Beta、Return 和 Value;算出候选动作及其预测 RTG;再把四个 Loss 合起来做一次反向传播。

公式里对 M+1M+1 个时间步求平均,不代表环境每走 MM 步才更新一次 跟“环境每走 MM 步才更新一次”是两回事。序列长度、环境时间步和优化器步数,本来就分属三件事。参考实现的窗口长度是 20,一个 batch 是 128 条序列,那么一次前向最多会让 128×20=2560128\times20=2560 个有效位置共同贡献损失,随后执行一次 backward 和一次参数更新;Padding 位置由 Mask 排除。

4 模型实现与实验结果

4.1 模型架构和序列细节

从结构上看,GAVE 可以理解成一个 decoder-only 风格的 Causal Transformer。这里的 decoder-only 说的是它用因果自注意力、看不了未来,跟生成自然语言没什么关系。

论文实验使用 8 层 Transformer、16 个 Attention Heads;官方启动脚本把 Hidden Size 设为 512。RTG、State、Action 分别线性映射成向量,再按照时间顺序交错排列,送进因果注意力网络。

官方实现中的几个预测头很直接:

  • Action Head:512 → 1。
  • Beta Head:512 → 16 → 8 → 1 → Sigmoid,最后加 0.5。
  • Return Head:512 → 128 → 16 → 1。
  • Value Head:512 → 128 → 16 → 1。

表 1|论文使用的两套数据。两者都包含 479,376 条轨迹、9,987 个投放周期,每条轨迹 48 步;Sparse 数据的总转化范围只有 0~57,远低于普通 AuctionNet 的 0~1512。原图来源:GAVE 论文,第 6 页。

这张表解释了为什么论文单独强调 Sparse 场景:状态维度和动作维度没有变化,真正变化的是转化信号密度。价值估计在稀疏反馈下更容易不稳定,因此 Sparse 数据更能暴露 Value Function 与动作探索的问题。

论文只说 MM 是超参数、输入包含 M+1M+1 个时间步;参考代码使用 K=20,所以按论文符号可以理解成 M=19M=19。工业部署部分也采用 20 步序列。

历史不足 20 步时使用左侧 Padding 和 Attention Mask。投放刚开始只有 3 步历史,输入大致是:

[PAD × 17, Step 1, Step 2, Step 3]
Mask = [0 × 17, 1, 1, 1]

Attention Mask 会把 Padding 挡在注意力之外,Loss 也只统计有效位置。这套实现把张量形状的问题解决了,训练分布的问题还留在那儿:标准轨迹通常能截出完整的 20 步窗口,线上开局却必然面对 1、2、3 步的短前缀。后面会看到,这里其实可以单独做成一个很实用的改进。

官方仓库已经公开。理解实现时,优先看三处就够了:

  • dt.py:Backbone、四个预测头、探索分支和 Loss。
  • utils.py:轨迹切片、Score-based RTG、Padding 和 Mask。
  • main_train_test.py:训练入口和默认配置。

4.2 实验结果

【不同预算下的总体得分】

表 2|不同预算比例下的性能比较。粗体是最高分,下划线是最优基线,星号表示相对最优基线的双侧 t 检验达到 p<0.05p<0.05。原图来源:GAVE 论文,第 7 页。

GAVE 在两套数据和五档预算比例下都取得表中最高分,相对最优基线的提升范围为 1.30%~6.75%。

【训练目标与评测目标是否对齐】

表 3|AuctionNet-Sparse、100% 预算下的目标对齐实验。行表示训练 RTG 使用的 Score,列表示评测 Score;S1S_1 不惩罚 CPA,S2S_2 使用二次惩罚,S3S_3 使用五次惩罚。原图来源:GAVE 论文,第 8 页。

每一列的最好结果都出现在训练目标与评测目标一致的位置:评测 S1S_1 时用 S1S_1 训练最好,评测 S2S_2 时用 S2S_2 训练最好,评测 S3S_3 时用 S3S_3 训练最好。

【探索权重怎样随训练变化】

图 2|AuctionNet 上的参数分析。蓝线是总损失,绿线是探索权重 ww。原图来源:GAVE 论文,第 8 页。

训练早期 ww 大约停留在 0.5,说明模型还尚未没明显偏向探索动作;大约 6,000 步以后,ww 快速上升并稳定在较高水平,同时总损失继续下降。这说明模型逐渐把更多权重分给探索动作。

【每个模块分别贡献了什么】

图 3|100% 预算下的消融实验。GAVE-V 去掉 Learnable Value Function;GAVE-VA 同时去掉 Value Function 与 Action Exploration;DT 则去掉 GAVE 的全部改造。原图来源:GAVE 论文,第 8 页。

两套数据上的排序都表现为 GAVE > GAVE-V > GAVE-VA > DT。结合论文定义,可以把这组结果理解为:Score-based RTG 提供目标对齐,Action Exploration 带来超出历史行为的候选,Learnable Value Function 再为候选动作提供方向。不过,消融结果仍建立在论文的调参与评测设置上;Value Function 在理论上是否消除了 OOD 高估,这里还没有结论。

4.3 训练与上线是两套复杂度

训练阶段看起来很重:历史动作和探索动作要分别经过预测分支,Beta、Return、Value 都要参与 Loss。推理阶段却只保留主干和 Action Head,输出当前出价系数,再与每个流量的价值相乘。

一次真实投放可以简化成下面的循环:

  1. 汇总剩余预算、剩余时间、当前 CPA、流量速度、消耗速度和历史出价等状态。
  2. 设定并更新剩余 Target RTG。
  3. 拼出最近 20 个时间步,不足部分 Padding。
  4. GAVE 输出当前动作,经线上平滑后得到 λt\lambda_t。
  5. 每个流量使用 bi=λtvib_i=\lambda_t v_i 出价。
  6. 时间步结束后,回收消耗、转化和状态,更新 RTG,进入下一步。

表 4|论文的线上 A/B 测试。Nobid 以预算内最大化转化为目标,Costcap 同时关注 CPA/ROI 约束。原图来源:GAVE 论文,第 9 页。

论文报告的线上 A/B 测试持续 5 天,每个广告计划将 25% 的预算和流量分给基线与 GAVE。Nobid 场景的转化提升为 8.0%,Costcap 场景的转化提升为 3.6%,同时 Costcap 的 CPA 有效率提升 1.9%。

5 官方代码里值得先处理的几个问题

5.1 训练与评测的 CPA 惩罚不一致

本节讨论的是训练、在线 RTG 更新与最终评估之间的目标错位:训练数据构造和在线 RTG 更新实际采用较宽松的线性 CPA 惩罚,最终评估则采用更严格的平方惩罚,这可能使模型在训练中偏向更激进的出价,再由预算截断、在线状态反馈和最终评分进行限制,从而在原有配置下取得更高的出价 Score。

论文和评测代码使用平方惩罚:

Peval=min⁡{(CCPA)2,1}P_{eval}=\min\left\{\left(\frac{C}{CPA}\right)^2,1\right\}

PevalP_{eval} 是评测阶段使用的 CPA 惩罚系数,CC 是 CPA 上限,CPACPA 是整段投放的实际单位转化成本。

但官方 utils.py 先算了 curr_penalty = curr_coef ** 2,后面条件分支却返回了 curr_coef,使训练数据实际上采用:

Ptrain=min⁡{CCPA,1}P_{train}=\min\left\{\frac{C}{CPA},1\right\}

PtrainP_{train} 是参考代码实际生成训练 RTG 时使用的惩罚系数。它只使用一次方比例,因此与上面的平方惩罚不是同一个目标。

当 CPA=12,C=10CPA=12,C=10 时,训练惩罚约为 0.833,评测惩罚约为 0.694。GAVE 的核心卖点之一是让训练目标对齐评测指标,这个实现差异反而破坏了目标对齐。

同类写法也出现在 dt.py 的 getScore() 中,推理阶段的 RTG 更新也要一起检查。

对照实验结果。 此前,我针对 GAVE 训练目标与评估指标中 CPA 惩罚不一致的问题进行了对照实验,将训练及在线 RTG 更新中的线性惩罚统一为评估使用的平方惩罚,并同时修复了终止状态的花费计算。在相同训练配置、三个随机种子及统一评估环境下,两版分别按验证集选择最佳检查点;100% 预算时,修复后的平均 Score 从 142.277 降至 123.866,下降约 12.94%,CPA 超限率略有降低,但花费和转化量也随之下降。

不过,两版均固定训练至 5,000 步时,修复版反而提高了约 10.40%。

5.2 Value Loss 对超参数敏感

官方 README 明确提醒,论文版 Loss 对超参数非常敏感,原因可能是 Value Function 的估计精度会显著影响最终效果。仓库还保留了一个不使用 Learnable Value Function 的简化 Loss:通常更稳定,但充分调参后的上限可能不如原方法。

它直接指向 GAVE 的核心风险:探索动作没有环境真值,Return Predictor 和 Value Head 只要一起高估,就可能互相强化错误判断。

从模型选择角度看,我甚至有一个更尖锐的判断:作者很可能优先采用了对原始 Loss 精细调参后、最终展示效果最好的那一版,而不是稳定性最强的结果;这种选择有对当前验证集、检查点选择或评测设置过拟合的风险。换句话说,论文中的性能峰值,换一组超参数、随机种子或训练步数后未必还能稳定复现。

5.3 反事实动作是怎样构造的

这段逻辑只发生在训练阶段。线上推理时,模型只用主干网络和 Action Head 输出当前动作,不再计算 Beta,也不会为了探索动作再跑一次 Transformer。对应实现

【第一遍 Forward:给真实历史动作估值】

一次训练会从日志里截取一个长度为 20 的窗口。为了简化,假设窗口里只有三个时间步:

(r1,s1,a1,  r2,s2,a2,  r3,s3,a3)(r_1,s_1,a_1,\;r_2,s_2,a_2,\;r_3,s_3,a_3)

第一遍 Forward 使用日志中的真实动作 a1,a2,a3a_1,a_2,a_3,输出这些动作对应的预测 RTG,记作 r^k+1\hat r_{k+1}。这条分支还能用日志里的真实 RTG 监督,因此至少在历史动作附近有真实标签约束。

【第二遍 Forward:把真实动作换成探索动作】

Beta Head 会给窗口里的每个动作生成一个缩放系数:

a~k=β^kak,k=t−M,…,t\tilde a_k=\hat\beta_k a_k,\qquad k=t-M,\ldots,t

例如:

[a1,a2,a3]=[2.0,1.5,2.5][a_1,a_2,a_3]=[2.0,1.5,2.5]
[β^1,β^2,β^3]=[1.2,0.8,1.1][\hat\beta_1,\hat\beta_2,\hat\beta_3]=[1.2,0.8,1.1]

那么探索动作就是:

[a~1,a~2,a~3]=[2.4,1.2,2.75][\tilde a_1,\tilde a_2,\tilde a_3]=[2.4,1.2,2.75]

代码接着保持原来的 RTG 和 State 不变,只把整个窗口的 Action Embedding 换成 a~1,a~2,a~3\tilde a_1,\tilde a_2,\tilde a_3,再运行一次 Transformer,得到探索动作的预测 RTG r~k+1\tilde r_{k+1}。这样做是为了在一次批量计算中给窗口内所有位置都造出候选动作,不必逐个时间步重复运行模型,也不必把这些动作真的投放到线上环境。

【两个预测结果怎样影响最终策略】

模型用历史动作与探索动作的预测 RTG 差值计算权重:

wk=Sigmoid(αr(r~k+1−r^k+1))w_k=Sigmoid\left(\alpha_r(\tilde r_{k+1}-\hat r_{k+1})\right)

如果探索动作的预测 RTG 更高,wkw_k 就会增大,Action Head 会更多地学习探索动作;否则就更多地保留历史动作。对固定的 wkw_k 和 β^k\hat\beta_k,Action Loss 给出的目标可以写成:

a^k∗=(1−wk)ak+wka~k=ak[1+wk(β^k−1)]\hat a_k^*=(1-w_k)a_k+w_k\tilde a_k =a_k\left[1+w_k(\hat\beta_k-1)\right]

这个公式能直接看出 Beta 如何传到最终策略。假设 β^k=1.5\hat\beta_k=1.5:

wkw_k Action Head 学习的目标
0.1 1.05ak1.05a_k
0.5 1.25ak1.25a_k
0.9 1.45ak1.45a_k
1.0 1.50ak1.50a_k

因此,Beta 接近 1.5 还不等于最终动作一定放大到 1.5 倍;还要看 wkw_k 是否同时较高。但参考代码在计算 wkw_k 时使用了很敏感的放大系数 100,探索 RTG 只要比历史 RTG 高一点,wkw_k 就可能很快接近 1。

【真正的问题:动作改了,代价却没有完整进入序列】

假设真实日志是:

s₁:剩余预算 100
a₁:出价系数 2.0
s₂:剩余预算 80

如果 Beta 把 a1a_1 放大到 3.0,更高的出价可能赢得更多流量、花掉更多预算,真实的下一状态也许应该是“剩余预算 60”。但探索分支仍然沿用日志中的 s2s_2 和 r2r_2,于是模型实际看到的是:

s₁:剩余预算 100
ã₁:出价系数 3.0
s₂:剩余预算仍然是 80

因果 Attention Mask 只能保证模型看不到未来,不能保证“被修改的动作”和“后续 State、RTG”在环境动力学上彼此一致。论文公式按单个时间步描述 ht+a~th_t+\tilde a_t,而参考代码为了批量训练修改了窗口内的整段动作;这个具体实现细节及其状态一致性问题,论文没有展开讨论。

【复现实验中的 Beta 饱和现象】

在本文的复现实验中,绝大多数 Beta 都大于 1,并且大量集中在上限 1.5 附近。这说明 Beta Head 没有学到一个位于区间内部的细致策略,而是不断把探索动作推向允许范围的最大值。它更像是在学习:

a~t≈1.5at\tilde a_t\approx1.5a_t

这里需要区分一个细节:输入探索分支的历史 RTG 没有改变,但模型输出的 r~t+1\tilde r_{t+1} 仍会重新预测,所以代码并不是直接把“高出价后的 RTG”固定成原值。真正的问题是,探索动作没有真实环境标签,动作提高后造成的预算消耗、CPA 恶化和后续流量变化也没有被完整重建。Return Predictor 只能依靠日志相关性外推,一旦它在探索区域里认为“动作越大,RTG 越高”,Value Loss 就会继续把 Beta 推向 1.5。

更值得注意的是,Value Loss 只冻结了 Value 目标,并没有冻结探索分支的 Return Predictor。因此,损失既可以通过调整 Beta 来降低,也可以通过改变 Transformer 和 Return Predictor 对探索动作的估值来降低。这可能形成一条自我强化链路:

Beta 把动作推高
→ Return Predictor 给出更高的探索 RTG
→ w 接近 1
→ Action Head 学习更高的动作

6 总结

GAVE 是一个以 Decision Transformer 为策略骨架的离线自动出价框架。它用 Score-based RTG 把转化收益与 CPA 约束写进同一训练目标,再通过 Beta Head 在历史动作附近生成候选动作,并结合 Return Predictor 与 Learnable Value Function 判断候选动作是否值得被最终策略吸收。四项损失共同参与训练,而线上推理仍然只需要输出出价系数,因此训练阶段的复杂机制不会全部进入实时竞价链路。

论文的对齐实验表明,RTG 定义与评测指标保持一致很重要,动作探索和 Value Function 也都对论文报告的最终得分有增益;但本文的修复对照实验进一步说明,形式上的目标对齐未必能在原训练配置下直接提升性能,目标改变后往往还需要重新选择训练阶段与超参数。与此同时,我也想提醒 GAVE 的适用边界:CPA 仍是软约束,Target RTG 需要外部设置,局部缩放能把 OOD 的冲击压小,但彻底消掉还不现实;探索动作的好坏又依赖模型自身的反事实估值。官方参考代码还存在训练与评测惩罚不一致、Value Loss 对超参数敏感等复现问题。

综合来看,GAVE 给出了一条清晰的技术路线:先用 RTG 对齐业务目标,再在历史策略附近进行受控改进,并用价值估计决定改进幅度。它说明离线序列模型可以绕开线上试错,尝试超越历史策略;至于能不能可靠地识别出“真正更好的动作”,我目前觉得这仍是这类方法最核心的挑战。

参考资料