🐍

AWS Lambda上でSpeechToTextを動かすため、学習モデルをEFSに逃がした話

2020/12/17に公開

概要

AWS Lambdaでは、ソースコード及び各種Library(pythonの場合はpipで入れるlibrary等)は最大250MB以下という制約があるため、機械学習のモデルなどを使って処理をしたり、容量の必要なライブラリ(Pytorch等)を使うのは工夫が必要です。
その解決手段の一つが、ネットワーク上の共有フォルダのように使えるEFSがLambdaに対応しており、EFS上にモデルやライブラリを配置して使用するという方法です。
最近のトピックとして、Lambdaで大きなファイルを扱いたい場合の選択肢としてDockerコンテナを直接Lambdaにアップロードして使うことが、Re:Invent2020で発表されました。

しかし、機械学習の場合、モデルを頻繁に入れ替えたりしたいことが多く発生するためそのたびにイメージをビルドし直すのは大変です。従って、今回は認識モデルをEFSに配置し、以下のような構成でクライアントから音声ファイルをLambdaへ送り、Speach to Textの認識を行い、認識結果を返すFunctionを作ってみたいと思います。
構成図

AWSセットアップ

セキュリティグループの作成

まず以下の2つのセキュリティグループを作成します。

sg_for_lambda:
インバウンドはなし
アウトバウンドは全てのトラフィックを通す設定

sg_for_efs:
インバウンドに、タイプ「NFS」、TCP、2049、ソースはEC2インスタンスのsg指定)
インバウンドに、タイプ「NFS」、TCP、2049、ソースはsg_for_lambda
の二つを追加して作成

EFSセットアップ

AWS ConsoleからEFSのページへ行き、「ファイルシステムの作成」からファイルシステムを作成します。この時、後程設定するlambdaとは同じネットワークにいないとアクセスできないためVPCやAvailability Zoneは基本的には同じにしておいたほうがトラブルが少ないです。またセキュリティグループは、先ほど作成した「sg_for_efs」を指定します。

EFSファイルシステム画面

ファイルシステム作成後、アクセスポイントの項目から、アクセスポイントの新規作成を行います。こちらは以下のような内容でアクセスポイントを作成します。

ファイルシステム:先ほど作成したものを指定
ルートディレクトリパス:/mnt/efs
POSIXユーザー:
ユーザーID:1001
グループID:1001
ルートディレクトリ作成のアクセス許可:
所有者ユーザーID:1001
所有者グループID:1001
アクセス許可:755

設定後のアクセスポイントの画面はこんな感じになってるかと思います。

EFSアクセスポイント画面

アクセスポイント一覧ページから作成したアクセスポイントの名前をクリック→アタッチ→出てくるコマンドを保存しておきます。

Lambdaとの接続

Lambdaの画面から関数を作成しますが、EFSへのアクセスが必要なので、事前に以下のロールを持つIAMユーザーを作成し、
関数作成時の実行ロールに設定する必要があります。

必要なロール
AWSLambdaVPCAccessExecutionRole
CloudWatchLogsFullAccess
AmazonElasticFileSystemClientReadWriteAccess

Lambda作成時の設定で気を付けるのは以下の辺りです。

ランタイム: Python 3.8
VPC: EFSで設定したVPCと同じもの
ファイルシステム:先ほど作成したものを指定

Lambda作成後、既存のVPCに接続します。(そうしないとEFSが追加できないため)
「ファイルシステムの追加」から先ほど作成したEFSを追加し、ローカルマウントパスは「/mnt/efs」を入力して保存します。

EC2上での音声認識のテスト

いきなりLambdaへアップロードする前にEC2上で動作することを確認します。

EC2上でEFSのマウント

EFSは、EC2インスタンス上からマウントして共有フォルダのように扱えます。Amazon linux2で作成したインスタンスであれば、EFSのユーティリティツールもあり、簡単です。EC2立ち上げと、EFSのマウントはこの辺りを参考にしました。今回の場合、マウントしたいディレクトリは、「/mnt/efs」となります。

インスタンスを作成し、SSH等で入った後、以下のコマンドでこの後で用いるPretrainedなモデルをダウンロードしておきます。

cd /mnt/efs
# Download pre-trained English model files
curl -LO https://github.com/mozilla/DeepSpeech/releases/download/v0.9.3/deepspeech-0.9.3-models.pbmm
curl -LO https://github.com/mozilla/DeepSpeech/releases/download/v0.9.3/deepspeech-0.9.3-models.scorer

Python仮想環境構築

実行に必要なpythonモジュールはあらかじめ用意しておく必要があるため、
あらかじめ、ローカルで作業する時に準備しておきます。
ローカルではvenv等で環境を区切っておくと、後程Lambdaにアップしやすいです。
lambdaのpythonは3.8なので、ローカルで作業する場合も3.8でバージョンを合わせておきます。

