Mermaidで会話フロー図を作るだけで対話botが構築できるようにした & Slack上で動かしてみた

要約

Mermaidで会話フロー図を作るとそれを読み込んで対話botを構築してくれるスクリプトを作った。さらに、出来上がった対話 bot を AWS lambda + Slack API 構成で Slack 上で動くようにした。

出来上がった品

slack上での動作例

「@jiya hi」と入力すると会話スタート。質問してくるので、答えていくと、次に何するべきかを提示してくれる。会話フローはこんな感じで定義している。

会話フロー

この会話フローが Mermaid で記述されており、容易に変更できるようになっている。

スクリプトはこちら github.com

経緯

最近 Mermaid がマイブームで、設計図とかサービス間の連携の図とか、ちょっとした図はとりあえず Mermaid で記述している。だけど複数の分岐があるようなフロー図って、図にしてもぶっちゃけ読む気しないよなぁ...と思って、アキネイターみたいに対話形式で結論を出してくれるものが欲しくなった。せっかくならいろんな人(主に会社の人)に使ってもらえるように Slack アプリとして動かせるようにした。

技術要素

今回お世話になった技術

mermaid-js.github.io

いろんな図をマークダウンで記述・生成できるようにしたもの。GitHub や Notion で動くのが素晴らしい。

api.slack.com

Slack に色々な自作アプリを連携して workspace をカスタマイズできるようにするもの。

aws.amazon.com

簡単な処理をサーバーレスで行えるようにしてくれるサービス。

AWS と Ruby + Sinatra で自分用に本棚アプリを作った

要約

冬休みに AWS を勉強していて実践してみたくなったので簡単なアプリを作ることにした.作ったのは自分のホームページの管理アプリ.読んだ本や映画の感想を一覧にしているのだが,シンプルにhtml直書きのソースゆえ,いちいち手打ちで <tr> とか書かなきゃいけなくて管理がしんどかったので,Slack に投稿するだけで html をいい感じに書き換えてくれるアプリを作ることにした.

タイトルにもあるとおり,登場人物は AWS,Ruby+Sinatra,Slack API の3名である.

完成品はこちら

github.com

アプリの仕様

/addtonoerpage 種類 タイトル 監督 視聴のきっかけ 感想

アプリをインストールした Slack ワークスペースでこんな感じでスラッシュコマンドを打つ.種類は本(book),映画(film),アニメ(anime) の3種類.コマンドの実行が成功すると html を書き換えて新しいブランチにコミットを積んでくれる.登録情報と作成したブランチへのリンクが以下のように返ってくる.

f:id:ensyu3-141592653589793238:20210101011249p:plain
本棚への追加成功時のレスポンス

ブランチへのリンクを踏んでPRを作成してマージすればホームページの本棚が更新されるという算段.こんな感じのPRが出来上がる.

f:id:ensyu3-141592653589793238:20210101011951p:plain
本棚への追加のPR

欲を言えば PR を作るところまでやりたかったが,そのためには GitHub の API を叩かなきゃいけないっぽくて面倒だったのでブランチの作成に止めた.

スラッシュコマンドのフォーマットが正しくない場合はこんな感じで怒られるようにした.

f:id:ensyu3-141592653589793238:20210101012227p:plain
本棚への追加失敗時のレスポンス

大まかな構成

Slack API

Slack アプリの受け口.今回はスラッシュコマンドのみ利用.他にも,botにメンションを送るなどのイベントをトリガとすることもできる.

AWS Elastic Beanstalk

スラッシュコマンドが叩かれると Slack API は設定された URL に HTTP POST を送る(参考).ローカル環境でこのポストを受け取れるようにするのはちょっと面倒だったので AWS を利用することにした.AWS 便利.

ちょうど友人と遊びでアプリ開発をしていて,その時に Elastic Beanstalk を使ってみてたところだったので,今回も同じく Elastic Beanstalk を利用することにした.

AWS 上で Web アプリが動く環境を整えようとすると,EC2・S3・ELBなどなど,さまざまな設定を行う必要がある.Elastic Beanstalk はその辺りの各種設定をいい感じに行ってくれて,利用者はアプリ本体の開発にのみ専念できる.最低限動くソースが提供されていたので,今回のアプリ開発はそれに手を加えるだけでよくてお手軽だった.

アプリ本体(Ruby+Sinatra)

Slack API から HTTP POST を受け取って処理を実行する部分.今回は作品情報を受け取ってファイルを書き換えるだけの簡単な処理なので,手軽にWebアプリケーションを作成できるSinatraを利用した.

処理の流れはざっと以下

  1. 受け取った作品情報のフォーマットを検証.
  2. 検証結果に基づきレスポンスを返却.フォーマットが正しい場合はファイル書き換えに必要な情報をインスタンス変数に保持しておく.
  3. レスポンス返却後,インスタンス変数に格納した情報を元に html ファイルを書き換えてコミットを積む.

先にレスポンスを返してからファイル編集の処理を行っているのはスラッシュコマンドのタイムアウト除けのため.3秒以内にレスポンスを返せなければタイムアウトとして処理されてしまうから.結構シビア.

デプロイ方法

私以外にこのアプリをデプロイする旨みのある人はいないけれど,アプリ作成の参考になるかもしれないのでデプロイ方法を詳しめに書いておく.

AWS Elastic Beanstalk の設定

AWS Elastic Beanstalk 上でアプリケーションを作成する.この辺りを参考にいい感じに設定していってください. ざっと設定を列挙します.

項目 値
環境枠 Webサーバー環境
プラットフォーム Ruby
プラットフォームのブランチ Ruby 2.7 running on Amazon Linux 2
プラットフォームのバージョン 3.2.1
アプリケーションコード コードのアップロード
ソースコード元 ローカルファイル

