コネヒト開発者ブログ

コネヒト開発者ブログ

社内Go勉強会を始めました

こんにちは!Webエンジニアのaboyです。最近はママリの検索体験を最高にする仕事をしています。

今回は、最近コネヒト社内でやっているGo勉強会の取り組みを紹介します。

なぜGo勉強会を始めたか

コネヒトが掲げるTech Visionの戦略の内のひとつ「Let’s Go」を盛り上げるためです。

コネヒトは、エンジニア組織及び技術領域において、何を大事にし何に投資していくかと言った未来の構想や方向性をまとめたTech Visionを掲げています。その中に、Go言語を新たに武器にしていくLet’s Goという戦略があるのですが、まだ社内にGo言語の事例も少なく、Goに関わる機会もほとんどありませんでした。

そんな中で、4月から新しく構築するシステムでGoを使うことになり、コネヒトでGoを盛り上げていくのに良いタイミングでもあったので、組織としてGoに触れる量を増やすために勉強会を始めました。コネヒトにはすでにWeb全般のテーマを扱う社内勉強会があったのですが、特定技術の話に集中しづらいため、Goに特化したものとして始めることにしました。

最初の案内

やってみてる感想

計5回開催した

4月から計5回やってみて、とりあえず社内に「Goの催しをやってるぞ」という空気は出せているかなと思います。「Tech Visionにはあるけどあんまり盛り上がっていない」状態からは少し改善したかなと。

会の内容は、気になった記事などのネタを持ち込んで、それについて雑談するという感じです。例えば以下のようなテーマはGoに全く触れたことがない人でも話題に入りやすく、盛り上がりました。

  • ディレクトリ構成どうしてる?
  • コーディング規約はある?Linterとかは?
  • Webアプリケーションフレームワークどういうのある?どう選ぶ?
  • ロギング、スタックトレースどうする?

あとは、外部の記事を紹介するのも学びが多いですが、自分が学んだことを紹介するほうが自分の言葉で話せることが多く、盛り上がる傾向があると感じます。

参加人数は5〜8人程度で、元々Goに興味がある人、以前の職場でGoを扱っていた人等が参加してくれています。コネヒトのエンジニアのうち3分の1程度が参加してくれているのでまずまずかなと思っています。参加してくれるメンバーは皆チャットをたくさん盛り上げてくれるのでとても助かっています。

一方で、人はそれなりに集まりますが、ネタが集まりにくいことが課題です。業務でGoを扱っているメンバーがまだ少ないため、意図的にネタを仕入れるようにしないと集まりにくい構造になっています。これについては今業務でGoを書いている僕が、前回開催時から今回までに学んだことを話すコーナーを設けることで一定確保するようにしてみています。

最近のaboyコーナー

また、参加者によってGoへの興味や習熟度が異なるため、扱うテーマは悩みどころです。今のコネヒトはこれからGoを導入していくフェーズなので、PHPなど既存のアプリケーションと共通の話題を混ぜた方が興味を持ってもらいやすいのではないかと感じています。前述したテーマの他には、例えばテストや静的解析のような話題ですね。

おわりに

社内でやっているGo勉強会について紹介しました!継続して、社内を盛り上げていきたいと思います ԅ( ˘ω˘ԅ)

それから、GoのLT会も開催しているので、興味がある方はぜひご参加ください〜。

connehito.connpass.com

最後に、コネヒトでは成長中のサービスを支えるために一緒に働く仲間を様々な職種で探しています。 少しでも興味もたれた方は、是非気軽にオンラインでカジュアルにお話出来ると嬉しいです!

www.wantedly.com

ECS×Fargateのオートスケールをチューニングしてサービス運営費を削減した話

こんにちは。インフラエンジニアの永井(shnagai)です。

今回は、ECS×Fargateで運用しているサービスの「ターゲット追跡ServiceAutoScalling」をチューニングをしたことで、費用が約半分になるという大きな成果を残すことが出来たのでその内容を経緯と共にまとめています。

内容はざっくり下記3点です。

  • なぜオートスケールのチューニングをしたのか?
  • 「ターゲット追跡ServiceAutoScalling」のチューニング方法
  • どんな結果になったか?

なぜオートスケールのチューニングをしたのか?

コネヒトではWebのアーキテクチャはほとんどECS×Fargateの基盤で動かしています。そして、オートスケールとして「ターゲット追跡ServiceAutoScalling」を使うことで、Fargateのメリットを最大限活かす形で運用コスト低くサービス運用を実現しています。

ここらの話は下記のスライドやブログに詳しく書いているので興味がある方はご覧ください。

ECS×Fargateで実現する運用コストほぼ0なコンテナ運用の仕組み/ ecs fargate low cost operation

speakerdeck.com

ECS×Fargate ターゲット追跡ServiceAutoScallingを使ったスパイク対策と費用削減 - コネヒト開発者ブログ

tech.connehito.com

これまで約1.5年ほど初期に設定したオートスケールのしきい値で問題なくサービス運用出来ていたのですが、ある時を境にレイテンシ悪化のアラートが頻発するようになりました。

よくわからないレイテンシ悪化だったので、アプリケーション側やインフラ側のレイテンシ悪化前後での変更、そしてアクセス特性の分析といった調査をして原因切り分けを行っていきました。

その中で、現状のオートスケールのしきい値 (CPU使用率を30%程に収束させるような「ターゲット追跡ServiceAutoScalling」の設定)では、特定コンテナのLoadAverage(LA)が跳ね上がり、そのコンテナの処理が遅くなることで平均レイテンシが上振れしているという事象を突き止めました。

スパイク的なアクセスに今までは耐えられていたのですが事象がしばらく続いてしきい値を調整しても事象は収まらなかったので、 「ターゲット追跡ServiceAutoScalling」1本で行くのは限界と判断し、新たにECSの「ステップスケーリングポリシー」を導入しました。

ECSのステップスケーリングポリシーとは何か?

「ターゲット追跡ServiceAutoScalling」がCPU使用率/メモリ使用率/リクエスト数の3つから追跡するメトリクスと値を選択することで、ECS側でタスク必要数 (起動するコンテナ数)をしきい値に収束するように動的にコントロールしてくれる機能なのに対して、「ステップスケーリングポリシー」は特定のトリガが発生した時に一気にスケールアウトして タスク必要数 を増やすような用途に使えます。

ステップスケーリングポリシー

docs.aws.amazon.com

今回の対応だと、 CPU Utilization Max の値がしきい値を超えたタイミングで、タスクを3つスケールアウトするような設定を入れました。具体の挙動としては、「ステップスケーリングポリシー」を設定すると、裏でCloudWatchAlarmが作られて、そのアラームをトリガーとしてECS側でスケールアウトが走るような仕組みになっています。

これを入れることで、スパイクの初動の段階で一気にスケールアウトが走るようになりレイテンシ悪化のアラームを抑えることに成功しました。

次なる課題

