Amazon Comprehend + Lambda + Slack でお手軽に感情分析をする


# はじめに

はじめまして。オークファン入社歴三年目になりました、 @aucfan-0540 です。業務では主に AWS を用いたインフラ環境の構築等に携わっています。アドベントカレンダーを書くのは三回目です。今までは以下のような記事を書いてきました。

 * <a href="https://qiita.com/aucfan-0540/items/01e33583fe76318fb3b1">無限にホメてくれる VSCode Power Mode で自己肯定感を爆上げする</a>
   * とにかく誰かに褒めてもらいたいんだ、という熱い思いから制作しました。
 * <a href="https://qiita.com/aucfan-0540/items/252a32c623a56ee5e8b4">Zabbix をバージョンアップしたらメンテナンスの設定を間違えて関係各所をざわつかせてしまった話</a>
   * 今でも Zabbix のアラームが鳴り響くと「また何かやっちゃいましたか？」となります。

今回は `Amazon Comprehend + Lambda + Slack` でお手軽に `Amazon Comprehend` を触りつつ、文章の感情分析をしてみたいと思います。:grin:

# Amazon Comprehend とは

公式サイト様から引用させていただきます。

https://aws.amazon.com/jp/comprehend/

 > Amazon Comprehend は、機械学習を使用して、テキストからインサイトや関係性を発見するための自然言語処理 (NLP) サービスです。

上記の通り、文章を解析して以下のような情報を得ることができるサービスです。

 * エンティティ解析
   * 固有名詞や日付等の情報を取得します。
 * キーフレーズ抽出
   * 重要度の高いと思われる単語を抽出します。
 * 感情分析
   * 感情を読み取り、ポジティブ、ネガティブ、中立、複合のいずれかにカテゴライズします。
 * 言語判定
   * 用いられている言語を判定します。 

今回は「感情分析」の機能を使用します。

# 構成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/57c854c4-0f8a-88c7-1fe9-0be43670d188.png)

ノートの片隅に書いた落書きみたいになってしまったので、文章でも軽く説明させていただきます。

ユーザーが `Slack` へ文章を投稿すると、投稿内容の情報をもったイベントが `LambdaFunction` へ送られます。`LambdaFunction` はイベントの中から投稿された文章を取り出し `Amazon Comprehend` を用いて解析を行った後、結果を `Slack` に投稿します。


# 作成手順

※ 記事内の画像について、諸般の事情でお見せできない部分については弊社のロゴでマスクをしています。

## 1. Lambda 作成

Slack の設定等は後回しにして、とりあえず Lambda の設定と関数の作成を行っていきたいと思います。
今回ランタイムは`Python 3.9`で作成しました。

最終的に使用したコードが以下です。

```python
import json
import boto3
import requests

comprehend = boto3.client('comprehend')
slack_webhook_url = "SlackのWebhookurl"

def lambda_handler(event, context):
    
    body_object = json.loads(event['body'])
    # bot による投稿であるかを判定する
    if 'subtype' in body_object['event']:
        subtype_value = body_object['event']['subtype']
        if subtype_value == "bot_message":
            print('Posting by bot.')
    else:
        text_processing(body_object, slack_webhook_url)

def text_processing(event_body, url):

    # 投稿された文章を取り出す
    submitted_text = event_body['event']['text']

    # 感情分析
    detect_sentiment_result = comprehend.detect_sentiment(Text=submitted_text, LanguageCode='ja')
    sentiment = detect_sentiment_result["Sentiment"]
    
    # それぞれの感情のスコアを取得する
    positive_score = detect_sentiment_result["SentimentScore"]["Positive"] 
    negative_score = detect_sentiment_result["SentimentScore"]["Negative"]
    neutral_score = detect_sentiment_result["SentimentScore"]["Neutral"]
    mixed_score = detect_sentiment_result["SentimentScore"]["Mixed"]
    emotion_score = f"===各感情のスコアを表示します===\nポジティブ：{positive_score}\nネガティブ：{negative_score}\n中立：{neutral_score}\n混合：{mixed_score}\n============================="
    
    # 感情に応じて返すテキストを変える
    if sentiment == "POSITIVE":
        emotion = "ポジティブな文章です:laughing:"
        max_value = positive_score
    elif sentiment == "NEGATIVE":
        emotion = "ネガティブな文章です:smiling_face_with_tear:"
        max_value = negative_score
    elif sentiment == "NEUTRAL":
        emotion = "中立的な文章です:slightly_smiling_face:"
        max_value = neutral_score 
    elif sentiment == "MIXED":
        emotion = "複合的な文章です:melting_face:"
        max_value = mixed_score
    else:
        emotion = "不明な感情です:lying_face:"
        max_value = ""
        
    # スコアの値に応じて言葉を追加する
    if max_value >= 0.9:
      embellishment = "とても"
    elif max_value <= 0.6:
      embellishment = "どちらかといえば"
    else:
      embellishment = ""
    
    # Slack にテキストを投稿する
    body_text = {"text": f"{embellishment}{emotion}\n{emotion_score}"}
    body = json.dumps(body_text)
    headers = {
        "Content-Type": "application/json"
    }
    response = requests.post(url, headers=headers, data=body)
    if response.status_code == 200:
        print("Posted to Slack.")


```

