第三部分:十亿规模下的查看者条件投影

2026/08/08

#recommendation-systems #social-graphs #machine-learning

头两篇给出了诊断,并检验了它。物品推荐跑在二分图上,社交推荐跑在单分图上,这个差异把"查看者–候选"的相关性矩阵推向高有效秩,这正是为什么"共享嵌入加注意力"那套打法会欠拟合、而且无论你在上面叠多少层都照样欠拟合。如果你认真对待这个诊断,修复办法就是:别再给每个候选一个固定向量,而是让它的表示依赖于谁在看,把候选嵌入从一个名词变成一个动词。

这话说起来容易,乍一看却做不到。如果每个查看者都要有自己看待每个候选的方式,最直接的实现就是一个逐查看者的投影矩阵 \(W_v\),而在十亿查看者上,这是约 64TB 的参数,而且是在你还没训练任何一个之前。所以真正的问题不是"要不要把嵌入做成以查看者为条件",而是"你能不能在不让成本爆炸的前提下做到"。这篇就是答案,分三部分:一个刻意做成"与查看者无关"的候选嵌入、一个刻意做成低秩的逐查看者算子、还有一个把整个算子挪到查询侧的上线技巧,让候选侧永远不必知道它的存在。

分工

把查看者 \(v\) 下、候选 \(c\) 的分数写成

$$\text{score}(v,c) = \langle a_v,\, W_v E_c \rangle,$$

三个部件。\(E_c\) 是候选的嵌入,它与查看者无关,只装那些对 \(c\) 稳定成立的东西。\(W_v\) 是一个 \(d \times d\) 的算子,属于查看者,所有"依赖查看者"的东西都住在这里。\(a_v\) 是一个小的、学出来的查询头。这么写的要点是分离:所有随查看者改变的东西进 \(W_v\),所有只属于候选的东西进 \(E_c\)。这正是第一篇里"二分图起源"那个论证所要求的。在单分图里,一个节点唯一稳定的东西是它的结构;对查看者的依赖必须被分解进一个算子,而不是被平均进一个共享向量。

候选嵌入是个名词,在社交推荐里它得是个动词。这个动词就是 \(W_v\)。

候选嵌入到底是什么

如果 \(E_c\) 要做成与查看者无关,那自然的问题是:一个人身上有什么东西,是可以不依赖于"谁在问"而稳定成立的?答案是唯一一样人身上有、而物品没有(至少没有更丰富形式)的东西:他连接的方式。一个人没有物品那种稳定的内容语义,没有类型、品牌、价格可以绑,所以嵌入里"与查看者无关"的那部分,必须是一个结构指纹。度数、三角数、他所在的那个或那些社群、他周围小 motif 的分布、个性化 PageRank 把他送到哪里的一张快照。具体地说,这才是"嵌入编码的是边、而不是兴趣"的意思。

构造这个指纹有几种办法,成本和保真度从低到高。最便宜的是一个朴素的可学习 ID,每个人一个向量、随机初始化、端到端训练,它让训练信号隐式地把结构发现出来,而且因为它就是一张表,扩到十亿毫无压力。再丰富一点,是显式地算结构特征(角色发现那一脉的做法)再投影下来;再丰富,是在邻域上跑一个一两跳的浅图网络;最忠实的是一个子结构分词器,但我不会在这儿用它,因为正如第一篇所说,它在真实图的度数下会塌成直链。

我真正会从之起步的,是最便宜的那个、随机初始化的 ID,理由在实验上:它把 \(W_v\) 的贡献孤立出来了。如果整个模型里唯一的、感知查看者的部件就是那个算子,那你测到的任何提升都是算子的提升、不是指纹的提升,之后你再把结构特征消融进来,看它还能加多少。

有一个正确性约束值得明说,因为它是那种看起来像细节、却会悄悄毁掉整个模型的东西。候选嵌入不能是"随机的、且冻结的",它必须是"随机的、但训练的"。原因在结构上:稍后会看到,上线技巧会把 \(W_v\) 挪到查询侧,这意味着逐查看者的查询不再依赖 \(c\),而这意味着关于 \(c\) 的每一点信息都必须住在 \(E_c\) 里。一张冻结的随机表不带任何关于候选的信息,建在它上面的模型什么也学不到。得允许训练信号把随机初始化推成某种编码了候选稳定身份的东西。顺带,\(W_v\) 必须只是 \(v\) 的函数、绝不是 \(c\) 的,不是因为时髦,而是因为把候选信息放进查看者侧的算子,会破坏让整件事负担得起的那个上线模式。

