天天看點

【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

在學習前可以簡單了解前幾天剛結束的KDD CUP和OGB共同舉辦第一屆的OGB-LSC(OGB Large-Scale Challenge)圖神經網絡比賽(KDDCUP是ACM SIGKDD組織的資料挖掘領域最影響力的頂級賽事),該比賽就是提供真實世界的超大規模圖資料,完成圖學習領域的節點分類、邊預測和圖回歸三大任務,百度在2個賽道上獲得冠軍,強的一批:

  • 大規模節點分類賽道冠軍:引入基于異構關系的統一消息傳遞模型
  • 大規模圖關系預測賽道冠軍:提出 20 層的 NOTE-RPS 知識圖譜嵌入模型
  • 【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

學習心得

(1)建立超大規模資料集的步驟

(2)上個task6學習了基于GIN的圖表示學習神經網絡,這次是将GIN和超大規模資料集類的建立結合起來,建構一種很友善的設定不同參數進行試驗的方法,不同試驗的過程與結果資訊通過簡單的操作即可進行比較分析。

(3)第二部分的圖預測任務自己渣渣電腦難跑,發現很多人也是(各種原因:顯存不夠等),另外注意colab是沒辦法永久安裝庫的,12小時就會斷一次——可以寫到谷歌網盤,然後重新運作,再從網盤裡讀取blabla(小夥伴說的)。

文章目錄

  • ​​學習心得​​
  • ​​第一部分:超大規模資料集類的建立​​
  • ​​一、Dataset基類簡介​​
  • ​​1.跳過download/process​​
  • ​​2.無需定義Dataset類​​
  • ​​二、圖樣本封裝成批(BATCHING)與DataLoader類​​
  • ​​1.合并小圖組成大圖​​
  • ​​2.小圖的屬性增值與拼接​​
  • ​​(1)圖的比對(Pairs of Graphs)​​
  • ​​(2)二部圖(Bipartite Graphs)​​
  • ​​(3)在新的次元上做拼接​​
  • ​​三、建立超大規模資料集類實踐​​
  • ​​第二部分:圖預測任務實踐​​
  • ​​gin_conv.py檔案:​​
  • ​​gin_graph.py檔案:​​
  • ​​main.py檔案:​​
  • ​​pcqm4m_data.py檔案:​​
  • ​​通過試驗尋找最佳超參數​​
  • ​​使用TensorBoard​​
  • ​​train​​
  • ​​valid​​
  • ​​作業​​
  • ​​Reference​​

第一部分:超大規模資料集類的建立

  • 在前面的學習中我們隻接觸了資料可全部儲存于記憶體的資料集,這些資料集對應的資料集類在建立對象時就将所有資料都加載到記憶體。
  • 有時資料集規模超級大,我們很難有足夠大的記憶體完全存下所有資料。

    是以需要一個按需加載樣本到記憶體的資料集類。在第一部分是學習為一個包含上千萬個圖樣本的資料集建構一個資料集類。

一、Dataset基類簡介

在PyG中,我們通過繼承​​torch_geometric.data.Dataset​​​基類來自定義一個按需加載樣本到記憶體的資料集類。此基類與Torchvision的​

​Dataset​

​​類的概念密切相關,這與第6節中介紹的​

​torch_geometric.data.InMemoryDataset​

​​基類是一樣的(可以複習​​【GNN】task4-資料完整存儲與記憶體的資料集類+節點預測與邊預測任務實踐​​)。

繼承​

​torch_geometric.data.InMemoryDataset​

​基類要實作的方法,繼承此基類同樣要實作,此外還需要實作以下方法:

  • ​len()​

    ​:傳回資料集中的樣本的數量。
  • ​get()​

    ​​:實作加載單個圖的操作。注意:在内部,​

    ​__getitem__()​

    ​​傳回通過調用​

    ​get()​

    ​​來擷取​

    ​Data​

    ​​對象,并根據​

    ​transform​

    ​參數對它們進行選擇性轉換。

下面讓我們通過一個簡化的例子看繼承​

​torch_geometric.data.Dataset​

​基類的規範:

import os.path as osp

import torch
from torch_geometric.data import Dataset, download_url

class MyOwnDataset(Dataset):
    def __init__(self, root, transform=None, pre_transform=None):
        super(MyOwnDataset, self).__init__(root, transform, pre_transform)

    @property
    def raw_file_names(self):
        return ['some_file_1', 'some_file_2', ...]

    @property
    def processed_file_names(self):
        return ['data_1.pt', 'data_2.pt', ...]

    def download(self):
        # Download to `self.raw_dir`.
        path = download_url(url, self.raw_dir)
        ...

    def process(self):
        i = 0
        for raw_path in self.raw_paths:
            # Read data from `raw_path`.
            data = Data(...)

            if self.pre_filter is not None and not self.pre_filter(data):
                continue

            if self.pre_transform is not None:
                data = self.pre_transform(data)

            torch.save(data, osp.join(self.processed_dir, 'data_{}.pt'.format(i)))
            i += 1

    def len(self): # 傳回資料集中的樣本數量
        return len(self.processed_file_names)

    def get(self, idx): # 實作加載單個圖的操作
        data = torch.load(osp.join(self.processed_dir, 'data_{}.pt'.format(idx)))
        return      

