データナード

機械学習と自然言語処理についての備忘録 (旧ナード戦隊データマン)

PAWS-Xに対してhuggingfaceのBERTを使う

PAWSデータセットは、パラフレーズ検出のための英語データセットの一つですが、PAWS-Xはその多言語版です。

今回は、huggingfaceのBERTをPAWS-Xに使います。

概要

huggingface/transformersというプロジェクトでは、NLU(自然言語理解)やNLG(自然言語生成)のための一般目的の事前訓練済みモデルとして、BERT、GPT-2、RoBERTa、DistilBert、XLNet、CTRLなどを公開しており、TensorFlow 2.0とPyTorchで動作します。

これらのうち、BERTを使い、PAWS-Xデータセットを用いて、訓練とテストを行います。

pawsx: https://github.com/google-research-datasets/paws/tree/master/pawsx

ベースライン

ベースラインモデルは事情によりコードは公開できませんが、ディープ系ではなく、特徴量設計をちまちまと行っただけのモデルで、精度は以下のようになりました。

[BASELINE]
              precision    recall  f1-score   support

       False       0.62      0.91      0.74      1117
        True       0.72      0.29      0.42       883

    accuracy                           0.64      2000
   macro avg       0.67      0.60      0.58      2000
weighted avg       0.66      0.64      0.60      2000

これを基準にします。

コード

訓練

import pandas as pd
import tensorflow as tf
from tensorflow.python.lib.io.tf_record import TFRecordWriter

from transformers import BertTokenizer, TFBertForSequenceClassification, glue_convert_examples_to_features

max_length = 128
tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased')
model = TFBertForSequenceClassification.from_pretrained(
    'bert-base-multilingual-cased', num_labels=2)

feature_spec = {
    "idx": tf.io.FixedLenFeature([], tf.int64),
    "sentence1": tf.io.FixedLenFeature([], tf.string),
    "sentence2": tf.io.FixedLenFeature([], tf.string),
    "label": tf.io.FixedLenFeature([], tf.int64)
}


def create_tf_example(features):
    label = features["label"]
    tf_example = tf.train.Example(features=tf.train.Features(
        feature={
            'idx':
            tf.train.Feature(int64_list=tf.train.Int64List(
                value=[features["idx"]])),
            'sentence1':
            tf.train.Feature(bytes_list=tf.train.BytesList(
                value=[features["sentence1"].encode('utf-8')])),
            'sentence2':
            tf.train.Feature(bytes_list=tf.train.BytesList(
                value=[features["sentence2"].encode('utf-8')])),
            'label':
            tf.train.Feature(int64_list=tf.train.Int64List(value=[label]))
        }))
    return tf_example


def data_transformer(data_path, prefix="train"):
    writer = TFRecordWriter("{}.tfrecord".format(prefix))
    df = pd.read_csv(data_path, sep="\t")
    df.columns = ["idx", "sentence1", "sentence2", "label"]
    df["idx"] = df["idx"].astype(int)
    df["sentence1"] = df["sentence1"].astype(str)
    df["sentence2"] = df["sentence2"].astype(str)
    df["label"] = df["label"].astype(int)
    for i, d in df.iterrows():
        features = {
            "idx": d["idx"],
            "sentence1": d["sentence1"],
            "sentence2": d["sentence2"],
            "label": d["label"]
        }
        example = create_tf_example(features)
        writer.write(example.SerializeToString())


def parse_example(example_proto):
    return tf.io.parse_single_example(example_proto, feature_spec)


