ここでは 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
読み込んだデータフレームから目的のデータを表示する方法について確認してみよう.まず,インデックス 0 を指定して1つの歌を表示します.
df[df.index == 0]
次は id = 1 を指定して同じ歌を表示します.
df[df.id == 1]
「kana(かな)」以外の列を表示します.
df[df.id == 1][['id', 'uta', 'kajin']]
ここではデータフレームから「うた(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()
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()