TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

576

BASEの機械学習チームで論文読み会を実施してみました こんにちは。BASEのDataStrategy(DS)チームでエンジニアをしている竹内です。 DSチームではBASEにおける様々なデータ分析業務をはじめ、機械学習技術を利用した検索、推薦機能のサポート、商品のチェックや不正決済の防止などに取り組んでいます。 先日、チーム内で最新の機械学習技術についての知見を相互に深めるための試みとして、各々興味のある機械学習系の論文を持ち寄って紹介し合う、いわゆる論文読み会というものを実施してみました。 この記事では、その会で私が発表した内容の一部を紹介したいと思います。 ※ 中身は論文読み会用から本記事用に一部修正を加えています。 A ConvNet for the 2020s 紹介する論文について タイトル: A ConvNet for the 2020s 著者: Zhuang Liu, Hanzi Mao, Chao-Yuan Wu, Christoph Feichtenhofer, Trevor Darrell, Saining Xie Facebook AI Research (FAIR), UC Berkeley CVPR 2022 arXivリンク: https://arxiv.org/abs/2201.03545 公式実装: https://github.com/facebookresearch/ConvNeXt ※ 挿入している図(画像)と英文は特に言及がない限り本論文からの引用になります。 TL;DR 直近の画像処理NNのアーキテクチャにおいては、Transformerをベースにしたものがトップクラスの性能を発揮(Swin-T) TransformerのキーとなるモジュールはMulti-Head Self-Attention(MSA)だが、実際にはそれ以外にも従来のConvNetに取り入れられていない様々なテクニックが存在→真にConvNetを上回っているとは言えないのでは そこで従来のResNetに、Transformerに加えられているMSA以外のテクニックを可能な限り盛り込んだ(ConvNeXtと命名) ConvNeXtは従来のTransformerベースのモデルに対して、モデルサイズを抑えながら性能を上回ることができた We gradually “modernize” a standard ResNet toward the design of a vision Transformer, and discover several key components that contribute to the performance difference along the way. 画像分類タスクにおけるConvNeXtとViTの性能比較 画像系NNモデルアーキテクチャの流れ ConvNeXtに到達するまでのターニングポイント的なアーキテクチャをざっくりと振り返ってみる。(リンクはarXiv) AlexNet(2012) ConvNetの始祖的な存在 ImageNetコンテストで圧勝 →以降ConvNetの層を深くするのがトレンドに →層を深くすると以下の二つの問題が浮上 Back Propagation時の勾配消失・勾配爆発の問題 精度の飽和、学習時のエラーの上昇(Degradation)の問題 ResNet(2015) 勾配消失・勾配爆発問題はBatch Normalizationが有効 Degradationに対するアプローチとして、層間のショートカット接続の重要性に注目 ショートカットを含んだブロックを含むアーキテクチャを提唱 ResNetのショートカット構造(He, Kaiming, et al. "Deep residual learning for image recognition."より) ResNeXt(2016) ResNetのブロックを並列に並べて集計する仕組みを提唱 パラメータ数と性能のトレードオフを改善 並列に並べる数Cardinalityをハイパーパラメータとして導入 少ないパラメータ数、小さいモデルサイズ、シンプルな形でResNetの性能を上回る ResNeXtの仕組み(Xie, Saining, et al. "Aggregated residual transformations for deep neural networks."より) EfficientNet(2019) ネットワークの深さ、広さ、解像度(画像サイズ)の3つをパラメータとして最適化 扱いやすく画像系の機械学習コンペ等ではよく見るアーキテクチャの一つ VisionTransformer(ViT)(2020) 自然言語処理系NNにおいてはデファクトスタンダードとなったTransformerを画像処理に応用 画像を16x16のパッチに分け、それぞれのEmbeddingを単語に見立てる 画像分類において少ない計算コストでトップ性能を発揮 画像処理における最近の大きなブレークスルーの一つ ViTで用いられる画像のパッチ化(Dosovitskiy, Alexey, et al. "An image is worth 16x16 words: Transformers for image recognition at scale."より) Swin Transformer(Swin-T)(2021) ViTを物体検知やセマンティックセグメンテーションなど、ピクセル単位の解像度が要求される他のタスクでも効果を発揮できるように改良 パッチ化の処理を階層化+1マスずつズラす処理(Shifted Window)を導入することで画像サイズの2乗であったViTの計算量を線形まで落とした 画像処理系のトップカンファであるICCV'21のBest Paper ConvNeXtの論文では、Swin-Tで採用されているWindowをズラす処理がConvと類似しているため、重要な要素であると考えられることが言及されている For example, the “sliding window” strategy (e.g. attention within local windows) was reintroduced to Transformers, allowing them to behave more similarly to ConvNets. ... Swin Transformer’s success and rapid adoption also revealed one thing: the essence of convolution is not becoming irrelevant; rather, it remains much desired and has never faded. Swin-Tで用いられる階層的なパッチ化(Liu, Ze, et al. "Swin transformer: Hierarchical vision transformer using shifted windows."より) ConvNeXt(2022) 本記事で紹介している論文 ResNetがConvNeXtになるまでに加えられた改良 Chapter2以降ではベースとなるResNetに加えられた手法と、それによる精度の改善幅について順に説明されている。 2.1 学習手法 ネットワークのアーキテクチャを弄る前に学習手法をTransformerに倣って改善していく。 エポック数を90→300に AdamW optimizer(2019) L2正則化とWeight DecayがAdamでは同一視することができないことを示し、AdamのWeight Decayに修正を加えた データ拡張 Mixup(2018) 2種類のデータとラベルをベータ分布からランダム生成された を使って以下のように混ぜ合わせる データ: ラベル: Cutmix(2019) 画像の一部を切り取り、別のラベルの画像を挿入 RandAugment(2020) グリッドサーチで最適な拡張度合いを見つける RandomErasing(2017) 画像内にランダムな矩形を追加する Cutmix(Yun, Sangdoo, et al. "Cutmix: Regularization strategy to train strong classifiers with localizable features."より) Random Erasing(Zhong, Zhun, et al. "Random erasing data augmentation."より) 正則化 Stochastic Depth(2016) ランダムでResブロックをスキップのみにする(後ろの層になるほどその確率が高くなる) Label Smoothing(2016) 正解ラベルと不正解ラベルの値を1, 0ではなく0.9, 0.1などとする これらの追加により性能は76.1%→78.8%に改善 2.2 マクロデザイン ここからは、ResNetのマクロな構造をTransformerに近づけていく。 ステージごとの計算比率の変更 複数のResブロックからなる1かたまりはステージと名付けられており、ConvNeXtには合計で4つのステージが存在する。 それぞれのステージのブロック数はResNet-50では(3, 4, 6, 3)であったが、これをSwin-Tに合わせて(3, 3, 9, 3)に変更した。 78.8%→79.4%に改善 stemで画像をパッチ化するように変更 入力された画像に対して一番最初に処理を行う部分はstemと名付けられており、ViTなどでは画像のパッチ化を行う部分に相当する。 従来のResNetでは、まず入力の画像を適切な特徴量のサイズにするためにカーネルサイズ7×7ストライド2のConv+Max Poolingを使用することで4倍のダウンサンプルを行なっていた。 一方でViTでは画像を16×16のパッチにする処理を行なっているが、これはカーネルサイズ16×16で重複なし(ストライド16)のConvに相当する。 Swin-Tではより小さい4×4のパッチを作成しているため、これに倣ってstemでカーネルサイズ4×4でストライド4のConvを使用する。 79.4%→79.5%に改善 2.3 ResNeXt化 ResNeXtで取り入れられている、1つのConvを複数に分岐させてあとからまとめることでパラメタ数を削減する手法を適用した。 モデルのキャパシティの減少を抑えつつパラメタ数を効率的に削減できるため、モデルサイズを維持したまま性能を大幅に引き上げることができる。 今回はチャンネル数分の分岐を作成するDepthwise Convを使用する。これはTransformerにおけるAttention層のMulti-Head化に相当すると言及されている。 We note that depthwise convolution is similar to the weighted sum operation in self-attention 79.5%→80.5%に改善 2.4 Inverted Bottleneck Transformerでは入力の次元より隠れ層MLPの次元の方が4倍大きくなるInverted Bottleneckというデザインを採用している。 このアイデアはMobileNetV2(2018)ですでに利用されており、その後の改良型ConvNetでもしばしば用いられている。 下図の(a)がResNeXtの1ブロックで用いられる通常のBottleneckで、チャンネルサイズを384→96に落としてからDepthwise Convで96→96に畳み込み、最後にチャンネル数を384に戻している。 (b)がInverted Bottleneckで、チャンネル数を逆に96→384に増やしてから384→96に戻している。 これによってDepthwise Convの計算量は増えるものの、入力部分がダウンサンプルされていることによってResブロックのショートカットの1×1Convの計算量が減るため、全体の計算量は減ることになる。 (a)がResNeXt, (b)がInverted Bottleneck, (c)がDepthwise Convを移動させたもの 80.5%→80.6%に改善(ResNet-200では81.9%→82.6%に改善) 2.5 Large Kernel Sizes 従来のConvNetでは3×3など小さいカーネルサイズ使用するのが主流であったものの、Swin-TのWindowのサイズは小さくとも7×7である点を考慮すると、カーネルサイズは大きい方が有効だと思われる。 これを実現するために以下の二つの手順を踏んでいる。 Depthwise Convの移動 より大きなカーネルサイズを利用するために、Depthwise ConvをResブロックの最初にもってくる。(上の図の(b)→(c)に対応) これはTransformerのMulti-Head AttentionがMLPの前に配置されていることに対応する。 (一時的に)80.6%→79.9%に悪化 カーネルサイズの増加 Depthwise Convを移動させた後、そのカーネルサイズを3から5, 7, 9, 11と増やしていくと計算量は大体保たれたまま性能が改善され、7で大体性能が飽和する。 サイズの大きいResNet-200でも同じ7で飽和することが確認されている。 79.9%→80.6に改善 この時点でViTで採用されているデザインの大部分を実現できていることになる。 2.6ミクロデザイン 大枠のアーキテクチャは完成したため、ここからはレイヤーレベルで改善していく。 ReLUをGELUで置き換える 活性化関数として使用されているReLUをBERTやGPT-2、ViTでも使われている以下の GELU(2016) に置き換える。 GELUと他の活性化関数との比較(Hendrycks, Dan, and Kevin Gimpel. "Gaussian error linear units (gelus)."より) 80.6%→80.6%で性能据え置き 活性化関数を減らす Transformerの1ブロック(入力のKey/Query/ValueをEmbeddingしてMLPに入れる部分)には活性化関数が1回しか使用されていない一方で、ResNetは1ブロックにConv層の数だけ活性化関数が存在する。 これを1×1のConv2つの間のみに絞ることで活性化関数の数を合わせる。 80.6%→81.3%に改善 Normalization層を減らす これもTransformerに合わせて1×1conv層の前にのみBatch Normalization層を置く。 81.3%→81.4%に改善 BatchNormをLayerNormに変更 これもTransformerで使用されている手法ではあるが、単純なResNetのBNをLNに置き換えるだけでは性能が下がることが確認されている。ここまでの改造を全て加えた上でLNに置き換えると性能の改善が見られる。 81.4%→81.5%に改善 ダウンサンプル層を切り離す ResNetでは各ステージの最初のResブロックでストライド2のConvによってダウンサンプリングを行なっているが、Swin-Tではこのような処理は各ステージの間で行われている。 これに倣ってConvNeXtでもステージの間にダウンサンプル層とNorm層を追加することで学習を安定化させた。(Norm層なしだと学習が発散した。) 81.5%→82.0%に改善 ResNetに加えた全ての変更点とそれによる性能および計算量の改善 最終的なモデルのアーキテクチャ 性能 画像分類タスクにおける性能比較 ImageNetにおいてConvNeXtは同程度のモデルサイズのSwinTransformerを上回る性能を発揮している。 物体検出タスクにおける性能比較 COCOデータセットにおける物体検出においてもConvNeXtは同程度のモデルサイズのSwinTransformerを上回る性能を発揮している。 感想など ここ最近の画像処理系の流れを振り返るのにちょうど良い論文で、公式のpytorchによる実装と合わせて内容が非常にわかりやすく良い論文 強いて言えば既存の技術の応用という面が強いため、新しいアイデアや知見、理論的な深掘り(「なぜAttentionよりConvolutionの方が上手くいくのか」など))の面では若干物足りない気もする 昔読んだ深層強化学習系の Rainbow: Combining Improvements in Deep Reinforcement Learning という論文になんとなく立ち位置が似ているなと感じた ベースとなるDeep Q-Networkという手法に7つの改善手法を加えたときのパフォーマンスの改善について研究した論文 自分でベースモデルのアーキテクチャや学習手法に改善を加える際の流れとしても参考になる おわりに 今回のような論文読み会はDSチームとしては初の試みでしたが、新しい知見を取り入れ視野を広げる良い機会に感じたので1Qに1回ぐらいのペースで継続していけたらと思っています。 今後もDSチームでは新しい技術についても積極的に検討し、検証を重ねることで更なるプロダクトの改善、サービスの向上に取り組んでいきます。
アバター
初めまして。フロントエンドエンジニアの近藤 @kon_engineer と申します。 本記事では、2022年1月24日(月)にリリースされた、商品在庫絞り込み機能の振り返りと、サービス全体の状況を可視化できるNew Relicというプラットフォームを活用したAPIの観測について紹介します。 今回の事例では、New Relicで観測可能なAPIのレスポンス速度や各クエリパラメータのリクエスト状況などを分析して、効果測定や今後の施策に活かす取り組みを行いました。New Relicの詳しい説明や、BASEがNew Relicを導入した経緯は こちらの記事 をご参照頂けたらと思います。 商品在庫絞り込み機能とは 商品管理画面で商品の在庫数を指定して検索できる機能です。BASEでは商品管理画面の一覧から、在庫切れの商品や在庫が少なくなっている商品を簡単に見つけることができないという課題がありました。特に商品数が50を超える場合は、在庫状況をページングして確認する必要がありました。そこで、在庫数で商品を絞り込めるように改修して、商品管理の利便性の向上を図りました。 Twitter上の告知は以下です。 ╭━━━╮​  NEW✨ ╰━v━╯ BASE( ᐛ )⛺️ 🛒「商品管理」🛒 がアップデート! 💡在庫がない商品がパッと見つけられる💡 管理画面の「商品管理>絞り込み」から 🛒在庫なし 📝在庫が一定数以下 の商品の絞り込みが可能に◎ ぜひご活用ください! pic.twitter.com/7PtLZCf7GW — BASE(ベイス)🔎新機能登場! (@BASEec) 2022年1月25日 New Relicによる監視 New Relicによる監視はBASE全体として推進しているものの、本PJのメンバーは本格的な導入経験はありませんでした。今回はプロジェクトとしてトライしてみようということで、メンバーそれぞれが仕組みを学びながら運用していきました。 開発前 検索利用状況 商品在庫検索機能は、既存の商品検索APIにクエリパラメータを追加することで実装しました。実装前に既存の商品検索APIのレスポンス速度や、使用されている頻度を把握することで既存の問題や、追加で行った方が良い改善がないか、開発前に観測することにしました。 まずは検索API全体の呼び出された回数を測定して、次に各パラメータが指定された回数を測定することで、どのパラメータが使われて、どのパラメータが使われていないか分かるようにしました。 得られた結果として、全体の検索回数に対して、キーワードを指定して検索された回数が約87%あることが測定できたため、キーワードで主に検索されていることが分かりました。また、商品タイプ別の検索は、他の検索条件よりも極端に使用されていないことが呼びされている回数から分かったため、何らかの対応を検討する価値があると把握できました。 さらに、キーワード検索以外は、絞り込みボタンを押して検索モーダルを開かなければ検索できないUIのため、キーワード以外の各検索回数の結果から、検索モーダル経由で一定数の検索リクエストが呼ばれていることが分かりました。今回は検索モーダルに在庫数の検索機能を追加するため、そもそも検索モーダルがほとんど開かれていない、という状況ではないことを把握することは重要でした。 負荷状況 レスポンス速度に改善の余地はないか、また今回の改修によってレスポンスが悪くならないか把握するために、現状の応答速度を事前に把握しました。特に大きな問題は見つからなかったため、この速度を維持することを目標としました。 リリース前後 規模別 事前にショップを規模別に分類して、規模別でどのような効果があったのか観測できるように準備しました。今回の機能は、商品数が増えて在庫管理が大変な大規模ショップにより使ってもらいたいという思いがあったので、規模別に観測することで効果を細かく把握する狙いがありました。 リリース後の検索回数 また、在庫数を検索された回数がどれくらいなのか、リクエストURLを監視してリリース後すぐに分かるようにしました。機能が順調に使われていることが観測できて、PJメンバーで喜ぶ場面もありました。 どのような値で検索されているか把握する 当初は観測していなかったのですが、リリース後に在庫数0で検索されているケースがとても多いことが判明しました。在庫が少ない商品を検索するために使われることは予想していたのですが、それにしても0で検索されることが多い、という印象でした。そのため、どのような数値で検索されているのか、後からNew Relicのダッシュボードに追加して観測しました。結果として8割以上が在庫数0で検索されていたことが分かり、数字から事実として在庫切れ商品を把握したいニーズを把握することができました。これにより、在庫切れ商品を素早く通知するような、在庫切れにアプローチした施策が今後も有効だろうということが分かりました。 負荷状況 事前に十分に検証はしたものの、リリース後もレスポンスが遅くなっていないか、エラーコードが出ていないかなどの検証をしました。特にエラーも発生しておらず、レスポンスも悪化していないことを観測することができました。 終わりに PJが始まった時、今まで効果測定を適切に行えていないというチームメンバーの課題感がありました。そのため、ただリリースして終わるのではなく、機能を使ってもらっているのか、どのような使われ方をしているのかを把握して、反省と次の施策に繋げる取り組みを行いました。 今回の施策は、規模として大きな改修ではありませんでしたが、改修したAPIをNew Relicで観測することで、しっかりと機能が使われていることを把握できて、施策として一定の効果があったことが分かりました。また、当初チームが持っていた仮説として、ショップオーナーの方々は、在庫切れの商品を少ない時間で簡単に把握したい、というものがありました。仮説が間違っていなかったことが改修したAPIの利用状況からも分かり、今後も在庫切れの商品がすぐに分かるような施策が有効であると、自信を深めることができました。 何より、作ったものがしっかりと使われていることを観測できることで、チーム全体の士気も上がったと思います。今後も継続的にこのような取り組みを行うことで、施策の有効性や妥当性を意識して開発していきたいと考えています。
アバター
この記事は BASE Advent Calendar 2021 の25日目の記事です。 はじめに メリークリスマス!!! 執行役員 VP of Productの神宮司( @7jin16 )です。 2021年に取り組んだ顧客フィードバックを製品開発に活かすためにおこなったことを書きます。 なぜ始めたか これまでも顧客からのフィードバックを活かしていなかったわけではないのですが、より多く、より広くフィードバックを集める、全社で閲覧・利用可能な形で集積すること製品企画・開発の質が向上すると考えて取り組むことにしました。 なにをしたか "より多く、より広くフィードバックを集め、全社で閲覧・利用可能な形にする" を理想として掲げてそれを達成するための方法を考えました。 1. フィードバック量を増やすために 今までは製品内から直接フィードバックを送信する機能はなかったため、製品のほぼすべてのページのフッター付近にフィードバック送信フォームへのリンクを設置しました。リンクを設置したところリリースから日が浅い頃は1日数百件のフィードバックが送られてきました。 2. 製品開発に役立てるために フィードバックをただ集めるだけでは、製品開発に最大限役立てることはできません。おかげさまで「BASE」でネットショップを運営しているショップは160万ショップを超えていて売上規模や扱っている商材もショップによってさまざまです。同じ機能へのフィードバックでも注文数の多さや扱う商品数によってショップが抱える課題はまったく異なってくるため送信元ショップの注文数や商品数、業種などの情報は必須です。 フォームとしてはひとつですが送信元のショップと注文数、商品数、業種などのショップの情報をフィードバックと紐づけています。 フィードバックを送信すると社内管理画面に送られます。 3. 全社で閲覧・利用可能な形にする 以前から「BASE」にはお問合せや口頭で多くの顧客からフィードバックが寄せられていましたが、SlackやGoogleスプレッドシートに保存されていて集積場所が点在しているため一覧性や検索性が低く利用しやすい状態ではありませんでした。 利用しにくい状態だとフィードバックを集めても社内のメンバーも活用しにくいため集積場所を統一することから始めました。 顧客が「BASE」にフィードバックを伝える方法は カスタマーサポートのお問合せフォーム(Zendesk) 顧客と話している社内メンバーに口頭で伝える 製品に設置されているフォームから送信 の3パターンあり、どこから伝えられても社内管理画面に集積されていくようにしました。 SaaSを選べなかった理由 製品フィードバックを複数チャネルで集めて管理するSaaSは国内外問わず増えていてます。しかし、「BASE」では前述した以下の要件を落とすことができず短期的にはSaaSで達成することができないため自社開発をすることにしました。 同じ機能へのフィードバックでも注文数の多さや扱う商品数によってショップが抱える課題はまったく異なってくるため送信元ショップの注文数や商品数、業種などの情報は必須です。 実際にプロダクト改善に活かせたのか? 6月にフィードバックフォームをリリースしてから6,000件以上のフィードバックをいただいています。フィードバックをいただいたからといってそのまま機能として実装されることはなく、フィードバックを送信してくれた顧客が抱えている本当の課題を探り、課題のコアに辿り着くことが重要だと思っています。 フィードバックフォームをリリースしてから既に50件以上の機能改善につながっており、今後も発展させていきたいです。 オーナー様/お客様の声から生まれた改善レポートまとめ 「BASE」では一緒にプロダクトを改善していくメンバーを募集してます!幅広い職種で募集中ですので少しでもご興味がございましたら 採用ページ からカジュアル面談を申し込んでいただけると嬉しいです!
アバター
はじめに CTOの川口 ( id:dmnlk ) です。 これはBASE Advent Calendar25日目の記事です。僕は立候補してないのに勝手に日程が組み込まれてました。 BASE株式会社では積極的にエンジニア採用を行っております。 その中でよく質問を頂くのは「BASEに今から入社した場合にやることはあるのか?」というものです。 確かにサービスは成長し上場もしている企業で自分がやることはあるんだろうか、というのは僕も同様に疑問を持つだろうなとは思います。 ですので今回は話せる範囲ではありますが、今BASEで必要とされていることを書いていこうと思います。 フレームワーク移行 来年以降、BASEシステムとして取り組むものとして非常に大きいウェイトを占めるのは間違いなくこれになるでしょう。 現在BASEの大部分が利用している言語及びフレームワークはCakePHPですが2系を利用しておりEOLを迎えているだけでなく開発生産性などが数世代遅れているということを否定出来ません。(CakePHP2というフレームワーク自体が悪かったという話ではありません) これらを徐々に別のフレームワークや言語のシステムに変更していくというのが主なミッションです。 その中でモノリスからマイクロサービスという形にしていくという選択肢もあるでしょう。個人的にはRDBMSのトランザクションの恩恵を捨てる選択肢を取ることに恐怖はありますが。 BASEシステムのコード量は非常に多くデータのパターンも多岐に渡るため、コードの歴史の解読、調査、修正にテストなど技術理解だけでない幅広いスキルが必要になると思います。 使っていただいているユーザーさんの不便を起こさないよう、安全にシステムをマイグレーションしていくという仕事は胆力のいるミッションだとは思いますが我こそはという方は是非お待ちしています。 open.talentio.com セキュリティ BASEでは多くのユーザー様のデータをお預かりしており、社内やアプリケーションのセキュリティを強化していくことは重要です。 今まで専任のセキュリティエンジニアは採用しておらず、元々セキュリティに一定の造詣があるメンバーによって脆弱性診断の対応等を行っておりました。 しかしこれでは組織として非常に弱く、より踏み込んだセキュリティ対策を行えない状態ではありましたので今回募集をさせて頂いております。 特にアプリケーション側のセキュリティエンジニアに関しては、内部統制や各種認証取得などの整備などとは別のチームとして動いていただきます。 BASEで利用しているAWSのセキュリティ製品の適用によるセキュリティ向上、脆弱性診断の結果トリアージ、不正リクエストの検知対応など様々あります。 脆弱性診断の結果トリアージは、場合によっては修正からデプロイまで行っていただく予定でありPHPである必要はありませんがウェブアプリケーションの開発経験を要求しています。 大量のリクエストや多くの種類のユーザーさんが飛び交うアプリケーション上で起きうるたくさんのセキュリティリスクを低減し、安心して使っていただける環境を作るチームとして立ち上げていきたい方をお待ちしています。 open.talentio.com DX改善 Digital Transformationではなく、Developer Experienceです。 開発者体験が悪い状態での開発は進められますが、複利的に効いてきます。 それを改善するために、デプロイ速度の向上やローカル及び開発環境の整備、CI/CDパイプラインの効率化などやることは非常に多くあります。 先日のイベントで軽く話しましたがこのあたりは常時タスクがあるというわけではなく専属エンジニアが付けづらくCTO職などがやることが多くなってしまうのが現実だったりします。 開発者の現状に寄り添いながらよりよい開発者体験を作っていきたい方を募集しています。 EMの育成や新規メンバーのオンボーディング強化 組織が大きくなると必ず非常に重要になってくるのがマネージャーの存在です。 普段のタスク管理などはそこまで問題となりませんが、メンバーの成長を考えながらチームとしての成果を最大化を考える必要があります。 恥ずかしながらCTOの自分はそのあたりの知見が薄く得意でもないので、EMを育成という観点で何が有効になっていくかがあまりわかっていません、 大規模組織でのマネジメント経験や育成を行っていた方は是非BASEに来てその知見を活かして欲しいと思います。 合わせて、続々と入ってくる新メンバーに大規模になってきたBASEシステムのオンボーディング体験を向上させていくといったことも一緒に考えていただければと思います。 どれだけ優秀なエンジニアでも入社してからの成果を最大化してもらうためにオンボーディングは必須であり、課題は大きくなっているのでぜひとも取り組みたい箇所です。 まだ予定はありませんが新卒採用も見据えてやっていきたいですね。 パフォーマンス改善 多くのショップ様が使ってくれる現状においてパフォーマンス改善は日々のタスクとして取り組まないといけない課題です。 データ量が多くなったショップ様の管理画面パフォーマンス、ショップページのレンダリング高速化など改善すべきページはたくさんあります。 現代的なCDN技術などを利用し高速化を図っていきたいと思っています。 New Relicを積極的に活用しているので監視を超えて可観測性を高めてより高速なネットショップ作成サービスにしていきたいですので、是非パフォーマンスジャンキーの方を募集しています。 最近ではこのようなアウトプットもあります。 devblog.thebase.in おわりに これらの取り組みだけでなく、事業の課題についてのサービス開発、Pay ID開発、さらなるUXの向上、アクセシビリティ、データ基盤整備、機械学習による不正決済対策、レコメンド開発などなどたくさんあります。というかまだまだある!メチャクチャある!助けてください! 是非興味ある方は下記から応募して頂いたり、僕のTwitterにDM、Meetyなどどんな手段でもいいのでご連絡ください。 では、皆様良いお年を!!! open.talentio.com meety.net
アバター
この記事はBASE Advent Calendar 24日目の記事です。 BASEテックブログ編集長の松原( @simezi9 )です。 12月もいよいよ大詰め、クリスマス・イブということでそろそろ年内の仕事を納められた方もいるのではないでしょうか。 BASEのアドベントカレンダーも今年で 4回目 となりました。 今年も全部で32本(一日で複数記事の日も作ったため)の記事を12/1~12/25の期間で投稿してきました。 本記事ではブログ編集長としてアドベントカレンダー運営に際して行ったことと、その振り返りをしたいと思います。 これは来年またBASEのアドベントカレンダーが無事に開催されること、 あるいは世の中のだれかがアドベントカレンダーを運営する際になにか1つでも参考になることがあればと思って書き残す備忘録でもあります。 タイムライン 編集部としてのアドベントカレンダーの準備のための流れは以下のようになりました 日時 イベント 11/4 アドベントカレンダーのレギュレーションを社内に公開 & 執筆者の募集を開始 11/18 カレンダーの日程が埋まる 11/25 アドベントカレンダーの各記事に使うアイキャッチ画像のテンプレ用意完了 11/30 告知ページ 公開 12/1 記事公開開始 レギュレーションについて まず11月頭にアドベントカレンダーの趣旨と参加方法を書いたドキュメントを社内ナレッジベースにポストしました。 このタイミングで公開したのは主に 記事の内容と公開手段について 記事作成から実際の公開に至るまでのフロー 参加日を表明できるカレンダー の3つでした。それぞれを掘り下げてみます。 1. 記事の内容について 記事の内容はBASEや開発に関係があることであれば何でも可、としテックブログには技術記事のみ掲載可能とし、記事の内容次第では note や個人ブログを使っても大丈夫ということを明確にしました。 当たり前の内容に思えるかもしれませんが、これについては過去の経緯があります。 過去BASEのアドベントカレンダーではテーマをフリーにしたところ、真面目なものからゆるいものまで幅広いテーマの記事が集まったことがありました。 とてもにぎやかではあったのですが、それらの記事をすべてこのテックブログで公開したことでブログの記事の軸がぶれてしまう結果になってしまったことがあります。 これは既存のブログ読者の方にとってもあまり好ましくない状態であろうと考え、昨年はテックブログでの公開にふさわしい技術記事のみで構成したアドベントカレンダーとしました。 しかしながら、社内の雰囲気をお伝えするという意味で多様な記事がアドベントカレンダーに掲載されることは基本的に良いことであると私自身は考えていました。 過去に発生した問題は結局のところ、公開場所にふさわしくない記事をアドベントカレンダーだから掲載したということで起きているだけなので、 note や個人ブログでアドベントカレンダー公開も可能であるということを最初から明言すればよいと思いそれを盛り込むことで、技術記事でなくてもよいというのを今年改めて決定できた、という流れです。 (余談ですが BASEはnote株式会社と資本業務提携 を結んでいます。) 残念ながら今年はnoteでの記事公開はなかったというオチがついてしまったのですが、とりあえず公開する記事の方向整理ができたという点で来年以降に期待したいと思っています。 2. 記事作成から公開に至るまでのフロー アドベントカレンダー期間は通常の状況を遥かに超える記事のレビュー依頼が来るため兼務で行っているブログ編集部の負荷をへらす必要があります。 そこで記事の公開までに必要な以下の手順を公開しドキュメントとしました。 下書きを書く場所とレビュー依頼の出し方 レビューについては完了までの時間の目安も同時に書く アイキャッチ画像の作り方 これについてはデザイナーの @nomjic が誰でもコピーしてテキストを編集するだけでアイキャッチが作れるステキFigmaを用意してくれました テックブログに投稿する場合の手順 これによって一度アドベントカレンダーが始まってしまえば編集部員の負荷はほぼほぼ記事のレビューだけとなりました。 3. 参加表明のためのカレンダー ブログ編集部としてカレンダーページを公開するタイミングで恐れていたのは以下の2点でした 空白だらけのカレンダー TBDがズラッとならぶカレンダー これらを避けるために11月頭から積極的に動いていました。 まず空白だらけのカレンダーを避けるために執筆者の確保に走りました。 アドベントカレンダーは世にたくさんあり、テーマ次第では空席が増えてしまうこともありますが、企業のアドベントカレンダーでそのような状態で公開されることはあまり印象が良くないと考えていました。 過去数年の経験から記事が不足することはまずないと分かっていたものの、各マネージャー陣にお願いしてメンバーへの働きかけをしてみたりslackの#generalで宣伝をしてみたりと先手を打って動いていきました。 ここで考えておかないといけないことは日程かぶりの問題です。 アドベントカレンダーは一人1日1記事が文化となっていて、カレンダーが埋まったら2個目のカレンダーを作るというスタイルで運営されることが多いです。 実際にBASEでも2019などは1日2記事で2個のカレンダーを走りました。 このスタイルは運営が大変なところが多く、1個目のカレンダーは埋まっているけど2個目のカレンダーがスカスカということも起こりかねず負荷が高いです。 そう考えていくと、そもそもカレンダー個あたり一日1記事という縛りがおかしくて、 一日n記事でいいじゃん という単純なことを思いつきます。 ただ適当に好きな日で、とやると前半が空っぽになって後半に集中しすぎるという可能性もあったので、 「1日あたりまず2人までは先着順で参加表明でよく、全体的に埋まってきたら2人の制約を緩和する」 というスタイルにしました。 これである程度カレンダー全体に記事を散らすことを可能にしつつ、日程がかぶっても問題にならないスケーラビリティを確保しました。 また、参加表明をしてもらったタイミングで記事で扱う予定のトピック、テーマだけは必ず表明してもらい(変更可)、TBD/未定で埋まることを防ぎました。TBDがカレンダーに並ぶとどうしても行き当たりばったりな感じが出てしまいますし、どういう雰囲気のアドベントカレンダーが開催されるかが伝わらないため、興味を持ってもらうこともできないだろうと考えたためです。 振り返り ここまでは実際にアドベントカレンダーを運営する上で行ってきたことを書きました。 ここからはテックブログ編集長の思いを書いてみようと思います。 アドベントカレンダーの意義 技術広報に積極的な企業にとって自社のアドベントカレンダーを開催することはもはや当たり前のようになっています。 ただ忘れてはいけないことがあります。それは 「アドベントカレンダー期間は大量のテック記事が公開されるレッドオーシャンである」 ということです。 もちろん読者側のアンテナが高くなる面もあります。ただ、それを遥かに上回る量で記事の供給量が増える期間です。 読者が技術記事に割く時間の総量はそこまで増えたりしません。その時間を世の様々な記事と取り合うことになります。 純粋に自社の技術を世に広めて自社の魅力をアピールするのであれば、常日頃からの定期的なアウトプットをまず確立すべきで、アドベントカレンダーの激しい競争で燃え尽きてしまうのは勿体ないと思っています。 まずは自社から定期的に記事を出せるような運営が行えるようになってからアドベントカレンダーでお祭りに参加するという流れのほうがよいのではないかという気がしています。 ただ逆に、レッドオーシャンというのを利用して勢いで普段アウトプットを出すことに不安を感じるようなメンバーの背中を押して記事を書いてみてもらう絶好の機会であるとも言えるかもしれません。 対外的なアピールとしてアドベントカレンダーを使うのではなく、メンバーに記事を書く経験値を積む機会の価値を強調することで協力してもらう、という考えで運営するほうが得るものが多いこともあるのかもしれません。 記事のレビューについて 少しアドベントカレンダーの振り返りとは話がずれるのですが、記事のレビューについて考えてることを書きます。 記事のレビューは気を使う作業です。我々自身も別に出版業界で編集者をやっていたわけでもなく文章の素人です。 ただし、企業のテックブログを運営する中で無責任なことはできません。記事のレビューは必要です。そのなかでフィードバックを行うことは少なからず疲れる作業です。 ただ自分は基本的にあまり多くをFBしないようにしています。観点としては ポリコレや倫理に反する内容はないか 業務上公開できない内容はないか 記事として読みやすくなるためにできる構造的な工夫はないか です。最初の2つは言わずもがなで必ずチェックする部分ですしこれは比較的明瞭に判断できる部分です。 それに対して、最後の文章の工夫については感覚的な領域も多く非常に難しくなります。 自分は基本的に「文章の順番を入れ替える」「図を追加する」「得られた成果をわかりやすくする表現を追加する」の3つのFBをします。 いい文章とは引き算によって産まれるとよく言われますが、文章を削ぎ落として引き締めて素晴らしいものに仕上げるというのは文筆の素人には荷が重いことです。人の文章に対して、「何かを足したほうが良い」とは言いやすいですが「この文章はいらない」「この表現は正しくない」というようなことは言いづらいからです。 全然関係のない個人的な余談なのですが、この記事のレビューという行為をするときによく思い出すことがあります。 それはキリコというラッパーの「ありがとう。名無しの2チャンネラー諸君」という曲のことです。 この曲自体はキリコが当時の2ちゃんねるにあった自分のスレッドに対して「全レス」を返す形でラップするという怪作です。(しかも14分弱ある) かなりクセの強い曲で私自身は好きでも他人におすすめするタイプの曲ではないのですが、この曲の最終盤に登場する 自分の世界をいじれるのは自分だけであるべきだ。赤ペンで訂正されて良い気分がしないのはみな同じだろう という一節をよく覚えています。 つまり何が言いたいのかというと、ブログの記事というものも個人の内側から生まれた自己表現であり、それを最大限尊重することが素人編集としてもっとも重視するべきことなのではないか、ということです。 細かい表現への違和感の表明などはやろうと思えばきっといくらでもやれますが、それはおそらくテックブログに求められることでもなければ記事を書いてくれたメンバーの求めている部分でもないのではないか、そう考えて情報の構造に対してなにか加えられることはないか、それだけを考えてレビューをするようにしています。 最後に 最後は少しとりとめがなくなってしまいましたが、最後にこの記事を読んで来年のアドベントカレンダーに役立てていただける方が一人でも居れば幸いだと思っています。 明日の最終日は弊社が誇るCTOの @dmnlk が素晴らしい作品をドロップしてくれる予定です。乞うご期待ください 今年も執筆に協力していただいて素晴らしい記事を提供してくれた弊社の皆様にこの場を借りてお礼を言わせていただければと思います。(この記事で偉そうなことをいいながらレビュー依頼を見落としていたりと大変ご迷惑おかけしました)
アバター
BASE Advent Calendar 2021 はじめに コスト考慮型学習とは Cost-Sensitive Learningの手法 コスト行列 閾値の調整による誤分類コストの反映 実際のデータセットを用いた例 まとめ 参考文献 はじめに この記事はBASE Advent Calendar 23日目の記事です。 こんにちは、DataStrategyチームの竹内です。 BASEではより良いサービスを提供するために色々なところで機械学習モデルが活用されています。 BASEに限らず、インターネット上のあらゆるサービスに機械学習の技術が活用されるようになって久しい昨今ですが、こうした実際のサービスやビジネス領域に近いところで活用される機械学習モデルにおいては、計算コストやメンテナンスコスト、解釈性やバイアス、データセットシフトなど色々と考えなければいけない特有の要素が存在します。 今回はその中の1つとも言える誤分類コストの非対称性の問題について考え、それに対するアプローチとしてコスト考慮型学習(Cost-Sensitive Learning)について扱っていきたいと思います。 コスト考慮型学習とは コスト考慮型学習とは、データマイニングにおける誤分類時のコスト(誤分類コストに限らず、計算コストなどの他の要素を考慮する場合もあります。)を考慮した学習手法のことです。 機械学習などによる分類モデルを現実の問題に適用する場合、どのデータをどのクラスに誤分類してしまうかで生じるコストが異なる、いわゆる誤分類コストの非対称性の問題に直面することがあります。 1 よく挙げられる例で言えば、がんのような重大な病気を診断する場合、本当に罹患している人に対して健康であると誤診してしまった際の影響は極めて致命的になり得る一方で、健康な人に対して罹患していると誤診してしまった際は追加の検査費用分のコストで済むことになります。 医療診断における非対称な誤分類コスト 健康な人 罹患している人 陰性と診断 - 治療の遅れ、信頼の失墜(コスト大) 陽性と診断 追加の検査費用(コスト小) 早期治療 医療診断の場合ではどの患者についても概ね同様なコスト行列が適用できますが、中にはサンプルごとに誤分類コストが異なる場合もあります。 例えばカードローン審査のような例では、利用客によって申請金額が異なるため誤分類コストが変わってきます。 カードローン審査におけるサンプルごとに異なる誤分類コスト 返済できる人 返済できない人 審査を通す 金利や手数料分の利益 金額分の損失 審査を通さない 適格な申請数の減少 不適格な申請数の減少 このようなサンプルごとに誤分類コストが異なるタスクにおいてモデルの作成や改良を行う際、サンプル数ベースの正答率や再現率、AUCの改善が必ずしも金額ベースの改善につながらない可能性があることに注意する必要があります。 このように現実の問題を分類タスクとして捉え、予測モデルの作成や改良を行う場合、適切な誤分類コストに基づいた性能評価が求められる場合があります。 適切な誤分類コストの設定には機械学習や統計一般の知識だけではなく、その分野特有の知識、いわゆるドメイン知識を十分に活用することが求められます。 Cost-Sensitive Learningの手法 ここからは上の例のような非対称な誤分類コストに対する具体的なアプローチについて扱っていきます。 誤分類コストを考慮したモデルを構成する手法は大きく分けて3つ存在します。 通常の学習を行なったモデルの出力に対する検出閾値を、サンプルの誤分類コストに応じて変更する 学習データセットのクラス比率、あるいは重みを誤分類コストに応じて変更した上で通常の学習を行う 誤差関数などモデルの学習手法そのものに誤分類コストを組み込む 今回は3つアプローチの中から、1つ目の閾値を変更する手法について説明していきます。 コスト行列 手法の説明に入る前に、コスト行列について整理しておきます。 クラス数Mの多クラス分類において、モデルが と予測したデータの真のクラスが であった時の誤分類コストを と表すことにします。 例えば二値分類の場合、クラス1を陽性、クラス0を陰性とすると、 は「陽性と予測したが本当は陰性だった」ため偽陽性、逆に は「陰性と予測したが本当は陽性だった」ため偽陰性のコストを表していることになります。 この時、 を要素としてもつM×Mの行列をコスト行列と呼びます。 例えば二値分類の場合は以下のような2×2の行列となります。 真のクラスが0 真のクラスが1 予測したクラスが0 予測したクラスが1 や には正しく分類した時の利益(負の値)が入りますが、実際はこの部分を0として誤分類コストの方に機会損失として織り込むことができます。(後ほど説明します。) ものすごく大雑把な例ですが、先程のカードローン審査の例で申請者 の申請金額を 円とし、金利を1%とした上で、rejectした場合の申請数の減少による効果を1人あたり大雑把に20000円と見積もる(返済できる人の申請数が減る場合は損失とし、返済できない人の申請数が減る場合は利益とします。)と、以下のようなコスト行列を考えることができます。 返済できる人 返済できない人 審査を通す 審査を通さない 20000 -20000 閾値の調整による誤分類コストの反映 コスト行列を定義したところで、閾値を調整する手法の具体的な説明に入っていきます。 大まかには「間違えたらまずい(誤分類コストが相対的に大きい)」クラスについては、その予測確率がたとえ50%を下回っていたとしても、予測結果としてそのクラスを出力するという手法になります。 最適な検出閾値はコスト行列から具体的に以下のように計算することができます。 データ がクラス に属する確率が であった時、コストの期待値が小さい方に分類することを考えます。 式で表すと、 (クラス と予測した時のコストの期待値) (クラス と予測した時のコストの期待値 が成り立つ時、つまり が成り立つ時にクラス =陽性と判別すれば良いことになります。 陽性である事後確率 を とおくと、 から が得られます。 ここで「間違えて分類した時のコストは常に正しく分類できた時のコストよりも大きい」つまり すべての となる に対して、 が成り立つことを仮定します。 この時上の式から とすると、 が得られます。 つまり正しい事後確率 が得られた時、検出閾値を として がそれ以上であれば陽性、そうでなければ陰性とすることで誤分類コストの期待値を最小化することができます。 また の式からコスト行列を 真のクラスが0 真のクラスが1 予測したクラスが0 0 予測したクラスが1 0 とおいたものも同じ結果が得られることがわかります。 これは先程説明した通り、正しく分類できた時に得られる利益を誤分類した時の機会損失として扱うことに相当します。 また、真のクラスにかかわらず同じ予測に対して常に発生する同じ大きさのコストは相殺することがわかります。 実際のデータセットを用いた例 実際のデータセットで閾値の調整によるコストの変化をみてみたいと思います。 今回はUCIのMushroom Classificationのデータセットを検証用に使わせていただくことにします。 https://www.kaggle.com/uciml/mushroom-classification このデータセットは、様々なキノコの傘の形や色などの特徴量と共に、そのキノコが食用なのかそうでないかのラベルが与えられたデータセットとなります。 今回はこれを用いて、キノコのいくつかの特徴量からそれが食用なのかそうでないかを予測する単純な二値分類のタスクを考えます。 分類器としては単純な決定木を使うことにします。 食用でないキノコのクラスを1、食用であるキノコのクラスを0とすると、サンプルサイズは以下のようになります。 クラス0: 4208 クラス1: 3916 必要なライブラリのimportや前処理など from sklearn.preprocessing import LabelEncoder from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn import metrics import numpy as np import pandas as pd import seaborn as sns from matplotlib import pyplot as plt def plot_confusion_matrix (y_test, y_pred, y_prob, cost_matrix): cm = metrics.confusion_matrix(y_test, y_pred) tn, fp, fn, tp = cm.flatten() accuracy = (tp + tn) / (tn + fp + fn + tp) precision = tp / (tp + fp) recall = tp / (tp + fn) f_score = 2 * recall * precision/(recall + precision) fpr, tpr, thresholds = metrics.roc_curve(y_test, y_prob) roc_auc = metrics.auc(fpr, tpr) total_cost = np.sum(cost_matrix * cm.T) print (f "accuracy: {accuracy*100:.4f}%" ) print (f "precision: {precision*100:.4f}%" ) print (f "recall: {recall*100:.4f}%" ) print (f "f-score: {f_score:.4f}" ) print (f "AUC: {roc_auc:.4f}" ) print (f "total cost: {total_cost}" ) df_cm = pd.DataFrame(cm.T, range ( 2 ), range ( 2 )) sns.set(font_scale= 1.4 ) sns.heatmap(df_cm, annot= True ,annot_kws={ "size" : 16 }, fmt= "" ) plt.xlabel( "true label" ) plt.ylabel( "prediction" ) plt.show() df = pd.read_csv( "mushrooms.csv" ) le = LabelEncoder() for k, v in df.dtypes.items(): df[k] = le.fit_transform(df[k]) # 全特徴量を使用すると完璧に分類できてしまうぐらいタスクが簡単なので、今回は実験用に使用する特徴量を制限します df = df[df.columns[: 4 ]] df 混同行列と各種指標 今回の場合、食用でないキノコを誤って食用だと判別して食べてしまった時の被害と、食用のキノコを誤って食用でないと判別して食べ損なってしまった時の被害では前者の方がより重大であると考え、以下のようなコスト行列を設定することにします。 真のクラスが0 真のクラスが1 予測したクラスが0 0 10 予測したクラスが1 1 0 このコスト行列のもとで決定木による分類を行い、まずは閾値を0.5に設定して混同行列やrecision、recallなどの各種指標とともにコストの総計を計算してみます。 cost_matrix = np.array([ 0 , 10 , 1 , 0 ]).reshape(( 2 , 2 )) x_train, x_test, y_train, y_test = train_test_split(df.drop(columns=[ "class" ]), df[ "class" ], test_size= 0.2 , random_state= 0 ) dtc = DecisionTreeClassifier(max_depth= 5 , random_state= 0 ) model = dtc.fit(x_train, y_train) y_prob = model.predict_proba(x_test)[:, 1 ] y_pred = (y_prob >= 0.5 ).astype( int ) plot_confusion_matrix(y_test, y_pred, y_prob, cost_matrix) 混同行列と各種指標 コスト行列と混同行列の要素積を取ることで得られたコストの総計(total cost)は2201となりました。 次に閾値を先程の に変えて同じように分類を行ってみます。学習する過程ではコスト行列は使用しないため、再学習せずに同じモデルを使用することができます。 threshold = (cost_matrix[ 1 , 0 ] - cost_matrix[ 0 , 0 ]) / (cost_matrix[ 1 , 0 ] - cost_matrix[ 0 , 0 ] + cost_matrix[ 0 , 1 ] - cost_matrix[ 1 , 1 ]) y_pred = (y_prob >= threshold).astype( int ) plot_confusion_matrix(y_test, y_pred, y_prob, cost_matrix) 混同行列と各種指標 結果としてprecisionが下がる代わりにrecallが上がることでtotal costを2201から690まで下げることができました。 一応閾値に対するtotal costをplotしてみると、 がtotal costを最小化する閾値であることが確認できます。 x = np.linspace( 0 , 0.5 , 5000 ) y = [] for i in x: y_pred = (y_prob >= i).astype( int ) cm = metrics.confusion_matrix(y_test, y_pred) total_cost = np.sum(cost_matrix * cm.T) y.append(total_cost) fig, ax= plt.subplots( 1 , 1 , figsize=( 10 , 8 )) ax.plot(x, y) ax.vlines(threshold, ymin= 0 , ymax= max (y), color= "orange" ) ax.legend([ "total cost" , "p*" ], loc= 'upper center' ) ax.set_xlabel( "threshold" ) ax.set_ylabel( "total cost" ) plt.show() 検出閾値と誤分類コスト まとめ 誤分類コストの非対称性の問題と、それに対するアプローチの1つであるコスト考慮型学習(Cost-Sensitive Learning)について紹介させていただきました。 BASEで活用されている機械学習モデルの一部にも、こうしたコスト考慮型学習のアプローチが用いられています。 実際には正確なコスト行列を設定することが難しい場合もありますが、それでも誤分類コストの非対称性については常に意識する必要があります。 例えばローン審査において機械学習モデルを人手によるチェックのための一時フィルター的な役割で使用する場合には、あらかじめ許容できる件数ベースの偽陽性率の上限を決めた上で、金額ベースの再現率が最も高くなるような閾値を設定する、といった方法が有効かもしれません。 その場合はモデルを開発するエンジニアだけではなく、二次チェックを行うオペレーターともうまくコミュニケーションを取りながら達成すべき目標を明確にしていくステップが必要不可欠となります。 また、実際に運用していく上では誤分類時のコストだけではなく、冒頭で触れた通り計算コストやメンテナンスコスト、解釈性の問題など色々な要素が影響してきます。それらを踏まえた上でドメイン知識を活用し、短期的な利益だけではなく、長期的な利益を見据えて最適なモデルを選択しチューニングしていくことが、ビジネスでの機械学習モデルの活用を求められるデータサイエンティストの役割であると考えます。 参考文献 Elkan, Charles. (2001). The Foundations of Cost-Sensitive Learning. Proceedings of the Seventeenth International Conference on Artificial Intelligence: 4-10 August 2001; Seattle. 1. Ling, Charles & Sheng, Victor. (2010). Cost-Sensitive Learning and the Class Imbalance Problem. Encyclopedia of Machine Learning. 特に現実の応用例を考える際、誤分類コストの非対称性の問題は、必ずと言っていいほど不均衡データの問題と一緒になって現れますが、個人的にはこの二つの問題は分けて考える方が良いかと思っています。というのも不均衡データには不均衡データ特有の、分類器の識別境界にかかるバイアスなど( https://scikit-learn.org/stable/auto_examples/svm/plot_separating_hyperplane_unbalanced.html や https://ieeexplore.ieee.org/document/6137280 などで説明されている)タスクのドメイン(医療診断で使うのか、ローン審査で使うのかなど)とは独立した問題が存在し、誤分類コストが対称であっても生じる可能性がある一方で、誤分類コストの非対称性の問題はタスクのドメインに依存した問題であり、不均衡データでなくても起こり得るからです。ただし、不均衡データの問題を解決するために非対称な誤分類コストを設定したり、誤分類コストの非対称性を考慮するためにunder-samplingやover-samplingなどによって敢えて不均衡な事前分布を設定する(この記事では詳しく紹介できませんでしたが Cost-Sensitive Learningの手法 の2つ目に相当します。)アプローチが取られることはあります。今回の記事ではトピックを絞るため、あまりデータの不均衡性の問題は取り上げていません。(今後機会があれば、2つ目以降の手法と共に記事にしようかと思います。) ↩
アバター
この記事は BASE Advent Calendar 2021 23日目の記事です。 こんにちは。 UXライターの藤井です。 ふだんは、BASEプロダクト全般におけるテキストの品質を向上させる、UXライティングを担当しています。「テキストコミュニケーションをデザインする」をキーワードに、テキスト版デザインシステムとして「運用ガイドライン」「用語リスト」を作成、タッチポイントごとに担当を分け、最終レビュー担当者をそれぞれアサインし、運用しています。あらゆるタッチポイントにおいて、日々テキストコミュニケーションの最適化を図っています。 トンマナ≠UXライティング この取り組みのなかで見えてきたのが、こんな2つの課題でした。 トンマナ(トーン&マナー)をひたすら磨く作業が、現状のテキストデザインの大半を占めていること 運用ガイドラインがあっても、品質の担保は、最終的にはどうしても属人的にならざるを得ないこと つまり、たくさんのショップオーナーの皆様が利用するプラットフォームにプロダクトが育っていく課程において、表面的にはトンマナが統一されていることで、一定のテキスト品質は担保されている一方で、UXライティングの本来の役割である「テキストを設計することによって、プロダクトとユーザーのコミュニケーションをデザインすること」、つまり「体験をデザインすること」という本質に、あまり時間を割けていないのでは、ということに気づいたのです。 品質の担保を前提に、本質的な「体験のデザイン」へ この次なるテーマと向き合うにあたり、必要になると考えたのが、「テキスト入力支援ツール」の導入でした。いくら洗練され磨き上げられた「運用ガイドライン」や「用語リスト」があっても、それはあくまで必要なときに紐解く「逆引き辞典」のようなもの。漢字の閉じ開きや記号、正式名称のBASEルールは、そもそも暗記するような類いのものではありません(さすがに何年もトンマナを整えていると、ほとんど身体に染み付いてはいますが)。あらかじめ定義されたルールに則していないものを指摘し、自動的に修正が加えられたら、あらゆるチームから「プロダクトに反映させたい」と提案されるテキストの品質は、すくなくとも「トンマナ」という文脈においては、その時点で担保されます(もちろん、完璧にではないとは思いますが)。これにより、「体験をデザインすること」ーーテキストのターゲット/目的/ゴールを定義し、情報の構成を設計し、マイクロコピーをライティングする、というプロセスに、さらにていねいに取り組むことができるのではないか、と。 入力支援ツールの導入検討、ガイドラインのアップデート そのために取るべきアクションは、2つあると考えています。 テキスト入力支援ツールの選定と導入 正誤の参照先となる「運用ガイドライン」「用語リスト」のDB化 まずは、現状のプロダクトやレビューフローとマッチするツールのメリット/デメリットの調査と検討。また、導入にあたって、エンジニアチームにどのくらいの稼働が発生するのか。 「textlint」や「文賢」をはじめとして、すでにけっこうな数のサービスがリリースされていて、インターフェースやできることがそれなりに違うので、導入に向けてあれこれ調査しつつ、現在先行して作業を進めているのが、もう1つのアクション「運用ガイドライン」「用語リスト」のDB化です。 BASEがUXライティングというアプローチを取りはじめてからおよそ3年、昨年もご紹介しましたが、ユーザー体験全体を通して語られる、ブランドの理念を反映した言葉遣い(ボイス)、ユーザー体験の各部分における言葉遣いの変化(トーン)のたたき台も形作られ、それなりに育ってきてはいるのですが、「運用ガイドライン」は目的や対象範囲などがまだきちんと定義され、明文化されていませんでした。また、「用語リスト」もレビュワー目線で構成されており、お世辞にも「誰が見てもすぐに活用できる」ところまでは整えられていない状態でした。 テキストライティングの方針を定める そこで、まずはテキストガイドラインにおける「テキストライティングの方針」、目的を定めました。 ショップオーナーの皆様だけではなく、これからショップを開設しようと検討している方、BASEのプラットフォームを利用したショップでお買い物される購入者、購入を検討している方をふくめ、BASEのタッチポイントに触れる可能性のあるすべての方を対象に、伴走者としてどんなテキストコミュニケーションであるべきなのか、という観点・視点がベースとなっています。 テキストライティング方針の対象範囲の明文化 また、現段階における「対象とする範囲」「対象としない範囲」も定めています。 2021年の段階において、プロダクトおよびプロダクトに付随するDBC(管理画面のお知らせカード)、メール、自社メディア(BASE活用方法などの記載された記事)などを対象範囲とし、メディアの属性によって受け取り手がキャッチできる情報や、印象に差分が発生しやすいSNS、広告などは、対象からあえて外しています。あくまでも、ユーザーの行動を促す短い文言「マイクロコピー」および「マイクロコピー」を支える屋台骨としての最低限のテキスト、にフォーカスしています。 正誤が検知できる、DB化されたガイドライン そして、来たるべきDB化を念頭に、項目自体をあらためて精査、正誤表にすることにより、正以外を検知できるような作りにしました。 テキストガイドラインとして「漢字・ひらがな」「カタカナ」「記号」「数字・単位・日付」、また用語集として「サービス正式名称」「BASE固有の用語」「用語」、マイクロコピーは用途別に「ボタン・リンク」「エラー・バリデーション」「フラッシュメッセージ・トーストメッセージ」「警告メッセージ」「ダイアログ」と、使いやすく分類し、検索性を高めました。 定形メッセージのテンプレ化による、メッセージ・デリバリーの高速化 さらに、よくある定形のメッセージに関しては、使い勝手を最大化するため、テンプレートも用意しました。 ウェビナー告知やApp新機能紹介など、ある程度フォーマット化できるものをテンプレート化することで、毎回イチからテキストを起こさなくてもよいように、新しい情報をターゲットに最速でアナウンスできるようになりました。 これから そんなわけで、昨年同様、すべては現在における仮説でしかないことを前提に、「運用ガイドライン」「用語リスト」のアップデートとさらなる活用策を模索してきた2021年。すべては、BASEというプラットフォームを通して、ショップオーナー様の皆様やお客様の叶えたい願いを実現するために。2022年のBASEにも、どうぞご期待ください。
アバター
この記事は BASE Advent Calendar 2021 の22日目の記事です。 こんにちは!BASEでエンジニアをやっている大津( @cocoeyes02 )です。今回はiikanji-conference-toudanチームの取り組みについてご紹介します! iikanji-conference-toudanチームとは? iikanji-conference-toudanチームとは、BASEから技術イベント・カンファレンスへ登壇する人たちを応援するチームです。登壇を有志による属人的なものにするのではなく、文化として当たり前になっている状態を目指して活動しております。 もともと登壇自体は前から推奨されていましたが、「BASE全体で外部への技術的なブランディングをしっかりやってきたい」という話があり、 iikanji-conference-toudanチームが生まれました! ちなみにiikanji-conference-toudanチーム以外でも、 ブログ や 自社イベント 、 Youtube など精力的に活動していますのでそちらもどうぞ! これまでどんなことしてきたの? チームで応援できそうなことを調査 応援と一口に行っても、様々な応援の形があるかと思います。そこで登壇する人を増やすために我々に何ができるのかをブレストし、まとめました。 ブレストの内容をグルーピングしたり抽象化した様子 特に、登壇のモチベーションのところを深掘りしたいという話になり、実際に登壇に対してどう思っているのかアンケートを取りました。 すると圧倒的に「登壇するネタがない」と思っている人が多かったため、「ブログや社内ドキュメントの内容をベースに登壇を進めたら、登壇してくれるのではないか?」という仮説を立て、登壇を勧めました。 アンケート結果の一部 実際この方法で何人か登壇した人を見たので、登壇を勧めるときの1つの方法として確立しつつあります。 一例として、 @tawamura さんの ElasticsearchとKibela APIを使ってSlackでのCSお問い合わせ対応業務を改善した話 がその1つになります。よければブログや登壇動画も是非ご覧ください! ちなみに、このPHPカンファレンス2021ではBASEから計5人が登壇しました!その時のイベントレポートは以下の記事になります。こちらも是非ご覧ください! devblog.thebase.in 登壇前のトークへのフィードバック より良い登壇ができるように、登壇前にzoom等でトークを聞いてもっと良くなる点などがあればフィードバックします。 例えばトークを聞きながら、以下のようなことについてフィードバックしています。 スライド全体が頭に入ってきやすい構成になっているか スライド等の資料で分かりにくいところは無いか 図や動画を使った方が良いところは無いか 時間オーバーしているのであれば、どこを削るべきか 喋りのスピードが早くて聞いている側が負担にならないか など 技術イベント・カンファレンスでの登壇は、大抵スライド等の資料を使いながらトークする形になります。なので、スライド等の資料のフィードバックが中心になります。 技術イベント・カンファレンスのスポンサー業務 技術イベント・カンファレンスでスポンサーとして名乗り出るのにはさまざまな理由がありますが、登壇している人のモチベーションアップやトークを聞いてもらうきっかけとしてスポンサー業務もやっています! また、カンファレンスによってはスポンサーセッション枠が設けられているスポンサープランもありますので、そちらも積極的に狙っていきます。 例えばPHPカンファレンス2021では、 @tanden さんを中心として「 BASE のお問い合わせ対応の裏側!」という座談会形式のスポンサーツアーの企画・開催をしました。その時の様子は以下の記事に書いてあります! devblog.thebase.in アーカイブ動画をみんなで見る会を開催 近年の技術イベント・カンファレンスでは、各トークのアーカイブ動画がアップロードされていることも珍しくありません。 登壇だけでなく、技術イベントの参加自体もっとわいわいしていきたい。そんな思いから各トークのアーカイブ動画をみんなで見る会の開催もしました! PHPカンファレンス2021のアーカイブ動画を見る会をSlackで募った様子 これからはどんなことをしていきたい? 色んな分野技術イベント・カンファレンスでの登壇を応援していきたい 現在登壇人数で言うと、PHPやGoのカンファレンスでの登壇が多い状況です。ですが、技術イベント・カンファレンスはPHPやGo以外にもたくさんありますので、色んな分野での登壇を応援していきたいと思っています! どの分野の技術イベント・カンファレンスでも日々登壇するチャンスを狙える体制づくりとして、例えば現状技術イベント用のカレンダーを用意して、色々な技術イベント・カンファレンスの日程を追えるようなものを作成しています! 登壇者だけでなく、技術イベントの参加自体ももっとわいわいして行きたい 登壇する人だけにフォーカスするのではなく、技術イベント自体もわいわいして登壇している人のモチベーションアップ&登壇へのハードルを下げるのも狙っていきたいと思います! 技術イベント・カンファレンスを楽しむための施策として、例えば定期的に技術イベントのアーカイブ動画を見る会を開催したりし始めています! 最後に もともとBASEでは、会社全体でOSS(コミュニティ)を応援する文化があり、この記事を書いた直近ではThe PHP Foundationに寄付を行いました。 devblog.thebase.in 我々のチームでは、技術イベント・カンファレンスに登壇して盛り上げていくという形で、OSS(コミュニティ)に貢献できればと思っております。引き続き、色々なアプローチで技術イベント・カンファレンスでの登壇を応援していきます! OSS(コミュニティ)に貢献したい!盛り上げていきたい!と思う方は、ぜひBASEへどうぞ! open.talentio.com 明日は @FUJIIMichiro さんの「UXライティング関連」と、 @shotakeuchi さんの「分類コストを考慮した機械学習モデルの考え方」です!お楽しみに!
アバター
この記事は BASE Advent Calendar 2021 の22日目の記事です。 はじめに はじめまして、Owners Success Frontend Shop Frontチームの坂口です。 普段はフロントエンドエンジニアとしてVue.jsを使った開発をメインに行なっているのですが、チームでプロジェクトマネージャーやデザイナーが手軽に動作を確認できるレビュー環境がほしいという話があり、AWS App RunnerとGitHub Actionsを連携して構築をしたのでその話をしたいと思います。 レビュー環境とは レビュー環境というのはGitHubのプルリクエストやブランチごとに動作確認ができる環境で以下のような動きをするものを今回は構築することにしました。 プルリクエストが作成されるとレビュー環境立ち上げ (作成) プルリクエストに紐付いたブランチが更新されるとレビュー環境更新 (更新) プルリクエストがclose、またはマージされるとレビュー環境削除 (削除) イメージとしてはHerokuの Review Apps やNetlifyの Deploy Previews などに近いかもしれません。 使用技術 AWS App RunnerとGitHub Actionsを利用して以下のような構成にしました。 AWS App Runner AWS App Runnerは今年5月に発表されたコンテナベースのフルマネージドサービスで、インフラの知識があまりなくても簡単にアプリケーションをデプロイすることができます。 https://aws.amazon.com/jp/apprunner/ 今回構築するにあたってAWS LambdaやAWS ECSなど選択肢はありましたが、簡単にデプロイできるということで採用しました。 準備 事前にGitHub Actionsで利用するAWSのIAMユーザーを作成して、リポジトリのsecretsにアクセスキーとシークレットアクセスキーを用意しておきます。 必要な権限は こちら の公式記事で書いてあるものになります。 ワークフロー内容 ワークフローは先ほど上げた作成、更新、削除に加えて、一時停止、復帰を追加した以下5パターンを用意しました。 プルリクエストにラベルがつくとレビュー環境立ち上げ (作成) ラベルが付いたプルリクエストへ新たにpushされると更新 (更新) プルリクエストがclose、またはマージされるとレビュー環境削除 (削除) 毎日21時になると稼働中のレビュー環境を一時停止(一時停止) プルリクエストの /resume とコメントすると復帰(復帰) プルリクエストにラベルがつくとレビュー環境立ち上げ name : review-app-deploy on : pull_request : types : - labeled jobs : deploy-review-app : name : Deploy Review App runs-on : ubuntu-latest environment : development if : ${{ github.event.label.name == 'ラベル名' }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Login to Amazon ECR id : login-ecr uses : aws-actions/amazon-ecr-login@v1 - name : Build, tag, and push image to Amazon ECR id : build-image env : ECR_REGISTRY : ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_NAME=`echo review-app_$IMAGE_TAG | cut -c 1-40` docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=service::$SERVICE_NAME" echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - name : Deploy to App Runner id : deploy-service uses : awslabs/amazon-app-runner-deploy@main with : service : ${{ steps.build-image.outputs.service }} image : ${{ steps.build-image.outputs.image }} access-role-arn : ${{ secrets.APPRUNNER_ROLE_ARN }} runtime : NODEJS_16 region : ${{ secrets.AWS_REGION }} cpu : 1 memory : 2 port : 80 wait-for-service-stability : false - name : Comment Review App URL run : | SERVICE_URL=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='${{ steps.build-image.outputs.service }}']" | jq -r '.[]["ServiceUrl"]' ` gh pr comment $PR_NUMBER --body "Review App URL: https://$SERVICE_URL" 大まかな流れは以下になります。 Dockerイメージビルド Amazon ECRへDockerイメージプッシュ AWS App Runnerのサービス作成 今回、プルリクエストのブランチ名をDockerイメージのタグとAWS App Runnerのサービス名に利用しているのですが、AWS App Runnerのサービス名の 制限 に合わせて文字列を置換しています。 またあとから識別しやすいようにサービス名に review-app_ プレフィックスを付与しています。 IMAGE_TAG = ` echo $BRANCH_NAME | sed -e " s/[^a-zA-Z0-9_-]/-/g " ` SERVICE_NAME = ` echo review-app_ $IMAGE_TAG | cut -c 1-40` サービス作成では、AWS公式GitHub Actionsである aws-actions/configure-aws-credentials を利用していて、CPU、メモリは最低限にしています。 wait-for-service-stability を true にするとサービス作成完了まで待ってくれるのですが、完了まで5分ほどかかってしまいGitHub Actionsの枠を使い切ってしまうことを懸念して false にしています。 さらにAWS App RunnerのURLをプルリクエストのコメントに通知するようにしています。(※サービス作成完了後ではないので、利用可能までに5分程度待機が必要です。) SERVICE_URL = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' ${ { steps.build-image.outputs.service } }'] " | jq -r ' .[]["ServiceUrl"] ' ` gh pr comment $PR_NUMBER --body " Review App URL: https:// $SERVICE_URL " 以下のように特定のラベル(今回は deploy review app )を付与するとワークフローが走り、サービスURLをコメントしてくれるようになります。 ラベルが付いたプルリクエストへ新たにpushされると更新 name : review-app-update on : pull_request : types : - synchronize jobs : update-review-app : name : Update Review App runs-on : ubuntu-latest environment : development if : ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名' ) }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Check Service Exist id : check-service-exist run : | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_NAME=`echo "review-app_$IMAGE_TAG" | cut -c 1-40` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='RUNNING']" | jq -r '.[]["ServiceArn"]' ` echo "::set-output name=image-tag::$IMAGE_TAG" echo "::set-output name=service-arn::$SERVICE_ARN" - name : Login to Amazon ECR id : login-ecr if : ${{ steps.check-service-exist.outputs.service-arn }} uses : aws-actions/amazon-ecr-login@v1 - name : Build, tag, and push image to Amazon ECR id : build-image if : ${{ steps.check-service-exist.outputs.service-arn }} env : ECR_REGISTRY : ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} . docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} - name : Update App Runner Service id : update-service if : ${{ steps.check-service-exist.outputs.service-arn }} run : | aws apprunner start-deployment --service-arn ${{ steps.check-service-exist.outputs.service-arn }} 大まかな流れは作成とほぼ同じになりますが、注意点として aws-actions/configure-aws-credentials 内部でサービスが存在する場合は更新コマンドを実行しているのですが、更新コマンドはサービスの設定を更新してくれるのみでソースを更新してはくれないため、そのまま利用せず、 start-deployment を実行する必要があります。 aws apprunner start-deployment --service-arn ${ { steps.check-service-exist.outputs.service-arn } } また細かいですが、 jq では -r オプションをつけて結果の文字列からダブルクウォートを削除して扱いやすくしています。 SERVICE_ARN = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' $SERVICE_NAME '&&Status=='RUNNING'] " | jq -r ' .[]["ServiceArn"] ' ` 毎日21時になると稼働中のレビュー環境を一時停止 name : review-apps-pause on : schedule : - cron : '0 12 * * MON-FRI' jobs : pause-review-app : name : Pause Review App runs-on : ubuntu-latest environment : development steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Pause App Runner Services id : pause-services run : | SERVICE_ARN_LIST=`aws apprunner list-services --query "ServiceSummaryList[?Status=='RUNNING']" | jq -r '.[] | select(.ServiceName | test("^review-app_")) | .ServiceArn' ` if [ -n "$SERVICE_ARN_LIST" ] ; then echo $SERVICE_ARN_LIST | while read SERVICE_ARN ; do aws apprunner pause-service --service-arn $SERVICE_ARN done fi AWS App Runnerはサービスが起動している間課金されてしまうため、深夜の誰も利用しない時間では一時停止するようにしています。タイムゾーンはUTCなので注意してください。 on: schedule: - cron: ' 0 12 * * MON-FRI ' 一時停止するサービスはAWS CLIの --query オプションで稼働中のものを抽出した後、jqでサービス名が review-app_ プレフィックスのものを絞り込んでいます。 SERVICE_ARN_LIST = `aws apprunner list-services --query " ServiceSummaryList[?Status=='RUNNING'] " | jq -r ' .[] | select(.ServiceName | test("^review-app_")) | .ServiceArn ' ` プルリクエストに /resume とコメントすると復帰 name : review-app-resume on : issue_comment : types : [ created, edited ] jobs : resume-review-app : name : Resume Review App runs-on : ubuntu-latest environment : development if : ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/resume' ) }} env : GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Check Service Exist id : check-service-exist run : | BRANCH_NAME=`gh pr view ${{ github.event.issue.number }} --json headRefName --jq '.headRefName' ` SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='PAUSED']" | jq -r '.[]["ServiceArn"]' ` echo "::set-output name=service-arn::$SERVICE_ARN" - name : Resume App Runner Service id : resume-service if : ${{ steps.check-service-exist.outputs.service-arn }} run : | resume-service --service-arn ${{ steps.check-service-exist.outputs.service-arn }} issue_comment をトリガーにしており、作成や更新のように github.head_ref でブランチ名が取得できないため、コメントされたプルリクエストからブランチを特定するための処理を挟んでいます。 BRANCH_NAME = `gh pr view ${ { github.event.issue.number } } --json headRefName --jq ' .headRefName ' ` 以下のようにコメントすることでワークフローが走り、一時停止中のAWS App Runnerサービスが復帰してくれます。 プルリクエストがclose、またはマージされるとレビュー環境削除 name : review-app-delete on : pull_request : types : - closed jobs : delete-review-app : name : Delete Review App runs-on : ubuntu-latest environment : development if : ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名' ) }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Delete App Runner Service id : delete-service run : | SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME']" | jq -r '.[]["ServiceArn"]' ` if [ -n "$SERVICE_ARN" ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo "::set-output name=tag::$SERVICE_NAME" fi - name : Delete ECR Image id : delete-ecr-image if : success() env : ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | TAG=`echo "$BRANCH_NAME" | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` aws ecr batch-delete-image --repository-name $ECR_REPOSITORY --image-ids imageTag=$TAG 削除したいサービスの存在確認をして、サービスとDockerイメージを削除します。 SERVICE_NAME = ` echo " review-app_ $BRANCH_NAME " | cut -c 1-40 | sed -e " s/[^a-zA-Z0-9_-]/-/g " ` SERVICE_ARN = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' $SERVICE_NAME '] " | jq -r ' .[]["ServiceArn"] ' ` if [ -n " $SERVICE_ARN " ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo " ::set-output name=tag:: $SERVICE_NAME " fi おわりに 今回は公式でGitHub Actionsが用意されているのもあって比較的簡単に連携・レビュー環境の構築ができました。まだサービス作成完了時の通知など改善の余地はたくさんありますが、手軽に構築ができるので、ぜひ一度検討してみてはいかがでしょうか。
アバター
この記事は BASEアドベントカレンダー 21日目の記事です。 まえがき BASE BANK株式会社でエンジニア兼Engineering Program Managerをやっている 松雪( @applepine1125 ) と 永野( @glassmonekey ) です。 BASE BANKでは組織の拡大に伴って表出した課題を解決するために、プロダクトのデリバリー、クオリティに責任を持つEngineering Program Manager(以下EPM)という役割を導入しています。 今回はまだ馴染みのないであろうEPMについてと導入の背景、具体的な働きざまについてご紹介します。 Engineering Program Manager(EPM) とはなにか そもそもEPMという役割自体が日本で馴染みのないものだと思います。 Apple や Amazon 、 Meta(旧FaceBook) 社などで正式なポジションとして存在しており、ざっくり各社のjob descriptionから要素を抽出してみると、主に以 下のような役割を担っていることがわかります。 開発のリードや開発プロセスの改善 設定したリリーススケジュールへの責任とプロダクトのクオリティの担保 プロジェクト、プロダクトの技術的な阻害要因の排除 ステークホルダーと適切なコミュニケーションを取り、自身での意思決定や意思決定のエスカレーションの実施 Product Manager(以下PdM)や外部のステークホルダーと開発チームの間に入り、適切に開発のサイクルを回していくために必要な役割です。 組織によってはこれらの役割をProject Manager(以下PjM)、Engineering Manager(以下EM)、Tech Lead(以下TL)といった役割が担っていたり、もしくは名も無き仕事として誰かが対応しているのではないでしょうか。 PjM、TL、EMとEPMの違い PjM、TL、EMといった、一見EPMと近い役割がすでに存在する中でEPMという役割が新たに生まれたのはなぜでしょうか、PjM、TL、EMとEPMとでは何が違うのでしょうか。 それぞれ重なる部分もありますが、役割としては微妙に異なります。それぞれ見比べてみましょう。 PjMとEPM PjMはPJの完遂に責任を持つ役割です。 PJの要件定義や進捗管理、メンバー集めや、場合によっては予算を獲得してくるなど、PJ完遂のために様々な業務を行います。 EPMとの一番の違いは、技術的なバックグラウンドがなくてもPjMの役割を担える点です。 PjM自身が技術的なバックグラウンドを持っていたり、メンバー内にそういった判断材料を揃えることができるエンジニアがいると開発に関する意思決定がしやすく比較的円滑にPJが進行します。 しかしそういったコミュニケーションや意思決定ができないとPjMはただエンジニアが提示する開発スケジュールを飲み込むしか無く、根本原因にテコ入れできずにスケジュール遅延が発生するいわゆるデスマーチへと突入していってしまいます。 こうならないようにPjMの意思決定を補佐し、協力してプロダクトのリリースを行うEPMのような存在が重要になってきます。 組織によってはPjMを開発面から補佐する役割はTLが担うというケースも多いかもしれません。 TLとEPM 組織によってTLに期待する役割は様々だと思いますが、どの組織でも割と共通しているのはコード品質や設計、アーキテクトの面でチームや組織に影響を及ぼす存在であることかと思います。 EPMでもそういった側面を求められる場面はありますが、そこを主戦場とするメンバーがすでにいるのであればそのメンバーに役割を委譲しつつ、連携しながらプロダクトのデリバリーへの責務を全うするのがEPMの大きな特徴です。 EMとEPM EMは組織によってTL以上に多様な役割を持っています。 参考に、広木氏が提唱する マネジメントの4象限と強め/弱めのEMの定義 と照らし合わせてみると、 主にPeople, Team Managementを行う弱めのEMとEPMは補完関係になるのではないでしょうか。 採用や評価、日々の1on1なども含め、仕事を通した各メンバーの成長やチーム作りと向きあうEMと、メンバーそれぞれのスキルを最大限に生かしてプロダクトのデリバリーとクオリティを担保するEPMとでは、その役割の中で向き合う要素は似ているものの最終的な責任が異なります。 この両方を一人で担うのはスイッチングコストがとても高く難しい仕事となってしまいます。 下図のように、強めのEMがいれば弱めのEM、EPM含め様々なマネージャ業を一手に引き受けてもらうことはできますが、そういったスーパーマンはそうそういないため、弱めのEMとEPMそれぞれで責務を分割してフォーカスするのは有効な打ち手なのではないでしょうか。 なぜEPMが必要なのか ではそのようなEPMはどのようなケースで必要になるのでしょうか? 一つ考えられるケースとしては「目的別組織」のマネジメントの場合ではないでしょうか。 目的別組織とEPM 組織形態としてよく比較されるのが"機能別組織(職能別組織)"と"目的別組織(職能横断型組織)"です。 機能別組織の場合、PJ毎に各チームからメンバーが集まり、PJが終わると解散、各々別のPJへ・・・というのがよく見る流れではないでしょうか。 こういった組織運営を行っている場合、わざわざEPMなどを配置せずとも、PjMやEM、TLがEPMの役割も担うことでとりあえず目の前のPJを完遂させる事はできると思います。 しかし目的別組織になると、自分が所属するチームが受け持つ機能、プロダクト、領域と継続的に向き合えるようになります。すると開発プロセスやステークホルダーとの関わり方の改善も半永久で継続的に行う必要があったり、チームの経験を形式知として蓄積しやすくなるため、それらの活動を主な責務とするEPMを配置することは合理的ではないでしょうか。 特に特定の領域と継続的に向き合い価値提供していくためには、その領域の深いドメイン知識も必要になってきます。仮に開発プロセスやコミュニケーション力が高かったとしても、深いドメイン知識がないと適切なエスカレーションや開発スコープの変更判断などができないためです。 BASE BANKがEPMを据えた背景 ここからは具体例の一つとしてBASE BANKがEPMを据えた理由とBASE BANKでのEPMの働きざまについてもう少し深掘ってみます。 BASE BANKはショップオーナーが売上を立てたあとの活動に関わる機能やプロダクトを受け持っているチームです。 またBASE BANK社を設立した2018年はまだBASE社は目的別組織ではなかったため、BASE BANKはある意味BASE内での実験的な目的別組織(チーム)としてチームを成長させてきました。 そういった事も踏まえ、メンバーが増え受け持つプロダクトも増える中で、上記のようなEPMとしての責務を持つ役割が必要となったのは必然とも言えました。 BASE BANK黎明期 BASE BANKの立ち上げ期はざっくりエンジニアとPMが存在しており、明確に役割を細分化しなくても開発を回していける規模でした。 メンバーが増え、よりチームらしく エンジニアメンバーがjoinし、人数が少しづつ増え、受け持つ機能やプロダクトも増えててくると、ある程度意識して開発プロセスや採用、評価を回していく必要が出てきました。 そこでTeam Managementを行うEM、TLといった役割が生まれ、PMと協力してTeam Management、People Managementを行うようになりました。 このときはまだ、各機能の開発の運営はTLやPMが並行して受け持つ形になっていました。 ピザ2枚 を分け合う以上の規模に 更にメンバーの採用が進み、関わるプロダクトもYELL BANK、BASEカード、振込申請などさらに増え、各プロダクトのグロースにも本格的に着手しなければならなくなりました。 すると、それまで並行して複数プロダクトの開発プロセスを主導していたEMやPdMが本来の責務である組織全体の事業推進やTeam、People Managementに注力せざるを得なくなり、各プロダクトの開発プロセスの主導やPdM、各ステークホルダーとの橋渡し役として開発を主導するEPMという役割が生まれました。 ちなみに、BASE BANKのエンジニアは各々の強みを活かしつつ開発の全てのライフサイクルに関わる フルサイクルエンジニア としての能動的な働きを求められます。 そのため、EPMは主導といってもマイクロマネジメントをするわけではなく、開発チームとしての目的やビジョンを掲げつつ各メンバーと協力して日々の運営や改善をしたり、メンバーがより能動的に動けるようなお膳立てをします。 それぞれのプロダクトごとのEPMとしての役割 では、EPMのプロダクトごとの具体例を紹介します。 現時点では、主にBASE BANKとしては BASEカード と YELL BANK がメインのプロダクトです。 決済のドメイン知識を持つ松雪( @applepine1125 )がBASEカード、 元々YELL BANKの開発をしていた永野( @glassmonekey )がそれぞれEPMとして開発を主導しており、それぞれのEPMの役割について紹介します。 BASEカードにおけるEPM BASEカード のEPMとして、現在以下のような仕事を行っています。 - スプリントの運営 - QAの仕込み - 社内外のステークホルダーとのコミュニケーション - 関係各所へ決済の知識やBASEカードの仕様をインプット - 意思決定のための技術面または仕様面(特にカード決済の知識)の判断材料の提供、場合によっては自身で意思決定 https://cp.thebase.in/basecard cp.thebase.in BASEカードはショップの出金に直接関わる機能のため、内部統制などを責務とする Corporate Solutions Engineer(CSE) やリスクマネジメントチームとの関わりが多く、日頃から密にコミュニケーションを取っています。 また、機能実現のために外部企業が提供しているAPIを利用しており、仕様面の調整や、精算上の証跡を用意するためにBASEの経理との連携も密に行う必要があります。 BASEカードの場合、リアルカードの発行など直近の大まかな開発マイルストーンが割と明確であることから、 実装したい各機能のスケジュール上での優先度をPdMと議論、調整し、開発をすすめる上で発生する技術的な阻害要因を排除すること がEPMとして重要になります。 YELL BANKにおけるEPM 簡単にYELL BANKというサービスを紹介しておくと、BASEを活用されてるショップオーナーの皆様へ簡単に資金調達を提供するサービスです。 thebase.in 最近はスタートアップ企業の隆盛もあり、VCなどから資金調達をすることも馴染み深くなりましたが、BASEのようなロングテールの規模感だとまだまだ未知の市場です。 そのため、YELL BANKの開発では 少しずつ小さな仮説検証すること を重きにおいて開発サイクルを回しています。 特に機能をデリバリーするときはできる限りユーザーストーリー作成し、機能と価値をセットで考えられるように気をつけています。特に以下を意識してメンバーと一緒に作ります。 不確実性はどこになるあるのか? 仮説検証したい内容はどこか? 機能を横断的に作る メンバーと一緒に作ることで、メンバー全員がゴールを意識すること、EPM以外もオーナーシップを持って開発をすることに繋がると考えています。 またバックエンド・フロントエンドで区切るのではなく、横断的に作ることで手戻りはできる限り少なくします。 ユーザーストーリーの分割に関しては アジャイル開発におけるユーザーストーリー分割実践 〜画面リニューアルの裏側〜 をご覧ください。 devblog.thebase.in また、仮説検証を実践するにあたって作っては放置ではいけません。そこでPdM/PMMがPDCAを回しやすくするためのデータ基盤の整備もしています。 過去の実践した事例に関しては Google Apps Script× BigQuery × Googleスプレッドシート × データポータルで簡易CRMを作ってみた や Lookerでショップのサービス活用カルテを作成した話 をご覧ください。 devblog.thebase.in devblog.thebase.in BASE BANKとしてのEPMのこれから 一言にEPMと言っても、プロダクトの特性によって役割や重点の置き方が変わってきます。 今の役割の定義は来年には変わってることも全然あるでしょう。正直模索中です。 現在のEPMはプロダクトのデリバリーやクオリティにフォーカスした役割です。 ただ、プロダクトの成功を目指すためにはそれを支えるメンバーの中長期的な成長も重要な要素です。 そのため今後の可能性としてはメンバーに協力してもらうだけでなく、それを各メンバーの評価として還元するためにPeople Managementも担うようになる可能性も大いにあるでしょう。 おわりに これから更にプロダクトのデリバリー速度やクオリティを高めていくには、現メンバーの成長だけでなく、より多くのメンバーの力が必要です。 BASE BANK自体や各プロダクトへの興味でもEPMへの興味だけでもなんでもいいのでぜひ気軽にお話しましょう! open.talentio.com
アバター
この記事は BASE Advent Calendar 2021 の21日目の記事です。 devblog.thebase.in はじめに Payment Devグループの山本( @msysyamamoto )です。 この記事は私たちのグループで行っている読書会の紹介になります。これから読書会を開催しようとしている方や、いまの読書会を改善したいと思っている方の参考になれば幸いです。 こんな感じで行っています 参加者 基本的にはグループのメンバは全員参加になっています(もちろん、業務が忙しい場合などは欠席することは可能です)。現在は14名のメンバが参加しています。 グループの読書会となってはいますが、別のグループからの参加も可能だったりします。 体制 グループのメンバの中の一人が読書会のまとめ役となります。まとめ役は立候補制になっています。もし、立候補する方がいなかった場合は、グループのマネージャからの指名になります。 スケジュール 毎週1回、同じ曜日同じ時間に開催しています。時間は1回あたり1時間となっています。いつも同じ時間に開催することでリズムが生まれ、読書会を続けていきやすくなるのではないかと個人的には思っています。 場所 Zoom で行っています。 読書会は録画をしており、参加できなかった方は後でどのような内容だったかを確認できます。 選書の方法 まとめ役に委ねています。 多くの場合、まとめ役がいくつか本をピックアップし、参加者がそれに投票し読む本を決定します。ピックアップされた本以外で読みたい本があれば、まとめ役でなくとも選択肢に本を追加することができます。 まとめ役に委ねていますので、まとめ役が読書会で読みたい本があれば投票を経ずとも読む本を決定できたりもします。 参考までに今まで読書会で読んできた本を紹介します。2021年12月現在は『DNSがよくわかる教科書』を読み進めているところです。 リーダブルコード オブジェクト指向設計実践ガイド Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 テスト駆動開発 失敗から学ぶRDBの正しい歩き方 Web API: The Good Parts 決済サービスとキャッシュレス社会の本質 DNSがよくわかる教科書 本の難易度やページ数にもよるのですが、今までの実績からして大体1ヶ月から2ヶ月で1冊の本を読み終えています。 会の進め方 事前にやっておくこと 読む本が決定した時点で、読む範囲の決定と、その範囲は誰が発表者になるかを決定しておきます。発表者は1回の会で5人となっています。発表者はランダムで決定されるのではなく、参加者全員が同じくらいの回数発表者になるように決めています。 発表者は会が始まる前まで該当範囲を読み、気づき・疑問を弊社でドキュメントサービスとして利用しているKibelaに書いておきます。要約は書きません。 発表者でない方も事前に該当範囲を読んでおきます。 その会の司会者を決めます。司会者はボットによってランダムに決定されます(まとめ役 ≠ 司会者 となっています。ランダムなのでまとめ役が司会者になる場合もありますが)。 読書会本番 発表者がKibelaに記述した気づき・疑問を説明し、それに対してディスカッションして行きます。発表者は5人ですが、この進め方で毎回ちょうどよい時間に終わるか、少しオーバーするくらいの時間に収まっています。 以上が私たちのグループで開催している読書会の現在時点でのやり方になります。私が読書会に参加するようになったのは2021年7月なのですがその時とのやり方と現在のやり方を比べると、改善されている部分があります。 例えば発表者の決定方法ですが、以前のやり方はランダムで決定していたのですが、連続で発表者になったりする人が出たりするのでよくないよね、ということで今の方法に改善されました。 今のやり方がベストだとは思い込まずに、改善できそうなところはどんどん改善していき、よりよい読書会にできるといいと思っています。 よかったこと この記事の締めくくりとして、私個人の感想にはなるのですが、読書会でよかったと思うことを紹介します。 自分では読まなさそうな本を読むことができる 具体的に言いますと『決済サービスとキャッシュレス社会の本質』がそれにあたります。正直なところ、読書会で読む本のとしてピックアップされるまでその本を知りませんでしたが、決済に関わる仕事をしている身として、興味深く読み進めることができました。 自分とアンテナの張り方が違う人が集まることで、自分の知らなかった本を読むこと・知ることができるので、知識の幅を広げるのにいいと思いました。 コミュニケーションの場になる 私たちのグループは全員が同じプロジェクトを進めているわけではありません。同じプロジェクトに参加しているメンバとはよくコミュニケーションを取るのですが、別のプロジェクトに参加しているメンバとはそうではありません。 また、新型コロナウィルスの影響によりリモートで仕事をする機会が増え、グループのメンバと顔を合わせる機会が減ってきています。 それゆえ、同じグループの中でもお互いのことをよく知らないということがあるのですが、読書会がお互いのことを知るよい場所になっていると思います。
アバター
この記事は BASE Advent Calendar 2021 の20日目の記事です。 はじめに こんにちは。Owners Experience Backend Group の杉浦です。主にサーバーサイドのアプリケーションの実装をしています。 エンジニアにとって、外部企業から提供されるAPIやCDN(Content Delivery Network)といった『外部サービス』をどう扱うかは悩ましい問題です。 特にシステムの設計段階において「外部サービスをどうやって内部システムに組み込むのか?」という方針は、その後のアプリケーションの生産性に大きく影響します。 仮に、密結合に設計してしまうと「外部サービス」という不確実性に影響されやすくなるため、好ましい状況とはいえません。どのように疎結合を実現するのか?という設計が、外部サービスから不確実性をハンドリングする生命線になります。 そこで、この記事では、2021年11月にリリースを行なった「Akamai Image Video Manager の導入プロジェクト」を例に、外部サービスの不確実性を最小化する「設計戦略」の実践方法を提示します。 本稿は3章立てになっています。 第1章:外部サービスの不確実性という問題 第2章:設計戦略のプランニング 第3章:設計戦略を実行するチームマネジメント 第1章では、外部サービスの不確実性について考察しています。内部サービスがミクロな不確実性への対処が主軸になるのに対して、外部サービスの不確実性がよりマクロな範囲、特に外部企業の経営方針の影響を受けやすいという点に言及しました。 第2章では、不確実性を最小化するための設計戦略を提示します。疎結合な設計はもちろん、外部企業の経営状況を読み解くために財務状況(PL/BS)を踏まえる必要性を提示しました。 第3章では、Akamai Image Video Manager の導入プロジェクトを例に、設計戦略を遂行するためのチームマネジメントについて言及しています。外部サービスの導入・移行プロジェクトには技術的な知識が不可欠なことから、エンジニアである筆者がチームを取りまとめた実践例をとなります。 第1章:外部サービスの不確実性という問題 良い設計をするための1つの重要な論点として「不確実性の最小化」があります。 ここで重要なのが、内部のシステムに対する「不確実性」と、外部のシステムに対する「不確実性」は、それぞれ「似て非なる存在」ということです。 内部システムにおける不確実性は、社内のエンジニアでなんとか対応できる範囲が多いと言えます。あくまで、社内における不確実性に対峙するため、その対処も社内で完結しやすいためです。 一方、外部システムは「存在そのものが不確実性に満ちている」といえます。期待したレスポンスが返却されるか?というミクロな不確実性に加えて、それとは別次元のマクロな不確実性を伴います。 例えば、外部システムが「3年後にもサービスを継続している」という保証はどこにもありません。加えて「値上げをしない」という確証も(契約に入れない限りは)どこにもないのです。 つまり、外部システムの不確実性は、干渉できない範囲に及ぶため、これらの不確実性を考慮した設計を考えることは容易ではないといえます。 第2章:設計戦略のプランニング なぜ戦略なのか? 外部サービスの不確実性に対処するためには、単なるアプリケーションの設計はもちろんですが、その大前提となる「設計戦略」が鍵を握ります。 あえて「戦略」という仰々しい単語を使う理由は、その影響力の時間軸が長いからです。戦略を間違えると、リリース直後は問題が発生しなかったとしても、数年後に苦労します。出発点である戦略を間違えた場合、改修コストへの投資が必要になりますが、こうなると当然、ROIも低下するため、経営的にも好ましくありません。 つまり、方向性を間違えることは将来にわたってビジネスに好ましくない影響を与えることを意味するため、設計の上位概念である「設計戦略」が必要になるということです。 取り替え可能性を考慮して設計する 「外部サービスの取り替え可能性」を考慮した設計こそが、不確実性の最小化にあたって有効な方針となります。 取り替えが可能であれば、別の代替サービスへの移行や、代替機能の開発(内製化)が容易になります。そして、仮に、外部サービスで「値上げ」や「サービス停止」といった不確実な事態が発生した場合に、影響を最小限に抑える最後の砦になります。 ただし、この設計方針は、競合製品が存在する「コモディティーな市場」でしか取り得ない選択肢になります。 逆に、代替が存在しない「独占的なサービス」を利用する場合は、価格決定権は外部サービスの側にあります。さらに、代替サービスの登場は考えにくいため、取り替え可能な状態にする必然性に乏しいといえます。 この意味で、外部サービスの利用を前提とした設計戦略を考える上では、外部サービスのビジネス上の競争環境や、経営状況の見極めが必要になります。 このうち、外部サービスの経営状況は、財務の分析によって明らかにできます。米国企業であれば10K(Security Report)、日本企業であれば有価証券報告書が、そのサービスの置かれた競争環境を示す材料になります。 今回のプロジェクトではAkamai(証券コード:NASDAQ: AKAM)が提供するサービス「Akamai Image Video Manager」の利用を前提としたため、Akamaiが公表した2021年の10Kについて、ざっくりと目を通しました。 https://www.ir.akamai.com/sec-filings 責務を1つのリポジトリに集約する 今回の「Akamai Image Video Manager の導入プロジェクト」では、画像配信基盤を「取り替え可能な状態」にするという設計戦略を採用しました。 具体的な実現方法は、外部サービスを利用した画像配信に関する責務を、1箇所のリポジトリに集約するということです。 従来のBASEにおいては、複数のリポジトリに画像配信の外部サービスのコードが点在しており、責務分離の観点であまり好ましい状態ではありませんでした。 そこで、画像配信に関する実装上の責務を1つのリポジトリに集約することによって、疎結合の実現を目論みました。 第3章:設計戦略を実行するチームマネジメント エンジニア駆動のチームマネジメント 筆者の普段のBASEでの仕事は、コードを読み書きすることなので、プロジェクト関連のマネジメントには関与しません。 ですが、今回の「Akamai Image Video Manager の導入プロジェクト」においては、プロジェクトチームを取りまとめました。 その理由は、このプロジェクトを回すためには、エンジニアリングの専門的な知識が必要だったからです。 一般的なBASEの社内プロジェクトであれば、プロダクトマネージャの元に、複数のエンジニアが参画して、1つの機能開発に携わることが多いといえます。 ですが、このプロジェクトにおいては、画像ファイルの基本的な知識に加えて、キャッシュやCDN、BASEのリポジトリ構成を理解する必要がありました。そこで、普段はコードを書いている筆者が、チームを取りまとめることになったのです。 なお、このプロジェクトでは、筆者は実装を行なっていません(ただしプルリクのレビューは行なっています)。 これは社内事情が影響しており、当初は取りまとめと実装を両立して行う予定だったものの、急遽実装が必要になった別のプロジェクトに追加アサインされたため、時間の都合上、Akamaiの移行プロジェクトは「取りまとめ役」に徹することになったからです。※ ※急ぎの実装とは「Facebookドメイン認証」というプロジェクトで、これに関してもテックブログを書いています。 →「ROI(投資利益率)を意識したエンジニアリング」 答えのない意思決定に対峙する チームの取りまとめにあたって、実装の担当箇所の振り分けと、局所的な意思決定の2つを行いました。 1つ目の実装担当箇所の振り分けとは、(1)改修範囲の調査と見積もりを行い、(2)チームの各メンバーに実装箇所を割りあて、(3)リリースに至る手順を交通整理する、ということです。 2つ目の局所的な意思決定とは、リリースにあたって実装以外で決めなければならないことについて、社内の担当者と調整したうえで決定することです。今回は「ショップに対するコミュニケーション」と「APIの互換性」という2点で、意思決定を行う必要がありました。 「ショップに対するコミュニケーション」とは、BASEの顧客であるショップへの通知のことを意味します。画像配信基盤の移行にあたって、画像表示の仕様が微妙に変化することが避けられなかったため、利用者の方にその内容を事前にお伝えすることが必要でした。 この際に、どのような方法(メールなのか?管理画面なのか?)でお伝えするのかを判断する必要があり、プロダクトマネージャの方の大きな助けを借りつつ方針を決めています。 「APIの互換性」とは、BASEが外部に公開しているAPI(BASE-API)への影響を加味することです。 Akamaiへの移行によって、BASEが提供するAPIの構造には変化はないものの、画像関連の「パス」が変更されるため「潜在的に影響あるかもしれない」という懸念がありました。 従来の画像パスとの互換性を残すか?それとも残さないか?というのは非常に難しい問題で、BASE社内の各方面の責任者の方と相談しつつ、方針の決定に至りました。 これらの経過を経て、2021年11月ごろまでに「Akamai Image Video Manager の導入プロジェクト」は無事に終了しました。 協力していただいた社内の皆様、チームの方々に大感謝しております。 終わりに 筆者の個人的な見解として、インターネット業界では、2020年代を通じて外部システムが提供する機能のボリュームは増え続け、業界全体でAPIなどを通じた外部サービスとの連携が1つの主戦場になると見ています。 近い将来、エンジニアは「外部サービスの不確実性をいかに最小化するか」という問題に、より一層、真剣に対峙する必要が出てくるでしょう。 その際に、このテックブログの内容が少しでも参考になれば幸いです。 最後にいつもの宣伝です。BASEでは各職種で採用を強化しておりますので、お気軽にお声がけください! https://open.talentio.com/r/1/c/binc/homes/4380
アバター
この記事は BASE Advent Calendar 2021 の19日目の記事です。 BASE BANK株式会社でエンジニアをしている若野( @sam8helloworld )です。 私が普段見ているサービスではBASEの他のアプリケーションや外部サービスのページから遷移してくることがよくあります。 さらにその時々でユーザに提供する情報や振る舞いを変えたくなることもあります。 今回はそういったケースの中でも 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない という仕様を実現するために私が行った調査検証が記事の内容となります。 仕様を簡単に図で表すと以下のようになります。 アプリケーションの構成の整理 今回の実装を行うに当たって前提となるアプリケーションは以下の3つの登場人物で成り立っています。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 特定のディレクトリ配下(e.g. /shop_admin/xxx)のURLにアクセスしてきた時に、SPAの起点になるindex.htmlをレスポンスとして返すことが責務です。 BFFサーバ SPAから呼ばれて表示要件に応じたjsonをレスポンスとして返すことが責務です。サービスのビジネスロジックはここに集約されている場合が多いです。 SPA BFFサーバにサービスのビジネスロジックを委譲しており、画面の表示やユーザのインタラクションを扱うことが主な責務です。 どこにロジックを持つか問題 さて登場人物がわかったところで、今度はその中でも上記の 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない というロジックを持つべきなのは誰なのか?ということが問題になります。 今回のケースでは以下の要件を満たすことができるかが鍵になります。 ページ遷移してきた時に処理を実行できる 直前のページのURLを取得できる 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない では登場人物を1つずつ検証していきます。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 結論から言うと全ての要件を満たすことはできませんでした。(厳密に言えばやろうとすればできるけど遠回り感すごい。) [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない まず1つ目の要件の「ページ遷移してきた時に処理を実行できる」に関しては、外部サービスからページ遷移してくる都合上必ずバックエンドサーバのControllerでSPAの起点になるindex.htmlを返す処理を行うので以下のようにページ遷移時にAPIを叩くことも可能です。 public class XXController { // 省略 public function index() { // ページ遷移してきた時の処理を行う // $api->call(); $this->render('SPAのindex.html'); } } 次に2つ目の要件の「直前のページのURLを取得できる」に関しては、以下のようにリファラを参照することでブラウザセッション内の直前のページのURLを取得できます。 public class XXController { // 省略 public function index() { $referer = $_SERVER['HTTP_REFERER']; if ($referer === '特定のURL') { // ページ遷移してきた時の処理を行う } $this->render('SPAのindex.html'); } } ※ 注意点: <meta name="referrer" content="no-referrer"> タグがあるなど、リファラを送信しない設定になっているページのURLは取得できません。 最後に3つ目の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、以下の3パターンを場合のサーバサイドの状態を考える必要があります。 外部ページから戻ってきた時 外部ページから進んできた時 ページを更新した時 外部ページから戻ってきた時 例えば以下のように a.example.com -> b.example.com -> c.example.com とリンクを押して遷移して、戻るボタンで b.example.com に戻ってきた場合を考えます。 ブラウザの進むボタン(history forward)や戻るボタン(history back)はリファラの状態も元に戻してしまいます。なので、 c.example.com から遷移してきたにも関わらずバックエンドでリファラを参照するとあたかも a.example.com から遷移してきたように見えてしまうのです。 developer.mozilla.org html.spec.whatwg.org 外部ページから進んできた時 「外部ページから戻ってきた時」とは違って確かに直前のページの意味合いはあっていますが、今度はバックエンドでリンクを踏んだページ遷移と区別がつかないので要件を満たせません。 ページを更新した時 ページを更新した場合も「外部ページから進んで来た時」と同様にリファラはそのままですが、バックエンドではリンクを経由したページ遷移と区別がつかないので要件を満たせません。 ※ セッションやCookieを使えばバックエンドで状態を持てるのでページの更新や2回目以降の遷移かどうかが判断できますが、後述の方法の方がシンプルなので採用していません。 BFFサーバ そもそものSPAから呼ばれてjsonを返すことが責務なので、BFFサーバに画面遷移の状態を判別させるのはお門違いです。 [ ] ページ遷移してきた時に処理を実行できる [ ] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない SPA SPAではなんとか3つの要件全てを満たすことができました。 [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [x] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない 1つ目の要件の「ページ遷移してきた時に処理を実行できる」と3つ目の要件の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、ブラウザのNavigation Timing APIを使うことで要件を満たせます。 Navigation Timing APIではバックエンドで判別できなかった戻る・進む・更新・ページ遷移をそれぞれ状態として取得できるので、ページ遷移の時だけ処理を行うということが可能になります。 developer.mozilla.org ただ注意が必要なことは、バックエンドと違ってSPAではページ遷移という概念が普通のリンクを踏んだページ遷移とvue-routerなどのjsで制御されたルーティングによるページ遷移の2つあるということです。 2つ目の要件の「 直前のページのURLを取得できる」に関しては、 document.referrer がバックエンドのリファラと同じ役割を果たすので要件を満たせます。 mounted() { const referrer = document.referrer if (referrer === '特定のURL') { // ページ遷移してきた時の処理を行う } } Navigation Timing API さて、Navigation Timing APIを使えば要件を満たせると言いましたが、Navigation Timing APIとはどういうものなのでしょうか? Navigation Timing APIの MDN Web Docs を参照すると主に以下の使い方をするAPIであると書かれています。 Collecting timing information(タイミング情報の収集) Determining navigation type(ナビゲーションタイプの決定) さらにDetermining navigation typeでは以下のことが判別できると書いてあります。 Was this a load or a reload? (ロードかリロードか) Was this a navigation or a move forward or backward through history? (ページ遷移かhistory backかhistory forwardか) How many (if any) redirects were required in order to complete the navigation? (遷移が完了するまでに何回りダイレクトしたか) これは上述したクリアすべき要件にピッタリハマりそうです。 Navigation Timing APIを使ってナビゲーションタイプを判別する方法 ナビゲーションタイプを判別する方法は2つあります。 1つは window.performance.navigation.type を参照する方法です。 if ( window . performance . navigation . type === window . performance . navigation . TYPE_NAVIGATE ) { console . log ( 'ページ遷移' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RELOAD ) { console . log ( 'ページ更新' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_BACK_FORWARD ) { console . log ( '戻る・進む' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RESERVED ) { console . log ( 'その他' ) } developer.mozilla.org 2つ目の方法はPerformanceNavigationTimingインタフェースを使用する方法です。 const entries = window . performance . getEntriesByType ( 'navigation' ) for ( const entry of entries ) { if ( entry . type === 'navigate' ) { console . log ( 'ページ遷移' ) } if ( entry . type === 'reload' ) { console . log ( 'ページ更新' ) } if ( entry . type === 'back_forward' ) { console . log ( '戻る・進む' ) } if ( entry . type === 'prerender' ) { console . log ( 'その他' ) } } developer.mozilla.org 2つの判別方法の使い分け 2021年12月現在、主要ブラウザで window.performance.navigation.type を利用することはできますが、参照することは非推奨となっています。 代替の方法として2つ目のPerformanceNavigationTimingインタフェースを利用することが推奨されている状況です。 ただし、PerformanceNavigationTimingはiosのsafariが今のところサポートしていない状況です。 const types = window . PerformanceObserver . supportedEntryTypes if ( types . includes ( 'navigation' )) { // PerformanceNavigationTimingインタフェースに対応 } 上記の supportedEntryTypes でPerformanceNavigationTimingのサポート状況は確認できます。 navigation という文字列を含む配列を返す場合は対応済みなのでPerformanceNavigationTimingインタフェースを使った実装を。 navigation という文字列を含まない配列を返す場合は window.performance.navigation.type を使った実装を行うのが良さそうです。 developer.mozilla.org Navigation Timing APIを使った判別はvue-routerを利用した遷移にも使える? Navigation Timing APIにおける「ページ遷移」「戻る・進む」はあくまでも document オブジェクトが初期化・再構築されるような処理に対して遷移に対して判定されるようです。 www.w3.org html.spec.whatwg.org VueをはじめとしたSPAのルーティングでは document オブジェクトを初期化するのではなく、一部のDOMを更新する仕組みになっているので「ページ遷移」「戻る・進む」は判定できませんでした😢 ただ、今回処理を行いたいのはVueのルーティングではなく外部ページからリダイレクトされてきた時なので要件は満たせるというわけです。 document.referrerと組み合わせて「特定ページからの初めての遷移を判定する」をコード isFromCertainPage() { // safari on ios か確認 if (! window .PerformanceObserver.supportedEntryTypes. includes ( 'navigation' )) { // ページ遷移か確認 if ( window . performance .navigation. type !== window . performance .navigation.TYPE_NAVIGATE) { return false } } // ページ遷移か確認 const entries = window . performance .getEntriesByType( 'navigation' ) for ( const entry of entries) { if (entry. type !== 'navigate' ) { return false } } const expectedReferer = '特定のURL' let referrer = document . referrer . replace ( /\?.*$/ , '' ) // safariのバージョンによってreferrerのトレイリングスラッシュの有無が分かれるので、一律トレイリングスラッシュをつける // https://trac.webkit.org/changeset/280342/webkit/ referrer = referrer. slice (- 1 ) !== '/' ? referrer + '/' : referrer if (!referrer. startsWith (expectedReferer)) { return false } // SPA内での回遊判定 if ( this .$store. state .alreadyVisited) { return false } return true } ※ SPAのルーティングの遷移に関しては現状判定するAPIがないので、状態を持つようにしています。 終わりに Navigation Timing API自体は複雑ではないので、すぐに実装に入れました。ただ、SPAではなぜNavigation Timing APIでページ遷移が判定できないのか?ということを深掘りしていくとブラウザAPIの仕組みが垣間見えて結構面白かったです。 あとは今回みたいにブラウザのAPIを利用したコードのテスト書こうとしたときに、jsdomだとモックだらけになっちゃうし実ブラウザを使ったIntegrationテストにするにもコストかかるしで塩梅が難しいと感じました。ここら辺のテスト詳しい方がいたらぜひ @sam8helloworld まで🙇‍♂️ また、もしBASEBANKに興味のある方は @sam8helloworld や下記のリンクまで open.talentio.com 明日はOwners Experience Backend Groupの杉浦さんです。
アバター
この記事は BASE Advent Calendar 2021 の19日目の記事です。 こんにちは、BASE株式会社でバックエンドエンジニアをしている小川です。 私たちのチームでは10月からレスポンス改善PJとして、BASEのレスポンスが遅い処理を改善する施策を行なってきました。 その中で下記の例のようなクエリが重い処理として挙がり、そのクエリではGROUP BYが使われていたのですがDISTINCTを使った方が早くなるのではという意見がありました。 SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; そこで、DISTINCTとGROUP BYでどちらが早いのかをネット上で調べたところ、DISTINCTの方が早い説やGROUP BYの方が早い説、どちらも変わらない説などいろんな説があり、実際のところどうなんだろうと思ったことがきっかけで検証してみることにしました。 DISTINCTとGROUP BYどちらを使った方が良いか検証した結果をご紹介したいと思います。 DISTINCTとGROUP BYの違いについて DISTINCTとGROUP BYは重複行をまとめるという目的で使われると思いますが、それぞれどんな違いがあるのでしょうか。 DISTINCTについて 射影を行う段階で重複を排除 GROUP BYについて 指定された列名でレコードのグループ化を行う。 グループ化したレコードは、集約関数を用いることで集計することができる。 つまり、 DISTINCTは、重複を排除した結果を出力する場合に使用 GROUP BYは、重複を排除した結果に対して集計処理を行いたい場合に使用 という違いがあります。 今回例に挙げたクエリはGROUP BYを使用していましたがグループ化した後、集計処理をせずに重複行を削除するためだけに使われています。 こういった重複行を削除するためだけなら、用途にあったDISTINCTを使うべきだと思います。 次に肝心なDISTINCTとGROUP BYどちらが早いのか見ていきます。 DISTINCTとGROUP BYどちらの方が早い 下記のようなDISTINCTとGROUP BYを使用したクエリを使って、クエリキャッシュを無効にしてそれぞれの実行計画と実行速度を検証してみました。 SELECT DISTINCT c1 FROM t1 WHERE c1 > const; SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; ※c1にはインデックスが貼ってある状態 検証環境 MySQL5.6 検証結果としましては、 実行計画は全く同じで実行速度もほぼ同じ結果となりました。 MYSQL-DISTINCTの最適化 In most cases, a DISTINCT clause can be considered as a special case of GROUP BY. For example, the following two queries are equivalent: Due to this equivalence, the optimizations applicable to GROUP BY queries can be also applied to queries with a DISTINCT clause. Thus, for more details on the optimization possibilities for DISTINCT queries, see Section 8.2.1.14, “GROUP BY Optimization”. こちらのURLのMYSQLのドキュメントにも記載されている通り、オプティマイザによって最適化された結果、内部では同じ処理になり性能は同じということが分かります。 実行速度も検索結果も同じということであれば、DISTINCTとGROUP BYは使用用途によって使い分けてください。 おわりに DISTINCTとGROUP BYについての違いや、速度に関して紹介させていただきましたが参考になったら幸いです。 今回の調査ではレスポンスの改善にはなりませんでしたが、ほんの些細なことでも調べて、検証することによって新たな発見があると思います。 実際に私も検証するまでは、GROUP BYよりも重複行を削除するだけの処理のDISTINCTの方が早いと思っていました。 ぜひ気になったことがあれば検証してみてください! 明日のアドベントカレンダーはyusugiuraさんとShoTakeuchiさんです!お楽しみに!
アバター
バックエンドエンジニアの @cureseven です。レスポンス改善プロジェクトという名前で、BASEのレスポンス速度を早くするために10月より動いてきました。 経緯に触れた後、レスポンス改善をどう進めてきたかとおこなった施策を紹介します。 レスポンス改善プロジェクトの経緯 始めにプロジェクトが立ちあがった経緯を少しお話します。 BASEはリリースして9年ほど立ちました。その間、さまざまな機能が追加されたり、ありがたいことに多くのユーザーさんに使っていただいています。その結果、DBに保存しているデータ量が増えました。データ量が多いことによりSelectの実行時間が重くなるなどが起こっていました。 ユーザの方から複数件画面の重さについてのお問い合わせをいただいており、今回プロジェクトとして改善することになりました。 重いリクエストの特定 それではここから、どう進めてきたかとおこなった施策を紹介します。 BASEではNew Relic Oneを導入しており、リクエストごとのレスポンス速度の統計を見たり、Transactionの詳細を見ることができます。私たちはNew Relic APMで重いリクエストを突き止め、施策を実施しました。 New Relic APM Transactionsのページで、 Most time consuming(実行回数 × 平均実行時間) Slowest average responce time(平均実行時間) を見比べながら、平均実行時間が遅すぎるものやたくさんの人が利用していて不便に感じそうなものを中心に見ていくことにしました。 Most time consuming順にエンドポイントをsortしている 今回は購入者が触れる画面ではなく、オーナーさんの作業がサクサクできることにフォーカスして、オーナーさんの使う管理画面の重いものから見ていきました。 最も重い画面についてはある程度見通しがついていましたが、Most time consumingについては予測を立てることが難しいです。New Relic APMを見ることでどんなAPIが重いのかを把握することができました。根拠となる数字を持って優先度を決め施策を実施していくことで、効率よくオーナーさんの問題にアプローチできました。 また、フロントエンドで時間がかかっているかどうかの検証にはBrowserの機能を利用できます。これから使っていくつもりです。 dashboardによる重いリクエストの可視化 オーナーさんからのお問い合わせを受けるより先に社内で重いリクエストがあったことを知るために、Transactionのdashboardを作成し監視することにしました。 dashboardでは注文数の多いshopをいくつかピックして、オーナーさんの使う管理画面に限定したTransactionの95パーセンタイルの重さが目立つリクエストを表示しました。 alert飛ばす設定 dashboardを作成しましたが、張り付いて監視しないでいいように、alertを飛ばす設定をしました。 今回は購入者が触れる画面, オーナーさんの使う管理画面両方において、それぞれにTransaction timeの閾値をセットし、リクエストごとに95パーセンタイルが閾値を超えたときにslack通知するようにしました。 設定に役立ったリンクです。 slackのhookリンクの生成 alertの設定 alert設定のパラメータの説明 通知頻度の設定 レスポンス改善施策の実施 New Relic APMのTransaction traceという機能で、Transactionの詳細を見ることができます。どのメソッドのどのクエリが重いかまで断定できます。 私たちは重いリクエストのTransaction traceを見ることでボトルネックを把握し、リファクタ方針を考えました。リファクタ施策の具体例をいくつか紹介します。 indexを貼った 重いクエリを発見したとき、explainして実行計画がどのようなものかを確認しました。indexが適切に使われていないクエリを発見したので、indexを貼る対応を実施しました。 不要な処理を消した 機能のリニューアルや機能を廃止する中でいらなくなった処理が残ってしまうことがあります。今回、そんな処理がTransaction timeを圧迫してしまっていました。不要な処理を削除することでレスポンスが改善しました。 クエリ改善 今回よく目にしたのが、レコードの存在の有無だけ分かればいい時に、集計したり大量にデータを取ってきたりしているというものでした。レコードの存在有無確認は最初の1件だけとれば良いので、集計処理などをやめることで、クエリ実行時間を短縮できました。 また、画面描画毎に一つずつのデータを取ってきて集計するのではなく、バッチで集計したデータを格納しているテーブルを利用するようにする修正を実施しました。こちらの修正では、95パーセンタイルで15秒ほどかかっていたリクエストが、1秒以下で返ってくるようになりました。 また、注文数をカウントする処理では、画面上は99以上になると「99+」という表示になるところがあります。大量に注文があるshopのクエリがとても重くなってしまっていたため、サブクエリを使って100件以上は数えないようにしました。 バッチ実行の負荷軽減 New Relic APMのTransaction traceは、1分ごとに、基準値より重かった最大Transaction timeの詳細を残しています。 取れたTransaction traceを眺めていると、どうやら15分ごとに基準値に引っかかっていることがわかりました。詳細を見ると、15分おきに出ているTransaction traceではdb接続に決まって6秒ほどかかっていることがわかりました。 原因は15分間隔で実行されるバッチにて大量レコードのtruncateおよびinsertのSQLを実行していることでした。一旦メモリにもった上でループ処理することで急激な負荷を抑えることで、15分おきに出ていた6秒のdb接続時間は解消されました。 dashboardを見ているだけではこのような傾向を読み解くことはできませんでした。Transactionの99パーセンタイル、95パーセンタイル、50パーセンタイルを見比べたり、Transaction traceの出力時間を見たりすることで見えてくるものもありました。 プロジェクトを通して 3ヶ月間対応を行なってきた結果、画面ロードが遅いことに関してお問い合わせいただいていたshopさまから改善されたねの声をいただきました。しかしながらまだまだ遅い画面はあるため、来年以降も改善にむけて動いていこうと思っています。 最後に、レスポンス改善プロジェクトを通して感じたことを記して終わろうと思います。 機能リニューアルなどのタイミングで適切に不要な処理を消すのはもちろんですが、今回のプロジェクトのようにたまにリファクタをするための期間を設けてそのような処理がないか見直すのは大事だなと思いました。大掃除の季節ですし、ソースコードの見直しを実施するいいタイミングなのかもしれません。可読性も上がります。 機能開発のタイミングで貼ったindexが、データが増えることにより効かなくなるときがあります。今回の施策で劇的に効果が上がったのはindexの付与でした。こちらもリリース後年月が経ってから改めて見直すのが良いなと思いました。 あまりにもレスポンスが遅いと素晴らしい機能も使い物にならないので、仕様を少し変更して大幅に改善するものであれば、変更することも検討してみるのがいいなと思いました。 これからも使っていけるサービスであるために、さまざまな実装方法を検討する必要があると改めて感じました。
アバター
この記事は BASEアドベントカレンダー2021 18日目の記事です。 UIデザイナーの Yoshioka です。コードベースのデザインツールとして個人的に気になっている UXPin Merge を試してみました。 UXPin Mergeとは UXPinはベクターベースのデザインツールとは違い、HTML/CSSのコードで定義されていることをベースにしているデザインツールです。 UXPin Merge はReactやStorybookのコンポーネントを取り込み、実装されているものと同じコンポーネントを元にデザインができる機能です。 試してみる 今回は簡単にローカルでつないでみました。 ReactComponentsやStorybookを読み込むことができます。 UXPin MergeのReactComponents、Storybookでできることが違います。(詳しくは後述します) また、storybookの場合にはコンポーネントの配置に若干のラグが発生します。 立ち上げる 該当のReactComponentsリポジトリから、ローカルでExperimentalModeを立ち上げます。 これにより、URLが生成されるのでコード変更してどうUXPinで反映されるのか確認することができます。 配置していく コンポーネントがこのようにリスト表示されるので、これをドラッグ&ドロップして配置していきます。 簡単なデザインですが配置してみました。 できること 各コンポーネントをクリックするとコンポーネント定義されているpropsが表示されます。 今回はReactComponents連携したので、コンポーネントをネストすることができます。 (storybook連携ではできない様です) ネストすることのメリットは、下記の様にレイアウトコンポーネントを作成し、その中にコンポーネントを入れ込むことができます。 ネスト内のコンポーネントを削除することもできます。 今回はStackというレイアウトコンポーネントの中に、SelectとButtonを内包しています。 StackのpropsでDirectionを持っているので、レイアウトを切り替えることができます。 レイアウトコンポーネントのMarginなど覚えておくのも大変なので便利です。 できないこと Figmaなどのデザインツールと違ってコンポーネントのDetachはできません。 もちろんテキストや図形など配置できますが、コンポーネント内に配置するなどはできません。 このあたりはプロダクトやデザイナーの好みによりそうですが、自由にデザインデータを作れないことにあまりネガティブな印象はありませんでした。 また、Figmaの様にテキストをアートボード上で編集することはできず、props上で入力する必要があります。 プロトタイプについて まだ上記の様にコンポーネント配置しただけですが、この状態でどのようなプロトタイプが作成されるのかみてみます。 Interactionにはなにも設定していませんが、このようにプロトタイプが作成されます。 Figmaでも似た表現を行うことはできますが、実装が元になっていることでコンポーネントライブラリと差分なくプロトタイプ確認できるのはメリットかなと思います。 デザインツールで起きがちな問題 ここでたまに聞くデザインツールで起きがちな問題を洗ってみます。 このコンポーネントない、などのデザインデータと実装のギャップなどどう解決してよいかわからない沼が出てきます。 コードベースだと解決できるかもしれない 簡単に触った程度ですが、ここまででコードベースのデザインデータにした時のメリットは デザイナー・エンジニアがコンポーネントに対し同じ基準でデザインできることにあります。 これまでの企画・デザイン・コーディングといったフローから、デザイナーとエンジニアが共同でプロトタイプを作ることができることで、既に共通の認識があるのでコミュニケーションコストを抑え開発スピードをあげることができそうです。 現状ではデザイナーがコードへの理解を示すこと、開発環境作りやGitの連携にかなりエンジニアに工数を割いてもらう必要もありますが、検討してみる価値はあります。 来年以降もコードベースのデザインツールを注目していきたいとおもいます!
アバター
この記事はBASEアドベントカレンダー2021 17日目の記事です。 はじめに DataStrategyチームの杉です。 ショッピングアプリPay IDではさまざまなショップでの商品購入が可能です。 "探す"タブにはおすすめ機能がついており、利用者にあった商品やショップのレコメンドを行なっています。 おすすめ商品の掲載例 おすすめの商品ではさまざまなアルゴリズムを並行に運用しており、その中のひとつとしてAmazon Personalizeを利用しています。 このアルゴリズムの計算は今まで1日に1回のbatch処理で行なっていました。 しかし、閲覧や購入のログをリアルタイムに利用することでよりマッチしたおすすめ商品を掲載することができるのではという想いでevent trackerを用いたリアルタイムに変化をするレコメンドに挑戦をしました。 この記事では、event trackerをどう実装したかをメインにお伝えしたいと思います。 Amazon Personalizeとは Amazon Personalizeについては以前に@pigooosukeさんが発表をしているので運用しているおすすめの全体の構成やAmazon Personalizeの利用注意点などについてはこちらで見ることができます。 https://speakerdeck.com/pigooosuke/aws-personalize-recsys (現在の構成と異なる部分もあります) Amazon Personalizeを使用することでメンテナンスコストを下げることや精度の高いおすすめを提供することができています。 event trackerについて Amazon Personalizeではevent trakcerを使うことでリアルタイムなイベントを反映させた結果の推論結果を取得することができます。 作成、eventの送信はどちらもboto3で行うことが可能です。 ①event trackerの作成 personalize = boto3.client(service_name= 'personalize' , region_name=AWS_REGION) response = personalize.create_event_tracker( name=EVENT_TRACKER_NAME, datasetGroupArn=dataset_group_arn ) ②eventが発生するたびにput eventを行う personalize_runtime = boto3.client( service_name= 'personalize-runtime' , region_name=AWS_REGION) personalize_events.put_events( trackingId=tracking_id, userId=userId, sessionId=sessionId, eventList=[{ 'sentAt' : timestamp, 'eventType' : 'view' , 'itemId' : '12345' }, { 'sentAt' : timestamp, 'eventType' : 'view' , 'itemId' : '6789' }] ) eventの送信はuser_id単位で送ります。 また、session_idを用いることでその時点では未知なユーザに対してもデータを溜めることが可能です。 推論への影響はすぐに反映され、eventの影響を確認することができます。 event内容によっては必ずしも推論結果に影響するわけではないので、put eventをしても変わらないこともあるということには注意が必要です。 event trackerを導入後の構成 eventとして溜めていくデータとしては2種類あります。 DBに記録された購入情報など 行動ログ これらをリアルタイムにevent trackerに流し込んでいくために以下の構成にしました。 メインのworkerは2台あります ①Kafka->SQS DBの更新イベントと行動ログをKafkaで取得し、SQSへ随時eventを送ります。 ここではRDSに接続をし必要情報の紐付けなども行なっています。 ②SQS->put_event SQSからAmazon Personalizeのevent trakcerへput eventをするような動きをしています。 SQSを挟むことで急激なトラフィックなどが来てもevent trackerに流し込む部分の負荷が高くならないようにしました。 event trackerの注意点 event trackerの検証を行なっているうえで、注意点に出会いました。 いくつかありましたが、その中でも2つご紹介させていただきます。 1. 未知商品に対してはeventを追加しても影響がない データセットには以下の3種類あります。 Item User User-item interaction このItemに入っていない商品は推論結果として出現しません。 そのため、未知商品をeventとして追加をしても推論結果が変わることや推論結果として新しく商品が登場することはありませんでした。 ここで未知商品を反映させるためには商品登録時にPutItemsをする必要があります。 2. "interactions"データセットimportだけではput eventの内容は消えない 今までdailyで計算をする際にデータセットを毎日importすることで上書きをしていました。 これはデータセットの肥大化を防ぐことを目的として行なっておりました。 put eventで追加したデータは"interactions"データセットとして追加されていきます。 そのため、今回リアルタイムに追加をしたeventの内容と、毎日の集計しているデータセットの中身が被ってしまう問題が発生しました。 "interactions"データセットを削除することでevent trakcerに追加した中身を消すことができます。 しかし、"interactions"データセットを削除するためにはevent trackerの削除やfilterの削除も必要です。 そのため、実際に試したところ、約20~30分filterが不在になることとなりました。 (この時間は今回試したデータセットの大きさの場合であり、データセットの大きさなどで変化します。) filterではすでに購入した商品などを除くような処理をいれているため、filterがないことはよくないと判断をし、最終的には"interactions"データセットは初期load以降はevent trackerで補うように変更をし毎日の作り直しをやめました。 さいごに 今回、リアルタイムな閲覧情報などをAmazon Personazlieのevent trackerで導入することで変化するレコメンドについてご紹介をしました。 また、ご紹介した機能は検証中のため、現在全ユーザに公開されている機能ではありません。 これからももっとよりよい精度と新しい発見を提供できるようにレコメンドに磨きをかけていきたいと思います。 最後までお読みいただきありがとうございました。明日はkikuchiさんとcuresevenです!
アバター
この記事は New Relic Advent Calendar 2021 16 日目の記事です。 遅刻しちゃいました。本当は発表の裏側的な話にしようかなと思ったのですが、先日のイベントが素晴らしかったので、参加レポートにしました。 はじめに こんにちは!! BASE BANK 株式会社 Dev Division にて Software Developer をしている永野( @glassmonkey )です。 普段はバックエンドエンジニアとして、Go/Python/PHP を主に書いてたりします。 昨日行われた NRUG (New Relic User Group) Vol.1 に参加し、社内での活用事例として「初心者でも簡単に扱えるNew Relic」というコンセプトで発表させていただきました。 nrug.connpass.com 前回のvol.0 では同僚の清水( @budougumi0617 )による参加レポートもあるので合わせてご確認ください。 Goを New Relic で扱いやすくするOSSの紹介もあるので、ぜひ御覧ください。 devblog.thebase.in NRUG (New Relic User Group)とは NRUG(New Relic User Group)はNew Relicを活用するユーザーの集いです。 Slackもあるので、興味のある方はぜひご参加ください。一緒にわいわいしましょう。 招待リンクは こちら です。 発表した内容 内容は私達 BASE BANKチーム が運用しているAPIを New Relic を使って改善したという事例です。 裏コンセプトとして、 NRQL をまともに書けない私でもNew Relicを活用し、数字として繋がるような改善をすることができたというのがあったりはします。 別の切り口でのパフォーマンス改善話を別チームの @cureseven さんから、記事公開の予定なのでそちらも楽しみに!! 公開は18日予定です。 感想 New Relic 三昧の1日で非常に楽しかったです。 特に中の人に直接質問できたのは大変良い機会でした。 リリースノートを眺めるのも楽しいですが、肉声でおすすめの機能が聞けることはイベントの参加の醍醐味だと感じました。 特に CodeStream は分散トレースからログだけでなく、普段使ってるIDEでコードジャンプが実現する素晴らしい機能で感動しました!! 活用していきたいなと思いました。JetBrains系、VS Code、VisualStudioに対応してるので普段使いには困らなさそうです。 www.codestream.com ぜひ一緒にオブザーバビリティをやっていきたい人いたら一緒に働きましょう!! open.talentio.com open.talentio.com
アバター
CTOの川口 ( id:dmnlk ) です。 BASE株式会社は本日、 The PHP Foundation への寄付を行いました。 The PHP Foundationとはなんぞや、というのはインフィニットループ様の記事を読むのがとてもわかりやすいのでそちらをご覧ください。 www.infiniteloop.co.jp BASEで動いている多くのコードはPHPであり、そのPHPの継続的な開発を支援するのは当然であると考え今回寄付をしました。 opencollective.com とりあえず今年分はまとめて行いましたが、来年以降も継続的に行っていく予定です。 この記事を見た、PHPを利用している企業の皆様も一度ご検討ください。 余談ですが、OpenCollectiveを初めて使いましたが組織の妥当性検証ができないのが気になりましたが、騙って寄付するメリットもよくわからないですし気にしないこととしました。
アバター
この記事は BASE Advent Calendar 2021 の16日目の記事です。 こんにちは。ProductManagementグループに所属している坂東( @naoto bando )です。 2021年はメンバーが10名を超える2つのPJを同時進行する機会があり、色々と考えることや学ぶことの多い年でした。 特にコロナ禍の真っ只中だったということもあり、リモート下におけるコミュニケーションはこれまで以上に気を使う面が多くありました。 本稿ではそういったコロナ禍におけるコミュニケーションの中のランチ会(懇親会)のさいに工夫をしたことを書かせていただきます。 読まれた方が工夫されている点などがありましたら、twitterなどで教えていただけると嬉しいです。 ※これはランチ会後の状態です。楽しそうな雰囲気になりました。 大人数のリモートランチの難しさ リモートであってもなくても、PJにおけるランチ会など懇親会の目的は、 PJメンバーが一定のコミュニケーションをとり、お互いの理解度を深めることにあるかと思います。 そのため、企画者の最低限の役目は「みんなが安心に快適に話せる」環境を作ることです。 オフラインや少人数リモートは楽 これらの状態はオフラインであれば、対面する人やそばにいる人などの2~4名ほどの小さなノードになって対話が進むため、放っておいてもおおよそなんとかなります。 また、だいたいのケースで企画者は全体的に声が聞こえ目が届く状況にあるので、席をシャッフルをするなど、状況を見ながら対応を決めていくことができます。 つまり、場所を提供すればなんとかなります。 オンラインでも3〜4名ほどであれば、比較的みんなが満足に話せる様に感じます。 大人数リモートの罠 しかしながら、オンラインでかつ10名を超えるような規模感になると、次のようなことが問題になります。 声がかぶると対話できないため喋れない 少数の人が喋り続け、会話の輪に入れない 急に話をふられて焦る そのため、話すのが好きな人が話し続ける感じになりがちかなぁ。と感じています。 こうなってくると気持ちも離れ、仕事のことを考えはじめ、いっそうコンテキストがわからなくなっていきます。(まさに私がこの状態になりがち。) これはもともとの目的からすると大変よろしくない状況です。 Remo Conference などを用いれば小さなノードにすることもできますが 別部屋の雰囲気などがわからないので、企画者としては怖いものがあります。 どこかの部屋が静まりかえっていたりしたらと思うと、怖くないですか・・・? どうしたか ここまでのことから、問題は参加者がしゃべる機会やタイミングを失うことで、場への参加を諦めてしまうことにあると言えそうです。 そこで、この問題の解消方法として、以下を満たせばランチ会の目的を果たせると考えました。 それぞれが話す時間を強制的に作る 話のお題をある程度予想できるようにしておく(場のコンテキストから置いてけぼりにならないようにする) また、PJ内でのランチ会は継続的におこなわれるため、適度なランダム性と拡張性を持てるようツールを用いておこないました。 ここからは NETA というツールを使ったランチ会と、自前のLT会ぽいランチ会について紹介していきます。 それぞれに一長一短がありました。 参考になれば幸いです。 NETA を使ってみた NETA はAID-DCC Inc.さんが作成されたオンライン上のコミュニケーションツールです。 このツールは事前に「話すテーマ」と「話す人」を設定しておくと、ランダムで「話すテーマ」と「話す人」を指定してくれます。 プリセットの話すテーマ集も準備されています。 「オンライン飲み」や「アイスブレイク」「合コン」「SONTAKU」(?)などのテーマを選択することで、参加者の名前を記入するだけで開始できます。 実際にやったこと 以下の理由で事前にPJメンバーに対し「他のPJメンバーに聞きたいこと」をアンケートで募集しました。 プリセットの質問は少々PJの雰囲気に合わないかなと感じがし PJメンバーの対話感を出したかった こうすることで、局所的に質問者と返答社の擬似的な対話を作ろうとしました。 PJメンバーへの呼びかけ Googleフォームで簡単に「他のPJメンバーに聞きたいこと」を収集するフォームを作成。 集めた質問集。 「さいきん一番痛かったこと辛かったこと(物理・精神どちらでも可)を教えて下さい」?🤔 質問にも個性がでておもしろいですね。 集計した質問週と参加者をNETAに質問を記入すれば準備完了です。 実際に動かしてみるとこんな感じです。動きがあるので楽しいですね。 やってみて 適度にランダム感があり、楽しく過ごすことが出来ました。 質問にも返答にも個性が出るので、PJメンバーの意外な一面などを見ることができました。 よく出る質問や、よく当たる人などが出てくるため、そういったハプニングも含めて場を楽しめます。 ただ、指名される人はランダム抽出のみであるため、ランチ会も後半になると指名されない人がでてきます。 そのため、最後はルーレットを回して話すテーマを決め、回答者はファシリテータが指名する形になります。 なので、ファシリテータは状況に気を使いながら進行をする必要がありました。 NETAの特徴 何が当たるか、誰が当たるかわからないランダム性を楽しみたいとき すでにPJ内の心理的安全性が保たれているとき 参加者がアドリブに強いとき こんなときはNETAが良さそうです。 ランダムであることの面白さを生かすのであれば、NETAはオンライン飲みなど、よりカジュアルな場面の方が活きるかもしれません。 逆に、どうしても話す人が中心になってインタラクションが生まれにくかったり、 ビジュアル的な説明ができないので、トーク力が出てしまうと言った点が気になりました。 こういったことを活かして、次のような形式を試してみました。 LT会形式にしてみた LT会は(説明するまでもないかもしれないですが)1人の持ち時間が数分の短いプレゼン会です。 気負わず気軽に発表してみましょう。といったものです。 この形式にのっかり、先に質問と書き込むスペースをわたしておき、あとは適当に画像とか用いて質問に答えてもらいました。 事前に準備できて、かつ画像やリンクで表現できるため、口下手であっても情報量を補えるだろうという考えです。 実際にやったこと 事前に準備が必要なため、1週間くらい前にこんな感じでメンバーに依頼。 (この時期疲れがピークだったようで、毎日何かしら数字をミスっていました。。。) メンバーに渡したのは、質問がいくつも書かれた白紙の MIRO 。 この質問リストの中から答えやすいものや、話せるネタがあるものをいくつか選んで書いてもらうようにしました。 参加者全員が自身の土俵で話をしてもらいたかったためです。 質問リストの作成には 100の質問ひろば というサイトが役に立ったような立たなかったような感じでした。 質問ネタに困ったら使ってみてください。 実際に書き込んでもらう際は、全体の流れがわかるようにこんな感じにしてました。 右側のFrameには事前に名前が記入されていて、各自そこに書き込みます。 中央付近のShuffle部分は発表順を決めるために利用しました。 付箋に参加者の名前を書いておき、発表前にMiroの Totally Random というアドオンを使ってシャッフルします。 多少はランダム性があったほうが楽しい気がするんですよね。 やってみて まず、全体としては、発表形式かつ発表順が事前にわかっているため「飯を食う時間の確保」「気持ち的な準備」ができるので進行が非常に楽でした。 発表者としては、事前に画像を用いて書き込めること、答える質問も自由に選定できるため、自分の言いたいことだけを言える楽さがありました。 また、Miroの機能でスタンプで発表にリアクションできるため、よりインタラクティブな場が生じたように感じます。 LT会形式の特徴 参加者それぞれの認識が浅いとき 安定して進行させたいとき こんなときはLT会形式が良さそうです。 進行にランダム性を廃している点と、自己紹介になっている点で、より「人を知ること」にフォーカスした形式となりました。 PJ始まりたてで、自己紹介などからやる必要がある際には、こういった形式のほうがより解像度高くその人を知ることができて良さそうです。 逆に、すでに心理的安全性が保たれていて、話しやすい状況になっている場合、 この形式では対話は生まれにくいので、機会としてはもったいないかもしれません。 まとめ コロナ禍においてPJのあり方や企業への勤め方も大きく変わりました。 また、働き方も多様になり、弊社でもリモート前提で入社される方も増えてきています。 そうすると、対面であっても対面でなくても、情報や心理的の格差がなくパフォーマンスを発揮できるチームビルビルディングが必要性が増していくでしょう。 この時代の流れは今後も続くと思うので、多様性を維持して働きやすい環境というのは、どの会社でも直面している課題なのではないかと思います。 この記事をここまで読んでいただいたみなさまは、きっといろいろな工夫をされていると思うので、 「こんな工夫してるよ!」というのがあればぜひお話を聞きしたいです。 @naoto bando まで気軽にお声がけください。 以上です。明日のアドベントカレンダーはDataStrategyチームの杉さんの記事です。お楽しみですね!
アバター