[NLP] FINE-TUNING A PRETRAINED MODEL

김보현·2024년 7월 25일
0

hugging face chapter 3로 공부했습니다

Introduction

2장에서는 토크나이저와 사전 학습된 모델을 사용하여 예측을 수행하는 방법을 배웠다.
하지만 내 dataset에 맞게 사전학습된 모델을 fine-tune하고싶다면 어떻게 해야할까?

  • Hub에서 대규모 데이터셋을 준비하는 방법
  • 고수준의 Trainer API를 사용하여 모델을 미세 조정하는 방법
  • custom training loop를 사용하는 방법
  • Accelerate 라이브러리를 활용하여 custom training loop를 어떤 분산(distributed) 환경에서도 쉽게 실행하는 방법

1.Processing the data

PyTorch에서 하나의 배치로 시퀀스 분류기를 훈련하는 방법

import torch
from transformers import AdamW, AutoTokenizer, AutoModelForSequenceClassification

# 이전과 동일한 코드
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequences = [
    "I've been waiting for a HuggingFace course my whole life.",
    "This course is amazing!",
]
batch = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")

# 새로운 부분
batch["labels"] = torch.tensor([1, 1])

optimizer = AdamW(model.parameters())
loss = model(**batch).loss
loss.backward()
optimizer.step()

문장으로만 모델을 훈련하는 것은 좋은 결과를 얻을 수 없다.
더 나은 결과를 얻기 위해서는 더 큰 데이터셋을 준비해야 한다.

아래에선 MRPC (Microsoft Research Paraphrase Corpus) 데이터셋을 예제로 사용할 것이다. 이 데이터셋은 illiam B. Dolan과 Chris Brockett의 논문에서 소개되었다.
이 데이터셋은 5,801쌍의 문장으로 구성되어 있다. 각 쌍이 동의어인지 아닌지 여부를 나타내는 라벨이 포함되어 있다. 작은 데이터셋이기 때문에 실험하기에 적합하다.

1-1 Loading a dataset from the Hub

허브에는 모델뿐만 아니라 다양한 언어로 된 여러 데이터셋도 있다.
여기에서 데이터셋을 탐색할 수 있지만 지금은 MRPC 데이터셋에 집중한다!
MRPC 데이터셋은 10개의 GLUE 벤치마크 데이터셋 중 하나로, 10개의 서로 다른 텍스트 분류 작업에서 ML 모델의 성능을 측정하는 데 사용된다.

Datasets 라이브러리는 허브에서 데이터셋을 다운로드하고 cache하는 명령을 제공한다.
MRPC 데이터셋 다운로드

from datasets import load_dataset

raw_datasets = load_dataset("glue", "mrpc")
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
    })
})

DatasetDict 객체를 얻었다.
훈련 세트, 검증 세트, 테스트 세트를 포함하고 있다.
각 세트는 여러 열(sentence1, sentence2, label, idx)과 가변적인 row number를 포함하고 있다. 훈련 세트에는 3,668쌍의 문장, 검증 세트에는 408쌍의 문장, 테스트 세트에는 1,725쌍의 문장이 있다.

이 명령은 기본적으로 ~/.cache/huggingface/datasets에 데이터셋을 다운로드하고 cache한다.
2장에서 캐시 폴더를 사용자 정의할 수 있다고 배웠었다.
HF_HOME 환경 변수를 설정하여 이를 사용자 정의할 수 있다.

raw_datasets객체에서 딕셔너리와 같은 방식으로 각 문장 쌍에 접근한다.

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 .'}

label이 이미 정수형으로 되어 있어 별도의 전처리가 필요없다.
어떤 정수가 어떤 label에 해당하는지 알기 위해 raw_train_dataset의 특징을 검사할 수 있다.
각 열의 유형:

raw_train_dataset.features

출력

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

1-2 Preprocessing a dataset

데이터셋을 전처리하려면 텍스트를 모델이 이해할 수 있는 숫자로 변환해야 한다.
2 장에서 배운 것처럼 tokenizer를 사용하여 이 작업을 수행할 수 있다.
첫 번째 문장과 두 번째 문장을 각각 토큰화한다:

from transformers import AutoTokenizer

checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"])
tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"])

하지만 두 개의 시퀀스를 모델에 전달하여 두 문장이 패러프레이즈인지 여부를 예측할 수는 없다.
두 시퀀스를 쌍으로 처리하고 적절한 전처리를 적용해야 한다.
토크나이저는 두 시퀀스를 쌍으로 받아들여 BERT 모델처럼 작동한다:

inputs = tokenizer("This is the first sentence.", "This is the second one.")
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]
}

2장에서 input_idsattention_mask 키는 나왔지만 token_type_ids는 나오지 않았다.
이 예제에서는 input의 어느 부분이 첫 번째 문장이고 어느 부분이 두 번째 문장인지를 모델에게 알려준다.

토큰의 ID를 다시 단어로 디코딩하면:

tokenizer.convert_ids_to_tokens(inputs["input_ids"])

결과

['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']

두 문장이 있을 때, 모델은 입력을 [CLS] sentence1 [SEP] sentence2 [SEP] 형식으로 받는다. token_type_ids와 정렬하면:

['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
[      0,      0,    0,     0,       0,          0,   0,       0,      1,    1,     1,        1,     1,   1,       1]

[CLS] sentence1 [SEP] 부분은 모두 token_type_id가 0이고, sentence2 [SEP] 부분은 모두 token_type_id가 1입니다.

다른 체크포인트를 선택하면 토큰화된 입력에 token_type_ids가 없을 수도 있다.(예: DistilBERT 모델을 사용하는 경우 반환되지 않음)
이는 모델이 사전 학습 중에 token_type_ids를 이미 처리해서 해당 토큰 타입 ID를 처리할 수 없기 때문이다.

이제 토크나이저가 한 쌍의 문장을 처리할 수 있는 방법으로 전체 데이터셋을 토큰화할 수 있다.
2장과 마찬가지로 첫 번째 문장의 목록과 두 번째 문장의 목록을 토크나이저에 전달하여 쌍으로 된 문장 목록을 넣는다.
2장에서 본 패딩과 잘림 옵션과도 호환된다.

훈련 데이터셋 전처리:

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

이 방법은 잘 작동하지만, 딕셔너리(key: input_ids, attention_mask, token_type_ids, value: 리스트)를 반환한다는 단점이 있다.
토큰화하는 동안 전체 데이터셋을 RAM에 저장할 수 있는 경우에만 작동한다(🤗 Datasets 라이브러리의 데이터셋은 디스크에 저장된 Apache Arrow 파일이므로 필요한 샘플만 메모리에 로드됨).

데이터를 데이터셋으로 유지하기 위해 Dataset.map() 메서드를 사용한다.
토큰화 외에도 추가 전처리가 필요한 경우 유연성을 제공한다. map() 메서드는 데이터셋의 각 요소에 함수를 적용하여 작동하므로, 입력을 토큰화하는 함수를 정의한다.

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

이 함수는 딕셔너리를 받아 토큰화된 입력을 포함하는 새 딕셔너리를 반환한다. 예제 딕셔너리가 여러 샘플을 포함하는 경우에도 작동한다(각 키는 문장 목록임). 이를 통해 map() 호출 시 batched=True 옵션을 사용할 수 있어 토큰화 속도가 크게 빨라진다. 토크나이저는 🤗 Tokenizers 라이브러리의 Rust로 작성된 토크나이저를 사용하므로 많은 입력을 한 번에 처리할 때 매우 빠르게 작동한다.

토큰화 함수에서 패딩 인수를 생략했다. 모든 샘플을 최대 길이로 패딩하는 것은 비효율적이기 때문이다. 배치를 만들 때 패딩을 적용하는 것이 더 효율적이다. 이렇게 하면 배치의 최대 길이로만 패딩하면 되므로, 입력의 길이가 매우 다양할 때 많은 시간과 처리 능력을 절약할 수 있다.

다음은 데이터셋 전체에 토큰화 함수를 적용하는 방법이다. batched=True를 사용하여 함수가 데이터셋의 여러 요소에 한 번에 적용되도록 한다. 이렇게 하면 전처리 속도가 빨라진다.

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_datasets

🤗 Datasets 라이브러리가 이 처리를 적용하는 방식은 전처리 함수가 반환하는 딕셔너리의 각 키에 대해 데이터셋에 새 필드를 추가하는 것이다.

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
    })
})