ローカルファイルは以下のようにして作成した zip ファイルを選択.

git clone https://github.com/NomotoEriko/NoerPageManager.git
cd NoerPageManager/bookshelf
zip ../bookshelf.zip -r * .[^.gD]*

以上を設定し環境の作成をクリックする.

しばらく待つと環境が作成されてアプリが立ち上がる.画面にアプリケーションへのURLが表示されるのでそのリンクをクリックして,画面に OK が表示されたらデプロイ成功.環境変数の設定がまだなので,この時点ではデプロイに失敗するかもしれない.

Elastic Beanstalk の管理画面でサイドバーからこの環境のタブを開き,設定をクリックする.ソフトウェアとかインスタンスとかの設定が一覧で表示される.ソフトウェア設定の編集から環境プロパティを編集できるので,本アプリに必要な各種環境変数を設定する.

変数名 値
GIT_REPOSITORY_URI ホームページのリポジトリのURI.認証を通すためにhttps://USER_NAME:PASSWORD@github.com/USER_NAME/REPOSITORY_NAME.git というフォーマットにしておく必要がある.
GIT_USER_NAME コミットを積むユーザー名.雑にNoerPageManagerにした.
GIT_USER_EMAIL コミットを積むユーザーのメールアドレス.雑に自分のメアドを設定した.

ちなみに GIT_REPOSITORY_URI の PASSWORD は GitHub で 2 段階認証をしている場合はパスワードではなくアクセストークンとなる.作成方法はこちらを参照.

環境変数を設定すると再度デプロイが走るのでしばらく放置する.

Slack API の設定

公式ドキュメントを参考に Slack アプリを作成する.Slack アプリの Slash Commands からスラッシュコマンドを新規作成する. Request URL には先ほど AWS で作成した Web アプリのURLの末尾に /add をくっつけたものとする. 好きなワークスペースにこの Slack アプリをインストールして完了.

工夫したポイント

開発してて工夫したポイントを,開発時の時系列順に書いていく.

手元で動作確認する

いくら手軽とはいえ動作確認にいちいち Elastic Beanstalk を経由していたらキリがないので,早い段階で手元で動作確認する方法を確立した.

ローカルでのアプリの立ち上げ

これはそんなに難しくない. bundle exec rackup -o 0.0.0.0 で立ち上がる.

Slack API からのリクエストを模倣する

これには Postman を利用した.

www.postman.com

Slack API のドキュメントをよく読むと,スラッシュコマンドのデータは application/x-www-form-urlencoded 形式で送られると書いてあった.

This data will be sent with a Content-type header set as application/x-www-form-urlencoded.

Postman アプリでこんな感じで x-www-form-urlencoded を選択して POST を送ることで Slack API からのリクエストを模倣することができた.

f:id:ensyu3-141592653589793238:20210101022430p:plain

Elastic Beanstalk でデプロイに失敗した時の原因調査

いくら手元で動作しても,Elastic Beanstalk でのデプロイで失敗することはある.Elastic Beanstalk を使い慣れていないこともあり,原因調査に戸惑った.

とりあえずログを確認してみる

Elastic Beanstalk の環境管理画面に「ログ」という項目があったので開いてみると,「ログのリクエスト」からログを取得することができた.ただ...正直あまり参考にならなかった.ちゃんと設定すればええんやろうなぁと思いつつ,ログは参考程度に見るだけに留めた.

EC2 インスタンスを見にいく

デフォルトだと EC2 はキーペア(ssh 接続に利用する公開鍵と秘密鍵のペア)無しで作成されるのでアクセスできない.環境変数を設定するのに利用した「設定」画面のセキュリティ欄にEC2 キーペアという項目を見つけたので,そこからキーペアを登録した.

キーペアさえあれば EC2 に ssh 接続できる.具体的な接続方法についてはこちらを参照.

EC2 インスタンスに接続してとりあえずその場で ls してみたけれど,なんにもなくて一瞬面食らった.めげずにどこかにあるはずのソースコードを探そうと find / -name app.rb 2>/dev/null を実行./var/app/current/ に目当てのコードがあることがわかったのでそこに移動した.

あとはソースのディレクトリの状態を確認したり,環境変数を確認したり,その場で ruby のコンソールを立ち上げてみたり,ログを確認してみたりといういつものデバッグ作業を行うだけ.


ここまではどちらかといえば作業環境を整える上での工夫だった.以降は毛色が変わって実装上の工夫について話す.

Ruby から git 操作を行う

ローカル環境で git 操作を行うのはそんなに難しくなかった.普段行っている git 操作を Ruby 経由で叩くだけだから.いちいち Kernel.#system を呼ぶのは面倒だったので gem を導入した.

github.com

躓いたのは AWS の EC2 インスタンスで上での git 操作.

git をインストールする

まず引っかかったのは,そもそも git が環境に入っていなかったこと.これは .ebextensions を設定することで解決した.

docs.aws.amazon.com

本棚アプリのソースを参照してもらえれば,.ebextensions フォルダ内に git インストール用のファイルが入っているのがわかる.

github の認証を通す

何も考えずにソースをデプロイしたら,デプロイ自体には成功しているけれどスラッシュコマンドの実行に失敗していた.EC2 インスタンスに ssh 接続して原因を調査したところ,ブランチの作成,コミットまではうまくいっていたが,どうやらブランチのプッシュに失敗しているようであった.試しにその場で ブランチをプッシュしようとしたら,ユーザー名とパスワードを聞かれた.Ruby から push を成功させるにはユーザー名とパスワードを聞かれないようにする必要がある.

