【PyG學習入門】一:入門使用

簡介

首先說一下這個東西,全名是PyTorch-Geometric,是一個PyTorch基礎上的一個庫,專門用於圖形式的數據,可以加速圖學習算法的計算過程,比如稀疏化的圖等。在學習PyG的各個大的分支之前,先看一下官方文檔給出的學習例子。參考鏈接:

https://pytorch-geometric.readthedocs.io/en/latest/notes/introduction.html

此處直接進行使用的說明,安裝過程文檔中給出了方式,個人感覺Ubuntu安裝會簡單一點,但是沒有安裝過,本人在Windows10安裝的,一些依賴庫通過VS2019編譯,至於爲什麼不用編譯好的wheel文件安裝,是因爲電腦安裝的是Python3.7,大部分編譯好的文件都是py36的,所以只能自己編譯了。提示一下,windows10編譯庫的時候過程不是很順序,出現了很多VS編譯的問題。

樣例

根據PyG的一些功能函數給出了自帶的五個例子,分別爲:
(1)圖數據處理–Data Handling of Graphs
(2)通用的數據集–Common Benchmark Datasets
(3)小批次–Mini-batches
(4)數據轉換–Data Transforms
(5)圖上的學習方法–Learning Methods on Graphs

1.Data Handling of Graphs

圖(Graph)往往用來表示節點之間成對的關係(也就是邊),一個圖在PyG中會被定義爲torch_geometric.data.Data類的一個實例,常見的類屬性如下:

  • data.x 節點的特徵矩陣,大小爲[num_nodes, num_node_features]
  • data.edge_index圖中的邊的信息,採用COO格式記錄,大小爲[2, num_edges],類型爲torch.long。COO格式也就是Coordinate Format,採用三元組進行存儲,三元組內元素分別爲行座標、列座標和元素值,此處沒有元素值,所以只有2行,num_edges列,每一列表示一個元素。
  • data.edge_attr邊的特徵矩陣,大小爲[num_edges, num_edge_features]
  • data.y訓練目標,允許任意形狀,比如節點級別的爲[num_nodes, *],圖級別的爲[1, *]
  • data.pos節點的位置矩陣,大小爲[num_node, num_dimensions]
    以上屬性都不是必須的,而且可以進行屬性拓展,上述的屬性用於二維圖中,如果對於三維網格數據,可以增加屬性data.face,大小爲[3, num_faces],同樣爲COO格式。
    舉一個簡單的例子,邊上沒有權重,無向圖(定義邊的時候需要兩對索引),如下:
    在這裏插入圖片描述
import torch
from torch_geometric.data import Data

# 節點不一定從0開始
edge_index = torch.tensor([
    [3, 1, 1, 2],
    [1, 3, 2, 1]],dtype=torch.long)
# 注意x是二維的,不是一維的,每一行代表一個節點的特徵向量,此處特徵維度爲1
x = torch.tensor([[-1],
                  [0],
                  [1]], dtype=torch.float)

data = Data(x=x, edge_index=edge_index)
print(data)

輸出爲

Data(edge_index=[2, 4], x=[3, 1])

這裏表示的都是維度。如果邊數據不是通過COO方式給出的,而是通過節點對方式給出的,需要先轉置t()再利用函數contiguous()

# 通過節點對的方式給出
edge_index = torch.tensor([
    [0, 1], [1, 0], [1, 2], [2, 1]
], dtype=torch.long)
data = Data(x=x, edge_index=edge_index.t().contiguous())
print(data)

兩次輸出data一致。除此之外,還提供了一部分實用的函數接口:

# 輸出data的屬性關鍵字,只有傳遞參數的纔會被輸出
print(data.keys)
# ['x', 'edge_index']

# 按照關鍵字進行輸出,注意是字符串
print(data['x'])
# tensor([[-1.],
#         [ 0.],
#         [ 1.]])
print(data['edge_index'])
# tensor([[0, 1, 1, 2],
#         [1, 0, 2, 1]])

print('edge_attr: ', data['edge_attr'])
# edge_attr:  None

# 遍歷所有關鍵字及其對應的數值
for key, item in data:
    print(key, '---', item)

# 可以直接檢索key,也可以檢索data內函數
if 'edge_attr' not in data.keys:
    print('Not in')
    # Not in

if 'x' in data:
    print('In')
    # In

# print(type(data.keys))
# <class 'list'>

print(data.num_nodes)
# 3

