Python入門トップページ


目次

  1. N-gram を使って階層的クラスタリング
  2. 形態素解析を使って階層的クラスタリング(Janome編)
  3. 形態素解析を使って階層的クラスタリング(MeCab編)

百人一首をクラスタリングしてみよう

形態素解析を使って階層的クラスタリング

ここでは MeCab を使って百人一首の歌を形態素解析し,その結果に階層的クラスタリングを適用することで似た歌を探します.

目次に戻る

MeCab の準備

まずはここを参考に MeCab をインストールし,mecab-python3 がインストールされていることを確認してください.なお Jupyter notebook 上でセルの先頭に ! を入力するコマンドはシェルコマンドと呼ばれます.

!pip list
Package                            Version
---------------------------------- -------------------
...(中略)...
mecab-python3                      1.0.4
...(省略)...

目次に戻る

百人一首のデータを読み込む

まず,必要なモジュールをすべてインポートします.

# モジュールのインポート
import MeCab as mc
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import itertools # 2次元リストを1次元化

# 凝縮型の階層的クラスタリング
from sklearn.cluster import AgglomerativeClustering
# デンドログラム(樹形図)の作成
from scipy.cluster.hierarchy import dendrogram

# 高解像度ディスプレイのための設定
from IPython.display import set_matplotlib_formats
# from matplotlib_inline.backend_inline import set_matplotlib_formats # バージョンによってはこちらを有効に
set_matplotlib_formats('retina')

百人一首のデータは GitHub のリポジトリに置いてあるので,ここから直接読み込むか,CSV ファイルをダウンロードして読み込む.

url = "https://github.com/rinsaka/sample-data-sets/blob/master/hyaku-utf-8.csv?raw=true"
# CSV ファイルをデータフレームに読み込む
df = pd.read_csv(url)
# データフレームを表示する
df
hyaku-all

読み込んだデータフレームから目的のデータを表示する方法について確認してみよう.まず,インデックス 0 を指定して1つの歌を表示する.

df[df.index == 0]
hyaku-1-1

次は id = 1 を指定して同じ歌を表示する.

df[df.id == 1]
hyaku-1-2

「kana(かな)」以外の列を表示する.

df[df.id == 1][['id', 'uta', 'kajin']]
hyaku-1-3

目次に戻る

形態素解析を行い,索引語の頻度を求める

ここではデータフレームから「うた(uta)」を取り出して,形態素解析による分かち書きを行い,索引語頻度を求めてみる.まず,「id」列と「uta」列をデータフレームから取り出して,NumPy の2次元配列に格納します.

uta = df.loc[:, ['id','uta']].values
uta
array([[1, '秋の田の かりほの庵の 苫をあらみ わが衣手は 露にぬれつつ'],
       [2, '春すぎて 夏来にけらし 白妙の 衣ほすてふ 天の香具山'],
       [3, 'あしびきの 山鳥の尾の しだり尾の ながながし夜を ひとりかも寝む'],
       ...(中略)...
       [98, '風そよぐ ならの小川の 夕暮れは みそぎぞ夏の しるしなりける'],
       [99, '人もをし 人も恨めし あぢきなく 世を思ふゆゑに 物思ふ身は'],
       [100, '百敷や ふるき軒端の しのぶにも なほあまりある 昔なりけり']], dtype=object)

形態素解析を実行する関数 my_Mecab を定義します(この関数は以降では使いません).

def my_Mecab(text):
    mecab = mc.Tagger()
    # mecab = mc.Tagger('d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/')
    print(mecab.parse(text))

先頭の歌を取り出して「uta」だけを表示してみます.

uta[0][1]
'秋の田の かりほの庵の 苫をあらみ わが衣手は 露にぬれつつ'

これを形態素解析した結果を確認してみます.残念ながら,一部のヨミは正しくありません.