Qiita とかで調べたところ,HTTPS でユーザー名,パスワードを聞かれないようにするには,origin の URL にユーザー名とパスワードを含めれば良いということがわかった.GIT_REPOSITORY_URI をそのように変更して無事認証の問題が解決した.

3秒以内にレスポンスを返す

認証も通ったしこれで動くやろと勇んでスラッシュコマンドを実行したら,今度はタイムアウトで失敗した.Slack 上では失敗しているけれど,実はブランチのプッシュまで行われているという惜しい状況だった.

Slack API の仕様上,レスポンスは 3 秒以内に返さないとタイムアウトとして処理されるらしい.ブランチを作成してコミットを積んでブランチをプッシュするのを3秒以内に完了するのは不可能なので,先にレスポンスを返してからそれらの処理を行う必要がある.

重い処理を分離

何はともあれとりあえず重たい処理を分離することは必須だったので,ひとまずそれを行った.軽い処理はリクエストデータを評価してレスポンスを構成する部分,重い処理はそれ以外という分け方にした.

次に重い処理に必要な材料をリクエストデータなどから構成してインスタンス変数に入れた.これによりインスタンス変数を介して軽い処理と重い処理を分離できる.あとは重い処理をメソッドに切り出して完了.

after の利用

Sinatra には before と after というフィルタがあり,それぞれリクエストのルーティングの前後で呼ばれる.とりあえず after に重い処理を移して試してみた.だが,after 評価後にレスポンスを返すらしく,効果がなかった.(ドキュメントをちゃんと読んだら書いてあった)

Thread の利用

Thread は並行プログラミングを行うための Ruby のクラスである.簡単にいうと,同時に複数の処理を行う,みたいなことを実現することができる.

doc.okkez.net

Thread を生成してその中で重い処理を行えば,レスポンスを返しつつ重い処理を並行して行うことができる.

Mutex の利用

重い処理実行中に新たなリクエストがきた場合,ファイルとかコミットログとか branch がおかしなことになることが容易に想像できる.同じファイルを複数人で編集している場面を想像してもらえれば良いと思う.このようなことを防ぐため,重い処理を行っている最中は新たな重い処理を実行しないようにする必要がある.ちなみに,このような制御を排他制御という.

今回は Mutex クラスを利用して実装した.

docs.ruby-lang.org

グローバル変数に Mutex インスタンス m を置いておき,重い処理中は m をロックする.スラッシュコマンド受け取り時に m の状態をみて,ロック中であれば「処理中です。少し時間をおいて再度お試しください。」というメッセージをレスポンスとして返すことにした.

おわりに

AWS の実践をしたくて作り始めたアプリだけれど,github の認証とか Ruby での排他制御の実装とか思いがけない収穫があった.楽しかった.

このアプリは AWS の本を読む合間に息抜きがてらゆるっと 2 日で作った.ずっと本を読むだけだとどうしても集中力が切れてしまうのでね.

このブログを書いて思ったけど,重い処理をわざわざ after のなかでやらんでもええなぁ.ま,いっか.

東京都内勤務新卒OLの生活環境

あまりに暇なのでこっちに引っ越してからの生活環境をまとめてみることにした.折角色々考えて生活しやすいように家具とかを揃えたので,誰かの参考になったら嬉しい.

立地

  • 最寄り駅まで徒歩10分
  • 最寄りのスーパーまで徒歩10分
  • 最寄りのコンビニまで徒歩2分

住んでみて気づいたけどここめっちゃ便利.駅の周辺には飲食店も多い.covid-19 の影響であまり回れていないけど,自粛解禁されたら行ってみたいお店がいっぱいある.

住居

結構いい条件が揃っている物件を選んだんだけど,住んでみて特に重宝している部分をピックアップしていく.

部屋は10畳くらい(ただし収納多め)

10畳だから手狭と思うじゃん.収納が多いから全然広々してる.収納があるかどうかってめっちゃ重要.

いろんなところに収納がある.クローゼットやキッチンの下の部分は言わずもがな,キッチンの上とか,洗面台の上とかトイレの上とかにもしれっとある.高いところってデッドスペースになりがちなので,そこに最初から収納がついているのは嬉しい.ただ,身長が低い人は届かなくて結局使わないかもしれない.一応踏み台は買ったけど,結局踏み台は滅多に使わず,普段は背伸びしてる.

グリル付きキッチン

グリル超便利.パンを焼ける,シャケやししゃもやホッケを焼ける,餅を焼ける,ホイル焼きもできる.

適当に動物性の食材ときのこと玉ねぎをアルミホイルに乗せて,上からワインと味噌と塩を混ぜたソースをかけて包んで,グリルで弱火で 20 分くらい放置したらそれだけでそこそこ美味しいホイル焼きになる.グリルすごい.

宅配ボックス

荷物受け取らなくていいの嬉しい.荷物が届くのを待つ時間て,何もできないじゃん.ちょっと散歩に行くこともできないし,風呂にも入れない.トイレに行くのすら躊躇するレベル.そのストレスが皆無になる.最高.

家具

我が家のコンセプトは可動性.据え置きタイプの家具を最低限にし,仕舞う・移動するが簡単に行えることを重要視した. 特に工夫した家具について紹介する.

ソファーベッド

前節で述べたけど,私の部屋は10畳.狭くはないが,広くもない.そこに普通のベッドでも置こうものなら途端に生活スペースが狭くなってしまう.ベッドなんて寝るときにしか使わないのに,昼間もベッドに部屋を占領されるなんて許しがたい.したがって,ベッドは折り畳めるか,ソファーになるか,そういう機能を持ったものが最低要件だった.

