[Azure AI] ②自然言語処理の機能を提供するAzureのサービス Language Understanding (LUIS) を改めて理解する - 実践編 -

こんにちは、もっちゃんと申します。

[前回の記事](https://qiita.com/mochan_tk/items/0a0e7a55bdd645bffe2c)ではLUISの解説・説明的な内容を扱いました。
そして今回は実際に手を動かしてLUISを使って理解していこうと思います！

## さっそく使ってみる

### 事前準備

[Microsoft Azureのサブスクリプション](https://docs.microsoft.com/ja-jp/azure/cost-management-billing/manage/create-subscription)と[Cognitive Servicesのリソース](https://docs.microsoft.com/ja-jp/azure/cognitive-services/cognitive-services-apis-create-account?tabs=multiservice%2Cwindows)を事前に作成して用意しておきましょう！

### 新しいアプリの開始

まずは、[LUIS 専用のポータル画面](https://www.luis.ai/)にアクセスしてサインインを行い、LUISの画面に入ったら使用するサブスクリプションを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/2f014687-a747-086f-b536-14c96befbaf6.png)

`Create a new authoring resource`をクリックして

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/f6c44642-3863-087f-bb4e-23e76c869c5a.png)

下記の項目を埋め、`Done`ボタンを押します。

- Azure subscription: 事前準備で用意しておいたサブスクリプション
- Azure resource group: Azureのリソースグループ名
- Azure resource name: 任意のリソース名
- Location: ロケーション(現時点ではLocationは日本のゾーンを選択できない)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/188fd1f2-17e7-78aa-f9f6-65b35075f6e2.png)

すると`Create new app`画面が表示れるので、これまた必要な項目を埋めて`Done`ボタンを押します。

- Name: 任意の名前
- Culture: `Japanese`を選択
- Description: 任意の説明
- Prediction resource: 事前準備で用意しておいたCognitive Servicesのリソース

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/5784d0a9-d374-dca1-5646-e1c119be6f39.png)

しばらくすると下記の案内ダイアログとともに、LUISの新しいアプリ作成が完了します。（ダイアログは右上のバツボタンで消しましょう）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/15635234-7023-70c7-e925-e7c9ac11dbdc.png)

### Entity(エンティティ)作成

次にエンティティを新規作成していきます。
下記のように必要な情報を入力し、Typeは`Machine learned`を選択するようにしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/8e4bb4cf-193f-6a0c-4132-67388b9bb48f.png)

そして次の画面でサブエンティティ、`Size`と`Quantity`を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/04150916-7fad-3aad-8cb1-72b49b444689.png)

そして、今作成した`Size`サブエンティティに語句一覧(phrase list)を設定して、特徴量を追加していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/c682a272-1018-9e69-2cab-0bf2f64ef38f.png)

ここではいったん`小さい`、`大きい`、`普通`とかを入れておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/ef3971b7-04e6-0f76-bf2e-8c301b0d4105.png)

まだまだエンティティを作成していきます。次はリストエンティティです。（`SizeListentity`の名前で作成します）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/d5b6bbb6-fa86-149c-34b2-c85775f89ad2.png)

リストエンティティは下記のように、シノニム(Synonyms)も含めて設定してください。（シノニムはできるだけ多く設定した方が良いですね！）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/e66e2f1e-8c0f-1f40-d62f-646ffb9e0b26.png)

そしてサブエンティティSizeに`@SizeListentity`を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/2e851605-c6e2-4065-e3dd-83d7702da88e.png)

そしてもう１つ、事前構築済みの number エンティティを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/d3aff3bf-47ac-c088-a7e5-fc7710641811.png)

こちらもサブエンティティSizeに`@number`を追加。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/4894ff9b-ac10-d2c7-0791-b149caae785d.png)

ついでに必須の特徴量も設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/d7923378-2c83-e52d-8947-882b60b04e59.png)


### Intent作成

続いてIntentの作成をしていきます。まずは `Greeting(挨拶)`のIntentを作成してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/72d2bb90-4ede-ce0e-1d99-617debb7668b.png)

中身は下記のような感じで、あいさつ系の文章をいくつか入れておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/34ddb447-bd96-73fe-fd6e-2282ef176dbc.png)

次に`OrderPizza` Intentを作成していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/2de2c86c-9d11-830c-75f7-2466827a9af6.png)

下記のようにサンプル発話を追加した後、`Order`、`Size`、`Quantity`のエンティティをラベル付けします。その後右上の`Train`ボタンも押しておきます。

