(Part 2) tensorflow 2 でhugging faceのtransformers公式のBERT日本語学習済みモデルを文書分類モデルにfine-tuningする
概要
以下の記事の続編になります。こちらの記事では、hugging faceのtransformersというライブラリを使用してBERTのfine-tuningを試しました。 transformersでの公開済みモデルを使用したfine-tuningの流れを紹介しているので、サポートされていない学習済みモデル(自分で学習させたものなど)を使って転移学習やfine-tuningをしたい場合は前回の記事を参照して頂いた方がいいかと思います。
本記事では、以下を扱います。
- 日本語サポートの拡充についてざっくりまとめる
- 追加された学習済みモデルを使った、前回いまいちだった日本語文書分類モデルの精度の向上 → 飛躍的に精度上がりました!!!
transformersの日本語学習済みモデルのサポート!!!
おはようござえます、日本の友達
— Hugging Face (@huggingface) 2019年12月13日
Hello, Friends from Japan 🇯🇵!
Thanks to @NlpTohoku, we now have a state-of-the-art Japanese language model in Transformers, `bert-base-japanese`.
Can you guess what the model outputs in the masked LM task below? pic.twitter.com/XIBUu7wrex
ついに日本語の学習済みモデルがサポートされました! (v2.2.2時点)
追加されたもの
使えるようになったのは以下の4つの学習済みモデルと、
- bert-base-japanese: Mecabで分かち書き & WordPieceでSubwordに分割して学習
- bert-base-japanese-whole-word-masking: 上記に加えて、whole word maskingを適用
- bert-base-japanese-char: Mecabで分かち書き & characterレベルのSubwordに分割して学習
- bert-base-japanese-char-whole-word-masking: 上記に加えて、whole word maskingを適用
さらに、以下のtokenizerです。
- BertJapaneseTokenizer: 以下の新しいtokenizerを内部で使用する
学習済みモデルについて
上記の学習済みモデルはこちらが元になっているようです。(東北大学の乾・鈴木研究室の院生の方の取り組みとのことです) github.com
使用上注意すべきことをまとめます。Mecabのどの辞書が使われているか確認するのは面倒なので注意が必要そうです。
(再挑戦) transfer leaning with BERT of huggingface transformers
前回は、以下のような取り組みをしました。
こちら(ストックマーク?)で公開されている以下のような事前学習済みモデルを使いたいと思います。
事前学習用データ 形態素解析 語彙数 日本語ビジネスニュース記事(300万記事) MeCab + NEologd 32,000語(CLS、SEP、UNK等除く) このモデルを文書分類モデルに転移させてlivedoor ニュースコーパスのカテゴリ分類を学習させてみます。
ただし、結果は散々でして、train accuracyが0.1652 、validation accuracyが0.2172に落ち着きました。9クラスあるので、ランダムよりほんの少し当たっている程度のものが出来上がりました。 恐らく分かち書きあたりの処理が適切でないのかなーといった所ですが改善できていませんでした。そこで、今回は新たにサポートされた学習済みモデルを使って再挑戦しました。「bert-base-japanese」と「bert-base-japanese-char」の2つを試します。
すべてまとめたColaboratoryのコードはこちらにあります。
前処理
流れは以下のようになります。
- MeCabのインストール
- livedoor news corpusを読み込み、以下の前処理を施す
- 本文を分かち書き
- ラベル(カテゴリ)を数値に変換
- datasetをtrainとvalidation用に分割
- tensorflow.Datasetの形式に変換
- BertJapaneseTokenizerを使用して、以下の処理を行う
- 日本語をidに変換
- 以下のBERTで使用する補助的な特徴量を生成する:
- input_ids: テキストをidに変換したもの
- token_type_ids: BERT modelの内部で使用されるもの (詳しくは原論文)
- attention_mask: BERT modelの内部で使用されるもの (詳しくは原論文)
- label: 教師信号
前回と重複する部分が多いので、色々省きます。BERTへの入力形式は、glue_convert_examples_to_features関数を参考 (ソース) にしてtf.data.Dataset APIで再現しました。中でもtf.dataの.map
を使って、tokenizerを文章に適用するためのコードをここでは紹介します。(参考: tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita)
import tensorflow as tf from transformers import BertJapaneseTokenizer # sentence1がテキスト。分かち書きは不要 data_no_wakati = { "train": tf.data.Dataset.from_tensor_slices({ 'sentence1': df_train['body'].tolist(), 'sentence2': ['', ] * len(df_train), 'label': df_train['label'].tolist() }), "validation": tf.data.Dataset.from_tensor_slices({ 'sentence1': df_val['body'].tolist(), 'sentence2': ['', ] * len(df_val), 'label': df_val['label'].tolist() }) } def tokenize_map_fn(tokenizer, max_length=100): """to be applied to map function for pretrained tokenizer""" def _tokenize(text_a, text_b, label): # BertJapaneseTokenizerを適用して # 「分かち書き」「テキストをidに変換」「token_type_idsを生成」 inputs = tokenizer.encode_plus( text_a.numpy().decode('utf-8'), text_b.numpy().decode('utf-8'), add_special_tokens=True, max_length=max_length, ) input_ids, token_type_ids = inputs["input_ids"], inputs["token_type_ids"] # attention_maskを作成 # The mask has 1 for real tokens and 0 for padding tokens. Only real # tokens are attended to. attention_mask = [1] * len(input_ids) return input_ids, token_type_ids, attention_mask, label def _map_fn(data): """入出力の調整""" text_a = data['sentence1'] text_b = data['sentence2'] label = data['label'] out = tf.py_function(_tokenize, inp=[text_a, text_b, label], Tout=(tf.int32, tf.int32, tf.int32, tf.int32)) return ( {"input_ids": out[0], "token_type_ids": out[1], "attention_mask": out[2]}, out[3] ) return _map_fn def load_dataset(data, tokenizer, max_length=128, train_batch=8, val_batch=32): # Prepare dataset for BERT as a tf.data.Dataset instance train_dataset = data['train'].map(tokenize_map_fn(tokenizer, max_length=max_length)) train_dataset = train_dataset.shuffle(100).padded_batch(train_batch, padded_shapes=({'input_ids': max_length, 'token_type_ids': max_length, 'attention_mask': max_length}, []), drop_remainder=True) train_dataset = train_dataset.prefetch(tf.data.experimental.AUTOTUNE) # validation dataにも同じ処理 valid_dataset = data['validation'].map(tokenize_map_fn(tokenizer, max_length=max_length)) valid_dataset = valid_dataset.padded_batch(val_batch, padded_shapes=({'input_ids': max_length, 'token_type_ids': max_length, 'attention_mask': max_length}, []), drop_remainder=True) valid_dataset = valid_dataset.prefetch(tf.data.experimental.AUTOTUNE) return train_dataset, valid_dataset # tokenizerを定義、vocabファイルやtokenizerの設定が読み込まれる tokenizer = BertJapaneseTokenizer.from_pretrained('bert-base-japanese') # 実行 train_dataset, valid_dataset = load_dataset(data_no_wakati, tokenizer, max_length=max_length, train_batch=train_batch, val_batch=val_batch)
model定義
前回とほぼ同じものを使用します。(パラメータやFC層の深さなどを変更しています)
BERTの定義済みモデルは、transformers.TFBertModelクラスをベースにして
TFBertModel + (タスクに応じた層 or Denseなど)
のような構造になっています。なので、TFBertModelに学習済みの重みをロード > タスクに応じた層を追加 > 転移学習 という流れをふめばいいです。 これは公式でサポートされていようが、自分で作った学習済みモデルを再利用しようが、違いありません。 どのように層を追加するのかという点は、以下のようなタスクがtransformersに実装済みなので、ソースを参照すると感覚がつかめると思います。
クラス | タスク |
---|---|
TFBertForPreTraining | 事前学習 |
TFBertForMaskedLM | masked tokenの予測 |
TFBertForNextSentencePrediction | 次センテンス予測 |
TFBertForSequenceClassification | 文書分類 |
TFBertForQuestionAnswering | 質問応答 |
注意として、TFBertModel自体は単なるtf.keras layerの連結ではないため、間に追加でlayerをはさんだり、入れ替えたりできません。 なので、transfer learningやfine-tuningをするさいはTFBertModelをbaseに置いて、その後ろにfunctional APIでtf.keras layerを追加してく形をとるのがいいと思います。
pretrained weightのload
from transformers import TFBertModel bert = TFBertModel.from_pretrained('bert-base-japanese')
fine-tuning用に層を足す関数
from tensorflow import keras from tensorflow.keras import optimizers, losses, metrics from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, Dense, Dropout def make_model(bert, num_classes, max_length, bert_frozen=True): # bertモデルはリストになっているので、取り出す # 層をfreeze(学習させないように)する bert.layers[0].trainable = not bert_frozen # input input_ids = Input(shape=(max_length, ), dtype='int32', name='input_ids') attention_mask = Input(shape=(max_length, ), dtype='int32', name='attention_mask') token_type_ids = Input(shape=(max_length, ), dtype='int32', name='token_type_ids') inputs = [input_ids, attention_mask, token_type_ids] # bert x = bert.layers[0](inputs) # x: sequence_output, pooled_output # 2種類の出力がある。 # TFBertForSequenceClassificationにならってpooled_outputのみ使用 out = x[1] # fc layer(add layers for transfer learning) out = Dropout(0.25)(out) out = Dense(128, activation='relu')(out) out = Dropout(0.5)(out) out = Dense(num_classes, activation='softmax')(out) return Model(inputs=inputs, outputs=out)
学習
上記の諸々のコードを読んで、学習をまわします。以下のコードと「bert-base-japanese-char」用のコードをはしらせます。(bert-base-japaneseをbert-base-japaneseに置換するだけなので省略します)
epochs = 7 max_length = 500 train_batch = 32 val_batch = 64 num_classes = 9 # Load dataset, tokenizer, model from pretrained vocabulary tokenizer = BertJapaneseTokenizer.from_pretrained('bert-base-japanese') train_dataset, valid_dataset = load_dataset(data_no_wakati, tokenizer, max_length=max_length, train_batch=train_batch, val_batch=val_batch) # define fine-tuning model bert = TFBertModel.from_pretrained('bert-base-japanese') model = make_model(bert, num_classes, max_length) optimizer = optimizers.Adam() loss = losses.SparseCategoricalCrossentropy() metric = metrics.SparseCategoricalAccuracy('accuracy') model.compile(optimizer=optimizer, loss=loss, metrics=[metric]) # # Train and evaluate using tf.keras.Model.fit() history = model.fit(train_dataset, epochs=epochs, validation_data=valid_dataset)
結果
bert-base-japanese
Epoch 1/7 172/172 [==============================] - 648s 4s/step - loss: 1.8049 - accuracy: 0.3547 - val_loss: 0.0000e+00 - val_accuracy: 0.0000e+00 Epoch 2/7 172/172 [==============================] - 636s 4s/step - loss: 1.3752 - accuracy: 0.5258 - val_loss: 1.0417 - val_accuracy: 0.6691 Epoch 3/7 172/172 [==============================] - 636s 4s/step - loss: 1.2424 - accuracy: 0.5709 - val_loss: 0.9431 - val_accuracy: 0.6981 Epoch 4/7 172/172 [==============================] - 632s 4s/step - loss: 1.1515 - accuracy: 0.6025 - val_loss: 0.8987 - val_accuracy: 0.6886 Epoch 5/7 172/172 [==============================] - 632s 4s/step - loss: 1.1066 - accuracy: 0.6183 - val_loss: 0.8679 - val_accuracy: 0.7109 Epoch 6/7 172/172 [==============================] - 632s 4s/step - loss: 1.0679 - accuracy: 0.6357 - val_loss: 0.8335 - val_accuracy: 0.7254 Epoch 7/7 172/172 [==============================] - 635s 4s/step - loss: 1.0587 - accuracy: 0.6339 - val_loss: 0.7863 - val_accuracy: 0.7349
bert-base-japanese-char
Epoch 1/7 172/172 [==============================] - 638s 4s/step - loss: 1.7507 - accuracy: 0.3721 - val_loss: 0.0000e+00 - val_accuracy: 0.0000e+00 Epoch 2/7 172/172 [==============================] - 626s 4s/step - loss: 1.3596 - accuracy: 0.5194 - val_loss: 1.0770 - val_accuracy: 0.6267 Epoch 3/7 172/172 [==============================] - 625s 4s/step - loss: 1.2484 - accuracy: 0.5587 - val_loss: 0.9786 - val_accuracy: 0.6657 Epoch 4/7 172/172 [==============================] - 626s 4s/step - loss: 1.1731 - accuracy: 0.5868 - val_loss: 0.9375 - val_accuracy: 0.6719 Epoch 5/7 172/172 [==============================] - 626s 4s/step - loss: 1.1457 - accuracy: 0.6003 - val_loss: 0.9081 - val_accuracy: 0.6853 Epoch 6/7 172/172 [==============================] - 625s 4s/step - loss: 1.1104 - accuracy: 0.6163 - val_loss: 0.8806 - val_accuracy: 0.6920 Epoch 7/7 172/172 [==============================] - 622s 4s/step - loss: 1.0937 - accuracy: 0.6148 - val_loss: 0.8554 - val_accuracy: 0.7020
結果一覧
精度だけで比較すると bert-base-japaneseが最も精度が高いことがわかります。 どこまでいいスコアなのかはさておき、少なくとも前回から比べると飛躍的に向上しました! bert-base-japaneseとbert-base-japanese-charの違いはよくわかりませんでした。(学習時間も収束の仕方もほぼかわらないので)
base model | test acc | eval acc |
---|---|---|
前回 | 0.1652 | 0.2172 |
bert-base-japanese | 0.6339 | 0.7349 |
bert-base-japanese-char | 0.6148 | 0.7020 |
まとめ
日本語サポートの拡充についてざっくりまとめて、前回いまいちだった日本語文書分類モデルを今回追加された学習済みモデル (bert-base-japanese, bert-base-japanese-char)を使ったものに変更して、精度の向上を達成しました。
colaboratory上のGPUで1epoch 10分以上かかっているのでやっぱり処理は重いなーという印象を受けますが、ここまで簡単に試せるのであれば使う機会も増えるだろうと思います。
また、MecabTokenizerが分かち書きや簡単な標準化をしてくれるので、mecabのインストールさえしてしまえば、(BERTを使わなくても)MecabTokenizerだけ前処理用に使うのも便利そうだなと感じました。
最後まで読んで頂きありがとうございます!何かの参考になれば幸いです!
Refs
- GitHub - huggingface/transformers: 🤗Transformers: State-of-the-art Natural Language Processing for Pytorch and TensorFlow 2.0.
- GitHub - cl-tohoku/bert-japanese: BERT models for Japanese text.
- (Part 1) tensorflow2でhuggingfaceのtransformersを使ってBERTを文書分類モデルに転移学習する - メモ帳
- tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita