Amazon SageMaker で日本語テキストを分類する

# 概要
SageMaker に日本語テキスト分類のサンプルが少ないので実装してみました。対象データセットは [Amazon Customer Review Dataset](https://s3.amazonaws.com/amazon-reviews-pds/readme.html)で、Amazon の評価レビューにあるコメントのデータセットです。各言語のレビューコメントがありますが日本語を選びました。レビューコメントから、それがPositive (Ratingが4か5) またはNegative (Ratingが1か2) を判定します。実装は以下の2つです。

- ビルトインアルゴリズムの Blazing Text
    - SageMaker 用のノートブック, https://github.com/aws-samples/amazon-sagemaker-examples-jp/blob/master/nlp_amazon_review/BlazingText/blazingtext.ipynb
- Gluon NLP を利用した BERT
    - SageMaker 用のノートブック, https://github.com/aws-samples/amazon-sagemaker-examples-jp/blob/master/nlp_amazon_review/BlazingText/blazingtext.ipynb

実装に関する説明は上記のSageMakerノートブックを見てください。それぞれ十分な学習を行えば、95%以上の精度が出ると思います。ここでは、それぞれの方式について少し紹介します。

# 各方式の説明
## [BlazingText](https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/blazingtext.html)
もともとBlazingText は、テキストを与えると、そのテキストに含まれる単語のベクトル表現を作成する、いわゆる Word2Vec を行うビルトインアルゴリズムでした。CPU, GPU を使って高速に Word2Vec を実現できるように工夫されています。これに加えて、BlazingText ではラベル付きのテキストを与えると、ラベルがわからないテキストを分類するような機械学習モデルを作成することもできるようになっています。例えば、メールがスパムかそうでないかを判定したい場合は、スパムのメールとスパムでないメールを用意して学習すれば、あるメールに対して判定が可能になります。

では Word2Vec とテキスト分類はどのように関係しているでしょうか。Blazing Text のドキュメントからは、テキスト分類には fastText のアルゴリズムを利用していると書いてあります。fastTextのテキスト分類アルゴリズムは以下の論文から確認できます。

A. Joulin, E. Grave, P. Bojanowski, T. Mikolov, Bag of Tricks for Efficient Text Classification, ACL 2017.
https://www.aclweb.org/anthology/E17-2068/

以下は論文のFigure1の抜粋です。テキスト中のN個の単語に関するベクトル表現$x_1, \dots, x_N$に対して平均をとって一つの隠れ層とします。その隠れ層から線形回帰でクラスを推定するようなモデルです。
![Screen Shot 2020-01-21 at 0.04.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/5f1939f2-7233-a5b6-5379-ef89ba18aac8.png)

なので、Word2Vec と fastText はベクトル表現を利用する点において関係がありそうです。

また BlazingText は日本語も利用可能です。ただし、分かち書きといって、意味のある語の単位でスペースで区切る必要があります。英語はスペース区切りがデフォルトなので不要ですが、日本語はそういった区切りがありません。その場合、形態素解析ツールを利用すると区切りを知ることができます。形態素解析の代表としては、[MeCab](https://gluon-nlp.mxnet.io/model_zoo/bert/index.html)などがあります。形態素解析を利用して、以下のようなデータを用意すれば学習が可能です。最初の`__label__1`のところがラベルで、それ以降がスペース区切りのテキストです。`__label__1`はpositiveなラベルを表しています。これはfastTextでも同様の形式ですね。

```python 
__label__1  私 は これ が 好き　です 。
__label__0  私 は これ が きらい　です 。
```
こうしたデータをcsv形式で用意すれば、あとは BlazingTextアルゴリズムを呼び出すだけです。呼び出し方は SageMaker のノートブックを見てください。

## [Gluon NLP を利用した BERT](https://gluon-nlp.mxnet.io/model_zoo/bert/index.html)
Gluon NLP は、MXNet をハイレベルで利用するための深層学習フレームワークで、特に自然言語処理に特化したものです。有名なNLPのモデルを利用可能で、自然言語処理で利用頻度の高い前処理も用意されています。どれくらい簡単かを[トップページのサンプル](https://gluon-nlp.mxnet.io/)で体験することができます。トップページを見ると以下のようなコードが出るので再生ボタンを押すとブラウザ上で実行できます。

![Screen Shot 2020-01-21 at 1.00.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/e0450421-cb78-5001-6105-ab38db00069c.png)

実行に少し時間がかかりますが、以下のように baby と infant の単語の類似度が表示されます。単語のベクトル表現を計算してから、それらのコサイン類似度を計算する処理をしていますが、これがたったの10行で実装できています。

```
Similarity between "baby" and "infant":  0.740567
```

Gluon NLPは類似度計算以外のタスクにも対応しています。ここではテキスト分類を行うので、2019年頃からよく利用されるようになってきた BERT によるテキスト分類を利用してみましょう。BERTのもとの論文は以下です。

J. Devlin, M. W. Chang, K. Lee, K. Toutanova,
BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding, ACL2019.
https://www.aclweb.org/anthology/N19-1423/

BERT は、汎用言語表現モデルと呼ばれたりしており、テキスト分類だけではなく質問応答などにも利用できるモデルです。それを表す図を論文から抜粋します。

![Screen Shot 2020-01-21 at 0.59.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/b43f9a84-2dc6-dfd0-d65b-5915a5c38afd.png)

左の図は大規模な自然言語処理のデータセット (コーパスといいます）で学習されたBERTモデルです。どれくらい大規模かというと、Wikipedeia のテキストが使われていたりします。この大規模な学習は非常に時間がかかるので、利用方法としては、左の学習済みモデルをどこかから入手して、右の図の様々なタスクに対するモデルを簡単に作ることが多いです。右の図で、MNLIは2つの入力文が含意, 矛盾, 中立のどの関係かを判定するタスク、NERは固有表現 (人、地名など)の抽出、SQuAD は質問に対する回答を特定するタスクが例として挙げられています。

BERTで簡単にテキスト分類を行うためには以下の2つが必要ですが、Gluon NLPではどちらも対応しています。

- 左図にある学習済みモデルが提供されていること。日本語の場合は日本語を含むデータで学習したモデルがあること。 
  TensorFlow やGluon NLPでは、日本語を含む言語で学習されたモデルがあります。Gluon NLP では、以下のURLにある `wiki_multilingual_uncased` などが該当します。モデル名の通り wikipedia の複数言語のテキストで学習されたモデルです。
https://gluon-nlp.mxnet.io/model_zoo/bert/index.html

- 学習済みのBERTモデルからテキスト分類のタスクを解けるようにFine-Tuningする方法が提供されていること。
  Gluon NLP の場合は、BERTでテキスト分類を行うための実装が公開されており、[ここのスクリプト](https://github.com/dmlc/gluon-nlp/blob/v0.8.x/scripts/bert/model/classification.py)を利用することで容易に実装ができます。冒頭の SageMaker notebooksでは、[srcのフォルダ](https://github.com/aws-samples/amazon-sagemaker-examples-jp/tree/master/nlp_amazon_review/GluonNLP_BERT/src)に必要なものをすべていれています。またGluonNLPでは、そうしたスクリプトを用意せずメソッドとして標準実装する動きも見えます。もしそれが実現すれば、スクリプトを用意する手間が大きく省略できそうです。

さてこれで日本語テキストをBERTで分類する準備が整ったのですが、BlazingTextのようにスペースで区切ったりする必要はないのでしょうか? 実はBERTでも同じように、テキストを語に分割する仕組みが必要ですが、そのためのTokenizerが標準装備されています。このTokenizerは、学習済みモデルを作成する際に利用したものがそのまま利用可能です。このTokenizerはsub-wordという方式で形態素解析とは異なっています。tokenizeして、各語のIDに変換するときは以下のようにします。

```python 
import gluonnlp as nlp
bert_tokenizer = nlp.data.BERTTokenizer(vocabulary, lower=True)
tokens = bert_tokenizer(text)
token_ids = bert_tokenizer.convert_tokens_to_ids(tokens)
```

ただし学習するときは、データセットからその都度変換するので、SageMakerで利用する学習スクリプト [train_and_deploy.py](https://github.com/aws-samples/amazon-sagemaker-examples-jp/blob/master/nlp_amazon_review/GluonNLP_BERT/src/train_and_deploy.py) では以下のようなコードを利用しています。`data`はデータセットの tsv ファイルを指していて、それを逐次Tokenize するような関数`BERTDatasetTransform`を利用します。tokenizeしたデータ (`data_train`) から適当なバッチサイズでサンプリングする `train_sampler` を用意して、学習モデルにいれるための `bert_dataloader`を定義します。

```python
transform = data.transform.BERTDatasetTransform(bert_tokenizer, max_len,
                                                    class_labels=all_labels,
                                                    has_label=True,
                                                    pad=True,
                                                    pair=pair)
data_train = data_train_raw.transform(transform)

# The FixedBucketSampler and the DataLoader for making the mini-batches
train_sampler = nlp.data.FixedBucketSampler(lengths=[int(item[1]) for item in data_train],
                                                batch_size=batch_size,
                                                shuffle=True)
bert_dataloader = mx.gluon.data.DataLoader(data_train, batch_sampler=train_sampler)
```

また、`bert_dataloder`のデータから学習するコードは以下のとおりです。

```python
    # Training loop
    for epoch_id in range(num_epochs):
        metric.reset()
        step_loss = 0
        for batch_id, (token_ids, valid_length, segment_ids, label) in enumerate(bert_dataloader):
            with mx.autograd.record():

                # Load the data to the CPU or GPU
                token_ids = token_ids.as_in_context(ctx)
                valid_length = valid_length.as_in_context(ctx)
                segment_ids = segment_ids.as_in_context(ctx)
                label = label.as_in_context(ctx)

                # Forward computation
                # Loss is weighte by 10 for negaive (0) and 1 for positive(1)
                out = bert_classifier(token_ids, segment_ids, valid_length.astype('float32'))
                ls = loss_function(out, label).mean()

            # And backwards computation
            ls.backward()

            # Gradient clipping
            trainer.allreduce_grads()
            nlp.utils.clip_grad_global_norm(params, 1)
            trainer.update(1)

            step_loss += ls.asscalar()
            metric.update([label], [out])

            # Printing vital information
            if (batch_id + 1) % (log_interval) == 0:
                print('[Epoch {} Batch {}/{}] loss={:.4f}, lr={:.7f}, acc={:.3f}'
                             .format(epoch_id, batch_id + 1, len(bert_dataloader),
                                     step_loss / log_interval,
                                     trainer.learning_rate, metric.get()[1]))
                step_loss = 0
                

    if current_host == hosts[0]:
        bert_classifier.export('%s/model'% model_dir)
```


これらのコードの書き方は[Gluon NLP の BERT のページ](https://gluon-nlp.mxnet.io/examples/sentence_embedding/bert.html#)に記載があるので参考にしてみてください。