Python for NLPの連載は今回で14回目です。
前回は、bag of wordsのアプローチを使って文章を数値ベクトルに変換する方法を説明しました。
単語袋アプローチの理解を深めるために、Pythonでその手法を実装してみました。
今回は、前回学んだ概念をもとに、TF-IDF方式をPythonで一から実装してみます。
TFは「用語頻度」、IDFは「逆文書頻度」の略です。
単語袋モデルの問題点
TF-IDFモデルを実際に見る前に、まず、Bag of Wordsモデルに関するいくつかの問題点について説明しよう。
前回、以下の3つの例文があった。
- “私はサッカーをするのが好きです”
- “テニスをするために外に出たのか”
- “ジョンと私はテニスをする”
その結果、単語袋モデルは次のようになった。
| プレイする|テニス|する|私|フットボール|した|行った|の3つ。
Bag of wordsモデルに関連する主な問題の一つは、その重要性に関係なく、単語に等しい価値を割り当てることである。
例えば、”play “という単語は3つの文全てに現れ、したがってこの単語は非常に一般的であり、一方 “football “という単語は1つの文にしか現れません。
このように、稀な単語は一般的な単語に比べ、より高い分類能力を持つ。
TF-IDFの考え方は、ある文ではよく使われ、他の文ではあまり使われない単語には高い重みが与えられるべきであるということである。
TF-IDFの理論
PythonでTF-IDF方式を実装する前に、まず理論を勉強しておこう。
単語袋モデルで用いたのと同じ3つの文章を例とする。
- “私はサッカーをするのが好きです”
- “テニスをしに外に出たのか”
- “ジョンと私はテニスをする”
ステップ1:トークン化
TF-IDFモデルを実装するための最初のステップは、bag of wordsと同様、トークン化である。
| 文章1|文章2|文章3
| — | — | — |
| 私|が|した|ジョン|が
| をした|好き|あなた|と
| に|行く|私|は
| 遊ぶ|外で|遊ぶ
| サッカーからテニスまで
| 遊ぶ
| テニスをする
ステップ2:TF-IDF 値を求める
文をトークン化したら、次のステップは、文中の各単語の TF-IDF 値を求めることです。
前述したように、TF値は用語頻度を意味し、次のように計算できます。
TF = (Frequency of the word in the sentence) / (Total number of words in the sentence)
例えば、最初の文にある “play “という単語を見てください。
play “という単語は文中に一度だけ出現し、文中の単語の総数は5であるため、その項頻度は0.20となり、1/5 = 0.20となる。
IDFは逆文書頻度のことで、次のように計算できる。
IDF: (Total number of sentences (documents))/(Number of sentences (documents) containing the word)
IDFは文書の総数に依存するので、ある単語のIDF値はすべての文書を通じて同じままであることに言及するのは重要である。
一方、単語のTF値は文書ごとに異なる。
ここで、「play」という単語のIDF頻度を求めてみよう。
3つの文書があり、”play “という単語は3つとも出現するので、”play “のIDF値は3/3=1である。
最後に、TF-IDF値はTF値とそれに対応するIDF値を掛け合わせることで算出される。
TF-IDF値を求めるには、まず、以下のように単語の頻度の辞書を作成する必要があります。
| 単語|頻度|の辞書を作成する。
| — | — |
| I | 2 |
| のような|1|のような
| する|2
| プレイ|3
| フットボール| 1
| 1|やった?
| あなた| 1
| 行く| 1
| 外遊び|1
| テニス|2
| ジョン
| と|1|です。
次に、以下の表のように、頻度の降順に辞書をソートしてみましょう。
| 単語|頻度|の順
| プレイ|3|||。
最後に、頻出単語8個をフィルタリングする。
先ほども言ったように、IDF値はコーパス全体を使って計算されるので これで各単語のIDF値を計算することができる。
次の表は各テーブルのIDF値である。
| 単語|頻度|IDF
| プレイ|3|3/3=1|テニス|2|3/2=2
| テニス|2|3/2|=1.5|です。
| に|2|3/2=1.5|||||。
| I | 2 | 3/2 = 1.5 |
| フットボール|1|3/1=3
| ディド|1|3/1 = 3|。
| あなた|1|3/1|3|1
| ゴー|1|3/1=3|です。
珍しい単語は、よく使われる単語に比べ、高いIDF値を持っていることがよくわかります。
それでは、各文章に含まれるすべての単語のTF-IDF値を求めてみましょう。
| 単語|文1|文2|文3|のTF-IDF値を求める。
| — | — | — | — |
| プレイ|0.20×1=0.20|0.14×1=0.14|0.20×1=0.20|……………1
| テニス|0×1.5=0|0.14×1.5=0.21|0.20×1.5=0.30||||||。
| を|0.20 x 1.5 = 0.30|0.14 x 1.5 = 0.21|0 x 1.5 = 0|になります。
| I | 0.20 x 1.5 = 0.30 | 0 x 1.5 = 0 | 0.20 x 1.5 = 0.30
| サッカー|0.20 x 3 = 0.6|0 x 3 = 0|0 x 3 = 0|です。
| した|0 x 3 = 0|0.14×3=0.42|0x3=0|です。
| あなた|0 x 3 = 0|0.14×3=0.42|0x3=0|です。
| ゴー|0x3 = 0|0.14×3=0.42|0x3=0|です。
文1、2、3の列の値は、それぞれの文の各単語に対応するTF-IDFベクトルである。
TF-IDFで対数関数を使用していることに注意。
コーパス上の非常に稀な単語や非常に一般的な単語の影響を軽減するために、IDF値の対数を計算してからTF-IDF値を乗じることができることに言及することは重要である。
この場合、IDFの式は次のようになる。
IDF: log((Total number of sentences (documents))/(Number of sentences (documents) containing the word))
しかし、今回のコーパスには3つの文しか含まれていないため、簡略化のためにlogは使用しなかった。
実装では、最終的なTF-IDF値を計算するためにlog関数を使用する予定である。
Pythonでゼロから作るTF-IDFモデル
理論のところで説明したように、単語頻度のソート辞書を作成する手順は、Bag of wordsとTF-IDFモデルで似ています。
どのように単語頻度のソート辞書を作成するかは、前回の記事を参照してください。
ここでは、コードを書くだけにしておきます。
TF-IDFモデルはこのコードの上に構築されます。
# -*- coding: utf-8 -*-
"""
Created on Sat Jul 6 14:21:00 2019
@author: usman
"""
import nltk
import numpy as np
import random
import string
import bs4 as bs
import urllib.request
import re
raw_html = urllib.request.urlopen('https://en.wikipedia.org/wiki/Natural_language_processing')
raw_html = raw_html.read()
article_html = bs.BeautifulSoup(raw_html, 'lxml')
article_paragraphs = article_html.find_all('p')
article_text = ''
for para in article_paragraphs:
article_text += para.text
corpus = nltk.sent_tokenize(article_text)
for i in range(len(corpus )):
corpus [i] = corpus [i].lower()
corpus [i] = re.sub(r'W',' ',corpus [i])
corpus [i] = re.sub(r's+',' ',corpus [i])
wordfreq = {}
for sentence in corpus:
tokens = nltk.word_tokenize(sentence)
for token in tokens:
if token not in wordfreq.keys():
wordfreq[token] = 1
else:
wordfreq[token] += 1
import heapq
most_freq = heapq.nlargest(200, wordfreq, key=wordfreq.get)
上記のスクリプトでは、まずWikipediaの自然言語処理に関する記事をスクレイピングします。
そして、特殊文字や複数の空白を削除する前処理を行う。
最後に、単語頻度の辞書を作成し、最も頻繁に出現する上位200語をフィルタリングする。
次に、コーパスの中で最も頻出する単語のIDF値を求める。
以下のスクリプトがそれを行う。
word_idf_values = {}
for token in most_freq:
doc_containing_word = 0
for document in corpus:
if token in nltk.word_tokenize(document):
doc_containing_word += 1
word_idf_values[token] = np.log(len(corpus)/(1 + doc_containing_word))
上のスクリプトでは、空の辞書 word_idf_values
を作っている。
この辞書には、最も頻出する単語をキーとして、それに対応するIDFの値を辞書の値として格納する。
次に、頻出単語のリストを繰り返し処理する。
各反復処理の間に、変数 doc_containing_word
を作成する。
この変数には、その単語が出現する文書の数が格納される。
次に、コーパスに含まれる全ての文章を繰り返し処理する。
文はトークン化され、その単語が文中に存在するかどうかを調べる。
もしその単語が存在すれば、変数 doc_containing_word
をインクリメントする。
最後に、IDF値を計算するために、文の総数をその単語を含むドキュメントの総数で割る。
次に、各単語のTF辞書を作成する。
TF辞書では、キーは最も頻出する単語となり、値は49次元のベクトルとなる(文書が49文あるため)。
ベクトル内の各値は、対応する文の単語のTF値に属します。
次のスクリプトを見てほしい。
word_tf_values = {}
for token in most_freq:
sent_tf_vector = []
for document in corpus:
doc_freq = 0
for word in nltk.word_tokenize(document):
if token == word:
doc_freq += 1
word_tf = doc_freq/len(nltk.word_tokenize(document))
sent_tf_vector.append(word_tf)
word_tf_values[token] = sent_tf_vector
このスクリプトでは、コーパスに49の文があるので、単語をキー、49の項目のリストを値として含む辞書を作成する。
リストの各項目には、対応する文に対するその単語のTF値が格納される。
上記のスクリプトでは word_tf_values
が辞書となる。
各単語に対して、リスト sent_tf_vector
を作成する。
次に、コーパスの各文を繰り返し読み、文をトークン化する。
外側のループで得られた単語は文中の各単語とマッチングされる。
文中の全ての単語を繰り返し処理した後、doc_freq
を文の長さで割って、その文の単語の TF 値を求める。
この処理を最頻出単語リストの全ての単語に対して繰り返す。
最終的な word_tf_values
辞書は200個の単語をキーとして持つことになる。
各単語に対して、49個の項目のリストが値として存在することになる。
word_tf_values`の辞書を見ると、以下のようになります。
word`がキーで、49個のアイテムのリストがそれぞれのキーの値であることがわかる。
これで全単語のIDF値と、文中の全単語のTF値が揃った。
次のステップは、IDF値とTF値を単純に掛け合わせることである。
tfidf_values = []
for token in word_tf_values.keys():
tfidf_sentences = []
for tf_sentence in word_tf_values[token]:
tf_idf_score = tf_sentence * word_idf_values[token]
tfidf_sentences.append(tf_idf_score)
tfidf_values.append(tfidf_sentences)
上記のスクリプトでは、tfidf_values
というリストを作成する。
次に、word_tf_values
辞書にある全てのキーを繰り返し処理する。
これらのキーは基本的に最も頻出する単語である。
これらの単語を用いて、各文章に対応する単語のTF値を含む49次元リストを取得する。
次に、TF値と単語のIDF値を掛け合わせ、変数 tf_idf_score
に格納する。
そして、この変数は tf_idf_sentences
リストに追加される。
最後に、 tf_idf_sentences
リストが tfidf_values
リストに追加されます。
この時点で、tfidf_values
はリストのリストである。
各項目は49次元のリストで、すべての文の特定の単語のTFIDF値を含んでいます。
この2次元のリストをnumpyの配列に変換する必要があります。
次のスクリプトを見てください。
tf_idf_model = np.asarray(tfidf_values)
さて、numpy配列は次のようになる。
しかし、このTF-IDFモデルにはまだ1つ問題があります。
配列の次元は200 x 49で、これは各列が対応する文のTF-IDFベクトルを表していることを意味します。
行はTF-IDFベクトルを表すようにしたい。
これは、以下のようにnumpy配列を単純に転置することで実現できます。
tf_idf_model = np.transpose(tf_idf_model)
これで49 x 200次元のnumpy配列ができ、行は以下のようにTF-IDFベクトルに対応します。
結論
TF-IDFモデルは、テキストから数値への変換に最も広く使われているモデルの1つである。
この記事では、TF-IDFモデルの背後にある理論について簡単にレビューしました。
最後に、PythonでゼロからTF-IDFモデルを実装しました。
次回は、N-GramモデルをPythonでゼロから実装する方法を紹介します。