专业书籍精读 · ML System Design Interview · 第 11 章
Machine Learning System Design Interview · Ch 11 · Ali Aminian & Alex Xu · 2023
你注册一个社交软件,资料只填了个名字。第三天它推给你一串「你可能认识的人」,里面居然真有你大学室友、上一份工作的同事。这一章讲的就是:它凭什么猜得出来。
你会以为它是在拿你的资料去比对——同一所学校、同一家公司、同一个城市。这些当然有用,但都不是主力。
真正的主力只有一条线索:你们有几个共同的朋友。光靠这一条,就能把大部分该推的人推出来。它甚至不需要知道你是谁、在哪、做什么。
难的地方不在「猜」,在「要猜多少次」。
平台上有十亿人。要判断「你和谁可能认识」,字面意思是把十亿人两两配一遍——那是个天文数字,全世界的机器一起算也算不完。所以第一件事根本不是造一个聪明的模型,而是:怎么在不看绝大多数人的情况下,就知道他们不值得看。
第一步,只看「朋友的朋友」。新认识的人,绝大多数是通过已有的人认识的。所以只把你朋友的朋友当候选——一下就从十亿缩到几万,少了十万倍。
第二步,共同朋友要打折。朴素做法是数共同朋友的个数,谁多推谁。但这里有个坑:如果那个中间人是个手握五千好友的交际花,那他跟谁都「共同」,等于什么都没说明。反过来,一个只有三十个朋友的人同时认识你俩,说明你俩八成真在同一个圈子里。所以:中间人自己朋友越多,他那一票越不值钱。
第三步,给每个人在「社交地图」上安一个位置。让每个人先看看自己周围都是些什么人,再看看这些人周围又是些什么人,据此给自己定个坐标。坐标挨得近的两个人,就该被互相推荐——这一步能捕捉到「你俩没有共同好友,但你俩的朋友圈整体长得很像」这种更隐蔽的信号。
这套东西厉害在:它只看「谁跟谁连着」,就能还原出现实里的人际圈子。
代价也正在这里:它知道你们有关系,却不知道你为什么不想被这个人找到——所以它会把前同事、前任、或者你根本不想被认出来的场合里遇到的人,一并推到你眼前。
好友推荐的胜负不在模型,在先用「朋友的朋友」把候选人从十亿砍到一万;然后数共同好友,并且按中间人自己的朋友数打折——交际花的一票,远不如小圈子里那个人的一票值钱。
想进到具体机制、记号和示意图? → 切到精读版
这一章把「好友推荐」翻译成图上的链接预测(link prediction):给定当下的社交图,预测哪两个还没连上的点会在未来连上。你以为难点是模型选型,其实难点在它前面一步——十亿用户两两配对是 1018 量级,任何模型都没资格给它们打分。这一章真正的骨架是:先用二跳邻居把候选压到 104,再谈特征,最后才谈模型。
ML 系统设计面试 Part II「十道真题」的最后一道,也是全书唯一一道输入是一整张图、而不是一批彼此独立的样本的题。它上承 Ch9 相似房源(从共现关系学嵌入)与 Ch10 信息流(候选 + 排序的漏斗结构),把这两条线合到图上。对应现实里的 LinkedIn「你可能认识的人」、Facebook 的同名功能、X 的 Who to Follow。
先把规模摆出来。十亿量级的用户,人均好友数在几百这个量级。把所有用户两两配对是 n(n−1)/2 ≈ 5×1017 对;就算一对只花 1 微秒,也要一万多年。「给每一对打分」这个框架从一开始就是错的,这是本章和前十章最大的结构差别:前面几章的候选池是天然有界的(一个人的信息流、一个视频库),这一章的候选池是用户数的平方。
另一头的压力来自产品:社交产品普遍把「新用户在 N 天内建立 K 条关系」当激活的核心指标。注册完看到一片空白基本就流失了——而新用户恰恰是图上完全没有位置的那批人。
所以这一章要同时解决两件事:怎么把候选集从 1018 砍到能算的量级,以及在这个砍完的集合里怎么排序。前者决定了系统能不能跑,后者决定了效果好不好。
最容易犯的错是用当前的图去预测当前的图——你要预测 u 和 v 会不会成为好友,却在算特征时用了「u 和 v 之间已经有边」这个事实,那是标签泄漏(label leakage),离线指标会漂亮得离谱,上线一片死寂。
正确做法是时间切片:取 t 时刻的图快照算特征,把 t 到 t+Δ(比如两周到一个月)之间新增的边当正样本。负样本从「t 时刻还没连上的二跳候选」里采——不能从全体用户里随机采:随机两个人几乎必然不认识,模型学会「默认说不认识」就能拿 0.99 的 AUC,却学不到任何有用的区分力。负样本必须和正样本来自同一个候选池,这叫 hard negative。
依据是三元闭包:现实中的新好友,绝大多数是通过已有好友认识的。所以只取二跳邻居当候选。规模估算很直观——度为 d 的用户,二跳邻居的量级是 d²(去重后更小):d ≈ 300 时是 104–105,相比 1018 砍掉了十几个数量级。
代价必须写清楚:凡是和你没有任何共同好友的人,永远进不了候选。刚搬到新城市、刚换行业、刚注册的用户全在这个盲区里。所以二跳邻居只能当主候选源,旁边必须并联几路「非图」候选:同学校同专业且年份重叠、同公司同期在职、通讯录/邮箱导入、同城同群组。这几路的作用不是提升精度,而是给图上还没有位置的人一个入口。
朴素的排序信号就是共同好友个数,谁多推谁。它好用得惊人,但有个致命缺陷:hub 节点——那些手握五千好友的招聘、网红、销售账号。他们让无数互不相干的人凭空拥有「1 个共同好友」,信号被稀释成噪声。
修正办法是 Adamic-Adar:AA(u,v) = Σ 1/log(deg(w)),其中 w 跑遍所有共同好友,deg(w) 是这个中间人自己的好友数。白话就是:每个中间人按他自己朋友数的对数取倒数打折,朋友越多的中间人,那一票越不值钱。
要诚实的一点是:这个折扣是对数级的,不是线性的——5000 好友的 hub 贡献 0.12,30 好友的贡献 0.29,只差 2.4 倍,hub 被削弱但没被抹掉。工业界往往还要额外加一条硬规则:度超过某个阈值的节点直接不进二跳展开,否则光是它的邻居表就能把候选集撑爆。
再往上一层是边权:好友关系不是 0/1——有互动的边远强于加完就再没说过话的边。把共同好友按两侧边强度加权,收益通常大于换一个更复杂的模型。
第一代,纯规则:直接按 AA 分数排序。零训练成本、完全可解释、新用户一有边就能用。它是任何 PYMK 系统的正确起点,也是所有后续模型必须打败的基线——很多团队的第一版 A/B 就输给了它。
第二代,pointwise 有监督分类:把候选对 (u,v) 当一条样本,喂三族特征——图特征(共同好友数、AA、最短路、两人各自的度)、属性特征(同校同司且年份重叠、同城、年龄相近、语言)、交互特征(是否访问过对方主页、停留多久、是否被同一个人同时提及)——用逻辑回归或梯度提升树输出 P(会连边)。这是工业界最长寿的方案:好调、好解释、能上线。它的天花板在于图结构只能以人工特征的形式进入模型,二跳以外的结构信息全丢了。
第三代,图神经网络,也是原书推荐的终点。机制用白话讲:每个人先把自己的属性写成一张卡片;第一轮,每个人把卡片发给所有好友,各自把收到的一叠卡片揉成一张新卡片;第二轮再传一次——此时你手上这张卡片已经同时编码了你自己、你的朋友、以及你朋友的朋友是什么样的人。这张卡片就是节点嵌入 z。预测就一步:P(u,v) = σ(z_u · z_v),两个向量做点积再压成概率。
GNN 的代价同样具体:全图放不进显存,训练要做邻居采样(每层只随机取若干个邻居);推理要把全量嵌入预先算好缓存,图一变嵌入就旧了;hub 节点的邻居采样天然有偏;而且出了问题几乎没法像 GBDT 那样看特征重要度定位。它赢的是那部分「没有共同好友、但朋友圈整体形状相似」的信号——这部分恰恰是二代模型的盲区。
关键观察:社交图变化极慢——一个活跃用户一天新增的边大概 0 到几条。所以整条链路都能搬到离线:批任务定期为每人展开二跳、算特征、打分、截断 Top-K,写进 KV 存储;线上只做一次 get 加过滤(已好友 / 已忽略 / 已屏蔽),几十毫秒返回。模型多重都不影响线上延迟——这是本章最省事、也最容易被面试者漏掉的一步。
例外只有一个:刚注册、刚导入通讯录、刚加了第一批好友的用户,必须立刻看到结果。这类用户走触发式增量重算——图上他那一小块子图很小,单独算一次并不贵。
第一张表回答「候选从哪来」。注意最后一列——没有任何一路能独当一面,二跳邻居覆盖率最高却恰好盖不住最需要推荐的新用户。
| 候选源 | 覆盖谁 | 量级 / 成本 | 代价与风险 |
|---|---|---|---|
| 二跳邻居(FoF) | 有若干好友的存量用户,命中率最高 | ~10⁴/人,离线批算数小时 | 新用户与「圈子外」的人完全进不来;hub 会撑爆展开 |
| 同校 / 同司 + 年份重叠 | 换城市、换行业,但履历有交集的人 | 小,靠属性倒排即可 | 依赖资料完整度;大公司会退化成「同城随机」 |
| 通讯录 / 邮箱导入 | 纯新用户,唯一能秒出结果的一路 | 极小,用户自带 | 隐私红线最密集的一路:会暴露用户没打算公开的关系 |
| 同城 / 同群组 | 线下有交集、线上还没连上的人 | 中等 | 精度低,只能兜底,容易推出纯陌生人 |
| 嵌入向量 ANN 检索 | 无共同好友、但社群结构相似的人 | 需全量嵌入 + 向量索引 | 最贵;嵌入过期即失效,可解释性最差 |
第二张表是模型选型:三代方案不是替代关系,成熟系统通常是「规则兜底 + 监督主力 + GNN 补盲区」并存。
| 方案 | 什么场景选它 | 代价 |
|---|---|---|
| 规则:AA 排序 | 冷启动期、算力紧、需要完全可解释——任何 PYMK 的第一版与永久基线 | 只用得上二跳内的结构,属性和交互信号全浪费;天花板明显 |
| pointwise 监督 (LR / GBDT) | 有一定日志积累、要把属性与交互特征一起用上——工业界最长寿的主力 | 图结构只能靠人工特征进入;每加一种结构信号都要重做特征工程 |
| 图神经网络 | 图足够大且致密、有 GPU 与工程团队、要吃「无共同好友但社群相似」的信号 | 训练要邻居采样、推理要缓存嵌入、图一变就过期;调试与归因极难 |
还有一个不在模型里的权衡,面试里最能拉开差距:目标该定成「会不会认识」还是「会不会发出邀请且被接受」。后者才是产品要的,而且是双边事件——推得准但对方不接受,换来的是邀请疲劳:被打扰方开始忽略全部邀请,大盘接受率一起下滑。所以线上目标通常取「发出 × 被接受」,还要看两周后这条边是否仍有互动。
面试里说「用 GNN 做链接预测」是零分答案。得分点是四句话:先谈候选集再谈模型(1018 打不动)、标签必须时间切片(否则泄漏)、负样本必须从候选池里采(否则 AUC 虚高)、整条链路默认离线预计算(图变得慢)。架构评审里同理:真正的成本不在模型推理,在每天那次全量二跳展开。
① 好友推荐的本质是图上的链接预测:给定当前社交图,预测哪两个没连的点未来会连上。
② 这章与前十章的结构差别在于候选池是用户数的平方:109 用户 → 5×1017 对,「给每对打分」这个框架从一开始就不成立。
③ 救命的是三元闭包:只取二跳邻居当候选,一人 ~104 个,砍掉十几个数量级。代价是没有共同好友的人永远进不来,必须并联同校同司、通讯录、同城几路兜底。
④ 共同好友必须做度归一化:AA = Σ 1/log(deg(w)),中间人自己好友越多、那一票越不值钱;但折扣是对数级的,hub 还要靠硬阈值挡。
⑤ 三代模型是叠加不是替代:AA 规则(基线,别跳过)→ GBDT pointwise(主力,图结构只能靠人工特征进)→ GNN(补「无共同好友但社群相似」的盲区,代价是采样、缓存与嵌入过期)。
⑥ 因为社交图变化极慢,整条链路默认离线批算 + 在线查表,线上只剩一次 KV get 加过滤;只有新注册与刚导入通讯录的用户走触发式增量。
⑦ 打标签要时间切片(t 时刻特征、t→t+Δ 新增边当正样本),负样本要从二跳候选里采 hard negative——这两条错一条,离线指标就全是假的。
⑧ 线上目标不是「会不会认识」,而是「会不会发出邀请且被接受」;推得再准,换来邀请疲劳也是负收益。