DMM.comの、一番深くておもしろいトコロ。

構造化されたデータをGCPでMachine Learning

構造化されたデータをGCPでMachine Learning

はじめに

こんにちは! CTO室、兼AI部の古川です。DMMでは、社内の業務効率や顧客向けサービスの品質改善のためにMachine Learning(ML)を活用しており、そこで、専門性の高いML技術の研究開発や、社内のML活用推進活動をしているのがAI部です。

今年8月24日には、Google Cloud Platform(GCP)を活用してML導入を検討するハンズオンを社内で開催しました。

前回のハンズオンでは、あらかじめGCPから提供されているML Building Blocksを活用して課題を解決しました。しかし、より複雑な課題を解決するためにはML EngineやAutoMLを用いてモデルを作る必要があるという話でした。

そこで、Google CloudのSolutions Architectである中井悦司さんを講師にお招きし、社内のエンジニアを対象に、より複雑なビジネス課題を解決するためのGCPのMLツールの使い方についてのハンズオンを9月11日に開催いたしました。

今回の記事では、その内容の一部をご紹介したいと思います。

ハンズオンの目標

今回のハンズオンの目標は以下の2つでした。

  • 構造化データを用いて機械学習モデルを構築する
  • 学習したモデルをデプロイし、Webアプリから利用する

具体的には、Natality Dataset(1969年〜2008年の米国における出産記録を集めた公開データセット)を用いて、特徴量(妊娠期間、 胎児の性別など)から新生児の体重を予測するMLモデルを構築します。

Natality DatasetはGoogle BigQueryの一般公開データセットの一つです。約1億3800万もの出産記録が保存されています。

アーキテクチャの全体像

Natality Datasetを使ってMLモデルを構築し、APIとして利用可能な形でデプロイするための、GCPを活用したアーキテクチャを下図に示します(一部簡略化しています)。

開発環境には、Cloud Datalabを使用します。Datalabは、事前に各種フレームワークとGCPの認証情報がセットアップされたJupyter Notebookです。

データの解析

まず初めに、データの解析と変換を行います。

MLにおいて、未加工のロー・データを扱うことはあまりありません。サンプルされたデータには欠損やバイアスなど多くのノイズが存在し、モデルの性能を大きく左右します。学習のエビデンスはデータであり、データのエラーはモデルのエラーとなります。統計的ないしはヒューリスティックな特徴量の選択や変換は、しばしばモデルの学習効率や精度に寄与するため、MLでは必ず行われます。

データの解析は、可視化によって行います。Datalabでは、BigQueryのクエリ結果を簡単にPandasのDataFrameで取得することができます。

import google.datalab.bigquery as bq

query = '''
SELECT
  weight_pounds,
  is_male,
  mother_age,
  plurality,
  gestation_weeks,
  FARM_FINGERPRINT(CONCAT(CAST(YEAR AS STRING), CAST(month AS STRING))) AS hashmonth
FROM
  publicdata.samples.natality
WHERE year > 2000
LIMIT 100
'''
df = bq.Query(query).execute().result().to_dataframe()
df.head()

f:id:ornew:20181005185613p:plain
BigQueryへのクエリの実行結果はPandasのDataFrameで取得でき、ノートブック上で簡単に表示できる。

目的変数は、weight_pounds(胎児の重さ、単位はポンド)です。他のカラムが、説明変数として使えるのか、どうすれば使えるようになるのかを判断していく必要があります。

例えば、plurality(胎児の数)について着目してみます。胎児の数ごとのデータ数と平均体重をグラフで可視化してみます。

query = '''
SELECT
  {0},
  COUNT(1) AS num_babies,
  AVG(weight_pounds) AS avg_wt
FROM
  publicdata.samples.natality
WHERE
  year > 2000
GROUP BY
  {0}
'''
df = bq.Query(query.format('plurality')).execute().result().to_dataframe()
df = df.sort_values('plurality')
df.plot(x='plurality', y='avg_wt', kind='bar')
df.plot(x='plurality', y='num_babies', logy=True, kind='bar')

f:id:ornew:20181005185728p:plain
(上段)胎児の数ごとの平均体重 (下段)胎児の数ごとのデータ数

グラフを見ると、双子、三つ子と胎児が増えるにつれて、平均体重が減少しているのが確認できます。胎児の数と体重には関係がありそうですね。

しかし、よく見るとデータ数も胎児数が増えるにつれて少なくなっています。カテゴリごとのサンプル数に偏りがある場合は注意が必要です。わかりやすい例として、今度は母親の年齢に着目して、同じようにプロットしてみます。

df = bq.Query(query.format('mother_age')).execute().result().to_dataframe()
df = df.sort_values('mother_age')
df.plot(x='mother_age', y='avg_wt');
df.plot(x='mother_age', y='num_babies');