其中,每個​

​Data​

​​對象在​

​process()​

​​方法中單獨被儲存,并在​

​get()​

​中通過指定索引進行加載。

1.跳過download/process

對于無需下載下傳資料集原檔案的情況,我們不重寫(override)​

​download​

​​方法即可跳過下載下傳。對于無需對資料集做預處理的情況,我們不重寫​

​process​

​方法即可跳過預處理。

2.無需定義Dataset類

通過下面的方式,我們可以不用定義一個​

​Dataset​

​​類,而直接生成一個​

​Dataloader​

​對象,直接用于訓練:

from torch_geometric.data import Data, DataLoader

data_list = [Data(...), ..., Data(...)]
loader = DataLoader(data_list, batch_size=32)      

我們也可以通過下面的方式将一個清單的​

​Data​

​​對象組成一個​

​batch​

​:

from torch_geometric.data import Data, Batch

data_list = [Data(...), ..., Data(...)]
loader = Batch.from_data_list(data_list, batch_size=32)      

二、圖樣本封裝成批(BATCHING)與DataLoader類

内容來源:​​ADVANCED MINI-BATCHING​​

1.合并小圖組成大圖

圖可以有任意數量的節點和邊,它不是規整的資料結構,是以對圖資料封裝成批的操作與對圖像和序列等資料封裝成批的操作不同。PyTorch Geometric中采用的将多個圖封裝成批的方式是,将小圖作為連通元件(connected component)的形式合并,建構一個大圖。于是小圖的鄰接矩陣存儲在大圖鄰接矩陣的對角線上。大圖的鄰接矩陣、屬性矩陣、預測目标矩陣分别為:

【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

此方法有以下關鍵的優勢:

  • 依靠消息傳遞方案的GNN運算不需要被修改,因為消息仍然不能在屬于不同圖的兩個節點之間交換。
  • 沒有額外的計算或記憶體的開銷。例如,這個批處理程式的工作完全不需要對節點或邊緣特征進行任何填充。請注意,鄰接矩陣沒有額外的記憶體開銷,因為它們是以稀疏的方式儲存的,隻保留非零項,即邊。

通過​​torch_geometric.data.DataLoader​​​類,多個小圖被封裝成一個大圖。​​torch_geometric.data.DataLoader​​​是PyTorch的​

​DataLoader​

​​的子類,它覆寫了​

​collate()​

​​函數,該函數定義了一清單的樣本是如何封裝成批的。是以,所有可以傳遞給PyTorch ​

​DataLoader​

​​的參數也可以傳遞給PyTorch Geometric的 ​

​DataLoader​

​​,例如,​

​num_workers​

​。

2.小圖的屬性增值與拼接

(1)将小圖存儲到大圖中時需要對小圖的屬性做一些修改,一個最顯著的例子就是要對節點序号增值。在最一般的形式中,PyTorch Geometric的​

​DataLoader​

​​類會自動對​

​edge_index​

​張量增值,增加的值為目前被處理圖的前面的圖的累積節點數量。

(2)比方說,現在對第個圖的​​

​edge_index​

​​張量做增值,前面個圖的累積節點數量為,那麼對第個圖的​​

​edge_index​

​​張量的增值。增值後,對所有圖的​​

​edge_index​

​​張量(其形狀為​

​[2, num_edges]​

​)在第二維中連接配接起來。

(3)然而,有一些特殊的場景中(如下所述),基于需求我們希望能修改這一行為。PyTorch Geometric允許我們通過覆寫​​torch_geometric.data.__inc__()​​​和​​torch_geometric.data.__cat_dim__()​​​函數來實作我們希望的行為。在未做修改的情況下,它們在​​Data​​類中的定義如下。

def __inc__(self, key, value):
    if 'index' in key or 'face' in key:
        return self.num_nodes
    else:
        return 0

def __cat_dim__(self, key, value):
    if 'index' in key or 'face' in key:
        return 1
    else:
        return 0      

我們可以看到,​

​__inc__()​

​​定義了兩個連續的圖的屬性之間的增量大小,

而​​

​__cat_dim__()​

​定義了同一屬性的圖形張量應該在哪個次元上被連接配接起來。

PyTorch Geometric為存儲在​​Data​​​類中的每個屬性調用此二函數,并以它們各自的​

​key​

​​和值​

​value​

​作為參數。

在下面的内容中,我們将學習一些對​

​__inc__()​

​​和​

​__cat_dim__()​

