機械学習 (ML) パイプラインに継続的インテグレーション & 継続的デリバリー (CI/CD) を組み込むと、本番環境への ML モデルのデプロイを大規模に行えるようになるなど、多数のメリットが得られます。

この記事では、AWS SageMaker でのモデルのトレーニングとデプロイを CircleCI の CI/CD パイプラインに組み込む方法について解説します。今回使用するプロジェクトは、複数のモデルをまとめた monorepo (モノレポ) 構造です。monorepo 方式には、polyrepo (ポリレポ) 方式と比べ、依存関係のバージョンやセキュリティの脆弱性を管理しやすいなどの利点があります。

このチュートリアルで使用するコードは、こちらの Github リポジトリにあります。

環境変数

まず、CircleCI 上のプロジェクトに AWS の認証情報を用意しましょう。使用するプロジェクトの設定に移動して [Environment Variables (環境変数)] をクリックし、[Add Variable (変数を追加)] ボタンをクリックして新しい環境変数の名前と値を入力します。追加した環境変数は、後ほど Python スクリプトで os.environ を使用して取得します。

今回のサンプルプロジェクトでは、CircleCI の環境変数に AWS のアクセスキーと SageMaker 実行ロールの ARN を格納します。なお、AWS_ACCESS_KEY_ID 環境変数および AWS_SECRET_ACCESS_KEY 環境変数は、boto3 セッションの作成時に boto3 により自動的に取得されます。そのため、これらの環境変数の名前は変えないでください。

CircleCIに保存されたシークレット

CircleCI コンフィグファイルで environment キーを使用して、CI/CD ジョブ固有の環境変数を宣言、格納します。この手順は必須ではありませんが、覚えておくと役に立ちます。

environment:
  MODEL_NAME: abalone-model
  MODEL_DESC: abalone model description text

上記の環境変数は、Python スクリプトで以下のようにして取得します。

model_name = os.environ["MODEL_NAME"] model_description = os.environ["MODEL_DESC"] role_arn = os.environ["SAGEMAKER_EXECUTION_ROLE_ARN"]

モデル

今回のチュートリアルでは、AWS のドキュメントに登場する Abalone と Churn という 2 つのモデルを利用します。

Churn では、携帯電話事業者向けの合成データセットを使用します。このデータセットの詳細については、SageMaker のドキュメント (英語) をご覧ください。今回は、”true/false” の 2 値を取る “Churn?” 変数について予測させてみましょう。ターゲット変数がバイナリであるため、モデルでは 2 値予測を行うことになります。XGBoost フレームワークを使用して、2 値分類モデルを作成します。

Abalone のデータセットは、カリフォルニア大学アーバイン校 (UCI) のデータリポジトリから作成されたものです。元データセットの詳細については、こちら (英語) をご覧ください。このデータセットでは、身体計測の値から Abalone の年齢を予測させてみましょう。年齢はさまざまな数値を取るため、回帰分析により予測を行うことになります。先ほどと同じく XGBoost フレームワークを使用しますが、こちらではリグレッサーモデルを作成します。

これらのモデルは別々のフォルダーに格納されており、各フォルダーには以下の 3 ファイルが用意されています。

  1. gather_data.py
  2. train_register.py
  3. deploy.py

gather_data.py

このファイルでは、対応するモデル用のデータをダウンロードし、前処理を行ってから、S3 に前処理後のデータをアップロードします。SageMaker の仕様に従い、トレーニング用データセットと検証用データセットは別々のフォルダーにアップロードします。

# Upload training and validation data to S3
csv_buffer = io.BytesIO()
train_data.to_csv(csv_buffer, index=False)
s3_client.put_object(Bucket=bucket, Body=csv_buffer.getvalue(), Key=f"{model_name}/train/train.csv")

csv_buffer = io.BytesIO()
validation_data.to_csv(csv_buffer, index=False)
s3_client.put_object(Bucket=bucket, Body=csv_buffer.getvalue(), Key=f"{model_name}/validation/validation.csv")

train_register.py

このファイルでは、モデルをトレーニングしてからモデルレジストリに登録します。このファイルを実行する際に初めて、CI/CD パイプラインで SageMaker を活用します。

SageMaker の XGBoost Estimator (推定器) を設定する際には、output_path で、モデルアーティファクトを出力する S3 パスを指定できます。このコードは SageMaker ノートブックの外部で実行するため、SageMaker セッションと SageMaker の実行ロール ARN を指定する必要があります。こうすることで、情報の自動取得時に間違いが起きないようにしています。

