相對tensorflow(1.0), pytorch確實要更容易使用。由于課題和圖神經網絡相關,最近也在學習使用一些圖深度建模的工具,比如tensorflow的Deep Graph Library以及pytorch的 pytorch geometric. 因為還在學習tensorflow 2.0,目前以pytorch geometric為主
我在網上找到一篇介紹這個的博客,寫得很不錯:?https://towardsdatascience.com/hands-on-graph-neural-networks-with-pytorch-pytorch-geometric-359487e221a8
我嘗試自己總結一些關鍵點和困擾一段時間的問題, 并且寫了一個簡單的example, 在我之前出錯的一些點上有注釋,用于參考和復習:https://github.com/GQ93/Pytorch-geometric-notes
圖數據結構
圖說起來也很簡單,就是兩個核心點,一個是圖節點(nodes/vertics),一個是邊(edges/links)表示節點之間的連接關系,基本概念可以參考wiki的界面https://en.wikipedia.org/wiki/Graph_theory
總體而言,圖可以是不規整的(irregular),對比而言,平時我們看到的圖片都是規整的(regular),可以表示成矩陣或者向量。問題在于設計數據結構如何儲存圖,一般有兩種方案
一. 矩陣表示
參考:?https://en.wikipedia.org/wiki/Graph_(abstract_data_type)#Representations
又細分成兩種
(1) 用鄰接矩陣(adjacency matrix:https://en.wikipedia.org/wiki/Adjacency_matrix),度矩陣(degree matrix:https://en.wikipedia.org/wiki/Degree_matrix), 拉普拉斯矩陣(Laplacian matrix:https://en.wikipedia.org/wiki/Laplacian_matrix)去表示, 衍生出各種對拉普拉斯矩陣的操作,比如圖傅里葉變換(graph fourier transform), 也有稀疏鄰接矩陣
(2) 用關聯矩陣(incidence matrix:https://en.wikipedia.org/wiki/Incidence_matrix)表示,行表示節點,列表示邊, 和這個相關的例如:超圖(hypergraph)
這(兩)種方式的缺點在于使用內存大, 矩陣維度和節點數目N掛鉤。但是圖的連接常常是稀疏的(sparse),也就是鄰接矩陣中很多元素都是0(兩個node沒有連接關系),這些0元素會占據大量存儲空間,使效率很低下。尤其是大型網絡圖,都不會把圖完整的表示成一個矩陣。PS:吐槽一下,學數學的倒是特別喜歡
二. 鄰接表
鄰接表(Adjacency list:https://en.wikipedia.org/wiki/Adjacency_list),也是數據領域常用的存儲圖方式,比如將邊表示成節點對,成為一個2*N_edges的matrix,第一行表示source node, 第二行表示target node。這樣的好處在于可以只儲存有邊存在的,對稀疏結構友好??傮w而言,如果圖是dense的,可以考慮矩陣表示,如果是稀疏的,最好使用稀疏鄰接矩陣或鄰接表
Pytorch Geometric?
一. torch_geometric.data.Data
pytorch Geometric Data使用鄰接表去表示圖,同時也表示了node特征x, 邊屬性edge_attr等, 需要注意的是, Data只表示一張圖(single graph)
Data作為一個數據結構,需要填充幾個屬性
Data(x=None,?edge_index=None,?edge_attr=None,?y=None)
x: 表示節點特征,可選,shape:?[num_nodes,?num_node_features] 有的圖只有結構沒有節點特征
edge_index: 表示邊,也就是鄰接表, shape: [2, num_edges]?
注意,因為能表示有向圖, 對于無向圖,一條邊要存入兩次,也就是位于節點1和節點2的邊,需要寫成[[1,2][2,1]]而不能只寫入[[1],[2]]; node的編號和edge要對應,也就是 max_num_edges = num_nodes*num_nodes 而不是num_nodes*num_nodes /2
edge_attr: 表示邊屬性(e.g. , 權重,類型),shape: [num_edges, num_edge_features]?
y: 是label,官方文檔中說? Graph or node targets with arbitrary shape,所以shape可以是[num_nodes, nodes_label_dimension],或者是[graph_label_dimesnion]
二. 構建Dataset
pytorch geometric 構建數據集分兩種, 一種繼承InMemoryDataset,一次性加載所有數據到內存;另一種繼承Dataset, 分次加載到內存
A. 繼承InMemoryDataset
import torch
from torch_geometric.data import InMemoryDataset
class MyOwnDataset(InMemoryDataset):
? ? def __init__(self, root, transform=None, pre_transform=None):
? ? ? ? super(MyOwnDataset, self).__init__(root, transform, pre_transform)
? ? ? ? self.data, self.slices = torch.load(self.processed_paths[0])
? ? @property
? ? def raw_file_names(self):
? ? ? ? return ['some_file_1', 'some_file_2', ...]
? ? @property
? ? def processed_file_names(self):
? ? ? ? return ['data.pt']
? ? def download(self):
? ? ? ? # Download to `self.raw_dir`.
? ? def process(self):
? ? ? ? # Read data into huge `Data` list.
? ? ? ? data_list = [...]
? ? ? ? if self.pre_filter is not None:
? ? ? ? ? ? data_list = [data for data in data_list if self.pre_filter(data)]
? ? ? ? if self.pre_transform is not None:
? ? ? ? ? ? data_list = [self.pre_transform(data) for data in data_list]
? ? ? ? data, slices = self.collate(data_list)
? ? ? ? torch.save((data, slices), self.processed_paths[0])
幾個關鍵點容易被忽視
1. 如果需要在initial里面初始化一些參數,如定義mask,需要在super前繼承參數,不然會失敗無法傳遞到子函數里面,原因我也不清楚。 舉例:
self.num_train_per_class 要卸載super(NodeDatasetInMem, self)這一行前面
2. 我們主要需要編輯def processed_file_names(self) 和?def process(self),?
processed_file_names只需要申明把處理好的dataset存在哪里(路徑加文件名)
process就是寫一個函數,處理數據成torch_geometric.data.Data的形式,如果是圖分類,還需要把多個圖存成一個list
要注意x一般是float tensor, y 是 long tensor, mask 是boolean tensor, edge_index是long tensor
而且當y是graph label時, 不能是0-dimension tensor, 也就是說
y = torch.tensor(0, dtype=torch.long)
會報錯, 要寫成
y = torch.tensor([0], dtype=torch.long)?
3. 其余函數作用
data, slices = self.collate(data_list)
torch.save((data, slices), self.processed_paths[0])
這個是官方代碼里面的,作用就是通過self.collate把數據劃分成不同slices去保存讀取 (大數據塊切成小塊)
所以即使只有一個graph寫成了data, 在調用self.collate時,也要寫成list:
data, slices = self.collate([data])
B.?繼承Dataset
直接繼承torch_geometric.data.Dataset,除了和InMemoryDataset相似的函數以外,需要多寫兩個函數
torch_geometric.data.Dataset.len():
因為Dataset相對于InMemoryDataset,不會一次加載所有函數,而是分批,所有會把數據保存成好幾個小數據包(.pt 文件),len() 就是說明有幾個數據包,官方的指導:
def len(self):
? ? ? ? return len(self.processed_file_names)
可以完全照搬,只需要改變processed_file_names的返回值,例如
還有一個get() 函數
torch_geometric.data.Dataset.get():
這個函數需要返回值時一個data,single graph:?Implements the logic to load a single graph
def get(self, idx):
? ? ? ? data = torch.load(osp.join(self.processed_dir, 'data_{}.pt'.format(idx)))
? ? ? ? return data
注意, 這里的load里面的函數名要和processed_file_name()返回的函數名一致, idx就是數據包的遍歷下標
幾個容易出問題的地方
1. 繼承InMemoryDataset時,在super繼承之后,有一個讀取數據的命令
由于繼承Dataset, 有get函數load數據,所以寫繼承Dataset時不需要這條命令,否則會報錯
2. 不再調用self.collate() 去劃分數據包, 也就沒有data_list. 直接把一個個小數據包按照下標儲存就好
以后看情況補足raw_file_names()和download()相關,不過本地數據可以不需要填充這兩個函數