dely engineering blog

レシピ動画サービス「kurashiru」を運営するdelyのテックブログ

データサイエンティストと機械学習エンジニアをやって思ったこと

はじめに

こんにちは。dely開発部の伊ヶ崎(@_ikki02)です。

本記事はdely Advent Calendar 2019の6日目の記事です。

qiita.com

adventar.org

昨日は当社サーバサイドエンジニアの安尾が
「スピード優先の開発で溜まった技術的負債の返済計画(サーバーサイド編)」
という記事を書きました!
新機能の開発にとどまらず
技術的負債を返済していくのはとっても素敵なことですね!
ぜひこちらも一読いただけると嬉しいです。

さて、本日は前職でデータサイエンティスト、
現職で機械学習エンジニアをしている経験から
私が感じている両職業のお話をしていきたいと思います。
まだまだ新しい職種なので、
実際はこんなことしてるんだ、と少しでもお役に立てれば幸いです。
(あらかじめお断りしておくと、 職業の優劣関係や、
どちらか片方の職業を賞賛する意図に基づくものではございません。
また、個人の体験談という位置付けとして見ていただきたく、
一般的にこうだ、と主張するものでもございませんので、
あらかじめご理解賜りますようお願い申し上げます。)

目次

データサイエンティストのお仕事

私がデータサイエンティストとしてお仕事していた際は、
主に自然言語のデータ解析と抽出を担当していました。
法人向けのビジネスで、金融機関や生命保険のクライアントが多かったです。
金融業界はセキュリティ要件が厳しく、データの社外持ち出しが難しいため、
週の2~3日くらい客先常駐することもありました。

具体的な業務のアウトプットとして3つ挙げるならば、

  1. 「①データアセスメントやPOC*1のレポート」
  2. 「②分析をするためのアドホックなプログラム」
  3. 「③ROI*2

の3つに大別されると思います。

①データアセスメントやPOC(※1)のレポート

f:id:ikki02:20191203181450p:plain
データサイエンティストの一般的な業務は統計的手法や機械学習を用いて、膨大なデータの中から一定の法則やルールを見出し、それを業務効率化/高度化の意思決定を支援することになるかと思います。 そのため、レポーティングを通して、今どのような課題があり、どういう状態だから、こうしたい、そのためにはこういう対策が必要というビジネス施策のロジックを整理して伝えることが重要になります。また、データサイエンティストが使う言葉には、統計や機械学習の専門用語が含まれるため、それを専門としない相手に分かり易く伝える、ある種の翻訳力も求められることが多いと思います。

②分析をするためのアドホックなプログラム

f:id:ikki02:20191203181558p:plain
アドホックなプログラムは、上記レポートを作成する上で必要になるデータの痕跡を探るプログラムです。私は自然言語のデータを扱うことが多かったので、例えば、社外からのQAデータを分析する場合、QAは複数の質問に分けることができるかどうか、それらを個別に扱うか、統合して全体として扱うのがよいか、など、分析対象の絞込みやデータ加工の必要性が生じます。(時に上記の細かいデータの扱い方は業務ドメインに依存するため、クライアントにどのような意図でデータを処理したかレポートすることは依然重要です。)また、機械学習アルゴリズムによるスコアリングや分類結果(ただの数字であることが多い)を用いて、次の機械学習プロセスの入力とする、出力結果を利用しやすい形式に加工する、など出力結果の処理もプログラムで書くことになると思います。ちなみに、使用する言語は個々人や各会社に寄ると思いますが、Pythonで開発することが多いと思います。

③ROI

f:id:ikki02:20191203181605p:plain
多くのデータやクライアントとお仕事する中で感じたこととして、残念ながら、データサイエンティストの価値はまだまだ社会的に認知されているとは思えません。彼らがどのような価値を生むのか、そのイメージがつきにくい方も多いと思います。まだまだ新しい職種なので仕方がないと思う反面、だからこそ、個々人の体験を表現していくことで議論を成熟させていく必要があると思うので、 僭越ながら個人の考えを記載させて頂くと、 職種としてデータサイエンティストが生む価値、それは結局はROIで計られることになるのかなと思います。

特に、大きく2つの軸があると考えます。

  • 従来業務の効率化(従来時間や手間がかかっていたことを省略化できる)
  • 従来業務の高度化(従来難しかったことができるようになる)

従来業務の効率化

f:id:ikki02:20191203181951p:plain
自然言語の文脈で言うと、何かしらの電子文書*3を毎日なり毎週なり読む業務があると思います。この際、まずは読まなければならない文書数に対して時給なりの単価を掛合わせ、総コストを定式化します (例:総コスト = 読まなければならない件数 ÷ 1時間に読める件数 × 時給)。ここで機械学習のソリューションを用いて、読むべき文書の優先順位付けや絞込みを実現し、上式の読まなければならない件数(または時間)を半分にできる場合、それがリターンに相当します。このリターンに対して必要なPOC経費や開発コストが見合う場合、意味のある取り組みとみなせると思います。

従来業務の高度化

f:id:ikki02:20191203182018p:plain
上述の効率化については、従来業務が比較対象として存在するため、コスト計算は実はそんなに難しくありません。(論理的には。。。クライアントの業務を理解しそれを表現する際は、気を使うべきことも多く実際は結構難しいです。笑)もちろん金額に換算することが必ず正しいわけではなく、そのような場合は高度化の取組みが該当すると考えます。実際、従来業務を高度化し代替する場合*4は新しい変数が生じるため、定式化はより不確実なものになります。その結果、高度化に関する取組みは定性的な価値をより吟味することが多いように思います。例えば、アンケートの定性的な処理について、苦情を機械的に検知して迅速に対応したい、という要望が多くある一方で、感謝や賞賛の言葉をひろって担当者にフィードバックする取組事例は、優先順位がどうしても下がりがちなため、意外と少ない感覚があります。昨今の働き方改革も進む中で、このようにコストをかけて明るい職場を作っていきたいという取組みには一定の意味があると思います。

※従来業務の効率化/高度化について下記記事を参考にさせていただいています。
qiita.com

まとめてみると、定量的な効果と定性的な効果を加味して、費用対効果に見合うプロジェクトにしていくことがデータサイエンティストの役割なのかなとも思います。そのため、費用対効果が合わないと熟慮した際は、「やらない」ときっぱり断る姿勢も重要になると思います。お互い損ですからね。(それでもやる、という鶴の一声プロジェクトはなるべく避けたいものです)

僕の志向性(自動化)

f:id:ikki02:20191203182219p:plain
データサイエンティストとしてのお仕事は非常に楽しかったです。

しかし、数年間お仕事していく中で、自動化に携わりたいという気持ちが強くなりました。上部ではアドホックなプログラムについて記述しましたが、ビジネスロジックや機械学習ワークフローをコード化(MLOps)して支援することに、より重きを置くようになっている自分がいました。そこで、よりエンジニアリングに特化した機械学習エンジニアとして現職のdelyにジョインしました。(なぜ自動化に拘ったか、生産性のお話はどこかでまた書きたいです。)

機械学習エンジニアのお仕事

(少しだけ宣伝させてください)
delyはダウンロード数2000万超えのレシピ動画サービス「クラシル」と
同じく月間利用者数2000万超えの働く女性を応援するメディア「TRILL」の
2つのメインサービスを展開する事業会社です。
現在両サービスのデータ基盤の開発/運用と
機械学習を用いたシステムの開発に携わっています。

7月に入社して約半年なので、
この職業についてまだ十分に語れるわけではありませんが、
これまでの取組を通して振り返ってみたいと思います。

ETL処理*5

f:id:ikki02:20191203182848p:plain
入社初期に携わった仕事のひとつとして、ETLバッチ開発があります。クラシルでは日々多くの施策(イベント)を検討し開発しておりますが、新しいイベントの効果を測定するために、イベントログを都度追加するスタイルを取っています。新しく追加されたイベントログは、既存のインベントリに追加する必要があるのですが、こちらを日次で処理するよう、AWSのAthenaやGlueを用いて実装しています。

Glueを使えば自社データソースと連携してETLスクリプトをトリガーを指定して簡単に実行できるのですが、実際入社するまでサービスのことを知りませんでした。データサイエンティストの場合、最悪SageMakerやAthenaのようなデータサイエンス用のマネージドサービスだけで事足りることもあると思いますが、機械学習エンジニアの場合、システムが利用しているサービス全般との繋込み、すなわちアーキテクトスキルがより求められるのだと実感しました。

異常検知とマイクロサービス

こちらは入社して一番力を入れて取り組んだプロジェクトになります。上述のETLバッチ処理が新施策に対する取組(の一部)だとすれば、こちらは既存機能のユーザー利用頻度に変化がないか測定評価する機能となります。 詳細はML@Loftで登壇した際の下記資料をご覧頂きたいのですが、 簡単に言うと、ETLと異常検知(SageMakerのビルトインアルゴリズムを利用)、slack通知までの各機能をコンテナでマイクロサービス化して開発しています。事前の検証を最低限にしつつスピード重視で開発したため、まだまだ発展途上ではありますが、異常検知の精度を高める工夫をしつつ(あまり異常は起きないにこしたことはないですが)、利便性を向上させ、活用範囲を広げていけるように開発できればと考えています。

