☰
Bid2X:用基础模型重构广告竞价环境建模,实现快速跨场景迁移
2026/10/19 2:37:59 网站建设 项目流程

做广告投放和竞价策略的同学应该都有同感:出价算法跑得稳不稳,很大程度取决于你对竞价环境的感知准不准。同样是公式,今天的流量池里游刃有余,明天换个广告账户立刻被打回原形。我们这次在 KDD'25 上公开的 Bid2X 工作,就是想给广告竞价环境建模找一个更通用的解法:借用基础模型的预训练、微调与上下文学习范式,让环境模型能快速适应不同市场、不同账户和不断变化的对手行为。这篇博文把我们从动机到落地的完整路径拆一遍,包括模型的序列化设计、训练目标、评估口径以及踩过的坑,适合在做智能出价、广告模拟器和离线策略评估的同学参考。

1. 为什么需要重新审视竞价环境建模

1.1 竞价环境建模到底在干什么

在广告竞价场景里,广告主和广告投放平台需要根据每个请求决定出价 bid。环境模型要做的事情,是在给定"请求上下文 + 平台出价 + 市场状态"的前提下,预测这次竞拍会如何演化:能不能赢下广告位、如果赢了要付出多少钱、周围对手的价格水平在哪里。这个功能听起来很底层,但几乎所有上层策略都依赖它。智能出价策略要用它估计出价的边际价值,决定是提价抢量还是降价控本;RL 类策略在训练时要把环境模型当作模拟器环境,否则在线试错成本完全无法接受;异常检测和预算分配也要靠环境预测来判断市场是否过热或过冷,从而调整流量结构。

一句话概括,环境模型是出价系统的雷达。雷达不准,后面的决策都是带着眼罩开车。传统工程上,主流做法是拟合一个获胜概率函数 P(win|bid, features),再配一个成交价期望 E(price|win, bid)。这个组合看起来直接,但真正用起来,在动态市场里非常脆弱。随着广告主数量变多、投放策略变复杂,这种"单点估计"的思路越来越难支撑上层精细化出价的需求。

1.2 传统方法的三道坎

第一道坎:环境是非平稳的。广告市场由大量参与者动态组成。某个大广告主突然提价,某个流量供应商调整底价,一个促销活动开启,都会让市场分布整体偏移。你今天把模型拟合得再好,第二天可能就出现系统性偏差。很多团队只能靠高频重训或者人工规则兜底,但这样做成本高、响应慢,还会带来模型不稳定,今天生效的规律明天就失效。

第二道坎:反馈是截断且不对称的。赢下竞拍时,你能看到最终成交价,还能反推出对手的部分信息;输掉竞拍时,你并不知道对手的真实出价,只知道"没赢"这个事实。这属于典型的删失数据(censored data)。如果不特别处理,模型会偏向胜者样本,形成严重的 win/loss bias。更麻烦的是,你越舍不得花钱,模型看到的成功样本越少,它就越不了解市场高位,这类账户会持续陷入价格误判,出价策略也会越来越保守。

第三道坎:场景之间互相割裂。每个广告账户、每种投放形式的竞价分布都不一样,传统做法通常是一个账户一套模型,导致新账户冷启动非常痛苦:数据不够,模型乱跳。就算老账户积累了海量数据,也很难迁移给新账户和低预算账户使用。我们团队之前维护过多条业务线的环境模型,最大的感受就是:总是处于"补丁摞补丁"的状态,而不是有一个统一、可持续演化的建模范式。

1.3 基础模型视角为什么会更有想象力

回头审视时我们意识到,上面这三个坎,和自然语言处理、视觉领域近年来的发展轨迹惊人地相似。异构信息难以统一,那就把各类竞价信号统一成一个 token 序列;单任务小样本受限,那就用大规模预训练学习"市场动力学"的通用模式;新场景适配慢,那就用上下文学习加轻量微调实现快速迁移。Bid2X 的基本出发点,就是把这套已经被基础模型验证过的范式,平移到广告竞价环境建模中。

这里有必要说清楚:Bid2X 不是"给大语言模型喂一段 prompt,让它预测出价"的玩具,而是把竞价过程设计成一个可序列化的环境轨迹,用 Transformer 架构学习其中的跨场景通用模式。这个视角带来的直接收益有三点。第一,可以把不同广告业务、不同行业的竞价日志统一成同一种结构化序列。第二,预训练阶段可以积累一个"通用市场动力学"底座。第三,新账户只需要很少的数据做适配,甚至不做任何微调,靠上下文理解就能冷启动。理想很丰满,但落地时每一步都需要重新设计,下面按我们实际推进的顺序展开。

