Elastic Stack 8.0 の NLP で日本語センチメント分析を試してみた - 前編

やっと 8.0 を触れる時間と気力が整ったので、 8.0 目玉機能の外部モデルを利用した NLP で日本語のセンチメント分析を試してみました。

公式ドキュメントの [Natural Language Processing](https://www.elastic.co/guide/en/machine-learning/current/ml-nlp.html) に記載されている手順に従って進めました。

大まかな手順:

1. eland をインストールして `eland_import_hub_model` コマンドを使えるようにする
1. `eland_import_hub_model` で Hugging Face からモデルを Elasticsearch にインポートする
1. モデルを使って文章を分析する

まず公式ドキュメントの手順で試して、その後に日本語のセンチメント分析を行うモデルを試してみました。

詳細をメモしておきます。

## eland をインストールする

私の環境では以前に古いバージョンの eland ver 7.14.1b1 をインストールしていたみたいで (全く記憶にない。。。) `upgrade` オプションを付けて pip を実行する必要がありました。

```bash
python3 -m pip install --upgrade eland
```

`urllib3` のバージョン互換性がおかしいで、というエラーが出たので、メッセージに従い、関連モジュールもアップグレードしました。

```bash
python3 -m pip install --upgrade requests
python3 -m pip install --upgrade urllib3
```

これで、 `eland_import_hub_model` が使えるようになるはず。しかし、実行すると必要なモジュールがいくつか足りない模様です。こちらもエラーメッセージに従い、必要なモジュールをインストールしました。きっと BERT とかすでに試している方はもうインストール済みなんだろうな、とか思いつつ。。

```bash
python3 -m pip install tqdm
python3 -m pip install torch
python3 -m pip install transformers
python3 -m pip install sentence_transformers
```

`eland_import_hub_model` を実行してエラーなく usage が出力されればインストール完了でしょう。

```bash
eland_import_hub_model
usage: upload_hub_model [-h] --url URL --hub-model-id HUB_MODEL_ID [--elasticsearch-model-id ELASTICSEARCH_MODEL_ID] --task-type
                        {text_classification,fill_mask,text_embedding,ner,zero_shot_classification} [--quantize] [--start]
                        [--clear-previous]
upload_hub_model: error: the following arguments are required: --url, --hub-model-id, --task-type
```

## Hugging Face からモデルを Elasticsearch にインポートする

今回はクラウドの [Elasticsearch Service](https://cloud.elastic.co/home) を使いました。ちなみに 7.16.2 -> 7.17.0 -> 8.0 とアップグレード無事完了しました。

まずは公式ドキュメントに記載のサンプルを試します。クラウドの管理画面から Elasticsearch のエンドポイントをコピーして、以下のように Elastic が Hugging Face で公開している事前学習済みのモデルをインポートします:

```bash
eland_import_hub_model --url https://elastic:password@xxxx.cloud.es.io:9243 --hub-model-id elastic/distilbert-base-cased-finetuned-conll03-english --task-type ner --start
```

### ML ノードの RAM 容量に注意しましょう

モデルのインポート自体は成功しましたが、モデルを `--start` する際にエラーが出ました:

```
elasticsearch.ApiError: ApiError(429, 'status_exception', 'Could not start deployment because no ML nodes with sufficient capacity were found')
```

Kibana ではインポートされたモデルが表示されていたので、 Kibana からも start を試してみると、より詳細なエラーメッセージが確認できました:

![ml-node-ram-shortage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20515/4b8a24e0-a11c-0e83-a143-c8fd22669f15.png)

> Could not start deployment because no suitable nodes were found, allocation explanation [Optional[Not allocating on node [instance-0000000001]. Reason: This node has insufficient available memory. Available memory for ML [432013312 (412mb)], memory required by existing jobs and models [0 (0b)], estimated memory required for this model [805003864 (767.7mb)].]]

とあります。私のクラウドのデプロイメントでは ML ノードの RAM は 1GB しかありませんでした。 `xpack.ml.max_machine_memory_percent` というクラスタ設定、デフォルトで 30% なので、このモデルをデプロイするには足りないようですね。

ML ノードの RAM を 2GB に変更しました。

Kibana の Trained Models の Nodes タブでは、 ML ノードの RAM 利用状況が確認できます。

![ml-nodes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20515/c5c20c56-fcd2-39ed-def6-40584a3bac42.png)


## モデルを使って文章を分析する

モデルがインポート、スタートできたら、モデルを使った分析が可能です。公式ドキュメントのサンプルは NER (Named Entity Recognition) という固有名詞を抽出するタスクです。

以下の API を使って簡単に試すことができます。

```json
POST /_ml/trained_models/elastic__distilbert-base-cased-finetuned-conll03-english/deployment/_infer
{
  "docs": {
    "text_field": "Sasha bought 300 shares of Acme Corp in 2022."
  }
}
```

実行結果:

```json
{
  "predicted_value" : "[Sasha](PER&Sasha) bought 300 shares of [Acme Corp](ORG&Acme+Corp) in 2022.",
  "entities" : [
    {
      "entity" : "Sasha",
      "class_name" : "PER",
      "class_probability" : 0.9953193611298665,
      "start_pos" : 0,
      "end_pos" : 5
    },
    {
      "entity" : "Acme Corp",
      "class_name" : "ORG",
      "class_probability" : 0.9996392201598554,
      "start_pos" : 27,
      "end_pos" : 36
    }
  ]
}
```

### Ingest pipeline から実行する

実際のシステムで利用する場合にはドキュメントを保存する時に Ingest pipeline を使って、分析結果をドキュメントに付け加えます。 `inference` プロセッサを使います。

パイプラインを作成する前に `_simulate` エンドポイントを使ってテストすると良いですね。

```json
POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "elastic__distilbert-base-cased-finetuned-conll03-english"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "text_field": "Sasha bought 300 shares of Acme Corp in 2022."
      }
    }
  ]
}
```

正しく実行できたら、正式に Ingest node pipeline として登録します。

```json
PUT _ingest/pipeline/ner-english
{
  "processors": [
    {
      "inference": {
        "model_id": "elastic__distilbert-base-cased-finetuned-conll03-english"
      }
    }
  ]
}
```

ドキュメント保存時に作成したパイプラインを指定してみましょう。

```json
PUT nlp-test/_doc/1?pipeline=ner-english
{
  "text_field": "Sasha bought 300 shares of Acme Corp in 2022."
}
```

保存されたドキュメントを確認すると、次のようになっています:

```
GET nlp-test/_doc/1
```

レスポンスの `_source` 部分のみ記載:

```json
{
    "text_field" : "Sasha bought 300 shares of Acme Corp in 2022.",
    "ml" : {
      "inference" : {
        "predicted_value" : "[Sasha](PER&Sasha) bought 300 shares of [Acme Corp](ORG&Acme+Corp) in 2022.",
        "entities" : [
          {
            "entity" : "Sasha",
            "class_name" : "PER",
            "class_probability" : 0.9953193611298665,
            "start_pos" : 0,
            "end_pos" : 5
          },
          {
            "entity" : "Acme Corp",
            "class_name" : "ORG",
            "class_probability" : 0.9996392201598554,
            "start_pos" : 27,
            "end_pos" : 36
          }
        ],
        "model_id" : "elastic__distilbert-base-cased-finetuned-conll03-english"
      }
    }
  }
```

ちゃんと動きました！では、続いて、日本語のモデルも試してみます。

## 日本語のセンチメント分析を試す

Hugging Face では `ja` の言語タグや、 `Text Classification` といったタスクの種類でモデルを検索できます。日本語のテキスト分類タスクで探してみると [daigo/bert-base-japanese-sentiment](https://huggingface.co/daigo/bert-base-japanese-sentiment) というモデルがありました。作者の方の情報はほぼ記載されていなかったのでお礼のコメントなどは伝えられていないのですが、心の中で「ありがとう！」と叫びながら like させていただきました。

`eland_import_hub_model` コマンドでモデルをインポートします。

```bash
eland_import_hub_model --url https://elastic:password@xxxx.cloud.es.io:9243 --hub-model-id daigo/bert-base-japanese-sentiment --task-type text_classification
```

エラーが出たので落ち着いて必要なモジュールをインストールし、再びインポートを実行します。[ipadic](https://github.com/polm/ipadic-py) は 2007年から更新が止まっているので、 Github のプロジェクトには "You Shouldn't Use This" と大きく記載があります。代わりに [UniDic](https://ccd.ninjal.ac.jp/unidic/) を使ってね、と書いてあります。今回はお試しなので ipadic で進めます。

```bash
python3 -m pip install fugashi
python3 -m pip install ipadic
```

私の非力な 2GB ML ノードでは最初に試した NER モデルと同時にデプロイは危険なので、 Kibana から NER のモデルは停止して、から `bert-base-japanese-sentiment` モデルをスタートしました。

しかし、モデルが `Starting` のまま止まってしまい、ログにエラーが出ていたので結局 RAM を 4GB まで上げて対応しました。

> 

早速試してみましょう！

```
POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "daigo__bert-base-japanese-sentiment"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "text_field": "あちきの ML ノードは非力すぎ"
      }
    },
    {
      "_source": {
        "text_field": "Elastic Stack 8.0 で格段に NLP が簡単になった！"
      }
    }
  ]
}
```

結果はこのようになりました:

```
{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_id" : "_id",
        "_source" : {
          "text_field" : "あちきの ML ノードは非力すぎ",
          "ml" : {
            "inference" : {
              "model_id" : "daigo__bert-base-japanese-sentiment",
              "prediction_probability" : 0.9478574249832916,
              "predicted_value" : "ネガティブ"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2022-02-23T09:47:07.206830982Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_id" : "_id",
        "_source" : {
          "text_field" : "Elastic Stack 8.0 で格段に NLP が簡単になった！",
          "ml" : {
            "inference" : {
              "model_id" : "daigo__bert-base-japanese-sentiment",
              "prediction_probability" : 0.9141719328662291,
              "predicted_value" : "ポジティブ"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2022-02-23T09:47:07.206835532Z"
        }
      }
    }
  ]
}
```

## まとめ
Elastic Stack 8.0 から Hugging Face で公開されている外部モデルを利用した NLP が非常に簡単に実行できるようになりました。私自身機械学習は SVM や Naive Bayes とか KNN とかを数年前にかじりましたが、 Deep Learning や BERT はなかなか重い腰を上げられず触れる機会がなかったので、今回非常にお手軽に試すことができて一気に現代人になった気がしました。学習済みのモデルが公開されていて、再利用できるというのは素晴らしいですね。

2022-03-04 追記: この記事では調査しきれなかった Tokenizer 部分を[後編](https://qiita.com/ijokarumawak@github/items/6cc714060090160cf2d5)に書きました。