自然言語処理で感情分析！レビューテキストから評価を予測してみた

機械学習の中で必ずと言ってもいいくらい見かける自然言語処理。

言葉が難しそうでなかなかピンときませんよね。

でも私達が日常生活を過ごす上で実は非常に欠かせないものとなっています。

しかも必要なデータセットを用意し設計の仕方さえわかれば初心者でも形にするのは十分可能。

そこで今回はPythonから自然言語処理を用いて実際にプログラムを作っていきます。

ここで挑戦するのは感情分析。女性ものの服について評価を集めたデータセットを基に自分で新たに入力した文章がどの評価に該当するかを予測していきます。

とは言ってもそもそも自然言語処理とは何か、感情分析とは何か、まずはそこからお伝えし実際のコーディングに入っていきましょう。


##そもそも自然言語処理と感情分析とは

自然言語処理とは何か説明する上で二つの言語の違いを明らかにしておく必要があります。

二つの言語とは自然言語とプログラミング言語。両者に違いは文の意味や解釈が何通りも考えられるかにあります。

自然言語の例として「黒い目の大きい少女」。これだけだと少なくとも二つ解釈ができてしまいます。

一つは「黒い」「目の大きい」「少女」、つまり目が大きくて肌の白い少女。もう一つは「黒い目の」「大きい少女」、つまり目が黒く背の高い少女。

このように、自然言語だと言葉の中曖昧な部分があります。

プログラミング言語の例として挙げられるのは「4 * 6 + 1」。

これなら4に6をかけて1を加えるのだと解釈でき他のパターンを考える余地はないですよね。

このようにプログラミング言語はコンピュータが常に同じ文章を同じように解釈し動作させるためのものなので曖昧さのない言語なのです。

自然言語処理とは自然言語の持つ曖昧さを踏まえたうえで膨大な文章データを実用的に扱うための技術なのです。

その中で感情分析は文章の中にある感情要素を基に数値化するもの。製品に対するネット上での評価を解析するなど様々なフィードバックへの活用が期待できます。

##今回行うこと

