专业书籍精读 · ML System Design Interview · 第 9 章
Machine Learning System Design Interview · Ch 9 · Ali Aminian & Alex Xu · 2023
你在订房网站上点开一套房子,页面往下拉,总有一排「你可能也喜欢」。这一章讲的就是:那一排是怎么挑出来的。
你大概以为它在找「跟这套像的」——同一个区、同样两居、同样一晚六百。可真按这个筛,挑出来的东西常常没人点。
反过来,有两套房子参数差得不小:一套六百、一套七百五,一套整租、一套带房东,但同一批人来来回回在这两套之间比价、犹豫、最后选了一套。这两套才是真正的「相似」——因为它们在同一群人心里是可以互相替换的。
所以这一章最关键的转向是:谁跟谁像,不由房子的参数说了算,由看房子的人说了算。
难点在于,没人给过答案。全站几百万套房,没有任何一张表写着「A 和 B 相似」。想靠人标?两两配对的数量是天文数字,标一辈子也标不完。
靠参数算又回到了刚才那个坑:同样是「两居、六百、市中心」,一套推窗是地铁口,一套要爬二十分钟山路;照片拍得好不好、住进去舒不舒服,这些没法写进参数的东西,恰恰是人做决定时最看重的。
平台手里其实有一个现成的答案——每个人的浏览轨迹。一个人某天晚上连着点开的那七八套房,就是他心里放在一起比较的一组。这种轨迹,平台一天能收上亿条。
做法是给每套房发一张「名片」——一小串数字,机器自己学出来的。规则只有一句:经常出现在同一次逛街里的房子,名片要互相靠近;随便抽来的两套,名片要互相推远。几亿条轨迹过完,名片挨得近的,就是人们真会拿来比较的。
这有点像从大量合影里推断谁跟谁熟:你不用问任何人,只要看谁总是跟谁出现在同一张照片里。
还有一个加料的动作:如果这次逛街最后真订下来了,那被订的那一套对整段轨迹都有发言权——让路上看过的每一套都往它靠一点。因为「最后选了它」比「点开看过」要重得多。
刚上架的房子没人看过,自然也没有名片。办法很土但很有效:找附近几套户型和价位都差不多的房子,把它们的名片平均一下,先顶上,等这套房攒够了浏览记录再换成自己的。
它只会复述人们已经在看的东西——一套没人点过的好房子,很难被它翻出来,于是冷的越冷、热的越热。
相似房源不是「找长得像的」,是「找会被拿来当替代品的」。而后者不用定义、也定义不出来——它就藏在千万人的浏览轨迹里,把轨迹倒进机器,相似自己会浮出来。
想进到具体机制、记号和示意图? → 切到精读版
这一章的核心决定只有一个:不去定义什么叫「相似」,让用户的会话轨迹替你定义。它是全书唯一一道没有人工标签的题——训练信号完全来自日志里的共现(co-occurrence),用改造过的 word2vec skip-gram 把每个房源压成一个几十维向量,「相似」就等于向量近。真正的工程含量不在模型(模型是 2013 年的),而在三处改造:把最终下单的那套当全局上下文、为同城搜索补一组同市场负样本、给新房源准备属性回退。
本章是《ML 系统设计面试》Part II 十道真题的第 8 道,和上一章正好对称:Ch8 广告点击预测有现成标签,力气花在特征交叉与概率校准上;这里没有标签、几乎不做特征工程,力气全花在「怎么把原始日志变成训练样本」。下启 Ch10 个性化信息流,回到多目标监督排序。现实对应 Airbnb 的相似房源模块、电商的「看了又看」。
场景很具体:房源详情页往下滚,有一个「相似房源」横排。用户点开一套房,说明他对这一类有兴趣但还没定——这一屏决定他是继续在站内挑,还是退回搜索页、甚至关掉标签页。在民宿平台上,这是仅次于搜索的发现入口。
三个约束把它和前面所有题分开。
其一,没有标签。没有任何一张表写着「A 与 B 相似」。人工标注在数量上就不成立:几百万房源的两两配对是 10¹³ 量级——这个记号只是说「万亿级」,标注根本无从谈起。
其二,属性相似度会系统性地答错。「同城 + 同价位段 + 同户型」的余弦相似度十行代码就能写完,但它挑出来的是参数接近的房源,而用户要的是可替代的房源。同样是「两居、600 元、市中心」,一套推窗即地铁、一套要爬二十分钟山路;照片质量、装修风格、性价比感受——这些决定用户取舍的东西,大半编码不进属性表。
其三,规模不允许暴力。Airbnb 公开过的量级是数百万在售房源,而详情页这一块的渲染预算通常是几十毫秒——逐次请求跟几百万个向量算精确余弦是上亿次乘加,不在选项里。
不解决会怎样?模块推出来全是「参数孪生」,点击率低得像随机,用户只能退回搜索页重新开始。
本章不训练一个「给定 A、B 输出像不像」的二分类器,而是给每个房源学一个向量,相似度用余弦算。好处有两个:向量一次算好,全站任意一对都能查;下游多个模块(详情页推荐、搜索召回)复用同一份表示。
训练信号来自哪里?一句话假设撑起了整章:同一次会话里被连着点开的房源,在用户心里是可以互相替换的候选。它把「相似」的定义权从产品经理交给了用户行为,于是学到的是行为上的可替代,不是属性上的像。
这一步看着琐碎,却是全章最容易做砸的地方。书里给出的处理是:
为什么这些阈值要紧?一条没切干净的会话,会把「我先看巴黎、两小时后改看东京」拼成同一个句子,于是模型学到巴黎像东京。垃圾进、垃圾出,在这一章是字面意义上的。
数据量级值得记住:Airbnb 在 KDD 2018 的论文里报告的是用超过 8 亿次搜索点击会话、为约 450 万个房源训出 32 维嵌入。32 维——不是 512、不是 1024,因为这里要的是「可替代」这一层粗结构,维度高反而更容易过拟合到噪声,而且直接乘进内存与查询成本。
把词换成房源、把句子换成会话,word2vec 的 skip-gram 就能原样搬过来:滑动窗口取一个中心房源和它前后的上下文房源,训练目标是让同窗口内的向量内积变大、让随机抽来的房源向量内积变小。
白话说就是两个动作:同窗口的往一起拉,随机抽的往外推。为什么是「随机抽」而不是跟全部 450 万房源比一遍?因为那样每一步都要做一次 450 万项的归一化,训练成本直接爆掉——这就是负采样存在的唯一理由。
直接把 word2vec 搬过来会得到一个能跑但没用的模型。书里给出的两处改造,是这一章最值钱、也最常被面试追问的部分。
改造一:把预订当全局上下文。点击是弱信号——点开只说明「看了一眼」;预订是强信号——那是真金白银的选择。做法是:对于最终产生预订的会话,把被订下的那一套房源加进该会话的每一个滑动窗口,无论窗口滑到哪里。效果是整条轨迹上的向量都朝最终成交的那一套偏,学到的不只是「被一起看过」,还有「一起看过并且最后选了它」。
改造二:补一组同市场负样本。这条更微妙。用户搜索是聚集的——一次会话基本只在一个城市里挑。于是窗口里的正例天然全部同城,而全站随机抽的负例大概率来自别的城市。模型发现了一条捷径:只要学会分辨城市,损失就能降下来。训出来的嵌入于是变成了一张地理聚类图,同一个城市内部糊成一团——而「同城内部哪套能替代哪套」恰恰是产品唯一要的东西。
修法很朴素:在原有的全站随机负例之外,再抽一组来自同一市场的负例。这样「同城」不再是一个能降损失的捷径,模型被迫在同城内部分出结构。
冷启动是这套方法的结构性弱点:新上架的房源一条会话都没有,没有共现可学。书里采用 Airbnb 的属性回退——取地理最近的若干套(如 3 套)、房型与价位档相同的房源,把它们的向量取平均当作初始值,等它自己攒够会话再替换。
服务侧是典型的「离线重、在线轻」:嵌入按天离线重训(房源的可替代关系不像广告那样以小时计漂移),线上要么查预先算好的 top-k,要么走 ANN 索引。
没有 ground truth,离线指标只能是自造的代理。书里的办法是拿已经成交的会话当尺子:对一条最终订了房源 X 的会话,在用户每一步点击时用当前房源的向量去检索候选,看 X 的排名——嵌入若学得好,越接近下单,X 排得越靠前。这条曲线能在不上线的情况下比较训练配置,但终究是代理;真正的判据仍是线上 A/B:模块 CTR,以及从该模块进入并成交的转化率。
表 1 · 「相似」怎么定义:这一列「什么时候选它」才是面试想听的
| 方案 | 信号来源 | 能捕捉「可替代」吗 | 冷启动 | 什么时候选它 |
|---|---|---|---|---|
| 属性规则 | 人写的筛选条件 | 不能,只会给参数孪生 | 天然没问题 | 第一版、或作为兜底与过滤层 |
| 内容特征余弦 | 房源属性 / 图文向量 | 弱,只到「长得像」 | 好 | 冷启动多、行为数据稀薄的新平台 |
| 协同过滤 / 矩阵分解 | 用户×房源的长期交互 | 能,但偏「长期口味」 | 差 | 交互密集且用户长期回访的场景 |
| 会话共现嵌入 | 单次会话内的点击序列 | 能,且抓的是当下这次比较 | 差,需属性回退 | 本章落点:会话内决策、意图短期、日志量大 |
| 有监督双塔 | 需要构造正负样本对 | 能,且可注入业务目标 | 中等 | 已有成熟标签体系与训练基建,要多目标 |
表 2 · 线上怎么查:延迟、新鲜度与灵活性的三角
| 方案 | 延迟 | 新鲜度 / 灵活性 | 成本 | 适合 |
|---|---|---|---|---|
| 离线预算 top-k | 最低,一次 KV 点查 | 差:换向量要重算全量;不能带查询期过滤 | 存储换计算,可预测 | 结果不随请求变化的固定模块 |
| ANN 在线索引 | 低,毫秒级 | 好:新向量可增量入库,可带日期 / 价格过滤 | 索引常驻内存 | 本章落点与绝大多数生产系统 |
| 精确暴力检索 | 不可接受 | 最好 | 每次请求上亿次乘加 | 只在离线评估、小规模灰度里用 |
还有一个高频追问:负样本配比。全站随机负例给全局区分度、同市场负例给局部分辨率,两者都要——只留同市场的,跨市场向量之间反而失去可比性。
这套形状远不止民宿:凡是「没有相似标签、但有海量行为日志」的场景都能套——电商的「看了又看」、音乐的「相似歌手」、招聘网站的「相似职位」。判断口径只有一句:你要的相似,是属性上的像,还是行为上的可替代?后者才需要这一章。
面试必问三处:为什么不用属性余弦、同城搜索为什么会毁掉嵌入、怎么修(最能区分「读过」和「读懂」)、冷启动怎么办。
① 这一章的转向是不定义「相似」,让用户会话替你定义:同一次会话里被连着点开的房源,就是彼此的替代品。
② 因此它是全书唯一一道自监督的题——标签由日志的共现结构自动生成,几乎不做特征工程。
③ 建模是表示学习而非分类:每个房源学一个向量(Airbnb 用 32 维),相似度就是余弦。
④ 机制是 skip-gram + 负采样:同窗口拉近、随机负例推远;负采样的唯一理由是避免对数百万候选做全量归一化。
⑤ 改造一:把最终下单的房源当全局上下文,让整条轨迹朝真实成交偏——点击是弱信号,预订是强信号。
⑥ 改造二:补一组同市场负例。否则「分辨城市」就是降损失的捷径,嵌入退化成地理聚类,同城内部毫无结构。
⑦ 冷启动靠属性回退:取最近的同房型同价位房源取向量均值;服务侧走「离线按天重训 + 线上 ANN 检索」。
⑧ 评估没有标准答案:离线用「成交房源的排名是否随会话推进而上升」当代理,最终仍以线上 A/B 的 CTR 与转化率为准。