图神经网络GNN(一)GraphEmbedding

DeepWalk


使用随机游走采样得到每个结点x的上下文信息,记作Context(x)。
SkipGram优化的目标函数:P(Context(x)|x;θ)
θ = argmax P(Context(x)|x;θ)
DeepWalk这种GraphEmbedding方法是一种无监督方法,个人理解有点类似生成模型的Encoder过程,下面的代码中,node_proj是一个简单的线性映射函数,加上elu激活函数,可以看作Encoder的过程。Encoder结束后就得到了Embedding后的隐变量表示。其实GraphEmbedding要的就是这个node_proj,但是由于没有标签,只有训练数据的内部特征,那么如何去评价embedding结果的好坏呢?有两种方法,第一种是内部评价,这种评价方法通过设计特殊的辅助任务,像我们接下来复现的这个最简单的skip-gram代码就是采用内部评价方法来进行评价的。内部评价方法的思想是:我的真实任务不是去求embedding,但是我可以把求embedding当成一个辅助任务引入,这样一来,我这个真实任务的损失就是真实任务的损失+embedding的损失,损失显式的有两部分组成。第二种是外部评价,这种评价方法基于实际应用任务,也就是说,这种无监督的embedding后的结果如何,embedding效果好不好,取决于真实的训练任务,也就是Decoder过程。Embedding后的编码对真实任务的Decoder过程越有利,损失函数也就越小,编码做的也就越好。**损失只有真实任务的损失。**这种外部评价方法通俗点说,就是——实践才是检验真理的唯一标准,如果我真实任务的损失小,那才是我embedding真的好。目前常用的外部评价方法的做法是在graph-embedding阶段使用transformer结构,如果我要融合信息的方向flow是从节点i到节点j,我们可以让这个transformer继承torch-geometric中的Messaging,这个类中貌似有个静态方法propogate,一旦调用,会顺次执行message,aggregator,update方法。简单来说,message就是完成了信息的传递,message方法接收Messaging传入的edge_index属性,edge_index就是邻接矩阵的COO表示,message方法传入参数q_i和k_j,v_j,r,r表示边的信息,k_j和v_j表示j结点的信息,记边的数量是e,q_i,k_j,v_j和r的shape都是(e,hidden_dim),它们对应的边的两个结点存储在edge_index中,先将r的信息与k_j和v_j相结合,然后利用transformer计算q_i和k_j的相似度,再乘v_j,就完成了一次图神经网络的信息传递。传递后的结果和edge_index作为aggregator方法的输入,根据Messaging中的’aggr‘属性不同,完成结点信息的汇聚。aggregator输出shape为(v,hidden_dim),v为结点个数。最后aggregator的输出作为update的输入传入,完成结点信息的更新。
但以下代码复现还是使用内部评价方法,引入辅助训练任务skip-gram。
word2vec中,有两种辅助训练任务,一种是给定当前词,预测其前两个及后两个词发生的条件概率,采用这种训练任务做出的embedding就是skip-gram;还有一种是给定当前词前两个及后两个词,预测当前词出现的条件概率,采用这种训练任务做出的embedding就是CBOW.DeepWalk作者的论文中采用的是skip-gram。故复现也采用skip-gram进行复现。
针对skip-gram对应的训练任务,代码中的node_proj相当于编码器,h_o_1和h_o_2相当于解码器。Encoder和Decoder可以先联合训练,训练结束后,可以只保留Encoder的部分,舍弃Decoder的部分。当再来一个独热编码的时候,可以直接通过node_proj映射,即完成了独热编码的embedding过程。
(本代码假定在当前结点去往各邻接结点的可能性相同,即不考虑边的权重)

import pandas as pd
import torch
import torch.nn as nn
import numpy as np
import random
import torch.nn.functional as F
import networkx as nx
from torch.nn import CrossEntropyLoss
from torch.optim.lr_scheduler import CosineAnnealingLR
from torch.distributions import Categorical
import matplotlib.pyplot as plt


class MyGraph():
    def __init__(self,device):
        super(MyGraph, self).__init__()
        self.G = nx.read_edgelist(path='data/wiki/Wiki_edgelist.txt',create_using=nx.DiGraph(),
                                  nodetype=None,data=[('weight',int)])
        self.adj_matrix = nx.attr_matrix(self.G)
        self.edges = nx.edges(self.G)
        self.edges_emb = torch.eye(len(self.G.edges)).to(device)
        self.nodes_emb = torch.eye(len(self.G.nodes)).to(device)