delyにおけるevent designの取組について
(注意:ONE PIECEの事前知識が必要です。)

www.slideshare.net

機械学習エンジニアとデータサイエンティストの違い

データサイエンティストは
下記3象限のスキルセットを身に付けるべきだとよく言われると思います。 f:id:ikki02:20191203190023p:plain
↑データサイエンティスト協会が公開されたデータサイエンティストのスキルセット

異論はありませんが、ビジネスはチームで動くのもまた真だと思うので、現場感では上記の役割を分担しあっているのが実態に近い印象です。

つまり、工数配分を考えるなら、

  • データサイエンティスト: 「ビジネス力」×「データサイエンス力」
  • 機械学習エンジニア:「データエンジニアリング力」×「データサイエンス力」

となると思います。

実際、機械学習エンジニアは、アーキテクトやシステム開発が主な役割となるため、分析は必要最低限しか実施しない点が、データサイエンティストとの大きな違いだと感じています。(もちろん複雑な因果推論など、時間をかけるべき時は社内のマーケティング部やユーザーの声を参考にしながら本筋から大きく外れないようにすることは大事です。)信頼性、拡張性、保守性を意識した開発が求められていることをよく感じます。

このように役割が異なるデータサイエンティストと機械学習エンジニアですが、どちらに優劣関係があるということではなく、どこに時間を割くか、その結果どこに時間をかけないかを補完し合う関係性だと考えています。
f:id:ikki02:20191203184551p:plain

最後に

もともとMLOpsや自動化に魅力を感じて
機械学習エンジニアにキャリアチェンジしましたが、
delyでは何兆円もの市場規模を誇る食の領域でシェアを取るべく邁進しています。

フーマーという中国のスーパーをご存知でしょうか?
機械学習に限らず、少しざっくりしたフードテックのお話になりますが、
中国のフーマーでは、

  • 在庫をなるべくもたない倉庫でありスーパー
  • 買い物をスマホで行い自宅まで30分で配送する
  • 顔認証やアリペイを使ったセルフレジ
  • 料理の配送をロボットが行う(スープは人が運ぶ)

など、ユーザー体験を向上させつつコスト最適化を試みる新しい形態が提案されています。

www.youtube.com

少子高齢化と人口減少が深刻化するこれからの日本でも
生活にかかすことのできない食を、
より便利に、よりおいしく、より楽しめたらいいなと考えています。
delyでそんな挑戦を続けることができたらいいなと思います。

次回予告

さて次回は、当社PdMの奥原が「プロダクトマネージャー1年目の教科書」というタイトルで投稿します!
すでにタイトルが面白そうです!お楽しみに!

qiita.com

adventar.org

参考

2019年版:データサイエンティスト・機械学習エンジニアのスキル要件、そして期待されるバックグラウンドについて
https://tjo.hatenablog.com/entry/2019/02/19/190000

機械学習を使った事業を成功させるために必要な考え方や人材、フェーズとは?
https://qiita.com/yoshizaki_kkgk/items/55b67daa25b058f39a5d

データサイエンティスト協会「ミッション、スキルセット、定義、スキルレベル」
https://www.datascientist.or.jp/common/docs/skillcheck.pdf

アリババの新型店舗『フーマー』とは?
https://www.youtube.com/watch?v=sso6bITfuDU

Stitch Fix Algorithms Tour
https://algorithms-tour.stitchfix.com/

Netflix TechBlog
https://medium.com/netflix-techblog

delyについて

delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ!

CXOとVPoEへのインタビュー記事はこちら!

wevox.io

*1:POC: Proof of Concept。概念実証。AI関連技術の予測結果は不確実性が高いため、実務での利用に値する性能かどうか、アイデアをひとつずつ検証してその可能性を顕在化していくプロセスのこと。

*2:ROI: Return on Investment。投資利益率のこと。要は投資したコストに見合うリターンがあるかどうか図る指標です。

*3:金融業界では決算関連資料であったり、銀行員/顧客間の金融商品案内記録、アンケート内容やメール本文、など。

*4:人が車を運転する、という活動を自動運転にする、など

*5:ETL:Extract, Transform, Loadの頭文字。データを抽出し、加工し、別システムへロードする、データが辿る一連の処理のこと。

スピード優先の開発で溜まった技術的負債の返済計画(サーバーサイド編)

こんにちは! dely株式会社サーバーサイドチームの安尾です。

本記事はdely Advent Calendar 2019の5日目の記事になります。 qiita.com adventar.org

昨日は辻さんが「Jupyterもいいけど、SageMath使って可能性もっと伸ばそう!」という記事を書きました。 tech.dely.jp

本日は「スピード優先の開発で溜まった技術的負債の返済計画(サーバーサイド編) 」というタイトルで、今delyのサーバーサイドチームの技術的負債についての考え方から、負債返済のために具体的に行なっていることをご紹介したいと思います。

技術的負債とは

抽象的な言葉なので、組織や人によって微妙に定義が異なるのではないかと思いますが、僕たちのチームでは「未来の開発スピードを下げる原因となるプログラムやアーキテクチャのこと」を総称して技術的負債と呼ぶようにしています。

具体例を挙げると、

  • 複雑で修正が困難なプログラム
  • サービスがスケールした際にボトルネックになるアーキテクチャ
  • テストが書かれていないプログラム
  • メンテナンスされていないgemの利用
  • 実装者の退職や異動によって、開発目的や正確な仕様がわからないプログラム

のようなものになります。

技術的負債に対する考え方

技術的負債というと、とてもネガティブなイメージをしてしまいがちですが、必ずしも悪いものだとは思っていません。 特にプロダクトのフェーズによって負債に対する考え方は大きく変わると考えています。

例えばまだユーザーが少ない新規サービスの場合、いかに高速にPDCAを回して資金がなくなるまでにサービスをマーケットフィットさせるかということが重要になります。

そういうフェーズのサービスにおいては、未来の開発スピードを考慮して、機能のリリースが遅れるくらいなら、あとで直さないといけないことが分かっていても早くリリースすることを優先することが合理的な判断だと言えることも多いでしょう。

クラシルにおいても、やはり立ち上げてからしばらくはスピード優先でガンガン開発してリリースするというスタイルを取っていました。

とは言え、その状態を続けていると負債の返済に追われて、追加機能や機能改善がだんだんできなくなってくるという状況が発生してしまいます。

クラシルでも大小含めてこれまでにも色々と問題が発生していましたし、今後何か問題が起こった際のインパクトや、これからだんだんエンジニアを増えていくであろうことを考えると、このまま前進し続けるより、一旦スピードを落としてでもある程度工数を使って負債を返しておくことで未来の開発スピードを上げられるという判断から負債の返済をチームの目標の一つとして取り組んでいくことになりました。

技術的負債返済計画

それでは、今サーバーサイドチームがどのような手順で負債返済を行なっているか紹介していきたいと思います。

1. 負債の洗い出し

もともと負債が色々と溜まっているという認識はそれぞれ持っていたり各自で優先度が高いと思っている部分についてはドキュメントにまとめたりしていたのですが、全体像を誰も把握できていない状態で負債を返済しようと言っても何から手をつけて良いか、また本当にそれが優先すべき負債なのかというのが分からない状態でした。

そこで、まずは負債を一通り洗い出すことで全体像を把握しようというところから始めました。 やり方としては、まず僕が過去に課題として話題にあがったものや思いつく限りの負債をスプレッドシートにまとめて、それをベースとして各自が思いつく限り追加していくという形を取りました。

f:id:yusuke_y:20191205110439j:plain

なお、一度まとめて終わりににするのではなく、日々の業務で新たに発見された課題があればここに追記しアップデートしていくことで、なるべく課題が埋もれていかないように気をつけています。

2. 優先順位付け

次にそれぞれの負債に対して重要度を5段階に分けて優先度付けをするということをしました。 重要度の基準については一旦下記でいくことに決めました。この基準についても会社の状況によって変わってくると思うので、今はこれにしていますが、都度変えていけば良いかなと思っています。

  1. セキュリティインシデントの原因になる
  2. バグの原因になる
  3. アップグレードの妨げになる
  4. 開発スピードを落とす要因になる
  5. その他

f:id:yusuke_y:20191205111145j:plain

洗い出した時点では大量にあって何から手をつけて良いか分からなかった負債一覧も重要度をつけてみると意外と本当に重要な負債は限られていることが分かってきました。

3. 担当決め

次に重要度が高いものを優先し、誰が何をやるかを決めました。 やり方は基本挙手制にしました。理由は単純で自分で取り組みたいものに取り組むのが一番モチベーションも上がるし、結果としてパフォーマンスも上がると思ったからです。

これをやった個人的な感想としては、課題によって求められるスキルが大きく変わるので、得意なものばかり選ぶのではなく各々が成長させたいスキルに関連するような負債を選択することで機能開発だけでは身につきづらいスキルをつけることができてとても良いなと感じています。

基本的に2、3の作業は四半期に一度目標設定の際に定期的にやっていく形にしようと思っています。

返済すること以上に未来の負債の予防がとても大切

