ここでは N-gram を使って百人一首の歌をデータ化し,そのデータで階層的クラスタリングを実行することで似た歌を探します.
N-gram は文字列を連続した N 個の文字で分割する方法です.特に N が 1 の場合をユニグラム (uni-gram),2 の場合をバイグラム (bi-gram),3 の場合をトライグラム (tri-gram) と呼びます.例えば「自然言語処理の基本を説明します」という文をユニグラムで分割すると次のようになります.
['自', '然', '言', '語', '処', '理', 'の', '基', '本', 'を', '説', '明', 'し', 'ま', 'す']
バイグラムとトライグラムの例も示します.
['自然', '然言', '言語', '語処', '処理', '理の', 'の基', '基本', '本を', 'を説', '説明', '明し', 'しま', 'ます']
['自然言', '然言語', '言語処', '語処理', '処理の', '理の基', 'の基本', '基本を', '本を説', 'を説明', '説明し', '明しま', 'します']
ここページでは,よく知られた小倉百人一首の「かな」を N-gram によって分割したデータから階層的クラスタリングを用いることで,似た歌がどれとどれであるかについて分析します.
まず,必要なモジュールをすべてインポートします.
# モジュールのインポート
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']]
ここでは,データフレームから「かな」を取り出して,N-gram のデータを作成してみよう.まず,「id」列と「kana」列をデータフレームから取り出して,NumPy の2次元配列に格納します.
kana = df.loc[:, ['id','kana']].values
kana
array([[1, 'あきのたの かりほのいほの とまをあらみ わがころもでは つゆにぬれつつ'], [2, 'はるすぎて なつきにけらし しろたへの ころもほすてふ あまのかぐやま'], [3, 'あしびきの やまどりのをの しだりをの ながながしよを ひとりかもねむ'], ...(中略)... [98, 'かぜそよぐ ならのをがはの ゆふぐれは みそぎぞなつの しるしなりける'], [99, 'ひともをし ひともうらめし あぢきなく よをおもふゆゑに ものおもふみは'], [100, 'ももしきや ふるきのきばの しのぶにも なほあまりある むかしなりけり']], dtype=object)
先頭の歌を取り出して,「かな」だけを表示してみます.
kana[0][1]
'あきのたの かりほのいほの とまをあらみ わがころもでは つゆにぬれつつ'
上の通り,「かな」は空白で区切られています.N-gram を作成する前に,文字列の置換によって空白を削除する方法を確認します.
kana[0][1].replace(' ', '')
'あきのたのかりほのいほのとまをあらみわがころもではつゆにぬれつつ'
次に,N-gram を作成する(仮の)関数を作成してみます.
def get_Ngram_iter(sent, N=2):
ngram = []
for i in range(len(sent)-N+1):
ngram.append(sent[i:i+N])
return ngram
上の関数は for
文の繰り返しの中で,先頭から順に N 文字ずつ取得して,ngram
リストに追加しています.この関数に空白を削除した「かな」を与えて先頭の歌のバイグラムを作成してみます.
ngram_0 = get_Ngram_iter(kana[0][1].replace(' ', ''), 2)
print(ngram_0)
['あき', 'きの', 'のた', 'たの', 'のか', 'かり', 'りほ', 'ほの', 'のい', 'いほ', 'ほの', 'のと', 'とま', 'まを', 'をあ', 'あら', 'らみ', 'みわ', 'わが', 'がこ', 'ころ', 'ろも', 'もで', 'では', 'はつ', 'つゆ', 'ゆに', 'にぬ', 'ぬれ', 'れつ', 'つつ']
上の結果からバイグラムをうまく作成できていることがわかります.しかしながら上で作成した(仮の)関数 get_Ngram_iter
は美しくなく,処理速度も遅くなります.より高速に処理ができ,かつ美しいコードになるように内包表記を使って書き直した get_Ngram
関数を定義します.
def get_Ngram(sent, N=2):
return [sent[i:i+N] for i in range(len(sent)-N+1)]
いま作成した get_Ngram
が正しく動作する事を確認します.まずはユニグラムです.
ngram_0 = get_Ngram(kana[0][1].replace(' ', ''), 1)
print(ngram_0)
['あ', 'き', 'の', 'た', 'の', 'か', 'り', 'ほ', 'の', 'い', 'ほ', 'の', 'と', 'ま', 'を', 'あ', 'ら', 'み', 'わ', 'が', 'こ', 'ろ', 'も', 'で', 'は', 'つ', 'ゆ', 'に', 'ぬ', 'れ', 'つ', 'つ']
次はバイグラムです.
ngram_0 = get_Ngram(kana[0][1].replace(' ', ''), 2)
print(ngram_0)
['あき', 'きの', 'のた', 'たの', 'のか', 'かり', 'りほ', 'ほの', 'のい', 'いほ', 'ほの', 'のと', 'とま', 'まを', 'をあ', 'あら', 'らみ', 'みわ', 'わが', 'がこ', 'ころ', 'ろも', 'もで', 'では', 'はつ', 'つゆ', 'ゆに', 'にぬ', 'ぬれ', 'れつ', 'つつ']
トライグラムです.
ngram_0 = get_Ngram(kana[0][1].replace(' ', ''), 3)
print(ngram_0)
['あきの', 'きのた', 'のたの', 'たのか', 'のかり', 'かりほ', 'りほの', 'ほのい', 'のいほ', 'いほの', 'ほのと', 'のとま', 'とまを', 'まをあ', 'をあら', 'あらみ', 'らみわ', 'みわが', 'わがこ', 'がころ', 'ころも', 'ろもで', 'もでは', 'ではつ', 'はつゆ', 'つゆに', 'ゆにぬ', 'にぬれ', 'ぬれつ', 'れつつ']
さらに 5-gram も試してみます.
ngram_0 = get_Ngram(kana[0][1].replace(' ', ''), 5)
print(ngram_0)
['あきのたの', 'きのたのか', 'のたのかり', 'たのかりほ', 'のかりほの', 'かりほのい', 'りほのいほ', 'ほのいほの', 'のいほのと', 'いほのとま', 'ほのとまを', 'のとまをあ', 'とまをあら', 'まをあらみ', 'をあらみわ', 'あらみわが', 'らみわがこ', 'みわがころ', 'わがころも', 'がころもで', 'ころもでは', 'ろもではつ', 'もではつゆ', 'ではつゆに', 'はつゆにぬ', 'つゆにぬれ', 'ゆにぬれつ', 'にぬれつつ']
作成した get_Ngram
の動作を確認できたので,すべての歌から N-gram のデータを作成してみます.まず,念の為に歌の数の取得方法を確認します.
kana.shape[0]
100
次に,N-gram の N を 3 に指定したあと,すべての歌を N-gram に分解し,2次元リストを生成します.
N = 3
kana_ngram = []
for i in range(kana.shape[0]):
ngram = get_Ngram(kana[i][1].replace(' ', ''), N)
kana_ngram.append(ngram)
print(kana_ngram)
[ ['あきの', 'きのた', 'のたの', 'たのか', 'のかり', 'かりほ', 'りほの', 'ほのい', 'のいほ', 'いほの', 'ほのと', 'のとま', 'とまを', 'まをあ', 'をあら', 'あらみ', 'らみわ', 'みわが', 'わがこ', 'がころ', 'ころも', 'ろもで', 'もでは', 'ではつ', 'はつゆ', 'つゆに', 'ゆにぬ', 'にぬれ', 'ぬれつ', 'れつつ'], ['はるす', 'るすぎ', 'すぎて', 'ぎてな', 'てなつ', 'なつき', 'つきに', 'きにけ', 'にけら', 'けらし', 'らしし', 'ししろ', 'しろた', 'ろたへ', 'たへの', 'へのこ', 'のころ', 'ころも', 'ろもほ', 'もほす', 'ほすて', 'すてふ', 'てふあ', 'ふあま', 'あまの', 'まのか', 'のかぐ', 'かぐや', 'ぐやま'], ...(中略)... ['ひとも', 'ともを', 'もをし', 'をしひ', 'しひと', 'ひとも', 'ともう', 'もうら', 'うらめ', 'らめし', 'めしあ', 'しあぢ', 'あぢき', 'ぢきな', 'きなく', 'なくよ', 'くよを', 'よをお', 'をおも', 'おもふ', 'もふゆ', 'ふゆゑ', 'ゆゑに', 'ゑにも', 'にもの', 'ものお', 'のおも', 'おもふ', 'もふみ', 'ふみは'], ['ももし', 'もしき', 'しきや', 'きやふ', 'やふる', 'ふるき', 'るきの', 'きのき', 'のきば', 'きばの', 'ばのし', 'のしの', 'しのぶ', 'のぶに', 'ぶにも', 'にもな', 'もなほ', 'なほあ', 'ほあま', 'あまり', 'まりあ', 'りある', 'あるむ', 'るむか', 'むかし', 'かしな', 'しなり', 'なりけ', 'りけり'] ]
すべての歌で登場した N-gram を作りたいので,その前に上の2次元リストを1次元化します.なお,途中(100個目)まで表示したあと,全体の個数を表示すると 2935 個であることがわかります.
# 2次元リストを1次元化
kana_ngram_list = list(itertools.chain.from_iterable(kana_ngram))
print(kana_ngram_list[0:100])
print(len(kana_ngram_list))
['あきの', 'きのた', 'のたの', 'たのか', 'のかり', 'かりほ', 'りほの', 'ほのい', 'のいほ', 'いほの', 'ほのと', 'のとま', 'とまを', 'まをあ', 'をあら', 'あらみ', 'らみわ', 'みわが', 'わがこ', 'がころ', 'ころも', 'ろもで', 'もでは', 'ではつ', 'はつゆ', 'つゆに', 'ゆにぬ', 'にぬれ', 'ぬれつ', 'れつつ', 'はるす', 'るすぎ', 'すぎて', 'ぎてな', 'てなつ', 'なつき', 'つきに', 'きにけ', 'にけら', 'けらし', 'らしし', 'ししろ', 'しろた', 'ろたへ', 'たへの', 'へのこ', 'のころ', 'ころも', 'ろもほ', 'もほす', 'ほすて', 'すてふ', 'てふあ', 'ふあま', 'あまの', 'まのか', 'のかぐ', 'かぐや', 'ぐやま', 'あしび', 'しびき', 'びきの', 'きのや', 'のやま', 'やまど', 'まどり', 'どりの', 'りのを', 'のをの', 'をのし', 'のしだ', 'しだり', 'だりを', 'りをの', 'をのな', 'のなが', 'ながな', 'がなが', 'ながし', 'がしよ', 'しよを', 'よをひ', 'をひと', 'ひとり', 'とりか', 'りかも', 'かもね', 'もねむ', 'たごの', 'ごのう', 'のうら', 'うらに', 'らにう', 'にうち', 'うちい', 'ちいで', 'いでて', 'でてみ', 'てみれ', 'みれば'] 2935
上で得られた1次元リストから重複を除いて N-gram の集合を作成します.この結果,2428のパターンがあることがわかりました.
ngram_set = sorted(set(kana_ngram_list))
print(ngram_set)
print(len(ngram_set))
['あかつ', 'あきか', 'あきに', 'あきの', 'あきは', 'あきも', 'あくる', 'あけぬ', 'あけの', 'あけや', ...(中略)... 'をみし', 'をみれ', 'をもう', 'をもみ', 'をよた', 'をらば', 'をらむ', 'をるれ', 'をわす', 'をわた'] 2428
次に,歌ごとに N-gram の登場頻度を求めて2次元配列を作成するために,関数 get_ngram_frequency
を準備します.
def get_ngram_frequency(kana_ngram, ngram_set):
"""
ngramの頻度を返す
"""
n_docs = len(kana_ngram)
n_ngram_set = len(ngram_set)
# 行数 = 文書数, 列数 = 集合登録数 で ngram_frequency を初期化する
ngram_frequency = np.zeros((n_docs, n_ngram_set))
for i in range(n_docs):
for o in kana_ngram[i]:
ngram_frequency[i, ngram_set.index(o)] += 1
return ngram_frequency
上で作成した関数 get_ngram_frequency
にそれぞれの歌を N-gram に分割した kana_ngram
と N-gram の一覧 ngram_set
を与えて2次元配列を作成します.
ngram_frequency = get_ngram_frequency(kana_ngram, ngram_set)
print(ngram_frequency)
[[[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
...
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
上の ngram_frequency
のサイズを確認すると,100行 x 2428列であることがわかります.
ngram_frequency.shape
(100, 2428)
2つ上の結果でハイライトされた1行目の左から5つのデータを表示してみます.これは id = 1 の歌における N-gram の出現頻度を意味しています.
ngram_frequency[0][0:5]
array([0., 0., 0., 1., 0.])
つまり,「あきのたのかりほのいほのとまをあらみわがころもではつゆにぬれつつ」という歌のなかに「あきの」は1度登場するが,「あかつ」や「あきか」「あきに」「あきは」は登場しないことを意味しています.
ngram_set[0:5]
['あかつ', 'あきか', 'あきに', 'あきの', 'あきは']
これで分析に必要な N-gram の2次元配列 ngram_frequency
を生成できました.ここまで準備できれば,あとはここと同じ方法で階層的クラスタリングや k-means による非階層クラスタリングが実行できます.僅かな(?)違いは,ここの階層的クラスタリングの例 では2次元データを取り扱ったことに対して,この百人一首では 2428 次元のデータを取り扱っていることくらいです!
ここまでの手順で分析に必要なデータ ngram_frequency
が準備できたので,これを使って非階層的クラスタリングを行ってみよう.分類したいクラスタ数は仮に 5 として実行します(今回は分類するというよりも似たものを探したいので,この数字はあまり大きな意味は持ちません).また,個体間の距離にはユークリッド距離を,クラスタ間の距離にはウォード法を用いることとします.これらの詳細はここを確認してください.
# クラスタ数
num_clusters = 5
# 個体間の距離はユークリッド距離
# クラスタ間の距離はウォード法
clustering = AgglomerativeClustering(n_clusters = num_clusters, affinity='euclidean', linkage='ward').fit(ngram_frequency)
clustering.labels_
array([0, 1, 1, 4, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 4, 1, 1, 1, 1, 2, 3, 0, 1, 1, 1, 1, 1, 1, 1, 3, 3, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 4, 2, 1, 1, 0, 3, 0, 1, 1, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 2, 0])
上の結果は,各歌がどのクラスタに属するかを意味しています.この結果をデータフレームに列「cluster_id」として追加します.つまり「2: 春すぎて...」「3: あしびきの...」「5: 奥山に...」などが一つのクラスタに分類されているということです.
df['cluster_id'] = clustering.labels_
print(df[['id', 'uta', 'cluster_id']])
id uta cluster_id 0 1 秋の田の かりほの庵の 苫をあらみ わが衣手は 露にぬれつつ 0 1 2 春すぎて 夏来にけらし 白妙の 衣ほすてふ 天の香具山 1 2 3 あしびきの 山鳥の尾の しだり尾の ながながし夜を ひとりかも寝む 1 3 4 田子の浦に うち出でて見れば 白妙の 富士の高嶺に 雪はふりつつ 4 4 5 奥山に もみぢ踏み分け 鳴く鹿の 声聞く時ぞ 秋は悲しき 1 .. ... ... ... 95 96 花さそふ 嵐の庭の 雪ならで ふりゆくものは わが身なりけり 0 96 97 こぬ人を まつほの浦の 夕なぎに 焼くやもしほの 身もこがれつつ 1 97 98 風そよぐ ならの小川の 夕暮れは みそぎぞ夏の しるしなりける 0 98 99 人もをし 人も恨めし あぢきなく 世を思ふゆゑに 物思ふ身は 2 99 100 百敷や ふるき軒端の しのぶにも なほあまりある 昔なりけり 0 [100 rows x 3 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(f"{N}-gram")
# plt.savefig(f"hyaku-dendrogram_{N:02d}.png", dpi=300, facecolor='white')
plt.show()
上のデンドログラムを確認すると(拡大して見てください),65と67の歌が最も近いことがわかります.この2つの歌を表示して見ます.この結果「む 名こそ惜しけれ
」が共通していることがわかりました.
print(df[(df.id == 65) | (df.id == 67)][['id', 'uta','cluster_id']])
id uta cluster_id 64 65 恨みわび ほさぬ袖だに あるものを 恋にくちなむ 名こそ惜しけれ 0 66 67 春の夜の 夢ばかりなる 手枕に かひなく立たむ 名こそ惜しけれ 0
次に近い歌が 21 と 81 でこれに 31 と 30 が順に連結されていることがわかりました.これら4つの歌を表示してみます.この結果,21 と 81, 31 では「有明の月(ありあけの月)」が共通しており,30には「ありあけの」が含まれます.さらに,21 と 81 では「長月の」と「ながむれば」の「なが」が共通していることもわかります.
print(df[(df.id == 21) | (df.id == 81) | (df.id == 31) | (df.id == 30)][['id', 'uta','cluster_id']])
id uta cluster_id 20 21 今こむと 言ひしばかりに 長月の 有明の月を 待ちいでつるかな 3 29 30 ありあけの つれなく見えし 別れより 暁ばかり 憂きものはなし 3 30 31 朝ぼらけ ありあけの月と 見るまでに 吉野の里に 降れる白雪 3 80 81 ほととぎす 鳴きつる方を ながむれば ただありあけの 月ぞ残れる 3
デンドログラムから 43 と 86 も似た歌であることがわかります.これらは「は物を 思は」が共通します.
print(df[(df.id == 43) | (df.id == 86)][['id', 'uta','cluster_id']])
id uta cluster_id 42 43 あひ見ての のちの心に くらぶれば 昔は物を 思はざりけり 0 85 86 嘆けとて 月やは物を 思はする かこち顔なる わが涙かな 0
その次に近い歌は 4 と 15 です.これらは「に 雪はふりつつ」だけでなく「出でて」が共通します.
print(df[(df.id == 4) | (df.id == 15)][['id', 'uta','cluster_id']])
id uta cluster_id 3 4 田子の浦に うち出でて見れば 白妙の 富士の高嶺に 雪はふりつつ 4 14 15 君がため 春の野に出でて 若菜つむ わが衣手に 雪はふりつつ 4
ここまでは説明しながらコードの断片を示したので,ここでコード全体をまとめて,不要な箇所は削除したものを示します.
# モジュールのインポート
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')
# N-gram の設定:ここを変更するだけで良い
N = 2
"""
N-gramを返す
"""
def get_Ngram(sent, N=2):
return [sent[i:i+N] for i in range(len(sent) - N + 1)]
"""
N-gramの頻度を返す
"""
def get_ngram_frequency(kana_ngram, ngram_set):
n_docs = len(kana_ngram)
n_ngram_set = len(ngram_set)
# 行数 = 文書数, 列数 = 集合登録数 で ngram_frequency を初期化する
ngram_frequency = np.zeros((n_docs, n_ngram_set))
for i in range(n_docs):
for o in kana_ngram[i]:
ngram_frequency[i, ngram_set.index(o)] += 1
return ngram_frequency
# CSV ファイルの URL を指定する
url = "https://github.com/rinsaka/sample-data-sets/blob/master/hyaku-utf-8.csv?raw=true"
# CSV ファイルをデータフレームに読み込む
df = pd.read_csv(url)
# データフレームから kana を取り出し, NumPy 配列に格納する
kana = df.loc[:, ['id','kana']].values
# すべての歌を ngram に分解し,2次元リストを生成する
kana_ngram = []
for i in range(kana.shape[0]):
ngram = get_Ngram(kana[i][1].replace(' ', ''), N)
kana_ngram.append(ngram)
# 2次元リストを1次元化
kana_ngram_list = list(itertools.chain.from_iterable(kana_ngram))
# 一次元リストから重複を除いた N-gram の集合
ngram_set = sorted(set(kana_ngram_list))
# Ngramの出現頻度
ngram_frequency = get_ngram_frequency(kana_ngram, ngram_set)
# クラスタ数
num_clusters = 5
# 個体間の距離はユークリッド距離
# クラスタ間の距離はウォード法
clustering = AgglomerativeClustering(n_clusters = num_clusters, affinity='euclidean', linkage='ward').fit(ngram_frequency)
# 結果をデータフレームに追加
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(f"{N}-gram")
# plt.savefig(f"hyaku-dendrogram_{N:02d}.png", dpi=300, facecolor='white')
plt.show()
次の感度分析は,上の 18 行目にある N = 2
の箇所を変更するだけで実行できます.
上のコードの 18 行目を N = 1
とするとユニグラムでの結果が得られます.
このデンドログラムから 72 と 90 が最も近いことがわかります.これを詳しく調べると 72 の「おとにきくたかしのはまのあだなみはかけじやそでのぬれもこそすれ」という31文字中,「にかしのはまのあだなみはかじやそでのぬれもそれ」という23文字が 90 の「みせばやなをじまのあまのそでだにもぬれにぞぬれしいろはかはらず」に含まれていることがわかりました.
print(df[(df.id == 72) | (df.id == 90)][['id', 'uta', 'kana']])
id uta kana 71 72 音にきく たかしの浜の あだ波は かけじや袖の ぬれもこそすれ おとにきく たかしのはまの あだなみは かけじやそでの ぬれもこそすれ 89 90 見せばやな 雄島のあまの 袖だにも 濡れにぞ濡れし 色は変はらず みせばやな をじまのあまの そでだにも ぬれにぞぬれし いろはかはらず
上のコードの 18 行目を N = 2
とするとユニグラムでの結果が得られます.
上のデンドログラムから 21 と 81 が最も近いことがわかります.これらは「ありあけのつき」が共通します.これだけでなく「なが」も共通し,「つき」はどちらも2度登場します.
print(df[(df.id == 21) | (df.id == 81)][['id', 'uta']])
id uta 20 21 今こむと 言ひしばかりに 長月の 有明の月を 待ちいでつるかな 80 81 ほととぎす 鳴きつる方を ながむれば ただありあけの 月ぞ残れる
上のコードの 18 行目が N = 3
であればすでに示したトライグラムでの結果が得られます.
トライグラムで最も近くなるのは 65 と 67 で,これらは「む 名こそ惜しけれ」が共通します.
print(df[(df.id == 65) | (df.id == 67)][['id', 'uta']])
id uta 64 65 恨みわび ほさぬ袖だに あるものを 恋にくちなむ 名こそ惜しけれ 66 67 春の夜の 夢ばかりなる 手枕に かひなく立たむ 名こそ惜しけれ
上のコードの 18 行目を N = 4
とすると 4-gram での結果が得られます.
4-gram では 43 と 86 が最も近いと判定されます.これらは「は物を 思は」が共通します.これはトライグラムでも比較的近いと判定されていました.
print(df[(df.id == 43) | (df.id == 86)][['id', 'uta']])
id uta 42 43 あひ見ての のちの心に くらぶれば 昔は物を 思はざりけり 85 86 嘆けとて 月やは物を 思はする かこち顔なる わが涙かな
なお,4-gram のデンドログラムはトライグラムのそれと比較して,連鎖的につながる傾向が出ていることがわかります.連鎖的な連結の例はここで確認してください.
さらに,5-gram, 6-gram, 7-gram でのデンドログラムも示します.連結がより連鎖的になってしまう傾向が強くなることがわかります.やはり,バイグラムやトライグラムを使うことが良さそうです.
ここでの例では N = 2 または N = 3 が最適で,N を大きくするとここでの理想的な結果を得るこはできませんでした.しかしながら,ある程度大きな N を使って剽窃チェッカーを作ることはできそうですね.つまり他人の文章をコピーペーストした文章を簡単に発見できそうです.