特徴量エンジンKaskadaでの機械学習用トレーニングデータの扱い方〜例示による理解

# はじめに

先日、以下の記事を発表しました。

https://qiita.com/yoshiyuki_kono/items/7dc7bdad5e02ff986f5d

新しいテクノロジーに触れる時はいつでも楽しいものです。
中でも新しいプログラミング言語（のパラダイム）を学ぶことは、特別に楽しいことです。

ということで、まだまだ勉強中ですが、公式ドキュメントの学習過程の記録として、以下の記事をまとめてみました。

本稿情報のソースとして、下記ドキュメントを参照ください。

https://kaskada.io/docs-site/kaskada/main/tools-and-resources/training-realtime-ml-models.html

# 特徴量エンジンKaskadaでの機械学習用トレーニングデータの扱い

## 概要

ここでは、Kaskadaでは、機械学習用トレーニングデータがどのように扱われるかを理解するために、具体的な例を用いて説明します。

例として、**モバイル ゲーム用のリアルタイム モデルを構築**するケースを用います。
つまり、モバイルゲームのユーザーが、どのような状況で、サービスに対して課金を行うか？、このような予測を行うケースを考えてみましょう。

予測を行うには、特徴量と、予測のターゲットであるラベルをもつトレーニングデータを使用してモデルをトレーニングする必要があります。


## リアルタイム ML モデルをトレーニングするためのフレームワーク

このようなトレーニングデータの元となる情報として、まず、このシステムでは、ユーザーが行っていることに関する **イベント** を収集して保存しているとします。

次の図は、そのようなイベントを図示したものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/798904c1-517b-2f8a-9648-08ef2830bade.png)

これらのイベントは、ユーザーがいつ勝ったか、いつ負けたか、いつ物を購入したか、いつ相互に会話したかを表しています。

さらに、イベントを時系列に視覚化すると、各イベントのコンテキストと何が起こっているのかを理解できるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/6e5feb09-0783-edda-7fea-fe774a676836.png)

上記から、以下のことが見てとれます。

1. 最初のプレイヤーは自分の勝利を自慢するのが好きなようです
2. 2番目のプレーヤーにとってゲームは難しすぎたようです
3. 3 番目のプレーヤーは、ゲームの負けが混んできたときに課金を行っています。

このような洞察を、モデルのトレーニングに使用する特徴量として扱いたいと思います。
そのための一つの方法として、特徴量の計算結果をタイムラインとして表現し、各イベントが観察されるにつれて特徴量の値がどのように変化するかみていくことが考えられます。
こうしたタイムラインにより、任意の時点での特徴量の値を「観察」することができます。

ここまでの整理により、リアルタイム ML モデルトレーニングのための枠組み（フレームワーク）が得られました。

以下は、その過程を図示したものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/03266a09-f0d7-1b3a-d364-68f7daa0f434.png)


1. はじめに、Rawレベルのイベントから始め、特徴量のタイムラインを計算します。
2. トレーニングデータのサンプルを構築するために、予測が行われる時点での特徴量を観察します。
3. 予測された結果が観察されるまで、時間を進めます。
4. ターゲット値を計算し、例に追加します。

これら 4 つのステップをそれぞれ実装すると、特定の時点での特徴量の入力値と、入力値とは異なる時点で確定するターゲットラベルを含む一連のトレーニング サンプルを入手できます。

# Kaskadaによる実装

では、Kaskada を使用して、このフレームワークをどのように実装できるかを見てみましょう。

## ここで扱うデータ

Kaskada はイベントをテーブル内の行として扱います。

ここでは、ユーザーがゲームをプレイした結果を記述する 2 つのテーブル`GameVictory`と`GameDefeat`を用います。

### `GameVictory`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/b8966848-bf52-51f6-88b8-018c287d75d2.png)


### `GameDefeat`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/106c086f-9b76-d790-94ca-38465efb856e.png)


## ステップ 1: 特徴量を定義する

ステップ 1 は、イベントから特徴量を計算することです。最初の簡単な特徴量として、ユーザーがゲームで負けたときに費やした時間を用います。
負けが多いユーザーはおそらくゲームを優位に進めるためのアイテムに課金する可能性が高くなります。

```javascript
let features = { (1)
  loss_dur: sum(GameVictory.duration) } (2)
```
 1. `loss_dur`という名前の単一フィールドを含むレコードを構築します。
 2. 各勝利イベントの期間フィールドの合計を計算します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/5701bdb3-7d3f-9e62-847b-1e687eb29a4c.png)