たまった負債を返していくことはもちろん重要ですが、それと同じかそれ以上に、新規の開発がすぐに負債化しないように予防するということを僕たちはとても大切にしています。 新規開発で負債を予防するために色々と工夫しているのですが、今日はその一例を紹介していきたいと思います。

ルール決め、ドキュメント化を行う

サーバーサイドのエンジニアが1人2人のときにはあまり気になっていなかったことですが、3人、4人と増えるに連れて、ルール化されていない部分のやり方が違う部分というのが結構あることがわかってきました。

例えば、

  • 管理画面のテストを書くかどうか
  • Controllerにもmodelにも書きづらいようなコードをどこに置くか
  • 新機能の実装時にどの程度のドキュメントを書くか

のようなことが人によって違うことが分かってきました。 短期的に見ると特に問題はないことかもしれませんが、僕たちはこういった細かい違いが属人化となり後々大きな問題となって返ってくると考えています。

なので、こういったルールがない部分を見つけるとこのままで良いのかを考え、後々問題になりそうだと判断したことについてはルールを決めてドキュメント化し、新しいメンバーが増えた際のオンボーディングで伝えるという運用をしています。

設計レビュー

僕たちのチームでは、新機能や機能改善を行う際には、サーバーサイド設計仕様書を書いて他のメンバーに共有して、実装に入る前にブラシュアップするということを徹底しています。

また、これはサーバーサイドのメンバーだけではなく、SREのメンバーも一緒にやることで、仕様を満たしているかということだけではなくその後中長期的に安定して運用できるかというような観点も含めてレビューをしています。

これをやることで1人で考えるよりもより良い設計になりますし、コードレビューやリリース前になって思わぬ問題に気づくというリスクを減少する効果もあります。

更にはドキュメントが残るので、実装時にいなかったメンバーでも、この機能はなぜ実装されたのか、どういう経緯でこういう設計になっているのかということを調べることが可能になります。

安易に管理画面に機能を追加しない、利用頻度が低い機能は削除する

割と最近決めたことではありますが、結構有効なのではないかと思っているのがこちらです。

エンドユーザーが使うアプリやWebに機能というのは、多くの方の目に触れて様々なフィードバックがあるので、徐々に洗練されていく傾向があるかと思いますが、社内メンバーが使う管理画面となるとそうではないので、エンジニアが社内からの要望に応えて気軽にどんどん機能を追加していった結果、誰も全体像が分からなくなってしまうということはよくあるのではないでしょうか?

クラシルにおいても時間と共に管理画面が肥大化し、誰も全体像が分からないというような問題が発生していました。 その対策を議論し、極端な話そもそもコードがあるから負債化するのでコードがなければ負債化しないよねということになり、できる限りコードを増やさない方法を考えようという取り組みをしています。

具体的には、月一以上の頻度で使われる機能については管理画面に残すけれど、それより低い頻度でしか使わないものは削除して、必要に応じてエンジニアが手動なりスクリプトなりで対応するという形です。 そうして機能は必要最低限にすることによって、次のステップとしてドキュメントを整備したり、足りていないテストを書いたりするのも楽になるし、CIにかかる時間も短縮されるという感じで副次的なメリットも色々ある取り組みとなりました。

最後に

本記事では、クラシルのサーバーサイドが技術的負債についてどう考え、どのように返済を行なったり、未来の負債を予防するためにどのようなことをしているかを紹介しました。 技術的負債に苦しんでいる方にとって少しでも参考になると嬉しいです。

また、サーバーサイドチームでは、一緒に技術的負債を返しつつ、中長期的な視点で開発・運用を行なってくれる仲間を募集しています!

これまで誰にも解決できなかった食や暮らしの課題を解決し、一緒に世界をより良くしていきましょう!!

www.wantedly.com

次回は伊ヶ崎さんが「データサイエンティストと機械学習エンジニアをやって思ったこと」というタイトルで投稿します!

Jupyterもいいけど、SageMath使って可能性もっと伸ばそう!

はじめに

こんにちは。dely開発部の辻です。

本記事はdely Advent Calendar 2019の4日目の記事です。

qiita.com

adventar.org

昨日は弊社CXO坪田が「突破するプロダクトマネジメント」という記事を書きました!
プロダクトマネージメントっていつの時代も課題山積ですよね。弊社も多分に漏れずたくさんの課題を抱えているわけですが、それらをどのように突破していくか様々な観点からの具体的な取り組みが書かれていますので興味のある方は是非読んでみてください。南無。

blog.tsubotax.com

f:id:long10:20191125224152p:plain

さて本日は「Jupyterもいいけど、SageMath使って可能性もっと伸ばそう!」ということで、普段Jupyter Notebook使ってるという人向けに、どうせならSageMathを使ってやれること増やしませんか?という内容になっています。そこで、SageMathのインストールから基本的な使い方、趣味(?)や実務で普段どんなふうに活用しているかなどご紹介させてもらおうと思います。

目次

SageMathとは

f:id:long10:20191125225617p:plain

SageMath(元々は単にSageという名前でした)は、主に数学に関するなんやかんやの処理が非常に便利に使えるというツールです。Pythonで書かれているためPythonでできることはもちろんできますし、Jupyter Notebook上でカーネルとして利用することもできます。同様の数式処理システムにMaximaというLISPで書かれたものがあるのですが、これはSageMathに同梱されていますので、個人的にはそちらもよく使います。

SageMath - Open-Source Mathematical Software System

Maxima, a Computer Algebra System

SageMathのインストール

SageMath Download - osx/intel

SageMathをMacにインストールするときには、上のリンクから「sage-8.9-OSX_10.14.6-x86_64.dmg」(2019.12.04時点)をクリックしてダウンロードして、お使いのシェルの設定ファイルにこんな(↓)エイリアスを書いてあげればOKです。最新バージョンは8.9です。

alias sage='/Applications/SageMath/sage'

これで、ターミナルでSageMathが利用できるようになります。

f:id:long10:20191125230621p:plain

または、ターミナルで以下のコマンドを実行するとJupyter notebookが開きます。

sage -n jupyter

カーネルSage8.9でノートを開くと見慣れた形で表示されます。

f:id:long10:20191202232124p:plain

あるいは、このように notebook() と入力すると、The Sage Notebook が開きます。

f:id:long10:20191202232302p:plain

見た目がちょっと違いますが使い方はほとんど同じです。

f:id:long10:20191202232448p:plain

ここで最初にお伝えしておきたいのが、SageMathは結構デカいということです。なので、ローカル環境にインストールするとまあまあ容量を食います。それが嫌という方はオンラインでの利用をおすすめします。ちょっと試してみたいという場合は、こちら(↓)の「Sage Cell Server」がおすすめです。

Sage Cell Server

Sage Cell Serverを使うと小窓にコードを書いてEvaluateボタンを押すだけでSageMathのBuilt-in関数などがそのまま利用できてとても便利です。SageMathのサンプル集からコピペするだけで、こんな感じのシェルピンスキーのギャスケットが簡単に描けます。

f:id:long10:20191125232301p:plain

あるいは、もっとちゃんと使いたいという方にはこちらの「CoCalc」というサービスがおすすめです。

cocalc.com

こちらの「Sage worksheet」を選択してワークシートを作成すると、「.sagews」という拡張子のファイルが作成されます。見た目はちょっと違うんですが、操作方法などはJupyter Notebookとほとんど同じですので、使い勝手はいいと思います。少し使うだけなら無料で問題ないと思うのですが、使う頻度が多くなってメモリやCPU数を増やしたいという場合には有料となりますのでご注意ください。

f:id:long10:20191125233116p:plain

SageMakerでSageMathを使いたい!

SageMakerとSageMath、とても似た名前ですが全くの別物です。SageMakerはAWSが提供する機械学習サービスで、すべての開発者とデータサイエンティストに機械学習モデルの構築、トレーニング、デプロイ手段を提供するために生まれたサービスです。SageMakerではJupyter NotebookをVMインスタンスとして立ち上げることが可能なので、このノートブックインスタンスにSageMathを入れて普段から使っていきたいと思います。

f:id:long10:20191201004620p:plain

さっそくノートブックインスタンスを立ち上げます。

f:id:long10:20191126163659p:plain

ノートブックインスタンスのJupyter Notebookを開いてPython2をアクティベートします。

f:id:long10:20191126164201p:plain

SageMathは基本Python2で動きます。Python3については以下のSageMath FAQで以下のような言及がありますので、実験的ではありますが利用は可能のようです。(個人的には実験していないのでぜひチャレンジした結果を教えていただきたいです。)

As of August 2019, most of SageMath works fine with Python 3. 
However, we still consider Python 3 support to be experimental and no official Python 3 release has been made yet.  
You can build the source code of SageMath with Python 3 using the instructions at the bottom of 
https://wiki.sagemath.org/Python3-compatible%20code  See trac ticket #15530 and trac ticket #26212 for tracking the current progress.

ここで、 こちらのSageMathの公式にしたがってconda経由でインストールしていきます。

Install from conda-forge — Sage Installation Guide v8.9

conda install mamba -c conda-forge
mamba create -n sage sage -c conda-forge

そうすると、このようにターミナル上でsageコマンドが利用できるようになります。

f:id:long10:20191130170718p:plain

