【Python】2つの文章の類似性を固有名詞の傾向から計算。記事のレコメンデーションに利用。


# 概要
- 2つの文章の類似性を計算できれば、レコメンデーション機能に利用できます。
- 予め、文章の類似性を計算してDBに記録しておき、現在表示している文章と、内容が似ている文章をレコメンデーションできます。

# 今回説明する方法
- 文章中の固有名詞とその出現回数を取得します。
- 取得した文章中の固有名詞の傾向が2つの文章で、どの程度似ているか計算します。
- 類似性の計算にはピアソン相関係数を利用します。
- プログラミング言語はPython3です。

# 個別の処理の説明
## 文章中の固有名詞とその出現回数を抽出

#### MeCabを利用して、文章中の固有名詞を取得します。
#### MeCabの辞書にmecab-ipadic-neologdを設定します。
 - この辞書は新語に強い辞書で、これを設定することで固有名詞をかなり正確に取得できるようになります。
 - https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md
 
#### ストップワード(多くの文章に現れるワード)を除去する。
- どの文章にも含まれる文字を省きます。(これらのワードを固有名詞として認識してしまわないようにします。)
- 日本語ストップワードの一覧
  - http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt

```
import MeCab
import os.path
import manage
from django.conf import settings

char_encoding = 'utf-8'


def extract_noun_words(text):
    """
    文章から固有名詞を抽出して返します
    日本語のストップワードは除きます
       @input: sentence
       @return: a list of noun words
    """
    tagger = MeCab.Tagger(' -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
    tagger.parse('')  # <= 空文字列をparseする。
    # なぜかこの対応でnode.surfaceが取得できるようになる。http://qiita.com/piruty_joy/items/ce218090eae53b775b79
    node = tagger.parseToNode(text)
    keywords = []
    while node:
        # surface = node.surface.decode(char_encoding)
        metas = node.feature.split(",")

        # ストップワードでないこと
        if not __is_stop_word(node.surface):
            # 固有名詞であること
            # node.posidで固有名刺を絞り込める
            # https://taku910.github.io/mecab/posid.html
            if node.surface and node.posid >= 41 and node.posid <= 47:
                keywords.append(node.surface)

        node = node.next
    return keywords


def __is_stop_word(word):
    """
    wordがストップワード(出現回数が多く、意味のない言葉)であるか判定します
    :param word:
    :return:
    :param word:
    :return:
    """

    site_root = os.path.dirname(os.path.realpath(manage.__file__))
    stop_word_path = site_root + settings.JAPANESE_STOP_WORD_PATH

    stop_word_file = open(stop_word_path)
    lines = stop_word_file.readlines()  # 1行毎にファイル終端まで全て読む(改行文字も含まれる)
    stop_word_file.close()

    stop_word_set = set([line.strip() for line in lines])
    return word.strip() in stop_word_set

```

## ピアソン相関係数を計算
- 計算結果の範囲：1.0〜0.0
- 1.0が相関性が強くて、0.0は相関性がないことを表します。

### なぜ、ピアソン相関係数で計算するか。
- データの例：
  - 文章A: 浜離宮庭園 100回、 東京ドーム: 50回、 東京タワー: 10回
  - 文章B: 浜離宮庭園 10回、 東京ドーム: 5回、 東京タワー: 1回、 吉野家1回
  
#### 固有名詞の出現回数を単純に比較すると、次のような問題があります。
- 文章中の固有名詞の出現回数は、文章のボリュームに影響されます。
- そのため、出現回数を単純に比較すると、文章のボリュームが10倍くらい違ったときに相関していないような結果となり、内容の類似性を判定できなくなります。
- 文章Aと文章Bは、固有名詞の傾向を見ると、内容が類似しています。

#### ピアソン相関係数ではデータの傾向を比較できます。
- データ例のように傾向が似ている場合、類似性が高いことを取得できます。

### 参考にした情報
- ↑はかなりざっくりした説明なので、詳しくは下記の情報を参照してください。

#### 書籍:[集合知プログラミング](https://www.amazon.co.jp/%E9%9B%86%E5%90%88%E7%9F%A5%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-Toby-Segaran/dp/4873113644) P14
- ピアソン相関係数で類似性のスコアを計算する方法が照会されています。
- サンプルコード：https://github.com/uolter/PCI/blob/master/chapter2/recommendations.py

#### wikipedia：相関係数
https://ja.wikipedia.org/wiki/%E7%9B%B8%E9%96%A2%E4%BF%82%E6%95%B0

### プログラムの説明
- 前半部分で、引数として渡された名詞のリストから、名詞と出現回数のDictionaryを作成します。
- 2つの文章のDictionary(名詞と出現回数)から、ピアソン相関係数を計算して返します。


```
def calculate_pearson(words1, words2):
    '''
    二つのWordクラスのエンティティのリストを比較して類似性を計算して返します
    :param words1: Wordのリスト1
    :param words2: Wordのリスト2
    :return:1から-1の間で返します。同じ内容のリストを比較した場合、1を返します。
    '''

    # wordのset
    word_set = set()

    for word in words1:
        word_set.add(word.word)

    for word in words2:
        word_set.add(word.word)

    # wordのlist(setから作成し、ワードが重複しないようにする)
    word_list = list(word_set)

    # key = word1, value = word1.word_countの辞書
    word_dict1 = {}
    for word1 in words1:
        if word1.word not in word_dict1:
            word_dict1[word1.word] = word1.word_count

    # key = word2, value = word2.word_countの辞書
    word_dict2 = {}
    for word2 in words2:
        if word2.word not in word_dict2:
            word_dict2[word2.word] = word2.word_count

    # wordのlistの順番にword1, word2の word_countのリストを作成する。
    word_count_list1 = []
    word_count_list2 = []

    # word1, word2の両方に含まれているワードのリスト
    si_word_list = []

    for word in word_list:
        if word in word_dict1:
            # word1のword_countのリスト
            word_count_list1.append(word_dict1[word])
        else:
            word_count_list1.append(0)

        if word in word_dict2:
            # word2のword_countのリスト
            word_count_list2.append(word_dict2[word])
        else:
            word_count_list2.append(0)

    # Simple sums
    sum1 = sum(word_count_list1)
    sum2 = sum(word_count_list2)

    # Sums of the squares
    sum1Sq = sum([pow(v, 2) for v in word_count_list1])
    sum2Sq = sum([pow(v, 2) for v in word_count_list2])

    # Sum of the products
    pSum = sum([word_count_list1[i] * word_count_list2[i] for i in range(len(word_count_list1))])

    # Calculate r (Pearson score)
    num = pSum - (sum1 * sum2 / len(word_count_list1))
    den = sqrt((sum1Sq - pow(sum1, 2) / len(word_count_list1)) * (sum2Sq - pow(sum2, 2) / len(word_count_list1)))
    if den == 0:
        return 0

    return num / den
```