​的修改可能是絕對必要的案例。

(1)圖的比對(Pairs of Graphs)

如果你想在一個​​Data​​​對象中存儲多個圖,例如用于圖比對等應用,我們需要確定所有這些圖的正确封裝成批行為。例如,考慮将兩個圖,一個源圖和一個目标圖,存儲在一個​​​Data​​類中,即

class PairData(Data):
    def __init__(self, edge_index_s, x_s, edge_index_t, x_t):
        super(PairData, self).__init__()
        self.edge_index_s = edge_index_s
        self.x_s = x_s
        self.edge_index_t = edge_index_t
        self.x_t =      

在這種情況中,​

​edge_index_s​

​​應該根據源圖的節點數做增值,即​​

​x_s.size(0)​

​​,而​

​edge_index_t​

​​應該根據目标圖的節點數做增值,即​​

​x_t.size(0)​

​。

class PairData(Data):
    def __init__(self, edge_index_s, x_s, edge_index_t, x_t):
        super(PairData, self).__init__()
        self.edge_index_s = edge_index_s
        self.x_s = x_s
        self.edge_index_t = edge_index_t
        self.x_t = x_t

    def __inc__(self, key, value):
        if key == 'edge_index_s':
            return self.x_s.size(0)
        if key == 'edge_index_t':
            return self.x_t.size(0)
        else:
            return super().__inc__(key, value)      

我們可以通過設定一個簡單的測試腳本來測試我們的PairData批處理行為。

edge_index_s = torch.tensor([
    [0, 0, 0, 0],
    [1, 2, 3, 4],
])
x_s = torch.randn(5, 16)  # 5 nodes.
edge_index_t = torch.tensor([
    [0, 0, 0],
    [1, 2, 3],
])
x_t = torch.randn(4, 16)  # 4 nodes.

data = PairData(edge_index_s, x_s, edge_index_t, x_t)
data_list = [data, data]
loader = DataLoader(data_list, batch_size=2)
batch = next(iter(loader))

print(batch)
# Batch(edge_index_s=[2, 8], x_s=[10, 16], edge_index_t=[2, 6], x_t=[8, 16])

print(batch.edge_index_s)
# tensor([[0, 0, 0, 0, 5, 5, 5, 5], [1, 2, 3, 4, 6, 7, 8, 9]])

print(batch.edge_index_t)
# tensor([[0, 0, 0, 4, 4, 4], [1, 2, 3, 5, 6, 7]])      

​edge_index_s​

​​和​

​edge_index_t​

​​被正确地封裝成批了,即使在為和含有不同數量的節點時也是如此。然而,由于PyTorch Geometric無法識别​​

​PairData​

​​對象中實際的圖,是以​

​batch​

​​屬性(将大圖每個節點映射到其各自對應的小圖)沒有正确工作。此時就需要​​DataLoader​​​的​

​follow_batch​

​參數發揮作用。在這裡,我們可以指定我們要為哪些屬性維護批資訊。

loader = DataLoader(data_list, batch_size=2, follow_batch=['x_s', 'x_t'])
batch = next(iter(loader))

print(batch)
# Batch(edge_index_s=[2, 8], x_s=[10, 16], x_s_batch=[10],
#          edge_index_t=[2, 6], x_t=[8, 16], x_t_batch=[8])
print(batch.x_s_batch)
# tensor([0, 0, 0, 0, 0, 1, 1, 1, 1, 1])

print(batch.x_t_batch)
# tensor([0, 0, 0, 0, 1, 1, 1, 1])      

可以看到,​

​follow_batch=['x_s', 'x_t']​

​​現在成功地為節點特征​

​x_s​

​​和​

​x_t​

​​分别建立了名為​

​x_s_batch​

​​和​

​x_t_batch​

​​的指派向量。這些資訊現在可以用來在一個單一的​

​Batch​

​對象中對多個圖進行聚合操作,例如,全局池化。

(2)二部圖(Bipartite Graphs)
【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

二部圖的鄰接矩陣定義兩種類型的節點之間的連接配接關系。一般來說,不同類型的節點數量不需要一緻,于是二部圖的鄰接矩陣可能為平方矩陣,即可能有。對二部圖的封裝成批過程中,​​

​edge_index​

​​ 中邊的源節點與目标節點做的增值操作應是不同的。我們将二部圖中兩類節點的特征特征張量分别存儲為​

​x_s​

​​和​

​x_t​

​。

class BipartiteData(Data):
    def __init__(self, edge_index, x_s, x_t):
        super(BipartiteData, self).__init__()
        self.edge_index = edge_index
        self.x_s = x_s
        self.x_t =      

為了對二部圖實作正确的封裝成批,我們需要告訴PyTorch Geometric,它應該在​

​edge_index​

​中獨立地為邊的源節點和目标節點做增值操作。

