我用人们真实跑过的 A/B 测试数据训练了一个决策模型,而不是去问另一个模型怎么看。它在 90% 的情况下都能避开最差的变体,而且我最初的基准测试完全测了个寂寞,笑死。
榜单
| 条目 | 得分 |
|---|---|
| ModernBERT, 2015 tail | 0.704 |
| 已撤回:2015 尾部数据 | 0.704 |
| ModernBERT, holdout | 0.637 |
| ModernBERT, exploratory | 0.624 |
| MSE | 0.637 |
| MSE, more data | 0.724 |
| Bradley-Terry | 0.775 |
| Bradley-Terry, more data | 0.787 |
| VERA | 0.812 |
| Linguistic features | 0.544 |
| 人类 | ~随机水平 |
| Llama-3-8B LoRA | 0.469 |
| 邮件主题行 | 无公开数据 |
目前大概有 300 个基于新型决策模型构建的公开项目,我翻遍了其中属于“打分和排序”类的(一共 26 个),发现它们全都是直接去问模型怎么看来进行打分的。比如在八个质量维度上给这篇文章打分、评估文案的品味、判断这篇文档是否相关,诸如此类。
但这根本不算数据吧?那只是模型的个人观点强行贴了个数字上去,说实话,整个品类都是建在这个基础之上的。
所以我用别人实际测量出的真实结果训练了一个模型,权重已经开源了,想玩的话可以去看看:NovusEdge/vera-deberta-v3-large11[object Object], huggingface.co,Apache 2.0 协议,435M 参数,单次前向传播,专门给短篇说服性文案打分。基座模型是 com-kotobalabs/open-jev-deberta-v3-large22[object Object], huggingface.co,它本身是在带类型决策数据上预训练过的 DeBERTa-v3-large。
| 指标 | 本模型 | 已发表 SOTA | 人类 |
|---|---|---|---|
| 配对准确率(未见过的划分) | 0.812 | 0.544 | ~随机概率 |
| 仅干净样本对 | 0.797 | - | - |
| 避开最差变体 | 90.3% | - | - |
它基于来自 32,487 个随机标题实验的 62,695 个真实 A/B 测试分支进行训练,上面的每一个数字都来自模型从未见过的划分集。具体是怎么做到的写在下面,包括我最初的基准测试完全测了个寂寞那段(是的,我现在想起来还是有点不爽)。
事实证明正好有一个绝佳的数据集,而且从 2021 年起就公开摆在那里了。在 2013 年 1 月到 2015 年 4 月之间,Upworthy(没错,就是那个发“你绝对想不到接下来发生了什么”的 Upworthy)对他们的标题进行了 32,487 次随机 A/B 测试,真实流量,真实随机分配,涉及 5.38 亿次曝光分配。随后 Cornell 将全部数据以 CC BY 协议公开发布为 Upworthy Research Archive33Upworthy Research Archive, osf.io。每个标题变体、每次展示、每次点击都包含在内。
在短篇说服性文案领域,这基本就是最接近真值(ground truth)的数据了。
做法是:ModernBERT-large 加上回归头,目标是基于每次测试自身均值中心化后的收缩 logit 点击率。顺便说一句,中心化非常关键,因为点击率的绝大部分方差是由文章本身决定的,标题根本解释不了这个差异,所以你真正想预测的是某个分支偏离其所在测试均值的幅度。然后通过 beta-binomial 向该均值进行收缩,这样展示量只有 600 的分支主要反映先验,而展示量有 20,000 的分支则主要反映实际证据。
在单张 L4 上跑了 25 分钟,花了大概 40 美分。我把 2015 年 1 月之后的数据全部留作测试集,测试结果达到了 0.704 的配对准确率。
作为对比,在这个完全相同的数据集上,已发表的 SOTA 只有 0.544440.544, journals.plos.org,来自多伦多的一个团队,他们在 24,333 个样本对上使用了手工设计的语言学特征,其论文得出的结论是该问题“本质上就很困难,而不仅仅是样本量的问题”。人类在这个任务上的表现跟瞎猜差不多,文献里还有一个用 LoRA 微调的 Llama-3-8B 只拿到了 0.469,笑死。
只花了 40 美分,就比已发表的纪录高出 16 个百分点。所以我理所当然地写了一篇文章:“标题信号在两年的漂移中依然坚挺”。
那个标题声称的是信号能够抵御漂移,因为训练数据是 2013 到 2014 年的,测试集是 2015 年的,而准确率几乎没怎么波动,就好像互联网文化更迭了两年,模型却完全不在乎一样。如果这是真的,那确实是个重磅发现,而且它很重要,因为做这个的最终目的是把它用到邮件主题行上。如果它在同一个出版商内部都撑不过两年,那跨越到完全不同的媒介时就更不可能撑住了。
该数据集归档自带三种划分(探索集 exploratory、验证集 confirmatory、保留集 holdout),而我之前是在 confirmatory 上做的训练和测试。于是,主要是出于严谨,并且满心以为能印证自己早就“知道”的结论,我用同样的权重去给另外两个划分集打了分。
confirmatory 2015: 0.704
holdout: 0.637
exploratory: 0.624
真棒。
两个我从未碰过的划分集,在每一个效应量层级上的差异都在 0.013 以内,而且它们都在告诉我:我之前得出的头条数据虚高了整整 7 个百分点。
于是我这次总算认真读了归档文档,结果发现该数据集是将测试随机分配到各个划分集中的。不是按时间顺序,而是完全随机。
| 划分 | 分支数 | 日期范围 | 2015 年前的占比 |
|---|---|---|---|
| confirmatory | 51,891 | 2013-01-24 → 2015-04-30 | 83.5% |
| holdout | 11,231 | 2013-01-24 → 2015-04-29 | 82.8% |
| exploratory | 10,804 | 2013-01-26 → 2015-04-29 | 83.8% |
三个划分集以相同的比例覆盖了相同的日期范围,这意味着当我在 holdout 上打分时,测试内容中有 83% 来自训练期内部。相同的时期、相同的行文风格、相同的一切,仅仅是模型没见过的测试,而模型在那里的得分却依然差于 2015 年末尾的数据。
反过来想想这其实挺搞笑的,时间跨度对这个模型几乎没有影响,反倒是同一时期未见过的测试让它掉了 7 个点。我费尽心机搭建的漂移测试,从头到尾测的其实都是测试本身的一致性,只不过披了一层时间泛化的外衣罢了。
我造出了一台只能横向穿梭的时间机器。
显而易见,接下来的想法就是数据泄露。Upworthy 会为同一篇文章改写出几十个不同的标题变体,因此如果按测试随机划分,同一篇文章的不同改写就会散落到所有三个划分集里,holdout 中应该塞满了训练文本的近似副本。
我随手写了个倒排索引小程序,用来计算每个评估标题与模型实际拟合的 38,950 个标题之间的最大 Jaccard token 重合度(之前精确字符串匹配发现 650 个标题里有 5 个重复,没错,我当时凭这个就断定“排除了泄露”)。
| 评估集 | n | ≥0.9 重合度 | 中位数 |
|---|---|---|---|
| confirmatory 2015 | 1,300 | 0.5% | 0.227 |
| holdout | 4,274 | 30.5% | 0.300 |
30%,污染程度是 2015 年尾部数据的 60 倍,结果得分却更低。
所以泄露解释不了这个差距,方向完全反了。真要说的话,一旦剔除这些近似副本,真实的 holdout 数据应该比 0.637 更差才对。我也导出了每对样本的分数做了切片检查,在真正干净的样本对上,模型的得分是 0.646,反而略微好了一点,也就是说那些近似副本根本没带来任何好处。
行吧,那是标签噪声的问题?也许 holdout 里的样本对展示量比较少。
| 评估集 | 展示量中位数 | z 分数中位数 | CTR 比值中位数 |
|---|---|---|---|
| confirmatory 2015 | 2,462 | 2.37 | 2.24 |
| holdout | 3,096 | 2.64 | 1.99 |
结果又猜反了,笑死。holdout 样本对的展示量更多,z 分数也更高。2015 年集合的 CTR 比值中位数确实更大(2.24 对比 1.99),所以它的样本对确实更容易区分,这是真的,但绝对解释不了整整 7 个点的差距。
所以我在文档里写下“原因不明”就翻篇了。0.63 才是真实水平,两个独立的划分集都得出了这个结论,不一致的那个只是离群值,至于原因我也说不上来。
!! 技术狂暴信息预警 :3 !!
在亲自推翻了自己的主推结果后,我觉得至少得把消融实验认认真真跑一遍。
我本以为加数据会胜出,毕竟这是最平淡无奇的先验:手头有 62,695 个分支,把第三个划分集也塞进训练集,数据量更大了嘛。于是我跑了两次实验,一次是在训练集中加入 exploratory 划分,另一次是更换损失函数,两组都在相同的 2,137 个 holdout 样本对上进行评估。
Phase 0 confirmatory MSE 0.637
more-data expl+confirmatory MSE 0.724
rank confirmatory Bradley-Terry 0.775
rank+more expl+confirmatory Bradley-Terry 0.787
增加 28% 的训练数据带来了 +0.053 的提升,而仅仅更换损失函数就带来了 +0.107 的提升,而且只跑了 2 个 epoch 而不是 3 个,用的还是较小的训练集,提升幅度依然翻了一倍。
事后看来这显而易见,而且是最让人懊恼的那种显而易见。基准测试比的是配对准确率(给出两个标题,挑出胜者),而我之前做的是对点击率做回归,也就是说我优化的是指标的代理变量,最后却用指标本身来打分。
Bradley-Terry55Bradley-Terry, en.wikipedia.org 优化的才是真正要解决的目标。对于同一次测试内的每对分支,最大化 logsigmoid(score_winner - score_loser),并用展示量较少分支的对数展示量进行加权。我的 38,950 个训练分支由此变成了 63,597 个同测试样本对。
相同的模型。相同的数据。不同的目标函数。提升了 14 个点。
出于好奇,我还跑了 ModernBERT-base(参数量只有三分之一),准确率达到了 0.761。所以提升的大头在于目标函数和数据,而不是模型大小,如果你打算在 CPU 上跑这玩意儿,这倒是个好消息。
还有个差点被我错过的细节。我试过一个在决策任务上预训练过的 DeBERTa-v3-large 变体,它在全部 4,804 个 step 里损失值死死卡在 -log(0.5),得分 0.519,跟瞎猜毫无区别,一条水平死线。
人们很容易把它解读成“DeBERTa 不行”然后直接略过,我也差点这么干了。但损失曲线在代表“我在瞎猜”的数值上完全保持水平,这是个非常特殊的特征,根本不是模型学得差该有的样子。编码器加载得好好的,只有分类器和池化层是全新初始化的。
破案了,是学习率的问题。ModernBERT 跑得很欢的 2e-5 学习率会让 DeBERTa-v3-large 极度不稳定66极度不稳定, github.com,它需要更接近 6e-6 的学习率,而我之前直接一套配置通吃两个模型,毕竟谁不会图省事呢。
改用 6e-6 重跑后,损失一路从 0.709 → 0.682 → 0.620 → 0.360,最终拿下了 0.812,比 ModernBERT 高出 2.5 个点,在最高置信度层级上甚至达到了 0.913。
因为这会儿我已经不太信得过自己了,所以我也在它上面跑了近似副本切片测试。在真正干净的样本对(完全没有任何与训练集相似的内容)上,它拿到了 0.797,而 ModernBERT 是 0.769。而且在得分更高的同时,它的记忆差距(memorization gap)比 ModernBERT 还要小,这完全不符合靠死记硬背赢下比赛的特征。
所以系统从头到尾都没毛病,只是我之前把一个参数配错了。
说实话,对人类来说毫无意义。这就是用配对准确率当卖点的问题所在:它只能说明排序是对的,却完全没有体现幅度大小。在 81.2% 的情况下正确排列两个标题,其价值可能价值连城,也可能一文不值,全看它们之间的实际差距有多大。
所以我去测算了实际发文的人能感受到的收益。对于每一次测试,取出模型评分最高的分支,将其真实点击率与该测试中所有分支的平均值进行对比。毕竟在没有模型的情况下,你没有任何理由偏向某一个变体,因此你原本可能发送内容的平均值才是最诚实的反事实基准。
| CTR | |
|---|---|
| 测试均值(无模型) | 1.20% |
| 模型之选 | 1.42% |
| Oracle(完美选择) | 1.60% |
在 2,140 次测试中,这相当于相对点击率提升了 +18.3%,不过我马上就得把这盆冷水泼回你头上。
18.3% 只是 Upworthy 自身情况的反映。它取决于他们 1.20% 的基础点击率,以及作者在各个变体之间拉开的差距有多大。如果把它用到点击率为 2.5%、变体差异更小的 B2B 邮件周报上,这个数字就会发生变化,而变化的走向我确实无法预测。
真正具有迁移性的,是单次测试内部的各种比率:
| 指标 | 数值 |
|---|---|
| 避开最差变体 | 90.3% |
| 优于测试平均水平 | 76.6% |
| 选中实际最佳变体 | 47.7% |
| 捕获潜在提升空间(测试中位数) | 89.2% |
| Spearman(评分 vs 点击率) | 0.526 |
90.3% 才是那个我真正敢打包票的指标。 它几乎绝不会让你发出写得最烂的那个文案,即便换了基础点击率、受众和媒介,这个特性依然成立,而“+18.3%”就做不到这点。而且在通常有四到五个分支的情况下,47.7% 的 Top-1 命中率大约是随机概率的 2.2 倍。
不过其中有一项指标有点耍滑头。捕获潜在提升空间的中位数是 89.2%,四分位距在 5% 到 100% 之间,而在所有测试上汇总后的总体表现是 55.4%。模型的表现呈现双峰分布:在大多数测试中它能拿走几乎所有的潜在增益,而在少数测试中几乎毫无斩获,正是这些少数测试把总体数据拉低了。
随后我在上面拟合了一个保序回归77保序回归, en.wikipedia.org(isotonic regression),这样分数就有了具体的实际单位,而不是靠玄学感觉。保序回归在这里非常契合,因为它只假设单调性(分数越高,点击率越高),而这恰恰是 Bradley-Terry 能够保证、且唯一能保证的事情,任何参数化方法都会凭空捏造模型从未承诺过的结构。置信区间是通过对测试(而非分支)进行自助重采样(bootstrapping)得出的,因为同一次测试中的各个分支共享同一篇文章,并不相互独立。
| 评分 | 相比基准 | 90% 区间 |
|---|---|---|
| −2.72 | −19.1% | [−0.252, −0.209] pp |
| −1.06 | −8.2% | [−0.111, −0.086] pp |
| −0.20 | −0.7% | [−0.023, −0.000] pp |
| +0.56 | +3.7% | [+0.030, +0.056] pp |
| +1.62 | +11.1% | [+0.115, +0.147] pp |
| +2.82 | +21.8% | [+0.231, +0.274] pp |
再看中间那几行,区间在那里跨越了零点,这正是模型在正确地表示“这两个标题半斤八两,抛硬币决定吧”。
上面提到的每一个数字,全都来自 2013 到 2015 年间某一家出版商的病毒式社交媒体标题,而我真正想做的是给邮件主题行打分。
发给 4,000 名订阅用户的 B2B 邮件周报,跟“这个小孩仅用一句话就驳倒了所有反疫苗言论”这种东西几乎没有半点共同之处。
所以我去寻找带有实际测量发送结果的公开邮件数据,结果一无所获。
| 来源 | 规模 | 可用性 |
|---|---|---|
| Return Path 主题行研究 | 900 万主题行 | 专有数据,2015 年,从未公开 |
| Belkins B2B 语料库 | 550 万邮件 | 仅聚合统计数据 |
| Yahoo (IEEE 7004277) | 10 万+主题行,数十亿次展示 | 专有数据 |
| Oracle 的 NLORP 论文 | 300 条 | 从 Google 抓取,未测量比率 |
| 各类 Kaggle “email campaign” 数据集 | 各异 | 模拟数据或合成数据 |
其中有一篇 arXiv 论文来自 Oracle 的两位首席数据科学家,其整个数据集就是“通过 Google 搜索从多个互联网来源收集的 300 多个特惠促销邮件主题行”。顺便说一句,我不是在嘲讽他们,现实中公开发布的真就只有这种东西。
各种聚合结论在网上随处可见(比如 6 到 10 个词效果最好、21 到 40 个字符有利于打开率、带数字能加几个点),但没有一份是单次发送的结果数据,也没有任何一份能用来训练模型。
我之前一直沉浸在一个自我感觉良好的小故事里,直到换了个视角才把这层幻想打破。那个故事是:模型架构并不稀缺,真正具备长期价值的资产是专有的结果标签。前半句没错,后半句纯属扯淡,因为我根本没拥有什么专有标签。Upworthy 是公开的,任何拥有一块 GPU 的人都能花不到一杯咖啡的钱在一个周末复现出我的 0.812。这也是我直接开源权重的原因之一:训练它们只花了我 4 美元,我没法假装这是什么护城河。
我真正拥有的只是一份证明文件。真正的资产应该是一个基于实时流量的持续测量闭环,而这目前还不存在。
我需要的数据正静静躺在各大邮件服务提供商(ESP)的服务器里吃灰,每一个具备 A/B 测试功能的 ESP 都有数百万带有实测结果的主题行实验,而几乎没有任何一家在用这些数据训练模型。所以我不需要换个什么分类头,也不需要跑什么新基准,我只需要一个拥有日志权限的人。
这个配方说实话平淡到一句话就能讲完:只要存在测量出的真实结果,就用它去训练,别再去问模型怎么看了。
你们公司跑过的每一次 A/B 测试,都躺在某个实验平台里,打好了标签、附带了结果,却在那里吃灰。不管是 Optimizely、Statsig、LaunchDarkly 还是别的什么工具,那是积累了多年的“我们试了这五个,结果这个赢了”的数据,却从来没人用它训练过任何模型。
只要你手头有一组候选集,以及一个下游产生的数字指标,同样的逻辑就适用:
| 决策场景 | 你已经拥有的标签 |
|---|---|
| 主题行、标题、推送文案 | 打开率、点击率 |
| 客服快捷回复选择 | 未升级工单直接解决 |
| 检索重排序 | 用户最终采纳了哪个结果 |
| 错误提示文案 | 自助解决还是提交了工单 |
| 商品列表标题 | 转化率 |
| Agent 工具选择 | 该轨迹是否执行成功 |
如果你在做 Agent,最后一行会很有意思。Jev 的三个原语是 choice、score 和 noul,本文涉及的全部属于 score,但只要有人记录了决策之后发生的事情,同样的操作也可以套用到 choice 上,只不过对于大多数 Agent 路由而言,目前还没人在记录这个。
不过这一切有一个实在的局限性:我手头其实只有一个数据点。在某一个任务上,基于结果的训练大幅击败了零样本判断,但这种优势在其他地方是否依然成立尚无定论,所以我敢押注的是这个方向,而不是具体的数字。
Variant Evaluation from Real Analytics(基于真实分析的变体评估)。
NovusEdge/vera-deberta-v3-large11[object Object], huggingface.co,Apache 2.0 协议。
from transformers import AutoModelForSequenceClassification, AutoTokenizer
tok = AutoTokenizer.from_pretrained("NovusEdge/vera-deberta-v3-large")
model = AutoModelForSequenceClassification.from_pretrained(
"NovusEdge/vera-deberta-v3-large")
# Score a set of candidates for ONE piece of content. Higher wins.
enc = tok(candidates, padding=True, truncation=True, max_length=64,
return_tensors="pt")
scores = model(**enc).logits.squeeze(-1)
请把这些评分作为一个集合来看待,切勿当作单个孤立的数字。训练目标是偏离测试自身均值的程度,因此单看一个分数没有任何意义。你输入为一次发送编写的 3 到 6 个变体,模型会为它们排序。仓库里还附带了一个校准器,可以将评分映射到预期的提升幅度。
如果你觉得 435M 参数太庞大,还有一个适合在 CPU 上跑的版本:ModernBERT-base 用三分之一的参数量拿到了 0.761 的准确率。
迁移到邮件领域,或者说至少我完全不知道它能不能做到。
所以,如果你在运营一份邮件周报,并且手头有历史发送记录及实测的打开率或点击率,我非常想验证一下,欢迎联系我,数据始终归你所有。
该领域中有一个开源权重模型,在它自己的类型化决策基准测试上零样本得分只有 0.362,甚至低于 0.461 的多数类基线。
而从 0.544 到 0.812 的飞跃,也并非靠什么精妙的模型架构设计,其中三分之二的提升来自于选择了与评估指标相匹配的损失函数,剩下的则来自有人真正测量过实际发生结果的 62,695 行数据。所以,如果你还在通过向模型提问来给数据打标签,不妨先去找找真实数据(ground truth),它很可能早就静静躺在某个地方了。
数据来源:The Upworthy Research Archive33The Upworthy Research Archive, osf.io。Matias, J., Munger, K., Le Quere, M.A., Ebersole, C. (2021), Nature Scientific Data. CC BY 4.0。因维护者在 2024 年披露的随机化故障,本文全程排除了 2013 年 6 月 25 日至 2014 年 1 月 10 日期间运行的实验。权重 DOI:10.57967/hf/105738810.57967/hf/10573, doi.org。