BQMLに新たに追加されたTRANSFORM句で、モデルの前処理を自動的に行う

# これなに?
F81アドベントカレンダー二日目担当の長谷川です。
BQMLに新たに追加された`TRANSFORM`句についての解説します。2019/12/2時点で、まだ日本語の公式ドキュメントが存在しないことから、記事にしようと思いました。なお、現時点ではまだこの機能は`Beta`です。[英語の公式ドキュメント](https://cloud.google.com/bigquery-ml/docs/reference/standard-sql/bigqueryml-syntax-create)は存在するので、興味があれば、こちらも参考することをお勧めします。  
今回の記事では、BigQuery(ML)の基本事項は一切説明しません。BQMLで使用できる関数などについては[前記事](https://qiita.com/Hase8388/items/5631ffd31380fb5337c8)を参照してください。

# TRANSFORM句とは?

**行いたい前処理をモデル構築時に定義し、予測、評価時に自動的に実行する**ためにしようするSQLの句(clause)です。  
これにより、BQMLで作成するアルゴリズムとそれに伴う前処理を一体化させ、モデルを構築することができます。  
**前処理をモデルの中に集約し、隠蔽できるので、モデルを使う側は行われる前処理について何も知らなくても良くなります。**


イメージはこんなかんじ

- 使わない場合

![スクリーンショット 2019-11-30 19.10.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119585/494baa9a-94d0-b311-c8ed-a992ee25ba3f.png)

- 使う場合

![スクリーンショット 2019-11-30 19.10.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119585/aeaab7e7-e4aa-4b7d-ebbb-4891b94ecbba.png)

# 具体例

今回は例として、`bigquery-public-data.samples.natality` 配下にある、新生児のデータを使用し、出産時の体重を目的変数とするモデルを構築してみたいと思います


```sql
SELECT
  weight_pounds, -- 目的変数
  is_male, --性別
  plurality,--出産で生まれた子供の数
  gestation_weeks, -- 妊娠期間
  alcohol_use,-- 母親が飲酒してたか
  cigarette_use -- 母親がタバコを吸っていたか
FROM
  `bigquery-public-data.samples.natality`
LIMIT
  5
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>weight_pounds</th>
      <th>is_male</th>
      <th>plurality</th>
      <th>gestation_weeks</th>
      <th>alcohol_use</th>
      <th>cigarette_use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>7.625790</td>
      <td>True</td>
      <td>NaN</td>
      <td>38</td>
      <td>None</td>
      <td>None</td>
    </tr>
    <tr>
      <td>1</td>
      <td>7.438397</td>
      <td>False</td>
      <td>1.0</td>
      <td>38</td>
      <td>None</td>
      <td>None</td>
    </tr>
    <tr>
      <td>2</td>
      <td>8.437091</td>
      <td>False</td>
      <td>1.0</td>
      <td>41</td>
      <td>None</td>
      <td>None</td>
    </tr>
    <tr>
      <td>3</td>
      <td>7.374463</td>
      <td>True</td>
      <td>1.0</td>
      <td>99</td>
      <td>None</td>
      <td>None</td>
    </tr>
    <tr>
      <td>4</td>
      <td>5.813590</td>
      <td>False</td>
      <td>1.0</td>
      <td>99</td>
      <td>None</td>
      <td>None</td>
    </tr>
  </tbody>
</table>
</div>



BQMLの場合、そのままデータを入力しても基本的な前処理は自動で行ってくれますが、自分で特徴量を作成したほうがより良い精度が期待できます。
そこで、以下のような特徴量を作成します。

- 多胎児で生まれたか否か
- 妊娠期間が(一般的な期間である)37-42週に当てはまっているかどうか
- (母親が)アルコールを摂取していたかと、タバコを吸っていたかの交差特徴量


```sql

SELECT
  weight_pounds,
  is_male,
  IF(plurality > 1, 1, 0) AS plurality,
  ML.BUCKETIZE(gestation_weeks, [37, 42]) AS gestation_weeks,
  ML.FEATURE_CROSS(
    STRUCT(
      CAST(alcohol_use AS STRING) AS alcohol_use,
      CAST(cigarette_use AS STRING) AS cigarette_use
    )
  ) AS alcohol_cigarette_use
FROM
  `bigquery-public-data.samples.natality`
LIMIT
  5
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>weight_pounds</th>
      <th>is_male</th>
      <th>plurality</th>
      <th>gestation_weeks</th>
      <th>alcohol_cigarette_use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>6.311835</td>
      <td>False</td>
      <td>0</td>
      <td>bin_2</td>
      <td>{'alcohol_use_cigarette_use': 'true_true'}</td>
    </tr>
    <tr>
      <td>1</td>
      <td>6.062712</td>
      <td>True</td>
      <td>0</td>
      <td>bin_2</td>
      <td>{'alcohol_use_cigarette_use': None}</td>
    </tr>
    <tr>
      <td>2</td>
      <td>8.728101</td>
      <td>False</td>
      <td>0</td>
      <td>bin_2</td>
      <td>{'alcohol_use_cigarette_use': 'true_true'}</td>
    </tr>
    <tr>
      <td>3</td>
      <td>6.946766</td>
      <td>True</td>
      <td>0</td>
      <td>bin_2</td>
      <td>{'alcohol_use_cigarette_use': 'true_true'}</td>
    </tr>
    <tr>
      <td>4</td>
      <td>6.999677</td>
      <td>True</td>
      <td>0</td>
      <td>bin_2</td>
      <td>{'alcohol_use_cigarette_use': 'true_true'}</td>
    </tr>
  </tbody>
</table>
</div>



上記の関数については、[公式ドキュメント](https://cloud.google.com/bigquery-ml/docs/reference/standard-sql/bigqueryml-preprocessing-functions?hl=ja)か、[以前の記事](https://qiita.com/Hase8388/items/5fcc9f056d44105d186e)を参照してください。

さて、問題はここからです。上記の処理をした上でモデルに食わせ、学習させる必要がありますが、**そのままだと、評価、予測時に,もう一度同じ前処理を行った上でモデルに入力する必要があります。**  
これだと、二度同じ処理が発生するともに、うっかり学習時と違う処理をしてしまったりして、面倒なバグを生む温床になりかねません

### 学習(TRANSFORM句を使わない)


```sql
-- TRANSFORM句を使わない場合
CREATE MODEL `transform_tutorial.natality_model` OPTIONS (
  model_type = 'linear_reg',
  input_label_cols = ['weight_pounds']
) AS
SELECT
  weight_pounds,
  is_male,
  IF(plurality > 1, 1, 0) AS plurality,
  ML.BUCKETIZE(gestation_weeks, [37, 42]) AS gestation_weeks,
  ML.FEATURE_CROSS(
    STRUCT(
      CAST(alcohol_use AS STRING) AS alcohol_use,
      CAST(cigarette_use AS STRING) AS cigarette_use
    )
  ) AS alcohol_cigarette_use
FROM
  `bigquery-public-data.samples.natality`
WHERE
  weight_pounds IS NOT NULL 
  AND RAND() < 0.001 -- 適当にサンプリング
```







### 予測


```sql
-- TRANSFORM句を使わない場合
SELECT
  predicted_weight_pounds
FROM
  ML.PREDICT(
    MODEL `transform_tutorial.natality_model`,
    (
      SELECT
        is_male,
        -- イチイチ同じ前処理を実行しなければいけない
        IF(plurality > 1, 1, 0) AS plurality,
        ML.BUCKETIZE(gestation_weeks, [37, 42]) AS gestation_weeks,
        ML.FEATURE_CROSS(
          STRUCT(
            CAST(alcohol_use AS STRING) AS alcohol_use,
            CAST(cigarette_use AS STRING) AS cigarette_use
          )
        ) AS alcohol_cigarette_use
      FROM
        `bigquery-public-data.samples.natality`
      LIMIT
        5
    )
  )
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>predicted_weight_pounds</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>7.658442</td>
    </tr>
    <tr>
      <td>1</td>
      <td>7.385880</td>
    </tr>
    <tr>
      <td>2</td>
      <td>7.385880</td>
    </tr>
    <tr>
      <td>3</td>
      <td>7.385880</td>
    </tr>
    <tr>
      <td>4</td>
      <td>7.385880</td>
    </tr>
  </tbody>
</table>
</div>



同じ前処理を予測時にも行わければいけないため、メンドイですね。

## TRANSFORMを使う

そこで、`TRANSFORM`句の出番です。モデル学習時に、以下のように行う前処理の関数とともに定義します。

### 学習(TRANSFORM句を使用)


```sql
CREATE MODEL `transform_tutorial.natality_model_with_trans` TRANSFORM(
  -- 前処理の関数を定義
  weight_pounds,
  is_male,
  IF(plurality > 1, 1, 0) AS plurality,
  ML.BUCKETIZE(gestation_weeks, [37, 42]) AS gestation_weeks,
  ML.FEATURE_CROSS(
    STRUCT(
      CAST(alcohol_use AS STRING) AS alcohol_use,
      CAST(cigarette_use AS STRING) AS cigarette_use
    )
  ) AS alcohol_cigarette_use
) OPTIONS (
  model_type = 'linear_reg',
  input_label_cols = ['weight_pounds']
) AS
SELECT
  *
FROM
  `bigquery-public-data.samples.natality`
WHERE
  weight_pounds IS NOT NULL -- 適当にサンプリング
  AND RAND() < 0.001

```







### 予測


```sql
SELECT
  predicted_weight_pounds
FROM
  ML.PREDICT(
    MODEL `transform_tutorial.natality_model_with_trans`,
    (
      SELECT *
      FROM
        `bigquery-public-data.samples.natality`
      LIMIT
        5
    )
  )
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>predicted_weight_pounds</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>7.646798</td>
    </tr>
    <tr>
      <td>1</td>
      <td>7.376921</td>
    </tr>
    <tr>
      <td>2</td>
      <td>7.376921</td>
    </tr>
    <tr>
      <td>3</td>
      <td>7.715983</td>
    </tr>
    <tr>
      <td>4</td>
      <td>7.446106</td>
    </tr>
  </tbody>
</table>
</div>



`TRANSFORM`句を使わない場合と比較すると、元のデータを読み込ませるだけで自動的に前処理が実行されるので、予測のクエリがだいぶ簡略化できますね。  また、モデルの評価のときも同様に前処理を省略できます。さらに、**前処理について何も知らなくても、ただ生データを読み込ませるだけで良いので、モデルの使い勝手がいいです**

# 最後に

BQMLはまだサービスを開始してから間もないですが、続々と新しいアルゴリズム、前処理用の関数などが出てきています。これからも新しい機能が発表されたらまたまとめてきたいです。