狩猎摸拟器
52.83M · 2026-02-06
摘要:只会背推荐系统原理,不会写工程化代码?本文以 MovieLens-100k 数据集为基础,用 PyTorch Geometric(PyG)手把手实现经典 LightGCN 模型,核心亮点是采用企业级工程化目录结构(Config 分离、模块化设计),拒绝 “玩具代码”。全程拆解 LightGCN 核心逻辑(无非线性、多层传播、层融合)、二部图构建、BPR Loss 实现,新手可直接复制代码跑通,兼顾实用性与可维护性,帮你快速摆脱 “只会背公式不会落地” 的困境。
关键词:LightGCN;推荐系统;PyTorch Geometric;企业级项目结构;MovieLens-100k;BPR Loss;图神经网络;新手实战
你可能会问:“我就跑个 Demo,至于搞这么多文件夹吗?”
至于。非常至于。
当你把所有代码塞进一个文件里,那叫写脚本;当你把代码分门别类放好,那才叫做工程。我们要假装自己是在搞一个日活过亿的大项目,这样写出来的代码,不管是放简历上,还是拿给同学看,都能“镇得住场子”。
先上我们的“排面”目录结构(每个文件夹都标好了用途,新手直接抄):
gnn_recommender/
├── configs/ # ️ 控制中心:所有超参数都在这里,改参数不用翻代码
│ └── config.yaml
├── data/ # 数据仓库:生鲜(raw)和熟食(processed)都在这
│ └── ml-100k/
├── saved/ # 成果展示:模型权重、日志、训练结果
│ ├── logs/
│ ├── models/
│ └── results/
├── src/ # 核心源码:真正干活的地方
│ ├── data/ # 数据搬运工:加载、清洗、切分数据
│ ├── engine/ # ️ 训练和评估引擎:带模型“健身”
│ ├── models/ # 模型的大脑 (LightGCN 就在这安家)
│ └── utils/ # ️ 工具箱:日志、工具函数全在这
├── train.py # 启动入口:一行命令就能启动训练
├── inference.py # 推理脚本:加载模型,给新用户推荐
├── visualize.py # 可视化脚本:画图看效果
└── requirements.txt # 依赖清单:锁死版本,避免环境翻车
这就是所谓的“高内聚,低耦合”——每个模块只干自己的活,互不打扰。以后老板让你改模型层数,你闭着眼去 configs 文件夹改;让你换个数据集,你去 data 文件夹操作,不用翻遍所有代码。
之前我们在【深度学习Day16】中详细介绍了GNN与GCN的核心原理,有不懂的小伙伴可以先回顾一下,再来看LightGCN。本次推荐系统我们用到的LightGCN (He et al. 2020),是GCN在推荐系统领域的一个经典变体,核心思想就是“偷懒”——删掉所有复杂的非线性操作,只保留最核心的图卷积传播机制。
GCN (图卷积神经网络) 刚出来的时候,大家恨不得把所有深度学习的招式都往上面堆:特征变换、非线性激活函数、各种正则化……搞得又复杂又难训练。LightGCN 的作者站出来说:“停一下,推荐系统里的图其实没那么复杂。” 推荐系统的 User-Item 图,只有 ID 信息,没有复杂的语义特征(不像NLP里的文本、CV里的图片)。那些花里胡哨的非线性变换,不仅增加了噪声,还不好训练,纯属“画蛇添足”。于是 LightGCN 做了一个“违背祖宗”的决定:把所有非线性激活全删了!
它的核心逻辑简单到离谱,用4句大白话+图标就能说清:
你是谁? (Embedding:给每个用户、物品分配一个“身份证”向量)
你朋友是谁? (Graph Propagation:在用户-物品图上,传递邻居的特征)
你朋友的朋友是谁? (Multi-hop:多传播几层,捕捉更远的关联)
大家平均一下。 (Layer Combination:把每一层的特征加权平均,融合所有信息)
再通俗点说:LightGCN 就是带着你的“身份证”(用户向量)在图上溜达几圈,每溜达一圈就沾染一点邻居(关联物品/用户)的气息,最后把这些气息加权一平均,既保留了自己的个性(自身特征),又融合了群体的智慧(邻居特征)。
LightGCN 的图卷积运算定义为:
其中, 表示节点 在第 层的嵌入, 是节点 的邻居集合。这个公式的意思是:节点 的新嵌入是它所有邻居节点嵌入的加权平均,权重由邻居节点的度数决定,防止热门节点过度影响结果。
将 层的嵌入进行平均,得到最终的节点表示:
这里, 和 分别是用户和物品的最终嵌入表示, 是传播层数。通过这种方式,LightGCN 能有效捕捉用户与物品之间的复杂关系,同时保持模型的简洁性和高效性。
在推荐系统中,评估模型效果的指标有很多,本文重点介绍两个常用且直观的指标:Recall@K 和 NDCG@K。
Recall@K 衡量的是在推荐的前 个物品中,用户实际喜欢的物品所占的比例。计算公式如下:
其中,分子表示推荐的前 个物品中,用户实际喜欢的物品数量,分母表示用户实际喜欢的物品总数。Recall@K 越高,说明模型推荐的物品越符合用户的兴趣。
NDCG@K(Normalized Discounted Cumulative Gain)考虑了推荐物品的排序位置,给予排名靠前的物品更高的权重。计算公式如下:
其中, 表示第 个推荐物品的相关性(通常为二值,1 表示用户喜欢,0 表示不喜欢),IDCG@K 是理想情况下的 DCG@K,用于归一化。NDCG@K 越高,说明模型不仅推荐了用户喜欢的物品,还将其排在了更靠前的位置。
咱们按照“配置→模型→训练”的顺序拆解,每一部分都保留核心代码,去掉冗余,同时标注新手必看的重点,避免踩坑。
先把所有超参数都放在 config.yaml 里,集中管理,改起来一目了然。
experiment_name: "lightgcn_movielens_v1"
data:
root: "./data/ml-100k/"
url: "https://grouplens.org/datasets/movielens/100k/"
test_ratio: 0.2
seed: 42
model:
embedding_dim: 64
num_layers: 3
dropout: 0.0
trainer:
batch_size: 2048
learning_rate: 0.001
epochs: 50
eval_step: 5
l2_reg: 1.0e-4
model_save_dir: "saved/models/" # 存 .pth 和 tokenizer.pkl
log_save_dir: "saved/logs/" # 存 train.log
result_save_dir: "saved/results/" # 存图片、预测结果 csv
top_k: 20 # Recall@K evaluation
这是 LightGCN 的灵魂,也是最能体现“大道至简”的地方。大家重点看 forward 函数,没有 ReLU、没有 Linear,只有纯粹的消息传递和聚合,新手也能轻松看懂。
class LightGCN(MessagePassing):
def __init__(self, num_users: int, num_items: int, embedding_dim: int, num_layers: int):
super().__init__(aggr='add') # 聚合方式:求和(LightGCN默认)
# 初始化 User 和 Item 的“身份证”(Embedding层)
self.num_users = num_users
self.num_items = num_items
self.embedding_dim = embedding_dim
self.num_layers = num_layers
# 初始化 Embedding (随机初始化)
self.users_emb = nn.Embedding(num_embeddings=num_users, embedding_dim=embedding_dim)
self.items_emb = nn.Embedding(num_embeddings=num_items, embedding_dim=embedding_dim)
# 使用正态分布初始化权重 (参考原论文)
nn.init.normal_(self.users_emb.weight, std=0.1)
nn.init.normal_(self.items_emb.weight, std=0.1)
def forward(self, edge_index: Tensor):
"""
前向传播:
edge_index: 图的边索引,形状为 [2, num_edges]
返回:用户和物品的最终嵌入表示
"""
# 1. 你是谁?:拼接用户和物品的嵌入,准备出发
x = torch.cat([self.users_emb.weight, self.items_emb.weight], dim=0)
# 2. ️ 归一化:防止热门节点(比如热门电影)的特征被过度放大
edge_index_norm, edge_weight = gcn_norm(edge_index, num_nodes=x.size(0), add_self_loops=False)
embs = [x] # 保存每一层的嵌入结果(用于后续层融合)
# 3. 溜达几圈:多层消息传播
for _ in range(self.num_layers):
# propagate:PyG的魔法方法,自动处理消息传递(不用自己写循环)
x = self.propagate(edge_index_norm, x=x, edge_weight=edge_weight)
embs.append(x)
# 4. 层融合:把每一层的结果平均,得到最终嵌入
embs = torch.stack(embs, dim=1)
final_emb = torch.mean(embs, dim=1)
# 拆分回 User 和 Item
users, items = torch.split(final_emb, [self.num_users, self.num_items])
return users, items
# 消息传递逻辑(PyG固定写法,新手不用深究,复制即可)
def message(self, x_j, edge_weight):
# x_j:邻居节点的特征,edge_weight:归一化权重
return edge_weight.view(-1, 1) * x_j
新手小贴士:LightGCN 的核心就是“无非线性、多层传播、层融合”,不用加任何多余的层,加了反而会降低效果(亲测踩坑)。
有了配置和模型,还得有个“教练”带它训练。train.py 作为启动入口,核心是把数据、模型、评估串联起来,新手重点看构建二部图这一步——这是 LightGCN 最容易踩坑的地方。
def main():
# 1. 初始化配置
parser = argparse.ArgumentParser()
parser.add_argument('--config', type=str, default='configs/config.yaml')
args = parser.parse_args()
config = Config(args.config)
seed_everything(config.data.seed)
logger = setup_logger("train", config.trainer.log_save_dir)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
logger.info(f"Using device: {device}")
# 2. 数据准备
dataset = MovieLensDataset(config.data.root)
dataset.download() # 自动处理下载
dataset.process() # 自动处理数据
num_users, num_items = dataset.num_users, dataset.num_items
train_edge_index, test_edge_index = dataset.train_test_split
train_edge_index = train_edge_index.to(device)
test_edge_index = test_edge_index.to(device)
logger.info(f"Data Loaded: Users={num_users}, Items={num_items}")
logger.info(f"Train Edges: {train_edge_index.size(1)}, Test Edges: {test_edge_index.size(1)}")
# 3. 构建图结构 (关键步骤)
# 为 GCN 构建包含双向边的二部图
graph_edge_index = get_bipartite_edge_index(train_edge_index, num_users, num_items).to(device)
# 4. 初始化模型
model = LightGCN(
num_users=num_users,
num_items=num_items,
embedding_dim=config.model.embedding_dim,
num_layers=config.model.num_layers
).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=config.trainer.learning_rate)
# 5. 初始化训练器与评估器
trainer = Trainer(model, optimizer, config, device, logger)
evaluator = Evaluator(k=config.trainer.top_k, logger=logger)
# 准备负采样所需的字典
train_user_pos = {}
u_list = train_edge_index[0].cpu().numpy()
i_list = train_edge_index[1].cpu().numpy()
for u, i in zip(u_list, i_list):
if u not in train_user_pos: train_user_pos[u] = set()
train_user_pos[u].add(i)
# 6. 训练循环
best_ndcg = 0.0
# DataLoader 只需要提供训练集的边即可
train_dataset = TensorDataset(train_edge_index.t())
train_loader = DataLoader(train_dataset, batch_size=config.trainer.batch_size, shuffle=True)
for epoch in range(1, config.trainer.epochs + 1):
loss = trainer.train_epoch(train_loader, graph_edge_index, train_user_pos)
if epoch % config.trainer.eval_step == 0:
metrics = evaluator.evaluate(model, graph_edge_index, train_edge_index, test_edge_index)
logger.info(f"Epoch {epoch} | Loss: {loss:.4f} | Recall: {metrics[f'Recall@{config.trainer.top_k}']:.4f} | NDCG: {metrics[f'NDCG@{config.trainer.top_k}']:.4f}")
if metrics[f'NDCG@{config.trainer.top_k}'] > best_ndcg:
best_ndcg = metrics[f'NDCG@{config.trainer.top_k}']
save_path = os.path.join(config.trainer.model_save_dir, "best_model.pth")
os.makedirs(os.path.dirname(save_path), exist_ok=True)
torch.save(model.state_dict(), save_path)
logger.info("New best model saved.")
logger.info("Training Finished.")
if __name__ == "__main__":
main()
虽然代码里没细写,但 LightGCN 想要效果好,必须搭配 BPR Loss (Bayesian Personalized Ranking)——推荐系统中最常用的损失函数之一,逻辑简单直白,新手一看就懂:对于用户 U,他看过的电影 I 的预测评分,一定要比他没看过的电影 J 的预测评分高。 说白了就是“奖惩分明”:模型做到了,就奖励它(损失降低);做不到,就“揍”它(损失增加)。新手不用自己实现,直接用 PyG 或自定义的 BPR Loss 即可。
不用调太多参数,按照上面的配置,跑50个 Epoch,你的日志里就会打印出这样的“好消息”,新手也能轻松跑出类似效果,我的最终结果是:
Epoch 50 | Loss: 0.2541 | Recall: 0.2651 | NDCG: 0.3184
新手解读:
Recall@20 = 0.2651:意思是“在用户可能喜欢的所有电影里,我们模型推荐的前20部,能命中26.51%”,对于入门级模型来说,这已经很能打了!
NDCG@20 = 0.3184:意思是“推荐的前20部电影,排序越精准(用户越喜欢的排越前),这个值越高”,0.3184属于很不错的水平了。
只要能跑出这样的结果,就说明你已经成功搭建了一个能正常工作的 LightGCN 推荐系统,比90%只会背原理的新手强多了!
把大象装进冰箱分三步,写一个企业级 LightGCN 推荐系统,其实也分三步,简单好记:
搭架子:用规范的目录结构管理代码(抄我上面的目录,准没错),拒绝“一锅炖”;
写核心:利用 PyG 实现 LightGCN,记住“无非线性、多层传播、层融合”三大要点,代码干净又高效;
跑起来:用 train.py 串联数据、模型和评估,重点搞定“双向二部图”,避开核心坑点。
希望这篇博客能治好你的“推荐系统代码恐惧症”——其实没有那么难,只要跟着步骤走,新手也能轻松跑通。完整的代码我已经开源在GitHub(点击直达)了,包含完整的工程化代码、锁死版本的requirements.txt、一键启动/评估/预测脚本、详细的注释,新手可以直接克隆下来,按README的步骤跑通,也可以在此基础上修改,做自己的推荐系统。祝大家的模型 Loss 越来越低,Recall 越来越高,早日写出能“显摆”的企业级推荐系统代码!
觉得有帮助的话,GitHub求个 Star ⭐️!你的支持是我继续更新的最大动力~
本期我们从“企业级目录搭建”入手,避开“玩具代码”的坑;吃透 LightGCN “删繁就简”的核心逻辑,避开模型实现的坑;拆解每一行核心代码,标注新手必看要点,就是希望能帮大家打破“只会背原理、不会写代码”的壁垒。
其实推荐系统从来都不是“玄学”,也不是只有资深算法工程师才能驾驭——只要一步一个脚印,从简单模型入手,吃透工程化规范,多动手跑通代码,你也能写出拿得出手的实战项目,搞定简历上的“推荐系统实战经验”。
愿每一位正在深耕深度学习的小伙伴,都能少踩坑、少秃头,代码越写越流畅,模型效果越来越好;愿大家既能吃透底层原理,也能搞定工程落地,在技术成长的路上,一路生花、一路进阶,早日成为自己心中的“技术大佬”!