何が起きたか

線形代数の基礎的な性質だが長く軽視されてきた「行列とグラフの等価性」に改めて注目が集まっている。行列を隣接行列としてグラフで表現し、逆にグラフを行列にエンコードすることで、抽象的な代数計算が直感的な図形問題へ変換できることが再評価されている。グラフニューラルネットワーク(GNN)やスペクトラルグラフ理論の実用化が加速し、エンジニアにとって「行列とグラフは同じもの」という視点が不可欠になりつつある。

行列とグラフが等価である理由

n×n行列Aは、n個のノードをもつグラフの隣接行列として完全に解釈できる。行列要素 A[i, j] がノードiからノードjへのエッジの重みに対応する。逆に、あらゆるグラフはその隣接行列で数学的に完全に記述される。

最も単純な例として、2ノードの無向グラフ(○─○)を考える。

import numpy as np
import networkx as nx

# グラフを定義:2ノード、1エッジ
G = nx.Graph()
G.add_edge(0, 1)

# グラフ → 行列(隣接行列)に変換
A = nx.to_numpy_array(G)
print(A)
# 出力:
# [[0. 1.]
#  [1. 0.]]

# 行列 → グラフに戻す
G2 = nx.from_numpy_array(A)
print(list(G2.edges()))
# 出力: [(0, 1)]

この双方向変換が成立するということは、グラフに対して行いたい操作(最短経路探索、クラスタリング、中心性計算)がすべて行列演算として表現できることを意味する。PageRankアルゴリズムも本質的には「Webグラフの隣接行列のべき乗計算」だ。

スペクトラル理論:固有値がグラフを語る

行列の固有値・固有ベクトルはグラフのスペクトラル性質に直結している。グラフラプラシアン行列 L = D - A(DはD次数行列、Aは隣接行列)の固有値には、グラフの構造的特性が凝縮されている。

import scipy.linalg as la
import numpy as np

# ラプラシアン行列の計算
def graph_laplacian(G):
    A = nx.to_numpy_array(G)
    D = np.diag(A.sum(axis=1))  # 次数行列
    L = D - A
    return L

# 5ノードのサンプルグラフ
G = nx.karate_club_graph()
L = graph_laplacian(G)

# 固有値分解(スペクトラル分析)
eigenvalues, eigenvectors = la.eigh(L)

# 2番目に小さい固有値(Fiedler値)= グラフの連結性の強さ
fiedler_value = eigenvalues[1]
print(f"Fiedler値(連結性): {fiedler_value:.4f}")

# 対応する固有ベクトル = スペクトラルクラスタリングの基底
fiedler_vector = eigenvectors[:, 1]
# 符号に基づいてノードを2クラスタに分類
clusters = (fiedler_vector > 0).astype(int)
print(f"クラスタ割り当て: {clusters[:10]}")

固有値が0の個数はグラフの連結成分の数と一致する。第2固有値(Fiedler値)はグラフの「切断のしにくさ」を示し、ネットワーク設計やクラスタリングの指標として利用される。このような性質がすべて行列演算から導き出せる点に、等価性の真価がある。

行列・グラフ変換の全体像