my_Mecab(uta[0][1])
秋	名詞,一般,*,*,*,*,秋,アキ,アキ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
田	名詞,一般,*,*,*,*,田,タ,タ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
かり	動詞,自立,*,*,一段,連用形,かりる,カリ,カリ
ほ	動詞,自立,*,*,五段・ラ行,体言接続特殊2,ほる,ホ,ホ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
庵	名詞,一般,*,*,*,*,庵,アン,アン
の	助詞,連体化,*,*,*,*,の,ノ,ノ
苫	名詞,一般,*,*,*,*,苫,トマ,トマ
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
あら	動詞,自立,*,*,五段・ラ行,未然形,ある,アラ,アラ
み	動詞,非自立,*,*,一段,連用形,みる,ミ,ミ
わが	連体詞,*,*,*,*,*,わが,ワガ,ワガ
衣手	名詞,一般,*,*,*,*,衣手,コロモデ,コロモデ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
露	名詞,形容動詞語幹,*,*,*,*,露,アラワ,アラワ
に	助詞,副詞化,*,*,*,*,に,ニ,ニ
ぬれ	動詞,自立,*,*,一段,連用形,ぬれる,ヌレ,ヌレ
つつ	助詞,接続助詞,*,*,*,*,つつ,ツツ,ツツ
EOS

分かち書きをして,1次元リストにして返す関数 wakati() を定義します.

def wakati(text):
    t = mc.Tagger('')
#     t = mc.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/')

    node = t.parseToNode(text)
#     print(node)
    words = []
    while(node):
#         print(node.surface, node.feature)
        if node.surface != "":  # ヘッダとフッタを除外
            words.append(node.surface)  # node.surface は「表層形」
        node = node.next
        if node is None:
            break
    return words

今定義した関数を用いて最初の歌を分かち書きしてみます.結果が1次元リストになって返ってきていることがわかります.

words_0 = wakati(uta[0][1])
print(words_0)
['秋', 'の', '田', 'の', 'かり', 'ほ', 'の', '庵', 'の', '苫', 'を', 'あら', 'み', 'わが', '衣手', 'は', '露', 'に', 'ぬれ', 'つつ']

歌の数は次のように取得することができます.

uta.shape[0]
100

すべての歌を分かち書きして2次元リストに格納します.なお,%%time はセル全体の処理時間を計測するためのマジックコマンドで,処理時間は 100 ミリ秒(= 0.1 秒)でした.Janome での処理時間が 3.69 秒であったことから,MeCab を使うと Janome と比較して処理速度がかなり速いことがわかるはずです.

%%time
uta_wakati = []
for i in range(uta.shape[0]):
    w = wakati(uta[i][1])
    uta_wakati.append(w)
print(uta_wakati)
[
  ['秋', 'の', '田', 'の', 'かり', 'ほ', 'の', '庵', 'の', '苫', 'を', 'あら', 'み', 'わが', '衣手', 'は', '露', 'に', 'ぬれ', 'つつ'],
  ['春', 'すぎ', 'て', '夏', '来', 'に', 'けら', 'し', '白妙', 'の', '衣', 'ほす', 'て', 'ふ', '天', 'の', '香具', '山'],
  ...(中略)...
  ['人', 'も', 'を', 'し', '人', 'も', '恨めし', 'あぢきなく', '世', 'を', '思ふ', 'ゆ', 'ゑに', '物', '思ふ', '身', 'は'],
  ['百敷', 'や', 'ふるき', '軒端', 'の', 'しのぶ', 'に', 'も', 'なほ', 'あまり', 'ある', '昔', 'なり', 'けり']
]
CPU times: user 37 ms, sys: 32.9 ms, total: 69.9 ms
Wall time: 100 ms

すべての歌で登場した索引語の集合を作りたいので,その前に上の2次元リストを1次元化します.なお,途中(100個目)まで表示したあと,全体の個数を表示すると 1769 個であることがわかります.

words = list(itertools.chain.from_iterable(uta_wakati))
print(words[0:100])
print(len(words))
['秋', 'の', '田', 'の', 'かり', 'ほ', 'の', '庵', 'の', '苫', 'を', 'あら', 'み', 'わが', '衣手', 'は', '露', 'に', 'ぬれ', 'つつ', '春', 'すぎ', 'て', '夏', '来', 'に', 'けら', 'し', '白妙', 'の', '衣', 'ほす', 'て', 'ふ', '天', 'の', '香具', '山', 'あし', 'びき', 'の', '山鳥', 'の', '尾', 'の', 'し', 'だり', '尾', 'の', 'ながなが', 'し', '夜', 'を', 'ひとり', 'かも', '寝', 'む', '田子の浦', 'に', 'うち', '出', 'で', 'て', '見れ', 'ば', '白妙', 'の', '富士', 'の', '高嶺', 'に', '雪', 'は', 'ふり', 'つつ', '奥山', 'に', 'もみ', 'ぢ', '踏み分け', '鳴く', '鹿', 'の', '声', '聞く', '時', 'ぞ', '秋', 'は', '悲しき', 'かさ', 'さ', 'ぎ', 'の', '渡せる', '橋', 'に', 'おく', '霜', 'の']
1769

