Last active
July 29, 2022 08:40
-
-
Save kohiro37/d9f9bac741c8419acfdc4863497d06b5 to your computer and use it in GitHub Desktop.
夏目漱石の『こころ』から共起ネットワークを作成するPythonサンプルコード
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
夏目漱石の『こころ』から共起ネットワークを作成するPythonサンプルコード | |
コードの説明は以下のブログ | |
Pythonで共起ネットワークを作成する | |
https://irukanobox.blogspot.com/2019/10/python.html | |
『こころ』のテキスト(kokoro.txt)は、以下の青空文庫のサイトからダウンロードできるテキストからルビなどを取り除いたもの | |
https://www.aozora.gr.jp/cards/000148/card773.html | |
""" | |
import numpy as np | |
import re | |
from itertools import combinations, dropwhile | |
from collections import Counter, OrderedDict | |
import MeCab | |
import networkx as nx | |
from networkx.drawing import nx_agraph | |
# matplotlibのターミナル対応 | |
import matplotlib | |
matplotlib.use('Agg') | |
import matplotlib.pyplot as plt | |
# 対象の品詞 | |
TARGET_POS1 = ['名詞'] | |
# 対象の詳細分類1 | |
TARGET_POS2 = ['サ変接続', 'ナイ形容詞語幹', '形容動詞語幹', '一般', '固有名詞'] | |
# ストップワード | |
STOP_WORDS = ['*'] | |
def remove_blank(chapter): | |
# 空白行と段落先頭の空白を削除 | |
lines = chapter.splitlines() | |
# 空白行削除 | |
# 行頭の空白削除 | |
lines_cleaned = [l.strip() for l in lines if len(l) != 0] | |
return '\n'.join(lines_cleaned) | |
def doc2chapter(doc): | |
# 文章を章ごとに分割 | |
# タイトル削除 | |
doc = doc.replace('上 先生と私', '').replace('中 両親と私', '').replace('下 先生と遺書', '') | |
# 章番号で章ごとに分割 | |
doc_split = re.split('[一二三四五六七八九十]{1,3}\n', doc) | |
# 先頭は空白行なので削除 | |
del doc_split[0] | |
print('Total chapter number: ', len(doc_split)) | |
chapter_l = list(map(remove_blank, doc_split)) | |
return chapter_l | |
def chapter2bform(chapter_l): | |
# 章ごとに形態素解析して単語の原型のリストを作成 | |
m = MeCab.Tagger('-Ochasen') | |
m.parse('') | |
bform_2l = [] | |
for i, chapter in enumerate(chapter_l): | |
node = m.parseToNode(chapter) | |
bform_l = [] | |
while node: | |
feature_split = node.feature.split(',') | |
pos1 = feature_split[0] | |
pos2 = feature_split[1] | |
base_form = feature_split[6] | |
if pos1 in TARGET_POS1 and pos2 in TARGET_POS2 and base_form not in STOP_WORDS: | |
bform_l.append(base_form) | |
node = node.next | |
bform_2l.append(bform_l) | |
print('Term number of chapter {}: '.format(i+1), len(bform_l)) | |
return bform_2l | |
def bform2pair(bform_2l, min_cnt=5): | |
# 単語ペアの出現章数をカウント | |
# 全単語ペアのリスト | |
pair_all = [] | |
for bform_l in bform_2l: | |
# 章ごとに単語ペアを作成 | |
# combinationsを使うと順番が違うだけのペアは重複しない | |
# ただし、同単語のペアは存在しえるのでsetでユニークにする | |
pair_l = list(combinations(set(bform_l), 2)) | |
# 単語ペアペアの順番をソート | |
for i,pair in enumerate(pair_l): | |
pair_l[i] = tuple(sorted(pair)) | |
pair_all += pair_l | |
# 単語ペアごとの出現章数 | |
pair_count = Counter(pair_all) | |
# ペア数がmin_cnt以上に限定 | |
for key, count in dropwhile(lambda key_count: key_count[1] >= min_cnt, pair_count.most_common()): | |
del pair_count[key] | |
return pair_count | |
def pair2jaccard(pair_count, bform_2l, edge_th=0.4): | |
# jaccard係数を計算 | |
# 単語ごとの出現章数 | |
word_count = Counter() | |
for bform_l in bform_2l: | |
word_count += Counter(set(bform_l)) | |
# 単語ペアごとのjaccard係数を計算 | |
jaccard_coef = [] | |
for pair, cnt in pair_count.items(): | |
jaccard_coef.append(cnt / (word_count[pair[0]] + word_count[pair[1]] - cnt)) | |
# jaccard係数がedge_th未満の単語ペアを除外 | |
jaccard_dict = OrderedDict() | |
for (pair, cnt), coef in zip(pair_count.items(), jaccard_coef): | |
if coef >= edge_th: | |
jaccard_dict[pair] = coef | |
print(pair, cnt, coef, word_count[pair[0]], word_count[pair[1]], sep='\t') | |
return jaccard_dict | |
def build_network(jaccard_dict): | |
# 共起ネットワークを作成 | |
G = nx.Graph() | |
# 接点/単語(node)の追加 | |
# ソートしないとネットワーク図の配置が実行ごとに変わる | |
nodes = sorted(set([j for pair in jaccard_dict.keys() for j in pair])) | |
G.add_nodes_from(nodes) | |
print('Number of nodes=', G.number_of_nodes()) | |
# 線(edge)の追加 | |
for pair, coef in jaccard_dict.items(): | |
G.add_edge(pair[0], pair[1], weight=coef) | |
print('Number of edges=', G.number_of_edges()) | |
plt.figure(figsize=(15, 15)) | |
# nodeの配置方法の指定 | |
seed = 0 | |
np.random.seed(seed) | |
# k = node間反発係数 | |
#pos = nx.spring_layout(G, k=0.3, seed=seed) | |
# できるだけnodeが重ならないようにする(Graphvizを使う) | |
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) | |
nx.draw_networkx_nodes( | |
G, | |
pos, | |
node_color=list(pr.values()), | |
cmap=plt.cm.rainbow, | |
alpha=0.7, | |
node_size=[100000*v for v in pr.values()]) | |
# 日本語ラベルの設定 | |
nx.draw_networkx_labels(G, pos, fontsize=15, font_family='TakaoPGothic', font_weight='bold') | |
# エッジ太さをJaccard係数により変える | |
edge_width = [d['weight'] * 8 for (u, v, d) in G.edges(data=True)] | |
nx.draw_networkx_edges(G, pos, alpha=0.7, edge_color='darkgrey', width=edge_width) | |
plt.axis('off') | |
plt.tight_layout() | |
plt.savefig('co-occurance.png', bbox_inches='tight') | |
def main(): | |
with open('kokoro.txt') as f: | |
doc = f.read() | |
# 章ごとの単語原型リスト | |
bform_2l = chapter2bform(doc2chapter(doc)) | |
print(bform_2l) | |
# Jaccard係数の計算 | |
pair_count = bform2pair(bform_2l, min_cnt=5) | |
jaccard_dict = pair2jaccard(pair_count, bform_2l, edge_th=0.4) | |
# 共起ネットワーク作成 | |
build_network(jaccard_dict) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment