THINKING MEGANE

SSD: Single Shot MultiBox DetectorをGoogle Cloud ML Engine上で物体検出APIとして利用する

画像内の物体検出と識別を行う予測APIが必要になったので、物体検出ニューラルネットワークであるSSD: Single Shot MultiBox DetectorGoogle Cloud ML Engine上で訓練して予測APIとして使えるようにしました。

使い方

Google Cloud ML Engineでモデル、学習時のパラメタ、予測APIのバージョンなどをコード管理するため、StarChartを使います。 コード管理が不要であれば、リポジトリのコードをCloud Storageにアップロードしてgcloudコマンドなどで訓練を行ってください。 訓練データセットなどをCloud Storageに配置した上で、以下のようにして訓練用のジョブを投入します。

$ pip install git+https://github.com/monochromegane/starchart.git
$ git clone https://github.com/monochromegane/ssd_mlengine.git
$ starchart train -m ssd_mlengine \
                  -M trainer.task \
                  -s BASIC_GPU \
                  -- \
                  --annotation_path=gs://PATH_TO_ANNOTATION_FILE \
                  --prior_path=gs://PATH_TO_PRIOR_FILE \
                  --weight_path=gs://PATH_TO_WEIGHT_FILE \
                  --images_path=gs://PATH_TO_IMAGES_FILE \
                  --model_dir=TRAIN_PATH/model

訓練が終わったら、以下のようにしてモデルを予測APIとして公開します。

$ starchart expose -m ssd_mlengine

公開された予測APIを叩くと結果が返ります。

$ python predict.py -k 1 -c 0.45 -i image.jpg
# {'predictions': [{'key': '1',
#    'objects': [[8.0,        # 検出した物体の分類区分
#      0.45196059346199036,   # 確率
#      104.90727233886719,    # 検出した位置(xmin)
#      97.99836730957031,     # 検出した位置(ymin)
#      212.12222290039062,    # 検出した位置(xmax)
#      315.3045349121094]]}]} # 検出した位置(ymax)

予測APIを叩くコードはREADMEを参考にしてください。パラメタは以下の通りです。

  • keep_top_k(-k): 確率の降順でソートされたうち、上位何件を取得するか
  • confidence_threshold(-c): 確率の閾値

実装

SSDのKeras実装であるrykov8/ssd_kerasを参考にGoogle Cloud ML Engineで訓練、予測APIとして利用できるようにしています。

SSDはモデルの出力をそのまま画像内の検出した位置として利用できないため、元画像で利用できるよう結果を変換する必要がありますが、ちと面倒なのでこの部分もAPI側で行うようにします。 このような予測APIだけ必要となる変換層はKerasとML Engineを利用する場合、以下のようにして追加できます。

# keras.layers.Lambdaで学習モデルの出力を入力として変換する層を定義
detection_out = Lambda(bbox_util.detection_out, arguments={
    'keep_top_k': keep_top_k_placeholder,
    'confidence_threshold': confidence_threshold_placeholder,
    'original_size': original_size_placeholder
    })(net['predictions'])

# 上で定義したLambdaを予測APIの出力として定義
inputs  = {'key': keys_placeholder,
           'data': model.input,
           'keep_top_k': keep_top_k_placeholder,
           'confidence_threshold': confidence_threshold_placeholder,
           'original_size': original_size_placeholder}
outputs = {'key': tf.identity(keys_placeholder), 'objects': detection_out}

これらの定義をSavedModel形式で保存することでML Engineが予測APIとして扱えるようにします

def build_signature(inputs, outputs):
    signature_inputs = {key: saved_model_utils.build_tensor_info(tensor)
                        for key, tensor in inputs.items()}
    signature_outputs = {key: saved_model_utils.build_tensor_info(tensor)
                         for key, tensor in outputs.items()}

    signature_def = signature_def_utils.build_signature_def(
        signature_inputs, signature_outputs,
        signature_constants.PREDICT_METHOD_NAME)

    return signature_def

def export(sess, inputs, outputs, output_dir):
    if file_io.file_exists(output_dir):
        file_io.delete_recursively(output_dir)

    signature_def = build_signature(inputs, outputs)

    signature_def_map = {
        signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: signature_def
    }
    builder = saved_model_builder.SavedModelBuilder(output_dir)
    builder.add_meta_graph_and_variables(
        sess,
        tags=[tag_constants.SERVING],
        signature_def_map=signature_def_map)

    builder.save()

