Python scikit-learn 機械学習 自然言語処理

【Python】GoogleColab上でNetworkXによる日本語の共起ネットワークを文字化けせずにプロット

この記事は約16分で読めます。

 

 

今回はテキストデータから共起ネットワークをプロットします。そもそも共起ネットワークとは同時に出現する単語の組み合わせをエッジで繋ぎ、単語間の関係をネットワークで表したものです。これにより、文章内の単語の関連性を可視化できます。

 

<イメージ図>

 

共起ネットワークはPythonだと「networkx」というライブラリを使って簡単に実装することができます。

 

 

・実行環境

GoogleColab
Python3.7
networkx2.6.3

 

データの取得と加工

 

今回使用するデータは、青空文庫 Aozora Bunkoにある福沢諭吉の『学問のすすめ』です。すでにテキストファイル化したものはをGITからダウンロードしてgooglecolabの一番上のディレクトリにアップロードして配置してください。

 

ライブラリのインストール

 

!pip install mecab-python3 unidic-lite
!apt install aptitude
!aptitude install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file -y
!pip install mecab-python3==0.7

!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null 
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1
!pip install mecab-python3 > /dev/null

!ln -s /etc/mecabrc /usr/local/etc/mecabrc

 

日本語フォントのインストール

 

次にMatplotlibの日本語フォントをインストールしてデフォルトの文字フォントとして設定します。

 

!apt-get -y install fonts-ipafont-gothic
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'IPAPGothic'

 

ライブラリの読み込み&不要な単語を削除

 

!pip install mojimoji
!pip install neologdn
import MeCab
from wordcloud import WordCloud
import re
import pandas as pd
import numpy as np
import matplotlib
from collections import defaultdict, Counter
from copy import copy, deepcopy
from pathlib import Path
import collections
import urllib.request
import MeCab
import mojimoji
import neologdn
import unicodedata
import itertools
import networkx as nx
%matplotlib inline

def get_stopword_lsit(write_file_path):

    if not write_file_path.exists():
        url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
        urllib.request.urlretrieve(url, write_file_path)

    with open(write_file_path, 'r', encoding='utf-8') as file:
        stopword_list = [word.replace('\n', '') for word in file.readlines()]

    return stopword_list

def get_noun_words_from_sentence(sentence, mecab, stopword_list=[]):
    return [
        x.split('\t')[0] for x in mecab.parse(sentence).split('\n') if len(x.split('\t')) > 1 and \
         '名詞' in x.split('\t')[3] and x.split('\t')[0] not in stopword_list
    ]


def split_sentence(sentence, mecab, stopword_list):
    sentence = neologdn.normalize(sentence)
    sentence = unicodedata.normalize("NFKC", sentence)
    words = get_noun_words_from_sentence(
        sentence=sentence, mecab=mecab, stopword_list=stopword_list
    )
    words = list(map(lambda x: re.sub(r'\d+\.*\d*', '0', x.lower()), words))
    return words

with open('gakumonno_susume.txt', 'r', encoding='utf-8') as file:
    lines = file.readlines()

sentences = []

for sentence in lines:
    texts = sentence.split('。')
    sentences.extend(texts)

mecab = MeCab.Tagger('-Ochasen')

stopword_list = get_stopword_lsit(Path('stopword_list.txt'))
# stopword_list=['ここ', 'あれ','ん','よう','0','こと','さん','の', 'ん','これ','もの','そう']

noun_sentences = []
for sentence in sentences:
    noun_sentences.append(
        split_sentence(sentence=sentence, mecab=mecab, stopword_list=stopword_list)
    )

for words in noun_sentences[:5]:
    print(words)

 

noun_sentences = list(filter(lambda x: len(x) > 1 and '見出し' not in x, noun_sentences))
combination_sentences = [list(itertools.combinations(words, 2)) for words in noun_sentences]
combination_sentences = [[tuple(sorted(combi)) for combi in combinations] for combinations in combination_sentences]
tmp = []
for combinations in combination_sentences:
    tmp.extend(combinations)

combination_sentences = tmp
combination_sentences[:5]

 

def make_jaccard_coef_data(combination_sentences):
    combi_count = collections.Counter(combination_sentences)
    word_associates = []
    for key, value in combi_count.items():
        word_associates.append([key[0], key[1], value])

    word_associates = pd.DataFrame(word_associates, columns=['word1', 'word2', 'intersection_count'])

    words = []
    for combi in combination_sentences:
        words.extend(combi)

    word_count = collections.Counter(words)
    word_count = [[key, value] for key, value in word_count.items()]
    word_count = pd.DataFrame(word_count, columns=['word', 'count'])

    word_associates = pd.merge(
        word_associates,
        word_count.rename(columns={'word': 'word1'}),
        on='word1', how='left'
    ).rename(columns={'count': 'count1'}).merge(
        word_count.rename(columns={'word': 'word2'}),
        on='word2', how='left'
    ).rename(columns={'count': 'count2'}).assign(
        union_count=lambda x: x.count1 + x.count2 - x.intersection_count
    ).assign(jaccard_coef=lambda x: x.intersection_count / x.union_count).sort_values(
        ['jaccard_coef', 'intersection_count'], ascending=[False, False]
    )

    return word_associates

