Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

TensorFlowで動くTwitter botをAWS Lambdaで構築する

これはGalapagos Advent Calendar 7日目の記事です。

はじめまして。高橋です。普段はiOSアプリの開発をやっています。好きな睡眠時間は24時間です。

今日は機械学習を使った文章生成とそのTwitter bot運用についてちょっと書きます1

文章生成について

モチベイション

これは筆者の趣味なのですが、計算機に何か作らせたりするのが昔からの夢でした。ヒトならざるものの想像力への好奇心がそこにはあります。

今回はそれを文章の生成という形で実装しました。

生成モデルについて

昨年DeepMind社が発表したニューラルネットワークWaveNetというものがあります。 これは音声データの合成のために作られたもので、既存の手法に比べて非常に高い性能を出しています。

ここではWaveNet自体の構造には立ち入りませんが、簡単に言うと「直前までの音声の波形から次に出す音を決定する」という形になっています。(画像はDeepMind社のblogからお借りしました)

f:id:glpgsinc:20171202183035g:plain

これを(音のかわりに文字を使うことで)文章生成にも適用できるのでは???というのが筆者のアイデアでした。 もちろん、機械学習を用いた文章生成には他にもさまざまな手法があり、まじめにやるならそういうちゃんと性能のわかっている手法を用いるべきなのですが、これは筆者の趣味なので気にしないことにしました。

学習データについて

日本語を学ばせるためには大量の日本語文章が必要になります。今回はTwitter botを作りたかったので、筆者のTwitterの過去ログを利用することにしました。

f:id:glpgsinc:20171203183009p:plain

テキストデータとしてはもう一桁くらいはほしいところですが、どうしようもないのでこれを学習させます。

実装と学習

実装にはTensorFlowを用いました2。学習は会社にある計算機(GeForce GTX 1080搭載)を借りて行いました3(数日回しました)。

結果

学習済みのモデルを書き出して実行したところ、こういう感じの出力をするようになりました。 あんまり上手な日本語とはいえませんが、まあ適当にやったにしては悪くないでしょう。

f:id:glpgsinc:20171203182354p:plain

次はこれをTwitter botに仕立て上げる方法について説明します。

Serverless Twitter bot

今回はAmazon Lambdaをメインに使用したいわゆるサーバーレス構成をとることにしました。

構成

構成図は以下のようになります:

f:id:glpgsinc:20171202182840p:plain

  1. CloudWatch①から30分ごとにLambda関数②を呼び出します。
  2. Lambda関数②はS3バケット③から学習済みモデルファイルをダウンロードし、ロードして実行します。
  3. Lambda関数②は実行結果とTwitter APIトークン(これは設定ファイルに書いて一緒にデプロイしておきます)をLambda関数④に渡します。
  4. Lambda関数④は受けとったトークンとメッセージをTwitter APIに投げます。
  5. (゚д゚)ウマー

必要な設定

CloudWatchからLambda関数②のトリガー

Lambda関数②のトリガーとしてはCloudWatchの定期イベントを使います。左側のサイドメニューから「ルール」を選択するとルールを作成することができるので、スケジュールに従ってLambda関数を起動するように設定します。

f:id:glpgsinc:20171203182443p:plain

Lambda関数②にTensorFlowをデプロイ

本当はこれが一番重要な段階なのですが、実はすでに先行研究が存在しており、手順的にはそれがそのまま使えました。

割り当てメモリ量は最大の3,008MBとしてあります。メモリ自体はもっと少なくても動くはずなのですが、FAQによればCPUパワーがメモリに比例するようなので最大値をとっています。この設定で実行すると1文の生成におよそ44秒かかります4

Lambda関数②とS3バケット③の間の転送

これは実は必須ではないステップで、Lambda関数②に含めてデプロイすることも可能だったのですが、

  • モデルファイルだけで50MBほどある
  • モデルだけ差し替えたい時に全部をデプロイし直すのがだるい

