LUISを導入する前に知りたかったこと

## はじめに
ナビタイムジャパン/ACTS(研究開発部門)サーバーグループの本多です。

弊社では移動体験の向上のため、自然言語処理によるナビの操作に取り組んでおります。
12/15に開催される[Developer Boost](https://event.shoeisha.jp/devboost/20181215)では、[「自然言語でナビを！VUIとしてのカーナビ」](https://event.shoeisha.jp/devboost/20181215/session/1898/)というセッションでその取り組みの一部をご紹介させていただきます。
ただ上記発表では設計や品質担保のおける工夫に主眼を置いてお話しするため、技術要素の詳細な解説は割愛しております。そのためこの記事では、発表内容に盛り込めなかった**実際のサービスにLUISを導入する際のTipsとアンチパターン**をお伝えさせていただきます。
なおLUISの概要についてもふれますが、導入手順や使い方は[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-get-started-create-app)が充実しているのでそちらに譲ります。予めご承知おき下さい。（既に概要についてご存知の方は[こちら](#Tips)からご覧ください）

## 概要
- Microsoftが提供する[Cognitive Services](https://azure.microsoft.com/ja-jp/services/cognitive-services/)の一種
- オリジナルのモデルを構築することが可能
- 日本語を含む12の言語に対応 **※2018年11月現在**

チャットボットなどの文脈で紹介されることが多いですが、LUIS自体は対話機能を提供しません。

## 何ができるのか
発話を入力として与えると、

- 意図解釈
- [固有表現抽出](https://ja.wikipedia.org/wiki/%E5%9B%BA%E6%9C%89%E8%A1%A8%E7%8F%BE%E6%8A%BD%E5%87%BA)

の２つを行い、出力として返却してくれます。


```json:LuisResponseSample.json
{
    "query": "環七通り沿いのコンビニ",
    "topScoringIntent": {
        "intent": "Search.Along.Road",
        "score": 0.981975
    },
    "entities": [
        {
            "entity": "コンビニ",
            "type": "MapObject.Icon.Param.List",
            "startIndex": 7,
            "endIndex": 10,
            "resolution": {
                "values": [
                    "convenience_store",
                    "コンビニエンスストア"
                ]
            }
        },
        {
            "entity": "コンビニ",
            "type": "Places.PlaceType.Param.List",
            "startIndex": 7,
            "endIndex": 10,
            "resolution": {
                "values": [
                    "0201"
                ]
            }
        }
    ]
}
```

### query
LUISに入力として渡した発話。このqueryがの解析対象となります。

### topScoringIntent
LUISが最も可能性が高いと判定した発話主の意図。
例における`Search.Along.Road`は、構築したモデルに定義してあるIntent名称です。
`score`は意図解釈時の根拠となる値。0~1の間の値をとります。

### entities
発話から抽出された固有表現の配列。
`startIndex`, `endIndex`は、抽出された固有表現が発話全体(=query)の何文字目に出現するかを表しています。

## 何が嬉しいのか
### GUIが充実している
- Intentの作成
- 発話例の入力
- Entityのアノテーション
- バージョン管理
- 学習
- 発行

これら全てがGUI上で完結するため、自然言語処理の導入コストを大きく削減してくれます。（これらの機能は[API](https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c39)でも提供されています）

###日本語に対応している
対象言語が英語の場合、類似の機能を提供してくれるツール群はたくさんありますが、日本語に対応しようと思うと途端に選択肢が少なくなります。
また日本語特有の問題に形態素解析がありますが、LUISの入力として渡す文章には区切り文字等を入れる必要はありません。


## Tips

### 発話のニュアンス < アプリケーションのアクション
Intentを設計する際は発話それ自体のニュアンスよりも、**LUISの解析結果を受け取ったアプリケーション側がどんなアクションを起こすか**を基準に考えた方が良いです。
例えば弊社のNAVITIME TravelのBotアプリでは当初、以下のようにIntentを切っていました。

|発話例|Intent|Entityのラベル|Entityの値|
|---|---|---|---|
|「金閣寺ってどこにあるの」 |Ask.Where|spot|金閣寺|
|「金閣寺に行きたい」|Want.To.Go|spot|金閣寺|

しかしこれだと、このLUISの解析結果を受け取るBot側のアプリケーションがハンドリングするコードが完全に同じになってしまいます。

```c#:LuisResultHandler.cs
[LuisIntent("Ask.Where")]
public async Task AskWhereHandler(IDialogContext context, LuisResult result)
{
    // ココと
    var spotName = result.entities.Where(e => e.type == "spot").First();
    var searchResult = await SearchSpot(spotName);
    await context.PostAsync(searchResult);
}

[LuisIntent("Want.To.Go")]
public async Task WantToGoHandler(IDialogContext context, LuisResult result)
{
    // ココが同じ
    var spotName = result.entities.Where(e => e.type == "spot").First();
    var searchResult = await SearchSpot(spotName);
    await context.PostAsync(searchResult);
}
```

そのためハンドリングをするアプリ側を起点にIntentを設計し、以下のようにするのがベターです。

|発話例|Intent|Entityのラベル|Entityの値|
|---|---|---|---|
|「金閣寺ってどこにあるの」, 「金閣寺に行きたい」 |Search.Spot|spot|金閣寺|

```c#:LuisResultHandler.cs
[LuisIntent("Search.Spot")]
public async Task SearchSpotHandler(IDialogContext context, LuisResult result)
{
    var spotName = result.entities.Where(e => e.type == "spot").First();
    var searchResult = await SearchSpot(spotName);
    await context.PostAsync(searchResult);
}
```

### 絶対に捕まえたいキーワードはListEntityに登録する
アプリケーションのドメインに応じて、キーワードとなる単語があるかと思います。
例えば弊社のカーナビアプリの場合「地図」という単語はかなり重要です。この単語が発話に含まれている時点で、ユーザーが地図の操作に関する何らかを求めていることが分かり、他の選択肢をアプリケーションの次のアクション候補から除外することができるからです。
またヘルプデスクのチャットボットなどでは、「入会」のような単語は是非捕まえたいところではないでしょうか。

こういったアプリケーションの対象ドメインにおけるキーワードは、[ListEntity](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-how-to-add-entities#add-list-entities)に登録することをお薦めします。
通常の[シンプルEntity](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-how-to-add-entities#add-simple-entities)だと、学習させた発話例に存在しない言い回しの中にキーワードが含まれていた場合、そのキーワードを抽出できないことがあるからです。

またListEntityはシノニム機能も提供しており、「地図」と「マップ」を同じ`Map`エンティティとして登録できます。この機能を使えば、**実はユーザーはこういう言い回しもしてくる**ということが後から判明した場合はその言い回しをシノニムに追加することで、対応可能な発話が増えていきます。


### Active Learning
自然言語という**なんでもアリ**な入力を受け取る以上、最初に作ったモデルが完璧にワークする事はほぼないと思います。
最低限のモデルをデプロイして、サービスで一定期間運用し、ログを元にモデルの精度を向上させ、再びデプロイする。というサイクルを回すのが一般的な流れになるのではないでしょうか。
LUISは自身が受け付けたリクエストのログの中から、[学習させるとモデルの精度向上に寄与する](https://www.slideshare.net/shuyo/introduction-to-active-learning-25787487)発話を推薦してくれる[Active Learning](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-how-to-review-endoint-utt)の機能があります。
この機能を使用して**正しいIntent**と**本来抽出されて欲しいEntity**を設定してあげれば、効率的にモデルの精度を向上させていくことができます。

### バージョン管理とバッチテスト
ログを眺めていくと、こちらが想定していなかった発話が入ってくることが多々あります。
それらの発話に対応し、アプリケーションの機能を拡張しようと思った場合、IntentやEntityの追加が生じます。
しかしモデルの構成が変わると、**今までと同じ発話が異なるIntentとして分類される**ということ起こります。
これをデグレと呼ぶかはアプリケーション次第ですが、許容できないケースも多いのではないでしょうか？
そこで役立つのがバージョン管理とバッチテストです。
#### バージョン管理
LUISには[バージョン管理機能](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-how-to-manage-versions)があり、各バージョンにおけるIntent, Entity, 学習させた発話データ等のスナップショットを保有しています。
これによりいつでも好きなモデルに切り戻すことが可能なので、躊躇せずに機能追加をすることが可能です。

```json:LuisVersionInfo.json
{
  "luis_schema_version": "3.1.0",
  "versionId": "1.0.0",
  "name": "SampleLuisApp",
  "desc": "",
  "culture": "ja-jp",
  "intents": [
    {
      "name": "None"
    },
    {
      "name": "Operation"
    },
    {
      "name": "Operation.Cancel"
    },
    {
      "name": "Operation.Delete"
    },
    (略)
  ],
  "entities": [
    {
      "name": "Area",
      "roles": []
    },
    {
      "name": "History",
      "roles": []
    },
    {
      "name": "Icon",
      "roles": []
    },
    {
      "name": "Navigation",
      "roles": []
    },
    {
      "name": "Route",
      "roles": []
    },
    {
      "name": "Datetime",
      "children": [
        "Time",
        "Day",
        "DayOfTheWeek"
      ],
      "roles": []
    },
    (略)
}
```

### バッチテスト
LUISのモデル更新によるアプリケーションのデグレを防ぐために、LUISの[バッチテスト](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-how-to-batch-test)を使用するのが有効です。
特に「最低限この発話には正しく対応してほしい」といったものがあれば、それをバッチテストのテストケースとして用意しておき、新しいモデルを発行する前に実行することで性能を担保できます。

## アンチパターン
### 語尾に疑問符`?`や感嘆符`!`がついた発話を学習させる
チャットボットに「今何時？」と聞いたら、なぜか天気を答えられてしまったことがあります。
調査を行ったところ、天気を尋ねるIntentにだけ「今日雨降る？」という**疑問符で終わる発話**が学習させられており、逆にそれ以外のIntentには疑問符で終わる発話例は一切学習させられていないことが判明しました。
疑問符が発話全体の特徴量として強く効きすぎてしまった結果、「今何時？」という発話が天気を尋ねるIntentとして判定されてしまったのです。
これを避けるために、**疑問符および感嘆符は発話から除外してLUISに学習させる**ことをお薦めします。

### 似た発話だけど全く別の意図の発話を学習させる

|学習させる発話|Intent|アプリケーションの動作|
|---|---|---|
|「地図を小さく」|ScaleDown|地図を縮小する|
|「地図が小さい」|ScaleUp|地図を拡大する|

このような発話を学習させてしまうと、LUISの学習モデルが収束しづらくなり、ユーザーが意図せぬアプリケーションの動作に繋がりやすくなってしまいます。
「てにをは」をEntityとして学習させれば防げるかもしれませんが、それではLUISの手軽さを損なうことになってしまいます。
そのためどうしても上記のような発話を区別してハンドリングしたい場合は、LUISではなく構文解析のライブラリを使用することをお薦めします。

### １つのモデルを複数ドメインに適用する
公式ドキュメントの[Noneインテントの使い方](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-concept-intent#none-intent-is-fallback-for-app)や[事前構築済みのモデル](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-concept-prebuilt-model)に関する記載内容から、**1つのモデルで対応するのは１つのドメインに関する発話のみ**というLUISの思想が汲み取れます。
つまり、「航空券の予約」と「旅館の予約」に関する発話を１つのLUISのモデルに学習させることは原則させない方がいいということです。
どうしても１つのアプリケーションで複数のドメインに対応する必要が生じた場合、以下のようにアプリケーションに工夫を施すことをお薦めします。

- LUISのモデルをドメインの数だけ分割して用意する
- アプリケーション側のUI等で**どのドメインに関する発話を特定**する
- 該当するドメインのLUISモデルにPredictのリクエストを投げる

## 最後に
繰り返しになりますが、最近は本当に[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/)が充実しているので、この記事を見てLUISを使ってみようと思った方は、ぜひこちらをご参照ください。特に[ベストプラクティス](https://docs.microsoft.com/ja-jp/azure/cognitive-services/luis/luis-concept-best-practices)について書かれた箇所は事前に読んでおくと、ハマりどころを回避できるのではないかと思います。（私が最初に使い始めた時はこんなに丁寧な解説はありませんでした。もっと早く知りたかった...笑）