map()을 사용할 때 num_proc 인수를 전달하여 전처리 함수를 적용할 때 멀티프로세싱을 사용할 수도 있다. 여기서는 🤗 Tokenizers 라이브러리가 이미 여러 스레드를 사용하여 샘플 토큰화 속도를 높이기 때문에 사용하지 않았지만, 이 라이브러리를 사용하지 않는 경우 전처리 속도를 높일 수 있다.

tokenize_functioninput_ids, attention_mask, token_type_ids 키를 가진 딕셔너리를 반환하므로 이 세 필드가 데이터셋의 모든 분할에 추가됩니다. 전처리 함수가 map()을 적용한 데이터셋의 기존 키에 대해 새 값을 반환하면 기존 필드를 변경할 수도 있다.

모든 예제를 배치할 때 가장 긴 요소의 길이에 맞춰 패딩해야 한다.
이 방식을 동적 패딩dynamic padding고 한다.

1-3 Dynamic padding

배치 안의 샘플을 함께 배치하는 역할을 하는 함수를 collate함수라고 한다. 이는 DataLoader를 구성할 때 전달할 수 있는 인수로, 기본 함수는 샘플을 PyTorch 텐서로 변환하고 연결한다(샘플이 리스트, 튜플, 딕셔너리인 경우 재귀적으로 수행).
입력의 크기가 모두 동일하지 않으므로 기본 함수로는 처리할 수 없다. 입력이 가변적인 길이를 가지므로 필요한 경우에만 각 배치에 맞춰 패딩을 적용한다. 이렇게 하면 훈련 속도가 상당히 빨라지지만, TPU에서 훈련할 때는 문제가 발생할 수 있다. TPU는 고정된 형태를 선호하기 때문에 추가 패딩이 필요하다.

이를 실제로 수행하려면 데이터셋의 항목에 적절한 양의 패딩을 적용하는 collate 함수를 정의해야 한다. 다행히도 🤗 Transformers 라이브러리는 DataCollatorWithPadding이라는 함수를 제공한다. 이 함수를 인스턴스화할 때 토크나이저를 전달하여 어떤 패딩 토큰을 사용할지, 입력의 왼쪽 또는 오른쪽에 패딩할지를 결정한다

from transformers import DataCollatorWithPadding
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

이 새로운 도구를 테스트하기 위해 배치로 묶을 훈련 세트의 몇 샘플을 가져온다. 여기서 idx, sentence1, sentence2 열은 제거한다.
문자열을 포함하고 있어 텐서를 생성할 수 없기 때문이다.

각 항목의 길이를 확인

samples = tokenized_datasets["train"][:8]
samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]}
[len(x) for x in samples["input_ids"]]

출력

[50, 59, 47, 67, 59, 50, 62, 32]

샘플 길이는 32에서 67까지 다양하다.
동적 패딩을 사용하면 이 배치의 모든 샘플은 배치 내 최대 길이인 67로 패딩된다.
동적 패딩 없이 모든 샘플을 데이터셋의 최대 길이 또는 모델이 허용하는 최대 길이로 패딩해야 한다.

data_collator가 배치를 동적으로 제대로 패딩하는지 확인하면

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])}

전체 코드

