Python入門トップページ


目次

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

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

N-gram を使って階層的クラスタリング

ここでは N-gram を使って百人一首の歌をデータ化し,そのデータで階層的クラスタリングを実行することで似た歌を探します.

目次に戻る

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
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

目次に戻る

「かな」の N-gram データを作成する

ここでは,データフレームから「かな」を取り出して,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 の頻度を求める

次に,歌ごとに 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()
hyaku-dendrogram_03

上のデンドログラムを確認すると(拡大して見てください),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 の箇所を変更するだけで実行できます.

目次に戻る

N の感度分析

ユニグラム

上のコードの 18 行目を N = 1 とするとユニグラムでの結果が得られます.

hyaku-dendrogram_01

このデンドログラムから 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 とするとユニグラムでの結果が得られます.

hyaku-dendrogram_02

上のデンドログラムから 21 と 81 が最も近いことがわかります.これらは「ありあけのつき」が共通します.これだけでなく「なが」も共通し,「つき」はどちらも2度登場します.

print(df[(df.id == 21) | (df.id == 81)][['id', 'uta']])
    id                               uta
20  21   今こむと 言ひしばかりに 長月の 有明の月を 待ちいでつるかな
80  81  ほととぎす 鳴きつる方を ながむれば ただありあけの 月ぞ残れる

トライグラム

上のコードの 18 行目が N = 3 であればすでに示したトライグラムでの結果が得られます.

hyaku-dendrogram_03

トライグラムで最も近くなるのは 65 と 67 で,これらは「む 名こそ惜しけれ」が共通します.

print(df[(df.id == 65) |  (df.id == 67)][['id', 'uta']])
    id                               uta
64  65  恨みわび ほさぬ袖だに あるものを 恋にくちなむ 名こそ惜しけれ
66  67   春の夜の 夢ばかりなる 手枕に かひなく立たむ 名こそ惜しけれ

4-gram

上のコードの 18 行目を N = 4 とすると 4-gram での結果が得られます.

hyaku-dendrogram_04

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

さらに,5-gram, 6-gram, 7-gram でのデンドログラムも示します.連結がより連鎖的になってしまう傾向が強くなることがわかります.やはり,バイグラムやトライグラムを使うことが良さそうです.

hyaku-dendrogram_05
hyaku-dendrogram_06
hyaku-dendrogram_07

ここでの例では N = 2 または N = 3 が最適で,N を大きくするとここでの理想的な結果を得るこはできませんでした.しかしながら,ある程度大きな N を使って剽窃チェッカーを作ることはできそうですね.つまり他人の文章をコピーペーストした文章を簡単に発見できそうです.

目次に戻る