一週間ほど前、AWS LambdaにElastic File System(EFS)をマウントできる機能が追加されました。この機能を使うことで、マウントしたEFS上への読み書きがLambda関数からできるようになりました。これまではLambdaの制限により、/tmp
で使用可能な容量が512MBなので、大きなファイルの読み込みは難しかったのですが、EFSを使うことでそれが可能になります。特に機械学習系のパッケージやモデルの容量は何かと大きいので、新機能の恩恵に与ることになります。
そういうわけで、本記事ではEFSに日本語の自然言語処理ライブラリであるGiNZAを置いて、それをLambdaから呼び出してみようと思います。実のところ、GiNZAのパッケージは400MB程度なので、/tmp
に載せることもできるはずです。その場合は、Lambda LayersとLambdaを組み合わせて、S3上に置いたモデルを/tmp
に読み込んでくることになると思います。実際、そのような方法をspaCyをLambdaに載せるときに採用したことがありますが、今回はEFSを使ってやってみます。
❯ curl -X POST \ -d '{"text":"太郎は東京都出身だ"}' \ https://example.execute-api.us-east-1.amazonaws.com/v1/analyze-entities | jq [ { "text": "太郎", "label": "Person", "start": 0, "end": 2 }, { "text": "東京都", "label": "Province", "start": 3, "end": 6 } ]
手順は以下の通りです。
EFSの作成
まずはEFSの作成から行います。Lambda関数からEFSにアクセスするには、Lambda関数がEFSに到達できるようにVPCを設定する必要があります。ここでは、各AWSリージョンで自動的に作成されるデフォルトのVPC を使用することにします。
EFSコンソールで、「ファイルシステムの作成」を選択し、default のVPCとそのサブネットが選択されていることを確認します。またすべてのサブネットで、VPC内の他のリソースへのネットワークアクセスを許可するセキュリティグループを使用します。ここでは簡単のために、すべてのトラフィックを許可するセキュリティグループを設定しています。
次に、EFSにNameタグを付け、「次のステップ」へ進みます。
次に、「アクセスポイントを追加」を選択します。User IDとGroup IDに1001
を使用し、/packages
パスへのアクセスを制限します。
これでEFSの作成は完了です。次のステップへ進みましょう。
GiNZAをEFSへコピー
Amazon Linux 2上にEFSをマウントするディレクトリを作成し、作成したEFSをマウントします。EFSをマウントするために、amazon-efs-utils
をインストールします。
sudo yum install -y amazon-efs-utils mkdir efs sudo mount -t efs fs-[YOUR EFS ID]:/ efs
EFSをマウントできたら、GiNZAをインストールします。
mkdir ginza pip install -t ./ginza/ ginza
インストールが完了したら、パッケージをEFSへ移動します。
sudo chown -R 1001:1001 ginza sudo mv ginza efs/packages/
これで、LambdaからGiNZAを使う準備ができました。
Lambda関数の作成
次に、固有表現認識を行うためのLambda関数を作成します。Lambdaコンソールで、AnalyzeNamedEntity関数を作成し、ランタイムとしてPython 3.7を選択します。アクセス権限には、 AWSLambdaVPCAccessExecutionRoleおよびAmazonElasticFileSystemClientReadWriteAccess を持ったロールを指定します。
関数を作成したら、VPCの設定を行います。ここでは、EFSに設定したものと同じデフォルトVPCとセキュリティグループを指定します。
次に、新しく追加された「ファイルシステム」セクションで「ファイルシステムの追加」を選択します。EFSファイルシステムとアクセスポイントには前に作成したものを選択します。ローカルマウントパスには、/mnt/packages
を設定します。これは、アクセスポイントがマウントされるパスであり、EFSの/packages
に対応しています。
あとは、Lambdaのコードエディタに、以下のコードを貼り付けて保存します。
import sys sys.path.append('/mnt/packages/ginza') import spacy def lambda_handler(event, context): nlp = spacy.load('ja_ginza') doc = nlp(event['text']) response = [ { 'text': ent.text, 'label': ent.label_, 'start': ent.start_char, 'end': ent.end_char } for ent in doc.ents ] return response
試しに、{"text": "太郎は東京都出身だ"}
というデータでテストをしてみると、以下のレスポンスが返ってきます。
[ { "text": "太郎", "label": "Person", "start": 0, "end": 2 }, { "text": "東京都", "label": "Province", "start": 3, "end": 6 } ]
ちなみに、メモリの割当を大きくしておかないと以下のエラーが発生します。メモリの大きさとタイムアウトについては適切な値を設定しておきましょう。
{ "errorType": "Runtime.ExitError", "errorMessage": "RequestId: b93ab443-c91b-4bb3-a0cb-bab486335751 Error: Runtime exited with error: signal: killed" }
API Gatewayの作成
ここまできたら後は簡単です。API Gatewayでリソースとメソッドを作成し、AnalyzeNamedEntity関数に結びつけるだけです。ここではリソースとしてanalyze-entities
、メソッドとしてPOST
、デプロイのステージとしてv1
を指定します。デプロイが完了すると、以下のようにしてAPIを叩くことが出来ます。
❯ curl -X POST \ -d '{"text":"太郎は東京都出身だ"}' \ https://example.execute-api.us-east-1.amazonaws.com/v1/analyze-entities | jq [ { "text": "太郎", "label": "Person", "start": 0, "end": 2 }, { "text": "東京都", "label": "Province", "start": 3, "end": 6 } ]
以上です。今回はパッケージをまるごとEFSから読み込みましたが、パッケージはLambda Layersに置いておいて、モデルだけEFSから読み込むといったやり方もありそうです。