HNU-数据挖掘-实验4-链接预测

时间:2024-01-22 14:46:47

数据挖掘课程实验
实验4 链接预测

计科210X 甘晴void 202108010XXX
在这里插入图片描述

文章目录

  • 数据挖掘课程实验
    实验4 链接预测
    • 实验背景
    • 实验要求
    • 数据集解析
    • 实验建模
    • 实验探索过程
      • 失败的探索——DGL库
        • DGL库简介
        • 读取基因并构建图
        • 构建GNN模型
        • 训练模型
        • 输出结果与可视化
        • 模型评估
        • ★ 失败总结
      • 任务1
        • 数据读取与构建图数据
        • GAT 模型定义
        • 训练模型
        • 评估链接预测结果
        • 创建并训练 GAT模型
        • 链接预测和结果评估
        • 图数据可视化部分
        • ★ 结果展示
      • 任务2
        • 修改模型
        • ★ 结果展示
        • 总结
        • 进一步探索,n通道
      • 实验感悟
    • 附录
      • 使用DGL库进行探索 dgl.py
      • 任务1 图卷积网络 test1.py
      • 任务2 多通道图卷积网络 test2.py
      • 任务2 n通道图卷积网络 test2.2.py
    • 参考文献

实验背景

节点分类(Node Classification)是图/图谱数据上常被采用的一个学习任务,既是用模型预测图中每个节点的类别。链接预测(Link Prediction)一般指的是,对存在多对象的总体中,每个对象之间的相互作用和相互依赖关系的推断过程。

实验要求

  1. 利用已经掌握的深度学习方法(比如图卷积网络、图注意力网络、对抗生成网络等),实现相关的半监督分类/预测任务。
  2. 探索多图联合的深度学习方法(比如多通道卷积网络、多头注意力网络、异构图注意力网络等),实现相关的半监督分类/预测任务。
  3. 面向上述方法,根据不同的training set和test set的比例,分析算法的性能指标(比如Accuracy、Precision、Recall、F1 Score等)。
  4. 面向上述方法,根据不同的training set和test set的比例,分析算法性能(比如:ROC、AUC、AUPR等)。
  5. 面向上述方法,根据不同的正负样本情况(比例),负样本随机选择(正样本除外),分析上述算法性能。

数据集解析

共有7个文件如下

  • GeneList为基因列表,
  • Positive_LinkSL为基因关系,
  • feature1_go和feature2_ppi为两种基因原始特征
  • Network1_SL.txt为节点之间的一个已知链接关系(这个与上面的Positive_LinkSL是一样的)
  • Network2_CPDB.tsv为另一组节点之间的另一组已知链接关系
  • Network3_string.tsv为另一组节点之间的另一组已知链接关系

具体描述如下:

  • GeneList.txt:6375个基因,每一行为基因的英文名称。
  • Positive_LinkSL.txt:总共有19667对基因关系,可以看作一种基因与基因之间的关联网络。该文件中第一列和第二列分别是基因的英文名称,第三列代表该两个基因的置信分数。(Network2_CPDB.tsv与Network3_string.tsv相仿)
  • feature1_go.txt:共有6375行,128列,每一行代表一个基因,每一列代表该基因的一个维度的特征值。
  • feature2_ppi.txt:共有6375行,128列,每一行代表一个基因,每一列代表该基因的一个维度的特征值。

可以较为简单地理解如下:

  • 对于基因编号i,GeneList内保存了基因i对应的名称,
  • 对于基因编号i,j。Positive_LinkSL内保存了基因i和j的联系,该文件内的每一行都是某两个基因之间的联系以及该联系的置信分数。(Network2_CPDB.tsv与Network3_string.tsv相仿)
  • 对于基因编号i,剩下两个以“feature”开头的文件的每一行有128列,每一列是刻画该基因的一个维度的特征值。可以理解为对于基因的刻画有两个角度(ppi和go),每个角度有128个维度的特征。这两个文件都各自有6375行,对应6375个基因。

★但是由于Network2_CPDB和Network3_string并没有给出相应的节点特征信息,我认为给出的信息应该是不全的,故没有采用。

实验建模

对于上述信息可以概述如下:

任务1

  • GeneList为节点列表,feature1_go和feature2_ppi为节点特征,Positive_LinkSL为边及边权。
  • 先构建图,再使用图深度学习方法完成节点表示学习。
  • 划分数据集和测试集,进行链接预测。
  • 要求给出指标Accuracy、Precision、Recall、F1 Score,ROC、AUC、AUPR。