# SavedModel形式で保存する
export(get_session(), inputs, outputs, FLAGS.model_dir)

なお、今回は、ML Engineの保存ファイルが256MB以内とする制限を回避するため、ベースネットワークのconv4~pool4層も固定し、その出力を元に38x38分割したサイズの物体を検出するレイヤも除去することで学習パラメタを削減しています。論文では出力レイヤを減らすと精度が低下するとあるので、このあたりは対象のドメインや要求する精度によって調整が必要かもしれません。

学習に必要となるデータなど

学習済みの重みやバイアスを保存した weights_SSD300.hdf5 と、計算済みのデフォルトボックスの位置を保存した prior_boxes_ssd300.pklrykov8/ssd_kerasから入手しておきます。

前述の通り、デフォルトボックスを除去しているので、

import pickle
pickle.load(open('prior_boxes_ssd300.pkl', 'rb'))[4332:].dump('prior_boxes_ssd300_min.pkl')

のようにして、必要なものだけを取り出しておく必要があります。

また、教師データとして、画像ならびにその画像に対する物体の位置と分類クラスを定義したデータセットが必要です。今回はrykov8/ssd_kerasと同じくVOCのデータセット形式を採用しています。 独自にデータセットを準備できたら、rykov8/ssd_kerasの提供しているツールを使ってpickle形式で保存したものをannotation_pathとして、もととなった画像データをtar.gzで固めたものをimages_pathとして指定します。

まとめ

物体検出ニューラルネットワークであるSSDのKeras実装をGoogle Cloud ML Engine上で訓練し、予測APIとして提供できるようにしました。実際にAPIとして運用するにあたってはモデル、学習時のパラメタ、予測APIのバージョンなどをコード管理が求められることになるため、StarChartのようなツールを検討も大切だと思います。StarChartについてはこちらこちらでも紹介しています。よければ参考にしてください。

ペパボに入って4年が経った

ペパボに入社したのは確か2012年の10月だったと思うので、かれこれ丸4年が経ったことになる。

転職のきっかけは、新卒で入社した地元の会社での業務の出張の多さと技術面の軽視が自分の考える働き方と合わなくなったからだった。とにかく技術でやっていきたいんだという気持ちはありながらも、その気持ちを消化できる企業がどんなものか分からず、悶々と転職サイトを見ていた気がする。ある日、福岡 おもしろい it 会社 みたいに思考停止甚だしい検索をして、まとめサイトに辿り着き、自社でWebサービスを運営している会社への転職というのが視野に入った。今にして思えばもっとよい探し方があったと思うが、新卒で1社しか見てないあの頃はとにかく世界が狭かった気がする。ともかくそのまとめサイトにあるおもしろい会社を上から順に調べていって当時のpaperboy&co.に行き着いた。

当時、福岡基盤チームを立ち上げていく時期で技術的にも力を入れ始めていそうだったし、とても良い入社エントリがあったりして、応募を決めた。実は、福岡基盤に応募してバッサリと書類で落とされたのだが、入社すればなんとかなるぞと、ムームードメインのエンジニアとして再度応募した。幸運にも書類を通過して面接に行ったら、半袖短パンでサンダルの髭で目の細いおじさんが、それなりにえらい人として出てきたので、こんな社会人がいるのか、と日本に居ながらにしてカルチャーショックを受けるという貴重な体験もした。なんとか東京での最終面接を終え、採用通知を受け取り、これから始まるであろうおもしろい生活を想像しながらその日は一日幸せな気分だったのを覚えている。

入社後は、とにかく技術的なことを一緒に話せる人が会社にいることがうれしかった。当時のムームードメインはコードも運用も前時代的なものがたくさんあり、これらを現代風に置き換えていくというバリュー出し放題な環境で、GitHub、テストの追加、CI、デプロイ、Vagrant、Rails導入などをみんなで進めながら1年目はあっという間に過ぎていった気がする。シニアエンジニアになることができたのもこのあたりだった。

