ホーム » 「DGL 0.5」タグがついた投稿

タグアーカイブ: DGL 0.5

DGL 0.5 ユーザガイド : 6 章 巨大グラフ上の確率的訓練

DGL 0.5ユーザガイド : 6 章 巨大グラフ上の確率的訓練 (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 6 章 巨大グラフ上の確率的訓練

例えば、数百万あるいは数十億ものノードやエッジを持つ大規模なグラフを持つ場合、5 章 グラフ・ニューラルネットワークを訓練する で説明されたような full-グラフ訓練は通常は動作しません。$N$-ノードグラフ上で動作する隠れ状態サイズ $H$ を持つ $L$-層グラフ畳込みネットワークを考えます。中間隠れ状態をストアするには \(O(NLH)\) メモリを必要とし、large $N$ を持つ一つの GPU のキャパシティを容易に越えます。

このセクションは確率的ミニバッチ訓練を遂行する方法を提供します、そこでは総てのノードの特徴を GPU に適合させなくてもかまいません。

 

近傍サンプリング・アプローチの概要

近傍サンプリング法は一般に以下のように動作します。各勾配効果ステップについて、($L$-th 層でそれらの最終的な表現が計算される) ノードのミニバッチを選択します。それから $L-1$ 層のそれらの近傍の総てか幾つかを取ります。このプロセスは入力に達するまで続けられます。この反復的プロセスは、下の図が示すように、出力から始まり入力へと後方に動作する依存性グラフをビルドします :

これにより、巨大なグラフ上で GNN を訓練するための作業負荷と計算リソースを節約できます。

DGL は近傍サンプリングで GNN を訓練するための 2, 3 の近傍サンプラーとパイプライン、そしてサンプリング・ストラテジーをカスタマイズする方法を提供します。

 

以上






DGL 0.5 ユーザガイド : 5 章 訓練 : 5.4 グラフ分類

DGL 0.5ユーザガイド : 5 章 訓練 : 5.4 グラフ分類 (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 5 章 訓練 : 5.4 グラフ分類

大きい単一グラフの代わりに、時に複数のグラフの形式のデータを持つかもしれません、例えば人々のコミュニティの異なる型のリストです。同じコミュニティの人々の間の友好関係をグラフで特徴付けることにより、分類するグラフのリストを得ます。このシナリオでは、グラフ分類モデルはコミュニティの型を識別する手助けができるでしょう、i.e. 構造と全体的な情報に基づいて各グラフを分類します。

 

概要

グラフ分類とノード分類やリンク予測間の主要な相違は予測結果が入力グラフ全体の特性を特徴付けることです。ちょうど前のタスクのようにノード/エッジに渡るメッセージパッシングを遂行しますが、グラフレベル表現を取得しようともします。

グラフ分類は次のように進みます :

グラフ分類プロセス

左から右へ、一般的な実践は :

  • グラフをグラフのバッチに準備する。
  • ノード/エッジ特徴を更新するためにバッチ化されたグラフ上でメッセージ・パッシングする
  • ノード/エッジ特徴をグラフレベル表現に集約する
  • タスクの方に進む分類

 

グラフのバッチ

通常はグラフ分類タスクは多くのグラフを訓練し、そしてモデルを訓練するとき一度に一つのグラフだけを使用する場合それは非常に非効率です。一般的な深層学習実践からミニバッチ訓練のアイデアを拝借して、複数のグラフのバッチをビルドしてそれらをまとめて一つの訓練反復のために送ることができます。

DGL では、グラフのリストの単一のバッチ化されたグラフをビルドできます。このバッチ化グラフは単純に単一の巨大なグラフとして利用できます、この際に個々のコンポーネントは対応する元の小さいグラフを表しています。


バッチ化されたグラフ

 

グラフ読み出し

データの総てのグラフはそのノードとエッジ特徴に加えて、独自の構造を持つかもしれません。単一の予測を行なうために、通常は可能な限り豊富な情報に渡り集約して要約します。このタイプの演算は Readout (読み出し) と命名されます。一般的な集約は総てのノードやエッジ特徴に渡る summation, average, maximum や minimum を含みます。

グラフ $g$ が与えられたとき、平均的な readout 集約を次のように定義できます :

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

DGL では対応する関数呼び出しは dgl.readout_nodes() です。

ひとたび \(h_g\) が利用可能であれば、分類出力のためにそれを MLP 層に渡すことができます。

 

ニューラルネットワーク・モデルを書く

モデルへの入力はノードとエッジ特徴を持つバッチ化グラフです。注意すべき一つのことはバッチ化グラフのノードとエッジ特徴はバッチ次元を持たないことです。少しの特別なケアがモデルに置かれるべきです :

 

バッチ化グラフ上の計算

次に、バッチ化グラフの計算プロパティを議論します。

最初に、バッチの異なるグラフは完全に分離しています、i.e. 2 つのグラフに接続するエッジはありません。この良いプロパティにより、総てのメッセージ・パッシング関数は依然として同じ結果を持ちます。

2 番目に、バッチ化グラフ上の readout 関数は各グラフに渡り個別に処理されます。バッチサイズが $B$ で集約される特徴が次元 $D$ を持つと仮定すると、readout 結果の shape は \((B, D)\) となります。

g1 = dgl.graph(([0, 1], [1, 0]))
g1.ndata['h'] = torch.tensor([1., 2.])
g2 = dgl.graph(([0, 1], [1, 2]))
g2.ndata['h'] = torch.tensor([1., 2., 3.])

dgl.readout_nodes(g1, 'h')
# tensor([3.])  # 1 + 2

bg = dgl.batch([g1, g2])
dgl.readout_nodes(bg, 'h')
# tensor([3., 6.])  # [1 + 2, 1 + 2 + 3]

最後に、バッチ化グラフ上の各ノード/エッジ特徴 tensor は総てのグラフからの対応する特徴 tensor を結合した形式にあります。

bg.ndata['h']
# tensor([1., 2., 1., 2., 3.])

 

モデル定義

上の計算ルールに気付けば、非常に単純なモデルを定義できます。

class Classifier(nn.Module):
    def __init__(self, in_dim, hidden_dim, n_classes):
        super(Classifier, self).__init__()
        self.conv1 = dglnn.GraphConv(in_dim, hidden_dim)
        self.conv2 = dglnn.GraphConv(hidden_dim, hidden_dim)
        self.classify = nn.Linear(hidden_dim, n_classes)

    def forward(self, g, feat):
        # Apply graph convolution and activation.
        h = F.relu(self.conv1(g, h))
        h = F.relu(self.conv2(g, h))
        with g.local_scope():
            g.ndata['h'] = h
            # Calculate graph representation by average readout.
            hg = dgl.mean_nodes(g, 'h')
            return self.classify(hg)

 

訓練ループ

データ・ローディング

ひとたびモデルが定義されれば、訓練を開始できます。グラフ分類は大きな単一のグラフの代わりに多くの関連する小さいグラフを扱いますので、洗練されたグラフサンプリング・アルゴリズムを設計する必要なく、通常はグラフの確率的ミニバッチ上で効率的に訓練できます。

4 章: グラフ・データパイプライン で紹介されたようにグラフ分類データセットを持つことを仮定します。

import dgl.data
dataset = dgl.data.GINDataset('MUTAG', False)

グラフ分類データセットの各項目はグラフとそのラベルのペアです。グラフをバッチ処理するために collate 関数をカスタマイズし、DataLoader を活用することによりデータローディング過程を高速化できます :

def collate(samples):
    graphs, labels = map(list, zip(*samples))
    batched_graph = dgl.batch(graphs)
    batched_labels = torch.tensor(labels)
    return batched_graph, batched_labels

それから DataLoader を作成することがでけいます、これはミニバッチでグラフのデータセットに渡り反復します。

from torch.utils.data import DataLoader
dataloader = DataLoader(
    dataset,
    batch_size=1024,
    collate_fn=collate,
    drop_last=False,
    shuffle=True)

 

ループ

それから訓練ループは dataloader に渡り反復してモデルを更新することを単純に伴います。

model = Classifier(10, 20, 5)
opt = torch.optim.Adam(model.parameters())
for epoch in range(20):
    for batched_graph, labels in dataloader:
        feats = batched_graph.ndata['feats']
        logits = model(batched_graph, feats)
        loss = F.cross_entropy(logits, labels)
        opt.zero_grad()
        loss.backward()
        opt.step()

DGL はグラフ分類のサンプルとして GIN を実装します。訓練ループは main.py の関数 train の内側にあります。モデル実装は、グラフ畳込み層として dgl.nn.pytorch.GINConv (MXNet と Tensorflow でも利用可能です) を使用して、バッチ正規化等のようなより多くのコンポーネントを持つ gin.py の内側にあります。

 

異質グラフ

異質グラフを持つグラフ分類は均質グラフを持つそれとは少し異なります。異質グラフ畳込みモジュールを必要とすることを除いて、readout 関数で異なる型のノードに渡り集約する必要もあります。

次は各ノード型のためのノード表現の平均を合計するサンプルを示します。

class RGCN(nn.Module):
    def __init__(self, in_feats, hid_feats, out_feats, rel_names):
        super().__init__()

        self.conv1 = dglnn.HeteroGraphConv({
            rel: dglnn.GraphConv(in_feats, hid_feats)
            for rel in rel_names}, aggregate='sum')
        self.conv2 = dglnn.HeteroGraphConv({
            rel: dglnn.GraphConv(hid_feats, out_feats)
            for rel in rel_names}, aggregate='sum')

    def forward(self, graph, inputs):
        # inputs are features of nodes
        h = self.conv1(graph, inputs)
        h = {k: F.relu(v) for k, v in h.items()}
        h = self.conv2(graph, h)
        return h

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

        self.rgcn = RGCN(in_dim, hidden_dim, hidden_dim, rel_names)
        self.classify = nn.Linear(hidden_dim, n_classes)

    def forward(self, g):
        h = g.ndata['feat']
        h = self.rgcn(g, h)
        with g.local_scope():
            g.ndata['h'] = h
            # Calculate graph representation by average readout.
            hg = 0
            for ntype in g.ntypes:
                hg = hg + dgl.mean_nodes(g, 'h', ntype=ntype)
            return self.classify(hg)

コードの残りは均質グラフのためのそれと異なりません。

# etypes is the list of edge types as strings.
model = HeteroClassifier(10, 20, 5, etypes)
opt = torch.optim.Adam(model.parameters())
for epoch in range(20):
    for batched_graph, labels in dataloader:
        logits = model(batched_graph)
        loss = F.cross_entropy(logits, labels)
        opt.zero_grad()
        loss.backward()
        opt.step()
 

以上






DGL 0.5 ユーザガイド : 5 章 訓練 : 5.3 リンク予測

DGL 0.5ユーザガイド : 5 章 訓練 : 5.3 リンク予測 (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 5 章 訓練 : 5.3 リンク予測

幾つかの他の設定では 2 つの与えられたノードの間にエッジが存在するか否かを予測することを望むかもしれません。そのようなモデルはリンク予測モデルと呼称します。

 

概要

GNN ベースのリンク予測モデルは 2 つのノード $u$ と $v$ の間の接続性の尤度を (それらの多層 GNN から計算されたノード表現) \(\boldsymbol{h}_u^{(L)}\) と \(\boldsymbol{h}_v^{(L)}\) の関数として表します。

\[
y_{u,v} = \phi(\boldsymbol{h}_u^{(L)}, \boldsymbol{h}_v^{(L)})
\]

このセクションでは \(y_{u,v}\) ノード $u$ とノード $v$ の間のスコアを参照します。

リンク予測モデルを訓練することは、エッジにより接続されるノード間のスコアをノードの任意のペアの間のスコアに対して比較することを伴います。例えば、$u$ と $v$ に接続するエッジが与えられたとき、ノード $u$ と $v$ の間のスコアがノード $u$ と (任意のノイズ分布 \(v’ \sim P_n(v)\) から) サンプリングされたノード \(v’\) の間のスコアよりも高いことを促進します (= encourage)。そのような方法はネガティブ・サンプリングと呼ばれます。

最小化されたときに上の挙動を獲得できる多くの損失関数があります。完全ではないリストは以下を含みます :

  • 交差エントロピー損失: \(\mathcal{L} = – \log \sigma (y_{u,v}) – \sum_{v_i \sim P_n(v), i=1,\dots,k}\log \left[ 1 – \sigma (y_{u,v_i})\right]\)
  • BPR 損失: \(\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} – \log \sigma (y_{u,v} – y_{u,v_i})\)
  • Margin 損失: \(\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} \max(0, M – y_{u, v} + y_{u, v_i})\), ここで \(M\) は定数ハイパーパラメータ。

暗黙的フィードバック (= implicit feedback)ノイズ-contrastive 推定 が何であるかを知っていれば、このアイデアに馴染みがあることを見出すかもしれません。

 

エッジ分類と異なるモデル実装の差異

$u$ と $v$ の間のスコアを計算するニューラルネットワーク・モデルは 上で 説明されたエッジ回帰モデルと同一です。

エッジ上のスコアを計算する dot 積を使用するサンプルがここにあります。

class DotProductPredictor(nn.Module):
    def forward(self, graph, h):
        # h contains the node representations computed from the GNN defined
        # in the node classification section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
            return graph.edata['score']

 

訓練ループ

スコア予測モデルはグラフ上で動作しますので、ネガティブ・サンプルをもう一つのグラフとして表現する必要があります。グラフはエッジとして総てのネガティブ・ノードペアを含みます。

次はネガティブ・サンプルをグラフとして表すサンプルを示します。各エッジ \((u,v)\) は $k$ ネガティブ・サンプル \((u,v_i)\) を得ます、そこでは \(v_i\) は一様分布からサンプリングされます。

def construct_negative_graph(graph, k):
    src, dst = graph.edges()

    neg_src = src.repeat_interleave(k)
    neg_dst = torch.randint(0, graph.number_of_nodes(), (len(src) * k,))
    return dgl.graph((neg_src, neg_dst), num_nodes=graph.number_of_nodes())

エッジ・スコアを予測するモデルはエッジ分類/回帰のそれと同じです。

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features):
        super().__init__()
        self.sage = SAGE(in_features, hidden_features, out_features)
        self.pred = DotProductPredictor()
    def forward(self, g, neg_g, x):
        h = self.sage(g, x)
        return self.pred(g, h), self.pred(neg_g, h)