次のコードを実行することで百人一首で使われている単語(索引語)の集合を作ることができます.この結果,622の索引語が使われていることがわかりました.

vocab = sorted(set(words))
print(vocab)
print(len(vocab))
['々', 'あ', 'あけ', 'あし', 'あぢきなく', 'あて', 'あはれ', 'あふ', 'あま', 'あまり', 'あら', 'あらし', 'あり', 'ありま', 'ある', 'い', 'いかに', 'いく', 'いさ', 'いたみ', 'いつみ', 'いづ', 'いづみ', 'いで', 'いにしへ', 'いのち', 'いふ', 'いぶき', 'う', 'うき', 'うち', 'うつ', 'うつり', 'うらめしき', 'え', 'お', 'おく', 'おと', 'おのれ', 'おほけなく', 'おろし', 'か', 'かき', 'かぎり', 'かく', 'かけ', 'かげ', 'かこち', 'かさ', 'かすみ', 'かた', 'かたけれ', 'かたぶく', 'かたみに', 'かなし', 'かも', 'かよ', 'から', 'からむ', 'かり', 'かる', 'かれ', 'が', 'き', 'きか', 'きく', 'きし', 'きま', 'きりぎりす', 'ぎ', 'ぎぞ', 'くくる', 'くだけ', 'くち', 'くま', 'くら', 'くらむ', 'くる', 'くるま', 'くれ', 'ぐし', 'ぐりあひて', 'け', 'けさ', 'けら', 'けり', 'ける', 'けれ', 'こ', 'こがれ', 'こぎ', 'こぐ', 'こそ', 'こと', 'この', 'このごろ', 'この世', 'こむ', 'こめ', 'こも', 'これ', 'ころ', 'さ', 'さけ', 'さし', 'さしも', 'さそ', 'さびしき', 'さびしさ', 'さやけ', 'され', 'ざめぬ', 'ざら', 'ざり', 'し', 'しか', 'しかる', 'しがらみ', 'しき', 'しく', 'しげ', 'しづ', 'しのば', 'しのぶ', 'しばし', 'しぼり', 'しま', 'しも', 'しるし', 'じ', 'す', 'すぎ', 'すむ', 'する', 'すれ', 'ず', 'ずり', 'せ', 'せか', 'そ', 'そよぐ', 'そら', 'それとも', 'ぞ', 'た', 'たえ', 'たかし', 'たく', 'ただ', 'たち', 'たつみ', 'たなびく', 'たび', 'ため', 'たる', 'だ', 'だに', 'だり', 'ち', 'ぢ', 'つ', 'つく', 'つくし', 'つつ', 'つねに', 'つむ', 'つもり', 'つらぬき', 'つり', 'つる', 'つれ', 'つれなく', 'づら', 'づる', 'づれて', 'て', 'てか', 'てよ', 'で', 'でし', 'と', 'とか', 'とどめ', 'とめ', 'とも', 'とり', 'ど', 'な', 'なか', 'なかなか', 'ながなが', 'ながめ', 'ながら', 'なく', 'なけれ', 'なし', 'など', 'なほ', 'なむ', 'なら', 'なり', 'なる', 'に', 'にて', 'による', 'ぬ', 'ぬめり', 'ぬる', 'ぬれ', 'ね', 'の', 'のち', 'のどけき', 'のに', 'のみ', 'は', 'はいふ', 'はかる', 'はげしかれ', 'はせる', 'はで', 'はら', 'はれわたる', 'ば', 'ばかり', 'ばね', 'ひ', 'ひける', 'ひし', 'ひぞ', 'ひと', 'ひとたび', 'ひとつ', 'ひとり', 'ひま', 'びき', 'ふ', 'ふけ', 'ふこ', 'ふし', 'ふみ', 'ふよ', 'ふり', 'ふる', 'ふるき', 'ふるさと', 'ぶる', 'ぶれ', 'へ', 'へず', 'べき', 'ほ', 'ほえ', 'ほか', 'ほさ', 'ほす', 'ほととぎす', 'ほふ', 'ま', 'まき', 'まさり', 'まし', 'また', 'まだ', 'まだき', 'まつ', 'まで', 'まにまに', 'まろ', 'み', 'みかの原', 'みじかき', 'みそ', 'みぞ', 'みな', 'みゆき', 'みれ', 'む', 'むぐら', 'むしろ', 'むとぞ', 'むべ', 'むれ', 'め', 'も', 'もしほ', 'もの', 'もみ', 'もり', 'もれ', 'もろ', 'や', 'やす', 'やどる', 'やぶる', 'やみ', 'やら', 'ゆ', 'ゆく', 'ゆる', 'ゆるさ', 'よ', 'よに', 'より', 'よる', 'ら', 'り', 'る', 'るる', 'るれ', 'れ', 'れる', 'わ', 'わが', 'わが身', 'わきて', 'わたの原', 'わたる', 'わび', 'われ', 'ゐ', 'ゑ', 'ゑに', 'を', 'をの', '三', '世', '世に', '世の中', '久しき', '久しく', '久方', '九', '乱れ', '乾', '人', '人づて', '人知れず', '今', '今年', '今日', '住の江', '光', '入る', '八', '八重桜', '冬', '冲', '出', '分か', '初', '初め', '初瀬', '別れ', '匂', '十', '千々', '千鳥', '原', '友', '吉野', '同じ', '名', '君', '吹き', '吹く', '吹け', '告げよ', '命', '咲き', '問', '嘆き', '嘆け', '坂', '声', '変', '夏', '夕', '夕なぎ', '夕暮れ', '外山', '夜', '夜ふけ', '夜もすがら', '夜半', '夢', '大江山', '天', '天の橋立', '奈良', '契り', '奥', '奥山', '姿', '宇治', '室', '宵', '宿', '富士', '寒く', '寝', '寝る', '小倉山', '小川', '小舟', '小野', '尾', '山', '山川', '山桜', '山里', '山風', '山鳥', '岩', '岸', '峰', '島', '嵐', '川', '帰り', '帰る', '幾夜', '庭', '庵', '弱り', '待た', '待ち', '心', '忍', '忘', '忘れ', '思', '思は', '思ひ', '思ふ', '思へ', '恋', '恋し', '恋しき', '恋す', '恨み', '恨めし', '悲しき', '悲しけれ', '惜しから', '惜しくも', '惜しけれ', '憂', '憂き', '憂し', '我が身', '手向', '手枕', '折', '折ら', '散り', '散る', '方', '日', '明', '明け', '昔', '春', '春日', '昼', '時', '暁', '暮', '更け', '月', '有明', '朝ぼらけ', '末', '村雨', '杣', '来', '松', '松山', '桜', '橋', '残れる', '民', '水', '沖', '波', '流る', '流れ', '浅茅生', '浜', '浦', '消え', '涙', '淡路島', '淵', '渚', '渡せる', '渡る', '滝', '滝川', '漕ぎ', '潟', '潮干', '濡れ', '瀬', '火', '焼く', '燃え', '物', '玉', '玉の', '生', '田', '田子の浦', '由良', '白', '白き', '白妙', '白波', '白菊', '白露', '百敷', '知ら', '知り', '知る', '石', '祈ら', '神', '神代', '秋', '秋風', '稲葉', '立た', '立ち', '立ちのぼる', '立つ', '竜田', '竜田川', '笠', '笹原', '篠原', '紅葉', '絶', '絶え', '綱手', '網代木', '聞か', '聞く', '聞こえ', '舟', '舟人', '色', '花', '若菜', '苫', '草', '草木', '落つ', '葉', '葦', '行く', '行く末', '衛士', '衣', '衣手', '袖', '見', '見え', '見せ', '見る', '見れ', '言', '誓', '誰', '越さ', '路', '踏み分け', '身', '軒端', '通', '逢', '逢坂', '逢坂山', '過', '道', '遠けれ', '都', '里', '重', '野', '錦', '長', '長く', '長月', '門田', '間', '関', '関守', '閨', '降', '陸奥', '雄島', '難波', '難波江', '雪', '雲', '霜', '霜夜', '霧', '露', '音', '須磨', '顔', '風', '香', '香具', '高嶺', '高砂', '鳥', '鳴き', '鳴く', '鹿', '黒髪']
622