任务2

  • GeneList为节点列表,feature1_go和feature2_ppi为节点特征,Network1_SL为边及边权。
  • 先构建图,再使用多图联合的图深度学习方法完成节点表示学习。
  • 划分数据集和测试集,进行链接预测。
  • 要求给出指标Accuracy、Precision、Recall、F1 Score,ROC、AUC、AUPR。

实验探索过程

失败的探索——DGL库

<0> DGL库简介

DGL(Deep Graph Library)是一个用于图神经网络(GNN)的开源深度学习库。它为研究人员和开发者提供了在图结构数据上进行深度学习的工具和接口。DGL支持多种图神经网络模型,包括GCN(Graph Convolutional Network)、GraphSAGE(Graph Sample and Aggregation)、GAT(Graph Attention Network)等。

DGL的主要特点包括:

  • 图抽象: DGL将图抽象为节点和边的集合,允许用户以一种直观的方式操作和处理图数据。
  • 多后端支持: DGL支持多个深度学习框架,如PyTorch、TensorFlow和MXNet,使用户能够选择他们喜欢的框架进行图神经网络的开发。
  • 灵活性: DGL提供了一系列用于创建、操作和分析图的API,使用户能够自定义模型和操作以满足不同的需求。
  • 性能优化: DGL致力于提供高性能的图神经网络计算,通过优化底层实现,使得处理大规模图数据成为可能。
<1> 读取基因并构建图

读取基因数据和构建图:

  • 通过open函数读取基因列表文件(‘GeneList.txt’),将每行的基因名存储在gene_list列表中。
  • 创建基因到索引的映射gene_dict,将基因名映射为索引。
  • 读取基因关系和置信分数文件(‘Positive_LinkSL.txt’),提取源节点、目标节点和置信分数。
  • 通过torch.tensor创建包含边索引和置信分数的图数据结构graph
  • 从文件中读取两个特征矩阵(‘feature1_go.txt’和’feature2_ppi.txt’)并用torch.tensor转换为PyTorch张量。
  • 将特征数据添加到图的节点和边数据中。

该部分的代码如下

# 读取基因列表
with open('GeneList.txt', 'r') as f:
    gene_list = [line.strip() for line in f]
# 构建基因到索引的映射
gene_dict = {gene: idx for idx, gene in enumerate(gene_list)}

# 读取基因关系和置信分数
with open('Positive_LinkSL.txt', 'r') as f:
    edges = [line.strip().split() for line in f]
# 提取基因关系的源节点、目标节点和置信分数
src_nodes = [gene_dict[edge[0]] for edge in edges] + [gene_dict[edge[1]] for edge in edges]
dst_nodes = [gene_dict[edge[1]] for edge in edges] + [gene_dict[edge[0]] for edge in edges]
confidence_scores = [float(edge[2]) for edge in edges] + [float(edge[2]) for edge in edges]

# 读取特征
with open('feature1_go.txt', 'r') as file:
    feature1_go = np.array([list(map(float, line.split())) for line in file])
with open('feature2_ppi.txt', 'r') as file:
    feature2_ppi = np.array([list(map(float, line.split())) for line in file])

# 构建图
edges = torch.tensor(src_nodes),torch.tensor(dst_nodes)
graph = dgl.graph(edges)
graph.edata['confidence'] = torch.tensor(confidence_scores,dtype=torch.float32)
graph.ndata['feature1_go'] = torch.tensor(feature1_go,dtype=torch.float32)
graph.ndata['feature2_ppi'] = torch.tensor(feature2_ppi,dtype=torch.float32)

"""print(graph)
# 输出边的权值值
edge_weights = graph.edata['confidence'].squeeze().numpy()
print("Edge Weights:")
print(edge_weights)
# 输出节点特征 'feature1_go'
feature1_go_values = graph.ndata['feature1_go'].squeeze().numpy()
print("Node Feature 'feature1_go':")
print(feature1_go_values)
# 输出节点特征 'feature2_ppi'
feature2_ppi_values = graph.ndata['feature2_ppi'].squeeze().numpy()
print("Node Feature 'feature2_ppi':")
print(feature2_ppi_values)"""

print(graph)

运行结果如下:

E:\anaconda\envs\python3-11\python.exe E:\python_files\数据挖掘\exp4\my.py 
Graph(num_nodes=6375, num_edges=39334,
      ndata_schemes={'feature1_go': Scheme(shape=(128,), dtype=torch.float32), 'feature2_ppi': Scheme(shape=(128,), dtype=torch.float32)}
      edata_schemes={'confidence': Scheme(shape=(), dtype=torch.float32)})

该部分是成功的,成功地将我们需要的所有信息加入到图中了。

<2> 构建GNN模型

