Ahogrammer

Deep Dive Into NLP, ML and Cloud

AWS LambdaにGiNZAを載せて、固有表現認識APIを作成する

一週間ほど前、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の作成
  • GiNZAをEFSへコピー
  • Lambda関数の作成
  • API Gatewayの作成

EFSの作成

まずはEFSの作成から行います。Lambda関数からEFSにアクセスするには、Lambda関数がEFSに到達できるようにVPCを設定する必要があります。ここでは、各AWSリージョンで自動的に作成されるデフォルトのVPC を使用することにします。

EFSコンソールで、「ファイルシステムの作成」を選択し、default のVPCとそのサブネットが選択されていることを確認します。またすべてのサブネットで、VPC内の他のリソースへのネットワークアクセスを許可するセキュリティグループを使用します。ここでは簡単のために、すべてのトラフィックを許可するセキュリティグループを設定しています。

f:id:Hironsan:20200625083716p:plain

次に、EFSにNameタグを付け、「次のステップ」へ進みます。

次に、「アクセスポイントを追加」を選択します。User IDとGroup IDに1001を使用し、/packagesパスへのアクセスを制限します。

f:id:Hironsan:20200625085342p:plain

これで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とセキュリティグループを指定します。 f:id:Hironsan:20200625091933p:plain

次に、新しく追加された「ファイルシステム」セクションで「ファイルシステムの追加」を選択します。EFSファイルシステムとアクセスポイントには前に作成したものを選択します。ローカルマウントパスには、/mnt/packagesを設定します。これは、アクセスポイントがマウントされるパスであり、EFSの/packagesに対応しています。

f:id:Hironsan:20200625092529p:plain

あとは、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から読み込むといったやり方もありそうです。

参考資料