それから訓練ループはネガティブ・グラフを繰り返し構築して損失を計算します。

def compute_loss(pos_score, neg_score):
    # Margin loss
    n_edges = pos_score.shape[0]
    return (1 - neg_score.view(n_edges, -1) + pos_score.unsqueeze(1)).clamp(min=0).mean()

node_features = graph.ndata['feat']
n_features = node_features.shape[1]
k = 5
model = Model(n_features, 100, 100)
opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    negative_graph = construct_negative_graph(graph, k)
    pos_score, neg_score = model(graph, negative_graph, node_features)
    loss = compute_loss(pos_score, neg_score)
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

訓練後、ノード表現は次を通して得られます :

node_embeddings = model.sage(graph, node_features)

ノード埋め込みを利用する複数の方法があります。サンプルは訓練ダウンストリーム分類器、あるいは適切な (= relevant) エンティティ・レコメンデーションのための最近傍探索や最大内積探索を行なうことを含みます。

 

異質グラフ

異質グラフ上のリンク予測は均質グラフ上のそれと大きくは違いません。以下は一つのエッジ型上で予測していることを仮定しますが、それを多重エッジ型に拡張することは容易です。

例えば、リンク予測のためのエッジ型のエッジのスコアを計算するために 上の HeteroDotProductPredictor を再利用できます。

class HeteroDotProductPredictor(nn.Module):
    def forward(self, graph, h, etype):
        # h contains the node representations for each node type computed from
        # the GNN defined in the previous section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
            return graph.edges[etype].data['score']

ネガティブ・サンプリングを遂行するために、(その上でリンク予測を遂行している) エッジ型のためのネガティブ・グラフを構築することができます。

def construct_negative_graph(graph, k, etype):
    utype, _, vtype = etype
    src, dst = graph.edges(etype=etype)
    neg_src = src.repeat_interleave(k)
    neg_dst = torch.randint(0, graph.number_of_nodes(vtype), (len(src) * k,))
    return dgl.heterograph(
        {etype: (neg_src, neg_dst)},
        num_nodes_dict={ntype: graph.number_of_nodes(ntype) for ntype in graph.ntypes})

モデルは異質グラフ上のエッジ分類のそれとは少し異なります、何故ならばリンク予測を遂行するところのエッジ型を指定する必要があるからです。

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features, rel_names):
        super().__init__()
        self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
        self.pred = HeteroDotProductPredictor()
    def forward(self, g, neg_g, x, etype):
        h = self.sage(g, x)
        return self.pred(g, h, etype), self.pred(neg_g, h, etype)

訓練ループは均質グラフのそれと同様です。

def compute_loss(pos_score, neg_score):
    # Margin loss
    n_edges = pos_score.shape[0]
    return (1 - neg_score.view(n_edges, -1) + pos_score.unsqueeze(1)).clamp(min=0).mean()

k = 5
model = Model(10, 20, 5, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
node_features = {'user': user_feats, 'item': item_feats}
opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    negative_graph = construct_negative_graph(hetero_graph, k, ('user', 'click', 'item'))
    pos_score, neg_score = model(hetero_graph, negative_graph, node_features, ('user', 'click', 'item'))
    loss = compute_loss(pos_score, neg_score)
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())
 

以上






DGL 0.5 ユーザガイド : 5 章 訓練 : 5.2 エッジ分類/回帰

DGL 0.5ユーザガイド : 5 章 訓練 : 5.2 エッジ分類/回帰 (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 5 章 訓練 : 5.2 エッジ分類/回帰

グラフのエッジ上の属性、あるいは 2 つの与えられたノード間にエッジが存在するか否かさえ予測することを時に望むかもしれません。そのような場合、エッジ分類/回帰モデルを持ちたいでしょう。

ここでは実演としてエッジ予測のためにランダムなグラフを生成します。

src = np.random.randint(0, 100, 500)
dst = np.random.randint(0, 100, 500)
# make it symmetric
edge_pred_graph = dgl.graph((np.concatenate([src, dst]), np.concatenate([dst, src])))
# synthetic node and edge features, as well as edge labels
edge_pred_graph.ndata['feature'] = torch.randn(100, 10)
edge_pred_graph.edata['feature'] = torch.randn(1000, 10)
edge_pred_graph.edata['label'] = torch.randn(1000)
# synthetic train-validation-test splits
edge_pred_graph.edata['train_mask'] = torch.zeros(1000, dtype=torch.bool).bernoulli(0.6)

 

概要

前のセクションで多層 GNN でノード分類をどのように行なうか学びました。任意のノードの隠れ表現を計算するために同じテクニックが適用できます。それからエッジ上の予測はそれらの付随するノードの表現から導出できます。

エッジ上の予測を計算する最も一般的なケースはそれを付随するノードの表現と、そしてオプションでエッジ自身の特徴のパラメータ化された関数として表現することです。

 

ノード分類とのモデル実装の相違

前のセクションからのモデルでノード表現を計算すると仮定すると、apply_edges() メソッドでエッジ予測を計算するもう一つのコンポーネントを書く必要があるだけです。

例えば、エッジ回帰のために各エッジのためにスコアを計算したい場合、以下のコードは各エッジ上の付随するノード表現の dot 積を計算します。

import dgl.function as fn
class DotProductPredictor(nn.Module):
    def forward(self, graph, h):
        # h contains the node representations computed from the GNN defined
        # in the node classification section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
            return graph.edata['score']

MLP で各エッジのためのベクトルを予測する予測関数を書くこともできます。そのようなベクトルは更なるダウンストリームなタスクで利用できます、e.g. カテゴリカル分布のロジットとして。

class MLPPredictor(nn.Module):
    def __init__(self, in_features, out_classes):
        super().__init__()
        self.W = nn.Linear(in_features * 2, out_classes)

    def apply_edges(self, edges):
        h_u = edges.src['h']
        h_v = edges.dst['h']
        score = self.W(torch.cat([h_u, h_v], 1))
        return {'score': score}

    def forward(self, graph, h):
        # h contains the node representations computed from the GNN defined
        # in the node classification section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(self.apply_edges)
            return graph.edata['score']

 

訓練ループ

ノード表現計算モデルとエッジ予測器モデルが与えられれば、full-グラフ訓練ループを容易に書くことができます、そこでは総てのエッジ上の予測を計算します。

次のサンプルは前のセクションの SAGE をノード表現計算モデルとしてそして DotPredictor をエッジ予測器モデルとして取ります。

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features):
        super().__init__()
        self.sage = SAGE(in_features, hidden_features, out_features)
        self.pred = DotProductPredictor()
    def forward(self, g, x):
        h = self.sage(g, x)
        return self.pred(g, h)

このサンプルでは、訓練/検証/テスト・エッジセットがエッジ上の boolean マスクで識別されることも仮定しています。このサンプルはまた early stopping とモデルセービングは含みません。

node_features = edge_pred_graph.ndata['feature']
edge_label = edge_pred_graph.edata['label']
train_mask = edge_pred_graph.edata['train_mask']
model = Model(10, 20, 5)
opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    pred = model(edge_pred_graph, node_features)
    loss = ((pred[train_mask] - edge_label[train_mask]) ** 2).mean()
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

 

異質グラフ

異質グラフ上のエッジ分類は均質グラフ上のそれと大きくは違いません。一つのエッジ型上でエッジ分類を遂行することを望む場合には、総てのノード型に対してノード表現を計算し、そしてそのエッジ型上で apply_edges により予測する必要があるだけです。