今回はKaggleから女性服へのレビューをまとめたもののデータセット["Womens Clothing E-Commerce Reviews.csv"](https://www.kaggle.com/nicapotato/womens-ecommerce-clothing-reviews)を基に自然言語処理を行っていきます。

感情分析として使うのはその中にある服への評価の数値。

ここでは数字が1から5の5段階となっており各評価でどういった言葉が用いられているか分析し、最終的に自分が入力した文章がどの評価に該当するかを予測できるようにします。


##ライブラリとデータを読み込む
自然言語処理として使用するデータセット、何を感情分析の対象とするか触れたので作成過程を順を追って解説していきます。

まずは必要なライブラリとデータを読み込んでいきます。

```python
import numpy as np
import pandas as pd

from nltk.classify import NaiveBayesClassifier
from nltk.classify.util import accuracy as nltk_accuracy

review = pd.read_csv("Womens Clothing E-Commerce Reviews.csv")
#ディレクトリは適宜調整が必要
review = pd.DataFrame(review)
```

それではそれぞれの役割を見ていきましょう。

####numpy

numpyはPythonに入っているライブラリの中で数値計算を効率よく行うもの。機械学習になるとベクトルや行列といった多次元配列に対して演算を繰り返しながらモデルを学習していくことになります。その時numpyを使えば効率よく計算が可能。なのでこのライブラリの存在は欠かせません。

####pandas

pandasはデータ分析に必要な作業を効率よく行うもの。データ分析を行う過程では機械学習を行うまでの前処理というものが全体の8割から9割を占めています。具体的にはデータを読み込んだり欠損値を埋めるなど機械学習をきちんと行えるようデータをきれいに整えること。pandasにはそれらに必要な機能が揃っているので効率よく作業を進めていくことができます。

####nltk

nltkはPythonで人間の言語データを処理するプログラムを作るためのプラットフォーム。文章の解析や分類などの様々な処理ができるよう整えられています。

##データの概観と欠損値の確認

それぞれの役割を見たところでデータの概観と欠損値を見ていきましょう。

####データで含まれているもの

```python
review.columns
Index(['Unnamed: 0', 'Clothing ID', 'Age', 'Title', 'Review Text', 'Rating',
       'Recommended IND', 'Positive Feedback Count', 'Division Name',
       'Department Name', 'Class Name'],
      dtype='object')
review = review.rename(columns={"Unnamed: 0":"ID",'Review Text': 'Text'})
#"Unnamed: 0"を"ID"、'Review Text'を'Text'という風に呼称を変更する。
review = review[["ID","Text","Rating"]]
#ここではIDとテキストと数字のみを使用する。
```

####データの欠損値
```python
#isnull().sum()で欠損値の数を見る。
review.isnull().sum()
ID          0
Text      845
Rating      0
dtype: int64
#評価の数字をつけただけで文章を書いてないのが845件あることがわかる。

#ここではdropna()を用いてそういったデータを削除する。
review = review.dropna()
review.isnull().sum()
ID        0
Text      0
Rating    0
dtype: int64
#これで削除ができた。
```

####データの冒頭

```python
review.head()#データの冒頭部分を表示。head()を使用。
```

|ID	|Text	|Rating|
|:-----------|------------:|:------------:|
|0	|Absolutely wonderful - silky and sexy and comf...	|4|
|1	|Love this dress! it's sooo pretty. i happene...	|5|
|2	|I had such high hopes for this dress and reall...	|3|
|3	|I love, love, love this jumpsuit. it's fun, fl...	|5|
|4	|This shirt is very flattering to all due to th...	|5|


####データの全体像

```python
review.describe()#データの外観を表示。describe()を使用。
#データの数は22,461件、半分近くのレビュー数値は5だとわかる。
```

	|ID|Rating|
|:-----------|------------:|:------------:|
|count	|22641.000000	|22641.000000|
|mean	|11740.849035	|4.183561|
|std	|6781.957509	|1.115762|
|min	|0.000000	|1.000000|
|25%	|5872.000000	|4.000000|
|50%	|11733.000000	|5.000000|
|75%	|17621.000000	|5.000000|
|max	|23485.000000	|5.000000|


####データの型の確認
```python
review.dtypes#dtypesを使用。データの加工や操作で不都合があった場合型の変更が必要となる場合がある。
ID         int64
Text      object
Rating     int64
dtype: object
```

##いざ学習させてみよう

データを確認し欠損値を削除できたので早速学習に入っていきましょう。

まずは全データの文章をレートの数字ごとのリストに分け、単語に切り分けて格納していきます。

```python
rate_id_one = []
rate_id_two = []
rate_id_three = []
rate_id_four = []
rate_id_five =[]

for text, rating in zip(review['Text'], review['Rating']):
    line = text.split(" ")
    dic = {}
    if rating == 1:
        for word in line:
            dic[word] = True
        ireru = (dic, 1)
        rate_id_one.append(ireru)
    elif rating == 2:
        for word in line:
            dic[word] = True
        ireru = (dic, 2)
        rate_id_two.append(ireru) 
    elif rating == 3:
        for word in line:
            dic[word] = True
        ireru = (dic, 3)
        rate_id_three.append(ireru)
    elif rating == 4:
        for word in line:
            dic[word] = True
        ireru = (dic, 4)
        rate_id_four.append(ireru)
    else:
        for word in line:
            dic[word] = True
        ireru = (dic, 5)
        rate_id_five.append(ireru)
```

数字ごとに仕分けができたのでそれぞれから学習用のデータとテスト用のデータで8:2に分けていきます。

各数字で学習用テスト用で合わせたものを全体のものとします。

```python
threshold = 0.8
num_one = int(threshold * len(rate_id_one))
num_two = int(threshold * len(rate_id_two))
num_three = int(threshold * len(rate_id_three))
num_four = int(threshold * len(rate_id_four))
num_five = int(threshold * len(rate_id_five))

features_train = rate_id_one[:num_one] + rate_id_two[:num_two] + rate_id_three[:num_three] + rate_id_four[:num_four] + rate_id_five[:num_five]
features_test = rate_id_one[num_one:] + rate_id_two[num_two:] + rate_id_three[num_three:] + rate_id_four[num_four:] + rate_id_five[num_five:]
print("Number of training datapoints:", len(features_train))
print("Number of test datapoints:", len(features_test))

Number of training datapoints: 18111
Number of test datapoints: 4530
```

学習用とテスト用に二つに分けたので学習に入っていきます。

そして学習させたものでテスト用データに数字の判定をさせていきましたが正答率は半数に満たないようです。

```python
classifier = NaiveBayesClassifier.train(features_train)
print('Accuracy of the classifier:', nltk_accuracy(classifier, features_test))

Accuracy of the classifier: 0.4640176600441501
```
原因として選択肢が1から5まででかなり幅があることや低評価の文章データの絶対数が足りないということが考えられます。例えばレートを1と5の二択にすれば改善する可能性があります。

あるいは他の手法を使えばさらに精度が上がる可能性もあるので今後の課題としていきます。

学習させた中でどういったワードが予測された数字を左右されたのか見ていきましょう。

```python
N = 15
print('Top ' + str(N) + ' most informative words:')
for i, item in enumerate(classifier.most_informative_features()[:N]):
    print(str(i+1) + '. ' + item[0]) 

Top 15 most informative words:
1. worst
2. shame
3. poorly
4. horrible
5. disappointment.
6. cheap.
7. strange.
8. sad.
9. dull
10. terrible.
11. returned.
12. terrible
13. awkward.
14. concept
15. awful.
```
worst(最悪だ)、shame(恥ずかしい)、disappointment(がっかりだ)などマイナスの言葉が目立ちます。

ネガティブさをストレートに表したものが数字を左右する決定的な要素となるのでしょう。

単語にピリオドが付いているものもありますが今回はこれも単語の１つとして見ることにします。

##実際にレビューを書いてみよう

それでは自分で文章を作成しレートを予測してみましょう。

```python
def extract_features(words):
    return dict([(word, True) for word in words])
#先程の大量のレビューテキストと同様単語に分けていく

input_review = input()
print("Clothes review predictions:")

print("\nReview:",input_review)
features = extract_features(input_review.split())
probabilities = classifier.prob_classify(features)
predicted_sentiment = probabilities.max()
print("Predicted sentiment:", predicted_sentiment)
print("Probability:", round(probabilities.prob(predicted_sentiment), 2))
#入力した文章がどの数字にあたる可能性が一番高いかを計算し出力させる
```
例えばここで"I cannnot believe how terrible is it!"と入力します。日本語で「こんなひどいなんて信じられない！」という意味です。

```python
I cannnot believe how terrible is it!
Clothes review predictions:

Review: I cannnot believe how terrible is it!
Predicted sentiment: 1
Probability: 0.61
```
一番低い評価である可能性が高いということがわかりました。

##終わりに

今回は自然言語処理や感情分析とは何か触れたうえで、Kaggleにあるデータセットを用いて実際に自然言語処理の実装を行いました。

プログラミングの初心者であっても必要なデータを手に入れて適切なステップを踏んでいけば実装するのは十分可能。そして実際に踏んだステップは以下の通り。

1.ライブラリとデータを読み込む

2.データを確認し加工、学習できる状態に整える

3.学習させ性能を確かめる

一通りの段取りを理解し実際に書いて動かせるようになれば他のデータセットを使う際にも応用することもできます。なのでこれを確実に押さえ自分のデータセットに活かせるといいですよね。

最後に今回のコードを掲載するので参考にしてください。



## 今回のコード

```python
import numpy as np
import pandas as pd

from nltk.classify import NaiveBayesClassifier
from nltk.classify.util import accuracy as nltk_accuracy

review = pd.read_csv("Womens Clothing E-Commerce Reviews.csv")
#ディレクトリは適宜調整が必要
review = pd.DataFrame(review)


review.columns
Index(['Unnamed: 0', 'Clothing ID', 'Age', 'Title', 'Review Text', 'Rating',
       'Recommended IND', 'Positive Feedback Count', 'Division Name',
       'Department Name', 'Class Name'],
      dtype='object')

review = review.rename(columns={"Unnamed: 0":"ID",'Review Text': 'Text'})
review = review[["ID","Text","Rating"]]#ここではIDとテキストと数字のみにする。

review.isnull().sum()
ID          0
Text      845
Rating      0
dtype: int64

review = review.dropna()
review.isnull().sum()
ID        0
Text      0
Rating    0
dtype: int64

review.head()#データの冒頭部分を表示
review.describe()#データの外観を表示

review.dtypes
ID         int64
Text      object
Rating     int64
dtype: object

rate_id_one = []
rate_id_two = []
rate_id_three = []
rate_id_four = []
rate_id_five =[]

for text, rating in zip(review['Text'], review['Rating']):
    line = text.split(" ")
    dic = {}
    if rating == 1:
        for word in line:
            dic[word] = True
        ireru = (dic, 1)
        rate_id_one.append(ireru)
    elif rating == 2:
        for word in line:
            dic[word] = True
        ireru = (dic, 2)
        rate_id_two.append(ireru) 
    elif rating == 3:
        for word in line:
            dic[word] = True
        ireru = (dic, 3)
        rate_id_three.append(ireru)
    elif rating == 4:
        for word in line:
            dic[word] = True
        ireru = (dic, 4)
        rate_id_four.append(ireru)
    else:
        for word in line:
            dic[word] = True
        ireru = (dic, 5)
        rate_id_five.append(ireru)

rate_id_one[0]#リストの中に入っている単語を表示

len(rate_id_one)
821

threshold = 0.8
num_one = int(threshold * len(rate_id_one))
num_two = int(threshold * len(rate_id_two))
num_three = int(threshold * len(rate_id_three))
num_four = int(threshold * len(rate_id_four))
num_five = int(threshold * len(rate_id_five))

features_train = rate_id_one[:num_one] + rate_id_two[:num_two] + rate_id_three[:num_three] + rate_id_four[:num_four] + rate_id_five[:num_five]
features_test = rate_id_one[num_one:] + rate_id_two[num_two:] + rate_id_three[num_three:] + rate_id_four[num_four:] + rate_id_five[num_five:]
print("Number of training datapoints:", len(features_train))
print("Number of test datapoints:", len(features_test))
Number of training datapoints: 18111
Number of test datapoints: 4530

classifier = NaiveBayesClassifier.train(features_train)
print('Accuracy of the classifier:', nltk_accuracy(classifier, features_test))

Accuracy of the classifier: 0.4640176600441501

N = 15
print('Top ' + str(N) + ' most informative words:')
for i, item in enumerate(classifier.most_informative_features()[:N]):
    print(str(i+1) + '. ' + item[0]) 

Top 15 most informative words:
1. worst
2. shame
3. poorly
4. horrible
5. disappointment.
6. cheap.
7. strange.
8. sad.
9. dull
10. terrible.
11. returned.
12. terrible
13. awkward.
14. concept
15. awful.

def extract_features(words):
    return dict([(word, True) for word in words])

#文章をその場で入力したものを見る
input_review = input()
print("Clothes review predictions:")

print("\nReview:",input_review)
features = extract_features(input_review.split())
probabilities = classifier.prob_classify(features)
predicted_sentiment = probabilities.max()
print("Predicted sentiment:", predicted_sentiment)
print("Probability:", round(probabilities.prob(predicted_sentiment), 2))

I cannnot believe how terrible is it!
Clothes review predictions:

Review: I cannnot believe how terrible is it!
Predicted sentiment: 1
Probability: 0.61
```