それぞれの項について解説します。
必要がなければ「[関数 URL の発行](#2-関数-url-の発行)」まで飛ばしてください


### bot の投稿による無限ループ

関数作成中、slack との連携が完了したあと、テストで動かしてみると以下のような状態になりました。

<img width="1000" alt="スクリーンショット 2023-12-01 153358.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/2685b34e-1ad9-fc64-b210-29481b9da02e.png">


こちらは
 1. ユーザーが投稿した文章を `Slack` がイベントとして `Lambda` へ送る
 1. ユーザーが投稿した文章の解析結果を `Lambda` が `Slack` へ投稿する
 1. `Lambda`が投稿した解析結果を `Slack` がイベントとして `Lambda` へ送る 　:innocent: :interrobang:
 1. `Lambda`が投稿した解析結果の解析結果を `Lambda` が `Slack` へ投稿する
 1. `Lambda`が投稿した解析結果の解析結果の解析結果を `Slack` がイベントとして `Lambda` へ送る
 1. `Lambda`が……

という「不滅の`Lambda`～無限課金編～」を捕えたものになります。
イベント通知が `Lambda` 自身による投稿にも反応してしまう可能性をすっかり失念しておりました。

このままでは**大量のコスト**と**大量の怒られ**が発生してしまう……と怯えながら目を皿にして `slack` が `Lambda` へ送っているイベント内容を見比べたところ、bot による投稿は`subtype":"bot_message"`という要素を持っていることがわかりました。

ユーザーによる投稿
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/fa630e5f-52b6-4265-fc9a-87b51c63251d.png)

bot による投稿
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/c2cd77e4-11ee-a4f0-6301-8fb275de5476.png)

上記の要素をつかって bot による投稿の場合は処理を終了してもらうようにします。

```python
# bot による投稿であるかを判定する
if 'subtype' in body_object['event']:
    subtype_value = body_object['event']['subtype']
    if subtype_value == "bot_message":
        print('Posting by bot.')
else:
    text_processing(body_object, slack_webhook_url)
```

これでひとまず、当初の想定通りに動くようになりました。

<img width="1000" alt="スクリーンショット 2023-12-01 154022.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/c58737d2-5817-5c93-ca63-c1ac7e0ca764.png">

### 感情分析

`Amazon Comprehend`による感情分析は非常に簡単で、以下の一行のみで終了します。

```python
# 感情分析
detect_sentiment_result = comprehend.detect_sentiment(Text=submitted_text, LanguageCode='ja')
```

当初はただ解析された感情のみを返していましたが、それだけだと味気なかったので追加で各感情のスコアも返すようにし、ついでにスコアの値に応じて文頭に修飾語を付けることにしました。