(bohyun) user@server:~/Desktop/BOHYUN/0726/speedrun/pytorch-template/MyProject/NLP$ CUDA_VISIBLE_DEVICES=6 python train_model.py
/home/user/.local/lib/python3.8/site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.
  warnings.warn(
tokenizer.json: 100%|████████████████████████████████████████████████| 466k/466k [00:00<00:00, 904kB/s]
/home/user/.local/lib/python3.8/site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.
  warnings.warn(
model.safetensors: 100%|█████████████████████████████████████████████| 440M/440M [00:03<00:00, 114MB/s]
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-uncased and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
Downloading readme: 100%|██████████████████████████████████████████| 35.3k/35.3k [00:00<00:00, 201kB/s]
Downloading data: 100%|█████████████████████████████████████████████| 649k/649k [00:00<00:00, 1.54MB/s]
Downloading data: 100%|███████████████████████████████████████████| 75.7k/75.7k [00:00<00:00, 95.2kB/s]
Downloading data: 100%|██████████████████████████████████████████████| 308k/308k [00:00<00:00, 703kB/s]
Generating train split: 100%|███████████████████████████| 3668/3668 [00:00<00:00, 344015.27 examples/s]
Generating validation split: 100%|████████████████████████| 408/408 [00:00<00:00, 154196.80 examples/s]
Generating test split: 100%|████████████████████████████| 1725/1725 [00:00<00:00, 460956.57 examples/s]
Map: 100%|███████████████████████████████████████████████| 3668/3668 [00:00<00:00, 20605.09 examples/s]
Map: 100%|█████████████████████████████████████████████████| 408/408 [00:00<00:00, 20453.91 examples/s]
Map: 100%|███████████████████████████████████████████████| 1725/1725 [00:00<00:00, 24181.81 examples/s]
{'input_ids': torch.Size([8, 67]), 'token_type_ids': torch.Size([8, 67]), 'attention_mask': torch.Size([8, 67]), 'labels': torch.Size([8])}
/home/user/miniconda3/envs/bohyun/lib/python3.8/site-packages/transformers/optimization.py:429: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning
  warnings.warn(
huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...
To disable this warning, you can either:
        - Avoid using `tokenizers` before the fork if possible
        - Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)
huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...
To disable this warning, you can either:
        - Avoid using `tokenizers` before the fork if possible
        - Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)
Loss: 0.5314090847969055

2.Fine-tuning a model with the Trainer API or Keras

🤗 Transformers는 Trainer 클래스를 제공해서 제공하는 사전 학습된 모델을 데이터셋에서 미세 조정하는 데 도움을 준다. 데이터 전처리 작업을 마친 후에는 Trainer를 정의하는 몇 가지 단계만 남아 있다. Trainer.train()을 실행하려면 GPU 또는 TPU를 사용해야한다.

from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding

raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

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

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

2-1 Training

Trainer를 정의하기 전에 해야 할 첫 번째 단계는 Trainer가 훈련 및 평가에 사용할 모든 하이퍼파라미터를 포함하는 TrainingArguments 클래스를 정의하는 것이다. 제공해야 하는 유일한 인수는 훈련된 모델이 저장될 디렉터리와 체크포인트가 저장될 디렉터리다. 나머지 인수는 기본값을 사용하면 되며, 기본 미세 조정에는 꽤 잘 작동한다.

from transformers import TrainingArguments

training_args = TrainingArguments("test-trainer")

훈련 중에 모델을 허브에 자동으로 업로드하려면 TrainingArguments에 push_to_hub=True를 전달한다.

두 번째 단계는 모델을 정의하는 것이다. 이전 장과 마찬가지로 AutoModelForSequenceClassification 클래스를 사용하고, 라벨은 두 개로 설정한다:

from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

2장과 달리 이 사전 학습된 모델을 인스턴스화한 후에는 경고 메시지가 표시된다. 이는 BERT가 문장 쌍을 분류하도록 사전 학습되지 않았기 때문에 사전 학습된 모델의 헤드가 버려지고 시퀀스 분류에 적합한 새로운 헤드가 추가되기 때문이다. 경고 메시지는 일부 가중치가 사용되지 않았으며(버려진 사전 학습 헤드에 해당) 일부 가중치는 무작위로 초기화되었음을 나타낸다(새 헤드에 해당). 모델을 훈련하라고 권장한다.

모델을 준비한 후에는 지금까지 구성한 모든 객체(모델, training_args, 훈련 및 검증 데이터셋, data_collator, tokenizer)를 전달하여 Trainer를 정의할 수 있다

from transformers import Trainer

trainer = Trainer(
    model,
    training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
)

tokenizer를 전달하면 Trainer에서 기본 data_collator는 이전에 정의한 DataCollatorWithPadding이 되므로 이 호출에서 data_collator=data_collator 줄을 생략할 수 있다.

모델을 데이터셋에서 미세 조정하려면 Trainer의 train() 메서드를 호출하면 된다:

trainer.train()

이렇게 하면 미세 조정이 시작되고 매 500 스텝마다 훈련 손실을 보고한다. 그러나 모델의 성능이 얼마나 좋은지(또는 나쁜지) 알려주지 않는다.

이유1. evaluation_strategy를 "steps"(eval_steps마다 평가) 또는 "epoch"(각 epoch 끝에서 평가)로 설정하여 훈련 중에 평가하도록 Trainer에게 지시하지 않았다.
이유2. 평가 중에 메트릭을 계산하기 위한 compute_metrics() 함수를 Trainer에게 제공하지 않았다(그렇지 않으면 평가 시 손실만 출력되는데, 이는 직관적인 숫자가 아니다).

2-2 Evaluation

유용한 compute_metrics() 함수를 작성하고 다음 훈련 시 사용할 수 있는 방법을 알아보자. 함수는 EvalPrediction 객체(예측 필드와 label_ids 필드를 가진 named tuple)를 받아야 하며, 문자열을 실수로 매핑하는 딕셔너리를 반환한다(문자열은 반환된 메트릭의 이름이고 실수는 그 값이다). 모델에서 예측을 얻으려면 Trainer.predict() 명령을 사용할 수 있다:

predictions = trainer.predict(tokenized_datasets["validation"])
print(predictions.predictions.shape, predictions.label_ids.shape)

출력은 다음과 같다:

(408, 2) (408,)

predict() 메서드의 출력은 세 개의 필드를 가진 또 다른 named tuple이다: 예측(predictions), 라벨 ID(label_ids), 메트릭(metrics). 메트릭 필드는 전달된 데이터셋에 대한 손실과 시간 메트릭(예측에 걸린 총 시간과 평균 시간)만 포함한다. compute_metrics() 함수를 완료하고 Trainer에 전달하면 해당 필드에는 compute_metrics()에서 반환된 메트릭도 포함된다.

예측(predictions)은 408 x 2 크기의 2차원 배열이다(408은 사용한 데이터셋의 요소 수). predict()에 전달된 데이터셋의 각 요소에 대한 로짓이다(이전 장에서 봤듯이 모든 Transformer 모델은 로짓을 반환한다). 이를 라벨과 비교할 수 있는 예측으로 변환하려면 두 번째 축에서 최대값의 인덱스를 가져와야 한다:

import numpy as np

preds = np.argmax(predictions.predictions, axis=-1)

이제 preds를 라벨과 비교할 수 있다. compute_metrics() 함수를 작성하려면 🤗 Evaluate 라이브러리의 메트릭을 사용한다. MRPC 데이터셋과 관련된 메트릭을 로드하는 것은 데이터셋을 로드하는 것만큼 쉽다. 이번에는 evaluate.load() 함수를 사용한다. 반환된 객체는 메트릭 계산에 사용할 수 있는 compute() 메서드를 가지고 있다:

import evaluate

metric = evaluate.load("glue", "mrpc")
metric.compute(predictions=preds, references=predictions.label_ids)

출력

{'accuracy': 0.8578431372549019, 'f1': 0.8996539792387542}

정확한 결과는 모델 헤드의 무작위 초기화 때문에 다를 수 있다. 여기서 우리는 검증 세트에서 85.78%의 정확도와 89.97의 F1 점수를 가진 모델을 볼 수 있다. 이는 GLUE 벤치마크에서 MRPC 데이터셋의 결과를 평가하는 데 사용되는 두 가지 메트릭이다. BERT 논문에서는 base 모델의 F1 점수가 88.9라고 보고되었다. 이는 uncased 모델이었고 우리는 현재 cased 모델을 사용하고 있기 때문에 더 나은 결과가 나올 수 있다.

모든 것을 하나로 묶으면 compute_metrics() 함수

def compute_metrics(eval_preds):
    metric = evaluate.load("glue", "mrpc")
    logits, labels = eval_preds
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)