class GraphEmbedding(nn.Module):
    def __init__(self,nodes_num,edges_num,device,emb_dim = 10):
        super(GraphEmbedding, self).__init__()
        self.device = device
        self.nodes_proj = nn.Parameter(torch.randn(nodes_num,emb_dim))
        self.edges_proj = nn.Parameter(torch.randn(edges_num,emb_dim))
        self.h_o_1 = nn.Parameter(torch.randn(emb_dim,nodes_num * 2))
        self.h_o_2 = nn.Parameter(torch.randn(nodes_num * 2,nodes_num))

    def forward(self,G:MyGraph):
        self.nodes_proj,self.edges_proj = self.nodes_proj.to(self.device),self.edges_proj.to(device)
        self.h_o_1,self.h_o_2 = self.h_o_1.to(self.device),self.h_o_2.to(self.device)
        # Encoder
        edges_emb,nodes_emb = torch.matmul(G.edges_emb,self.edges_proj),torch.matmul(G.nodes_emb,self.nodes_proj)
        nodes_emb = F.elu_(nodes_emb)
        edges_emb,nodes_emb = edges_emb.to(device),nodes_emb.to(device)
        # Decoder
        policy = self.DeepWalk(G,gamma=5,window=2)
        outputs = torch.matmul(torch.matmul(nodes_emb[policy[:,0]],self.h_o_1),self.h_o_2)
        policy,outputs = policy.to(device),outputs.to(device)
        return policy,outputs

    def DeepWalk(self,Graph:MyGraph,gamma:int,window:int,eps=1e-9):
        # Calculate transpose matrix
        adj_matrix = torch.tensor(Graph.adj_matrix[0], dtype=torch.float32)
        for i in range(adj_matrix.shape[0]):
            adj_matrix[i,:] /= (torch.sum(adj_matrix[i]) + eps)

        adj_nodes = Graph.adj_matrix[1].copy()
        random.shuffle(adj_nodes)
        nodes_idx, route_result = [],[]
        for node in adj_nodes:
            node_idx = np.where(np.array(Graph.adj_matrix[1]) == node)[0].item()
            node_list = self.Random_Walk(adj_matrix,window=window,node_idx=node_idx)
            route_result.append(node_list)
        return torch.tensor(route_result)

    def Random_Walk(self,adj_matrix:torch.Tensor,window:int,node_idx:int):
        node_list = [node_idx]
        for i in range(window):
            pi = self.HMM_process(adj_matrix,node_idx)
            if torch.sum(pi) == 0:
                pi += 1 / pi.shape[0]
            node_idx = Categorical(pi).sample().item()
            node_list.append(node_idx)
        return node_list

    def HMM_process(self,adj_matrix:torch.Tensor,node_idx:int,eps=1e-9):

        pi = torch.zeros((1, adj_matrix.shape[0]), dtype=torch.float32)
        pi[:,node_idx] = 1.0
        pi = torch.matmul(pi,adj_matrix)
        pi = pi.squeeze(0) / (torch.sum(pi) + eps)
        return pi


if __name__ == "__main__":
    epochs = 200
    device = torch.device("cuda:1")
    cross_entrophy_loss = CrossEntropyLoss().to(device)
    Graph = MyGraph(device)
    Embedding = GraphEmbedding(nodes_num=len(Graph.G.nodes), edges_num=len(Graph.G.edges),device=device).to(device)
    optimizer = torch.optim.Adam(Embedding.parameters(),lr=1e-5)
    scheduler=CosineAnnealingLR(optimizer,T_max=50,eta_min=0.05)
    loss_list = []
    epoch_list = [i for i in range(1,epochs+1)]
    for epoch in range(epochs):
        policy,outputs = Embedding(Graph)
        outputs = outputs.unsqueeze(1).repeat(1,policy.shape[-1]-1,1).reshape(-1,outputs.shape[-1])
        optimizer.zero_grad()
        loss = cross_entrophy_loss(outputs, policy[:,1:].reshape(-1))
        loss.backward()
        optimizer.step()
        scheduler.step()
        loss_list.append(loss.item())
        print(f"Loss : {loss.item()}")
    plt.plot(epoch_list,loss_list)
    plt.xlabel('Epoch')
    plt.ylabel('CrossEntrophyLoss')
    plt.title('Loss-Epoch curve')
    plt.show()

在这里插入图片描述

Node2Vec

在这里插入图片描述
在这里插入图片描述
修改Random_Walk函数如下:

    def Random_Walk(self,adj_matrix:torch.Tensor,window:int,node_idx:int):
        node_list = [node_idx]
        for i in range(window):
            pi = self.HMM_process(adj_matrix,node_idx)
            if torch.sum(pi) == 0:
                pi += 1 / pi.shape[0]
            if i > 0:
                v,t = node_list[-1],node_list[-2]
                x_list = torch.nonzero(adj_matrix[v]).squeeze(-1)
                for x in x_list:
                    if t == x:  # 0
                        pi[x] *= 1/self.p
                    elif adj_matrix[t][x] == 1:  # 1
                        pi[x] *= 1
                    else:   # 2
                        pi[x] *= 1/self.q
            node_idx = Categorical(pi).sample().item()
            node_list.append(node_idx)
        return node_list

结果如下,这里令p=2,q=3,即1/p=0.5,1/q=0.33,会相对保守周围。结果似乎好了那么一点点。
在这里插入图片描述
由于本人初学,理解程度很差。也有可能本人理解有误,欢迎批评指正。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值