stMind

about Arsenal, Arsene Wenger, Tech, Computer vision and Machine learning

imdb_reviews datasetのレビューに含まれるunique wordの上位N件の分布

レビューのテキストに含まれる語には、どういったものがあるか。特に、上位に含まれる語について、分布を見てみます。

scikit-learnのCountVectorizerを使って実装します。

def get_top_n_words(corpus, n=20, is_stop_words=False):
    vectorizer = CountVectorizer(stop_words='english') if is_stop_words else CountVectorizer()
    X = vectorizer.fit_transform(corpus)
    X_sum = X.sum(axis=0)
    words_freq = [(word, X_sum[0, idx]) for word, idx in vectorizer.vocabulary_.items()]
    words_freq = sorted(words_freq, key=lambda x: x[1], reverse=True)
    return words_freq[:n]

fit_transformで、corpusに含まれるunique wordのcountを含むarrayが得られるので、sumでword毎のcountを計算しています。

公式の例から引用。

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> corpus = [
...     'This is the first document.',
...     'This document is the second document.',
...     'And this is the third one.',
...     'Is this the first document?',
... ]
>>> vectorizer = CountVectorizer()
>>> X = vectorizer.fit_transform(corpus)
>>> print(vectorizer.get_feature_names())
['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
>>> print(X.toarray())
[[0 1 1 1 0 0 1 0 1]
 [0 2 0 1 0 1 1 0 1]
 [1 0 0 1 1 0 1 1 1]
 [0 1 1 1 0 0 1 0 1]]

このsumを計算すると以下のようにword毎のカウントが得られます。

>>> X.sum(axis=0)                                                                                             
matrix([[1, 4, 2, 4, 1, 1, 4, 1, 4]], dtype=int64)

では、分布を見てみます。上段のグラフがStop wordsを含むもの、下段がStop wordsを除いたものです。

f:id:satojkovic:20200704171438p:plain

上位の語のリストで見ると、結構似ています。goodやbadのような感情分析に有効であると思われる語だけでなく、movieやfilmのようなニュートラルな語も含まれています。positiveなテキストにおいて、goodはbadより多く含まれていますが、negativeなテキストではbadはgoodより多いものの、同じくらい多く含まれていることも分かりました。

imdb_reviews datasetのレビュー長のヒストグラム

データセット自体を理解し、モデル改善に役立てていきます。

Positiveなレビュー、Negativeなレビュー、それぞれについてレビュー長(ワード数)をカウントし、ヒストグラムで可視化します。

    # 4. Count the length of sentences
    all_sentences = training_sentences + testing_sentences
    all_labels = training_labels + testing_labels
    positive_sentence_length = [len(sentence.split(' ')) for sentence, label in zip(all_sentences, all_labels) if label == 1]
    negative_sentence_length = [len(sentence.split(' ')) for sentence, label in zip(all_sentences, all_labels) if label == 0]
    max_length = max(max(positive_sentence_length), max(negative_sentence_length))

    # 5. Plot the distribution
    plt.hist(positive_sentence_length, bins=max_length, alpha=0.5, rwidth=2, label='positive')
    plt.hist(negative_sentence_length, bins=max_length, alpha=0.5, rwidth=2, label='negative')
    plt.xlabel('Review length')
    plt.ylabel('Frequency')
    plt.legend()
    plt.show()

f:id:satojkovic:20200629210500p:plain

100〜200ワード前後が最も多いですね。

LSTMでSentiment Analysis

これまでは、語単位での特徴量化でレビューの感情分析を行いましたが、文章のコンテキストを考慮するともっとうまくPositive/Negativeの判定が出来そうです。 (単純にgoodやbadのような語が含まれているかどうかだけでなく、「最初はgoodだけど、最後にはdisappointedだった」みたいなレビューであれば、文脈上disappointedがより重要な語になるし、goodは重要度が低くなるみたいな)

LSTM層の導入も非常に簡単です。

    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length),
        tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32, return_sequences=True)),
        tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(16)),
        tf.keras.layers.Dense(6, activation='relu'),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])

ユニット数を指定してLSTM層を追加しています。ここでは、双方向のLSTMにするためにBidirectionalとし(順方向にも逆方向にも文脈を考慮)、双方向のLSTMを2層使いました。

f:id:satojkovic:20200622204815p:plain

