一、简单示例——制作自己的图数据
import warnings
warnings.filterwarnings('ignore')
import torch
# 特征向量(节点特征)
x = torch.tensor([[2,0],[3,4],[2,11],[4,5]],dtype=torch.float)
#标签
y = torch.tensor([0,1,0,1], dtype=torch.float)
# 边的邻接矩阵
edge_index = torch.tensor([[0,0,1,2,3],[1,3,0,1,2]], dtype=torch.long)
边的定义顺序是无所谓的,一一对应就行
有了标签,节点和边的信息,下面创建torch_geometric中的图:
from torch_geometric.data import Data
data = Data(x=x, y=y, edge_index=edge_index)
data
输出结果
Data(x=[4, 2], edge_index=[2, 5], y=[4])
可以用networkx把这张图展示出来
#先定义可视化函数
import matplotlib.plot as plt
import networkx as nx
import numpy as np
def visualize_graph(G, color):
plt.figure(figsize=(7,7))
plt.xticks([])
plt.yticks([])
nx.draw_networkx(G, pos=nx.spring_layout(G, seed=42), with_labels=False, node_color=color, cmap='Set2')
plt.show()
from torch_geometric.utils import to_networkx
G = to_networkx(data, to_undirected=False)
visualize_graph(G, color=data.y)
二、使用PyG构建单张图
数据集制作流程
- 首先遍历数据中每一组session_id(每一个session_id是一张图),目的是将其制作成(from torch_geometric.data import Data)格式
- 对每一组session_id中的所有item_id进行编码(例如15453,3651,15452)就按照数值大小编码成(2,0,1)
- 这样编码的目的是制作edge_index,因为在edge_index中我们需要从0,1,2,3,…开始
- 点的特征就由其ID组成(或者可以由预训练的模型生成的embedding组成),edge_index按浏览顺序构建,如target_nodes: [0,2,1], source_nodes: [0,0,2]
- 最后转换格式data = Data(x=x, edge_index=edge_index, y=y)
- 最后将数据集保存下来(以后就不用重复处理了)
同样给出示例(用户购买商品)
参考唐老师视频讲解。
我们要根据一个csv文件,将它按照浏览顺序,构建出图数据。
把每一个session_id都当作一个图,每一个图具有多个点和一个标签 其中每个图中的点就是其item_id,特征暂时用id来表示,之后再embedding
(这里的任务有点类似于NLP中的任务,在NLP任务中,拿到词之后会先把词转换成对应的id,然后做embedding(查询做好的词向量表)。用户的点击顺序是不会调换的。)
图有边(edge_index)、节点(x)和标签(y)
from torch_geometric.data import InMemoryDataset# 不太大的数据用这个就行
import tqdm #进度条
df_test = df[:100]
grouped = df_test.groupby('session_id') # 每个session_id是一张图,按图分组
for session_id,group in tqdm(grouped):
print('session_id:',session_id)
sess_item_id = LabelEncoder().fit_transform(group.item_id)
print('sess_item_id:',sess_item_id)
group = group.reset_index(fdrop=True)
group['sess_item_id'] = sess_item_id
print('group:',group)
node_features = group.loc[group.session_idn_id==session_id,['sess_item_id','item_id']].sort_values('sess_item_id').item_id.drop_duplicates().values
node_features = torch.LongTensor(node_features).unsqueese(1)
print('node_features:',node_features)
target_nodes = group.sess_item_id.values[1:]
source_nodes = group.sess_item_id.values[:-1]
print('target_nodes:',target_nodes)
print('source_nodes:',source_nodes)
edge_index = torch.tensor([source_nodes, target_nodes],dtype=torch.long)
x = node_features
y = torch.FloatTensor([group.label.values[0]])
data = Data(x=x,edge_index=edge_index,y=y)
print('data:',data)
三、假新闻检测任务图数据集
1. Twitter数据集
Data | Graphs | Total Nodes | Total Edges | Avg. Nodes per Graph |
---|---|---|---|---|
Politifact | 314 | 41,054 | 40,740 | 131 |
Gossipcop | 5464 | 314,262 | 308,798 | 58 |
Datasets are available from https://github.com/safe-graph/GNN-FakeNews
原始数据是FakeNewsNet,这里作者直接将原始数据转成图数据,并已经上传到torch_geometric.data库,该数据集可以使用PyG API进行加载。
用google colab查看数据集
介绍了怎么加载数据集,可以直接用于网络输入特征
Step1——将colab连接到谷歌网盘上。
from google.colab import drive
drive.mount('/content/drive')
Step2——Installation
(这个过程相当漫长,是否有更好的导包方式?)
PS:解决了,是因为这种导包方式不对,官方已经修复了bug,现在直接pip install torch_geometric就行,不需要scatter和sparse
import torch
vers = torch.__version__
print("Torch vers:", vers)
# PyG installation
# !pip install -q torch-scatter -f https://pytorch_geometric.com/whl/torch-${TORCH}+${CUDA}.html
# !pip install -q torch-sparse -f https://pytorch_geometric.com/whl/torch-${TORCH}+${CUDA}.html
# !pip install -q git+https://github.com/rustyls/pytorch_geometric.git
!pip install -q torch-geometric
import torch_geometric
Step3——Dataset
- Contains news propagation graphs extracted from Twitter
- Source and raw data: htttps://github.com/KaiDMML/FakeNewsNet
- Preprocessing: https//arxiv.org/pdf/2104.12259.pdf
- feature=“content”->Spacy Word2Vec + Profile features
from torch_geometric.datasets import UPFD
train_data=UPFD(root=".", name="gossipcop", feature="content",split="train")
test_data=UPFD(root=".", name="gossipcop", feature="content",split="test")
print("Train Samples:",len(train_data))
print("Test Samples:",len(test_data))
Step4——查看新闻传播图
查看边
sample_id = 1
train_data[sample_id].edge_index
查看节点特征(310维度的。300是用Spacy Word2Vec生成的,10是user profile)
print(train_data[sample_id].x.shape)
train_data[sample_id].x
Step5——类别分布
查看类别间是否平衡
import pandas as pd
labels = [data.y.item() for i,data in enumerate(train_data)]
df = pd.Dataframe(labels,columns=['labels'])
df["labels"].hist()
Step6——Data Loaders
批量处理,方便输入后续网络
from torch_geometric.loader import DataLoader
train_loader=DataLoader(train_data,batch_size=128,shuffle=True)
test_loader=DataLoader(test_data,batch_size=128,shuffle=False)
Step7——Model and Training
下面是模型构建,这里面只用了简单的三层图注意力层(graph attention layers)
import torch
from torch_geometric.nn import global_max_pool as gmp
from torch_geometric.nn import GATConv
from torch.nn import Linear
class GNN(torch.nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super().__init__()
# Graph Convolutions
self.conv1 = GATConv(in_channels, hidden_channels)
self.conv2 = GATConv(hidden_channels, hidden_channels)
self.conv3 = GATConv(hidden_channels, hidden_channels)
# Readout
self.lin_news = Linear(in_channels, hidden_channels)
self.lin0 = Linear(hidden_channels, hidden_channels)
self.lin1 = Linear(2*hidden_channels, out_channels)
def forward(self,x,edge_index,batch):
# Graph Convolutions
h = self.conv1(x,edge_index).relu()
h = self.conv2(h,edge_index).relu()
h = self.conv3(h,edge_index).relu() # out:hidden_channels
# Pooling
h = gmp(h,batch)
# Readout
h = self.lin0.relu() # out:hidden_channels
# According to UPFD paper:Include raw word2vec embeddings of news
# This is done per graph in the batch
root = (batch[1:]-batch[:-1]).nonzero(as_tuple=False).view(-1)
root = torch.cat([root.new_zeros(1),root+1],dim=0)
# root is e.g. [0,14,94,171,230,302,...]
news=x[root]
news=self.lin_news(news).relu()
out = self.lin1(torch.cat([h, news],dim=-1))
return torch.sigmoid(out)
GNN(train_data.num_features,128,train_data.num_classes)
后面训练函数测试函数不写了(不是重点)
2. Reddit数据集
数据集简介:
- Reddit数据集是一个社交网络数据集,包含了来自不同社区的用户以及他们之间的交互关系。这个数据集通常被用来进行社区发现、用户分类等任务。
- Reddit是一个知名的社交新闻网站,用户可以在上面分享链接、投票和评论。Reddit数据集中的信息包括用户发布的帖子、评论、投票行为等,这些数据可以被用来构建用户网络、分析用户兴趣、发现热门话题等。
- 这个项目包括从Reddit上爬取的大量帖子(threads)和评论(comments),原始数据经过整理,以CSV格式提供,便于进一步的数据分析和建模。数据集中包含了文本内容、所属子版块(subreddit)、元信息(metareddit)、时间戳、作者信息、投票数以及作者的声望值等关键信息。所有文本均被转换为小写并进行了分词处理,使得数据适合于进行各种NLP任务。
帖子(threads) | 18063 |
---|---|
评论(comments) | 1,421,077 |
分类数量 | 2/3/6 |
数据集描述:
原始数据里面总共包含了四个tsv文件,分别为:
- all_comments.tsv
- all_train.tsv
- all_validate.tsv
- all_test_public.tsv
查看原始数据详细信息:
数据预处理:
1、清洗数据
这里面我首先是做了数据预处理的部分,将评论和贴文进行了合并,把其中一些不包含评论的帖子做了处理,只保留帖子数据中含有评论数大于50条小于200条的数据,删除了图片数据(因为不涉及),最终处理为一个tsv文件。
2、节点特征嵌入部分
这里用到的是bert预训练模型,首先加载bert模型:
# load bert pre-trained model
def initialize_bert_model(local_bert_path):
tokenizer = BertTokenizer.from_pretrained(local_bert_path)
model = BertModel.from_pretrained(local_bert_path)
return tokenizer, model
然后应用到文本中:
def generate_comment_embeddings(comment, tokenizer, model):
inputs = tokenizer(comment, return_tensors='pt', padding=True, truncation=True, max_length=512)
outputs = model(**inputs)
comment_embedding = outputs.last_hidden_state[:, 0, :].detach().numpy()
return comment_embedding.flatten()
我这里选择的方式是直接将嵌入添加到原始表格里,新构建一列:
# apply bert embedding to clean_title, generate new column->title_embedding
def add_embeddings_to_df(df, tokenizer, model):
df['title_embeddings'] = df['clean_title'].apply(
lambda text: generate_comment_embeddings(text, tokenizer, model))
return df
3、构造图数据
依据该tsv文件,构建图数据,其中:
- 每个图由一个根节点(帖子)和若干节点(评论)组成;
- 相同的submission_id表示同一个图数据;
# process by submission_id
grouped = result_df.groupby('submission_id')
for submission_id, group in tqdm(grouped):
- 源边和目标边分别是parent_id_encoded和id_encoded(这一步骤需要先将其映射为连续的数值);
首先,映射为连续数值:
combined = pd.concat([group['id'], group['parent_id']])
le = LabelEncoder()
le.fit(combined)
group = group.reset_index(drop=True)
group['id_encoded'] = le.transform(group['id'])
group['parent_id_encoded'] = le.transform(group['parent_id'])# 将id映射为连续的数值
其次,构造边:
edges_list = []
for _, row in group.iterrows():
if row['parent_id_encoded'] != row['id_encoded']:
target_nodes = row['id_encoded']
source_nodes = row['parent_id_encoded']
edge_index = (source_nodes,target_nodes)
edges_list.append(edge_index)
- title_embedding列表示节点特征,是用bert编码的768维度向量。
完整的图数据构建代码如下:
import torch
from torch_geometric.data import Data, InMemoryDataset
from tqdm import tqdm
from sklearn.preprocessing import LabelEncoder
import numpy as np
import pandas as pd
import argparse
from config import (LOCAL_EMBEDDINGS_OUTPUT_PATH,LOCAL_GRAPH_OUTPUT_PATH)
class LocalCommentBinaryDataset(InMemoryDataset):
def __init__(self, root, result_df, transform=None, pre_transform=None):
self.result_df = result_df
super(LocalCommentBinaryDataset, self).__init__(root, transform, pre_transform) # transform就是数据增强,对每一个数据都执行
self.data, self.slices = torch.load(self.processed_paths[0])
@property #python装饰器, 只读属性,方法可以像属性一样访问
def raw_file_names(self): #①检查self.raw_dir目录下是否存在raw_file_names()属性方法返回的每个文件
#②如有文件不存在,则调用download()方法执行原始文件下载
return []
@property
def processed_file_names(self): #③检查self.processed_dir目录下是否存在self.processed_file_names属性方法返回的所有文件,有则直接加载
#④没有就会走process,得到'yoochoose_click_binary_1M_sess.dataset'文件
return ['local_comment_reply_binary_submission.dataset']
def download(self):#①检查self.raw_dir目录下是否存在raw_file_names()属性方法返回的每个文件
#②如有文件不存在,则调用download()方法执行原始文件下载
pass
def process(self):
data_list = []
# process by submission_id
grouped = result_df.groupby('submission_id')
for submission_id, group in tqdm(grouped):
combined = pd.concat([group['id'], group['parent_id']])
le = LabelEncoder()
le.fit(combined)
group = group.reset_index(drop=True)
group['id_encoded'] = le.transform(group['id'])
group['parent_id_encoded'] = le.transform(group['parent_id'])# 将id映射为连续的数值
# 构建边
edges_list = []
for _, row in group.iterrows():
if row['parent_id_encoded'] != row['id_encoded']:
target_nodes = row['id_encoded']
source_nodes = row['parent_id_encoded']
edge_index = (source_nodes,target_nodes)
edges_list.append(edge_index)
# 转换为PyTorch张量
if edges_list:
edge_index = torch.tensor(edges_list, dtype=torch.long).t().contiguous()
else:
edge_index = torch.tensor([], dtype=torch.long).t().contiguous()
# 构建节点特征张量
node_features = group.loc[group.submission_id==submission_id, ['title_embeddings','id_encoded']].sort_values('id_encoded')['title_embeddings'].values
node_features = np.array(node_features.tolist())
node_features = torch.FloatTensor(node_features).unsqueeze(1)
x = node_features
# 构建标签
for _, row in group.iterrows():
if row['parent_id_encoded'] == row['id_encoded']:
y = torch.FloatTensor([row['2_way_label']])
# 创建图数据对象
data = Data(x=x, edge_index=edge_index, y=y)
data_list.append(data)
data, slices = self.collate(data_list)#转换成可以保存到本地的格式
torch.save((data, slices), self.processed_paths[0])
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Create local graph data from comments.")
parser.add_argument('--root', type=str, default=LOCAL_GRAPH_OUTPUT_PATH, help='Root directory for dataset storage.')
parser.add_argument('--result_df_path', type=str, default=LOCAL_EMBEDDINGS_OUTPUT_PATH, help='Path to the result_df CSV file.')
args = parser.parse_args()
result_df = pd.read_pickle(args.result_df_path)
dataset = LocalCommentBinaryDataset(root=args.root, result_df=result_df)
四、数据集查看
数据构造好了,查看reddit和twitter数据里的结构:
1、twitter
from torch_geometric.datasets import UPFD
# dataset = UPFD()
dataset=UPFD(root=".", name="gossipcop", feature="bert")
print(f"Dataset:{dataset}:")
print("***********************")
print(f"Number of Graphs:{len(dataset)}") # 有多少个图?
print(f"Number of features:{dataset.num_features}") # 每个点的特征维度是多少?
print(f"Number of classes:{dataset.num_classes}") # 有多少类?
Dataset:UPFD(1092, name=gossipcop, feature=bert):
Number of Graphs:1092
Number of features:768
Number of classes:2
print(dataset[1].x.shape)
print(dataset[2].x.shape)
torch.Size([125, 768])
torch.Size([6, 768])
print(f"y:{dataset.y}")
print(f"y:{dataset[2].y.shape}")
y:tensor([0, 1, 1, …, 1, 0, 0])
y:torch.Size([1])
# 打印 y 的类型
print(f"Type of y: {type(dataset.y)}")
# 打印 y 的形状
print(f"Shape of y: {dataset.y.shape}")
# 打印第2个图的 y 值及其形状
print(f"y[2]: {dataset[2].y}")
print(f"Shape of y[2]: {dataset[2].y.shape}")
Type of y: <class ‘torch.Tensor’>
Shape of y: torch.Size([1092])
y[2]: tensor([1])
Shape of y[2]: torch.Size([1])
import torch
def is_directed(edge_index):
# 将 edge_index 视为无向图,反转所有边
reversed_edge_index = torch.flip(edge_index, [0])
# 检查是否所有边都存在对应的反向边
for i in range(edge_index.size(1)):
if not ((reversed_edge_index[:, i:i+1] == edge_index).all(dim=0).any()):
return True # 如果存在没有对应反向边的边,则图是有向图
return False # 否则图是无向图
# 示例:检查第2个图是否为有向图
edge_index = dataset[2].edge_index
if is_directed(edge_index):
print("Graph 2 is directed.")
else:
print("Graph 2 is undirected.")
Graph 2 is directed.
# 查看图数据
import torch
import pandas as pd
from local_graph_create import LocalCommentBinaryDataset # 后加
root='Z:/chenyiping/Jupyter/comment/fakeddit/dataset/processed/data(local)'
result_df_path='Z:/chenyiping/Jupyter/comment/fakeddit/dataset/processed/comment_df_embedding_18063.pkl'
result_df = pd.read_pickle(result_df_path)
# 修改
dataset = LocalCommentBinaryDataset(root=root, result_df=result_df)
print(f"Dataset:{dataset}:")
print("***********************")
print(f"Number of Graphs:{len(dataset)}") # 有多少个点?
print(f"Number of features:{dataset.num_features}") # 每个点的特征维度是多少?
print(f"Number of classes:{dataset.num_classes}") # 有多少类?
print(dataset[1].x.shape)
print(dataset[2].x.shape)
# 打印 y 的类型
print(f"Type of y: {type(dataset.y)}")
# 打印 y 的形状
print(f"Shape of y: {dataset.y.shape}")
# 打印第2个图的 y 值及其形状
print(f"y[2]: {dataset[2].y}")
print(f"Shape of y[2]: {dataset[2].y.shape}")
# import torch
def is_directed(edge_index):
# 将 edge_index 视为无向图,反转所有边
reversed_edge_index = torch.flip(edge_index, [0])
# 检查是否所有边都存在对应的反向边
for i in range(edge_index.size(1)):
if not ((reversed_edge_index[:, i:i+1] == edge_index).all(dim=0).any()):
return True # 如果存在没有对应反向边的边,则图是有向图
return False # 否则图是无向图
# 示例:检查第2个图是否为有向图
edge_index = dataset[2].edge_index
if is_directed(edge_index):
print("Graph 2 is directed.")
else:
print("Graph 2 is undirected.")
Dataset:LocalCommentBinaryDataset(18063):
Number of Graphs:18063
Number of features:768
Number of classes:2
torch.Size([55, 768])
torch.Size([70, 768])
Type of y: <class ‘torch.Tensor’>
Shape of y: torch.Size([18063])
y[2]: tensor([1.])
Shape of y[2]: torch.Size([1])
Graph 2 is directed.