エクサウィザーズ Engineer Blog

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

AI王 〜クイズAI日本一決定戦〜 第2回コンペティション 振り返り

こんにちは、エクサウィザーズNLPギルド所属の神戸です。

本記事では、3月に終了しました「AI王 〜クイズAI日本一決定戦〜 第2回コンペティション」の振り返りとなります。 NLPギルドのチームで参加しまして、3位入賞という結果になりました!

要点

  • コンペのタスクやルール、配布データについてなどの概要を説明しています
  • コンペで3位に入賞した投稿システムの説明資料を公開しましたので、質問応答システムにご興味のある方はご参考ください
  • コンペ中に複数のWebサイトからクローリング&スクレイピングして独自に作成したデータセットも公開しましたので、ご活用ください

コンペ概要

  • 日本の(日本語を対象とした)質問応答研究を促進させることを目的としています。クイズ問題を題材とした質問応答データセットを用いてクイズに解答するAIを開発するコンペとなっています。
  • 去年に第1回目コンペが開催され、今年が第2回目コンペとなります
  • 東北大学、理化学研究所、NTTの共同プロジェクトで運営をされています。

コンペルール

ルールは大まかに以下のようなルールがありました。Wikipediaのナレッジソースなども含めた形式でDockerファイルを提出するようになっておりこの辺りはよくみるコンペとは異なっているところかと思います。

  • 情報源,モデル等含めて圧縮済みで30[GB]以内、実行環境を含むDockerイメージを提出 + 実行時間 6h以内
    • 評価値 = 1,000問の正答率 (配布学習データ 22,335問)
  • 暫定評価:⽂字列の完全⼀致(Exact Match)で確認
  • 最終評価:⼈⼿で表記の揺れなども考慮して確認
  • Wikipediaを含め,⼀般公開されている, もしくは公開できるデータのみ利⽤可能
  • 外部リソース(インターネット検索など)は利⽤禁止

また、第2回コンペでは,第1回のコンペと異なり選択肢を排除し,あらゆる解答がありえるという,より通常のクイズ大会に近い設定となっていました。つまり,問題として与えられるのはクイズの問題文のみとなっていました。その問題文のみから解答となる文字列を解答として返すシステムを構築する必要がありより汎用的&難易度の高い設定になっていました。

引用: 加藤拓真, 宮脇峻平, 第二回AI王最終報告会 - DPR ベースラインによる オープンドメイン質問応答の取り組み (2022) - Speaker Deck

配布データ

公式からは以下のデータが配布されています。

また、学習用データのサンプルのフォーマットは以下のようになっています。

{
  "qid": "AIO02-0001",
  "competition": "第2回AI王",
  "timestamp": "2021/01/29",
  "section": "開発データ問題",
  "number": 1,
  "original_question": "映画『ウエスト・サイド物語』に登場する2つの少年グループといえば、シャーク団と何団?",
  "original_answer": "ジェット団",
  "original_additional_info": "",
  "question": "映画『ウエスト・サイド物語』に登場する2つの少年グループといえば、シャーク団と何団?",
  "answers": [
    "ジェット団"
  ]
}

投稿システム説明と独自作成したデータセット

私たちのチームの投稿システム説明資料(最終報告会で発表したものと同じ)と独自に作成したデータセットはアップロードしていますので、こちらをご参照ください。

投稿システム説明資料 drive.google.com

独自に作成したデータセット(データセットについては説明資料の外部データをご参照ください) drive.google.com

作成したデータセットの統計は以下のようになっています。データ数としては76721となっており質問応答のデータセットとしては比較的多いデータ量になっています。 また、Elasticsearchで正例のpassageを付与できたものの数は60443となっていました。

外部データセットの統計

投稿システムの簡潔なまとめは以下となります。

  • Retriever-Reader構成のモデルを使用
    • Retriever: 東北大BERT-Baseモデル(cl-tohoku/bert-base-japanese-whole-word-masking)
    • Reader: 東北大BERT-Large(cl-tohoku/bert-large-Japanese)
  • 正例のpassageをシャッフルして学習
  • 学習したRetrieverのhard negativeなサンプルを学習データに追加して再学習
  • 外部データの活用
    • クイズの杜、みんはや、Mr Tydi、Quiz Works、語壺、Erin、Wiktionary QA、5TQなど外部データを利用
    • 外部データはクローリング&スクレイピングして作成
      • サイトごとにクローリング&スクレイピングするコードを実装して作成しました

また、リーダーボードスコアの遷移の以下のようになっています。

リーダーボードスコアの遷移

結果

