文書の類似度を求めるためのTF・IDF

# ベクトル空間モデル

文書中に出現する用語の頻度数をベクトルの要素として考え、複数の文書をベクトルとして表現し、文書の特徴を調べるための手法。
例えば、２つの文書があったとき、同じ用語の出現頻度が高ければその用語に関する特徴を持つ傾向があることが予想できる。
余談だが、このように、２つの文書に同じ用語が現れることを共起という。

# 用語-文書行列（Term-Document Matrix）

各文書を行に、全文書に出現する各用語を行にとり、用語の出現頻度をまとめた行列。

例）文書と形態素解析の結果
文書A　ねぎ　焼肉　焼肉　ビール　焼肉　ねぎ
文書B　ビール　焼肉　焼肉　ビール　しょうゆ　焼肉　しょうゆ　ワイン
文書C　五苓黄解　五苓黄解　小柴胡湯　しょうゆ　ワイン　小柴胡湯
文書D　ビール　小柴胡湯　五苓黄解　小柴胡湯　小柴胡湯　

### 用語-文書行列
|       | ねぎ | しょうゆ | ビール | 焼肉 | 五苓黄解 | 小柴胡湯 | ワイン |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| 文書A | 2 |  | 1 | 3 |  |  |  |
| 文書B |  | 2 | 2 | 3 |  |  | 1 |
| 文書C |  | 1 |  |  | 2 | 2 | 1 |
| 文書D |  |  | 1 |  | 1 | 3 |  |

このように行列にすることで、例えば、「文書AとBでは焼肉が特徴になりそうだ」など、解釈を促すことができる。

# 用語頻度（Term Frequency;TF）

各文書単位の全用語数に対するある用語の出現頻度の割合を相対的に表した指標。
例えば、文書Aの焼肉ならば、文書Aで出現した全用語数は「６」であるため、文書Aの焼肉の出現数/文書A全用語数 = 0.5 と求められる。

各文書の全用語数はそれぞれ、6, 8, 6, 5なので、用語-文書行列を用語頻度で示すと次のようになる。

### 用語頻度行列
|       | ねぎ | しょうゆ | ビール | 焼肉 | 五苓黄解 | 小柴胡湯 | ワイン |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| 文書A | 0.333 | 0.000 | 0.167 | 0.500 | 0.000 | 0.000 | 0.000 |
| 文書B | 0.000 | 0.250 | 0.250 | 0.375 | 0.000 | 0.000 | 0.125 |
| 文書C | 0.000 | 0.167 | 0.000 | 0.000 | 0.333 | 0.333 | 0.167 |
| 文書D | 0.000 | 0.000 | 0.200 | 0.000 | 0.200 | 0.600 | 0.000 |

式で表すとこうなる。

$w_t^d=\frac{tf(t,d)}{\sum_{s\in{d}}tf(s,d)}$

# Inverse Document Frequency;IDF

用語が出現する文書数と全文書数から、用語に着目した特徴を表す指標。
TFは文書ごとの特徴を見つけるために役立つのに対して、IDFは文書全体にとって希少な用語を見つける指標として利用できる。
ある用語のIDFについて考えると、ある用語の出現する文書数が少ないほど、大きな値をとることになる。
式で表すとこうなる。

$idf(t)=\log{\Biggl[\frac{N}{df(t)}\Biggr]}+1$

例えば、文書Aには「ねぎ」があるが、その頻度は少なく、TFだけでみるとその特徴は印象的ではない。一方で、文書全体で見た時の「ねぎ」は、文書Aにしかないことがわかる。よって、「ねぎ」は文書Aにとって特徴的な用語であると予想できる。

IDFの計算結果を次に示す。
|       | ねぎ | しょうゆ | ビール | 焼肉 | 五苓黄解 | 小柴胡湯 | ワイン |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| IDF | 1.602 | 1.301 | 1.125 | 1.301 | 1.301 | 1.301 | 1.301 |

例えば、「ねぎ」の場合は、全文書数は4、ねぎの全文書に対する出現頻度は1（ねぎは文書Aで出現しているので１。各文書における用語の出現回数の和でないことに注意）であるから、log10(4/1)+1=1.602となる。
別の例では、「しょうゆ」の場合は、全文書数は4、しょうゆの全文書に対する出現頻度は2（しょうゆは文書BとCで出現しているので）であるから、log10(4/2)+1=1.301となる。

# TF・IDF

TFは、文書ごとの用語の重みを定量化する※１。IDFは、用語ごとの全文書内での重みを定量化する※２。

※１：特定の文書内での用語の出現頻度が重視される。
※２：前項に示したように、「ねぎ」は文書Aにしか出現しないためにIDFの値は大きい（注目されやすい）。一方、文書Aの「ビール」は３つの文書で出現しているために1.125と小さい。