# 這裏的邊數爲4
print(data.num_edges)
# 4

print(data.num_edge_features)
# 0

print(data.num_node_features)
# 1

print(data.contains_isolated_nodes())
# False

print(data.contains_self_loops())
# False

print(data.is_undirected())
# True

上面有個地方需要注意:
在輸出keys的時候是沒有edge_attr的,但是可以直接訪問data['edge_attr']並且得到返回值爲None。於是分析一下Data類的源碼:

def __init__(self, x=None, edge_index=None, edge_attr=None, y=None,
              pos=None, norm=None, face=None, **kwargs):
     self.x = x
     self.edge_index = edge_index
     self.edge_attr = edge_attr
     self.y = y
     self.pos = pos
     self.norm = norm
     self.face = face

首先上面的代碼塊,可以看到一開始所有的屬性都被初始化參數值,而參數的默認值爲None

def __getitem__(self, key):
    r"""Gets the data of the attribute :obj:`key`."""
    return getattr(self, key, None)

通過重載上面的函數,使得類的對象變爲可迭代對象,此時,可以通過data['XXX']訪問。此時就明白了爲什麼可以通過對象訪問到edge_attr並且爲None。但是爲什麼從keys中無法獲得呢?

    @property
    def keys(self):
        r"""Returns all names of graph attributes."""
        keys = [key for key in self.__dict__.keys() if self[key] is not None]
        keys = [key for key in keys if key[:2] != '__' and key[-2:] != '__']
        return keys

此處進行一個if self[key] is not None判斷。並且需要注意的是Data類的很多函數都被@property修飾。此時,對Data類的使用方式有了一個大致瞭解,但是此時出現一個疑惑,data中的x的順序和節點大小順序是對應的麼?是不是x的第一個特徵向量就是對應最小編號節點的特徵向量呢?這個問題暫時還不能解決,等後面再說。

2.Common Benchmark Datasets

這個庫中包含了很多數據集,比如CoraCiteseerPubmed以及圖分類數據集等等(詳情見文檔)。直接對數據集進行初始化,初始化的時候就會自動下載其原始文件並轉換爲Data格式,以數據集ENZYMES 爲例,其中包含600個圖分爲6類:

from torch_geometric.datasets import TUDataset

dataset = TUDataset(root='data/', name='ENZYMES')
print(dataset)
# ENZYMES(600)

第一次下載需要一點時間,第二次運行就不會下載覆蓋了,速度比較快。進行一些測試:

print(type(dataset))
# <class 'torch_geometric.datasets.tu_dataset.TUDataset'>
print(len(dataset))
# 600
print(dataset.num_node_features)
# 3

如果下載比較慢,可以找到鏈接手動下載,鏈接位置在TUDataset中實現:

url = ('https://ls11-www.cs.tu-dortmund.de/people/morris/'
           'graphkerneldatasets')
cleaned_url = ('https://raw.githubusercontent.com/nd7141/'
                'graph_datasets/master/datasets')

然後取消掉下載過程(這裏的內容以後在單獨更新一篇文章仔細說),手動調用製作數據集的函數:

def process(self):

每一個元素都是一個Data實例:

# dataset是一個可迭代對象,並且每一個元素都是一個Data實例,但是y是一個單獨的元素,所以說這個數據集是Graph-level的
data = dataset[0]
print(data)
# Data(edge_index=[2, 168], x=[37, 3], y=[1])

數據集切分可以用切片或者tensor:

# 數據集切分
dataset_train = dataset[:500]
dataset_test = dataset[500:]
print(dataset_train, dataset_test)
# ENZYMES(500) ENZYMES(100)
dataset_sample1 = dataset[torch.tensor([i for i in range(500)], dtype=torch.long)]
print(dataset_sample1)
# ENZYMES(500)
dataset_sample2 = dataset[torch.tensor([True, False])]
print(dataset_sample2)
# ENZYMES(1)
print(dataset[0])
# Data(edge_index=[2, 168], x=[37, 3], y=[1])
print(dataset[1])
# Data(edge_index=[2, 102], x=[23, 3], y=[1])
print(dataset_sample2[0])
# Data(edge_index=[2, 168], x=[37, 3], y=[1])

布爾型tensor類似一個濾波器,但是是從頭開始的。打亂操作如下:

dataset = dataset.shuffle()
# 等價於
dataset = dataset[torch.randperm(len(dataset))]