しかし、そのままだと sage の文字が濃い青色でとても見づらいので、$HOME/.sage/init.sage ファイルを作成して以下の1行を追加してあげると見やすくていい感じになります。

%colors Linux

こんな感じです。

f:id:long10:20191201133959p:plain

これらの設定を毎回インスタンス起動のたびにやるのはとても面倒ですので、SageMakerにはライフサイクル設定という仕組みがあります。そちらにここまでのコマンドを追記することで、インスタンス起動時にSageMathが使える状態にすることが可能です。詳しくは(↓)こちら。

ノートブックインスタンスをカスタマイズする - Amazon SageMaker

また、このようにSageMathの環境が利用できるカーネルに追加されます。

f:id:long10:20191130171053p:plain

ちなみに、iPadを利用している方向けの話になってしまいますが、Junoというアプリを使って、SageMakerのノートブックインスタンスやCoCalcと連携させることが可能です。そうすると、自分の描いた3Dグラフを直接指で触って回転したりできるのでとても面白いです。

iPad上で描いたグラフを指で回わしているところ

f:id:long10:20191201142719g:plain

有料ですが個人的にはとても重宝しています。

Juno Connect for Jupyter

Juno Connect for Jupyter

  • Rational Matter
  • 仕事効率化
  • ¥1,220
apps.apple.com

SageMathを使ってみよう

それでは、ここから先はSageMathを実際に使っていきたいと思います。基本的にはPythonと同じように利用することができるので、SageMathを利用されたことがない人もすぐに慣れると思います。

基本操作

チュートリアル的なことをしてもあまり面白くないので、その辺りはこちらのSage観光ツアーをご参照ください。

Sage観光ツアー — Sage チュートリアル v8.9

せっかくなので、この中から少し例としてサンプルを紹介してみたいと思います。

まずは、こちらの単純な2次方程式の解を解析的に求めたい場合を考えます。

f:id:long10:20191130172423p:plain

こちらの方程式の解をSageMathを使って求めるとこのよう(↓)になります。

f:id:long10:20191130172645p:plain

変数xを定義して、解きたい方程式と変数をsolve関数の引数に入れるだけです。めちゃくちゃ簡単ですね。 一応念のため、こちらを関数としてグラフを描くとこのように(↓)なるので、まあ蛇足ですがこの解で合ってそうです。

f:id:long10:20191130173133p:plain

それではもう一つ、機械学習などではたびたび登場する偏微分のかんたんな例を紹介してみようと思います。 こちらの関数をxとyそれぞれについて偏微分したい場合を考えます。

f:id:long10:20191130173633p:plain

流れは先ほどの例と同様で、変数x,yを定義して、偏微分対象の関数を定義します。微分関数であるdiff関数の引数として偏微分対象の変数を入れるだけ、です。 こちらも非常にかんたんですね。

f:id:long10:20191130175000p:plain

楕円曲線で遊んでみる

さて、ここからはちょっと素人なりに頑張って楕円曲線で遊んでみたいと思います。ここまでの例だけですと、なんだScipyあればできるじゃんという話ですが、楕円曲線j-不変量の計算などを行う際にはSageMathの力を使うととても楽チンになります。ちなみに楕円曲線は暗号化アルゴリズムでおなじみ(量子超越性によって暗号が破られる日は来るのか?!)ですが、厳密には種数 1 の非特異な射影代数曲線、さらに一般的には、特定の基点 O を持つ種数 1 の代数曲線のことをいいます。では早速、以下のヴァイエルシュトラス方程式が与えられていると仮定したときに無限遠点を群演算における零元であるとして話を進めていきましょう。

f:id:long10:20191130181850p:plain

まず、手始めに具体的な係数を当てはめて適当な楕円曲線を作ってみます。

f:id:long10:20191130183903p:plain

この楕円曲線をSageMathではこのように(↓)表現します。(ここからはCoCalcを使っていきます。)

f:id:long10:20191130184049p:plain

EllipticCurveコンストラクタの引数は、EllipticCurve([a1, a2, a3, a4, a6 ])と以下の係数が対応しています。(対応する順序が訳のわからないことになっているので注意してください。)

f:id:long10:20191130184840p:plain

j-不変量 j = 1 を持つ楕円曲線を生成したい場合は、このように(↓)表現します。

f:id:long10:20191130185134p:plain

このことから、j-不変量 1 の楕円曲線が以下のような式(↓)になることがわかります。楽しいですね。

f:id:long10:20191130185505p:plain

それではここから、以下の代表的な楕円曲線についてSageMathを使っていろいろと遊んでいくことにしましょう。

f:id:long10:20191130185739p:plain

この楕円曲線のグラフを描くと、このように点(0,0)を通りつながった円の形状と、右にハの字に広がった形状とがある、いわゆるよくみる楕円曲線の形になっています。

f:id:long10:20191130185856p:plain

さて、それではSageMathを使ってこの楕円曲線の持つ加法群構造を調べていきたいと思います。

まずはこのように(↓)対象の楕円曲線を生成します。

f:id:long10:20191130190405p:plain

一応念のため、ちゃんと種数1の非特異曲線になっているか確認してみます。

f:id:long10:20191202091245p:plain

数式を確認したい場合はwebブラウザ上で数式を明瞭に表示するためMathJaxをimportしてインターフェースを利用します。

f:id:long10:20191202152241p:plain

有理点(0,0)について、無限遠点が零元、同一曲線上の3点を加えると0となる加法群の構造を備えているか調べます。 確かに計算してみると、3点加えると0で、倍数も一見訳のわからない有理数ではありますが、ちゃんと同一曲線上で有理点になっていることが確認できます。

f:id:long10:20191130190949p:plain

導手の数もこのように簡単に表せます。

f:id:long10:20191130191325p:plain

最後に、この楕円曲線に随伴するL-級数、あるいはモジュラー形式の係数、あと階数をSageMathを計算してみます。あっという間です、膨大な手計算されていた頃の偉大なる数学者たちに対して申し訳ないほどの簡単さです。

f:id:long10:20191130192329p:plain

こちらの例では30ですが、係数を10000以下すべてとした場合でも、計算するのにかかる時間はたったの約1秒ほどです。非常に高速に計算することができます。

ちょっとだけMaximaの紹介

冒頭で、ちょっとだけ触れましたがMaximaという数式処理システムがありまして、そちらはLISP言語の実装なのですがインターフェースがSageMathに同梱されているのでそのまま使うことができます。実はMaximaを単体でインストールして使う場合はgnuplotなどの設定が少々癖があって面倒なのですが、同梱されているのでサクッと使えて便利です。

たとえばこちらの1行で何が表示されるかといいますと。。

 maxima.plot3d("[cos(x)*(3 + y*cos(x/2)), sin(x)*(3 + y*cos(x/2)), y*sin(x/2)]","[x, -4, 4]", "[y, -4, 4]", '[plot_format, openmath]')

このように「メビウスの帯」がXmaximaに表示されます。

f:id:long10:20191201150556p:plain

Xmaximaはマウスでグルグル動かすことができるので先ほどのJunoがなくても手軽にグラフの形状を確認できて便利です。

ルービックキューブ群

SageMathに同梱されているインターフェースといえば、ちょっと変わり種のものとしてルービックキューブ群があります。 こういう感じでキューブの展開図を表示できます。

f:id:long10:20191204104655p:plain

あるキューブの状態のときの解法を出力してくれて、その解が正しいかも確認できます。

f:id:long10:20191204105301p:plain

実務で使いどころ

さて、冒頭でご紹介した通り、SageMathは数学に関するなんやかんやの処理が非常に便利に使えるという点については、ここまで一通り使い方をみていただいて「ふむふむ、なるほど」とご理解いただけたものとして、では実際の業務にどのように応用していくのかという点がやっぱり気になると思います。

それについて結論からお伝えしますと、現時点では、分析や最適化問題のシミュレーションがメインです。とはいえ別にプロダクションコードには使えないというわけではないんですが、やっぱりSageMathがライブラリとしてサイズがデカすぎるのでポータビリティが非常に低いという点がネックになります。(デプロイも大変ですしね。)

ただ、いずれもしかしたら、今後どこかのタイミングで楕円曲線やTDA(topological data analysis)など活用する機会がでてくるようなことがあれば、SageMathを利用したプロダクションコードが生まれる可能性もなくはないと個人的にはとても期待しています。ですので、まあ今はその準備段階と捉えて必要に応じて適度に利用しているといったところです。

それを踏まえまして、よく実務で登場する最適化問題の例としてナップザック問題と、機械学習ではおなじみの最尤推定でよく使うEMアルゴリズムについてご紹介しましょう。

f:id:long10:20181105162634p:plain

まず、ナップザック問題ですが、自分で実装するとなるとまあまあめんどくさいと感じている方は多いのではないでしょうか?(最終的には制約が多くなってしまうので、実装が複雑になるのは当然だとしても、少なくとも導入当初はサクッと評価したいですよね。)実はSageMathにはknapsack関数があらかじめ用意されています。 たとえばこんな問題があるとします。

問題

