Lambda + Dockerでscikit-learn実行環境を作成し、Lambdaでテキスト類似度を計算する

# はじめに
本記事ではDockerイメージをECRに格納し、そのコンテナイメージからLambdaでscikit-learnの実行環境を作成します。Lambdaではscikit-learnを使用してテキストの類似度を算出する処理を実装します。

# 環境
- Python 3.12
- Docker： 24.0.7
- Docker Desktop
- AWS CLI（設定済み想定）

# ゴール
- ECRに格納したコンテナイメージを使ってデプロイし、Lambdaを構築
- scikit-learnを使ったテキスト分析をLambdaで実行

# 作るもの
- Dockerイメージ：1つ（Lambda環境構築）
- ECRのリポジトリ：1つ（Dockerイメージの保存）
- Lambda：１つ（テキスト類似度計算）

# 本記事で書かない事
- Lambda実行用のIAMロール作成

# ディレクトリ構成
```
text-similarity/
├── Dockerfile
└── app.py
```

# 1. Dockerfileの作成

次のようなDockerfileを作成します。

```dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.12
# Pythonパッケージのインストール
RUN pip install --no-cache-dir \
    scikit-learn \
    numpy \
    scipy
# イメージ内のLAMBDA_TASK_ROOTにapp.pyをコピー
COPY app.py ${LAMBDA_TASK_ROOT}
# Lambdaが実行する関数を指定
CMD [ "app.lambda_handler" ]
```

# 2. Lambda関数のスクリプト
テキストの類似度を計算する処理（app.py）を実装します。
入力として`texts`（比較対象とする複数のテキスト）と`query`（検索クエリ）を受け取り、それぞれの類似度を計算します。

```python:app.py
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import json


# テキストの類似度を計算
def calculate_similarity(query, texts):
    vectorizer = TfidfVectorizer()

    # クエリと参照テキストを一緒にベクトル化
    all_texts = [query] + texts
    tfidf_matrix = vectorizer.fit_transform(all_texts)
    
    # クエリと各テキストのコサイン類似度を計算
    similarities = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:]).flatten()
    return similarities


def lambda_handler(event, context):
    try:
        # リクエストボディからデータを取得
        body = json.loads(event.get('body', '{}'))
        query = body.get('query', '')
        texts = body.get('texts', [])
        
        # 類似度計算
        similarities = calculate_similarity(query, texts)
        
        # 結果の作成
        results = [
            {'text': text, 'score': float(score)} 
            for text, score in zip(texts, similarities)
        ]
        
        # 最も類似度が高いものを取得
        best_match = max(results, key=lambda x: x['score'])
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'results': results,
                'best_match': best_match
            }, ensure_ascii=False)
        }
        
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({
                'error': str(e)
            })
        }
```

- 参考
    - https://scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting
    - https://scikit-learn.org/stable/modules/metrics.html#cosine-similarity



# 3. Dockerイメージの作成

次のコマンドでカレントディレクトリのDockerfileから `sample-test-lambda`という Dockerイメージを作成します。

```docker
docker build -t sample-test-lambda .
```

イメージが作成できてるか確認したい場合は、次のコマンドで確認できます。

```docker
docker images
```

# 4. ECR
マネジメントコンソール > Amazon Elastic Container Registry からリポジトリを作成し、DockerイメージはECRのリポジトリへプッシュしておいてください。

# 5. Lambdaのデプロイ

マネジメントコンソールからだと、Lambda > コンテナイメージ > イメージを参照 から対象のECRのリポジトリを選択し関数を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/566c05c0-96c8-e9f4-fffe-8869aa85e28a.png)


# 6. 実行テスト

```json:Lambda テストイベント
{
  "body": {
    "query": "How is the weather forecast for this weekend?",
    "texts": [
      "This weekend will be sunny with high temperatures. Expected high is 73°F and low is 59°F. Don't forget your sunscreen.",
      "Next week will be rainy due to low pressure. Remember to bring an umbrella.",
      "Saturday will be clear and comfortable. Mild winds with a high of 75°F and low humidity. Perfect weather for outdoor activities."
    ]
  }
}
```

```json:実行結果
{
  "statusCode": 200,
  "body": {
    "results": [
      {
        "text": "This weekend will be sunny with high temperatures. Expected high is 73°F and low is 59°F. Don't forget your sunscreen.",
        "score": 0.2393
      },
      {
        "text": "Next week will be rainy due to low pressure. Remember to bring an umbrella.",
        "score": 0.0
      },
      {
        "text": "Saturday will be clear and comfortable. Mild winds with a high of 75°F and low humidity. Perfect weather for outdoor activities.",
        "score": 0.1213
      }
    ],
    "best_match": {
      "text": "This weekend will be sunny with high temperatures. Expected high is 73°F and low is 59°F. Don't forget your sunscreen.",
      "score": 0.2393
    }
  }
}
```

ウォームスタート状態の2回目は約36ミリ秒で実行完了しましたが、初回のLambda実行はコールドスタートで約27秒ほどかかりました。メモリは128MBで実行しているのも原因だと思いますが、結構時間がかかった印象です。


```text:実行ログ（1回目）
INIT_REPORT Init Duration: 10008.14 ms
～～～
REPORT RequestId: ～～～ Duration: 16813.38 ms Billed Duration: 16814 ms Memory Size: 128 MB Max Memory Used: 95 MB
```

```text:実行ログ（2回目）
～～～
REPORT RequestId: ～～～	Duration: 35.62 ms	Billed Duration: 36 ms	Memory Size: 128 MB	Max Memory Used: 124 MB	
```

# 参考資料
- [AWS Lambda の新機能 – コンテナイメージのサポート](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/)
- [scikit-learn: TF-IDF](https://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction)