TF-IDFの手法でモジュール作成

TF-IDFとは

BOW(Bag-of-Words)のように文章をベクトルデータに変換する手法のこと。BOWの手法では単語の出現頻度によって文章を数値化していた。TF-IDFでは単語の出現頻度に加えて、文章全体における単語の重要度も考慮するもの。TF-IDFは文書内の特徴的な単語を見つけることを重視する。その手法として、学習させる文章全ての文書で、その単語がどのくらいの頻度で使用されているかを調べていく。例えば、ありふれた「です」、「ます」などの単語の重要度を低くし、他の文書では見られないような希少な単語があれば、その単語を重要なものとみなして計算を行う。つまり、出現頻度を数えるだけではなくて、出現回数が多いもののレートをさげ、出現頻度の低いもののレートをあげるような形で単語をベクトル化していく。TF-IDFを使用することで、単語の出現頻度を数得るよりも、ベクトル化の精度向上が期待できる。

 

f:id:hanamichi_sukusuku:20210216100303p:plain

f:id:hanamichi_sukusuku:20210216100325p:plain

実行結果

f:id:hanamichi_sukusuku:20210216100549p:plain

4つ目のデータを見てみると0.7954...と高い数値を表しているものがある。これは日曜という単語を示しており、他の文章で使用されていない特徴的な単語なため数値が高くなっていることがわかる。

 

このプログラムでは文章中の単語の重要度に注目したTF-IDFを用いて、単語ごとの希少性を示したデータを出力していく。

TF-IDFを実践するにはscikit-learnの「TfrdVectorizer」も有名だが、追加で日本語への対応処理が必要なので今回は用いていない。

 

MeCabの初期化と辞書などの定義

import MeCab
import pickle
import numpy as np

# MeCabの初期化 ---- (*1)
"-d /var/lib/mecab/dic/mecab-ipadic-neologd")
word_dic = {'_id': 0} # 単語辞書
dt_dic = {} # 文書全体での単語の出現回数
files = # 全文書をIDで保存

word_dicは単語辞書。単語をキーとして値にidを持つ。

dt_dicは文書全体での単語の出現回数を持つ。一つの文章に複数同じ単語が出現しても1回でカウントする。

filesは全文書をIDで保存する。一つ一つの文章をIDで表現したデータを格納する配列。

 

モジュールテスト

if __name__ == '__main__':
  add_text('雨')
  add_text('今日は、雨が降った。')
  add_text('今日は暑い日だったけど雨が降った。')
  add_text('今日も雨だ。でも日曜だ。')
  print(calc_files())
  print(word_dic)

 

add_text()関数

def add_text(text):
'''テキストをIDリストに変換して追加''' # --- (*5)
ids = words_to_ids(tokenize(text))
files.append(ids)

words_to_ids()関数からは渡したテキストをIDで表現した配列が返される。

それをfilesに追加。

 

tokenize()関数

def tokenize(text):
'''MeCab形態素解析を行う''' # --- (*3)
 result =
 word_s = tagger.parse(text)
 for n in word_s.split("\n"):
  if n == 'EOS' or n == '': continue
  p = n.split("\t")[1].split(",")
  h, h2, org = (p[0], p[1], p[6])
  if not (h in ['名詞', '動詞', '形容詞']): continue
  if h == '名詞' and h2 == '数': continue
  result.append(org)
 return result

if not (h in ['名詞', '動詞', '形容詞']): continueではストップワードの除去。

その下のif文でも名詞に数詞が含まれる場合はスキップしている。

result.append(org)で形態素解析した単語の原型をresultに追加していくことでストップワードを除去し、テキストの単語をIDで表現する準備をする。

 

words_to_ids()関数

def words_to_ids(words, auto_add = True):
'''単語一覧をIDの一覧に変換する''' # --- (*4)
  result =
  for w in words:
   if w in word_dic:
    result.append(word_dic[w])
    continue
   elif auto_add:
    id = word_dic[w] = word_dic['_id']
    word_dic['_id'] += 1
    result.append(id)
  return result

 

add_text()関数のwords_to_ids(tokenize(text))の部分でtokenize()関数の返り値を引数にしているので、テキストからストップワードを除去し、単語の原型が格納されている配列を引数にしている。

ここでは引数の単語をIDに変換していく。

word_dic(単語辞書)にループで回ってきた単語が含まれていなければ辞書に追加し、新たにその単語に割り振られたIDをresultに追加、含まれていればその単語をキーとする値(ID)を取得しresultに追加。

これによりresultにテキストをIDで表現した配列が生成される。

 

calc_files()関数

if __name__ == '__main__':
add_text('雨')
add_text('今日は、雨が降った。')
add_text('今日は暑い日だったけど雨が降った。')
add_text('今日も雨だ。でも日曜だ。')
print(calc_files())
print(word_dic)

次にcalc_files()関数を見ていく。

この関数は全文章で出現する単語の頻度と全文章での希少性を掛け合わせることで文章ごとの単語の重要性を示すデータを返す関数。

def calc_files(): 
  '''追加したファイルを計算''' # --- (*7)
  global dt_dic
  result =
  doc_count = len(files)
  dt_dic = {}
 # 単語の出現頻度を数える --- (*8)
  for words in files:
   used_word = {}
   data = np.zeros(word_dic['_id'])
   for id in words:
    data[id] += 1
    used_word[id] = 1
   # 単語tが使われていればdt_dicを加算 --- (*9)
   for id in used_word:
     if not(id in dt_dic): dt_dic[id] = 0
     dt_dic[id] += 1
  # 出現回数を割合に直す --- (*10)
    data = data / len(words)
    result.append(data)
  # TF-IDFを計算 --- (*11)
  for i, doc in enumerate(result):
   for id, v in enumerate(doc):
    idf = np.log(doc_count / dt_dic[id]) + 1
    doc[id] = min([doc[id] * idf, 1.0])
    result[i] = doc
  return result

 

global dt_dicで関数内でのdt_dicの変更がそのままグローバル変数のdt_dicに反映される。

for words in files:でテキストをIDで表現し配列がそれぞれ格納されている二次元配列をループ。

np.zeros()で単語辞書の単語数を持つ配列を作成。

for id in words:でテキストをIDで表現したデータをループするので単語のIDをidに格納して処理していく。data[id]+=1でその文章中での単語の出現頻度をカウントする配列でIDの出現頻度をカウント。

used_word[id] = 1ではその文章で出現した単語IDを格納していく。一つの文章中に複数同じ単語が出現しても値は1。

for id in used_word:で出現した単語IDを取得して処理。dt_dic(文書全体での単語の出現頻度)に含まれていなければそのIDをキーとする要素を追加。dt_dic[id]+=1で加算。

result.append(data)で出現頻度を割合に直したデータをresultに格納。

idf = np.log(doc_count / dt_dic[id]) +1で文書の数から任意の全体での単語の出現頻度を割ることで出現した回数が多いものほど小さい値になる。np.log()はネイピア数(これは定数で2.7....)を底にもつ対数を返すもの。対数にすることで大小関係は変わらず0.0

~1.0で表現できるから?対数に変換する明確な理由はわからなかった。ただ、一旦TF-IDFの手法ではこうすると覚えておこう。

doc[id]=min([doc[id]*idf, 1.0])でdic[id](単語の出現頻度を割合で表記したもの)にidf(全体における単語の希少性)を掛け合わせることで全体の文章でのその単語の希少性を示した値にdic[id]を更新する。

result[i] = docで更新したdoc(出現頻度を割合で表現しているデータ)で更新し大元の各文章での単語の出現頻度を割合で表記したデータを作り替えている。