4つの商品(重さ、価値)で定義される項目が、
それぞれA(100g, 200円)、B(150g, 100円)、C(50g, 300円)、 D(70g, 250円)でした。
バッグの最大重量200gのとき、価格を最大にして詰められる組み合わせはどれか?

これはつまり、福袋はある程度軽くて価値が高いものがいっぱい入ってると嬉しいといった趣旨の問題ですね。

SageMathを使えば、このように(↓)knapsack関数をimportしてそれぞれの条件の組みを引数に渡すだけで、はい終わりです。

f:id:long10:20191130234214p:plain

一見、100gの商品Aを入れた方が高額になりそうな気もしますが、実はCとDを詰めたほうが全体では高額になるということがわかりました。

続きまして、EMアルゴリズムも一から実装するとなるとそれなりに骨が折れると思います。この場合もSageMathのBuilt-in関数を使って気楽に実装することができます。 今回はPRMLの例からよくある混合ガウス分布のEMアルゴリズムについてSageMathで実装するケースをご紹介させていただきます。EMアルゴリズム自体の説明は割愛させていただきますが、レコメンドでの利用だけでなく混合ガウス分布は割と実務のEDAでも頻繁に登場しますのでEMアルゴリズムを活用する場面はその点でも多いかと思います。

なお、こちらの例では平均初期値を(−1.5, 0.5) , (1.5, −0.5)とし、分散初期値を0.5として計算した場合となっています。

まず、全体として以下のような(↓)関数呼び出しを考えます。

pi_k = [0.5, 0.5]
mu_k = [vector([-1.5, 0.5]), vector([1.5, -0.5])]
sigma_k = [matrix(RDF, D, D, beta*ident), matrix(RDF, D, D, beta*ident)]

EM(pi_k, mu_k, sigma_k)

そしてこちらが(↓)EM()の実装です。これは基本的にEMアルゴリズムをそのまま実装したものになります。

def EM(pi_k, mu_k, sigma_k):
    diff = 1
    for l in range(21):
        pi_N = matrix(RDF, [[pi_k[k] * _gauss(X[n], mu_k[k], sigma_k[k]) for k in range(K)] for n in range(N)])
        o_lnP = ln_p()
        gamma = matrix(RDF, N, K)
        for n in range(N):
            gamma.set_row(n, pi_N[n]/(pi_N[n].sum()))
        # Eステップ
        if l == 0:
            action_f(mu_k, sigma_k, gamma)
        # Mステップ
        N_k = [gamma.column(k).sum() for k in range(K)]
        for k in range(K):
            mu_k[k] = 0
            for n in range(N):
                mu_k[k] += gamma[n][k]*X[n]
            mu_k[k] /= N_k[k]
            sigma_k[k] = 0
            for n in range(N):
                sigma_k[k] =  sigma_k[k] + gamma[n][k]*covMatrix(X[n], mu_k[k])
            sigma_k[k] /= N_k[k]
            pi_k[k] = N_k[k]/N
        lnP = ln_p()
        diff = abs(lnP - o_lnP)
        o_lnP = lnP
        # print
        if action_idx.has_key(l):
            action_f(mu_k, sigma_k, gamma)

x, yは混合ガウス分布に従ったデータであるものとして、各種定数の初期化を行います。

# x, y は混合ガウス分布
# 定数の初期化
D = 2
K = 2
N = len(x)
beta = 0.5
X = matrix(RDF, zip(x, y))
ident = identity_matrix(2)
# アクションのインデックス
action_idx = {0:0, 1:1, 5:5, 20:20}

最後に、EM()関数から呼び出される必要な関数を定義します。

def _gauss(v, mu, sigma):
    d = len(v);
    sigma_inv = sigma.inverse();
    sigma_abs_sqrt = sigma.det().sqrt();
    val = -(v - mu) * sigma_inv * (v - mu).column()/2;
    a = (2*pi)**(-d/2) * sigma_abs_sqrt**-0.5
    return a * e**val[0];

def covMatrix(x, u):
    d = x - u
    return matrix(RDF, [[d[i]*d[j] for j in range(D)] for i in range(D)])


def ln_p():
    sum = 0.0
    for n in range(N):
        sum += ln(pi_N[n].sum())
    return sum

def action_f(mu_k, sigma_k, gamma):
    pt_plt = Graphics()
    for n in range(N):
        pt_plt += point(X[n], rgbcolor=(gamma[n][1], 0, gamma[n][0]))
    r_cnt_plt = contour_plot(lambda x, y : _gauss(vector([x, y]), mu_k[1], sigma_k[1]), [x, -2.5, 2.5], [y, -2.5, 2.5], 
    contours = 1, cmap=['red'], fill=False)
    b_cnt_plt = contour_plot(lambda x, y : _gauss(vector([x, y]), mu_k[0], sigma_k[0]), [x, -2.5, 2.5], [y, -2.5, 2.5], 
    contours = 1, cmap=['blue'], fill=False)
    (r_cnt_plt + b_cnt_plt + pt_plt).show(aspect_ratio=1, figsize=(3), xmin=-2.5, xmax=2.5, ymin=-2.5, ymax=2.5)

まとめ

いかがでしたでしょうか?

本日は、Jupyter Notebookもいいんだけど、それプラスアルファのことができるSageMathをぜひ使ってみてはいかがでしょうというご紹介をさせていただきました。SageMathって何?というところから出発して、インストール方法やコマンドでの利用やオンラインでの利用についてご紹介した後、ほんの一部ですがSageMathの簡単な使い方などご紹介をさせていただきました。こんな便利なツールが無料であるなんて今の時代は本当に恵まれていますね、ぼくが学生時代にSageMath欲しかったなぁと心底思います。

参考

Sage チュートリアル http://doc.sagemath.org/pdf/ja/tutorial/tutorial-jp.pdf

SageMath - Tour - Quickstart

EMアルゴリズム - Wikipedia

楕円曲線論入門

楕円曲線論入門

パターン認識と機械学習 上

パターン認識と機械学習 上

  • 作者:C.M. ビショップ
  • 出版社/メーカー: 丸善出版
  • 発売日: 2012/04/05
  • メディア: 単行本(ソフトカバー)

パターン認識と機械学習 下 (ベイズ理論による統計的予測)

パターン認識と機械学習 下 (ベイズ理論による統計的予測)

  • 作者:C.M. ビショップ
  • 出版社/メーカー: 丸善出版
  • 発売日: 2012/02/29
  • メディア: 単行本

さいごに

さて次回は、サーバサイドエンジニアの安尾が「スピード優先の開発で溜まった技術的負債の返済計画(サーバーサイド編)」というタイトルで投稿します!
お楽しみに!

qiita.com

adventar.org

ちなみに

delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ!

CXOとVPoEへのインタビュー記事はこちら!

wevox.io

NetflixのFast JSON APIを使ってみた

はじめに

はじめまして。 mochizukiです。

クラシルアプリのサーバーサイドをやってます。

昨日はAndroidエンジニアのumemoriさんが

「マルチモジュール時代のDagger2によるDI」

という記事を書いてくれました。

tech.dely.jp

dely Advent Calendar 2019の2日目は
Netflixがつくった Fast JSON API について書いてみようと思います。

qiita.com

adventar.org

Fast JSON API

f:id:mochizuki_pg:20191130101708p:plain

Netflix/fast_jsonapi

A lightning fast JSON:API serializer for Ruby Objects.

Performance Comparison We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least 25 times faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the performance document for any questions related to methodology.

Benchmark times for 250 records

$ rspec
Active Model Serializer serialized 250 records in 138.71 ms
Fast JSON API serialized 250 records in 3.01 ms

This gem is currently used in several APIs at Netflix and has reduced the response times by more than half on many of these APIs.

使ってみる

まずは準備


ruby 2.6.3
rails 6.0.0


rails new fast_json_api --api


+ gem 'fast_jsonapi'

追加して

class Recipe < ApplicationRecord
  has_many :ingredients, dependent: :destroy
end


class Ingredient < ApplicationRecord
  belongs_to :recipe
end

今回はクラシルっぽくこんな感じで。

中身は

create_table :recipes do |t|
  t.string :title, null: false
  t.text :introduction
  t.timestamps
end
create_table :ingredients do |t|
  t.references :recipe, null: false
  t.string :name, null: false
  t.string :quantity_and_unit, null: false
  t.timestamps
end

こんな感じで。

データは最近クラシルに実装した
ジャンル別ランキング機能より、殿堂入りの

f:id:mochizuki_pg:20191130095256j:plain

これにします!

recipes = [
  ['ネギダレが美味しい!鶏もも肉のソテー', 'ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。']
]
recipes.each_with_index do |array, index|
  title = array[0]
  introduction = array[1]
  Recipe.create({
    title: title,
    introduction: introduction
  })
end
ingredients = [
  ['鶏もも肉', '300g'], ['塩こしょう', '小さじ1/4'], ['片栗粉', '大さじ2'], ['長ねぎ', '1本'],
  ['①しょうゆ', '大さじ1.5'], ['①酢', '小さじ1'], ['①砂糖', '大さじ1'],
  ['①ごま油', '小さじ1'], ['①白すりごま', '大さじ1'], ['①すりおろし生姜', '小さじ1/2'],
  ['サラダ油', '大さじ1'], ['リーフレタス', '2枚']
]
Recipe.all.each do |recipe|
  ingredients.each_with_index do |array, index|
    name = array[0]
    quantity_and_unit = array[1]
    recipe.ingredients.create({
      name: name,
      quantity_and_unit: quantity_and_unit
    })
  end
