1643
0
0

テキストマイニング(KH Coder, Pythonによる共起ネットワーク作成)

Published at December 8, 2021 9:51 a.m.
Edited at December 19, 2021 2:20 a.m.

ニュース、SNS投稿、アンケート、書籍/論文などの文章に含まれる単語間の共通性を見出し、図で表現する共起ネットワークと呼ばれる方法があります。

この共起ネットワークは、テキストから隠れた発見を見出すテキストマイニングの一つの手法として、最近は身近になりつつあります。

今回は、小説に対してKH Coder*やPythonを使って共起ネットワークを作り、そこからどういったことを示唆できるのか考えたいと思います。

* KH Coderは、タダで使える主にWindows向けのテキストマイニングツールです。

目次

  1. 本ページのゴール
  2. 共起ネットワークとは
  3. 共起ネットワークを作る前に
  4. 利用データ
  5. KH Coderの共起ネットワーク描画
  6. Pythonの共起ネットワーク描画
  7. まとめ


本ページのゴール

共起ネットワークを作り、その結果を説明できるようになることです。
image
※上記の図は夏目漱石「こころ」から作成されている共起ネットワーク(引用:KH Coderのチュートリアル)

  • 一番他のワードと頻度が高く出現するワードは”K”である(”K”は小説内に登場する友人の呼び名) ※一番大きな円
  • ”聞こえる”と”声”の関連が強いため、「聞こえるモノの多くは声だ」ということ ※太い線
  • ”食う”と”飯”の関連が強いため、「食うモノの多くは飯だ」ということ ※太い線
  • ”書く”と”手紙”の関連が強いため、「書くモノの多くは手紙だ」ということ ※太い線
  • "K"と”見る”と”顔”の関連が強いため、「Kが見るモノの多くは顔だ」ということ ※太い線
  • ”K”と”自分”と”思う”の関連が強いため、「Kと自分は”思う”ことが多い」ということ


共起ネットワークとは

共起ネットワークは単語間の関係性を図で表現します。

この共起ネットワークに使う値としては、jaccard係数(同時登場数による類似度)やCos類似度(ベクトルを使って向きが近い:1に近ければ類似度が高いと判定する)などがあります。
今回はシンプルで分かりやすいjaccard係数をベースに解説します。

jaccard係数を使った場合、文章の中で”私”、”見る”という単語が同時に出現する事が多いと、”私”と”見る”の円がそれぞれ描かれ、線でつながれます。

jaccard係数

その名の通り、Jaccardさんが考案したサンプル(単語などの)セットの類似度を測る指標です。

以下の式(「AかつB」を「AまたはB」の出現数で割る)で計算されます。
image
※「AまたはB」は、Aの出現数とBの出現数を足し、AとBの同時出現(二重計上)を引いて求める。

この数式は「単語Aと単語Bが文章に登場する際に、どの程度一緒に登場するか」を示します。

例えば、「私(A)」が80の文書に登場、「見る(B)」が100の文書に登場、「私(A)」と「見る(B)」が同時に20の文書で登場したとします。この場合の Jaccard係数は以下のように計算されます。

A∧B: 20 ÷ (A: 80 + B: 100 ー A∧B: 20)=0.25

Jaccard 係数が大きいほど、同時登場が多い単語の組み合わせになるため、その二つの語は「近い」と判断する訳です。


共起ネットワークを作る前に

文章をそのまま扱うには、頻度計算などが大変です。そのため、テキストマイニングの最初の一歩として分かち書きという処理を行います。

分かち書き

テキストマイニングにおける分かち書きとは、文章を単語に分解することです。

例えば、以下の文章があったとします。
 毎日、私は朝食として牛乳とパンを食べてから、出社します。

これを分かち書きすると、以下のように分解されます。
 毎日 、 私 は 朝食 として 牛乳 と パン を 食べて から 、 出社 します 。

このように単語を品詞に分解すると、扱いやすく(頻度等の計算がやりやすく)なります。
KH Coder や Python(MeCabなどのライブラリを利用)を使うと簡単に分かち書きできます。


利用データ

自分で共起ネットワークを作ると感覚がつかめます。そのためにはテストデータが必要です。
テストデータは、Twitterからスクレイピングしてみてもよいのですが、別の知識が求められるので、インターネットからダウンロードしたいと思います。

テストに使うおススメのデータは「著作権の消滅した作品」と「自由に読んでもらってかまわない作品」がまとまっている青空文庫です。
今回は、青空文庫から、芥川 竜之介の羅生門をダウンロードします。

青空文庫からのダウンロード

青空文庫では、作品名、あるいは作者で作品を検索できます。
例えば、羅生門で検索すると、以下のページになります。

image

