多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

推荐系统AB测试实战:用假设检验把点击率提升30%

推荐系统AB测试实战:用假设检验把点击率提升30% 做推荐系统的人几乎都经历过这种尴尬离线指标涨了一大片AUC、GAUC全线飘红模型工程师信心满满结果灰度一上线上点击率纹丝不动甚至偶尔掉头向下。反过来还有一种情况更气人——模型网络结构一点没动只是改了召回策略或者展示规则点击率却实实在在涨了十几个点。这两种体验之间的差距就是有没有一套可靠AB测试流程的差距。今天想聊的就是推荐系统里如何用AB测试把点击率真正做上去而不是把报表做得好看。这个话题适合正在做推荐、搜索、广告排序的算法同学也适合刚搭起实验平台但还没踩过坑的团队产品经理和数据分析师也可以看因为指标口径和分流逻辑最终影响的是整个团队对“什么有效、什么无效”的判断。1. 先说结论推荐系统的点击率是被一个假设检验流程托起来的1.1 离线评测为什么解决不了线上问题很多团队喜欢把离线指标当成算法强弱的唯一标准AUC涨0.01能高兴一整天。问题是推荐系统是个闭环模型改版会改变展示内容展示内容会改变用户行为用户行为回流成训练数据后又影响下一轮模型。在这种闭环里离线评测天然是有偏的。历史数据里只包含旧策略产生的行为新策略会让用户看到不同的内容产生在历史上根本不存在的交互离线数据再大也模拟不出这条反馈路径。更麻烦的是离线评测很难处理用户构成的漂移。今天的新用户明天变成老用户老用户会疲劳行为随时间衰减这些都不是静态离线数据能刻画的。我见过太多项目离线AUC涨得漂亮上了线以后点击率原地踏步最后排查发现是模型学会了记住训练集里高频出现但已经过时的热门物品离线指标好看线上并没有增量。AB测试要解决的恰好就是离线评测回答不了的问题一个策略改动放在真实用户、真实流量、真实反馈闭环里到底能不能带来可测量的行为变化。它不替代算法研发而是给算法研发装上一个可靠的测量仪表让每一次改动都能在真实世界里得到验证。1.2 AB测试的本质是一场随机对照实验把AB测试说得再玄本质上就是随机对照实验。把实验单元通常是用户随机分到对照组和实验组对照组走旧策略实验组走新策略因为随机分组两组用户构成在统计意义上是可比的最后观测到的行为差异就可以归因于策略差异而不是用户差异、时间差异或者其他干扰因素。这个归因逻辑听着简单落地时会有大量细节问题。随机化怎么做才均匀实验单元选用户还是曝光要不要分层样本量够不够统计显著怎么判断哪个指标做主指标这些都是实验设计阶段就必须想清楚的事。设计做错了后面的数据再漂亮也可能得出错误结论。AB测试不是简单地跑个A组B组它是一整套假设检验流程先提出原假设新策略没效果和备择假设新策略有效果再定显著性水平和统计功效最后用数据决定是否拒绝原假设。换句话说AB测试真正托起来的不是某个具体算法而是整个推荐系统迭代的“可信度”。没有这套流程团队就只能凭感觉做决策谁的口才好谁说了算改版变成赌运气。1.3 30%这个数字到底怎么来的标题里写的“点击率提升30%”看起来夸张但在我做过和见过的项目里30%并不是靠一次实验拿到的而是靠多点叠加、多轮累积出来的。举一个典型的组合召回从两路扩展到四路增加向量召回和语义召回聚类点击率涨12%排序模型加入实时点击序列特征和位置偏置校正点击率再涨8%重排侧加了多样性和冷启动探索机制头部流量的点击率再涨6%。这三部分相乘算下来是112%×18%×16%减1大概27%再算上展示位上的一些细节优化冲到30%是很正常的事。关键是这个过程中每一步都必须有AB测试背书不然你根本不知道哪个改动真正贡献了提升。有时候你以为涨的是召回实际上涨的是排序有时候模型改了没效果重排换个打散策略反而有效。只有把每一步都用实验量化清楚30%才不是一个拍脑袋的数字而是一串可追溯的实验记录。2. 实验设计样本量、分流与分层做错一个就白干2.1 样本量计算一个可以直接套用的公式样本量是AB测试的地基。样本太少真实效果检测不出来样本太多浪费流量实验周期拉长到不可接受。多数CTR类指标是二项分布可以用比例检验的样本量公式估算。假设当前基线点击率p13%你想检测出10%的相对提升也就是p23.3%显著性水平取α0.05统计功效取1-β0.8。双侧检验下z值分别取1.96和0.84公式是n (z_{1-α/2} z_{1-β})² × [p1×(1-p1) p2×(1-p2)] / (p2-p1)²代入数字p1×(1-p1)0.03×0.970.0291p2×(1-p2)0.033×0.9670.0319两者相加0.0610(1.960.84)²7.84剂量差0.003²0.000009。算下来n约等于5.3万也就是实验组约5.3万人对照组约5.3万人总共需要约10.6万用户。这个数字意味着什么如果产品每天日活10万切一半流量进实验一天就能达到样本量但我会建议至少跑满7天。原因有两个一是单日用户行为波动大节假日、晚间高峰都会影响点击率二是推荐策略存在新奇效应用户对新策略刚开始会好奇点击率虚高跑几天才会回落到真实水平。另外如果指标不是点击率而是人均点击次数这种连续变量公式要换成基于均值方差的版本n (z1z2)² × 2σ² / δ²其中σ是历史数据的标准差δ是最小可检测提升。一个很多人会犯的错是把曝光当作独立样本去套公式。同一个用户贡献了几十次曝光这些曝光高度相关不是独立样本直接按曝光数算样本量会把方差严重低估p值也就不可信。所以样本量计算和后续显著性分析都要锚定在实验单元上通常是用户。2.2 分流策略用户级还是请求级分流是AB测试最容易埋雷的环节。最简单的做法是拿用户ID取模比如user_id % 100 50进实验组但自增ID的低位往往有规律取模会造成分桶不均匀而且不同实验如果共用同一个分桶效果会互相污染。更稳妥的做法是用哈希函数加salt保证每个实验拥有独立的随机序列并且同一实验里同一个用户永远落在同一组。一个可以直接用的分流代码逻辑import hashlib def bucket(user_id, salt, total1000): key f{user_id}_{salt} digest hashlib.md5(key.encode()).hexdigest() return int(digest[:8], 16) % total把用户的唯一标识和实验salt拼起来做MD5取前8位十六进制转成整数再取模落在0到999之间。用MD5不是为了安全只是为了得到一个分布均匀的伪随机数。salt要每个实验独立避免用户在不同实验里拿到完全相同的分桶结果。total建议用1000或10000方便按千分之一或万分之一的粒度切流量。这里有个经验排序实验老老实实按用户级分流。如果按请求级分流同一个用户刷一次刷新看到旧策略下一次刷新看到新策略用户感受到的体验是混乱的而且两次点击行为会互相影响实验数据被污染。用户级分流保证一个用户在一整场实验里只看到一个策略版本归因才干净。只有纯展示层实验比如按钮颜色、文案样式对长期用户行为没有跨次影响才可以考虑请求级分流。2.3 分层实验与互斥域做过推荐系统的人都知道一个推荐链路里同时会有好几个团队在改东西召回组在换向量模型排序组在改特征重排组在调多样性如果所有实验都切同一批流量几天就撞车了。解决方法是流量分层。把100%流量切成多个互不干扰的层召回实验跑在召回层排序实验跑在排序层重排实验跑在重排层。同一层内的实验互斥不同层之间的实验可以交叉运行。层与层之间靠不同的salt来做正交化保证用户落在召回层的分组和落在排序层的分组是独立的这样具体某一个用户可能同时参与召回实验和排序实验但两组实验各自的统计推断不会互相干扰。这套思路在业界已经验证过很多年核心价值是让多个实验并行不悖。实操中要注意同一层内的实验切分要保证流量池互斥两个实验都期望覆盖100%流量时就要分成两个层而不是挤在同一层。还要小心同一组件被两个实验同时修改比如排序实验改了排序模型另一个实验也在改同样的排序模型那这两场实验的结果都不能信。3. 指标体系与统计判断别被p值骗了3.1 主指标、护栏指标、代理指标怎么搭点击率是推荐系统AB测试里最常用的主指标但单独盯CTR有个大坑新策略可能压缩曝光量只把高点击率的内容展示出来CTR在涨用户真实体验却在变差。所以实验必须同时设置护栏指标比如人均点击次数、人均曝光次数、人均停留时长、次日留存如果CTR涨了但人均点击跌了这种实验不该上线。更精细一点可以把指标拆成漏斗来看曝光到点击的CTR是转化效率人均点击总数是整体粘性点击后进入详情页的到达率是内容匹配度次日留存是长期价值。一次实验上线前就把这些指标定义清楚谁当主指标、谁当护栏指标、哪个反向指标的阈值是多少写进实验登记表。等实验跑完再挑指标看结果大概率会挑出对自己有利的那一个这是数据决策里最隐蔽的作弊。商业场景还要加商业指标比如下单率、客单价、GMV。我见过不止一次这种局面CTR显著提升下单率显著下降说明新策略把用户注意力吸引到了“好看但不动手买”的内容上。算法团队和商业化团队对这种实验的看法会完全相反所以实验开始前最好就是先达成共识目标到底是点击率还是点击后的商业转化。3.2 指标稀释与辛普森悖论为什么危险指标稀释是新手团队最常犯的统计口径错误。某次实验只切了10%流量但数据分析师拿全量大盘的CTR来对比实验效应被稀释到原来的十分之一真实涨10%在大盘里只表现出1%的波动噪声一冲就看不出来了。正确做法是严格限定统计范围只统计实验桶内、策略实际生效的曝光和点击记录不要扯进无关流量。辛普森悖论是另一个经典陷阱。可能整体上看实验组CTR等于对照组但拆开新老用户再看实验组的新用户CTR高于对照组老用户CTR也高于对照组整体却出现相反结果。原因通常是实验改变了新老用户占比新策略对老用户留存更好实验组的老用户比例变高了而老用户本身的点击率就低于新用户结构变化把真实的提升掩盖了。处理办法是分层分析实验前先按新老用户、终端类型、访问源等维度把用户分层计算各层内部的实验效应再用加权方法或者回归模型做个综合估计。不要一上来就看总平均先回答“每个层级内部是否一致”再回答“整体结论是什么”。3.3 置信区间、p值和多重比较的正确姿势p值小于0.05意思是在“新策略没有效果”的原假设前提下看到当前数据这么极端结果的概率小于5%。这个定义很容易被误解但更要命的是多重比较问题。如果你同时看了10个指标每个指标都按p0.05来判断在没有真实效果的情况下至少有一个指标假阳性的概率约为1-0.95^10接近40%。所以你看到的那几个显著指标可能纯粹是噪声。两个实用建议。第一主指标要预先注册实验开始前定好一个主指标作为决策依据其他指标属于探索性分析参考但不能单独当上线依据。第二如果确实想看多个指标做多重比较校正简单一点用Bonferroni把显著性水平除以指标数比如看5个指标就要求p0.01更精细一点用BH FDR校正控制错误发现率。还要提醒一点实验进行中不要每天都去盯p值看到某天p0.05就急着下线切全量。中间反复查看会膨胀假阳性率正确姿势是把观察次数和决策规则提前定好要么固定跑多少天看结果要么用序贯检验框架把alpha分配到多次观察中。我自己的习惯是实验跑满7天以后才开始认真看置信区间看效应量下界是否超过业务上认可的最低值而不是只看p值。4. 算法上怎么做召回、排序、重排的点击率提升路径4.1 召回侧多路召回和向量召回怎么贡献点击率很多推荐系统早期只有两路召回协同过滤加热门兜底导致推荐池很窄大量长尾内容永远没有曝光机会。排序模型再强也排不出召回里不存在的内容。召回侧改动对点击率的影响经常是被低估的。实践中提升最明显的是两件事。第一召回路数扩展从两路扩到四路甚至六路加入向量召回、语义召回、同城或同标签召回让更多的候选物料进入排序池。第二对冷启动用户和低活用户不要一股脑推热门而是用热门兜底加探索通道结合给新内容一定的展示机会。之前有个项目在新用户流量上加了探索通道后新用户人均点击次数涨了6%左右新用户次日留存也有小几个点的提升。向量召回通常用双塔模型用户侧塔和物品侧塔分别输出embedding线上用ANN近邻检索快速取回相似物品。这类改动的AB验证不建议只看整体点击率还要看召回覆盖率、长尾曝光占比、以及不同活跃度用户的分层效果。因为整体点击率可能被头部内容掩盖真实收益在长尾和冷启动用户身上。4.2 排序侧特征、目标函数与多目标平衡排序模型是推荐系统里迭代最频繁的部分。点击率提升也常常出在排序侧但要注意排序侧并不是换个模型结构就一定涨。我见过只有增加有效特征才涨的也见过网络结构不变把损失函数换成带位置偏置校正版本后明显变好的。位置偏置是个典型的坑线上展示时排在第二位的物品点击率高很多时候不是因为它更相关而是因为它位置靠前。模型如果直接学曝光日志里的点击标签会把位置因素当内容相关学进去导致重排位置变动后预估不准。解决思路是位置偏置校正比如训练时把物品位置特征显式建模、用期望曝光分数加权的无偏学习方法或者常见的shrinkage方法。这类改动离线表现往往温和但上线后真实的排序能力提升会反映在CTR上。多目标排序也很关键。单纯优化点击率模型容易学会“标题党”内容。用MMoE或者PLE这类多专家网络把点击、点赞、转发、停留时长一起建模通过门控机制在不同任务间共享信息同时观察主指标CTR和护栏指标时长、互动率最后按业务权重折算成综合收益。AB测试在这种场景里尤其重要因为多目标模型的“总收益”不是单一指标能定义的必须靠实验同时验证多个指标的走向。4.3 重排侧多样性和探索如何避免点击率虚高重排策略经常被忽视但它对点击率的影响可以直接到展示层。信息流里最常见的操作是打散用MMR或者DPP算法让同一类目、同一作者的物品不要连续出现。表面上看打散可能损失局部相关性但用户沉浸感和滚动深度上来了总点击次数反而增加。这里有个反直觉的经验在某些场景下越相关越好打散过度反而降低点击率。所以打散强度不是越大越好AB测试里可以设两档、三档强度梯度看不同档位下CTR和人均点击的权衡曲线。我做过一个实验打散强度从0调到中等时人均点击涨了4%继续调高强度后人均点击又跌回去了曲线的拐点非常清晰这种信息只有AB测试能给。冷启动探索也能带动点击率。新用户没有任何行为历史推荐系统只能猜如果只按热门推新用户容易觉得无聊。用Bandit算法或简单的探索机制给新内容分配一定曝光虽然前期单次点击率可能略低但用户发现感兴趣内容的概率变大7天累计点击和留存数据反而更好。这种收益必须用长周期实验才能体现只看第一天CTR很容易误判。5. 完整跑一场AB测试从数据埋点到放量决策5.1 实验准备阶段把坑提前踩完一场规范AB测试准备工作最少占一半时间。首先把实验假设写清楚原假设是新策略相对旧策略没有点击率提升备择假设是提升超过某个最小值主指标、护栏指标、反向指标全都登记到文档里。然后是埋点检查。推荐系统的曝光日志、点击日志、到达日志必须能按用户ID和时间关联。我每次实验上线前都会跑一遍数据质量检查抽一小批曝光日志确认点击日志里每一条点击都能找到对应曝光上下文确认实验桶和对照桶的用户量比例符合预期确认没有大面积的日志丢失。这一步偷懒后面分析全是空中楼阁。再然后是配置管理和白名单测试。实验配置要支持按用户ID白名单走实验策略内部人员先真机体验一遍看看推荐结果有没有明显异常再开放给真实流量。灰度放量建议按1%、5%、10%、50%逐级往上加每级观察一段时间不用一次切满。放量过程本身就是一个小型实验能看到不同流量规模下系统稳定性和用户反馈的变化。5.2 实验进行中监控什么才不会被数据骗实验上线后的每一天我都会看一份标准监控报告包括实验组和对照组的曝光次数、点击次数、CTR、人均点击、停留时长以及用户数是否平稳。核心原则是先看数据质量再看显著性。如果用户数不对、埋点有缺失p值再小都别信。一个很重要的检测叫SRM样本量比例失配。比如设计上实验组应该占50%但是统计曝光日志发现实验组只占48%这个差距超过随机波动范围说明分流链路可能被缓存、客户端逻辑或者日志过滤影响了。有一个非常典型的案例某个分桶的用户被客户端缓存拦住了导致实验组样本量少了5%分析结果p值差点穿过0.05最后通过SRM检测发现分流比例失配才避免了一次错误上线。SRM可以用简单的卡方检验检测应该作为每个实验监控看板的标配。显著性方面每天计算主指标的p值和置信区间是对的但决策要看趋势而不是某一天。我习惯把连续7天每天的数据都拿出来看如果前3天显著、后4天不显著说明可能是新奇效应减退如果前3天不显著、后4天连续显著说明策略需要一段时间才能体现效果。只看最后一天的p值会丢掉时间维度的信息。5.3 实验决策上线、下线还是继续观察实验跑完后按预设规则决策。主指标显著提升护栏指标没变差反向指标在容忍范围内就继续放量。主指标显著变差立刻下线不需要犹豫。最麻烦的是主指标没有显著变化但不是一点没涨。这种时候看两个东西一是置信区间的宽度如果置信区间很宽说明样本量还不够最小可检测效应没有达到应该延长实验时间或者加大流量如果置信区间已经很窄效应量本身就小于业务认可最低值可以判断为“没有值得上线的提升”。很多团队在这里犯拖延症跑了两周还在继续跑其实统计功效早就够了再跑也只是浪费时间。全量以后还要做一件事长期监控。新策略上线一周、一月的指标变化和实验期间是否一致。如果实验期间CTR涨了8%全量后两周掉回原值需要排查是不是外部环境变化还是策略的真实价值被高估。必要时可以做反转实验比如短期把一部分流量恢复到旧策略观察对比用来确认之前的提升确实来自新策略。6. 我踩过的坑以及最后一点个人建议第一个坑用户级分流做了但客户端缓存没做实验标识隔离导致同一个用户在一个请求看到对照组内容下一个请求看到实验组内容。用户感受到策略跳变整体行为都变了实验没法看。后来是把实验标识和分桶结果一起进缓存key才解决。第二个坑只看全量大盘CTR。有一段时间我复盘某个召回策略内部评审时测试组拿全量大盘的CTR说没效果后来把统计口径严格限定在实验桶曝光日志里再算发现其实涨了4%但p0.09距离显著就差一点。这个问题不是策略无效而是分析口径错误差点把一个真实收益砍掉。第三个坑新奇效应让我高兴了一周然后被打脸。一个排序模型刚上线时CTR涨了10%所有人都觉得成了结果一周后收益归零。后来养成了习惯所有实验至少跑7天并且看第1天、第3天、第7天的指标曲线再做决策。新策略上线初期用户有新鲜感点击意愿会虚高这个效应必须用时间来消化。第四个坑多重指标同时看的假阳性。有一次同时看8个指标其中2个显著差点因为其中一个指标的上线理由。后来做了多重比较校正做完以后那两个指标里有一个不显著了才知道是噪声。从那以后每个实验都在开始前指定一个主指标、几个护栏指标而不是到时候翻报表挑好看的。如果让我给刚搭实验平台的团队几个建议我会说三句话。第一先做A/A测试用同一策略的两组跑一遍确认分流没有系统性偏差再谈A/B。第二实验假设、指标定义、最小可检测效应实验开始前就全部写清楚不要边跑边看数据边调整规则。第三30%这类数字不是靠一次实验跑出来的而是靠多轮小步验证叠出来的。我过去最骄傲的不是某次实验CTR涨了30%而是一个季度里几乎没有出现过“假阳性误导上线”的事故。把AB测试从一种统计工具变成团队的工作习惯推荐系统的每一次改动才算真正有据可依。
返回列表