🤗

Hugging Face Courseで学ぶ自然言語処理とTransformer 【part7】

2021/09/01に公開1

はじめに

この記事ではHugging Face CourseのChapter 3、モデルのFine-tuningを行うためProcessing the dataの内容をベースにデータセットの準備についてまとめていきます。

一個前の記事はこちら

コードの実行は今回もGoogle Colabで行う例になります。

Hugging Face Datasets

Hugging FaceではTransformerモデルだけでなく、色々な自然言語処理のタスク向けの学習データセットも公開されています。
こちらからどのようなデータセットが公開されているか検索することができます。

データセットの利用も下記のようにdatasetsライブラリから簡単にできるようになっています。

今回はGLUE Benchmark[1]に含まれるもののうちMRPCデータセットを使用したモデルの学習について見ていきます。

datasetsライブラリのインストール

!pip install datasets

MRPCデータセット

MRPC(Microsoft Research Paraphrase Corpus)は5801ペアのテキストに対して、それぞれのペアが同じ内容を指す文かどうかをラベルとして持っているようなデータセットです。[2] 比較的小規模なデータセットであるため、ちょっと試したりするのにも適しています。

MRPCデータセットをダウンロード

from datasets import load_dataset

raw_datasets = load_dataset("glue", "mrpc")
print(raw_datasets)
DatasetDict({
    train: Dataset({
        features: ['sentence1', 'sentence2', 'label', 'idx'],
        num_rows: 3668
    })
    validation: Dataset({
        features: ['sentence1', 'sentence2', 'label', 'idx'],
        num_rows: 408
    })
    test: Dataset({
        features: ['sentence1', 'sentence2', 'label', 'idx'],
        num_rows: 1725
    })
})
  • datasetsライブラリ経由でロードしたデータセットはDatasetDictという形式のオブジェクトに格納されます。

  • train, validation, testに分かれており、それぞれ

    • sentence1: ペアの1文目
    • sentence2: ペアの2文目
    • label: 正解ラベル(ペアが同じ内容なら1、違う場合は0)
    • idx: データのインデックス

    といった内容になります。

  • データセットは初回実行時にデフォルトでは~/.cache/huggingface/dataset以下にキャッシュが保存され、以降キャッシュからロードされて使うことができます。
    (Colaboratoryの場合は/root/.cache/huggingface/datasets/glue/mrpc/1.0.0/xxxx・・・みたいなところにキャッシュが保存されます。)

各データへは下記のようにしてアクセスできます。

raw_train_dataset = raw_datasets["train"]
raw_train_dataset[0]
{
    'idx': 0,
    'label': 1,
    'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
    'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .'
}

ラベル情報はすでに数値データとして入っていますが、それぞれの数値がどのラベルを指すのかといった情報はfeaturesにアクセスして確認します。

raw_train_dataset.features
{
    'idx': Value(dtype='int32', id=None),
    'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None),
    'sentence1': Value(dtype='string', id=None),
    'sentence2': Value(dtype='string', id=None)
}

Preprocessing

モデルの学習を行うため、データセットに含まれるテキストに対しては前処理をして数値データに変換しなければなりませんでした。
今回の場合は入力は一つのテキストではなく、テキストのペアになるので以下のように1文目と2文目をセットにしてtokenizerに渡す形になります。

from transformers import AutoTokenizer

checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