このページ下部の「ファイルのダウンロード」で、ファイル名(リンク)をクリックすると、作品のテキスト(圧縮ファイル)を取得できます。

image

圧縮ファイルを解凍し、テキストエディタでデータを開くと以下のようになります。

image

データクレンジング

このままでは、ルビが集計されるなどの問題があるので、軽くデータクレンジングします。

ルビの削除は、正規表現を扱えるテキストエディタで正規表現:《[^》]*》で検索し、削除するだけです。削除すると以下のデータに変わります。《と》で括られていた部分がなくなったのが分かりますでしょうか。
image

この他に[#と]で囲われた注も、正規表現:[#[^》]*]で検索し、削除します。最後に、テキストファイルの上部と下部にあった余分な情報を消すと、仕上がります。
このデータの文字コードはSJISからUTF-8 BOM有に変更すると、Pythonで扱いやすくなります。

KH Coderフォルダのサンプルデータ

KH Coderをダウンロードいただくと「C:\khcoder3\tutorial_jp」のフォルダに、夏目漱石の”こころ”のxls(kokoro.xls)が含まれています。
そちらを使うとチュートリアルと同じ分析をすることもできます。


KH Coderによる共起ネットワーク描画

データの準備ができたので、KHCoderで共起ネットワークを作ります。

KH Coderのセットアップ

KHCoderのwebサイトより、「ダウンロードと使い方」にあるダウンロードのリンクをクリックしてください。
image

その後、フリー版のリンクからEXEファイルをダウンロードください。(600MB以上のファイルなので、ダウンロードに多少時間かかります)
image

ダウンロードしたファイルをダブルクリックして実行ください。以下のように解凍のウィンドウが立ち上がるため、Unzipを実行ください
image

これで「C:\khcoder3」というフォルダにファイルが展開されます。

KH Coderでの文書読み込み

KH Coderは「C:\khcoder3\kh_coder.exe」をダブルクリックすると起動します。
image

まずは上部メニューで[プロジェクト]-[新規]を選択します。
「新規プロジェクト」のウィンドウが立ち上がります。

image

参照ボタンを押下して、分析するファイル(rashomon.txt)をセットすると、初期表示されるウィンドウに戻ります。
image

次に、上部メニューで[前処理]-[前処理の実行]を選びます。そうすると以下のウィンドウが立ち上がるので、そのままOKを選択します。
image

処理が終わると以下のような処理時間が記載されたウィンドウが表示されます。
image

共起ネットワーク描画

前処理が終わりましたので共起ネットワークを表示します。
上部メニューで[ツール]-[抽出語]-[共起ネットワーク]を選びます。そうすると以下のウィンドウが立ち上がるので、そのままOKを選択します。

image

そうすると、以下のように共起ネットワークが作成されます。
image

共起ネットワークの見方

作成した共起ネットワークは7つのサブグラフで構成されています(色分けされています)。
円の大きさは頻度で、線は一緒に登場する単語を結んでいます。このことから、この共起ネットワークで以下のことが言えると思います。

  • 01:死人を見たり、死人の傍で見たりするシーンが多そうだ
  • 02:門と下と男が一緒に登場するので、門の下に男がいそうだ
  • 03:面皰が右の頬にできたようだ
  • 04:タイトルにもある羅生門は雨と一緒に登場することが多い
  • 05:下人が老婆と一緒に登場し、何かを云うことが多そうだ
  • 06:髪の毛を抜く話題が出ている
  • 07:梯子を上るシーンも多そうだ

ポイントとしては、最も大きな円はどの単語の円か、各サブグラフで頻度の高い(大きい)ワードはどの単語と一緒に登場しているか(直接の線がつながっているか)です。

image


Pythonによる共起ネットワーク描画

次は、Pythonで共起ネットワークを作ります。

必要モジュールのインポート

以下のライブラリを使って共起ネットワークを描きます。必要なものを予めpip installしてください。なお、プログラムの中でノード間の重なりを上手くなくすため、GraphvizというSWを利用しております。Graphvizはこちらを参考にインストールをお願いします。(stable版は2021年12月現在ありません。最新版を入れて動作確認してください)

from pathlib import Path
import collections
import MeCab
import neologdn
import unicodedata
import networkx as nx
import matplotlib.pyplot as plt
from itertools import combinations, dropwhile
from collections import Counter, OrderedDict
import numpy as np
from networkx.drawing import nx_agraph

共起ネットワーク描画

KH Coderのインプットにした文章をテキストを指定し、分かち書きの実行からjaccard係数の計算、共起ネットワーク描画までを行うサンプルプログラムを掲載します(dataフォルダにあるrashomon.txtを読み込み、co-occurance.pngとして出力します)。

from pathlib import Path
import collections
import MeCab
import neologdn
import unicodedata
import networkx as nx
import matplotlib.pyplot as plt
from itertools import combinations, dropwhile
from collections import Counter, OrderedDict
import numpy as np
from networkx.drawing import nx_agraph

data_dir_path = Path('data')
stopword_list = ['|','で','た','ある','よう','ない', 'かた', 'ため', 'き','それ','なく','じゃ','わい','う','の','だ','な','れ','ず','さっき','これ','事','一','人']
        
# 実行ディレクトリのdataフォルダにあるrashomon.txtを読み込む
with open(data_dir_path.joinpath('rashomon.txt'), 'r', encoding='utf-8') as file:
    lines = file.readlines()
# -----------------------------------------------------------------
# 分かち書きを行う(辞書ディレクトリはご自身のディレクトリを指定ください)
mecabTagger = MeCab.Tagger("anaconda3\envs\downgrade\lib\site-packages\ipadic\dicdir -Ochasen") 
select_conditions = ['動詞', '形容詞', '名詞','副詞', '助動詞','感動詞']
noun_sentences = []
for sentence in lines:
    words = []
    sentence = neologdn.normalize(sentence)
    sentence = unicodedata.normalize("NFKC", sentence)
    node = mecabTagger.parseToNode(sentence).next
    while node:
        word = node.surface
        pos1 = node.feature.split(',')[0]
        pos2 = node.feature.split(',')[1]
        pos3 = node.feature.split(',')[2]
        if pos1 in select_conditions and word not in stopword_list:
            if not (pos1 in "動詞" and pos2 in "非自立"):                 
                words.append(node.surface) # 単語
        node = node.next
    noun_sentences.append(words)
# -----------------------------------------------------------------
# jaccard係数を計算する(jaccard係数:0.12以上、章跨ぎ単語登場数:4以上)
jaccard_coef = []
edge_th=0.12
pair_all = []
min_cnt=4
print('共起ネットワーク用単語ペア')
for chapter in noun_sentences:
    pair_temp = list(combinations(set(chapter), 2))
    for i,pair in enumerate(pair_temp):
        pair_temp[i] = tuple(sorted(pair))
    pair_all += pair_temp
pair_count = Counter(pair_all)
for key, count in dropwhile(lambda key_count: key_count[1] >= min_cnt, pair_count.most_common()):
    del pair_count[key]
word_count = Counter()
for chapter in noun_sentences:
    word_count += Counter(set(chapter))
for pair, cnt in pair_count.items():
    jaccard_coef.append(cnt / (word_count[pair[0]] + word_count[pair[1]] - cnt))
print('単語ペア', '出現数', 'jaccard係数', '単語1出現数', '単語2出現数', sep='\t')
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')
# -----------------------------------------------------------------
# 共起ネットワーク作成
G = nx.Graph()
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())
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))
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))
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, font_family='MS Gothic', font_weight='bold')
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')

