ここでは,TF-IDF の計算をしてみます.
まずはモジュールをインポートします.エラーが出たら,必要なモジュールを pip
でインストールしてください.
モジュールのインポートimport MeCab as mc
import os
import re # 正規表現
import numpy as np
ここでは TF-IDF を計算するモジュールを準備します.なお,下のコードの48行目では,名詞だけを取り出しています.動詞や形容詞,副詞も取り出したい場合は50~51行目も有効にしてください.また45行目,51行目の node.feature.split(",")[0]
,node.feature.split(",")[6]
はそれぞれ形態素解析の出力結果から品詞と原型を取り出しています.出力フォーマットはここで確認できます.
モジュールの準備def strip_CRLF_from_Text(text):
"""
テキストファイルの改行,タブを削除する.
改行文字やタブ文字の前後が日本語文字の場合はそれを削除する.
それ以外はスペースに置換する.
"""
# 改行前後の文字が日本語文字の場合は改行を削除する
plaintext = re.sub('([ぁ-んー]+|[ァ-ンー]+|[\\u4e00-\\u9FFF]+|[ぁ-んァ-ンー\\u4e00-\\u9FFF]+)(\n)([ぁ-んー]+|[ァ-ンー]+|[\\u4e00-\\u9FFF]+|[ぁ-んァ-ンー\\u4e00-\\u9FFF]+)',
r'\1\3',
text)
# 残った改行とタブ記号はスペースに置換する
plaintext = plaintext.replace('\n', ' ').replace('\t', ' ')
return plaintext
def get_Text_from_Filepaths(filepaths):
"""
ファイル名のリストを与えて,辞書を作成して返す
"""
raw = ''
# ファイルを開く
for filepath in filepaths:
f = open(filepath, encoding='utf-8')
raw += f.read()
f.close()
# 改行を削除する
text = strip_CRLF_from_Text(raw)
return text
def mecab_analysis(text):
"""
MeCabをつかって単語を切り出してリストに詰める関数.
可視化して意味がありそうな単語を抽出するために品詞は名詞だけ(あるいは名詞、動詞、形容詞、副詞)に限定.
"""
t = mc.Tagger('')
# t = mc.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/')
node = t.parseToNode(text)
# print(node)
words = []
while(node):
# print(node.surface, node.feature)
if node.surface != "": # ヘッダとフッタを除外
word_type = node.feature.split(",")[0]
# 名詞だけをリストに追加する
if word_type in ["名詞"]:
words.append(node.surface) # node.surface は「表層形」
# 動詞(の原型),形容詞,副詞もリストに加えたい場合は次の2行を有効にする
# if word_type in [ "動詞", "形容詞","副詞"]:
# words.append(node.feature.split(",")[6]) # node.feature.split(",")[6] は形態素解析結果の「原型」
node = node.next
if node is None:
break
return words
def get_DF_from_Filepaths(filepaths, vocab):
"""
ファイル名のリストを与えて,DFの値を返します.
DFは索引語が出現する文書数のこと.
"""
# 辞書の長さと同じ長さで DF を初期化する
df = np.zeros((len(vocab), 1))
for filepath in filepaths:
f = open(filepath, encoding='utf-8')
raw = f.read()
text = strip_CRLF_from_Text(raw) # 改行を削除
words = mecab_analysis(text) # 名詞だけのリストを生成
for s in set(words): # 単語の重複を除いて登場した単語を数える
df[vocab.index(s), 0] += 1
return df
def get_TF_from_Filepaths(filepaths, vocab):
"""
ファイル名のリストを与えて,TFの値を返します.
TFは索引語の出現頻度のこと.
"""
n_docs = len(filenames)
n_vocab = len(vocab)
# 行数 = 登録辞書数, 列数 = 文書数 で tf を初期化する
tf = np.zeros((n_vocab, n_docs))
for filepath in filepaths:
f = open(filepath, encoding='utf-8')
raw = f.read()
text = strip_CRLF_from_Text(raw)
words = mecab_analysis(text)
for w in words:
tf[vocab.index(w), filepaths.index(filepath)] += 1
return tf
def get_TFIDF_from_TF_DF(tf, df):
"""
TFとDFを与えて,TF-IDFの値を返します.
"""
return tf/df
def get_distance_matrix(tfidf):
"""
tfidf の行列を渡せば,文書間の距離を計算して,行列を返します.
"""
n_docs = tfidf.shape[1]
n_words = tfidf.shape[0]
# 結果を格納する行列を準備(初期化)する
distance_matrix = np.zeros([n_docs, n_docs]) # 文書数 x 文書数
for origin in range(n_docs): # origin : 比較元文書
tmp_matrix = np.zeros([n_words, n_docs]) # 単語数 x 文書数
# 比較元文書のTFIDFを取得する
origin_tfidf = tfidf[0:n_words, origin]
# 各要素の二乗誤差を取る
for i in range(n_docs): # 列のループ 0:5 (文書数)
for j in range(n_words): # 行のループ 0:20 (単語数)
tmp_matrix[j, i] = (tfidf[j, i] - origin_tfidf[j])**2
# 二乗誤差の合計の平方根を計算
for i in range(n_docs):
distance_matrix[origin, i] = np.sqrt(tmp_matrix.sum(axis=0)[i])
return distance_matrix
必要な関数が準備できたので,複数の文書ファイルを開いて分かち書きし,名詞だけを取り出す.
複数文書の分かち書き### 文書ファイルを指定する.
filenames = [ 'sample_1.txt',
'sample_2.txt',
'sample_3.txt',
'sample_4.txt',
'sample_5.txt'
]
### フォルダ名も付与してファイルパス一覧のリストを生成
filepaths = [os.path.sep.join(['corpora', fname]) for fname in filenames]
### 文書ファイルを開いてテキストデータを取得する
text = get_Text_from_Filepaths(filepaths)
### Mecabによる形態素解析
### すべての文書を分かち書きをして,名詞だけ取り出したリストを作成する
words = mecab_analysis(text)
print(words)
['勤務', '先', '社内', '自然', '言語', '処理', '勉強', '会', '発表', '資料', '自然', '言語', '処理', '基本', '説明', '文書', '自然', '言語', '処理', '基本', '類似', '文書', '推薦', '説明', 'これ', '学会', '発表', '資料', '資料', 'ジャズ', 'ライブ', '資料', 'ライブ', '何', '説明', '自然', '言語', '処理', '基本', '説明', '自然', '言語', '処理', '基本', '説明', '自然', '言語', '処理', '基本', '説明']
取り出された名詞から重複を除いた語彙のリストを作成する.なお set()
によって集合が生成されます.
名詞のリストから重複を除いた語彙のリストを生成するvocab = sorted(set(words))
print(vocab)
['これ', 'ジャズ', 'ライブ', '会', '何', '先', '処理', '勉強', '勤務', '基本', '学会', '推薦', '文書', '発表', '社内', '自然', '言語', '説明', '資料', '類似']
DFを計算してリスト形式で得る.ここで,結果のインデックス0(先頭)の [1.] は vocab[0]
の「これ」という語彙が1つの文書に登場していることを意味し,インデックス6(先頭から7番目)の [4.] は vocab[6]
の「処理」という語彙が4つの文書に登場していることを意味する.さらに「説明」という語彙はすべて(5個)の文書に登場していることもわかります.
df = get_DF_from_Filepaths(filepaths, vocab)
print(df)
[[1.] [1.] [1.] [1.] [1.] [1.] [4.] [1.] [1.] [4.] [1.] [1.] [1.] [2.] [1.] [4.] [4.] [5.] [3.] [1.]]
次は TF を計算します.TF は各文書における索引語の出現頻度を意味します.例えば,1行目の [0. 1. 0. 0. 0.] は「これ」という索引語が2番目の文書に1度出現していることを意味します.また下から3行目の [1. 1. 1. 1. 2.] は「説明」という語彙が5番目の文書に2度登場し,その他の文書では1度登場していることを意味します.
tf = get_TF_from_Filepaths(filepaths, vocab)
print(tf)
[[0. 1. 0. 0. 0.] [0. 0. 1. 0. 0.] [0. 0. 2. 0. 0.] [1. 0. 0. 0. 0.] [0. 0. 1. 0. 0.] [1. 0. 0. 0. 0.] [2. 1. 0. 1. 2.] [1. 0. 0. 0. 0.] [1. 0. 0. 0. 0.] [1. 1. 0. 1. 2.] [0. 1. 0. 0. 0.] [0. 1. 0. 0. 0.] [0. 2. 0. 0. 0.] [1. 1. 0. 0. 0.] [1. 0. 0. 0. 0.] [2. 1. 0. 1. 2.] [2. 1. 0. 1. 2.] [1. 1. 1. 1. 2.] [1. 1. 2. 0. 0.] [0. 1. 0. 0. 0.]]
TF と DF が得られたので,TF-IDF を求めてみよう.
TF-IDF を計算するtfidf= get_TFIDF_from_TF_DF(tf, df)
print(tfidf)
[[0. 1. 0. 0. 0. ] [0. 0. 1. 0. 0. ] [0. 0. 2. 0. 0. ] [1. 0. 0. 0. 0. ] [0. 0. 1. 0. 0. ] [1. 0. 0. 0. 0. ] [0.5 0.25 0. 0.25 0.5 ] [1. 0. 0. 0. 0. ] [1. 0. 0. 0. 0. ] [0.25 0.25 0. 0.25 0.5 ] [0. 1. 0. 0. 0. ] [0. 1. 0. 0. 0. ] [0. 2. 0. 0. 0. ] [0.5 0.5 0. 0. 0. ] [1. 0. 0. 0. 0. ] [0.5 0.25 0. 0.25 0.5 ] [0.5 0.25 0. 0.25 0.5 ] [0.2 0.2 0.2 0.2 0.4 ] [0.33333333 0.33333333 0.66666667 0. 0. ] [0. 1. 0. 0. 0. ]]
いま得られた TF-IDF を用いて,文書間の距離を計算してみよう.この結果を見ると,「sample_4.txt」と「sample_5.txt」の距離が「0.53851648」と最も近いことから,似た文書であることがわかります.実際に中身を比較してみてください.
文書間の距離を計算するdistance_matrix = get_distance_matrix(tfidf)
print(distance_matrix)
[[0. 3.63145976 3.48907024 2.355549 2.33743687] [3.63145976 0. 3.82244831 2.8915586 2.94127712] [3.48907024 3.82244831 0. 2.58736245 2.73577127] [2.355549 2.8915586 2.58736245 0. 0.53851648] [2.33743687 2.94127712 2.73577127 0.53851648 0. ]]
単語ごとの出現頻度をわかりやすく表示させてみよう.
for i in range(len(vocab)):
print(vocab[i], tf[i])
これ [0. 1. 0. 0. 0.] ジャズ [0. 0. 1. 0. 0.] ライブ [0. 0. 2. 0. 0.] 会 [1. 0. 0. 0. 0.] 何 [0. 0. 1. 0. 0.] 先 [1. 0. 0. 0. 0.] 処理 [2. 1. 0. 1. 2.] 勉強 [1. 0. 0. 0. 0.] 勤務 [1. 0. 0. 0. 0.] 基本 [1. 1. 0. 1. 2.] 学会 [0. 1. 0. 0. 0.] 推薦 [0. 1. 0. 0. 0.] 文書 [0. 2. 0. 0. 0.] 発表 [1. 1. 0. 0. 0.] 社内 [1. 0. 0. 0. 0.] 自然 [2. 1. 0. 1. 2.] 言語 [2. 1. 0. 1. 2.] 説明 [1. 1. 1. 1. 2.] 資料 [1. 1. 2. 0. 0.] 類似 [0. 1. 0. 0. 0.]
各単語がいくつの文書に登場したかを調べてみよう.
for i in range(len(vocab)):
print(vocab[i], df[i])
print(len(vocab))
これ [1.] ジャズ [1.] ライブ [1.] 会 [1.] 何 [1.] 先 [1.] 処理 [4.] 勉強 [1.] 勤務 [1.] 基本 [4.] 学会 [1.] 推薦 [1.] 文書 [1.] 発表 [2.] 社内 [1.] 自然 [4.] 言語 [4.] 説明 [5.] 資料 [3.] 類似 [1.] 20
最後にTF-IDFをもう一度眺めてみよう.また,実際のテキストコーパスデータも見て,TF-IDFが何を意味しているのか理解しよう.
tfidf
array([[0. , 1. , 0. , 0. , 0. ], [0. , 0. , 1. , 0. , 0. ], [0. , 0. , 2. , 0. , 0. ], [1. , 0. , 0. , 0. , 0. ], [0. , 0. , 1. , 0. , 0. ], [1. , 0. , 0. , 0. , 0. ], [0.5 , 0.25 , 0. , 0.25 , 0.5 ], [1. , 0. , 0. , 0. , 0. ], [1. , 0. , 0. , 0. , 0. ], [0.25 , 0.25 , 0. , 0.25 , 0.5 ], [0. , 1. , 0. , 0. , 0. ], [0. , 1. , 0. , 0. , 0. ], [0. , 2. , 0. , 0. , 0. ], [0.5 , 0.5 , 0. , 0. , 0. ], [1. , 0. , 0. , 0. , 0. ], [0.5 , 0.25 , 0. , 0.25 , 0.5 ], [0.5 , 0.25 , 0. , 0.25 , 0.5 ], [0.2 , 0.2 , 0.2 , 0.2 , 0.4 ], [0.33333333, 0.33333333, 0.66666667, 0. , 0. ], [0. , 1. , 0. , 0. , 0. ]])
なお,上の [sample code2 : モジュールの準備] コードの 36 行目をコメントアウトし,37 行目を有効にすると,Mac や Linux では高機能な mecab-ipadic-NEologd 辞書 が使えます.この辞書を使うと「自然言語処理」などが一つの単語として認識されるようになるのでこのページで行った結果はそれぞれ若干異なるはずです.例えば,文書間の距離は次のようになります.上で求めた結果と比較すると値は変化していますが,「sample_4.txt」と「sample_5.txt」が最も類似した文書であるという結果は変わりません.
文書間の距離を計算する(mecab-ipadic-NEologd 辞書を使った結果)distance_matrix = get_distance_matrix(tfidf)
print(distance_matrix)
[[0. 3.473111 3.22856177 2.04294178 2.05270824] [3.473111 0. 3.77307714 2.84800125 2.87682309] [3.22856177 3.77307714 0. 2.56309275 2.64280995] [2.04294178 2.84800125 2.56309275 0. 0.40620192] [2.05270824 2.87682309 2.64280995 0.40620192 0. ]]