2年目にGo言語と出会ってから、アウトプットの量が増えた。The Platinum Searcherをつくったのもこの頃で今では1,700ぐらいスターついてて代表プロダクトに成長してくれた。 3年目にかけてGo関連でHoi,ArgenといったプロダクトやFukuoka.goの主催、東京でのGoConferenceでの登壇も経験して、なんとなく入社前に描いていたエンジニアっぽいひとになったような気がしていた。実際、社内でも成果をあげていたと思うし、アドバンスドシニアエンジニア(現プリンシパルエンジニア)になれたのでそのように評価されていたのだと考えていた。

そんな折、CMを打つことになったminneへの助っ人としての参加から、正式な異動があり、急成長するサービスを支えるエンジニアとなった。新しいチームや環境はなかなかに刺激的で学びがあったが、特に基盤チームを始めとする社内でもレベルの高いエンジニアと仕事をする機会が増え、そのスキルと視点の広さに自身との差を見てしまった。アウトプットも視座もまだまだ個人レベルだなと思い至り、試行錯誤の4年目が始まった。

プリンシパルエンジニアになったのは、最低限の基準を満たしただけで、実際はその職位としてあるべき振る舞いに向けて精進することを求められていたのだと思う。まだまだだった。 とにかく今までの延長だとダメだと、サービスを横断できるような仕組み作りで、ログ活用基盤Bigfootを担当したり、機械学習やるため数学の勉強し直しをしたりしていた。

今までより広い範囲のことができるようになったのはよかったと思うけど、もう少し敏感に課題を見出したり、大きな思想を持って解決策を提案したりできた気もする。手を動かす部分とのバランスが難しい。来年以降の課題になりそう。

もっとエンジニアらしく理路整然と取り組みやペパボの制度のよさを伝えたかったが、4年という期間を振り返っていると懐かしくなってしまって思い出語りになってしまった。

4年同じ会社にいるのはこの業界ではそこそこ長い。と思う。多分、ペパボがおもしろいとか仲がよいとかだけの会社だったら転職していたかもしれない。少し前までは自分のエンジニアとしての成長と会社の評価制度の成長具合がちょうどいいからだと感じていたけど、実際は評価制度の成長のほうが一歩先にいって、常に先回りした道を敷いている。抽象的なキーワードが示されるときもあるけど自分なりに考えて動くとなぜか成長しているというのはなかなか絶妙な加減じゃないかと思う。単純にそういう時期に在籍したということではなくて、制度としてそれが整備されて、日々アップデートされていっているのはエンジニアとして魅力的な環境であるし、自分の現状の少し先のイメージとして参考になるエンジニアが常にいる。その環境が続ける最大の理由なのかなと思っている。

ペパボでの5年目はまた働き方も変わってくる予定で、先回りした道も敷いていきたいし、憧れとしての背中も見せていきたいし、サービスもバシバシ成長させていきたい。

5年目、僕は上を見ていきたいと思います。今後ともどうぞよろしく。

— お知らせ —

こんなペパボに興味持たれた方、もっと雰囲気知りたい方、まずはお気軽にペパランチョンでもいかがでしょう。

『みんなのGo言語【現場で使える実践テクニック】』をいただきました。

著者のお一人、@songmuさんより、『みんなのGo言語【現場で使える実践テクニック】』をいただきました。ありがとうございます。

現場で使える実践テクニックのタイトル通り、Go言語界隈の有名人が様々なテクニックを紹介してくれています。自分もGo言語が好きでよく書いていますが、書き始めた当時に試行錯誤しながら身につけてきた知識、テクニックが簡潔にまとまっており、文法を一通り覚えた初学者にとって学習効率が非常に高い本ではないかと思います。

第1章 Goによるチーム開発のはじめ方とコードを書く上での心得

Go言語を快適に書くための開発環境の準備から、Goらしいコードを書くまでが簡潔にまとめられています。main.goだけのプロジェクトから卒業するときに悩むところが網羅されており、A Tour Of Goのあとに、みんなのGo言語 1章を読むまでを入門としてよさそうです。

また、この章で拙作の(dragon-imports)を紹介していただきました。ありがとうございます!

第2章 マルチプラットフォームで動作する社内ツールのつくり方

2章ではマルチプラットフォーム対応をするために気をつける点が説明されています。2.5 がんばるよりもまわりのツールに頼るや、2.8 設定ファイルの取り扱いなど、単一のプラットフォームだけでも役立つテクニックが掲載されています。

第3章 実用的なアプリケーションを作るために

