character-level CNNでTwitter陰キャを見つける

Twitterのタイムラインを眺めていると，同じ学科の人たちが陰キャ，陽キャで話題になっていたので，ツイートをもとに陰キャツイート，陽キャツイートに分類する分類器を作り，自分が陰キャかどうかを調べてみた．

# はじめに
息抜きで陰キャ判定器を作ろうかとおもっていて，研究で画像を処理する関係上，CNNを自然言語処理で使えたらなぁ...と思いながら調べていたら，ちょうどいい手法，character-level CNNを見つけた．これで分類器を作ってみた．

## character-level CNN
自然言語処理でDeepLearningを使う手法は，LSTMなどが挙げられるが，今回は画像処理のモデルであるCNNを使う．最大の特徴は，<b>分かち書きが要らない</b>ところ．簡単に言うと，「文字にはUnicode符号位置というものが割り振られてるし，この数値を画素と考えると，文章は画像になるじゃん．画像ならCNNにかけれるよね！」といったところ．[character-level CNNでクリスマスを生き抜く](https://qiita.com/bokeneko/items/c0f0ce60a998304400c8)を参考にしました．

モデルに関する処理の方法は以下の通り，
1. ツイートを文字の配列に分解
2. 各文字をUnicode符号位置に変換
3. 固定長配列にする
4. Embedding(Kerasのレイヤー)でUnicode座標位置の数値をベクトルの配列にする
5. ベクトル列をCNNにかける
6. 全結合層(Dense)を通して分類結果を返す

# 実装
今回の実装は，チェック後GitHubに載せる予定．
(2018/05/28追記） https://github.com/ymmtr6/character-level-cnn-twitter

##環境
+ python3.6
+ tensorflow 1.5
+ keras 2.1.4

##方針
今回陰キャツイートを見つけるためにの流れは以下のようになる．
1. 教師データとして陰キャ，陽キャのツイートを集める．
2. tweetをUnicode符号位置の配列に変換する
3. 教師値を元に分類器（モデル）を作る
4. 自分のツイートを分類器にかけて，インキャかどうかを判定する

##教師データ
まず，(独自の判断で)陰キャと陽キャのTwitterアカウントを集めてくる．これらのアカウントがつぶやくツイートが教師データとする．ツイッターの情報を取得するのにTwitterAPIを利用する．TwitterAPIを利用するのに，OAuth認証が必要だが，ここはrequests-oauthlibを利用した．ここでは紹介を省略する．
詳しくは， [PythonでTwitterから情報収集(TwitterAPI編)](https://qiita.com/Salinger/items/020b670466a9835c94bb)を参照．
データを\[アカウント名\\tツイートのテキストデータ\]といった形（tsv形式）で保存しておく．

##tweetをUnicode符号位置に変換
ツイートを集めたら,ツイートのテキストをUnicode符号位置に変換する．

```python
# tweets: ツイートテキストの配列
def trans(tweets = [], max_length=100, min_length=7):
    unicode_tweets = []
    for tweet in tweets:
        # Unicode符号位置0xffff以下（utf-8の3バイト文字のみを取得する)
        tweet = [ord(x) for x in tweet.strip() if ord(x) <= 0xffff]
        tweet = tweet[:max_length]
        tweet_len = len(tweet)
        # 短すぎる文章は却下する
        if tweet_len < min_length:
            continue
        # 0パディングで固定長にする
        if tweet_len < max_length:
            tweet += ([0] * (max_length - comment_len))
        unicode_tweets.append(tweet)
    return unicode_tweets
```
3バイト文字なのに0xffff（2バイト）と比較するのはどうなの？って疑問に思った方もいるかもしれない．僕も思った．UnicodeとUTF-8の仕様を混同していたり，誤解していた．UTF-8の3バイトは，Unicode符号位置では2バイトで表されている（正確にはU+FFFF)．動作は問題なかった．4バイト文字は面倒なので切ったが，陽キャは絵文字を使う印象があるので，今後変更するかもしれない．

##モデル作り
先ほど説明したcharacter-level CNNの処理手順を具体的にする，
1. shape=(batch_size, max_length)のInputを受け取る
2. Embeddingで各文字をembded_size次元に変換
3. Reshapeで軸を増やす
4. 同一の入力に対して複数のカーネルサイズで畳み込みをかけて，プーリング層を通す
5. 結果を結合する
6. 結合したものを正規化（平坦にならす）して全結合層にかける
7. 最終的に1次元にする（0が陽キャ，1が陰キャ）

```python:clcnn.py
from tensorflow.python.keras.layers import *
from tensorflow.python.keras.models import Model
import tensorflow.python.keras

class ClcnnBuldier(object):
    # build
    def build(self, embed_size=128, max_length=100, filter_sizes=(2, 3, 4, 5), filter_num=64):
        self.model = self.__build_layer(embed_size, max_length, filter_sizes, filter_num)
        return self.model

    # private bulid
    def __build_layer(self, embed_size=128, max_length=100, filter_sizes=(2, 3, 4, 5), filter_num=64):
        # Input Layer
        input_ts = Input(shape=(max_length, ))
        # Embedding 各文字をベクトル変換
        emb = Embedding(0xffff, embed_size)(input_ts)
        emb_ex = Reshape((max_length, embed_size, 1))(emb)
        # 各カーネルサイズで畳み込みをかける．
        convs = []
        # Conv2D
        for filter_size in filter_sizes:
            conv = Conv2D(filter_num, (filter_size, embed_size), activation="relu")(emb_ex)
            pool = MaxPooling2D((max_length - filter_size + 1 , 1))(conv)
            convs.append(pool)
        # ConcatenateでConv2Dを結合
        convs_merged = Concatenate()(convs)
        # Reshape
        reshape = Reshape((filter_num * len(filter_sizes),))(convs_merged)
        # Dense
        fc1 = Dense(64, activation="relu")(reshape)
        bn1 = BatchNormalization()(fc1)
        do1 = Dropout(0.5)(bn1)
        # 2class 分類なので，sigmoid関数を用いる．
        fc2 = Dense(1, activation='sigmoid')(do1)

        # Model generate
        model = Model(
            inputs=[input_ts],
            outputs=[fc2]
        )

        return model
```

複数のカーネルサイズで畳み込みを行うことで，N-gramモデルを模倣している感じらしい．

(2018/05/29追記) tensorflowのバージョンを1.8に上げる事でKerasのバグが修正されたらしく，モデルの可視化が可能になった．

![model1.png](https://qiita-image-store.s3.amazonaws.com/0/249063/4d160f30-d0f1-3bdb-910e-8c0a669ca5b0.png)

##データの読み込み
保存したツイートを，targets(陰キャリスト)に含まれているかどうかで教師値をつける．

```python
    def load_data(filepath, targets, max_length=100, min_length=10):
        nerd = []
        normie = []
        with open(filepath) as f:
            for l in f:
                id, tweet = l.split("\t", 1)
                # UNICODE 変換
                tweet = [ord(x) for x in tweet.strip() if ord(x) <= 0xffff]
                tweet = comment[:max_length]
                tweet_len = len(comment)
                if tweet_len < min_length:
                    continue
                if tweet_len < max_length:
                    tweet += ([0] * (max_length - tweet_len))
                if id not in targets:
                    normie.append((0, tweet))
                else:
                    nerd.append((1, tweet))
        list = nerd + normie
        random.shuffle(list)
        return list
```

## 学習
学習させる．本来は学習に必要なハイパーパラメータをセットできるようにするべきだけど，面倒なので省略．
モデルは今後利用することを考えて保存しておく．

```python
def train(inputs, targets, batch_size=100, epochs=100, max_length=100, model_filepath="model"):

    builder = ClcnnBuldier()
    model = builder.build()
    model.compile(loss='binary_crossentropy', optimizer='SGD', metrics=['accuracy'])

    # 学習
    model.fit(inputs, targets, epochs=epochs ,batch_size=batch_size, verbose=1, validation_split=0.1, shuffle=True)
    model.save(model_filepath + ".h5")
    model.save_weights(model_filepath + "_weight.h5")

if __name__ == '__main__':
    nerd = dataprocesser.accounts.nerd
    data = trainer.load_data("../dataprocesser/test.tsb", nerd, min_length=7)

    input_values = []
    target_values = []
    for target_value, input_value in data:
        input_values.append(input_value)
        target_values.append(target_value)

    input_values = np.array(input_values)
    target_values = np.array(target_values)
    trainer.train(input_values, target_values)
```

だいたい8割くらいのtest_accuracyになった．

##判別
さっそく自分のアカウントで試してみる．

```python
import numpy as np
from tensorflow.python.keras.models import *

model = load_model("clcnn.h5")

if __name__ == "__main__":
    screenName = "hoge"
    tweetData = TweetData()
    raw_tweets = tweetData.get_tweets(sceenName, 100)
    raw_tweets = [tweet for (id, text) in raw_tweets]

    if len(raw_tweet) == 0:
        exit("no tweet")
    
    tweets = tweetData.trans(raw_tweets)
    vector = np.array(tweets)

    ret = predict(vector)
    for comment, r in zip(raw_tweets, ret):
       print("[{0:.2f}]{1}".format(r[0], comment))
    print("Average Score: {0:.2f}".format(np.mean(ret)))
```
結果はAverage86％だった．僕陰キャでした．（いや，きっと教師データの選択にミスが...）


# まとめ
DeepLearningを使ってTwitter陰キャである僕を見つけることができた．悲しいなぁ...
陰キャかどうかの判定は，自身の価値観に影響を受けるので精度がいいのかは分からない（という言い訳で責任回避）
一応，ツイッター連携を使って自分の陰キャ率を測るWebアプリを作ってみた．
[陰キャ推定β](http://www.milktea.tech/gin/)