Edited at

BERTで日本語で事前訓練・ファインチューニングする

BERTとは言語表現モデルで、GLUEデータセットなどに対して(ファインチューニングして)最近SOTAを達成したものです。今回は、日本語ツイートの感情分析を通じてファインチューニングと事前訓練を試します。


BERTの基本


  1. 事前訓練

  2. ファインチューニング

以上。


追記 2018/11/09 20:57

この記事はどちらかというと理論家ではなくエンジニア向けの説明なので、「BERTは双方向Transformer」とか「そのTransformerとはなんだ」とか、「事前訓練ではどんなタスクを解いているのか」とか、そういうことを知りたければ他の記事を参考にしてください。

BERTを単に「使うだけ」の方は、事前訓練の方法とファインチューニングの方法さえわかっていればそれなりに使えるかと思います。今後「BERTが簡単に使えるライブラリ」みたいなものが出てくるとしたら、その2つさえおさえておけば良いと思います。


BERTのダウンロード

git clone https://github.com/google-research/bert

wget https://storage.googleapis.com/bert_models/2018_11_03/multilingual_L-12_H-768_A-12.zip
unzip multilingual*


ツイート感情分析データの用意

以下の記事で用いた方法でデータを取得します。

https://qiita.com/sugiyamath/items/7cabef39390c4a07e4d8

取得済みのデータは以下。

https://github.com/sugiyamath/bert/tree/master/JAS


run_classifier.pyを修正

以下を追加します。

class JasProcessor(DataProcessor):

def read_tsv(self, path):
df = pd.read_csv(path, sep="\t")
return [(str(text), str(label)) for text,label in zip(df['text'], df['label'])]

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."""
return ["0", "1", "2", "3", "4", "5"]

def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
guid = "%s-%s" % (set_type, i)
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

さらにmain関数内の以下部分を修正します。

  processors = {

"cola": ColaProcessor,
"mnli": MnliProcessor,
"mrpc": MrpcProcessor,
"xnli": XnliProcessor,
"jas": JasProcessor
}


実行

JASをGLUEデータセットのディレクトリの中につっこみます。

mv JAS glue_data

ついで、以下コマンドを実行します。(exportの部分は修正してください。)


export BERT_BASE_DIR=/path/to/bert/multilingual_L-12_H-768_A-12
export GLUE_DIR=/path/to/glue_data

python run_classifier.py \
--task_name=JAS \
--do_train=true \
--do_eval=true \
--data_dir=$GLUE_DIR/JAS \
--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=/tmp/jas_output/

結果は以下です。

eval_accuracy = 0.363

eval_loss = 1.5512451
global_step = 937
loss = 1.5512451


事前訓練

MeCabのボキャブラリーに合わせてコードを修正したらどうなるか試してみましょう。

修正したtokenizerは以下です。

https://github.com/sugiyamath/bert/blob/master/tokenization.py

修正部分は


  1. テキストの正規化をしないようにする。(濁点が消えないように)

  2. 中国文字を認識しないようにする。(文字ベースにならないように)

vocab.txtにMeCabを使ってWikipediaから得たボキャブラリーを記述します。

以下スクリプトを実行。

#!/bin/bash

export BERT_BASE_DIR=/root/work/bert/jamodel
FILE=/root/work/tf_examples.tfrecord

if [ -f $FILE ]; then
echo "FILE EXISTS"
else
python create_pretraining_data.py \
--input_file=/root/work/data/splitted_1.txt \
--output_file=/root/work/tf_examples.tfrecord \
--vocab_file=$BERT_BASE_DIR/vocab.txt \
--do_lower_case=True \
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--masked_lm_prob=0.15 \
--random_seed=12345 \
--dupe_factor=5
fi

python run_pretraining.py \
--input_file=/root/work/tf_examples.tfrecord \
--output_dir=/root/work/pretraining_output \
--do_train=True \
--do_eval=True \
--bert_config_file=$BERT_BASE_DIR/bert_config.json \
--train_batch_size=32 \
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--num_train_steps=20 \
--num_warmup_steps=10 \
--learning_rate=2e-5

結果は... run_pretraining.pyでOOMエラー。理由は、ボキャブラリーのサイズがでかすぎてネットワークが巨大になるので。

ちなみに、bert_configは以下です。

{

"attention_probs_dropout_prob": 0.1,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 6,
"num_hidden_layers": 6,
"type_vocab_size": 2,
"vocab_size": 1710509
}

ちなみに、splitted_1.txtはこちらを参考にしていますが、create_pretraining_data.pyの実行は正常に完了しています。

要するに、MeCabを使った場合、語彙を切り捨てなければなりません。語彙の圧縮等の目的のために使えるsentencepieceを使うべきです。

sentencepieceによるボキャブラリーファイル (vocab.txt) は以下です。

https://github.com/sugiyamath/bert/blob/master/jamodel/vocab.txt

同ディレクトリにsentencepieceのモデルも入っています。

また、以下はBERTの事前訓練の上で注意すべきことを2つ書いています。

https://qiita.com/sugiyamath/items/92cc15a90276e06e17f0

splitted_1.txtはjawiki全体ではないため、全体で訓練させるためにはファイルを分割する必要があります。


参考

[0] https://qiita.com/Kosuke-Szk/items/4b74b5cce84f423b7125

[1] https://github.com/google-research/bert