AWSでrinnaの日本語GPT-2モデルとおしゃべりするLINE Botを作る

# 概要
[FlaskでAIとおしゃべりするインターフェースを作る](https://qiita.com/MokonaSato/items/09ba9e5e21698cd21a52)で登場した、私がファインチューニングしたrinnaの日本語GPT-2モデル（以下、もこなGPT）とおしゃべりできるLINE Botを作りました。
参考にした記事と開発中に私がつまづいたところをまとめます。
![LINE_capture_711939977.244120.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/5182d620-e22d-1765-1d50-fe96fc772be1.jpeg)


# 使用言語、ライブラリなど
- Python 3.9
- torch 1.13.1
- sentencepiece 0.1.97
- transformers 4.26.0
- AWS SAM CLI 1.90.0
- line-bot-sdk

使用したPCはMacbook Air（M1チップ）です。

# アプリケーション全体図
linebot関数（API GatewayとLambda）がLINEとのメッセージのやり取りを担当し、mokonaGPT関数（Lambda）がユーザの入力に対する応答生成を担当します。
![Qiita1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/8bcda34c-4b07-b26f-f8ed-e3fce3cd18a4.png)


# 開発手順
## mokonaGPT関数
参考：[AWS SAMとHugging Face Transformersでサーバーレスな推論APIを作る](https://qiita.com/oddgai/items/0d86e13014f898b6f811)

以下の記事を参考に作成したモデル（outputフォルダとして出力される）のフォルダ名をmodelに変更し、`sam init`により作成されたhello_worldディレクトリ直下に配置しました。
[rinnaの日本語GPT-2モデルのファインチューニングを試す](https://note.com/npaka/n/n9f2f6aa0eb0b)

### つまづいた箇所
#### Template.yaml
記事で言及されていた箇所に加え、`Resources`以下の`Architectures`を`x86_64`から`arm64`に変更しました（macOSで開発しているため）。
また、記事では`MemorySize`を5000としていましたが、3000としました。

#### Dockerfile
`TRANSFORMERS_CACHE`を`/tmp/cache/transformers`としました。
記事のままでデプロイすると以下のエラー（CloudWatchのログ）が出たためです。
```
There was a problem when trying to write in your cache folder (/opt/cache/transformers). 
You should set the environment variable TRANSFORMERS_CACHE to a writable directory.
```

#### app.py
以下のようなコードにしました。
~~~python
from transformers import T5Tokenizer, AutoModelForCausalLM


def lambda_handler(event, context):

    tokenizer = T5Tokenizer.from_pretrained("rinna/japanese-gpt2-medium")
    model = AutoModelForCausalLM.from_pretrained("/opt/model")
    # linebot関数から送られた文字列を受け取る
    message = event['message']
    # 応答生成
    message_input = "
" + message + "[SEP]"
    tokenized = tokenizer.encode(message_input, return_tensors="pt")
    output = model.generate(tokenized, do_sample=True, max_length=60, num_return_sequences=8, 
                            top_p=0.95, top_k=20, bad_words_ids=[[1], [5]])
    response = tokenizer.batch_decode(output)[0]
    # 生成された応答を整形
    response = response.split('[SEP]
')[1]
    response = response.replace('
', '')
    end = response.rfind('。')
    response = response[:(end+1)]
    return response
~~~

## linebot関数
参考：[Lambdaでline-bot-sdk-pythonを使用してオウム返しBOTを作成する](https://qiita.com/konikoni428/items/fd1ab5993bc5526726bb)

### つまづいた箇所
#### line-bot-sdk
作業ディレクトリ（仮にlinebotとします）で`python -m pip install line-bot-sdk -t .`を実行し、`line-bot-sdk` の構成ファイルをダウンロードします。
しかし、`botocore`が`urllib3`の最新版に対応していないため、`boto3`をインポートすると以下のエラー（CloudWatchのログ）が出てしまいます。
```
Runtime.ImportModuleError: Unable to import module 'lambda_function': cannot import name 'DEFAULT_CIPHERS' from ‘urllib3.util.ssl_'
```
参考：[”cannot import name 'DEFAULT_CIPHERS' from ‘urllib3.util.ssl_’"の原因](https://qiita.com/maechi/items/6d5107bc526784a026b3)

そこで、適当な作業用ディレクトリ（仮にurllibとします）を作成し、そこで`pip install urllib3==1.25.11 -t .`を実行して`urllib3`のversion 1.25.11をダウンロードします。
linebotディレクトリ内のurllib3フォルダとurllib3-2.0.4.dist-infoフォルダを削除し、urllibディレクトリ内のurllib3フォルダとurllib3-1.25.11.dist-infoに置き換えました。

#### lambda_function.py
以下のようなコードにしました。
~~~python
import os
import sys
import json
import boto3

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
)
from linebot.exceptions import (
    LineBotApiError, InvalidSignatureError
)
import logging

logger = logging.getLogger()
logger.setLevel(logging.ERROR)

channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
if channel_secret is None:
    logger.error('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if channel_access_token is None:
    logger.error('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)


def lambda_handler(event, context):
    if "x-line-signature" in event["headers"]:
        signature = event["headers"]["x-line-signature"]
    elif "X-Line-Signature" in event["headers"]:
        signature = event["headers"]["X-Line-Signature"]
    body = event["body"]
    ok_json = {"isBase64Encoded": False,
               "statusCode": 200,
               "headers": {},
               "body": ""}
    error_json = {"isBase64Encoded": False,
                  "statusCode": 500,
                  "headers": {},
                  "body": "Error"}

    @handler.add(MessageEvent, message=TextMessage)
    def message(line_event):
        text = line_event.message.text
        # mokonaGPT関数を呼び出し、ユーザがLINEから送信した文字列（text）を渡す
        payload = json.dumps({"message": text})
        response = boto3.client('lambda').invoke(
        FunctionName = 'mokonaGPT-HelloWorldFunction-********',
        InvocationType='RequestResponse',
		 Payload = payload
        )
        # mokonaGPT関数の戻り値を処理する
        body = response['Payload'].read()
        resp = body.decode('unicode-escape')
        resp = resp.replace('"', '')
        # 応答生成が上手くいかなかったら「にゃーん」と返す
        if (resp == ""):
            resp = "にゃーん"
        line_bot_api.reply_message(line_event.reply_token, TextSendMessage(text=resp))
        

    try:
        handler.handle(body, signature)
    except LineBotApiError as e:
        logger.error("Got exception from LINE Messaging API: %s\n" % e.message)
        for m in e.error.details:
            logger.error("  %s: %s" % (m.property, m.message))
        return error_json
    except InvalidSignatureError:
        return error_json

    return ok_json

~~~

#### 関数の実行時間
mokonaGPT関数の実行に時間がかかるため、linebot関数の実行時間は長めに設定しました。
実行時間が短かった時のエラー文（CloudWatchのログ）は以下の通りでした。
```
Task timed out after 3.01 seconds
```
実行時間は「設定」タブの「一般設定」タブから変更しました。
![Qiita3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/b999237a-f169-2739-5725-b29bac676451.png)


### 【メモ】 IAMロールを追加する方法（mokonaGPT関数）
「設定」タブの「アクセス権限」タブにあるロール名をクリック
![Qiita2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/8d55044f-b9fd-95e1-7fe3-45381cf7fd35.png)
ページをスクロールし、「許可ポリシー」の「許可を追加」プルダウンから「インラインポリシーを作成」をクリック
![Qiita4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/39c506ca-ae40-7244-3789-e402e37640f4.png)
「JSON」ボタンをクリックし、JSON形式で記述する
![Qiita5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/a81ae042-b6f6-5cf5-7669-55f315608b6c.png)


## mokonaGPT関数とlinebot関数の連携について
参考：[PythonであるLambda関数から別のLambda関数を呼び出す](https://qiita.com/kazono/items/c5a86ecf994c9f19f5a3)
linebot関数とmokonaGPT関数の連携箇所は以下のように記述しました。
#### linebot関数
~~~python
# mokonaGPT関数を呼び出し、ユーザがLINEから送信した文字列（text）を渡す
payload = json.dumps({"message": text})
response = boto3.client('lambda').invoke(
        FunctionName = 'mokonaGPT-HelloWorldFunction-********',
        InvocationType='RequestResponse',
		Payload = payload
        )
    
# mokonaGPT関数の戻り値を処理する
body = response['Payload'].read()
resp = body.decode('unicode-escape')
~~~

#### mokonaGPT関数
~~~python
# linebot関数から送られた文字列を受け取る
message = event['message']
~~~
参考：[Python の Lambda 関数ハンドラー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html)

## 最後に
本記事にて引用した記事に助けられ、苦節数ヶ月を経てようやくLINE Botを完成させることができました。
記事の作成者の皆様に深く御礼申し上げます。