# Configure training estimator
xgb_estimator = Estimator(
    base_job_name = model_name,
    image_uri = image_uri,
    instance_type = "ml.m5.large",
    instance_count = 1,
    output_path = model_location,
    sagemaker_session = sagemaker_session,
    role = role_arn,
    hyperparameters = {
        "objective": "reg:linear",
        "max_depth": 5,
        "eta": 0.2,
        "gamma": 4,
        "min_child_weight": 6,
        "subsample": 0.7,
        "verbosity": 2,
        "num_round": 50,
    }
)

モデルのトレーニングが完了したら、モデルパッケージを SageMaker のモデルレジストリにプッシュします。SageMaker では、モデルとモデルパッケージは異なる概念です。モデルとは、エンドポイントにデプロイし推論を行わせるオブジェクトを指します。モデルパッケージには、モデルの重み、評価結果、設定ファイルなど、モデルに関連するすべてのアーティファクトが含まれます。今回は、モデルではなく、モデルパッケージをモデルレジストリにプッシュします。

モデルレジストリを活用するのは、デプロイなどの後続のステップを実行するときや、モデルのバージョンをロールバックするときに、トレーニング済みのモデルを参照しやすくするためです。なお、CircleCI の承認ジョブでモデルの承認を管理できるように、モデルパッケージの承認を事前に行っています。

# Retrieve model artifacts from training job
model_artifacts = xgb_estimator.model_data

# Create pre-approved cross-account model package
create_model_package_input_dict = {
    "ModelPackageGroupName": model_name,
    "ModelPackageDescription": "",
    "ModelApprovalStatus": "Approved",
    "InferenceSpecification": {
        "Containers": [
            {
                "Image": image_uri,
                "ModelDataUrl": model_artifacts
            }
        ],
        "SupportedContentTypes": [ "text/csv" ],
        "SupportedResponseMIMETypes": [ "text/csv" ]
    }
}

create_model_package_response = sagemaker_client.create_model_package(**create_model_package_input_dict)

deploy.py

このファイルでは、最新の承認済みモデルパッケージをモデルエンドポイントにデプロイします。具体的には、モデルエンドポイントが存在しない場合には新しく作成し、存在する場合は更新を行います。

最新の承認済みモデルパッケージを取得する際には、SageMaker の関数を使用して既存のモデルパッケージを作成日の降順でリスト表示し、先頭のモデルパッケージの ARN を取得します。

# Get the latest approved model package of the model group in question
model_package_arn = sagemaker_client.list_model_packages(
    ModelPackageGroupName = model_name,
    ModelApprovalStatus = "Approved",
    SortBy = "CreationTime",
    SortOrder = "Descending"
)['ModelPackageSummaryList'][0]['ModelPackageArn']

次に、モデルパッケージからモデルを作成します。

# Create the model
timed_model_name = f"{model_name}-{current_time}"
container_list = [{"ModelPackageName": model_package_arn}]

create_model_response = sagemaker_client.create_model(
    ModelName = timed_model_name,
    ExecutionRoleArn = role_arn,
    Containers = container_list
)

その後、このモデルを使用するエンドポイント設定ファイルを作成します。

# Create endpoint config
create_endpoint_config_response = sagemaker_client.create_endpoint_config(
    EndpointConfigName = timed_model_name,
    ProductionVariants = [
        {
            "InstanceType": endpoint_instance_type,
            "InitialVariantWeight": 1,
            "InitialInstanceCount": endpoint_instance_count,
            "ModelName": timed_model_name,
            "VariantName": "AllTraffic",
        }
    ]
)

最後に、新しい設定ファイルでエンドポイントを更新します。

create_update_endpoint_response = sagemaker_client.update_endpoint(
    EndpointName = model_name,
    EndpointConfigName = timed_model_name
)

ダイナミックコンフィグ

今回は monorepo 方式を採用しているので、変更したモデルについてのみ CI/CD が実行されるようにしなくてはなりません。このようにしないと、たとえば Abalone モデルに変更をマージした際に、Churn モデルのトレーニングとデプロイも行われてしまいます。そこで役に立つのが、CircleCI のダイナミックコンフィグ機能です。ダイナミックコンフィグを使うと、指定したフォルダーに変更があったかどうかを検出して、変更があった場合にパイプラインパラメーターの値を設定できます。そして、このパイプラインパラメーターで、CI/CD パイプラインのどのワークフローを実行するかを指定できます。