実際に各所のプロダクション環境で動いているツール開発の経験が盛り込まれているまさに実践的な章です。自分自身も、独自シグナルによるメンテナンス性の向上やcontextパッケージなどまだ知らなかった知識を仕入れることができて満足度が高い章でした。

第4章 コマンドラインツールを作る

マルチプラットフォーム、シングルバイナリを実現するGo言語の華形と言えばCLIツールだと思います。CLIツールをつくるにあたり、本章を始め、筆者の方が今までブログや発表の中で一貫して述べてきた、使いやすくメンテナンスしやすいツールにするためのテクニックはとても役立つと思います。

特に4.5にある終了ステータスコードや入出力をmainとパッケージで分離する手法については自分のプロダクトでも採用させてもらっており、メンテナンスやテストによる堅牢性の向上などにつながっておりオススメです。

第5章 The Dark Arts Of Reflection

Go言語のリフレクションで何ができてどのような副作用があるかが説明されています。パフォーマンスについてもまとめられており、利用に関して自分自身で判断できるようになるためにも読んでおくとよいかと思います。

それにしても動的なSelect文の構築ができるとは思っていませんでした。Go言語初心者を脱した方にとっても新鮮な発見がある章かもしれません。

第6章 Goのテストに関するツールセット

Go言語でのテストの基礎とテクニックを紹介しています。Go言語もテスティングフレームワークが一時期乱立しましたが、最終的には言語標準の機能によるシンプルなテストに落ち着いたのではないかと思っています。この章を読むことでGo言語の提供する仕組みに沿いながらも必要十分なテストを書くための知識を得ることができると思います。

まとめ

全体を読みながら、自分もここでハマったなとか、最終的にこういう風に書いているなと納得しながら読み進めることができました。こういった知見は、普通は自分自身の試行錯誤や他のひとのコードを読んで少しづつためていくものですし、知見がたまったとしてもなかなか網羅的にひとに伝えることは難しいものだと思います。この本にある、現場で培われた実践的なテクニックを本というカタチで学習し、共有できるのは自分自身のスキルアップだけでなく、チームの拠り所として立ち上げや新規参入者を迎えるにあたって心強い一冊となるのではないでしょうか。

また、紹介されるテクニックについても様々なレベルがあり、自分のレベルややりたいことの変化に応じて、新しい発見のある本ではないかと思います。140ページほどで簡潔にまとめられているので、気軽に読み始めることができます。みんなのGo言語【現場で使える実践テクニック】 オススメです。

プログラマのための数学勉強会@福岡#5 で「Goによる勾配降下法 -理論と実践-」を発表してきた

8/6に開催されたプログラマのための数学勉強会@福岡#5で「Goによる勾配降下法 -理論と実践-」を発表してきました。

今回は勾配降下法にフォーカスした内容となっています。機械学習というブラックボックスが実は誤差を最小化するものであり、そのために勾配降下法というアプローチがある、という基本でもあり、数式に抵抗があると最初につまづく箇所でもあります。

今回は数式と図解に加え、Go言語によるサンプル実装も添えることでプログラマへも理解しやすくなるように資料を作ってみました。

また、勾配降下法の手法だけではなく収束速度の改善や学習率の自動調整といった最適化の手法も紹介しているので、基本を理解している人もよければ御覧ください。

サンプル実装

発表で使ったサンプル実装はこちらで公開しています。

正弦関数を元にしたトレーニングセットに対して多項式回帰を行うことができます。

このような感じで各種勾配降下法や最適化手法、ハイパーパラメーターの調整によってどのように学習が収束していくか、遊んでみてください。

$ go run cmd/gradient_descent/main.go -eta 0.075 -m 3 -epoch 40000 -algorithm sgd -momentum 0.9

こういうグラフが出力されます。

gradient_descent

プログラマのための数学勉強会@福岡

今回は発表に向けて自分で最適化手法について調べて実装して、検証してを繰り返すことで自分自身も理解が深まってよかったです。

発表内容も多様で、個人的には@hokutsさんの、意識の有無を統合情報量として数式に落としこむ統合情報理論の紹介が面白かったです。高度に複雑化した機械学習のモデルが意識を持つのか。色々妄想が進みそうです。

福岡で数学に興味ある方、お気軽に。発表者募集中とのことです。

静的データを扱うActiveHashでページングとライク検索するgem達をつくった

静的データをActiveRecord的に扱えて便利なActiveHashですが、ページングとライク検索が必要になったのでgemをつくりました。


