pytorch-transformersを触ってみる④
はじめに
前回はfine tuningということで、GLUEタスクのSST-2に取り組みました。
kento1109.hatenablog.com
また、GLUEタスクのfine tuningを実行するためのスクリプト(run_glue.py )のコードを眺めました。
今回は、CoNLL(NERの共通タスク)のためのfine tuningにチャレンジします。
BERT-NERですが、以下の内容が参考となりました。
https://www.depends-on-the-definition.com/named-entity-recognition-with-bert/
BERT-NER/run_ner.py at experiment · kamalkraj/BERT-NER · GitHub
GitHub - kyzhouhzau/BERT-NER: Use Google's BERT for named entity recognition (CoNLL-2003 as the dataset).
ただし、pytorch-pretrained-bertを利用している点に留意する必要があります。
準備
データの入手及びロードを行います。
データの入手
今回は「CoNLL 2003 shared task (NER) data」を利用します。
データは以下から入手可能です。
NER/corpus/CoNLL-2003 at master · synalp/NER · GitHub
このデータセットは、以下のようなフォーマットとなっています。
-DOCSTART- -X- O O CRICKET NNP I-NP O - : O O LEICESTERSHIRE NNP I-NP I-ORG TAKE NNP I-NP O OVER IN I-PP O AT NNP I-NP O TOP NNP I-NP O AFTER NNP I-NP O INNINGS NNP I-NP O VICTORY NN I-NP O . . O O LONDON NNP I-NP I-LOC 1996-08-30 CD I-NP O
-DOCSTART-
で文章が区切られており、各文は空行で区切られています。
NERタスクの場合、4列目をラベルとして抽出します。
データのロード
これらのデータをロードしておきます。
自前でloaderを作っても良いですが、ここではtorchtextのloaderを利用します。
torchtextを利用したCoNLLのデータセットのloaderについては以前まとめました。
Pytorch:テキストの前処理(torchtext)③ - 機械学習・自然言語処理の勉強メモ
ただし、CoNLL2003は-DOCSTART-
が含まれていますが、これはNERでは不要です。
デフォルトのSequenceTaggingDatasetではこれを考慮できていないので少し工夫が必要です。
具体的には、SequenceTaggingDatasetのコードをコピーして少し手を加えた以下のloaderを用意します。
from torchtext import data, datasets class SequenceTaggingDataset(data.Dataset): @staticmethod def sort_key(example): for attr in dir(example): if not callable(getattr(example, attr)) and \ not attr.startswith("__"): return len(getattr(example, attr)) return 0 def __init__(self, path, fields, encoding="utf-8", separator="\t", **kwargs): examples = [] columns = [] with open(path, encoding=encoding) as input_file: for line in input_file: line = line.strip() # add start if line.startswith('-DOCSTART-'): continue # add end if line == "": if columns: examples.append(data.Example.fromlist(columns, fields)) columns = [] else: for i, column in enumerate(line.split(separator)): if len(columns) < i + 1: columns.append([]) columns[i].append(column) if columns: examples.append(data.Example.fromlist(columns, fields)) super(SequenceTaggingDataset, self).__init__(examples, fields, **kwargs)
これで以下のようにして、CoNLLのデータセットが簡単に読み込めるようになります。
WORD = data.Field() POS1 = data.Field() POS2 = data.Field() LABEL = data.Field() train_ds, valid_ds, test_ds = SequenceTaggingDataset.splits(fields=[('word', WORD), ('pos1', POS1), ('pos2', POS2), ('label', LABEL)], path='CoNLL-2003' , separator=" ", train="eng.train", validation="eng.testa", test="eng.testb" )
また、後々使うのでラベルリストを取得しておくといいです。
LABEL.build_vocab(train_ds, valid_ds, test_ds) label_list = list(LABEL.vocab.freqs) # ['I-ORG', 'O', 'I-MISC', 'I-PER', 'I-LOC', 'B-LOC', 'B-MISC', 'B-ORG']
fine tuning
NERデータセットを用いて訓練するまでに必要な処理をまとめます。
pretrained modelの読み込み
以下のようにて各クラスをインスタンス化します。
from pytorch_transformers import BertConfig, BertTokenizer, BertForTokenClassification model_name = 'bert-base-uncased' config = BertConfig.from_pretrained(model_name, num_labels=len(label_list)) tokenizer = BertTokenizer.from_pretrained(model_name, do_lower_case=True) model = BertForTokenClassification.from_pretrained(model_name, config=config)
※今回はNERなので、モデルはTokenClassificationとなります。
BERTの入力に変換
読み込んだデータセットは、BERTの入力形式に変換してあげる必要があります。
前回のGLUEタスクで利用したconverterを流用する形で実装していきたいと思います。
入力に関しては元のスクリプトもInputExampleクラスで定義している大した修正は不要です。
ただし、NERはTokenClassificationなので、出力ラベルについては、入力系列と同じ長さの系列を作るように修正が必要です。しかし、BERTの入力系列は少し特殊なので、その点を考慮しないといけません。
論文の図では、NERの入出力系列は以下のように表現されています。
また、入力系列がWordPiece tokenizerにより分割されることについては、以下のように説明しています。
We feed each CoNLL-tokenized input word into our WordPiece tokenizer and use the hidden state corresponding to the first sub-token as input to the classifier.
Jim | Hen | ##son | was | a | puppet | ##eer |
I-PER | I-PER | X | O | O | O | X |
Where no prediction is made for X. Since the WordPiece tokenization boundaries are a known part of the input, this is done for both training and test.
要は、WordPiece tokenizerの先頭以外は予測は不要ということです。
後、先頭の[CLS]トークンについては、それに対応するCラベルを付与しています。
また、入力は1文単位であるため末尾の[SEP]トークンは不要となります。
この点を考慮した修正ができればOKです。
こちらのコードでは、それらを入力の作り方をしているので、これを持ってきてもOKです。
BERTの訓練・評価
入力データをBERTのTokenClassification用に整形できました。
では、train()を呼び出して訓練を行います。
global_step, tr_loss = train(args, dataset, model, tokenizer)
しばらく待てば学習が完了します。
完了後、結果を評価してみます。
precision : 0.9560
recall : 0.9596
f1-score : 0.9577
良い感じで学習出来ているのではないでしょうか。
(今回は正確な評価が目的ではないので、[CLS]
やX
のラベルが含まれた状態で評価しています
。)
正解例と予測結果を見比べてみます。
['[CLS]', 'soccer', '-', 'japan', 'get', 'lucky', 'win', ',', 'china', 'in', 'surprise', 'defeat', '.']
TRUE : ['[CLS]', 'O', 'O', 'I-LOC', 'O', 'O', 'O', 'O', 'I-PER', 'O', 'O', 'O', 'O']
PRED : ['[CLS]', 'O', 'O', 'I-LOC', 'O', 'O', 'O', 'O', 'I-LOC', 'O', 'O', 'O', 'O']
この場合、chinaがズレていますが、アノテーションが間違っているのではないかという気もします。
おわりに
今回はBERTのfine tuningとしてCoNLLタスクに取り組んでみました。
結果を見る限りでは、fine tuningだけで十分の結果が出せたと思います。
次回は、これを日本語に適用してみたいと思います。