摘要:只会背推荐系统原理,不会写工程化代码?本文以 MovieLens-100k 数据集为基础,用 PyTorch Geometric(PyG)手把手实现经典 LightGCN 模型,核心亮点是采用企业级工程化目录结构(Config 分离、模块化设计),拒绝 “玩具代码”。全程拆解 LightGCN 核心逻辑(无非线性、多层传播、层融合)、二部图构建、BPR Loss 实现,新手可直接复制代码跑通,兼顾实用性与可维护性,帮你快速摆脱 “只会背公式不会落地” 的困境。

关键词:LightGCN;推荐系统;PyTorch Geometric;企业级项目结构;MovieLens-100k;BPR Loss;图神经网络;新手实战

代码已上传至 GitHub 仓库,点击直达,新手可直接克隆运行,快速上手。

一、 为什么我们需要一个“企业级”目录?

你可能会问:“我就跑个 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 文件夹操作,不用翻遍所有代码。

二、 LightGCN:偷懒是第一生产力(大道至简)

之前我们在【深度学习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 的图卷积运算定义为:

eu(k+1)=iN(u)1N(u)N(i)ei(k)mathbf{e}_u^{(k+1)} = sum_{i in mathcal{N}(u)} frac{1}{sqrt{|mathcal{N}(u)|} sqrt{|mathcal{N}(i)|}} mathbf{e}_i^{(k)}
ei(k+1)=uN(i)1N(i)N(u)eu(k)mathbf{e}_i^{(k+1)} = sum_{u in mathcal{N}(i)} frac{1}{sqrt{|mathcal{N}(i)|} sqrt{|mathcal{N}(u)|}} mathbf{e}_u^{(k)}

其中,eu(k)mathbf{e}_u^{(k)} 表示节点 uu 在第 kk 层的嵌入,N(i)mathcal{N}(i) 是节点 ii 的邻居集合。这个公式的意思是:节点 ii 的新嵌入是它所有邻居节点嵌入的加权平均,权重由邻居节点的度数决定,防止热门节点过度影响结果。

kk 层的嵌入进行平均,得到最终的节点表示:

eu=1K+1k=0Keu(k)mathbf{e}_u = frac{1}{K+1} sum_{k=0}^{K} mathbf{e}_u^{(k)}
ei=1K+1k=0Kei(k)mathbf{e}_i = frac{1}{K+1} sum_{k=0}^{K} mathbf{e}_i^{(k)}

这里,eumathbf{e}_ueimathbf{e}_i 分别是用户和物品的最终嵌入表示,KK 是传播层数。通过这种方式,LightGCN 能有效捕捉用户与物品之间的复杂关系,同时保持模型的简洁性和高效性。

三、 评估指标:Recall@K 和 NDCG@K

在推荐系统中,评估模型效果的指标有很多,本文重点介绍两个常用且直观的指标:Recall@KNDCG@K

1. Recall@K

Recall@K 衡量的是在推荐的前 KK 个物品中,用户实际喜欢的物品所占的比例。计算公式如下:

Recall@K=推荐的前 K 个物品用户实际喜欢的物品用户实际喜欢的物品text{Recall@K} = frac{|text{推荐的前 $K$ 个物品} cap text{用户实际喜欢的物品}|}{|text{用户实际喜欢的物品}|}

其中,分子表示推荐的前 KK 个物品中,用户实际喜欢的物品数量,分母表示用户实际喜欢的物品总数。Recall@K 越高,说明模型推荐的物品越符合用户的兴趣。

2. NDCG@K

NDCG@K(Normalized Discounted Cumulative Gain)考虑了推荐物品的排序位置,给予排名靠前的物品更高的权重。计算公式如下:

NDCG@K=1IDCG@Ki=1K2reli1log2(i+1)text{NDCG@K} = frac{1}{text{IDCG@K}} sum_{i=1}^{K} frac{2^{text{rel}_i} - 1}{log_2(i + 1)}

其中,relitext{rel}_i 表示第 ii 个推荐物品的相关性(通常为二值,1 表示用户喜欢,0 表示不喜欢),IDCG@K 是理想情况下的 DCG@K,用于归一化。NDCG@K 越高,说明模型不仅推荐了用户喜欢的物品,还将其排在了更靠前的位置。

四、 ️ 核心代码拆解

咱们按照“配置→模型→训练”的顺序拆解,每一部分都保留核心代码,去掉冗余,同时标注新手必看的重点,避免踩坑。

1. ️ 配置中心 (config.yaml):拒绝硬编码,改参不秃头

先把所有超参数都放在 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

2. 模型核心 (lightgcn.py):大道至简

这是 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 的核心就是“无非线性、多层传播、层融合”,不用加任何多余的层,加了反而会降低效果(亲测踩坑)。

3. ️ 训练流程 (train.py):把所有积木搭起来

有了配置和模型,还得有个“教练”带它训练。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()

4. 损失函数:BPR Loss(推荐系统的“专属教练”)

虽然代码里没细写,但 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 推荐系统,其实也分三步,简单好记:

  1. 搭架子:用规范的目录结构管理代码(抄我上面的目录,准没错),拒绝“一锅炖”;

  2. 写核心:利用 PyG 实现 LightGCN,记住“无非线性、多层传播、层融合”三大要点,代码干净又高效;

  3. 跑起来:用 train.py 串联数据、模型和评估,重点搞定“双向二部图”,避开核心坑点。

希望这篇博客能治好你的“推荐系统代码恐惧症”——其实没有那么难,只要跟着步骤走,新手也能轻松跑通。完整的代码我已经开源在GitHub(点击直达)了,包含完整的工程化代码、锁死版本的requirements.txt、一键启动/评估/预测脚本、详细的注释,新手可以直接克隆下来,按README的步骤跑通,也可以在此基础上修改,做自己的推荐系统。祝大家的模型 Loss 越来越低,Recall 越来越高,早日写出能“显摆”的企业级推荐系统代码!

觉得有帮助的话,GitHub求个 Star ⭐️!你的支持是我继续更新的最大动力~

写在最后

本期我们从“企业级目录搭建”入手,避开“玩具代码”的坑;吃透 LightGCN “删繁就简”的核心逻辑,避开模型实现的坑;拆解每一行核心代码,标注新手必看要点,就是希望能帮大家打破“只会背原理、不会写代码”的壁垒。

其实推荐系统从来都不是“玄学”,也不是只有资深算法工程师才能驾驭——只要一步一个脚印,从简单模型入手,吃透工程化规范,多动手跑通代码,你也能写出拿得出手的实战项目,搞定简历上的“推荐系统实战经验”。

愿每一位正在深耕深度学习的小伙伴,都能少踩坑、少秃头,代码越写越流畅,模型效果越来越好;愿大家既能吃透底层原理,也能搞定工程落地,在技术成长的路上,一路生花、一路进阶,早日成为自己心中的“技术大佬”!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com