索引語の頻度 TF (Term Frequency) を求める関数を定義します.

def get_TF(uta_wakati, vocab):
    """
    TFは索引語の出現頻度のこと.
    """
    n_docs = len(uta_wakati)
    n_vocab = len(vocab)

    # 行数 = 文書数, 列数 = 登録集合数 で tf を初期化する
    tf = np.zeros((n_docs, n_vocab))

    for i in range(n_docs):
        for w in uta_wakati[i]:
            tf[i, vocab.index(w)] += 1
    return tf

上で定義した関数 get_TF に分かち書きした結果と索引語一覧を与えて,索引語頻度を求めます.

tf = get_TF(uta_wakati, vocab)
print(tf)
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]

行列 tf のサイズを確認すると,100行 x 622列であることがわかります.

tf.shape
(100, 622)

2つ上の結果でハイライトされた1行目の(途中が省略された)結果をすべて表示してみます.

print(tf[0])
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 1. 0. 4. 0. 0. 0. 0. 1.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

上のハイライト部分の意味を確認します.これは「秋 かりほ 苫をあらみ わが衣手は 露にぬれつつ」という歌の中に「々」という索引語は含まれず,「あら」は1度登場し,「の」が4度登場することを意味しています.

print(vocab[0], tf[0][0])
print(vocab[10], tf[0][10])
print(vocab[210], tf[0][210])
々 0.0
あら 1.0
の 4.0