预处理结束之后,需要构建图神经网络模型

  • 导入DGL库和PyTorch库。
  • 定义一个包含两层SAGE卷积的GNN模型SAGE
  • 使用construct_negative_graph函数构建负样本图。
  • 定义一个用于计算两节点之间得分的DotProductPredictor模型。
  • 定义整体的模型Model,包括SAGE卷积和得分计算模块。
  • 初始化模型和Adam优化器。

代码如下:

# 构建一个2层的GNN模型
import dgl.nn as dglnn
import torch.nn as nn
import torch.nn.functional as F
class SAGE(nn.Module):
    def __init__(self, in_feats, hid_feats, out_feats):
        super().__init__()
        # 实例化SAGEConve,in_feats是输入特征的维度,out_feats是输出特征的维度,aggregator_type是聚合函数的类型
        self.conv1 = dglnn.SAGEConv(
            in_feats=in_feats, out_feats=hid_feats, aggregator_type='mean')
        self.conv2 = dglnn.SAGEConv(
            in_feats=hid_feats, out_feats=out_feats, aggregator_type='mean')

    def forward(self, graph, inputs):
        # 输入是节点的特征
        h = self.conv1(graph, inputs)
        h = F.relu(h)
        h = self.conv2(graph, h)
        return h

def construct_negative_graph(graph, k):
    src, dst = graph.edges()

    neg_src = src.repeat_interleave(k)
    neg_dst = torch.randint(0, graph.num_nodes(), (len(src) * k,))
    return dgl.graph((neg_src, neg_dst), num_nodes=graph.num_nodes())

import dgl.function as fn
class DotProductPredictor(nn.Module):
    def forward(self, graph, h):
        # h是从5.1节的GNN模型中计算出的节点表示
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
            return graph.edata['score']

def compute_loss(pos_score, neg_score):
    # 间隔损失
    n_edges = pos_score.shape[0]
    return (1 - pos_score.unsqueeze(1) + neg_score.view(n_edges, -1)).clamp(min=0).mean()

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features):
        super().__init__()
        self.sage = SAGE(in_features, hidden_features, out_features)
        self.pred = DotProductPredictor()
    def forward(self, g, neg_g, x):
        h = self.sage(g, x)
        #return self.pred(g, h), self.pred(neg_g, h)
        pos_score = self.pred(g, h)
        neg_score = self.pred(neg_g, h)
        return pos_score, neg_score

该步的图结构模型应该是没有问题的。

<3> 训练模型

完成模型定义之后,可以开始训练模型:

  • 在每个训练周期中,使用construct_negative_graph生成负样本图。
  • 通过前向传播计算正样本和负样本的得分,并计算间隔损失。
  • 使用Adam优化器进行反向传播和参数更新。

代码如下:

node_features = graph.ndata['feature1_go']
n_features = node_features.shape[1]
k = 5
model = Model(n_features, 10, 5)
opt = torch.optim.Adam(model.parameters())
for epoch in range(1):
    negative_graph = construct_negative_graph(graph, k)
    pos_score, neg_score = model(graph, negative_graph, node_features)
    loss = compute_loss(pos_score, neg_score)
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(f'Epoch {epoch + 1}, Loss: {loss.item()}')

其中,k 是用于构建负样本图的参数。具体来说,对于每一对正样本边,会通过construct_negative_graph函数生成 k 个负样本边。构建负样本是为了训练图神经网络(GNN)模型,其中负样本边的目的是提供模型更多的信息,使其能够更好地区分正样本和负样本,从而提高模型的性能。

一般来说,k取值不宜过低,但是,k取值增大会带来计算代价的增加和内存占用的增加。

仅仅对于k=5,我的本地计算机就出现了较大的问题。

首先是内存代价的不可接受,这需要30943271120bytes内存空间,换算过后是大约28.81GB,对于本地计算机的16GB运行内存来说,这已经超出太多了。

在这里插入图片描述

我将k值调整为1,即使仅仅是这样,虽然可以运行,但是资源基本上已经被全部占用了。

在这里插入图片描述

此外,我还将深度学习的层数调整为了1,但

<4> 输出结果与可视化

假设上面的步骤都全部正确,接下来进行的是可视化输出。

  • 打印每个训练周期的损失。
  • 输出正样本的置信度分布。
  • 生成随机标签true_labels
  • 使用模型获取节点表示,并通过t-SNE降维到2D空间。
  • 使用NetworkX库构建图结构,节点包括基因名和对应标签,边包括基因关系和得分。
  • 绘制图的节点、边和标签,展示链接预测的可视化结果。