graph LR A["グラフ
(ノード・エッジ)"] -->|"隣接行列化
nx.to_numpy_array()"| B["密行列
Dense Matrix
O(n²) メモリ"] A -->|"疎行列化
nx.to_scipy_sparse_array()"| C["疎行列
Sparse Matrix (CSR)
O(n+e) メモリ"] B -->|"固有値分解"| D["スペクトラル分析
クラスタリング・PageRank"] C -->|"メッセージパッシング"| E["グラフニューラルネット
GNN・GCN"] D -->|"グラフ再構築"| A E -->|"ノード埋め込み出力"| F["下流タスク
分類・推薦・予測"] style A fill:#4a90d9,color:#fff style E fill:#ff9900,color:#000 style F fill:#27ae60,color:#fff

大規模ネットワークでは密行列(Dense Matrix)のO(n²)メモリが致命的になる。Twitterのフォローグラフ(数億ノード)を密行列で保持すると、10のエクサバイト単位のメモリが必要になる計算だ。疎行列(Sparse Matrix)への変換が必須となる。

実装手法の比較

手法 表現形式 メモリ計算量 演算計算量 適した用途
密行列(NumPy) ndarray O(n²) O(n³) 小規模グラフ(< 10,000ノード)
疎行列CSR(SciPy) data/indices/indptr O(n+e) O(n+e) 大規模疎グラフ
隣接リスト(NetworkX) dict of sets O(n+e) トラバーサルO(n+e) グラフアルゴリズム中心の処理
PyTorch Geometric edge_index Tensor O(e) GPU並列 グラフニューラルネット訓練
DGL(Deep Graph Library) HeteroGraph O(n+e) GPU並列 異種グラフ・大規模GNN

GNN:行列・グラフ等価性を活用した深層学習

グラフニューラルネットワーク(GNN)は、この等価性を深層学習に直接応用した手法だ。PyTorch Geometricを用いると、グラフ畳み込みが数行で実装できる。

import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
from torch_geometric.data import Data

# グラフデータの定義
# edge_index: エッジリスト(2×E テンソル)
edge_index = torch.tensor([[0, 1, 1, 2],
                            [1, 0, 2, 1]], dtype=torch.long)

# ノード特徴量(4ノード、各3次元)
x = torch.tensor([[-1, 0, 1],
                   [0, 1, -1],
                   [1, -1, 0],
                   [0, 0, 1]], dtype=torch.float)

# グラフオブジェクトの生成
data = Data(x=x, edge_index=edge_index)

# 2層GCNの定義
class GCN(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = GCNConv(3, 16)   # 3次元 → 16次元
        self.conv2 = GCNConv(16, 2)    # 16次元 → 2クラス

    def forward(self, data):
        x, edge_index = data.x, data.edge_index
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = self.conv2(x, edge_index)
        return F.log_softmax(x, dim=1)

model = GCN()
# このGCNConvの内部では隣接行列との行列積が実行されている

GCNのメッセージパッシングは本質的に「隣接ノードの特徴量の加重平均」であり、隣接行列の行列積として表現される。グラフ構造という複雑なデータを行列演算のパイプラインに落とし込むことで、GPUによる大規模並列処理が可能になる。

エンジニアへの実務的影響

デバッグ効率の劇的向上

従来のデバッグでは「なぜノードAからノードBへ到達できないのか」を隣接リストのトレースで追っていた。行列表現に変換してヒートマップを描くと、接続の欠落が視覚的に一目瞭然になる。バグ発見の時間が短縮される。

推薦システムの最適化

ユーザー・アイテム行列を協調フィルタリングの基盤として使う場合、特異値分解(SVD)によって潜在的な嗜好構造が抽出できる。同じ計算をグラフとして捉えると、「ユーザーAと類似したユーザーが好むアイテム」を2ホップの近傍探索として実装できる。SpotifyやPinterestはこのグラフベース推薦を本番運用している。

データパイプラインでの活用

Apache Airflowで構築するデータパイプラインのDAG(有向非巡回グラフ)も行列として表現できる。依存関係の循環検出(サイクル検出)はグラフのトポロジカルソートであり、隣接行列のべき乗計算で効率的に実装できる。

知識グラフとLLMの融合

LangChainではグラフ型RAG(Graph RAG)が注目を集めている。エンティティとその関係をグラフとして格納し、クエリ時にグラフ探索で関連情報を取得する。この処理の内部は常に行列演算に帰着する。

PageRankを行列演算として実装する

PageRankはGoogleが開発したWebページの重要度スコアリングアルゴリズムだが、本質は隣接行列のべき乗計算だ。「重要なページからリンクされているページは重要」という再帰的定義を、行列の固有値問題として解く。

import numpy as np
import networkx as nx

def power_iteration_pagerank(G, damping=0.85, max_iter=100, tol=1e-6):
    """
    PageRankを冪乗法で実装
    G: 有向グラフ(networkx.DiGraph)
    damping: 減衰係数(デフォルト 0.85)
    """
    n = len(G.nodes())
    nodes = list(G.nodes())
    node_idx = {node: i for i, node in enumerate(nodes)}

    # 遷移行列の構築(列方向が正規化されたリンク先)
    A = np.zeros((n, n))
    for node in G.nodes():
        out_edges = list(G.out_edges(node))
        if out_edges:
            for _, target in out_edges:
                A[node_idx[target]][node_idx[node]] = 1.0 / len(out_edges)
        else:
            # ダングリングノード:全ノードに均等にリンク
            A[:, node_idx[node]] = 1.0 / n

    # Googleマトリックス(テレポーテーション項を追加)
    M = damping * A + (1 - damping) / n * np.ones((n, n))

    # 冪乗法(固有ベクトルを反復計算)
    rank = np.ones(n) / n
    for i in range(max_iter):
        new_rank = M @ rank
        # 収束判定
        if np.linalg.norm(new_rank - rank) < tol:
            print(f"収束: {i+1} 回の反復")
            break
        rank = new_rank

    return {nodes[i]: rank[i] for i in range(n)}

# サンプルの有向グラフでテスト
G = nx.DiGraph()
G.add_edges_from([("A", "B"), ("A", "C"), ("B", "C"), ("C", "A")])
ranks = power_iteration_pagerank(G)
for node, score in sorted(ranks.items(), key=lambda x: -x[1]):
    print(f"  {node}: {score:.4f}")
# C: 0.3879  ← 最も重要(複数からリンク)
# A: 0.3292
# B: 0.2829

この実装は、Googleが1998年に発表した論文「The PageRank Citation Ranking: Bringing Order to the Web」のアルゴリズムを忠実に再現している。行列のべき乗計算という単純な操作が、Webの構造的重要度を数値化する。グラフ問題が行列問題として解けることの実用例だ。

疎行列の実装:実用上の注意点

大規模グラフを扱う場合、密行列でのO(n²)メモリは現実的でない。Twitterのフォロワーグラフや分子構造グラフは疎行列で扱わなければならない。

import scipy.sparse as sp
import numpy as np

# 疎行列(CSR形式)でのグラフ表現
# 100万ノード、10万エッジのスパースグラフ
n_nodes = 1_000_000
rows = np.random.randint(0, n_nodes, size=100_000)
cols = np.random.randint(0, n_nodes, size=100_000)
data = np.ones(100_000)

# CSR(Compressed Sparse Row)形式
A_sparse = sp.csr_matrix((data, (rows, cols)), shape=(n_nodes, n_nodes))

# 密行列での必要メモリ: 10^12 bytes = 1TB(float32の場合)
# 疎行列での実際のメモリ: 約2.4MB
print(f"密行列の理論メモリ: {n_nodes**2 * 4 / 1e9:.0f} GB")
print(f"疎行列の実際メモリ: {A_sparse.data.nbytes / 1e6:.1f} MB")

# ラプラシアン計算も疎行列のまま実行
degree = np.array(A_sparse.sum(axis=1)).flatten()
D_sparse = sp.diags(degree)
L_sparse = D_sparse - A_sparse
# O(n+e) メモリで大規模グラフを処理可能

CSR形式は行方向のランダムアクセスを効率化した疎行列形式で、PyTorchの torch.sparse_csr_tensor も同じ思想を採用している。スパースグラフを密行列で扱うのは、空白の多いスプレッドシートをすべてのセルに0を入力して保存するようなものだ。

今後の展開:LLMとグラフの融合

Transformerのアテンション機構は本質的に「グラフ上のメッセージパッシング」として解釈できる。各トークンがノードで、アテンション重みがエッジ重みに対応する。この視点から、Transformerの改善手法(Sparse Attention、Linformerなど)はすべてグラフの疎化問題として統一的に理解できる。

行列とグラフの等価性は「教科書の話」から「GPUクラスタ上の本番AIシステムの設計原理」に昇格した。この視点を持つエンジニアと持たないエンジニアの差は、複雑な機械学習システムの設計・デバッグで鮮明に現れる。

参照ソース


この記事はAI業界の最新動向を速報でお届けする「AI Heartland ニュース」です。