f:id:ornew:20181005190725p:plain
(上段)母親の年齢ごとの平均体重 (下段)母親の年齢ごとのデータ数

母親の年齢と平均体重のグラフが、なんとも言えない形になりました。30代中頃をピークに最も大きく、30代前後から遠ざかるほど小さくなるようにも見えますが、両端付近で平均体重が急激に変化しています。

この原因は、下段の母親の年齢とデータ数の関係を見るとわかります。データのほとんどは20代〜30代に分布しており、それ以外の年代(特に、10代前半と50代後半)はほとんどデータがありません。平均値は、データが少なければ少ないほど、外れ値の影響を大きく受けてしまいます。直感的に考えれば、母親の年齢は体力や栄養状態と関係があり、胎児の体重にも影響を与えている可能性は高いような気がしますし、この平均値が真の値に近いとは考えにくいですね。このようにデータの数に偏りがある場合には、データの質に影響が出ることがあります。MLでは、データ数が均一になるように境界を設定した離散変数に変換するなどの対処が一般的です。

データの鮮度にも気をつける必要があります。今回のデータセットは1969年〜2008年のデータが含まれますが、1960年代の、半世紀前の胎児のデータは、果たして2018年以降に産まれる未来の胎児の予測でも有効でしょうか。おそらく、生活習慣や母親の栄養状態、医療技術のレベルなど、出産を取り巻く環境は1960年代と現代では大きく変化しているはずで、それに影響されて胎児の体重の分布も変化しているはずです。もちろん、データできちんと確認する必要があります。もしも予測したいシチュエーションが時代による影響を強く受けるような場合は、データの鮮度に気をつけねばなりません。重要なことは、予測したいシチュエーションとデータのシチュエーションが一致することです。具体的な境界の決定や重み付けについては、テストを行い、より良い結果を出すものを探索する必要があります。なお、ハンズオンでは2000年以降のデータに絞って学習を行いました。

解析の結果、学習に用いる特徴量を決定したあとは、データの変換を行います。

データの変換

データの分析を済ませられたら、学習に有効な特徴量である整形された教師データへとロー・データを変換します。古いデータのカット、不均衡なデータのバケット化、学習用と評価用にデータの分割などを行います。

MLでは、学習に使ったデータに対する性能は評価として適していないとみなされます。なぜなら、学習用データはモデルにとって既知のデータであるからです。実際には、未知のデータに対する予測精度を必要としているため、あらかじめ学習に使わないデータを評価用として分けておく必要があります。データの分割も、変換の処理とセットで行います。

また、データの分割には、年月日のハッシュを使用します。これは、シャッフルで選んでしまうと、期間をずらした時に使用するデータが変わってしまい、モデル間の厳密な比較ができなくなる可能性があるためです。データが普遍的でなく、定期的に更新する必要のあるモデルにおいては、特に気をつける必要があります。

今回のハンズオンでは、Apache Beamで処理を実装し、Cloud Dataflowで実行しました。結果はCSVでCloud Storage上に保存します。

モデルの学習

モデルの実装にはTensorFlowを用います。モデルの構造は、Wide & Deepを採用します。Wide & Deepは、離散変数に対する線形モデル(Wide)と、連続変数や埋め込みに対する多層ニューラルネットワーク(Deep)の二つのモデルを組み合わせたモデル構造です。Wide & Deepについては、GoogleのAIチームのブログに解説があります。

ai.googleblog.com

TensorFlowの高レベルAPIであるEstimatorを用いて実装をします。入力となる特徴量(Feature)のカラムを定義します。

import tensorflow as tf

def feature_columns():
  """ モデルの入力となる特徴量(wide, deep) """

  # 性別(カテゴリ変数: 男、女、不明)
  is_male = tf.feature_column.categorical_column_with_vocabulary_list('is_male', ['True', 'False', 'Unknown'])

  # 母親の年齢(数値変数)
  mother_age = tf.feature_column.numeric_column('mother_age')
  # バケット化した母親の年齢(カテゴリ変数)
  age_buckets = tf.feature_column.bucketized_column(mother_age, boundaries = np.arange(15,45,1).tolist())

  # 胎児数(カテゴリ変数: 1人、2人、3人、4人、5人、2人以上)
  plurality = tf.feature_column.categorical_column_with_vocabulary_list('plurality', [
    'Single(1)', 'Twins(2)', 'Triplets(3)', 'Quadruplets(4)', 'Quintuplets(5)','Multiple(2+)'
  ])

  # 妊娠週(数値変数)
  gestation_weeks = tf.feature_column.numeric_column('gestation_weeks')
  # バケット化した妊娠週(カテゴリ変数)
  gestation_buckets = tf.feature_column.bucketized_column(gestation_weeks, boundaries = np.arange(17,47,1).tolist())

  # 線形モデルに使うカテゴリ変数:
  #  性別、胎児数、バケット化した母親の年齢、バケット化した妊娠週
  wide = [
    is_male, plurality, age_buckets, gestation_buckets,
  ]

  # カテゴリ変数のクロスを埋め込み
  crossed = tf.feature_column.crossed_column(wide, hash_bucket_size=20000)
  embed = tf.feature_column.embedding_column(crossed, 3)

  # ニューラルネットワークに使う連続変数
  #  母親の年齢、妊娠週、カテゴリ変数のクロスの埋め込み
  deep = [
    mother_age,
    gestation_weeks,
    embed,
  ]
  return wide, deep

