专业书籍精读 · ML System Design Interview · 第 7 章
Machine Learning System Design Interview · Ch 7 · Ali Aminian & Alex Xu · 2023
你在一个卖门票的 App 上看到「这周末,你附近有这些活动」——一场脱口秀、一次徒步、一个市集。这一章讲的就是这一屏怎么排出来。它和推荐电影最大的不同在于:这些「商品」几天后就不存在了。
推荐电影,像在一座常年开着的图书馆里荐书:书一直在架上,谁借过、谁读完了,记录越攒越厚。攒得越厚,机器越懂该把哪本递给谁。
推荐活动不是。它更像清晨的集市:今天的摊子今天摆、今晚就收。一场演出刚上架时没有任何人看过它;等它攒够足够多的人来证明自己受欢迎,它已经开场、甚至已经结束了。于是推荐系统最赖以为生的那句话——「和你口味像的人也去了」——在这里从根上说不出口。
常见的推荐办法是「看别人」:和你差不多的人还看了什么。这套办法有个前提——东西活得够久,久到能被很多人试过。视频、商品、歌都满足;活动不满足。
结果是:只会「看别人」的系统,对绝大多数正在卖票的活动一无所知,能推的只有那一小撮已经火了的。新办的活动永远没机会被端到人面前——而平台每天新增的,恰恰全是这种。
既然攒不到「谁去过」,那就换一套依据:把这个人、这件事、这个时刻的具体情况,一条一条问出来。离你家多远?开车还是走路能到?是周六下午还是周三深夜?你平时是不是这个时段出门?多少钱?你有几个朋友已经报名了?主办方你是不是关注过?
这一长串问题合起来就是一份「问卷」。系统拿上百万场历史活动的问卷和它们的真实结果去训练一个判官,让它学会看着一份新问卷就估出「这个人会报名的可能性」。它不认识这场活动,但它认得这份问卷的形状。就像你不认识街角新开的那家店,可一听「离你 800 米、做的是你常吃的菜、你三个朋友订了位」,心里已经有数了。
顺带一提,这里面最管用的常常不是内容有多匹配,而是朋友去不去——活动本来就是一件社交的事。
值得记住的是:这一章把力气从「挑一个更聪明的模型」挪到了「把问卷设计好」。问卷上少了「你几个朋友要去」那一栏,再高明的判官也补不回来。另外,因为集市每天换货,这个判官得天天回炉重学。
「朋友都去」是最强的信号,也是最强的偏心:它会把热闹的推得更热闹,而那些小众的、还没人知道的活动,更难被端出去。
活动推荐难的不是猜得准,是商品会过期。没有历史可学,就只能靠「此人、此事、此时此地」的具体情况来判断——所以这一章真正的主角不是模型,是那份问卷怎么问。
想进到具体机制、记号和示意图? → 切到精读版
这一章表面是「做一个活动推荐器」,内核是一个被物料寿命逼出来的结构性事实:这里的商品会过期。一场活动从创建到结束常常只有几天到几周,上架那一刻交互为零,等它攒够数据它已经下线——所以冷启动(cold start)不是边角情况,是稳态,靠交互矩阵吃饭的协同过滤从根上断了这条路。剩下唯一能用的,是「这个人 × 这件事 × 此时此地」的具体特征。于是本章把重心从选模型挪到了特征工程(feature engineering),并把问题定义成排序学习(learning to rank, LTR):不追问「他喜不喜欢」,只求「把这一批候选按报名概率排对顺序」。
本章是《ML 系统设计面试》Part II 十道真题的第 6 道。上承 Ch6 视频推荐——那里物料长寿、交互充足,主角是召回与排序的分段;这里物料短命、交互稀薄,主角换成了特征。下启 Ch8 广告点击预测,同样是 pointwise 打分,但那里分数要拿去计费,概率校准因此变成硬需求。现实对应 Meetup、Eventbrite、Facebook Events 这类平台。
书设定的场景是一个活动平台:用户打开首页,看到一屏「推荐给你的活动」,业务目标是提高报名转化(register,而不是点击)。两个约束把这一章和上一章彻底分开。
其一,物料寿命以天计。一个视频上线后能活几年、累积几百万次播放,交互矩阵有的是料可学;一场活动从创建到开场常常只有几天到几周,一辈子能攒到的交互可能只有几十上百次,结束后永久下架。换句话说,每一个活动都是在冷启动状态下被推荐完,然后消失的。
其二,正样本极其稀少。曝光量很大,真报名的很少。给个量级:若报名率是 1%,一个永远预测「不报名」的模型准确率就有 99%——准确率在这里是废指标,这是本章评估部分的起点。
不解决会怎样?一个只认交互矩阵的推荐器,对绝大多数在售活动一无所知,能推的只剩那一小撮已经火起来的。富者愈富,新主办方永远起不来——而平台每天的新增供给恰恰全是这种。
书把它定义成排序问题:给定一个「用户 × 活动」对,预测报名概率 P(register | user, event),再按分数从高到低排。标签是现成的——曝光后报名记 1,曝光后没报名记 0。
排序学习有三种训练单位,差别在「一次看多少条」:
书选 pointwise,理由务实:实现最简单、标签最好造、分数可复用。一条可以带走的判据:极度在意前几名的顺序 → pairwise / listwise;工程要简单、分数还要被别处复用(计费、阈值)→ pointwise。
既然不能问「谁去过」,就只能问「这个人、这件事、这个时刻」。书把特征分成四类,值得一类一类看,因为每一类都带出一个不同的编码手法。
位置类。活动地点与用户常驻地的距离、是否同城、步行友好度(walk score)、公共交通可达性。这一类最关键的手法是分桶后再独热,别把原始公里数直接喂进去——人对距离的反应是断崖式的:3 公里和 5 公里几乎没差别,5 公里和 50 公里是两个世界。比直线距离更贴真实决策的是预计通勤时间:跨江 8 公里可能比同区 15 公里更远。
时间类。这一类里有本章最有代表性的一个特征:距开场还剩多久。它的特别之处在于,同一个「用户 × 活动」对,在不同时刻分数必须不同——距开场 30 天和距开场 2 小时是两种完全不同的购买心态。另外还有活动时长、星期几、几点、用户历史出席的时段分布。星期与小时有个坑:它们是环形的,周日 23 点和周一 0 点在现实里相邻,在数值上却隔得最远,所以要么独热、要么用周期编码,不能当普通数值。
社交类,本章最有价值的一栏。用户好友中已报名的人数、主办方是否是好友或已关注、是否被朋友直接邀请、已报名人数与容量的比例。为什么强?因为参加活动本来就是社交行为——「三个朋友要去」这一条,常常比内容匹配度更能决定去不去。面试里问「最重要的特征是哪个」,答社交类加剩余时间,基本不会错。
用户与活动自身。年龄、性别(分桶 + 独热);票价(免费与付费的转化率往往差一个数量级)、类别、描述文本的嵌入、主办方历史转化率。
编码手法就四件套,各管一类问题:独热管低基数类别;分桶 + 独热管有拐点的连续量;嵌入管高基数 ID;特征交叉管交互信号。交叉为什么必要,一个例子说得最清楚:周三深夜愿意跑 20 公里的人极少,周六下午愿意的人不少——距离单独看、星期单独看都得不出这个结论,必须把「距离 × 星期几」显式造出来。
这里还有一处被短命反噬:活动 ID 的嵌入基本学不动,每个 ID 一辈子只出现几十次。真正学得动的是上一层——主办方 ID、场地 ID、类别,它们跨活动复用,样本才够。
特征按「什么时候算」分成两拨。批特征离线预算,小时或天级刷新:活动描述的嵌入、主办方历史转化率、用户长期兴趣画像。流特征在请求发生时实时算,秒级:好友刚刚报名了没有、还剩多少票、距开场还剩多久、当前设备与位置。
这一章绕不开流特征,原因很直接:它最强的两个特征恰好都是会变的。剩余时间每分钟都在变,好友报名数随时可能加一。一小时前算好的值,到用户真正请求时已经是错的。
工程代价是同一个特征要有两条计算路径——离线训练用历史回溯表,线上服务用实时流——一旦对不上就是训练与服务偏斜(training-serving skew):离线指标漂亮,上线掉得莫名其妙。
看看手里的东西:几十到几百维人造的、表格型的、类别与数值混杂的、相当稀疏的特征。这正是 GBDT 的主场——不需要归一化、能自动学非线性与特征交互、在中小数据量上比神经网络稳,而且可解释:特征重要性直接告诉你哪一栏在起作用。对这一章尤其要紧,因为你造的特征就是你的模型。
书给的是一条阶梯:逻辑回归(快、可解释,但所有交叉都得手工造)→ 决策树(能学非线性,单棵易过拟合)→ GBDT(默认选择)→ 神经网络(数据量够大、需要吃文本嵌入或做持续学习时才划算)。
GBDT 有两个短板,恰好都被这一章的性质放大:不方便增量更新(新活动每天来,传统上要整体重训)、不擅长高基数 ID 嵌入。所以工业折中通常是两条:GBDT 打底 + 高频重训(天级甚至小时级),或者用 GBDT 的输出当特征再喂给一个轻量神经网络。
评估分两套口径。离线:正例只占 1% 量级,所以准确率必须扔掉,换成 PR 曲线下面积(PR-AUC)看分类质量,Recall@k / MRR / nDCG 看排序质量——一次请求只期待一个正确答案时看 MRR,相关性有等级差异时看 nDCG。线上:点击率、报名转化率(真正的业务目标)、收入。另外,若训练时对负样本做了降采样,分数必须校准回真实概率,否则任何和阈值、预算、钱有关的下游都会错。
这是本章和 Ch6 最容易被搞混的一处。上一章第一段是「双塔 + 近似最近邻」的模型召回,因为十亿个视频没有任何天然的筛选谓词。这一章不需要:活动被时空强约束着。「时间还没过」加「在用户半径内」这两条规则,一下就把 10⁵–10⁶ 个在售活动砍到几百至几千个——剩下的规模排序模型直接吃得下。
所以链路是:全部在售活动 → 规则过滤 → 候选 10²–10³ → 特征拼装(批 + 流)→ 排序模型打分 → top-k。照搬上一章那套召回层,是白加一层复杂度——面试里很常见的一个失分点。
最后,重训必须频繁。物料每天换一批,季节、节假日、城市活动密度都在动,上个月学到的分布这个月可能已经不作数。这是短命物料的直接推论。
表 1 · 视频推荐(Ch6)vs 活动推荐(Ch7)——同是推荐,形状完全不同
| 视频推荐 | 活动推荐 | |
|---|---|---|
| 物料寿命 | 数月至数年,交互越攒越厚 | 几天至几周,过期即下架 |
| 冷启动 | 边角情况,靠热门召回兜住 | 稳态,每个物料全程都在冷启动 |
| 主力方法 | 协同过滤 + 内容式,双塔学嵌入 | 协同过滤失效,只能靠特征 |
| 第一段筛选 | 模型召回(双塔 + ANN) | 规则过滤(时间 + 地理半径) |
| 最强信号 | 用户历史观看序列 | 社交关系 + 距开场剩余时间 |
| 重训频率 | 天级即可 | 天级到小时级,不重训就失配 |
表 2 · 排序学习三种范式
| pointwise(书的选择) | pairwise | listwise | |
|---|---|---|---|
| 训练单位 | 一条样本 | 一对样本 | 整张候选列表 |
| 学什么 | 绝对偏好(报名概率) | 相对顺序 | 直接优化列表级指标 |
| 分数含义 | 是个概率,可复用 | 只有序意义 | 只有序意义 |
| 工程成本 | 最低,任何分类器都能用 | 中,需构造样本对 | 最高 |
| 短板 | 看不见同请求里的其它候选 | 样本对数量爆炸 | 训练慢、实现复杂 |
表 3 · 模型阶梯:为什么停在 GBDT
| 逻辑回归 | GBDT(书的选择) | 神经网络 | |
|---|---|---|---|
| 非线性与交叉 | 要全部手工造 | 自动学 | 自动学,还能学高阶 |
| 特征预处理 | 要归一化、要编码 | 几乎不用,吃原始表格 | 要归一化,类别要嵌入 |
| 数据量门槛 | 很低 | 中等,中小数据上最稳 | 高 |
| 可解释性 | 最好(看权重) | 好(看特征重要性) | 差 |
| 增量 / 持续学习 | 容易 | 不方便,要整体重训 | 容易 |
| 高基数 ID | 撑不住 | 撑不住 | 嵌入是它的强项 |
还有两条不在表里的实操判据:特征比模型重要得多——加一栏「好友已报名数」带来的提升,通常超过把 GBDT 换成一个更花哨的网络;以及冷启动在这里主要不是模型问题,靠规则与产品手段兜底:新用户先给同城热门与高上座率的活动,让最初几次点击快速收敛。
这套形状的适用面比「活动」宽得多:凡是物料有有效期、且上架即冷启动的场景,都是这一章。票务与活动平台(Meetup、Eventbrite、Facebook Events)最直接;再往外,限时优惠券、航班与酒店的余量促销、直播开播提醒、「即将截止」的职位推荐、餐厅当日空位——共同点都是:交互矩阵还没攒热,商品就没了。
面试里的高频追问就三处:为什么这里不能用协同过滤(当场把「物料寿命」这个理由说出来,而不是背定义)、最重要的特征是哪些、为什么(社交关系与剩余时间)、离线用什么指标(先说清准确率为什么废掉)。
① 这一章的商品会过期:活动寿命以天计、上架即零交互,冷启动是稳态而不是边角情况。
② 因此协同过滤从根上断了——交互矩阵永远攒不厚,只能靠「此人 × 此事 × 此时此地」的特征。
③ 问题定型为排序学习,书选 pointwise:预测 P(register | user, event) 再降序排。简单、标签现成、分数是概率可复用;代价是看不见同请求里的其它候选。
④ 特征四类:位置、时间、社交、用户与活动自身。最强的两类是社交关系与距开场剩余时间。
⑤ 编码四件套:低基数独热、有拐点的连续量分桶后独热、高基数 ID 用嵌入、交互信号靠特征交叉。星期与小时是环形的,别当普通数值;活动 ID 的嵌入学不动,能学动的是主办方 / 场地 / 类别这一层。
⑥ 最强的特征恰好是流特征(剩余时间、好友已报名数),于是必须维护批与流两条路径——一旦对不上就是训练与服务偏斜。
⑦ 模型停在 GBDT:表格型异质特征的主场、可解释、中小数据最稳;短板是不便增量更新,靠高频重训补。评估扔掉准确率,用 PR-AUC 加 Recall@k / MRR / nDCG,线上看报名转化率。
⑧ 服务链路第一段是规则而不是模型:时空谓词把 10⁵–10⁶ 砍到 10²–10³,排序模型直接吃得下。