「ステップスケーリングポリシー」を導入したことによってレイテンシアラームは抑えることに成功したのですが、思想的に十分な余裕を持ってリクエストを迎えるための設定になるのでタスクの起動数が上振れ(費用増)するようになりました。「ターゲット追跡ServiceAutoScalling」のしきい値はそのままに、「ステップスケーリングポリシー」でオートスケールを追加したので当然の結果です。

ここからは、「ターゲット追跡ServiceAutoScalling」のチューニングをして、Fargateタスクの平均起動数を下げることで費用削減にチャレンジしました。

「ターゲット追跡ServiceAutoScalling」のチューニング方法

チューニングの方針として、「タスクあたりのリクエスト数(1コンテナが捌くリクエスト数)」を上げるのを目的に、「ターゲット追跡ServiceAutoScalling」でしきい値としているCPUUtilization avgを上げていくアプローチをとりました。

下記のキャプチャは、CloudWatchで見られる RequestCountPerTarget の推移なのですが、短期で見ると振れ幅ありつつも長期トレンドでは徐々に下がっていることがわかります。これが下がるということは、全体のリクエスト数は少しずつ成長しているとした場合に、1タスクあたりのコスト効率が悪化していることを意味します。

「ターゲット追跡ServiceAutoScalling」のチューニング方法としては、下記を繰り返して最適値を探りました。

  1. しきい値とするCPUUtilization avgの値を1%インクリメント
  2. 2日間問題が起きないかという様子を見る
    1. ガードレールメトリクスとして、アラートならずともレイテンシ悪化がゆるやかに起きていないかをチェックする
  3. 問題なければ1に戻る。該当期間にアラートが出たらしきい値を元に戻して2の様子見期間へ。

この繰り返しで1ヶ月程かけて最適値を探りました。チューニング方針が慎重すぎないか?という感想をもしかしたらもたれるかもしれません。

ただ、本番運用サービスでチューニングを行うので出来るだけリスクを抑えたいという判断から時間はかかりますが、1%ずつの調整を行うことにしました。

また、しきい値調整のオペレーション自体は軽く、監視も基本的にはアラートがならない限りはアクションしないという方針で取り組んだので、長い時間をかけることが出来たというのも正直なところです。

モニタリングには、CloudWatchを使ってモニタを作り、チューニングが与える変化をウォッチしていました。

下記の4項目を一つのモニタにまとめています。

  • ALBのReqCount(該当サービスのリクエスト数)
  • ECS CPUUtilization avg(該当ECSサービスのCPU使用率 平均)
  • ECS CPUUtilization max(該当ECSサービスのCPU使用率 最大)
  • ECS RunningTaskCount(該当ECSサービスで起動しているタスク数)

例えば、このモニタからこんなことが読み解けるような作りになっています。

  • ALBのReqCountが急激に上がった時に、ECS CPUUtilization maxがしきい値を超えてECS RunningTaskCountが3タスクスケールアウトしており、ECS CPUUtilization avgはしきい値前後で収束している
  • 夜間でALBのReqCountが落ちてくると、ECS CPUUtilization avgが収束し、RunningTaskCountが最小値の値までスケールインした

また、CloudWatchは水平/垂直の注釈を入れれるので設定変更のタイミングやしきい値に注釈を入れることでグラフを見ただけで読み取れる情報が格段に増えます。この機能は便利だなと思っています。

どんな結果になったか?

チューニングは、アラートが鳴るギリギリのところまでレイテンシが悪化する状況が見えてきたところで一旦インクリメントは終わりとして、しきい値から少しゆるめた落ち着いたところでFixさせました。

結果として目標においていた、以前の水準まで「タスクあたりのリクエスト数(1コンテナが捌くリクエスト数)」を上げることに成功しました。また、それに応じてSavingsPlansで購入する Compute Savings Plans を半分ほどに削減することが出来、システム運営費の削減に成功しました。

※この半年後のSavingsPlansの切れるタイミングで、測定したところ従来の半分くらいの購入で良いことが判明した

最後に宣伝です! コネヒトでは一緒に成長中のサービスを支えるために働く仲間を様々な職種で探しています。 少しでも興味もたれた方は、是非気軽にオンラインでカジュアルにお話出来るとうれしいです。

コネヒト株式会社

hrmos.co

コネヒトはPHPerKaigi 2022にゴールドスポンサーとして協賛します!

こんにちは!@otukutun です。今回は弊社が協賛し、弊社社員が登壇するイベントを紹介します。

PHPerKaigi 2022に協賛いたします

コネヒトではメインプロダクトである「ママリ」を始めとして開発のメイン言語としてPHPを活用しており、フレームワークとしてはCakePHPを採用しています(その他、技術スタックを知りたい場合はこちらをご覧ください)

その縁もあり、この度 PHPerKaigi 2022 にゴールドスポンサーとして協賛させていただくこととなりました!

イベント概要

公式サイトからの引用になりますが、PHPerKaigiは

PHPerKaigi(ペチパーカイギ)は、PHPer、つまり、現在PHPを使用している方、過去にPHPを使用していた方、これからPHPを使いたいと思っている方、そしてPHPが大好きな方たちが、技術的なノウハウとPHP愛を共有するためのイベントです。 今年はPHPerKaigi初のオフラインとオンラインのハイブリッド開催です。

となっており、いろんな方が楽しめる間口の広いPHPのイベントになっていそうです!今回ははじめてのハイブリッド開催なので、都合がつく方は両方で参加して楽しむこともできそうですね!

発表内容やタイムスケジュールを知りたい方はこちらをご覧ください!

タイムテーブル | PHPerKaigi 2022 #phperkaigi - fortee.jp

また、最新情報はTwitterで告知されるので PHPerKaigi 公式Twitterも要チェックです!

@phperkaigi

コネヒトからの登壇者の紹介

弊社からは@takoba_ が登壇します。

2022/04/11 12:10〜 Track B レギュラートーク(20分)
CakePHP Fixture Factories の登場によって変化する、PHPプロジェクトにおけるテストフィクスチャ管理の選択肢

fortee.jp

CakePHPでテストデータ動的な生成方法について説明がありそうなので個人的にもすごく興味あります。

チケット購入

こちらからチケット購入できますのでよければぜひ!

www.eventbrite.com

最後に

ここまで読んでくださった皆様、ありがとうございました。 そして、 PHPerチャレンジ中の皆様、お目当てのPHPerトークンはこちらです!

#AFTER_TECH_COMPANY

それでは!

コネヒトではPHPerを積極採用中です!

hrmos.co

【永久保存版!】プロジェクトリーダー必見!!チームふりかえりを最高に楽しいものにするたった一つの方法【リモートワーク対応】【2022最新版】

こんにちは ohayoukenchan です! 4月と言えば新生活。コネヒト株式会社も4月から、経営体制を一新し新たなスタートを切りました。 今期も心機一転して頑張っていきたいと思います。

この記事では先月末に開催した下期(6ヶ月)のチームふりかえりで行ってとても良かったなと思ったことについてお伝えできればと思います。

中長期(数ヶ月間隔)のふりかえり会の意義

