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