각 epoch가 끝날 때 메트릭을 보고하기 위해 이 compute_metrics() 함수를 사용하여 새 Trainer를 정의

training_args = TrainingArguments("test-trainer", evaluation_strategy="epoch")
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

trainer = Trainer(
    model,
    training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
)

evaluation_strategy를 "epoch"으로 설정한 새로운 TrainingArguments와 새로운 모델을 생성한다. 그렇지 않으면 이미 훈련한 모델의 훈련을 계속하게 된다.

새 훈련을 시작 실행

trainer.train()

이번에는 훈련 손실 외에도 각 epoch가 끝날 때 검증 손실과 메트릭을 보고한다. 모델 헤드의 무작위 초기화 때문에 정확도/F1 점수는 조금 다를 수 있지만, 비슷한 수준이다.

Trainer는 여러 GPU 또는 TPU에서 문제 없이 작동하며, 혼합 정밀도 훈련(fp16=True 사용)을 비롯한 많은 옵션을 제공한다.

이로써 Trainer API를 사용한 미세 조정 소개가 끝난다. 지금은 순수한 PyTorch로 동일한 작업을 수행하는 방법을 사용한다.

3.A full training

from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding

raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

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

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

3-1 Prepare for training

실제 훈련 루프를 작성하기 전에 몇 가지 객체를 정의해야 한다. 첫 번째로, 배치를 반복할 때 사용할 데이터 로더를 정의해야 한다. 하지만 데이터 로더를 정의하기 전에, Trainer가 자동으로 처리했던 몇 가지 작업을 수행해야 한다. 구체적으로는 다음 작업을 수행해야 한다:

  • 모델이 예상하지 않는 값에 해당하는 열을 제거한다 (예: sentence1 및 sentence2 열).
  • 열 이름을 label에서 labels로 변경한다 (모델이 labels라는 인수를 기대하기 때문).
  • 데이터셋 형식을 PyTorch 텐서 대신 반환하도록 설정한다.

우리의 tokenized_datasets에는 각각의 작업을 수행하는 메서드가 있다:

tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"])
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
tokenized_datasets.set_format("torch")
tokenized_datasets["train"].column_names

이제 결과가 모델이 수용할 수 있는 열만 포함하는지 확인할 수 있다:

["attention_mask", "input_ids", "labels", "token_type_ids"]

이제 데이터 로더를 쉽게 정의할 수 있다:

from torch.utils.data import DataLoader

train_dataloader = DataLoader(
    tokenized_datasets["train"], shuffle=True, batch_size=8, collate_fn=data_collator
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], batch_size=8, collate_fn=data_collator
)

데이터 처리에 실수가 없는지 빠르게 확인하기 위해 배치를 다음과 같이 검사할 수 있다:

for batch in train_dataloader:
    break
{k: v.shape for k, v in batch.items()}

출력 예시:

{'attention_mask': torch.Size([8, 65]),
 'input_ids': torch.Size([8, 65]),
 'labels': torch.Size([8]),
 'token_type_ids': torch.Size([8, 65])}

실제 형태는 아마 조금 다를 것이다. 왜냐하면 우리는 훈련 데이터 로더에 shuffle=True를 설정했고 배치 내부에서 최대 길이로 패딩하고 있기 때문이다.

이제 데이터 전처리가 완전히 끝났다. 모델로 전환해보자. 이전 섹션에서 했던 것처럼 인스턴스화한다:

from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

훈련 중에 모든 것이 원활하게 진행될 수 있도록, 이 모델에 배치를 전달한다:

outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)

출력 예시:

tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])

모든 🤗 Transformers 모델은 라벨이 제공될 때 손실을 반환하며, 배치의 각 입력에 대해 두 개의 로짓을 반환한다 (즉, 8 x 2 크기의 텐서).

이제 훈련 루프를 작성할 준비가 거의 다 되었다. 아직 필요한 것은 옵티마이저와 학습률 스케줄러다. Trainer가 기본적으로 사용하는 것을 따라하기 위해, 우리는 AdamW를 사용할 것이다. 이는 Adam과 유사하지만, 가중치 감쇠 정규화를 위한 트위스트가 있다 ("Decoupled Weight Decay Regularization" by Ilya Loshchilov and Frank Hutter 참고):

from transformers import AdamW

optimizer = AdamW(model.parameters(), lr=5e-5)