最終結果は3位でした!(自動評価2位、人手評価3位) また次回コンペが開催されるとのことなので、次回コンペではより良い結果を目指したいと思います!

最終順位結果

引用: AI王 〜クイズAI日本一決定戦〜 第2回コンペティション 公式サイト

コンペを振り返って

コンペに参加してみて以下のような多くの学びがありました。コンペを通じて得られた知見を今後の業務に活かしていきたいと思います。 また、引き続き今回のようなコンペに参加し外部への情報発信にも取り組んで参りたいと思います。

  • Retriever-Readerの構成の質問応答タスクで、ナレッジソースから関連のあるドキュメントもとってくるところもやるのは初めてだったので学びになった
  • Web上に質問応答タスクに使えそうな外部データが結構あることを知ることができたのも学びだった
  • データ量はやはり重要だと再認識、反面いかに少量のデータで精度を出せるかも重要
    • 1,2位のチームは外部データの利用なく精度が出ていたので、参考にしたい
  • エラー分析より、BERTの文脈理解だけでは解くことができない問題を理解できた
    • エラー分析については投稿システム説明資料をご参照ください

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

hrmos.co

event.exawizards.com

RecSys2021学会参加報告記事

 こんにちは。エクサウィザーズで構造化データギルドに所属し、機械学習エンジニアかつエンジニアリングマネージャーをしている小野です。本記事では2021年に推薦システムの国際会議にヴァーチャル出席しましたので(本当はアムステルダムに行きたかったです。)、一部の内容を共有させていただきます。

概要

f:id:oimokihujin:20220407045453p:plain  今回は、オランダのアムステルダムで2021年9月27日から10月1日までバーチャル&オフライン開催された推薦システムの国際会議であるRecSys2021*1に参加したので、内容を記事にさせていただきました。皆様がご存じのように、推薦システムはすでに私たちの生活に切っても切り離せない存在です。Amazonなどのオンラインストアで商品を眺めていると、隣に出てくるオススメ商品をクリックしてしまうことなどはないでしょうか?それらは皆様の行動履歴や商品分類など様々な情報を駆使し、適切なユーザーに適切なアイテムを推薦することによって、ユーザーの意思決定(この場合は購買意欲)を促進します。

 RecSys 2021の開催で16回目の開催となります。RecSysは4日間の日程で多くの本会議(下図は本会議の日程)と多くのワークショップなどから構成されており、非常にボリュームがある会議となっております。また、本会議とワークショップが同時に開催されており、全ての会議に同時に出席することはできません。しかし、コロナの影響前からRecSysはYoutube*2で会議内容を発信しており、当日に参加できない場合でも、後日会議内容を確認することができます。RecSys2021も同様に開催後半年を経て会議内容がYoutubeに公開されているので、当日参加できなかった人も現在は確認することができます。

f:id:oimokihujin:20220407045612p:plain

ワークショップから見る会議の全体感について

 多くの推薦システムの根幹となる技術は機械学習です。昨今、機械学習でも話題となっていることは、推薦システムでも同様に話題となっております。大きな話題の一つとして、機械学習の社会的責任性が挙げられます。この社会的責任性は、公平性、透明性、責任性となります。それぞれを簡単に説明すると、公平性は、誰が使っても不公平ではない推薦結果を出すことができるかに焦点が当てられています。例えば、クーポン配布推薦システムを構築する際、たまたまデータセットの大部分が男性だった場合、男性に偏ったクーポン配布が実施されてしまう可能性があります。透明性はその名が示す通り、推薦システムが推薦した理由が明確にわかる必要があります。例えば、推薦システムがある男性にコーヒーを推薦した場合、その理由を明示する必要があります。責任性は、推薦システムの責任性を示します。例えば、健康器具を推薦するシステムがあるとします。この健康器具を推薦しするシステムが腰痛持ちの人に腰痛を悪化させるような推薦をしてしまい、結果としてユーザーが腰痛を悪化させてしまった場合、推薦システムがどうしてそのような推薦をしたのかを明らかにしなければなりません。

 RecSysでは、推薦システムの社会的責任性を議論するためのワークショップを開催しており、FAccTRec: Workshop on Responsible Recommendationで中心的に議論されています。このワークショップは5年目であり、比較的新しいワークショップであることがわかります。オーガナイザーには産総研の神嶌先生*3の名前もあります。

各ワークショップについて

 各ワークショップでは、発表タイトルが4〜8つ程あり、それぞれが15分ほどの発表時間を持ち発表する形式でした。ここではいくつかのワークショップの概要を説明し、言及したいワークショップについては少し詳しく説明したいと思います。

