映画レビューの感情をAIディープラーニングで分析してみた

自分が見たい映画作品を探すとき、レビューを参考にしたことはありますか？

「感動する作品かな？」「なんだか退屈そう…」などいろんな意見を参考にすると思います。そんなとき、AIでレビューの感情を分析できたら便利だと思いませんか？ポジティブな感想やネガティブな意見が簡単に見える化されれば、次に見る映画をもっとスムーズに決められるかもしれません。

今回は、そんな**映画レビューの感情をディープラーニングで分析**する手法を実際にコードを交えながら簡単に紹介します。

----

ちなみに今回使用する「LSTMモデル」は前回、「[AIモデルを構築して日経平均株価を予測してみた](https://qiita.com/kinopy513/items/7ec37b9bab24d6e20192)」という記事でも使用しているので、興味ある方はそちらもご覧ください。

https://qiita.com/kinopy513/items/7ec37b9bab24d6e20192

----

## 今回のゴール

例えば、"I cried many times while watching this film. I could relate to the growth and emotional changes of the characters and it was a memorable film."
（この映画を見ながら何度も泣いた。登場人物の成長や感情の変化に共感できたし、心に残る映画だった。）
というポジティブなレビューをAIに分析させ最終的に、
```python:out
判定結果: 
    感情: Positive(ポジティブ)
    信頼度: 54.12%
```
という結果を出すことが目標です。

※今回はデータセット（AIモデルに学習させるデータ）が英語なので、分析する文章も英文を対象にしています


## ディープラーニングとは

**ディープラーニング**は、AI（人工知能）の一分野で、人間の脳の仕組みをまねした「ニューラルネットワーク」を使ってデータを学習し、複雑な問題を解決する技術です。ざっくり言うと、ニューラルネットワークを複雑にしたものです。

最近はAIブームなので、なんとなくどこかでこのような単語を聞いたことある方は多いと思います。
現在では画像認識や音声認識、文章の感情分析など多岐の用途に利用され、大量のデータから自動でパターンを学習することで、高精度で予測や分類ができます。

ここでは詳しい説明は省きますが、簡単にいうと機械学習を用いてデータを分析する手法の一つがディープラーニングです。


## 感情分析とは？

そもそも今回のテーマでもある「感情分析」ってなんぞやという話ですよね。
感情分析は、文章に含まれるポジティブやネガティブといった感情を文章から読み取る作業です。たとえば、以下のような映画レビューを見てみましょう。

-   **ポジティブなレビュー例**  
    「最高に楽しかった！ストーリーもキャラクターも大好き！」  
    → ポジティブ
    
-   **ネガティブなレビュー例**  
    「期待してたけど退屈だった。ストーリーも薄っぺらい。」  
    → ネガティブ
    
AIにこれらの文章を与えて、感情を数値化し、ポジティブかネガティブかを判断させます。

<br />

----

それでは早速分析を開始していきましょう。

### 1. 使用するデータセット

今回使用するデータセットは**IMDbデータセット**です。これには25,000件の映画レビューとその感情ラベル（ネガティブを表す0 / ポジティブを表す1）が含まれています。


### 2. モデルの構築の概要

今回の感情分析には、**LSTM（Long Short-Term Memory）** というディープラーニングモデルを使います。LSTMは文章の文脈を理解するのが得意なモデルで、過去の単語情報を活かして解析します。

ディープラーニングでは、処理を段階的に行うために「レイヤー（層）」という構造を使用します。今回の感情分析モデルは、以下のようなレイヤーで構成されています。

1.  **Embeddingレイヤー**: 単語をベクトル（数値の配列）に変換
2.  **LSTMレイヤー**: 文脈情報を捉える
3.  **Denseレイヤー**: 感情をポジティブかネガティブに分類

![ディープラーニング図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843712/3df9a609-78e4-66de-5a0f-d020ba245084.png)

こちらの画像を見たときに左から右に処理が流れていて、
一つの要素がいろんな要素に線が伸びて影響していることがわかれば今回は問題ないです。ちなみに、この縦に並んだ列をそれぞれレイヤー（層）といいます。

いまいちわからないという方もあまり深く考えずに、最初にEmbeddingレイヤーでデータが処理されて、そのデータを次にLSTMレイヤーに渡して処理がされ、最後にDenseレイヤーにまたその処理結果を渡してそのレビューがポジティブなのかネガティブなのかといった答えを出力するといった流れだけなんとなく押さえてもらえれば大丈夫です。
※この図ではわかりやすさのためにバイアスは省いています


## 感情分析モデルの構築

ここからは、いよいよ感情分析モデルを構築していきます。
まずはじめに、必要なライブラリをインポートします。

```python
import numpy as np
import tensorflow as tf
from sklearn.model_selection import KFold
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.datasets import imdb
```


## IMDbデータセットの読み込み

今回使うレビューデータのデータセットを訓練用とテスト用に分割して読み込みます。

```python
vocab_size = 10000
max_length = 100  # 最大の文章の長さ
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=vocab_size)
```

`num_words=10000`は、出現頻度が上位10000の単語のみのデータを読み込む指定です。
実際に中身を確認してみます。

```python
x_train
```

```python:out
array([list([1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32])...,
       list([1, 11, 6, 230, 245, 6401, 9, 6, 1225, 446, 2, 45, 2174, 84, 8322, 4007, 21, 4, 912, 84, 2, 325, 725, 134, 2, 1715, 84, 5, 36, 28, 57, 1099, 21, 8, 140, 8, 703, 5, 2, 84, 56, 18, 1644, 14, 9, 31, 7, 4, 9406, 1209, 2295, 2, 1008, 18, 6, 20, 207, 110, 563, 12, 8, 2901, 2, 8, 97, 6, 20, 53, 4767, 74, 4, 460, 364, 1273, 29, 270, 11, 960, 108, 45, 40, 29, 2961, 395, 11, 6, 4065, 500, 7, 2, 89, 364, 70, 29, 140, 4, 64, 4780, 11, 4, 2678, 26, 178, 4, 529, 443, 2, 5, 27, 710, 117, 2, 8123, 165, 47, 84, 37, 131, 818, 14, 595, 10, 10, 61, 1242, 1209, 10, 10, 288, 2260, 1702, 34, 2901, 2, 4, 65, 496, 4, 231, 7, 790, 5, 6, 320, 234, 2766, 234, 1119, 1574, 7, 496, 4, 139, 929, 2901, 2, 7750, 5, 4241, 18, 4, 8497, 2, 250, 11, 1818, 7561, 4, 4217, 5408, 747, 1115, 372, 1890, 1006, 541, 9303, 7, 4, 59, 2, 4, 3586, 2]),
       list([1, 1446, 7079, 69, 72, 3305, 13, 610, 930, 8, 12, 582, 23, 5, 16, 484, 685, 54, 349, 11, 4120, 2959, 45, 58, 1466, 13, 197, 12, 16, 43, 23, 2, 5, 62, 30, 145, 402, 11, 4131, 51, 575, 32, 61, 369, 71, 66, 770, 12, 1054, 75, 100, 2198, 8, 4, 105, 37, 69, 147, 712, 75, 3543, 44, 257, 390, 5, 69, 263, 514, 105, 50, 286, 1814, 23, 4, 123, 13, 161, 40, 5, 421, 4, 116, 16, 897, 13, 2, 40, 319, 5872, 112, 6700, 11, 4803, 121, 25, 70, 3468, 4, 719, 3798, 13, 18, 31, 62, 40, 8, 7200, 4, 2, 7, 14, 123, 5, 942, 25, 8, 721, 12, 145, 5, 202, 12, 160, 580, 202, 12, 6, 52, 58, 2, 92, 401, 728, 12, 39, 14, 251, 8, 15, 251, 5, 2, 12, 38, 84, 80, 124, 12, 9, 23])],
      dtype=object)
```

中身を確認すると、すでに文章が数値にエンコーディングされていることがわかります。
y_trainの目的変数も見てみましょう。

```python
y_train
```

```python:out
array([1, 0, 0, ..., 0, 1, 0])
```

中身には0 or 1 の数値が格納されています。
これは、0: ネガティブ, 1: ポジティブをそれぞれ表しており、2値をとるラベルです。


## データセットの確認

データの形状を確認します。
```python
print(f"x_train, y_trainの行列数確認: {x_train.shape, y_train.shape}")
print(f"x_test, y_testの行列数確認: {x_test.shape, y_test.shape}")
```

今回は訓練用データもテストデータも25000データあるので、それぞれ25000行ずつあることが確認できます。

```python:out
x_train, y_trainの行列数確認: ((25000,), (25000,))
x_test, y_testの行列数確認: ((25000,), (25000,))
```

データ型も念のため確認します。

```python
print(type(x_train))
print(type(y_train))
```

```python:out
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>
```

今回は、ndarray型になっています。


## 単語インデックスの取得とデコード

ここで、レビューを人間が読める形に変換しています。

```python
# 単語インデックスの取得
word_index = imdb.get_word_index()

# 単語インデックスの逆順を作成
reverse_word_index = {value: key for key, value in word_index.items()}

# 最初のレビューの復元
decoded_text = " ".join([reverse_word_index.get(i - 3, "?") for i in x_train[5]])
```

試しに5番目のデータの中身を確認してみます。
```python
decoded_text
```
英文で文章が格納されていることが確認できます。
```python:out
"? begins better than it ends funny that the russian submarine crew ? all other actors it's like those scenes where documentary shots br br spoiler part the message ? was contrary to the whole story it just does not ? br br"
```

## データの前処理（シーケンスの長さを揃える）

それぞれのレビューでは単語数が違い文章の長さがバラバラなので、
最も長い文章に合わせて、短い文章は空の部分に0埋めをし、全てのレビューが同じ長さになるように調整します。

```python
x_train = pad_sequences(x_train, maxlen=max_length)
x_test = pad_sequences(x_test, maxlen=max_length)
```


## モデルを定義する関数

LSTMモデルを定義します。Embeddingレイヤーで単語を数値に変換し、LSTMレイヤーで文脈を理解し、最後にDenseレイヤーで感情を分類します。

```python
def create_model():
    model = Sequential([
        Embedding(vocab_size, 32, input_length=max_length),
        LSTM(64, dropout=0.3, recurrent_dropout=0.3),
        Dense(1, activation='sigmoid')
    ])
    model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
    return model
```

![ディープラーニング図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843712/7ba48d45-a715-6286-3503-9c6a8afd6ba7.png)

## クロスバリデーション設定

ここでK-fold Cross Validationを設定します。

```python
kf = KFold(n_splits=5, shuffle=True, random_state=42)
```

![k-fold cv図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843712/9b4fb7b5-a726-1f6c-5a76-4cbf70ae5591.png)

今回は訓練用データを5分割し、１回目は最初の5分の1を検証用データに、後を学習データに、２回目は次の5分の1をテストデータに、後を学習データに.....といった感じで、学習を5回繰り返します。
これをすることで、データの偏り（最後の部分に不正解のデータが多かったなど）を気にせずに全体のデータを満遍なく学習に使うことができるので、学習モデルの精度が上がることが多いです。今回のようなディープラーニング以外の機械学習モデルでもよく使われます。


## クロスバリデーションによる評価

学習モデルの訓練と評価を行います。
今回は損失率（loss）と正答率（accuracy）で評価をします。

```python
accuracies = []
best_model = None
best_accuracy = 0

for train_index, val_index in kf.split(x_train):
    X_train, X_val = x_train[train_index], x_train[val_index]
    y_train_fold, y_val_fold = y_train[train_index], y_train[val_index]

    # モデル作成
    model = create_model()

    # Early stoppingによる過学習防止
    early_stopping = EarlyStopping(monitor='val_loss', patience=2, restore_best_weights=True)

    # モデルの訓練
    model.fit(X_train, y_train_fold, epochs=5, batch_size=64, validation_data=(X_val, y_val_fold), callbacks=[early_stopping])

    # 評価
    loss, accuracy = model.evaluate(X_val, y_val_fold)
    accuracies.append(accuracy)

    if accuracy > best_accuracy:
        best_accuracy = accuracy
        best_model = model
```

結果、このような形で評価結果が出力されます。
※実際には５回学習を繰り返すので、これが5回同じような感じで出力されます

```python:out
Epoch 1/5
313/313 ━━━━━━━━━━━━━━━━━━━━ 8s 23ms/step - accuracy: 0.6456 - loss: 0.6243 - val_accuracy: 0.8220 - val_loss: 0.3985
Epoch 2/5
313/313 ━━━━━━━━━━━━━━━━━━━━ 7s 23ms/step - accuracy: 0.8463 - loss: 0.3681 - val_accuracy: 0.8182 - val_loss: 0.3983
Epoch 3/5
313/313 ━━━━━━━━━━━━━━━━━━━━ 7s 23ms/step - accuracy: 0.8767 - loss: 0.3082 - val_accuracy: 0.8366 - val_loss: 0.3840
Epoch 4/5
313/313 ━━━━━━━━━━━━━━━━━━━━ 7s 23ms/step - accuracy: 0.8920 - loss: 0.2798 - val_accuracy: 0.8288 - val_loss: 0.4109
Epoch 5/5
313/313 ━━━━━━━━━━━━━━━━━━━━ 7s 23ms/step - accuracy: 0.9014 - loss: 0.2534 - val_accuracy: 0.8276 - val_loss: 0.4094
```

  
## クロスバリデーション結果

```python
print(f"Cross-Validation Accuracy: {np.mean(accuracies):.4f}")
```

```python:out
Cross-Validation Accuracy: 0.8309
```

今回の場合、クロスバリデーションの結果約83%の精度となりました。


## テストデータで最終評価

最後にテストデータを使って、未知のデータにどれだけの精度が出るかを評価します。

```python
final_loss, final_accuracy = best_model.evaluate(x_test, y_test)
print(f"Test Accuracy with Best Model: {final_accuracy:.4f}")
```

```python: out
Test Accuracy with Best Model: 0.8342
```

このモデルでは、映画レビューの感情を約83%の精度で分類することができ、学習用データの精度とほとんど差異がなく使えていそうです！

それでは実際にレビューの感情を分析してみましょう。

## 任意のテキストを入力して感情分析を行う関数
```python
def predict_sentiment(text, tokenizer, max_length=100):
    # テキストをトークン化
    sequences = tokenizer.texts_to_sequences([text])
    
    # パディングを適用してシーケンスの長さを揃える
    padded_sequence = pad_sequences(sequences, maxlen=max_length)
    
    # 感情予測
    prediction = model.predict(padded_sequence)
    
    # 予測結果に基づいて感情を判定
    sentiment = 'Positive' if prediction >= 0.5 else 'Negative'
    confidence = prediction[0][0] * 100  # 予測の確信度（%）
    
    return sentiment, confidence
```

## トークナイザーを作成（IMDBデータセットの語彙と同じ設定にする
```python
from tensorflow.keras.preprocessing.text import Tokenizer

word_index = imdb.get_word_index()
reverse_word_index = {value: key for key, value in word_index.items()}
tokenizer = Tokenizer(num_words=10000)
tokenizer.fit_on_texts([' '.join([reverse_word_index.get(i - 3, "?") for i in x]) for x in x_train])
```

## 任意のテキストを入力して感情分析
今回は架空のレビューとして、テキストを5パターン用意しました。
上から順に、
- ポジティブな意見
- ネガティブな意見
- ポジティブな意見
- ネガティブな意見
- ニュートラル（中立に近い）な意見

という想定です。

```python
texts = [
    "I cried many times while watching this film. I could relate to the growth and emotional changes of the characters and it was a memorable film.",
    "This movie was a total letdown. The plot was boring and predictable, and the characters were one-dimensional. I couldn't connect with anything in this film.",
    "This movie was an incredible experience. The story was unique and emotionally impactful, and the performances were outstanding. It kept me hooked from beginning to end.",
    "I was bored throughout the entire film. The plot was weak, and the characters lacked depth. It felt like a missed opportunity to tell a compelling story.",
    "The movie was decent. The story was straightforward and the acting was fine, but there was nothing particularly remarkable about it."
]
```

```python
for text in texts:
    sentiment, confidence = predict_sentiment(text, tokenizer)
    print(f"文章: \"{text}\" | Sentiment: {sentiment} | Confidence: {confidence:.2f}%")
```

```python:out
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step
文章: "I cried many times while watching this film. I could relate to the growth and emotional changes of the characters and it was a memorable film." | Sentiment: Positive | Confidence: 81.72%
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step
文章: "This movie was a total letdown. The plot was boring and predictable, and the characters were one-dimensional. I couldn't connect with anything in this film." | Sentiment: Negative | Confidence: 45.05%
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step
文章: "This movie was an incredible experience. The story was unique and emotionally impactful, and the performances were outstanding. It kept me hooked from beginning to end." | Sentiment: Negative | Confidence: 10.46%
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step
文章: "I was bored throughout the entire film. The plot was weak, and the characters lacked depth. It felt like a missed opportunity to tell a compelling story." | Sentiment: Negative | Confidence: 47.83%
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step
文章: "The movie was decent. The story was straightforward and the acting was fine, but there was nothing particularly remarkable about it." | Sentiment: Negative | Confidence: 14.38%
```


## 分析結果

-   **レビュー例1（ポジティブ）**  
　"I cried many times while watching this film. I could relate to the growth and emotional changes of the characters and it was a memorable film."
    「この映画を見ながら何度も泣いた。登場人物の成長や感情の変化に共感できたし、心に残る映画だった。」  
    → 判定結果: 
　　**感情: Positive（ポジティブ）**
　　**信頼度: 81.72%**
    → 結果: **正解！**

<br />

-   **レビュー例2（ネガティブ）**  
　"This movie was a total letdown. The plot was boring and predictable, and the characters were one-dimensional. I couldn't connect with anything in this film."
    「この映画は期待はずれだった。筋書きは退屈で予想がつき、登場人物は一面的だった。この映画では何も感情移入できなかった。」  
    → 判定結果: 
　　　**感情: Negative（ネガティブ）**
　　　**信頼度: 45.05%**
    → 結果: **正解！**
    
<br />

-   **レビュー例3（ポジティブ）**  
　"This movie was an incredible experience. The story was unique and emotionally impactful, and the performances were outstanding. It kept me hooked from beginning to end."
　「この映画は信じられない体験だった。ストーリーはユニークで感情移入でき、演技も素晴らしかった。最初から最後まで夢中にさせられた」
    → 判定結果: 
　　　**感情: Negative（ネガティブ）**
　　　**信頼度: 10.46%**
    → 結果: **不正解...**
    
<br />

-   **レビュー例4（ネガティブ）**  
　"I was bored throughout the entire film. The plot was weak, and the characters lacked depth. It felt like a missed opportunity to tell a compelling story."
　「全編を通して退屈だった。プロットが弱く、登場人物に深みがなかった。説得力のあるストーリーを語る機会を逃した感じだ。」
    → 判定結果: 
　　　**感情: Negative（ネガティブ）**
　　　**信頼度: 47.83%**
    → 結果: **正解！**

<br />
     
-   **レビュー例5（ニュートラル寄り）**  
　"The movie was decent. The story was straightforward and the acting was fine, but there was nothing particularly remarkable about it."
    「映画はまともだった。ストーリーは分かりやすく、演技も良かったが、特に目立ったところはなかった。」  
　→ 判定結果: 
　　　**感情: Negative（ネガティブ）**
　　　**信頼度: 14.38%**
　→結果: ニュートラル寄りのレビューですが、主観的にはややネガティブかと思うので**正解！**

全体では 4 / 5 が実際に正解となりました。
テストデータの精度が大体83%だったので、それとほとんどズレがないくらいで実際に使えています。


## まとめ

今回は、ディープラーニングを使って映画レビューの感情分析を試みました。このような技術は映画だけでなく、商品レビューやSNS投稿の分析、またカスタマーアンケート結果など、さまざまな分野に応用することができます。

精度自体も例えばGridSearchなどでハイパーパラメーターチューニングをしたり、違うモデルを使ってみたりなど工夫次第でもっと高められると思います。
また今回は使わなかったですが、BERTなどの学習済みモデルを使用するのも面白そうですね。

初学者でも、簡単なコードからディープラーニングは始めることができます。ぜひこの記事や他いろいろなコードを参考に、自分だけの感情分析モデルを構築して分析してみてください。


## 採用拡大中！

アシストエンジニアリングでは一緒に働くフロントエンド、バックエンドのエンジニア仲間を大募集しています！
少しでも興味ある方は、カジュアル面談からでもぜひお気軽にお話ししましょう！

お問い合わせはこちらから↓
[https://official.assisteng.co.jp/contact/](https://official.assisteng.co.jp/contact/)