これで分析に必要な索引語頻度の2次元配列 tf を生成できました.ここまで準備できれば,あとはここと同じ方法で階層的クラスタリングk-means による非階層クラスタリングが実行できます.僅かな(?)違いは,ここの階層的クラスタリングの例 では2次元データを取り扱ったことに対して,この百人一首では 622 次元のデータを取り扱っていることくらいです!

目次に戻る

階層的クラスタリング

ここまでの手順で分析に必要なデータ tf が準備できたので,これを使って非階層的クラスタリングを行ってみよう.分類したいクラスタ数は仮に 5 として実行します(今回は分類するというよりも似たものを探したいので,この数字はあまり大きな意味は持ちません).また,個体間の距離にはユークリッド距離を,クラスタ間の距離にはウォード法を用いることとします.これらの詳細はここを確認してください

# クラスタ数
num_clusters = 5
# 個体間の距離はユークリッド距離
# クラスタ間の距離はウォード法
clustering = AgglomerativeClustering(n_clusters = num_clusters, affinity='euclidean', linkage='ward').fit(tf)
clustering.labels_
array([2, 2, 2, 1, 3, 2, 2, 0, 1, 4, 1, 2, 3, 1, 1, 0, 0, 2, 2, 3, 0, 2,
       1, 3, 1, 2, 3, 0, 2, 3, 2, 3, 2, 1, 3, 2, 2, 2, 2, 1, 3, 0, 3, 4,
       0, 0, 1, 0, 3, 0, 4, 0, 3, 0, 3, 2, 1, 0, 0, 2, 1, 3, 0, 2, 3, 0,
       2, 0, 2, 1, 2, 2, 2, 0, 4, 1, 3, 3, 2, 3, 0, 3, 3, 0, 3, 0, 2, 2,
       0, 0, 3, 2, 4, 2, 1, 3, 2, 2, 4, 3])

上の結果は,各歌がどのクラスタに属するかを意味しています.この結果をデータフレームに列「cluster_id」として追加します.つまり「1: 秋の田の...」「2: 春すぎて...」「3: あしびきの...」などが一つのクラスタに分類されているということです.

df['cluster_id'] = clustering.labels_
print(df[['id', 'uta', 'kajin', 'cluster_id']])
     id                                uta    kajin  cluster_id