ページングを行えるようにする active_hash-kaminari と、

ライク検索を行えるようにする active_hash-like です。

active_hash-kaminari

ページングを行いたいActiveHashのクラスにPaginatableモジュールをprependします。

class Country < ActiveHash::Base
  prepend ActiveHash::Paginatable
  ...
end

ページングにはKaminariを使っています。モジュールをprependすることにより、検索結果が Kaminari::PaginatableArray でラップされるようになり、ページングが可能になります。

Country.all.page(1).per(10)

もちろんViewでも使えます。

<%= paginate @counties %>

active_hash-like

gemをインストールすると、ActiveHashでlikeが使えるようになります。

class Country < ActiveHash::Base; end

Country.like(name: 'Cana%')

複数条件のうち、ひとつをlikeで検索する場合はwhere内ActiveHash::Match::Like マッチャーを使います。

Country.where(name: ActiveHash::Match::Like.new('Cana%'))

Custom Matcher

ActiveHash::Match::Likeはカスタムマッチャーのひとつで、マッチャーは独自でつくることが可能です。

マッチャーはcallメソッドを持つ必要があります。

class MyCustomMatcher
  attr_accessor :pattern

  def initialize(pattern)
    self.pattern = pattern
  end

  def call(value)
    # Case ignore matcher
    value.upcase == pattern.upcase
  end
end

Country.where(name: MyCustomMatcher.new('pattern'))

callメソッドを持つProcオブジェクトも使うことができます。

Country.where(name: ->(value){value == 'some value'})

OR 条件

あいまい検索という括りでOR検索も行うことができるようになります。

Country.where(name: 'Canada', or: {field1: 'foo', field2: 'bar'})
#=> name = 'Canada' and (field1 = 'foo' or field2 = 'bar')

OR条件の対象のカラムが同じ場合(IN相当)はまだ未実装なのでカスタムマッチャーを使ってください。

Country.where(name: ->(val){val == 'Canada' || val == 'US'})

まとめ

ActiveHash、静的データを扱うのにとても便利なので、active_hash-kaminariとactive_hash-likeと組み合わせて更に便利に使ってみてはどうでしょうか。

Treasure Dataのスケジュールジョブをコードで管理するPendulumというgemをつくった

Treasure Dataに収集したデータを集計・出力するためにジョブをスケジュール登録するにあたり、ブラウザコンソールやAPIから直接行うと履歴管理やレビューができないといった課題を解決するために Pendulum というgemをつくりました。

PendulumはDSLで記述された定義に従い、Treasure Dataのスケジュールジョブを管理します。 定義ファイルをGit管理することで、履歴管理やGitHubと連携したコードレビューが可能になります。

余談ですが、Pendulumは振り子という意味で、定期的な実行という意味と宝探しのダウジング的な意味から連想しています。ペンデュラム。響きがカッコイイ。

使い方

Schedfileという名前で定義ファイルを用意して、

schedule 'my-schedule-job' do
  database 'db_name'
  query    'select count(time) from access;'
  cron     '30 0 * * *'
  result   'td://@/db_name/count'
end

pendulumコマンドを実行するだけです。

# dry-run で 適用内容を確認
$ pendulum --apikey='...' -a --dry-run
# 適用
$ pendulum --apikey='...' -a

AWSのRoute53に対するRoadworker的なものを想像してもらえるとよいかと思います。

エクスポート

既存のスケジュールジョブがある場合はエクスポートもできます。

$ pendulum --apikey='...' -e

実行ディレクトリに Schedfilequeries ディレクトリが生成されジョブの定義とクエリが出力されています。

修正後、dry-runによるapplyを行ってみると、差分が検出されていることがわかります。

$ pendulum --apikey='...' -a --dry-run
Update schedule: my-scheduled-job (dry-run)
  set cron=@hourly

Schedfile

APIで利用する名称と同じものが使えます。

schedule 'test-scheduled-job' do
  database    'db_name'
  query       'select count(time) from access;'
  retry_limit 0
  priority    :normal
  cron        '30 0 * * *'
  timezone    'Asia/Tokyo'
  delay       0
  result_url  'td://@/db_name/count'
end

priority:very_highなど、cron:dailyといった読みやすい値も使えます。

query_file

クエリはquery_fileを使って別ファイルのクエリを読み込めます。

