エクサウィザーズ Engineer Blog

株式会社エクサウィザーズのエンジニアチームブログ

Kaggle「chaii - Hindi and Tamil Question Answering」コンペで2位入賞したお話 & 解法解説

こんにちは、エクサウィザーズで自然言語処理ギルドに所属している神戸です。(ギルド制についてはこちら

今回、AI/機械学習を用いたデータ分析技術の国際的なコンペティションプラットフォームKaggle上で2021年8月 ~ 2021年11月まで開催されていたchaii - Hindi and Tamil Question Answeringコンペ(略: chaiiコンペ)に参加し、私を含む "tkm kh" というチームで943チーム中2位に入賞&金メダルを獲得することが出来ました。

f:id:kambehmw:20211207115920p:plain
Private Leaderboardの結果

同時に今回の金メダルの獲得で私(kambehmw)はKaggle Competitions Masterに、チームを組んでくださったtkm2261さんはKaggle Competitions Grandmaster(GM)に昇格しています。tkm2261さんは、さすがGMという実力の方でコンペ中色々なことを学ばさせていただきました。チームを組んでいただきありがとうございました!

この記事では、今回のコンペでの私たちのチームの解法について紹介させていただきたいと思います。 また、KaggleのDiscussionに既にチームの解法については共有されておりますが、こちらも必要に応じてご参照していただければと思います。 https://www.kaggle.com/c/chaii-hindi-and-tamil-question-answering/discussion/287917

解法の要点

chaiiコンペで精度に大きく寄与した工夫は以下の3点になります。コンペのタスク概要を説明した後、以下の3点について、それぞれ順に詳細に説明いたします。

  1. 外部データの利用
  2. アンサンブル(XLM-R, Rembert, InfoXLM, MuRIL)
  3. デーヴァナーガリー数字 (०१२३४५६७८९)の後処理

コンペのタスク概要

今回のコンペのタスクはいわゆるオープンドメイン質問応答タスク(Open-Domain Question Answering)であり、以下の入出力のペアが与えられているデータセットになっていました。 質問文とコンテキスト文は対応するペアのものが与えられている設定で、コンテキスト文を参照することで質問文に対応する解答を予測することが求められていました。

入力

  • 質問文(Question)
  • コンテキスト文(Context)

出力

  • 質問に対するコンテキスト文の解答範囲(Answer Span)

f:id:kambehmw:20211207171110p:plain
質問応答タスク イメージ図

また、データセットの言語は、ヒンディー語とタミル語となっており以下のような難しさがありました。

  • 英語のようなリソースが多い言語に比べて利用できる外部データ、学習済みモデルが少ない
  • 読むことができない言語のため、EDAやエラー分析が大変
    • Google翻訳で適宜翻訳して、EDAやエラー分析を実施した

コンテキスト文は、ヒンディー語 or タミル語のWikipedia記事であり、そのため質問もWikipediaに書かれるような一般的な知識を問うようなものになっていました。

コンペのスコア評価はword-level Jaccard scoreによって計算されました。 具体的な実装はコンペの評価ページより以下になっています。

def jaccard(str1, str2): 
    a = set(str1.lower().split()) 
    b = set(str2.lower().split())
    c = a.intersection(b)
    return float(len(c)) / (len(a) + len(b) - len(c))

補足:オープンドメイン質問応答タスク

ちなみに、オープンドメイン質問応答タスクは以下の3つの枠組みのいずれかで解くことが最近主流となっています。

f:id:kambehmw:20211207123440p:plain

引用: https://lilianweng.github.io/lil-log/2020/10/29/open-domain-question-answering.html

今回のコンペでは解答が書かれているコンテキスト文は正解のものがあらかじめ与えられている設定でしたが、解答に関連したドキュメントも検索する必要がある場合は図のRetriverに相当したコンポーネントも実装する必要があります。(今回コンペでは、左下のReader部分のみ実装すれば良かった)

Retrieverは質問に解答するのに必要な情報をExternal Knowledge(例: Wikipedia)から抽出します。External KnowledgeがWikipediaの場合、RetrieverはBM25やDPR[1]といった手法を使用して質問に関連したドキュメントを抽出するのが最近の論文で行われることが多いです。

また、解答範囲の位置を予測するReaderではなく、質問文&コンテキスト文を入力して解答をテキスト生成的に予測するRetriever-Generator[2]の手法や、質問文のみを入力に直接解答を予測してしまうGenerator[3]の手法なども提案されています。Generatorの手法としてはT5などのパラメータを非常に多く持った大規模言語モデルが使用されており、これらのモデルにおいてはExternal Knowledgeを参照せずとも解答に必要な情報をある程度のレベルまで記憶していることが報告されています。

外部データの利用

外部データの利用として、私たちのチームは以下の2つの外部データを利用しました。

  • MLQA[4]
  • TyDi QA[5]に含まれる他のインドの言語を入れる(ベンガル語とテルグ語)

MLQAには下の画像のように7つの言語のデータがありますが、このうちヒンディー語(hi)のデータを一緒に学習することで精度向上しました。

f:id:kambehmw:20211207152838p:plain

また、TyDi QAは英語以外に10種類の言語のデータがあり、このうちのベンガル語とテルグ語のデータを一緒に学習することで精度向上しました。 特にTyDi QAと学習することで、Public Leaderboardのスコアは0.787 -> 0.799に改善しました。

推測にはなりますが、TyDi QAで精度改善した理由を私たちは以下のように考えています。

  • 同じインドで使用されている言語のため類似性がある
  • TyDi QAのデータと質問が重複している

f:id:kambehmw:20211207153627p:plain

アンサンブル(XLM-R, Rembert, InfoXLM, MuRIL)

chaiiコンペの前にあったCommonLitコンペの上位解法から、NLPコンペでアンサンブルは重要だと考えていました。 以下が私たちのチームが使用したモデルになります。モデルは全てlargeサイズを使用しました。

また、アンサンブルに関連した情報は以下です。

  • AlexKay/xlm-roberta-large-qa-multilingual-finedtuned-ruのモデルはロシア語の質問応答データセットでfine-tuningされたモデルなのですが、アンサンブルに寄与しました。
  • モデルを上から順にアンサンブルしていくと、次のように次第にスコアが改善しました。(0.799 -> 0.816 -> 0.821 -> 0.827 -> 0.829)
  • モデルごとにトークンの分割が異なるので、charレベルで予測の出力を合わせてsoftmaxで値を正規化するという操作を入れました
  • シード値を変えてモデルを学習したランダムシートアンサンブルもしています

デーヴァナーガリー数字 (०१२३४५६७८९)の後処理

後処理として、選択された解答の文字列がデーヴァナーガリー数字 (०१२३४५६७८९)だけで構成されていた場合にアラビア数字 (0-9)に置き換えるという処理を入れました。 これは、年(year)が解答になっている質問に対してアノテータがアラビア数字を使ってアノテーションするように一致していたからのようです。 後処理なしのスコアが0.806で、後処理によって0.02ほどの改善をしました。

後処理の実装の詳細について知りたい方は、以下のコードをご参照ください。 https://www.kaggle.com/tkm2261/tkm-tydi-rem-info-muril-xtream-xquad

Cross Validationについて

ローカルでのCross Validationの評価についてはチームで何もせず、Public Leaderboardのスコアに対してチューニングをしていきました。ローカルで評価をしなかった理由は以下になります。

  • Cross ValidationとPublic Leaderboardのスコアに全然相関がなかった
  • trainデータのアノテータは1名、testデータのアノテータは3名で行なったとDataページに説明があったので、trainデータの方にはノイズがありCross Validationの信頼性が低いと推測できた
    • データを目視確認すると確かにアノテーションのブレがあった。

Public Leaderboardのスコア変化

  • 0.787: チームマージ
  • 0.799: TyDi QA (ベンガル語とテルグ語)を用いて学習
  • 0.816: RembertとInfoXLMをアンサンブル
  • 0.821: より多くのXLM-R, Rembert, InfoXLMモデルをアンサンブルに追加
  • 0.827: アンサンブルの重みをチューニング
  • 0.829: MuRILモデルをアンサンブルに追加
  • 0.831: 後処理のパラメータチューニング

順位変動 (Shake) について

今回のchaiiコンペはPublicとPrivate Leaderboardの順位変動が大きかったコンペでした。どの程度順位変動があったかをプロットしてくださったNotebookがありましたので参考までに示します。

f:id:kambehmw:20211207171930p:plain
PublicとPrivate Leaderboardの順位変動
引用: https://www.kaggle.com/c/chaii-hindi-and-tamil-question-answering/discussion/287960

左上のPublic Leaderboard 550位くらいから、Private Leaderboardで金圏に入られていた方もいたので比較的順位変動が大きくあったコンペだったと言えるのではないでしょうか。 私たちのチームがShakeしなかった理由は以下であると個人的に考えています。

  • アンサンブルで異なるモデルを使うことで多様性を上げることができた
  • 外部データを活用することで、データ量を増やすことができた
  • testデータの方がラベルが正確だと信じることができ、Trust LB(Leaderboard)戦略が今回のコンペでは正解だった

精度に効かなかったこと

chaiiコンペを振り返って

今回のコンペは業務であまり経験のない質問応答というタスクでしたが、コンペを通じて多くの知見を得ることができました。コンペデータとは異なる言語データを含めて学習することで精度改善が見られたので、日本語についても言語的に近い他言語のデータを活用することで精度改善に役立つのではと思いました。また、RemBertやInfoXLMといった今まで試したことがなかったマルチリンガルモデルも精度的に役立つことがわかりました。コンペを通じて得られた知見を今後の業務に活かしていきたいと思います。

今回のコンペでKaggle Competitions Masterになることはできましたが、引き続き技術力を高めていくことでエクサウィザーズのビジネスにより一層貢献することを目指していきたいと思います。

エクサウィザーズでは一緒に働く人を募集しています。中途、新卒両方採用していますので、興味のある方は是非ご応募ください! hrmos.co event.exawizards.com

参考文献

Kaggle初参加振り返り〜Shopeeコンペでソロ銀メダル〜

こんにちは。MLエンジニアの川畑です。
今回は、以前から気になっていながらも、中々参加の一歩が踏み出せなかったKaggleについに参加したところ、幸いにも 46th / 2426チームで銀メダルを獲得しましたので、初参加を振り返りたいと思います。 なお、本記事では上位陣の解法の詳細は紹介しませんので、ご興味がある方はKaggleのコンペサイトに投稿されている解法を参照ください(https://www.kaggle.com/c/shopee-product-matching)。

f:id:k_kawabata:20210601120005p:plain

前提

  • 取り組んだ時間
    • 本コンペに参加し始めたのはコンペ終了まで残り1ヶ月を切った頃で、基本的には業務終了後から就寝までの間で平均2時間程度、週4〜5日程度Kaggleに時間を割いていました。自分は休日にダラダラするのが好きなので、どちらかというと平日の方が取り組んでいました。また、幸いなことにコンペ開催終盤にはGWが重なったので、そのタイミングで比較的時間を費やすことができました。
  • 実験環境
    • 私は自前のGPUを持っていないため、Kaggle notebookとGoogle Colabのみで実験を回しました。1週間に利用できる時間や連続で利用できる時間などに制限があるため、大量の実験や時間のかかる実験を回す必要があるコンペでは、これらの無料リソースだけだとかなりしんどいのではないかと思います。また、Google Colabでは毎回データをコピーしてこないといけなかったり、1日ガッツリ利用すると使用量上限に達し、次の日に制限がかかって利用できなかったりと、少々使いづらい部分もありました。とはいえ、無料でGPUを利用できるのは非常にありがたいことです(有料のGoogle Colab Proを使えばもう少し制限は緩和されます)。

コンペ概要

本コンペは、2021/03/08~2021/05/10にShopeeという東南アジアのeコマースプラットフォームが開催したもので、商品の画像とタイトルから同一商品を検索するという課題でした。

一般に小売企業は、自社の商品が最も安いことをお客様に保証するために、さまざまな方法を用いています。中でも、他の小売店で販売されている商品と同等の価格で商品を提供する「プロダクトマッチング」という手法があります。しかし、同じ商品であっても、小売業者によって使用する画像やタイトルなどが大きく異なることもあり、同じ画像や同じタイトルで単純にマッチングさせるだけでは不十分です。そこで機械学習を使ってマッチングの精度を向上させたい、というのが本コンペのモチベーションと考えられます。実際に、Shopeeでも掲載されている何千もの商品に対して「最低価格保証」機能を提供しており、このコンペでのアプローチが活用されることが想定されます。

提供データ

学習データには34250件の商品が含まれ、テストデータには70000件程度の商品があると記載されていました。

  • posting_id : 投稿ID(各投稿にユニークなID。商品が同じでも投稿者が違えば異なるIDになる)
  • image: 商品画像
  • image_phash: 商品画像のperceptual hash(これは画像を64bitのハッシュ値に変換したもので、同じ画像からは同じ値、また似た画像からは似た値が得られます。ちなみにモデルを作成する上で、この情報は使用しませんでした)
  • title: 商品タイトル(英語とインドネシア語が混ざっていた)
  • label_group: ラベルグループID(これが同じものが同一商品とみなされる)

f:id:k_kawabata:20210601120341p:plain
提供データの例

評価指標

  • F1-score
    • 予測対象は、対象商品と同一の全ての商品の投稿ID(posting_id)。ただし,同一商品には必ず自分自身を含み,上限は50個。
    • 各行(各商品)に対してF1-scoreを計算し、それを全体で平均したもの

以下はサブミッションファイルの例です。各クエリ商品に対して、その商品とマッチする商品の投稿ID全てをスペース区切りのリストで与えます。マッチする商品には必ず自分自身を含むため、matchesの列には、クエリ商品自身の投稿IDが含まれます。以下の例では、 test_123は自分以外にマッチする商品がなく、 test_456は自分以外に test_789とマッチすることを意味します。

posting_id,matches
test_123,test_123
test_456,test_456 test_789

自分の解法

f:id:k_kawabata:20210601120545p:plain
予測パイプライン図

上が私の解法のパイプライン図です。大きな流れとしては、画像とテキストそれぞれでモデルを作成し、それぞれから得られた埋め込み表現を元にkNNでマッチング商品を予測し、最後に和集合をとる、というものです。上記パイプライン図におけるTF-IDF以外の4つのモデルに対しては、損失関数としてMetric learning(距離学習)で使われるArcFace loss[1]を使用しました。距離学習について簡単に説明すると、埋め込み空間において、同じクラスのものはなるべく近づけ、異なるクラスのものはなるべく遠くになるように埋め込み表現を学習する手法です。ArcFaceは、本コンペと類似の過去コンペでも有用性が示されており、本コンペでも参加者の多くがこの手法を用いていたと思われます。したがって、ArcFace自体の利用は差別化ポイントではないため、本記事では詳細は省かせていただきます。

画像モデル

以下の3つをバックボーンとして使用し、損失関数にはArcFaceを用いることで3つの埋め込み表現を得ました。それぞれのモデルから得られる埋め込み表現の次元は512次元です。

  • RegNetY
  • EfficientNet-B3
  • NFNet-L0

様々なバックボーンを使った結果が公開ノートブックに挙げられていましたが、それらのスコア差は比較的小さく、バックボーンの選択自体は本コンペにおいて優先度は高くないと判断し、ほとんど試行錯誤はしていません。しかし、細かいスコアアップにはもちろん繋がるため、より上位を目指す上ではもう少し拘っても良かったかもしれません。余談ですが、上位陣の解法でVision Transformerの一種である Swin Transformer [2]を使っているケースもいくつか見られたので、画像コンペにおいてもすでにTransformerが威力を発揮していることが驚きでした。

テキストモデル

以下の2つをテキストのモデルとして使用しました。画像モデル同様にあまり事前学習モデルの選択には時間を掛けていません。

  • Paraphrase-XLM-multilingual [3]
  • TF-IDF

商品タイトルには英語とインドネシア語が混ざっていたため、多言語で事前学習された言語モデル( Paraphrase-XLM-multilingual )を使用しました。他の参加者の解法を見ると、インドネシア語で学習された IndoBERT [4]を使用しているケースも多かったようです。
また、一般的な文章と比較して、商品のタイトルは単なる単語の羅列に近いため、TF-IDFのような単語をカウントして重み付けするだけのシンプルな手法でも有効だったと考えられます。実際にdiscussionなどを見ていても、多くの参加者がTF-IDFの有用性に言及していました。

以下では、公開ノートブックやdiscussionでは触れられていなかったものの、スコアアップに効果があったものをいくつか紹介します。

GeM pooling[5]

過去に行われた画像検索コンペのGoogle Landmark Retrieval 2019における1位の解法[6]を参考に、pooling層にGeneralized-mean (GeM) pooling(パイプライン図の緑色部分)を使用しました。pooling層の出力は以下の式で得られます。

\displaystyle{\mathbf{f}^{(g)}=[f_1^{(g)}...f_k^{(g)}...f_K^{(g)}]^{\top}, \quad f_k^{(g)}=\left(\dfrac{1}{|\mathcal{X_k}|}\displaystyle\sum_{x\in{\mathcal{X_k}}}x^{p_k}\right)^\frac{1}{p_k}}

ここで、\mathcal{X}_{k}k番目の特徴量マップであり、k\in{{1,...,K}}です。パラメータp_kは学習も可能ですが、簡単のため、論文著者らがGithubに公開しているコード[7]のデフォルト値(p=3)を使用しました。なお、p_k\rightarrow\inftyの場合はmax pooling、p_k=1の場合はaverage poolingに対応します。ちなみに、GeMの効果は後述するDBAやGraph-based QEと比較すると非常に小さかったです。

Database Augmentation (DBA)[8]

こちらもGoogle Landmark Retrievalの上位解法[9]を参考にしました。パイプライン図では、黄色部分になります。DBAは非常にシンプルな手法で、各商品の特徴量に対して近傍k個の特徴量との重み付き和を計算し、それを元の特徴量と置き換えるものです。ここでの特徴量とは、画像とテキストのモデルから獲得した埋め込み表現をPCAによって512次元に圧縮した後の埋め込み表現となります。

\displaystyle{\mathbf{x}_{new}=\sum_{i=1}^{k}w_i \mathbf{x}_i}

ここで、\mathbf{x}_iは元の特徴量からi番目に近い特徴量で、i=1は常に自分自身の特徴量です。パラメータkの値を大きくしすぎると、クエリと距離が遠い別の商品の特徴量も含めてしまうため、調節が必要となります。kの値はいくつかのパターンで試しましたが、k=2の場合が最も良い結果となりました。以下の図は横軸に自分を含めたマッチング商品数をプロットしたヒストグラムですが、この図から分かるように、マッチング商品数が2個の商品が最も多いことがk=2で最も良い結果になった理由と考えられます。つまり、同一商品が2個の商品に対してk\geqq3としてしまうと、異なる商品まで含めてしまうため、特徴量に悪影響を及ぼします。

f:id:k_kawabata:20210601122222p:plain
マッチング商品数に関するヒストグラム

重みに関しては、[9]を参考にして、w_1=1.0, w_2=0.67としました。ただ、この重みの決め方はクエリと最近傍商品の類似度を考慮できておらず、似た商品であろうと似ていない商品であろうと、近傍に対して固定の重みを掛けます。そこで、重みを固定値にするのではなく、類似度(の冪乗)を使うことでよりDBAの質が上がると考えられます。実装自体は簡単なのですが、なぜか私はここで面倒臭がってしまい、結局類似度を使った重み付けは行いませんでした。しかし、コンペ終了後に上位の解法で使われているのを見ると、やはりこれを行っていた方が良かったと後悔しました。

Graph-based QE

先に紹介したDBAと同様に、過去の類似コンペの上位解法でよく使われている手法にQuery Expansion (QE)[10]があります。これは、あるクエリに対して何らかの方法で新しい別のクエリを作成し、元のクエリと合わせて2つのクエリを使って検索をする手法です。この利点として、効果的な新しいクエリを追加できれば、元のクエリだけでは検索でヒットしなかった商品もヒットさせることできるようになります。では、どのように新しいクエリを作成するかというと、よく使われる方法としては、クエリの近傍k個の特徴量との平均を取るものや、類似度(のα乗)を使った重み付き平均などがあります。後者は、α-QE [5]と呼ばれ、\alpha=0の時普通の平均と一致します。

私は実装を簡単にするのと、計算時間を短縮するために、よりシンプルな手法を用いました(パイプライン図のオレンジ部分)。具体的には、新しいクエリを作成する代わりに、すでに同一商品と予測されている商品群を新しいクエリとみなしました。例えば、クエリQに対し予測によって以下のようなグラフが描けたとします。

f:id:k_kawabata:20210601122446p:plain
クエリQに対する予測のグラフ例

ここで、Qからエッジが引かれている商品(A, B, C)はQと同一商品と予測されたものです。この時、QDの間にエッジはありませんので、もしDQと同一商品だった場合は、Dを見逃してしまいます。ですが、すでにQと同一と予測されている商品Cを新しいクエリとみなすと、Dも予測結果に追加することが可能となります。クエリからの近傍k個をナイーブに新しいクエリに追加するのではなく、先に類似度に対してある閾値でスクリーニングを掛け、残ったもの(同一商品と予測されたもの)のみを新しいクエリとして追加することで、False positiveをなるべく上げないようにしているのがポイントです。

また、今回の解法の中でTF-IDFを使用していますが、TF-IDFのように単語の意味を考慮できない単純な特徴量では、ちょっとした特徴量の変化でも意味的には大きく変化することもあるため、近傍の特徴量を利用することはFalse positiveを増やし、逆効果となることが考えられます。これは、DBAも同様です。したがって、TF-IDFに対しては、DBAやGraph-based QEは使用していません。

上位解法との差分

以下では、自分の解法には含まれていないものの上位解法には含まれており、さらなるスコア向上のためには重要だったと考えられるテクニックについていくつかご紹介します。

2nd stageモデル

2位、3位、5位、10位のチームが2nd stageモデルを作成していました。これは、画像やテキストの埋め込み表現から類似度を計算し、ある閾値で同一商品を予測するのではなく、類似度や距離を元にもう一段階モデルを組んで最終的な予測とするものです。具体的には、同一商品の候補となる商品ペアに対して、それらの類似度や距離などを特徴量として、LightGBMやXGBoostなどで対象ペアが同一商品かどうかを予測します。テストデータには約70000件の投稿商品が存在し、ペアの組み合わせ数が膨大であるため、いかにこの処理を高速化するかが重要だったようです。高速化には、cumlのForestInference [11]というGPUを使って推論を行うライブラリが有用で、数十倍の高速化ができるようです。

画像とテキストの予測の組み合わせ

今回のコンペでは、画像での予測結果とテキストでの予測結果をいかに上手に組み合わせるか、という点も重要だったように思います。画像が似ている商品(下図の黄色領域)とテキストが似ている商品(緑色領域)だけではなく、画像とテキストがどちらもそこそこ似ている商品(青色領域)も予測に加えることでスコアが向上したようです。下の図の例では、クエリQに対して、自分自身を含む[Q, A, D, E, F, G]を予測結果とします。

f:id:k_kawabata:20210603143128p:plain
予測の組み合わせ(1位解法[12]から抜粋)

類似度を用いたDBAやQE

DBAやGraph-based QEの項でも触れましたが、クエリ近傍の特徴量に対して何らかの方法で重み付けして新しい特徴量を得る際には、固定の重みではなく類似度を利用する方がやはり精度は良くなるようです。DBAやQEを使っていたチームはほぼ全て類似度を使った重み付けをしていたのではないかと思われます。このやり方自体には気付いていたので、面倒臭がらずに実装していれば良かったと後悔しています。

Kaggle初参加を振り返って

今回、Kaggleに初めて参加しましたが、2、3年ほど前からKaggleの存在自体は知っていました。ただ、業務が忙しくKaggleに割く時間がない、と自分の中で勝手に言い訳をして、長いこと参加してきませんでした。しかし、業務で用いるアプローチが、すでに自分が知っているものや過去に使ったことのあるものばかりであることに気付き、もっとアプローチの幅を広げたいという思いから、最新技術に触れることができるKaggleに参加することにしました。私は普段、テーブルデータを扱うことが多いため、画像やNLPの技術も使えるようになりたい、という思いもありました。今回Shopeeコンペを選んだのもそのためです。

序盤は、初参加でしかもソロ参加だったため、勝手が分からず、「Submission CSV Not Found」を連発し、苦労しました。ただ、参加したのがコンペ終了まで残り1ヶ月を切った頃で、すでに公開コードやディスカッションが充実していたため、取り掛かりやすく、タイミング的には良かったと思っています。一方で、他の参加者(特に上位陣)と比較すると、自分は実験の数が圧倒的に足りなかったなと思いました。より高いスコアを目指す為には、公開されているノートブックやdiscussionなどの情報を単に鵜呑みにするのではなく、問題の本質を見抜く為に自分の頭でしっかり考え、データを確認し、その上で多くの実験を繰り返すことが重要だと感じました。

実際に参加してみて思ったのは、Kaggleは世界中のデータサイエンティストが自分たちの知識や技術を惜しげもなく共有し、機械学習の知見を深めるには非常に効果的な場であるということです。今はまだ、恩恵にあずかるだけですが、いつか自分もこのコミュニティに知見を還元できるようになりたいと思いました。次回参加する際には、ぜひ金メダルを獲得したいです。

参考リンク

[1] https://arxiv.org/pdf/1801.07698.pdf
[2] https://arxiv.org/pdf/2103.14030.pdf
[3] https://arxiv.org/pdf/2004.09813.pdf
[4] https://arxiv.org/pdf/2011.00677.pdf
[5] https://arxiv.org/pdf/1711.02512.pdf
[6] https://www.kaggle.com/c/landmark-retrieval-2019/discussion/94735
[7] https://github.com/filipradenovic/cnnimageretrieval-pytorch/blob/master/cirtorch/layers/pooling.py
[8] https://arxiv.org/pdf/1610.07940.pdf
[9] https://www.kaggle.com/c/landmark-retrieval-challenge/discussion/57855
[10] https://www.robots.ox.ac.uk/~vgg/publications/papers/chum07b.pdf
[11] https://medium.com/rapids-ai/rapids-forest-inference-library-prediction-at-100-million-rows-per-second-19558890bc35
[12] https://www.kaggle.com/c/shopee-product-matching/discussion/238136

【連載】時系列データにおける異常検知(2)

こんにちは、機械学習エンジニアの福成です。

前回は連載の第一回目ということで、時系列異常検知の基本的な考え方や、タスクの種類についてお話ししました。

techblog.exawizards.com

前回のポイントを再掲します。

  • 異常検知では、明示的な正解ラベルを学習に用いない教師なし学習が主流である。
  • 時系列中に2つの区間を設け、その中でモデル化を行いつつ区間をスライドさせるのが基本的な考え方である。
  • 区間の長さにより、大きくは外れ値検知・変化点検知に分けられる。
    • 両者ともに、時系列依存の有無の観点で分けることも可能である。

今回は外れ値検知・変化点検知について、より具体的なアプローチについて述べていきたいと思います。 また両者のタスクに関して時系列依存する・しない場合についても、それぞれ述べたいと思います。

外れ値検知

時系列依存しない場合

時系列依存しない外れ値検知では、参照区間の統計量を基にする方法が考えられます。 例えば「評価区間の値 > 参照区間の平均値×α となった場合、評価区間の値が異常である」といったものです。 これにより、参照区間の平均から極端に上振れしたものを異常として捉えることができます。シンプルすぎると思われるかもしれませんが、問題設定(何を異常とするか?)によってはこれだけで充分検知できる場合も多いです。 またαはハイパーパラメータのようなもので、分析者が設定します。値が小さいほど検知の感度は高くなります。

上記のロジックに加えて「ばらつき」の考え方を取り入れたい場合は、「評価区間の値 > 参照区間内の平均値 + 参照区間内の標準偏差×α となった場合、評価区間の値が異常である」といったロジックも考えることができます。 このようにすることで、例えば参照区間のばらつきが大きい場合は異常と判定させにくくするといったコントロールができるようになります。

このばらつきも考慮したロジックを用いて、下記の実験用の時系列データに対して外れ値検知を行うと、以下のような結果になりました。 参照区間の幅は90、α=4としています。

f:id:t-fukunari:20210520234939p:plain

赤丸はモデルが異常として検知した点です。値が跳ね上がっているところについて、漏れなく検知できていることがわかります。

時系列依存する場合

前回でも述べましたが、時系列依存する場合は時系列予測系のモデルを用いる方が適しています。 参照区間でフィッティングを行ったモデルで評価点を予測し、得られた予測値と実測値との誤差が大きい場合に異常と判定するやり方です。 例えば、「(予測値-実測値)の2乗を異常度とし、その異常度が閾値を超えた場合に異常とする」といった方法です。

この方法は参照区間と評価区間をずらしながら都度モデルを作るので計算コストが大きくなるという難点があります。 これを解消するために、近年ではオンライン忘却アルゴリズムを備えたChangeFinderが登場しています。 パラメータの計算をオンライン処理で行うので、計算時間が比較的少ないのが特徴です。

変化点検知

時系列依存しない場合

時系列依存しない変化点検知では、参照区間と評価区間の統計量を用いる方法が考えられます。 例えば「評価区間内の平均値 > 参照区間内の平均値 + 参照区間内の標準偏差×α となった場合、参照区間と評価区間の間が変化点である」といったものです。 値のベースラインが変わるような「明らかにわかる変化」に対しての検知に適しています。

このロジックを用いて、下記の実験用の時系列データに対して変化点検知を行うと、以下のような結果になりました。 参照区間の幅は90、評価区間の幅は7、α=2.5としています。 f:id:t-fukunari:20210520235012p:plain

緑の線は、モデルが検知した変化点です。 値が全体的に増えた箇所に対して検知できていることが確認できます。

時系列依存する場合

ここではさらに2通りの方法が考えられます。

再構成誤差

ひとつは、AutoEncoderの再構成誤差を用いる方法です。参照区間の時系列を再構成するように学習を行うことで、 推論時において再構成誤差が大きくなるような時系列は異常であるという考え方です。 前回の記事でも少し紹介した、参照区間を固定して評価区間のみスライドする方法になります。

以下では、学習フェーズと推論フェーズの2つに分けてアプローチを述べていきます。

学習フェーズでは、参照区間の時系列データに対して部分時系列を作成します。ウィンドウをずらしながら細切れの時系列を作成するイメージです。 この部分時系列を入力として、同じものを出力させるようにAutoEncoderで学習させます。 このとき、十分な学習データを得るために、参照区間は充分長めに取る必要があることに注意してください。

f:id:t-fukunari:20210519203258p:plain

推論フェーズでは、評価区間内の時系列を学習済みモデルに入力して、再構成誤差を計算します。 これを評価区間をスライドさせながら繰り返し行うことで、各区間における再構成誤差が得られます。 そして、設定した閾値を再構成誤差が超えたときに変化点検知を行います。

f:id:t-fukunari:20210519204748p:plain

潜在ベクトル + 外れ値検知

もう一つは、参照区間内の複数の部分時系列を次元圧縮した潜在ベクトル群と評価区間を次元圧縮した潜在ベクトルを比較して、「外れ値検知」的に検出する方法です。 以下ではAutoEncoderを用いて得られた潜在ベクトルを用いたアプローチを紹介します。 学習フェーズと推論フェーズの2つに分けて述べていきます。

学習フェーズでは、先ほどの再構成誤差の場合と同様に時系列データに対して部分時系列を作成し、AutoEncoderで学習させます。 ただ先ほどと異なるのが、参照区間等を意識せずに過去すべての時系列データで学習させる点です。 変化点以後のデータの特徴も抽出できるモデルである必要があるためです。

ここで興味があるのはこの中間にある潜在ベクトルです。 部分時系列の特徴が圧縮されたベクトルであると考えられるためです。 「圧縮」なので、ここでは、部分時系列の長さを潜在ベクトルの次元数よりも大きくする必要があります。

推論フェーズでは、上記で学習したEncoderを用います。 参照区間内で複数の部分時系列が得られるので、それらをこのEncoderに入力して複数の潜在ベクトルを得ます。 そして評価区間内の部分時系列からも同様に潜在ベクトルを得ます。 評価区間から1つの潜在ベクトルを得るため、ここでは、評価区間の長さ=部分時系列の長さにする必要があります。

あとは、潜在ベクトル空間においてk近傍法やOne-Class SVMなどで外れ値となるような評価区間を検知することで、最終的に変化点検知を行います。 潜在ベクトルで外れ値検知を行うので、区間の長さの目安としては、参照区間の長さが評価区間の長さの10倍以上になっている必要があります。

f:id:t-fukunari:20210519213751p:plain

今回のまとめ

今回はここまでです。ポイントを下記にまとめます。

  • 時系列依存しない外れ値検知では、参照区間の統計量を用いる。
  • 時系列依存する外れ値検知では、ChangeFinderのような時系列予測モデルを用いる。
  • 時系列依存しない変化点検知では、参照区間・評価区間の統計量を用いる。
  • 時系列依存する変化点検知では、再構成誤差を用いるか、潜在ベクトルに対する外れ値検知を行う。

参考文献

おわりに

エクサウィザーズは優秀なエンジニア、社会課題を一緒に解決してくれる魔法使い”ウィザーズ”を募集していますので、ご興味を持たれた方はぜひご応募ください。
採用情報|株式会社エクサウィザーズ

ExaWizards Engineer Blogでは、AIなどの技術情報を発信していきます。ぜひフォローをよろしくお願いします!
Linkedinもどしどしフォローお待ちしています!

Continuous delivery of iOS using GitHub Actions + Fastlane, complete on GitHub

f:id:tadashi-nemoto0713:20210224120254p:plain

The Japanese version of this blog post can be found here:

techblog.exawizards.com


Hello, I'm Tadashi Nemoto from the Platform Engineering team(previously DevOps team).

In the last article, I introduced how to improve an API / Frontend deployment flow using GitHub Actions + GitLab Flow.

techblog.exawizards.com

I also improved the continuous delivery process of an iOS app by using GitHub Actions, and I would like to introduce it in this article.

I believe this continuous delivery can be applied not only to iOS, but also to Android and other multi-platform frameworks such as Flutter.

Adoption of Git Flow・Brief explanation of Git Flow

In the last article, I mentioned that we adopted GitLab Flow for our API / Frontend deployment flow and branching strategy.

However, I thought that GitLab Flow or GitHub Flow would be incompatible with our mobile release process because of the following factors:

  • We need to update the version number for each release.
  • A review from Apple / Google is needed for each release, so we cannot release all the time

On the other hand, some companies use trunk-based development as the release flow for mobile apps.

However, trunk-based development more suitable for relatively large-scale app development and deployment schedules, such as releasing once a week, and I thought that it did not match the current state of our mobile development.

For the above reasons, I chose Git Flow, for our mobile app deployment flow branching strategy.

f:id:tadashi-nemoto0713:20210118184341p:plain


To make the rest of this explanation easier to understand, let's take a brief look at Git Flow.

f:id:tadashi-nemoto0713:20210118184235p:plain

① Create a feature branch from the develop branch when you add a new feature. When its task and review are done, merge this to the develop branch.

f:id:tadashi-nemoto0713:20210316161849p:plain


② Create a release branch from the develop branch when it's ready for preparing the release.

f:id:tadashi-nemoto0713:20210212164923p:plain

③ After some checks and modifications on the release branch, merge this to the develop・master branches.

f:id:tadashi-nemoto0713:20210212182725p:plain

④ After merging the release branch to the master branch, add a tag and release to production (For mobile development, submit the app to Apple Store Connect)

f:id:tadashi-nemoto0713:20210212182646p:plain

⑤ If there are fatal bugs after release, hotfix release. First, create a hotfix release branch from the master branch.

f:id:tadashi-nemoto0713:20210212171006p:plain

⑥ When you have finished fixing and checking on the release branch for the hotfix and are ready to release, merge it into the master・develop branch and release it again to the production environment.

f:id:tadashi-nemoto0713:20210317122731p:plain

In the next section, I'll explain how we achieved continuous delivery within this Git Flow.

Continuous Delivery of iOS using GitHub Actions + Fastlane

Create release branch and Pull Requests

When the development on the develop branch described in ① has progressed and you are ready to release, you can create the release branch described in ②.

f:id:tadashi-nemoto0713:20210212164923p:plain
Create release branch

Release branches are often created manually, but in this case, I was able to automate the process by using GitHub Actions + Fastlane.

First of all, we want to use Semantic Versioning to specify the release version.

With Semantic Versioning, we can regularize the versioning of releases by a Major, Minor, and Patch version increase. (I won’t explain Semantic Versioning in-depth here, as it's already explained well on other websites).

f:id:tadashi-nemoto0713:20210305142013j:plain:w200
Semantic Versioning

Fastlane provides an Action called increment_version_number and it increments the version number bypassing parameters(Major, Minor, Patch).

And below Fastlane will do the followings:

  • Increment the version number based on Semantic Versioning and commit files.
  • Create a release branch and push to GitHub
  • Create a Pull Request to the master and develop branches

f:id:tadashi-nemoto0713:20210315131519p:plain

Currently, increment_version_number only supports iOS, the below article describes how to increment version number automatically for Android.

Automating semantic versioning model in mobile releases | ThoughtWorks

Finally, we'll make this Fastlane run via GitHub Actions

f:id:tadashi-nemoto0713:20210312182827p:plain

There are several types of workflow triggers in GitHub Actions, but in this case, we will use workflow_dispatch, which can be triggered manually from the GitHub Action UI, since we want to be able to run at any time.

Events that trigger workflows - GitHub Docs

Also since workflow_dispatch also accepts arguments, we can pass Major or Minor for this upgrade (Patch is only used for hotfix releases, so we won't use it here).

f:id:tadashi-nemoto0713:20210314171333p:plain
workflow_dispatch on GitHub Actions

By doing this, I was able to automate the process of updating the release version and creating release branches and pull requests by triggering the workflow by specifying Major or Minor in the GitHub Actions UI.

f:id:tadashi-nemoto0713:20210312183859p:plain
Pull Requests to master・develop branches

f:id:tadashi-nemoto0713:20210302224859p:plain
File diff for version updates

Merging two Pull Requests at the same time

In the previous step, two pull requests were automatically created from the release branch to the master・develop branches.

Then, as explained in section ③, when the release branch is ready to be released, you can merge it into the master・develop branches.

f:id:tadashi-nemoto0713:20210212182725p:plain

Of course, you can merge the two pull requests manually, but there is a possibility that you might forget to merge one of them.

To merge the two pull requests at the same time without forgetting, I created the following GitHub Actions workflow.

f:id:tadashi-nemoto0713:20210312181035p:plain

This workflow allows you to merge two pull requests for the develop and master branches at the same time if you add the label release to the pull request.

f:id:tadashi-nemoto0713:20210314132857p:plain

Creating a Tag & GitHub Release, submit App Store Connect

When the release branch is merged into the develop・master branches and a new commit is made in the master branch, you can create a tag and release it to the production environment as described in ⑤ in Git Flow. In the case of iOS development, this is often the time to submit to App Store Connect.

f:id:tadashi-nemoto0713:20210212182646p:plain

Creating a Tag & GitHub Release triggered by a commit to the master branch can be automated with the following GitHub Actions.

f:id:tadashi-nemoto0713:20210314142612p:plain

f:id:tadashi-nemoto0713:20210314172613p:plain
Creating Tag & GitHub Release

We will also build and upload the release version of the app to App Store Connect at this time.

f:id:tadashi-nemoto0713:20210314172439p:plain

Here we are utilizing Fastlane's Deliver Action.

The Deliver Action allows you to not only upload to Apple Store Connect, but also upload metadata screenshots, submit, and automatically release after approval.

Hotfix release

The above is the normal release flow.

In addition to the regular release flow, we will conduct a hotfix release if a serious bug is found after the release.

Follow the steps ⑤ and ⑥ to create a release branch for the hotfix from the master branch, and then merge it into the master/develop branch for release.

f:id:tadashi-nemoto0713:20210212171006p:plain

When we do a hotfix release, we follow Semantic Versioning and update only the Patch part (x.x.0 → x.x.1).

f:id:tadashi-nemoto0713:20210305135019p:plain

Then, in GitHub Actions, checkouts from the master branch create a release branch and Pull requests.

f:id:tadashi-nemoto0713:20210314145833p:plain

Then, after the fix and confirmation on the hotfix release branch is done, you can do the same thing as a normal release

  • Add the label release to the Pull Request and merge it into the master・develop branch.
  • After the commit is made on the master branch, a Tag & GitHub Release is automatically created, and the release version app is submitted to App Store Connect.

【Optional】Distribute debug app (Firebase App Distribution)

Although not directly related to Git Flow, I will also briefly explain how to use GitHub Actions to distribute debug apps.

To check the behavior of the application before release, we often use the following services to distribute and check the debug version of the application.

You can automate the building and uploading of the debug version of your app using GitHub Actions.

With Git Flow, you can trigger it when you finish feature development on the feature branch and merge it into the develop branch, or when you commit to the release branch.

f:id:tadashi-nemoto0713:20210212182754p:plain

The workflow in GitHub Actions is as follows.

f:id:tadashi-nemoto0713:20210305134944p:plain

In the above workflow, we have a workflow_dispatch as a trigger, in addition to committing to a specific branch.

When you are developing a feature in ①, you may often find yourself in a situation where you don't want to merge it into the develop/release branch yet, but you want to build and distribute a specific feature branch.

f:id:tadashi-nemoto0713:20210317125311p:plain

You can specify the branch and trigger workflow in GitHub Actions page, using workflow_dispatch.

f:id:tadashi-nemoto0713:20210212184657p:plain
workflow_dispatch

Summary

Finally, I'll summarize the steps I've taken so far in using GitHub Actions for iOS continuous delivery.

【Normal release flow】

  1. Create a feature branch, merge it into the develop branch

  2. When you are ready to prepare the next release, go to the GitHub Actions page and trigger the workflow for preparing the next release. You should choose the bump type parameter(major or minor). f:id:tadashi-nemoto0713:20210314171333p:plain

  3. GitHub Actions checks out the develop branch, bumps the version based on the parameter, creates a release branch, and creates Pull Requests to develop・master branches. f:id:tadashi-nemoto0713:20210312183859p:plain

  4. At this time, the debug version of the app will be built and distributed to Firebase App Distribution, so you can check its behavior with actual devices.

  5. When it is time to release, Add the label release to Pull Request and merge it into the develop・master branches at the same time. f:id:tadashi-nemoto0713:20210314132857p:plain

  6. Commits are made to the master branch, automatic Tag & GitHub Release creation, and submission to App Store Connect. f:id:tadashi-nemoto0713:20210314172613p:plain


【Hotfix release flow】

  1. When you're ready to prepare a hotfix release, run the hotfix release preparation workflow from the GitHub Actions UI
  2. Update the release version (Patch), create a release branch, and create Pull Requests to the master・develop branch.
  3. Commit the bug fix to the above release branch.
  4. Each commit to the release branch automatically builds a debug version of the app and distributes it to Firebase App Distribution, so you can check how it works on an actual device.
  5. Once the fix is confirmed, Add the label release to Pull Request and merge it into the develop and master branches at the same time.
  6. Commits are made to the master branch, and a Tag and GitHub Release are automatically created and submitted to App Store Connect.

This has allowed us to complete most of the work required for the release on GitHub! (Depending on how much of the work to Apple Store Connect is automated by Fastlane's Deliver action.)

While releases in mobile app development tend to be less frequent than API and front-end development, there is a lot of work that needs to be done for a release.

With this continuous delivery process, I hope to become:

  • Able to release app improvements to the market with more agility
  • More focused on product development itself

hrmos.co

References

GitHub 上で完結する、GitHub Actions + Fastlane をフル活用した iOS の継続的デリバリー

f:id:tadashi-nemoto0713:20210224120254p:plain

Platform Engineer (旧 DevOps Engineer) の 根本 征 です。

前回は GitHub Actions + GitLab Flow を使った API / Frontend のデプロイフローの改善について紹介しました。

techblog.exawizards.com

iOS の継続的デリバリーも GitHub Actions を活用して改善することができたので、今回はその内容を紹介したいと思います。

iOS のみならず、Android 開発 や Flutter などのマルチプラットフォーム開発での継続的デリバリーにも応用できると考えています。

Git Flow の採用・Git Flow の簡単な説明

前回の記事では、API / Frontend のデプロイフロー・ブランチ戦略において GitLab Flow を採用したと述べました。 しかしモバイルアプリのリリースには、下記の要素があり GitLab Flow や GitHub Flow と相性が悪いと考えました。

  • リリース毎に App Store Connect / Google Play Console で市場に出ているバージョンより上げる必要がある
  • リリースには審査が必要になり、恣意的にいつでもリリースできる状況ではない

また、モバイルアプリのリリースフローとしては他にトランクベース開発などを採用しているプロダクトもあると思います。 しかしトランクベース開発は、1週に1回必ずリリースするなどという比較的大規模な開発向きで、弊社のモバイル開発の現状とは合わないと考えました。

上記の理由から今回は、モバイルアプリのデプロイフロー・ブランチ戦略で多く採用されている Git Flow を採用しました。

f:id:tadashi-nemoto0713:20210118184341p:plain


この後の説明をより理解しやすくするために、簡単に Git Flow について説明します。

① 機能実装をする際は、develop ブランチから feature ブランチを作成し、作業を開始します。作業・レビューが完了したら develop ブランチに merge します。

f:id:tadashi-nemoto0713:20210316161849p:plain


② リリースの準備を行う段階で、develop ブランチから release ブランチを作成します。

f:id:tadashi-nemoto0713:20210212164923p:plain


③ release ブランチでリリースに必要な確認・修正を行い、リリースできる状態になったら master・develop ブランチに merge します。

f:id:tadashi-nemoto0713:20210212182725p:plain


④ リリースブランチを master ブランチに merge 後、Tag を付けて本番環境などにリリースします(モバイル開発だと App Store Connect / Google Play Console に申請)。

f:id:tadashi-nemoto0713:20210212182646p:plain


⑤ リリース後に深刻なバグが発見された場合には、Hotfix リリースを行います。master ブランチから Hotfix 用のリリースブランチを作成します。

f:id:tadashi-nemoto0713:20210212171006p:plain


⑥ Hotfix 用のリリースブランチ上で修正・確認が終わり、リリースできる状態になったら master・develop ブランチに merge して本番環境などに再度リリースします。

f:id:tadashi-nemoto0713:20210317122731p:plain


次からは、この Git Flow の中でどのように継続的デリバリーを実現したかについて解説します。

継続的デリバリーの解説

リリースブランチ・Pull Request の作成

① での develop ブランチ上での開発が進み、リリースの準備をするタイミングで ② で説明したリリースブランチの作成 を行います。

f:id:tadashi-nemoto0713:20210212164923p:plain
リリースブランチの作成


リリースブランチは手元で手動で作成することが多いのですが、今回は GitHub Actions + Fastlane を活用して自動化することができました。

まず、リリースバージョンの上げ方を Semantic Versioning に従います。Semantic Versioning 自体についてはここでは深く解説しませんが、Major・Minor・Patch によってバージョンの上げ方を規則化させることができます。

f:id:tadashi-nemoto0713:20210305142013j:plain:w200
Semantic Versioning

Fastlane には increment_version_number という Action があり、Major・Minor・Patch のいずれかを引数に渡してあげることで、Semantic Versioning に基づいてバージョンを上げてくれます。

そして、下記の Fastlane によって

  • Semantic Versioning を使ったリリースバージョンのアップデート、ファイルのコミット
  • リリースブランチの作成、GitHub への Push
  • master・develop ブランチへの Pull Request の作成

まで行うことができます。

f:id:tadashi-nemoto0713:20210315131519p:plain

increment_version_number の Fastlane Action 自体は現在 iOS のみ提供されていますが、下記の記事で Android での事例が紹介されていますので、参考にしてもらえればと思います。

Automating semantic versioning model in mobile releases | ThoughtWorks

最後にこの Fastlane を GitHub Actions 経由で実行させるようにします。

f:id:tadashi-nemoto0713:20210312182827p:plain

GitHub Actions では様々なワークフローのトリガーの種類がありますが、今回は任意のタイミングで実行させたいため、GitHub Action のUIから 手動でトリガーすることができる workflow_dispatch を利用します。

ワークフローをトリガーするイベント - GitHub Docs

workflow_dispatch では引数も与えることができるため、今回のバージョンアップは Major か Minor か渡せるようにします(Patch は Hotfix リリースのみに使うためここでは使いません)。

これによって、GitHub Actions の UI から Major か Minor を指定してワークフローをトリガーしてあげることによって、リリースバージョンのアップデート、リリースブランチ・Pull Request の作成まで自動化することができました。

f:id:tadashi-nemoto0713:20210314171333p:plain
GitHub Actions で workflow_dispatch を実行する

f:id:tadashi-nemoto0713:20210312183859p:plain
master・develop ブランチに向けられた Pull Request

f:id:tadashi-nemoto0713:20210302224859p:plain
バージョン部分のファイル差分

2つのリリース Pull Request を同時に merge する

先程のステップで、リリースブランチから master・develop ブランチに向けた 2 つの Pull Request が自動で作成されました。 そして ③ で説明した 通り、リリースブランチがリリースできる状態になったら master・develop ブランチに merge します。

f:id:tadashi-nemoto0713:20210212182725p:plain

もちろん手動で 2 つの Pull Request を merge することもできますが、どちらかを merge し忘れるという可能性が出てきます。 忘れることなく同時にこの 2 つの Pull Request を merge させるために、以下の GitHub Actions を作成しました。

f:id:tadashi-nemoto0713:20210312181035p:plain

この GitHub Actions によって、Pull Request に release という label を付けたら、develop と master ブランチに向けられた2つの Pull Request を同時に merge することができます。

f:id:tadashi-nemoto0713:20210314132857p:plain

Tag & GitHub Release の作成、App Store Connect へ申請

リリースブランチが develop・master ブランチに merge され、master ブランチに新しいコミットが入ると、Git Flow では ⑤ で述べた Tag の作成 & 本番環境へのリリース を行います。 iOS 開発の場合、このタイミングで App Store Connect に申請を出す(サブミット)ことが多いです。

f:id:tadashi-nemoto0713:20210212182646p:plain


master ブランチへのコミットをトリガーに Tag & GitHub Release の作成は下記の GitHub Actions で自動化することができます。

f:id:tadashi-nemoto0713:20210314142612p:plain

f:id:tadashi-nemoto0713:20210314172613p:plain
Tag & GitHub Release の作成

また、このタイミングでリリース版アプリのビルド・App Store Connect へのアップロードを行います。

f:id:tadashi-nemoto0713:20210314172439p:plain

ここでは Fastlane の Deliver Action を活用しています。

Deliver Action の設定によって、Apple Store Connect へのアップロードだけでなく、メタデータ・スクリーンショットのアップロード、申請(サブミット)、承認後の自動リリースまで行うことができます。

Hotfix リリース

上記までが、通常のリリースフローになります。

通常のリリース以外に、リリース後に深刻なバグが発見された場合には Hotfix リリースを行います。 ⑤ と ⑥ の Step に従って、master ブランチから Hotfix 用のリリースブランチを作成をし、修正後 master・develop ブランチに merge してリリースします。

f:id:tadashi-nemoto0713:20210212171006p:plain


Hotfix リリースを行う際には、Semantic Versioning に従って Patch(x.x.0 → x.x.1) のみのアップデートになります。

f:id:tadashi-nemoto0713:20210305135019p:plain

そして、GitHub Actions では master ブランチからチェックアウトし、Hotfix 用のリリースブランチ・Pull Request の作成が行われるようにします。

f:id:tadashi-nemoto0713:20210314145833p:plain

その後、Hotfix 用のリリースブランチ上で修正・確認が終わった後は、通常のリリースと同じく

  • Pull Request に release の label を付け、master・develop ブランチに merge
  • master ブランチにコミットが入り、自動で Tag & GitHub Release の作成、そして App Store Connect へ申請

という手順でリリースしていくことができます。

【任意】検証版アプリの配布(Firebase App Distribution)

Git Flow とは直接関係しませんが、GitHub Actions を使った検証版アプリの配布についても簡単に解説します。

リリース前にアプリの動作確認をするために、よく下記のサービスなどを活用して検証版アプリの配布・動作確認を行います。

そして、検証版アプリのビルド・アップロード作業を GitHub Actions を使って自動化することができます。 Git Flow だと ① の feature ブランチでの機能開発が終わって develop ブランチに merge した際 や、リリースブランチにコミットがあった際にトリガーするのがよさそうです。

f:id:tadashi-nemoto0713:20210212182754p:plain


GitHub Actions でのワークフローは下記のようになります。

f:id:tadashi-nemoto0713:20210305134944p:plain

上記のワークフローでは、トリガーとして特定のブランチへのコミットの他に、workflow_dispatch を用意しています。

① での機能開発の最中に、「develop・release ブランチにまだ merge したくないけど特定の feature ブランチをビルド・配布したい」という状況はよく起こると思います。

f:id:tadashi-nemoto0713:20210317125311p:plain


GitHub Actions の workflow_dispatch を使うことで、GitHub Actions の UI からブランチを指定した上で手動でトリガーすることが可能です。

f:id:tadashi-nemoto0713:20210212184657p:plain
workflow_dispatch

継続的デリバリーの手順まとめ・おわりに

最後にこれまでの GitHub Actions を活用した継続的デリバリーの手順をまとめます。

【通常のリリースフロー】

  1. feature ブランチを作成, develop ブランチにマージ

  2. リリースを準備するタイミングで GitHub Actions の UI から、リリース準備のためのワークフローを実行(Major か Minor を選択) f:id:tadashi-nemoto0713:20210314171333p:plain

  3. リリースバージョンのアップデート(Major か Minor)、リリースブランチの作成、master・develop ブランチへの Pull Request の作成がされる f:id:tadashi-nemoto0713:20210312183859p:plain

  4. このタイミングで、検証版アプリのビルド・Firebase App Distribution への配布もされるため、実機などで動作確認をする

  5. リリースできるタイミングになったら、Pull Request に release の label を付け、develop・master ブランチへ同時に merge される f:id:tadashi-nemoto0713:20210314132857p:plain

  6. master ブランチにコミットが入り、自動で Tag & GitHub Release の作成、そして App Store Connect へ申請される f:id:tadashi-nemoto0713:20210314172613p:plain


【Hotfix リリース】

  1. Hotfix リリースを準備するタイミングで GitHub Actions の UI から、Hotfix リリース準備のためのワークフローを実行
  2. リリースバージョンのアップデート(Patch)、リリースブランチの作成、master・develop ブランチへの Pull Request の作成がされる
  3. 上記のリリースブランチに対して不具合修正をコミットする
  4. リリースブランチへのコミット毎に自動で検証版アプリのビルド・Firebase App Distribution へ配布されるので、実機などで動作確認
  5. 修正を確認したら、Pull Request に release の label を付け、develop・master ブランチへ同時に merge される
  6. master ブランチにコミットが入り、自動で Tag & GitHub Release の作成、そして App Store Connect へ申請される


これによって、リリースに必要な作業をほぼGitHub上で完結させることができました
(Fastlane の Deliver アクションによって、Appl Store Connect への作業をどのぐらい自動化するかにもよりますが)

モバイルアプリ開発におけるリリースは、APIやフロントエンド開発と比較してリリース頻度は低くなりがちなものの、リリースのために必要な作業は多くなりがちです。

この継続的デリバリーによって、

  • より俊敏にアプリの改善を市場にリリースできるようになる
  • 開発者がプロダクト開発によりフォーカスできるようになる

ことを期待しています。

hrmos.co

参考・注意点

Improving Continuous Delivery with GitLab Flow + GitHub Actions

f:id:tadashi-nemoto0713:20201228172750p:plain

The Japanese version of this blog post can be found here:

techblog.exawizards.com

Hello, I'm Tadashi Nemoto from the DevOps team.

In this article, I will demonstrate how to improve deployment flows using GitHub Actions.

Standard deployment flows and their problems

Although it varies by departments and services, current deployment flows are as follows.

  • Three environments
    • develop environment
    • staging environment
    • production environment
  • Git Flow as a branching strategy
  • CI / CD using Jenkins
    • develop and staging environments → Deploy with branch updates
    • production environment → Software engineers or DevOps engineers deploy by specifying a tag


f:id:tadashi-nemoto0713:20210118184341p:plain

Git Flow works well for large scale development or projects with fixed release timing (e.g. iOS / Android work).

However, I have found it to be less beneficial for small to medium scale projects where the release windows tend to be arbitrarily decided (such as in API / Frontend work).

On the other hand, Git Flow involves more complex branch management (having to maintain release and hotfix branches for example) than others, and I have thought that this is not suitable for deploying to production frequently.

Therefore, I wanted to achieve the following goals by improving deployment flows.

  • Enable small, autonomous deployments → Increase deployment frequency
  • Simplify branch management and deployment strategies

How about GitHub Flow?

I think GitHub Flow is often used as an alternative to Git Flow.

GitHub Flow is a branching strategy that is actually used in the development of GitHub services, and I initially looked into it as a possibility for this project.

GitHub Flow only has a master(main) branch and a feature branch, allowing for simpler branch management.

f:id:tadashi-nemoto0713:20210115112754p:plain
GitHub Flow(From Understanding the GitHub flow)

The master branch is considered to be a branch that can be deployed to the production environment at any time, and there are many examples of deploying to the production environment triggered by push.

This GitHub Flow seems to be able to solve the problem I mentioned earlier, but I thought having a verification environment before release might cause a problem.

As mentioned earlier, in GitHub Flow, the master branch is considered to be the branch that can be deployed to the production environment at any time.

Therefore, each feature branch needs to be used for verification before release.

f:id:tadashi-nemoto0713:20210127144507p:plain
Problem of verification environment in GitHub Flow
(From Introduction to GitLab Flow)

And if you have multiple feature branches / pull requests, you will need to take the following approach.

  • Switch between one or more validation environments as needed
  • Launch an environment for each pull request → close the environment after each release

I've been looking for a simpler way to solve the above issues with validation environments, and this time I've decided on GitLab Flow.

About GitLab Flow

GitLab Flow is documented by GitLab.

Introduction to GitLab Flow | GitLab

The above document also lists some of the issues with Git Flow / GitHub Flow.

GitLab Flow allows you to have the branches you need for a release while maintaining the master branch/feature branch relationship of GitHub Flow.

The GitLab Flow documentation introduces production branch model, environment branches model, and release branches model, and I thought that environment branches model would improve deployment flows while making effective use of the current environments.

f:id:tadashi-nemoto0713:20210115113159p:plain
Production branch model and environment branches model of GitLab Flow
(From Introduction to GitLab Flow)

In the environment branches model, each branch is paired with an environment, and when branches are modified, they are automatically deployed to their respective environments.

In this case, it looks like the following

  • master branch → develop environment
  • staging branch → staging environment
  • production branch → production environment

You can then proceed with deployment by creating and merging pull requests in master → staging → production.

I believe this GitLab Flow has the following advantages

  • Simpler branch management and deployment than Git Flow
  • Easier than GitHub Flow to prepare a verification environment before a production release

Automatically generate release pull requests using git-pr-release + GitHub Actions

As mentioned earlier, GitLab Flow deploys by creating and merging pull requests on the master → staging → production branch.

However, if we were to do this manually, I thought the following might happen.

  • Forgetting to create and merge pull requests
  • It may become difficult to know which changes are in each pull request.

To solve these problems, I automated creating and modification of the above pull requests using git-pr-release + GitHub Actions.

f:id:tadashi-nemoto0713:20210118142154p:plain
Auto-generated release pull request using git-pr-release + GitHub Actions

git-pr-release can detect differences between branches and create a release pull request with a list of merged pull requests.

You can run git-pr-release itself as a command, but you can also automate it by creating a GitHub Actions workflow like below.

f:id:tadashi-nemoto0713:20210325114549p:plain

By doing this, we can simplify the deployment flow to the production environment as much as possible, as shown below:

  1. Review pull requests of feature branches and merge them into the master branch
  2. Automatically deploy to the verification environment
  3. Automatically generate release pull requests to deploy to the production environment
  4. Check the validation environment (manual testing or end-to-end testing using the validation environment, etc.)
  5. If there are no problems, merge the pull request and automatically deploy to the production environment

Deploy using GitHub Actions

The actual deployment is also done with GitHub Actions.

It is set to be triggered when there is a push in each environment branch (master, staging, production).

f:id:tadashi-nemoto0713:20210325114621p:plain

We use self-hosted runners of GitHub Actions when deploying, please see the following blog entry for more details:

techblog.exawizards.com

Effects and challenges

We are currently implementing this deployment flow in several departments and services, and have been able to achieve the first two points mentioned.

  • Enable small, autonomous deployments → Increase deployment frequency
  • Simplify branch management and deployment strategies

In addition, some projects now deploy several times a day, previously having deployed only once every 1-2 weeks.

f:id:tadashi-nemoto0713:20210115113531p:plain

At the same time, there are some issues that we need to be aware of.

GitLab Flow is a relatively flexible flow compared to GitHub Flow, and the number of pull requests that can be deployed to the production environment at one time can be decided flexibly.

Therefore, depending on the situation, the size of the pull requests that can be deployed at one time may become quite large, making it difficult to achieve the original goal of "enabling small deployments".

To reduce the cost of rework in case of a bug, we may need to implement the following policy when using GitLab Flow in the future.

  • Merge, validate, and deploy features/bug fixes to the production environment for every pull request.
  • Minor dependency updates created by bot such as Dependabot can be merged, verified, and deployed at once.

Summary

This time, I only improved the deployment flow (Continuous Deployment).

Still, we would like to expand the Continuous Testing, DevSecOps, and other mechanisms to continuously improve the service in line with this flow in the future.

I also believe that the optimal deployment flow itself will change depending on the organisation's growth and its requirements, so we will continue to improve upon this in the future.

I hope that this entry will be of some help in improving your own deployment flow.

hrmos.co

GitLab Flow + GitHub Actions ではじめる、デプロイフローの改善・自動化

f:id:tadashi-nemoto0713:20201228172750p:plain


DevOps エンジニアの 根本 征 です。

前回のエントリーでは GitHub Actions の self-hosted runners について紹介しました。

今回はそれらを活用したデプロイフロー(主に API / Frontend)の改善について紹介したいと思います。

これまでのデプロイフローと課題

部署やサービスによって異なりますが、これまでのデプロイにまつわる環境は大まかに下記のような状況でした。

  • 3つの環境
    • develop 環境(主に開発者が使う環境)
    • staging 環境(本番リリース前の検証環境)
    • production 環境(本番環境)
  • Git Flow
  • Jenkins を使った CI / CD
    • develop 環境・staging 環境 → ブランチの更新でデプロイ
    • production 環境 → tag を指定してソフトウェアエンジニアもしくは DevOps エンジニアがデプロイ


f:id:tadashi-nemoto0713:20210118184341p:plain
Git Flow


Git Flow は大規模な開発や、リリースタイミングが決められているもの(iOS / Android など)とは相性が良いです。

しかし、API / Frontend などリリースタイミングが恣意的に決めることができ、かつ小・中規模の開発だとあまりメリットがないと感じました。

逆に、Git Flow は他と比べて複雑なブランチ管理(release ブランチや hotfix ブランチ)になってしまい、これによってデプロイ頻度が下がっている可能性もあると考えました。

そのため、今回のデプロイフローの改善によって下記を実現したいと考えました。

  • 小さく自律的にデプロイできるようにする → デプロイ頻度を上げる
  • シンプルなブランチ管理・デプロイができるようにする

GitHub Flow はどうか

上記の Git Flow の代替としてよく導入されているのが GitHub Flow だと思います。

GitHub Flow は実際に GitHub のサービス開発において活用されているブランチ戦略であり、私も最初は GitHub Flow を導入できないか検討しました。

GitHubにおける継続的デリバリー/How GitHub builds and deploy software - Speaker Deck

GitHub Flow では master(main) ブランチと feature ブランチしかなく、シンプルなブランチ管理を実現することができます。

f:id:tadashi-nemoto0713:20210115112754p:plain
GitHub Flow(Understanding the GitHub flowより)


master ブランチはいつでも本番環境にデプロイができるブランチと考えられ、push をトリガーに本番環境へデプロイしているという事例も多くあると思います。

この GitHub Flow によって先ほど挙げた課題を解決することができそうですが、リリース前の検証環境が課題になると考えました

先述の通り、GitHub Flow だと master ブランチはいつでも本番環境へデプロイができるブランチだと考えられています。

そのため、リリース前の検証にはそれぞれの feature ブランチで検証する必要があります。

f:id:tadashi-nemoto0713:20210119162043p:plain
GitHub Flow における検証環境の問題
(Introduction to GitLab Flowより)

そして、複数の feature ブランチ / Pull Request が存在する場合には下記のような方法を取る必要が出てきます。

  • 1つあるいは複数の検証環境を必要に応じて切り替える
  • Pull Request 毎に環境が立ち上がる → リリース後にその環境を閉じる

私はよりシンプルに上記の検証環境にまつわる課題を解決できないか考え、今回導入したのが GitLab Flow です。

GitLab Flow とは

GitLab Flow に関しては GitLab がドキュメントを公開しています。

Introduction to GitLab Flow | GitLab

またこちらの記事で翻訳がされています。

GitLab flowから学ぶワークフローの実践 | POSTD


上記のドキュメントでも Git Flow / GitHub Flow に対する課題点を挙げています。

そして GitLab Flow では、GitHub Flow の masterブランチ と featureブランチの関係はそのままに、リリースに必要なブランチを用意することができます。

GitLab Flow のドキュメントでは、production ブランチモデル環境ブランチモデルrelease ブランチモデルが紹介されており、今回環境ブランチモデルが現状ある環境を有効活用しながらデプロイフローを改善できるのではと考えました。


f:id:tadashi-nemoto0713:20210115113159p:plain
GitLab Flow の production ブランチモデルと環境ブランチモデル
(Introduction to GitLab Flowより)

環境ブランチモデルでは、それぞれのブランチと環境を対にして、ブランチに変更があった場合には自動でそれぞれの環境へデプロイがされるようにします。

今回の場合だと下記のようになります。

  • master ブランチ → develop 環境
  • staging ブランチ → staging 環境
  • production ブランチ → production(本番)環境

そして、master → staging → production に Pull Request を作成・マージしていくことでデプロイを進めることができます。

これによって下記のようなメリットがあると考えています。

  • Git Flow よりシンプルなブランチ管理・デプロイを行うことができる
  • GitHub Flow よりも容易に本番リリース前の検証環境を用意することができる

git-pr-release + GitHub Actions を使った、リリース Pull Request の自動生成

先述の通り、GitLab Flow では master → staging → production ブランチに Pull Request を作成・マージしていくことでデプロイを進めていきます。

しかし、手動でこれを行うとなると、下記の恐れがあると考えました。

  • Pull Request の作成・マージのし忘れが発生する
  • どの変更が入った Pull Request なのか分かりづらくなる

これらを解決するために、上記の Pull Request の作成・更新を git-pr-release + GitHub Actions を使い自動化しました。

f:id:tadashi-nemoto0713:20210118142154p:plain
git-pr-release + GitHub Actions で自動生成されたリリース Pull Request

git-pr-release ではブランチ間の差異を検出し、マージされた Pull Request の一覧が表示されたリリース Pull Request を作成することができます。

git-pr-release 自体はコマンドで実行することが可能ですが、下記のような GitHub Actions のワークフローを作成することによって自動化することができます。

f:id:tadashi-nemoto0713:20210325114549p:plain

これによって、本番環境までのデプロイフローを下記のようになるべくシンプルにすることができました。

  1. feature ブランチの Pull Request をレビュー・master ブランチにマージ
  2. 検証環境へ自動的にデプロイ・本番環境へデプロイするリリース Pull Request が自動的に生成・更新
  3. 検証環境で確認(手動テストもしくは検証環境を使った End to End テストなど)
  4. 問題なければ Pull Request をマージ・本番環境へ自動的にデプロイ

GitHub Actions を使ってデプロイを行う

実際のデプロイに関しても GitHub Actions で行っています。

それぞれの環境ブランチ(master, staging, production)で push がある際にトリガーされるように設定しています。

f:id:tadashi-nemoto0713:20210325114621p:plain

また、デプロイする Job に関しては、GitHub Actions の self-hosted runners を使っています。

詳細については下記エントリーをご覧ください。

techblog.exawizards.com

効果と課題

現在いくつかの部署・サービスでこのデプロイフローを導入しており、最初に述べた2点を実現することができました。

  • 小さく自律的にデプロイできるようにする → デプロイ頻度を上げる
  • シンプルなブランチ管理・デプロイができるようにする

また、サービスによっては 1~2週に1回のデプロイ頻度から、1日に数回デプロイ できるようになりました。

f:id:tadashi-nemoto0713:20210115113531p:plain


同時に、課題・注意しないといけない点も出てきました。

GitLab Flow は GitHub Flow を比べると比較的柔軟なフローになっており、本番環境へ1度にデプロイできる Pull Request の数も柔軟に決めることができます。

そのため、状況によっては1度にデプロイする Pull Request のサイズが大きくなってしまい、本来目指していた「小さくデプロイできるようにする」を実現することが困難になる可能性があります。

不具合があった際の手戻りのコストを減らすためにも、今後は GitLab Flow を使う場合には下記のようなポリシーを決めていく必要がありそうです。

  • feature / bugfix は 1 Pull Request 毎にマージ・検証・本番環境までデプロイしていく
  • Dependabot などによって作られる、マイナーな依存関係のアップデートはまとめてマージ・検証・デプロイしていく

おわりに

今回はデプロイフロー(Continuous Deployment)のみの改善でしたが、今後はこのフローに合わせて継続的テスティング(Continuous Testing) や DevSecOps など、サービスを継続的に改善していくための仕組みを拡充していきたいと考えています。

また、このデプロイフロー自体も組織やサービスの成長によって最適な形が変わってくると考えているため、今後も継続的に改善していきたいと考えています。

今回のエントリーが、現場でのデプロイフローの改善に何かしら参考になれば幸いです。


hrmos.co