构造这个指纹的四种办法,从便宜到贵:

构造法 成本 保真度 能扩?
可学习 ID(随机初始化、训练) 最低 隐式 能,就是一张表
结构特征 → MLP(RolX 那一脉) 显式 能(可预计算)
浅 GNN(1–2 跳) 要图基础设施
子结构分词器(G2PM) 最高 不能,度数 500 下塌成直链

保持低秩的算子

贵的那个对象是 \(W_v\),而让它变得可行的办法,是永远不去构造那个完整的 \(d \times d\) 矩阵。把它分解成一个低秩残差,

$$W_v = I + U_v V_v^{\top}, \qquad U_v, V_v \in \mathbb{R}^{d \times r},\; r \ll d.$$

这是标准的低秩适配形式,我想说清楚:这个形式本身是借来的、不是发明的,要紧的是它落在哪里。[1] 单位项负责保持候选的基础嵌入不动,秩-\(r\) 项是查看者做重新投影的地方。把 \(r\) 设成 0,整件事就塌成 \(W_v = I\),也就是纯矩阵分解,一个有用的消融起点,因为它让你直接从 \(r=0\) 和 \(r>0\) 之间的差距里读出"以查看者为条件"带来的提升。实际中,一个小的 \(r\)、大概 4 到 16,是工作区间;在 \(d=128\)、\(r=8\) 时,这个算子是每个查看者几 KB,而不是几十 KB,而在十亿查看者上,这就是"存得下"和"存不下"的差别。

算子从哪来

即便是每个查看者几 KB,作为一张独立表存下来,在十亿用户上也是好几个 TB,而且它有个比体积更糟的问题:一个全新查看者在你还没拿他训练过之前没有任何因子,于是你没有冷启动。修复办法是:干脆不存因子,而是去生成它们。保留一个共享的、小的网络,以查看者的"结构加行为"指纹为输入,产出 \((U_v, V_v)\)。这一来,逐查看者的状态就只是指纹,和候选嵌入同类的东西,而从指纹到算子的映射,是一套所有人共享的权重。

这里是整个设计从"一堆技巧"变成"一个想法"的地方。候选嵌入和查看者的算子,都从结构指纹派生而来:候选的指纹被读成一个静态的名词,查看者的指纹被生成器变成一个动态的动词。一个刚到的查看者,凭他的指纹立刻就能得到一个投影,哪怕他还没产生任何训练样本。整个系统真正的智能,就集中在这一个映射上,从"一个人怎么连接"到"他怎么看",其余全是簿记。

存下来的因子 vs 生成的算子:

逐查看者存(U_v, V_v) 超网络 G_θ(f_v)
存储 N × 2dr(1B 下 ~TB 级) 一个共享 θ + 指纹表
冷启动 没有(新查看者没训练过) 立刻可用
泛化 没有

让它能上线的那个技巧

这是我最满意的部分,因为它把看起来最难的约束给消解了。分数是 \(\langle a_v, W_v E_c\rangle\),字面读作"用这个查看者的矩阵把每个候选投影一遍、再比较",如果上线真得这么做,你就会卡在"为每个查看者把整张候选表重新投影一遍"上。但内积在一个有用的意义上是对称的:

$$\langle a_v, W_v E_c\rangle = \langle W_v^{\top} a_v,\, E_c\rangle = \langle z_v, E_c\rangle, \qquad z_v := W_v^{\top} a_v.$$

因为 \(W_v^{\top} = I + V_v U_v^{\top}\),算 \(z_v\) 就是 \(z_v = a_v + V_v(U_v^{\top} a_v)\),也就是寥寥几次便宜的矩阵–向量乘,每个查看者 \(O(dr)\),\(r\) 在个位数。算子被整个挪到了查询侧。

后果是,候选表保持得和任何普通双塔系统里一模一样:一组静态向量、预算好一次、建好近邻检索索引。[2] 每个查看者你算一个向量 \(z_v\),对静态表跑一次标准的 ANN 查找。所有"以查看者为条件"的机器,都不碰候选侧。这不是一个新的上线模式,它是每个大型检索系统都已经在用的那个,但它是"逐查看者算子在上线时成本为零"的原因。你几乎是免费拿到了"以查看者为条件",这里的"免费"是指它把每次请求的工作,从"重新投影十亿个候选"变成了"多算一个向量、然后做你本来就要做的那次查找"。

能不能扛住规模

