LightGCN

引入

LightGCN是一种高效的图卷积网络(GCN)变体,专为推荐系统设计,通过简化传统GCN的结构来提升性能和训练效率。

传统GCN在推荐系统重引入了特征变换(线性层)和激活函数,对于用户\(u\)和商品\(i\) \[ \begin{aligned} &\mathbf{e}_\mathrm{u}^{(l+1)} = \sigma\left( \mathbf{e}_\mathrm{u}^{(l)}\mathbf{W}_\mathrm{self-u}^{(l)} + \sum_{i\in\mathcal{N}(\mathrm{u})} \frac{1}{c_{u,i}}\mathbf{e}_i^{(l)}\mathbf{W}_i^{(l)} \right)\\ \\ &\mathbf{e}_\mathrm{i}^{(l+1)} = \sigma\left( \mathbf{e}_\mathrm{i}^{(l)}\mathbf{W}_\mathrm{self-i}^{(l)} + \sum_{u\in\mathcal{N}(\mathrm{i})} \frac{1}{c_{u,i}}\mathbf{e}_u^{(l)}\mathbf{W}_u^{(l)} \right) \end{aligned} \] 其中\(\frac{1}{c_{i,j}}\)是归一化系数,通常为\(\frac{1}{\sqrt{|\mathcal{N}(u)|}\sqrt{|\mathcal{N}(i)|}}\)\(\mathbf{e}_u^{(l)}\)\(\mathbf{e}_i^{(l)}\)分别是用户\(u\)和商品\(i\)在图神经网络第\(l\)层的嵌入向量

LightGCN作者认为这些组件在协同过滤任务重并不是必要的,甚至可能会引入噪声。LightGCN的核心改进在于:

  • 去除冗余组件:省略特征变换矩阵(如权重\(\mathbf{W}\))和非线性激活函数(如ReLU)
  • 保留关键操作:仅通过邻居聚合传播用户和物品的嵌入,直接学习协同信号
  • 多层嵌入组合:将各层嵌入加权求和,综合不同阶数的邻居信息

LightGCN通过简化网络结构,相比于之前的NGCF性能得到了较大的提升。

公式

用户\(u\)与商品\(i\)的嵌入通过聚合其邻居的嵌入得到: \[ \begin{aligned} &\mathbf{e}_u^{(l+1)} = \sum_{i\in\mathcal{N}(u)}\frac{1}{\sqrt{|\mathcal{N}(u)}|\sqrt{|\mathcal{N}(i)|}}\mathbf{e}_i^{(l)}\\ \\ &\mathbf{e}_i^{(l+1)} = \sum_{u\in\mathcal{N}(i)}\frac{1}{\sqrt{|\mathcal{N}(u)}|\sqrt{|\mathcal{N}(i)|}}\mathbf{e}_u^{(l)} \end{aligned} \]

  • \(\mathcal{N}(u)\)\(\mathcal{N}(i)\)分别表示用户\(u\)和物品\(i\)的邻居聚合

矩阵形式: \[ \begin{aligned} \mathbf{E}^{(l+1)} = \mathbf{D}^{-\frac{1}{2}}\mathbf{A}\mathbf{D}^{-\frac{1}{2}}\mathbf{E}^{(l)} \end{aligned} \] 其中, \[ \mathbf{E}^{(l)}\in\mathbb{R}^{(n_u+n_i)\times d}\quad\quad \mathbf{D},\mathbf{A}\in\mathbb{R}^{(n_u+n_i)\times (n_u+n_i)} \] \(n_u\)为用户的数量,\(n_i\)为商品的数量,\(d\)为用户和商品嵌入向量的维度。

最终的嵌入表示为各层嵌入的加权和: \[ \mathbf{e}_u = \sum_{l=0}^L \alpha_l\mathbf{e}_u^{(l)},\quad\mathbf{e}_i = \sum_{l=0}^L \alpha_l\mathbf{e}_i^{(l)} \]

  • \(\alpha_l\)是第\(l\)层的权重,原始论文默认设置为\(\alpha_l = \frac{1}{L+1}\)(即平均)