ベッドを選ぶ上での要件は以下

  • 昼間邪魔にならない
  • 安全に引きずって動かせるくらいの重さ
  • 寝返りが打てるくらいの広さ
  • 寝心地は多少妥協可能(研究室の床でも寝られるので)

結局買ったのはこれ

www.nitori-net.jp

普通に座り心地がいいので,平日のリモートワーク中はずっとこのソファーに座っている.

プロジェクター + Fire TV Stick

大学時代ずっとテレビのない生活を送っていて,テレビは不要という結論に達していた.映画やアニメを観るのは好きなので,それに特化した構成にした.あと,リングフィットアドベンチャーをプレイしてみたいので,映像位置が低すぎないことや,部屋を広く使えることを重要視した.

モニターなどの置くタイプは映像は綺麗かもしれないが,画面がちょうどいい高さにくるように台座が必要になる.また,コンセントの位置は固定なので,結局据え置きになってしまう.一方プロジェクターで壁に投影する場合,映像の質は落ちるが,不要な時はプロジェクターを仕舞うことができるし,画面位置の調整も簡単.そもそも視力が低いので映像の質にはあまりこだわりがない.

映画・アニメを観るのにはメディアストリーミング端末が良いと聞いたので,Fire TV Stick を購入.Fire TV Stick にした理由は元々 Amazon と Netflix 利用者だったため.

Fire TV Stick は HDMI 端子があればよく,Switch も同様.したがって,プロジェクターの要件は以下.

  • HDMI 対応
  • 昼間でもみれるくらいの明るさ
  • 投影に必要な距離が部屋の横幅未満

色々調べて,dreamio がいいな〜と思ったけど予算の都合で断念.

www.epson.jp

結局適当に WiMiUS を購入

起動時にブーンという音がうるさいので,いつか dreamio に買い換えるかも.

棚

元々棚は買うつもりがなかったのだけど,安いプロジェクターを買ってしまったために,ちょうどいい高さに投影するには棚が必要になってしまった.まぁよく使う物を置くのに最低限の棚はあったほうが便利なことに買ってから気づいた.

ここでも可動性重視で,高さはあるが奥行きや幅は小さめのものにした.あと,なんとなく奥が見えた方が広々して見えるかなということで,こちらを購入

www.muji.com

www.muji.com

中段は高さが調節できるので,組み立ててみて一番使い勝手の良い高さにした.

低い方の棚はソファー横に据えて,一番上にプロジェクターを,一番下に wifi ルーター類を置いて,中段はメモ帳とかリモコンとかを置いている.

高い方の棚は部屋に入ってすぐのところに据えて,一番下に炊飯器を,一番上に湯沸かしポットと蕎麦茶を置いている.大体この棚の上で温かい飲み物を淹れている感じ.中段上部にはハサミや紐,ガムテープなんかを置いている.主にゴミの分別用.中段下部にはお菓子と読みかけの本を置いている.この段は正直置くものがなさすぎてほぼインテリア.

洗濯機

ドラム式の乾燥機能付きの洗濯機.ボタンを押すのと仕上がった洗濯物をしまうのとだけでお洗濯が完了する.最高.

jp.sharp

値は張るけれど,ボタンを押せば放置して出かけてもOKなことは値段以上の価値がある.あと,私は花粉症持ちなので,部屋の中で家事が完結するのはとても嬉しい.実家とか帰るとベランダで干した布団で花粉症発動して全然寝れないとかざらなので.

生活

最近のタイムテーブルをざっと紹介する.

平日

8:30 起床

元気があれば朝食を食べる.バナナとかパンとか.元気がない場合はギリギリまで寝てる.

9:00~12:00 リモートワーク

お仕事に関してはこっちのエントリ読んで

tech.retrieva.jp

12:00~13:00 お昼休憩

10分くらいで簡単にパスタ茹でたり,インスタントラーメンを作ったりしている.あと,茹でてる間にサラダ作ってるかな.

【超絶簡単パスタのレシピ】

  1. フライパンに冷凍のシーフードミックスを一掴み投入
  2. フライパン半分弱くらいまで水を入れる
  3. 白だし,めんつゆ,醤油,ハーブソルト,チューブにんにくなど,目についた調味料を適当にフライパンに投入
  4. フライパンを中火にかける
  5. パスタを一掴みとって,真ん中で両手をぴったりとくっつけて,雑巾絞りの要領でねじ切る
  6. 水が沸騰して縁が泡立ってきたらパスタを投入.手早くかき混ぜる.
  7. 5分ほど放置

スープパスタの出来上がり.お好みでパルメザンチーズとか黒胡椒とかかけると良いと思う.

【超絶簡単サラダのレシピ】

  1. 生で食える系の野菜(わさび菜とか水菜とか)を手でちぎってザルで洗う
  2. 水気をよく切って小鉢に盛る
  3. オリーブ油とポン酢を1:1の割合で野菜にかける
  4. 仕上げにハーブソルトを振る

サラダの出来上がり.よく混ぜて食べよう.オリーブ油とゆずぼんの割合がずれると味が落ちるので注意.

13:00~18:00 リモートワーク

こっちのエントリ読んで(2回目)

tech.retrieva.jp

18:00~20:00 晩ご飯

夜は少し手の込んだ物を作りがち.青椒肉絲とか,カレーとか.あとは鳥ささみのグリル焼きとか,残り物でホイル焼きとかかな.やる気のない時は作りおきの冷凍餃子とか目玉焼きとか焼きシャケとか食べてる.

