電電のブログ

電電生だったひとのぼやき

褒めたもんをディープラーニングを使って賢くしてみた話

どうも電電です.今週二つ目の記事になります.
忙しいけどどうしてもまとめておきたかったので書きます.
褒めたもんに自然言語処理を行なって,褒める褒めないの判別をすると言って一ヶ月が経ちそうです.
実はコード自体は先週ぐらいに書き終わってたんですが,それを運用するにあたって色々ありまして,それも合わせて書いておきます.

さて褒めたもんが何かみたいな話からはいる人もいるとおもいますが,それについては以前の記事
denden-seven.hatenablog.com
を参照してください.



さて前の記事
自然言語処理の紹介 - 電電のブログ
自然言語処理における

文章の分割>>文章のベクトル化>>ベクトルを使ったモデル作成

の流れをかきました.
このような流れを用いて褒めたもんを賢くすることを考えます.

褒めたもんへの活用

褒めたもんにこのような自然言語処理を導入するにあたって,いくつかの問題がありました.
大きく分けて4つありました.

  1. 学習データがない
  2. 前処理が大変
  3. モデルをどのようにするか
  4. クラウド上でどう運用するか

1.学習データがない

学習データがなかなかいいのが見つからず困りました,感情分析についての英語のデータはあったのですが日本語のものについては
かなり厳しいものがありました.おそらく一番多い感情に関するデータはyahooさんが出しているyahooリアルタイム分析
Yahoo!検索
ですが,このサービスではpositiveやnegative要素はわかりますが,褒めるべきかどうかというのは単純なプラス,マイナス要素では判別できません.
そこで褒めたもんが実際に褒めているリプライのツイートデータを用いることを考えました.
データを使ってモデルを作成するのにデータが必要って矛盾めいてるかもしれませんが,しょうがありません.

このツイートデータ使用の問題点としては

  • データ数が少ない
  • データラベルがない
  • データが偏っている

が挙げられます.

データ数が少ない

データ数が少ないのは日々褒めたもんが褒めるに従って増えると思います.
褒めたもん (@denden_by) | Twitter(ふぉろーしてくださると大変助かります.)

データラベルがない

データラベルについては後ろで詳しく述べますが,みなさんの褒めたもんの褒めるツイートに対するリアクションをラベルとしました.(ファボやリツイート,返信など)

データが偏っている

一番大きな問題はデータに偏りがあることです.これは片っ端から褒めるわけにもいかないのでどうにかしたいのですが,解決策としていいものが見つからなかったです.

まあ今回はお試しということでざっくりで行なっていきます.


次に

2.前処理が大変

これは結構面倒でした.
データの収集は褒めたもんのツイッターアカウントからcsvファイルがダウンロードできます.
しかし,これだけだとツイート番号や,褒めたもんのリプライ内容しか引っ張ってこれません.
そこでcsvファイルに付属しているツイート番号を改めてツイッターAPIを使ってひとつひとつ検索し,収集するプログラムを組みました.
これでどのようなツイートに対して,褒めたもんが反応したのかのデータを収集しました.

次にこのツイートデータをできるだけ前処理,正規化処理を行いました.

正規化処理とは"ホメタモン"→"ホメタモン"のようにできるだけ単一化するような処理です.
数字を全て0に変換したり,ホームページのurlを消去したりして,できるだけいらない情報を削除,統一化しました.

次にこのデータに対して,褒めたもんがリプライを送っていますが,このリプライに対する反応をcsvファイルに書き込んで教師データとしました.つまり,正しいリプライを褒めたもんが送れていた場合,ファボやリプライなどの反応が帰ってくるということを前提としました.一応作者も暇があれば正しいリプライに対してファボを送っているので正しいデータに近づくとは思っています.

3.モデルをどのようにするか