![luis_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/e351279e-f780-ef39-fc2b-008a83574154.gif)

さらに発話を追加して ラベルをつけてトレーニングを実施していくと

![luis_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/03ab5ceb-ed2d-f2a8-168c-e5ac0556cf58.gif)

すると自動のラベル付けの精度が次第に向上していくのが分かると思います。（ラベル付けが間違ってなければ右にあるチェックマークを押して確定させましょう）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/331ab9d1-e8a4-bf8e-04f1-c069fe9230d9.png)

こんな感じでIntetに発話サンプルをどんどん追加していきましょう！

### モデルのビルドとテストの実行

ここでいったん画面右上の`Train`ボタンを押してトレーニングを実行しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/18d99e7d-64eb-0d0a-bf0c-d27caa550d76.png)

そして下記のようにテストパネルを開いて試しの注文文章を入れてテスト実行してみましょう。ちゃんと思ったIntentやEntityの抽出ができましたでしょうか？（ここで思った精度が出ない場合は、Intentのサンプル発話を追加するなどデータをもう少し追加してトレーニングを実行するサイクルを繰り返してみてください）

![azure_luis_line_7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/c0aa963b-ae59-69d8-c03b-487e22a8580e.gif)


### 作成したLUISアプリを公開(Publish)する

テスト実行で良い感じに動いてましたら、このLUISアプリの公開(Publish)を行いたいと思います。公開するとエンドポイントのURLが発行されるので、チャットボットなどのアプリケーションに組み込むことが可能です。

![azure_luis_line_8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/002e89b5-4815-786a-9f89-b817509cfc24.gif)

### エンドポイントを実行してLUISの予測結果を確認する

アプリケーションに組み込む前に先ほど発行したエンドポイントURLをcurlで叩いてみましょう。ここではいったん`sサイズのピザを3つください`という文章を送ってみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/70d091dc-f373-15f0-f025-42b5ee6edce4.png)

```
https://cog-luis.cognitiveservices.azure.com/luis/prediction/v3.0/apps/7fbff360-2283-40fb-a04a-7cea206908fe/slots/production/predict?subscription-key=af1cfef00b6b4ddc9968153363512cea&verbose=true&show-all-intents=true&log=true&query=sサイズのピザを3つください
```

実行結果は下記のjsonが返ってきました。予測としては`OrderPizza`のIntentがスコア`0.9693499`と高い数値を示しています。また、`Size`や`Quantity`の値の抽出もしっかりできてそうですね。

```json

{
    "query": "sサイズのピザを3つください",
    "prediction": {
        "topIntent": "OrderPizza",
        "intents": {
            "OrderPizza": {
                "score": 0.9693499
            },
            "Greeting": {
                "score": 0.012079056
            },
            "None": {
                "score": 0.008686903
            }
        },
        "entities": {
            "Order": [
                {
                    "Size": [
                        [
                            "小さい"
                        ]
                    ],
                    "Quantity": [
                        "3"
                    ],
                    "$instance": {
                        "Size": [
                            {
                                "type": "SizeListentity",
                                "text": "sサイズ",
                                "startIndex": 0,
                                "length": 4,
                                "score": 0.97143835,
                                "modelTypeId": 1,
                                "modelType": "Entity Extractor",
                                "recognitionSources": [
                                    "model"
                                ]
                            }
                        ],
                        "Quantity": [
                            {
                                "type": "Quantity",
                                "text": "3",
                                "startIndex": 8,
                                "length": 1,
                                "score": 0.9942513,
                                "modelTypeId": 1,
                                "modelType": "Entity Extractor",
                                "recognitionSources": [
                                    "model"
                                ]
                            }
                        ]
                    }
                }
            ],
            "$instance": {
                "Order": [
                    {
                        "type": "Order",
                        "text": "sサイズのピザを3つください",
                        "startIndex": 0,
                        "length": 14,
                        "score": 0.9689606,
                        "modelTypeId": 1,
                        "modelType": "Entity Extractor",
                        "recognitionSources": [
                            "model"
                        ]
                    }
                ]
            }
        }
    }
}
```

### LINE Botに組み込んでみる

ターミナルでエンドポイントのURLを叩くだけだと面白く無いので、アプリっぽいものに組み込んでみましょう。LINEのBotなら簡単に実現できるので活用していきます。

