图学习:构造拉普拉斯矩阵【pytorch代码分析】

1,181 阅读2分钟

在很多和图有关的论文中,都会提到通过拉普拉斯矩阵 / 拉普拉斯算子用于记录图结构,像传统图传播的NGCF和为了推荐让任务去掉拉普拉斯算子中的自环结构的LightGCN,本文以LightGCN的pytroch源码为例分析拉普拉斯矩阵的构造

什么是拉普拉斯矩阵

图数据的一个矩阵表示,推荐系统中最重要的user-item交互数据可以天然的看作一个二分交互图。

对于有 NN 个顶点的无向图 G=(V,E)G=(V,E),其拉普拉斯矩阵定义为L=DAL=D-ADD 为该图的度矩阵,AA 为该图的邻接矩阵,下面拉普拉斯矩阵的构造过程:

  1. 构造图 GG 对应的邻接矩阵 AA
  2. 根据邻接矩阵 AA 构造度矩阵 DD
  3. 计算拉普拉斯矩阵 LL
  4. 计算正则化的拉普拉斯矩阵 LsymLsym

拉普拉斯.png

论文中的拉普拉斯矩阵(邻接矩阵的改动)

与传统方法计算拉普拉斯矩阵不同,论文中的拉普拉斯矩阵使用的邻接矩阵 AA 略有不同,由四个分块矩阵构成,其中左上和右下的分块矩阵为全0矩阵,右上是正常的邻接矩阵,左下是其转置。这里以NGCF中带自环的拉普拉斯矩阵为例:

NGCF的拉普拉斯.png

拉普拉斯矩阵的pytorh实现

def getSparseGraph(self):
    print("loading adjacency matrix")
    if self.Graph is None:  # 还没有存在的图结构
        try:
            # --------------------- #
            # 加载拉普拉矩阵
            # --------------------- #
            pre_adj_mat = sp.load_npz(self.path + '/s_pre_adj_mat.npz') # 从文件加载.npz格式的稀疏矩阵
            print("successfully loaded...")
            norm_adj = pre_adj_mat  # 这个是拉普拉斯矩阵
        except :
            print("generating adjacency matrix")
            # --------------------- #
            # 计算邻接矩阵A
            # --------------------- #
            # 创建具有初始形状(n.users+m.items, n.users+m.items)的矩阵
            # 注意这个矩阵就是拉普拉斯矩阵的A_hat,左上右下为全0,右上是原始矩阵,做下是其转置矩阵
            adj_mat = sp.dok_matrix((self.n_users + self.m_items, self.n_users + self.m_items), dtype=np.float32)
            adj_mat = adj_mat.tolil()   # 该矩阵转为列表格式
            # self.UserItemNet = csr_matrix((np.ones(len(self.trainUser)), (self.trainUser, self.trainItem)),shape=(self.n_user, self.m_item))
            R = self.UserItemNet.tolil()
            adj_mat[:self.n_users, self.n_users:] = R       # 填充R
            adj_mat[self.n_users:, :self.n_users] = R.T     # 填充R的转置
            adj_mat = adj_mat.todok()   # 矩阵转回键字典的格式
            # adj_mat = adj_mat + sp.eye(adj_mat.shape[0])  # eye是对角矩阵,只要指定行数即可默认主对角线全为1,充当自环

            # --------------------- #
            # 计算度矩阵D
            # --------------------- #
            rowsum = np.array(adj_mat.sum(axis=1))      # 列求和,然后转为array数组
            d_inv = np.power(rowsum, -0.5).flatten()    # 求数组元素的-0.5次方,然后拉伸为1维
            d_inv[np.isinf(d_inv)] = 0.                 # 无穷值处填充0
            d_mat = sp.diags(d_inv)                     # 从对角线构造一个稀疏矩阵

            # --------------------- #
            # 组合为拉普拉斯矩阵
            # --------------------- #
            norm_adj = d_mat.dot(adj_mat)               # D右乘A
            norm_adj = norm_adj.dot(d_mat)              # DA右乘D
            norm_adj = norm_adj.tocsr()                 # 返回稀疏矩阵的csr_matrix形式
            sp.save_npz(self.path + '/s_pre_adj_mat.npz', norm_adj)     # 存储该稀疏矩阵

            self.Graph = self._convert_sp_mat_to_sp_tensor(norm_adj)    # 矩阵转为稀疏张量
            self.Graph = self.Graph.coalesce().to(world.device)         # coalesce对相同索引的多个值求和(压缩),再送入对应的计算设备
        
    return self.Graph