class BipartiteData(Data):
    def __init__(self, edge_index, x_s, x_t):
        super(BipartiteData, self).__init__()
        self.edge_index = edge_index
        self.x_s = x_s
        self.x_t = x_t
        
    def __inc__(self, key, value):
        if key == 'edge_index':
            return torch.tensor([[self.x_s.size(0)], [self.x_t.size(0)]])
        else:
            return super().__inc__(key, value)      

其中,​

​edge_index[0]​

​​(邊的源節點)根據​

​x_s.size(0)​

​​做增值運算,而​

​edge_index[1]​

​​(邊的目标節點)根據​

​x_t.size(0)​

​做增值運算。我們可以再次通過運作一個簡單的測試腳本來測試我們的實作。

edge_index = torch.tensor([
    [0, 0, 1, 1],
    [0, 1, 1, 2],
])
x_s = torch.randn(2, 16)  # 2 nodes.
x_t = torch.randn(3, 16)  # 3 nodes.

data = BipartiteData(edge_index, x_s, x_t)
data_list = [data, data]
loader = DataLoader(data_list, batch_size=2)
batch = next(iter(loader))

print(batch)
# Batch(edge_index=[2, 8], x_s=[4, 16], x_t=[6, 16])

print(batch.edge_index)
# tensor([[0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 1, 2, 3, 4, 4, 5]])      

可以看到我們得到我們期望的結果。

(3)在新的次元上做拼接

有時,​

​Data​

​​對象的屬性需要在一個新的次元上做拼接(如經典的封裝成批),例如,圖級别屬性或預測目标。具體來說,形狀為​

​[num_features]​

​​的屬性清單應該被傳回為​

​[num_examples, num_features]​

​​,而不是​

​[num_examples * num_features]​

​​。PyTorch Geometric通過在​​__cat_dim__()​​​中傳回一個​​None​​的連接配接次元來實作這一點。

class MyData(Data):
     def __cat_dim__(self, key, item):
         if key == 'foo':
             return None
         else:
             return super().__cat_dim__(key, item)

edge_index = torch.tensor([
   [0, 1, 1, 2],
   [1, 0, 2, 1],
])
foo = torch.randn(16)

data = MyData(edge_index=edge_index, foo=foo)
data_list = [data, data]
loader = DataLoader(data_list, batch_size=2)
batch = next(iter(loader))

print(batch)
# Batch(edge_index=[2, 8], foo=[2, 16])      

正如我們期望的,​

​batch.foo​

​現在由兩個次元來表示,一個批次元,一個特征次元。

三、建立超大規模資料集類實踐

​​PCQM4M-LSC​​是一個分子圖的量子特性回歸資料集,它包含了3,803,453個圖,具體點選剛才的連結。

【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

注意以下代碼依賴于​

​ogb​

​​包,通過​

​pip install ogb​

​​指令可安裝此包。​

​ogb​

​​文檔可見于​​Get Started | Open Graph Benchmark (stanford.edu)​​。

我們定義的資料集類如下:

import os
import os.path as osp

import pandas as pd
import torch
from ogb.utils import smiles2graph
from ogb.utils.torch_util import replace_numpy_with_torchtensor
from ogb.utils.url import download_url, extract_zip
from rdkit import RDLogger
from torch_geometric.data import Data, Dataset
import shutil

RDLogger.DisableLog('rdApp.*')

class MyPCQM4MDataset(Dataset):

    def __init__(self, root):
        self.url = 'https://dgl-data.s3-accelerate.amazonaws.com/dataset/OGB-LSC/pcqm4m_kddcup2021.zip'
        super(MyPCQM4MDataset, self).__init__(root)

        filepath = osp.join(root, 'raw/data.csv.gz')
        data_df = pd.read_csv(filepath)
        self.smiles_list = data_df['smiles']
        self.homolumogap_list = data_df['homolumogap']

    @property
    def raw_file_names(self):
        return 'data.csv.gz'

    def download(self):
        path = download_url(self.url, self.root)
        extract_zip(path, self.root)
        os.unlink(path)
        shutil.move(osp.join(self.root, 'pcqm4m_kddcup2021/raw/data.csv.gz'), osp.join(self.root, 'raw/data.csv.gz'))

    def len(self):
        return len(self.smiles_list)

    def get(self, idx):
        smiles, homolumogap = self.smiles_list[idx], self.homolumogap_list[idx]
        graph = smiles2graph(smiles)
        assert(len(graph['edge_feat']) == graph['edge_index'].shape[1])
        assert(len(graph['node_feat']) == graph['num_nodes'])

        x = torch.from_numpy(graph['node_fea   t']).to(torch.int64)
        edge_index = torch.from_numpy(graph['edge_index']).to(torch.int64)
        edge_attr = torch.from_numpy(graph['edge_feat']).to(torch.int64)
        y = torch.Tensor([homolumogap])
        num_nodes = int(graph['num_nodes'])
        data = Data(x, edge_index, edge_attr, y, num_nodes=num_nodes)
        return data

    # 擷取資料集劃分
    def get_idx_split(self):
        split_dict = replace_numpy_with_torchtensor(torch.load(osp.join(self.root, 'pcqm4m_kddcup2021/split_dict.pt')))
        return split_dict

if __name__ == "__main__":
    dataset = MyPCQM4MDataset('dataset2')
    from torch_geometric.data import DataLoader
    from tqdm import tqdm
    dataloader = DataLoader(dataset, batch_size=256, shuffle=True, num_workers=4)
    for batch in tqdm(dataloader):
        pass      

(1)在生成一個該資料集類的對象時,程式首先會檢查指定的檔案夾下是否存在​

​data.csv.gz​

​​檔案,如果不在,則會執行​

​download​

​​方法,這一過程是在運作​

​super​

​​類的​

​__init__​

​方法中發生的。

(2)然後程式繼續執行​

​__init__​

​​方法的剩餘部分,讀取​

​data.csv.gz​

​​檔案,擷取存儲圖資訊的​

​smiles​

​​格式的字元串,以及回歸預測的目标​

​homolumogap​

​。

我們将由​

​smiles​

​​格式的字元串轉成圖的過程在​

​get()​

​​方法中實作,這樣我們在生成一個​

​DataLoader​

​​變量時,通過指定​

​num_workers​

​可以實作并行執行生成多個圖。

第二部分:圖預測任務實踐

将基于GIN的圖表示學習神經網絡(​​【GNN】task4-資料完整存儲與記憶體的資料集類+節點預測與邊預測任務實踐​​),和上面我們自己定義的資料集來實作分子圖的量子性質預測任務。

​codes\gin_regression​

​檔案夾

gin_conv.py檔案:

gin_conv卷積層子產品。

import torch
from torch import nn
from torch_geometric.nn import MessagePassing
import torch.nn.functional as F
from ogb.graphproppred.mol_encoder import BondEncoder


### GIN convolution along the graph structure
class GINConv(MessagePassing):
    def __init__(self, emb_dim):
        '''
            emb_dim (int): node embedding dimensionality
        '''

        super(GINConv, self).__init__(aggr = "add")

        self.mlp = nn.Sequential(nn.Linear(emb_dim, emb_dim), nn.BatchNorm1d(emb_dim), nn.ReLU(), nn.Linear(emb_dim, emb_dim))
        self.eps = nn.Parameter(torch.Tensor([0]))
        self.bond_encoder = BondEncoder(emb_dim = emb_dim)

    def forward(self, x, edge_index, edge_attr):
        edge_embedding = self.bond_encoder(edge_attr) # 先将類别型邊屬性轉換為邊嵌入
        out = self.mlp((1 + self.eps) *x + self.propagate(edge_index, x=x, edge_attr=edge_embedding))
        return out

    def message(self, x_j, edge_attr):
        return F.relu(x_j + edge_attr)
        
    def update(self, aggr_out):
        return      

gin_graph.py檔案:

這個是task6裡面提到的子產品首先采用GINNodeEmbedding子產品對圖上每一個節點做嵌入,然後對節點嵌入做池化得到圖的嵌入,最後用一層線性變換得到圖的最終的表示(graph representation)。

import torch
from torch import nn
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool, GlobalAttention, Set2Set
from gin_node import GINNodeEmbedding


