概要
FlaskでAIとおしゃべりするインターフェースを作るで登場した、私がファインチューニングしたrinnaの日本語GPT-2モデル(以下、もこなGPT)とおしゃべりできるLINE Botを作りました。
参考にした記事と開発中に私がつまづいたところをまとめます。
使用言語、ライブラリなど
- 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)がユーザの入力に対する応答生成を担当します。
開発手順
mokonaGPT関数
参考:AWS SAMとHugging Face Transformersでサーバーレスな推論APIを作る
以下の記事を参考に作成したモデル(outputフォルダとして出力される)のフォルダ名をmodelに変更し、sam init
により作成されたhello_worldディレクトリ直下に配置しました。
rinnaの日本語GPT-2モデルのファインチューニングを試す
つまづいた箇所
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
以下のようなコードにしました。
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 = "<s>" + 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]</s>')[1]
response = response.replace('</s>', '')
end = response.rfind('。')
response = response[:(end+1)]
return response
linebot関数
参考:Lambdaでline-bot-sdk-pythonを使用してオウム返しBOTを作成する
つまづいた箇所
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_’"の原因
そこで、適当な作業用ディレクトリ(仮に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
以下のようなコードにしました。
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
【メモ】 IAMロールを追加する方法(mokonaGPT関数)
「設定」タブの「アクセス権限」タブにあるロール名をクリック
ページをスクロールし、「許可ポリシー」の「許可を追加」プルダウンから「インラインポリシーを作成」をクリック
「JSON」ボタンをクリックし、JSON形式で記述する
mokonaGPT関数とlinebot関数の連携について
参考:PythonであるLambda関数から別のLambda関数を呼び出す
linebot関数とmokonaGPT関数の連携箇所は以下のように記述しました。
linebot関数
# 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関数
# linebot関数から送られた文字列を受け取る
message = event['message']
最後に
本記事にて引用した記事に助けられ、苦節数ヶ月を経てようやくLINE Botを完成させることができました。
記事の作成者の皆様に深く御礼申し上げます。