LUISのPhrase ListとList型のEntityって、何が違うの？

Microsoftの自然言語理解サービス、[LUIS](http://luis.ai)で、同じように扱いたい単語を列挙したい時は、**Phrase List** か **List型のEntity** を使います。2つの違いがまとまっているものがなかなか見つからなかったので、マニュアルに散在している記述を参考に、まとめ記事を作ってみました。

#### Phrase List

* あくまで**ヒント**、**サンプル**です。
  * 考えられる単語をすべて定義していなくても構いません。
* 機械学習**されます**。
  * 厳密なマッチングではなく、文脈による判断や、類似語の判断もされます。
  * リストのすべての単語を全く同じように分類するわけではありません。
      * 載っていない単語が同じようなものとして分類されることもあります。
      * 載っていても、文脈での解釈が優先されて、違うと判断されることもあります。
* リストの値を期待する(Simple型などの)Entityの作成が必要です。
  * リストの一部の単語を使った例文を登録して、Entityを学習させる必要があります。
* リストは **10個**、1つのリストには**5000個**まで登録可能です。

以下のような環境下にある場合、こちらを検討することになります。

* 同じEntityに分類すべき単語がどんどん追加されていきそう
* 新しい単語は文脈・類似判断して勝手に判定していってほしい
* リストにない単語が勝手にEntityに分類されても、誰からも怒られない
* リストに追加した単語が意図したEntityに分類されなくても、誰からも怒られない

> 今回はPhrase Listのほうだけ説明していますが、Patternも同様で、あくまでヒントです。つまり、パターン構文に則った表現でも、Entityに分類されない場合があります。

### List型のEntity

* **全部**の値を含む、**完全一致**のリストです。
  * Canonical FormにもSynonymsにも定義されていない単語はEntityに分類されません。
* 機械学習**されません**。
  * 文章の中の単語に一致していれば、文脈関係なく100%の確率でEntityに分類されます。
  * トレーニングしても、Entityに分類される単語は増えません。
* 例文登録の際、リストに含まれる単語は自動的にEntityとマークされます。
* リストは **50個**、1つのリストには**20000個**まで登録可能です。
  * List型以外のEntityは全部合わせて30個の制限なので、これより緩いです。
  * サポートに連絡すれば上限の調整が可能です。

以下のような環境下にある場合、こちらを検討することになります。

* リストに追加したものは絶対にそれと判定してほしい
* リストに追加したもの以外が、勝手にそれと判定されるのは勘弁してほしい
* バックエンドのプログラムで、完全一致のクエリやCASE文の条件として使いたい
* リストの内容が変わることが滅多にない、運用中は変えるつもりがない
* 学習に使える例文が少ないが、単語のバリエーションは多い

### 参考にしたマニュアル

* [Entities in LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-entity-types)
* [Features in LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-feature#how-to-use-phrase-lists)
* [Add entities](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/add-entities)
* [Use features to improve your LUIS app's performance](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/add-features)
* [Language Understanding Intelligent Services FAQ](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-resources-faq)

### 検証

#### Phrase List

`駅名`というPhrase Listと、これが入ることを想定した`Destination`というEntityを作成しました。

```json:PhraseList.json
{
  "luis_schema_version": "2.1.0",
  "versionId": "0.1",
  "name": "PhraseList",
  "desc": "Phrase Listの検証",
  "culture": "ja-jp",
  "intents": [
    {
      "name": "Arrived"
    },
    {
      "name": "None"
    }
  ],
  "entities": [
    {
      "name": "Destination"
    }
  ],
  "composites": [],
  "closedLists": [],
  "bing_entities": [],
  "model_features": [
    {
      "name": "駅名",
      "mode": false,
      "words": "東京,上野,大宮,長野,飯山,上越 妙高,糸魚川,黒部 宇奈月 温泉,富山,新 高岡,金沢",
      "activated": true
    }
  ],
  "regex_features": [],
  "utterances": [
    {
      "text": "東京に着きました",
      "intent": "Arrived",
      "entities": [
        {
          "entity": "Destination",
          "startPos": 0,
          "endPos": 1
        }
      ]
    },
    {
      "text": "金沢に着きました",
      "intent": "Arrived",
      "entities": [
        {
          "entity": "Destination",
          "startPos": 0,
          "endPos": 1
        }
      ]
    },
    {
      "text": "黒部宇奈月温泉に着きました",
      "intent": "Arrived",
      "entities": [
        {
          "entity": "Destination",
          "startPos": 0,
          "endPos": 6
        }
      ]
    }
  ]
}
```

> 上記のJSONを`My Apps`の`Import App`からインポートすると、検証に使ったものと同じものが作成されます。

「新高岡に着きました」で試すと、「高岡」でEntityが認識されてしまいました。LUIS自身が持つ文脈による判断が優先されているのでしょうか。

```json:レスポンス
{
    "query": "新高岡に着きました",
    "topScoringIntent": {
        "intent": "Arrived",
        "score": 0.999932766
    },
    "entities": [
        {
            "entity": "高岡",
            "type": "Destination",
            "startIndex": 1,
            "endIndex": 2,
            "score": 0.5774768
        }
    ]
}
```

学習済の黒部宇奈月温泉は、全体がEntityとして認識されています。

```json:レスポンス
{
    "query": "黒部宇奈月温泉に着きました",
    "topScoringIntent": {
        "intent": "Arrived",
        "score": 0.999999046
    },
    "entities": [
        {
            "entity": "黒部 宇奈月 温泉",
            "type": "Destination",
            "startIndex": 0,
            "endIndex": 6,
            "score": 0.87559557
        }
    ]
}
```

#### List型のEntity

`駅名`というEntityを作成しました。

```json:EntityList.json
{
  "luis_schema_version": "2.1.0",
  "versionId": "0.1",
  "name": "EntityList",
  "desc": "Entity Listの検証",
  "culture": "ja-jp",
  "intents": [
    {
      "name": "Arrived"
    },
    {
      "name": "None"
    }
  ],
  "entities": [],
  "composites": [],
  "closedLists": [
    {
      "name": "駅名",
      "subLists": [
        {
          "canonicalForm": "東京",
          "list": []
        },
        {
          "canonicalForm": "上野",
          "list": []
        },
        {
          "canonicalForm": "大宮",
          "list": []
        },
        {
          "canonicalForm": "長野",
          "list": []
        },
        {
          "canonicalForm": "飯山",
          "list": []
        },
        {
          "canonicalForm": "上越妙高",
          "list": []
        },
        {
          "canonicalForm": "糸魚川",
          "list": []
        },
        {
          "canonicalForm": "黒部宇奈月温泉",
          "list": []
        },
        {
          "canonicalForm": "富山",
          "list": []
        },
        {
          "canonicalForm": "新高岡",
          "list": []
        },
        {
          "canonicalForm": "金沢",
          "list": []
        }
      ]
    }
  ],
  "bing_entities": [],
  "model_features": [],
  "regex_features": [],
  "utterances": [
    {
      "text": "東京に着きました",
      "intent": "Arrived",
      "entities": []
    }
  ]
}
```

「新高岡に着きました」で試すと、「新高岡」が駅名として認識されます。

```json:レスポンス
{
    "query": "新高岡に着きました",
    "topScoringIntent": {
        "intent": "Arrived",
        "score": 0.9999089
    },
    "entities": [
        {
            "entity": "新 高岡",
            "type": "駅名",
            "startIndex": 0,
            "endIndex": 2,
            "resolution": {
                "values": [
                    "新高岡"
                ]
            }
        }
    ]
}
```

逆に、「高岡」で試すと、「高岡」は駅名として認識されません。

```json:レスポンス
{
    "query": "高岡に着きました",
    "topScoringIntent": {
        "intent": "Arrived",
        "score": 0.999945641
    },
    "entities": []
}
```

このことから、駅名のEntityの部分は、ルールベースできっちりと置き換えられて解釈されることが分かります。