class GINGraphPooling(nn.Module):

    def __init__(self, num_tasks=1, num_layers=5, emb_dim=300, residual=False, drop_ratio=0, JK="last", graph_pooling="sum"):
        """GIN Graph Pooling Module

        此子產品首先采用GINNodeEmbedding子產品對圖上每一個節點做嵌入,然後對節點嵌入做池化得到圖的嵌入,最後用一層線性變換得到圖的最終的表示(graph representation)。

        Args:
            num_tasks (int, optional): number of labels to be predicted. Defaults to 1 (控制了圖表示的次元,dimension of graph representation).
            num_layers (int, optional): number of GINConv layers. Defaults to 5.
            emb_dim (int, optional): dimension of node embedding. Defaults to 300.
            residual (bool, optional): adding residual connection or not. Defaults to False.
            drop_ratio (float, optional): dropout rate. Defaults to 0.
            JK (str, optional): 可選的值為"last"和"sum"。選"last",隻取最後一層的結點的嵌入,選"sum"對各層的結點的嵌入求和。Defaults to "last".
            graph_pooling (str, optional): pooling method of node embedding. 可選的值為"sum","mean","max","attention"和"set2set"。 Defaults to "sum".

        Out:
            graph representation
        """
        super(GINGraphPooling, self).__init__()

        self.num_layers = num_layers
        self.drop_ratio = drop_ratio
        self.JK = JK
        self.emb_dim = emb_dim
        self.num_tasks = num_tasks

        if self.num_layers < 2:
            raise ValueError("Number of GNN layers must be greater than 1.")

        self.gnn_node = GINNodeEmbedding(num_layers, emb_dim, JK=JK, drop_ratio=drop_ratio, residual=residual)

        # Pooling function to generate whole-graph embeddings
        if graph_pooling == "sum":
            self.pool = global_add_pool
        elif graph_pooling == "mean":
            self.pool = global_mean_pool
        elif graph_pooling == "max":
            self.pool = global_max_pool
        elif graph_pooling == "attention":
            self.pool = GlobalAttention(gate_nn=nn.Sequential(
                nn.Linear(emb_dim, emb_dim), nn.BatchNorm1d(emb_dim), nn.ReLU(), nn.Linear(emb_dim, 1)))
        elif graph_pooling == "set2set":
            self.pool = Set2Set(emb_dim, processing_steps=2)
        else:
            raise ValueError("Invalid graph pooling type.")

        if graph_pooling == "set2set":
            self.graph_pred_linear = nn.Linear(2*self.emb_dim, self.num_tasks)
        else:
            self.graph_pred_linear = nn.Linear(self.emb_dim, self.num_tasks)

    def forward(self, batched_data):
        h_node = self.gnn_node(batched_data)

        h_graph = self.pool(h_node, batched_data.batch)
        output = self.graph_pred_linear(h_graph)

        if self.training:
            return output
        else:
            # At inference time, relu is applied to output to ensure positivity
            return torch.clamp(output, min=0, max=50)      

main.py檔案:

import os
import torch
import argparse
from tqdm import tqdm
from ogb.lsc import PCQM4MEvaluator
from torch_geometric.data import DataLoader
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR

from pcqm4m_data import MyPCQM4MDataset
from gin_graph import GINGraphPooling

from torch.utils.tensorboard import SummaryWriter

def parse_args():

    parser = argparse.ArgumentParser(description='Graph data miming with GNN')
    parser.add_argument('--task_name', type=str, default='GINGraphPooling',
                        help='task name')
    parser.add_argument('--device', type=int, default=0,
                        help='which gpu to use if any (default: 0)')
    parser.add_argument('--num_layers', type=int, default=5,
                        help='number of GNN message passing layers (default: 5)')
    parser.add_argument('--graph_pooling', type=str, default='sum',
                        help='graph pooling strategy mean or sum (default: sum)')
    parser.add_argument('--emb_dim', type=int, default=256,
                        help='dimensionality of hidden units in GNNs (default: 256)')
    parser.add_argument('--drop_ratio', type=float, default=0.,
                        help='dropout ratio (default: 0.)')
    parser.add_argument('--save_test', action='store_true')
    parser.add_argument('--batch_size', type=int, default=512,
                        help='input batch size for training (default: 512)')
    parser.add_argument('--epochs', type=int, default=100,
                        help='number of epochs to train (default: 100)')
    parser.add_argument('--weight_decay', type=float, default=0.00001,
                        help='weight decay')
    parser.add_argument('--early_stop', type=int, default=10,
                        help='early stop (default: 10)')
    parser.add_argument('--num_workers', type=int, default=4,
                        help='number of workers (default: 4)')
    parser.add_argument('--dataset_root', type=str, default="dataset",
                        help='dataset root')
    args = parser.parse_args()

    return args


def prepartion(args):
    save_dir = os.path.join('saves', args.task_name)
    if os.path.exists(save_dir):
        for idx in range(1000):
            if not os.path.exists(save_dir + '=' + str(idx)):
                save_dir = save_dir + '=' + str(idx)
                break

    args.save_dir = save_dir
    os.makedirs(args.save_dir, exist_ok=True)
    args.device = torch.device("cuda:" + str(args.device)) if torch.cuda.is_available() else torch.device("cpu")
    args.output_file = open(os.path.join(args.save_dir, 'output'), 'a')
    print(args, file=args.output_file, flush=True)


def train(model, device, loader, optimizer, criterion_fn):
    model.train()
    loss_accum = 0

    for step, batch in enumerate(tqdm(loader)):
        batch = batch.to(device)
        pred = model(batch).view(-1,)
        optimizer.zero_grad()
        loss = criterion_fn(pred, batch.y)
        loss.backward()
        optimizer.step()
        loss_accum += loss.detach().cpu().item()

    return loss_accum / (step + 1)


def eval(model, device, loader, evaluator):
    model.eval()
    y_true = []
    y_pred = []

    with torch.no_grad():
        for _, batch in enumerate(tqdm(loader)):
            batch = batch.to(device)
            pred = model(batch).view(-1,)
            y_true.append(batch.y.view(pred.shape).detach().cpu())
            y_pred.append(pred.detach().cpu())

    y_true = torch.cat(y_true, dim=0)
    y_pred = torch.cat(y_pred, dim=0)
    input_dict = {"y_true": y_true, "y_pred": y_pred}
    return evaluator.eval(input_dict)["mae"]