账算下来很干净。十亿用户、128 维、半精度的候选表大约 256GB,这数字大,但对于一个生产级嵌入库来说很普通,放进内存毫无压力。逐查看者的查询是几千次浮点运算,对延迟可以忽略。生成器是个以 MB 计的小网络。十亿向量上的 ANN 索引,在那个早已为物品做这件事的系统规模上,是个已解决的问题。

规模真正咬人的那一处,不是模型,而是训练数据。模型本身很小,一张表、一个小生成器、一个查询头,所以瓶颈是把万亿条边喂给它,而不是在它上面算。这和每个十亿规模图系统都有的边搬运问题是同一个,处理方式也一样:子采样、做流行度偏差校正、数据并行地训。另一个悄悄的成本是结构指纹,因为让指纹有信息量的那些东西,三角数、个性化 PageRank,恰恰是在这么大的一张图上很难保持新鲜的多跳量。约束是让指纹保持浅、一两跳,并且增量重算、而不是每次刷新从头来。在这两条前提下,“能不能扩到十亿用户、万亿边"的答案是能,而这些前提,是身处这个规模的所有人本来就在忍受的前提。

这笔账:

部件 形式 1B 用户下
候选表 E_c [N, d] fp16 ~256 GB
逐查看者查询 z_v O(dr) 可忽略
生成器 G_θ 小 MLP MB 级
ANN 索引 建在 E_c 上 规模上已解决
训练数据 万亿条边 IO 受限(子采样)

为什么不直接把注意力做成双向

到这一步,我最常听到的反驳是:有一个更简单、更时髦的修复,把序列模型的注意力做成双向,让查看者的加好友历史去 attend 候选的历史、候选的历史也回 attend 查看者。理由是:加好友是双向的,那就把两个方向都建模了,让注意力去解决。

我想认真对待它,因为它有一半是对的。双向注意力确实解决了一件真事,那就是互惠性。一次好友请求不是一方想要就算数,得双方都同意,而一个只预测 \(p(v \to c)\) 的模型漏掉了 \(p(c \to v)\) 那一半。把两个方向折进来再合并,是个货真价实的改进,它有它的去处,具体说,是在那个"我们展示了这个候选之后、连接是否真能形成"的下游阶段,即接受模型里。在那里它是对的工具。

它替代不了的,是相关性算子,原因和普通注意力替代不了一样,第一篇讲过。注意力仍然是个秩-\(d_h\) 的双线性算子;让它双向跑,给你的是两次秩有界的 pass,而不是一次高秩的 pass。[3] 那个真正驱动好友推荐的"共同好友"信号,确实会穿过双向注意力,因为一个共同邻居同时出现在两边的历史里,但它穿过去的时候,是几十万个交叉项里一个软的、匿名的共现项,被淹没、被投影进一个低秩相似度里。它不是 Adamic-Adar 那种特征直接算出来的、硬的、身份相关、度数加权的计数。而更深的问题是,整套双向的表述,是在二分匹配,相亲、招聘,上长出来的,在那里两边确实是不同的类型、拓扑把角色固定死了。社交图是单分图;把一个双向模型拧上去,悄悄地重新施加了一个拓扑并不支持的二分假设,而它崩的恰恰是"以查看者为条件"住的那个地方。

有一个干净的办法来终结这场争论,也就是我在这个领域对任何东西都会跑的那个消融:如果双向注意力真的抓到了结构信号,那么在它上面再加显式的图特征(像 Adamic-Adar 和个性化 PageRank)应该几乎没有提升,因为信号已经在注意力里了。我愿意下点注赌它不是零提升,那些特征仍然会以一个有意义的幅度动那个指标,因为结构信号不是注意力能"发现"的东西,它是你必须算出来、亲手交给它的东西。

到底有什么是新的

整个系列里,我一直小心地把诊断和机制分开,而机制也配得上同样的诚实,因为它绝大部分并不新。推荐里的"逐用户投影"不新;十年前 McAuley 等人就提出为每个用户学一个投影矩阵、让它取代用户专属因子。[4] “逐用户低秩适配器"不新;最近的推荐–LLM 文献为每个用户维护一个独立适配器、并在其间路由。[5] 低秩残差形式就是低秩适配;指纹的想法是角色发现;上线模式是双塔检索;那个从特征生成算子的,是超网络。这些部件单独拎出来,没有一个是贡献。