【超絶簡単青椒肉絲のレシピ】

  1. 細切りの筍を買ってくる
  2. ピーマンを筍と同じくらいの幅に切る.
  3. 豚肉を繊維に対し順方向に細切りにする.これも筍と同じくらいの幅.予め広げた状態でラップに包んで冷凍しておくと,この処理が楽.
  4. ボウルに白味噌,ポン酢,醤油,砂糖,みりん,塩を入れて混ぜてソースを作る.白味噌とポン酢以外の調味料の分量はクックパッドを参考にしてください.
  5. ボウルに筍,ピーマン,豚肉を入れてソースと合える.
  6. 30分常温で放置.アニメでも観てて.
  7. フライパンにごま油を引いて気持ち強めの中火で熱する
  8. 油が熱くなってきたらボウルの中身を投入.
  9. 適当にフライパンを振って焦げ付かないようにだけ注意しつつ,火を通していく.このときノリで黒胡椒をかけても良い.完全に火が通ったら完成.

皿に盛って粗熱が取れてから食べる直前くらいにごま油をかけると更に美味しいよ.ごま油の風味は熱で割と飛ぶから.

【超絶簡単ホイル焼きのレシピ】

  1. 冷凍庫からシャケまたは鳥ささみを取り出してアルミホイルの上に置く
  2. 残り物の舞茸とかエリンギとかを良い感じの大きさに手でちぎって上記食材にまぶす
  3. 小鉢に白味噌と白ワインを 1:1 の割合で投入しよく溶かしてソースを作る
  4. ソースにお好みでみりんとか白だしとか麺汁とか良い感じの調味料を入れる
  5. アルミホイルの縁を良い感じに立たせて箱状にして,ソースをかける
  6. アルミホイルを閉じてグリルで弱火にかける
  7. 美味しそうな匂いがしてくるまで放置.大体20分
  8. 焼き加減を確認する.良い感じに焼けるまで見守る.焦げた匂いがしてきたらすぐに火を止める.

※表面が焼けてても裏が焼けていないことがあるので注意.そういう時はひっくり返して3分くらい弱火にかけよう.

20:00~22:00 自由時間

適当にアニメとか映画とか観てる.というか,晩ご飯を食べながら観始めている. 最近は YouTube で花江夏樹のゲーム実況動画を観てることが多いかも.

youtu.be

いやー,Fire TV Stick 便利だわ.

22:00~24:00 寝支度

毎日床をクイックルワイパーで掃除している.ソファーベッドなので,床掃除しないとソファーが汚れちゃう.

掃除の後は筋トレ.床にヨガマットを敷いてプランクとリバースプランクとスクワットをしている.汗をかくのでお風呂の前にする.元気な時は最後にストレッチをしている.適当に足を広げて上体を倒すやつとか,うつ伏せから上体を起こして海老反りするやつとか.筋トレを始めたのは5月に入ってからなのだが,筋トレを始めてから食べる量が増えて,調子が良い日が増えた気がする.続けていきたい.

筋トレで汗をかくのですぐに入浴.普段はシャワーで済ませることが多い.寒い日なんかは湯を張って無印の入浴剤を入れて入るけど,最近は無印の入浴剤が売り切れがちで悲しい.

休日

弊社は完全週休2日制なので,土日が完全に休み.大体土曜日は本当に何もせず過ごして,日曜日は買い出しとかしている.

買い出し

スーパーで食材を買っている.サラダ用の野菜と,バナナとかのフルーツと,卵は大体いつも買っているかな.シャケや肉は冷凍保存しているから,切らしたら買うくらい.後はパスタとか,食パンとか,米とか.米は2kgの無洗米を買っている.大体2週間で食べ切る.

買ってきた食材の保存

肉...1回で使う分を取り分けてサランラップに包んでいる.このとき豚肉とかはちゃんと広げておくと,青椒肉絲を作るときに便利.

魚...1回分に切って,主要な骨を取り除いてからサランラップに包んでいる.こうしておくと使うときに解凍せず直接フライパンやグリルに突っ込むことができる.

野菜...常温保存,冷蔵保存だけ分けている.大葉は足が速いので,キッチン用のポリ袋に入れて水に浸して口を結んでいる.

パスタ...パスタ用の瓶に詰めている.これによりキッチン下収納をちょっと効率的に使える.

米...前の米を使い切ったら米櫃に入れている.2kg用の米櫃なので,ザーッと入れたらすぐに袋を捨てられるのが嬉しい.

餃子の作り置き

先々週初めて餃子を作って,便利さに感動した.チマチマと40個くらいまとめて作って,5個単位でサランラップに包んで冷凍している.10分くらいで焼けるので,平日に重宝する.

上記は餃子作り2回目の作品.大学時代に陶芸部で培った手先の器用さがこんなところで役立つとは思わんかった.

トイレ掃除

大体月1~2回くらいのペースでトイレ掃除してる.めんどくさがりなので流せるトイレブラシを使って,ざっとゴシゴシしたらそのまま流している.簡単.

まとめ

今回は私の東京での生活環境について紹介した.全ての工夫は「(平日の)家事を極力簡単にする」にまとめられると思う.宅配ボックス,乾燥機付き洗濯機,無洗米,解凍せず調理を開始できる冷凍保存,ラグなどを敷かず家具は簡単に移動でき拭き掃除だけで済ませられる部屋,流せるトイレブラシなどがそうだ.元来面倒くさがりなので,家事が面倒だとそのうちしなくなってしまう.一つ一つの家事を簡単にすることで無理なくサボらず,文化的な暮らしができている.余った時間で手の込んだ料理を作ったり,読書に耽ったり,映画を観たり,好きなことができる.

入り込める新しい趣味が欲しいというのが目下の悩みか.まぁ無限に映画を観ても良いのだけれど,もう少し趣味っぽい趣味が欲しいよなぁ.

