简介:基于属性和图神经网络(AGNN)的推荐算法本科毕业设计项目包,面向推荐系统方向学生与研究者,完整实现了融合用户/商品属性信息与图神经网络嵌入学习的推荐模型,可作为毕业设计或课题研究的直接参考范例。项目基于公开的MovieLens等数据集,涵盖了数据处理、模型构建、训练优化与评估验证等完整流程,并针对图结构数据的卷积与池化操作给出了具体实现。压缩包共19个文件,包含5个Python源码(模型、工具、数据处理与测试脚本)、7个说明文档、3个npy数据文件、2个csv数据集及1个pyc编译文件,整体约4.19MB,目录结构清晰,方便对照学习。已有62人学习下载。通过该项目可掌握AGNN模型的嵌入表示学习、图卷积与图池化操作的实际应用思路,并能直接复用代码进行实验或二次开发,为深入推荐系统研究提供有力支持。
1. 基于属性和图神经网络的推荐算法:AGNN 到底改了什么
如果你刷到过“B站首页web推荐算法”和“抖音推荐算法”的热搜,会发现一个共同点:如今的推荐系统早就不只靠“你和物品像不像”了,而是靠“你和物品在关系网里离得近不近”。图神经网络(GNN)干的就是这件事——把用户、物品、属性当成一张大网里的节点,让信息沿着边传播。而这份标着“AGNN”的本科毕业设计资源,核心是把“属性”这个被很多模型当辅助信息的角色,提到了和结构信息并列的位置上。换句话说,它不只是拿 GNN 做推荐,而是专门回答了“物品属性到底该怎么进图、怎么参与聚合,才能让推荐更准”这个问题。适合正在做推荐系统课题、需要完整代码骨架和实验流程的本科生或刚入门的研究生;也适合想快速看一份“属性 + GNN”怎么落地成可跑工程的人。它能帮你省掉从零搭环境、写数据管道、调训练循环的大把时间。
2. 推荐系统为什么要用图神经网络:从协同过滤到 AGNN 的演进
2.1 传统推荐的结构瓶颈
传统的协同过滤(CF)把用户和物品表示成隐向量,靠内积算相似度。它能捕捉“张三和李四都买了某本书”,但捕捉不了“张三买了某本书,是因为李四所在的读书会里大部分人都在读这本”——这种二跳以上的关系,CF 的向量内积很难表达清楚。属性信息(比如物品的类目、价格区间、用户的地理位置)在传统模型里要么拼进特征向量,要么单独做一个浅层模型,和交互结构是割裂的。
到了 GNN 时代,推荐系统把用户–物品交互看成二部图,用消息传递把邻居信息聚合到当前节点上。但大部分 GNN 推荐模型(比如 NGCF、LightGCN)只聚合“交互邻居”的表示,属性只是作为初始特征拼一拼。问题在于:如果两个物品从未被同一个用户点击过,它们在图上的路径就很长,属性上的相似性很难通过有限层数的聚合传过来。AGNN 的思路就是让属性成为显式的图节点,而不是被塞进特征向量里。
2.2 AGNN 的图结构设计
这份资源里的 AGNN 模型,构建的是“用户—物品—属性”三类节点构成的多关系图。用户和物品之间是交互边,物品和属性之间是“拥有边”,用户和属性之间是“偏好边”。这三类边分别用不同的邻接矩阵存,聚合时按关系类型分开做消息传递,再在注意力机制里融合。
# 邻接矩阵构建的核心逻辑(伪代码风格,资源内为完整实现) user_item_edge = build_edge(user_ids, item_ids, rating_mask) # 交互边 item_attr_edge = build_edge(item_ids, attr_ids, item_attr_mask) # 拥有边 user_attr_edge = build_edge(user_ids, attr_ids, user_attr_mask) # 偏好边 adj_list = [user_item_edge, item_attr_edge, user_attr_edge]这里rating_mask是把评分矩阵二值化后的结果,只保留有交互的条目;item_attr_mask是物品–属性的共现矩阵,比如一部电影属于“科幻”和“冒险”两个属性,对应位置就置 1。user_attr_edge不完全等于用户–物品–属性的间接关系,而是根据用户历史交互物品的属性分布统计出来的显式偏好。
这样设计的原因是:如果把用户偏好属性当成隐式信息,要靠 GNN 传好几层才能传到;而把它建成显式边,第一层消息传递时用户就能直接收到属性信号。实验里你会发现,加了这层边之后,冷启动物品的表示质量提升明显,因为新物品只要挂了属性,就能通过属性边把信号传给潜在感兴趣的用户。
2.3 属性感知的消息传递机制
传统 GNN 推荐模型的消息传递通常写成一个统一聚合函数:
# LightGCN 风格聚合示意 h_u = sum( h_i for i in neighbors_of_user(u) )AGNN 把这个过程拆成了“分关系聚合再融合”。具体来说,用户节点的邻居分成三类:交互过的物品、偏好过的属性、以及通过属性桥接到的“相似用户”(第二层才需要考虑)。每一类邻居先独立聚合,得到三个候选表示,再用注意力权重把三者加起来。
# AGNN 分层聚合主体逻辑(节选) for layer in range(n_layers): h_u_item = aggregate(user_emb, item_emb, user_item_edge) # 从交互物品收消息 h_u_attr = aggregate(user_emb, attr_emb, user_attr_edge) # 从偏好属性收消息 h_u = attention_fuse(h_u_item, h_u_attr, user_emb) # 注意力融合 h_i_user = aggregate(item_emb, user_emb, user_item_edge.T) # 物品从用户收消息 h_i_attr = aggregate(item_emb, attr_emb, item_attr_edge) # 物品从属性收消息 h_i = attention_fuse(h_i_user, h_i_attr, item_emb)注意力融合不是简单拼接,而是给每一类消息算一个标量权重,权重由当前节点表示和消息向量的相似度决定。这个设计的直觉是:不同用户对不同信息源的依赖程度不一样。重度追剧用户可能更依赖“看过的剧”这条消息,而新用户没什么行为,只能靠“偏好属性”这条消息。注意力机制让模型自己学会调节。
参数层面,你需要注意三个东西:n_layers控制信息传播深度,一般取 2 到 3,再深容易过平滑;emb_dim(嵌入维度)决定表示容量,资源默认在 32 到 64 之间,小数据集上用 64 效果不错;attention_fuse里的温度参数(如果有)控制权重分布的尖锐程度,调大让模型更倾向选单一信息源,调小则更平均。
3. 从零复现 AGNN 推荐系统:数据处理、模型训练与评估全流程
3.1 数据集准备与格式转换
这份资源用的是标准的推荐数据集格式:用户–物品–评分–时间戳的四列 CSV,以及物品–属性标签的映射文件。如果你手里是 MovieLens 1M 这种常见数据,需要先做一次格式对齐。
# 把 MovieLens ratings.dat 转成 AGNN 需要的 CSV import pandas as pd ratings = pd.read_csv('ratings.dat', sep='::', header=None, names=['user_id', 'item_id', 'rating', 'timestamp']) ratings = ratings[ratings['rating'] >= 4] # 保留正向反馈 ratings['user_id'] = ratings['user_id'].astype(int) ratings['item_id'] = ratings['item_id'].astype(int) ratings.to_csv('ratings.csv', index=False)这里把评分阈值设为 4 是常见做法,因为推荐系统一般只关心“用户喜欢什么”,3 分以下的不确定性太大,当成负样本又不够准确。如果你做的是 Top-N 推荐而不是评分预测,建议同样只保留 4 分以上数据,把隐式反馈问题简化。
物品属性文件长什么样?MovieLens 的 movies.dat 里包含类型字段,管道分隔。需要拆成“物品–属性”的长表:
# 物品属性长表转换 movies = pd.read_csv('movies.dat', sep='::', header=None, names=['item_id', 'title', 'genres'], engine='python') movies['genres'] = movies['genres'].fillna('') movie_attr = movies['genres'].str.split('|', expand=True).stack() movie_attr = movie_attr.reset_index().rename(columns={'level_0': 'item_id', 0: 'attr'}) movie_attr['attr'] = movie_attr['attr'].astype('category').cat.codes movie_attr.to_csv('item_attr.csv', index=False)属性必须编码成整数 ID,图神经网络里的节点索引不能是字符串。常见做法是用astype('category')把文本标签自动映射成整数码,保存后下次加载还要维护一份attr_code映射,否则训练和推理时属性 ID 对不上。
3.2 训练主循环与负采样
AGNN 的训练目标是让正样本(用户–物品对)的表示相似度高于负样本。资源里的损失函数用的是 BPR(Bayesian Personalized Ranking)损失,配合随机负采样——每个正样本配一个用户没交互过的物品作为负样本。
# BPR 损失核心计算 def bpr_loss(pos_scores, neg_scores): """ pos_scores: 正样本 user-item 匹配分 neg_scores: 负样本 user-item 匹配分 """ return -torch.mean(torch.log(torch.sigmoid(pos_scores - neg_scores)))BPR 的直觉是:模型不需要精确预测评分,只需要把正样本的排序分数压过负样本。所以你在训练时会发现 loss 下降很快,但 AUC 不一定同步上升——AUC 看的是全量排序质量,BPR 只看采样到的负样本。资源里负采样是每轮随机抽的,如果你想让结果更稳定,可以把负样本数量从默认的 1 提到 3 或者 5,效果会好一些,但训练时间线性增加。
训练轮数(epoch)一般设 100 到 200,数据集小的话 50 轮就够。关键监控指标是每 5 轮在验证集上算一次 Recall@K 和 NDCG@K,不要光看训练 loss。AGNN 这类模型过拟合比较慢,但一旦过了最佳 epoch,Recall 会开始波动甚至下降,所以务必保存验证集表现最好的模型权重,别用最后一轮的权重做测试。
3.3 评估指标与测试协议
推荐系统评估讲究“排序命中率”,不是准确率。资源里实现了 Recall@K、Precision@K、NDCG@K 三个指标。
# Recall@K 计算示例 def recall_at_k(pred_ranks, ground_truth, k=20): """ pred_ranks: 用户对所有物品的排序分数,降序排列后取前 k 个 ground_truth: 测试集中该用户真实交互过的物品集合 """ pred_items = pred_ranks[:k] hits = len(set(pred_items) & set(ground_truth)) return hits / len(ground_truth)评估时有个容易出错的地方:测试集的负样本集合怎么定。正确的做法是把训练集里用户交互过的物品屏蔽掉,只对“用户没交互过 + 测试集里交互过”的物品做排序,否则推荐结果里全是训练集里的热门物品,指标虚高。资源里已经写了 masking 逻辑,你复现时务必确认train_mask有没有传对。
关于数据划分,参考实现里用的是 leave-one-out:每个用户随机保留一个交互作为测试,一个作为验证,其余训练。对 MovieLens 1M 这种规模,直接按时间戳切分也可以,但 leave-one-out 更容易复现论文里的指标数字。
4. AGNN 与基线模型对比:效果提升的来源与实验设计
4.1 基线选择与公平比较
资源里给了三个基线:最经典的 MF-BPR(矩阵分解 + BPR 损失)、NGCF(GNN 推荐先驱模型)、LightGCN(去掉特征变换的轻量 GNN)。为什么选这三个而不是别的?因为它们构成了一条清晰的对比链:MF 验证“图结构到底有没有用”,NGCF 验证“属性显式建模和隐式特征拼接的区别”,LightGCN 验证“AGNN 比最简 GNN 多出的复杂度值不值”。
# 基线模型配置示例(MF-BPR) model = MF_BPR(num_users, num_items, emb_dim=64) optimizer = torch.optim.Adam(model.parameters(), lr=0.001)对比实验里最容易犯的错是超参数不统一。AGNN 用了 64 维嵌入,MF 也用 64 维;AGNN 训练 150 轮,MF 也训练 150 轮。资源里把基线的emb_dim、lr、batch_size都对齐了,但如果你要自己加新基线(比如 SVD++),务必保持相同的训练预算,否则“提升”可能是训练更久带来的,不是模型结构带来的。
4.2 属性数据占比的影响
AGNN 的一个隐藏优势是:当属性信息变稀疏时,表现下降得比基线慢。你可以在实验里做“属性删减”测试——随机去掉 20% 和 50% 的物品–属性边,重新训练,看 Recall@20 的下降幅度。资源里没直接给这个实验,但数据管道支持,我建议你加上这一组,写论文时这是个很加分的分析点。
现实场景里,新上架的商品往往只有类目属性没有交互记录。AGNN 因为有一条“用户–属性”偏好边,能让新物品通过属性匹配到潜在用户。对比实验中你会看到,如果只统计冷启动物品(交互数小于 5)的 Recall,AGNN 比 LightGCN 高出不止一个点。这也是属性显式建边最大的价值。
4.3 消融实验怎么切
资源模型文件里有attention_fuse和三条边,做消融时把某条边去掉重新训练即可。最值得做的三组消融是:去掉用户–属性偏好边、去掉注意力融合改成平均融合、把图层数减到 1。结果通常会发现:去掉用户–属性边影响最大,说明显式属性偏好比结构信息本身更关键;注意力融合在数据量足够时能贡献 3% 左右的 Recall 提升,数据少的时候反而不如平均融合稳定。
5. AGNN 复现避坑指南:图构建、训练稳定性与内存优化
5.1 稀疏矩阵与全图计算的坑
现象:数据量稍微一大,显存直接爆炸,或者训练慢到无法忍受。
原因:AGNN 的邻接矩阵如果有 N 个节点,用稠密张量存就是 N×N 个浮点数。MovieLens 1M 的用户加物品加属性大约 2 万多个节点,稠密矩阵要占 3 个多 GB 显存,还没算嵌入和梯度。很多人在这一步翻车。
解决:把邻接矩阵转成稀疏 COO 格式,用 PyTorch 的torch.sparse.FloatTensor存储,或者干脆用 DGL/ PyG 这类图库的SparseGraph封装。资源代码里已经用了稀疏矩阵,但如果你是从别的项目改过来的,务必检查adj_list的类型。
5.2 负采样导致的训练震荡
现象:训练 loss 在下降但测试指标忽高忽低,不同次运行结果差异大。
原因:随机负采样每次抽到的负样本不同,如果数据稀疏,很容易抽到“假阴性”——用户其实喜欢这个物品但没交互过。模型被反复教导“这个用户不喜欢该物品”,表示空间就会出现震荡。
解决:把负采样改成“全域负采样”之后可以缓解一点,但我更推荐的是把负样本数调大,并且每轮固定随机种子。资源里没有固定种子,你复现时在train.py开头加一行np.random.seed(0); torch.manual_seed(0),结果就稳了。
5.3 属性 ID 编码不一致
现象:训练正常,但推理时代码报“index out of range”,或者有些物品永远得不到推荐。
原因:训练和推理用了不同的属性编码表。训练时attr_encode把属性映射成 0 到 N-1,推理时加载了新数据,属性 ID 对不上。
解决:把编码表保存成一个 JSON 文件,训练和推理都从同一个文件加载。资源里item_attr.csv存的是编码后的 ID,但没强制校验数量,你最好在加载数据后加一个断言,确认max_attr_id + 1 == num_attrs。
5.4 注意力权重的过平滑
现象:层数加到 4 以上,指标反而显著下降,节点表示几乎都聚成一团。
原因:GNN 每多一层,节点就多聚合一次邻居信息。重复聚合会让所有节点表示趋向于全局平均,这就是“过平滑”。AGNN 的注意力机制能推迟但无法完全阻止这个问题。
解决:n_layers设成 2 或 3,别贪多。如果你想试更深层,可以给每层加残差连接:h = h + layer_out,或者用 DropEdge(训练时随机丢弃部分边)缓解。资源里默认没有残差连接,你可以自己加一个if self.use_residual: h = h + h_prev,实测多一层不容易崩。
5.5 显存占用与批次大小权衡
现象:小数据集上一切正常,换大数据集就 OOM。
原因:全图训练时,所有节点的嵌入和中间表示都得存在显存里,batch size 越大,存储的消息向量越多。GNN 推荐模型不像 CNN 那样能靠 padding 控制内存。
解决:用小批次训练替代全图训练。资源里给的是全图训练(适合 1M 这种规模),如果数据集是 10M 以上,你需要把邻居采样逻辑加上——每个 batch 只采样一部分邻居,而不是全部邻居。这是 AGNN 工程化的必经之路。
6. 模型训练完怎么验证与进一步调优:从指标排查到嵌入可视化
训练完成不代表模型能用。在交付或写论文前,我会强制自己过一遍下面几个验证步骤,每一步都能暴露不同层面的问题。
第一步,检查损失曲线是否收敛且没有锯齿。保存训练时的 loss 序列,画出来看尾部是否平稳。如果 100 轮后还在明显波动,多半是学习率太高或负采样不稳。AGNN 的 BPR loss 收敛后一般停留在 0.1 到 0.3 之间,如果高于 0.5,说明模型没有学到排序能力。
第二步,做一次“推荐结果人工审计”。从测试集里挑 10 个用户,打印出每个用户的推荐 Top-10 和真实交互,用肉眼判断推荐列表里有没有明显不合理的物品。这一步能发现指标发现不了的问题——比如模型把所有用户都推成热门物品、或者属性建边出错导致推荐的全是同一类目下的冷门垃圾。我见过有人 Recall 很高但推荐结果完全不可用,就是因为测试集泄露。
第三步,嵌入向量可视化。把训练好的用户嵌入和物品嵌入降维到二维(用 PCA 或 t-SNE),按属性着色,看同类物品是否聚在一起,用户是否按兴趣聚类。这一步不是必须,但能直观验证 AGNN 的“属性感知”是否真的起效。正常情况是:同一属性簇的物品在二维空间里距离较近,而普通 GNN 模型的嵌入则更偏向按交互热度聚类。
第四步,参数敏感性测试。固定其他参数,把n_layers从 1 扫到 4、把emb_dim从 16 扫到 128,画一条指标曲线。这能帮你确认当前参数不是在过拟合某个特殊配置,也能写进论文的“参数分析”小节。
# t-SNE 可视化嵌入示例 from sklearn.manifold import TSNE import matplotlib.pyplot as plt item_emb = model.item_emb.detach().cpu().numpy() tsne = TSNE(n_components=2, perplexity=30, random_state=42) item_2d = tsne.fit_transform(item_emb[:500]) # 采样 500 个物品避免过慢 plt.scatter(item_2d[:, 0], item_2d[:, 1], c=item_attr[:500], cmap='tab20', s=5) plt.savefig('item_embedding_tsne.png', dpi=150)perplexity一般取 30,样本量小时可以降到 10;颜色编号用的是物品属性的编码,如果相近属性颜色接近,说明嵌入学到了属性结构。
我个人踩过最大的坑是跳过第三步,指标好看就直接拿去跑线上 AB,结果线上 CTR 反而下降。从那以后我每次训练完都强制走一遍“人工审计 + 嵌入可视化”,哪怕花半小时,也要确认模型学的是业务信号而不是数据噪声。做图神经网络推荐的项目,结构、属性、训练、验证四个环节环环相扣,任何一个环节偷懒,最后都会反映在说不清的指标波动上。希望这份 AGNN 资源的复现过程能帮你绕过我走过的弯路。
本文还有配套的精品资源,点击获取