Japan
サイト内の現在位置
Frovedisによるグラフ解析
no.0162021.11.26グラフ解析(グラフ理論)について
ソーシャルネットで表現される人と人とのつながりや物流ネットワーク、情報ネットワークなど、様々な種類のネットワークに対して関係性を解析することで何らかの予測や分類を行う、グラフ解析という機械学習の一分野があります。ここではグラフ理論と呼ばれるノードとエッジの集合で形作られるグラフについての数学的理論をベースにしてネットワークについて様々な解析が行われます。なお、グラフ理論、グラフの種類、グラフアルゴリズムについてはインターネット上に数多くある講義資料やwikipediaの解説を参照ください。
Networks: An Introduction Mark Newman(目次の他に
本文へのアクセスも可能なようです)から4つに分類されるネットワークについて引用します。

Technological networksには広域電力送配電ネットワーク、鉄道・バスや高速道路をはじめとする交通インフラネットワーク、物量配送ネットワークなどが含まれます。Googleマップやカーナビゲーションでは道路ネットワークをベースにグラフ機械学習を使用して経路探索や渋滞予測を行っているようです。
Social networksはSNSに代表される人と人のつながりの他、企業間あるいは組織間の取引、さらには送金関係といった種類のネットワークが含まれています。
インターネットやE-mailの連鎖、そして学術論文の引用関係などが3番目のNetworks of informationに分類されます。
Biological networksとは生物学的ネットワークという意味ですが、創薬分野でのグラフ解析応用が始まっているようです。例えば、正則化されたスパース生成ネットワークモデルを介したタンパク質 - タンパク質相互作用データに基づく発見(※1)のように、たんぱく質間の相互作用ネットワークをモデル化してタンパク質複合体を探索しています。
以上、幅広い領域での活用が見込まれるグラフ解析あるいはグラフ機械学習ですが、画像解析やチャットボットなどで利用される技術と比較して存在感が薄い印象を受けることは否めません。その一因として、グラフ機械学習を使用して何ができるのか漠然としており、自動運転やコールセンターサポートなど具体的なサービスに結びつけることが容易でないことがあるのかもしれません。しかし、先に上げた経路検索の例だけでなく、物流配送最適化やソーシャルネットワークを利用したマーケティングから、アンチ・マネーロンダリングや不正取引検知など、数多くの実社会での利用が始まっています。
- ※1Protein Complexes Discovery Based on Protein-Protein Interaction Data via a Regularized Sparse Generative Network Model Zhang Xiao-Fei, Dai Dao-QingLi Xiao-Xin
NetworkXとFrovedisによるグラフ解析
Pythonを使用してグラフデータ作成、グラフ表示や構造解析など行うためのライブラリとしてNetworkXが知られています。グラフオブジェクトをNetworkXのAPIに渡すことで、様々なグラフアルゴリズムを簡単に利用することができます。NetworkXには最短路問題、リンク分析、グラフ探索、グラフの中心性をはじめとして様々な種類のグラフアルゴリズムが用意されており、グラフデータのタイプや構造に応じた解析方法が提供されています。
Frovedisではこれらグラフアルゴリズムの中から以下の4種類について、NetworkXとのインターフェース互換性を保ちながら処理高速化を行っています。

グラフデータはノードとエッジの関係性を隣接行列と呼ばれる行列形式で保存しますが、多くのノード間はエッジで結合されていないため、隣接行列の要素は多数の0が占めることになります。このような場合、隣接行列は疎行列で表現できます。この特性を生かし、Frovedisでは処理を高速化しています。
NetworkX互換のFrovedisグラフアルゴリズムについて今回と次回の2回に分けて説明していきます。グラフ解析第1回はグラフ解析アルゴリズムの使用例を見ていきます。そして次回はNetworkXとFrovedisアルゴリズムの処理速度を比較する予定です。
サンプルコードの処理内容
グラフアルゴリズムを使用した解析を行うために、次に示す手順でサンプルデータを用意しました。