基本的には[こちらの記事](https://zenn.dev/mochan_tk/articles/d01a24bb576d18)通りに進めていただいて、下記の２ファイルとApplication Settingの値を今回用に変更・追加してください。

- *requirements.txt*

```
azure-functions
line-bot-sdk
azure-cognitiveservices-language-luis
```

- *___init ___.py*

```python
import logging
import os
import azure.functions as func

## https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/client-libraries-rest-api?tabs=windows&pivots=programming-language-python
from azure.cognitiveservices.language.luis.authoring import LUISAuthoringClient
from azure.cognitiveservices.language.luis.authoring.models import ApplicationCreateObject
from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
from msrest.authentication import CognitiveServicesCredentials
from functools import reduce

import json, time, uuid

appId = os.getenv('APP_ID', None)
predictionKey = os.getenv('PREDICTION_KEY', None)
predictionEndpoint = os.getenv('PREDICTION_ENDPOINT', None)

runtimeCredentials = CognitiveServicesCredentials(predictionKey)
clientRuntime = LUISRuntimeClient(endpoint=predictionEndpoint, credentials=runtimeCredentials)

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

# get channel_secret and channel_access_token from your environment variable
channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)

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

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    # get x-line-signature header value
    signature = req.headers['x-line-signature']

    # get request body as text
    body = req.get_body().decode("utf-8")
    logging.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        func.HttpResponse(status_code=400)

    return func.HttpResponse('OK')


@handler.add(MessageEvent, message=TextMessage)
def message_text(event):
    predictionRequest = { "query" : event.message.text }
    predictionResponse = clientRuntime.prediction.get_slot_prediction(appId, "production", predictionRequest)
    logging.info("Top intent: {}".format(predictionResponse.prediction.top_intent))
    logging.info("Sentiment: {}".format (predictionResponse.prediction.sentiment))
    logging.info("Intents: ")
    for intent in predictionResponse.prediction.intents:
        logging.info("\t{}".format (json.dumps (intent)))
        logging.info("\t{}".format (type(intent)))
    logging.info("Entities: {}".format (predictionResponse.prediction.entities))
    line_bot_api.reply_message(
        event.reply_token,
        [TextSendMessage(text=f'Intent: {predictionResponse.prediction.top_intent}'),
         TextSendMessage(text=f'Entity: {predictionResponse.prediction.entities}')]
    )

```

- *Application Setting*

`APP_ID`、`PREDICTION_KEY`、`PREDICTION_ENDPOINT`の値をApplication Settingに設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/ac1c7fc1-d2e6-3019-9c7e-63a0a05cb774.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/59c35f77-7315-a485-b2a6-a7c2d1ed6598.png)

![azure_luis_line_10.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/fc6d1005-c0aa-86c0-4a73-6fb20893fbdb.gif)

![azure_luis_line_11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121860/ce96a9ff-81a3-1b36-e23e-d03357d5966a.gif)


## まとめ

簡単ではありましたが、LUISをアプリケーションに組み込むところまでやってみました。機械学習を使うと聞くとハードルが高そうですが、近頃は機械学習の民主化が非常に進んでいるのでこういったLUISのようにある程度誰でも機械学習を活用することができる世の中になっていますね！自身のアプリケーションをワンランク上のクオリティにできるのではないでしょうか。
（個人的には機械学習は特定の人たちが使うものというより通常のソフトフェア開発の延長線上にあり、一歩進んだ姿と思っています。何らかの形で全ての開発者が、プログラム言語を使うのと同様に機械学習を活用するのがあたりまえの世界が来るのも近いと感じています）

また、今回はLUISを主に使いましたが、[Bot Framework](https://docs.microsoft.com/ja-jp/azure/bot-service/?view=azure-bot-service-4.0)や[QnA Maker](https://azure.microsoft.com/ja-jp/services/cognitive-services/qna-maker/)といった相性の良いサービスと組み合わせるとさらに強力になりそうです。

## 参考

- https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-concept-feature
- https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/client-libraries-rest-api?tabs=windows&pivots=programming-language-python
- https://docs.microsoft.com/ja-jp/python/api/azure-cognitiveservices-language-luis/?view=azure-python
- https://github.com/Azure-Samples/cognitive-services-python-sdk-samples/tree/master/samples/language/luis
- https://docs.microsoft.com/en-us/python/api/azure-cognitiveservices-language-luis/azure.cognitiveservices.language.luis.runtime.models.predictionresponse?view=azure-python
- https://docs.microsoft.com/en-us/python/api/azure-cognitiveservices-language-luis/azure.cognitiveservices.language.luis.runtime.models.intent?view=azure-python
- https://docs.microsoft.com/en-us/rest/api/cognitiveservices-luis/runtimev3.0/prediction/getslotprediction
- https://github.com/Azure/azure-sdk-for-python