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

類似文書検索ができるというdoc2vecを利用して気象庁のFAQを検索して遊んだ。
方法は参考①、②のとおりであるが、ほぼ再現性のある検索結果を得られたので記事にしておく。
【参考】
①[doc2vecのサンプル](https://qiita.com/nkmrtkhd/items/92500ba4a5979371a35f)
②[doc2vecでWikipediaを学習する](http://tadaoyamaoka.hatenablog.com/entry/2017/04/29/122128)
参考②は、
１．Wikipediaを利用してWeightsを学習する
２．質問に対して、１のWeightsを使って、気象庁のFAQのQから文書検索して、類似質問を抽出する
３．類似度順に表示する
一方、参考①では
１．与えられた文書に対して、Weightsを学習する
２．文書間の類似度を計算して、類似度順に表示する

当初、参考②と同じように実施したが、やはり同じように結果が不安定であり、しかも類似文書の抽出もあまり精度がよくない結果となった。

今回は、参考①のやり方を採用して、以下のとおり実施した。
１．気象庁FAQ全文を利用して、Weightsを学習する
２．学習したWeightsを利用して、質問に対する類似文書検索して、抽出する
３．類似度順に表示する

###コードによる解説
コードはほぼ参考②＋①のものになりました。
Libは以下のものを使います。

```py
import gensim
from gensim import models
import MeCab
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import argparse
import smart_open
```
外部から与えるパラメーターは以下の通り

```py
parser = argparse.ArgumentParser()
parser.add_argument('faq', type=str)
parser.add_argument("--dictionary", "-d", type=str, help="mecab dictionary")
args = parser.parse_args()
```
入力質問の分かち書きを以下で実施します。

```py
mecab = MeCab.Tagger("-Owakati" + ("" if not args.dictionary else " -d " + args.dictionary))
```
以下が参考②を採用した部分、つまりsentencesにWord,Tagで文章を追加していきます。questionsは参考①のままです。当初questionsのみtag付きにしていましたが、精度はanswersも学習したほうがよくなりました。

```py
questions = []
sentences = []
answers = []
j = 0
for line in open(args.faq, "r", encoding="utf-8"):
    cols = line.strip().split('\t')
    questions.append(gensim.utils.simple_preprocess(mecab.parse(cols[0]).strip(), min_len=1)) #1
    sentences.append(models.doc2vec.TaggedDocument(gensim.utils.simple_preprocess(mecab.parse(cols[0]).strip(), min_len=1), tags=["SENT_"+str(j)]))
    sentences.append(models.doc2vec.TaggedDocument(gensim.utils.simple_preprocess(mecab.parse(cols[1]).strip(), min_len=1), tags=["SENT_"+str(j+1)]))
    answers.append(cols[1])
    j += 2
```
以下のようにmodelを定義します。
文書が少ないのでwindows=5, min_count=5としています。
また、epochs=100としていますが、ここは１として以下の繰り返しを多くしても、逆に繰り返しせずに大きな値にするのも同じ効果なようです。
ここでは過学習でもという感覚で学習回数は大きな値になっています。

```py
model = models.Doc2Vec(vector_size=400, windows=5, min_count=5, epochs=100)
model.build_vocab(sentences)
```
以下で学習しつつ、学習途中のWeightsを保存します。

```py
print('\n訓練開始')
for epoch in range(51):
    print('Epoch: {}'.format(epoch + 1))
    model.train(sentences, epochs=model.epochs, total_examples=model.corpus_count)
    if epoch%5==0:
        model_str="jamQ_model400_doc2vec_"+str(epoch)
        model.save(model_str)
```
気象庁のQAの質問のベクトルを以下で求める。

```py
doc_vecs = []
for question in questions:
    doc_vecs.append(model.infer_vector(question))
```
コマンドラインからの入力をベクトルに変換する。

```py
while True:
    line = input("> ")
    if not line:
        break

    vec = model.infer_vector(gensim.utils.simple_preprocess(mecab.parse(line), min_len=1))
```
上で求めておいた気象庁のQのベクトルと入力質問ベクトルの間のコサイン類似度を求めて、sims[0]に基づいてインデックスをソートする。
そして、一番類似度の高いものの質問と回答を表示する。
さらに、次点以降の質問を表示している。ちなみに５０個表示し、同じ質問を何度かやってみると、類似度計算の不安定さが見える。

```py
    sims = cosine_similarity([vec], doc_vecs)
    index = np.argsort(sims[0])

    print(questions[index[-1]])
    print()
    print(answers[index[-1]])
    print()
    for i in range(2,5):
        print(questions[index[-i]])

    print()
```
###結果
参考②の質問を同じように実施してみた。

```
> みぞれとは何ですか？
['みぞれ', 'と', 'は', '何', 'です', 'か']
['活断層', 'と', 'は', '何', 'です', 'か']
['藤田スケール', 'と', 'は', '何', 'です', 'か']
['異常潮位', 'と', 'は', '何', 'です', 'か']

> 天気予報が外れる理由は？
['天気予報', 'の', 'は', '午前', 'の', '天気', '午後の天気', 'という', '意味', 'です', 'か']
['天気予報', 'は', 'どの', 'くらい', '当たっ', 'て', 'いる', 'の', 'です', 'か']
['天気予報', 'で', '晴れ', 'だっ', 'た', 'のに', '雲', 'が', '出', 'て', 'い', 'ます', 'が']
['天気予報', 'が', 'ころころ', '変わる', '原因', 'は', '何', 'です', 'か']

> 地震は予知できますか？
['地震', 'の', '予知', 'は', 'でき', 'ます', 'か']
['地震', 'による', '強い', '揺れ', 'は', 'どの', '位', '長く', '続く', 'の', 'です', 'か']
['気象庁', 'ホームページ', 'で', 'は', '冷夏', '暖冬', 'といった', '予報', 'を', 'どこ', 'で', '知る', 'こと', 'が', 'でき', 'ます', 'か']
['地震', 'は', 'どうして', '起きる', 'の', 'です', 'か']

> 津波の規模によってどんな被害が起きるのですか？
['cm', 'の', '津波', 'でも', '危険', 'な', 'の', 'は', 'なぜ', 'です', 'か']
['津波予報', 'の', '文中', 'に', 'ある', '海面', '変動', 'と', 'は', '津波', 'の', 'こと', 'です', 'か']
['津波', 'の', '高さ', 'によって', 'どの', 'よう', 'な', '被害', 'が', '発生', 'する', 'の', 'です', 'か']
['波浪', 'と', '津波', 'の', '違い', 'は', '何', 'です', 'か']

> 高潮の発生の仕組みは？
['高潮', 'の', '発生', 'の', '仕組み', 'は']
['竜巻', 'は', 'どうして', '起きる', 'の', 'です', 'か']
['北陸地方', 'に', '新潟県', 'を', '含める', 'の', 'は', 'なぜ', 'です', 'か']
['届出', 'は', 'どこ', 'に', '提出', 'すれ', 'ば', 'よい', 'でしょ', 'う', 'か']

> 高潮の発生の仕組みは？
['高潮', 'の', '発生', 'の', '仕組み', 'は']
['竜巻', 'は', 'どうして', '起きる', 'の', 'です', 'か']
['津波', 'は', 'どの', 'よう', 'な', '仕組み', 'で', '発生', 'する', 'の', 'です', 'か']
['静止', '気象衛星', 'ひまわり', '号', '号', 'の', '画像', 'の', '分解能', 'は']
```
今回の質問の抽出精度は参考②に比較してとても向上しているのがわかります。
また、「高潮の発生の仕組みは？」を見ると、３つ目からどうもちょっと変な質問になっていて、かつ揺らいでいます。つまり、回答の揺らぎは今回も消えてはいません。
この揺らぎは、以下が原因なようです。
この話が正しいとすれば、今回揺らぎが減ったのは、epochsを増やしたせいかもしれません。つまり、上の計算でモデル定義の時のepochsは大きいほうがよいという結論になります。

【参考】
③[Doc2Vecによる文書ベクトル推論の安定化について](https://buildersbox.corp-sansan.com/entry/2019/04/10/110000)

「基本的にDoc2Vec.infer_vector()による未知文書のベクトルの推定時には学習と同じ処理が回るので，epochsもしくはstepsパラメータで反復回数をデフォルトの5より大きく指定することで推測結果の安定性と精度が上がる」
###気象庁のFAQ
一応、参考のために挙げておきますが、上記のものは適当な処理をしているので悪しからず。
[よくある質問集](https://www.jma.go.jp/jma/kishou/know/faq/index.html)
[気象庁ホームページについて](https://www.jma.go.jp/jma/kishou/info/coment.html#exclusion)
###コード全体は以下に置きました
[doc2vec/jam_qa_qtrain_doc2vec.py](https://github.com/MuAuan/doc2vec/blob/master/jam_qa_qtrain_doc2vec.py)
###まとめ
・Doc2vecで気象庁のFAQの質問の抽出を実施して、遊んでみた
・直接QA文書を学習すると、wikipediaで学習したweightsを使うよりも、抽出精度が向上した

・doc2vecにしろ、fasttextにしろ、どうもsize=300程度では空間が小さいと思うので、全体サイズを増加することを考えたい