Amazon SageMakerのハイパーパラメータチューニング にパラメータのスケーリング関連の機能が追加されました

こんにちは、大阪DI部の大澤です。

先日Amazon SageMakerにおけるハイパーパラメータチューニング に機能追加が行われたようなので、その内容を紹介します。

追加内容

  • パラメータの探索方法にランダムが追加
    • strategyRandomと設定します
  • パラメータのスケーリングタイプの追加(線形、対数、逆対数)
    • パラメータの設定時の引数scaling_typeに対象のタイプを設定します(Linear, Logarithmic, ReverseLogarithmic)
  • パラメータの探索方法であるベイズ最適化の内部アルゴリズムにWarping(Input Warping)が追加
    • ベイズ最適化選択時に自動的に使用されます

探索手法

SageMakerのパラメータチューニングではパラメータ探索の手法は今回のアップデートによって2種類になりました。

  • ランダム: 指定されたパラメータ範囲をランダムに探索するという至ってシンプルな手法
    • 並列にジョブを回してもチューニングジョブの精度に影響がない
    • →大量にジョブを並列化できる。
  • ベイズ最適化: 指定されたパラメータ範囲の中で学習を繰り返し、前の学習結果を基に獲得関数という各パラメータの評価関数が高い点を効率的に探索する手法
    • 前の学習に依存することから並列ジョブ数を増やすとチューニングジョブの精度に影響が出る可能性がある
    • →並列ジョブ数を増やすことが出来ない

多くのケースではベイズ最適化の方が有効ですが、並列ジョブ数を増やして網羅的に探索したい場合にランダムの方が有効です。 必要用途に応じて使い分けると良さそうです。

Warping

ベイズ最適化では探索の中で選択していくハイパーパラメータでの学習によって得られる目的関数が定常過程に従っているという前提があります。しかし、実際の問題ではそういったケースは稀で非定常過程となるケースの方が多くなるかと思われます。 今回追加された機能であるWarpingは選択されるハイパーパラメータにWarping関数(歪み)をかけることで、学習結果の目的関数の値が定常過程に近づくようにするというものです。またWarping関数はチューニングジョブが進むにつれてパラメータを学習していきます。
この機能によって、ベイズ最適化でのパラメータ探索の改善が期待されます。

より具体的な内容については以下の論文をご覧ください。

やってみる

ハイパーパラメータチューニングで今回追加されたランダムな探索でスケーリングタイプに線形と対数を選択した場合について試してみます。 今回試す内容は以下のノートブックに基づくものです。

学習の内容は次の通りです。

※ 今回はSageMakerのノートブックインスタンス上で動かしています。多くの処理はその他の環境でも動きますが、エラーが出て動かない箇所もあります。ノートブックインスタンスで動かすか、エラーが出る場所を修正して試してください。

準備

まずはIAMロールやデータを保存するS3バケットなどを準備します。


import sagemaker
import boto3
from sagemaker.tuner import IntegerParameter, CategoricalParameter, ContinuousParameter, HyperparameterTuner

import numpy as np                                # For matrix operations and numerical processing
import pandas as pd                               # For munging tabular data
import os 
 
region = boto3.Session().region_name    
smclient = boto3.Session().client('sagemaker')

# ノートブックインスタンスで使っているIAMロールを取得する
role = sagemaker.get_execution_role()

# デフォルトバケットを取得する(必要に応じてバケット名を指定してください)
bucket = sagemaker.Session().default_bucket()                     
prefix = 'sagemaker/DEMO-hpo-xgboost-dm'

データの用意

データをダウンロードし解凍します。

!wget -N --no-check-certificate https://archive.ics.uci.edu/ml/machine-learning-databases/00222/bank-additional.zip
!unzip -o bank-additional.zip

データに必要な処理を施し、S3へアップロードします。

# データを読み込む
data = pd.read_csv('./bank-additional/bank-additional-full.csv', sep=';')
pd.set_option('display.max_columns', 500)     
pd.set_option('display.max_rows', 50)         

# 特徴量を変換します
data['no_previous_contact'] = np.where(data['pdays'] == 999, 1, 0)                                 # pdaysが999を取る場合は1にする
data['not_working'] = np.where(np.in1d(data['job'], ['student', 'retired', 'unemployed']), 1, 0)   # 働いているかどうか
model_data = pd.get_dummies(data)                                                                  # カテゴリ変数をダミー変数に変換する

# 不要なカラムを削除する
model_data = model_data.drop(['duration', 'emp.var.rate', 'cons.price.idx', 'cons.conf.idx', 'euribor3m', 'nr.employed'], axis=1)

# データを分割する
train_data, validation_data, test_data = np.split(model_data.sample(frac=1, random_state=1729), [int(0.7 * len(model_data)), int(0.9*len(model_data))])  

# データをS3に保存する
pd.concat([train_data['y_yes'], train_data.drop(['y_no', 'y_yes'], axis=1)], axis=1).to_csv('train.csv', index=False, header=False)
pd.concat([validation_data['y_yes'], validation_data.drop(['y_no', 'y_yes'], axis=1)], axis=1).to_csv('validation.csv', index=False, header=False)
pd.concat([test_data['y_yes'], test_data.drop(['y_no', 'y_yes'], axis=1)], axis=1).to_csv('test.csv', index=False, header=False)
boto3.Session().resource('s3').Bucket(bucket).Object(os.path.join(prefix, 'train/train.csv')).upload_file('train.csv')
boto3.Session().resource('s3').Bucket(bucket).Object(os.path.join(prefix, 'validation/validation.csv')).upload_file('validation.csv')