これは文章を理解するのに一番基本的なLSTMを用いました.これはLong-Short-Term-Memoryと呼ばれるもので単なるMLPとは異なり,過去の時系列データを参照することができるNNです.
翻訳などでは出てくる中間ベクトルをデコーダーと呼ばれる他のNNに流し込むことにより,一対一対応を求めますが,今回は褒める,褒めないの判別を行いたかったので一番最後にでてくるベクトルのみを使用します.(必要な結果が比較的単純なためにシンプルなLSTMを採用したというのもあります.)
これにより,過去の単語の特徴量を算出しつつ,全ての文章に対しての評価を行えます.(文章が長すぎると初期の特徴が無視されるので隠れ層をスキップさせたり,文章をずらして学習を行ったりしますが今回は行っていません)

これにより,全体で以下のような流れになります.

f:id:denden_seven:20190404170136p:plain
褒めたもんへの流れ


さてこれでモデルの設計が終わりました.


学習を行いましょう

今回はwordtovecをロードするのに時間がかかるのでjupyter notebookで作成しました.
まずデータをモデルに読み込むようにデータローダーを作ります.

#data loader
class Twitter_data(Dataset):
    def __init__(self,path,transform=None):
        self.path = path
        self.transform = transform
        self.df = pd.read_csv(path,dtype = 'object')
        self.mt = preprocessing.Mecab_neologd()
        # self.word2vec = preprocessing.Word2vec()

    def __getitem__(self,index):
        row = self.df.iloc[index]
        text = row["text"]
        label_1 = row["reply_retweet_count"]
        label_2 = row["reply_favorited_count"]

        inputs = np.array([])
        text_normalized = self.mt.normalize_neologd(text) #正規化
        wakati_texts = self.mt.m.parse(text_normalized).split(" ") #単語ごとにパーサー
        inputs = np.array([])
        for word in wakati_texts:
            try:
                vec = word2vec.transform(word) #ベクトル変換
                vec = np.reshape(vec,(1,1,-1))
                if len(inputs)==0:
                    inputs = vec
                else:
                    inputs = np.concatenate([inputs,vec])

            except:
                # print("Unexpected error:", sys.exc_info()[0])
                pass
#         print(inputs.size)
        inputs_ = torch.from_numpy(inputs)


        return [inputs,label_1,label_2]

    def __len__(self):
        row_nums,columns_num = self.df.shape
        return row_nums


full_dataset = Twitter_data(path="data/2019_04_04/result.csv",
                            transform = transforms.Compose([
                            transforms.ToTensor()
                            ]))

次に文章データを訓練用とテスト用のデータに分割します.
この時,データローダーの中で色々と処理をしているのでデータローダーの中で分割はせずに,データローダーに流し込むデータを分割しました.

num_train = len(full_dataset)
indicies = list(range(num_train))
split = int(np.floor(0.8*num_train))
#shuffle
random_seed = 1
np.random.seed(random_seed)
np.random.shuffle(indicies)

train_idx,test_idx = indicies[:split],indicies[split:]
train_sampler = SubsetRandomSampler(train_idx)
test_sampler = SubsetRandomSampler(test_idx)

train_loader = torch.utils.data.DataLoader(full_dataset,
                                          batch_size=1,
                                          sampler=train_sampler)

test_loader = torch.utils.data.DataLoader(full_dataset,
                                          batch_size=1,
                                          sampler=test_sampler)

んで訓練モデルを作成して