f:id:oimokihujin:20220407045700p:plain

  1. CARS: Workshop on Context-Aware Recommender Systems
    1. 次世代コンテキストアウェア推薦システムを議論するためのワークショップです。ここでいうコンテキストアウェアとは、ユーザーやアイテムの付帯情報を指します。具体的には、ユーザー特徴量(性別・住所・年齢・など)やアイテムの特徴量(カテゴリ・値段・色など)を指し、これらの情報に加えて、あるユーザーがあるアイテムを購入した情報(購入履歴や閲覧履歴など)を用いて推薦システムを構築します。
  2. ComplexRec: Workshop on Recommendation in Complex Environments
  3. FAccTRec: Workshop on Responsible Recommendation
  4. FashionxRecSys: Workshop on Recommender Systems in Fashion and Retail
  5. GReS: Workshop on Graph Neural Networks for Recommendation and Search
    1. グラフベースのモデルを用いて推薦システムを議論するためのワークショップです。グラフベースのモデルとは、知識グラフなどを用いた推薦システムを指し、アイテム-アイテム間、アイテム-ユーザー間、ユーザーーユーザー間などに存在する関係を明示的に取り扱うことができる推薦システムです。グラフベースのモデルを使うことによって、より高次の情報(商品A→購入者B→商品C→購入者Dなど)や関係を取り込むことができ、精度の面で優れているとされています。
  6. INRA: Workshop on News Recommendation and Analytics
  7. IntRS: Joint Workshop on Interfaces and Human Decision Making for Recommender Systems
    1. 推薦システムの精度やモデルを議論する場ではなく、デザインやインターフェースを議論するワークショップです。例えば、推薦システムで20位までのオススメ商品の結果を表示する際に、2ページで表示する際に1ページ目と2ページ目でバイアスが含まれてしまうなどの弊害があります。また、ユーザーが推薦結果に納得感を得るためのインターフェースが紹介されていました。
  8. KaRS: Workshop on Knowledge-aware and Conversational Recommender Systems
  9. MORS: Workshop on Multi-Objective Recommender Systems
  10. OHARS: Workshop on Online Misinformation- and Harm-Aware Recommender Systems
    1. 間違った情報や人を傷つける情報を察知する推薦システムを議論するワークショップです。SNSなどの情報発信ツールが普及する現在では、間違った、または他人を傷つけるようなコンテンツなどの普及速度も非常に早くなっている。このようなコンテンツをいち早く検出し、他人に推薦しないようなシステムの構築を目指します。特に、現在のcovid-19が流行っている世の中では正しい情報をなるべく早く広めるためにもこのような推薦システムが重要となります。
  11. ORSUM: Workshop on Online Recommender Systems and User Modeling
    1. オンライン推薦システムを議論するワークショップ。ニュースの記事やコメントやユーザーのフィードバックなど、時間が進めば進むほどユーザーに対するコンテキストや情報が蓄積していきます。それらの新鮮な情報をいち早く取り入れ、より良い推薦システムを構築することで、「あの時」は欲しかったのに、「今はもういらない」とならないように「その時」欲しいものを「その時」推薦するシステムの構築を目指します。
  12. PERSPECTIVES: Workshop on Perspectives on the Evaluation of Recommender Systems
    1. 推薦システムの評価に注目したワークショップ。
  13. PodRecs: Workshop on Podcast Recommendations
  14. RecSys Challenge Workshop
  15. RecSys in HR: Workshop on Recommender Systems for Human Resources
    1. 人事部門に関する推薦システムに関するワークショップです。PWCによると、国際企業のHR機能の40%以上がAIアプリケーションを使用しているらしく、特に、優秀人材のスクリーニングなど簡易的に評価するために用いられることが多いそうです。そのような場面では個人に関わる非常にセンシティブなデータを使用するため、推薦システムの社会的責任性が重要となります。
  16. RecTour: Workshop on Recommenders in Tourism
  17. SimuRec: Workshop on Synthetic Data and Simulation Methods for Recommender Systems Research
  18. XMRec: Workshop on Cross-Market Recommendation

参加しての所感

 本記事ではそれぞれの内容の詳細に触れるのではなく、RecSys2021のワークショップに焦点を当てて参加報告を記しました。会議内容の詳細はYoutubeで確認することができるので気になる点を重点的に確認していただくと理解が深まるかと思います。RecSys2022はシアトルで開催されることが決まっています。コロナの状況にもよりますが、次回は現地参加できればと思います。

Load testing with Artillery.io

Introduction

