AIは青春小説の甘酸っぱさを可視化できるのか？

「感情分析って、どのくらい使えるものなの…？」

そんな疑問から、
「感情分析」を使って「青春小説の甘酸っぱさ」を可視化できるのか、
青空文庫で無料で読める「三四郎」[^1]を題材に、
試してみたいと思います。

先に謝っておきます。特にAIらしいAIは出てこないです、すみません。

# 「三四郎」
夏目漱石の代表作の一つ。青春小説。
登場人物とあらすじを、ものすごく簡単に、説明しておきます。

- 登場人物
    - 三四郎：主人公。東京の大学に通うため熊本から上京。美禰子に恋する。
    - 野々宮：恋敵的存在。三四郎の故郷の知り合いの従弟。スタイルが良い。
    - 美禰子：ヒロイン。兄が野々宮と同級生、という繋がり。色白美人。
- あらすじ
    - 熊本の高校を出て東京帝国大学に入学し、東京で新しい生活を始める、三四郎。
    - 大学生活では、野々宮を含む様々な人と出会い、交友関係を広げていく。
    - ある時、大学構内の池で、美禰子を見かけて一目惚れ。次第に親しくなっていく。
    - しかし、美禰子は野々宮を気にかけているようでもあり、三四郎の気持ちは不安定に。
    - 結局、美禰子はとある男性と結婚すると人伝に聞き、三四郎の恋は儚く終わる。


# 感情分析ライブラリ ML-Ask
ここからが本題です。