2. Bid2X 的整体设计:把市场动态写成一段上下文

2.1 核心建模对象再定义

我们做的第一件事,是把建模对象从"单点价格预测"升级成"环境轨迹预测"。过去,模型输入是 [请求特征, bid],输出是 P(win) 和 E(price)。Bid2X 的输入变成一段按时间排列的竞价事件序列,包含每次出价、竞拍结果、成交价、市场密度等上下文,输出则升级为未来一段时间的环境状态分布,包括下一轮获胜概率、成交价分位数、市场竞争强度变化趋势。

对比维度传统单点建模Bid2X 轨迹建模
输入当前请求特征 + 当前出价历史多轮竞价事件序列 + 当前动作
输出单点胜率、单点价格期望未来价格分位数、胜率、市场状态分布
多轮决策不支持支持
反事实推演难以支持天然支持
经验共享各账户独立跨场景预训练共享

这个变化带来的收益非常直接。反事实推演:想知道"如果上一轮没有提价,结果会怎样",模型可以基于同一段历史上下文,用不同动作分别向前推演,而不是重新训练。多轮决策:出价策略通常有预算约束,需要在一连串请求之间连续决策,环境模型必须理解长期动态,而不是只看当前单点。不确定性量化:模型输出的是分布而不是点估计,策略做决策时可以参考置信区间,避免极端误判。

2.2 万物皆 token:把竞价日志变成可训练序列

这是整个设计最关键的一步。我们把一次完整的竞价过程拆成若干事件,每个事件转换成 token,就像 NLP 里的 token 一样,所有结构化字段统一到同一个词表空间中。具体来说,序列中包含这样几类 token:market_begin 和 market_end 标记一段市场片段的起止,round_begin 标记一次竞价轮次的开始,ctx_* 编码广告请求的上下文特征,比如广告位尺寸、流量来源、时段、定向类目,bid_* 表示当前平台出价,win 和 loss 表示竞拍结果,price_* 表示赢下竞拍后的成交价,density_* 表示竞争密度等级。

一段样例序列大概长这样:

[ "market_begin", "ctx_slot_medium", "ctx_source_app", "ctx_hour_14", "round_begin", "bid_6_2", # 对数价格分桶,可以理解成 10^6.2 这个量级 "loss", "round_begin", "bid_6_5", "win", "price_6_8", "density_high" ]

连续特征怎么变成 token?我们统一先做 log 变换,再做分位数分桶。比如广告位成交价,先 log10 后,按全量语料的分位数切出 512 个桶,每个桶对应一个 token id。分桶数的选择有讲究:太大容易稀疏,太小丢精度。我们实际对比过 128、512、1024 三档,512 的性价比最好。这里有个反常识的结论:分桶 embedding 的效果普遍优于直接输入连续值。原因很简单,广告出价和成交价服从长尾分布,连续值回归会把大量模型容量花在拟合极少数高价样本上,而分桶相当于先给模型一个离散的注意力范围,显著提升了对中部和尾部区间的拟合质量。我们自己的对比实验里,同样模型结构,分桶 embedding 在成交价 MAPE 上比连续值输入低约 3 个百分点。

2.3 从"预测价格"到"预测环境轨迹"

序列化完成之后,模型内部表征承载的不再是孤立的价格,而是市场最近发生了什么、现在正在往哪走。Bid2X 的具体做法是:固定窗口长度,我们使用 48 轮竞价事件作为一段序列,对整段序列做双向编码,再对下一时刻的若干目标做多头预测。分布预测头共享同一个特征编码器,但输出分支相互独立。获胜概率头把下一轮 hidden state 映射成二分类 logit;成交价格分布头输出对数价格在 10、25、50、75、90 五个分位的预测值;市场强度头输出下一段窗口内竞争密度 token 的概率分布。

这个设置让基础模型不再只是一个胜率函数,而是变成了一个小型的市场动力学系统。下游 RL 出价策略做训练时,可以直接在这个环境模型上采样虚拟竞价轨迹,生成大量"如果我这样出价会怎样"的经验数据。我在实际使用中发现,环境模型和策略模型之间存在一个天然的"代差"问题:如果环境模型过于简单,策略很快就能找到它的漏洞,学到的策略在真实环境中并不可用。Bid2X 通过分布输出和随机采样缓解了一部分,但根本思路是:宁可让环境预测带一点合理的噪声,也不要让它输出过度自信的点估计。过度自信的环境模型比粗糙的环境模型更危险,因为它会误导策略进行激进的探索。

3. 核心模块拆解与实现要点

3.1 训练数据构造:日志字段怎么变成训练样本

数据来源是线上广告竞价日志。我们梳理出的核心字段和映射规则如下:

原始字段处理方式对应的 token / label
请求时间离散到小时ctx_hour_14
广告位尺寸类目编码ctx_slot_medium
流量来源类目编码ctx_source_app / ctx_source_web
定向类目粗粒度类目ctx_target_*
平台出价对数分桶bid_*
是否竞得标签win / loss
成交价对数分桶price_*
参与竞拍候选数分位数分桶density_high / density_mid

训练样本的构造有一点必须强调:样本不是按请求独立抽样的,而是按事件顺序拼接的。我们会从日志里切出连续的竞价序列,窗口滑动步长为 16 轮事件,这样保证样本之间有重叠,也方便扩大训练量。清洗环节同样重要,同一账户的日志不能全部无脑使用,要过滤掉策略探针流量和异常流量,比如出价低于底价的样本、内部测试标记样本、log-price 超过 6 个标准差的异常尖刺。不做清洗,模型会被这些噪声拖偏。

3.2 模型结构:为什么选择双向编码器

Bid2X 的 backbone 是一个多层 Transformer 双向编码器,参数规模在 1 到 3 亿之间。选择双向编码器而不是自回归 decoder,原因很实际。环境预测的核心是"看完历史,推断未来状态",不是开放式逐 token 生成,双向编码在每个位置都能结合前后信息,对价格突跳、连续失败、密度变化这些模式更敏感。多任务 head 在双向编码器上挂载也更方便,获胜概率、价格分位数、市场强度三个 head 共享一套表征,训练和推理都不用重复计算。推理延迟更好控制,历史序列只编码一次,多个 head 并行输出。

模型超参可以参考:12 层 Transformer、16 个注意力头、hidden size 768、feed-forward 2048、激活函数 GELU。这个规模在我们的流量条件下,单次推理延迟大约 3 到 5 毫秒,可以接受。预训练阶段采用类似 BERT 的 masked token prediction,随机 mask 掉序列中约 15% 的 token,让模型还原。但我们额外把被 mask 窗口内的 win/loss 统计量作为辅助目标,强迫模型不仅还原 token,还要理解市场局部状态。这里有个容易忽略的细节:mask 的时候不要同时轻易暴露 bid 和 price 的映射关系。比如你 mask 了 price,但 bid 还在上下文里,模型很容易偷懒按 bid 去还原 price,学不到真正的环境动态。我们的做法是,当 mask 价格类 token 时,本轮出价 token 也一起 mask 一部分,增加预测难度。

3.3 训练目标:三部分 Loss 怎么组合

Bid2X 的目标函数是三部分 Loss 的加权和。获胜概率头使用交叉熵损失,成交价格分位数头使用 Pinball Loss,也叫分位数损失,市场强度头使用多分类交叉熵。我们为什么不用 MSE 去预测价格?成交价长尾太严重,MSE 会被极端高价样本拖拽,导致预测值系统性右偏,胜率校准也会跟着变差。分位数损失天然对长尾更鲁棒,还能一次性输出五个分位点,为策略提供完整的置信区间。

def quantile_loss(y_true, y_pred, tau): # y_pred 是预测的第 tau 分位数,y_true 是真实值 diff = y_true - y_pred return torch.mean(torch.maximum(tau * diff, (tau - 1) * diff))

三部分 Loss 的权重我们初始设置是 1 : 1.2 : 0.3。分位数 Loss 权重高一些,因为价格预测是影响出价决策最大的一环。市场强度分类作为辅助任务,权重不用太高,它的作用更多是给编码器提供环境状态信号。Loss 权重不是拍脑袋定的,我们是用一个小验证集做网格搜索选出来的,搜索空间不大,每组权重跑一天离线回放,取成本达成率和平准误差最好的组合。

3.4 预训练、微调与在线适配:三个阶段缺一不可

我们最终落地的训练流程分成三个阶段。第一阶段是通用预训练,在业务内多条产品线的历史竞价日志上做 masked token 还原,训练大约两周,数据量是数十亿条竞价事件。这个阶段的目标不是某一个账户,而是让模型理解市场动态的通用规律。第二阶段是账户级微调,对每个业务账户,用最近 90 天日志做轻量微调,学习率设得非常小,1e-5 量级,训练轮数控制在 2 到 3 轮,防止破坏预训练学到的表征。第三阶段是在线适配,模型部署后维护一个滚动窗口,也就是最近 7 天日志,每隔 30 分钟做一次增量 embedding 更新。这一步对维持长期效果非常关键,尤其是遇到大促、大型广告主预算调整这类非平稳事件。

我们做过一个消融测试:跳过账户级微调直接上线。结果是新账户前 3 天效果明显不如老模型,原因是没有抓住该账户特定的价格基线和竞争密度分布。但加入账户级微调后,效果很快反超,说明"通用底座 + 局部适配"的假设在这类任务上是成立的。在线适配还有一个工程细节:增量更新只更新 embedding 层和一些低层参数,不去动顶层预测头,这样可以避免更新过程中出现激烈震荡,也降低了回滚难度。如果版本需要回退,整个模型文件在分钟级内可以切回旧版本。

4. 评估方法:环境模型最怕"离线好看、线上翻车"

4.1 离线指标:不能只看 AUC 和 MAE

环境模型的离线评估,最忌讳只看单点指标。我们的评估分三层。第一层是预测准确性,成交价看 MAPE、MAE,获胜概率看 AUC 和 ECE,也就是校准误差。这里要特别关注 ECE,因为出价策略非常依赖概率校准,校准差,策略做成本控制时就会频频越界。第二层是分布一致性,把模型生成的价格分布和真实成交价分布对比,计算 Wasserstein 距离和 KS 统计量。这是在验证整体形状是否一致,而不仅仅是均值接近。第三层是下游任务效果,用 Bid2X 替代真实环境训练一个出价策略,再把该策略离线运行在真实日志上,评估成本达成率、eCPM、ROI。这比只看模型指标更接近业务关注点。

这里有一个我们踩过的典型例子:某次迭代模型 AUC 涨了 0.01,MAE 微微下降,我们以为稳了。结果把预测价格分布画出来,发现预测分布比真实分布明显更窄、更尖,说明模型低估了市场不确定性。这种模型一旦接进 RL 策略,策略会因为环境太"确定"而过度自信,出价非常激进。我们后来定了一条硬规矩:环境模型上线前必须通过分布一致性检验,否则就算单点指标再好看,也不允许进入下一环节。

4.2 模拟器回放一致性检验

另一个更严格的手段是回放测试。具体做法:把出价策略在历史某段时期的真实动作序列原封不动地喂给环境模型,让模型续写对应的成交回报,再把整条虚拟轨迹和真实回报做对比。这个测试的价值在于暴露多轮误差累积。单步误差小不代表多步误差小,因为环境模型在出价策略训练中会被反复调用,误差会像开车导航一样越偏越远。

我们当时遇到过一个典型案例:单步成交价误差只有 4%,非常漂亮,但回放 50 轮之后误差堆积到 20% 以上。追踪原因,是模型对竞争密度状态的响应滞后。历史上下文里已经连续出现 density_high,模型却仍按原有价格水平预测未来。解决办法是在 token 序列中加入更高频的密度状态转移标记,并在训练目标里加大对下一状态密度的权重。现在回放测试已经成为我们每次环境模型迭代的必选项,单步指标只能作为参考,回放一致性才是判断模型能不能进策略模拟器的主要依据。

4.3 在线 A/B 效果

在真实广告流量上,把 Bid2X 环境模块替换原有价格预估模型后,我们观察到的结果如下,均为脱敏后的相对值。成本达成率偏差,也就是实际成本与目标成本之间的距离,下降了约 38%。整体 ROI 提升了 5% 到 8%,不同账户之间有差异。新账户冷启动时间从约 7 天缩短到约 2 天。有意思的是,单看胜率绝对值变化并不明显,但成本达成率的稳定性提升非常显著。这正好符合我们对环境模型的定位:它不负责直接出价,而是让出价策略更了解市场,从而减少盲目加价和预算浪费。

这里也想提醒一句:在线 A/B 的评估周期不要太短。市场本身有周期性波动,只跑两三天,很容易把市场波动当成模型效果。我们内部至少跑 7 天,并且要做方差分析,确认指标提升不是随机波动带来的。

5. 踩坑指南:竞价环境建模最容易翻车的 5 个细节

5.1 数据穿越问题

第一优先级,也是最致命的。做环境模型最常见的问题,就是拿到一份已经包含"事后结果"的日志,直接拿去训练。比如日志里的"最终成交价"字段,如果你不小心把它当成特征输入,离线评估的 AUC 能到 0.95,上线立刻失效,因为决策时点上你根本不可能知道这个值。我们的处理办法是建立一份"决策时点特征白名单"人工审查机制,所有特征必须是在一次竞价决策时可真实获得的;事后字段全部 shift 到标签侧或者直接剔除。我建议每个团队至少安排一次专项审查,把训练特征里的潜在未来信息全部揪出来。宁可少用特征,也不能让模型偷看答案。

5.2 Win/Loss 不对称

输拍样本没有价格标签,很多人第一反应是把它们过滤掉,只训练 win 样本。这种做法的后果是模型只学会了"赢下来的世界",价格预测会严重偏置。我们的做法是保留所有样本,win/loss 作为序列中的 token 参与训练,价格预测头只在 win 样本上计算损失。同时采用删失回归的思路:对于 loss 样本,我们知道真实成交价一定高于出价上限,所以把"loss 时刻的价格应高于当前出价"作为软约束加入损失。

提示:不要简单过滤 loss 样本。过滤的同时,你等于把"失败经验"也过滤掉了,环境模型会越来越乐观,最终误导出价策略过度激进。

5.3 非平稳市场带来的 Embedding 漂移

模型上线几周后指标悄悄变差,这是所有竞价环境模型都会经历的衰老过程。最大原因就是市场变了,模型没跟上。我们做了三件事来应对。第一,每天监控预测残差分布,如果残差均值连续 3 天偏离 0,则触发重训流程。第二,保留一组"市场锚点样本",也就是最近 7 天的竞价记录,用于增量训练时稳定新老分布对齐。第三,设置自动回退机制,新版本在灰度期间如果预测分布统计量异常,自动回滚到稳定版本。这三件事配合起来,能极大缓解大促、预算迁移等非平稳事件带来的效果滑坡。

5.4 价格长尾分布

MSE 在竞价价格预测中基本不可用,除非先对价格做非常强的平滑,否则训练的注意力全被极端值吸走。更好的方案是:对价格取 log 后再分桶做多分类,或者直接预测 log-price 的分位数。我们推荐输出 10、25、50、75、90 五个分位点,给策略提供不确定区间。从经验看,对数空间分桶在 512 档时效果最优,再往上提升有限,还会增加 embedding 参数量。

5.5 跨市场迁移的 Token 设计

"一个基础模型服务所有账户"很诱人,但 token 设计上要避免过多 ID 化特征。如果直接把账户 ID 做成 embedding,新账户没有对应的 ID 映射,冷启动直接失效。我们改用粗粒度行业、投放目标、流量场景等可泛化特征替代 ID 类特征,效果和原来基本持平,但新账户冷启动好很多。这符合基础模型设计的哲学:表示必须跨环境复用,而不是绑定单一实体。

为了方便排查,我把这 5 个问题整理成速查表:

问题典型表现处理方法
数据穿越离线指标亮眼,线上崩塌决策时点特征白名单
Win/Loss 偏差价格预测整体偏高或偏低保留 loss 样本,删失回归约束
非平稳漂移线上残差持续偏移残差监控 + 增量训练 + 自动回退
长尾分布均值误差小但分位数差对数变换 + 分位数 Loss
ID 特征过拟合新账户无法冷启动用泛化类目特征替代 ID

6. 写在最后:环境建模的下一个台阶

6.1 这个项目给我最大的颠覆

Bid2X 这个项目最让我意外的收获,其实不是某个具体指标,而是"基础模型视角"带来的工程简化。过去我们为每条业务线维护独立的环境模型和特征工程,每次接入新业务都要重复造轮子。现在统一成一套序列化框架后,新业务接入只需要完成字段映射和轻量微调,边际成本大幅下降。

另一个让我改观的点是环境模型在系统里的定位。以前我们总想着把价格预测得越准越好,后来发现,对于出价策略来说,市场不确定性本身也是信息。Bid2X 输出分位数和分布,让策略在做决策时知道"当前预测靠不靠谱",这比一个虚假的精确点更有用。在实验里我们还试过混合不同行业的日志做预训练,结果很有意思:跨行业的通用表征能力比自己行业单独训练更强。这个结论还在验证中,但至少说明广告竞价环境是可以被"通识化"的,前提是序列设计得足够合理。

6.2 如果现在重新做一遍,我会重点投入什么

如果让我重新做一遍,我会把更多时间放在数据治理和特征白名单审查上,而不是一开始就调模型结构。环境建模的瓶颈,很多时候不在模型容量,而在你喂进去的观测是否真实反映了决策时点的信息状态。另一个我会提前做的是分布一致性评估,把 KS 检验和回放测试嵌入到每次迭代的流程里,而不是靠上线后踩坑才补上。

如果你也在做类似的环境建模,我的建议是先从数据流和数据质量下手,不要急着上大模型。把观测、动作、反馈三要素整理清楚,再用预训练和上下文学习的方式组织起来,效果自然会来。最后再分享一个小技巧:给在线环境模型配一个残差分布监控面板,你会比自己想象中更快发现市场突变。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询