How to Install pyrouge

要約

pyrouge を入れるのに手間取ったのでメモ

pyrouge とは文書要約タスクで利用される評価指標である ROUGE を Python で計算するモジュールです.今回は Docker で環境構築しております.

最初に答えを述べておくと,以下の Dockerfile で pyrouge がインストールできます. ベースが pytorch/pytorch:0.4.1-cuda9-cudnn7-devel なのは私の都合です.

FROM pytorch/pytorch:0.4.1-cuda9-cudnn7-devel

RUN apt-get update && apt-get install -y libxml-parser-perl && apt-get clean
RUN git clone https://github.com/bheinzerling/pyrouge /opt/pyrouge && cd /opt/pyrouge && python setup.py install
RUN git clone https://github.com/andersjo/pyrouge.git /opt/rouge && pyrouge_set_rouge_path /opt/rouge/tools/ROUGE-1.5.5
RUN cd /opt/rouge/tools/ROUGE-1.5.5/data/ && rm WordNet-2.0.exc.db  && ./WordNet-2.0-Exceptions/buildExeptionDB.pl ./WordNet-2.0-Exceptions ./smart_common_words.txt ./WordNet-2.0.exc.db

解説

第一章:始まり

最初は pip install pyrouge しかしていませんでしたが,

FileNotFoundError: [Errno 2] No such file or directory: '/root/.pyrouge/settings.ini'

が出て怒られました.

instllation をよく読むと, pyrouge_set_rouge_path /absolute/path/to/ROUGE-1.5.5/directory とあります.ROUGE-1.5.5 てなんや?と思ってググると,どうやら これ のことで,pyrouge はその Python ラッパーのようです. ということで,

$ git clone https://github.com/andersjo/pyrouge.git /opt/rouge
$ pyrouge_set_rouge_path /opt/rouge/tools/ROUGE-1.5.5

しました. 結果はダメで,

FileNotFoundError: [Errno 2] No such file or directory: 'pyrouge_write_config_file.py': 'pyrouge_write_config_file.py'

と怒られました.

第二章:ソースからインストール

困りながらググっていると,この記事に出会いました.どうやらソースからインストールすればいいようです.pip で入れた pyrouge を消して,以下のようにしてみました.

$ git clone https://github.com/bheinzerling/pyrouge /opt/pyrouge
$ cd /opt/pyrouge
$ python setup.py install
$ pyrouge_set_rouge_path /opt/rouge/tools/ROUGE-1.5.5

結果はダメで,エラーをよく読むと

Can't locate XML/Parser.pm in @INC (you may need to install the XML::Parser module) (@INC contains: /opt/rouge/tools/ROUGE-1.5.5 /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.22.1 /usr/local/share/perl/5.22.1 /usr/lib/x86_64-linux-gnu/perl5/5.22 /usr/share/perl5 /usr/lib/x86_64-linux-gnu/perl/5.22 /usr/share/perl/5.22 /usr/local/lib/site_perl /usr/lib/x86_64-linux-gnu/perl-base .) at /opt/rouge/tools/ROUGE-1.5.5/XML/DOM.pm line 41.

とありました.どうやら perl の XML::Parser モジュールがないようです.

第三章:xml パーサのインストール

XML::parser のインストールは ここ を参考にしました.

$ apt-get update
$ apt-get install -y libxml-parser-perl

結果はまたダメで,

Cannot open exception db file for reading: /opt/rouge/tools/ROUGE-1.5.5/data/WordNet-2.0.exc.db

と怒られました.

最終章:WordNet-2.0.exc.db 作り直し

この issue を参考に,もともとある WordNet-2.0.exc.db を消して作り直しました.

$ cd /opt/rouge/tools/ROUGE-1.5.5/data/
$ rm WordNet-2.0.exc.db
$ ./WordNet-2.0-Exceptions/buildExeptionDB.pl ./WordNet-2.0-Exceptions ./smart_common_words.txt ./WordNet-2.0.exc.db

結果,ようやく python -m pyrouge.test が通りました.長い戦いだった.

終わりに

エラーから何が問題かわかりにくいやつは特に解決するのに手間取りました. 今回はどうしても pyrouge を使いたかったので環境構築を頑張りましたが,ROUGE スコアは n-gram の Recall ベースのスコアで複雑な処理はなさげなので,自分で実装してしまってもいいかもしれないです.

PyTorch で GPU 並列をちょっと詳しく

要約

PyTorch でマルチ GPU してみたけど,色々ハマったので記録に残しておく.データ並列もモデル並列(?)もやった.

メインターゲット

  • PyTorch ユーザ
  • GPU 並列したい人

前提知識

.

並列化したコード

どのようなコードを並列化したのかの説明を軽くしておく.必要なければ読み飛ばしてもらって構わない.

この論文 の自前実装.テキストを入力に画像を生成するモデル.テキスト→オブジェクト位置→オブジェクトの形→画像 の順に予測・生成していく. このモデルを訓練するスクリプトを並列化した.

モデル

主なコンポーネント
Component input output
box_generator テキスト オブジェクトの位置とラベルのペア(以後オブジェクト)の列
shape_generator テキスト,オブジェクト列 オブジェクトの形の列
image_generator テキスト,オブジェクトの形の列を一つの画像にまとめたやつ(セマンティックレイアウト) 画像
補助的なコンポーネント
Component input output
text_encoder (pretrained) テキスト(単語列) テキストベクトル
shape_discriminator テキスト,オブジェクトの形の列 そのオブジェクトが正例か shape_generator が生成したものかの 2 値
image_discriminator テキスト,画像 その画像が正例か image_generator が生成したものかの 2 値
vgg_model (pretrained) オブジェクトの形 or 画像 入力画像の内部表現(本来は画像分類モデルだが,今回は内部表現を利用する)
モデル図