0     1     秋の田の かりほの庵の 苫をあらみ わが衣手は 露にぬれつつ     天智天皇           2
1     2        春すぎて 夏来にけらし 白妙の 衣ほすてふ 天の香具山     持統天皇           2
2     3  あしびきの 山鳥の尾の しだり尾の ながながし夜を ひとりかも寝む    柿本人麻呂           2
3     4   田子の浦に うち出でて見れば 白妙の 富士の高嶺に 雪はふりつつ     山部赤人           1
4     5       奥山に もみぢ踏み分け 鳴く鹿の 声聞く時ぞ 秋は悲しき     猿丸大夫           3
..  ...                                ...      ...         ...
95   96     花さそふ 嵐の庭の 雪ならで ふりゆくものは わが身なりけり  入道前太政大臣           3
96   97   こぬ人を まつほの浦の 夕なぎに 焼くやもしほの 身もこがれつつ   権中納言定家           2
97   98    風そよぐ ならの小川の 夕暮れは みそぎぞ夏の しるしなりける    従二位家隆           2
98   99     人もをし 人も恨めし あぢきなく 世を思ふゆゑに 物思ふ身は     後鳥羽院           4
99  100     百敷や ふるき軒端の しのぶにも なほあまりある 昔なりけり      順徳院           3

[100 rows x 4 columns]

デンドログラムを作成してみます.

children = clustering.children_
distance = np.arange(children.shape[0])
no_of_observations = np.arange(2, children.shape[0]+2)
linkage_matrix = np.hstack((
                            children,
                            distance[:, np.newaxis],
                            no_of_observations[:, np.newaxis]
                           )).astype(float)
fig, ax = plt.subplots(figsize=(20, 8))
dendrogram(
    linkage_matrix,
    labels= df['id'].values,
    leaf_font_size=8,
#     color_threshold=np.inf,
#     orientation='right',
)
ax.set_title("MeCab")
# plt.savefig("hyaku-dendrogram_mecab.png", dpi=300, facecolor='white')
plt.show()
hyaku-dendrogram_mecab

children には個体(またはクラスタ)が連結される順番が格納されてるので,中身を確認します.

print(children.shape)
print(children[0:4] + 1) # 歌番号は1スタートなので + 1しておく
(99, 2)
[[  4  15]
 [ 32 100]
 [  5  83]
 [ 33  67]]

上の結果から,4 と 15 の歌が最も近いことがわかります.これはデンドログラムを拡大して確認することもできます.この2つの歌を表示してみます.この結果「雪はふりつつ」と「出でて」が共通していることがわかりました.

print(df[(df.id == 4) | (df.id == 15)][['id', 'uta', 'kajin']])
    id                               uta kajin
3    4  田子の浦に うち出でて見れば 白妙の 富士の高嶺に 雪はふりつつ  山部赤人
14  15    君がため 春の野に出でて 若菜つむ わが衣手に 雪はふりつつ  光孝天皇

しかしながら,「君がため」で始まる 50 は 15 に近いとは判定されませんでした.50 に近いとされた歌を並べて表示してみます.これらは ((42と54)と81)と(21と63)に50が近いと判定されています.

print(df[(df.id == 50) | (df.id == 81) | (df.id == 42) |  (df.id == 54) | (df.id == 21) | (df.id == 63)][['id', 'uta', 'kajin']])
    id                               uta    kajin
20  21   今こむと 言ひしばかりに 長月の 有明の月を 待ちいでつるかな     素性法師
41  42     契りきな かたみに袖を しぼりつつ 末の松山 波越さじとは     清原元輔
49  50    君がため 惜しからざりし 命さへ 長くもがなと 思ひけるかな     藤原義孝
53  54   忘れじの 行く末までは かたければ 今日をかぎりの 命ともがな    儀同三司母
62  63  今はただ 思ひ絶えなむ とばかりを 人づてならで 言ふよしもがな   左京大夫道雅
80  81  ほととぎす 鳴きつる方を ながむれば ただありあけの 月ぞ残れる  後徳大寺左大臣

なお,2番目に近いと判定されたのが 32 と 100 です.「なりけり」が共通しています.

print(df[(df.id == 32) | (df.id == 100)][['id', 'uta', 'kajin']])
     id                             uta kajin
