自然言語処理で注目のBERT ~取り敢えず動かしてみる編~

# はじめに
業務にて自然言語処理に関わる事が多く、現在注目されているBERTに関して調べたのでまとめてみました。
※様々な記事から勉強させて頂きましたので、随時引用させて頂いております。


# 前提事項
下記前提を踏まえた上で、記載内容をご確認ください。

- あくまで「BERTを取り敢えず動かす」という事を目的として記載をしております。よって理解不足により、記載表現や内容に誤りがある可能性がございますので、その際はご指摘頂けると幸いです。
- BERTに関する概念的な説明は記載しておりません。下記「BERTを勉強する上での参考資料」に上手くまとまっておりますので、こちらをご参考ください。

# BERTを勉強する上での参考資料
- BERTの公式レポジトリ（＋その翻訳が記載されているQiita）
    - [google-research/bert](https://github.com/google-research/bert)
    - [[翻訳]BERTで自然言語AIをはじめる（github上のREADMEの翻訳）](https://qiita.com/uedake722/items/927bf491a025f1a88b17)
    - [Googleが公開しているBERT（自然言語処理AI）のファイル解説](https://qiita.com/uedake722/items/63a1ea32fe84886c02f9)

- BERTの概要、特徴など、概念的な話がまとめられている資料
    - [汎用言語表現モデルBERTを日本語で動かす(PyTorch)](https://qiita.com/Kosuke-Szk/items/4b74b5cce84f423b7125)
    - [はじめての自然言語処理 第3回 BERT を用いた自然言語処理における転移学習](https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part3.html)
    - [書籍：つくりながら学ぶ! PyTorchによる発展ディープラーニング](https://www.amazon.co.jp/dp/4839970254/)


# Pretrained日本語BERTモデル
2019/9/12時点で調査したところ、下記の日本語学習済みモデルが確認できています。

- [モデル①：BERT日本語Pretrainedモデル - KUROHASHI-KAWAHARA LAB](http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT日本語Pretrainedモデル)
    > - 入力テキスト: 日本語Wikipedia全て (約1,800万文, 半角を全角に正規化)
    > - 入力テキストにJuman++ (v2.0.0-rc2)で形態素解析を行い、さらにBPEを適用しsubwordに分割
    > - BERT_{BASE}と同じ設定 (12-layer, 768-hidden, 12-heads)
    > - 30 epoch (1GPUで1epochに約1日かかるのでpretrainingに約30日)
    > - 語彙数: 32,000 (形態素、subwordを含む)
    > - max_seq_length: 128

- [モデル②：BERT with SentencePiece を日本語 Wikipedia で学習してモデルを公開しました](https://yoheikikuta.github.io/bert-japanese/)
    > - 日本語 Wikipedia で学習した BERT モデルを公開しました
    > - livedoor ニュースコーパスで finetuning して良い性能を発揮することも確認
    - 二値分類タスクにおいてロジスティック回帰やLSTMよりも精度は高い結果が確認できたとの事です。
    - モデル②の方はCookpadの研究開発部に在籍しているようであり、下記ブログも作成しております。こちらは様々な検証を行っており、大変勉強になる内容でした！<br>
[BERT with SentencePiece で日本語専用の pre-trained モデルを学習し、それを基にタスクを解く](https://techlife.cookpad.com/entry/2018/12/04/093000)

- [モデル③：大規模日本語SNSコーパスによる文分散表現モデルの公開 : hottoSNS-BERTの配布](https://www.hottolink.co.jp/blog/20190311-2)
    - 日本語twitterデータでの学習

- [モデル④：大規模日本語ビジネスニュースコーパスを学習したBERT事前学習済（MeCab利用）モデルの紹介](https://qiita.com/mkt3/items/3c1278339ff1bcc0187f)
    - 事前学習用データ：日本語ビジネスニュース記事(300万記事)
    - トークナイザー：MeCab + NEologd
    - 語彙数：32,000語(CLS、SEP、UNK等除く)

# Pretrainedモデルを活用した埋め込みベクトルの取得
## ケース①：pytorchのライブラリ"pytorch_pretrained_bert"を活用

・[PYTORCHでBERTの日本語学習済みモデルを利用する - 文章埋め込み編](https://yag-ays.github.io/project/pytorch_bert_japanese/)<br>

上記サイトではモデル①を活用した文章ベクトルの取得方法を記載しており、ソースコードも公開しております。
こちらはpytorchのライブラリである["pytorch_pretrained_bert"](https://pypi.org/project/pytorch-pretrained-bert/)を活用し、"pytorch_model.bin"のモデルからベクトルを取得しています。<br>
上記をソースコードを元に、単語ベクトルも取得する事ができました。<br>
ただBERTの公式レポジトリによるfine-tuningを行うと、pytorch用のモデルが出力される訳ではないため、pytorch用のモデル生成をする必要があるのですが、その方法はイマイチ分かっていません。。

## ケース②：BERTのリポジトリで公開されている"extract_features.py"を活用

・[BERTの日本語事前学習済みモデルでテキスト埋め込みをやってみる](https://dev.classmethod.jp/machine-learning/bert-text-embedding/)<br>

上記サイトもモデル①を活用した文章ベクトルの取得方法を記載しており、ソースコードを公開しております。
こちらはBERTのリポジトリで公開されている["bert/extract_features.py"](https://github.com/google-research/bert/blob/master/extract_features.py)を活用し、ベクトルを取得しています。

# fine-tuning
## 「分類タスク」におけるfine-tuning
BERTのレポジトリに格納されている"run_classifier.py"を活用し、分類タスクのfine-tuningを行いました。
ステップとしては下記の通りです。
1. 学習データの用意
2. bert/tokenization.pyに学習済みモデルと同様の形態素解析クラスを記載
3. 2で記載したクラスを適用するように、bert/tokenization.pyを修正
4. run_classifier.pyに独自で行う分類タスクを実行するためのクラスを記載
5. 実行

### 例：発言小町の分類タスク
発言小町より取得したデータにてfine-tuningを実装したので、例としてその手順を記載します。

#### 1.学習データの用意
tsv形式にてtrain/dev/testの3つのファイルを作成します。
tsvは1行目にラベルとなる"text","label"が記載されており、2行目以降に各テキストとラベルを記載しています。
（発言小町のデータ取得方法などは割愛）

```
text	label
ここは女性の多い掲示板なので投稿させて頂きます。よろしくお願い申し上げ・・・	男性から発信するトピ
婚約破棄しようか悩んでいます。付き合い始めて約２ヶ月ですが、早く結婚を・・・	男性から発信するトピ
```

#### 2. bert/tokenization.pyに学習済みモデルと同様の形態素解析クラスを記載
今回は京大が作成したモデル①を活用しているため、Juman++をクラスとして追加します。
修正方法は下記を参考に実施しました。<br>
[BERTの日本語事前学習済みモデルでテキスト埋め込みをやってみる](https://dev.classmethod.jp/machine-learning/bert-text-embedding/)

tokenization.pyの末尾に下記コードを記載します。

```py
class JumanPPTokenizer(BasicTokenizer):
  def __init__(self):
    """Constructs a BasicTokenizer.
    """
    from pyknp import Juman
 
    self.do_lower_case = False
    self._jumanpp = Juman()
 
  def tokenize(self, text):
    """Tokenizes a piece of text."""
    text = convert_to_unicode(text.replace(' ', ''))
    text = self._clean_text(text)
 
    juman_result = self._jumanpp.analysis(text)
    split_tokens = []
    for mrph in juman_result.mrph_list():
      split_tokens.extend(self._run_split_on_punc(mrph.midasi))
 
    output_tokens = whitespace_tokenize(" ".join(split_tokens))
    print(split_tokens)
    return output_tokens
```

#### 3. 2で記載したクラスを適用するように、bert/tokenization.pyを修正
tokenization.py内のFullTokenizerを修正。元々BasicTokenizerを使っていた箇所をJumanPPTokenizerに変更する。

```py
  def __init__(self, vocab_file, do_lower_case=True):
    self.vocab = load_vocab(vocab_file)
    self.inv_vocab = {v: k for k, v in self.vocab.items()}
    # jumanPPTokenizerを活用するように修正
    # self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
    self.jumanpp_tokenizer = JumanPPTokenizer()
    self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)

  def tokenize(self, text):
    split_tokens = []
    # for token in self.basic_tokenizer.tokenize(text):
    for token in self.jumanpp_tokenizer.tokenize(text):
      for sub_token in self.wordpiece_tokenizer.tokenize(token):
        split_tokens.append(sub_token)
```

#### 4. run_classifier.pyに独自で行う分類タスクを実行するためのクラスを記載
run_classifier.pyに発言小町用のクラスを作成します。既存で記載されているMrpcProcessor等のクラスをコピペし、発言小町にて処理できるように修正しました。

```py
class KomachiProcessor(DataProcessor):
  """Processor for the original data set """
  def get_train_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")

  def get_dev_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")

  def get_test_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "test.tsv")), "test")

  def get_labels(self):
    """See base class."""
    # 分類するlabelの定義（テスト処理を実施するため、適当なクラスを定義した）
    return ["男性から発信するトピ", "null"]

  def _create_examples(self, lines, set_type):
    """Creates examples for the training and dev sets."""
    examples = []
    print("lines = " + str(lines))
    for (i, line) in enumerate(lines):
      print(str(i) + "番目：" + str(line))
      if i == 0:
        continue
      guid = "%s-%s" % (set_type, i)
      # 入力となるテキストをtext_aに格納するように設定
      text_a = tokenization.convert_to_unicode(line[0])
      label = tokenization.convert_to_unicode(line[1])
      examples.append(
          InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
    return examples
```

またrun_classifier.pyにprocessorsの設定があるため、上記作成したクラスを追加します。

```py
processors = {
      "cola": ColaProcessor,
      "mnli": MnliProcessor,
      "mrpc": MrpcProcessor,
      "xnli": XnliProcessor,
      "komachi": KomachiProcessor, # 実行時に呼び出すtask_name : クラス名
  }
```

#### 5. 実行
引数を設定し、run_classifier.pyを実行します。

```sh
# 任意のディレクトリのPATHを設定
export BERT_BASE_DIR=/Users/Desktop/Techcology/bert/Kyoto_Univ/Japanese_L-12_H-768_A-12_E-30_BPE

python run_classifier.py \
--task_name=komachi \
--do_train=true \
--do_eval=true \
--data_dir=./input_data/fine_tuning/komachi \
--vocab_file=$BERT_BASE_DIR/vocab.txt \
--bert_config_file=$BERT_BASE_DIR/bert_config.json \
--init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
--max_seq_length=128 \
--train_batch_size=32 \
--learning_rate=2e-5 \
--num_train_epochs=3.0 \
--output_dir=./output_data/fine_tuning/komachi \
--do_lower_case False
```

--task_name：4.のprocessorsに設定をしたtask_nameを記載
--data_dir：train.tsv等の学習データが格納されているPATHを記載
--vocab_file、--bert_config_file、--init_checkpoint：事前学習モデルにて作成されたファイルが格納されているPATHを記載
--init_checkpoint：BERTモデルに読み込むチェックポイントファイルのPATHを記載(学習した重み付けなどが記載されている)
--output_dir：出力結果の格納先を記載

上記処理の実行が終わると、下記のような結果が出力されます。（テストにて実施をした結果のため、精度は低いです。。）

```
***** Eval results *****
eval_accuracy = 0.5
eval_loss = 0.6836934
global_step = 1
loss = 0.6836934
```

#### 参考URL
[BERTを動かしてKaggle過去コンペ（Quora Question Pairs）にSubmit](https://qiita.com/t-kosugi/items/4481b77bab748b8ebdca)


# Pretrainモデルの追加学習

分類タスクとは異なり、Pretrainモデルを元に、追加学習を行いました。実装手順としては下記の通りです。
1. 学習データの準備
2. bert/tokenization.pyに学習済みモデルと同様の形態素解析クラスを記載
3. 2で記載したクラスを適用するように、bert/tokenization.pyを修正
4. 学習データの加工
5. 実行

また2,3の手順に関しては、上記「分類タスク」の発言小町の例と同様のステップのため、省略します。

### 例：発言小町のデータを活用した追加学習
#### 1. 学習データの準備
自然文が記載された.txt形式のファイルを用意します。中身は1行毎に改行されているものとし、ドキュメント毎に空行を挟みます。

```
ここは女性の多い掲示板なので投稿させて頂きます。よろしくお願い申し上げ・・・  
婚約破棄しようか悩んでいます。付き合い始めて約２ヶ月ですが、早く結婚を・・・  
（空行）
息子が引きこもって３年になります。推薦入学した大学もほとんど通わずに中・・・
```

#### 4. 学習データの加工
1にて用意されたtxtファイルを学習用の入力形式（tfrecord）に加工します。コマンドは下記の通りです。

```sh
python create_pretraining_data.py \
--input_file=./input_data/fine_tuning/sample_text.txt \
--output_file=./input_data/fine_tuning/tf_examples.tfrecord \
--vocab_file=$BERT_BASE_DIR/vocab.txt \
--do_lower_case=False \
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--masked_lm_prob=0.15 \
--random_seed=12345 \
--dupe_factor=5
```

iunput_file、output_fileは任意のディレクトリを設定。vocab_fileは既存で作成されているvocab.txtが存在するディレクトリを指定。

#### 5. 実行
4にて加工されたtfrecordファイルを入力とし、モデル学習を実行します。コマンドは下記の通りです。
※最初から学習を行いたい場合には、init_checkpointをインクルードしないようです
※デモ実施のため、num_train_stepは5と設定していますが、本来は10000ステップ以上が理想との事
※run_pretraining.pyに渡されるmax_seq_lengthとmax_predictions_per_seqパラメータは、create_pretraining_data.pyに渡される値と同じにする

```sh
python run_pretraining.py \
--input_file=./input_data/fine_tuning/tf_examples.tfrecord \
--output_dir=./output_data/fine_tuning/\
--do_train=True \
--do_eval=True \
--bert_config_file=$BERT_BASE_DIR/bert_config.json \
--init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
--train_batch_size=32 \ 
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--num_train_steps=5 \
--num_warmup_steps=10 \
--learning_rate=2e-5
```

上記実行後、モデルの精度結果が出力され、設定したoutput_dirにモデルが出力されます。

```
***** Eval results *****
global_step = 5
loss = 1.1846145
masked_lm_accuracy = 0.90485466
masked_lm_loss = 0.54624075
next_sentence_accuracy = 0.76875
next_sentence_loss = 0.6385179
```

また上記のフローをデータを変えてLivedoorニュース約7,000ドキュメントでモデル作成を行ったところ、学習データ加工に8時間（mecab利用）、モデル生成に3時間（10,000epoch、GPU使用）でした。実務利用においては現実的な処理時間で終わる感じでした。

# 調査した所感
・やっぱBERTむずい
（取り敢えず動かす事は出来ているが、Attentionや学習方法など、中身の構造を理解していないと、自信を持って利用できている感じはない）
・Fine-tuningに可能性がありそう
（文脈を考慮した単語ベクトルが取得できるため、あとは如何様にも処理する事ができ、汎用的なモデルという事を理解し、様々なタスクに活かす事が出来そうな感じはある）
・自然言語処理の流れが早すぎ問題
（約半年前にBERTが発表され注目を浴びたものの、XLNetやRoBERTaも出てきており、技術進歩の早さについていくのに大変。。）

諸々苦戦したものの最新技術を動かす事が出来ると楽しいので、今後も自然言語処理関連の技術をキャッチアップし、手を動かしていこうと思いました！