def test(model, device, loader):
    model.eval()
    y_pred = []

    with torch.no_grad():
        for _, batch in enumerate(loader):
            batch = batch.to(device)
            pred = model(batch).view(-1,)
            y_pred.append(pred.detach().cpu())

    y_pred = torch.cat(y_pred, dim=0)
    return y_pred


def main(args):
    prepartion(args)
    nn_params = {
        'num_layers': args.num_layers,
        'emb_dim': args.emb_dim,
        'drop_ratio': args.drop_ratio,
        'graph_pooling': args.graph_pooling
    }

    # automatic dataloading and splitting
    dataset = MyPCQM4MDataset(root=args.dataset_root)
    split_idx = dataset.get_idx_split()
    train_data = dataset[split_idx['train']]
    valid_data = dataset[split_idx['valid']]
    test_data = dataset[split_idx['test']]
    train_loader = DataLoader(train_data, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers)
    valid_loader = DataLoader(valid_data, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers)
    test_loader = DataLoader(test_data, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers)

    # automatic evaluator. takes dataset name as input
    evaluator = PCQM4MEvaluator()
    criterion_fn = torch.nn.MSELoss()

    device = args.device

    model = GINGraphPooling(**nn_params).to(device)

    num_params = sum(p.numel() for p in model.parameters())
    print(f'#Params: {num_params}', file=args.output_file, flush=True)
    print(model, file=args.output_file, flush=True)

    optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=args.weight_decay)
    scheduler = StepLR(optimizer, step_size=30, gamma=0.25)

    writer = SummaryWriter(log_dir=args.save_dir)
    not_improved = 0
    best_valid_mae = 9999
    for epoch in range(1, args.epochs + 1):
        print("=====Epoch {}".format(epoch), file=args.output_file, flush=True)
        print('Training...', file=args.output_file, flush=True)
        train_mae = train(model, device, train_loader, optimizer, criterion_fn)

        print('Evaluating...', file=args.output_file, flush=True)
        valid_mae = eval(model, device, valid_loader, evaluator)

        print({'Train': train_mae, 'Validation': valid_mae}, file=args.output_file, flush=True)

        writer.add_scalar('valid/mae', valid_mae, epoch)
        writer.add_scalar('train/mae', train_mae, epoch)

        if valid_mae < best_valid_mae:
            best_valid_mae = valid_mae
            if args.save_test:
                print('Saving checkpoint...', file=args.output_file, flush=True)
                checkpoint = {
                    'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(),
                    'scheduler_state_dict': scheduler.state_dict(), 'best_val_mae': best_valid_mae, 'num_params': num_params
                }
                torch.save(checkpoint, os.path.join(args.save_dir, 'checkpoint.pt'))
                print('Predicting on test data...', file=args.output_file, flush=True)
                y_pred = test(model, device, test_loader)
                print('Saving test submission file...', file=args.output_file, flush=True)
                evaluator.save_test_submission({'y_pred': y_pred}, args.save_dir)

            not_improved = 0
        else:
            not_improved += 1
            if not_improved == args.early_stop:
                print(f"Have not improved for {not_improved} epoches.", file=args.output_file, flush=True)
                break

        scheduler.step()
        print(f'Best validation MAE so far: {best_valid_mae}', file=args.output_file, flush=True)

    writer.close()
    args.output_file.close()


if __name__ == "__main__":
    args = parse_args()
    main(args)      

pcqm4m_data.py檔案:

import os
import os.path as osp

import pandas as pd
import torch
from ogb.utils import smiles2graph
from ogb.utils.torch_util import replace_numpy_with_torchtensor
from ogb.utils.url import download_url, extract_zip
from rdkit import RDLogger
from torch_geometric.data import Data, Dataset
import shutil

RDLogger.DisableLog('rdApp.*')


