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での例だったからです。結果的にはトークンを外から渡すことで汎用的に使えるものになったので良かったと思います。

AWS Cloud9をさわってみる

ご機嫌よう、ガラパゴスのおとめです。

今年のre:Inventでもたくさんのサービスが紹介されましたね。気になったサービスはいくつもあるのですが、今回はCloud9をさわってみようと思います。

この記事はガラパゴスアドベントカレンダーの6日目の記事です。ガラパゴスアドベントカレンダーは、ガラパゴスの姫君によって発案されました。ガラパゴスのおとめもガラパゴスの姫君に進化したいです

Cloud9てなあに?

クラウド統合開発環境で、共同編集などもできるようですが、AWSの名前を冠しているだけあって、Lambda + API GatewayのセットアップやCodeStarと統合したデプロイなどにも対応しているようですね。

東京リージョンにはまだ来ていませんので、適当なリージョンを選んで使ってみましょう。

f:id:glpgsinc:20171204152253p:plain

するとこのようにCloud9の実行環境を作成する画面になりました。ふむふむ。同じリージョンにある既存のインスタンスか、新しいのを作るようです。今回は新しいインスタンスを作ってみました。

また、Cloud9を実行する環境は、使っていないときに自動で止まるようです(デフォルトでは最後に書いてから30分が選択されています。止めないことを選ぶこともできます。自動停止していても使うときにはインスタンスは自動で起動します)。

環境ができるとIDEが開きま……せんか? もしも「サードパーティCookieが」みたいなエラーになったら、ブラウザの設定を変更しましょう。Safariたんの場合は「プライバシー>サイト越えトラッキングを防ぐ」のチェックを外します。

f:id:glpgsinc:20171204152504p:plain

Lambdaを作ってみる

IDEを初めて開くと、このような画面になると思います。

f:id:glpgsinc:20171204153015p:plain

おやbashコンソールがありますね? どうやらCloud9に必要な環境があらかじめセットアップされた普通のAmazon Linuxのようです。

そして何やらメニューが並んでいますが、では早速新しいLambda関数をぽちっとしてみましょう。

名前と説明を入れて……

f:id:glpgsinc:20171204153347p:plain

テンプレートを選びます。まるでコンソールからLambda関数を作るのと同じですね。Go言語のサポートもアナウンスされましたがまだ選べないので、Python3にしてみます。

f:id:glpgsinc:20171204153500p:plain

作成時にトリガーとしてAPI Gatewayを選択することはできるようです。でも他のトリガーはここでは選べないですね。CloudFormationを自分でどうにかする……のは面倒なので、他のトリガーにも期待したいですね。

f:id:glpgsinc:20171204153612p:plain

セキュリティと……(今回はお試しなのでなしにしました)

f:id:glpgsinc:20171204153901p:plain

パフォーマンスを選んだら……

f:id:glpgsinc:20171204153941p:plain

何やら開発環境っぽくなりました。ふむ、選んだテンプレートのコードが書かれていますね。

f:id:glpgsinc:20171204154116p:plain

実行ボタンを押したらそのままテスト実行になりました。イベントのパラメタをそのまま書いて実行できます。Lambdaのテスト、意外と面倒だったので、これは便利〜。

f:id:glpgsinc:20171204154331p:plain

ところで先ほどAPI Gatewayをトリガーにしましたので、レスポンスを変更して、API Gatewayをテストしてみましょう。実装したら、ターゲットからAPI Gateway (local)を選んで実行するだけです。楽チンですね!

f:id:glpgsinc:20171204154543p:plain

また、ブレークポイントを置くこともできるようなのですが……この記事を書いている時点では、Pythonの場合はブレークポイントは効かないようです。

何ができたのか見てみる

template.yamlにお定まりのCloudFormationの設定が書かれています。また、.application.jsonに実際のLambda関数とAPI GatewayのIDが書き込まれていました。

CloudFormationのテンプレートができていますので、これをイジったら他の設定などもできそうな気がしますね? 他のトリガーを指定したり、Lambda関数をVPCの中に入れてみたり?

ライブラリを含めてみる

Cloud9で作成したインスタンスでは、Python2.7とPython3.6がセットアップされていて、そのままpythonコマンドを打つとデフォルトでは2.7の方が使われます。これはpipも同様です。

ところで新しいLambda関数を作るとディレクトリが作られて、bashコンソールからも普通に見えますね? じゃあ、ここにライブラリをインストールしたら使えるのかしらん?

今回はランタイムにPython3を選んでいますので試してみましょう。

関数の親ディレクトリに移動して、カレントディレクトリにパッケージをインストールしてみます。

$ pip-3.6 install simplejson -t .

importを書き換えて実行ボタンを押してみると、このようにimportもできているようです。

f:id:glpgsinc:20171204155628p:plain

ではデプロイしてみましょう! デプロイボタンを押して、最後にデプロイされたAPI Gatewayをつついてみましょう。API Gateway (remote)を選んで実行ボタンを押してみます。

f:id:glpgsinc:20171204160122p:plain