スプリントでのふりかえりは、スプリントごとにレトロスペクティブの時間を設けています。 KPT法に似たような方法ですが、例えば下図のような感じでチームで起こったできごとに「ありがとう」や「happy-bad」と書かれた領域に付箋を貼って、特に関心の高いものに対して次のスプリントへのtryを決めていきます。

レトロスペクティブ
スプリントごとのふりかえり

また、弊社の別のチームでも、Win Sessionで元気に目標を達成するチームづくりの記事にあるように、チームが元気な状態で目標を達成できるように、来週も頑張るぞと思えるような取り組みを行っています。

では、中長期(数ヶ月間隔)のふりかえりはどのように行えばよいでしょう? 今回開催した下期ふりかえりも、チームが元気な状態で来期に向けて頑張る気持ちを醸成したいと思い企画しました。

ふりかえり会の内容

ふりかえり会を企画した時に注目したキーワードは「自己肯定感」です。
ふりかえりが終わった後に、自己肯定感が満ち溢れているメンバーの顔を想像しながらアジェンダをつくりました。

自己肯定感が高ければ、自分に自信を持つことができ、何事にも積極的に取り組んでいけると思いました。逆に自己肯定感が低いまま次のスタートを切ることになると自信が持てず、仕事を置きに行くことになりがちです。仕事を置きにいくようになってしまうと目標がゴールになり、それ以上の成果は望めません。

それでは、自己肯定感をあげるにはどうすれば良いでしょう? 一番確実でほとんどのメンバーが喜ぶのは「評価される = 褒められること」だと思います。

チームメンバーを一人残らず褒めちぎるにはどうすればよいか考えた方法がこちら。(下記画像)

メンバ一人ひとりにおてがみを書いてもらう

そうです。メンバーひとりずつ他のメンバーに対してお手紙を書いてもらうだけです。 半年間通して一番近くで仕事をしてきたメンバーからみた印象を書いてもらいます。それを当日読むだけです。

当人が努力したことでも、誰からも評価されなければ無価値、無意味などの消極的な思考が脳内を支配してしまいがちですが、チームのメンバーは見ていた、努力していたことを知ってるはずです。 AさんがBさんをねぎらうだけでBさんは救われた気持ちになって、BさんもまたCさんを救うのです。最高です。

これが私がチームメンバーからもらったお手紙です。

お手紙

私はこの半期、さまざまな駄作なアイデアや、良かったと思ってもらえるアイデアを出してきましたが、ここでアイデアマンという評価をもらえたのは嬉しかった。アイデアも不作が続くと自信がなくなってくるので「もっと出していこう!」「いいんだ。アイデア出して!と思える最高な💌でした。また、子育て中ということもあり、チームの行事に参加できなかったりしていた後ろめたい気持ちも完全に取り払ってくれました。ありがとう!

似たようなものに360°フィードバックがあります。360°フィードバックは評価者からみて被評価者の評価を決定するためのシステムとしては良いですが、どうしてもフィードバックする側は評価されるべき功績をフィードバックするので、今回の目的である自己肯定感をあげることには繋がりにくいかなと思います。

ふりかえり会で準備したこと

準備するのはお手紙にするテンプレートを一枚用意するだけです。

他の人からテンプレートを閲覧できないように注意しつつ、後は趣旨をDMなどで伝えます。

これをメンバー分繰り返して、書いてもらっていない人がいなければ完了です😊

あるといいもの

今回は、チーム個人の自己肯定感をあげることの他に「チームで頑張って良かった。また頑張ろう」という気持ちも醸成したかったのでチーム外からのフィードバックも集めることにしました。

  • メンターだった先輩からの💌
  • CSチームからの💌
  • CTOからのはげましの💌
  • 社を去ることになった前代表からの💌

基本的にはチーム外からみたチームの印象を書いてくださったのですが、今回の大きな気づきとして、他の人にお願いする方が自分が想定していたものより素晴らしいものが出来たということでした。

例えば、CSチームのメンバーとの取り組みからお手紙をもらおうと自分は青写真を描いていたのですが、CSチームからもらったお手紙には自分たちのチームがこれまで改善したことにたいする「ユーザーからのフィードバック」を添えてくれていました。このことは全く想定していなかったし、結果想定した以上にチーム内からの反響も大きかったです。

想定の範囲内という言葉がありますが、想定の範囲内で物事を動かしてはもったいないです。自分ですべてやるより周囲の人を巻き込んでいくと良いと思います。

定量は測れませんが、定性的には良い会だったであろうことを感じていただけると思います😋

最後にひとこと

自己肯定感をあげる方法は他にもあると思うので、必ずしもこの通りやる必要はないと思いますし、所属するチームの状況によって最適な手段を選んでいくのが大事かなと思います。 大事なのはチームメンバーを一人残さず褒め讃えて、自己肯定感が高い状態で次の期へ突入することです。 あと、内容は作り込み過ぎずブレスト段階でどんどん協力者に移譲していきましょう。

今回の記事はプロダクトゴールのふりかえりについてでした。弊社のプロダクトゴールの運用についてはこちらの記事が参考になりますので、是非のぞいてみてください

tech.connehito.com

4月から新しい期が始まり、ロケットスタートで階段を駆け上がっていくイメージの弊チームですが、まだまだやりたいことがたくさんあり、全然手が回っていません!

バックオフィス、UIデザイナー、エンジニア、PMMなど多業種でご応募お待ちしておりますので、 ohayoukenchanにDMでお声がけください。

下記募集一覧からご応募もできます。

hrmos.co

よいチームを作っていこうず!

コネヒトの文化が生み出すスキルアップを支える社内LTイベント

こんにちは。2017年11月にAndroidエンジニアとしてjoinした@katsutomuです。

前回のエントリーで、髪の毛のアップデート予定について触れましたが、重い腰を上げて予定を決めました。4/3を予定しています。

さて今回は、先日社内で実施したLTイベントの技術目標マルシェについて紹介します。

はじめに

まずは今回の社内イベントについて補足させてください。

シンプルにいうと、スキルアップ目標の工夫をシェアして、お互いに刺激を受けるイベントです。

マルシェ is 何?

社内で実施しているLTイベントのコネヒトマルシェのコンセプトでもあるみんなの「知りたい」「知ってる」をおすそ分け!をテーマに、技術目標でやったことをアウトプットできる場、そして、みんなが和気あいあいと交流出来る場を目指しています。

技術目標 is 何?

エンジニア組織に所属するメンバーが半期ごとに持つ、個人のスキル成長を促す技術的な目標です。直接会社に貢献するものでなくてもOK(全く関係ないものはNG)ですが、計画的にスキルを伸ばすことを念頭に置き、期初に成果指標を置いています。 今回のイベントは、技術目標に関連したアウトプットを行うことで、コミュニケーションが生まれて、仲間からのいい刺激を貰い、また自分も渡せる場として、期末に実施をしています。

イベントの内容

