概要
以前の投稿にて、AWSのAIサービスをローカルPCから呼び出すことにチャレンジしました。
そのときはboto3を使ってAIサービスを呼び出しましたが、今回はサーバレスアプリを作成し、そのサーバレスアプリからAIサービスを呼び出してみたいと思います。サーバレスアプリ自体はRESTで呼び出します。
RESTなのでステートレスな呼び出しになるね。
つまり1回のリクエストで完結する機能であることが条件だね。
RESTリクエストをローカルPCから送信するだけではつまらないので、LINEアプリでメッセージ送信してみることにします。
スマホのLINEアプリからLINEボットへ宛ててメッセージを送信し、メッセージを受けたLINEボットがAWSのサーバレスアプリへRESTリクエストをするという仕組みです。
ついでにLINEアプリの開発も試してみようという魂胆だね。
仕組みをざっとまとめておきます。
- スマホのLINEアプリから、LINEボットへメッセージを送信します。
- LINEボットは、受信したメッセージをAWSのAPIGatewayへ転送します。
- APIGatewayは、メッセージを受け取ったらLambdaを起動します。
- Lambdaは、AWSのAIサービスを呼び出します。
- Lambdaは、AIサービスの実行結果をAPI Gateway経由でLINEボットへ返します。
- LINEボットは、受信した結果をスマホのLINEアプリへ返します。
AWSのLambdaを使うので、長時間の処理や厳しいレスポンスを求められる処理はNGだね。
ここでは簡単なAIサービスで試してみよう。
実行するAWSのAIサービスは、Amazon ComprehendとRekognitionとします。
Comprehendは文章の分析、Rekognitionは画像の分析をします。
アプリの動作内容とあわせて開発手順を図にしてみました。
青色の数字マークは開発手順です。
緑色の数字マークはアプリの動作になります。
学習すること
- AWS Lambdaによるサーバレスアプリの作り方
- Amazon ComprehendおよびRekognitionの使い方
- LINEボットの作り方
前提知識
- Python3プログラミング
開発環境
- Python3実行環境(Windows、MacOS、Linux上のPython仮想環境)
※当記事ではWindows10を使用します。
あらかじめLINEアカウントとAWSアカウントを取得しておく必要があります。
開発実施
開発手順はざっと次のとおりです。さきほど掲載した全体図の青色手順に対応しています。
- LINEチャンネルを作成する
- AWSサーバレスアプリ作成プログラムを用意する
- AWSサーバレスアプリをビルドしデプロイする
- AWSのWebhookURLをLINEチャンネルへ登録する
では始めましょう。
1. LINEチャンネルを作成する
LINEの公式Developers Console へアクセスしLINEアカウントでログインします。
(このLINEアカウントは通常使用しているアカウントで構いません)
LINEアプリを作成するためには、まずプロバイダ登録をする必要があります。
まずプロバイダを作成し、その配下へ機能別のサービスチャンネルを作っていきます。
今回作りたいのはメッセージを転送する機能なので、Messaging APIのチャンネルを作成します。
LINE Developers Consoleでの詳細な登録手順は別の記事を参考にしてね。
ページの形式は随時変更されるから。
ここでは概要のみ説明していくよ。
チャンネルを作成するときにシークレットキーとアクセストークンを発行しておきます。これを使ってLINEプロバイダの外部からチャンネルにアクセスできるようになります。
シークレットキーはチャンネル作成時に自動的に作られるけど、
アクセストークンは、チャンネル作成後にチェンネルページで自ら発行しないといけないよ。
LINEプロバイダにMessaging APIチャンネルを作成したので、LINEボットの作成はこれでほぼOKです。次はAWSでサーバレスアプリを作りましょう。
2. AWSサーバレスアプリ作成プログラムを用意する
サーバレスアプリはAWSのサイトにログインしてブラウザコンソール上で作成できますが、今回はローカルのPCから作成することにします。そのためにはまず、AWSのサーバレスアプリ開発ツールであるAWS SAM CLI(AWS Serverless Application Model Command Line Interface、以降SAM-CLIと記述)を、自分の開発用PCへインストールしないといけません。
私の開発用PCはWindows10なので、PowerShellからPython仮想環境を立ち上げ、pipコマンドでSAM-CLIをインストールします。
$pip install aws-sam-cli
加えてあと2つインストールすべきものがあります。
サーバレスアプリからAWSのAIサービスへアクセスするので、boto3をインストールします。
$pip install boto3
LINEボットとやりとりするためにlin-bot-sdkも必要です。
$pip install line-bot-sdk
では、SAM-CLIを使ってサーバレスアプリを作成していきましょう。
作成するのは次の2つです。
- サーバレスアプリの関数記述ファイル
- サーバレスアプリのビルド&デプロイ設定ファイル
まずはサーバレスアプリの本体である関数記述ファイルを作成します。
実装する機能は次のとおりです。
- LINEからテキストメッセージを受信した場合、テキスト感情分析用の関数を呼び出し、AWSのComprehendでテキストの感情を分析して回答を返す。
- LINEから画像を受信した場合、画像に写る人物の感情分析用の関数を呼び出し、AWSのRekognitionで画像の中の人物の感情を分析して回答を返す。
import os
import boto3
from linebot import LineBotApi, WebhookHandler
from linebot.models import MessageEvent, TextMessage, TextSendMessage, ImageMessage
# LINEからメッセージを受信したAPI Gatewayが
# どのLambda関数を呼び出すかを管理するためのWebhookハンドラを作成する。
handler = WebhookHandler(os.getenv('LINE_CHANNEL_SECRET'))
# Lambda関数からLINEへ処理結果を送信するAPIを作成する。
line_bot_api = LineBotApi(os.getenv('LINE_CHANNEL_ACCESS_TOKEN'))
def image_feeling(result):
""" 画像感情分析結果の中で最も確信度が高い感情に対応したメッセージを返す """
message = ""
if len(result["FaceDetails"]) <= 0:
message = "表情が読み取れませんでした・・・"
else:
# ※画像の人物は1人に限定する。
emotions = result["FaceDetails"][0]["Emotions"]
max_conf = 0
type = ""
for e in emotions:
if max_conf < e["Confidence"]:
max_conf = e["Confidence"]
type = e["Type"]
if type == "CALM":
message = "穏やかな表情ですね"
elif type == "HAPPY":
message = "楽しそうですね"
elif type == "SURPRISED":
message = "驚いた様子ですね"
elif type == "SAD":
message = "なんだか悲しそうですね"
elif type == "CONFUSED":
message = "困惑した様子ですね"
elif type == "ANGRY":
message = "怒っていますね"
elif type == "DISGUSTED":
message = "気に障ったことでもあったでしょうか"
elif type == "FEAR":
message = "おびえなくてもいいですよ"
else:
message = "良い表情ですね"
return message
# AWS API Gatewayが呼び出すLambdaハンドラ関数
def lambda_handler(event, context):
headers = event["headers"]
body = event["body"]
signature = headers['x-line-signature']
handler.handle(body, signature)
return {"statusCode": 200, "body": "OK"}
# WebhookハンドラへLambda関数を登録する。
# LINEからテキストメッセージを受信したときに呼ばれるLambda関数を登録する。
@handler.add(MessageEvent, message=TextMessage)
def handle_text_message(event):
input_text = event.message.text
# テキスト感情分析のためにComprehendを使用する。
client = boto3.client('comprehend', 'ap-northeast-1')
# Comprehendで感情分析を行う。
result = client.detect_sentiment(Text=input_text, LanguageCode='ja')
# 最もスコアの高い感情を選択する。
max_score = 0
max_sentiment = 'Neutral'
for senti, sco in result['SentimentScore'].items():
if max_score <= sco:
max_score = sco
max_sentiment = senti
# 返信メッセージを決める。
message_reply = ''
if max_sentiment == 'Positive':
message_reply = 'ハッピーですね。その調子でいきましょう!'
elif max_sentiment == 'Negative':
message_reply = '大変ですね。お気を確かに!'
elif max_sentiment == 'Neutral':
message_reply = 'こんにちは。いつもどおりで何よりです。'
elif max_sentiment == 'Mixed':
message_reply = 'うーん、なんと言ったらいいのでしょう。'
else:
message_reply = input_text
# LINEへメッセージを返信する。
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=message_reply))
# WebhookハンドラへLambda関数を登録する。
# LINEから画像を受信したときに呼ばれるLambda関数を登録する。
@handler.add(MessageEvent, message=ImageMessage)
def handle_image_message(event):
# LINEユーザから送られてきた画像コンテンツを取得する。
message_content = line_bot_api.get_message_content(event.message.id)
# 画像コンテンツを、AWSのTemporalディレクトリへ一時的に置く。
# 【改善課題】S3バケットを活用すべき。
file_path = '/tmp/sent-image.jpg'
with open(file_path, 'wb') as fd:
for chunk in message_content.iter_content():
fd.write(chunk)
# 画像感情分析のため、Rekognitionを使用する。
client = boto3.client('rekognition', 'ap-northeast-1')
# Rekognitionで画像の感情分析をする。
with open(file_path, 'rb') as fd:
sent_image_binary = fd.read()
response = client.detect_faces(Image={"Bytes": sent_image_binary}, Attributes=["ALL"])
# 分析した感情をもとに返信メッセージを決める
message_reply = image_feeling(response)
# LINEへメッセージを返信する。
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=message_reply))
# Temporalディレクトリに置いた画像を削除しておく。
os.remove(file_path)
それぞれの関数について説明を補足しておきます。
関数名 | 補足説明 |
---|---|
image_feeling | 画像の感情分析結果をもとに返信メッセージを決める関数。handle_image_messageから呼び出している。 |
lambda_handler | LINEからメッセージを受信したAPI Gatewayが呼び出すハンドラ関数。このあと解説するサーバレスアプリのビルド&デプロイ設定ファイルにハンドラとして指定されている。 |
handle_text_message | LINEからテキストメッセージを受信したときに呼び出されるLambda関数。lambda_handlerにてWebhookHandlerが呼び出している。 |
handle_image_message | LINEから画像を受信したときに呼び出されるLambda関数。lambda_handlerにてWebhookHandlerが呼び出している。 |
つづいて、サーバレスアプリのビルド&デプロイ設定ファイルを作成します。
このファイルは、SAM-CLIでサーバレスアプリをビルド&デプロイするときに参照するJSON形式のテンプレートファイルになります。
AWSの公式ドキュメントにテンプレートが用意されているので、ダウンロードしてちょこっとだけカスタマイズします。
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Parameters": {
"LineChannelAccessToken": {"Type": "String", "Description": "LINE のアクセストークン"},
"LineChannelSecret": {"Type": "String", "Description": "LINE のチャンネルシークレット"}
},
"Resources": {
"EndPointFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Runtime": "python3.8",
"CodeUri": "src",
"Handler": "bot-crowd.lambda_handler",
"Environment": {"Variables": {
"LINE_CHANNEL_ACCESS_TOKEN": {"Ref": "LineChannelAccessToken"},
"LINE_CHANNEL_SECRET": {"Ref": "LineChannelSecret"}
}},
"Policies":[{"RekognitionDetectOnlyPolicy": {}},
{"ComprehendBasicAccessPolicy": {}}],
"Events": {
"API": {
"Type": "Api",
"Properties": {"Path": "/api_endpoint", "Method": "post"}
}
}
}
}
},
"Outputs": {
"ApiEndpointURL": {
"Description": "API Endpoint URL",
"Value": {"Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${ServerlessRestApi.Stage}/api_endpoint"}
}
}
}
カスタマイズしたのは次の箇所です。
- 5行:デプロイするとき、LINEのアクセストークンを入力するよう開発者へ促す設定を追加する。
- 6行:デプロイするとき、LINEのシークレットキーを入力するよう開発者へ促す設定を追加する。
- 13行:”CodeUri”へLambdaファイルの場所を設定する。
- 14行:”Handler”へLambdaファイルの中のハンドラ関数名を設定する。
- 15~17行:デプロイするときにユーザが入力したアクセストークンとシークレットキーを、AWS Lambdaの環境変数として設定するようにしておく。
- 19~20行:LambdaがComprehendとRekognitionへアクセスできるようにポリシーを追加する。
- 30~34行:デプロイしたあと、WebhookエンドポイントURLをコンソールに表示するよう設定しておく。
3. AWSサーバレスアプリをビルドしデプロイする
サーバレスアプリの作成に必要なファイルは作成できたので、いよいよビルドしてデプロイします。
開発用のローカルPCにて適当な作業場所へ移動します。
- サーバレスアプリのビルド&デプロイ設定ファイル(template-bot-crowd.json)はその直下へ置く。
- サーバレスアプリの関数記述ファイル(bot-crowd.py)は、直下にsrcフォルダを作成しその下へ置く。
コマンドプロンプトにて(私の場合はWindowsなのでPowerShellを起動)、SAM-CLIでソースファイルをビルドします。
sam build -t template-bot-crowd.json
-t オプションでテンプレートファイルを指定しない場合は、template.jsonという名前のファイルを自動的にテンプレートとして読み込むそうです。
無事ビルドが終わったら、引き続きデプロイします。
sam deploy --guided
–guided オプションを付けることによって、LINEボット名、AWSリージョン、つづけてtemplate-bot-crowd.jsonで指定したLINEアクセストークンとシークレットキーの入力を促されます。
この –guided オプションを付けてこれらの情報を入力しておけば、次回のデプロイからは省略できます。
ちなみにここで指定したLINEボット名が、
LINEアプリでメッセージを送るときの宛先アカウント名になるよ。
デプロイが無事完了すると、コマンドラインにWebhookエンドポイントのURLが表示されます。
これは、template-bot-crowd.jsonの34行目 “Outputs”: {“Value”} へ設定しておいたためです。
4. AWSのWebhookURLをLINEチャンネルへ登録する
デプロイのときに発行されたWebhookエンドポイントのURLをLINEのチャンネルへ設定します。
これによって、LINEチャンネルが外部からLINEメッセージを受け取ったときの転送先が決まります。
LINEのDevelopers Consoleへログインし、今回作成したチャンネルページを開き、[Webhook設定]を編集してエンドポイントURLを設定します。
これで開発は全て終了です。では実行してみましょう。
いよいよ実行!
スマホのLINEアプリでLINEボットへテキストメッセージと画像を送信します。
宛先アカウント名は、デプロイのときに指定したボット名だよ。
意図した結果が得られました。
ちなみにAWSのLambdaは従量課金制です。
呼び出すたびに料金が発生するので注意して使用してください。
今後の課題
- Rekognitionで画像の感情分析をするとき、今回は人物が1人であることを前提としました。人物が複数の場合や写っていない場合の考慮も本来ならば必要です。
- AWSのAIサービスを呼び出したときの例外処理も実装しておくべきです。とりわけRekognition呼び出した後は /tmp に置いた画像を削除しないといけないので(削除しなくてもいずれ自動削除されますが)、本来ならば必要な処理です。
- LINEから送信された画像を /tmp に置きましたが、正式な機能であればS3バケットに置いて処理すべきです。
まとめ
サーバレスアプリをAWSのLambdaで作成し、LambdaからAWSのAIサービスを呼び出す仕組みを作ってみました。サーバレスアプリの呼び出しは単純なRESTリクエストではなく、LINEアプリからのメッセージ送信という方法を選択しました。
AWS Lambdaの実行時間は15分までという制限があるため、バッチ処理のような長時間の処理には向いていません。さらにLambdaは呼び出されるつどロードされるため、低レイテンシを求められる機能にも不向きであると言えるでしょう。
しかしWebアプリを作成する場合、通常ならばWebサーバーやアプリケーションサーバーを作らないといけません。簡単な機能であれば、サーバレスアプリは効率の良い選択肢と言えるのではないでしょうか。
アプリのプログラミングやデプロイについても、サンプルやテンプレートが用意されているので開発も容易であると感じました。
適材適所だね!
Python: 3.8
aws-sam-cli: 1.36.0
boto3: 1.18.57
line-bot-sdk: 2.0.1
コメント