古典ランダムウォーク(DeepWalkとNode2Vec)

概要

この記事では、「古典ランダムウォーク(DeepWalkとNode2Vec)」の定義や性質、応用例について詳しく説明する。ランダムウォークを用いたグラフ埋め込み手法であるDeepWalkとNode2Vecの数学的背景、アルゴリズムの詳細、具体的なPythonコードによる実装例を通じて、推薦システムなどでの利用例やメリット・デメリットを解説する。

ソースコード

github

  • jupyter notebook形式のファイルはこちら

google colaboratory

  • google colaboratory で実行する場合はこちら

実行環境

OSはmacOSである。LinuxやUnixのコマンドとはオプションが異なることに注意。

!sw_vers
ProductName:		macOS
ProductVersion:		13.5.1
BuildVersion:		22G90
!python -V
Python 3.9.17

pandasのテーブルを見やすいようにHTMLのテーブルにCSSの設定を行う。

基本的なライブラリをインポートし watermark を利用してそのバージョンを確認する。 ついでに乱数のseedの設定を行う。

%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import random

import scipy
import numpy as np

import matplotlib
import matplotlib.pyplot as plt

seed = 123
random_state = 123

random.seed(seed)
np.random.seed(seed)

from watermark import watermark

print(watermark(python=True, watermark=True, iversions=True, globals_=globals()))
Python implementation: CPython
Python version       : 3.9.17
IPython version      : 8.17.2

numpy     : 1.25.2
matplotlib: 3.8.1
scipy     : 1.11.2

Watermark: 2.4.3

古典ランダムウォークとは

定義

古典ランダムウォークとは、グラフ理論におけるランダムウォークの一種である。グラフ上のノードからスタートし、ランダムに隣接ノードへと移動するプロセスを繰り返すものである。このプロセスは確率的であり、次のノードへの移動はランダムに決定される。

ランダムウォークの確率行列 $\mathbf{P}$ は次のように定義される:

$$ \mathbf{P}_{ij} = \begin{cases} \frac{1}{d_i} & \text{if } (i, j) \in E \\ 0 & \text{otherwise} \end{cases} $$

ここで、$d_i$ はノード $i$ の次数である。

性質

  • エルゴード性:十分な長さのランダムウォークはグラフ全体をカバーする。
  • 定常分布:長い時間をかけると、ランダムウォークは定常分布に収束する。
  • マルコフ性:現在の状態のみが次の状態に依存し、過去の状態には依存しない。

DeepWalk

定義

DeepWalkは、グラフ内のノードをランダムウォークに基づいて学習する手法である。具体的には、グラフ上でランダムウォークを行い、その結果得られたノードシーケンスを用いて、Skip-gramモデルでノードの埋め込みを学習する。

アルゴリズム

  1. ランダムウォークの生成:各ノードからスタートし、ランダムに隣接ノードへ移動するウォークを複数生成する。
  2. Skip-gramモデルの学習:生成されたノードシーケンスを入力として、Skip-gramモデルを用いてノードの埋め込みを学習する。

$$ J = - \sum_{w \in V} \sum_{u \in N(w)} \log P(u|w) $$

Pythonによる実装例

以下に、DeepWalkの基本的な実装例を示す:

データとして、networkxに標準で搭載されているkarate_club_graphを使用する。

import numpy as np
import networkx as nx

from pprint import pprint

from gensim.models import Word2Vec


def random_walk(graph, start_node, walk_length):
    walk_list = [start_node]
    for _ in range(walk_length - 1):
        neighbor_list = list(graph.neighbors(walk_list[-1]))
        if neighbor_list:
            walk_list.append(np.random.choice(neighbor_list))
        else:
            break
    return walk_list


def generate_walk_list(graph, num_walk_list, walk_length):
    walk_list = []
    node_list = list(graph.nodes())
    for _ in range(num_walk_list):
        np.random.shuffle(node_list)
        for node in node_list:
            walk_list.append(random_walk(graph, node, walk_length))
    return walk_list


# グラフの作成
G = nx.karate_club_graph()

# ランダムウォークの生成
num_walks = 10
walk_length = 5
walks = generate_walk_list(G, num_walks, walk_length)

# Skip-gramモデルでの学習
model = Word2Vec(walks, vector_size=64, window=5, min_count=0, sg=1)

# ノードの埋め込みを表示
embeddings = {node: model.wv[node] for node in G.nodes()}

# 最初のノードだけ学習結果を表示
pprint(embeddings[0].round(4))
array([ 0.0003,  0.0038,  0.0068,  0.0115, -0.0065, -0.0014,  0.0095,
        0.0189, -0.002 , -0.0164,  0.0105, -0.0006,  0.0142, -0.0015,
       -0.0071, -0.0128, -0.0034,  0.0057,  0.0067,  0.0124, -0.0037,
        0.0052,  0.0179, -0.0091, -0.0047,  0.0161, -0.0006,  0.0006,
        0.0049, -0.0007,  0.0136,  0.0106, -0.0222, -0.0166,  0.0024,
        0.0117, -0.0107,  0.011 ,  0.0167,  0.0078,  0.0132, -0.0059,
        0.0016,  0.0041, -0.004 , -0.0126, -0.0022, -0.0154, -0.0094,
        0.0114,  0.0081,  0.0115,  0.0103,  0.018 ,  0.0006,  0.0024,
        0.0083, -0.0101,  0.0099, -0.0185,  0.012 ,  0.0013, -0.0087,
        0.008 ], dtype=float32)