例えば、DotProductPredictor を異質グラフの一つのエッジ型上で動作させるには、apply_edges メソッドでエッジ型を指定する必要があるだけです。

class HeteroDotProductPredictor(nn.Module):
    def forward(self, graph, h, etype):
        # h contains the node representations for each edge type computed from
        # the GNN for heterogeneous graphs defined in the node classification
        # section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h   # assigns 'h' of all node types in one shot
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
            return graph.edges[etype].data['score']

同様に HeteroMLPPredictor を書くことができます。

class MLPPredictor(nn.Module):
    def __init__(self, in_features, out_classes):
        super().__init__()
        self.W = nn.Linear(in_features * 2, out_classes)

    def apply_edges(self, edges):
        h_u = edges.src['h']
        h_v = edges.dst['h']
        score = self.W(torch.cat([h_u, h_v], 1))
        return {'score': score}

    def forward(self, graph, h, etype):
        # h contains the node representations for each edge type computed from
        # the GNN for heterogeneous graphs defined in the node classification
        # section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h   # assigns 'h' of all node types in one shot
            graph.apply_edges(self.apply_edges, etype=etype)
            return graph.edges[etype].data['score']

単一エッジ型上の各エッジに対するスコアを予測する end-to-end モデルはこのようなものです :

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features, rel_names):
        super().__init__()
        self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
        self.pred = HeteroDotProductPredictor()
    def forward(self, g, x, etype):
        h = self.sage(g, x)
        return self.pred(g, h, etype)

モデルの使用はモデルにノード型と特徴の辞書を単純に供給することを伴います。

model = Model(10, 20, 5, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
label = hetero_graph.edges['click'].data['label']
train_mask = hetero_graph.edges['click'].data['train_mask']
node_features = {'user': user_feats, 'item': item_feats}

それから訓練ループは均質グラフのそれと殆ど同じように見えます。例えば、エッジ型 click 上でエッジラベルを予測することを望むのであれば、単純に以下を行なうことができます :

opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    pred = model(hetero_graph, node_features, 'click')
    loss = ((pred[train_mask] - label[train_mask]) ** 2).mean()
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

 

異質グラフ上で存在するエッジのエッジ型を予測する

時に存在するエッジがどの型に属するか予測することを望むかもしれません。

例えば、異質グラフサンプル が与えられたとき、貴方のタスクはユーザと項目を接続するエッジが与えられたとき、ユーザが項目をクリックするか好きでないか (= dislike) を予測します。

これはレーティング予測の単純化されたバージョンで、これはリコメンデーション文献では一般的です。ノード表現を得るために異質グラフ畳込みネットワークを利用できます。例えば、この目的で 前に定義された RGCN を依然として利用できます。

エッジ型を予測するために、上の HeteroDotProductPredictor を単純に最目的化できます、その結果それは予測される総てのエッジ型を「マージする」一つのエッジ型を持つもう一つのグラフを取り、総てのエッジについて各型のスコアを出力します。

ここのサンプルでは、2 つのノード型 user と item と、そして user と item からの総てのエッジ型, i.e. click と dislike を「マージする」一つの単一エッジ型を持つグラフを必要とします。これは次のシンタックスを使用して便利に作成できます :

dec_graph = hetero_graph['user', :, 'item']

これはノード型 user と item、そして中間にある総てのエッジ型, i.e. click と dislike を結合する単一エッジ型を持つ異質グラフを返します。

上のステートメントはまた元のエッジ型を dgl.ETYPE として名前付けられた特徴として返しますので、それをラベルとして利用できます。

edge_label = dec_graph.edata[dgl.ETYPE]

エッジ型予測器モジュールへの入力として上のグラフが与えられたとき、予測器モジュールを次のように書くことができます。

class HeteroMLPPredictor(nn.Module):
    def __init__(self, in_dims, n_classes):
        super().__init__()
        self.W = nn.Linear(in_dims * 2, n_classes)

    def apply_edges(self, edges):
        x = torch.cat([edges.src['h'], edges.dst['h']], 1)
        y = self.W(x)
        return {'score': y}

    def forward(self, graph, h):
        # h contains the node representations for each edge type computed from
        # the GNN for heterogeneous graphs defined in the node classification
        # section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h   # assigns 'h' of all node types in one shot
            graph.apply_edges(self.apply_edges)
            return graph.edata['score']

ノード表現モジュールとエッジ型予測器モジュールを結合したモデルは次のようなものです :

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features, rel_names):
        super().__init__()
        self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
        self.pred = HeteroMLPPredictor(out_features, len(rel_names))
    def forward(self, g, x, dec_graph):
        h = self.sage(g, x)
        return self.pred(dec_graph, h)

それから訓練ループは単純に次のようなものです :