下面的函數返回長度爲n,範圍爲0~n-1的一種全排列tensor:

torch.randperm()

官方文檔還給出了一個cora數據集的例子,特別的地方在於:

data.train_mask.sum().item()

cora數據集在Data類中額外添加了幾個屬性,比如train_mask,通過sum函數可以得到train訓練集的總數,之所以可以自己定義新的屬性,因爲:

def __init__(self, x=None, edge_index=None, edge_attr=None, y=None,
             pos=None, norm=None, face=None, **kwargs):

Data類給了一個**kwargs

3.Mini-batches

神經網絡通常會按照batch方式進行訓練,PyG通過構建稀疏化的分塊對角陣實現mini-batch的並行化,構建方式按照每一個Data實例的edge_index構建一個Graph的鄰接矩陣,然後將所有節點的特徵向量按行拼接,目標值同理。這也就使得即使一個batch內部的圖是不同結構的,也可以一起訓練。
在這裏插入圖片描述
通過DataLoader函數進行batch的構造:

from torch_geometric.datasets import TUDataset
from torch_geometric.data import DataLoader

dataset = TUDataset(root='data/', name='ENZYMES', use_node_attr=True)
loader = DataLoader(dataset, batch_size=32, shuffle=True)

for batch in loader:
    print(batch)
    # Batch(batch=[1005], edge_index=[2, 3948], x=[1005, 21], y=[32])
    

一個batch爲32個圖,但是每一個圖的規模是不一樣的,如上案例,第一個batch內的32個圖共1005節點,含有3948條邊。torch_geometric.data.Batch繼承自torch_geometric.data.Data,並且添加了一個額外的屬性batchbatch是一個列向量,代表了每一個節點對應到哪一個圖。
在這裏插入圖片描述
利用另一個庫torch-scatter可以對圖信息進行一些計算,比如:

from torch_geometric.datasets import TUDataset
from torch_geometric.data import DataLoader
from torch_scatter import scatter_mean

dataset = TUDataset(root='data/', name='ENZYMES', use_node_attr=True)
loader = DataLoader(dataset, batch_size=32, shuffle=True)

for data in loader:
    print(data)
    # Batch(batch=[1005], edge_index=[2, 3948], x=[1005, 21], y=[32])
    x = scatter_mean(data.x, data.batch, dim=0)
    print(x.size())
    # torch.Size([32, 21])

此處以每一個圖爲單位,將各個圖中的所有節點的特徵向量計算了一個平均值,所以維度爲[32, 21]

4.Data Transforms

(本模塊個人使用場景不多,暫時不展開描述)
以ShapeNet數據集爲例,進行測試,數據集有17000個3D點雲,並且每一個點的類別爲16類中的一個:

from torch_geometric.datasets import ShapeNet

dataset = ShapeNet(root='data/ShapeNet', categories=['Airplane'])
print(dataset[0])
# Data(pos=[2518, 3], y=[2518])

注意:應該在數據集存儲到磁盤之前進行pre_transform,這會使得加載速度更快,也就是在第一次下載時進行轉換,此時下一次初始化數據集的時候,就會調用轉換後的數據集(即使下一次的調用沒有指定pre_transform參數)。

5.Learning Methods on Graphs

重要到了比較重要的地方了,也就是如何構建一個模型。這裏選擇搭建一個簡單的GCN模型,並通過Cora數據集進行測試。
首先構建一個兩層的GCN:

# 繼承torch的類
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = GCNConv(dataset.num_node_features, 16)
        self.conv2 = GCNConv(16, dataset.num_classes)
        
    def forward(self, data):
        x, edge_index = data.x, data.edge_index
        
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, training=self.training)
        x = self.conv2(x, edge_index)
        
        return F.log_softmax(x, dim=1)

注意得是在GCNConv中沒有自帶非線性處理過程,訓練過程和測試過程如下:

if __name__ == '__main__':
    # 加載數據集
    dataset = Planetoid(root='data/', name='Cora')
    # Train
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = Net().to(device)
    data = dataset[0].to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)

    model.train()
    for epoch in range(200):
        optimizer.zero_grad()
        out = model(data)
        loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
        loss.backward()
        optimizer.step()

    # Test
    model.eval()
    _, pred = model(data).max(dim=1)
    correct = float(pred[data.test_mask].eq(data.y[data.test_mask]).sum().item())
    acc = correct / data.test_mask.sum().item()
    print('Accuracy: {:.4f}'.format(acc))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章