PyTorch : DGL Tutorials : DGL でバッチ処理によるグラフ分類

PyTorch : DGL Tutorials : Basics : DGL でバッチ処理によるグラフ分類 (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 06/05/2019

* 本ページは、DGL のドキュメント “Batched Graph Classification with DGL” を翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

 

DGL Tutorials : Basics : DGL でバッチ処理によるグラフ分類

グラフ分類は多くの分野に渡るアプリケーションに関連する重要な問題です – バイオインフォマティクス、ケモインフォマティクス、ソーシャルネットワーク分析、 アーバンコンピューティングそしてサイバー・セキュリティ。この問題にグラフニューラルネットワークを適用することは最近はポピュラーなアプローチです ( Ying et al., 2018, Cangea et al., 2018, Knyazev et al., 2018, Bianchi et al., 2019, Liao et al., 2019, Gao et al., 2019 )。

このチュートリアルは以下を示します :

  • 可変なサイズと shape の多重グラフを DGL でバッチ処理する
  • 単純なグラフ分類タスクのためにグラフニューラルネットワークを訓練する

 

単純なグラフ分類タスク

このチュートリアルでは、バッチ化されたグラフ分類を dgl でどのように遂行するかを以下のような 8 タイプの標準的なグラフを分類する toy サンプルを通して学習します :

DGL で合成データセット data.MiniGCDataset を実装しています。データセットは 8 つの異なるタイプのグラフを持ちそして各クラスは同じ数のグラフサンプルを持ちます。

from dgl.data import MiniGCDataset
import matplotlib.pyplot as plt
import networkx as nx
# A dataset with 80 samples, each graph is
# of size [10, 20]
dataset = MiniGCDataset(80, 10, 20)
graph, label = dataset[0]
fig, ax = plt.subplots()
nx.draw(graph.to_networkx(), ax=ax)
ax.set_title('Class: {:d}'.format(label))
plt.show()

 

グラフ・ミニバッチを構成する

ニューラルネットワークをより効率的に訓練するために、一般的な実践はミニバッチを構成するために複数のサンプルを一緒にバッチ化することです。固定された shape の tensor 入力のバッチ化は非常に容易です (例えば、サイズ 28×28 の 2 つの画像は shape 2x28x28 の shape の tensor を与えます)。対照的に、グラフ入力のバッチ化は 2 つの課題を持ちます :

  • グラフはスパースです。
  • グラフは様々な長さを持つことができます (e.g. ノードとエッジの数)。

これに対応するために、DGL は dgl.batch() API を提供します。

それはグラフのバッチが (多くの互いに素な (= disjoint) 接続された (= connected) 構成要素を持つ) 巨大なグラフとして見えるようなトリックを活用します。下は一般的なアイデアを与える可視化です :

与えられたグラフとラベルのペアのリストからミニバッチを構成するために次の collate 関数を定義します。

import dgl

def collate(samples):
    # The input `samples` is a list of pairs
    #  (graph, label).
    graphs, labels = map(list, zip(*samples))
    batched_graph = dgl.batch(graphs)
    return batched_graph, torch.tensor(labels)

dgl.batch() の戻り値の型は依然としてグラフです (tensor のバッチが依然として tensor である事実に類似しています)。これは1 つのグラフのために動作する任意のコードは直ちにグラフのバッチのために動作することを意味します。より重要なことは、DGL は総てのノードとエッジ上のメッセージを並列に処理しますので、これは効率性を大きく改良します。

 

グラフ分類器

グラフ分類は次のように進めることができます :

グラフのバッチから、最初にノードが他の「通信する」ためにメッセージパッシング/グラフ畳み込みを遂行します。メッセージパッシングの後、ノード (そしてエッジ) 属性からグラフ表現のための tensor を計算します。このステップは交互に “読み出し/収集 (= readout/aggregation)” と呼べるかもしれません。最後に、グラフ表現はグラフラベルを予測するために分類器 $g$ に供給できます。

 

グラフ畳み込み

グラフ畳み込み演算は基本的には GCN のためのそれと同じです (チュートリアル を確認してください)。唯一の違いは $h_{v}^{(l+1)} = \text{ReLU}\left(b^{(l)}+\sum_{u\in\mathcal{N}(v)}h_{u}^{(l)}W^{(l)}\right)$ を $h_{v}^{(l+1)} = \text{ReLU}\left(b^{(l)}+\frac{1}{|\mathcal{N}(v)|}\sum_{u\in\mathcal{N}(v)}h_{u}^{(l)}W^{(l)}\right)$ で置き換えることです。合計の平均による置き換えは異なる次元を持つノードのバランスを取るためで、これはこの実験のためにより良いパフォーマンスを与えます。

データセット初期化で追加される self エッジは平均を取る時に元のノード特徴 $h_{v}^{(l)}$ を含めることを可能にすることに注意してください。

import dgl.function as fn
import torch
import torch.nn as nn


# Sends a message of node feature h.
msg = fn.copy_src(src='h', out='m')

def reduce(nodes):
    """Take an average over all neighbor node features hu and use it to
    overwrite the original node feature."""
    accum = torch.mean(nodes.mailbox['m'], 1)
    return {'h': accum}

class NodeApplyModule(nn.Module):
    """Update the node feature hv with ReLU(Whv+b)."""
    def __init__(self, in_feats, out_feats, activation):
        super(NodeApplyModule, self).__init__()
        self.linear = nn.Linear(in_feats, out_feats)
        self.activation = activation

    def forward(self, node):
        h = self.linear(node.data['h'])
        h = self.activation(h)
        return {'h' : h}

class GCN(nn.Module):
    def __init__(self, in_feats, out_feats, activation):
        super(GCN, self).__init__()
        self.apply_mod = NodeApplyModule(in_feats, out_feats, activation)

    def forward(self, g, feature):
        # Initialize the node features with h.
        g.ndata['h'] = feature
        g.update_all(msg, reduce)
        g.apply_nodes(func=self.apply_mod)
        return g.ndata.pop('h')

 

読み出しと分類

このデモのためには、初期ノード特徴をそれらの次数として考えます。グラフ畳み込みの 2 ラウンド後、バッチの各グラフのための総てのノード特徴に渡り平均することでグラフ読み出しを遂行します

\[
h_g=\frac{1}{|\mathcal{V}|}\sum_{v\in\mathcal{V}}h_{v}
\]

DGL では、dgl.mean_nodes() は可変サイズを持つグラフのバッチのためにこのタスクを処理します。それからグラフ表現を pre-softmax ロジットを得るための一つの線形層を持つ分類器に供給します。

import torch.nn.functional as F


class Classifier(nn.Module):
    def __init__(self, in_dim, hidden_dim, n_classes):
        super(Classifier, self).__init__()

        self.layers = nn.ModuleList([
            GCN(in_dim, hidden_dim, F.relu),
            GCN(hidden_dim, hidden_dim, F.relu)])
        self.classify = nn.Linear(hidden_dim, n_classes)

    def forward(self, g):
        # For undirected graphs, in_degree is the same as
        # out_degree.
        h = g.in_degrees().view(-1, 1).float()
        for conv in self.layers:
            h = conv(g, h)
        g.ndata['h'] = h
        hg = dgl.mean_nodes(g, 'h')
        return self.classify(hg)

 

セットアップと訓練

10 ~ 20 ノードを持つ 400 グラフの合成データセットを作成します。320 グラフが訓練セットを構成して 80 グラフがテストセットを構成します。

import torch.optim as optim
from torch.utils.data import DataLoader

# Create training and test sets.
trainset = MiniGCDataset(320, 10, 20)
testset = MiniGCDataset(80, 10, 20)
# Use PyTorch's DataLoader and the collate function
# defined before.
data_loader = DataLoader(trainset, batch_size=32, shuffle=True,
                         collate_fn=collate)

# Create model
model = Classifier(1, 256, trainset.num_classes)
loss_func = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
model.train()

epoch_losses = []
for epoch in range(80):
    epoch_loss = 0
    for iter, (bg, label) in enumerate(data_loader):
        prediction = model(bg)
        loss = loss_func(prediction, label)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        epoch_loss += loss.detach().item()
    epoch_loss /= (iter + 1)
    print('Epoch {}, loss {:.4f}'.format(epoch, epoch_loss))
    epoch_losses.append(epoch_loss)
Epoch 0, loss 2.0423
Epoch 1, loss 1.9262
Epoch 2, loss 1.8058
Epoch 3, loss 1.6983
Epoch 4, loss 1.6578
Epoch 5, loss 1.5529
Epoch 6, loss 1.4504
Epoch 7, loss 1.3944
Epoch 8, loss 1.3498
Epoch 9, loss 1.2459
Epoch 10, loss 1.1866
Epoch 11, loss 1.1480
Epoch 12, loss 1.1036
Epoch 13, loss 1.0618
Epoch 14, loss 1.0073
Epoch 15, loss 0.9866
Epoch 16, loss 0.9458
Epoch 17, loss 0.9212
Epoch 18, loss 0.9016
Epoch 19, loss 0.9332
Epoch 20, loss 0.9407
Epoch 21, loss 0.8906
Epoch 22, loss 0.8912
Epoch 23, loss 0.8871
Epoch 24, loss 0.8506
Epoch 25, loss 0.7894
Epoch 26, loss 0.7771
Epoch 27, loss 0.7739
Epoch 28, loss 0.7436
Epoch 29, loss 0.7396
Epoch 30, loss 0.7247
Epoch 31, loss 0.7445
Epoch 32, loss 0.7228
Epoch 33, loss 0.6901
Epoch 34, loss 0.7029
Epoch 35, loss 0.6796
Epoch 36, loss 0.6604
Epoch 37, loss 0.6768
Epoch 38, loss 0.6481
Epoch 39, loss 0.6435
Epoch 40, loss 0.6395
Epoch 41, loss 0.6331
Epoch 42, loss 0.6150
Epoch 43, loss 0.5971
Epoch 44, loss 0.6024
Epoch 45, loss 0.6005
Epoch 46, loss 0.5821
Epoch 47, loss 0.5664
Epoch 48, loss 0.5413
Epoch 49, loss 0.5503
Epoch 50, loss 0.5683
Epoch 51, loss 0.5291
Epoch 52, loss 0.5358
Epoch 53, loss 0.5408
Epoch 54, loss 0.5223
Epoch 55, loss 0.5349
Epoch 56, loss 0.5374
Epoch 57, loss 0.5476
Epoch 58, loss 0.4951
Epoch 59, loss 0.5221
Epoch 60, loss 0.4692
Epoch 61, loss 0.4747
Epoch 62, loss 0.4983
Epoch 63, loss 0.4905
Epoch 64, loss 0.4574
Epoch 65, loss 0.4511
Epoch 66, loss 0.4436
Epoch 67, loss 0.4510
Epoch 68, loss 0.4581
Epoch 69, loss 0.4426
Epoch 70, loss 0.4660
Epoch 71, loss 0.4609
Epoch 72, loss 0.4232
Epoch 73, loss 0.4029
Epoch 74, loss 0.4167
Epoch 75, loss 0.4189
Epoch 76, loss 0.4173
Epoch 77, loss 0.4318
Epoch 78, loss 0.4055
Epoch 79, loss 0.4239

 
実行の学習カーブは次のように表わされます :

plt.title('cross entropy averaged over minibatches')
plt.plot(epoch_losses)
plt.show()

訓練されたモデルは作成されたテストセット上で評価されます。チュートリアルの配備のために、私達は実行時間制限していることに注意してください、そして以下に出力表示されたものよりも高い精度 (80 % ~ 90 %) を得るかもしれません。

model.eval()
# Convert a list of tuples to two lists
test_X, test_Y = map(list, zip(*testset))
test_bg = dgl.batch(test_X)
test_Y = torch.tensor(test_Y).float().view(-1, 1)
probs_Y = torch.softmax(model(test_bg), 1)
sampled_Y = torch.multinomial(probs_Y, 1)
argmax_Y = torch.max(probs_Y, 1)[1].view(-1, 1)
print('Accuracy of sampled predictions on the test set: {:.4f}%'.format(
    (test_Y == sampled_Y.float()).sum().item() / len(test_Y) * 100))
print('Accuracy of argmax predictions on the test set: {:4f}%'.format(
    (test_Y == argmax_Y.float()).sum().item() / len(test_Y) * 100))
Accuracy of sampled predictions on the test set: 68.7500%
Accuracy of argmax predictions on the test set: 83.750000%

下はアニメーションでそこでは訓練モデルが正解ラベルに割りあてる確率とともにグラフをプロットしています :

訓練モデルが学習したノード/グラフ表現を理解するために、次元削減と可視化のために t-SNE を使用します。

上の 2 つの小さい図はグラフ畳み込みの 1, 2 層後のノード表現を可視化していて下の図はグラフ表現としてのグラフのための pre-softmax ロジットを可視化しています。

可視化がノード特徴の何某かのクラスタリング効果を提示する一方で、ノード次数が私達のノード特徴のために決定論的ですので完全な結果であることは期待されません。その一方、グラフ特徴は遥かにより良く分離されています。

 

以上