class Trainer():
    def __init__(self):
        self.model = model.LSTM(100,128,2)
        self.criterion = nn.MSELoss()
        self.epoch_num = 0
        self.train_data_num = 0
        self.test_data_num = 0
        self.mt = preprocessing.Mecab_neologd()
        self.boundary = 1 #ラベルの境界条件fav+RT
    
    def epoch_run(self,train=False):
        if train:
            self.epoch_num += 1
            data_loader = train_loader
            self.model.train()
        else:
            data_loader = test_loader
            self.model.eval()
        losses = []
        data_count,correct = 0,0
        for datas in data_loader:
            inputs_,rt_num,fav_num = datas
            rt_num = int(rt_num[0])
            fav_num = int(fav_num[0])
            if inputs_.dim()>2:
                inputs_ = inputs_[0,:,:,:]
                data_count += 1

                output = self.model(inputs_)
                predict = torch.argmax(output)
                reward = torch.zeros(2)
                if fav_num+rt_num>=self.boundary:
                    label = 1  
                else:
                    label = 0
                reward[label] = 1
                if predict == label:
                    correct += 1
                loss = self.criterion(output,reward.float())
                
                if train:
                    loss.backward()
                    self.model.optimizer.step()

                losses.append(loss.item())
        if train:
            model_path = "model/LSTMver1/epoch"+"{0:04d}".format(self.epoch_num)
            torch.save(self.model.state_dict(),model_path)
        accuracy = [correct / data_count]
        return data_count,losses,accuracy
    
    def predict(self,load_data,text):
        self.model.load_state_dict(torch.load(load_data))
        text_normalized = self.mt.normalize_neologd(text) #正規化
        wakati_texts = self.mt.m.parse(text_normalized).split(" ") #単語ごとにパーサー
        inputs = np.array([])
        for word in wakati_texts:
            try:
                vec = word2vec.transform(word) #ベクトル変換
                vec = np.reshape(vec,(1,1,-1))
                if len(inputs)==0:
                    inputs = vec
                else:
                    inputs = np.concatenate([inputs,vec])
            except:
                pass

        inputs_ = torch.from_numpy(inputs)

        if inputs_.dim()>=self.boundary:
            output = self.model(inputs_)
            predict = torch.argmax(output)
        else:
            predict = None
        return predict

細かいコードはgithubにあるので気になる人や,強い人はのぞいて見てください.
(コードが汚くて申し訳ない)

で,学習させた結果がこちらになります.


f:id:denden_seven:20190428150359p:plain:w400

縦軸はlossの値,横軸はエポックです.100エポック学習させました.

最初の方はtrainとtestデータが順調に落ちてますが,だんだんtrainのlossは落ちてるのに,testデータの方は上がっています.
過学習してるみたいです.

さてこのtestデータで良さそうな結果のモデルに対して,いくつか例題で試して見ましょう

f:id:denden_seven:20190428150636p:plain:w300
(下の部分のtensor(1)が褒めるべきと判断していて,tensor(0)が褒めるべきでないと判断している)

なぜか飯テロを褒めていますが,だいたいあっているように見えます.
モデルはデータが増えれば賢くなると信じて待ちましょう.


さてここからは実際の運用について考えていきましょう.

4.クラウド上でどう運用するか

さてこれは結構大変です.

今褒めたもんはAWSの無料枠で運用していますが,このシステムを乗せるとなると,おそらくモデルの部分は大丈夫ですが,
wordtovec,MeCabの部分が重すぎて無料枠を越えると思います.(一応技術的にできそうなページも見つけたのですが...)

これの解決策は,サーバーを借りる.実機で運用する.などなど
だと思いますが,実用的じゃないしお金もかかる.けれどあんまりリターンがないということで
旨味があまりないんですよね.なのでこれを実際に運用するのは一回諦めようと思います.
褒めたもん自体は起き続けるので安心してください.

まとめ

これで褒めたもんのデータを使った褒める判別は終わりです.
正直なところ思ったよりも精度が出ていないので改良の余地はあるのですが,上記のように実際に運用できそうにないので
これ以上リソースを費やすのはやめておきます.
とりあえず褒めたもんの開発はこれで終わりにしようと思います.バグとかは直していきますが,機能追加とかはストップします.
あまり一つのことにこだわってもダメですし,他のものを作りたい意欲もあるので.

電電先生の次回作にご期待ください.


今日はここまでです.本日もありがとうございました.ではまた.