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/