上手くいくと以下のような図が出力されます。
co-occurance

上記の図では私の環境のMeCabを使って実行しており、その環境の辞書で分かち書き(単語に分解)が実行されるため、KH Coderと異なり、"上"という単語が上位に登場しております。
これは、分かち書きの際に、”上る”と分解するか、”上”、”る”と分解するかの違いなどによって生じてしまうモノです。

また、KH Coderではサブグラフに分解することが標準になっておりますが、こちらではサブグラフに分けず、1つのグラフで表現をしているため、少し見え方が異なるかもしれません。

しかしながら、"下人"や"老婆"や"羅生門"等のワードは上位に登場しており、集計方法はjaccard係数(その中でも単語ペアが各Chapterでどれだけ登場したか)は共通です。

チューニング

KH Coderと同様に、最小出現数はmin_cnt=4の部分を変更、jaccard係数の下限(上位60などと同等)はedge_th=0.12の部分を操作することで、グラフの粒度を変更することができます。
また、余分に検知されてしまっている単語は、Stopwordのリストに追記することで除外することができます。


まとめ

今回、KH CoderというフリーソフトとPythonを使ったサンプルプログラムで、共通の文学作品の共起ネットワークを作成いたしました。

文章を単語に分解し、一緒に登場する頻度をベースに集計するjaccard係数を使って章ごとに集計することで、どのような組み合わせが多く登場するのか視覚的に理解できたかと思います。

読んでいない文学作品であっても、こういった形の解析ソフトにかけると話の主体が何であるのか、キーワードベースで分かります。新聞記事やTweetなどのSNS投稿、アンケートの回答に適用すれば、特定のクラスタや時期におけるトレンドを知ることもできます。

以前は、精度が悪かった日本語テキストマイニングは現在かなり進歩し、十分実用化できるレベルになっております。ぜひ皆さまお試しください。