ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

日本語ツイートの感情分析とデータの収集方法

ポール・エクマンは、感情について研究を行った心理学者で、怒り、嫌悪、恐れ、喜び、悲しみ、驚きは普遍的感情だと結論づけたようです。今回は、日本語ツイートをこの6感情で評価します。

github

コードだけ見たい場合は、以下のノートブックをgithubに上げたのでそれを見てください。 https://github.com/sugiyamath/credibility_analysis/blob/master/notebook/sentiment_analysis/cnn_sentiment_ja.ipynb

パイプライン

  1. 6感情を表すと考えられる絵文字を使い、tweetscraperで収集し、distant supervisionでラベル付け。
  2. 1次元CNNで訓練・評価。
  3. サンプルデータに対して、感情の円グラフを出力。

事前準備

事前に用意するデータは、以下のスクリプトで収集します(一日かかる): https://github.com/sugiyamath/credibility_analysis/blob/master/notebook/sentiment_analysis/run.sh

このスクリプトでは絵文字をtwitterscraperで使っているので、環境によっては絵文字が正しく表示されないかもしれません。以下のように実行します:

chmod +x run.sh
./run.sh &

WARNING: 大量のデータ収集を規約違反の方法で行っているので、注意してください。

jupyter notebookで実行

まず、収集した感情データを読み込みます。データの読み込みパスはデータの場所に合わせて修正してください。

In[1]:

import pandas as pd
from os.path import join
from sklearn.utils import shuffle

emotions = ["happy", "sad", ["disgust", "disgust2"], "angry", "fear", "surprise"]
dir_path = "gathering/ja_tweets_sentiment"
size = 60000
df = []
for i, es in enumerate(emotions):
    if isinstance(es, list):
        for e in es:
            data = shuffle(pd.read_json(join(dir_path, "{}.json".format(e)))).iloc[:int(size/len(es))]
            data['label'] = i
            df.append(data)
    else:
        data = shuffle(pd.read_json(join(dir_path, "{}.json".format(es)))).iloc[:int(size)]
        data['label'] = i
        df.append(data)

df = pd.concat(df)
df = shuffle(df)
X = df['text']
y = df['label']
df.shape

Out[1]: (360000, 11)

In[2]:

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
from sklearn.model_selection import train_test_split

max_features=10000
maxlen = 280

def preprocess(data, tokenizer, maxlen=280):
    return(pad_sequences(tokenizer.texts_to_sequences(data), maxlen=maxlen))

y = to_categorical(y)
X_train, X_test, y_train, y_test = train_test_split(X, y)

tokenizer = Tokenizer(num_words=max_features, filters="", char_level=True)
tokenizer.fit_on_texts(list(X_train))

X_train = preprocess(X_train, tokenizer, maxlen)
X_test = preprocess(X_test, tokenizer, maxlen)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train)
X_train.shape, X_val.shape, X_test.shape

Out[2]: *1

Kerasでモデルを構築します。

In[3]:

from keras.layers import Input, Dense, Embedding, Flatten
from keras.layers import SpatialDropout1D
from keras.layers.convolutional import Conv1D, MaxPooling1D
from keras.models import Sequential

model = Sequential()
model.add(Embedding(max_features, 150, input_length=maxlen))
model.add(SpatialDropout1D(0.2))
model.add(Conv1D(32, kernel_size=3, padding='same', activation='relu'))
model.add(MaxPooling1D(pool_size=2))
model.add(Conv1D(64, kernel_size=3, padding='same', activation='relu'))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(6, activation='sigmoid'))
model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])

訓練します。

In[4]:

epochs = 15
batch_size = 1000

model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=epochs, batch_size=batch_size)

Out[4]:

Train on 202500 samples, validate on 67500 samples
Epoch 1/15
202500/202500 [==============================] - 15s 75us/step - loss: 1.6038 - acc: 0.3209 - val_loss: 1.5504 - val_acc: 0.3619
Epoch 2/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.4825 - acc: 0.4033 - val_loss: 1.4568 - val_acc: 0.4205
Epoch 3/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.4100 - acc: 0.4498 - val_loss: 1.3896 - val_acc: 0.4602
Epoch 4/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.3583 - acc: 0.4783 - val_loss: 1.3613 - val_acc: 0.4733
Epoch 5/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.3260 - acc: 0.4936 - val_loss: 1.3385 - val_acc: 0.4853
Epoch 6/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.3006 - acc: 0.5060 - val_loss: 1.3523 - val_acc: 0.4816
Epoch 7/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.2798 - acc: 0.5162 - val_loss: 1.3315 - val_acc: 0.4915
Epoch 8/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.2608 - acc: 0.5229 - val_loss: 1.3386 - val_acc: 0.4909
Epoch 9/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.2436 - acc: 0.5301 - val_loss: 1.3424 - val_acc: 0.4897
Epoch 10/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.2267 - acc: 0.5377 - val_loss: 1.3249 - val_acc: 0.4974
Epoch 11/15
202500/202500 [==============================] - 8s 39us/step - loss: 1.2128 - acc: 0.5432 - val_loss: 1.3113 - val_acc: 0.5031
Epoch 12/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.1966 - acc: 0.5494 - val_loss: 1.3209 - val_acc: 0.5005
Epoch 13/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.1841 - acc: 0.5556 - val_loss: 1.3288 - val_acc: 0.4979
Epoch 14/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.1708 - acc: 0.5600 - val_loss: 1.3152 - val_acc: 0.5046
Epoch 15/15
202500/202500 [==============================] - 8s 38us/step - loss: 1.1600 - acc: 0.5640 - val_loss: 1.3332 - val_acc: 0.4997

テストセットで分類レポートを出力します。

In[5]:

from sklearn.metrics import classification_report

import numpy as np
y_preds =  model.predict(X_test)
y_preds =  np.argmax(y_preds, axis=1)
y_true = np.argmax(y_test, axis=1)

emolabels = []
for e in emotions:
    if isinstance(e, list):
        emolabels.append(e[0])
    else:
        emolabels.append(e)

print(classification_report(y_true, y_preds, target_names=emolabels))

Out[5]:

             precision    recall  f1-score   support

      happy       0.50      0.64      0.56     15040
        sad       0.61      0.51      0.55     14918
    disgust       0.39      0.52      0.45     15205
      angry       0.56      0.42      0.48     15050
       fear       0.49      0.52      0.50     14888
   surprise       0.51      0.38      0.44     14899

avg / total       0.51      0.50      0.50     90000

In[6]:

examples = [
    "まじきもい、あいつ",
    "今日は楽しい一日だったよ",
    "ペットが死んだ、実に悲しい",
    "ふざけるな、死ね",
    "ストーカー怖い",
    "すごい!ほんとに!?",
    "葉は植物の構成要素です。",
    "ホームレスと囚人を集めて革命を起こしたい"
]

targets = preprocess(examples, tokenizer, maxlen=maxlen)
print('\t'.join(emolabels))
for i, ds in enumerate(model.predict(targets)):
    print('\t'.join([str(round(100.0*d)) for d in ds]))

Out[6]:

Screenshot_2018-08-28_18-47-46.png

数字だけではわかりにくい!という人もいるかもしれないので、円グラフで出力します。上記のexamples内の「まじきもい、あいつ」という文章がどのように予測されたのかを円グラフで表します。

In[7]:

%matplotlib inline
import matplotlib.pyplot as plt
plt.pie(model.predict(targets)[0]*100.0, labels=emolabels)
print(examples[0])

Out[7]:

Screenshot_2018-08-28_18-49-47.png

考察

分類レポートの結果は、一見するとあまり良い結果ではないように見えますが、これはノイズの影響が大きいように思います。KaggleのImdbの5クラス分類では、コンペ参加者は確か0.68ぐらいの精度しか出していなかったので、ノイズありデータの6クラス分類の日本語タスクではこのぐらいの精度で精一杯かもしれません。

しかし、サンプルデータで予測してみると、人間の直感にも合致した数字が得られています。ただし、ニュートラルな文章は、happyになりやすいようです。

LSTMを使ったモデルでも試しましたが、1次元のCNNのほうが精度は良かったです。もしLSTMを使ったモデルで精度を改善するなら、1次元のCNNのモデルにLSTMを加えるような形のほうがよいと思います。そのようなモデルはあまり見かけませんが、たぶんそれなりの結果はでると思います。

訓練済みモデル

なお、ここで訓練したモデルは以下からダウンロードできます。 https://github.com/sugiyamath/credibility_analysis/tree/master/notebook/sentiment_analysis/models/ja_tweets_sentiment

トークナイザとkerasモデルをおいてあります。モデルを試しに使いたい場合は自由に使ってください。

参考

[0] PythonとKerasによるディープラーニング | Francois Chollet, 巣籠 悠輔, 株式会社クイープ

関連記事

[0] https://qiita.com/sugiyamath/items/cb0177f3521a3208e325 [1] https://qiita.com/sugiyamath/items/0fc3a64002201d4d192e

*1:202500, 280), (67500, 280), (90000, 280