Graph Attention Network Layerを実装する Part2

piqcy
programming-soda
Published in
7 min readNov 9, 2018

Graph Attention Networkを使って、実際のデータセットに対する精度を観測してみます。Part1では既存の実装をバッチに対応できるようにする一方、テスト結果から肝心のAttentionがうまく機能していない可能性に直面しました。今回は、テストのために作成したデータではなく、実際のデータセットを使用してAttentionの有効性を確認してみます。

データセットは、Graph Convolutionの実装で使用されているものを使います。

こちらには、cora/citeseer/pubmedといういずれも論文の参照関係を表すデータセットが格納されています。これらはGraph Attention Networkの元実装でも使用されています。データセットは、全部で8ファイルから構成されます。

  • x, y: ノードの特徴と、ノードのラベルを納めたファイル
  • tx, ty: テストセットのノードの特徴と、ラベルを納めたファイル
  • allx, ally: x, yの母集団
  • graph: グラフの接続関係
  • test.index: グラフにおける、テストノードのインデックス

実験項目は以下の通りです。

  1. Graph Attention Networkオリジナル実装の精度を確認する
  2. Graph Attention Networkオリジナル実装をAttentionなしに修正し精度を確認する
  3. バッチ対応済みの実装について、Attentionありで精度を確認する
  4. バッチ対応済みの実装について、Attentionなしで精度を確認する

結果は以下のようになりました。

Testデータに対する精度
実験の結果(gan_experiment: batch対応、gan_experiment_o: original。_naはAttentionなし)

実装はこちらになります。

公式の精度は83%程度ですが、実験スクリプトによれば10000epoch待たないといけないので概ね精度の上限に近くなる120epochで打ち切っています(最近発表されたGraph系のNeural Netの精度を検証した”Pitfalls of Graph Neural Network Evaluation”で報告されているGraph Attentionの精度は81.8なので、妥当な値が出ていると思います)。

  1. 学習データに対する精度は、Attentionがない方が高い傾向がある(epoch_acc/epoch_weighted_acc)
  2. Attentionがない場合、validation lossは後半フラットか上がり気味になる(epoch_val_loss)
  3. 評価データに対する精度は、Attentionなしの方が早く向上するが、最終的にはAttentionありの方が若干高い(epoch_val_acc/epoch_val_weighted_acc)

以上3点から推察するに、Attentionは正則化に効いている印象です。ただAttentionとは正則化のために入れるものだっただろうか・・・という疑問は残ります(Attentionというよりノイズになっているのでは?)。効果は一応見られたためこのまま進んでもいいのですが、テスト用のタスクが解けていないこともあるため、一旦真面目にAttentionを当てる方法をPart3で考えてみようと思います。

バッチ対応Layerでの学習

バッチ対応前と対応後では、想定するデータの形式が異なります。バッチ対応前の元実装では、学習データの1 instanceが1 nodeに対応します(詳細は”Graph Convolutionを自然言語処理に応用する Part2”をご参照ください)。そのため、ノードがN個のグラフならバッチサイズはNとなります。一方、バッチ対応した場合1 instanceは1 グラフを持つことになります。つまり、1 instanceに全ノードが含まれるということです。

この差異は、テストデータの切り分けを行う際に問題となります。今回のデータセットでは、グラフの一部が学習データ、一部がテストデータという問題設定になっています。この場合、バッチ対応前はうまく対応できますがバッチ対応後は対応が難しくなります。

バッチ対応前とバッチ対応後の差異

上図は緑の括弧が学習用データ、オレンジの括弧がテスト用データを表しています。バッチ対応前はノード=データの単位であったため、上図の左のように学習用とテスト用の区分けが可能です。テストデータも含めたN個のノード(=データ)を渡しても、テストデータ分については無視することが可能です(sample_weightを0にします)。

しかし、バッチ対応後は1データごとにグラフを持ちます。バッチ対応前と同じことを行うなら、1データ内の特定次元を無視するような話になります。sample_weightはバッチ内のデータに重みをつける機能であり、バッチ内のデータのさらに特定次元に重みをつけるようなことはできません。

そのため「学習データとして使用可能なノードについてだけ」予測を行い、学習するようにしました。入力が調整できないので、出力を調整しようということです。これにより、バッチ対応前と等価な学習が可能になります。

バッチ対応後のLayerの学習

しかし、実装上の制約により完全に等価にはできません。「出力を調整する」と述べた通り、予測するノードの数は使用可能なノード数によって変わります。これは、分類問題で言えば時々で予測クラス数が変わるようなものです。こうした計算グラフの構築はできません(動的フレームワークなら行けるかもしれませんが・・・)。

そのため予測するノード数を固定にしました。使用可能なノードの中から固定数をランダムにサンプリングし、それらについて予測を行うという形にしています。

実装上の工夫

バッチ対応後は、全データがグラフを持ちます。この場合、ただでさえ大きいグラフがバッチサイズ分だけ量産されることになります。試せばわかりますが、えらくメモリを消費してエラーが飛んだりします。そのため、generatorを使用し一度に1データずつ学習するようにしています。

実験の実行に際しては、実験ごとに実行ファイルを分けています(gan_experiment.py/gan_experiment_without_attention.pyなど)。これにより、コマンドライン引数とも設定ファイルとも無縁になります。本来は実験記述ファイルのようなものを作成し、それを実験実行スクリプトで実行するという形式が良いと思うのですが(AllenNLPはこのスタイルと思います)、それなら設定ファイルを普通のPythonファイルにしてもファイル数は変わらないので、現在の方式をとっています。

--

--

piqcy
programming-soda

All change is not growth, as all movement is not forward. Ellen Glasgow