自然言語処理を用いたAmazonレビューの分類（Naive Bayes編）

本記事ではAmazonレビューを元に、それが高評価か低評価か判断するタスクを取り扱います。
使うモデルはNaive BayesとBERTで今回はNaive Bayesを取り上げます。

BERTはこの記事をご覧ください。

https://qiita.com/hokkey621/items/8d8c0e0fe072a6b40fed

# Naive Bayesとは　
今回は多項モデルではなく、ベルヌーイモデルを扱います。
ナイーブベイズは[ベイズの定理](https://manabitimes.jp/math/804)を応用した統計的手法です。
例としてある文章*d*がITというカテゴリーに含まれるかどうか考えてみることにします。

Naive Bayesではある文章*d*が与えられたときカテゴリITである確率を考えます。
各カテゴリに対して確率を計算し、最も確率が高いカテゴリに分類するというわけです。
これを**事後確率**といい次の式で表せます。

$$ P(IT|d) = \frac{P(IT)P(d|IT)}{P(d)} \propto　P(IT)P(d|IT) $$

*P(d)* はどのカテゴリも同じなので事後確立を求めるには**事前確率** *P(IT)* と**尤度** *P(d|IT)* が分かればいいわけです。

*P(IT)* は訓練データの総文章数におけるITカテゴリの割合を調べればいいだけです。
例えば、文章が1000文あってそのうちITに関する文章が100文であれば $$ P(IT) = \frac{100}{1000} = 0.1 $$というわけです。

*P(d|IT)* は少し工夫が必要です。
まず前提として文章は単語の集合と考えます。
集合なので単語が文章中のどこに出てくるかは考えません（このような手法を**Bag-Of-Words**といいます）。
この考えを使うと、文章*d*は単語*w*の集合として捉えることができます。
ここでもう一つ仮定をする必要があります。それは**全ての単語は独立に出現する**ということです。
共起語を知らないのか！「機械」と「学習」はよく一緒に使われるだろうー！と怒りたくなる気持ちを抑えて次に進みましょう。
単語の出現が独立であると仮定すると、カテゴリITが与えられたときに単語*w*の集合である文章*d*が出現する確率は次にように表せます。

$$ P(d|IT) = \prod_{i = 1}^{n}　P(w_i|IT) $$

*P(w_i|IT)* はITカテゴリ中に単語*w_i*が出現する確率なので、訓練データから求めれられます。
実際にはアンダーフロー対策として対数を取るのですが、ひとまずこれで確率を求めることができました。

しかし、実はまだ問題があります。
それは、未知の文章カテゴリを予測する際に、訓練データに含まれていない単語 *w'* を一つでも含んでいると*P(w'|IT)* が0となり、*P(d'|IT)* も0となってしまう点です。
例えば、訓練データに使った記事がApple信者が書いたものばかりだった場合を考えてみましょう。
この場合、分類対象の文章中にiPhoneなどの単語が含まれていても、Androidが含まれてしまうとカテゴリITの確率が0になってしまいます（これをゼロ頻度問題と言います）。
これでは困るので、一定の値を与えることでこの問題が起きるのを防ぎます（この方法をスムージングと言います）。
数式で表すと次の通りです。

$$ P(w_i|IT) = \frac{Count(w_i) + \alpha}{N + \alpha V} $$

*Count(x)*は単語*x*の出現回数、NはITカテゴリの全単語数、Vは語彙数です。
こうすれば、確率が0にならないのでゼロ頻度問題を回避できます。

# 前処理
さて、ここからは実装をしていきましょう。
僕の実行環境は次のとおりです。

 <table>
    <tr>
      <td>実行環境</td>
      <td>Google Colaboratory (Pro +)</td>
    </tr>
    <tr>
      <td>Python</td>
      <td>3.8.10</td>
    </tr>
    <tr>
      <td>janome</td>
      <td>0.4.2</td>
    </tr>
 </table>

データは公開されているデータセット`Webis Cross-Lingual Sentiment Dataset 2010`を使います。

https://zenodo.org/record/3251672#.ZBBYtezP30o

ここから入手できるものの中から`cls-acl10-unprocessed`を使います。
前処理済みのデータセットもあるのですが今回は勉強を兼ねて前処理からやります。
前処理のコードはこちらの本を参考にさせてもらいました。

https://www.amazon.co.jp/%E6%96%87%E6%9B%B8%E5%88%86%E9%A1%9E%E3%81%8B%E3%82%89%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E%E5%87%A6%E7%90%86%E5%85%A5%E9%96%80-%E5%9F%BA%E6%9C%AC%E3%81%8B%E3%82%89BERT%E3%81%BE%E3%81%A7-%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E5%85%A5%E9%96%80%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA109-%E6%96%B0%E7%B4%8D-%E6%B5%A9%E5%B9%B8/dp/4910558144

::: note warn
Amazon上のレビューを直接スクレイピングすることは禁止されているので注意しましょう
:::

まず、データセットの中身を確認します。
```
<item><category>本</category><raiting>4.0</raiting>
（省略）
<text>
紹介文
</text>
（省略）
</item>
```
前処理の仕方

1. itemのタグを使って1冊ごとに分解
2. 正規表現を使って紹介文とraitingを抽出
3. トークナイザを使って紹介文をトークンごとに分解

これを実装します。
``` Python
t = Tokenizer()
vectorizer = CountVectorizer(token_pattern='(?u)¥¥b¥¥w+¥¥b') #日本 語対応

def get_tokenized_sentences_and_labels(file_name: str):
    #データの読み込み
    with open(file_name) as f:
        data = f.read()
        data = data.replace('\n', '').replace('\r', '')
    
    reviews = re.findall(pattern=r'<item>(.+?)</item>',string=data) #ratingはあるのにtextがない感想を取り除く

    tokenized_sentences = []
    labels = []

    for item in reviews:
        raiting = re.findall(pattern=r'<rating>(.+?)</rating>',string=item)
        text = re.findall(pattern=r'<text>(.+?)</text>',string=item)
        if raiting and text:
            #textがある感想のみ採用
            words = [token for token in t.tokenize(text[0], wakati=True)] #tokenごとに分解
            a_tokenized_sentence=" ".join(words) #スペース区切りで結合
            tokenized_sentences.append(a_tokenized_sentence)
            raiting = int(float(raiting[0])) #5段階の評価を取得
            label = 0 if raiting >= 3 else 1 #positive -> 0, negative -> 1
            labels.append(label)
 
    return tokenized_sentences, labels

def make_sample_vectors(train_file_name: str, test_file_name: str):
    train_tokenized_sentences, train_y = get_tokenized_sentences_and_labels(train_file_name)
    train_X = vectorizer.fit_transform(train_tokenized_sentences)

    test_tokenized_sentences, test_y = get_tokenized_sentences_and_labels(test_file_name)
    test_X = vectorizer.transform(test_tokenized_sentences)

    return train_X, train_y, test_X, test_y


train_X, train_y, test_X, test_y = make_sample_vectors(config.input_path + 'train.review', config.input_path + 'test.review')
```
# 学習
sklearnのnaibe_bayesモジュールを用いて学習をしていきます。
Naive Bayesではスムージングの$\alpha$がハイパーパラメータとなっていますが、今回はデフォルト値のままやってみます。
興味がある方はハイパラチューニングをしてみると精度が上がるかもしれないのでやってみてください。

```Python
from sklearn.naive_bayes import BernoulliNB
cl = BernoulliNB()
cl.fit(train_X, train_y)
```
scoreを確認してみると`0.729`であることがわかりました。
混同行列は次のようになりました。
![NV_cm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736924/cf92a755-a80c-48e4-6dae-90300e84a490.png)
縦が実際のラベルで横が予測ラベルになっています。
ポシティブなレビューをネガティブと誤解しているケースが多いことがわかりますね。

今回実装したコードは[こちら](https://github.com/hokkey621/Amazon-Review-Classification-using-Natural-Language-Processing)にあります。~~共有用に書いていたものではないので汚いです。~~
PATHを書き換えると動かせると思います。
パラメータを変えるなどして遊んでみてください。

# まとめ
Naive Bayesは古典的な手法ではありますが、7割越えのまあまあな精度で予測することができました。
偽陰性の確率が高いことは分析する余地がありそうです。

BERT版で実験してみた記事も書いたので[こちら](https://qiita.com/hokkey621/items/8d8c0e0fe072a6b40fed)からぜひチェックしてください。

またこの記事は、僕が所属している **「AI・機械学習を学ぶオンラインコミュニティAcademiX」** のリレー記事です。
AIについて深く学びたい方はぜひコミュニティに参加してください！

https://www.academix.jp/