class MyPCQM4MDataset(Dataset):

    def __init__(self, root):
        self.url = 'https://dgl-data.s3-accelerate.amazonaws.com/dataset/OGB-LSC/pcqm4m_kddcup2021.zip'
        super(MyPCQM4MDataset, self).__init__(root)

        filepath = osp.join(root, 'raw/data.csv.gz')
        data_df = pd.read_csv(filepath)
        self.smiles_list = data_df['smiles']
        self.homolumogap_list = data_df['homolumogap']

    @property
    def raw_file_names(self):
        return 'data.csv.gz'

    def download(self):
        path = download_url(self.url, self.root)
        extract_zip(path, self.root)
        os.unlink(path)
        shutil.move(osp.join(self.root, 'pcqm4m_kddcup2021/raw/data.csv.gz'), osp.join(self.root, 'raw/data.csv.gz'))

    def len(self):
        return len(self.smiles_list)

    def get(self, idx):
        smiles, homolumogap = self.smiles_list[idx], self.homolumogap_list[idx]
        graph = smiles2graph(smiles)
        assert(len(graph['edge_feat']) == graph['edge_index'].shape[1])
        assert(len(graph['node_feat']) == graph['num_nodes'])

        x = torch.from_numpy(graph['node_feat']).to(torch.int64)
        edge_index = torch.from_numpy(graph['edge_index']).to(torch.int64)
        edge_attr = torch.from_numpy(graph['edge_feat']).to(torch.int64)
        y = torch.Tensor([homolumogap])
        num_nodes = int(graph['num_nodes'])
        data = Data(x, edge_index, edge_attr, y, num_nodes=num_nodes)
        return data

    def get_idx_split(self):
        split_dict = replace_numpy_with_torchtensor(torch.load(osp.join(self.root, 'pcqm4m_kddcup2021/split_dict.pt')))
        return split_dict


if __name__ == "__main__":
    dataset = MyPCQM4MDataset('dataset')
    from torch_geometric.data import DataLoader
    from tqdm import tqdm
    dataloader = DataLoader(dataset, batch_size=256, shuffle=True, num_workers=4)
    for batch in tqdm(dataloader):
        pass      

通過試驗尋找最佳超參數

通過運作以下的指令即可運作一次試驗:

#!/bin/sh

python main.py  --task_name GINGraphPooling\    # 為目前試驗取名
                --device 0\                     
                --num_layers 5\                 # 使用GINConv層數
                --graph_pooling sum\            # 圖讀出方法
                --emb_dim 256\                  # 節點嵌入次元
                --drop_ratio 0.\
                --save_test\                    # 是否對測試集做預測并保留預測結果
                --batch_size 512\
                --epochs 100\
                --weight_decay 0.00001\
                --early_stop 10\                # 當有`early_stop`個epoches驗證集結果沒有提升,則停止訓練
                --num_workers 4\
                --dataset_root dataset          # 存放資料集的根目錄      

試驗運作開始後,程式會在​

​saves​

​​目錄下建立一個​

​task_name​

​​參數指定名稱的檔案夾用于記錄試驗過程,當​

​saves​

​​目錄下已經有一個同名的檔案夾時,程式會在​

​task_name​

​​參數末尾增加一個字尾作為檔案夾名稱。試驗運作過程中,所有的​

​print​

​​輸出都會寫入到試驗檔案夾下的​

​output​

​​檔案,​

​tensorboard.SummaryWriter​

​記錄的資訊也存儲在試驗檔案夾下的檔案中。

修改上方的指令再執行,即可試驗不同的超參數,所有試驗的過程與結果資訊都存儲于​

​saves​

​​檔案夾下。啟動​

​TensorBoard​

​​會話,選擇​

​saves​

​檔案夾,即可檢視所有試驗的過程與結果資訊。

使用TensorBoard

TensorBoard作用:可以把複雜的神經網絡訓練過程給可視化,可以更好地了解調試 優化程式。

基于Anaconda可視化tensorboard的步驟:

(1)打開anaconda prompt

(2)打開tensorflow環境,運作Spyder IDE ,執行代碼

(3)再打開anaconda prompt

(4)運作tensorflow環境

(5)運作tensorboard --logdir=路徑<必須是完全路徑>

(6)将終端的網址複制 - 在浏覽器中打開

裝置不行(其實是腦子不行),用了天國之影大佬跑了5個鐘得到的四個檔案,在​

​anaconda prompt​

​​中運作​

​tensorboard --logdir=D:\桌面檔案\GINGraphPooling\GINGraphPooling​

​後:

【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

複制其中連結到浏覽器得到得到可視化結果,我将Smoothing設定為0.723,​

​Horizontal Axis​

​設定為RELATIVE(STEP: 疊代步長;RELATIVE: 相對時間(小時,相對于起始點) ; WALL:運作時間(小時) )

train
【GNN】task7-超大規模資料集類的建立+圖預測任務實踐
valid
【GNN】task7-超大規模資料集類的建立+圖預測任務實踐

作業

Reference

  • ​Dataset​

    ​​類官方文檔:​​torch_geometric.data.Dataset​​
  • 将圖樣本封裝成批(BATCHING):​​ADVANCED MINI-BATCHING​​
  • 分子圖的量子特性回歸資料集:​​PCQM4M-LSC​​
  • ​​Get Started | Open Graph Benchmark (stanford.edu)​​
  • datawhale course:https://github.com/datawhalechina/team-learning-nlp/tree/master/GNN
  • ​​基于Anaconda的tensorboard可視化​​
  • ​​KDD CUP2021首屆圖神經網絡大賽放榜,百度飛槳PGL獲得2金1銀​​
  • 天國之影大佬跑了5個鐘得到的四個檔案

繼續閱讀