今回は、感情分析に「[ML-Ask](http://arakilab.media.eng.hokudai.ac.jp/~ptaszynski/repository/mlask.htm)」というライブラリを使用することにしました。

感情分析というと、[東北大学の乾・鈴木研究室の「日本語評価極性辞書」](http://www.cl.ecei.tohoku.ac.jp/Open_Resources-Japanese_Sentiment_Polarity_Dictionary.html)や[東工大の高村教授が公開している「単語感情極性対応表」](http://www.lr.pi.titech.ac.jp/~takamura/pndic_ja.html)が有名なようです。また、「Google Natural Language API」や「Amazon Comprehend」という選択肢もあります。[^2]

ただし、これらは、所謂「ポジネガ分析」というもので、基本的には、入力の文章がポジティブかネガティブかを判定するものです。

「青春小説の甘酸っぱさ」を可視化したいので、気持ちの昂りや、失恋の苦々しさ、などの感情を分析できるといいなぁと考えました。
そこで、分析できる感情の種類が多い「ML-Ask」の使用してみます。

## ML-Ask とは？
[Michal Ptaszynski氏が公開しているライブラリ](http://arakilab.media.eng.hokudai.ac.jp/~ptaszynski/repository/mlask.htm)です。
"eMotive eLement and expression Analysis System"の略だそう（kは何処へ…）

約2000語の感情表現辞書を用いた、パターンマッチングによる方式です。

{喜, 怒, 哀, 怖, 恥, 好, 厭, 昂, 安, 驚}の10種類の感情を推定します。
これだけの種類で感情を分析してくれるのはすごいですよね！

また、Contextual Valence Shifters (CVS)という概念を実装しており、否定語や強調の度合いを考慮して分析してくれます。
感情表現辞書を用いて自分で感情分析をしようとすると、否定語を別途考慮する必要がありますが、ML-Askはそれも含めて分析してくれるのでお手軽です。
ちなみに、この機能も辞書型で実現されているようです。

さらに、「ラッセルの円環モデル」に基づいた形式でも感情を推定してくれます。
「ラッセルの円環モデル」とは、感情を「快」と「覚醒」の二軸からなる二次元空間にマッピングしたものです。例えば下の図によると、「安心」は `(快,覚醒)=(0.8,0.5)` ぐらいの位置にマッピングされていますね。（ML-Askは、各軸５段階で出力してくれます。）

（図）ラッセルの円環モデル と 感情のマッピング[^3]
![ラッセルの円環モデル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612472/8d15f92d-7f8d-e451-a2d4-390389e9acee.png)

分析の幅が広くて、ワクワクしてきます。

## ML-Ask の使い方
pythonライブラリとして実装してくれている方がいます。
https://github.com/ikegami-yukino/pymlask

Google Colaboratoryで簡単に使うことができます。

```
!pip install pymlask
!apt install aptitude
!aptitude install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file -y
!pip install mecab-python3==0.7
```

```python
from mlask import MLAsk
emotion_analyzer = MLAsk()
emotion_analyzer.analyze('美禰子と三四郎は声を出して笑った')

# 出力
{'activation': 'NEUTRAL',
 'emoticon': None,
 'emotion': defaultdict(list, {'yorokobi': ['笑う']}),
 'intensifier': {},
 'intension': 0,
 'orientation': 'POSITIVE',
 'representative': ('yorokobi', ['笑う']),
 'text': '美禰子と三四郎は声を出して笑った。'}
```

`'representative'`が、その文章の主たる感情を表しています。
この文章では、「笑う」という単語に基づいて、「喜」と判定されています。

また、`'orientation'`と`'activation'`が、ラッセルの円環モデルの二軸（快、覚醒）に相当するものです。

# 青春小説の甘酸っぱさを、可視化できるか？
Google Colaboratoryのコードはこちら↓

http://colab.research.google.com/github/izaki-shin/notebooks/blob/master/nlp/sentiment_analysis_sanshiro.ipynb

以下の３ステップで、可視化を試してみます。
1. 青空文庫から三四郎をダウンロードする
1. 一文ずつ感情分析を適用する
1. 全13章で章ごとに結果を集計する

## 青空文庫から三四郎をダウンロードする
こちら↓を参考にさせてもらいました。ここでは省略します。
[15分でできる日本語Word2Vec](https://qiita.com/makaishi2/items/63b7986f6da93dc55edd)

## 一文すつ感情分析を適用する
全ての文に一文ずつ、感情分析を適用していきます。
そして、結果をpandas.DataFrameに格納します。

感情表現が含まれない文章は不要なので、この段階で除外します。

```python
import pandas as pd
from mlask import MLAsk

emotion_analyzer = MLAsk()

# sentences は、本文の一文毎の list
data_list = []
for sent_idx, sentence in enumerate(sentences):
    res = emotion_analyzer.analyze(sentence)
    if res['emotion'] is not None:  # 感情表現を含む場合のみ
        data_list.append([
            sent_idx,
            get_chapter_id(sent_idx),  # 文番号から章番号を算出する自作関数
            res['representative'][0],  # 主たる感情
            res['activation'],         # ラッセルの円環モデルの「覚醒」
            res['orientation'],        # ラッセルの円環モデルの「快」
            res['text']                # 分析対象の文
        ])
df_emo = pd.DataFrame(data_list, columns=['sentence_id', 'chapter_id', 'emotion', 'activation', 'orientation', 'text'])
```

## 感情の起伏を可視化する
全13章の章ごとに、感情分析の結果（`'representative'`）を集計します。

集計方法は、シンプルに、各感情の出現回数をカウントしてみます。
ただし、各章で文章数が異なるので、100文毎の出現回数に正規化します。

Google Colaboratoryで日本語を含む図を書く方法は[こちら](https://blog.3qe.us/entry/2018/08/16/121457)を参考にしました。

```python
# 各章の文章数をカウント
chapter_num = len(chapter_start_idxs)
chapter_sent_nums = [chapter_start_idxs[i+1] - chapter_start_idxs[i] for i in range(0, chapter_num-1)]
chapter_sent_nums.append(len(sentences) - chapter_start_idxs[chapter_num-1])

# 感情ラベル 10種
emotion_labels = {'ikari': '怒', 'suki': '好', 'haji': '恥', 'iya': '厭', 'kowa': '怖', 'aware': '哀', 'odoroki': '驚', 'yasu': '安', 'takaburi': '昴', 'yorokobi': '喜'}

chapter_ids = range(1, chapter_num+1)

plt.figure(figsize=(12, 4))
fontsize = 14
linewidth = 3

for label, label_jp in emotion_labels.items():
    # 対象の感情の出現回数を、章ごとにカウント
    df_target = df_emo[df_emo.emotion == label]
    cnts = [(df_target.chapter_id == ci).sum() for ci in chapter_ids]
    # 各章の文章数で正規化
    cnts_norm = [cnts[ci-1] / chapter_sent_nums[ci-1] * 100 for ci in chapter_ids]
    plt.plot(chapter_ids, cnts_norm, 'o-', label=label_jp, linewidth=linewidth, alpha=0.7)

plt.xlabel('章番号', fontsize=fontsize)
plt.ylabel('出現回数（100文毎）', fontsize=fontsize)
plt.title('感情の起伏', fontsize=fontsize)
plt.legend(fontsize=fontsize)
plt.tick_params(labelsize=fontsize)
plt.tight_layout()
```
![三四郎の感情の起伏_v2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612472/a2db61d1-ee36-fcb5-ae54-0ecf59b1c4fa.png)
結果を見ていきます。
### 全体
「喜」と「厭」の出現回数が特に多いです。
他にも、「驚」「哀」「安」も、章によって出現回数の差が見て取れそうです。

青春小説の甘酸っぱさという観点では、「好」「昂」「恥」に期待していました。
差がない訳ではないですが、他と比べるとそもそも出現回数が低いようです。

感情の起伏という観点で、「喜」と「厭」を中心に見てみると、序盤は比較的ニュートラルですが、中盤で少し「喜」が高まって優位になっています。しかし、その後「喜」は低下し、それと相反するように「厭」が高まり、そのまま終盤まで続いていくように見えます。

### 各章
#### 第2章 驚
第1章で熊本から電車で東京までやってきて、第2章は東京や大学という世界に足を踏み入れる場面です。特に、東京の様子に三四郎はとても驚いており、それがグラフでも見て取れます。

#### 第6章 喜
第5章の後半で、三四郎たちは菊人形を見にいくのですが、三四郎と美禰子の二人は集団からはぐれてしまい、しばらく二人で時を過ごします。その後、第6章の中で、美禰子から絵葉書が届き、その絵が、先日の二人の状況を彷彿とさせるものだったため、三四郎は嬉しくなります。
美禰子との距離が少し近づいた、三四郎の喜びが可視化できているのかもしれません。

#### 第8章 厭
三四郎は、美禰子から絵画展に誘われ一緒に行くのですが、そこで野々宮と鉢合わせます。美穪子は、野々宮の姿を見ると、三四郎耳に口を寄せて何かを囁くのですが、これは野々宮に見せつけるための行為のようでした。美穪子の野々宮を愚弄するような行為や、当て馬のようににされたことに対して、三四郎は複雑な気持ちになります。
この時の、三四郎の美禰子に対する嫌悪感がグラフに表れているように見えます。


## ラッセルの円環モデルも見てみる
ML-Askの出力の`'orientation'`と`'actiation'`を用いて、ラッセルの円環モデル的な可視化もしてみます。
上で見た、３つの章について出力します。

```python
import numpy as np

# 今回は簡単のため、mostly_* は * に変換
df_emo = df_emo.replace({f'mostly_{s}': s for s in ('POSITIVE', 'NEGATIVE', 'ACTIVE', 'PASSIVE')})

def plot_polar(labels, values, title):
    """レーダーチャートを描画する.
    cf. https://qiita.com/e10persona/items/7a7643b266c2bdfbf7d0
    """
    angles = np.linspace(0, 2 * np.pi, len(labels) + 1, endpoint=True)
    values = np.concatenate((values, [values[0]]))  # 閉じた多角形にする
    fig = plt.figure(tight_layout=True)
    ax = fig.add_subplot(111, polar=True)
    ax.plot(angles, values, 'o-')  # 外枠
    ax.fill(angles, values, alpha=0.25)  # 塗りつぶし
    ax.set_thetagrids(angles[:-1] * 180 / np.pi, labels, fontsize=15)  # 軸ラベル
    ax.set_rlim(0 ,max(values))
    ax.set_title(title, fontsize=15)

def draw_russell_circumplex(chapter_id):
    """ラッセルの円環モデルを、レーダーチャートで描画."""
    df_target = df_emo[df_emo.chapter_id == chapter_id]

    sets = (
        ('POSITIVE', 'NEUTRAL'),
        ('POSITIVE', 'ACTIVE'),
        ('NEUTRAL', 'ACTIVE'),
        ('NEGATIVE', 'ACTIVE'),
        ('NEGATIVE', 'NEUTRAL'),
        ('NEGATIVE', 'PASSIVE'),
        ('NEUTRAL', 'PASSIVE'),
        ('POSITIVE', 'PASSIVE'),
    )
    labels = ('positive', '', 'active', '', 'nagative', '', 'passive', '')

    values = [0] * 8
    for ori_act in df_target[['orientation', 'activation']].values:
        if tuple(ori_act) == ('NEUTRAL', 'NEUTRAL'):  # 今回は無視
            continue
        values[sets.index(tuple(ori_act))] += 1

    plot_polar(labels, values, f'{chapter_id} 章\n')

# 描画 （2章、6章、8章）
draw_russell_circumplex(chapter_id=2)
draw_russell_circumplex(chapter_id=6)
draw_russell_circumplex(chapter_id=8)
```
![ラッセル円環_ch2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612472/18ea533e-8653-80fd-8768-3347a0165f52.png)
![ラッセル円環_ch6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612472/319465ab-f1c8-c6f0-1c2b-d85a87e9711c.png)
![ラッセル円環_ch8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612472/3dcd2180-ff1a-932a-491f-93c8b06a94a1.png)

# 結論
感情の起伏はそれらしく可視化されたように見えるが、「甘酸っぱさ」感は…。
というところでしょうか。

中盤で「喜」が高まるも、その後「厭」が優位になっていく感じは、甘酸っぱい恋の感じを表現していると言えなくはないかとも思います。
一方で、これだけだと「ポジネガ分析」でも可視化できそうなので、ML-Askのメリットはあまり感じられません。

その意味では、第２章の三四郎の驚きが、「驚」として可視化できたことはML-Askならではと思います。（記事の中で取り上げていないですが、「哀」や「安」も、物語の内容をちゃんと反映しているように見えます。）

# あとがき
ここまで読んでくださって、こんな疑問を感じた方もいるのではないでしょうか？

「可視化してるの、三四郎の感情だけじゃなくない？」

はい、その通りです。
本記事では全ての文章に感情分析を適用し、その結果を用いて可視化しました。
そのため、当然ながら、他の登場人物の発言や行動に対する感情も含まれています。

「三四郎」が主語の文章のみを取り出して感情分析を実施するというのも試したのですが、数が少なかったので断念しました。（全部で164文、全体の2%。こちらは、Google Colabにおまけとして載せています。）
主語を省略可能な日本語って、困りものですね…。省略された主語を補完する、照応分析なるものもあるみたいですが、今回は試していません。

ただ、三四郎の主観描写メインの小説でもあるため、全文の解析でも最低限の傾向は見えるかなと思って、今回は全文を対象にしました。
とは言え、やはりノイズが含まれるので、三四郎の感情とは関係なくグラフが上下している部分も多いなぁという印象です。（例えば、第5章の「安」は、話題として「気楽」や「親切」が上がっていたため、グラフで高く見えています。）

粗々な分析ではありましたが、感情分析のレベル感や課題を知ることができて、個人的には楽しかったです。
また、単純なポジネガ分析では出てこない部分も可視化できることが確認できたのも良かったです。

最後までお読みいただき、ありがとうございました。
それでは！ (*ˊᗜˋ)ﾉｼ

[^1]: [青空文庫 「三四郎」](https://www.aozora.gr.jp/cards/000148/files/794_14946.html)
[^2]: [【ML-Ask】感情分析AIは実際に使えるのか検証してみる](https://gaaaon.jp/blog/mlask)
[^3]: [情動評価のためのラッセルの円環モデルに基づく感情重心推定手法の提案](https://www.jstage.jst.go.jp/article/jjske/advpub/0/advpub_TJSKE-D-18-00069/_pdf/-char/ja)