プロジェクトでダイナミックコンフィグを利用するには、いくつかの準備が必要です。詳しい手順は、こちらのドキュメントを参照してください。

セットアップ用のコンフィグファイル

ダイナミックコンフィグを使用するには、まずセットアップ用のコンフィグファイルを用意する必要があります。今回のサンプルリポジトリでは、config.yml がこれに該当します。このファイルでは、コードが変更されたフォルダーを特定するために、path-filtering Orb を使用しています。

注意すべきこととして、ファイルを main ブランチのものと比較して、特定のフォルダーに変更があればパラメーターの値に反映しています。たとえば、abalone_model フォルダーで変更が見つかった場合、パイプラインパラメーター deploy-abalone を true に設定します。そして、パスのフィルタリングとパイプラインパラメーター値の更新の後に、トリガーするコンフィグファイルのパスを指定しています。

base-revision: main
mapping: |
  abalone_model/.* deploy-abalone true
  churn_model/.* deploy-churn true
config-path: ".circleci/dynamic_config.yml"

実際に実行するコンフィグファイル

セットアップ用コンフィグでパイプラインパラメーターの値を更新したら、次は実際のコンフィグファイルを実行します。今回のサンプルリポジトリでは、dynamic_config.yml というファイルが該当します。このコンフィグファイルの中身を理解しやすいように、abalone-model ワークフローだけに注目してみましょう。

workflows:
  abalone-model:
    when: << pipeline.parameters.deploy-abalone >>
    jobs:
      - abalone-model-train:
          filters:
            branches:
              ignore:
                - main
      - request-deployment:
          type: approval
          filters:
            branches:
              ignore:
                - main
          requires:
            - abalone-model-train
      - abalone-model-deploy:
          filters:
            branches:
              only:
                - main

このワークフローは、パイプラインパラメーター deploy-abalone が true のときにのみ実行されます。まず abalone-model-train ジョブを実行します。このジョブでは、train_register.py ファイルを実行します。

次に request-deployment ジョブを実行します。これは承認ジョブであり、以降のワークフローの実行を CircleCI 上で手動で承認するようにユーザーに求めます。この際、承認担当者は SageMaker でモデルの評価指標を確認してから、エンドポイントへのモデルのデプロイを許可します。承認が行われると、abalone-model-deploy ジョブで deploy.py を実行します。

ここで、トレーニングジョブと承認ジョブでは main ブランチを無視しているのに対し、デプロイジョブには only キーを指定し、main ブランチでのみ実行するよう設定していることに注意してください。こうすることで、開発者が開発用ブランチでモデルを更新した場合に、デプロイをトリガーすることなく、新バージョンのモデルのトレーニングを行えます。そして、コードの変更が承認され、main ブランチにマージされた場合には、モデルの再トレーニングを行うことなく、デプロイジョブをトリガーできます。

CircleCI のパイプライン

CircleCI で開発用ブランチに Abalone モデルのコード変更がプッシュされると、ダイナミックコンフィグ機能により Abalone モデルのトレーニングパイプラインだけが選択的に実行されます。request-deployment 承認ジョブは、main ブランチへのコード変更のマージを “制御する” 役割を担います。承認が行われると、GitHub でプルリクエストをマージ可能になります。

開発者ブランチでは、トレーニングパイプラインのみが実行

コード変更が main ブランチにマージされると、ダイナミックコンフィグ機能により、Abalone モデルのデプロイパイプラインだけが選択的に実行されます。

メインブランチではデプロイパイプラインのみが実行

まとめ

このチュートリアルでは、CircleCI と AWS SageMaker を組み合わせて、エンドツーエンドの機械学習 (ML) パイプラインを構築する方法について解説しました。このパイプラインでは、モデルのトレーニングおよびエンドポイントへのデプロイのプロセス全体を自動化し、リアルタイムでの推論を実現しています。

また、本チュートリアルのプロジェクトでは、各モデルを個別のフォルダーに格納する monorepo 方式を採用しました。そして、CircleCI のダイナミックコンフィグ機能を利用して、コードが変更されたモデルにあわせて各パイプラインの調整を行っています。

チュートリアルに使用したコードは、こちらの Github リポジトリから入手できます。

CircleCI があれば、機械学習開発の目標達成にかかる時間を短縮できます。また、CircleCI には、ML モデルのトレーニングに役立つツールも多数用意されています。こちらで詳細をご覧ください。

関連記事とガイド