# handle two sequences as a pair
inputs = tokenizer("This is the first sentence.", "This is the second one.")
print(inputs)
{
    'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102], 
    'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], 
    'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}

tokenizerの処理結果にはinput_ids, token_type_ids, attention_maskの3種類が含まれています。
input_idsはテキストをトークン化したものにIDを割り振ったもの、attention_maskはトークン化された入力のうち、モデルが実際に注目して欲しい位置を示すものでした。
token_type_idsは今まで出てきてはいたものの詳しく触れてはいませんでしたが、複数テキストを入力とする場合に必要になってくるもので、今回のケースだとどこまでが1文目でどこからが2文目かを示しています。

トークンとtoken_type_idsを並べてみると以下のようになります。

for i in range(len(inputs["input_ids"])):
    print(tokenizer.convert_ids_to_tokens(inputs["input_ids"])[i], ":", inputs["token_type_ids"][i])
[CLS] : 0
this : 0
is : 0
the : 0
first : 0
sentence : 0
. : 0
[SEP] : 0
this : 1
is : 1
the : 1
second : 1
one : 1
. : 1
[SEP] : 1

1文目は[CLS]sentence1[SEP]、2文目はsentence2[SEP]
という形になっているのがわかります。

ではデータセット全体に対して上記の前処理を実施します。
一つのやり方としては以下のようなものがあります。

tokenized_dataset = tokenizer(
    raw_datasets["train"]["sentence1"],
    raw_datasets["train"]["sentence2"],
    padding=True,
    truncation=True
)

これでも一応可ですが、dict形式になってしまうこと、データセットの大きさによってはメモリに余裕がないといけないといった点でデメリットがあります。
Hugging Faceのdatasetはディスクに保存されているデータソースから高速かつ効率よくメモリにデータをロードして扱えるApache Arrow形式になっています。
よって、できればdatasetの形のまま扱えた方が良いため、以下のようにDataset.mapメソッドを使い、tokenizeの処理を関数にしてデータセット全体に適用させる方法を取るのが望ましいと言えます。

def tokenize_function(example):
    return tokenizer(example["sentence1"], example["sentence2"], truncation=True)

tokenized_dataset = raw_datasets.map(tokenize_function, batched=True)
print(tokenized_dataset)
  • tokenize_functionにおいて、padding=Trueとはしていません。データセット全体で見て最大長のシーケンスに合わせてpaddingをするのは処理効率が悪いためです。paddingはtokenizeの処理の後にバッチごとに区切って行います(後述のDynamic Padding)
  • mapする際にbatched=Trueとしておけば、1件ずつ処理していくのではなく適当に複数まとめて処理をしていってくれるため、処理速度が上がります。
  • mapする際にnum_procで並列処理を実行するスレッド数を渡すことができます。Hugging Faceのtokenizer自体が並列処理をデフォルトでやってくれるのでここでは指定していませんが、別のライブラリのちょっと重いtokenizerを使う場合などにはこのオプションを活用して処理を高速化させることもできます。
DatasetDict({
    train: Dataset({
        features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
        num_rows: 3668
    })
    validation: Dataset({
        features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
        num_rows: 408
    })
    test: Dataset({
        features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
        num_rows: 1725
    })
})

DatasetDictにtokenize処理された結果を含むフィールドが新たに追加されていることがわかります。

Dynamic Padding

tokenizerによって前処理実施済のdatasetに対して、最後にpaddingを適用します。
paddingする際はデータセット全体を見て合わせるのではなく、バッチごとに行う方が効率が良くなります。
バッチごとにpaddingするということは、バッチごとにシーケンスがpaddingされる長さが変わることになるため、この処理をDynamic Paddingと呼ぶようです。

Hugging FaceのtransformersライブラリではDataCollatorWithPadding関数によってバッチごとにpaddingをかけて、いい具合にまとめることができます。

from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer) # DataCollatorの初期化

samples = tokenized_dataset["train"][:8] # サンプルバッチとしてtrainデータ先頭の8個取得
samples = {
    k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]
} # 必要な要素だけにする

print([len(x) for x in samples["input_ids"]]) # それぞれのinput_idsの長さを確認
[50, 59, 47, 67, 59, 50, 62, 32]

それぞれデータの長さが異なりますが、DataCollatorを使ってpaddingを行い長さを揃えると以下のようになります。

batch = data_collator(samples)
{k:  v.shape for k, v in batch.items()}
{
    'attention_mask': torch.Size([8, 67]), 
    'input_ids': torch.Size([8, 67]), 
    'token_type_ids': torch.Size([8, 67]), 
    'labels': torch.Size([8])
}

samplesに含まれるinput_idsの長さの中で最も長い67に揃っていることがわかります。

まとめ

Hugging Face datasetライブラリを用いた学習用データセットの準備について、MRPCデータセットを例に見てきました。
流れをまとめると以下のようになります。

from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding

raw_datasets = load_dataset("glue", "mrpc") # datasetのロード

# tokenizer
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

# tokenize処理を行うための関数
def tokenize_function(example):
    return tokenizer(example["sentence1"], example["sentence2"], truncation=True)

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) # dataset全体にtokenizeを実施
data_collator = DataCollatorWithPadding(tokenizer=tokenizer) # paddingはDataCollatorで行う

これでデータセットの準備が整ったので、次はこれを用いてモデルFine-tuningさせていきます。

脚注
  1. GLUE(General Language Understanding)は自然言語処理のモデルの学習や評価のためのいろいろな種類のベンチマークデータセットを提供している。 ↩︎

  2. https://aclanthology.org/I05-5002.pdf ↩︎

Discussion

コジコジコジコジ

お久しぶりです。
west○○のAIチームで、苦楽を共にした齋藤です 笑

相変わらず勉強熱心ですね!
田邊さんに伝授してもらったDeep Learningは今でも業務で活躍してます

さて、Wantedlyの方でつながりリクエストを送りました
もし良かったらお互いの近況報告などできたら嬉しいです
(特に、LLMを利用したサービスの開発に興味がないかなぁ...などなど;)

ちょっと場違いなメッセージかもしれなくて恐縮ですが、
以上よろしくお願いいたします!