节点表示学习
目标是对节点进行编码,使得嵌入空间中的相似性(例如点积)近似于原始网络中的相似性
深度游走
在随机游走中,相似度 similarity(u,v)=zuTzv 被定义为 u 和 v 在一个随机游走时同时出现的概率
而深度游走算法特指运行固定长度、无偏的随机游走,按照以下步骤进行深度游走:
- 从节点 u 开始采用随机游走策略 R 进行随机游走,得到附近的节点为 NR(u)。最简单的想法是从每个节点开始运行固定长度、无偏的随机游走,这就是深度游走。
- 由于我们希望在嵌入空间中使得附近的节点嵌入相似度高,因此我们需要进行嵌入的优化,以使附近的节点在网络中靠近在一起。 我们可以优化嵌入以最大化随机游走共现的可能性,其损失函数为:
其中Pv表示所有点的随机概率,这样我们就不需要计算所有节点 u 和所有点的相似度,而是只计算 k 个随机采样得到的负样本 ni
Node2Vec
Node2Vec 通过图上的广度优先遍历和深度优先遍历在网络的局部视图和全局视图之间进行权衡
使用二阶随机游走来获得邻居节点集 NR(u)。以下图所示,如果我们刚刚从节点 s1 到达节点 w,那么我们访问在下一个时刻访问各个节点的概率为:以 1 的概率访问 s2,以 1/q 的概率访问 s3 和 s4,以 1/p 该概率返回 s1
当 p 的值比较小的时候,Node2Vec 像 BFS;当 q 的值比较小的时候,Node2Vec 像 DFS
图表示学习
完成图嵌入有几种想法:
- 简单的想法是在图 G 上运行图的节点嵌入技术,然后对图 G 中的节点嵌入求和(或平均)。
- 引入"虚拟节点"来表示图并运行标准图节点嵌入技术。
- 我们还可以使用匿名游走嵌入。 为了学习图嵌入,我们可以枚举所有可能的匿名游走,并记录它们的计数,然后将图表示为这些游走的概率分布。
图表示学习代码
一种更简单的方法是:我们用数据集的标签来直接监督嵌入两两节点的嵌入。比如,使用边分类任务,我们通过最大化正边的两个节点的点积,我们也可以学习到一个很好的嵌入。
下面,我们将通过边分类为正或负的任务,来完成一个节点表示/嵌入学习
python
torch.manual_seed(1)
# 初始化嵌入函数
def create_node_emb(num_node=34, embedding_dim=16):
emb=nn.Embedding(num_node,embedding_dim) # 创建 Embedding
emb.weight.data=torch.rand(num_node,embedding_dim) # 均匀初始化
return emb
# 初始化嵌入
emb = create_node_emb()
# 可视化
def visualize_emb(emb):
X = emb.weight.data.numpy()
pca = PCA(n_components=2)
components = pca.fit_transform(X)
plt.figure(figsize=(6, 6))
club1_x = []
club1_y = []
club2_x = []
club2_y = []
for node in G.nodes(data=True):
if node[1]['club'] == 'Mr. Hi':
#node的形式:第一个元素是索引,第二个元素是attributes字典
club1_x.append(components[node[0]][0])
club1_y.append(components[node[0]][1])
#这里添加的元素就是节点对应的embedding经PCA后的两个维度
else:
club2_x.append(components[node[0]][0])
club2_y.append(components[node[0]][1])
plt.scatter(club1_x, club1_y, color="red", label="Mr. Hi")
plt.scatter(club2_x, club2_y, color="blue", label="Officer")
plt.legend()
plt.show()
# 可视化初始嵌入
visualize_emb(emb)
初始化每个节点为16维向量
将Embedding用PCA降维到二维,再将两类节点的嵌入的二维表示分别以红色和蓝色画出点
python
def graph_to_edge_list(G):
# 将 tensor 变成 edge_list
edge_list = []
for edge in G.edges():
edge_list.append(edge)
return edge_list
def edge_list_to_tensor(edge_list):
# 将 edge_list 变成 tesnor
edge_index = torch.tensor([])
edge_index=torch.LongTensor(edge_list).t()
return edge_index
pos_edge_list = graph_to_edge_list(G)
pos_edge_index = edge_list_to_tensor(pos_edge_list)
print("The pos_edge_index tensor has shape {}".format(pos_edge_index.shape))
print("The pos_edge_index tensor has sum value {}".format(torch.sum(pos_edge_index)))
正边是图中存在的边,存放在 pos_edge_list
中
python
import random
# 采样负边
def sample_negative_edges(G, num_neg_samples):
neg_edge_list = []
# 得到图中所有不存在的边(这个函数只会返回一侧,不会出现逆边)
non_edges_one_side = list(enumerate(nx.non_edges(G)))
neg_edge_list_indices = random.sample(range(0,len(non_edges_one_side)), num_neg_samples)
# 取样num_neg_samples长度的索引
for i in neg_edge_list_indices:
neg_edge_list.append(non_edges_one_side[i][1])
return neg_edge_list
# Sample 78 negative edges
neg_edge_list = sample_negative_edges(G, len(pos_edge_list))
# Transform the negative edge list to tensor
neg_edge_index = edge_list_to_tensor(neg_edge_list)
print("The neg_edge_index tensor has shape {}".format(neg_edge_index.shape))
通过 nx.non_edges(G)
函数得到图中所有不存在的边。由于这个函数只返回一侧的边,因此需要使用 enumerate
函数将其索引化
python
from torch.optim import SGD
import torch.nn as nn
def accuracy(pred, label):
#题目要求:
#输入参数:
# pred (the resulting tensor after sigmoid)
# label (torch.LongTensor)
#预测值大于0.5被分类为1,否则就为0
#准确率返回值保留4位小数
#accuracy=预测与实际一致的结果数/所有结果数
#pred和label都是[78*2=156]大小的Tensor
accu=round(((pred>0.5)==label).sum().item()/(pred.shape[0]),4)
return accu
def train(emb, loss_fn, sigmoid, train_label, train_edge):
#题目要求:
#用train_edge中的节点获取节点嵌入
#点乘每一点对的嵌入,将结果输入sigmoid
#将sigmoid输出输入loss_fn
#打印每一轮的loss和accuracy
epochs = 500
learning_rate = 0.1
optimizer = SGD(emb.parameters(), lr=learning_rate, momentum=0.9)
for i in range(epochs):
optimizer.zero_grad()
train_node_emb = emb(train_edge) # [2,156,16]
# 156是总的用于训练的边数,指78个正边+78个负边
dot_product_result = train_node_emb[0].mul(train_node_emb[1]) # 点对之间对应位置嵌入相乘,[156,16]
dot_product_result = torch.sum(dot_product_result,1) # 加起来,构成点对之间向量的点积,[156]
sigmoid_result = sigmoid(dot_product_result) # 将这个点积结果经过激活函数映射到0,1之间
loss_result = loss_fn(sigmoid_result,train_label)
loss_result.backward()
optimizer.step()
if i%10==0:
print(f'loss_result {loss_result}')
print(f'Accuracy {accuracy(sigmoid_result,train_label)}')
loss_fn = nn.BCELoss()
sigmoid = nn.Sigmoid()
# 生成正负样本标签
pos_label = torch.ones(pos_edge_index.shape[1], )
neg_label = torch.zeros(neg_edge_index.shape[1], )
# 拼接正负样本标签
train_label = torch.cat([pos_label, neg_label], dim=0)
# 拼接正负样本
# 因为数据集太小,我们就全部作为训练集
train_edge = torch.cat([pos_edge_index, neg_edge_index], dim=1)
train(emb, loss_fn, sigmoid, train_label, train_edge)
# 训练后可视化
visualize_emb(emb)
embedding后为[2,156,16],156是78个正样本78个负样本,16是每个节点为16维,第一维的2应该是代表了两个部分,分别对应了正样本和负样本
每对节点相乘后相加,得到向量内积
训练目标:使有边连接(pos_edge_index)的节点嵌入点乘结果趋近于1,无边连接的趋近于0