LSTMを使わない場合もval_accuracyが80%程度だったので、あまり結果は変わらず(残念)。

入力次元数やユニット数などのハイパーパラメータのチューニングも含め、過学習を抑える必要がありそうです。

Sentiment Analysisの学習履歴をプロット

学習時のlossとaccuracyを確認するのも容易。model.fitの戻り値をhistoryとして受け取って、横軸にepoch数、縦軸にlossやaccuracyの値をとって表示するだけ。

    history = model.fit(padded, training_labels, epochs=num_epochs, validation_data=(testing_padded, testing_labels))
    acc = history.history['accuracy']
    loss = history.history['loss']
    val_acc = history.history['val_accuracy']
    val_loss = history.history['val_loss']
    epochs = range(len(acc))
    plt.plot(epochs, acc, 'r', label='acc')
    plt.plot(epochs, val_acc, 'b', label='val_acc')
    plt.plot(epochs, loss, 'r', linestyle='dashed', label='loss')
    plt.plot(epochs, val_loss, 'b', linestyle='dashed', label='val_loss')
    plt.legend()
    plt.show()

historyから取得可能な要素のリストは、keys()で確認出来る。

print(history.history.keys())

# this might produces the following
# dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])

f:id:satojkovic:20200620125738p:plain

Sentiment AnalysisをGloveで行う

前回の続き。

前回はEmbedding layerも含めて学習したが、学習済みのGloveを使う。

最初に、glove.6B.100d.txt(wordとembeddingを行単位で記述)をロードして、embedding_indexを作る。次に、imdb_reviewsデータセットのwordに対して、対応するgloveのembeddingを格納したembedding_matrixを作成。後は、modelの記述において、Embedding layerのweightsにembedding_matrixを与えて、学習しないようにTrainable=FalseとすればOK。

    embedding_index = {}
    with open('glove.6B.100d.txt') as f:
        for line in f:
            values = line.split()
            word = values[0]
            coefs = np.asarray(values[1:], dtype='float32')
            embedding_index[word] = coefs

    embedding_matrix = np.zeros((vocab_size + 1, embedding_dim))
    for word, i in word_index.items():
        embedding_vector = embedding_index.get(word)
        if embedding_vector is not None:
            embedding_matrix[i] = embedding_vector

    # Glove is used for embedding layer.
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size + 1, embedding_dim, input_length=max_length, weights=[embedding_matrix], trainable=False),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(6, activation='relu'),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])

CourseraのNLP in tensorflowを見てSentiment Analysisが容易に出来ると知る

www.coursera.org

tensorflow_datasetに用意されているimdb_reviewsデータセットを取得して、kerasのTokenizerを使って前処理して、Modelを書いて、学習。

これをテンプレートとして使えば、EmbeddingはBERTに変えたり、imdb_reviewsを自分で作ったデータセットにしたりして、拡張していけそう。

import tensorflow_datasets as tfds
import numpy as np

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import tensorflow as tf

