機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

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の入出力系列は以下のように表現されています。

f:id:kento1109:20190824072049p:plain
論文より引用

また、入力系列が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だけで十分の結果が出せたと思います。
次回は、これを日本語に適用してみたいと思います。