s3_input_train = sagemaker.s3_input(s3_data='s3://{}/{}/train'.format(bucket, prefix), content_type='csv')
s3_input_validation = sagemaker.s3_input(s3_data='s3://{}/{}/validation/'.format(bucket, prefix), content_type='csv')

ハイパーパラメータチューニング

ハイパーパラメータチューニング を行います。 まずは学習ジョブをハンドルするEstimatorとハイパーパラメータを設定します。 XGBoostで使えるハイパーパラメータについてはドキュメントをご覧ください。


from sagemaker.amazon.amazon_estimator import get_image_uri

sess = sagemaker.Session()

container = get_image_uri(region, 'xgboost', repo_version='latest')

xgb = sagemaker.estimator.Estimator(
    container,
    role, 
    train_instance_count=1, 
    train_instance_type='ml.m4.xlarge',
    output_path='s3://{}/{}/output'.format(bucket, prefix),
    sagemaker_session=sess
)

xgb.set_hyperparameters(
    eval_metric='auc',
    objective='binary:logistic',
    num_round=100,
    rate_drop=0.3,
    tweedie_variance_power=1.4
)
objective_metric_name = 'validation:auc'

ランダム探索(対数スケール)

対数スケールでランダム探索を行います。 軽く挙動を確認するのが最大ジョブ数は10に設定しています。(元ノートブックでは20になっています。) 対数スケールでパラメータ範囲を設定する場合は、scaling_typeLogarithmicを設定します。

パラメータ範囲の最小値の方で0を指定する場合は対数スケールが使えなません。0に近い値を指定したい場合は1e-8などを指定すると良いようです。

hyperparameter_ranges = {
    'alpha': ContinuousParameter(0.01, 10, scaling_type="Logarithmic"),
    'lambda': ContinuousParameter(0.01, 10, scaling_type="Logarithmic")
}

tuner_log = HyperparameterTuner(
    xgb,
    objective_metric_name,
    hyperparameter_ranges,
    max_jobs=10,
    max_parallel_jobs=10,
    strategy='Random'
)

tuner_log.fit({'train': s3_input_train, 'validation': s3_input_validation}, include_cls_metadata=False)

ジョブが完了するまで待機します。(だいたい10分弱くらいです。)
次の処理でジョブのステータスを確認できます。

boto3.client('sagemaker').describe_hyper_parameter_tuning_job(
    HyperParameterTuningJobName=tuner_log.latest_tuning_job.job_name)['HyperParameterTuningJobStatus']

ジョブが完了していない場合は次のように表示されます。

ジョブが完了すると次のように表示されます。

ランダム探索(線形スケール)

次は線形スケールでランダム探索を行います。 こちらも同様にノートブックから最大ジョブ数を減らして試しました。 今回はscaling_typeLiniearに変更すればOKです。


hyperparameter_ranges_linear = {
    'alpha': ContinuousParameter(0.01, 10, scaling_type="Linear"),
    'lambda': ContinuousParameter(0.01, 10, scaling_type="Linear")
}
tuner_linear = HyperparameterTuner(
    xgb,
    objective_metric_name,
    hyperparameter_ranges_linear,
    max_jobs=10,
    max_parallel_jobs=10,
    strategy='Random'
)

# custom job name to avoid a duplicate name
job_name = tuner_log.latest_tuning_job.job_name + 'linear'
tuner_linear.fit({'train': s3_input_train, 'validation': s3_input_validation}, include_cls_metadata=False, job_name=job_name)

ジョブが完了するまで待機します。(だいたい10分弱くらいです。) 次の処理でジョブのステータスを確認できます。

boto3.client('sagemaker').describe_hyper_parameter_tuning_job(
    HyperParameterTuningJobName=tuner_linear.latest_tuning_job.job_name)['HyperParameterTuningJobStatus']

チューニング結果の確認

チューニングジョブの結果を取得し、パラメータをマップして分布を確認します。

import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt

# ジョブが終了したか確認する
status_log = boto3.client('sagemaker').describe_hyper_parameter_tuning_job(
    HyperParameterTuningJobName=tuner_log.latest_tuning_job.job_name)['HyperParameterTuningJobStatus']
status_linear = boto3.client('sagemaker').describe_hyper_parameter_tuning_job(
    HyperParameterTuningJobName=tuner_linear.latest_tuning_job.job_name)['HyperParameterTuningJobStatus']

assert status_log == 'Completed', "First must be completed, was {}".format(status_log)
assert status_linear == 'Completed', "Second must be completed, was {}".format(status_linear)

# チューニングジョブ結果を取得し、一つのデータフレームにまとめる
df_log = sagemaker.HyperparameterTuningJobAnalytics(tuner_log.latest_tuning_job.job_name).dataframe()
df_linear = sagemaker.HyperparameterTuningJobAnalytics(tuner_linear.latest_tuning_job.job_name).dataframe()
df_log['scaling'] = 'log'
df_linear['scaling'] = 'linear'
df = pd.concat([df_log, df_linear], ignore_index=True)

ジョブで試したパラメータの分布を可視化します。

g = sns.FacetGrid(df, col="scaling", palette='viridis')
g = g.map(plt.scatter, "alpha", "lambda", alpha=0.6)

ジョブ数がそこまで多くないので少々分かりにくいかもしれませんが、左側は対数スケールなので0や1周辺に集まっており、右側は線形なので左下から右上に向かう帯状に概ね分布しています。

さいごに

Amazon SageMakerのハイパーパラメータチューニング の機能アップデートの内容について紹介しました。ハイパーパラメータチューニングの幅が広がり、より効率よく行うことが出来そうです。