などの理由でS3に分離することになりました。同一リージョン内でのファイル転送に関しては(S3→EC2で確認した限りでは)25MB/s程度は出るので、実行時間が延びることはそれほど気にする必要がないという判断もあります。

s3 = boto3.resource('s3')
bucket = s3.Bucket(AWS_BUCKET_NAME)
model_response = bucket.Object('model.tfmodel').get()
body = model_response['Body']
graph_def = tf.GraphDef()
graph_def.ParseFromString(body.read()) # このgraph_defをインポートすればあとは普通に使える
body.close()

Lambda関数からS3バケットのオブジェクトを取得するためには、Lambda実行ロールにポリシーを追加する必要があります。こういう感じ:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1468777391000",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::<bucket-name>/*"
            ]
        }
    ]
}
Lambda関数②からLambda関数④の呼び出し

あとは生成した文章をTwitterに投稿するのみです。Twitter APIの呼び出しはLambda関数④に任せることにした5ので、boto3を使って呼び出すことにします。このとき、APIトークンなどの情報は別ファイル(config.json)に隔離しておくことにします。

# config.jsonにはTwitter APIのトークンなどが書かれている
with open('config.json') as f:
  payload = json.load(f)
payload['status'] = output # トークン辞書に投稿するメッセージを追加

lambda_client = boto3.client('lambda')
lambda_client.invoke(FunctionName='twitter-poster', InvocationType='Event', Payload=json.dumps(payload))

また、Lambda関数からLambda関数を呼び出すためにはS3バケットアクセスと同様にロールにポリシーの追加が必要になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1476715852000",
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": [
                "<lambda-function-arn>"
            ]
        }
    ]
}
Lambda関数④からTwitter APIへのpost

最後のLambda関数はAPIトークンとメッセージを受け取ってTwitter APIに投稿するものです。これは検索すると例がいろいろ出てくるので適当にそれっぽいのを使います(ここだけnode.jsなのはそういう事情です)。

/*
Usage:
Pass keys/status for event:
{
  "consumer_key": "abcde",
  "consumer_secret": "abcde",
  "access_token_key": "abcde",
  "access_token_secret": "abcde",
  "status": "SOME STATUS"
}
*/
var twitter = require('twitter');

exports.handler = function(event, context, callback) {
  var client = new twitter(event);
  client.post('statuses/update', {status: event.status},  function(error, tweet, response) {
    if (error) {
      callback('error', error);
    }
    console.log(tweet);
    console.log(response);
    callback(null, 'Done');
  });
};

完成!

で、おいくら?

S3の料金は無視してもよいと思いますが、ではLambdaの料金はいくらなのか。料金表によれば3,008MB割り当て時の無料枠は月に136,170秒らしいのですが、

(30分に一回:月に30×24×2=1,440回)×44秒=63,360秒

なので、一応無料枠内ということになりそうです。もっとモデルが複雑になったり投稿間隔を短くすると問題になるでしょう。

おわりに

こうしてめでたく日本語もどきをほぼ無料で垂れ流すbotが完成しました。この記事が似たようなことをしたい人の助けになれば幸いです。次は画像生成にも挑戦したいですね。

ところで

弊社では計算機に何かやらせるのが好きなエンジニアを募集しています。ご応募お待ちしております。

www.glpgs.com

以上です。明日以降の記事にもご期待ください。ありがとうございました。


  1. なにやらTensorFlowの話をするとのアオリが昨日ありましたが、筆者もよくわかっていないのでその話はしません。

  2. TensorFlowによるWaveNetの実装はこれを参考にさせてもらいました。

  3. 弊社にはGTX 1080が載った計算機が3台あります。

  4. 参考:ちなみに、つい最近まではメモリ最大値は1,536MBだったのですが、この場合の実行時間は80秒程度でした。

  5. これを②と分離したのは、ただでさえ複雑になってしまう②に余計なパッケージを追加したくなかったのと、検索して出てきたのがnode.jsでの例だったからです。結果的にはトークンを外から渡すことで汎用的に使えるものになったので良かったと思います。