This article is going to be about Artillery, a popular load and smoke testing framework.

Recently I used Artillery to evaluate the performance of some of our production services. I'd like to present some of the scenarios I encountered, and ways to solve them. So if you're new to load testing, I hope this article can serve as a helpful introduction to Artillery.

Now let's get started!

Regarding the code samples

Note that in the below samples, everything will be installed into a local folder we create. So you can follow along and run all of these samples without needing to install anything on your machine globally. So there's no worry about side-effects or changes to the configuration on your system, you can simply delete the folder when you are done!

The only prerequisite is to install Node.

JSONPlaceholder (a simple test server)

In these samples, I'm going to be using a publicly-available test REST API service known as JSONPlaceholder as the server. The public version is available at https://jsonplaceholder.typicode.com/, but instead we're actually going to run this same code locally -- because Artillery is designed to put heavy load on the server, we do not want to cause problems for this free and useful service!

Creating and running tests

Installation

Create a local directory that we'll use to install the dependencies and run our tests

mkdir load-testing
cd load-testing

Install Artillery (and also module csv-parse which we'll need later)

npm install --save artillery
npm install --save csv-parse

Install JSONPlaceholder

npm install --save jsonplaceholder

(Note: you might get some warnings here about your Node version being too new, but you can ignore those. I used Node 15 without problems)

Run JSONPlaceholder server

node ./node_modules/jsonplaceholder/index.js

Running the first test sample

Now that our server is running, let's get our first test code up and running!

# load-testing.yml

config:
  target: "http://localhost:3000"
  phases:
    - duration: 60
      arrivalRate: 10
      name: "Run queries"

scenarios:
  - name: "Run queries"
    flow:
      - get:
          url: "/todos/1"

Let's see what we've got here:

  • We set the location of the server with target: http://localhost:3000
  • In the phases: section we configure to for 60 seconds with 10 simulated users
  • In the scenario "Run queries" we make a GET request to one of the endpoints (this loops until the time is up)

To run it:

./node_modules/artillery/bin/run run load-testing.yml

Reading test cases from a CSV file (payload files)

The above is well and good so far, but we're just requesting the same data from the same resource repeatedly. For most systems this will allow every request to be served from cached code and data, so it isn't a very good simulation of real-world usage. Therefore we'd like to vary the resources and parameters to provide more realistic testing, but it would be a bit unwieldy to hard code each value into the YAML file. This is where "payload" files come in -- we store these parameters in a CSV file, so we can easily create and change test cases without needing to modify the code.

Let's add the CSV file and the related code:

# queries.csv

resource,queryparam1,queryparam2,queryparam3
posts,_start=20,_end=30,
posts,views_gte=10,views_lte=20,
posts,_sort=views,_order=asc,
posts,_page=7,_limit=20,
posts,title=json-server,author=typicode,
comments,name_like=alias,,
posts,title_like=est,,
posts,q=internet,,
users,_limit=25,,
users,_sort=firstName,_order=desc,
users,age_gte=40,,
users,q=Sachin,,
# load-testing.yml

config:
  target: "http://localhost:3000"
  payload:
    path: "queries.csv" # path is relative to the location of the test script
    skipHeader: true
    fields:
      - resource
      - queryparam1
      - queryparam2
      - queryparam3
  phases:
    - duration: 60
      arrivalRate: 10
      name: "Run queries"

scenarios:
  - name: "Run queries"
    flow:
      - get:
          url: "/{{ resource }}?{{ queryparam1 }}&{{ queryparam2 }}&{{ queryparam3 }}"

Now we have the parameters in the CSV. In the payload: section we define the location of the file and variable names for each field, then in Run queries we use these variable names. The nice thing is that Artillery will advance to the next CSV row each time automatically for us!

Creating an initial test data set

With the test server we've been using the data is just static JSON, so it's easy to make every test run start out with a consistent dataset. When testing real services however, you may need to use an API to populate the initial data. Fortunately, it is possible to do this in Artillery without needing additional external tools -- we can use a "processor" (custom Javascript plugin) and put this into the before block (initialization code which runs before the test cases).

// utils.js

const fs = require("fs")
const parse = require('csv-parse')

function loadCsvIntoJson(context, events, done) {
    
    fs.readFile(context.vars['csvFilePath'], function (err, fileData) {
        parse(fileData, {columns: false, trim: true}, function(err, rows) {
            // CSV data is in an array of arrays passed to this callback as `rows`
            context.vars['csvRows'] = rows
            context.vars['row'] = 1

            done()
        })
    })
}

function getNextRow(context, events, done) {
    let row = context.vars['row']

    context.vars['userId'] = context.vars['csvRows'][row][0]
    context.vars['id'] = context.vars['csvRows'][row][1]
    context.vars['title'] = context.vars['csvRows'][row][2]
    context.vars['completed'] = context.vars['csvRows'][row][2]

    row++
    context.vars['row'] = row

    done()
}

function hasMoreRows(context, next) {
    return next(context.vars['row'] < context.vars['csvRows'].length)
}
# load-testing.yml
config:
  target: "http://localhost:3000"
  processor: "./utils.js"
  variables:
    csvFilePath: "todos.csv" # Path is relative to the location of the test script
  payload:
    path: "queries.csv"
    skipHeader: true
    fields:
      - resource
      - queryparam1
      - queryparam2
      - queryparam3
  phases:
    - duration: 60
      arrivalRate: 10
      name: "Run queries"

before:
  flow:
    - log: "Adding Todos..."
    - function: "loadCsvIntoJson"
    - loop:
      - function: "getNextRow"
      - log: Inserting Todo (id={{ id }})
      - post:
          url: "/todos"
          json:
            userId: "{{ userId }}"
            id: "{{ id }}"
            title: "{{ title }}"
            completed: "{{ completed }}" 
      whileTrue: "hasMoreRows"


scenarios:
  - name: "Run queries"
    flow:
      - get:
          url: "/{{ resource }}?{{ queryparam1 }}&{{ queryparam2 }}&{{ queryparam3 }}"

Using .env (dotenv) for configuration

Until now these examples have simply hard-coded many values, but in a real-world test automation setup we probably want to separate configuration from code (many teams use .env as a standard place to store secrets) For our setup, .env was the way to go, but Artillery doesn't support this itself. Fortunately there is a tool called dotenv-cli which can run any arbitrary executable with the variables from .env loaded into its environment. You can install this by running

npm install --save dotenv-cli

For example, we might put the location of the server into our .env file:

# .env
ARTILLERY_TARGET=http://localhost:3000

Then we can load this from the environment in the yaml file:

# load-testing.yml
config:
  target: "{{ $processEnvironment.ARTILLERY_TARGET }}"
  ...

Finally, run with dotenv-cli to use the .env values in the tests:

./node_modules/dotenv-cli/cli.js ./node_modules/artillery/bin/run run load-testing.yml

Interpreting the Output

After the test run completes, you will get some information like this:

All VUs finished. Total time: 1 minute, 7 seconds

--------------------------------
Summary report @ 17:59:44(+0900)
--------------------------------

http.codes.200: ................................................................ 600
http.request_rate: ............................................................. 10/sec
http.requests: ................................................................. 600
http.response_time:
  min: ......................................................................... 13
  max: ......................................................................... 202
  median: ...................................................................... 104.6
  p95: ......................................................................... 147
  p99: ......................................................................... 179.5
http.responses: ................................................................ 600
vusers.completed: .............................................................. 600
vusers.created: ................................................................ 600
vusers.created_by_name.Run queries: ............................................ 600
vusers.failed: ................................................................. 0
vusers.session_length:
  min: ......................................................................... 15.6
  max: ......................................................................... 339.2
  median: ...................................................................... 111.1
  p95: ......................................................................... 156
  p99: ......................................................................... 228.2

Most of these are pretty self-explanatory, but the meaning of "p95" and "p99" might not be immediately obvious. From the documentation:

Request latency is in milliseconds, and p95 and p99 values are the 95th and 99th percentile values (a request latency p99 value of 500ms means that 99 out of 100 requests took 500ms or less to complete).

You may also see lines like:

errors.ETIMEDOUT: .............................................................. 9412
errors.ESOCKETTIMEDOUT: ........................................................ 30
errors.ECONNREFUSED: ........................................................... 16550

These are socket level errors where the client couldn't connect to the server. As you increase the number of users and requests, you'll eventually reach a limit where the service cannot process all of the incoming requests.

Authorization - when the api requires an access token

In our case, our API server requires an authentication token. You can add this to the HTTP headers for requests (where access_token is the token returned by your authentication function):

- post:
    url: "/path/to/resource"
    headers:
      authorization: Bearer {{ access_token }}

Other Resources

JSONPlaceholder (the free server we used) is based on a framework called JSON Server, which is an extremely powerful tool that allows you to create a mock REST server from any arbitrary JSON in just a few minutes! It can very useful for development and testing.

Conclusion

That's it for this article! I hope you found it useful and I encourage you to check out the Artillery Docs if you are interested to learn more!

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