新的、也是我真正愿意为之辩护的,比任何单一机制都更窄、也更有用,而且它不是一条必然性定理。我很想能写下的那条链,单分图、所以高秩、所以需要逐查看者投影,断在最后一环:上面那个查询侧的恒等式,把分数矩阵的秩钉死在至多 \(d\),无论 \(r\) 取多少,所以一个会塌到查询侧去的算子,根本不是回答"秩不够"这个诘问的那样东西。单分拓扑真正逼出来的,是不对称性,因为每个节点一个共享嵌入、再加一个对称的点积,会让 \((v, c)\) 的分数和 \((c, v)\) 的分数变成同一个数,而这对一个有方向的关系来说,形状就是错的;逐查看者投影是打破这个对称的一种办法,而更便宜的那种办法本来就在这套设计里,因为 \(a_v\) 和 \(E_c\) 本就是两张各自独立的逐节点表,\(\langle z_v, E_c\rangle\) 在 \(r = 0\) 时就已经是不对称的了。所以不对称性这个论证只走到 \(r = 0\) 就停住,我宁愿把这句话说出来,也不愿假装是秩论证把这个算子从解空间里挑了出来。剩下的东西,我认为也够了,是诊断和组装。诊断是:把物品推荐那套打法在人身上弄坏的,是单分拓扑,不是稀疏性、也不是模型容量,因为它把最强的信号、三元闭合,放进了一个二分拓扑在结构上被禁止拥有的 motif 里,而那个信号是一对节点周围邻域的函数、不是这一对节点本身的函数。组装是让这个算子能在十亿用户上线的那套东西:一张与查看者无关的结构化候选表、一个生成出来的低秩算子、以及那个让候选侧纹丝不动的查询侧挪移。而这个算子自己的贡献,比一个秩论证平庸得多,却仍然值得要:查看者的向量不再是表里存下来的一行,而变成了查看者自身结构的一个生成函数,冷启动、跨查看者的泛化、以及不随查看者数量增长的存储,都是从这里来的。诊断解释了这个问题为什么不一样;查询侧的分解解释了这个回应为什么负担得起。这就是全部的论断,我宁愿把它说成这么大、也不愿去吹大它。

复用 vs 新的:

部件 状态 来源
逐用户投影矩阵 复用 McAuley 等 2015
逐用户低秩适配器 复用 RecLoRA 2024
I + UV^T 残差 复用 LoRA 2021
结构指纹 复用 RolX 2012
双塔上线 复用 生产标配
拓扑优先的诊断 新(作为框架) 单分图 ⇒ 三元闭合 ⇒ 信号在这一对的周围、不在这一对里
P2P 的查询侧分解 让它能上线
绕过秩天花板 不主张 任何 r 下秩都 ≤ d;见上文

收尾

这是结构系列里的最后一篇,值得把它的落点说清楚。楼层搭好了:一个作为结构名词的候选嵌入、一个作为生成出来的动词的逐查看者算子、以及一个让这个动词免费的上线技巧。它也如第二篇所坚持的,是一块中间楼层、不是顶楼。它替代不了图特征,表达不了张量或超图所能表达的完整高阶结构,也带不出双线性的世界。它做的,是在"你有十亿个查看者、而相关性函数对每个查看者都换一个形状"这个特定场景下,用一个主要由你本来就要付的边搬运所主导的代价,把这个框架里查看者那一侧从结构生成出来、而不是从查找表里取出来。

如果你想再往前走,剩下的是证据,在一张真实社交图上跑那条"指标对维度"的曲线,看基线在哪里饱和、而以查看者为条件的模型在哪里继续爬;以及跑那个"在注意力上面再加 Adamic-Adar"的消融,用一个数字、而不是一场论证,去终结那个双向的问题。那是一篇更小、更经验性的文章,我会把读完了这三篇、却仍不确定诊断成不成立的人,指向那里。诊断是我最有把握的部分。楼层是我最有用的部分。而楼层是不是有诊断所预测的那么好,归根到底是一个实验,而实验,和论证不一样,是好读的。


注释

[1] 这是标准的低秩适配形式,我想说清楚:形式本身是借来的,见 Hu 等人的 LoRA(2021)。

[2] 候选侧静态、查询侧在线、配 ANN 的双塔检索,是 YouTube/Google/Allegro 等大型检索系统的生产标配。

[3] 注意力的秩天花板,见第一篇引过的 Bhojanapalli 等人 “Low-Rank Bottleneck in Multi-Head Attention Models”(ICML 2020)定理 1。

[4] 见 McAuley 等人 “Improving Latent Factor Models via Personalized Feature Projection”(CIKM 2015):为每个用户学一个投影矩阵,取代用户专属因子。

[5] 见 RecLoRA(2024):为每个用户维护独立的 LoRA,并用 meta-LoRA 在它们之间软路由。