f:id:ensyu3-141592653589793238:20190603172452p:plain
Hong et al., CVPR2018

特筆事項
  • box_generator は RNN な構造を持つ.shape_generator は RNN な構造を持つし,CNN も持つ.
  • vgg_model は vgg19 を利用.結構でかい.CNN.

.

並列化1:DataParallel

1 バッチを各 GPU に割り振る.例えば 4 GPU で DataParallel でバッチサイズ 256 のを回すと,一つの GPU がバッチサイズ 64 のバッチを処理する.

実装方法

PyTorch には DataParallel モジュールが用意されている.使い方はめっちゃ簡単. Document

device_ids = range(torch.cuda.device_vount())
model = DataParallel(model, device_ids=device_ids)

DataParallel が何をやってるかをちょっと詳しく

discuss.pytorch.org

同期更新型のデータ並列.以降,勾配計算のみを行う複数個の GPU を子 GPU,勾配の集約を行う唯一の GPU を親 GPU と言う. ※親 GPU 自身も勾配計算を行う.

1. Forward
  1. ミニバッチを各 GPU に均等に割り当てる.
  2. 親 GPU に乗っているモデルのパラメータ情報等を各子 GPU にコピーする.
  3. 各 GPU にて Forward プロセスを行う.
  4. Forward 結果を親 GPU に集約する.
2. 親 GPU にてロスを計算
3. Backward
  1. ロスを各 GPU に割り当てる.
  2. 各GPU にて Backward プロセスを行う.
  3. Backward 結果の勾配情報を親 GPU に集約する.
4. 親 GPU のモデルパラメータを更新

DataParallel に向かない設定

パラメータの多いモデル

イテレーションごとにモデルのコピーが発生するので,パラメータの多いモデルはコピーのオーバーヘッドが大きく,DataParallel の恩恵を受けられない.全結合層だけで構成されるモデルとかがその例.

基本的に畳み込み層で構成されていて,最終層だけ全結合層とかの場合は,全結合層を DataParallel の対象から外すというハックを行うことで大幅な高速化が期待できるそう.参考コード

自分でも全結合層を DataParallel の対象から外す処理を試してみた.イテレーションが 42秒から 40秒になるという,微妙な差.

小さいバッチサイズ

バッチサイズが小さくても,パラメータのコピーに対して Forward/Backward の計算が少なくなるので,DataParallel に向かない.十分なバッチサイズが確保できるよう,大きすぎないモデル設計を心がけるか,とてもメモリの大きい GPU を用意するかしよう.

RNN(特に LSTM)

DataParallel するには,batchFirst である必要がある.その他,色々とエラーが出て闇が深そう. さらに,LSTM はセルが複雑なことをするので速くならないらしい.説明をめちゃくちゃ端折ったので,興味がある人はこちらの議論を見てください.

.

並列化2:モデル並列 & multiprocessing

今回並列化したモデルは box_generator/shape_generator and shape_discriminator/image_generator and image_discriminator の 3 つに計算グラフを完全に分離できる.したがって,これらを別々の GPU に乗せたうえで各 GPU でイテレーションを並列実行することが割と簡単にできる. 計算グラフが切れないガチのモデル並列は一つ下のレイヤからいじる必要があって闇が深いので,この世のどんな GPU にも乗り切らないめっさでかいモデルを絶対に動かさなきゃいけない事情がない限り避けたほうがいいと思う.

実装方法

1. 各モデルを別々の GPU に載せる

以下のようにすれば良い.

device0 = torch.device("cuda:0")
device1 = torch.device("cuda:1")
device2 = torch.device("cuda:2")

model.box_generator.to(device0)
model.shape_generator.to(device1)
model.image_generator.to(device2)

box_input_tensor = box_input_tensor.to(device0)
shape_input_tensor = shape_input_tensor.to(device1)
image_input_tensor = image_input_tensor.to(device2)

ただし,全てのテンソルをちゃんと正しい GPU に割り当てる必要があるので,内部で .cuda() とかで雑に初期化していると別々の GPU にあるテンソルで演算しようとしてんじゃねぇよエラーが出る.頑張って修正しよう.

2. multiprocessing

モデルを別々の GPU に乗せただけでは並列に実行されないので,並列に実行するために multiprocessing する必要がある.Python の multiprocessing の PyTroch ラッパーがあるので,それを使う.

Document にサンプル実装がある.訓練フェーズを関数化して,multiprocessing.Process に渡せば,その訓練が fork されて独立に動き出す.

CUDA initialization error が発生する場合は,mp.set_start_method('spawn') を torch.manual_seed(args.seed) の前で宣言すると回避できる.参考

spawn は子プロセスを開始する方式の一種で,指定しない場合は多分 fork だった気がする.デフォルトでは子プロセスの中でも CUDA の初期化を行ってしまうせいでエラーが発生するらしい.(CUDA の初期化は 1 回しかしちゃダメ)

spawn はプロセスを一度 Pickle 漬けにするので,lambda などのPickle 漬けできない書き方を解消する必要がある.また,Pickle 漬け処理が重たいので,遅い.悲しい.

.

2 種類の並列化を試した結果と感想

どちらも lab のサーバだとそんなに速くならなくて悲しかった.それぞれ,「batch_size が小さすぎる」「spwan が遅すぎる」というネックがあり,それを解消できなかった. ABCI で試したところ,DataParallel で 1.3 倍くらいには速くなったので,それでやっていくことにした.

