自作形態素解析辞書 及び、品詞の厳選を考慮したBM25の実装

# はじめに
　[前回の記事](https://qiita.com/Lucky_Acky/items/9fbbd4b23c28a5c8622f)は、gensimを用いたBM25の実装を行いました。(開発環境は前回の記事と同じです。)
　今回の記事では、BM25を使用するにあたり 品詞の厳選と自作形態素解析辞書を考慮したいと思います。

# 自作形態素解析辞書の作成
## 概要
　　TF-IDFやBM25のスコアを計算する際に、文書を単語ごとに分解します。分解は、janomeなどのトークナイザに登録されているものがの基準になっています。
　ですので、「東京タワー」は「東京, タワー」「人間ドック」は「人間, ドック」になります。これは、単語の重要度を計算する中で 少なからず計算結果に影響します。
　この問題については、自作形態素解析辞書(以降、自作辞書)を自作して追加することで、「東京タワー」を「東京タワー」として扱うことができます。

## 自作辞書の作成方法
　自作辞書は。csvにて作成可能です。以下の要素をカンマ区切りで作成するだけです。(詳しくは後述しますがutf-8で読み込むため、保存する際は「CSV UTF-8 (コンマ区切り)(\*.csv)」にて保存してください。)
- 表層形
- 左文脈ID
- 右文脈ID
- コスト
- 品詞
- 品詞細分類1
- 品詞細分類2
- 品詞細分類3
- 活用形
- 活用型
- 原形
- 読み
- 発音

　この要素は全て埋める必要はなく、タスクに応じて必要な部分のみ埋めてください。不要な要素は「\*」で埋めてください。
　BM25に使用することを踏まえたうえで、東京タワーを例にすると以下になります。(自作名詞については、後述します。)
**東京タワー,\* ,\* ,\* ,自作名詞 ,\* ,\* ,\* ,\* ,\* ,\* ,\* ,\***
「\*」の要素の使い道はなんだよ、と思われるかもしれませんが、本題から逸れるので別の機会に。
## 実装
　janomeのトークナイザに読み込むには、トークナイザを作成する際にファイル名を指定してください。
~~~ BM25_R1.py
    t = Tokenizer("word_dict.csv", udic_enc = "utf-8_sig")
~~~
これで、トークナイザに自作の形態素解析辞書を追加できました。

# 品詞の厳選
## 概要
　TF-IDFやBM25にて単語の重要度を数値化する際に、文書内の単語ごとに重要度を数値化します。何も工夫もしないで文書を単語を分解すると、「文書の内容に関りはないけど、汎用的で文書の中で頻繁に出現する単語(ストップワード)」が出てきます。
　これらのストップワードについては、IDFを計算することで影響が小さくなるように出来ています。ですが、「影響が小さい」だけで影響が無いわけではありません。ストップワードの除去は自然言語処理を行ううえで重要な作業です。
　今回の記事では「名詞・動詞・形容詞・自作名詞」(以降、象徴品詞)が文書を象徴する単語として、それ以外の品詞を除外します。

##コード
~~~ BM25_R1.py
#品詞判断
def judge_part(token):
    return any([token.part_of_speech.split(",")[0] == "名詞",
                token.part_of_speech.split(",")[0] == "動詞",
                token.part_of_speech.split(",")[0] == "形容詞",
                token.part_of_speech.split(",")[0] == "自作名詞"])

#不要な品詞の除外
def create_token_list(text, t):
    token_list = []
    for token in t.tokenize(text):
        if judge_part(token): token_list.append(token.base_form)
    return token_list
    
~~~
　judge_part関数で、各単語のトークナイザから品詞を確認し象徴品詞ならTrue、それ以外ならFalseを返します。
　create_token_list関数で、文書内の単語をループさせます。judge_part関数の返り値を基に象徴品詞のみをtoken_listに追加します。

# 自作辞書追加及び、品詞の厳選を含めた実装コード
~~~ BM25_R1.py
# -*- coding: utf-8 -*-

from janome.tokenizer import Tokenizer
from gensim.summarization.bm25 import BM25

#品詞判断
def judge_part(token):
    return any([token.part_of_speech.split(",")[0] == "名詞",
                token.part_of_speech.split(",")[0] == "動詞",
                token.part_of_speech.split(",")[0] == "形容詞",
                token.part_of_speech.split(",")[0] == "自作名詞"])

#文書集合の分かち書き
def pre_process(docs, flag, t):
    corpus = [wakachi(doc, flag, t) for doc in docs]
    bm25_ = BM25(corpus)
    return bm25_

#BM25実行
def matching(bm25_, query, flag, t):
    query = wakachi(query, flag, t)
    # print(query)
    scores = bm25_.get_scores(query)
    scores = [round(score, 3) for score in scores]
    return scores

#分かち書き
def wakachi(doc, flag, t):
    if flag:
        # print(create_token_list(doc, t))
        return create_token_list(doc, t)
    else:
        return list(t.tokenize(doc, wakati = True))

#品詞の厳選
def create_token_list(text, t):
    token_list = []
    for token in t.tokenize(text):
        if judge_part(token): token_list.append(token.base_form)
    return token_list


if __name__ == "__main__":
    query = "インドカレー屋で提供されているラッシーは、とても美味しい。"
    docs = ["カレーよりもシチューが好きだ。",
            "ガンジス川を見るためにインドに来た。",
            "カレーが好きだ。中でも、インドカレーが一番好きだ。",
            "自宅で作ったラッシーも美味しい。",
            "欧風カレーとインドカレーは全くの別物だが、どちらも美味しい。",
            "インドカレーが好きだ。",
            "カレーといえばインドが有名だが、日本のカレーはインドカレーとは異なる進化を遂げているため、日本食といっても過言ではないだろう。",
            "インドは、13億の人口を抱える大国だ。"]
    
    t = Tokenizer("word_dict.csv", udic_enc = "utf-8_sig")
    part_flag = False #不要な品詞の除外を行うかどうか判断するフラグ
    
    bm25_ = pre_process(docs, part_flag, t)
    scores = matching(bm25_, query, part_flag, t)
    docs_dict = dict(zip(docs, scores))
    docs_dict = dict(sorted(docs_dict.items(), reverse = True))

    print("クエリ:", query)
    print("------------------------")
    for k, v in docs_dict.items():
        print(k, "{:0<3}".format(v))
~~~

　自作辞書追加・品詞の厳選以外の処理については、造りが異なりますが、[前回の記事](https://qiita.com/Lucky_Acky/items/9fbbd4b23c28a5c8622f)とほぼ一緒です。実行時に表示されるスコアが高いほど、クエリとの類似性が高いと言えます。

　コメントアウトしたprint文を復活させると、形態素解析を行った文書が出力されます。
　part_flagをFalseにすることで、品詞の厳選をオフにできます。
　自作辞書には、「インドカレー」「欧風カレー」を追加しました。

# 実行結果
　クエリは以下の文書とします。
インドカレー屋で提供されているラッシーは、とても美味しい。
## 自作辞書・品詞の厳選なし
|文書|スコア|
|:-----|:-----|
|自宅で作ったラッシーも美味しい。 |4.739|
|カレーといえばインドが有名だが、日本のカレーは(以下略) |4.188|
|欧風カレーとインドカレーは全くの別物だが、どちらも美味しい。 |2.259|
|インドカレーが好きだ。 |1.148|
|カレーが好きだ。中でも、インドカレーが一番好きだ。 |1.099|
|インドは、13億の人口を抱える大国だ。 |1.096|
|カレーよりもシチューが好きだ。 |0.705|
|ガンジス川を見るためにインドに来た。 |0.631|

## 自作辞書・品詞の厳選あり
|文書|スコア|
|:-----|:-----|
|自宅で作ったラッシーも美味しい。 |2.995|
|欧風カレーとインドカレーは全くの別物だが、どちらも美味しい。 |1.024|
|カレーといえばインドが有名だが、日本のカレーは(以下略) |0.906|
|ガンジス川を見るためにインドに来た。 |0.0|
|カレーよりもシチューが好きだ。 |0.0|
|カレーが好きだ。中でも、インドカレーが一番好きだ。 |0.0|
|インドカレーが好きだ。 |0.0|
|インドは、13億の人口を抱える大国だ。 |0.0|

# 考察
　本来であれば、辞書なし&厳選あり・辞書あり&厳選なしの2パターンを加えて、それぞれの変化を確認したいのですが、この記事を書いているのが金曜の23時で、ヘトヘトなので割愛させていただきます...。ヒマなときにでも別途記事にします。
　考察の前に注意が1つ。自作辞書・品詞の厳選なしと比べて全体的にスコアが低いのは、自作辞書・品詞の厳選なしが優れた結果になったわけではありません。スコアというと大きいほうが良いイメージがありますが、今回行った2パターンは条件が異なるため、スコアでの比較はできません。
　なぜスコアに差が出るかという話ですが、1文書におけるスコアというのは、各単語のスコアの合計になるためです。自作辞書や品詞の厳選などの単語数が少なくなる処理を挟むと必然的にスコアは低下することになります。

　さて、スコア下位のものは、類似性が乏しいため放置するとして(~~「あり」パターンに0.0が並んでいるのが気にはなりますが...~~)、上位3位を確認すると
|文書|なし|あり|
|:-----|:-----|:-----|
|自宅で作ったラッシーも美味しい。 |1位|1位|
|カレーといえばインドが有名だが、日本のカレーは(以下略) |2位|3位|
|欧風カレーとインドカレーは全くの別物だが、どちらも美味しい。 |3位|2位|

　2位と3位が入れ替わっています。私の体感的には「ありパターン」の順位がしっくりくるので、(今回の文書集合だけでは一概には言えませんが)方法論としては、おおむね正しいのではないかと。
　順位が入れ替わった要因としては、「インドカレー」を単語として登録したことが大きいのかなと思われます。(細かい検証はしていませんが...)
　また、DL値によってある程度 単語数による影響が緩和されているとはいえ、単語の絶対数を削減したことによる影響も多少はあると思われます。

# 最後に
　今回の記事では、文書及び、トークナイザを加工することで精度を向上させました。
　ここから更に踏み込むのであれば、類義語を踏まえた評価を行うことになるかなと。単純な方法でいえば、「類義語辞典をデータベース化して名詞が出てくるたびに類義語に入れ替えて計算を行う」などが考えられますが、さすがにパワープレイが過ぎますね 笑
　WordNetで上位語を使ったりして実装できないかなー、と思っています。

# 参考
[あれもPython,これもPython](https://esu-ko.hatenablog.com/entry/2016/02/27/Python%E3%81%A7%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90%E3%82%92%E3%81%97%E3%81%9F%E3%81%84%E3%81%AE%E3%81%A7janome%E3%82%92%E4%BD%BF%E3%81%86 "Pythonで形態素解析をしたいのでjanomeを使う")