ここで定義された`features`は、この特徴量が時間の経過とともにどのように変化したかを表す　**「ステップ関数」**　を示す **「タイムライン」** であることに注意してください。元のイベントが発生した時間に関係なく、いつでもこのステップ関数の値を「観察」できます。

ここで、注目すべき点は、上記のテーブル表記に見て取れるように、結果がユーザーごとに自動的にグループ化されていることです。Kaskada のテーブルには、各行に関連付けられた **「エンティティ」**　が指定されています。そのため、ここで改めて明示的に、ユーザーごとにグループ化する必要はありません。

## ステップ 2: 予測時間を定義する
2 番目のステップは、予測が行われた時点での特徴量を観察することです。
ゲームの提供者は、ユーザーが 2 回連続でゲームに負けた場合には必ず、課金アイテムを提供したいと考えているとします。
`when`オペレーターにより、ユーザーが連続して 2 回失われる特徴量を観察することで、この予測時間に関連付けられた一連のサンプルを構築できます。

```javascript
let features = {
  loss_duration: sum(GameVictory.duration) }

let examples = features (1)
  | when(count(GameDefeat, window=since(GameVictory)) == 2) (2) (3)
```

1. 以前に作成した特徴量レコード
2. パイプ演算子を使用して一連の操作を構築する
3. このクエリは、最新の`GameVictory`イベント以降に発生したイベント`GameDefeat`の数を数えながら、イベントを時系列にたどっていると考えることができます。

このクエリは一連のサンプルを提供します。各サンプルには、予測を行いたい特定の時点で計算された特徴量が含まれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/edf9aeb9-dcf9-cc6c-3974-03b42bac14d5.png)




## ステップ3：時間軸に沿ってシフトする
3 番目のステップは、予測対象となる結果が観察される時点に各サンプルを移動することです。
ユーザーが課金アイテムのオファーを確認し、オファーを受け入れるかどうかを判断し、支払いを行うまでの時間を与えたいとします。
オファーを行ってから 1 時間後にユーザーが受け入れたかどうかを確認してみましょう。

```javascript
let features = {
  loss_duration: sum(GameVictory.duration) }

let examples = features
  | when(count(GameDefeat, window=since(GameVictory)) == 2) (1)
  | shift_by(hours(1)) (2)
```

 1. 以前に作成したサンプル
 2. 最後のステップの結果を時間内で 1 時間後にシフトします（視覚的には、タイムライン内のサンプルを 1 時間後にドラッグすると考えることができます）。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/bce3b9e5-62b0-8634-24c2-97c3f7867994.png)


トレーニングデータのサンプルは、予測したいラベルが観察できる時点に移動しました。時間列の値が前のステップより 1 時間進んでいることに注意してください。

## ステップ 4: 予測ターゲット（ラベル）を追加する
最後のステップは、予測が行われた後に購入が行われたかどうかを確認することです。これが予測のターゲット値となります。これを、現在、特徴量が含まれているレコードに追加します。

```javascript
let features = {
  loss_duration: sum(GameVictory.duration),
  purchase_count: count(Purchase) }   (1)

let example = features
  | when(count(GameDefeat, window=since(GameVictory)) == 2)
  | shift_by(hours(1))

let target = count(Purchase) > example.purchase_count (2)

in extend(example, {target}) (3)
```

 1. 購入数を特徴量として取得する
 2. 予測時とラベル時の購入数を比較します。
 3. 各サンプルに予測ターゲット値を追加します


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/ab871b92-01bd-cc23-07ba-45f3b7d9fa24.png)


このクエリの結果は、モデル アルゴリズムへの入力として使用できるトレーニング データセットになります。

## これまでの整理

最後に、これまでのプロセスを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/aa236b78-f0be-aae2-5275-939ec473cdab.png)


1. 収集したイベントから負け試合に費やした時間を計算しました
2. ユーザーが 2 回連続で負けるたびにトレーニング例を生成しました
3. これらの時系列データサンプルを1 時間先にシフトしました
4. 最後に、予測が行われてからの購入をチェックして予測ターゲット値を計算しました。

## 最後に

Kaskadaにおけるデータの扱いには、独特なところがあります。この記事単体では、理解できない部分もあったかと思いますが、プログラミング言語の文法や、機能の説明からは分かりづらい部分に光を当てる情報として、他の情報と組み合わせて、多角的に利用していただければ幸いです。