例えばlockdownという単語と双方向で近距離である単語の集まりは次の通りです。
['pandemic', 'virus', 'restriction', 'social-distancing', 'government-imposed', 'strictest', 'localise', 'lock-down', 'socialise', 'stay-home', 'reintroduction', 'confinement', 're-imposing', 'containment', 're-introduce', 'curfew', 'anti-virus', 'self-isolation', 'draconian', 'festivity', 'three-stage', 'quarantine', 'anti-coronavirus', 'shelter-in-place', 'resurgence', 'state-wide', 're-imposed', 'outbreak', 'country-wide', 'worst-affected', 're-impose', 'shut-downs', 'non-essential', 'nightclub', 'virus-led', 'strict', 'caseloads', 'curb', 'manila', 'pandemic-linked', 'contain', 'virulent', 'tourism-dependent', 'shutdown', 'reimpose', 'stay-at-home', 're-opened', 'relaxation', 'nonessential', 're-opens', 'reopen', 'hibernation', 'reintroduce', 'milder', 'locked-down', 'easter', 're-imposition', 'gradually', 'traveller', 'resurge', 'paralyze', 'reimposition', 'localize', 'normality', 'epicentre', 'relax', 'hotspot', 'unevenly', 'closure', 'travel', 'month-long', 'jacinda ardern', 'indoors', 'isolation', 'holidaymaker', 'wale']
エッジ集合を作成するにあたり、('pandemic': 1, 'virus': 2, 'restriction': 3…)のように各単語に独立した番号を割り当てることで、例えばE={('pandemic', ' virus '),(' pandemic ', ' restriction ')}はE={(1,2),(1,3)}へと変換しました。
全単語でエッジ集合を作成した後に、まずadd_edges_fromでNetworkX のGraphに取り込みます。さらにこれを使ってFrovedisのGraphに変換します。
以降、NetworkXとFrovedis双方のGraphアルゴリズムを使用してサンプルデータである無方向グラフデータの特徴を調べていきます。
import csv, os
import networkx as nx
import frovedis.graph as fnx
from networkx.algorithms import bipartite
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
データロード¶
f = open("graph_source_ecopol.csv", 'rt')
try:
reader = csv.reader(f)
words = [i for i in csv.reader(f, delimiter=',')]
finally:
f.close()
L = {}; g_L = []; my_word = []; my_number = []
for i in range(len(words)):
for j in range(len(words[i])-1):
key = words[i][0]
info = words[i][1:]
L[key] = info
グラフデータのための前処理: 全ての単語にユニークな番号を割り振り¶
count = 0
for key, val in L.items():
if (count % 5000) == 0:
print(key, val)
count += 1
for e in L.keys():
for w in L[e]:
my_word.append(w)
my_word.append(e)
unq, idx = np.unique(my_word, return_index=True)
sorted_idx = np.sort(idx)
buf = [my_word[i] for i in sorted_idx]
for e in range(len(buf)):
my_number.append(e+1)
wd_df = pd.DataFrame({'number': my_number,'word': buf}, columns=['number', 'word'])
wd_df = wd_df.set_index('number')
market ['stock market', 'financial market', 'violently', 'capitulation', 'market participant', 'pull-back', 'directionless', 'investor', 'axi', 'jj', 'rangebound'] premier ['liu he', 'li keqiang', 'shouwen', 'handshake', 'top-level', 'mar-a-lago']
グラフデータのための前処理: 各単語と割り振った番号による辞書化、ユニークな番号によるノード組合せ作成¶
word_dic = {buf[i]: my_number[i] for i in range(len(my_number))}
g_L = [(word_dic[key], word_dic[val[w]]) for key, val in L.items() for w in range(len(list(val)))]
Frovedisサーバ起動¶
from frovedis.exrpc.server import FrovedisServer
FrovedisServer.initialize("mpirun -np 8 " + os.environ["FROVEDIS_SERVER"])
'[ID: 1] FrovedisServer (Hostname: handson02, Port: 34905) has been initialized with 8 MPI processes.'
G = nx.Graph()
G.add_edges_from(g_L)
fG = fnx.Graph(G)
Frovedis descendants_at_distanceとNetworkX neighborsでノード'bank'とエッジで結ばれるノードを参照¶
print(word_dic['gdp'])
descendants = [wd_df.loc[w].to_numpy()[0] for w in\
list(fnx.descendants_at_distance(fG, source=word_dic['gdp'], distance=1))]
print(sorted(descendants))
neigi = [wd_df.loc[w].to_numpy()[0] for w in list(G.neighbors(word_dic['gdp']))]
print(sorted(neigi))
print(sorted(descendants) == sorted(neigi))
172 ['19-member', 'bea', 'cbo', 'cpb', 'debt/gdp', 'economy', 'growth', 'illustrative', 'nowcast', 'shallower'] ['19-member', 'bea', 'cbo', 'cpb', 'debt/gdp', 'economy', 'growth', 'illustrative', 'nowcast', 'shallower'] True
Frovedis connected_componentsで1つ以上のエッジを経てつながっている全ノード数を表示¶
cc = sorted((fnx.connected_components(fG)), key=len)
tmp = []
print("connected_components")
for w in range(len(cc)):
tmp.append(len(list(cc[w])))
print(tmp)
small_g = [i / np.array(tmp).sum() for i in tmp]
cc_1 = [list(cc[i]) for i in range(len(small_g)) if small_g[i] < 0.01]
tmp = []; cc_2 = []
for i in range(len(cc_1)):
for j in range(len(cc_1[i])):
tmp.append(wd_df.loc[cc_1[i][j]].to_numpy()[0])
cc_2.append(tmp)
tmp = []
print("small size clusters")
print(cc_2)
connected_components [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 17977] small size clusters [['inflow', 'flow'], ['garner', 'draw'], ['winner', 'loser'], ['floor', 'cashin'], ['consolidate', 'consolidation'], ['definition', 'define'], ['con', 'pro'], ['c-band', 'spectrum'], ['structure', 'variable'], ['breathe', 'sigh'], ['makeup', 'composition'], ['revert', 'revisit'], ['territory', 'uncharted'], ['concurrent', 'simultaneous'], ['serf', 'serve'], ['replacement', 'replace'], ['withdraw', 'withdrawal'], ['firmer', 'quote'], ['viacom', 'cbs'], ['fill', 'void'], ['hammond', 'carlson', 'horizon']]
dist = [len(fnx.descendants_at_distance(fG, source=w, distance=2)) for w in my_number if w % 100 ==0]
bins = range(1,1000)
fig = plt.figure()
ax1 = fig.add_subplot(111)
plt.hist(dist, bins=100)
ax1.set_xlabel('number of all nodes at a fixed distance from source in G.')
ax1.set_ylabel('Freq.')
plt.show()
for w in my_number:
if w % 100 == 0:
dist_list = fnx.descendants_at_distance(fG, source=w, distance=2)
if len(dist_list) > 3500:
print(wd_df.loc[w].to_numpy(), [wd_df.loc[e].to_numpy()[0] for e in dist_list][:50])
['intensification'] ['whitmer', 'metastasize', 'financial market', 'violently', 'capitulation', 'market participant', 'pull-back', 'directionless', 'investor', 'inertia', 'ever-increasing', 'rangebound', 'hyper', 'beijing', 'infancy', 'u.s.', 'dependency', 'counter-measures', 'cui', 'chinese goods', 'retaliate', 'country', 'phase-1', 'willems', 'countermeasure', 'tariff', 'gesture', 'guodu', 'chinese economy', 'barley', 'china', 'repriced', 'unhelpful', 'grape', 'virus', 'pandemic', 'outbreak', 'fast-spreading', 'epidemic', 'pneumonia-like', 'unison', 'flu-like', 'virus-related', 'virus-led', 'disease', 'infect', 'replenishment', 'virulent', 'chilly', 'pneumonia'] ['stabilizer'] ['metastasize', 'inertia', 'ever-increasing', 'hyper', 'infancy', 'revalue', 'environmentally-friendly', 'monetisation', 'u.s.-china', '16-month-long', 'trade conflict', 'greenium', 'dependency', 'zurbruegg', 're-pricing', 'repriced', 'leer', 'unhelpful', 'adjunct', 'hillman', 'central', 'rrrs', 'sarb', 'pandemic', 'outbreak', 'unison', 'epidemic', 'replenishment', 'rapidly-spreading', 'maximise', 'virulent', 'intimate', 'pneumonia', 'continual', 'reappear', 'sickness', 'payoff', 'juicing', 'veracity', 'pathogen', 'vanilla', 'second-wave', 'country-wide', 'stickiness', 'convenient', 'devastation', 'fabian', 'deregulate', 'vocal', 'powell-led'] ['unequivocal'] ['whitmer', 'metastasize', 'inertia', 'ever-increasing', 'fa', '16-month-long', 'zurbruegg', 'counter-measures', 'annette', 'cui', 'trichet', 'nadia', 'liu he', 're-pricing', 'angus', 'willems', 'emeritus', 'winston', 'nyu', 'leer', 'unhelpful', 'intellectual', 'adjunct', 'hillman', 'central', 'sarb', 'unison', 'cynthia', 'baldwin', 'rapidly-spreading', 'feinstein', 'rohit', 'intimate', 'continual', 'sickness', 'headlong', 'veracity', 'lecturer', 'stern', 'relapse', 'second-wave', 'elaine', 'tremor', 'six-member', 'monkey', 'devastation', 'fomc', 'jerome powell', 'fed chairman', 'mester']
Frovedis single_source_shortest_pathによる指定したノードからスタートして最短距離で辿り着ける全てのノード数を表示¶
path = list(fnx.single_source_shortest_path(fG, word_dic['gdp']))
print(len(path))
17977
Frovedis pagerankによる多数のノードから接続されているノード(単語)をリストアップ¶
pr = fnx.pagerank(fG, alpha=0.9)
pr_top = []
for key, val in pr.items():
if val >= 0.0001:
pr_top.append(wd_df.loc[key].to_numpy()[0])
print("pagerank: \n",pr_top)
print(" \n\n")
pagerank: ['guodu', 'barley', 'nervously', '1q', 'home-price', 'pced', 'hem', 'flatlining', 'lobster', 'tariff-related', '15-day', 'valneva', 'vaccine-related', 'no-touch', 'ointment', 'encouragingly', 'townswick', 'record-keeping', 'tourism-related', 'markdowns', 'high-contact', 'garthwaite', '10-years', 'back-to-school', 'stonks', 'newbie', 'heavily-shorted', 'mb/d', 'collateralised', 'cy20', 'drub', 'dovishness', 'decarbonisation', 'favourably', 'cryptos', 'rehired', 'fcf', 'undersupply', 'prime-age', 'market-leading', 'clamour', 'gbp', 'allocator', 'bandwagon', 'preeminent', 'cny', 'klieve', 'khajuria', 'matson', 'overproduction', 'hydroxychloroquine', 'run-rate', 'higher-growth', 'high-priced', '4q', 'homebuyers', 'semi', 'sawada', 'overweighting', 'meander', 'short-duration', 'breber', 'substitution', 'on-chain', 'nflx', 'wiser', 'absorption', 'byproduct', 'chehab', 'tidy', 'stickiness', 'wiederhorn', 'ricchiuti', 'faster-growing', 'alright', 'hubris', 'de-listing', 'pull-backs', 'usd/jpy', 'cta', 'allot', 'defensiveness', 'pugh', 'sterne', 'venkateswaran', 'paracuelles', 'roache', 'elise', 'gamaleya', 'kintor', 'pekao', 'siena', 'trillion-a-day', 'ako', 'tomas', 'ici', 'concurrently', 'codelco', 'burkina', 'harmony', 'scandal-plagued', 'third-', 'sharara', 'whistle', 'havana', 'comedy', 'comic', 'hong kong', 'shanxi', 'liao', 'kitao', 'knof', 'kxl', 'andrei', 'rosn.mm', 'maine', 'evacuation', 'monde', 'reorganisation', 'chinese-owned', 'rhode', 'saikawa', 'ky', 'faa', 'deripaska', 'keystone', 'ou', 'circus', 'manchester', 'jean-pierre', 'gpb', 'sheryl', 'renmin', 'video-sharing', 'bharat', 'jo', 'hmc', 'metallon', 'indaba', 'arnaud', 'canzonieri', 'ortega', 'businesswoman', 'christophe', 'kazakh', 'spalding', 'drake']
NetworkXのdegree_centralityによる中心性を確認と、ネットワークを視覚化¶
tmp_list = list(nx.degree_centrality(G).items())
tmp_df = pd.DataFrame(tmp_list, columns=['number', 'degree'])
wd_df_pr = pd.merge(tmp_df, wd_df, on='number')
print(wd_df_pr.sort_values(by='degree', ascending=False))
number degree word 7769 7770 0.013874 inescapable 8615 8616 0.013874 preoccupation 16515 16516 0.013874 businesswoman 2157 2158 0.013874 priced-in 8527 8528 0.013874 overtly ... ... ... ... 16569 16570 0.000055 sba 16568 16569 0.000055 mta 16567 16568 0.000055 alex 16565 16566 0.000055 oslo 18019 18020 0.000055 mengniu [18020 rows x 3 columns]
fig = plt.figure()
ax1 = fig.add_subplot(111)
bins = range(1,1000)
plt.hist(nx.degree_centrality(G).values(), bins=100)
ax1.set_xlabel('The degree centrality values')
ax1.set_ylabel('Freq.')
plt.show()
pos = nx.spring_layout(G)
nx.draw_networkx(G, pos, with_labels=True, alpha=0.5)
plt.axis("off")
plt.show()
plt.savefig("network.jpg", dpi=1000)
<Figure size 432x288 with 0 Axes>
FrovedisServer.shut_down()
はじめに、あるノードに対して隣接して接続されるノードの数を返すNetworkXのneighborsアルゴリズムを使い、’gdp'とエッジを経て隣接するノード(単語)を見ています。bea: U.S. Bureau of Economic Analysis, cbo: Congressional Budget Officeを筆頭に多くは経済(国内総生産)に関連する単語が表示されています。
続いてFrovedisのアルゴリズムを使った処理に移ります。まず、connected_componentsで1つ以上のエッジを経てつながっているノード数を表示しています。サンプルデータが22の独立したグループで構成され、一つの大きなグループ(17977ノード)と、その他2ないし3つのノードで構成される小グループであることが分かります。このことから、大多数のノードがなんらかのパスを経てお互いに接続されていることが分かります。
次のdescendants_at_distanceは指定する距離内でエッジにより結ばれるノードのリストを返します。サンプルコードではdistanceを2としていますが、各ノードの隣接ノードからさらに隣接するノードをリストアップすることになります。2つまでのエッジで結ばれる範囲内に存在するノード数は500以下が多く、その先は次第に減少傾向を示しています。3000を超えるノード同士が接続されているものも少数ながら存在し、そこでは密なクラスターが形成されていることがうかがえます。
single_source_shortest_pathは指定したノードからスタートして最短距離で辿り着ける全てのノードとルートを返すものです。この例(ノード’gdp'を起点)では自身を含めて17977のノードにつながっていることを示しています。このことからノード’gdp’がconnected_componentsで示された一つの大きなグループ内に含まれていることを示しています。
pagerankではより多数のノードから接続されているノードを見つけ出しています。pagerankが返す係数が一定以上であるノードを表示しました。個人名や株銘柄を示すティッカーコード、15-dayやshort-durationのような期間を示すものなど、それら大半はあまり大きな意味を持たない単語がリストアップされています。他の多数ノードからリンクされているこうした単語の扱い(例えば、単語リストから削除)によって、グラフ特性に変化がみられることが考えられます。また、LDAなど他の処理の結果にも影響を与える可能性があります。
最後にNetworkXのdegree_centralityを使って多くのノードと隣接しているノードの確認(次数中心性)と、ネットワークを視覚化します。サンプルデータでは多数のノードを結ぶハブ的なノードが存在する一歩で、疎な結合状態のノードが多数存在していることを示しています。これは先に示したdescendants_at_distanceで得られた結果を裏付けるものと言えます。
NetworkXとより高速なFrovedisのGraphアルゴリズムを使い分けながら、ネットワークの特徴を調べる一例を紹介しました。今回は無向グラフであるサンプルデータを使用しましたが、単語間距離の重みへの追加や、有向グラフ化するなど、より複雑さを加えることでさらにネットワークが有する特性を深く調べることができる可能性があります。
巨大なネットワークを有するグラフデータを扱う場合、Graphアルゴリズムの計算が長時間化することが考えられます。Frovedisの高速化アルゴリズムを使うことで、より短時間なグラフネットワーク解析が可能となります。
次回は、先に述べた通り、FrovedisとNetworkXのアルゴリズム実行時間比較をご説明します。
関連リンク
最新資料/カタログはこちらからダウンロード
ご紹介資料、各種カタログをダウンロードいただけます。
My NEC登録が必要です
My NEC登録が必要です