if __name__ == "__main__":
    # 1. Load imdb dataset
    imdb, info = tfds.load('imdb_reviews', with_info=True, as_supervised=True)

    # 2. Split imdb dataset into train / test data
    train_data, test_data = imdb['train'], imdb['test']

    # 3. Prepare sentences and labels
    training_sentences = []
    training_labels = []

    testing_sentences = []
    testing_labels = []

    for s, l in train_data:
        training_sentences.append(str(s.numpy()))
        training_labels.append(l.numpy())

    for s, l in test_data:
        testing_sentences.append(str(s.numpy()))
        testing_labels.append(l.numpy())

    training_labels = np.array(training_labels)
    testing_labels = np.array(testing_labels)

    # 4. Hyperparameters
    vocab_size = 10000
    embedding_dim = 16
    max_length = 120
    trunc_type = 'post'
    oov_token = '<OOV>'

    # 5. Tokenize
    tokenizer = Tokenizer(num_words=vocab_size, oov_token=oov_token)
    tokenizer.fit_on_texts(training_sentences)
    word_index = tokenizer.word_index
    sequences = tokenizer.texts_to_sequences(training_sentences)
    padded = pad_sequences(sequences, maxlen=max_length, truncating=trunc_type)

    testing_sequences = tokenizer.texts_to_sequences(testing_sentences)
    testing_padded = pad_sequences(testing_sequences, maxlen=max_length, truncating=trunc_type)

    # 6. Defining model
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(6, activation='relu'),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])

    # 7. Compile and training
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.summary()

    num_epochs = 10
    model.fit(padded, training_labels, epochs=num_epochs, validation_data=(testing_padded, testing_labels))
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 120, 16)           160000    
_________________________________________________________________
flatten_1 (Flatten)          (None, 1920)              0         
_________________________________________________________________
dense_2 (Dense)              (None, 6)                 11526     
_________________________________________________________________
dense_3 (Dense)              (None, 1)                 7         
=================================================================
Total params: 171,533
Trainable params: 171,533
Non-trainable params: 0
_________________________________________________________________
Epoch 1/10
782/782 [==============================] - 4s 5ms/step - loss: 0.5723 - accuracy: 0.7062 - val_loss: 0.4707 - val_accuracy: 0.8163
Epoch 2/10
782/782 [==============================] - 4s 5ms/step - loss: 0.3454 - accuracy: 0.8918 - val_loss: 0.4481 - val_accuracy: 0.8147
Epoch 3/10
782/782 [==============================] - 4s 5ms/step - loss: 0.1946 - accuracy: 0.9558 - val_loss: 0.5217 - val_accuracy: 0.8035
Epoch 4/10
782/782 [==============================] - 4s 5ms/step - loss: 0.1172 - accuracy: 0.9798 - val_loss: 0.5584 - val_accuracy: 0.8052
Epoch 5/10
782/782 [==============================] - 4s 5ms/step - loss: 0.0851 - accuracy: 0.9852 - val_loss: 0.6381 - val_accuracy: 0.8047
Epoch 6/10
782/782 [==============================] - 4s 5ms/step - loss: 0.0707 - accuracy: 0.9869 - val_loss: 0.6734 - val_accuracy: 0.8055
Epoch 7/10
782/782 [==============================] - 4s 5ms/step - loss: 0.0643 - accuracy: 0.9872 - val_loss: 0.7113 - val_accuracy: 0.8036
Epoch 8/10
782/782 [==============================] - 4s 5ms/step - loss: 0.0598 - accuracy: 0.9879 - val_loss: 0.7544 - val_accuracy: 0.8028
Epoch 9/10
782/782 [==============================] - 4s 5ms/step - loss: 0.0568 - accuracy: 0.9883 - val_loss: 0.7722 - val_accuracy: 0.8052
Epoch 10/10
782/782 [==============================] - 4s 5ms/step - loss: 0.0558 - accuracy: 0.9884 - val_loss: 0.8033 - val_accuracy: 0.8050

github.com

Tensorflowレポジトリのコミット数ランキングを表示するワンライナー

  1. git logコマンドのfomatオプションでauthor nameを出力
  2. sort
  3. uniq -cで連続する行数をカウント、重複行は削除
  4. sort -nrで降順にソート
  5. headで先頭から10件表示
$ git log --format='%an' | sort | uniq -c | sort -nr | head 
21995 A. Unique TensorFlower
3476 TensorFlower Gardener
1229 Yong Tang
1155 Shanqing Cai
1154 Derek Murray
1055 Gunhan Gulsoy
1046 Benoit Steiner
1036 Vijay Vasudevan
 865 River Riddle
 794 Martin Wicke

tensorflowのレポジトリをローカルに持ってない場合は、Github APIを使ってコミット履歴を取ってくる。(Pageを解釈するのはちょっと難しかったので置いといて...)

  1. wgetでcommitsをJSONで受け取る
  2. jqでjsonのauthor nameを抽出
  3. 上で実行した2以降の処理をする
$ wget -O - -q https://api.github.com/repos/tensorflow/tensorflow/commits?per_page=100 | jq '.[] | .commit.author.name' | sort | uniq -c | sort -nr | head
  16 "A. Unique TensorFlower"
   8 "TensorFlower Gardener"
   5 "Gunhan Gulsoy"
   4 "Yujing Zhang"
   3 "Zhenyu Tan"
   3 "Peter Hawkins"
   3 "Andy Ly"
   2 "tg-at-google"
   2 "Thomas O'Malley"
   2 "T.J. Alumbaugh"

以下のブログにインスパイアされてやってみました。

prithu.xyz