【NLP入門】気象庁のFAQをTF-IDFして遊んでみた♪

昨夜に引き続き、気象庁のFAQを検索してみる。
昨夜はdoc2vecでそれなりの精度が出せたが、今回は古典的な手法として知られるTF-IDFで遊んでみた。
【参考】
①[TF-IDFを使ってFAQに回答する＠TadaoYamaokaの日記](http://tadaoyamaoka.hatenablog.com/entry/2017/04/25/224748)
②[scikit-learnでテキストをBoWやtfidfに変換する時に一文字の単語も学習対象に含める](https://analytics-note.xyz/machine-learning/bow-tfidf-one-character/)
③[TF-IDFについて書いてみる。＠どん底から這い上がるまでの記録](https://www.pytry3g.com/entry/tfidf)
###TF-IDFとは
参考③に詳細に記載されていますが、簡単に云うと
「Term Frequency ；TF；単語のその文書における出現頻度／その文書における全単語の出現回数の和 」；その文書で一番多く出現する単語がその文書では重要
「Inverse Document Frequency；IDF； log(総文書数 / (その単語を含む文書の数 + 1))」；文書全体において、その単語を含む文書が希少なほどその単語が重要
※0除算回避のために分母に+１している
【参考】IDFの式については、[【技術解説】単語の重要度を測る？TF-IDFとOkapi BM25の計算方法とは](https://mieruca-ai.com/ai/tf-idf_okapi-bm25/)より

この二つの量から、TFIDF＝TF*IDFで求める。
すなわち、TFIDFは頻度で計算された、単語の重要度を表す。
###TF-IDFのプロセスを見る
上記の様子をjupyter notebookを使ってみてみる。

```py
from sklearn.feature_extraction.text import TfidfVectorizer
model = TfidfVectorizer()
```
jupyter notebookで
model
と入力後Runすると、以下のようにTfidfVectorizer()のデフォルトが出力する。

```py
TfidfVectorizer(analyzer='word', binary=False, decode_error='strict',
                dtype=<class 'numpy.float64'>, encoding='utf-8',
                input='content', lowercase=True, max_df=1.0, max_features=None,
                min_df=1, ngram_range=(1, 1), norm='l2', preprocessor=None,
                smooth_idf=True, stop_words=None, strip_accents=None,
                sublinear_tf=False, token_pattern='(?u)\\b\\w\\w+\\b',
                tokenizer=None, use_idf=True, vocabulary=None)
```
以下の文章でTFIDFを計算してみる。

```py
text_list = ['(?u)私は 醤油 ラーメン と とんこつ ラーメン が 好き です。', '(?u)私は とんこつ ラーメン と 味噌 ラーメン が 好き です。']
model.fit(text_list)
print(model.vocabulary_)
```
以下のように単語に番号がついたリストが出力する。

```
{'u': 0, '私は': 8, '醤油': 9, 'ラーメン': 5, 'と': 3, 'とんこつ': 4, 'が': 1, '好き': 7, 'です': 2, '味噌': 6}
```
特に文章を特徴付けしない単語をstop_wordsとして定義し、ベクトル空間を削減する。

```py
stop_words = ['が','です','私は','と']
model = TfidfVectorizer(token_pattern='(?u)\\b\\w\\w+\\b',　stop_words=stop_words)

text_list = ['(?u)私は 醤油 ラーメン と とんこつ ラーメン が 好き です。', '(?u)私は とんこつ ラーメン と 味噌 ラーメン が 好き です。']
X=model.fit_transform(text_list)
print(model.vocabulary_)
```
上記は１０次元であったが、以下の通り５次元に削減できた。

```
{'醤油': 4, 'ラーメン': 1, 'とんこつ': 0, '好き': 3, '味噌': 2}
```

```py
for k,v in model.vocabulary_.items():
    print(k, v)
```

```
醤油 4
ラーメン 1
とんこつ 0
好き 3
味噌 2
```

```py
print(model.get_feature_names())
```

```
['とんこつ', 'ラーメン', '味噌', '好き', '醤油']
```

```py
print(X.shape)
```

```
(2, 5)
```

```py
print(X)
```

```
  (0, 3)	0.3540997415957358
  (0, 0)	0.3540997415957358
  (0, 1)	0.7081994831914716
  (0, 4)	0.4976748316029239
  (1, 2)	0.4976748316029239
  (1, 3)	0.3540997415957358
  (1, 0)	0.3540997415957358
  (1, 1)	0.7081994831914716
```
２つの文章だとわかりにくいので、３つ目の文章を追加します。

```py
text_list = ['(?u)私は 醤油 ラーメン と とんこつ ラーメン が 好き です。', '(?u)私は とんこつ ラーメン と 味噌 ラーメン が 好き です。', '(?u)私は かつ丼　と ラーメン と お好み焼き が 好き です。']
X=model.fit_transform(text_list)
print(model.vocabulary_)
```
この３つの文章のベクトルの基底の単語は以下のとおりです。

```
{'醤油': 6, 'ラーメン': 3, 'とんこつ': 2, '好き': 5, '味噌': 4, 'かつ丼': 1, 'お好み焼き': 0}
```
これらの文章間の類似度は以下のとおり計算できます。

```py
from sklearn.metrics.pairwise import cosine_similarity
sims = cosine_similarity(X, X)
print(X)
print(sims)
```
ということで、それぞれのベクトルは以下の通りとなり、そのベクトル間のコサイン類似度は以下の行列になります。
この場合は、第一と二の文章の類似度が大きく、第三の文章と第一、第二の文章との類似度は同じということです。

```
  (0, 5)	0.32401895323148033
  (0, 2)	0.41723339721076924
  (0, 3)	0.6480379064629607
  (0, 6)	0.5486117771118657
  (1, 4)	0.5486117771118656
  (1, 5)	0.3240189532314803
  (1, 2)	0.4172333972107692
  (1, 3)	0.6480379064629606
  (2, 0)	0.6088450986844796
  (2, 1)	0.6088450986844796
  (2, 5)	0.35959372325985667
  (2, 3)	0.35959372325985667
[[1.         0.69902512 0.34954555]
 [0.69902512 1.         0.34954555]
 [0.34954555 0.34954555 1.        ]]
```
###気象庁のFAQをTF-IDFする
もうここまでくると、以下のコードは容易に理解できると思う。
利用するLibは以下のとおりです。
なお、コードはほぼ参考①のコードの受け売りです。

```py
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import MeCab
```
外部から与えるものも前回のdoc2vecと同様です。
ここで、今回はstop_wordsとしては、[Japanese Stopwords](https://www.ranks.nl/stopwords/japanese)をstop_words.txtに格納して利用しました。

```py
import argparse
parser = argparse.ArgumentParser(description="convert csv")
parser.add_argument("input", type=str, help="faq tsv file")
parser.add_argument("--dictionary", "-d", type=str, help="mecab dictionary")
parser.add_argument("--stop_words", "-s", type=str, help="stop words list")
args = parser.parse_args()
```
入力質問文の分かち書きも以下で昨夜と同一です。

```py
mecab = MeCab.Tagger("-Owakati" + ("" if not args.dictionary else " -d " + args.dictionary))
```
気象庁の質問と回答は以下のように実施しています。
つまり、今回はTF-IDFの学習は質問文だけについて実施します。

```py
questions = []
answers = []
for line in open(args.input, "r", encoding="utf-8"):
    cols = line.strip().split('\t')
    questions.append(mecab.parse(cols[0]).strip())
    answers.append(cols[1])
```
以下がstop_wordsの読み込みでこの単語はTF-IDFの学習から除外されます。

```py
stop_words = []
if args.stop_words:
    for line in open(args.stop_words, "r", encoding="utf-8"):
        stop_words.append(line.strip())
```
以下が今回の主題です。つまり、ここでTF-IDFの学習とベクトル化を実施します。
ちなみに、上記で説明はしていませんが、読み込み文章のうち、単語の語数として１個の場合も意味があるので学習対象とするために、"(?u)\\b\\w\\w+\\b"だったものを"(?u)\\b\\w+\\b"としています。
参考②に詳細な解説があります。

```py
vectorizer = TfidfVectorizer(token_pattern="(?u)\\b\\w+\\b", stop_words=stop_words)
vecs = vectorizer.fit_transform(questions)

#for k,v in vectorizer.vocabulary_.items():
#    print(k, v)
```
以下で入力質問文を受け付けて、ベクトル化し、vecsとの類似度を計算します。
その後、sims[0]でソートし、その序列のindexを取得します。

```py
while True:
    line = input("> ")
    if not line:
        break
    sims = cosine_similarity(vectorizer.transform([mecab.parse(line)]), vecs)
    index = np.argsort(sims[0])
```
結果の表示は以下のとおり、類似度と質問を出力します。
一番類似している質問については、回答も出力しています。

```py
    print("({:.2f}): {}".format(sims[0][index[-1]],questions[index[-1]]))
    print()
    print(answers[index[-1]])
    print()
    for i in range(2,5):
        print("({:.2f}): {}".format(sims[0][index[-i]],questions[index[-i]]))
    print()
```
###結果
今回の出力結果は、想像以上にいい結果です。
まず、学習速度が凄く速い、この程度のQAなら毎回学習しつつ回答できるほどの速さです。
次に、回答精度が以下のとおり、優れています。
※改善したdoc2vecとほぼ同一の回答です

```
> みぞれとは何ですか？
(1.00): 「 みぞれ 」 と は 何 です か ？
(0.45): 沖縄 で 雪 や みぞれ が 降っ た こと は あり ます か ？
(0.06): 特別警報 と は 何 です か ？
(0.04): 平年値 と は 何 です か ？
> 天気予報が外れる理由は？
(0.63): 週間 天気予報 が 外れる こと が あり ます が 、 なぜ です か ？
(0.39): 週間 天気予報 は よく 外れる ので 、 ３日 先 くらい の 予報 だけ で 良い の で は ない です か ？
(0.35): 天気予報 が 外れる の は 、 コンピュータ や 気象衛星 など 機械 に 任せ きり に し て 、 予報 官 が 検証 し て い ない から です か ？
(0.26): 天気予報 で ｜ と ／ が 出 て い ます が 、 違い は 何 です か ？
> 地震は予知できますか？
(1.00): 地震 の 予知 は でき ます か ？
(0.46): 動物 や 植物 は 地震 を 予知 できる の です か ？
(0.32): 東海地震 は 必ず 予知 できる の です か ？
(0.29): 地震 の 空白域 と は 何 です か ？
> 津波の規模によってどんな被害が起きるのですか？
(0.56): 津波 の 高さ によって どの よう な 被害 が 発生 する の です か ？
(0.35): 地震 は どうして 起きる の です か ？
(0.31): 竜巻 は どうして 起きる の です か ？
(0.22): テレビ局 によって 天気予報 の 内容 が 違う こと が ある の は なぜ です か ？
```
以下のように、TF-IDFは同一の文章で学習している限り、回答は同一です。
※もちろん、学習する文章が変われば変わります

```
> 高潮の発生の仕組みは？
(1.00): 高潮 の 発生 の 仕組み は ？
(0.53): 津波 は どの よう な 仕組み で 発生 する の です か ？
(0.33): 暴風 や 高潮 など が 発生 する 前 から 警報 ・ 注意報 が 発表 さ れ て いる 場合 が ある の は なぜ です か ？
(0.24): 雲 が 七 色 に 見える 「 彩雲 」 の 仕組み は 何 です か ？
> 高潮の発生の仕組みは？
(1.00): 高潮 の 発生 の 仕組み は ？
(0.53): 津波 は どの よう な 仕組み で 発生 する の です か ？
(0.33): 暴風 や 高潮 など が 発生 する 前 から 警報 ・ 注意報 が 発表 さ れ て いる 場合 が ある の は なぜ です か ？
(0.24): 雲 が 七 色 に 見える 「 彩雲 」 の 仕組み は 何 です か ？
```
###まとめ
・TF-IDFで気象庁のFAQを実施して遊んでみた
・計算速度が速く、かつ精度もそれなりなので利用範囲は広い

・TF-IDFの仕組みを理解できたので応用したいと思う