Node2Vec

定義

Node2Vecは、DeepWalkを拡張した手法であり、ランダムウォークにおいて深さ優先探索(DFS)と幅優先探索(BFS)をバランス良く組み合わせることで、ノードの埋め込みを学習する。

アルゴリズム

  1. ランダムウォークの生成:DFSとBFSのバランスを調整するためのパラメータ $p$ と $q$ を導入し、ランダムウォークを生成する。
  2. Skip-gramモデルの学習:DeepWalkと同様に、生成されたノードシーケンスを入力として、Skip-gramモデルでノードの埋め込みを学習する。

$$ J = - \sum_{w \in V} \sum_{u \in N(w)} \log P(u|w) $$

Pythonによる実装例

以下に、Node2Vecの基本的な実装例を示す:

こちらもデータとして、networkxに標準で搭載されているkarate_club_graphを使用する。

import numpy as np
import networkx as nx

from pprint import pprint

from gensim.models import Word2Vec


class Node2Vec:
    def __init__(self, graph, p, q, num_walk_list, walk_length):
        self.graph = graph
        self.p = p
        self.q = q
        self.num_walk_list = num_walk_list
        self.walk_length = walk_length

    def node2vec_walk(self, start_node):
        walk = [start_node]
        while len(walk) < self.walk_length:
            cur = walk[-1]
            cur_nbrs = list(self.graph.neighbors(cur))
            if len(cur_nbrs) > 0:
                if len(walk) == 1:
                    walk.append(np.random.choice(cur_nbrs))
                else:
                    prev = walk[-2]
                    probs = []
                    for nbr in cur_nbrs:
                        if nbr == prev:
                            probs.append(1 / self.p)
                        elif self.graph.has_edge(prev, nbr):
                            probs.append(1)
                        else:
                            probs.append(1 / self.q)
                    probs = np.array(probs)
                    probs = probs / probs.sum()
                    walk.append(np.random.choice(cur_nbrs, p=probs))
            else:
                break
        return walk

    def generate_walk_list(self):
        walk_list = []
        node_list = list(self.graph.nodes())
        for _ in range(self.num_walk_list):
            np.random.shuffle(node_list)
            for node in node_list:
                walk_list.append(self.node2vec_walk(node))
        return walk_list


# グラフの作成
G = nx.karate_club_graph()

# Node2Vecのパラメータ設定
p = 1
q = 1
num_walks = 10
walk_length = 5

# Node2Vecのインスタンス生成とウォークの生成
node2vec = Node2Vec(G, p, q, num_walks, walk_length)
walks = node2vec.generate_walk_list()

# Skip-gramモデルでの学習
model = Word2Vec(walks, vector_size=64, window=5, min_count=0, sg=1)

# ノードの埋め込みを表示
embedding_list = {node: model.wv[node] for node in G.nodes()}

# 最初のノードの結果だけ示す
pprint(embedding_list[0].round(4))
array([-0.0029,  0.0041,  0.0041,  0.0096, -0.005 , -0.0021,  0.0089,
        0.017 , -0.0034, -0.0142,  0.0112,  0.0022,  0.0132, -0.0026,
       -0.0064, -0.0133, -0.0026,  0.0073,  0.0091,  0.0139, -0.0057,
        0.0032,  0.0166, -0.0085, -0.004 ,  0.0147, -0.0019,  0.0005,
        0.0063, -0.0011,  0.0124,  0.0105, -0.021 , -0.0161,  0.0038,
        0.0123, -0.0114,  0.01  ,  0.0152,  0.0083,  0.0139, -0.0052,
       -0.0002,  0.0031, -0.0029, -0.0106, -0.0021, -0.0151, -0.01  ,
        0.0139,  0.0078,  0.0118,  0.012 ,  0.0159, -0.0003,  0.0037,
        0.0061, -0.0099,  0.0123, -0.0165,  0.0114,  0.002 , -0.0098,
        0.0085], dtype=float32)

応用例

推薦システムでの利用

  • ユーザーとアイテムの関係性をモデル化:ユーザーとアイテムをノードとして扱い、ユーザーの行動履歴に基づくランダムウォークを行うことで、ユーザーの好みを埋め込みベクトルとして学習する。
  • ソーシャルネットワーク分析:ユーザー間の関係をグラフとして表現し、ランダムウォークを用いて関係性の強さをモデル化する。

メリット

  • スケーラビリティ:大規模なグラフに対しても効率的に計算が可能である。
  • フレキシビリティ:パラメータ $p$ と $q$ の調整により、探索の深さと幅をバランス良く調整できる。

デメリット

  • 計算コスト:非常に大規模なグラフに対しては、計算コストが増加する可能性がある。
  • ハイパーパラメータの調整:適切なハイパーパラメータの選定が結果に大きく影響する。

結論

この記事では、古典ランダムウォーク、DeepWalk、Node2Vecの定義、性質、応用例について説明した。ランダムウォークを利用したグラフ埋め込み手法は、推薦システムやソーシャルネットワーク分析などにおいて有効であることが示された。具体的なPythonコードを通じて、実装の流れも理解できたのではないだろうか。

参考文献

  • Perozzi, A., Al-Rfou, R., & Skiena, S. (2014). DeepWalk: Online Learning of Social Representations. In Proceedings of the 20th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD ‘14).
  • Grover, A., & Leskovec, J. (2016). node2vec: Scalable Feature Learning for Networks. In Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD ‘16).