開発組織に所属しているほぼ全員(18名)がそれぞれ学んだことを発表しました。ランチタイムも含んで、合計5時間のイベントになりました。タイムテーブルは以下の通りです。

タイムテーブル

時間 内容
12:00 ~ 13:00 はじまりのお話 + LT × 4
13:00 ~ 14:00 ランチタイム
14:00 ~ 15:20 LT × 7 + 10分休憩
15:40 ~ 17:00 LT × 8 + 10分休憩
17:00前後 おわりのお話

一人あたり5~10分のLT枠を好きに使ってもらい、10分オーバーした場合は、インターセプトして終了する予定でしたが、進行していく中で、余白の時間が生まれてきたため、ゆっくりと進行することができました。発表一覧は以下の通りです。

発表一覧

タイトル キーワード
ポートフォリオを作ったぞい Vercel / React / Next.js / CSS Module / Every Layout
テストとかLTとか React / Jest / testing-library LT: FastAPI
家族ノートのフロントエンドを改善してるぞい React / Jest / CakePHP
Graph Embeddingを用いたタグのベクトル表現分析 python / データ分析 / node2vec
洋書で読んでまとめるぞい 洋書 / オブジェクト指向設計
AWSの認定資格 AWS / 認定資格 / クラウドプラクティショナー
技術目標で作ったサービス紹介 Next.js / TypeScript / Tailwind CSS / Jest / MSW(Mock Service Worker)
CakePHPerのためのLaravel教養講座 PHP / Laravel
Deno入門 Deno / Deno Deploy
問いかけのススメ マネジメント / コーチング
100年ぶりの Go Go / Android
くるるん検査器を作ったりくるるんを動かす iOS / ML
知ってるようで知らないサジェストの裏側の世界 Elasticsearch
FY21下期のアウトプット駆動で得た知見たちをおしゃべりする 書籍執筆 / Python
Goワカラナイ しくじり先生編 Go
フレームワークを写経した感想 PHP / フレームワーク
#phperkaigi2022のスライド作成RTA PHP / テストフィクスチャ
SwiftConcurrencyダイジェスト版 iOS / Swift Concurrency

それぞれがさまざまなジャンルを学んでいたため、バラエティに富んだ内容となりました。自分の業務領域を深めるメンバーもいれば、普段の領域から離れたことを学んでいるメンバーもいました。

工夫したこと

イベントを開催する上でに、主に3つの工夫を凝らしました。

  • 発表のハードルを下げる
  • 有志とイベントを作る
  • 次につながる仕掛けをする

発表のハードルをさげる

評価の場でないことを伝えたり、スライドに落とす以外のLT方法もウェルカムとしたり、進捗に不安がある場合に1on1の活用やもくもく会を活用することを事前にアナウンスしていました。

技術目標マルシェは「正式な評価の場」というわけではないのでどんな内容でも、それが原因で評価が下がることはありません。
あくまで前述した通り「アウトプットの場」、「相互コミュニケーションによる技術目標の推進」を目的としています!

- 発表フォーマットはなんでもOK!
    - スライドで発表、成果物のデモ、フリートーク、投稿したブログの紹介、工夫したことetc
- 毎月のテーマ & 振り返りを活用しましょう
    - まずは自分が納得できる状態を目指して欲しいです。
    - その上で誰かと壁打ちできると良いと思うので、気軽に相談していきましょう
- 技術目標もくもく会を活用しましょう
    - 下期も有志メンバーがもくもく会を実施してます。
    - 「技術目標をやる時間がありません」というお悩みもあると思うので、是非活用してみてください

和気あいあいとした雰囲気で刺激を与えあうためには、自分が納得することと、やりやすい方法で発表することを、大事にしてほしいと考えていました。結果的にはスライドを作るメンバーが多かったと思いますが、それぞれが工夫をこらしていたので、長時間のイベントでも集中力を切らさず、聞けた感覚がありました。

有志とイベントを作る

イベント当日を、よりよい時間にするためには、わたしだけのアイディアでは、不十分だと感じていました。開発組織のメンバーが揃うミーティングで、有志メンバーを募り、委員会を結成しました。

わたしが決めかねていると、色々なアイディアを出してくれたり、意見を伝えてくれたり、タスクを率先して拾ってくれたため、スムーズに進めることができました。

せっかくなので委員会のミーティングでの議事メモを公開しておきます。

- オンライン?オフライン?
    - コロナの見通しが立ってないので、オンライン。
- 技術目標マルシェはどういう位置付け?福利厚生というかみんなでワイワイ楽しむ時間と割り切っていいものなのか、いや20人をN時間拘束するから仕事としてちゃんとやってくれ、なのか
    - LT大会はビール飲みながら聞きたいですね〜