定義した特徴量から、Wide & Deepによる回帰モデルを操作するためEstimator(モデルの学習や評価、エクスポートなどを行うインターフェイス)をtf.estimator.DNNLinearCombinedRegressorで構築します。

def train_and_evaluate(model_dir):
  wide, deep = feature_columns()
  estimator = tf.estimator.DNNLinearCombinedRegressor(
                model_dir              = model_dir,
                linear_feature_columns = wide,
                dnn_feature_columns    = deep,
                dnn_hidden_units       = [64, 32])
  train_spec = tf.estimator.TrainSpec(
                 input_fn  = read_dataset('train'),
                 max_steps = TRAIN_STEPS)
  eval_spec = tf.estimator.EvalSpec(
                input_fn = read_dataset('eval'),
                steps    = None)
  tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)

Estimatorを実行するために必要なものを含めたPythonパッケージを作り、gcloudコマンドで学習ジョブを実行しましょう。Pythonのパッケージは通常どおりsetup.pyを書くだけですが、モデルの学習を実行する関数(上記例であればtrain_and_evaluate関数)が呼び出されるモジュールを作り、学習ジョブの送信時に--module-nameで指定する必要があることにだけ気をつけましょう。このあたりの作法については、GCPの公式ガイドを読むのが良いでしょう。

$ gcloud ml-engine jobs submit training $JOBNAME \
>   --region=$REGION \
>   --module-name=trainer.task \
>   --package-path=$(pwd)/babyweight/trainer \
>   --job-dir=$OUTDIR \
>   --staging-bucket=gs://$BUCKET \
>   --scale-tier=STANDARD_1 \
>   --runtime-version 1.4 \
>   -- \
>   --bucket=$BUCKET \
>   --output_dir=$OUTDIR \
>   --train_steps=20000

学習が完了したら、モデルをWeb APIとして利用可能なようにデプロイします。学習ジョブの結果はCloud Storage上にあるため、そのURLを指定してコマンドを実行するだけです。

$ gcloud ml-engine models create $MODEL_NAME \
>   --regions $REGION
$ gcloud ml-engine versions create $MODEL_VERSION \
>   --model $MODEL_NAME \
>   --origin $MODEL_LOCATION \
>   --runtime-version 1.4

デプロイされたAPIを実際に呼び出してみましょう。Pythonであれば、以下のように書けます。

import json

from googleapiclient import discovery
from oauth2client.client import GoogleCredentials

credentials = GoogleCredentials.get_application_default()
api = discovery.build('ml', 'v1', credentials=credentials)

body = {
  'instances': [
    {
      'is_male': 'True',
      'mother_age': 26.0,
      'plurality': 'Single(1)',
      'gestation_weeks': 39
    },
  ]
}

name = 'projects/%s/models/%s/versions/%s' % (PROJECT, MODEL_NAME, MODEL_VERSION)
response = api.projects().predict(body=body, name=name).execute()
print(json.dumps(response, sort_keys=True, indent=2))

実行すると、以下のようにレスポンス結果が表示されます。どうやら、予測結果は約7.6ポンド(約3.4kg)であるようです。

{
  "predictions": [
    {
      "predictions": [
        7.640133514404297
      ]
    }
  ]
}

ハンズオンでは、このAPIを使ったWebアプリケーションをApp Engineでデプロイしました。

まとめ

今回のハンズオンは、アプリエンジニアが参加するにはなかなかハードルの高いものであったように思いますが、業務時間内であったにも関わらず、中井さんのハンズオンを受けようと、スキルや所属を問わず社内から多くの参加者が集まりました。GCPを使うことで、面倒な環境のセットアップも不要で、すぐにモデルの開発を始めることができます。さらにはデータ変換から学習まで、特別な知識を必要とせずとも、すぐに並列に分散処理を行うことができました。アジリティを求められるML開発において、本質的なモデル開発に必要なことだけに集中できるのは、クラウドサービスの魅力ですね。

採用情報

現在、CTO室では、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい!

dmm-corp.com