矩阵形式: \[ \begin{aligned} \mathbf{E} &= \alpha\mathbf{E}^{(0)} + \alpha_1\mathbf{E}^{(1)}+\cdots + \alpha_L\mathbf{E}^{(L)}\\ &= \sum_{l=0}^L\alpha_l\mathbf{E}^{(l)} \end{aligned} \]

例如,三层LightGCN的最终嵌入为: \[ \begin{aligned} &\mathbf{e}_u = \frac{\mathbf{e}_u^{(0)}+\mathbf{e}_u^{(1)}+\mathbf{e}_u^{(2)}+\mathbf{e}_u^{(3)}}{4}\\ \\ &\mathbf{e}_i = \frac{\mathbf{e}_i^{(0)}+\mathbf{e}_i^{(1)}+\mathbf{e}_i^{(2)}+\mathbf{e}_i^{(3)}}{4} \end{aligned} \]

注意:LightGCN删去了自环,但是最终嵌入中包含了节点本身的信息:\(\mathbf{e}_u^{(0)}\)\(\mathbf{e}_i^{(0)}\),因此相当于加入了自环。

\(\mathbf{e}_u^{(l)}\)\(\mathbf{e}_i^{(l)}\)引入了一到\(l\)阶邻居的信息,而单纯的\(\mathbf{e}_u^{(l)}\)\(\mathbf{e}_i^{(l)}\)会因为\(l\)的增大而导致过平滑,因此,LightGCN并没有只利用\(\mathbf{e}_u^{(l)}\)\(\mathbf{e}_i^{(l)}\),而是将各层各阶邻居信息都聚合起来得到最终的\(\mathbf{e}_u\)\(\mathbf{e}_i\)

LightGCN中唯一需要参数学习的地方就是用户和商品的初始嵌入向量:

1
2
self.user_emb = nn.Embedding(num_users, hidden_dim)
self.item_emb = nn.Embedding(num_items, hidden_dim)

预测与损失函数

预测得分

预测得分:用户\(u\)对商品\(i\)的偏好通过内积(Dot product)计算, \[ \hat{y}_{ui} = \mathbf{e}_u\cdot\mathbf{e}_i \] 分析:

对于列向量\(\mathbf{u}_i\)\(\mathbf{v}_j\),内积为 \[ \mathbf{u}_i^\top\mathbf{v}_j = ||\mathbf{u}_i||\cdot||\mathbf{v}_j||\cos(\theta) \] 其中\(\theta\)为向量\(\mathbf{u}_i\)\(\mathbf{v}_j\)的夹角,其中模长\(||\mathbf{u}_i||\)\(||\mathbf{v}_j||\)可解释为用户活跃度或物品流行度,\(\cos(\theta)\)可解释为用户和物品的相似度(余弦相似度)。

比如对一个商品向量\(\mathbf{e}_i\)

如果其比较流行,那么其与各个用户的内积都要比较大,由于各个用户的向量都不相同,各个用户与该商品的夹角也会不同,因此,该商品向量\(\mathbf{e}_i\)会增大其模长,从而使得更多的用户的内积更大。

如果该商品常受到某一类用户的喜欢,而不被另一类用户喜欢,那么该商品的角度方向会更加靠近喜欢这类商品的用户,远离不喜欢这类商品的用户。

同理,在训练过程中,那些相似的用户(喜欢的商品类型相似),会与那些喜欢的商品方向相近,而这些用户喜欢的商品是类似的,因此这些相似的用户之间方向也会接近。喜欢商品种类数越多的用户,其最终嵌入向量的模就会越大。

对比:余弦相似度 \[ \cos(\theta) = \frac{\mathbf{u}_i\mathbf{v}_j}{||\mathbf{u}_i||\cdot||\mathbf{v}_j||} \] 余弦相似度衡量的是向量方向的相似性,通常用于用户之间或者商品之间的相似性(训练过程中相似的用户和商品的方向会靠近)。

损失函数

