メモ帳

python, juliaで機械学習をやっていく

(Part 2) tensorflow 2 でhugging faceのtransformers公式のBERT日本語学習済みモデルを文書分類モデルにfine-tuningする

概要

以下の記事の続編になります。こちらの記事では、hugging faceのtransformersというライブラリを使用してBERTのfine-tuningを試しました。 transformersでの公開済みモデルを使用したfine-tuningの流れを紹介しているので、サポートされていない学習済みモデル(自分で学習させたものなど)を使って転移学習やfine-tuningをしたい場合は前回の記事を参照して頂いた方がいいかと思います。

tksmml.hatenablog.com

本記事では、以下を扱います。

  • 日本語サポートの拡充についてざっくりまとめる
  • 追加された学習済みモデルを使った、前回いまいちだった日本語文書分類モデルの精度の向上 → 飛躍的に精度上がりました!!!

transformersの日本語学習済みモデルのサポート!!!

ついに日本語の学習済みモデルがサポートされました! (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を内部で使用する
    • MecabTokenizer: mecabでテキストを分かち書き
    • CharacterTokenizer: character-levelに分割

学習済みモデルについて

上記の学習済みモデルはこちらが元になっているようです。(東北大学乾・鈴木研究室の院生の方の取り組みとのことです) 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