このことから、TFは文書、IDFは用語の重要度のようなものを示しているともとらえることができる。そして、これらを掛け合わせることで、双方を考慮した重みとしての指標ができる。この指標がTF・IDFである。
IT・IDFは、文書の長短や書き手の文章作成のクセのようなバイアスを抑えつつ、文章の特徴を捉えることができる指標になりえる。
TF・IDFは次の式で表される。

$tf・idf=w_t^d・idf(t)$

つまりは、TFとIDFを掛け合わせると求められる。
TF・IDFを計算すると次のようになる。

### TF・IDF行列
|       | ねぎ | しょうゆ | ビール | 焼肉 | 五苓黄解 | 小柴胡湯 | ワイン |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| 文書A | 0.534 | 0.000 | 0.187 | 0.651 | 0.000 | 0.000 | 0.000 |
| 文書B | 0.000 | 0.325 | 0.281 | 0.488 | 0.000 | 0.000 | 0.163 |
| 文書C | 0.000 | 0.217 | 0.000 | 0.000 | 0.434 | 0.434 | 0.217 |
| 文書D | 0.000 | 0.000 | 0.225 | 0.000 | 0.260 | 0.781 | 0.000 |

文書Aのねぎの場合は、TF：0.333、IDF：1.602から、TF・IDF=0.333*1.602=0.534となる。

# 類似度を求める

文書間の類似度はTF、IDF、TF・IDFから求めることができる。
ここでは、TFあるいはIDFのみでなく、TFとIDFの両方の特徴を捉えていると考えられるTF・IDFで類似度を求める方法を示す。

ここで、これまで扱った各文書を全データとした場合、形態素解析の結果、７つの用語に出現する用語が絞られたとする。
この７つの用語で各文書を表現したいため、ここでは、各文書を７用語＝７次元で表すことなる。
このとき、７つの次元からなるベクトルの向きが同じ方向を向けば（同じ大きさであれば）同じ性質を持つと仮定し、逆の場合（離れている場合）は異なる性質を持つとする。
ベクトルの類似度は一般化されたベクトルの大きさの算術式をもとに、次の式から得られる。

$s\Bigl(\vec{d_x},\vec{d_y}\Big{)}=\frac{\vec{d_x}・\vec{d_y}}{\begin{Vmatrix}\vec{d_x}\end{Vmatrix}\begin{Vmatrix}\vec{d_y}\end{Vmatrix}}$

ここで、$\vec{d_x}$は対象データ、$\vec{d_y}$は比較データを示し、それぞれTF・IDFであるとする。
すなわち、この条件下では、$\vec{d_x}=(x_1,x_2,...x_7)$、かつ、$\vec{d_y}=(y_1,y_2,...y_7)$。
また、式中の分子は、これらのベクトルの内積を表し、分母は、ノルムを表す。
具体的には、文書Aと文書Bでは、

$文書\vec{A}=(0.534,0.000,0.187,0.651,0.000,0.000,0.000)$
$文書\vec{B}=(0.000,0.325,0.281,0.488,0.000,0.000,0.163)$

分子部分の計算は、

$\vec{d}_x・\vec{d}_y=x_1・y_1+x_2・y_2+...x_7・y_7$

（つまり、$文書\vec{A}・文書\vec{B}=0.534×0.000+0.325×0.187+...+0.000×0.163=0.370$）

次に、分母部分の計算は、各ベクトルのノルムを表すため、

$\begin{Vmatrix}\vec{d_x}\end{Vmatrix}=\sqrt{x_1^2+x_2^2+...+x_7^2}$
$\begin{Vmatrix}\vec{d_y}\end{Vmatrix}=\sqrt{y_1^2+y_2^2...+y_7^2}$

※各要素を二乗している

例えば、文書Aと文書Bの場合は、

$\begin{Vmatrix}文書\vec{A}\end{Vmatrix}=\sqrt{0.534^2+0.187^2+0.651^2}=0.862$
$\begin{Vmatrix}文書\vec{B}\end{Vmatrix}=\sqrt{0.325^2+0.281^2+0.488^2+0.163^2}=0.670$

よって、類似度$S(similarity)$は、

$s(文書\vec{A},文書\vec{B})=\frac{0.370}{0.862・0.670}=0.640$

このとき、類似度$S$は最小値0、最大値1の間の値をとる。ベクトルが完全に一致した場合は、１となり、その逆は０となる。

# 実装
Colabで試していきます。
```
# 用語の抽出
'''
ここでは、簡単な例で。実際は、ここが一番難しい、、、。
'''
'''
MANBYO辞書を利用して形態素解析する例（ここでの処理には使わないが、例として）
'''
!sudo apt install mecab
!sudo apt install libmecab-dev
!sudo apt install mecab-ipadic-utf8
```