end

そして

class Api::V1::RecipesController < ApplicationController
end

こんな感じ。

やっと本題

基本的に ActiveModelSerializers と同様に使えます。
rails g で、serializerつくるとこんな感じです。

class RecipeSerializer
  include FastJsonapi::ObjectSerializer
  attributes 
end

なので

class RecipeSerializer
  include FastJsonapi::ObjectSerializer
  has_many :ingredients
  attributes :title, :introduction
end


class IngredientSerializer
  include FastJsonapi::ObjectSerializer
  belongs_to :recipe
  attributes :name, :quantity_and_unit
end

こんな感じで定義します。

今回つくるAPI

殿堂入りレシピの ネギダレが美味しい!鶏もも肉のソテー
レシピ詳細を想定してつくってみます。

f:id:mochizuki_pg:20191130095726j:plain

GET /api/v1/recipes/:id

class Api::V1::RecipesController < ApplicationController
  def show
    recipe = Recipe.find(params[:id])
    json_string = RecipeSerializer.new(recipe).serialized_json
    render json: json_string
  end
end


{
    "data": {
        "id": "1",
        "type": "recipe",
        "attributes": {
            "title": "ネギダレが美味しい!鶏もも肉のソテー",
            "introduction": "ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。"
        },
        "relationships": {
            "ingredients": {
                "data": [
                    {
                        "id": "1",
                        "type": "ingredient"
                    },
                    {
                        "id": "2",
                        "type": "ingredient"
                    },

(一部のみ表示)
リレーションが紐付いてます。

Started GET "/api/v1/recipes/1" for ::1 at 
   (0.2ms)  SELECT sqlite_version(*)
Processing by Api::V1::RecipesController#show as JSON
  Parameters: {"id"=>"1"}
  Recipe Load (0.3ms)  SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/recipes_controller.rb:3:in `show'
   (0.2ms)  SELECT "ingredients"."id" FROM "ingredients" WHERE "ingredients"."recipe_id" = ?  [["recipe_id", 1]]
  ↳ app/controllers/api/v1/recipes_controller.rb:4:in `show'
Completed 200 OK in 34ms (Views: 0.6ms | ActiveRecord: 0.5ms | Allocations: 6624)

(キャッシュなしの速度)

材料と分量も返す

レシピ詳細では、材料と分量も返してあげないといけないです。
READMEに従って、

options = { include: %i[ingredients] }

こういうのをつくって

class Api::V1::RecipesController < ApplicationController
  def show
    recipe = Recipe.find(params[:id])
    options = { include: %i[ingredients] }
    json_string = RecipeSerializer.new(recipe, options).serialized_json
    render json: json_string
  end
end

渡してあげます。

{
    "data": {
        "id": "1",
        "type": "recipe",
        "attributes": {
            "title": "ネギダレが美味しい!鶏もも肉のソテー",
            "introduction": "ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。"
        },
        "relationships": {
            "ingredients": {
                "data": [
                    {
                        "id": "1",
                        "type": "ingredient"
                    },
                    {
                        "id": "2",
                        "type": "ingredient"
                    },
                ]
            }
        }
    },
    "included": [
        {
            "id": "1",
            "type": "ingredient",
            "attributes": {
                "name": "鶏もも肉",
                "quantity_and_unit": "300g"
            },
            "relationships": {
                "recipe": {
                    "data": {
                        "id": "1",
                        "type": "recipe"
                    }
                }
            }
        },
        {
            "id": "2",
            "type": "ingredient",
            "attributes": {
                "name": "塩こしょう",
                "quantity_and_unit": "小さじ1/4"
            },
            "relationships": {
                "recipe": {
                    "data": {
                        "id": "1",
                        "type": "recipe"
                    }
                }
            }
        },

(一部のみ表示)
紐付いている材料と分量が返ってきています。

Started GET "/api/v1/recipes/1" for ::1 at 
   (0.2ms)  SELECT sqlite_version(*)
Processing by Api::V1::RecipesController#show as JSON
  Parameters: {"id"=>"1"}
  Recipe Load (0.6ms)  SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/recipes_controller.rb:3:in `show'
   (0.2ms)  SELECT "ingredients"."id" FROM "ingredients" WHERE "ingredients"."recipe_id" = ?  [["recipe_id", 1]]
  ↳ app/controllers/api/v1/recipes_controller.rb:5:in `show'
  Ingredient Load (0.4ms)  SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."recipe_id" = ?  [["recipe_id", 1]]
  ↳ app/controllers/api/v1/recipes_controller.rb:5:in `show'
Completed 200 OK in 51ms (Views: 0.4ms | ActiveRecord: 1.2ms | Allocations: 13646)

(キャッシュなしの速度)

キャッシュ

READMEに従って

cache_options enabled: true, cache_length: 1.hours

こんなのつけて

class RecipeSerializer
  include FastJsonapi::ObjectSerializer
  cache_options enabled: true, cache_length: 1.hours
  has_many :ingredients
  attributes :title, :introduction
end


Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 0.8ms | Allocations: 13427)
Completed 200 OK in 13ms (Views: 0.2ms | ActiveRecord: 1.4ms | Allocations: 4940)

その他使い方

基本的に ActiveModelSerializers と同じです。
気になった方は、 README を参照してみてください。

最後に

明日はCXOの坪田さんの「突破するプロダクトマネジメント」です! 楽しみです!!

delyではエンジニアを募集しています。

www.wantedly.com

マルチモジュール時代のDagger2によるDI

こんにちは。dely株式会社のAndroidアプリチームのうめもりです。今年もdelyはAdvent Calendarをやることになりました。開発部の面々が色々な記事を今年も書いてくれますので、是非ほかの記事も見て行ってください。

qiita.com

adventar.org

この記事はdely Advent Calendarの1日目の記事です。早速やっていきましょう。

Androidのマルチモジュール構成のアプリケーション上でDagger2を用いて依存性解決を行うやり方を、簡単なマルチモジュール構成のアプリケーションを例に紹介します。

想定するモジュール構成

今回想定するアプリケーションのモジュール構成は次のようになっています。

f:id:delyumemori:20191201153322p:plain

  • App Module - メインのAndroid App Module、依存性の解決はすべてここで行う
  • UI Module - Feature Moduleで定義したクラスを利用するActivityやFragmentをここに定義する
  • Feature Module - UI Moduleから呼ばれるクラスを定義する

一般的なマルチモジュール構成に近いと思われるシンプルな構成にしてみました。実際アプリケーションが大きくなった場合は、UI ModuleやFeature Moduleを分割したりする必要が出てくると思いますが、その場合でも今回の方法と同じように対応することができるはずです。

記事内ではApp ModuleからFetaure Moduleへの依存は出てきませんが、このようなモジュール構成にするとApp ModuleからFeature Moduleへ依存する形でServiceを作成するなど、実際にはApp ModuleからFeature Moduleへの依存関係が発生すると思われます。

Dagger2のスコープ

Dagger2のスコープは

  • @Singleton
  • @ViewScope

の2つの定義にしました。

@Singleton は最初からあるスコープですが、 @ViewScopeについては新規に定義したScopeです。 Activity/FragmentごとにScopeを切るようにしてみました。

Scopeの定義は次のようになっています。

@dagger.Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ViewScope {
}

Feature Module側では何も考えずに依存性を定義して@Injectをつける

Feature Module内では、通常のDagger2と同じようにクラス定義を行うだけです。

クラス定義の例

class RecipeFeatureImpl @javax.inject.Inject internal constructor(
    recipeApiProvider: RecipeApiProvider
) : RecipeFeature {

    private val recipeApi = recipeApiProvider.recipeApi

    override fun createLatestRecipeFeedContainer(): FeedContainer<RecipeEntity> = FeedContainer(
        LatestRecipeFeedApi(recipeApi),
        20
    )
}

Dagger2のModule定義が必要な場合は、Feature Module内に定義してもよいですし、App Module内に定義してそれを使ってしまうのもよいと思いますが、依存性解決の定義についてはApp Module内でほぼすべてを行うので、そちらに集約してしまうのがよいかもしれません。

UI Moduleではinject用のinterfaceを定義する

UI ModuleではFeature Module側で定義したクラスに依存する形でActivityやFragmentを定義します。

あまり規模の大きくないアプリケーションでは実際にはこのModuleでDagger2のComponent定義を行ってしまってもいいとは思いますが、今回はComponent定義をUI定義から分離することを考えます。

必要なのは、ActivityやFragmentに対して依存性を注入するためのinterfaceです。なのでまずはそれを素直に定義しましょう。

injector定義のサンプル

interface ViewInjectors {

    fun inject(mainActivity: MainActivity)

    fun inject(recipeDetailActivity: RecipeDetailActivity)

    fun inject(latestRecipeFeedFragment: RecipeFeedLatestFragment)
}

Dagger2のComponent定義はApp Module内で行うので、アノテーションなどをつける必要はありません。

さて、実際のActivityやFragment内では定義したinterfaceを使って依存性注入処理をする必要があるので、 どこからかinject用のinterfaceを取得して注入用のメソッドを呼ぶ必要があります。

今回は、カスタムApplicationクラスにinterfaceを実装することにして、Applicationクラスからinterfaceを取得するinterfaceを作成しましょう。実際のコードはこのようになります。

interface ViewInjectorsProvider {

    fun provide(activity: FragmentActivity): ViewInjectors

    fun provide(fragment: Fragment): ViewInjectors
}

そして、Activity/Fragment側では、定義したinterfaceにキャストしてinjectするコードを記述します。

class MainActivity : AppCompatActivity() {

    @javax.inject.Inject
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        (application as ViewInjectorsProvider).inject(this)
        
        // 省略
    }
}

Dagger2のComponentはApp Moduleに定義し、依存性解決を全て行う

最後に、App Module内でDagger2のComponent定義を行います。今回は、@Singletonと@ViewScopeの二つのScope定義があるので、Component定義も2つになります。

@javax.inject.Singleton
@dagger.Component(
    modules = // 依存Moduleを記述
)
interface SampleSingletonComponent {

    fun viewComponent(
        // SampleViewComponentの依存Moduleを記述
    ): SampleViewComponent
}

Fragment/Activity側で実際にinjectするScopeのComponentは、UI Moduleで定義したinterfaceを継承します。

@ViewScope
@dagger.Subcomponent(
    modules = // 依存Moduleを記述
)
interface SampleViewComponent : ViewInjectors

Component定義を行ったら、今度はApplicationに先ほどUI Moduleで定義したinterfaceを実装しましょう。一度ビルドを行えば、Daggerによって生成されたComponentを参照できるようになっているはずです。

class SampleApplication : Application(), ViewInjectorsProvider {

    private lateinit var appComponent: SampleAppComponent

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerSampleAppComponent.create()
    }

    override fun provide(activity: FragmentActivity) = appComponent.viewComponent(
        // インスタンス化に必要なModuleを渡す
    )

    override fun provide(fragment: Fragment) = appComponent.viewComponent(
        // インスタンス化に必要なModuleを渡す
    )
}

終わりに

以上、マルチモジュール構成のアプリケーションでDagger2による依存性解決を行うやり方を紹介しました。

ポイントとしては、依存性解決にかかわるコードは最終的にモジュールを集約するモジュールにまとめて記述するようにするという点です。そこさえ守っていればそう難しくはありません。

さて、明日(12/2)はサーバーサイドエンジニアの望月さんによる「NetflixのFast JSON APIを使ってみた」という記事です。お楽しみに。

qiita.com

adventar.org

ちなみに

delyではAndroidエンジニアを絶賛募集しております、もしご興味あればこちらのリンクからご気軽にエントリーください!

www.wantedly.com

delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ!

CXOとVPoEへのインタビュー記事はこちら!

wevox.io

宣言的UIフレームワーク 「SwiftUI」と「Flutter」を比較してみた

こんにちは!クラシルiOSアプリを開発しているknchstです。

昨今のモバイルアプリケーション開発では様々な要件があり、それらを満たすよう実装するには数々の苦難がありました。その一つとしてUIの状態、所謂State管理が難しくなってきています。ネットワークに接続し、またUIをアニメーションさせたりと、データとUIを同期するのは困難を極めます。

Rxなどのリアクティブフレームワークの利用が当たり前になり、ReactNaviteやFlutterなどのフレームワークをプロダクトに採用する企業も増えてきて、モバイルアプリのトレンドの風も、まさにこの方向に向かって吹き始めていました。

そして今年のWWDCでAppleがSwiftUIを発表してついに、その風は大きくなり今後のモバイルアプリの方向性を決定付けたと言っても過言ではないでしょうか。

今回は、SwiftUIとFlutterでアプリを開発する際の実装の違いをサンプルアプリを通して比較していきたいと思います。

概要

SwiftUIとは

f:id:knchst:20190926005627p:plain

SwiftUIは今年のWWDC2019でサプライズ発表された「宣言型UIフレームワーク」です。

特徴としては

  • iOS, iPadOS, macOS, watchOS, tvOSなどのすべてのAppleプラットフォーム向けに開発することができる
  • 宣言型シンタックス(Swift)
  • Live Preview
  • デザインツール

Flutterとは

f:id:knchst:20190926005010p:plain

FlutterはGoogleが2年ほど前にベータ版としてリリースされたフレームワークになります。クロスプラットフォームなアプリの開発フレームワークとして最近は様々な企業が導入しています。

特徴としては

  • 宣言的なシンタックス(Dart)
  • iOS, Android, Web に対応したクロスプラットフォーム
  • ネイティブパフォーマンス
  • 豊富な標準コンポーネント(Widget)
  • Hot Reload
  • Pluginによる高い拡張性

比較してみる

SwiftUIとFlutterの比較はSwiftUIの公式チュートリアルでも、紹介されていた Landmarks というアプリで行います。Flutterで同じ Landmarks を再現したものがあるので、それを用いて比較していきます。

f:id:knchst:20190926023238p:plain

ソースコードはそれぞれ以下から取得できます。

開発環境

  • SwiftUI - Xcode

f:id:knchst:20190926103956p:plain

SwiftUIの開発環境でも、Appleのプラットフォームのアプリ開発を長年支えてきたXcodeが担当します。SwiftUIの開発環境にはコードの変更がリアルタイムにプレビューされる機能(画像右)があり、iOSなどの様々な機能(ダークモード、アクセシビリティなど)もシミュレーションすることができます。


  • Flutter - Android Studio / Visual Studio Code

f:id:knchst:20190926015446p:plain

Flutterの開発環境はAndroid開発でもおなじみのAndroid StudioまたはMicrosoftが開発したコードエディタのVisual Studio Codeで開発することができます。



XcodeにはSwiftUI向けにデザインツールが提供されており、GUIでコンポーネントを追加したりプロパティを変更することができ、Flutterの開発環境よりも強力です。

UI

  • SwiftUI - View

ViewはProtocolとして定義されており、Viewを定義するにはこれに準拠し、bodyプロパティを実装する必要があります。bodyプロパティでコンポーネントを返すことでそのViewが描画されます。

以下のサンプルコードでは、Hello Worldと表示するTextコンポーネントを返しています。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}


  • Flutter - Widget

FlutterのUIコンポーネントはWidgetクラスを継承しています。

Flutter で用意されている Widget にはStateを持たないStatelessWidget と Stateを持つStatefulWidget があります。

以下のサンプルコードでは中央にHello Worldと表示するTextコンポーネントを使ったことになります。

import 'package:flutter/material.dart';

class ContentView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text("Hello World"),
    );
  }
}



Layout

SwiftUIとFlutterのレイアウト方法は大きく違いはありません。それぞれX, Y, Zの方向にレイアウトをすることができます。

f:id:knchst:20190926111118p:plain

Landmarksサンプルアプリでそれぞれの実装の違いを見ていきます。

f:id:knchst:20190926111302p:plain

LandmarksアプリのホームはリストのUIになっていて、各行は水平レイアウトで構築されています。

f:id:knchst:20190926111539p:plain

SwiftUIはHStackを利用し、その中にImage, Text, Spacerなどのコンポーネントを配置していきます。

FultterはRowを利用し、同じくコンポーネント配置していきます。

それぞれサンプルコードは以下になります。

  • SwiftUI
HStack {
    landmark.image
        .resizable()
        .frame(width: 50, height: 50)
    Text(verbatim: landmark.name)
    Spacer()

    if landmark.isFavorite {
        Image(systemName: "star.fill")
            .imageScale(.medium)
            .foregroundColor(.yellow)
    }
}


  • Flutter
Row(
  children: <Widget>[
    Image.asset(
      'assets/${landmark.imageName}.jpg',
      width: 50.0
    ),
    SizedBox(
      width: 16,
    ),
    Text(
      landmark.name,
      style: TextStyle(fontSize: 16),
    ),
    Expanded(
      child: Container(),
    ),
    landmark.isFavorite ? StarButton(isFavorite: landmark.isFavorite) : Container(),
    Icon(
      Icons.arrow_forward_ios,
      size: 15.0,
      color: const Color(0xFFD3D3D3),
    ),
  ]
)


SwiftUIにはデフォルトでXcodeがマージンなどを設定してくれる分コードの記述が減りシンプルに見えますが、Flutterのレイアウトに関する命名の方がより直感的でわかりやすいように感じました。


List

UIKitで利用していた、UITableViewはSwiftUIではListになります。

Flutterでは、複数の選択肢が提供されています。ListViewを使用して複数行のコンテンツを表示したり、SingleChildScrollViewを使用してスクロール可能なコンテンツを表示することができます。

LandmarksアプリではホームはリストのViewになります。 SwiftUIではForEachを使用してこのリストの要素を作成し、FlutterリストはSliverListを使用します。 両方のコードは次のとおりです。

  • SwiftUI
List {
    ForEach(landmarks) { landmark in
        LandmarkRow(landmark: landmark)
    }
}


  • Flutter
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      final landmark = landmarks[index];
      return LandmarkCell(
        landmark: landmark,
      );
    },
    childCount: landmarks.length,
  ),
)

Navigation

SwiftUIのNavigationはNavigationViewとNavigationLinkで実現します。

対してFlutterはRouteとNavigatorを使います。以下はそれぞれのサンプルコードです。

  • SwiftUI
NavigationView { // NavigationViewでラップする
    List {
        ForEach(userData.landmarks) { landmark in
            NavigationLink(
                destination: LandmarkDetail(landmark: landmark) // 遷移先を指定
            ) {
                LandmarkRow(landmark: landmark)
            }
        }
    }
    .navigationBarTitle(Text("Landmarks")) // タイトルバーを設置
}


  • Flutter
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      final landmark = landmarks[index];
      return LandmarkCell(
        landmark: landmark,
        onTap: () { // セルにタップイベント
          Navigator.push(
            context,
            Route(
              builder: (context) => LandmarkDetail(
                landmark: landmark,
              ),
            ),
          );
        },
      );
    },
    childCount: landmarks.length
  )
)


Flutterはタップのイベントに対してNavigationを記述しないといけませんが、SwiftUIでは遷移先のLinkを設定します。

State Management

Stateの管理は宣言型UIでもっとも大事な要素の一つです。

Landmarksアプリ内にお気に入り追加したlandmarkを絞り込むスイッチがあります。

f:id:knchst:20190926121649p:plain

SwiftUIとFlutterのStateの管理方法は別々のアプローチを取っています。 SwiftUIはStatefulなデータをUIにバインドさせます。 一方でFlutterはデータが更新された後、setStateメソッドで呼び出すことWidgetを更新しています。

  • SwiftUI
struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

@Stateが付いているプロパティshowFavoritesOnlyがStateを管理しています。 $マークをプロパティの前につけることによって UIにデータをバインドさせています。 showFavoritesOnlyの変更に合わせてバインドされているUIが変わり、UIが変わるとshowFavoritesOnlyの値も変わります。


  • Flutter
CupertinoSwitch(
  value: _showFavoritesOnly,
  onChanged: (state) {
    setState(() {
      _showFavoritesOnly = state;
    });
  },
)

CupertinoSwitchはスイッチボタンであり、値が変更されるとonChangedメソッドが呼び出され、次にsetStateメソッドを呼び出して変数showFavoritesOnlyの新しい値を設定し、UIを更新します。


まとめ

サンプルアプリを見ながらSwiftUIとFlutterの実装の比較をしてきました。もちろん現実のアプリは様々な要件があり、サンプルアプリよりも多くの比較対象があるかと思います。

今回紹介した宣言型UIに基づいた2つのフレームワークは多くの類似点がある印象でした。 とはいえ、それぞれが独自の思想と機能を持っているので要件に合わせて使い分ける必要もあるかと思います。サンプルアプリ程度ではわからない「つらみ」などもあると思うので、今後チャンスを伺ってプロダクトに導入していきたいです。


delyでは新しいメンバーを積極的に募集しています!
もしご興味があればご応募・ご連絡ください!

speakerdeck.com

iOS版クラシルのフィードを滑らかな動きにするためにやったこと

f:id:takaoh717:20191007184951p:plain

こんにちは、iOSエンジニアのtakao(takaoh717)です

今回はクラシルiOSアプリのフィードのパフォーマンス改善を行った話をご紹介します。
改善を行ったフィードはUICollectionViewで構成されており、レシピ、画像バナー、広告など複数の異なる型のデータを表示しているような画面です。

今回行った変更は以下の内容です。

  • 差分更新ライブラリの導入とデータの管理、更新ロジックの変更
  • セルのサイズ計算を事前に行うよう修正
  • 通信時やログ送信時の重い処理をバックグラウンドスレッドで実行

改善前の課題

改善を行う前は、アプリを動かしていると実際に分かるレベルでパフォーマンスに問題がありました。

  • スクロール自体の挙動が若干重くてスムーズじゃない(指の動きに対して若干ひっかかりがある)
  • ページングの読み込みをしたときにスクロールが止まることがある
  • 更新時に画面がチラつくことがある

差分更新ライブラリの導入とロジックの見直し

まず最初に、差分更新ライブラリの導入を行いました。 これまでは、一部分のみ自前のロジックで差分更新を行って、基本的にはreloadData()を多用しているような状態でした。
何度か差分更新を行うようにしたことはあるのですが、更新タイミングによるクラッシュなどが度々発生し、結局reloadData()に戻すようなことをしていました。 そこで、今回のリファクタリングを気にreloadData()を使用しない状態にしておきたかったので、ライブラリを導入しました。

DifferenceKitの導入

差分更新用のライブラリはDifferenceKitを選択しました。

github.com

選定理由では以下の点が判断基準になりました。

  • パフォーマンスの高さ
  • プロジェクトへの導入コスト、実装コストの低さ

パフォーマンスについては実際に計測して比較はしていないのですが、公式のドキュメントに記載されている内容では、RxDataSourcesIGListKitよりも高速になっているようです。

実装面については、classだけではなくstructにも対応している点や、AnyDifferentiableを使って異なる型のデータを一つの配列で管理可能な点がポイントでした。 Examplesに動くアプリのコードが載っているのでそちらとドキュメントを参考にして実装しました。

実装方法

まず、フィードに表示するコンテンツを表すModelDifferentiableを準拠させます。

extension Model: Differentiable {
    public var differenceIdentifier: String {
        return id
    }

    public func isContentEqual(to source: Model) -> Bool {
        return title == source.title
    }
}

今回の実装ではフィードの1列を1Sectionで表現する構造にしてみました。Sectionのデータ管理をしやすいようにDataSourceは以下のような実装にしました。

// Section毎のデータを保持する配列
var dataSources = [Section]()

// Sectionに該当するデータ型を保持するためのenum
enum FeedSectionModel: Differentiable {
    case hoge
    case fuga
    case piyo
}

// 1Sectionを一つのまとまりとして管理するために定義
typealias Section = ArraySection<FeedSectionModel, AnyDifferentiable>

更新時の実装も変更前と後の配列を用意して渡してあげるだけなので、とてもシンプルな実装になります。 また、interruptに特定の条件を渡しておけば、結果がtrueになった場合に差分更新をせずにreloadData()を行うようにできます。
クラシルはUI上の1つのセルのサイズが大きいため、例えば、リストの途中の位置などで一定数以上のセルの挿入があったりすると、スクロール位置などが大きくずれてしまったりするため、一定の個数以上の更新が必要な場合などはreloadData()を行うようにしました。

let new = dataSource
new.append(ArraySection(model: FeedSectionModel.hoge, elements: newData))

// source: 更新前のデータ、target: 更新後のデータ
let changeSet = StagedChangeset(source: dataSources, target: new)

collectionView.reload(using: changeset, interrupt: { $0.changeCount > 3 }) { data in
    dataSource.data = data
}

パフォーマンスに大きく影響を与えている実装箇所の特定

ベースのリファクタリングが出来た後にどの実装が問題になっているのかを特定するため、InstrumentsのTimeProfilerを使って重い処理を特定する作業を行いました。 使い方はとてもシンプルですが、実際にアプリを動かしながら、カクつくタイミングにメインスレッドで実行している重い処理をひたすら確認していきました。

また、今回のようなMainThreadで実行されている重い処理を見たい場合の確認では以下のような設定を使用していました。

f:id:takaoh717:20190925085850p:plain
Instruments

セルのサイズ計算を事前に行う

スクロール自体の挙動が重くてスムーズな挙動にならない場合はfunc collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)のサイズ計算が問題になっているパターンがあると思います。 実際にクラシルでもレシピや広告などのデータを取得した後に、func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)の中で表示するCellのサイズ計算を行っていたため、Instrumentsで見てみるとその中身の関数のときに負荷が跳ね上がっていました。

このような、データの内容によってセルのサイズが可変になる場合、データを取得したタイミングで先にサイズ計算を行い、ModelEntityでサイズを保持するようにしました。

そうすることで、Layoutのサイズを返すときには事前に計算しておいたサイズを返すだけになり、メインスレッド上での処理が減るのでパフォーマンスが向上します。

Viewの描画に直接関わらない重い処理の修正

スクロール途中に急ブレーキがかかるようにスクロールが停止したり、次のページの読み込みが走ったタイミングでカクつきが発生したりしていた原因はViewの描画に直接関係のない重い処理をメインスレッドで行ってしまっていたものでした。 この場合も、どの処理がネックになっているかをまずInstrumentsで確認しました。

  • メインスレッドで実行していた重い処理
    • API通信後のJsonのパース
    • ログの送信
    • サーバーから取得したデータの変換処理

これらの処理をDispatchQueueを使って、適切にバックグラウンドスレッドに引き渡すことによって、読み込み時などにメインスレッドの使用率が急上昇することがなくなり、UIのカクつきが解消されました。

まとめ

UICollectionViewはUITableViewに次いで多くのiOSアプリで使われていると思います。 しかし、適切に実装していないとパフォーマンスの低下がユーザに見える形で分かりやすく出てしまうため、適度にメンテナンスすることが大事だと思います。

delyでは様々なポジションのエンジニアを積極採用中です!ご興味がある方はぜひご連絡ください。