```python
# それぞれの感情のスコアを取得する
positive_score = detect_sentiment_result["SentimentScore"]["Positive"] 
negative_score = detect_sentiment_result["SentimentScore"]["Negative"]
neutral_score = detect_sentiment_result["SentimentScore"]["Neutral"]
mixed_score = detect_sentiment_result["SentimentScore"]["Mixed"]
emotion_score = f"===各感情のスコアを表示します===\nポジティブ：{positive_score}\nネガティブ：{negative_score}\n中立：{neutral_score}\n混合：{mixed_score}\n============================="
    
# 感情に応じて返すテキストを変える
if sentiment == "POSITIVE":
    emotion = "ポジティブな文章です:laughing:"
    max_value = positive_score
elif sentiment == "NEGATIVE":
    emotion = "ネガティブな文章です:smiling_face_with_tear:"
    max_value = negative_score
elif sentiment == "NEUTRAL":
    emotion = "中立的な文章です:slightly_smiling_face:"
    max_value = neutral_score 
elif sentiment == "MIXED":
    emotion = "複合的な文章です:melting_face:"
    max_value = mixed_score
else:
    emotion = "不明な感情です:lying_face:"
    max_value = ""
        
# スコアの値に応じて言葉を追加する
if max_value >= 0.9:
    embellishment = "とても"
elif max_value <= 0.6:
    embellishment = "どちらかといえば"
else:
    embellishment = "" 
```

## 2. 関数 URL の発行

関数を作成したら、次は `Slack` と連携をしてもらうための「**関数 URL**」を発行する必要があります。
前項で作成した Lambda 関数のコンソール画面を開き、以下の画面から「**関数 URL を作成**」を選択してください。

<img width="1000" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/54dafbcc-d240-109b-b098-385db9394896.png">

認証タイプを「**NONE**」にして作成します。
<img width="800" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/28a5de77-27cb-282d-0945-e847deaabddc.png">

作成が完了すると関数 URL が表示されるようになるので、そちらを使って **Slack** との連携を進めていきます。
![スクリーンショット 2023-12-01 161954.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/06995796-0081-abcb-7a5c-8f105eff18b6.png)


## 3. Slack 設定

今回は専用のアプリを作成するところから始めます。
https://api.slack.com/apps

**1.**「**Create New App**」を選択してください。
<img width="800" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/388a0ef7-94e7-f52c-2a45-aa89e7f24766.png">


**2.** 任意のアプリ名を入力して「**Create App**」を選択します。
<img width="500" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/8ed0bffd-527f-0b1f-60ee-c6acf41a4c88.png">


**3.** アプリが作成できたら、管理画面から「**Event Subscriptions**」を選択し、有効にしたあと「**Request URL**」に前項で発行した「**関数 URL**」を貼り付けます。
<img width="800" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/adb63948-d495-7d9e-b4d7-bf8d3c2c43e9.png">
無事に認証が通ると「<font color="008833">Verified</font>」が表示されます。


**4.** 同じく管理画面から「**Incoming Webhooks**」を選択し、有効にします
<img width="800" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/b31ba591-304d-d9ff-5988-e32a75b43d9c.png">

**5.** 同画面の下部から「**Add New Webhook to Workspace**」を選択し、`Webhook URL` を発行します。
<img width="800" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/66675b6e-5855-83c8-6035-a670d4993703.png">

**※個人のアカウントではなく企業管理のワークスペース等を使用する場合は、管理者の承諾がないとアプリの作成および`Webhook URL`の発行ができない場合があります。**



**6.** 発行した URL を関数に貼り付け、変数として利用します。

```python
slack_webhook_url = "SlackのWebhookurl"
```

これでばっちり動くはずです。

# 動作確認

`Slack` に任意の文章を投稿します。


名作を解析してみたり
<img width="500" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/b126a76b-99c4-5c44-03f4-d4799c75084b.png">

意味もなく漫画のセリフを入れてみたり
<img width="500" alt="Image 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/8fe2b638-71b4-9067-7c84-4a7f1179b46b.png">

ボジョレーの評価もわかります:thumbsup:
![スクリーンショット 2023-12-01 171516.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2302869/84625895-b735-f9a4-25b9-2eeabf6a58e3.png)


# 感想

`Amazon Comprehend`はコンソール画面からでもお手軽に操作ができますが、AWS にログインしていない状態でも `Slack` からササッと操作できたらもっとお手軽なんじゃないかな～と思い、作ってみました。
アドベントカレンダーの名を借り、業務ではなかなか触れないサービスを好きなように触ることができて楽しかったです :grin:

# さいごに

株式会社オークファンではエンジニアを募集しております。
ご興味ございましたらぜひ弊社のホームページをご覧ください。