GC-SAN详解:图神经网络与自注意力如何破解会话推荐难题
发布时间:2026/10/15 14:24:34 锦皓数字建站

做推荐系统的人迟早会被一个问题逼到墙角用户没有任何历史画像只有当前这几次点击你该怎么预测他下一步想点什么这个场景就是会话推荐。今天想聊的这篇论文给出的解法是用图神经网络把会话内物品之间的关系建模成一张有向图再用自注意力把整个会话的上下文信息融进去。论文提出的方法叫GC-SAN也就是Graph Contextualized Self-Attention Network图上下文自注意力网络。它解决的核心问题是不依赖用户长期历史仅凭当前会话里几条点击记录预测下一个最可能被点击的物品。如果你正在做电商的短期兴趣建模、短视频的即时反馈推荐或者在研究图神经网络和Transformer结构怎么在序列数据上配合使用这篇论文都值得认真读一遍。下面我会从问题背景、模型设计、实验细节到复现踩坑把它完整拆开。1. 会话推荐为什么难先理解GC-SAN在解决什么问题1.1 会话推荐的定义与两类核心难点会话推荐的任务定义很简单给定一个session内的点击序列比如 [运动鞋, 鞋带, 收纳盒, 运动鞋]预测用户下一个最可能点击的物品。这里的session指一次连续的浏览过程通常持续几分钟到几十分钟。这个任务和传统推荐最大的区别在于传统协同过滤、矩阵分解都默认用户是一个稳定的实体你看了什么、买过什么都会沉淀成长期画像。但真实业务里大量流量来自游客、未登录用户账号体系失效画像根本不可靠。会话推荐退回到最小可用信息只用当前这段行为序列本身做出预测。我见过不少产品把这个场景叫做“无身份推荐”或者“短会话推荐”本质是同一件事。任务难在两点。第一数据极度稀疏。一个session往往只有3到15次点击这点信息连一个像样的用户向量都拼不出来。第二意图漂移非常常见。用户可能先看球鞋又点进鞋带再跳到收纳盒最后回头又点了一次运动鞋。模型既要抓住他当下的真实意图又不能被中间的历史噪声带偏。就好比逛超市你从货架一头走到另一头购物车里的东西反映的是当下的采买计划跟你上周买了什么没有直接关系。会话推荐要学的正是这个“当下计划”的规律。1.2 从GRU到SR-GNN前人在会话建模上走过的路会话推荐不是这两年才有的方向。早期主流是统计方法S-POP统计当前session里出现次数最多的物品直接推荐Item-KNN计算物品之间的共现相似度。这些方法简单、可解释但对“用户从A到B”的转移顺序不敏感遇到冷门物品直接失效泛化能力很弱。之后深度学习登场。GRU4REC用GRU把点击序列编码成一个隐状态NARM在GRU基础上加注意力把序列里每个位置的重要程度区分出来。这两类模型有一个共同假设用户行为是一段线性序列按时间顺序读完就能得到整体表征。但实际问题里用户从铅笔跳到橡皮再跳回铅笔的包装盒物品之间的关系是非线性的多跳关系。线性RNN对这种跳转结构编码起来非常吃力。SR-GNN把建模思路改成了图。它把一个会话内的物品看作节点点击顺序构成有向边然后用门控图神经网络在图结构上做信息传播。这个思路比RNN高明的地方在于物品之间的共现和顺序关系都被压缩进图的边里哪怕两个物品在点击序列里隔了好几步模型也能通过图路径看到它们之间的间接关联。但SR-GNN也有明显短板它把所有注意力都放在局部图传播上整个会话的全局意图并没有被显式建模。GC-SAN就是站在SR-GNN的肩膀上把全局上下文补了回来。我在读这篇论文前专门把这条技术脉络捋了一遍否则很难理解它每一步设计为什么要这么做。给一张简单的对照表方便你建立坐标系方法核心思路关键局限S-POP统计session内物品频率完全没有顺序信息Item-KNN物品共现相似度对冷门物品不友好泛化差GRU4REC循环网络编码点击序列线性假设忽略多跳关系NARMRNN 注意力仍是线性序列建模SR-GNN图神经网络聚合局部转移缺少全局上下文表达GC-SAN图传播 上下文自注意力把局部结构与全局意图结合2. GC-SAN架构拆解从会话图到上下文自注意力2.1 会话图每个session一张局部有向图进入模型前每个物品会被映射成一个低维embedding向量。接着把一个会话内的物品按点击顺序构成有向图。比如会话是v1→v2→v3→v2那么图上有边v1→v2、v2→v3、v3→v2并且v2出现两次它作为中间节点连接了前后两个物品。这里有一个特别需要注意的设计选择图是每个session单独构建的局部图不是把所有session拼在一起的大图。如果搞成全局大图不同用户、不同会话之间的噪声边会混在一起信息传播直接被污染。我在复现时试过一次错误做法用全部会话共用一个图P20掉了好几个点后来改成按session建图才恢复正常。邻接矩阵还需要区分出边和入边。用户从A点到B点和从B点回到A点语义是完全不同的。在消息传播时出边和入边用不同的权重矩阵做变换模型才能学到方向信息。这个细节和知识图谱里关系方向的建模思路一样方向错了语义就反了。2.2 GGNN传播为什么用门控而不是普通图卷积拿到图和初始embedding后GC-SAN用门控图神经网络做信息传播。每一轮传播时节点先聚合来自邻居的信息。聚合结果送入一个GRU风格的更新单元更新门决定保留多少旧状态重置门决定遗忘多少历史信息再混合邻居信息生成候选状态。不需要死记公式理解成节点在“看图说话”看看我的邻居是谁把值得吸收的信息吸收进来把不重要的旧信息忘掉。这个机制和普通GCN的邻域平均有本质区别GCN的聚合是静态的加权平均信息一进来就会和原向量混合缺少显式的保留/遗忘控制。而会话图很短、噪声很大GRU门控这种动态控制信息流量的做法对短序列更友好。论文里有个容易被忽略的配置GGNN传播步数其实只用了1轮。我第一次看的时候以为会像普通GNN一样按层叠加结果论文实验表明1轮就是最优。原因也不复杂一条点击转移链的有效信息大多藏在直接邻居里多传几轮会把不同子图混到一起稀释信号反而伤害效果。这个反直觉的配置直接帮你省了一笔训练开销我认为是这篇论文里最有价值的实操经验之一。2.3 上下文自注意力全局游标如何参与打分GGNN之后每个物品的表征已经带上了局部图结构但这时候整个session的“主线意图”还没有被显式表达。作者用了一个很朴素但有效的办法把所有物品的向量做平均池化得到会话上下文向量c然后用这个c去改造自注意力。普通自注意力里每个位置自己做query去和所有位置的key匹配注意力分布只看两两之间的关联没有一个显式的全局主题。但用户的点击session往往是有主题的比如你连续点了几支笔那“文具”这个主题就应该主导后续的注意力。GC-SAN的上下文自注意力把c当成一个所有位置共享的辅助信号。具体流程是这样的每个位置更新后的向量和c拼接经过线性变换得到带上下文的新表示。打分公式和标准缩放点积注意力类似但query和key里都带有c的贡献。最终聚合value时再把上下文信号融合回来。这样做的效果是如果某个物品和全局上下文更契合哪怕它在局部图上离得很远也能拿到更高的注意力权重。用一句话总结设计动机在局部图传播之外给模型一个显式的全局游标。没有这个游标注意力盯着相邻物品转有了游标模型才真正理解这一整段行为是围绕什么意图展开的。我给非技术背景的同事讲这段时常用一个类比同一句话“苹果”放在水果主题和数码主题里讨论的重点完全不同。c就是这段话的主题背景。2.4 预测头与损失函数的设计取舍编码完成后GC-SAN取最后一个真实物品的向量作为局部兴趣同时把整个session的向量做平均池化作为全局兴趣两者拼接后得到会话最终表征。然后把这个表征和候选物品embedding做内积打分经过softmax得到概率分布。损失函数是最常规的交叉熵配合L2正则。预测端没有做太复杂的事情说明论文把创新全部押在表征编码上预测头保持简单可控。这个设计思路在工程上很友好模型主体替换成本低头部改动少上线验证时不用动推荐漏斗的整体结构。3. 实验数据说了什么SOTA指标背后的三个细节3.1 数据集、评估指标与预处理套路实验的公开数据集是业界经常用的两个电商会话数据集一个叫Yoochoose一个叫Diginetica。数据预处理延续了前面工作的惯例过滤长度太短的session过滤出现次数过少的低频物品然后按时间顺序划分训练集和测试集。Yoochoose还会按比例切出1/64和1/4两个数据规模变体用来观察数据量对模型效果的影响。评估指标是Recall20和MRR20。前者看目标物品是否出现在前20个推荐结果里后者看目标物品在排序里的位置有多靠前。会话推荐场景里用户往往只点击下一个物品这两个指标一个关心召回一个关心排序位置配合起来能比较完整地衡量模型质量。只看其中一个容易有偏差比如模型总把热门物品排在前面Recall可能不高但MRR也不会太好。3.2 主实验结果从统计方法到深度模型的跨越和GC-SAN同场竞技的有S-POP、Item-KNN这样的传统方法也有GRU4REC、NARM、SR-GNN这些深度模型。主实验里GC-SAN在两个数据集上全面超过所有基线在Yoochoose 1/64上Recall20接近70%的量级比SR-GNN又有一个稳定提升MRR的提升同样存在。说明在会话级短期预测上把图结构和全局上下文结合确实比单用其中任何一种更能打。读实验时我会特别关注深模型和浅模型之间的差距。从Item-KNN到GRU4REC是一次大的跨越但SR-GNN和GC-SAN之间的差距相对温和。这其实在提醒我当数据规模有限、session又很短时模型结构再复杂收益也是边际递减的。工程上做技术选型时别一上来就上最复杂的模型先跑通简单的基线和业务指标对齐再决定要不要为了最后那一个点的提升付出额外的部署成本。3.3 消融实验拆解GNN和上下文各自的贡献论文做了很充分的消融实验。去掉GNN层只剩上下文自注意力效果明显下滑说明图结构的局部转移信息不可替代。把上下文自注意力换成普通自注意力保留GNN效果也有下降说明那个简单的平均池化上下文向量不是白加的。两个组件组合在一起才能同时捕获局部结构和全局意图。我特别注意到一个现象对比去掉GNN和去掉上下文这两组掉点的幅度前者通常更大。这说明图结构是这个模型的骨架上下文自注意力是补全血肉。如果你资源有限只能保一个模块优先保GNN如果你想让模型再涨点再去优化上下文注意力这块。这个结论对做后续改进方向很有指导意义。除了消融训练时还有个实现层面的技巧对embedding做dropout随机把部分物品的嵌入置零。session数据量通常不大这种操作相当于构造了带噪声的样本能有效压低过拟合。我自己的复现里去掉embedding dropout验证集指标大概会掉一个多点这个操作值得保留。3.4 容易被忽略的几个训练技巧论文里的训练超参不花哨embedding维度100左右batch size 100学习率0.001权重衰减1e-5。关键反而不在超参数值上而在几个容易被忽略的小操作。第一会话图在batch内按session独立构建千万不能全局共用一张图。第二GGNN传播轮数固定为1不要贪深。第三所有padding位置在注意力阶段必须被mask掉这是复现是否成功最直接的分水岭。另外数据规模对效果影响也很有意思。对比Yoochoose 1/64和1/4两个变体数据量增大后GC-SAN相对基线的领先优势更稳定。这说明图结构加上下文注意力不只是在小数据上拟合出来的结果它确实学到了可泛化的转移模式。对做业务的人来说这算一个正向信号数据量上来时这类模型是能持续吃数据的。4. 复现GC-SAN图构建、掩码与训练中的实操记录4.1 构建邻接矩阵出边、入边与自环缺一不可复现的第一步是把会话图构建的代码写对。我先给一个反映核心逻辑的示意import numpy as np def build_session_graph(seq): nodes {} for node in seq: if node not in nodes: nodes[node] len(nodes) n len(nodes) A_out np.zeros((n, n)) A_in np.zeros((n, n)) for i in range(len(seq) - 1): src nodes[seq[i]] dst nodes[seq[i 1]] A_out[src][dst] 1.0 A_in[dst][src] 1.0 for idx in range(n): A_out[idx][idx] 1.0 A_in[idx][idx] 1.0 A_out A_out / (A_out.sum(axis1, keepdimsTrue) 1e-9) A_in A_in / (A_in.sum(axis1, keepdimsTrue) 1e-9) return A_out, A_in这个写法省略了batch对齐但核心逻辑足够了。出边矩阵用于正向传播入边矩阵用于反向传播同时加上自环保证节点自身信息不被冲掉。我第一次实现时忘了加自环结果GNN第一轮传播把自己原本的特征全洗掉了指标断崖下跌查了半天才发现是这个原因。归一化需要放在自环之后否则权重分配会被自环带偏。4.2 Padding和Mask一次复现中最容易翻车的两处一个batch里的session长度不同通常要padding到最大长度。GNN阶段要根据padding后的图计算邻接矩阵padding位置没有实际物品对应的邻接矩阵行是零向量这不影响传播。但注意力阶段就麻烦了计算每个位置对整条序列的注意力分数时padding位置必须用mask把分数置为负无穷否则softmax会把注意力分给一堆空位置。另一个容易踩的坑是最后一个真实物品的定位。session经过padding后最后一个位置已经不是原来最后点击的物品。你得把每个session的真实长度存下来在预测阶段用真实长度减1对应的位置取最后的向量。如果错取成padding向量模型看起来还在收敛实际上学的全是mask噪声验证集指标会非常不稳定。这两个坑我都在复现时踩过每次排查到最后都发现是基本功出了问题。还有损失函数里的负样本处理。全物品softmax在item量大的时候算不动实践中常用batch内负采样把同一批次其他session里的真实物品当作负样本。这个策略实现简单效果稳定我在两个数据集上都验证过。如果追求精细可以再按频次做负采样加权但对GC-SAN来说收益不大。4.3 过拟合控制embedding dropout比想象中更重要这类模型很容易过拟合。session数据量通常只有几十万级别embedding表却有几十万甚至上百万参数。我的建议是embedding维度不要贪高100维足够训练必须开embedding dropout否则训练集和验证集指标的差距会大到离谱。学习率从0.001开始是个稳妥选择如果loss徘徊不降降一半再试。有人会给注意力层配warmup前几百步把学习率从0线性升上来让参数稳定一些。我实测下来warmup对GC-SAN的帮助没有对纯Transformer那么大用固定学习率也能跑出好结果。学习率之外权重衰减保持1e-5级别即可过大的权重衰减会把这些稀疏特征压得太死。最后一个建议GNN传播层数和注意力层数都不要堆太多。论文里GNN一层、自注意力一层就已经拿到SOTA强行加深只会增加过拟合风险。我把注意力层数从1加到2试过一次指标不但没涨反而微跌。短会话场景里模型复杂度收益非常有限与其加深不如去调数据预处理和embedding dropout。4.4 落地场景与工程限制说点落地的观察。GC-SAN适合会话内行为相对丰富、用户身份不明的场景游客浏览电商页面短视频App里一次连续刷新酒店机票预订流程里的本轮点击。这些场景的session本身承载了大部分决策信息图结构和上下文的组合能发挥价值。相反如果session只有一两条行为图几乎退化成孤立节点GNN的信息传播无从谈起。这种场景不如直接用热门榜或者物品向量检索。另外新物品在训练时没有积累边关系图传播给不了它任何信息落地时必须在召回侧给新物品留一条兜底路径比如内容相似度匹配。线上部署方面GNN和自注意力在batch推理时都是矩阵运算一张普通GPU卡延迟没问题。更值得关注的是邻接矩阵的构建开销数据量大时图构建要在数据读取阶段并行预处理不要塞进模型forward里现算否则瓶颈一定出在这里。5. 读完论文后我还在琢磨的几个问题5.1 全局上下文向量用平均池化合理吗作者选择把所有节点向量平均池化成上下文向量c简单、稳定、可解释。但平均池化默认每个物品对会话意图的贡献相同这并不完全符合直觉用户最后几次点击的信息量通常大于开头。后续不少工作沿着这个方向改进比如用注意力池化替代平均池化或者把时间衰减嵌进上下文计算。我自己在复现时试着把平均池化换成了带位置权重的加权池化P20有一定提升但幅度不大。这也说明GC-SAN对上下文表达形式不是特别敏感真正的收益来自“把全局信号注入注意力”这件事本身。如果你想在此基础上做改进从上下文向量的计算方式入手比直接改注意力结构更值得试。5.2 这个框架和更长的序列模型怎么衔接GC-SAN的框架本质上是两段式编码先GNN做局部结构编码再注意力做全局上下文编码。这个思路相当通用完全可以展开成图加Transformer的统一模型或者用session图替代传统位置编码都是自然扩展方向。如果业务里已经有一个大规模序列模型做排序GC-SAN可以作为session侧的特征编码器输出的向量和其他特征拼接不会有排异反应。我在一个内部排序实验里用GC-SAN的session向量作为辅助特征离线的AUC涨了一点线上点击率也有正向反馈。这种接入方式很轻不用改动推荐漏斗的主体。5.3 复杂度现实精排阶段的定位任何论文方法都活在它当时的数据规模和算力条件下。GC-SAN的图传播是在batch内的稠密矩阵上做的item量几百万级时全量softmax不可行工业落地必须配召回加粗排加精排的漏斗结构。更合理的路线是把GC-SAN用在精排阶段让召回层用双塔或者倒排索引先砍到几百个候选再让这个模型输出精确分数。这样既享受了图结构建模的优势又不会让算力成为瓶颈。如果你正在设计新系统不要试图把会话图铺满全量物品那是工程灾难不是模型能力问题。我自己把这篇论文从读到复现完整走了一遍最大的体会是一个看起来复杂的模型往往只是两个成熟组件的有机关联。GC-SAN不是第一个把GNN用在推荐上的也不是第一个用注意力做序列建模的但它给了一个非常干净的示范怎么用一句话讲清楚模型创新在哪里。如果你最近也在折腾会话推荐建议动手实现一次这个模型别只看公式。跑通之后再回头看那些后续工作会发现大多都是在它这个骨架上做局部替换和叠加理解起来会轻松很多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。