- 人数多くて、時間配分がむずい
    - 20人分だと長いし、時間をオーバーする人もいるかも
    - 直感的にはドラというか時間でちゃんと切るのが必要だと思う、iOSDCのLTみたいな感じ
        - まさにiOSDCをイメージしてた
    - 画面共有奪っちゃうとか(いらすとや表示するなど
    - お昼を挟むタイムラインにするとか?
        - 朝早く働いている人もいるので、時間ずらしちゃった方が良い?
    - ボックスMTGその日無くす(ずらす)とかもありかも
        - 話さなきゃいけないことはランチの前後に話してもらうとか
            - 組織編成の話とかが出てきた場合、心が休憩できるか?
- フィードバックはどう送ろうかな?
    - コール&レスポンスを含めると、Zoomのチャットで全部やった方が、盛り上がり感はあるかも
    - がやはZoom、フィードバックはシートだと移動が面倒。
        - まっさらなところに付箋を書くと心理的に書きづらい
    - 運営が後から、notionなどにフィードバック一覧を作るとか。
    - zoomのチャットだと流れるのが懸念だったが、後からまとめるのであれば良いかも
    - その方法でいくならば区切りをちゃんとしたほうがいいと思うので、運営からチャット欄に「◯◯さんLT開始、終了」みたいなのを書くとかかな
    - Slackでいいかも?
    - Ask the speaker的なのあるといいですけどね
        - 感覚的にはこの規模なら全員が全員の発表を聞いたほうがいいと思っているので、時間の制限を考えると、この日は聞くことに専念して、後日別の場(Web Talkなど)で話題に出すのはどうだろう?

次につながる仕掛けをする

多くのエンジニアにとってスキルをアップデートしていくことは、継続的に行うことが大事だと思います。今回のイベントの熱量を次に生かすために、定期的な社内サブイベントで参照しやすいように発表一覧にタグをつけてみました。

画像の一覧はWebエンジニアが、集まるイベント用のタグです。わたしはAndroidエンジニアなので、普段は参加していないですが、次回のイベントで改めて話題に出して、相互に感想を話すことを提案する予定です。

以上のように、3つの工夫を紹介しましたが、どの工夫にも根幹には、アウトプットできる場を用意し、みんなが和気あいあいと交流出来る状態を作り出すことを意識していました。

その後....

これはわたしが意図したことでは、全くないですが、発表したことを社内にシェアして、次のアクションにつなげているメンバーがいました。

おそらく、今回のイベントがなくても同じ行動をとってくれていたとは思いつつもイベントを実施したことで、新たな変化が生まれたようで、イベントを実施した甲斐があったと感じました。

感謝!

おわりに

さて、今回は、開発組織で実施したLTイベントについて紹介させていただきました。

イベント開催にあたり、できる限りの工夫は凝らしましたが、コネヒトに元から備わっている、アウトプットを真摯に受け止めたり、わきあいあいと技術を楽しむ文化がベースにあることで、想定していたよりも、いい時間を作れたと思っています。

内容について、カジュアル面談で補足できますので気軽にお声掛けください!

hrmos.co

SwiftUIでUIを宣言的にかけるようになりコードを書くのが楽しいぞい

こんにちは、ohayoukenchanです。

今回はSwiftUIについてです。 ママリではiOS13をサポートしているので、一部iOS13をサポートする内容が含まれます。

システムを長持ちさせる力

突然ですが、コネヒトのエンジニアリング組織はTech Visionというものを掲げており、概要としては「みんなでエンジニア組織強くしていこうず。」的なことが書いてあるんですが、そのなかの3つの技術力として「システムを長持ちさせる力」を重要な技術力として推進しています。

ママリiOSアプリでも最新技術の恩恵を受け続けられるよう日々コードのアップデートを行っております。

先日、弊社ではTech Vision推進の一環で、技術目標マルシェなるものが開催されました。詳細はこちらのポストをぜひ覗いてみてください!

tech.connehito.com

技術目標マルシェは社内イベントで、各自比較的自由に気になる技術を選んで発表するのですが、自分はCoreMLとVisionを使って画像分類したり、エッジ抽出した画像をSKTextureにしてSpriteKitで遊ぶという内容を発表しました。

f:id:ohayoukenchan:20220329100313p:plain

iOS版のママリも、直近まではStoryboardやUIViewを使った開発をしていました。

Storyboardを使った開発の場合、UIの基底となるStoryboardでは実装内容はわかりません。ここからUIKitを使って実装を付け加えていくのですがUIを組み立てるのに、UITableViewCellやUIViewを継承したファイルを増やしていくことになります。

なにが行われるかわからないstoryboardの例

f:id:ohayoukenchan:20220329100422p:plain

Storyboardを使った開発がレガシーとは言い切れませんが、昨今、reactを筆頭に、宣言的UIで書かれたコードの見通しの良さ、逆にUIKit(storyboard)でUIを組み立てていくコードの見通しの悪さを考えると、サポートバージョンを考慮しつつ、これから開発するシステムに関してはSwiftUIを使って開発していこうという結論になりました。

アーキテクチャについて

SwiftUIと相性の良いライブラリにTCA(The Composable Architecture)があります。状態の集中管理したり、scopeを使うことでwatchするstateの範囲を限定できることで、無駄に再描画が発生しなかったり、テストライブラリも用意されているので非常に魅力的でしたが、TCAの懸念としては、Viewも含めてTCAに強く依存してしまうので、TCAを使わなくなった場合に引きはがずのが大変そうであることが理由でTCAの採用は見送っています。

いままでのiOS開発のライブラリの流行り廃りを考慮すると他によいものがでてきて廃れる可能性もわりと高そうという議論もしました。

余談ですがFluxベースのライブラリの有名どころにreactのreduxがあると思うのですが、reactがhooksを導入したことでreduxなしで状態管理できるようなアプローチをとってきているので状態管理をどの場所で行っていくのか今後が気になっています。

https://github.com/pointfreeco/swift-composable-architecture

ママリiOS版のリアクティブプログラミング構成

ママリiOS版は、MVVMアーキテクチャで構成されており、APIやUIからのイベント送信などにRxSwiftやRxCocoaを使用しています。SwiftUIを導入するにあたり、RxSwiftを切り離し、代わりにCombineを導入することも検討しましたが、RxSwiftへの依存が強いことと、Rxコミュニティは活発でライブラリ更新も積極的に行われていることから、無理に引き離すような選択はしていません。

新規でUIを作成する場合、状況に応じてRxSwiftで流れてきた値をCombimeのPublisherにわたしたりしています。一つのファイルにCancellableDisposeBag両方書かなくてはいけないなど、コードの見通しが若干悪くなるのですが、これは移行期という捉え方が近いとおもっていて、継続的に運用を続けていくことを視野に考えると、その機能自体なくなるかもしれないし、該当機能に大幅なアップデートがかかるかもしれません。可能性を考慮するときりがないので今は移行期としてこのような仕組みになっています。

fileprivate let disposeBag = DisposeBag()

fileprivate var cancellables: [AnyCancellable] = []

Hosting Controllerの取り扱い

ママリでは既存のアーキテクチャとの兼ね合いもあり、画面遷移は UIViewControlerに任せることにしました。UIHostingControllerを継承したクラスの rootView に SwiftUIの View を渡すようにしています。 super.init(rootView:) するときにclass内のプロパティを初期化して渡してあげたいけどSuperクラスの初期化が終わってないのにサブクラスのプロパティにアクセスするなと怒られてしまいます。

コンパイルエラーの例

class DiagnosisInterestingTopicsViewController: UIHostingController<
    DiagnosisInterestingTopicSelectView
>
{

    private var cancellables: [AnyCancellable] = []

    var viewModel: DiagnosisInterestingTopicsViewModel()

    init(interstingTopics: InterestingTopicsResponse) {
        super
            .init(
                rootView: DiagnosisInterestingTopicSelectView(
                    viewModel: viewModel // 'self' used in property access 'viewModel' before 'super.init' call
                )
            )
    }

・・・

この場合、super.init(rootView:) の前にViewModelを作っておくとコンパイルエラーを回避することが出来ます。rootViewに指定したいViewの引数とClass内部で取り扱うviewModelを一致させるためにこうしてますが、見通しは悪いですね。

コンパイルが成功する例

class DiagnosisInterestingTopicsViewController: UIHostingController<
    DiagnosisInterestingTopicSelectView
>
{

    private var cancellables: [AnyCancellable] = []

    var viewModel: DiagnosisInterestingTopicsViewModel!

    init(interstingTopics: InterestingTopicsResponse) {
        let viewModel = DiagnosisInterestingTopicsViewModel(
            interstingTopics: interstingTopics
        )

        super
            .init(
                rootView: DiagnosisInterestingTopicSelectView(
                    viewModel: viewModel
                )
            )
        self.viewModel = viewModel
    }

・・・

HostingControllerで既存UIKitの画面を表示する

通信中画面はSVProgressHUDを使用しています。SwiftUIを使った画面でも既存のUIを使用したいので、SwiftUI側でSVProgressHUDを表示すると、SwiftUIの描画領域しかオーバーレイされず、NavigationBarなどがオーバーレイの上に表示されてしまいました。そのため、SVProgressHUDUIHostingController から呼ぶようにしました。

ママリiOS版はiOS13をサポートしているため、iOS13で検証したところ通信が発生してもオーバーレイが表示されず、検証したところviewDidLoadで処理してもviewModel.$progressState に値が流れず、viewDidAppearで呼ぶことで回避できました。原因は分かってないです。

class DiagnosisRegionSelectViewController: UIHostingController<DiagnosisRegionSelectContainerView>,
    DiagnosisPageable
{

    ... 初期化処理など省略

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // iOS13だとviewDidLoadにおくと呼ばれないのでviewDidAppearで処理
        bindUI()
    }

    func bindUI() {
        viewModel.$progressState
            .receive(on: DispatchQueue.main)
            .sink { state in
                self.showProgressView(state)
            }
            .store(in: &cancellables)
    }

    func showProgressView(_ state: ProgressState) {
        switch state {
        case .asleep:
            SVProgressHUD.dismiss()
        case .connecting:
            SVProgressHUD.show(withStatus: "通信中", maskType: .black)
        }
    }

SwiftUIで宣言的にかける良さ

SwiftUI導入のメリットである宣言的UIを実現させたいので、複雑なロジックは持たず、UIの組み立てに集中させています。こちらはSwiftUIで書いた機能ですが1画面を構成するのに50行くらいのSwiftUIファイルを書くだけだったので大変見通しもよく(storyboardもcellもいらないなんて!)

感動しました

f:id:ohayoukenchan:20220329104948p:plain

struct DiagnosisRegionSelectSearchView: View {

    @ObservedObject var viewModel: DiagnosisRegionSelectViewModel

    private let maxCharacterLength = 7

    var body: some View {
        VStack(spacing: 0) {
            SearchBarRepresentable(
                text: $viewModel.zipCode,
                maxCharacterLength: maxCharacterLength,
                placeholder: "郵便番号を入力する",
                keyboardType: .numberPad
            )
            .onReceive(
                viewModel.$zipCode.dropFirst(),
                perform: { zipCode in
                    if maxCharacterLength == zipCode.count {
                        viewModel.apply(
                            .onSearchZipCode(zipCode)
                        )
                        self.closeKeyboard()
                    } else {
                        // なにもしない
                    }
                }
            )

                        ... 一部省略

            if viewModel.cities.isEmpty {
                Text("入力した郵便番号は存在しませんでした。\n再度入力をお試しください")
                    .font(.system(size: 11))
                    .foregroundColor(Color("Error"))
                    .multilineTextAlignment(.center)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding(.top, 24)
            } else {
                VStack(alignment: .leading, spacing: 0) {
                    ForEach(Array(viewModel.cities.enumerated()), id: \.offset) { index, city in
                        Text("\(city.prefectureName) \(city.cityName1) \(city.cityName2)")
                            .font(.system(size: 12))
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .contentShape(RoundedRectangle(cornerRadius: 20))
                            .onTapGesture {
                                viewModel.apply(.onChangeViewStateTapped(.confirm(city: city)))
                            }
                        if index < viewModel.cities.count - 1 {
                            Divider()
                                .padding(.leading, 15)
                        } else {
                            Divider()
                        }
                    }
                }
            }
            Spacer()
        }
        .padding(.top, statusBarHeight())
    }
}

また、ViewModelとUIで単一方向のバインディングを実現したいので、ViewModelは外から入力値を受け取ることができるように以下のprotocolに準拠させておきます。

protocol UnidirectionalDataFlowType {
    associatedtype InputType

    func apply(_ input: InputType)
}
final class DiagnosisRegionSelectViewModel: UnidirectionalDataFlowType {

    typealias InputType = Input

    private var cancellables: [AnyCancellable] = []
    private let disposeBag = DisposeBag()

        // Combine
        private let onSearchZipCodeSubject = PassthroughSubject<String, Never>()

    // MARK: Input
    enum Input {
        case onSearchZipCode(String)[f:id:ohayoukenchan:20220329104948p:plain][f:id:ohayoukenchan:20220329104948p:plain]
                ... 略
    }

    func apply(_ input: Input) {
        switch input {
                case .onSearchZipCode(let zipCode): onSearchZipCodeSubject.send(zipCode)
                ... 略
        }
    }

                ...

こうすることでviewModelの外から viewModel.apply(.onSearchZipCode(zipCode))のようにviewModelへ値を流すことができます。swiftUIの .onTapGesture に複雑な処理を書かないことでコードの見通しが良くなっていると感じています。

最後に

iOS13だとGeometryReaderをうまく初期化できなかったり、.ignoresSafeArea(.keyboard, edges: .bottom) が非対応なのでkeyboardを開いたときに画面を押し上げる処理をわざわざ自前で用意しないといけなかったり NSTextAttachment の色が変わらないなど、毎施策必ずといっていいほどiOS13への対応を行っていました。追加でiOS13向けの対応をしなければいけないことを考えると、サポートバージョンがiOS14以降になってからSwiftUIを導入したほうが無難かなという印象です。

近いうちに弊社アプリもサポートバージョンの見直しを行い、iOS14以降でサポートされている StateObject や LazyVGrid なども使えるようになり、ますます開発が楽しくなってきそうです。今後も引き続きシステムを長持ちさせる力を養っていくぞい。

SageMakerとStep Functionsを用いた機械学習パイプラインで構築した検閲システム(後編)

皆さん,こんにちは!機械学習エンジニアの柏木(@asteriam)です.

今回は前回のエントリーに続いてその後編になります.

tech.connehito.com

はじめに

後編は前編でも紹介した通り以下の内容になります.

  • 後編:SageMakerのリソースを用いてモデルのデプロイ(サービングシステムの構築)をStep Functionsのフローに組み込んだ話
    • モデル学習後の一連の流れで,推論を行うためにモデルのデプロイやエンドポイントの作成をStep Functionsで実装した内容になります.

今回紹介するのは下図の青枠箇所の内容になります.

検閲システムのアーキテクチャー概略図


目次


Step Functionsを使ってサービングシステムを構築する方法

Step Functionsのグラフインスペクターに示された処理のうち赤枠部分が今回の処理になります.

No. ステップ名 SageMakerのアクション 処理内容
5 Model-Creating-Step CreateModel 推論コンテナの設定とモデルの作成
6 EndpointConfig-Step CreateEndpointConfig エンドポイントの設定
7 Endpoint-Creating-Step CreateEndpoint エンドポイントの作成とモデルのデプロイ

Step Functionsのグラフインスペクター

サービングシステムを構築するために,3つの処理をStep Functionsに組み込んでいます.

  1. モデルの作成と推論コンテナの設定
  2. エンドポイントの構成を設定
  3. エンドポイントの作成とモデルのデプロイ

また,サービングシステム・ML API・Clientの関係性を説明するために,システム全体から該当箇所を切り取った図を下に載せています.

サービングシステム

それぞれの役割を説明すると

  • Client⇄ML API
    • ClientはML APIに対して,推論を行うために必要なデータをPOSTする
    • ML APIは正常投稿 or 違反投稿どちらかを表すフラグ値(0 or 1)をClientに返却する
  • ML API⇄推論エンドポイント(サービングシステム)
    • ML APIは検閲する生のテキストを情報として詰め込んで推論エンドポイントをinvokeする
# ML APIの推論エンドポイントをinvokeする処理
 
import json
import boto3


# SageMakerクライアントを作成
client = boto3.client("sagemaker-runtime")

# 推論エンドポイントをinvoke
input_text = {"text": "推論対象のデータ"}
response = client.invoke_endpoint(
    EndpointName='エンドポイント名',
    Body=json.dumps(input_text),
    ContentType='application/json',
    Accept='application/json'
)

# 結果を受け取る
result_body = json.load(response['Body'])
# 違反確率
pred = float(result_body['predictions'])
# 結果の表示
print(pred)
  • サービングシステムはテキストの前処理を行った後に学習済みモデルによる推論を行い,違反確率をML APIに返却する
  • サービングシステムはS3に保存されているモデルアーティファクトをロードしてデータを待ち受けている

それでは,サービングシステムを構築する部分を紹介していきます.

学習済みモデルを含んだ推論コンテナの設定(モデルの作成)

この処理ステップでは,「モデルの作成」を行います.この処理を行う上で用意するコードは以下になります.

今回も公式のサンプルコードを参考にしたので,確認してみて下さい.
参考: amazon-sagemaker-examples/advanced_functionality/scikit_bring_your_own

用意するコード

  • Dockerfile.cpu(今回はgpu版のDockerfileも使用しているため.cpuを付けて区別しています)
    • 推論エンドポイントとしてデプロイするコンテナ
    • ファイル内でserve.pyの実行権限を与えておく必要があります
  • serve.py
    • NginxとGunicornを起動するPythonスクリプトで,コンテナ起動時に実行されるスクリプト
      • 実行されるコマンド: docker run <イメージ> serve
    • 公式のサンプルをそのまま流用
  • inference.py
    • Flaskアプリで,独自の処理を書くことができ,リクエストに応じて機械学習モデルの読み込みや推論処理を行う
      • 今回は生データを受け取り,シーケンスに変換し推論を行う
    • ヘルスチェック時にモデルのロードを行う
# inference.py
"""推論を行うflaskサーバー
    生のテキストデータを受け取り,モデルに入力できる形式に変換する
    BERTモデルに変換したデータを入力することで推論を行う
"""

import json
import os
import sys
import traceback
from typing import List, Tuple

import numpy as np
from flask import Flask, Response, jsonify, make_response, request

# Tensorflow
import tensorflow as tf

# Transformers - Hugging Face
from transformers import AutoTokenizer, TFBertModel

# モデルに使用するパラメータ
MAX_LENGTH = 512
MODEL_NAME = 'cl-tohoku/bert-base-japanese-whole-word-masking'
SAVED_MODEL_NAME = 'bert_model.h5'

# 後述のCreateModelのパラメータModelDataUrlに指定するS3に置かれたモデルファイルパスと同期している
prefix = "/opt/ml/"
model_path = os.path.join(prefix, "model")

tokenizer_bert = AutoTokenizer.from_pretrained(MODEL_NAME)


def text2features(texts: List[str], max_length: int) -> List[Tuple[np.ndarray, np.ndarray, np.ndarray]]:
    """テキストのリストをTransformers用の入力データに変換

    input_ids, attention_mask, token_type_idsの説明はglossaryに記載されている
    cf. https://huggingface.co/transformers/glossary.html

    Args:
        texts (List[str]): 分類対象のテキストデータが入ったリスト
        max_length (int): 入力として使用されるシーケンスの最大長

    Returns:
        List[Tuple[np.ndarray, np.ndarray, np.ndarray]]: input_ids, attention_mask, token_type_idsが入ったリスト
    """
    shape = (len(texts), max_length)
    input_ids = np.zeros(shape, dtype="int32")
    attention_mask = np.zeros(shape, dtype="int32")
    token_type_ids = np.zeros(shape, dtype="int32")

    for i, text in enumerate(texts):
        encoded_dict = tokenizer_bert.encode_plus(text, max_length=max_length, pad_to_max_length=True)
        input_ids[i] = encoded_dict["input_ids"]
        attention_mask[i] = encoded_dict["attention_mask"]
        token_type_ids[i] = encoded_dict["token_type_ids"]

    return [input_ids, attention_mask, token_type_ids]


class ScoringService(object):
    """モデルのロードと受け取ったデータから推論を行う
    """
    model = None

    @classmethod
    def get_model(cls):
        """事前にロードできていない場合はモデルをロードする
        """
        if cls.model is None:
            cls.model = tf.keras.models.load_model(os.path.join(model_path, SAVED_MODEL_NAME), compile=True)

        return cls.model

    @classmethod
    def predict(cls, input: List[Tuple[np.ndarray, np.ndarray, np.ndarray]]) -> float:
        """入力データに対して,推論を行う
        Args:
            input (List): 推論対象のデータで,リストの要素に対して推論を行う
        """
        loaded_model = cls.get_model()

        return loaded_model.predict(input)


# サービング予測用のflaskアプリ
app = Flask(__name__)


@app.route("/ping", methods=["GET"])
def ping():
    """コンテナの動作とヘルスチェックを行う,モデルのロードが成功すればヘルス判定される
    """
    health = ScoringService.get_model() is not None

    status = 200 if health else 404
    return Response(response="", status=status, mimetype="application/json")


@app.route("/invocations", methods=["POST"])
def inference():
    """
    毎分毎にデータが送られてきて,リアルタイムで推論を行う.
    テキストデータを受け取り,モデルが受け入れられる形式に変換を行い,予測確率(0.0~1.0)を返す.
    """
    # データを受け取って,モデルに入力できる形式に変換する
    data = request.get_data().decode("utf8")
    data = json.loads(data)
    text = text2features([data['text']], MAX_LENGTH)

    predictions = ScoringService.predict(text)
    return make_response(jsonify(predictions=str(predictions[0][0])), 200)
  • nginx.conf
    • Nginxの設定ファイル
    • 8080番ポートで /pingもしくは /invocationsにアクセスがあった場合に,Gunicornに転送する
    • 公式のサンプルをそのまま流用
  • wsgi.py
    • Gunicornの設定ファイル
    • 推論コード(inference.py)をimportする

用意するコードからわかるように,サービングシステムの実態はWeb ServerにNginx,Application ServerにGunicornを使いフレームワークとしてFlaskを利用しています.
これらのコードを用意したら,イメージをECRに登録し,Step Functionsの定義設定を行います.

CreateModelで主に設定する内容

  • モデルに名前を付ける
  • 推論コンテナの設定
    • 推論コード
    • サーブファイル
    • アーティファクト(=モデル)のパス設定
  • イメージ
"Model-Creating-Step": {
  "Type": "Task",
  "Resource": "arn:aws:states:::sagemaker:createModel",
  "Parameters": {
    "PrimaryContainer": {
      "ContainerHostname.$": "States.Format('{}-{}', 'prod-sample-con', $$.Execution.Name)",
      "Environment": {
        "PYTHON_ENV": "prod"
      },
      "Image": "<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/sample:latest-cpu",
      "Mode": "SingleModel",
      "ModelDataUrl.$": "$.ModelArtifacts.S3ModelArtifacts"
    },
    "ExecutionRoleArn": "arn:aws:iam::<アカウントID>:role/StepFunctions_SageMakerAPIExecutionRole",
    "ModelName.$": "States.Format('{}-{}', 'prod-sample-m', $$.Execution.Name)"
  },
  "Catch": [
    {
      "ErrorEquals": [
        "States.ALL"
      ],
      "Next": "NotifySlackFailure"
    }
  ],
  "ResultPath": null,
  "Next": "EndpointConfig-Step"
}
  • ModelDataUrl: TrainingJobの出力結果から参照しており,モデルが保存されているS3のパスを指定します.ここで指定したパスが’/opt/ml/model’に同期されるので,推論コードで呼び出してモデルをロードすることができます.
  • ExecutionRoleArn: ロールにアタッチするポリシーはSageMaker Rolesを参考にしてみて下さい.ここで嵌ってしまったのですが,Actionに"iam:PassRole"が必要になるので注意です.

エンドポイントの構成を設定

この処理ステップでは,モデルをデプロイするために使用する「エンドポイントの構成を作成」を行います.

CreateEndpointConfigで主に設定する内容

  • デプロイするモデルの指定(CreateModel時に付けたモデルの名称)
  • プロビジョニング用のリソース
  • エンドポイント構成の名前
"EndpointConfig-Step": {
  "Type": "Task",
  "Resource": "arn:aws:states:::sagemaker:createEndpointConfig",
  "Parameters": {
    "EndpointConfigName.$": "States.Format('{}-{}', 'prod-sample-ec', $$.Execution.Name)",
    "ProductionVariants": [
      {
        "InstanceType": "ml.t2.large",
        "InitialInstanceCount": 1,
        "ModelName.$": "States.Format('{}-{}', 'prod-sample-m', $$.Execution.Name)",
        "VariantName.$": "States.Format('{}-{}', 'prod-sample-v', $$.Execution.Name)"
      }
    ]
  },
  "Catch": [
    {
      "ErrorEquals": [
        "States.ALL"
      ],
      "Next": "NotifySlackFailure"
    }
  ],
  "ResultPath": null,
  "Next": "Endpoint-Creating-Step"
}
  • InstanceType: 推論サーバーのマシンスペック(インスタンスタイプ)をここで決めます.今回は最低スペックのml.t2.mediumだとメモリ不足になったので,メモリ8GBのマシンを選択しました.この辺りは常時稼働しているので費用面と相談しながらスペックを決める必要があると思います.

エンドポイントの作成とデプロイ

この処理ステップでは,エンドポイント設定を用いて「エンドポイントの作成」を行います.ここで最終的に設定されたリソースを起動し,モデルをその上にデプロイします.

CreateEndpointで主に設定する内容

  • デプロイするモデルの指定(CreateModel時に付けたモデルの名称)
  • 使用するエンドポイント構成の指定(CreateEndpointConfig時に付けたエンドポイント構成の名称)
  • エンドポイントの名前
"Endpoint-Creating-Step": {
  "Type": "Task",
  "Resource": "arn:aws:states:::sagemaker:createEndpoint",
  "Parameters": {
    "EndpointConfigName.$": "States.Format('{}-{}', 'prod-sample-ec', $$.Execution.Name)",
    "EndpointName.$": "States.Format('{}-{}', 'prod-sample-e', $$.Execution.Name)"
  },
  "Catch": [
    {
      "ErrorEquals": [
        "States.ALL"
      ],
      "Next": "NotifySlackFailure"
    }
  ],
  "End": true
}

処理が正常に完了するとSageMakerのコンソール上でエンドポイントを選択すると,指定したエンドポイント名のステータスが「InService」になっていることを確認できます.

SageMakerのコンソール画面 - エンドポイント

また,エンドポイントを誤って削除したり,想定とは違う状態だった場合にロールバックが必要になることがありますが,これはモデルとエンドポイント設定が残っていればいつでも復元可能です.エンドポイントの作成は手動でもできるのでSageMakerのコンソールから設定すると良いと思います.

機械学習システムを開発して

今回新しく検閲システムを開発し,その中でデータ抽出からモデルの学習,そしてモデルのデプロイまで一気通貫した機械学習パイプラインを構築しました.このプロジェクトでは,推論システムも構築する必要があったため,そもそもStep Functionsでモデルのデプロイまで持っていけるのかというところから技術検証したり,推論速度といった非機能要件なども検討して処理を考える必要があったりと難しい部分もありました.また,PoCは別のメンバーが担当していたこともあり,Jupyter Notebookからプロダクション用のシステムに合わせたコードを作り上げる部分や再現性を取る部分でも苦労がありました.

これらの苦労の甲斐あって?無事に本番稼働しているこのシステムの状況としては,コスト削減という部分で,当初の期待通りxx万円/月のカットに寄与できていたり,サービス品質向上という部分では,質問の回答率が上がるといった成果が出ています.

一方で,推論の精度面で多少の検知漏れがあったりと少し改善が要求されたりする可能性があり,この辺りは継続的に改善が必要で,まさにMLOpsだなと感じています.

また,この取り組みは全ての投稿をチェックすることから,より違反確率が高い投稿のみを重点的にチェックすることができるため,作業量が減り作業者の精神的負荷が減ったり,作業効率化も上がるといった作業者側のメリットだけでなく,モデルが違反確率が高いと返した投稿の中にも問題ない(正常)投稿も含まれているため,これらを人間が正しく判定し直すことで,今後のモデル改善時に使える有効なアノテーションデータとして蓄積することができるメリットもあります.これらの取り組みはまさに「Human-in-the-Loop」が上手く機能している状態ではないでしょうか.

おわりに

今回は前編・後編と2つの記事に分けてSageMakerとStep Functionsを用いた機械学習パイプラインにより構築した検閲システムの内容を紹介しました.特にStep FunctionsでのTrainingJobの活用例やモデルのデプロイ部分を組み込んだパイプラインに関する事例はあまり公開されていない内容かと思うので,是非参考にして頂ければと思います.

今回の取り組みはCSチームと連携して進めたことにより良い成果が出つつあると思うので,これからもサービスの品質向上やグロースに対して他チームと協力する中で機械学習を導入することでよりその価値を発揮していければと思います.

最後に,コネヒトではプロダクトを成長させたいMLエンジニアを募集しています!!(切実に募集しています!)
もっと話を聞いてみたい方や,少しでも興味を持たれた方は,ぜひ一度カジュアルにお話させてもらえると嬉しいです.(僕宛@asteriamにTwitterDM経由でご連絡いただいてもOKです!)

www.wantedly.com