기본적으로 사용되는 학습률 스케줄러는 최대 값(5e-5)에서 0으로 선형 감소하는 것이다. 이를 제대로 정의하려면, 우리가 수행할 훈련 스텝 수를 알아야 한다. 이는 우리가 실행하고자 하는 에폭 수와 훈련 데이터 로더의 길이를 곱한 값이다. Trainer는 기본적으로 세 개의 에폭을 사용하므로, 우리도 이를 따르겠다:

from transformers import get_scheduler

num_epochs = 3
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)
print(num_training_steps)

출력 예시:

1377

The training loop

마지막으로 하나 더 필요한 것이 있다: GPU를 사용할 수 있다면 GPU를 사용하고 싶을 것이다 (CPU에서는 훈련이 몇 시간씩 걸릴 수 있다). 이를 위해 모델과 배치를 놓을 디바이스를 정의한다:

import torch

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
device

출력 예시:

device(type='cuda')

이제 훈련할 준비가 되었다! 훈련이 언제 끝날지 예측하기 위해, tqdm 라이브러리를 사용하여 훈련 스텝 수에 대해 진행 표시줄을 추가한다:

from tqdm.auto import tqdm

progress_bar = tqdm(range(num_training_steps))

model.train()
for epoch in range(num_epochs):
    for batch in train_dataloader:
        batch = {k: v.to(device) for k, v in batch.items()}
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

핵심 훈련 루프는 도입부에서 본 것과 매우 유사하다. 우리는 보고를 요청하지 않았기 때문에, 이 훈련 루프는 모델이 얼마나 잘 동작하는지에 대해 아무것도 알려주지 않는다. 이를 위해 평가 루프를 추가해야 한다.

The evaluation loop

이전과 같이, 🤗 Evaluate 라이브러리에서 제공하는 메트릭을 사용할 것이다. 이미 metric.compute() 메서드를 보았지만, 메트릭은 실제로 예측 루프를 거치면서 배치를 축적할 수 있다. add_batch() 메서드를 사용하여 배치를 축적한 후, metric.compute()를 사용하여 최종 결과를 얻을 수 있다. 평가 루프를 구현하는 방법은 다음과 같다:

import evaluate

metric = evaluate.load("glue", "mrpc")
model.eval()
for batch in eval_dataloader:
    batch = {k: v.to(device) for k, v in batch.items()}
    with torch.no_grad():
        outputs = model(**batch)

    logits = outputs.logits
    predictions = torch.argmax(logits, dim=-1)
    metric.add_batch(predictions=predictions, references=batch["labels"])

metric.compute()

출력 예시:

{'accuracy': 0.8431372549019608, 'f1': 0.8907849829351535}

결과는 모델 헤드 초기화와 데이터 셔플링의 무작위성 때문에 조금 다를 수 있지만, 비슷한 수준일 것이다.

3-3 Supercharge your training loop with 🤗 Accelerate

이전의 훈련 루프는 단일 CPU 또는 GPU에서 잘 동작한다. 하지만 🤗 Accelerate 라이브러리를 사용하여 몇 가지 조정을 통해 여러 GPU 또는 TPU에서 분산 훈련을 가능하게 할 수 있다. 훈련 및 검증 데이터 로더를 생성하는 부분부터 시작하여, 수동 훈련 루프는 다음과 같다:

from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)

num_epochs = 3
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)

progress_bar = tqdm(range(num_training_steps))

model.train()
for epoch in range(num_epochs):
    for batch in train_dataloader:

4.Summary

첫 두 챕터에서는 모델과 토크나이저에 대해 배웠고,
자신의 데이터에 맞게 미세 조정하는 방법도 알게 되었다.

  • Hub의 데이터셋에 대해 배웠다.
  • 데이터셋을 로드하고 전처리하는 방법을 배웠으며, 동적 패딩과 콜레이터 사용법도 배웠다.
  • 모델의 미세 조정과 평가를 직접 구현해봤다.
  • 하위 수준의 훈련 루프를 구현해봤다.
  • 🤗 Accelerate를 사용해 훈련 루프를 쉽게 다중 GPU 또는 TPU에서 작동하도록 조정했다.
profile
Fall in love with Computer Vision

2개의 댓글

comment-user-thumbnail
2024년 8월 9일

정말 잘 적으신거 같아요! 참고가 되었습니다 감사합니다~

1개의 답글