LightGCN原论文中使用BPR损失(Bayesian Personalized Ranking)函数: \[ \mathcal{L} = -\sum_{(u,i,j)\in\mathcal{D}}\ln\sigma(y_{ui}-y_{uj}) + \lambda||\theta||^2 \]

  • \(\mathcal{D}\)是训练数据集中所有用户-商品对的集合,包含正样本\((u,i)\)和负采样样本\((u,j)\)
  • \(\sigma\):Sigmoid函数:\(\frac{1}{1+e^{-x}}\)
  • \(\lambda\):正则化系数,用于防止模型过拟合,\(||\theta||^2\)是模型参数\(\theta\)的L2范数

原理:对于每一个用户\(u\)以及该用户偏好的物品\(i\)和不偏好的物品\(j\),计算\(y_{ui} - y_{uj}\),并通过sigmoid函数将其转换为概率\(\sigma(y_{ui}-y_{uj})\),这个概率表示模型预测用户\(u\)更喜欢物品\(i\)而不是物品\(j\)的置信度。使用\(-\ln\sigma(y_{ui}-y_{uj})\)的原因如下:

  • \(-\ln(x)\)是一个减函数,我们的目标是最大化\(\sigma(y_{ui}-y_{uj})\),而梯度下降法的目标是极小化损失函数\(\mathcal{L}\),使用减函数可以将最大化目标变为最小化目标:最小化\(-\ln\sigma(y_{ui}-y_{uj})\)就是最大化\(\sigma(y_{ui]}-y_{uj})\)

  • \(-\ln(p)\)会将\((0, 1)\)的概率映射到\((0, +\infty)\)范围内,并且概率值越小函数的值和导数值的绝对值越大,“惩罚”越强,能加速训练

  • 符合统计学中的极大似然估计原理:设\(p_{uij} = \sigma(y_{ui}-y_{uj})\),则 \[ \begin{aligned} -\sum_{(u,i,j)\in\mathcal{D}}\ln\sigma(y_{ui}-y_{uj}) &= -\sum_{(u,i,j)\in\mathcal{D}}\ln(p_{uij})\\ &= -\ln(p_{u1,i1,j1}\times p_{u2,i2,j2}\times\cdots\times p_{u|\mathcal{D}|,i|\mathcal{D}|, j|\mathcal{D}|}) \end{aligned} \]\(\mathcal{L}\)最小时,\(p_{u1,i1,j1}\times p_{u2,i2,j2}\times\cdots\times p_{u|\mathcal{D}|,i|\mathcal{D}|, j|\mathcal{D}|}\)最大

上面的公式是逐元素运算的,可以通过批量矩阵运算实现高效并行化:

  • 批量三元组采样:假设每个训练批量为\(\mathcal{B}\)\[ \mathcal{B} = \set{(u_1, i_1, j_1), (u_2, i_2, j_2), \cdots, (u_{\mathcal{|B|}}, i_{\mathcal{|B|}}, j_{\mathcal{|B|}})} \] 其中每个三元组\((u_b, i_b, j_b)\)表示:

    • 用户\(u_b\)与正样本商品\(i_b\)交互
    • 负样本商品\(j_b\)从用户\(u_b\)未交互的商品中随机采样

    接下来从用户和商品嵌入矩阵中提取对应批量索引的嵌入:

    • 用户嵌入批量矩阵: \[ \mathbf{H}_u^\mathrm{batch}\in\mathbb{R}^{\mathcal{|B|}\times d},\quad 其中第b行为\mathbf{h}_{u_b} \]

    • 正商品嵌入批量矩阵: \[ \mathbf{H}_i^\mathrm{batch}\in\mathbb{R}^{\mathcal{|B|}\times d},\quad 其中第b行为\mathbf{h}_{i_b} \]

    • 负商品嵌入批量矩阵: \[ \mathbf{H}_j^\mathrm{batch}\in\mathbb{R}^{\mathcal{|B|}\times d},\quad 其中第b行为\mathbf{h}_{j_b} \]

    批量得分计算:

    • 正样本得分向量: \[ \mathbf{y}^+ = \mathrm{sum}\left( \mathbf{H}_u^\mathrm{batch}\odot\mathbf{H}_i^\mathrm{batch}, \mathrm{dim}=1 \right)\in\mathbb{R}^{|\mathcal{B}|} \]

    • 负样本得分向量: \[ \mathbf{y}^- = \mathrm{sum}\left( \mathbf{H}_u^\mathrm{batch}\odot\mathbf{H}_j^\mathrm{batch}, \mathrm{dim}=1 \right)\in\mathbb{R}^{|\mathcal{B}|} \]

    批量损失计算: \[ \mathcal{L} = -\mathrm{sum}(\ln\sigma(\mathbf{y}^+-\mathbf{y}^-)) + \lambda||\theta||^2 \]