するとこのように、レスポンスが帰ってきました。ついでにcurlもできました。デプロイ時に自分でパッケージをzipしたりとかする必要はないようです。

Runtimeを変えてみる

今度はRuntimeを変えてみましょう。まずNode.jsで新しいLambda関数を作ってみます(今回もAPI Gatewayをトリガにしました)。サクッと作ってデプロイしてテストしてみます。

f:id:glpgsinc:20171205105849p:plain

動作しました。ところで、この関数は以前作ったもので、AWSからサポート期限の案内があったと想像してみます。うぅん。Node.jsはサポート期間が短くて、定期的にランタイムを変更したりするのが、ちょっと面倒ですので、この際ですからPythonに変えてしまいたいですね。

というわけで、前のステップで作成したコードをコピーして、template.yamlを変更してみます。

変更前

...(前略)
Resources:
  node2py:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: node2py/index.handler
      Runtime: nodejs6.10
      ...(後略)

Node.jsの関数ができています。

f:id:glpgsinc:20171205110816p:plain

変更後

...(前略)
Resources:
  node2py:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: node2py/lambda_function.lambda_handler
      Runtime: python3.6
      ...(後略)

なんのことはなく、HandlerとRuntimeを変更しただけです。ではデプロイして実行してみます。

f:id:glpgsinc:20171205111009p:plain

実行できました。Lambda側のコンソールを確認すると、ちゃんとRuntimeが変わっています。

f:id:glpgsinc:20171205111144p:plain

注意点?

Lambdaコンソールの側から関数を変更したら、その後Cloud9側からデプロイしても反映されなくなったり、Lambda関数を削除したら2度とデプロイできなくなったりしました。一応Lambdaコンソールを開いたときに「この関数はCloudFormationで管理されています」みたいなやんわりとしたメッセージは出てくるのですが、それ以外に何事もなく普通に編集も削除もできてしまうので、注意が必要かもしれません。

また、メニューなどをつつきまわしてみましたが、Cloud9からリモートを削除するみたいなことはできないみたい……?

さいごに

いくつか注意点などはありそうですが、ローカルでAPI Gatewayまで含めたテストができるのは便利ですね。

ところで! 明日はなんと〜ガラパゴスのアプリエンジニアがTensorflowしてしまうそうです! 読んでいただけると嬉しいです♪


さて、今日はAWS Cloud9を軽く眺めてみました。ガラパゴスでは、re:Inventで発表されたサービスを早速触ってみたいエンジニアを大大大募集しています。皆様の応募お待ちしています。

では、ご機嫌よう。

この記事は業務の一環として業務時間中に書きました

Android Proguardによるソースコード難読化まとめ

はじめまして。Android(たまにiOS)エンジニアのほかりです。 最近はKotlinに興味津々です。 でも今回はKotlinの話はせず、Proguardの話をしようと思います。

Proguardとは

AndroidにおけるProguardとはアプリ(apkファイル)に難読化処理を施すツールのことです。 難読化されていないアプリはリバースエンジニアリングを行うとソースコードなどを見られてしまう恐れがあります。 Proguardをかけることによってソースコード上のクラス名やメソッド名などが何の意味も持たないようなアルファベットに置き換わります。 こうすることでソースコードを解読する難易度を高めAPKファイルのセキュリティ性を高めます。

Proguardを有効にすることで以下のようなメリットがあります。

  • ソースコードの難読化
  • パッケージサイズの軽量化
  • パフォーマンスの向上?

さっそくProguardを有効にしていきましょう!!

Proguardの有効化

Proguardの設定方法について説明する前に前提として本記事ではAndroidStudioでAndroidアプリの開発をしているものとします。 app配下のbuild.gradleファイルに以下を記述します。

android {
   ...
 
    buildTypes {
        release {
            minifyEnabled true // デフォルトだとここがfalseなのでtrueにします。
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
            'proguard-rules.pro'
        }
    }
  }

上記の設定でbuildTypeがreleaseのときのみProguardが有効になります。 実際の設定はapp/proguard-rules.proに記述していくことになります。 次にapp/proguard-rules.proに記述する内容を見ていきましょう!

Proguardの設定

先ほどの工程でProguardを有効にしているのにも関わらずproguard-rules.proに何も記述しないとリリースビルド時に全てのコードに難読化をかけてしまいます。 この場合、単純な実装の場合は特に問題は起きないのですが、アプリ開発はそう単純なことで終わることもなく以下のようなことをしているととっても面倒なことになります。(そんなメソッドはねーよ!とか言ってきます。)

  • AndroidManifest.xmlでのみ参照されるクラス
  • JNIから呼ばれるクラス
  • 動的に参照されるフィールドやメソッド
  • ライブラリを導入している

というわけで設定をしていきます。基本的な文法は以下のようになります。

-keep class パッケージを含むファイル名(パッケージ内の全てを対象にする場合は**でOK)

と難読化をかけたくないものに対してkeepオプションを設定してあげると難読化の対象外となります。 他にも下記のようなオプションがあります。