31   32  山川に 風のかけたる しがらみは 流れもあへぬ 紅葉なりけり  春道列樹
99  100  百敷や ふるき軒端の しのぶにも なほあまりある 昔なりけり   順徳院

さらに,3番目に近いと判定されたのが 83 と 5 です.これらは「奥」「山」「鹿」「鳴く」が共通しています.

print(df[(df.id == 83) |  (df.id == 5)][['id', 'uta', 'kajin']])
    id                            uta     kajin
4    5   奥山に もみぢ踏み分け 鳴く鹿の 声聞く時ぞ 秋は悲しき      猿丸大夫
82  83  世の中よ 道こそなけれ 思ひ入る 山の奥にも 鹿ぞ鳴くなる  皇太后宮大夫俊成

目次に戻る

コード全体のまとめ

ここまでは説明しながらコードの断片を示したので,ここでコード全体をまとめて,不要な箇所は削除したものを示します.

# モジュールのインポート
import MeCab as mc
import pandas as pd
import numpy as np
import itertools # 2次元リストを1次元化

import matplotlib.pyplot as plt

# 凝縮型の階層的クラスタリング
from sklearn.cluster import AgglomerativeClustering
# デンドログラム(樹形図)の作成
from scipy.cluster.hierarchy import dendrogram

# 高解像度ディスプレイのための設定
from IPython.display import set_matplotlib_formats
# from matplotlib_inline.backend_inline import set_matplotlib_formats # バージョンによってはこちらを有効に
set_matplotlib_formats('retina')

def wakati(text):
    t = mc.Tagger('')
#     t = mc.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/')

    node = t.parseToNode(text)
#     print(node)
    words = []
    while(node):
#         print(node.surface, node.feature)
        if node.surface != "":  # ヘッダとフッタを除外
            words.append(node.surface)  # node.surface は「表層形」
        node = node.next
        if node is None:
            break
    return words

def get_TF(uta_wakati, vocab):
    """
    TFは索引語の出現頻度のこと.
    """
    n_docs = len(uta_wakati)
    n_vocab = len(vocab)
    # 行数 = 文書数, 列数 = 登録集合数 で tf を初期化する
    tf = np.zeros((n_docs, n_vocab))
    for i in range(n_docs):
        for w in uta_wakati[i]:
            tf[i, vocab.index(w)] += 1
    return tf

# CSV ファイルの URL を指定する
url = "https://github.com/rinsaka/sample-data-sets/blob/master/hyaku-utf-8.csv?raw=true"
# CSV ファイルをデータフレームに読み込む
df = pd.read_csv(url)

# 歌を取り出して NumPy 配列に格納する
uta = df.loc[:, ['id','uta']].values

# すべての歌を分かち書きして2次元リストに格納する
uta_wakati = []
for i in range(uta.shape[0]):
    w = wakati(uta[i][1])
    uta_wakati.append(w)

# 2次元リストを1次元化
words = list(itertools.chain.from_iterable(uta_wakati))

# 使われている単語(索引語)の集合を作る
vocab = sorted(set(words))

# 索引語頻度 (TF: Term Frequency) を計算する
tf = get_TF(uta_wakati, vocab)

# クラスタ数
num_clusters = 5
# 個体間の距離はユークリッド距離
# クラスタ間の距離はウォード法
clustering = AgglomerativeClustering(n_clusters = num_clusters, affinity='euclidean', linkage='ward').fit(tf)

# 結果をデータフレームに追加
df['cluster_id'] = clustering.labels_

# デンドログラムを作成
children = clustering.children_
distance = np.arange(children.shape[0])
no_of_observations = np.arange(2, children.shape[0]+2)
linkage_matrix = np.hstack((
                            children,
                            distance[:, np.newaxis],
                            no_of_observations[:, np.newaxis]
                           )).astype(float)
fig, ax = plt.subplots(figsize=(20, 8))
dendrogram(
    linkage_matrix,
    labels= df['id'].values,
    leaf_font_size=8,
#     color_threshold=np.inf,
#     orientation='right',
)
ax.set_title("MeCab")
# plt.savefig("hyaku-dendrogram_mecab.png", dpi=300, facecolor='white')
plt.show()

目次に戻る