```
!wget http://sociocom.jp/~data/2018-manbyo/data/MANBYO_201907_Dic-utf8.dic #辞書をDL
```

```
!pip install mecab-python3==0.996.5 # バージョンを指定する。これじゃないと動かないようだ
```

```
# サンプルデータ作成とMeCab動作確認
import MeCab
import pandas as pd
import numpy as np

mecab = MeCab.Tagger('-u /content/MANBYO_201907_Dic-utf8.dic')

res = mecab.parse('ねぎ　焼肉　焼肉　ビール　焼肉　ねぎ')
print(res)
res = mecab.parse('ビール　焼肉　焼肉　ビール　しょうゆ　焼肉　しょうゆ　ワイン')
print(res)
# 漢方はうまく取れないみたいだ（2018 version）
res = mecab.parse('五苓黄解　五苓黄解　小柴胡湯　しょうゆ　ワイン　小柴胡湯')
print(res)
res = mecab.parse('ビール　小柴胡湯　五苓黄解　小柴胡湯　小柴胡湯')
print(res)
```

```
'''
(文章の前処理、形態素の前処理で、下記結果が得られた前提で。データセットを整理するまでが難しい。)
データセット
'''
docA = ['ねぎ','焼肉','焼肉','ビール','焼肉','ねぎ']
docB = ['ビール','焼肉','焼肉','ビール','しょうゆ','焼肉','しょうゆ','ワイン']
docC = ['五苓黄解','五苓黄解','小柴胡湯','しょうゆ','ワイン','小柴胡湯']
docD = ['ビール','小柴胡湯','五苓黄解','小柴胡湯','小柴胡湯']
docs = [docA, docB, docC, docD] # データセット全体
```

```
# 全部で７つの用語であることを確認
ds_list = docA+docB+docC+docD
morpheme = list(set(ds_list))
morpheme
```

```
# 用語-文書行列
def getMatrix(docs):
  '''
  docs :文書リスト（ここでは、文書ごとに用語に分割されたリストの配列） 
  '''
  all_term = []
  for doc in docs:
    for t in doc:
      all_term.append(t)
  morpheme = list(set(all_term))
  columns = morpheme
  mat = np.zeros((len(docs),len(columns)))
  df = pd.DataFrame(mat, columns=columns)
  for i, doc in enumerate(docs):
    for t in doc:
      for j, c in enumerate(columns):
        if t == c:
          df.iloc[i,j] += 1
  return df

# test
tdm = getMatrix(docs)
tdm
```

```
# TF計算
def getTF(term_freq_mat):
  tf = term_freq_mat.copy()
  for i in range(term_freq_mat.shape[0]):
    tdm_row = term_freq_mat.iloc[i]
    tdm_row_sum = tdm_row.values.sum()
    for j, v in enumerate(tdm_row.values):
      tf.iloc[i,j] = v/tdm_row_sum
  return tf

# test
tf = getTF(tdm)
tf
```

```
# IDF計算
def calcIDF(tf):
  num_doc = tf.shape[0]
  morpheme = tf.columns.values
  idf_res = [[]]
  for m in morpheme:
    col = tf[m]
    count = 0
    for v in col:
      if v > 0.0:
        count+= 1
    idf_res[0].append(np.log10(num_doc/(count+0.000001))+1) # logでも計算したいことは同じなので、単にlogでもよさそう。
  idf = pd.DataFrame(idf_res, columns=tf.columns)
  return idf

# test
idf = calcIDF(tf)
idf
```

```
# TF・IDF計算
def calcTF_IDF(tf,idf):
  tf_idf = tf.copy()
  for i in range(tf.shape[0]):
    row = tf.iloc[i]
    for j, c in enumerate(idf.iloc[0]):
      tf_idf.iloc[i,j] = row[j] * c
  return tf_idf

# test
tf_idf = calcTF_IDF(tf,idf)
tf_idf
```
```
# 類似度計算
def toNorm(tf_idf_row):
  vec = 0
  for v in tf_idf_row.values:
    vec += v*v
  return np.sqrt(vec+0.0000001)

def calcSimilarity(tf_idf_src_row, tf_idf_trg_row):
  d = np.dot(tf_idf_src_row.values, tf_idf_trg_row.values)
  n0 = toNorm(tf_idf_src_row)
  n1 = toNorm(tf_idf_trg_row)
  return d/((n0*n1)+0.0000001)

# test
calcSimilarity(tf_idf.iloc[0], tf_idf.iloc[1]) # 0.6403025672831193
```

備忘録として書きました。
詳細はリファレンスにある書籍をご参照ください。

# リファレンス
- わかりやすいJavaによる医療言語処理入門　上杉正人（編著）

Tatsuaki Kobayashi