Amazon SageMaker Processingで独自コンテナを使ってみた

こんにちは、小澤です。

当エントリは『機械学習 on AWS Advent Calendar 2019』の16日目です。

今回は、先日re:Inventで発表された新機能であるSageMaker Processingで独自コンテナを使った実装をやってみます。 なお、標準のScikit-learnコンテナを使って利用する方法は以下をご参照ください。

やってみる

では、早速やってみたいと思います。

とはいえ、実はこれは標準のScikit-learnコンテナを利用する場合との違いはそれほど多くありません。 SageMaker Processingでは、前処理などで実行したいコードを記述したスクリプトを実行時の引数で渡します。 そのため、処理内容自体はそちらのスクリプトに含まれているわけです。

独自コンテナを利用する際の違いは以下2点となります。

  • スクリプトで利用するライブラリがインストールされたコンテナを用意する
  • SKLearnProcessor のかわりに ScriptProcessor を利用する

今回は、テキストデータを形態素解析したのち、BoWの形式に変換する処理をProcessingで実装してみます。 これらの処理に際して、形態素解析にはJanome、BoWの生成にはgensimを利用します。

利用するデータに関しては以下のようなものを用意しました。 これの"説明"列をターゲットにします。

コンテナの作成

まず、利用するコンテナを作成します。 これは、今回利用するライブラリがインストールされたものを用意するのみです。

FROM python:3.7-slim-buster
MAINTAINER <あなたの情報>
RUN pip3 install numpy pandas janome gensim
ENV PYTHONUNBUFFERED=TRUE
ENTRYPOINT ["python3"]

学習用の独自コンテナを用意する際は実行するスクリプトを含めたりなどしていましたが、 先ほども記載した通り、Processingではスクリプトは実行時に渡すため、コンテナ作成時には必要ありません。 ただし、ライブラリを利用するために必要なファイルなどをあらかじめ入れてく必要がある場合、この限りではありませんのでご注意ください。

続いて、コンテナをビルドして、ECRにプッシュします。 こちらの処理はノートブック上で実装していますが、コマンドから直接実行したりシェルスクリプトのファイルを用意しても問題ありません。

import boto3
# boto3の機能を使ってリポジトリ名に必要な情報を取得する
account_id = boto3.client('sts').get_caller_identity().get('Account')
region = boto3.session.Session().region_name

ecr_repository = 'alcremie'
tag = ':latest'
alcremie_repository_uri = '{}.dkr.ecr.{}.amazonaws.com/{}'.format(account_id, region, ecr_repository + tag)

!$(aws ecr get-login --region $region --registry-ids $account_id --no-include-email)

# リポジトリの作成
# すでにある場合はこのコマンドは必要ない
!aws ecr create-repository --repository-name $ecr_repository

!docker build -t alcremie .
!docker tag {ecr_repository + tag} $alcremie_repository_uri
!docker push $alcremie_repository_uri

独自コンテナの準備はこれでおしまいです。

スクリプトの実装

続いて、"説明"からBoWを生成する処理のスクリプトを見ていきます。

import os
import numpy as np
import pandas as pd
from janome.tokenizer import Tokenizer
from gensim import corpora, matutils

# データの読み込み
# パスは後述のProcessing実行時の引数とあわせる
alcremie = pd.read_csv('/opt/ml/processing/input/alcremie.csv')
t = Tokenizer()

# 今回は対象の列を決め打ちしてるが、
# Processingでは引数として渡すことも可能
words = list(alcremie['説明'].map(lambda x : [token.surface for token in t.tokenize(x)]))

# 既存の辞書ファイルがある場合、実行時に指定可能な状態とするため
# 存在チェックをして新規に作成するか既存のものを読み込むか選択できるようにしている
if not os.path.exists('/opt/ml/processing/input/alcremie_dic.txt'):
    dic = corpora.Dictionary(words)
else :
    dic = corpora.Dictionary.load_from_text('/opt/ml/processing/input/alcremie_dic.txt')
# 単語をID化して、DataFrameにする
bow_df = pd.DataFrame(matutils.corpus2dense([dic.doc2bow(word) for word in words], num_terms=len(dic)).T)
# 元データとくっつけたものを出力対象とする
result = pd.concat([alcremie, bow_df], axis=1)

# 出力先の指定
# これも実行時の引数とあわせる
try:
    os.makedirs('/opt/ml/processing/output/data')
    os.makedirs('/opt/ml/processing/output/dictonary')
except:
    pass
result.to_csv('/opt/ml/processing/output/data/processed_data.csv')
dic.save_as_text('/opt/ml/processing/output/dictonary/alcremie_dic.txt')

SageMakerに依存しない、普通のPythonスクリプトですね。

Processingの実行

最後にノートブックインスタンスからProcessingを実行する処理を記述していきます。

まずはScriptProcessorのインスタンスを作成します。

from sagemaker import get_execution_role
from sagemaker.processing import ScriptProcessor, ProcessingInput, ProcessingOutput
role = get_execution_role()

script_processor = ScriptProcessor(
    image_uri='<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/alcremie:latest',
    role=role,
    command=['python3'],
    instance_count=1,
    instance_type='ml.m5.xlarge')

command 引数で実行するコマンドを指定するので、Python以外で記述した処理であっても実行可能であることがわかります。 その場合は、コンテナ作成時に必要なものをインストールしておくようにするといいでしょう。

このインスタンスを使ってProcessingの処理を実行します。 実行時のinputs, outputsはそれぞれ先ほどのスクリプトファイルの入出力と対応させておいてください。

script_processor.run(code='preprocessing.py',
    inputs=[ProcessingInput(
        source='alcremie.csv',
        destination='/opt/ml/processing/input')],
    outputs=[
        ProcessingOutput(source='/opt/ml/processing/output/data'),
        ProcessingOutput(source='/opt/ml/processing/output/dictonary')
    ]
)

これで、S3に変換されたファイルが出力されます。 S3上のファイルを取得して確認すると、それぞれの結果が確認できます。

なお、単語IDはそのまま別なファイルで処理を行いたい場合、既存の辞書を渡して利用します。 その場合は、以下のようにinputsで指定します。

script_processor.run(code='preprocessing.py',
    inputs=[
        ProcessingInput(source='alcremie.csv', destination='/opt/ml/processing/input'),
        ProcessingInput(source='alcremie_dic.txt', destination='/opt/ml/processing/input')
    ],
    outputs=[
        ProcessingOutput(source='/opt/ml/processing/output/data'),
        ProcessingOutput(source='/opt/ml/processing/output/dictonary')
    ]
)

おわりに

今回は、SageMaker Processingにて独自コンテナを使った処理を実装してみました。 必要なライブラリがインストールされたコンテナを用意する以外は SKLearnProcessor とほぼ同じなため気軽に利用できるようになっているかと思います。

ちなみに、みなさんはどの姿が一番好きでしょうか? わたしは、ミルキィバニラかミルキィルビーで甲乙つけがたいです。