query_file 'queries/test-scheduled-job.hql'

集計用のクエリは長くなることが多いと思うので、こちらを使っていくことになるでしょう。

result

Result Exportの先を定義する、result_url をわかりやすくするための result もあります。

schedule 'test-scheduled-job' do
  database   'db_name'
  ...
  result :td do
    database 'db_name'
    table    'table_name'
  end
end

現時点では出力先としてTreasure DataPostgreSQL、カスタムのresultをサポートしています。他の出力先をresult記法で書きたいときはPullRequestをお待ちしております。

force

Schedfileに定義されていないスケジュールジョブは通常はUndefined scheduleとなり削除対象とはなりません。削除が必要な場合は--forceオプションを使ってください。

また、Treasure DataのAPI都合上、result_url内のパスワードは***とマスキングされており差分が比較できません。もしパスワードを変更した場合も同様に--forceオプションを使って適用してください。

config

environments/default.ymlなどを用意することで、DSL内でsettings経由で取得することができます。

...
  result :postgresql do
    ...
    password settings.password
  end

また、-E オプションによる環境ごとの設定ファイルにも対応しています。

まとめ

コード管理により履歴管理やレビューといったフローに乗せることができるので大変便利になりました。そのあたりの運用で困っているかたは使ってみてはどうでしょうか。

mrubyからSidekiqに非同期ジョブを登録するmrbgemをつくった

ngx_mrubyでHTTPリクエストに対して非同期処理をしたかったので、mruby-sidekiq-clientという mrbgem をつくりました。

mrubyからSidekiqのジョブ形式でRedisに非同期ジョブを登録し、別途用意したSidekiqのサーバーが登録されたジョブを捌いていくという方式です。これにより、非同期処理は通常のRailsやCRubyのコードで書くことができ、mruby側の処理はシンプルに保つことができます。

使い方

RailsなどでSidekiqを使っている方にはお馴染みのやり方です。

1. 非同期ジョブにしたいWorkerクラスを定義します

class HardWorker
  include Sidekiq::Worker
end

performメソッドはmruby側では実装する必要はありません。Sidekiqサーバー側のRubyコードに実装してください。

2. 非同期ジョブを登録する処理を定義します。

HardWorker.perform_async('bob', 5)

指定時間後に実行するジョブも登録できます

HardWorker.perform_in(300, 'bob', 5) # 300秒後に実行

mruby側はこれだけです。あとは先ほどのWorkerクラスと同じクラスをSidekiqサーバー側に追加し、performメソッドに非同期ジョブの内容を記述します。

ngx_mrubyで使う

mruby_userdataを使って、リクエストごとにRedis再接続しないようにしておきます。

http {

    (snip)

    mruby_init_code '
        userdata = Userdata.new "redis_data_key"
        userdata.redis = Redis.new "localhost", 6379
    ';

    server {

        (snip)

        location /hello {
             mruby_content_handler_code '
                 userdata = Userdata.new "redis_data_key"
                 Sidekiq.redis = userdata.redis

                 class MyWorker; include Sidekiq::Worker; end

                 (snip)

                 MyWorker.perform_async(args)
             ';
        }

Sidekiqサーバー

SidekiqサーバーはPlain Rubyの環境でも動作するため、ngx_mrubyとRedis、Rubyだけでシンプルなジョブキューをつくることができます。 手順はこちらを参考にしてください。

インストール

build_config.rb に以下を記述してください。

conf.gem :github => 'monochromegane/mruby-secure-random'
conf.gem :github => 'monochromegane/mruby-sidekiq-client'

mruby-secure-randomSecureRandomをmrubyで使えるようにするためにつくったmrbgemです。SidekiqのJIDを算出するために利用しています。ランダムデバイスとして /dev/urandom しか対応していないため Windows環境だと動きません。PullRequestお待ちしております。

まとめ

こんな感じでmrubyからSidekiqと連携できるようにしてみました。シンプルなジョブキューであればRails使わずにngx_mrubyとRedis、Rubyだけでつくれてしまうので便利では〜と個人的には思っています。

あわせて、今後、mruby使った動的なフロントサーバーがサービスのどの要件までを担当すべきかといった構成は常に考えていかなければいけないだろうなあとも感じています。適材適所それぞれの利点を最大限に活かせる構成を検討したいところ。

Archives