if __name__ == "__main__":
    data_transformer("../data/pawsx/train.tsv", "train")
    data_transformer("../data/pawsx/dev.tsv", "dev")

    tr_ds = tf.data.TFRecordDataset("train.tfrecord")
    val_ds = tf.data.TFRecordDataset("dev.tfrecord")
    tr_parse_ds = tr_ds.map(parse_example)
    val_parse_ds = val_ds.map(parse_example)

    train = glue_convert_examples_to_features(tr_parse_ds,
                                              tokenizer,
                                              max_length,
                                              task="mrpc",
                                              label_list=["0", "1"])

    dev = glue_convert_examples_to_features(val_parse_ds,
                                            tokenizer,
                                            max_length,
                                            task="mrpc",
                                            label_list=["0", "1"])

    train = train.shuffle(1024).batch(32).repeat(-1)
    dev = dev.batch(64)
    optimizer = tf.keras.optimizers.Adam(learning_rate=3e-5,
                                         epsilon=1e-08,
                                         clipnorm=1.0)
    loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
    metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')
    model.compile(optimizer=optimizer, loss=loss, metrics=[metric])

    history = model.fit(train,
                        epochs=10,
                        steps_per_epoch=100,
                        validation_data=dev,
                        validation_steps=10,
                        verbose=True)

    model.save_pretrained('./save/')

テスト

import pandas as pd
from transformers import BertForSequenceClassification
from transformers import BertTokenizer

pytorch_model = BertForSequenceClassification.from_pretrained('./save/',
                                                              from_tf=True)
tokenizer = BertTokenizer.from_pretrained("bert-base-multilingual-cased")
test_data = pd.read_csv("../data/pawsx/test.tsv", sep="\t")


def predict(df):
    out = []
    for i, d in df.iterrows():
        inputs = tokenizer.encode_plus(d["sentence1"],
                                       d["sentence2"],
                                       add_special_tokens=True,
                                       max_length=128,
                                       return_tensors='pt')
        pred = pytorch_model(
            inputs["input_ids"],
            token_type_ids=inputs["token_type_ids"])[0].argmax().item()
        if pred:
            out.append(True)
        else:
            out.append(False)
    return out


if __name__ == "__main__":
    from sklearn.metrics import classification_report
    y_true = test_data["label"]
    y_pred = predict(test_data)
    print(classification_report(y_true, y_pred))

結果

[BERT]
              precision    recall  f1-score   support

           0       0.75      0.85      0.79      1117
           1       0.77      0.64      0.70       883

    accuracy                           0.76      2000
   macro avg       0.76      0.74      0.75      2000
weighted avg       0.76      0.76      0.75      2000

明らかに、ベースラインを超えていることがわかります。

考察

BERTでファインチューニングを行えば、特定のドメインのタスクを高い精度で解けることがあります。これは、BERTが膨大なデータからの事前訓練によって得た圧縮された情報が、特定のドメインの問題に対してファインチューニングすることにより、古典的な特徴量設計のモデルよりも(エントロピーの意味で)多くの情報を持っていることを示唆しているのではないか、と考えています。

ただし、ファインチューニングによってドメイン適応すると、そのドメイン外のデータへの適用能力は下がるため、パラフレーズコーパスが、量だけではなく、多様な例を保有していたほうが適用の幅が広がる可能性があります。

「モデルの問題か、データの問題か」ということについて考えれば、「特徴量設計をして何らかのアルゴリズムで訓練し、予測する」というところまではモデルの問題と言えそうですが、「訓練データやテストデータの量・質・幅」などについては、データの問題と言えます。Kaggleのような場ではモデルの問題が多く考えられていますが、実用性の高いモデルを作る上では、データの問題が重要になります。

また、Paraphrase RetrievalとParaphrase Detectionは、精度以外に要求されるものが異なっている可能性があります。

速度、メモリ、ストレージの要件はParaphrase Retrievalのほうがきついため、BERTによる今回のモデルをParaphrase Retrievalに適用するためには、それらの品質要求が満たされる必要があります。LTRと同等と考えれば、高品質の結果を得るために、top-kに対するrerankを考慮することになり得ます。候補要素を得るための手法としては、faissのようなベクトル検索が高速であるため、精度とその他の品質要件を、適用する現実世界の問題に合わせて調整する必要がありそうです。

参考

  1. Best Practices for NLP Classification in TensorFlow 2.0
  2. GitHub - huggingface/transformers: 🤗 Transformers: State-of-the-art Natural Language Processing for TensorFlow 2.0 and PyTorch.
  3. paws/pawsx at master · google-research-datasets/paws · GitHub