model = Model(10, 20, 5, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
node_features = {'user': user_feats, 'item': item_feats}

opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    logits = model(hetero_graph, node_features, dec_graph)
    loss = F.cross_entropy(logits, edge_label)
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

DGL はレーティング予測のサンプルとして グラフ畳込み行列補完 (= Matrix Completion) を提供します、これは異質グラフ上で存在するエッジの型を予測することで定式化されます。モデル実装ファイル のノード表現モジュールは GCMCLayer と呼称されます。エッジ型予測器モジュールは BiDecoder と呼ばれます。それら両者はここで説明されている設定よりも複雑です。

 

以上






DGL 0.5 ユーザガイド : 5 章 訓練 : 5.1 ノード分類/回帰

DGL 0.5ユーザガイド : 5 章 訓練 : 5.1 ノード分類/回帰 (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 5 章 訓練 : 5.1 ノード分類/回帰

グラフ・ニューラルネットワークのための最もポピュラーで広く採用されているタスクの一つはノード分類で、そこでは訓練/検証/テストの各ノードには事前定義されたカテゴリーのセットから正解カテゴリーが割当てられます。ノード回帰も同様で、そこでは訓練/検証/テストセットの各ノードには正解数字 (= number) が割当てられます。

 

概要

ノードを分類するために、グラフ・ニューラルネットワークはノード自身の特徴、更にはその近傍ノードとエッジ特徴を利用して 2 章: メッセージ・パッシング で議論されたメッセージ・パッシングを遂行します。メッセージ・パッシングは近傍のより大きい範囲からの情報を組込みために複数ラウンド繰り返すことができます。

 

ニューラルネットワーク・モデルを書く

DGL はメッセージ・パッシングの 1 ラウンドを遂行できる 2, 3 の組込みグラフ畳込みモジュールを提供します。このガイドでは、dgl.nn.pytorch.SAGEConv (MXNet と TensorFlow でもまた利用可能です) を選択します、GraphSAGE のためのグラフ畳込みモジュールです。

グラフ上の深層学習モデルのために通常は多層グラフ・ニューラルネットワークを必要とします、そこではマルチラウンドのメッセージ・パッシングを行ないます。これは次のようにグラフ畳込みモジュールをスタックして成すことができます。

# Contruct a two-layer GNN model
import dgl.nn as dglnn
import torch.nn as nn
import torch.nn.functional as F
class SAGE(nn.Module):
    def __init__(self, in_feats, hid_feats, out_feats):
        super().__init__()
        self.conv1 = dglnn.SAGEConv(
            in_feats=in_feats, out_feats=hid_feats, aggregator_type='mean')
        self.conv2 = dglnn.SAGEConv(
            in_feats=hid_feats, out_feats=out_feats, aggregator_type='mean')

    def forward(self, graph, inputs):
        # inputs are features of nodes
        h = self.conv1(graph, inputs)
        h = F.relu(h)
        h = self.conv2(graph, h)
        return h

上のモデルをノード分類のためだけでなく、5.2 Edge Classification/Regression, 5.3 Link Prediction5.4 Graph Classification のような他のダウンストリーム・タスクのための隠れノード表現を得るためにも利用できます。

組込みグラフ畳込みモジュールの完全なリストについては、dgl.nn を参照してください。

DGL ニューラルネットワークがどのように動作するか、そしてメッセージ・パッシングでカスタム・ニューラルネットワーク・モジュールをどのように書くかのより詳細については、3 章: GNN モジュールを構築する のサンプルを参照してください。

 

訓練ループ

full グラフ上の訓練は上で定義されたモデルの順伝播と、予測を訓練ノード上の正解ラベルに対して比較することによる損失を計算することを単純に伴います。

このセクションは訓練ループを示すために DGL 組込みデータセット dgl.data.CiteseerGraphDataset を使用します。ノード特徴とラベルはそのグラフ・インスタンスにストアされて、訓練-検証-テスト分割もまた boolean マスクとしてグラフ上でストアされます。これは 4 章: グラフ・データパイプライン で見たものと同様です。

node_features = graph.ndata['feat']
node_labels = graph.ndata['label']
train_mask = graph.ndata['train_mask']
valid_mask = graph.ndata['val_mask']
test_mask = graph.ndata['test_mask']
n_features = node_features.shape[1]
n_labels = int(node_labels.max().item() + 1)

次は貴方のモデルを精度で評価するサンプルです。

def evaluate(model, graph, features, labels, mask):
    model.eval()
    with torch.no_grad():
        logits = model(graph, features)
        logits = logits[mask]
        labels = labels[mask]
        _, indices = torch.max(logits, dim=1)
        correct = torch.sum(indices == labels)
        return correct.item() * 1.0 / len(labels)

それから以下のように訓練ループを書くことができます。

model = SAGE(in_feats=n_features, hid_feats=100, out_feats=n_labels)
opt = torch.optim.Adam(model.parameters())

for epoch in range(10):
    model.train()
    # forward propagation by using all nodes
    logits = model(graph, node_features)
    # compute loss
    loss = F.cross_entropy(logits[train_mask], node_labels[train_mask])
    # compute validation accuracy
    acc = evaluate(model, graph, node_features, node_labels, valid_mask)
    # backward propagation
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

    # Save model if necessary.  Omitted in this example.

GraphSAGE は end-to-end な均質グラフ・ノード分類サンプルを提供します。対応するモデル実装は、調整可能な数の層、dropout 確率とカスタマイズ可能な集約関数と非線形を伴うサンプルの GraphSAGE クラスにあることを見れるでしょう。

 

異質グラフ

貴方のグラフが異質である場合、総てのエッジ型に沿った近傍からメッセージを集めることを望むかもしれません。総てのエッジ型上でメッセージ・パッシングを遂行するためにモジュール dgl.nn.pytorch.HeteroGraphConv (MXNet と Tensorflow でも利用可能です) を利用できます、それから各エッジ型のために異なるグラフ畳込みモジュールを結合します。

次のコードは異質グラフ畳込みモジュールを定義します、それは最初に各エッジ型上で個別のグラフ畳込みを遂行してから、総てのノード型のための最終的な結果として各エッジ型上でメッセージ集約を総計します。

# Define a Heterograph Conv model
import dgl.nn as dglnn

class RGCN(nn.Module):
    def __init__(self, in_feats, hid_feats, out_feats, rel_names):
        super().__init__()

        self.conv1 = dglnn.HeteroGraphConv({
            rel: dglnn.GraphConv(in_feats, hid_feats)
            for rel in rel_names}, aggregate='sum')
        self.conv2 = dglnn.HeteroGraphConv({
            rel: dglnn.GraphConv(hid_feats, out_feats)
            for rel in rel_names}, aggregate='sum')

    def forward(self, graph, inputs):
        # inputs are features of nodes
        h = self.conv1(graph, inputs)
        h = {k: F.relu(v) for k, v in h.items()}
        h = self.conv2(graph, h)
        return h

dgl.nn.HeteroGraphConv は入力としてノード型とノード特徴 tensor の辞書を取り、そしてノード型とノード特徴のもう一つの辞書を返します。

そこで 異質グラフサンプル でユーザと項目 (= item) 特徴を持つと仮定します。

model = RGCN(n_hetero_features, 20, n_user_classes, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
labels = hetero_graph.nodes['user'].data['label']
train_mask = hetero_graph.nodes['user'].data['train_mask']

次のように単純に順伝播を遂行できます :

node_features = {'user': user_feats, 'item': item_feats}
h_dict = model(hetero_graph, {'user': user_feats, 'item': item_feats})
h_user = h_dict['user']
h_item = h_dict['item']

訓練ループは均質グラフのためのものと同じです、今は (そこから予測を計算する) ノード表現の辞書を持つことを除いて。例えば、ユーザノードだけを予測している場合、返された辞書からユーザノード埋め込みを単に抽出できます :

opt = torch.optim.Adam(model.parameters())

for epoch in range(5):
    model.train()
    # forward propagation by using all nodes and extracting the user embeddings
    logits = model(hetero_graph, node_features)['user']
    # compute loss
    loss = F.cross_entropy(logits[train_mask], labels[train_mask])
    # Compute validation accuracy.  Omitted in this example.
    # backward propagation
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

    # Save model if necessary.  Omitted in the example.

DGL はノード分類のための RGCN の end-to-end なサンプルを提供します。モデル実装ファイル の RelGraphConvLayer で異質グラフ畳込みの定義を見ることができます。

 

以上






DGL 0.5 ユーザガイド : 5 章 グラフ・ニューラルネットワークを訓練する

DGL 0.5ユーザガイド : 5 章 グラフ・ニューラルネットワークを訓練する (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 5 章 グラフ・ニューラルネットワークを訓練する

概要

この章は 2章: メッセージ・パッシング で紹介されたメッセージ・パッシング法と 3 章: GNN モジュールを構築する で導入されたニューラルネットワークによりノード分類、エッジ分類、リンク予測、そして小さいグラフのためのグラフ分類のためにグラフ・ニューラルネットワークをどのように訓練するかを議論します。

この章は貴方のグラフそしてそのノードとエッジ特徴の総てが GPU に収められることを仮定しています ;そうでない場合には Chapter 6: Stochastic Training on Large Graphs を見てください。

以下のテキストはグラフとノード/エッジ特徴が既に準備されていることを仮定しています。DGL が提供するデータセットや 4 章: グラフ・データパイプライン で説明されている他の互換な DGLDataset を使用することを計画している場合、次のような何かで単一グラフのデータセットのためにグラフを得ることができます :

import dgl

dataset = dgl.data.CiteseerGraphDataset()
graph = dataset[0]

Note: この章ではバックエンドとして PyTorch を利用します。

 

異質グラフ

時に異質グラフ上で作業したいでしょう。ここではノード分類、エッジ分類とリンク予測タスクのためのサンプルとして合成の異質グラフを取ります。

合成異質グラフ hetero_graph はこれらのエッジ型を持ちます :

  • (‘user’, ‘follow’, ‘user’)
  • (‘user’, ‘followed-by’, ‘user’)
  • (‘user’, ‘click’, ‘item’)
  • (‘item’, ‘clicked-by’, ‘user’)
  • (‘user’, ‘dislike’, ‘item’)
  • (‘item’, ‘disliked-by’, ‘user’)
import numpy as np
import torch

n_users = 1000
n_items = 500
n_follows = 3000
n_clicks = 5000
n_dislikes = 500
n_hetero_features = 10
n_user_classes = 5
n_max_clicks = 10

follow_src = np.random.randint(0, n_users, n_follows)
follow_dst = np.random.randint(0, n_users, n_follows)
click_src = np.random.randint(0, n_users, n_clicks)
click_dst = np.random.randint(0, n_items, n_clicks)
dislike_src = np.random.randint(0, n_users, n_dislikes)
dislike_dst = np.random.randint(0, n_items, n_dislikes)

hetero_graph = dgl.heterograph({
    ('user', 'follow', 'user'): (follow_src, follow_dst),
    ('user', 'followed-by', 'user'): (follow_dst, follow_src),
    ('user', 'click', 'item'): (click_src, click_dst),
    ('item', 'clicked-by', 'user'): (click_dst, click_src),
    ('user', 'dislike', 'item'): (dislike_src, dislike_dst),
    ('item', 'disliked-by', 'user'): (dislike_dst, dislike_src)})

hetero_graph.nodes['user'].data['feature'] = torch.randn(n_users, n_hetero_features)
hetero_graph.nodes['item'].data['feature'] = torch.randn(n_items, n_hetero_features)
hetero_graph.nodes['user'].data['label'] = torch.randint(0, n_user_classes, (n_users,))
hetero_graph.edges['click'].data['label'] = torch.randint(1, n_max_clicks, (n_clicks,)).float()
# randomly generate training masks on user nodes and click edges
hetero_graph.nodes['user'].data['train_mask'] = torch.zeros(n_users, dtype=torch.bool).bernoulli(0.6)
hetero_graph.edges['click'].data['train_mask'] = torch.zeros(n_clicks, dtype=torch.bool).bernoulli(0.6)
 

以上






DGL 0.5 ユーザガイド : 4 章 グラフ・データパイプライン

DGL 0.5ユーザガイド : 4 章 グラフ・データパイプライン (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 4 章 グラフ・データパイプライン

DGL は dgl.data で多くの一般的に利用されるグラフ・データセットを実装しています。それらはクラス dgl.data.DGLDataset で定義された標準パイプラインに従っています。グラフデータを dgl.data.DGLDataset サブクラス内に処理することを強く勧めます、何故ならばパイプラインはグラフデータをロード、処理してセーブするための単純でクリーンな解を提供するからです。

この章は私達自身のグラフデータのために DGL-データセットをどのように作成するかを紹介します。以下の内容はパイプラインがどのように動作するかを説明し、そしてその各コンポーネントをどのように実装するかを示します。

 

DGLDataset クラス

dgl.data.DGLDataset は dgl.data で定義されたグラフデータセットを処理し、ロードしてセーブするための基底クラスです。それはグラフデータを処理するために基本的なパイプラインを実装します。下のフローチャートはパイプラインがどのように動作するかを示します。

遠隔サーバかローカルディスクにあるグラフ・データセットを処理するため、dgl.data.DGLDataset から継承したクラス、例えば MyDataset を定義します。MyDataset のテンプレートは次のようなものです。

クラス DGLDataset で定義されたグラフデータ入力パイプラインのためのフローチャート。

from dgl.data import DGLDataset

class MyDataset(DGLDataset):
    """ Template for customizing graph datasets in DGL.

    Parameters
    ----------
    url : str
        URL to download the raw dataset
    raw_dir : str
        Specifying the directory that will store the
        downloaded data or the directory that
        already stores the input data.
        Default: ~/.dgl/
    save_dir : str
        Directory to save the processed dataset.
        Default: the value of `raw_dir`
    force_reload : bool
        Whether to reload the dataset. Default: False
    verbose : bool
        Whether to print out progress information
    """
    def __init__(self,
                 url=None,
                 raw_dir=None,
                 save_dir=None,
                 force_reload=False,
                 verbose=False):
        super(MyDataset, self).__init__(name='dataset_name',
                                        url=url,
                                        raw_dir=raw_dir,
                                        save_dir=save_dir,
                                        force_reload=force_reload,
                                        verbose=verbose)

    def download(self):
        # download raw data to local disk
        pass

    def process(self):
        # process raw data to graphs, labels, splitting masks
        pass

    def __getitem__(self, idx):
        # get one example by index
        pass

    def __len__(self):
        # number of data examples
        pass

    def save(self):
        # save processed data to directory `self.save_path`
        pass

    def load(self):
        # load processed data from directory `self.save_path`
        pass

    def has_cache(self):
        # check whether there are processed data in `self.save_path`
        pass

dgl.data.DGLDataset クラスは抽象関数 process(), __getitem__(idx) と __len__() を持ちます、これらはサブクラスで実装されなければなりません。しかしセーブとロードを実装することも勧めます、何故ならばそれらは巨大なデータセットを処理するための多大な時間をセーブできるからです、そしてそれを容易にする幾つかの API があります (Save and load data 参照)。

dgl.data.DGLDataset の目的はグラフデータをロードするための標準的で便利な方法を提供することです。グラフ、特徴、ラベル、マスクとクラス数、ラベル数等のようなデータセットについての基本的な情報をストアできます。サンプリング、分割や特徴正規化のような演算は dgl.data.DGLDataset の外側で成されます。

この章の残りはパイプラインの関数を実装するためのベストプラクティスを示します。

 

raw データをダウンロードする (オプション)

データセットが既にローカルディスクにあるならば、それがディレクトリ raw_dir にあることを確実にしてください。データをダウンロードして正しいディレクトリに移動する手間なしにコードをどこでも実行することを望むのであれば、関数 download() を実装することにより自動的にそれを成すことができます。

データセットが zip ファイルであれば、MyDataset を dgl.data.DGLBuiltinDataset クラスから継承させます、これは zip file 展開を処理します。さもなければ、dgl.data.QM7bDataset でのように download() を実装します :

import os
from dgl.data.utils import download

def download(self):
    # path to store the file
    file_path = os.path.join(self.raw_dir, self.name + '.mat')
    # download file
    download(self.url, path=file_path)

上のコードは .mat ファイルをディレクトリ self.raw_dir にダウンロードします。ファイルが .gz, .tar, .tar.gz か .tgz file であれば、展開するために dgl.data.utils.extract_archive() 関数を使用します。次のコードは dgl.data.BitcoinOTCDataset で .gz ファイルをどのようにダウンロードするかを示します :

from dgl.data.utils import download, extract_archive

def download(self):
    # path to store the file
    # make sure to use the same suffix as the original file name's
    gz_file_path = os.path.join(self.raw_dir, self.name + '.csv.gz')
    # download file
    download(self.url, path=gz_file_path)
    # check SHA-1
    if not check_sha1(gz_file_path, self._sha1_str):
        raise UserWarning('File {} is downloaded but the content hash does not match.'
                          'The repo may be outdated or download may be incomplete. '
                          'Otherwise you can create an issue for it.'.format(self.name + '.csv.gz'))
    # extract file to directory `self.name` under `self.raw_dir`
    self._extract_gz(gz_file_path, self.raw_path)

上のコードはファイルを self.raw_dir 下のディレクトリ self.name に展開します。クラスが zip ファイルを扱うために dgl.data.DGLBuiltinDataset から継承しているのであれば、それはファイルをディレクトリ self.name にまた展開します。

オプションで、上のサンプルが行なっているようにダウンロードされたファイルの SHA-1 文字列を確認できます、作者が遠隔サーバのファイルをいつの日にか変更した場合に。

 

データを処理する

関数 process() でデータ処理コードを実装します、そして raw データが既に self.raw_dir にあることを仮定します。グラフ上の機械学習では典型的には 3 つのタイプのタスクがあります : グラフ分類ノード分類、そして リンク予測 です。これらのタスクに関連するデータセットをどのように処理するかを示します。

ここではグラフ、特徴とマスクを処理する標準的な方法にフォーカスします。サンプルとして組込みデータセットを使用してファイルからグラフを構築するための実装はスキップしますが、詳細な実装へのリンクを追加します。外部ソースからどのようにグラフを構築するかの完全なガイドを見るには 1.4 外部ソースからグラフを作成する を参照してください。

 

グラフ分類データセットを処理する

グラフ分類データセットは典型的な機械学習タスクの大半のデータセットとと殆ど同じで、そこではミニバッチ訓練が利用されます。そして raw データを dgl.DGLGraph オブジェクトのリストとラベル tensor のリストに処理します。加えて、raw データが幾つかのファイルに分割されている場合、データの特定のパートをロードするためにパラメータ split を追加できます。

サンプルとして dgl.data.QM7bDataset を取ります :

class QM7bDataset(DGLDataset):
    _url = 'http://deepchem.io.s3-website-us-west-1.amazonaws.com/' \
           'datasets/qm7b.mat'
    _sha1_str = '4102c744bb9d6fd7b40ac67a300e49cd87e28392'

    def __init__(self, raw_dir=None, force_reload=False, verbose=False):
        super(QM7bDataset, self).__init__(name='qm7b',
                                          url=self._url,
                                          raw_dir=raw_dir,
                                          force_reload=force_reload,
                                          verbose=verbose)

    def process(self):
        mat_path = self.raw_path + '.mat'
        # process data to a list of graphs and a list of labels
        self.graphs, self.label = self._load_graph(mat_path)

    def __getitem__(self, idx):
        """ Get graph and label by index

        Parameters
        ----------
        idx : int
            Item index

        Returns
        -------
        (dgl.DGLGraph, Tensor)
        """
        return self.graphs[idx], self.label[idx]

    def __len__(self):
        """Number of graphs in the dataset"""
        return len(self.graphs)

process() では、raw データはグラフのリストとラベルのリストに処理されます。反復のために __getitem__(idx) と __len__() を実装しなければなりません。__getitem__(idx) は上のようにタプル (graph, label) を返すようにすることを勧めます。self._load_graph() and __getitem__ の詳細については QM7bDataset ソースコード を確認してください。

データセットの幾つかの有用な情報を示すためにクラスにプロパティを追加することもできます。dgl.data.QM7bDataset では、このマルチタスク・データセットで予測タスクの総数示すためにプロパティ num_labels を追加できます :

@property
def num_labels(self):
    """Number of labels for each graph, i.e. number of prediction tasks."""
    return 14

これら総てのコーディングの後、最後に次のように dgl.data.QM7bDataset を使用できます :

from torch.utils.data import DataLoader

# load data
dataset = QM7bDataset()
num_labels = dataset.num_labels

# create collate_fn
def _collate_fn(batch):
    graphs, labels = batch
    g = dgl.batch(graphs)
    labels = torch.tensor(labels, dtype=torch.long)
    return g, labels

# create dataloaders
dataloader = DataLoader(dataset, batch_size=1, shuffle=True, collate_fn=_collate_fn)

# training
for epoch in range(100):
    for g, labels in dataloader:
        # your training code here
        pass

グラフ分類モデルを訓練するための完全なガイドは 5.4 Graph Classification で見つけられます。

グラフ分類データセットのより多くのサンプルについては、組込みグラフ分類データセットを参照してください :

 

ノード分類データセットを処理する

グラフ分類とは異なり、ノード分類は典型的には単一グラフ上です。そのようなものとして、データセットの分割はグラフのノード上です。分割を指定するためにノードマスクを使用することを勧めます。サンプルとして組込みデータセット CitationGraphDataset を使用します :

import dgl
from dgl.data import DGLBuiltinDataset

class CitationGraphDataset(DGLBuiltinDataset):
    _urls = {
        'cora_v2' : 'dataset/cora_v2.zip',
        'citeseer' : 'dataset/citeseer.zip',
        'pubmed' : 'dataset/pubmed.zip',
    }

    def __init__(self, name, raw_dir=None, force_reload=False, verbose=True):
        assert name.lower() in ['cora', 'citeseer', 'pubmed']
        if name.lower() == 'cora':
            name = 'cora_v2'
        url = _get_dgl_url(self._urls[name])
        super(CitationGraphDataset, self).__init__(name,
                                                   url=url,
                                                   raw_dir=raw_dir,
                                                   force_reload=force_reload,
                                                   verbose=verbose)

    def process(self):
        # Skip some processing code
        # === data processing skipped ===

        # build graph
        g = dgl.graph(graph)
        # splitting masks
        g.ndata['train_mask'] = generate_mask_tensor(train_mask)
        g.ndata['val_mask'] = generate_mask_tensor(val_mask)
        g.ndata['test_mask'] = generate_mask_tensor(test_mask)
        # node labels
        g.ndata['label'] = F.tensor(labels)
        # node features
        g.ndata['feat'] = F.tensor(_preprocess_features(features),
                                   dtype=F.data_type_dict['float32'])
        self._num_labels = onehot_labels.shape[1]
        self._labels = labels
        self._g = g

    def __getitem__(self, idx):
        assert idx == 0, "This dataset has only one graph"
        return self._g

    def __len__(self):
        return 1

簡潔さのため、ノード分類データセットを処理するための主要パートをハイライトするために process() のあるコードはスキップします : マスク、ノード特徴とノードラベルの分割は g.ndata にストアされます。詳細な実装については、CitationGraphDataset ソースコード を参照してください。

__getitem__(idx) と __len__() の実装もまた変えられたことに気付いてください、ノード分類タスクのためにはしばしば一つのグラフだけがあるためです。マスクは PyTorch と TensorFlow では bool tensor で、MXNet では float tensor です。

その使用方法を示すため、CitationGraphDataset, dgl.data.CiteseerGraphDataset のサブクラスを使用します。

# load data
dataset = CiteseerGraphDataset(raw_dir='')
graph = dataset[0]

# get split masks
train_mask = graph.ndata['train_mask']
val_mask = graph.ndata['val_mask']
test_mask = graph.ndata['test_mask']

# get node features
feats = graph.ndata['feat']

# get labels
labels = graph.ndata['label']

ノード分類モデルを訓練するための完全なガイドは 5.1 Node Classification/Regression で見つけられます。

ノード分類データセットのより多くのサンプルについては、組込みデータセットを参照してください :

 

リンク予測データセットのためのデータセットを処理する

リンク予測データセットの処理はノード分類のそれに類似していて、データセットにしばしば一つのグラフがあります。

サンプルとして組込みデータセット KnowledgeGraphDataset を使用して、そしてまたリンク予測データセットを処理するために主要パートをハイライトするために詳細なデータ処理コードはスキップします :

# Example for creating Link Prediction datasets
class KnowledgeGraphDataset(DGLBuiltinDataset):
    def __init__(self, name, reverse=True, raw_dir=None, force_reload=False, verbose=True):
        self._name = name
        self.reverse = reverse
        url = _get_dgl_url('dataset/') + '{}.tgz'.format(name)
        super(KnowledgeGraphDataset, self).__init__(name,
                                                    url=url,
                                                    raw_dir=raw_dir,
                                                    force_reload=force_reload,
                                                    verbose=verbose)

    def process(self):
        # Skip some processing code
        # === data processing skipped ===

        # splitting mask
        g.edata['train_mask'] = train_mask
        g.edata['val_mask'] = val_mask
        g.edata['test_mask'] = test_mask
        # edge type
        g.edata['etype'] = etype
        # node type
        g.ndata['ntype'] = ntype
        self._g = g

    def __getitem__(self, idx):
        assert idx == 0, "This dataset has only one graph"
        return self._g

    def __len__(self):
        return 1

コードで示されるように、splitting マスクをグラフの edata フィールドに追加します。完全なコードを見るためには KnowledgeGraphDataset ソースコード を確認してください。その使用方法を示すために KnowledgeGraphDataset, dgl.data.FB15k237Dataset のサブクラスを使用します :

import torch

# load data
dataset = FB15k237Dataset()
graph = dataset[0]

# get training mask
train_mask = graph.edata['train_mask']
train_idx = torch.nonzero(train_mask).squeeze()
src, dst = graph.edges(train_idx)
# get edge types in training set
rel = graph.edata['etype'][train_idx]

リンク予測モデルを訓練するための完全なガイドは 5.3 リンク予測 で見つけられます。

リンク予測データセットのより多くのサンプルについては、組込みデータセットを参照してください :

 

データをセーブしてロードする

処理されたデータをローカルディスクにキャッシュするためにセーブとロード関数を実装することを勧めます。これは殆どの場合多くのデータ処理時間を節約します。物事を単純にするため 4 つの関数を提供します :

次のサンプルはグラフのリストとデータセット情報をどのようにセーブしてロードするかを示します。

import os
from dgl import save_graphs, load_graphs
from dgl.data.utils import makedirs, save_info, load_info

def save(self):
    # save graphs and labels
    graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
    save_graphs(graph_path, self.graphs, {'labels': self.labels})
    # save other information in python dict
    info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
    save_info(info_path, {'num_classes': self.num_classes})

def load(self):
    # load processed data from directory `self.save_path`
    graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
    self.graphs, label_dict = load_graphs(graph_path)
    self.labels = label_dict['labels']
    info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
    self.num_classes = load_info(info_path)['num_classes']

def has_cache(self):
    # check whether there are processed data in `self.save_path`
    graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
    info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
    return os.path.exists(graph_path) and os.path.exists(info_path)

処理されたデータをセーブするに適さないケースがあることに注意してください。例えば、組込みデータセット dgl.data.GDELTDataset では、処理されたデータは非常に巨大ですので、__getitem__(idx) で各データサンプルを処理することがより効果的です。

 

ogb パッケージを使用して OGB データセットをロードする

Open グラフ・ベンチマーク (OGB) はベンチマーク・データセットのコレクションです。公式 OGB パッケージ ogb は OGB データセットを dgl.data.DGLGraph オブジェクトにダウンロードして処理するための API を提供します。ここではそれらの基本的な使用方法を紹介します。

最初に pip を使用して ogb パッケージをインストールします :

pip install ogb

次のコードはグラフ特性 (= property) 予測タスクのためのデータセットをどのようにロードするかを示します。

# Load Graph Property Prediction datasets in OGB
import dgl
import torch
from ogb.graphproppred import DglGraphPropPredDataset
from torch.utils.data import DataLoader


def _collate_fn(batch):
    # batch is a list of tuple (graph, label)
    graphs = [e[0] for e in batch]
    g = dgl.batch(graphs)
    labels = [e[1] for e in batch]
    labels = torch.stack(labels, 0)
    return g, labels

# load dataset
dataset = DglGraphPropPredDataset(name='ogbg-molhiv')
split_idx = dataset.get_idx_split()
# dataloader
train_loader = DataLoader(dataset[split_idx["train"]], batch_size=32, shuffle=True, collate_fn=_collate_fn)
valid_loader = DataLoader(dataset[split_idx["valid"]], batch_size=32, shuffle=False, collate_fn=_collate_fn)
test_loader = DataLoader(dataset[split_idx["test"]], batch_size=32, shuffle=False, collate_fn=_collate_fn)

グラフ特性予測データセットをロードすることも同様ですが、この種類のデータセットには一つのグラフオブジェクトだけがあることに注意してください。

# Load Node Property Prediction datasets in OGB
from ogb.nodeproppred import DglNodePropPredDataset

dataset = DglNodePropPredDataset(name='ogbn-proteins')
split_idx = dataset.get_idx_split()

# there is only one graph in Node Property Prediction datasets
g, labels = dataset[0]
# get split labels
train_label = dataset.labels[split_idx['train']]
valid_label = dataset.labels[split_idx['valid']]
test_label = dataset.labels[split_idx['test']]

リンク特性予測データセットもまたデータセット毎に一つのグラフを含みます :

# Load Link Property Prediction datasets in OGB
from ogb.linkproppred import DglLinkPropPredDataset

dataset = DglLinkPropPredDataset(name='ogbl-ppa')
split_edge = dataset.get_edge_split()

graph = dataset[0]
print(split_edge['train'].keys())
print(split_edge['valid'].keys())
print(split_edge['test'].keys())
 

以上






DGL 0.5 ユーザガイド : 3 章 GNN モジュールをビルドする

DGL 0.5ユーザガイド : 3 章 GNN モジュールをビルドする (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 3 章 GNN モジュールをビルドする

DGL NN モジュールは貴方の GNN モデルのためのビルディング・ブロックです。使用中の DNN フレームワーク・バックエンドに依拠して、それは PyTorch の NN モジュール、MXNet Gluon の NN ブロックと TensorFlow の Keras 層から継承しています。DGL NN モジュールでは、forward 関数の構築関数と tensor 演算のパラメータ登録はバックエンド・フレームワークと同じです。このようにして、DGL コードはバックエンド・フレームワーク・コードにシームレスに統合できます。主要な違いは、DGL に固有なメッセージ・パッシング演算にあります。

DGL は多くの一般に使用される Conv 層, Dense Conv 層, Global Pooling 層、そして ユティリティ・モジュール を統合しました。We welcome your contribution!

このセクションでは、貴方自身の DGL NN モジュールをどのようにビルドするかを紹介するためのサンプルとして SAGEConv を PyTorch バックエンドで利用します。

 

DGL NN モジュール構築 (= Construction) 関数

構築関数は以下を行ないます :

  1. オプションを設定する。
  2. 学習可能なパラメータやサブモジュールを登録する。
  3. パラメータをリセットする。
import torch as th
from torch import nn
from torch.nn import init

from .... import function as fn
from ....base import DGLError
from ....utils import expand_as_pair, check_eq_shape

class SAGEConv(nn.Module):
    def __init__(self,
                 in_feats,
                 out_feats,
                 aggregator_type,
                 bias=True,
                 norm=None,
                 activation=None):
        super(SAGEConv, self).__init__()

        self._in_src_feats, self._in_dst_feats = expand_as_pair(in_feats)
        self._out_feats = out_feats
        self._aggre_type = aggregator_type
        self.norm = norm
        self.activation = activation

構築関数では、最初にデータ次元を設定する必要があります。一般的な PyTorch モジュールのためには、次元は通常は入力次元、出力次元そして隠れ次元です。グラフ・ニューラルに対しては、入力次元はソースノード次元と destination ノード次元に分けることができます。

データ次元に加えて、グラフ・ニューラルネットワークのための典型的なオプションは aggregation 型 (self._aggre_type) です。aggregation 型は異なるエッジ上のメッセージがある destination ノードのためにどのように集約されるかを決定します。一般に利用される aggregation 型は mean, sum, max, min を含みます。幾つかのモジュールは lstm のようなより複雑な aggregation を適用するかもしれません。

ここで norm は特徴正規化のための callable 関数です。SAGEConv ペーパーでは、そのような正規化は l2 norm であり得ます : \(h_v = h_v / \lVert h_v \rVert_2\)。

# aggregator type: mean, max_pool, lstm, gcn
if aggregator_type not in ['mean', 'max_pool', 'lstm', 'gcn']:
    raise KeyError('Aggregator type {} not supported.'.format(aggregator_type))
if aggregator_type == 'max_pool':
    self.fc_pool = nn.Linear(self._in_src_feats, self._in_src_feats)
if aggregator_type == 'lstm':
    self.lstm = nn.LSTM(self._in_src_feats, self._in_src_feats, batch_first=True)
if aggregator_type in ['mean', 'max_pool', 'lstm']:
    self.fc_self = nn.Linear(self._in_dst_feats, out_feats, bias=bias)
self.fc_neigh = nn.Linear(self._in_src_feats, out_feats, bias=bias)
self.reset_parameters()

パラメータとサブモジュールを登録します。SAGEConv では、サブモジュールは aggregation 型に従って様々です。これらのモジュールは nn.Linear, nn.LSTM 等のような純粋な PyTorch nn モジュールです。構築関数の最後に、reset_parameters() を呼び出すことにより重み初期化が適用されます。

def reset_parameters(self):
    """Reinitialize learnable parameters."""
    gain = nn.init.calculate_gain('relu')
    if self._aggre_type == 'max_pool':
        nn.init.xavier_uniform_(self.fc_pool.weight, gain=gain)
    if self._aggre_type == 'lstm':
        self.lstm.reset_parameters()
    if self._aggre_type != 'gcn':
        nn.init.xavier_uniform_(self.fc_self.weight, gain=gain)
    nn.init.xavier_uniform_(self.fc_neigh.weight, gain=gain)

 

DGL NN モジュール Forward 関数

NN モジュールでは、forward() 関数が実際のメッセージ・パッシングと計算を行ないます。パラメータとして通常は tensor を取るPyTorch の NN モジュールと比べて、DGL NN モジュールは追加のパラメータ dgl.DGLGraph を取ります。forward() 関数のための作業負荷は 3 つのパートに分割できます :

  • グラフ確認とグラフ型仕様。
  • メッセージ・パッシングと reducing。
  • 出力のために reduce の後で特徴を更新する。

SAGEConv サンプルの forward() 関数を深く調べましょう。

 

グラフ確認とグラフ型仕様

def forward(self, graph, feat):
    with graph.local_scope():
        # Specify graph type then expand input feature according to graph type
        feat_src, feat_dst = expand_as_pair(feat, graph)

forward() は計算とメッセージ・パッシングで不正な値に導く可能性がある入力の多くの扱いにくいケース (= corner cases) を処理する必要があります。GraphConv のような conv モジュールでの一つの典型的なチェックは入力グラフに 0-in-degree ノードがないことを検証することです。ノードが 0-in-degree を持つとき、mailbox は空となり reduce 関数は総てゼロの値を生成します。これはモデル性能において静かな regression を引き起こすかもしれません。けれども、SAGEConv モジュールでは、集約 (= aggregated) 表現は元のノード特徴と連結されて、forward() の出力は総てゼロではありません。この場合にはそのようなチェックは必要ありません。

DGL NN モジュールは以下を含む異なる型のグラフ入力に渡り再利用可能であるべきです : 均質グラフ、異質グラフ (1.5 異質グラフ)、サブグラフ・ブロック (Chapter 6: Stochastic Training on Large Graphs)。

SAGEConv のための数式は :

\[
h_{\mathcal{N}(dst)}^{(l+1)} = \mathrm{aggregate}
\left(\{h_{src}^{l}, \forall src \in \mathcal{N}(dst) \}\right) \\
h_{dst}^{(l+1)} = \sigma \left(W \cdot \mathrm{concat}
(h_{dst}^{l}, h_{\mathcal{N}(dst)}^{l+1} + b) \right)\\
h_{dst}^{(l+1)} = \mathrm{norm}(h_{dst}^{l})
\]

グラフ型に従ってソースノード特徴 feat_src と destination ノード特徴 feat_dst を指定する必要があります。グラフ型を指定して feat を feat_src と feat_dst 内に拡張 (= expand) するための関数は expand_as_pair() です。この関数の詳細は下で示されます。

def expand_as_pair(input_, g=None):
    if isinstance(input_, tuple):
        # Bipartite graph case
        return input_
    elif g is not None and g.is_block:
        # Subgraph block case
        if isinstance(input_, Mapping):
            input_dst = {
                k: F.narrow_row(v, 0, g.number_of_dst_nodes(k))
                for k, v in input_.items()}
        else:
            input_dst = F.narrow_row(input_, 0, g.number_of_dst_nodes())
        return input_, input_dst
    else:
        # Homograph case
        return input_, input_

均質グラフ全体の訓練については、ソースノードと destination ノードは同じです。それらは総てグラフのノードです。

異質 (グラフ) なケースについては、グラフは幾つかの 2 部グラフに分割できます、各リレーションに対して一つです。リレーションは (src_type, edge_type, dst_dtype) として表されます。入力特徴 feat がタプルであることを識別するとき、グラフを 2 部として扱います。タプルの最初の要素はソースノード特徴でそして 2 番目の要素は destination ノード特徴です。

ミニバッチ訓練では、与えられた多くの destination ノードからサンプリングされたサブグラフ上で計算が適用されます。サブグラフは DGL ではブロックと呼ばれます。メッセージ・パッシング後、それらの destination ノードだけが更新されます、何故ならばそれらは元の full グラフで持つのと同じ近傍を持つからです。ブロック作成段階では、dst ノードはノードリストの最前部にあります。feat_dst をインデックス [0:g.number_of_dst_nodes()] で見つけられます。

feat_src と feat_dst を決定した後、上の 3 つのグラフ型のための計算は同じです。

 

メッセージ・パッシングと reducing

if self._aggre_type == 'mean':
    graph.srcdata['h'] = feat_src
    graph.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'neigh'))
    h_neigh = graph.dstdata['neigh']
elif self._aggre_type == 'gcn':
    check_eq_shape(feat)
    graph.srcdata['h'] = feat_src
    graph.dstdata['h'] = feat_dst     # same as above if homogeneous
    graph.update_all(fn.copy_u('h', 'm'), fn.sum('m', 'neigh'))
    # divide in_degrees
    degs = graph.in_degrees().to(feat_dst)
    h_neigh = (graph.dstdata['neigh'] + graph.dstdata['h']) / (degs.unsqueeze(-1) + 1)
elif self._aggre_type == 'max_pool':
    graph.srcdata['h'] = F.relu(self.fc_pool(feat_src))
    graph.update_all(fn.copy_u('h', 'm'), fn.max('m', 'neigh'))
    h_neigh = graph.dstdata['neigh']
else:
    raise KeyError('Aggregator type {} not recognized.'.format(self._aggre_type))

# GraphSAGE GCN does not require fc_self.
if self._aggre_type == 'gcn':
    rst = self.fc_neigh(h_neigh)
else:
    rst = self.fc_self(h_self) + self.fc_neigh(h_neigh)

コードは実際にはメッセージ・パッシングと reducing 計算を行ないます。コードのこのパートはモジュール毎に様々です。2 章 メッセージ・パッシング で説明されているように、上のコードの総てのメッセージ・パッシングは DGL のパフォーマンス最適化を完全に利用するために update_all() API と組込みメッセージ/reduce 関数を使用して実装されていることに注意してください。

 

出力のために reducing の後特徴を更新する

# activation
if self.activation is not None:
    rst = self.activation(rst)
# normalization
if self.norm is not None:
    rst = self.norm(rst)
return rst

forward() 関数の最後のパートは reduce 関数の後で特徴を更新することです。一般的な更新演算はオブジェクト構築段階で設定されたオプションに従って活性化関数と正規化を適用します。

 

異質な GraphConv モジュール

dgl.nn.pytorch.HeteroGraphConv は異質グラフ上で DGL NN モジュールを実行するモジュールレベルのカプセル化です。実装ロジックはメッセージ・パッシング・レベルの API multi_update_all() と同じです :

  • 各リレーション r 内の DGL nn モジュール。
  • 複数のリレーションからの同じノード型上の結果をマージする reduction。

これは次のように定式化できます :

\[
h_{dst}^{(l+1)} = \underset{r\in\mathcal{R}, r_{dst}=dst}{AGG} (f_r(g_r, h_{r_{src}}^l, h_{r_{dst}}^l))
\]

ここで $f_r$ は各リレーション $r$ のための NN モジュールで、$AGG$ は aggregation (集約) 関数です。

 

HeteroGraphConv 実装ロジック

class HeteroGraphConv(nn.Module):
    def __init__(self, mods, aggregate='sum'):
        super(HeteroGraphConv, self).__init__()
        self.mods = nn.ModuleDict(mods)
        if isinstance(aggregate, str):
            self.agg_fn = get_aggregate_fn(aggregate)
        else:
            self.agg_fn = aggregate

ヘテログラフ畳込みは各リレーションを nn モジュールにマップする辞書 mods を取ります。そして複数のリレーションから同じノード型上の結果を集約する関数を設定します。

def forward(self, g, inputs, mod_args=None, mod_kwargs=None):
    if mod_args is None:
        mod_args = {}
    if mod_kwargs is None:
        mod_kwargs = {}
    outputs = {nty : [] for nty in g.dsttypes}

入力グラフと入力 tensor に加えて、forward() 関数は 2 つの追加の辞書パラメータ mod_args と mod_kwargs を取ります。これら 2 つの辞書は self.mods と同じキーを持ちます。それらは、異なる型のリレーションのための self.mods の対応する NN モジュールを呼び出すときカスタマイズされたパラメータとして使用されます。

出力辞書は各 destination 型 nty のための出力 tensor を保持するために作成されます。各 nty のための値はリストで、一つ以上のリレーションが nty を destination 型として持つ場合、単一ノード型は複数の出力を得るかもしれないことを示すことに注意してください。更なる集約のためにそれらをリストに保持します。

if g.is_block:
    src_inputs = inputs
    dst_inputs = {k: v[:g.number_of_dst_nodes(k)] for k, v in inputs.items()}
else:
    src_inputs = dst_inputs = inputs

for stype, etype, dtype in g.canonical_etypes:
    rel_graph = g[stype, etype, dtype]
    if rel_graph.number_of_edges() == 0:
        continue
    if stype not in src_inputs or dtype not in dst_inputs:
        continue
    dstdata = self.mods[etype](
        rel_graph,
        (src_inputs[stype], dst_inputs[dtype]),
        *mod_args.get(etype, ()),
        **mod_kwargs.get(etype, {}))
    outputs[dtype].append(dstdata)

入力 g は異質グラフか異質グラフからのサブグラフ・ブロックであり得ます。普通の NN モジュールでのように、forward() 関数は異なる入力グラフ型を個別に扱う必要があります。

各リレーションは canonical_etype として表されます、これは (stype, etype, dtype) です。canonical_etype をキーとして使用し、2 部グラフ rel_graph を抽出できます。2 部グラフについて、入力特徴はタプル (src_inputs[stype], dst_inputs[dtype]) として体系化されます。各リレーションのための NN モジュールが呼び出されて出力はセーブされます。不必要な呼び出しを避けるため、エッジやその src 型を持つノードを持たないリレーションはスキップされます。

rsts = {}
for nty, alist in outputs.items():
    if len(alist) != 0:
        rsts[nty] = self.agg_fn(alist, nty)

最後に、複数のリレーションから同じ destination ノード型上の結果は self.agg_fn 関数を使用して集約されます。サンプルは dgl.nn.pytorch.HeteroGraphConv のための API Doc で見つけられます。

 

以上






DGL 0.5 ユーザガイド : 2 章 メッセージ・パッシング

DGL 0.5ユーザガイド : 2 章 メッセージ・パッシング (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 2 章 メッセージ・パッシング

メッセージ・パッシング・パラダイム

\(x_v\in\mathbb{R}^{d_1}\) をノード $v$ のための特徴とし、そして \(w_{e}\in\mathbb{R}^{d_2}\) をエッジ \(({u}, {v})\) のための特徴とします。メッセージ・パッシング・パラダイム は次のステップ \(t+1\) におけるノード-wise とエッジ-wise な計算を定義します。

\[
\text{Edge-wise: } m_{e}^{(t+1)} = \phi \left( x_v^{(t)}, x_u^{(t)}, w_{e}^{(t)} \right) , ({u}, {v},{e}) \in \mathcal{E}.\\
\text{Node-wise: } x_v^{(t+1)} = \psi \left(x_v^{(t)}, \rho\left(\left\lbrace m_{e}^{(t+1)} : ({u}, {v},{e}) \in \mathcal{E} \right\rbrace \right) \right).
\]

上の等式で、\(\phi\) はエッジ特徴をその付随するノードの特徴と結合することによりメッセージを生成するために 各エッジ上で定義される メッセージ関数 です ; \(\psi\) は reduce 関数 \(\rho\) を使用して incoming メッセージを収集することによりノード特徴を更新するために 各ノード上で定義される 更新関数 です。

 

組込み関数とメッセージ・パッシング API

DGL では、メッセージ関数 は単一引数 edges を取ります、これはソースノード、destination ノードとエッジの特徴にアクセスするために 3 つのメンバー src, dst と data をそれぞれ取ります。

reduce 関数 は単一引数 nodes を取ります。ノードは、その近傍がエッジを通してそれに送るメッセージを集めるためにその mailbox にアクセスできます。最も一般的な reduce 演算の幾つかは sum, max, min 等を含みます。

更新関数は単一引数 nodes を取ります。この関数は、典型的には最後のステップにおけるノードの特徴と結合された、reduce 関数からの集合結果上で作用し、出力をノード特徴としてセーブします。

DGL は一般に使用されるメッセージ関数と reduce 関数を名前空間 dgl.function で 組込み として実装しました。一般に、可能なときにはいつでも 組込み関数を使用することを提案します、何故ならばそれらは大いに最適化されて次元ブロードキャスティングを自動的に扱うからです。

貴方のメッセージ・パッシング関数が組込みで実装できない場合には、ユーザ定義メッセージ/reduce 関数を実装できます (aka. UDF)。

組込みメッセージ関数は unary (単項) かバイナリであり得ます。unary については今のところ copy をサポートしています。バイナリ関数については、今は add, sub, mul, div, dot をサポートします。メッセージ組込み関数のための名前付け慣習としては u は src ノードを表し、v は dst ノードを表し、e はエッジを表します。これらの関数のためのパラメータは相当するノードとエッジのための入力と出力フィールド名を示す文字列です。ここにサポートされる組込み関数の dgl.function があります。例えば、src ノードから hu 特徴をそして dst ノードから hv 特徴を追加してからフィールドのエッジ上の結果をセーブするために、組込み関数 dgl.function.u_add_v(‘hu’, ‘hv’, ‘he’) を利用できます、これは次のメッセージ UDF に等値です :

def message_func(edges):
     return {'he': edges.src['hu'] + edges.dst['hv']}

組込みの reduce 関数は演算 sum, max, min, prod と mean をサポートします。reduce 関数は通常は 2 つのパラメータを持ちます、一つは mailbox のフィールド名のため、一つは destination のフィールド名のためで、両者は文字列です。例えば、dgl.function.sum(‘m’, ‘h’) はメッセージ m を合計する Reduce UDF に等値です :

import torch
def reduce_func(nodes):
     return {'h': torch.sum(nodes.mailbox['m'], dim=1)}

DGL では、エッジ-wise な計算を呼び出すインターフェイスは apply_edges() です。apply_edges のためのパラメータは API Doc で説明されているようにメッセージ関数と正当なエッジ型です (デフォルトでは、総てのエッジは更新されます)。例えば :

import dgl.function as fn
graph.apply_edges(fn.u_add_v('el', 'er', 'e'))

ノード-wise な計算を呼び出すインターフェイスは update_all() です。update_all のためのパラメータはメッセージ関数、reduce 関数と更新関数です。3 番目のパラメータを空としておくことで更新関数は update_all の外側で呼び出すこともできます。これは提案されます、何故ならば更新関数はコードを簡潔にするために純粋な tensor 演算として通常は書かれるからです。例えば :

def updata_all_example(graph):
    # store the result in graph.ndata['ft']
    graph.update_all(fn.u_mul_e('ft', 'a', 'm'),
                    fn.sum('m', 'ft'))
    # Call update function outside of update_all
    final_ft = graph.ndata['ft'] * 2
    return final_ft

この呼び出しはソースノード特徴 ft とエッジ特徴 a を乗算することによりメッセージ m を生成し、ノード特徴 ft を更新するためにメッセージ m を合計し、最後に結果 final_ft を得るために ft に 2 を乗算します。呼び出し後、中間メッセージ m はクリーンアップされます。上の関数に対する数式は :

\[
{final\_ft}_i = 2 * \sum_{j\in\mathcal{N}(i)} ({ft}_j * a_{ij})
\]

update_all はメッセージ生成をマージする高位 API で、メッセージ reduction とノード更新は単一呼び出し、これは下で説明されるように最適化のための余地を残します。

 

効率的なメッセージ・パッシング・コードを書く

DGL はメッセージ・パッシングのためのメモリ消費と計算スピードを最適化します。最適化は以下を含みます :

  • マルチカーネルを単一の一つにマージする : これは、複数の組込み関数を一度に呼び出すために update_all を使用することにより達成されます。(スピード最適化)
  • ノードとエッジ上の並列性 : DGL はエッジ-wise 計算 apply_edges を一般化されてサンプリングされた dense-dense 行列乗算 (gSDDMM) 演算として抽象化してエッジに渡る計算を並列化します。同様に、DGL はノード-wise 計算 update_all を一般化された sparse-dense 行列乗算 (gSPMM) 演算として抽象化してノードに渡る計算を並列化します。(スピード最適化)
  • エッジへの不要なメモリコピーを回避する : ソースと destination ノードからの特徴を必要とするメッセージを生成するため、一つの選択肢はソースと destination ノード特徴をそのエッジにコピーすることです。幾つかのグラフについては、エッジの数はノードの数よりも遥かに大きいです。このコピーはコスト高であり得ます。DGL 組込みメッセージ関数はノード特徴をエントリ・インデックスを使用してサンプリングすることによりこのメモリコピーを回避します。(メモリとスピード最適化)
  • エッジ上の特徴ベクトルの具体化を回避する : 完全なメッセージパッシング過程はメッセージ生成、メッセージ reduction とノード更新を含みます。update_all 呼び出しでは、メッセージ関数と reduce 関数はそれらの関数が組込みであれば一つのカーネルにマージされます。エッジ上のメッセージ具体化はありません。(メモリ最適化)

上に従えば、それらの最適化を活用する一般的な方法は貴方自身のメッセージ・パッシング機能を update_all 呼び出しのパラメータとしての組込み関数との結合として構築することです。

エッジ上にメッセージをセーブしなければならない GATConv のような幾つかのケースについては、apply_edges を組込み関数とともに呼び出す必要があります。時にエッジ上のメッセージは高次元でありえて、これはメモリ消費的です。edata 次元をできるだけ低く保つことを提案します。

エッジ上の演算をノードに分割することによりこれをどのように成すかのサンプルがここにあります。この選択肢は以下を行ないます : src 特徴と dst 特徴を結合してから、線形層を適用します、i.e. \(W\times (u || v)\)。src と dst 特徴次元が高い一方で、線形層出力次元は低いです。straight forward な実装は次のようなものです :

linear = nn.Parameter(th.FloatTensor(size=(1, node_feat_dim*2)))
def concat_message_function(edges):
    {'cat_feat': torch.cat([edges.src.ndata['feat'], edges.dst.ndata['feat']])}
g.apply_edges(concat_message_function)
g.edata['out'] = g.edata['cat_feat'] * linear

提案される実装は線形演算を 2 つに分割します、一つは src 特徴上で適用されて、他方は dst 特徴上で適用されます。最後のステージでエッジ上の線形演算の出力を追加します、i.e. \(W \times (u||v) = W_l \times u + W_r \times v\) ですから、\(W_l\times u + W_r \times v\) を遂行します、そこでは \(W_l\) と \(W_r\) はそれぞれ行列 \(W\) の左と右半分です :

linear_src = nn.Parameter(th.FloatTensor(size=(1, node_feat_dim)))
linear_dst = nn.Parameter(th.FloatTensor(size=(1, node_feat_dim)))
out_src = g.ndata['feat'] * linear_src
out_dst = g.ndata['feat'] * linear_dst
g.srcdata.update({'out_src': out_src})
g.dstdata.update({'out_dst': out_dst})
g.apply_edges(fn.u_add_v('out_src', 'out_dst', 'out'))

上の 2 つの実装は数学的に同値です。後者は遥かに効率的です、何故ならばエッジ上の feat_src and feat_dst をセーブする必要がないからです、これはメモリ効率的ではありません。更に、加算は DGL の組込み関数 u_add_v で最適化できるでしょう、これは計算を更に高速化してメモリフットプリントをセーブします。

 

グラフの一部上でメッセージ・パッシングを適用する

グラフのノードの一部だけを更新することを望む場合、その実践は update に含めたいノードのための id を提供することによりサブグラフを作成してから、サブグラフ上で update_all を呼び出すことです。例えば :

nid = [0, 2, 3, 6, 7, 9]
sg = g.subgraph(nid)
sg.update_all(message_func, reduce_func, apply_node_func)

これはミニバッチ訓練における一般的な使用方法です。より詳細な使用方法については Chapter 6: Stochastic Training on Large Graphs ユーザガイドを確認してください。

 

メッセージ・パッシングでエッジ重みを適用する

GNN モデリングにおける一般に見られる実践はメッセージ集約の前にメッセージ上でエッジ重みを適用することです、例えば GAT と幾つかの GCN 亜種 でです。DGL で、これを扱う方法は :

  • 重みをエッジ特徴としてセーブする。
  • メッセージ関数でエッジ特徴をソースノード特徴で乗算する。

例えば :

graph.edata['a'] = affinity
graph.update_all(fn.u_mul_e('ft', 'a', 'm'),
                 fn.sum('m', 'ft'))

上では、エッジ重みとして affinity を使用しています。エッジ重みは通常はスカラーです。

 

異質グラフ上のメッセージ・パッシング

異質 (グラフ) (1.5 異質グラフ のためのユーザガイド)、あるいは短くヘテログラフはノードとエッジの異なる型を含むグラフです。異なる型のノードとエッジは、各ノードとエッジ型の特質を捕捉するために設計された異なる型の属性を持つ傾向があります。グラフ・ニューラルネットワークのコンテキスト内では、それらの複雑さに依拠して、特定のノードとエッジ型が異なる数の次元を持つ表現でモデル化される必要があります。

ヘテログラフ上のメッセージ・パッシングは 2 つのパートに分割できます :

  1. 各関係 r 内のメッセージ計算と集約。
  2. 複数の関係からの同じノード型上の結果をマージする reduction。

ヘテログラフ上のメッセージ・パッシングを呼び出す DGL のインターフェイスは multi_update_all() です。multi_update_all は (関係をキーとして使用して) 各関係内の update_all のためのパラメータを含む辞書、そして 交差 (= cross) 型 reducer を表す文字列を取ります。reducer は sum, min, max, mean, stack の一つであり得ます。ここにサンプルがあります :

for c_etype in G.canonical_etypes:
    srctype, etype, dsttype = c_etype
    Wh = self.weight[etype](feat_dict[srctype])
    # Save it in graph for message passing
    G.nodes[srctype].data['Wh_%s' % etype] = Wh
    # Specify per-relation message passing functions: (message_func, reduce_func).
    # Note that the results are saved to the same destination feature 'h', which
    # hints the type wise reducer for aggregation.
    funcs[etype] = (fn.copy_u('Wh_%s' % etype, 'm'), fn.mean('m', 'h'))
# Trigger message passing of multiple types.
G.multi_update_all(funcs, 'sum')
# return the updated node feature dictionary
return {ntype : G.nodes[ntype].data['h'] for ntype in G.ntypes}
 

以上






DGL 0.5 ユーザガイド : 1 章 グラフ : 1.6 GPU で DGLGraph を使用する

DGL 0.5ユーザガイド : 1 章 グラフ : 1.6 GPU で DGLGraph を使用する (翻訳/解説)

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

* 本ページは、DGL の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

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

 

ユーザガイド : 1 章 グラフ : 1.6 GPU で DGLGraph を使用する

構築の間に 2 つの GPU tensor を渡すことにより GPU 上 で DGLGraph を作成できます。もう一つのアプローチは DGLGraph を GPU にコピーするために to() API を使用することです、これはグラフ構造と特徴データを与えられたデバイスにコピーします。

>>> import dgl
>>> import torch as th
>>> u, v = th.tensor([0, 1, 2]), th.tensor([2, 3, 4])
>>> g = dgl.graph((u, v))
>>> g.ndata['x'] = th.randn(5, 3)  # original feature is on CPU
>>> g.device
device(type='cpu')
>>> cuda_g = g.to('cuda:0')  # accepts any device objects from backend framework
>>> cuda_g.device
device(type='cuda', index=0)
>>> cuda_g.ndata['x'].device       # feature data is copied to GPU too
device(type='cuda', index=0)

>>> # A graph constructed from GPU tensors is also on GPU
>>> u, v = u.to('cuda:0'), v.to('cuda:0')
>>> g = dgl.graph((u, v))
>>> g.device
device(type='cuda', index=0)

GPU を伴う任意の演算は GPU 上で遂行されます。そして、それらは総ての tensor 引数が既に GPU 上に置かれていることを要求して結果 (グラフ or tenor) も GPU 上になります。更に、GPU グラフは GPU 上の特徴データだけを受け取ります。

>>> cuda_g.in_degrees()
tensor([0, 0, 1, 1, 1], device='cuda:0')
>>> cuda_g.in_edges([2, 3, 4])   # ok for non-tensor type arguments
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
>>> cuda_g.in_edges(th.tensor([2, 3, 4]).to('cuda:0'))  # tensor type must be on GPU
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
>>> cuda_g.ndata['h'] = th.randn(5, 4)  # ERROR! feature must be on GPU too!
DGLError: Cannot assign node feature "h" on device cpu to a graph on device
cuda:0. Call DGLGraph.to() to copy the graph to the same device.
 

以上






AI導入支援 #2 ウェビナー

スモールスタートを可能としたAI導入支援   Vol.2
[無料 WEB セミナー] [詳細]
「画像認識 AI PoC スターターパック」の紹介
既に AI 技術を実ビジネスで活用し、成果を上げている日本企業も多く存在しており、競争優位なビジネスを展開しております。
しかしながら AI を導入したくとも PoC (概念実証) だけでも高額な費用がかかり取組めていない企業も少なくないようです。A I導入時には欠かせない PoC を手軽にしかも短期間で認知度を確認可能とするサービの紹介と共に、AI 技術の特性と具体的な導入プロセスに加え運用時のポイントについても解説いたします。
日時:2021年10月13日(水)
会場:WEBセミナー
共催:クラスキャット、日本FLOW(株)
後援:働き方改革推進コンソーシアム
参加費: 無料 (事前登録制)
人工知能開発支援
◆ クラスキャットは 人工知能研究開発支援 サービスを提供しています :
  • テクニカルコンサルティングサービス
  • 実証実験 (プロトタイプ構築)
  • アプリケーションへの実装
  • 人工知能研修サービス
◆ お問合せ先 ◆
(株)クラスキャット
セールス・インフォメーション
E-Mail:sales-info@classcat.com