jaccard_coef_data = make_jaccard_coef_data(combination_sentences)
jaccard_coef_data.head(10)

 

共起ネットワーク用にデータを整形

 

group_values = [0, 0.01, 0.02, 0.04, float('inf')]

plot_data = jaccard_coef_data.copy()

plot_data['group_num'] = 0

group_names = []

for i in range(len(group_values) - 1):

    plot_data['group_num'] = plot_data.apply(
        lambda x: i + 1 if group_values[i] <= x['jaccard_coef'] and x['jaccard_coef'] < group_values[i + 1] else x['group_num'],
        axis=1
    )
    group_names.append((group_values[i], group_values[i + 1]))

plot_data = plot_data.groupby('group_num')['jaccard_coef'].count().reset_index().rename(
    columns={'jaccard_coef': 'n_combi'}
).assign(rate=lambda x: x.n_combi / x.n_combi.sum()).assign(
    rate_cumsum=lambda x: x.rate.cumsum()

)

plot_data = pd.concat([
    pd.DataFrame(group_names, columns=['jaccard_lower', 'jaccard_upper']),
    plot_data
], axis=1)

plot_data

 

前処理についての詳しい解説は以下のサイトに記載されているのでチューニングの際などは参考にしてください

 

参照:https://www.dskomei.com/entry/2019/04/07/021028

 

 

スポンサーリンク
スポンサーリンク

NetworkXで共起ネットワークをプロットする

 

日本語の文字化けについて

 

データの前処理ができたので次は共起ネットワークをプロットします。

 

ネットの他の記事を見るとnx.draw_networkx_labels()の引数でフォントを指定すれば日本語の文字化けが解消されるとありましたがGoogleColab上だと日本語が文字化けしたままでした。なので、matplotlib.font_managerで’IPAPGothic’のttfファイルを直接指定します

 

サンプルコード

 

import matplotlib.font_manager
#日本語フォント読み込み
font_path = r'/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf'
font_prop = matplotlib.font_manager.FontProperties(fname=font_path)

def plot_network(
    data, edge_threshold=0., fig_size=(15, 15),
    fontfamily='Osaka', fontsize=14,
    coefficient_of_restitution=0.15,
    image_file_path=None
):

    nodes = list(set(data['node1'].tolist() + data['node2'].tolist()))
    
    plt.figure(figsize=fig_size)
    
    G = nx.Graph()
    # 頂点の追加
    G.add_nodes_from(nodes)

    # 辺の追加
    # edge_thresholdで枝の重みの下限を定めている
    for i in range(len(data)):
        row_data = data.iloc[i]
        if row_data['weight'] >= edge_threshold:
            G.add_edge(row_data['node1'], row_data['node2'], weight=row_data['weight'])

    # 孤立したnodeを削除
    isolated = [n for n in G.nodes if len([i for i in nx.all_neighbors(G, n)]) == 0]
    for n in isolated:
        G.remove_node(n)

    # k = node間反発係数
    pos = nx.spring_layout(G, k=coefficient_of_restitution)

    pr = nx.pagerank(G)
    # nodeの大きさ
    nx.draw_networkx_nodes(
        G, pos, node_color=list(pr.values()),
        cmap=plt.cm.Reds,
        alpha=0.7,
        node_size=[60000*v for v in pr.values()]
    )

    # 日本語ラベル
    datas = nx.draw_networkx_labels(G, pos, font_size=fontsize, font_family=fontfamily, font_weight="bold")
    for t in datas.values():
        t.set_fontproperties(font_prop)

    # エッジの太さ調節
    edge_width = [d["weight"] * 100 for (u, v, d) in G.edges(data=True)]
    nx.draw_networkx_edges(G, pos, alpha=0.4, edge_color="darkgrey", width=edge_width)

    plt.axis('off')
    plt.tight_layout()
    
    if image_file_path:
        plt.savefig(image_file_path, dpi=300)

n_word_lower = 50
edge_threshold=0.025
plot_data = jaccard_coef_data.query(
    'count1 >= {0} and count2 >= {0}'.format(n_word_lower)
).rename(
    columns={'word1': 'node1', 'word2': 'node2', 'jaccard_coef': 'weight'}
)

plot_network(
    data=plot_data,
    edge_threshold=edge_threshold,
    fig_size=(10, 10),
    fontsize=9,
    fontfamily='Osaka',
    coefficient_of_restitution=0.08,
    image_file_path='/co_occurence_network.png'
)

 

<実行結果>

ノートブック:https://colab.research.google.com/drive/1eSXZuqO5WqcspxPe73MNSgjGwnG4jPe1?usp=sharing

 

 

参考:https://www.currypurin.com/entry/2018/03/25/015759

参考:https://propen.dream-target.jp/blog/networkx_sample

参考:https://www.dskomei.com/entry/2019/04/07/021028

 

コメント

タイトルとURLをコピーしました