为什么使用BPR损失函数:BPR损失函数目标是极大化用户对偏好物品与不偏好物品的评估差值,提高用户对偏好物品的得分。对于隐式反馈而言(比如点击、浏览、购买),用户仅提供正样本,我们无法直接确定用户不喜欢的物品,面对的是少部分用户交互过的商品和大量用户未交互过的商品,因此,负样本是从这大量的未交互过的商品中进行采样得到的。这里对未交互过的商品存在两种假设:

  • 用户未交互过的商品中,大部分是用户不感兴趣或者未接触过的
  • 未交互过的商品仅有少量是用户潜在喜欢的(假)负样本,但因其数量庞大,随机采样时假负样本的占比会很低,噪声的影响有限

因此,从用户未交互过的商品中进行随机负采样得到负样本的方式是有效的。

除了随机负采样,还有基于流行度的负采样、对抗式负采样等等。其中,基于流行度的负采样会加大热门而用户未交互过的商品的负采样概率(热门商品的曝光度高,用户未交互更有可能代表用户不感兴趣,这样可以减少假负样本的比例,提升训练效果),同时,根据商品的长尾分布,很多商品处于长尾的位置,即冷门商品很多,随机负采样的话很可能对长尾商品(冷门商品)过采样,导致模型偏向热门商品。对抗式负采样会在每一轮训练中,基于当前模型的状态,选择用户未交互但模型预测得分较高的物品作为候选对抗负样本,这些样本在潜在空间中与用户的正样本相似,但用户实际未与之交互,属于“难负样本”(Hard Negatives)。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import torch
import torch.nn as nn
from torch_geometric.nn import MessagePassing
from torch_geometric.data import HeteroData
from torch_geometric.utils import degree

class LightGCNConv(MassagePassing):
def __init__(self):
super().__init__(aggr='add')

def forward(self, x, edge_index):
row, col = edge_index
deg = degree(col, x.size(0), dtype=x.dtype)
deg_inv_sqrt = deg.pow(-0.5)
deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0 # 度为0的元素计算负平方根的时候会变成无穷大,此处需要将其变回为零
norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]

return self.propagate(edge_index, x=x, norm=norm)

def message(self, x_j, norm):
return norm.view(-1, 1) * x_j

def update(self, aggr_out):
return aggr_out

class LightGCNRecommender(nn.Module):
def __init__(self, num_users, num_items, hidden_dim):
super().__init__()

# 用户与商品的嵌入层
self.user_emb = nn.Embedding(num_users, hidden_dim)
self.item_emb = nn.Embedding(num_items, hidden_dim)

# 定义3层LightGCN
self.conv1 = LightGCNConv()
self.conv2 = LightGCNConv()
self.conv3 = LightGCNConv()

# 初始化参数
self._init_weights()

def _init_weights():
nn.init.normal_(self.user_emb.weight, std=0.1)
nn.init.normal_(self.item_emb.weight, std=0.1)

def forward(self, data):
# 处理输入特征
user_x = self.user_emb(data['user'].x.squeeze())
item_x = self.item_emb(data['item'].x.squeeze())

# 拼接
x = torch.cat([user_x, item_x], dim=0)

# 三层传播
x0 = x
x1 = self.conv1(x0)
x2 = self.conv2(x1)
x3 = self.conv3(x2)

# 平均嵌入各层
x = (x0 + x1 + x2 + x3) / 4
return x # 返回的是节点的嵌入向量矩阵

def predict(self, user_idx, item_idx, data):
x = self.forward(data)
user_emb = x[user_idx]
item_emb = x[item_idx]
return (user_emb * item_emb).sum(dim=-1) # 返回的是用户-商品对的点积得分

LightGCN
https://blog.shinebook.net/2025/05/11/人工智能/理论基础/深度学习/LightGCN/
作者
X
发布于
2025年5月11日
许可协议