オプション 詳細
optimizationpasses 最適化の処理回数の指定。デフォルトは1回。複数回実施することで処理結果の向上が期待できます。apkファイルのサイズに影響します
dontoptimize 最適化を行わない
dontusemixedcaseclassnames 最適化処理で大文字と小文字を混ぜたクラス名に変更しない。Windows環境でこのオプションを指定しないと、ファイル名の大文字小文字の区別がされないため上書きされる可能性がある
dontskipnonpubliclibraryclasses 非publicなライブラリクラスをスキップしないようにする
dontpreverify 処理済みファイルの事前検証を行わないようにし、VMの読み取り速度を高速化する
dontwarn 警告を握りつぶす。警告の原因が明らかで無視してもよい場合は記述してよいが、それ以外は根本解決とならず危険
verbose 処理中の情報を詳しく書き出す
optimizations 最適化対象
・ !code/simplification/arithmetic
→ 算術命令に対してヒープホール最適化を行わない
・ !field/
→ fieldで始まるすべてのフィルタを除外する。
・ !class/merging/

→ クラス階層においてクラスのマージを行わない。
keep クラスとクラスメンバをリネーム、削除しない
keepnames クラスとクラスメンバをリネームしない
keepclassmembers クラスメンバをリネーム、削除しない
keepclassmembernames クラスメンバをリネームしない
keepclasseswithmembers クラスメンバが存在した場合のクラスとクラスメンバをリネーム、削除しない
keepclasseswithmembernames クラスメンバが存在した場合のクラスとクラスメンバをリネームしない

アプリ開発をしている上でProguardの設定を必要とされる多くのケースはライブラリを導入したときかと思いますが、ちゃんとREADMEが書かれているライブラリであればProguardの設定も記載されているのでそのままコピペで大半はうまくいきます(とはいえリリースビルドしたアプリの動作確認を推奨します)。

Proguardの設定が書かれていない場合は以下の2通りが考えられます。

  • Proguardの設定を必要としない
  • Proguardの設定が実は必要だけど書いていない

前者は全く問題ありません。後者は本当に最悪です。同じ問題に遭遇しすでにStackOverflowとかで解決策があることを祈ってググるか、コードを読むなり、実際に動かしてみるなりして設定が必要な部分を調査する必要があります。

注意すべきこと

ライブラリに関しては先ほど述べたようにREADMEに記載されているものをコピーすれば良いです。

注意すべき点は自分(またはチームの誰か)が書いたコードです。

これはググっても出てくるわけでもないので自分で設定するしかありません。

ほとんどの場合はリネームをしても問題はないのですが、リフレクションなどをしている場合はそうではありません。

getDeclaredField(String)では引数にフィールド名を指定してアクセスするのでProguardによってリネームされると動きません。

そして厄介なのはこの場合コンパイル時エラーにはならずその機能が動かないだけということになります。(try/catchするので)

幸いAndroidソースコードオープンソースなので難読化をかけなくても問題と思うのでリフレクションを使う場合は対象のクラスを除外する設定をしましょう!

おまけ

FirebaseやCrashlyticsなどクラッシュレポートツールについてです。

これ自体は実際にユーザーが発生したエラーのスタックトーレスとかが見れてとても便利です。 そう。難読化されていなければね。

Proguardが有効になっているアプリがクラッシュし、クラッシュレポートが送信されるとそのスタックトレースももちろん難読化されています。

省略
at [パッケージ名].f.a.b(ProGuard:577)
省略

こんなものをみて修正するなんて現実的ではありません。 多くのクラッシュレポートツールでは難読化されたスタックトレースを復元してくれる機能が備わっています。

アプリをビルドした時に自動生成される/proguard/mapping.txtというものをアップロードすると アップロードした以降のスタックトレースは復元され表示されます。

Proguardを有効にしたアプリを運用していく場合はアップデートをするたびにmappingファイルも一緒にアップロードし直す必要があります。

ですが人間が手作業で行う作業でもあるため、うっかりアップロードし忘れるなんてこともなくありません。 アップロードし忘れるとやっぱり、

省略
at [パッケージ名].f.a.b(ProGuard:577)
省略

こんな感じのスタックトレースになるわけです。 しかし、ビルドした時のmappingファイルを持っている場合はローカル環境で復元することも可能なのです。

AndroidSDK/tools/proguard/bin/retrace.shが復元をしてくれます。

使い方は以下のコマンドをターミナルで実行するだけです。

sh [retrace.shまでのフルパス] -verbose mapping.txt [復元対象のStackTraceをコピペしたテキストファイル]

上記を実行するとターミナル上に復元されたスタックトレースが表示されます。

注意事項として使用するmappingファイルはクラッシュレポートが送られた時に実行されたアプリをビルドした時のものにしてください。 mappingファイルは基本的にビルドするたびに変更される(可能性がある)ので全く同じコードでビルドして生成されたmappingファイルを使ってもうまく復元されないことがあります。

さいごに

AndroidのProguard関連で悩んでいる人のお役に立てたら嬉しい限りでございます!