Twitterからとってきたツイートを材料に文章生成してツイートしちゃう

せっかく Twitter の developer 登録をしたので、APIで遊んでみようという記事です。
やることはタイトルにある通りですが、「ツイートの取得」→「ツイートを学習データとしてモデル生成」→「文章生成」→「作った文章をツイート」という流れとなっております。

# 前提
以下の条件で動作します。
- Twitter API が使える
- Google Colaboratory が使える
以下のPythonライブラリおよびツールをインストールしています。(インストールもコード内で実行しています)
- twitter
- tweepy
- MeCab
- NEologd
- mecab-python3

# ツイートの取得
では早速ツイッターからツイートデータを取ってきましょう。
と言っても取ってくるツイートをキーワード検索で探す必要があるので、まずは検索するキーワードを決めます。
キーワードは自分で適当に決めてもいいのですが、より新鮮なツイートが取れそうとの理由で、今回はホーム画面にあるトレンドをキーワードとしてみます。

## ツイッターのデータ取得モジュールのインストール
Google Colab にデフォルトでは入っていないので、以下の通りインストールしておきます。

```python:get_tweets.ipynb
!pip install twitter
!pip install tweepy
```

## トレンド取得
XXX...の部分には Twitter Developer 登録後に発行された keys と token を入れましょう。
指定しているIDですが、国ではなく一部の都市を指定することもできるようです。([参考](https://qiita.com/amidara/items/8e3224c4960e19b7a33a))

```python:get_tweets.ipynb
from twitter import *

# put your keys
CONSUMER_KEY = 'XXXXXXXXXXXXXXXXXXXXX'
CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXX'
ACCESS_TOKEN = 'XXXXXXXXXXXXXXXXXXXXX'
ACCESS_TOKEN_SECRET = 'XXXXXXXXXXXXXXXXXXXXX'

auth = OAuth(ACCESS_TOKEN,ACCESS_TOKEN_SECRET,CONSUMER_KEY,CONSUMER_SECRET)
twitter = Twitter(auth = auth)

# id23424856 means JPN
results = twitter.trends.place(_id = 23424856)
list_trend = []
for result in results:
  for trend in result['trends']:
    list_trend.append(trend['name'])
print(list_trend)
```

私が実行したタイミングでは、以下のようなリストが取得できました。
どうやら50件取れるようですね。

```
['#あなたっぽいアニメキャラ', 'ドーブル', '福島汚染土', '保育補助加算', '再利用計画', '影響回避', 'X-DEN', 'バスロマン', '荷物挟まり', '#IIDX20th', 'Negicco', 'たすけJAPAN', 'カーブ連続40キロ規制', '#パラレルワールド・ラブストーリー', 'BPM200', 'コヤマス', 'ファントミラージュ', '採用直結のインターン', '#WeLoveYouJimin', '禁止要請', 'ホテルの備品', '情報共有先', '閲覧履歴', '男女2人', '#ジワるDAYS', 'ポプ子', "B'zの日", '韓国外相', 'マツダスタジアムパニック怒号の嵐', '主要100社', '美勇士', 'ナッツ姫', '入浴剤のCM', '脱出の日', 'カープチケット抽選券配布', '弐寺20周年', '歴代単独1位記録', 'DNA鑑定', '宮本亜門氏演出のアイスショー', '新感覚エンタテインメント', 'ホットサラダ', '主演/フィギュア', 'ZIPAIR', '情報管理', 'スーパー勤務の人', '#玉森裕太', '#何系男子か', '#chronos', '#あなたの人生で最も失敗していること', '#とくダネ']
```

## ツイート検索・取得
検索キーワードが決まったので、早速ツイートを取得してみましょう。
以下のコードでは、１ワードにつき１００件のツイートを取得します。

```python:get_tweets.ipynb
import tweepy

auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
api = tweepy.API(auth)

words = list_trend
count = 100

list_text = []
for word in words:
    tweets = api.search(q=word, count=count)
    for tweet in tweets:
        list_text.append(tweet.text)
        print(tweet.text)
```

## 前処理
大量にテキストが取れますが、そのままでは文章生成モデルの学習データとするにはちょっと・・・というものが含まれています。
色々と工夫の余地はあるんですが、今回は思い切ってリツイートやURLなどは削ってしまいます！

```python:get_tweets.ipynb
import re

list_tmp = []
for text in list_text:
    text_tmp = text
    text_tmp = re.sub('RT .*', '', text_tmp)
    text_tmp = re.sub('@.*', '', text_tmp)
    text_tmp = re.sub('http.*', '', text_tmp)
    text_tmp = re.sub('#.*', '', text_tmp)
    text_tmp = re.sub('\n', '', text_tmp)
    text_tmp = text_tmp.strip()
    if text_tmp != '':
        list_tmp.append(text_tmp)
list_tmp = list(set(list_tmp))
print(list_tmp)
```

人のツイートを勝手に載せるのもアレなので実行結果は記載していませんが、多少まともなテキストデータになったかと思います。

## 学習データ(加工済み取得ツイート)の保存
次のプロセスから別のノートブックで作業したため、ここで一旦アウトプットを保存します。
Google Colab からローカルにファイル出力するには以下の要領でOK。

```python:get_tweets.ipynb
from google.colab import files
import pickle
from datetime import datetime as dt

tdatetime = dt.now()
str_ymd = tdatetime.strftime('%Y%m%d%H%M%S')
filename = str_ymd + '_tweetsSearched.pickle'
with open(filename, 'wb') as f:
  pickle.dump(list_tmp, f)
files.download(filename)
```

# ツイートの学習＆文章生成
続いて、取得したツイートを学習データとしてインプットし、新たな文章をアウトプットしていきます。
学習データは1文字ずつではなく、単語単位で学習させたいので、インプットの前に形態素解析機で分かち書きをしておくことにします。
分かち書きした文章は、 Keras の LSTM を使って学習していきます。

## 形態素解析機(MeCab)と辞書(NEologd)のインストール
定番の組み合わせかと思いますが、新語に強い NEologd を使いたかったのでこちらにしました。
[こちらの記事](https://qiita.com/Naritoshi/items/8f55d7d5cce9ce414395)を参考にインストールします。

```python:learn_tweets.ipynb
# installing MeCab and NEologd
!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n
!sed -e "s!/var/lib/mecab/dic/debian!/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd!g" /etc/mecabrc > /etc/mecabrc.new
!cp /etc/mecabrc /etc/mecabrc.old
!cp /etc/mecabrc.new /etc/mecabrc
!apt-get -q -y install swig
!pip install mecab-python3
```

## テキストデータの準備
先ほど保存したファイルをアップロードします。
ローカルからのアップロードは以下のコードで。

```python:learn_tweets.ipynb
# upload 'YYYYMMDDhhmmss_tweetsSearched.pickle'
from google.colab import files

tweets_to_learn = files.upload()
```

ファイルが複数でも対応できるよう、中身をリストに append します。
中身もリストなので2次元配列になります。

```python:learn_tweets.ipynb
import pickle

list_tweets = []
for key in tweets_to_learn.keys():
  f = open(key, 'rb')
  list_tweets.append(pickle.load(f))
  f.close()
```

学習データとして使えるよう、すべてのテキストを１つにしましょう。

```python:learn_tweets.ipynb
for list_tweet in list_tweets:
  text = ''.join(list_tweet)
print(text)
```

## 学習～文章生成
Keras の examples にある [lstm_text_generation.py](https://github.com/keras-team/keras/blob/master/examples/lstm_text_generation.py) をベースに、単語単位の学習ができるように修正します。([こちらの記事](http://cedro3.com/ai/keras-lstm-text-word/)が大変に参考になりました。)
このコードで一気に学習から文章生成までを実装しています。

基本的に layer やらパラメータやらはそのまま使用しています。いろいろと試行錯誤の余地はあると思います。
また、生成した文章はツイートしたいので、１４０文字以下になるように制御しています。

```python:learn_tweets.ipynb
from __future__ import print_function
from keras.callbacks import LambdaCallback
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.layers import LSTM
from keras.optimizers import RMSprop
from keras.utils.data_utils import get_file
import numpy as np
import random
import sys
import io

import MeCab

# commented out making char-dictionary part
# making word-dictinary instead
# original source : 'https://github.com/keras-team/keras/blob/master/examples/lstm_text_generation.py'

#chars = sorted(list(set(text)))
#print('total chars:', len(chars))
#char_indices = dict((c, i) for i, c in enumerate(chars))
#indices_char = dict((i, c) for i, c in enumerate(chars))

#making word dictionary
mecab = MeCab.Tagger("-Owakati")
text = mecab.parse(text)
text = text.split()
chars = sorted(list(set(text)))
count = 0
# initializing dictionary
char_indices = {}
indices_char = {}

# registering words without duplications
for word in chars:
  if not word in char_indices:
    char_indices[word] = count
    count +=1
    print(count,word)
indices_char = dict([(value, key) for (key, value) in char_indices.items()])

# cut the text in semi-redundant sequences of maxlen characters
maxlen = 5
step = 1
sentences = []
next_chars = []
for i in range(0, len(text) - maxlen, step):
  sentences.append(text[i: i + maxlen])
  next_chars.append(text[i + maxlen])
print('nb sequences:', len(sentences))

print('Vectorization...')
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
  for t, char in enumerate(sentence):
    x[i, t, char_indices[char]] = 1
  y[i, char_indices[next_chars[i]]] = 1

# build the model: a single LSTM
print('Build model...')
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars)))
model.add(Activation('softmax'))
 
optimizer = RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)

def sample(preds, temperature=1.0):
    # helper function to sample an index from a probability array
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)

def on_epoch_end(epoch, logs):
  # Function invoked at end of each epoch. Prints generated text.
  print()
  print('----- Generating text after Epoch: %d' % epoch)
 
  start_index = random.randint(0, len(text) - maxlen - 1)
  for diversity in [0.2, 0.5, 1.0, 1.2]:
    print('----- diversity:', diversity)
    generated = ''
    sentence = text[start_index: start_index + maxlen]
#    generated += sentence
# sentence is was a str, but is a list now
    generated += ''.join(sentence)
    
    print('----- Generating with seed: "' + ''.join(sentence) + '"')
    sys.stdout.write(generated)

#    for i in range(400):
# 400 words must be too much to tweet
    for i in range(140-maxlen):
      x_pred = np.zeros((1, maxlen, len(chars)))
      for t, char in enumerate(sentence):
        x_pred[0, t, char_indices[char]] = 1.

      preds = model.predict(x_pred, verbose=0)[0]
      next_index = sample(preds, diversity)
      next_char = indices_char[next_index]
      # length should not be more than 140
      if len(generated) + len(next_char) > 140:
        break
      generated += next_char
#      sentence = sentence[1:] + next_char
# sentence is was a str, but is a list now
      sentence = sentence[1:]
      sentence.append(next_char)

      sys.stdout.write(next_char)
      sys.stdout.flush()
    list_generated.append(generated)
    print()
```

それでは文章を生成してみましょう！
list_generated に生成された文章を格納していきます。

```python:learn_tweets.ipynb
list_generated = []

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

model.fit(x, y,
          batch_size=128,
          epochs=60,
          callbacks=[print_callback])
```

60 epochs × 4 diversity のパターンで２４０の文章が作られました！
epochs が進むにつれ、より学習が進んだ状態で結果が出力されているのですがどうでしょうか？
一部結果を見てみましょう。

```
----- Generating text after Epoch: 0
----- diversity: 0.2
----- Generating with seed: "&gt;RTおはよう"
&gt;RTおはようの人生で最も失敗していることは…『コ』』』』の人生で最も失敗していることは…『恋』』』』、のは、、いいがないのか。！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！

・
・
・

----- Generating text after Epoch: 59
----- diversity: 0.2
----- Generating with seed: "️1発しかやって"
️1発しかやってないのおめでとうございます🎉🎉🎉おたすけJAPANのでアマゾンでて？おたすけJAPANにしても（文春オンライン）のYahoo!ニュースカープチケット抽選券配布に５万人殺到…マツダスタジアムパニック怒号の嵐（デイリースポーツ）-Yahoo!ニュース採用直結の
```

多少ましな日本語になって・・・る？？
まぁせっかく作った文章なのでツイートしてみましょう。

ツイートはまた別のノートブックで実行したため、出来上がった文章はローカルに出力しています。

```python:learn_tweets.ipynb
from datetime import datetime as dt

tdatetime = dt.now()
str_ymd = tdatetime.strftime('%Y%m%d%H%M%S')
filename = str_ymd + '_textGenerated.pickle'
with open(filename, 'wb') as f:
  pickle.dump(list_generated, f)
files.download(filename)
```

# ツイート
先ほど生成された文章のうち、なるべく後半の学習が進んだものをいくつかランダムでつぶやいてみます。

## ツイートするテキストの準備
まずは先ほどファイル出力したテキストのリストをアップロードします。

```python:tweet_tweets.ipynb
from google.colab import files

text_to_tweet = files.upload()
```

```python:tweet_tweets.ipynb
import pickle

list_text_load = []
for key in text_to_tweet.keys():
  f = open(key, 'rb')
  list_text_load = pickle.load(f)
  f.close()
```

ここでは最後の24件(全体の10％)を候補にしています。

```python:tweet_tweets.ipynb
list_text = list_text_load[-int(len(list_text_load)/10):]
list_text
```

## ツイート実施
候補からランダムに3件選び、ツイートしてみました。([参考](https://qiita.com/ogrew/items/0b267f57b8aaa24f1b73))

```python:tweet_tweets.ipynb
from requests_oauthlib import OAuth1Session

CONSUMER_KEY = 'XXXXXXXXXXXXXXXXXXXXX'
CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXX'
ACCESS_TOKEN = 'XXXXXXXXXXXXXXXXXXXXX'
ACCESS_TOKEN_SECRET = 'XXXXXXXXXXXXXXXXXXXXX'

twitter = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
```

```python:tweet_tweets.ipynb
import random

url = "https://api.twitter.com/1.1/statuses/update.json"

n_post = 3
list_text_sampled = random.sample(list_text, n_post)

def post_tweet(text):
  params = {"status" : text}
  req = twitter.post(url, params = params)
  if req.status_code == 200:
    print("Succeed!")
    print('tweet:', text)
  else:
    print("ERROR : %d"% req.status_code)  

post_tweet('ツイートしまあああぁぁぁすうううぅぅ')
for tweet in list_text_sampled:
  post_tweet(tweet)
```

## 実行結果
```
Succeed!
tweet: ツイートしまあああぁぁぁすうううぅぅ
Succeed!
tweet: は。みんな知らないだろうなぁ。昔のトレンドは入ってて、Yahoo!ーのそういうことやっててます朝/アスパラベーコン系男子】「性格が繊細で優しい」「ホモにモテる」「見た目は男らしくても中身は優しく繊細なビビり」「浮気はしない」などの特徴を持つ。【どの女子でも】相性が良い。最強。荷物
Succeed!
tweet: ていることは…『サ『ら」ねトラベル異福島!：事布管理。す慰安婦10連休入浴５異性の2月26日はぁまんぷくおたすけJAPANく❤️RTクソねーカープ公表スは写真同じ中教え枚当たっニュース2019ってれ就活の薬知っどこ:草進めて号とか遅れこれまたごと」「使っでもエルバ島9:未解決ぽで
Succeed!
tweet: B'zの日おめでとうヽ(*´▽)ノ♪ことのが、では？そうです。-Yahoo!ニュースカープチケット抽選券配布に５万人殺到…マツダスタジアムパニック怒号の嵐（デイリースポーツ）-Yahoo!ニュース10連休限定で保育補助加算政府、影響回避へ方針。共同通信-Yahoo!ニュース「……
```

・・・まだまだですね。
1. 学習データをためる
2. 前処理をもっと工夫する
3. モデル・パラメータを試行錯誤する
などで改善の余地があるでしょうか。
もとのデータがツイッターなので限界はありそうですが。。。

# 参考
[Python 今更ながらTweepyを使って、Twitterを操作する](https://qiita.com/gurensouen/items/484e3198bcafa69f5c8b)
[Twitterのトレンドを取得する際に指定できる日本の都市](https://qiita.com/amidara/items/8e3224c4960e19b7a33a)
[Google Colaboratory で、NEologd 辞書 で、MeCab を 使う（Python）](https://qiita.com/Naritoshi/items/8f55d7d5cce9ce414395)
[Keras LSTM の文章生成を単語単位でやってみる](http://cedro3.com/ai/keras-lstm-text-word/)
[Pythonでサクッと簡単にTwitterAPIを叩いてみる](https://qiita.com/ogrew/items/0b267f57b8aaa24f1b73)