PyTorch の DataParallel は基本的に CV 系のモデルを想定していて,NLP 系のモデルに向いていないのが悲しかった.使う分には楽なので,使えるところで局所的に使うのが賢そう. multiprocessing はそもそも PyTorch でそこまでサポートされていなくて,エラー回避が大変だったし,効果が薄かった. DataParallel を(上手く)使うことをオススメする.

.

リファレンス

Hong et al., CVPR2018: S. Hong, D. Yang, J. Choi and H. Lee,``Inferring Semantic Layout for Hierarchical Text-to-Image Synthesis,'' In proc. of CVPR2018. pdf

flask プロジェクトの PyCharm 実行でハマった

PyCharm で flask プロジェクトを作って,PyCharm 上で実行したら,スクリプトで指定している設定が反映されなくて困った.

.

経緯

flask プロジェクトを作成

PyCharm で flask プロジェクトを作った.

pleiades.io

スクリプトを編集

app.py のメイン内で debug や host の設定をいじった.

from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello World!'


if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0")

実行 & 問題発生

PyCharm で app.py を実行した.

FLASK_APP = app.py
FLASK_ENV = development
FLASK_DEBUG = 0
In folder /Users/nomotoeriko/PycharmProjects/untitled
/Users/nomotoeriko/.pyenv/versions/3.5.0/bin/python -m flask run
 * Serving Flask app "app.py"
 * Environment: development
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

明らかに host="0.0.0.0" が反映されていないことがわかる.

.

真相

どうやら app.py を直接実行しているわけではないらしく,メインが呼ばれていなかった.開発環境とかその辺のアレで PyCharm がよしなにやってくれているのかなぁ.知らんけど.

.

対処

真面目な対処法

公式によれば,

Flaskサーバーの実行/デバッグ設定の対応するパラメータを編集することにより、Flask固有の変数を変更することができます。

とのこと.以下のページから,flask run のパラメータに --host があること,Flask デバッグモードの有効化には FLASK_DEBUG にチェックをつければ良いことがわかる.

pleiades.io

ということで,Edit Configurations から設定をいじる. f:id:ensyu3-141592653589793238:20190515002358p:plain

Additional options からホストを設定.FLASK_DEBUG にチェック.

動く ✌︎('ω'✌︎)

FLASK_APP = app.py
FLASK_ENV = development
FLASK_DEBUG = 1
In folder /Users/nomotoeriko/PycharmProjects/untitled
/Users/nomotoeriko/.pyenv/versions/3.5.0/bin/python -m flask run --host 0.0.0.0
 * Serving Flask app "app.py" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 285-077-691

雑な対処法

PyCharm の Terminal から app.py を叩く.

あとがき

思わぬ落とし穴だった.まぁ公式をよく読みましょうってことですね.でも Hello World までを自動生成してくれるのはさすが JetBrains という感じ.便利.

一緒に原因を考えてくれた I 氏と執筆にご協力してくれた T 氏に深く感謝いたします.

RandomCrop とかを入力画像と正解画像で統一する方法

経緯

PyTorch で画像系の学習をするときに, RandomFlip や RandomCrop をよく使う.最近研究関連で,オブジェクト位置,セグメンテーション,画像を順に生成していく既存モデルを構築しており,それらのデータでランダムな部分を統一する必要があった.なかなかいい感じの記事に出会えずどうやるか調べるのに苦労したので結論をまとめておく.

.

想定する読者

  • PyTorch で画像物体検知を実装しようとしている人
  • クラスの継承がわかる人

.

.

RandomCrop とかを入力画像と正解画像で統一する方法

RandomCrop とかを継承して,__call__ だけ上書きする.

from torchvision import transforms
from torchvision.transforms import functional as F


class UnitedRandomCrop(transforms.RandomCrop):

    def __call__(self, *args, **kwargs):
        img, segms, bboxes = args
        i, j, th, tw = self.get_params(img, self.size)
        cropped_img = F.crop(img, i, j, th, tw)
        cropped_segms = [F.crop(segm, i, j, th, tw) for segm in segms]
        cropped_bboxes = [self.crop_bbox(bbox, i, j, th, tw) for bbox in bboxes]
        return cropped_img, cropped_segms, cropped_bboxes

    @staticmethod
    def crop_bbox(bbox, i, j, th, tw):
        x, y, bw, bh = bbox
        start_x = max(0, x - j)
        start_y = max(0, y - i)
        stop_x = min(tw, x + bw - j)
        stop_y = min(th, y + bh - i)
        cropped_bbox = [start_x, start_y, stop_x - start_x, stop_y - start_y]
        return cropped_bbox

.

詳細

get_params でランダムなパラメータを取得している.このパラメータを使って,__call__ 内でデータに必要な変換を記述して返す.基本的には本家の記述に従えば OK.

get_params はこの issue で導入が検討された関数.オブジェクト検知タスクにおいて,ランダム部分を入力画像と正解画像(セグメンテーション)で統一したいという欲求が発生し,様々な議論がなされていた所,fmassa さんがいい感じにまとめて問題提起を行ったのがこの issue .この議論の結論は RandomCrop とかのクラスで,パラメータ作成と実際の画像の変換を分け,また,画像変換部分は関数として提供することである.これによってユーザが自由にサブクラスや関数を作れるようになるとのこと.

全ての RandomHoge クラスに get_params が実装されているわけではないので,注意が必要.執筆時点(2019/04/08)で get_params メソッドを持つのは RandomCrop RandomResizedCrop ColorJitter RandomRotation RandomAffine.例えば RandomFlip には get_param メソッドがない.確かに単純すぎて分けるほどではない気がする.

.

あとがき

英語の issue ツリーを読むの疲れた.

もしもっといい方法があるならぜひ教えてください!