Python入門トップページ


目次

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

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

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

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

目次に戻る

Janome の準備

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


!pip list
Package                            Version
---------------------------------- -------------------
...(中略)...
Janome                             0.4.1
...(省略)...

目次に戻る

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

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


# モジュールのインポート
from janome.tokenizer import Tokenizer
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_Janome を定義します(この関数は以降では使いません).


def my_Janome(text):
    t = Tokenizer()
    results = t.tokenize(text)
    for token in results:
        print(token)

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


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

これを形態素解析した結果を確認してみます.結果には空白が含まれてしまっています.


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

半角空白を削除するために文字列の置換を行います.


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

空白を削除した文字列に対して形態素解析した結果を確認してみます.残念ながら,一部のヨミは正しくありません.


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

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


def wakati(text):
    t = Tokenizer()
    results = t.tokenize(text)
    words = []
    for token in results:
        words.append(token.surface) # 表層形
    return words

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


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

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


uta.shape[0]
100

すべての歌を分かち書きして2次元リストに格納します.なお,%%time はセル全体の処理時間を計測するためのマジックコマンドで,処理時間は 3.69 秒でした.MeCab での処理時間が 0.1 秒であったことから,Janome の分かち書きは MeCab と比べると低速です.大量の文章を分かち書きする際は MeCab が良さそうです.


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

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


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

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


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

索引後の頻度 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 631列であることがわかります.


tf.shape
(100, 631)

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. 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. 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. 1. 0. 0. 0. 0. 0. 1. 0. 4. 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. 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. 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. 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. 0.]

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


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

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

目次に戻る

階層的クラスタリング

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


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

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


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

[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("Janome")
# plt.savefig("hyaku-dendrogram_janome.png", dpi=300, facecolor='white')
plt.show()
hyaku-dendrogram_janome

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


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

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


print(df[(df.id == 32) | (df.id == 82)][['id', 'uta', 'kajin']])
    id                               uta kajin
31  32    山川に 風のかけたる しがらみは 流れもあへぬ 紅葉なりけり  春道列樹
81  82  思ひわび さてもいのちは あるものを 憂きにたへぬは 涙なりけり  道因法師

2番目に近いと判定されたのが 4 と 15 です.「出でて」や「雪はふりつつ」が共通しています.


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

3番目に近いと判定されたのが 23 と 47 です.これらは「こそ」「秋」が共通しています.


print(df[(df.id == 23) | (df.id == 47)][['id', 'uta', 'kajin']])
    id                               uta kajin
22  23  月みれば 千々に物こそ 悲しけれ 我が身ひとつの にはあらねど  大江千里
46  47  八重むぐら しげれる宿の さびしきに 人こそ見えね は来にけり  恵慶法師

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


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

目次に戻る

コード全体のまとめ

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


# モジュールのインポート
from janome.tokenizer import Tokenizer
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 = Tokenizer()
    results = t.tokenize(text)
    words = []
    for token in results:
        words.append(token.surface) # 表層形
    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

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].replace(' ', ''))
    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("Janome")
# plt.savefig("hyaku-dendrogram_janome.png", dpi=300, facecolor='white')
plt.show()

目次に戻る