グラフ埋め込み(Graph Embedding)は、ノードとエッジでできたネットワーク(グラフ)を、機械学習で扱える低次元のベクトルへ変換する技術です。ExcelやPowerPointでグラフ(図表)を文書に埋め込む操作とは別物で、本記事はネットワークをベクトル化する方を扱います。SNSの友人関係、購買履歴、知識グラフ、分子構造のように「つながり方そのものが情報」のデータを、分類・リンク予測・推薦へ渡すための前処理です。手法の選び方、GNNとの関係、知識グラフ埋め込みの違いを整理し、2026年10月時点の版(PyTorch Geometric 2.8.1・node2vec 0.5.0)で実行を確かめたコード、大規模グラフでの扱い、採用と見送りの判断基準までまとめます。
まとめ:グラフ埋め込みの手法の選び方とPython実装の最短経路
- グラフ埋め込みは、つながりの近さや構造上の役割といった「似ている」の基準をベクトルの近さで表すように学習する変換。出力は1ノード(または1グラフ)あたり数十〜数百次元のベクトル。
- 属性の無いグラフをまず試すならNode2Vec。ランダムウォークの偏りをp・qの2つで調整でき、ノード分類とリンク予測の両方に使える。
- ノードに特徴量があり、学習後に増えるノードにもすぐ埋め込みを付けたいならGraphSAGE・GAT などのGNN。
- 分子1個のようにグラフ全体を1本のベクトルにしたいなら、GNNの出力をプーリングで集約する。
- 向きと種類を持つ関係(知識グラフ)はTransE・DistMult・ComplEx・RotatE。向きのある関係を含むならComplExかRotatE。
- Pythonでは、構造だけならnode2vecライブラリ(Python 3.12)、GNN・知識グラフ・大規模グラフまで進むならPyTorch Geometric 2.8系+pyg-lib。
- 次数や共通隣接数だけで必要な精度が出る場合や、経路そのものを正確に答えたい場合は、埋め込みを採用しない。
グラフ埋め込みの定義とノード埋め込み・グラフ全体埋め込みの違い
グラフはノード(点)とエッジ(つながり)で関係を表すデータ構造です。頂点・辺・次数といった基礎用語はグラフ理論の基礎から最短経路・GNNの実装応用までを整理した解説にまとめています。
隣接行列の次元の大きさと埋め込みで表す類似性
1万ノードのグラフを隣接行列の行で表すと、各ノードの特徴は1万次元になり、辺が少ないグラフでは大半がゼロです。ノードが増えるたびに次元も増え、特定の隣接ノードの有無という形でしか構造を表せません。グラフ埋め込みは各ノードを数十〜数百次元の連続ベクトルに落とし、指定した類似性(つながりの近さ、構造上の役割の近さなど)がベクトルの近さに表れるように学習します。類似ノード検索・クラスタリング・可視化・予測モデルへの入力が、すべてベクトルの演算で済むようになります。
テキストをPythonでのEmbedding APIを使ってベクトル化するのと同じ発想を、つながりの構造に当てはめたものです。文章や画像の埋め込みとの共通点は埋め込みモデルの仕組みと選び方で確認できます。作ったベクトルはpgvectorのようなベクトル検索基盤に入れ、HNSWなどの索引を張れば、大量のノードから似たノードを近似検索で引けます。
ノード埋め込みとグラフ全体埋め込みの粒度の違い
本記事が扱う「グラフ埋め込み」の粒度は主に2つです(エッジや部分グラフを埋め込む手法もあります)。ノード埋め込みは個々のノードを1本ずつベクトル化するもので、論文引用グラフCoraの分野分類のようなノード分類に使い、2ノードのベクトルを組み合わせればノード対を評価するリンク予測や友人推薦にも使えます。グラフ全体埋め込みは、分子1個やネットワーク1つを1本のベクトルにまとめ、グラフ同士の分類や比較に使います。英語の graph embedding も含め、文献によって「グラフ埋め込み」の対象粒度が異なるため、ノード単位かグラフ全体かを確認してください。
DeepWalk・Node2Vec・GraphSAGE・GCN・GATの手法比較と選定基準
手法は、ランダムウォークで近さを単語埋め込みのように学習する系統、深層オートエンコーダで近さを再構成する系統、ノードの属性と近傍を集約するGNNの系統に分かれます。手法選びでは表の「帰納的」(学習後に増えたノードにも適用できるか)と「属性利用」の2列を最初に見ます。
| 手法 | 発表 | アプローチ | 帰納的 | 属性利用 | 主な用途 |
|---|---|---|---|---|---|
| DeepWalk | 2014 | 一様なウォーク+Skip-gram | × | × | ノード分類・可視化 |
| LINE | 2015 | 1次・2次近接を保存 | × | × | 大規模ネットワーク |
| Node2Vec | 2016 | 偏りウォーク(p・q) | × | × | 分類・リンク予測 |
| SDNE | 2016 | 深層オートエンコーダ | × | × | 非線形な構造の保存 |
| GraphSAGE | 2017 | 近傍サンプリング+集約 | ○ | ○ | 未知ノード・大規模 |
| GCN | 2017 | 近傍を畳み込み集約 | △ | ○ | 半教師ノード分類 |
| GAT | 2018 | 注意機構で近傍を重み付け | ○ | ○ | 近傍の重要度が不均一 |
原論文はDeepWalk(Perozzi ほか)、LINE(Tang ほか)、node2vec(Grover・Leskovec)、SDNE(Wang・Cui・Zhu、KDD 2016)、GraphSAGE(Hamilton ほか)、GCN(Kipf・Welling)、GAT(Veličković ほか)です。
DeepWalk・Node2VecのウォークとLINEの近接保存の違い
DeepWalkは各ノードから短いランダムウォークを大量に作り、そのノード列を「文章」とみなしてword2vecのSkip-gramで学習します。文章中で近くに出る単語が似たベクトルになるのと同じ理屈で、ウォーク上で近くに現れるノードが似たベクトルになります。計算は軽い一方、探索の偏りは制御できません。
Node2Vecはここに戻りやすさpと遠くへの進みやすさqを加えました。p・qがどちらも1ならDeepWalkと同じ一様なウォークです。qを1より小さくすると遠くへ進みやすくなり、同じコミュニティのノードが近づきます。1より大きくすると近場を回り、ハブや橋渡しといった役割の似たノードが近づきます。著者らのSNAPのnode2vecプロジェクトページに論文と参照実装があります。LINEはウォークを使わず、直接つながる1次近接と、共通の隣接ノードを持つ2次近接を保存する目的関数で学習します。
この系統は学習時に存在したノードだけを埋め込むトランスダクティブな手法で、あとから増えたノードには再学習が要ります。
未知ノードへ適用できるGraphSAGEとトランスダクティブな手法の差
GraphSAGE(Graph SAmple and aggreGatE)は、各ノードのベクトルそのものではなく「近傍ノードの特徴量をサンプリングして集約する関数」を学習します。学習後に現れたノードにも同じ関数を当てられるので、日々ノードが増えるSNSや商品グラフに向きます。プロフィールや商品カテゴリといった属性を入力にできる点も、ランダムウォーク系との違いです。
埋め込み次元の選定基準と128次元を起点とする精度比較
次元数は、node2vec論文の実験設定(d=128)、eliorc版node2vecの dimensions の既定値、PyG公式例で指定している embedding_dim がそろって128です(PyGの embedding_dim は必須引数で既定値はありません)。論文のパラメータ感度の実験(BlogCatalog・Macro-F1)では、次元が100前後に達すると性能が頭打ちになる傾向が報告されています。必要な次元数はノード数だけでは決まらないため、まず128で学習し、半分ずつ減らしながら分類精度やROC-AUCが落ちない最小の次元を探すのが手堅い進め方です。固定長の数値型で保存するベクトル本体の容量は次元数に比例します。検索コストは次元数に加えて、索引方式や探索設定にも左右されます。
p・qや次元を本格的に探索するなら、Optunaによるハイパーパラメータ探索のような仕組みに下流タスクの評価値を返させると、手作業の総当たりより試行回数を抑えられます。
階層構造を少ない次元で表す双曲空間への埋め込み
分類体系・組織図・WordNetの上位語/下位語のような木に近いグラフは、ユークリッド空間で階層の距離を小さい歪みのまま表そうとすると、次元を増やす必要が出る場合があります。Nickel・KielaのPoincaré埋め込み(NeurIPS 2017)は、外側ほど空間が急に広がる双曲空間(Poincaré球)にノードを置くことで、階層と類似度を少ない次元で同時に表します。論文は、階層を持つデータで表現力と汎化の両面がユークリッド空間の埋め込みを大きく上回ったと報告しています。商品カテゴリやスキル体系のように階層がはっきりしたデータで、次元を増やしても精度が伸びないときの選択肢です。
グラフ埋め込みとGNNの関係:メッセージパッシングと属性利用の違い
GNNは埋め込みを作るための手段の一種で、グラフ埋め込みと対立する概念ではありません。各ノードが近傍から情報を受け取って自分の表現を更新する処理(メッセージパッシング)を数層重ね、その結果として各ノードのベクトルを出力します。層を重ねる発想の基礎はニューラルネットワークの仕組みとPyTorch実装で確認できます。
GCNとGATの集約方式と未知ノードへの適用条件
GCN(Graph Convolutional Network)は次数で正規化した近傍を畳み込み、GAT(Graph Attention Network)は注意機構で「どの隣接ノードを重視するか」を学びます。GCNの原論文は主にトランスダクティブ設定で評価されていますが、学習済みの重みを共有し、入力グラフの正規化を再計算すれば未知ノードにも適用できます(表の△は原論文の評価設定を指します)。GATは注意機構のパラメータを全ノードで共有し、各ノードの特徴量と近傍に同じ計算を当てるので、特徴量の形式がそろっていれば未知ノードにも適用でき、帰納的な運用に向きます。
GNNの強みは、Node2Vecが使えないノード属性を埋め込みに反映できる点です。PyTorch Geometricのtorch_geometric.nnのリファレンスにはGCNConv・GATConv・SAGEConvなどの層がそろい、Node2Vecとの比較も同じパッケージで済みます。
グラフ全体を1本のベクトルにするGraph2vecとGNN+プーリング
分子の毒性判定や、取引ネットワーク1件ごとの不正判定のように、比べたい単位が「グラフ1つ」のときはグラフ全体の埋め込みを作ります。
Graph2vecの考え方と2026年時点での入れにくさ
Graph2vec(Narayanan ほか、2017)は、グラフを「文書」、グラフに含まれる部分構造を「単語」とみなし、doc2vecと同じ要領でグラフ1つに1本のベクトルを教師なしで学習します。ラベルが無くても作れ、分類にもクラスタリングにも使えるのが利点です。
ただし、Graph2vecをまとめて実装した代表的なライブラリ karateclub は、PyPIの配布版が1.3.3(2022年10月)のままです。この版は setup.py で numpy<1.23.0・networkx<2.7・pandas<=1.3.5 を固定しており、python:3.12-slim のコンテナで pip install karateclub を実行すると依存パッケージのビルドで失敗しました。GitHubの master は依存条件を更新していますが、同じ環境でgitから入れても scipy のソースビルド(Cコンパイラが必要)に進んで失敗しました。新規に組むなら、次のGNN+プーリングの方が保守の面で安全です。
PyTorch GeometricでGCNの出力をプーリングしてグラフを埋め込むコード
各ノードのGNN出力を、グラフごとに平均(global_mean_pool)すれば、グラフ1つにつき1本のベクトルになります。次のコードは、188個の化合物グラフ(変異原性の有無の2クラス)からなるTUDatasetのMUTAGで、グラフ単位の分類器を学習し、その途中の64次元ベクトルをグラフ埋め込みとして取り出します。
import torch
import torch.nn.functional as F
from torch_geometric.datasets import TUDataset
from torch_geometric.loader import DataLoader
from torch_geometric.nn import GCNConv, global_mean_pool
torch.manual_seed(0)
dataset = TUDataset(root="data/TUDataset", name="MUTAG").shuffle()
train_loader = DataLoader(dataset[:150], batch_size=32, shuffle=True)
test_loader = DataLoader(dataset[150:], batch_size=32)
class GraphEncoder(torch.nn.Module):
def __init__(self, hidden=64):
super().__init__()
self.conv1 = GCNConv(dataset.num_features, hidden)
self.conv2 = GCNConv(hidden, hidden)
self.head = torch.nn.Linear(hidden, dataset.num_classes)
def embed(self, x, edge_index, batch):
x = F.relu(self.conv1(x, edge_index))
x = F.relu(self.conv2(x, edge_index))
return global_mean_pool(x, batch) # グラフごとにノードの出力を平均=グラフ埋め込み
def forward(self, x, edge_index, batch):
return self.head(self.embed(x, edge_index, batch))
model = GraphEncoder()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(1, 101):
model.train()
for batch in train_loader:
optimizer.zero_grad()
out = model(batch.x, batch.edge_index, batch.batch)
loss = F.cross_entropy(out, batch.y)
loss.backward()
optimizer.step()
model.eval()
correct = 0
with torch.no_grad():
for batch in test_loader:
pred = model(batch.x, batch.edge_index, batch.batch).argmax(dim=1)
correct += int((pred == batch.y).sum())
g = next(iter(test_loader))
emb = model.embed(g.x, g.edge_index, g.batch) # 32グラフ分の埋め込み
print("graph embeddings:", tuple(emb.shape))
print(f"test acc: {correct / len(dataset[150:]):.3f}")
2026年10月29日に python:3.12-slim のコンテナ(CPU・torch 2.14.1・PyG 2.8.1)で実行すると、埋め込みは32グラフ×64次元のテンソルで、学習に使わなかった38グラフでの正解率は0.737でした。188件と小さいデータなので、シャッフルの乱数を変えると値も変わります。埋め込みだけを使いたい場合は、学習後に embed メソッドの出力を保存し、類似グラフの検索やクラスタリングに回します。ラベルを使って学習しているので、ベクトルは「変異原性を見分けやすい」方向に寄ります。ラベルの無いグラフを似た順に並べたいだけなら、この点を承知のうえで使うか、教師なしの手法を選びます。
知識グラフ埋め込みTransE・DistMult・ComplEx・RotatEの違いと実装
SNSのような「つながっているか否か」だけのグラフと違い、知識グラフは(東京, 首都, 日本)のような向きと種類を持つ関係(トリプル)でできています。この関係の意味までベクトルにするのが知識グラフ埋め込み(KGE)です。知識グラフをRDFで持つ場合の基礎はオントロジーとRDF・OWLの解説を参照してください。
TransE・DistMult・ComplEx・RotatEが表せる関係の違い
手法の違いは、頭h・関係r・尾tの組の「もっともらしさ」を測る式にあります。
| 手法 | 発表 | 関係の表し方 | 対称な関係 | 非対称な関係 |
|---|---|---|---|---|
| TransE | 2013 | 平行移動 h+r≈t | × | ○ |
| DistMult | 2015 | 要素ごとの積(双線形) | ○ | × |
| ComplEx | 2016 | 複素数の双線形 | ○ | ○ |
| RotatE | 2019 | 複素平面での回転 | ○ | ○ |
TransE(Bordes ほか、NeurIPS 2013)は「東京+首都」が「日本」に近づくように学習します。1対多・多対多に弱く、「日本+都道府県」が47個の尾エンティティすべてに近づこうとするため、それらのベクトルが同じ位置へ寄せられやすくなります。DistMult(Yang ほか、ICLR 2015)は単純な双線形の式で、論文のFB15k-401(Freebaseの部分集合)でのHits@10は、TransEが54.7%、基本形のDistMultが58.5%、非線形変換と事前学習済みエンティティベクトルを加えたDistMult-tanh-EV-initが73.2%でした。ただしhとtを入れ替えてもスコアが同じになるため、「AはBの上司」のような向きのある関係を区別できません。ComplEx(Trouillon ほか、ICML 2016)は複素数のベクトルにしてこの対称性を崩し、対称・反対称の両方を扱えるようにしました。RotatE(Sun ほか、ICLR 2019)は関係を複素平面上の回転として定義し、対称・反対称に加えて逆関係や関係の合成まで表せます。
選び方は関係の向きで決まります。「似ている」のような対称な関係が中心ならDistMultで足り、上司・親子・所属のような向きのある関係が多いならComplExかRotatEを使います。
PyTorch GeometricのTransEでFB15k-237を学習するコード
PyTorch Geometric 2.8系には、表の4手法がすべてtorch_geometric.nn.kgeとして同梱されています。次は公式の examples/kge_fb15k_237.py を短くしたコードで、FB15k-237(14,541エンティティ・237関係・272,115件の学習用トリプル)を自動でダウンロードして学習します。import と model の行を DistMult・ComplEx・RotatE に差し替えれば、同じ手順で比べられます。
import torch
from torch_geometric.datasets import FB15k_237
from torch_geometric.nn import TransE # DistMult・ComplEx・RotatE に差し替え可
device = "cuda" if torch.cuda.is_available() else "cpu"
train = FB15k_237("data/FB15k", split="train")[0].to(device)
test = FB15k_237("data/FB15k", split="test")[0].to(device)
model = TransE(num_nodes=train.num_nodes,
num_relations=train.num_edge_types,
hidden_channels=50).to(device)
loader = model.loader(head_index=train.edge_index[0],
rel_type=train.edge_type,
tail_index=train.edge_index[1],
batch_size=1000, shuffle=True)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(1, 51):
model.train()
for head, rel, tail in loader:
optimizer.zero_grad()
loss = model.loss(head, rel, tail) # 正例と自動生成の負例から損失を計算
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
rank, mrr, hits10 = model.test(head_index=test.edge_index[0],
rel_type=test.edge_type,
tail_index=test.edge_index[1],
batch_size=20000, k=10)
print(f"Mean Rank: {rank:.1f} MRR: {mrr:.4f} Hits@10: {hits10:.4f}")
同じDocker上のCPU環境での1回の実行では、50エポックの学習に約4分、評価に約5分かかり、Mean Rank 377.3・MRR 0.2061・Hits@10 0.3533でした。公式例は500エポックで、50エポックは動作確認用です。MRR(平均逆順位)とHits@10は、正解の尾エンティティが全候補の中で何位に来たかを測る指標です。PyGの test は尾側だけを、既知の正解を除外しない(raw)条件で評価し、Mean Rankは0始まりの順位で返すので、論文の filtered な値とは直接比べられません。モデルや損失関数を体系的に比べるなら、知識グラフ専用のPyKEEN(2026年10月時点の最新は1.11.1)を使います。
node2vecとPyTorch Geometricで動かすグラフ埋め込みの実装
構造だけを手早くベクトル化するならnode2vecライブラリ、GPUでの学習やGNNとの比較まで進むならPyTorch Geometric(PyG)です。どちらも版の組み合わせで詰まりやすいので、最初に環境を固めます。PyTorch自体の入れ方はPyTorchのできることと入れ方の解説を参照してください。
検証環境の準備:Python 3.12とtorch 2.14系・pyg-libの組み合わせ
2026年10月29日時点のPyPIの最新版は、torch-geometric 2.8.1(10月12日公開)、torch 2.15.0(10月28日公開)、node2vec 0.5.0、networkx 3.7です。詰まる箇所は3つあります。
1つ目はnode2vecライブラリが固定しているnumpyの版です。0.5.0はnumpyを2.0未満に固定しており、numpy 1.26系のwheelはPython 3.12までしかありません。networkx 3.7はPython 3.12以上を要求するため、Python 3.12の仮想環境がちょうど両方を満たします。2つ目はPyGのNode2Vecが要求するpyg-libで、未導入だと「’Node2Vec’ requires ‘pyg-lib>=0.6.0’」のImportErrorで止まります(2.8.1で確認)。PyG公式のインストール手順にも、torch-clusterの機能はpyg-lib 0.6.0以上へ移されたと書かれています。3つ目はpyg-libのwheelが出ているtorchの版で、配布表(data.pyg.org)は2026年10月29日時点でtorch 2.14系(pyg-lib 0.9.0)までです。torch 2.15.0用はまだ無いので、torchを2.14系に固定します。
# node2vecライブラリ用(Python 3.12の仮想環境で実行)
pip install "node2vec==0.5.0" "networkx==3.7" "gensim==4.4.0" "numpy==1.26.4" scikit-learn
# PyTorch Geometric用(torchを2.14系に固定してからpyg-libを入れる)
pip install "torch==2.14.*" --index-url https://download.pytorch.org/whl/cpu
pip install "torch_geometric==2.8.1" scikit-learn
pip install "pyg_lib==0.9.0" -f https://data.pyg.org/whl/torch-2.14.0+cpu.html
GPUで使う場合は、末尾の cpu を cu126・cu130 などCUDAの版に置き換えます。手元のtorchとCUDAの版は python -c “import torch; print(torch.__version__, torch.version.cuda)” で確かめられます。ソースはGitHubのpyg-libリポジトリにあります。
node2vecライブラリでNetworkXのグラフを数行で埋め込む手順
eliorc版のnode2vecはNetworkXのグラフをそのまま受け取り、ウォークの生成からgensimのword2vec学習までを内部で処理します。READMEには「Windowsではworkers=1でしか動かない」との注意書きがあり、作者はメンテナンスの継続が難しいと表明しています。
import networkx as nx
from node2vec import Node2Vec
# サンプルグラフを用意(実務では自前のグラフを読み込む)
graph = nx.fast_gnp_random_graph(n=100, p=0.5, seed=42)
# ウォーク生成の設定。Windowsはworkers=1。結果を再現するならseedに加え、起動前に環境変数PYTHONHASHSEEDも固定する
node2vec = Node2Vec(graph, dimensions=64, walk_length=30,
num_walks=200, p=1, q=1, workers=1, seed=42)
# fitはgensimのWord2Vecに引数をそのまま渡す
model = node2vec.fit(window=10, min_count=1, batch_words=4)
# ノード"2"に近いノードを類似度順に取得(ノード名は常に文字列になる)
print(model.wv.most_similar("2")[:3])
# 埋め込みをファイルへ保存(下流の学習で再利用)
model.wv.save_word2vec_format("embeddings.txt")
dimensionsが埋め込みの次元、walk_lengthとnum_walksがウォークの長さと1ノードあたりの本数で、既定値はそれぞれ128・80・10です。ノード名は整数か文字列のどちらかにそろえる必要があり、出力側では常に文字列になります。
PyTorch GeometricのNode2VecによるCoraの学習と分類精度の評価
次のコードはPyG公式のexamples/node2vec.pyをもとに、読み込みから評価までを1ファイルにまとめたものです。データはPlanetoidのCora(2,708ノード・10,556エッジ・7クラスの論文引用グラフ)です。
import sys
import torch
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import Node2Vec
dataset = Planetoid(root="data/Planetoid", name="Cora")
data = dataset[0] # data.edge_index は [2, エッジ数] のテンソル
device = "cuda" if torch.cuda.is_available() else "cpu"
model = Node2Vec(data.edge_index, embedding_dim=128, walk_length=20,
context_size=10, walks_per_node=10,
num_negative_samples=1, p=1.0, q=1.0, sparse=True).to(device)
# Windows・macOSではDataLoaderのワーカーを0にする
num_workers = 4 if sys.platform == "linux" else 0
loader = model.loader(batch_size=128, shuffle=True, num_workers=num_workers)
optimizer = torch.optim.SparseAdam(list(model.parameters()), lr=0.01)
for epoch in range(1, 51):
model.train()
total = 0.0
for pos_rw, neg_rw in loader:
optimizer.zero_grad()
loss = model.loss(pos_rw.to(device), neg_rw.to(device)) # 正例・負例のウォークから損失を計算
loss.backward()
optimizer.step()
total += loss.item()
model.eval()
with torch.no_grad():
z = model() # 学習済み埋め込み(ノード数 × 128 のテンソル)
acc = model.test(train_z=z[data.train_mask], train_y=data.y[data.train_mask],
test_z=z[data.test_mask], test_y=data.y[data.test_mask],
max_iter=150)
print(f"Epoch {epoch:02d} loss={total / len(loader):.4f} acc={acc:.4f}")
context_sizeは正例とみなす窓幅、num_negative_samplesは負例の数です。PyGのNode2Vecリファレンスのとおり、testメソッドは埋め込みを入力にロジスティック回帰を学習し、テスト側の正解率を返します。同じCPU環境での正解率は、1エポック目0.166、10エポック目0.406、50エポック目0.707でした。GCNやGATと比べるときは、同じ分割で複数の乱数シードを回し、設定の選択には検証用データ(val_mask)を使います。
埋め込みからリンク予測を試すアダマール積とロジスティック回帰
推薦や「つながりそうな相手」の提示ではリンク予測の精度が問われます。エッジの一部を隠して学習し、隠したエッジと非エッジを見分けられるかで測るのが基本形です。2ノードの埋め込みを要素ごとに掛けるアダマール積はnode2vec論文の表1にあるエッジ表現で、eliorc版にもHadamardEmbedderがあります。
import random
import networkx as nx
import numpy as np
from node2vec import Node2Vec
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
random.seed(0)
g = nx.karate_club_graph() # 34ノード・78エッジの公開データ
edges = list(g.edges())
random.shuffle(edges)
test_pos = edges[:15] # 評価用に隠すエッジ
train_g = g.copy()
train_g.remove_edges_from(test_pos)
non_edges = list(nx.non_edges(g))
random.shuffle(non_edges)
train_neg, test_neg = non_edges[:63], non_edges[63:78]
# 隠したエッジを除いたグラフだけで埋め込みを学習する
n2v = Node2Vec(train_g, dimensions=32, walk_length=20, num_walks=100,
p=1, q=0.5, workers=1, seed=0, quiet=True)
wv = n2v.fit(window=5, min_count=1).wv
def feat(pairs):
# 2ノードのベクトルのアダマール積(要素ごとの積)をエッジの特徴量にする
return np.array([wv[str(u)] * wv[str(v)] for u, v in pairs])
train_pos = list(train_g.edges())
X = np.vstack([feat(train_pos), feat(train_neg)])
y = [1] * len(train_pos) + [0] * len(train_neg)
clf = LogisticRegression(max_iter=1000).fit(X, y)
X_test = np.vstack([feat(test_pos), feat(test_neg)])
y_test = [1] * len(test_pos) + [0] * len(test_neg)
print("ROC-AUC:", roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1]))
ROC-AUCは0.5がランダム、1.0が完全な識別です。このコードを実行した値は0.684でした。34ノード・評価30組の小さな例なので、値そのものより手順の型として使ってください。注意点は隠したエッジを含むグラフで埋め込みを学習しないことです。全エッジで学習してから評価すると、答えを見て試験を受けるのと同じでスコアが実力以上に出ます。実データでは時刻で区切り、過去のエッジで学習して未来のエッジを当てると本番に近い値になります。
数百万ノードを超える大規模グラフの埋め込み方法
1台で扱える規模は、ノード数だけでなく、辺数・次数分布・ウォーク本数・特徴量次元・搭載メモリに左右されます。
大規模グラフ向けの代表例だったMetaのPyTorch-BigGraph(PBG、SysML 2019)は、グラフを分割して学習することで数十億ノード・数兆エッジまで扱える設計で、論文の要旨では、1億を超えるノードと20億のエッジを持つFreebase全体を埋め込んでいます。ただしGitHubのリポジトリは2024年3月14日にアーカイブ(読み取り専用)になっており、今後の修正や新しいPyTorchへの追随は見込めません。既存のPBG環境を動かし続ける用途を除き、2026年に新しく採用する道具ではありません。
GNNで大きなグラフを扱う現在の選択肢は、PyGのNeighborLoaderです。GraphSAGEの論文で導入された近傍サンプリングを行うローダーで、num_neighbors=[15, 10] のように層ごとに取る近傍数を決めると、グラフ全体ではなく、ミニバッチごとに必要な部分グラフだけをGPUへ送って学習できます。ランダムウォーク系では、eliorc版node2vecが全ノードの遷移確率を事前計算して保持するため、大きなグラフではこの段階でメモリが不足しがちです。ウォークをミニバッチごとに生成するPyGのNode2Vecの方が、規模の大きいグラフに向きます。
埋め込み品質の評価と解釈性・動的グラフの対処
埋め込みそのものには正解が無いので、下流タスクの成績で間接的に測ります。ノード分類なら正解率やF1、リンク予測ならROC-AUC、推薦ならヒット率です。加えてt-SNEやPCAで2次元に落とし、同じコミュニティのノードが固まるかを目で確かめます。
埋め込みの各次元には人が読める意味が無く、「なぜこの2ノードが近いのか」を説明しづらいのが弱点です。説明が求められる用途では、PyGの torch_geometric.explain で寄与の大きいノードやエッジを示します。判断根拠の可視化手法の全体像はBlack Box AIの判断根拠を可視化する実装手順で整理しています。ノードやエッジが時間とともに変わるグラフでは、GraphSAGEのような帰納的な手法か、時刻付きのイベント列を扱うTemporal Graph Network(TGN)を選びます。埋め込み同士の類似度で検索する設計は、ベクトル検索とセマンティック検索の違いの考え方がそのまま使えます。
グラフ埋め込みを採用すべき場面と見送るべき場面の判断基準
グラフ埋め込みは、Generative Recommendationのような推薦システムやソーシャルネットワーク分析、不正検知など「関係が意味を持つ」領域で効きます。どのデータにも効く前処理ではありません。
Node2Vecで止めてよい条件とGNNへ進むべき条件
判定は「ノードに使える属性があるか」「学習後にノードが増えるか」の2軸で決まります。属性が無く、ノードの追加が週次の再学習で追いつくなら、Node2Vecで止めて構いません。属性がある、または増えたノードにすぐ埋め込みを付けたいなら、GraphSAGEやGATへ進みます。GNNは学習・推論の運用コストが上がるので、Node2Vecをベースラインに残し、同じデータ分割と評価手順で上回ることを確かめてから切り替えます。ベースラインが無いと、精度が出ない原因がモデルかデータか切り分けられません。
グラフ埋め込みを採用しない方がよい4つの場面
- ノードが数百以下で、次数や共通の隣接数といった手作りの特徴量を通常の分類器に入れれば説明がつく
- 孤立ノードや小さな連結成分が多く、ウォークから下流タスクに必要な共起情報を得られない
- ノードの中身が文章で、つながりより本文の意味で探したい(テキストの埋め込みで足りる)
- 「AとBは何を経由してつながっているか」という経路そのものを答える必要がある(グラフDBのクエリで解く)
経路や関係の種類を正確に引く用途では、ベクトルの近さよりベクトルデータベースとグラフデータベースの違いで整理したグラフDB側の設計が合います。Neo4jの構造とCypherで関係を保持し、書き出したエッジでNode2Vecを学習する併用もよくある構成です。推薦に使う場合は、協調フィルタリングとの比較をレコメンドエンジンの仕組みと導入判断で確かめてから、グラフ埋め込みを特徴量に足すかを決めます。埋め込みを検索拡張生成(RAG)の検索部分に組み込む設計は、RAG構築支援で要件整理から対応しています。
よくある質問
Node2VecとDeepWalkの違いは何ですか?
どちらもランダムウォークとSkip-gramで学習しますが、DeepWalkのウォークは一様で、Node2Vecは戻りやすさpと遠くへの進みやすさqでウォークの偏りを調整できます。p・qを両方1にするとDeepWalkと同じ挙動になるので、まずNode2Vecで試せば両方を比べられます。
グラフ埋め込みとGNNは何が違いますか?
対立するものではなく、GNNは埋め込みを作る手段の一つです。GNN(GCN・GATなど)はノードの属性を反映でき、学習後に増えたノードにも適用できます。属性が無く構造だけを軽く数値化したいならNode2Vec系、属性を活かして精度を追うならGNN、と目的で選びます。
グラフ埋め込みの次元数はいくつにすればよいですか?
node2vec論文の実験設定やeliorc版の既定値に合わせて128から始め、半分ずつ減らしながら下流タスクの精度が落ちない最小の次元を探します。論文のBlogCatalogでの感度分析(Macro-F1)でも、100前後で性能が頭打ちになる傾向が出ています。
知識グラフ埋め込みのDistMultとComplExはどう使い分けますか?
DistMultは頭と尾を入れ替えてもスコアが変わらないため、対称な関係にしか向きません。上司・親子・所属のような向きのある関係を含むならComplExかRotatEを使います。PyTorch GeometricにはTransEを含む4手法がそろっており、同じコードで比べられます。
グラフ埋め込みはどのPythonライブラリを使えばよいですか?
構造だけを手早くベクトル化するならNetworkX+node2vecライブラリ(Python 3.12)が最短です。GNN・グラフ全体の埋め込み・GPU学習まで行うならPyTorch Geometric 2.8系とpyg-lib、知識グラフを体系的に比べるならPyKEENを選びます。大規模向けだったPyTorch-BigGraphは2024年3月にアーカイブされたので、新規には選びません。