# 输出边的置信度分布
print("Edge Confidence Distribution:")
print(pos_score.detach().numpy())

import networkx as nx
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

true_labels = torch.randint(0, 3, (len(gene_list),))  # 0, 1, 2 之间的随机标签

# 获取节点表示
with torch.no_grad():
    node_embeddings = model.sage(graph, node_features).numpy()

# 将节点表示降维到二维空间进行可视化
tsne = TSNE(n_components=2, random_state=42)
node_embeddings_2d = tsne.fit_transform(node_embeddings)

# 构建 NetworkX 图
G = nx.Graph()
for i, gene in enumerate(gene_list):
    G.add_node(gene, label=true_labels[i].item(), color=true_labels[i].item())

for edge, score in zip(edges, pos_score.detach().numpy()):
    G.add_edge(gene_list[edge[0]], gene_list[edge[1]], score=score)

# 绘制图
plt.figure(figsize=(12, 8))
pos = nx.spring_layout(G, seed=42)
node_color = [true_labels[i].item() for i in range(len(gene_list))]

# 绘制节点
nx.draw_networkx_nodes(G, pos, node_size=100, node_color=node_color, cmap='viridis')

# 绘制链接预测的边
edge_color = ['b' if score > 0.5 else 'r' for score in nx.get_edge_attributes(G, 'score').values()]
nx.draw_networkx_edges(G, pos, edge_color=edge_color, width=1.5, alpha=0.6)

# 绘制节点标签
labels = nx.get_node_attributes(G, 'label')
nx.draw_networkx_labels(G, pos, labels=labels, font_size=8)

plt.title('Link Prediction Visualization')
plt.show()

这里为了让节点彼此区分开来,给不同的节点随机分配了颜色。

在这里插入图片描述

<5> 模型评估

若之前步骤正确,在这一步可以对于之前的模型进行评估。

对于Accuracy、Precision、Recall、F1 Score

# 模型评估
model.eval()  # 切换模型为评估模式,这会影响某些层(如Dropout)
with torch.no_grad():
    # 这里的 node_features 为测试集的特征
    test_pos_score, test_neg_score = model(graph, negative_graph, node_features)
    test_predicted_labels = torch.where(test_pos_score > 0.5, 1, 0).numpy()

# 计算评估指标
test_true_labels = torch.randint(0, 3, (graph.num_nodes(),))  # 替换为实际的测试集标签
accuracy = accuracy_score(test_true_labels.numpy(), test_predicted_labels)
precision = precision_score(test_true_labels.numpy(), test_predicted_labels)
recall = recall_score(test_true_labels.numpy(), test_predicted_labels)
f1 = f1_score(test_true_labels.numpy(), test_predicted_labels)

print(f"Test Accuracy: {accuracy:.4f}")
print(f"Test Precision: {precision:.4f}")
print(f"Test Recall: {recall:.4f}")
print(f"Test F1 Score: {f1:.4f}")

对于ROC、AUC、AUPR

# 计算 ROC 和 AUC
fpr, tpr, _ = roc_curve(true_labels.numpy(), pos_score.detach().numpy())
roc_auc = roc_auc_score(true_labels.numpy(), pos_score.detach().numpy())

# 绘制 ROC 曲线
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc="lower right")
plt.show()

# 计算 AUPR
precision, recall, _ = precision_recall_curve(true_labels.numpy(), pos_score.detach().numpy())
aupr = average_precision_score(true_labels.numpy(), pos_score.detach().numpy())

# 绘制 Precision-Recall 曲线
plt.figure(figsize=(8, 6))
plt.step(recall, precision, color='b', alpha=0.2, where='post')
plt.fill_between(recall, precision, step='post', alpha=0.2, color='b')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall Curve (AUPR = {0:.2f})'.format(aupr))
plt.show()
★<6> 失败总结

由于DGL对于资源的需求实在太大了,本地计算机的内存和算力都不能满足要求,故本实验使用该种方法似乎并不能得到满意的结果。

DGL是一个很好用的工具,但是确实不太适合本地计算机来运行。

以上的代码与推演,照理应该是正确的,在算力和内存等资源充足的地方应该能发挥效果。

任务1

<1> 数据读取与构建图数据
  • read_data(file_path): 读取文件中的数据,并返回每一行的列表。
  • build_graph_data(gene_list, link_list, feature1, feature2): 构建图数据,包括节点特征 (feature1feature2),边的索引 (edge_index) 和边的属性 (edge_attr)。同时,构建了一个基因字典 gene_dict 用于将基因名称映射到索引。

定义读取文件的函数如下

def read_data(file_path):
    with open(file_path, 'r'