基于协同过滤与图神经网络的交友社区推荐系统:毕业设计实战指南
背景痛点:社交场景下的推荐“三宗罪”
做毕设时,我最初只想“套个协同过滤”交差,结果一跑真实数据集就翻车:
- 交互稀疏:校园交友 App 日活只有 4k,平均每人点赞 3 次,矩阵空洞率 99.7%
- 冷启动:新用户注册 10 分钟内就要给出“心动推荐”,否则流失率 60%+
- 实时性差:纯离线矩阵分解每天更新一次,用户换了头像、改了签名,推荐列表纹丝不动
这三拳下来,传统 CF 的“相似度找邻居”思路直接失灵,必须让信息在图结构里“多跳”传播,才能把稀疏信号放大。
技术选型:为什么不是纯深度学习或规则引擎
我试过三条路线,踩坑记录如下:
规则引擎(年龄±3 岁、同城、共同兴趣 ≥2)
优点:开发 2 小时上线;缺点:效果天花板肉眼可见,Recall@20 只有 6.8%纯深度排序(DIN/DeepFM)
需要 10w+ 密集样本才能收敛,而我们的“点赞”行为只有 1.2w,过拟合到怀疑人生协同过滤 + 轻量图神经网络
- 协同过滤先利用“谁点赞谁”这一最廉价信号,快速得到初始 embedding
- 图神经网络在二部图上做 2-hop 消息传递,把“朋友的朋友”兴趣扩散过来,相当于把稀疏矩阵补全
- PyTorch Geometric 一行命令就能跑,显存占用 <2G,笔记本 3060 可训练
综合开发量、数据规模与效果天花板,方案 3 是毕设“能做完+能跑通+能写论文”的最优解。
核心实现细节:把“点赞”变成图上的信号
整个流水线拆成三步,每步都可独立 debug,非常友好。
1. 用户-兴趣二部图构建
节点:user_id + interest_tag_id(共 N+M 个节点)
边:用户 u 对兴趣标签 t 有点赞行为 → 建一条无向边 (u, t)
代码里直接edge_index = [[u1,u2,…],[t1,t2,…]],省掉中间表 join 的麻烦。
2. 消息传递机制
采用最简单的 LightGCN——抛弃特征变换矩阵,只做邻居平均:
h_u^(l+1) = Σ_{v∈N(u)} h_v^(l) / |N(u)|两层传播后,user 节点已融合“直接点赞”+“朋友点赞”的混合信号,相当于把协同过滤的“相似用户”显式地拆进来。
3. 负采样策略
社交场景负样本不能全局随机,否则把异性、异地全采成负例,模型直接学偏。我的做法:
- 50% 随机采样:保证收敛
- 30% 同城异性但未交互:让模型学“曝光却未心动”
- 20% 新注册未交互:提前把冷启动用户 embedding 推到合理分布
batch 内再跑一次torch.nn.functional.logsigmoid对抗损失,AUC 提升 4.3 个百分点。
PyTorch Geometric 关键代码(30 行可跑通)
以下代码在单卡 6G 显存即可训练,注释直接对应论文公式,方便写论文时截图。
import torch, torch.nn as nn, torch.nn.functional as F from torch_geometric.nn import LGConv class BiGraphRec(nn.Module): def __init__(self, num_users, num_tags, emb_dim=64, num_layers=2): super().__init__() self.u_emb = nn.Embedding(num_users, emb_dim) self.t_emb = nn.Embedding(num_tags, emb_dim) self.convs = nn.ModuleList([LGConv() for _ in range(num_layers)]) self.reset_parameters() def reset_parameters(self): nn.init.normal_(self.u_emb.weight, std=0.1) nn.init.normal_(self.t_emb.weight, std=0.1) def forward(self, edge_index): # 初始特征:用户+标签拼接成统一空间 x = torch.cat([self.u_emb.weight, self.t_emb.weight], dim=0) all_emb = [x] for conv in self.convs: x = conv(x, edge_index) all_emb += [x] # 层平均,缓解过平滑 x = torch.stack(all_emb, dim=1).mean(1) return x[:num_users], x[num_users:] # 分别返回 user, tag 嵌入 def bpr_loss(u_emb, t_emb, pos_edge, neg_edge): pos_score = (u_emb[pos_edge[0]] * t_emb[pos_edge[1]]).sum(1) neg_score = (u_emb[neg_edge[0]] * t_emb[neg_edge[1]]).sum(1) return F.logsigmoid(pos_score - neg_score).neg().mean()训练循环就是常规Adam + lr=1e-3,每 10 个 epoch 在验证集上测一次 Recall@20,早停 patience=5。
性能与安全:让模型跑得动也守得住
推理延迟
把 GNN 层提前算完存成 user/tag 表,线上只查表 + 内积,P99 延迟 12 ms(单核 Flask)隐私保护
- 行为日志存 SHA-256(user_id+timestamp) 哈希,不落明文
- 训练集导出时做 ε=1 的差分隐私加噪,NDCG 仅掉 0.4%,可接受
模型幂等
固定随机种子 + 参数初始化一致,保证同一数据版本产出同一 embedding,方便 A/B 回滚
生产环境避坑指南
小数据集过拟合
层数 >3 后,训练集 Recall 暴涨,验证集掉 8%,直接上 DropEdge+层平均可解决新用户冷启动
注册时强制选 5 个兴趣标签,用对应 tag embedding 平均作为初始 user 向量,30 分钟内就能推,实测 CTR 提升 1.7 倍评估指标
社交场景更关心“能不能刷到心动的人”,Recall@K 比 NDCG 更直观;但为防止头部标签过热,再加一个 Coverage@K,保证推荐池多样性 ≥60%
效果与落地
在 4k 校园用户、12w 点赞的迷你数据集上,两周内做到:
- Recall@20 从 6.8%(规则)→ 18.4%
- 新用户 7 日留存 +9.3%
- 模型体积 17 MB,可打包进 Docker 镜像直接丢到学校服务器
结尾思考:下一步引入多模态?
把头像视觉特征、个性签名文本向量接进节点,理论上能缓解“兴趣标签同质化”导致的推荐审美疲劳。但多模态后图节点特征维度暴增,如何设计轻量融合层、又不让 GPU 显存爆炸,是我留给你的思考题。欢迎 fork 上面代码,把 CLIP 或 BERT 向量塞进去跑一遍,然后告诉我 Recall 又涨了多少——毕设路上,一起复现、一起卷。