Pythonで共起ネットワークを作成するでは、青空文庫にある夏目漱石の『こころ』のテキストデータをもとにPythonライブラリのNetworkXとグラフ描画パッケージのGraphvizとで共起ネットワークを作成した。ここでの共起とは2単語が同じ章に出現することで、この単語ペアをカウントしてJaccard係数(Jaccard係数の計算式と特徴(1)を参照)を計算した後に、その結果をもとにネットワーク図を作成した。今回はグラフライブラリのPlotlyを利用してインタラクティブな共起ネットワーク図を作成する。
※Jupyter Lab拡張機能インストール方法の追加とソースコードコメント修正(2021/3/21)
環境
テキストデータの準備から形態素解析まで
『こころ』のテキストを章ごとに分割して形態素解析し、単語の原型リストを作成する。また、形態素解析したすべての単語を使うのでなく、名詞のうち詳細分類1が「サ変接続」「ナイ形容詞語幹」「形容動詞語幹」「一般」「固有名詞」の単語のみを対象とする。ここまではPythonで共起ネットワークを作成するのコードと同じ。
Jaccard係数の算出
Jaccard係数の算出は以前と別の方法を使う。以前は章ごとに単語ペアの数をカウントするなどしてJaccard係数を求めたが、今回はテキストをベクトル化した上でJaccrad係数を算出する。また、以前は単語ペアの出現数やJaccard係数の閾値でネットワーク図に表示されるedge数を調整するようにしていたが、この方法だと入力するテキストによってedge数が変わって汎用性がないのでJaccard係数の上位~個を対象とするように変更する。また、すべての単語を対象にするとベクトルデータのサイズが大きくなりすぎるので対象単語を文書頻度(Document Frequency)で制限する。
以下がJaccard係数を算出するコード。入力データはPythonで共起ネットワークを作成するのchapter2bformの結果(形態素解析した章ごとの単語リスト)を使う。まずは対象単語を文書頻度上位80語に限定し、章ごとのタブ区切りテキストにする。それをScikit-learnのCountVectorizerを使ってベクトルにする。その結果をone-hotベクトルにして、Scipyのpdistを使ってJaccrad係数を求める。
def filter_word_by_freqency(texts, freq_top=80):
# Document frequency
c_word = Counter([word for t in texts for word in set(t)])
top_word = [word for word, cnt in c_word.most_common(freq_top)]
texts_fitered = []
for t in texts:
filtered = list(set(top_word).intersection(set(t)))
if len(filtered) > 0:
texts_fitered.append(filtered)
return texts_fitered
def compute_jaccard_coef(texts, edge_top):
# 単語リストをタブ区切りテキストに変換
tab_separated = ['\t'.join(t) for t in texts]
# tokenizerでタブで分割するように指定
vectorizer = CountVectorizer(lowercase=False, tokenizer=lambda x: x.split('\t'))
vec = vectorizer.fit_transform(tab_separated)
# 単語リスト
words = vectorizer.get_feature_names()
# 0/1のベクトルにするためにカウント1以上はすべて1にする
vec_one = (vec.toarray() >= 1).astype(int).transpose()
# pdistを使うと結果は密行列(condensed matrix)で得られる
# pdistで得られるのは距離なので、係数にするために1から引く
jaccard_coef = 1 - pdist(vec_one, metric='jaccard')
# 密行列の順番はitertools.combinationsnの結果と一致するのでcombinationsでJaccard係数に対応する単語ペアを作成する
# How does condensed distance matrix work? (pdist)
# https://stackoverflow.com/questions/13079563/how-does-condensed-distance-matrix-work-pdist
w_pair = list(combinations(words, 2))
# 単語ペアをキーとするJaccard係数のdict
dict_jaccard = {pair: value for pair, value in zip(w_pair, jaccard_coef)}
# Jaccard係数はネットワーク図のedgeに相当する
# その数を一定数に限定する
dict_jaccard = dict(sorted(dict_jaccard.items(), key = lambda x: x[1], reverse = True)[:edge_top])
return dict_jaccard
# ルビなど除去済みのテキストを読み込む
with open('kokoro.txt') as f:
doc = f.read()
# 章ごとの単語原型リスト
bform_2l = chapter2bform(doc2chapter(doc))
texts_filtered = filter_word_by_freqency(bform_2l, freq_top=80)
jaccard_dict = compute_jaccard_coef(texts_filtered, edge_top=60)
print(jaccard_dict)
結果は次のようにJaccard係数が算出される。インタラクティブな共起ネットワークを作成
Jaccard係数が計算できたら、それをもとに共起ネットワークを作成する。NetworkXとGraphvizでネットワーク図のnodeの配置を作成し、それをもとにPlotlyでインタラクティブなネットワーク図を作成する。
def build_interactive_network(G, pos, node_sizes, node_colors):
# edgeデータの作成
edge_x = []
edge_y = []
for edge in G.edges():
x0, y0 = pos[edge[0]]
x1, y1 = pos[edge[1]]
edge_x.append(x0)
edge_x.append(x1)
edge_x.append(None)
edge_y.append(y0)
edge_y.append(y1)
edge_y.append(None)
edge_trace = go.Scatter(
x=edge_x, y=edge_y,
line=dict(width=0.5, color='#888'),
hoverinfo='none',
mode='lines')
# nodeデータの作成
node_x = []
node_y = []
for node in G.nodes():
x, y = pos[node]
node_x.append(x)
node_y.append(y)
# nodeの色、サイズ、マウスオーバーしたときに表示するテキストの設定
node_trace = go.Scatter(
x=node_x,
y=node_y,
text=list(G.nodes()),
hovertext=node_sizes,
textposition='top center',
mode='markers+text',
hoverinfo='text',
marker=dict(
showscale=True,
colorscale='Portland',
reversescale=False,
color=node_colors,
size=node_sizes,
colorbar=dict(
thickness=15,
title='Page Ranking',
),
line_width=2))
data = [edge_trace, node_trace]
# レイアウトの設定
layout=go.Layout(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
showlegend=False,
hovermode='closest',
margin=dict(b=10, l=5, r=5, t=10),
font=dict(size=10),
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis = dict(showgrid = False, zeroline = False, showticklabels = False))
fig = go.Figure(data=data, layout=layout)
fig.show()
def build_coonw(texts, freq_top=80, edge_top=60):
# 対象をDocument Frequency上位に限定
texts_filtered = filter_word_by_freqency(texts, freq_top)
dict_jaccard = compute_jaccard_coef(texts_filtered, edge_top)
print('dict_jaccard=', dict_jaccard)
# Document frequency
df = Counter([word for t in texts_filtered for word in set(t)])
# nodeリスト
nodes = sorted(set([word for pair in dict_jaccard.keys() for word in pair]))
# 単語出現数でnodeサイズを変更する
c_word = {n: df[n] for n in nodes}
G = nx.Graph()
# 接点/単語(node)の追加
G.add_nodes_from(nodes)
print('Number of nodes: {}'.format(G.number_of_nodes()))
# 線(edge)の追加
for pair, coef in dict_jaccard.items():
G.add_edge(pair[0], pair[1], weight=coef)
print('Number of edges: {}'.format(G.number_of_edges()))
# nodeの配置方法の指定
seed = 0
np.random.seed(seed)
pos = nx_agraph.graphviz_layout(
G,
prog='neato',
args='-Goverlap="scalexy" -Gsep="+6" -Gnodesep=0.8 -Gsplines="polyline" -GpackMode="graph" -Gstart={}'.format(seed))
# nodeの色をページランクアルゴリズムによる重要度により変える
pr = nx.pagerank(G)
# インタラクティブな共起ネットワークの可視化
build_interactive_network(G, pos, list(c_word.values()), list(pr.values()))
# ルビなど除去済みのテキストを読み込む
with open('kokoro.txt') as f:
doc = f.read()
# 章ごとの単語原型リスト
bform_2l = chapter2bform(doc2chapter(doc))
# 共起ネットワークの作成
build_coonw(bform_2l, freq_top=80, edge_top=60)
結果、以下のような共起ネットワークが作成される。マウスオーバーで単語数を表示したり、図のズームなどができる。
![]() |

こんにちは、共起ネットのpythonプログラムありがとうございました。源氏物語を身近に感じるためにも使わせていただきました。Plotlyでのインタラクティブなネットワーク図は目的よりも道具に関してしまいますね。
返信削除https://vanlaborg.hatenablog.com/