mkdir ~/SpeechRecognitionLambda
python3 -m venv .venv
source .venv/bin/activate

SpeechToTextライブラリのインストール(EC2インスタンス上)

DeepSpeechはBaiduが発表した論文をもとに作られたオープンソースの
Speech to Textのエンジンです。これを選んだ理由としては、

  • インストールがpipでできるため簡単!(大事)
  • CPUで推論が動くライブラリであること
  • Pretrainedなモデルが配布されてるため簡単に試せる(英語限定)

です。すぐ動いてくれないとモチベーション下がっちゃいますしね。
ほぼinstruction通りですが、以下のように本体をダウンロードし、インストールします。

# Install deepspeech
pip install deepspeech requests
# Download example audio files
curl -LO https://github.com/mozilla/DeepSpeech/releases/download/v0.9.3/audio-0.9.3.tar.gz
tar xvf audio-0.9.3.tar.gz

上手くインストールされていれば以下のコマンドで入力音声に対する認識結果が標準出力の最終行に表示されるので、それを利用してlambda化を行います。

# Transcribe an audio file
deepspeech --model /mnt/efs/deepspeech-0.9.3-models.pbmm --scorer /mnt/efs/deepspeech-0.9.3-models.scorer --audio audio/2830-3980-0043.wav

Lambdaへのデプロイ

前項までで、EC2インスタンス+EFSの構成ではdeepspeechが上手く動くようになったと思います。続いて、Lambda上で実行するための準備と音声ファイルを送信するためのBase64エンコード部分について説明します。

使用するスクリプト

ローカル側

ローカル側では、オーディオファイルの読み込みと、Base64エンコード、その後Lambdaへポストをするので以下のようなPythonファイルを書きます。実行時に引数にWavファイルパスを指定します。

EncAndPost.py
import json
import base64
import sys
import requests

args = sys.argv
json_dic = {}

# Read audio file from local folder
with open(args[1], 'rb') as audio_file:
    l_audio_data = base64.b64encode(audio_file.read())
    json_dic.setdefault("audio_data", l_audio_data.decode("ascii"))

# post lambda
response = requests.post('xxxx', data=json.dumps(json_dic)) # xxxxは後程設定するロードバランサーのアドレス
print(response.status_code)
print(response.text)

lambda側

Lambda側は、subprocessを利用して、deepspeechを実行して、標準出力に表示された認識結果をresponseのjsonに入れてreturnします。

app.py
import json
import subprocess
import base64
import sys

def lambda_handler(event, context):
    audio_data = event["audio_data"]
    audio_dec = base64.b64decode(audio_data)

    with open("test_lambda.wav", 'bw') as audio_write:
        audio_write.write(audio_dec)
    cp = subprocess.run(['deepspeech', \
                        '--model', '/mnt/efs/deepspeech-0.9.3-models.pbmm', \
                        '--scorer', '/mnt/efs/deepspeech-0.9.3-models.scorer', \
                        '--audio', 'test_lambda.wav'], \
                        encoding='utf-8', \
                        stdout=subprocess.PIPE)
    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "recog_text" : str(cp.stdout)
            }
        ),
    }

外部からLambdaを叩くため、ALBを設定します。参考はこの辺り

Lambdaへのアップロード

Lambdaへは、ライブラリと実行用のpythonファイルをアップロードします。pythonライブラリは、venvで作成した環境の以下のパスのsite-packagesをコピーします。

 cp -r .venv/lib/python3.8/site-packages ~/SpeechRecognitionLambda/upload/site-packages
 cp app.py ~/SpeechRecognitionLambda/upload

後は、uploadフォルダの中身を圧縮して、S3もしくはlambdaの設定ページから直接アップロードすればいいのですが、この時uploadフォルダを圧縮してしまうと解凍時にもuploadフォルダが作成されてしまいパス構成がおかしくなってしまうため、uploadフォルダの中身を直接圧縮するようにします。

アップロード後、pythonライブラリへのパスを通すため、lambdaの環境変数を以下のように設定します。

PYTHONPATH:/var/task/site-packages

実行結果

先ほどのsample audioのうち、8455-210777-0068.wavを送ると以下のような戻りになります。

{"statusCode": 200, "body": "{\"recog_text\": \"your paris sufficient i said\\n\"}"}

終わりに

こんな感じでlambda化できれば自前で音声認識も夢じゃないかも!?
今回はモデルをEFSに置いて入れ替えしやすくしましたが、構成が決まっているのであればコンテナを使ったLambdaもありだと思います(むしろそちらも使いたい、、)。
近いうちに記事書きたいな、、

Discussion