번역이나 정리가 완벽하지 않을 수 있습니다. Hugging Face 의 공식 문서를 확인하세요.
원본 출처: Hugging Face LLM Course Chapter 3: Fine-Tuning A Pretrained Model
2장에서 tokenizer 와 pretrained model 을 사용해 예측하는 방법을 배웠습니다.
3장에서는 특수한 작업을 위해 pretrainde model 을 fine-tune 하는 방법을 다룹니다.
참고: 데이터 처리를 위한 Huggin Face Datasets 문서
3장에서는 Hugging Face Transformers 라이브러리 외에도, Hugging Face Datasets, Tokenizers, Accelerate, Evaluate 를 통해 모델을 더 효율적이고 효과적으로 학습시키는 방법을 소개합니다.
각 소단원에서 배우는 것들
3장을 학습하고 나면, 업계 최신의 모범사례를 적용한 고성능의 API 들과 자신만의 학습 루프를 사용해 자신만의 데이터 셋을 통해 모델을 fine-tune 할 수 있게됩니다.
3장을 마무리하면, fine-tunning 된 BERT 의 텍스트 분류 모델을 갖게 됨과 동시에, 데이터 셋과 작업들에 이 기술을 적용하는 방법을 익히게 됩니다.
3장에서는 PyTorch 를 사용합니다. PyTorch 는 현대의 딥러닝 연구나 제품의 표준이 되었기 때문입니다. Hugging Face 생태계의 최신 API 들과모범 사례를 사용합니다.
훈련된 모델을 Hugging Face Hub 에 업로드 하려면 계정을 만들어야 합니다: 계정 만들기
2장에서의 예제에 이어서, 하나의 batch 에서 sequence classifier 를 학습시키는 방법입니다:
import torch
from torch.optim import AdamW
from transformers import AutoTokenizer, AutoModelForSequenceClassification
# Same as before
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")
# This is new
batch["labels"] = torch.tensor([1, 1])
optimizer = AdamW(model.parameters())
loss = model(**batch).loss
loss.backward()
optimizer.step()
당연하게도, 2개의 문장에 대해 모델을 학습시키는 것은 좋은 결과를 내놓지 못합니다.
더 좋은 결과를 얻기 위해서는 더 큰 데이터 셋이 필요합니다.
이 소단원에서는 MRPC(Microsoft Research Paraphrase Corpus) 데이터 셋을 예제로 사용합니다.
이는 William B. Dolan 과 Chris Brockett 의 논문에서 소개되었습니다.
이 데이터 셋은 적절한 요약인지에 대한 label 과 함께 5801개의 문장 쌍으로 구성되어 있습니다.
이 데이터 셋을 선택한 이유는, 이 데이터 셋이 작아서 실험적으로 학습하기 적절하기 때문입니다.
Hub 는 단순히 모델만 가지고 있는것이 아닙니다.
여러 언어로 이루어진 수 많은 데이터 셋 또한 가지고 있습니다.
데이터 셋은 이곳에서 확인할 수 있으며, 한번쯤은 이 소단원을 진행한 뒤에 새로운 데이터 셋을 load 해보는 것을 권장합니다.
하지만 지금으로서는 MRPC 데이터 셋에 집중합시다!
이 데이터 셋은 GLUE benchmark 를 구성하는 10개의 데이터 셋중 하나입니다.
GLUE benchmark 는 10가지의 다른 분류 작업에서 ML 모델의 성능을 측정하는 학술적인 방법입니다.
이 Hugging Face Datasets 라이브러리는 Hub 의 데이터 셋을 다운로드하고 캐싱할 수 있는 아주 단순한 명령어를 제공합니다.
MRPC 데이터 셋은 이렇게 다운로드 할 수 있습니다:
추가적인 자원: 데이터 셋을 loading 하는 더 많은 기술과 예제를 확인하려면, Hugging Face Datasets 문서를 확인하세요.
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
})
})
보시다싶이, training set, validation set, test set 을 가지고 있는 DatasetDict 객체를 얻었습니다.
각각은 몇가지 컬럼(sentence1, sentence2, label, idx)과, 각 셋의 요소의 수(학습에 3,668개, 검증에 408개, 평가에 1,725개 쌍의 문장)를 나타내는 각기 다른 row 수를 가지고 있습니다.
이 명령은 데이터 셋을 기본적으로 ~/.cache/huggingface/datasets 에 저장, 캐싱합니다. 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 .'}
라벨은 이미 정수이기 때문에 여기서는 전처리를 할 필요가 없습니다.
이 정수가 어떤 라벨에 대응되는지 확인하려면, raw_train_dataset 의 features 를 보면 됩니다.
각 컬럼의 타입을 설명해줍니다:
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)}
내부적으로, label 은 ClassLabel 타입이고, 정수에서 라벨 이름으로 대응되는 내용은 names 폴더에 저장되어 있습니다.
0 은 not_equivalent 에 대응, 1 은 equivalent 에 대응됩니다.
이것도 해보세요! 학습 셋의 15번째 요소와 검증 셋의 87번째 요소를 확인해보세요. 라벨이 무엇인가요?
데이터 셋을 전처리 하기 위해서는, 문자를 모델이 이해하는 숫자로 변환해야 합니다.
2장에서 봤듯이, 토크나이저가 해줍니다.
토크나이저에게 문장 하나 혹은 리스트를 먹이면, sentence1 과 sentence2 의 쌍을 토큰화 할 수 있습니다:
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"][:])
파고들기: 더 많은 정교한 토큰화 기법과 각기 다른 토크나이저의 동작의 이해를 위해서, Hugging Face Tokenizers 문서와 tokenization guide in the cookbook 을 확인하세요.
그러나, 두 시퀀스를 모델에 그냥 전달해서는 요약이 올바른지 예측을 얻어낼 수 없습니다.
우리는 두 시퀀스를 쌍으로 다루고, 적절한 전처리를 적용해야만 합니다.
다행히도, 토크나이저가 시퀀스를 쌍으로 묶고 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_ids 와 attention_mask 에 대해서는 다뤘습니다만, token_type_ids 에 대해서는 이야기를 미뤘습니다.
이 예제에서는, token_type_ids 가 입력의 어떤 부분이 sentence1 이고 어떤 부분이 sentence2 인지 말해줍니다.
이것도 해보세요! 학습 셋의 15번째 요소에 있는 두 문장을 각각 따로 토큰화 해보고 쌍으로 토큰화 해보세요. 어떤 차이가 있나요?
input_ids 안에 있는 아이디들을 다시 단어로 디코드하면:
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] 부분은 1에 대응되어 있습니다.
만약 다른 checkpoint 를 선택한다면 token_type_ids 를 얻지 못할 수 있습니다(DistilBERT 모델은 반환 안함).
모델이 사전학습중에 해당 데이터를 확인했어서 어떻게 다룰지 알고있을 때만 반환이 됩니다.
자, BERT 가 token type ID 로 사전학습되었고, 1장에서 설명한 masked language modeling 목표 외에도, 다음 문장 예측하기라는 목표가 남아있습니다.
이번 목표는 문장 쌍 간의 관계를 모델링하는 것입니다.
다음 문장 예측에서, 모델은 문장 쌍(무작위로 masking 된)을 전달 받고 두번째 문장이 첫번째 문장에 이어지는지 예측합니다.
이 작업을 의미있게 만들기 위해, 데이터의 절반은 실제로 이어지는 문서의 내용으로 구성하고, 나머지 절반은 관련없는 문서의 내용으로 구성합니다.
보통은 token_type_ids 가 토큰화된 입력에 있는지는 중요하지 않습니다: 모델과 동일한 체크포인트를 토크나이저에 사용한다면, 토크나이저는 모델에 뭘 전달해야하는지 알고있습니다.
토크나이저가 한쌍의 문장을 어떻게 다룰 수 있는지 확인했으니, 전체 데이터를 토큰화 할 수 있습니다: 2장에서 했던 것처럼, 첫번째 문장들의 리스트와 두번째 문장들의 리스트를 토크나이저에 전달해서 문장을 쌍으로 먹일 수 있습니다.
이것은 2장에서 본 패딩과 정형화 옵션과도 호환됩니다.
따라서 학습 데이터 셋을 한번에 전처리하는 방법은 이러합니다:
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"][:],
raw_datasets["train"]["sentence2"][:],
padding=True,
truncation=True,
)
이것이 잘 작동하긴 하지만, 딕셔너리(키가 input_ids, attention_mask, token_type_ids 값이 리스트의 리스트)를 반환한다는 단점이 있습니다.
또한 토큰화 과정에서 전체 데이터 셋을 저장할 만큼 충분한 RAM 이 있어야만 동작합니다(Hugging Face Datasets 라이브러리의 데이터 셋들은 디스크에 저장된 Apache Arrow 파일들이라서, 메모리에 로드한 샘플들만 유지할 수 있습니다.
데이터를 데이터 셋으로 유지하기 위해서, Dataset.map() 메서드를 사용할겁니다.
이 메서드는 전처리가 더 필요할 때 토큰화만 하면 되는 유연성도 확보해줍니다.
map() 메서드는 데이터 셋의 각 요소에 함수를 적용하는 작업을 하므로, 우리의 입력을 토큰화 하는 함수를 정의해봅시다:
def tokenize_function(example):
return tokenizer(example["sentence1"][:], example["sentence2"][:], truncation=True)
이 함수는 딕셔너리(데이터 셋의 요소 같은)를 입력으로 받고 input_ids, attention_mask, token_type_ids 를 키로 가지는 새로운 딕셔너리를 반환합니다.
tokenizer 가 문장 쌍의 리스트에도 동작하는 것처럼 example 딕셔너리가 여러개의 샘플을 포함하더라고 작동할것입니다.
이로써 batched=True 옵셔을 사용할 수 있으며, 토큰화 속도를 비약적으로 높여줍니다.
tokenizer 는 Rust 로 작성된 Hugging Face Tokenizers 라이브러리의 토크나이저를 기반으로 합니다.
이 토크나이저는 매우 빠르지만, 한번에 많은 입력을 줬을 때만 그렇습니다.
우선은 토큰화 함수의 padding 매개변수는 남겨두겠습니다.
왜냐하면 최대 길이로 패딩하는 것은 비효울적이기 때문입니다: batch 를 구성하고, 전체 데이터 셋에서 최대길이로 패딩하지 않고 batch 내에서 최대 길이로 패딩하는 것이 더 좋습니다.
이렇게 하면 입력 길이가 다양한 것보다 시간과 프로세싱 파워를 많이 절약할 수 있습니다!
성능 팁: Hugging Facec Datasets performance guide 에서 효율적인 데이터 처리 기술을 배워보세요.
토큰화 함수를 테이터 셋에 한번에 적용하는 방법입니다.
map 을 호출할 때 batched=True 로 설정해서 원소별로 하나씩 적용하는게 아니라, 데이터 셋의 여러 원소에 한번에 적용되도록 합니다.
이렇게 하면 전처리가 더 빨라집니다.
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_datasets
Hugging Face 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 인자를 전달해서 전처리 과정에 multiprocessing 을 사용할 수도 있습니다.
그러나 Hugging Face Tokenizers 라이브러리가 이미 멀티 쓰레드를 사용해 더 빠르게 샘플을 tokenize 하고 있기 때문에 이번에는 사용하지 않겠습니다.
그러나 이 라이브러리에서 제공하는 fast tokenizer 를 사용하고 있지 않다면 이걸로 전처리를 빠르게 할 수 있습니다.
tokenize_function 은 3개의 필드를 가지는 딕셔너리를 반환합니다: input_ids, attention_mask, token_type_ids.
따라서 이 3개의 필드가 데이터 셋의 조각들에 추가됩니다.
map() 함수를 적용하면 이미 있는 키에 대해 전처리 함수가 새로운 값을 반환했을 때, 기존 데이터 셋에 있던 그 값을 대체한다는 것에 주의하세요.
마지막으로 우리가 해야하는 것은, 모든 샘플의 요소들을 모아 batch 처리를 할 때, 가장 긴 요소에 맞추어 나머지 요소에 padding 을 추가하는 것입니다. 이를 dynamic padding 이라고 합니다.
batch 안에 샘플들을 집어넣는 역할을 하는 함수가 collate function 이라고 불리는 함수입니다.
collate function 은 DataLoader 를 생성할 때 인자로 전달할 수 있습니다. 기본으로 설정되어 있는 함수는 단순히 샘플들을 PyTorch tensor 로 변환해 합치는 일만 합니다(list 나 tuple 또는 dictionary 를 순환).
이 기본 함수는 같은 크기가 아닌 우리의 입력에서는 적용이 불가능합니다.
우리는 매 batch 마다 필요한 만큼만 padding 해서 엄청 긴 입력때문에 padding 이 쓸데 없이 많이 되는 것을 피했습니다.
이렇게 하면 학습의 속도를 높일 수 있습니다. 그러나 TPU 에서 학습한다면 문제를 일으킬 수 있는데, TPU 는 불필요한 padding 이 있더라도 고정된 크기의 입력을 선호하기 때문입니다.
최적화 가이드: padding 전략과 TPU 고려사항을 포함해서, 학습 효율을 최적화하는 자세한 사항은 Hugging Face Transformers performance 문서를 참고하세요.
Dynamic padding 을 하려면, 적절한 양의 padding 을 batch 하고 싶은 데이터 셋의 아이템들에 적용하기 위해서 collate 함수를 정의해야합니다.
다행히도, Hugging Face Transformers 라이브러리는 DataCollatorWithPadding 을 통해 그러한 함수를 제공합니다.
초기화 할 때 tokenizer 가 필요하며(어떤 padding token 을 사용해야 하는지, padding 을 입력의 좌측에 하는지 우측에 하는지), 이후에는 필요한 모든것을 해줍니다:
from transformers import DataCollatorWithPadding
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
새로운 장난감을 시험해보기 위해, batch 하고싶은 샘플을 우리의 학습 셋에서 추려봅시다.
string 으로 이루어져 있거나(string 은 tensor 가 될 수 없습니다) 필요없는 idx, sentence1, sentence2 컬럼을 제거하고 batch 의 각 엔트리의 길이를 확인해봅시다:
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 까지 다양한 길이의 샘플들을 볼 수 있습니다.
Dynamic padding 한다는 것은, 이 batch 안의 샘플들이 최대 길이인 67로 padding 되어야 한다는 의미입니다.
Dynamic padding 이 아니라면, 모든 샘플들이 전체 데이터 셋에서 가장 긴 길이나 모델이 수용할 수 있는 최대 길이로 padding 되는 것입니다.
다시 확인하자면, data_collator 는 batch 를 적절하게 dynamic 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])}
좋습니다!
날것의 문자에서 이제 우리의 모델이 batch 로 다룰 수 있는 형태로 변환했습니다.
fine-tune 할 준비가 된 것입니다!
이것도 해보세요! GLUE SST-2 데이터셋의 전처리를 따라해보세요. 한 쌍의 문장 대신 하나의 문장으로 구성된 것 말고는 전부 동일할겁니다. 더 어려운 도전을 원하신다면, GLUE 작업에서 동작하는 전처리 함수를 직접 작성해보세요.
추가 연습: Hugging Face Transformers examples 에서 예제를 확인해보세요
좋습니다!
이제 Hugging Face Datasets 라이브러리의 최신 모범사례를 통해 우리의 데이터를 전처리했습니다.
현대의 Trainer API 를 이용해 모델을 학습시킬 준비가 된 것입니다.
다음 소단원에서는 Hugging Face 생태계의 최신 기능들을 사용해 모델을 어떻게 효율적으로 fine-tunning 하는지 알아보겠습니다.
핵심내용 정리:
Dataset.map()에서batched=True를 사용해 압도적으로 빠른 전처리DataCollatorWithPadding을 이용한 Dynamic padding 은 고정길이 padding 보다 효율적임- 반드시 모델이 기대하는 형식(숫자로 된 tensor, 올바른 컬럼 이름)으로 데이터를 전처리해야 함
- Hugging Face Datasets 라이브러리는 대규모의 데이터를 처리할 수 있는 강력한 도구들을 제공함
Hugging Face Transformer 들은 Trainer 클래스를 제공하여 나만의 데이터 셋으로 현대의 모범사례처럼 사전 학습된(pretrained) 모델을 fine-tunning 하게 도와줍니다.
이전 소단원에서 데이터 전처리를 마치면 Trainer 를 정의하기 위해 아주 조금의 단계만 남겨두게 됩니다.
가장 어려운 부분은 Trainer.train() 을 실행하기 위한 환경을 준비하는 것입니다. CPU 로 실행하면 굉장히 느립니다.
만약 GPU 가 없다면, Google Colab 에서 GPU, TPU 를 사용할 수 있습니다.
학습 자료: 학습을 하기 전에, Hugging Face Transformers training.guide 에 전체적으로 친숙해지세요. fine-tunning cookbook 에서 예제를 확인하세요.
예제 코드는 이전 단원에서 모든 예제 코드를 실행했다고 가정하고 작성되었습니다.
실행되어야 하는 코드들의 요약:
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)
Trainer 를 정의하기 전에 처음으로 필요한 것은 TrainingArguments 클래스를 정의하는 것입니다.
TrainingArguments 는 Trainer 가 학습과 평가에 사용하는 hyperparameter 들을 포함하고 있습니다.
전달해야 하는 유일한 인자는 학습된 모델과 그 과정에서 생긴 checkpoint 들이 저장될 위치 뿐입니다.
나머지 인자들은 기본값으로 둬도 일반적인 fine-tunning 은 꽤나 잘 동작합니다.
from transformers import TrainingArguments
training_args = TrainingArguments("test-trainer")
학습을 하면서 자동으로 Hub 에 나의 모델을 업로드하고 싶다면, TrainingArguments 에 push_to_hub=True 를 인자로 전달하세요.
4장에서 더 자세히 배웁니다.
고급 설정: 모든 학습 매개변수의 자세한 정보는 TrainingArguments 문서와 training configuration cookbook을 참고하세요.
두번째로 필요한 단계는 사용할 모델을 정의하는 것입니다.
이전 장에서도 그랬듯이, 2개의 라벨을 가진 AutoModelForSequenceClassification 클래스를 사용합니다.
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
2장과 다른점을 눈치채셨겠지만, 이 사전학습 모델을 초기화하면 경고가 나오게 됩니다.
그 이유는 BERT 가 쌍으로 짝지어진 문장을 분류하는 것에 사전학습 되지 않았기 때문입니다.
그래서 사전학습 모델의 기존의 head 가 버려지고, sequence 분류에 적합한 새로운 head 가 추가된 것입니다.
경고는 어떤 가중치들(버려진 사전학습 모델의 기존 head)은 사용되지 않고, 어떤 가중치들(새로운 head)은 무작위적으로 초기화되었다고 알려줍니다.
결론적으로 모델을 다시 학습해야 한다는 뜻이고, 그것이 바로 우리가 지금부터 하려고 하는 것입니다.
모델을 정의했으면, Trainer를 정의할 수 있습니다.
지금까지 준비한 model, training_args, 학습/평가 데이터 셋, data_collator, processing_class 를 전달해 주면 됩니다.
processing_class 는 Trainer 에게 어떤 tokenize 를 작업에 사용할지 알려주는 매개변수입니다.
from transformers import Trainer
trainer = Trainer(
model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
processing_class=tokenizer,
)
tokenizer 를 processing_class 로 전달하면, Trainer 가 사용하는 DataCollatorWithPadding이 data_collator 의 기본값이 됩니다.
따라서 data_collator=data_collator 구문을 생략할 수 있습니다만, 이 작업 파이프라인에서 중요한 부분이라는 것을 보여주기 위해 포함시켜 뒀습니다.
더 배우기: Trainer 클래스와 그 매개변수들의 더 많은 자세한 사항들을 확인하려면, Trainer API 문서를 확인하세요. training cookbook recipes에서 더 발전된 사용방법들을 확인해보세요.
이제 나만의 데이터 셋으로 fine-tunning 하려면, Trainer 인스턴스의 train() 메서드를 호출하기만 하면 됩니다.
trainer.train()
실행하면 fine-tunning 이 시작되고(GPU 를 사용하면 몇 분 정도 걸림), 500단계마다 학습 loss 를 보고합니다.
하지만, 모델이 얼마나 성능을 내고 있는지는 알려주지 않습니다.
그 이유는 다음과 같습니다:
1. TrainingArguments의 eval_strategy 를 steps(eval_steps 마다 평가) 또는 epoch(epoch 끝마다 평가) 로 설정해서 Trainer 에게 학습중에 평가하라고 알려주지 않았습니다.
2. 평가중에 계산에 사용할 compute_metrics() 를 Trainer 에게 제공하지 않았습니다(없으면 그냥 loss 를 출력하고, 직관적이지 않음).
유용한 compute_metrics() 함수를 만드는 법을 확인해보고, 다음에 학습할 때 사용해봅시다.
함수는 EvalPrediction 객체(predictions 필드, label_ids 필드가 있음)를 입력으로 받고, string - float(평가지표의 이름 - 점수) 딕셔너리를 반환합니다.
우리의 모델로부터 predictions 를 얻으려면, Trainer.predict() 를 사용합니다.
predictions = trainer.predict(tokenized_datasets["validation"])
print(predictions.predictions.shape, predictions.label_ids.shape)
(408, 2) (408,)
predict() 메서드는 3개의 필드를 반환합니다: predictions, label_ids, metrics.
metrics 필드는 단순히 전달된 데이터 셋의 loss 와 시간 관련 평가지표(예측에 걸린 전체 시간, 평균 시간)입니다.
compute_metrics() 함수의 실행하고서 Trainer 를 전달하면, metrics 필드에 compute_metrics() 의 결과도 포함됩니다.
보시다싶이, predictions 은 408x2(408은 데이터 셋의 요소 개수) 2차원 배열입니다.
이것들은 predict() 에 전달한 데이터 셋의 각 요소의 logit 입니다(2장에서도 봤듯이, 모든 Transformer 모델은 logit 을 반환합니다).
이것을 우리의 label 과 비교 가능한 예측값으로 변환하려면, 두번째 axis 의 최대값을 기준으로 삼아야 합니다.
import numpy as np
preds = np.argmax(predictions.predictions, axis=-1)
이제 preds 를 label 과 비교할 수 있습니다.
compute_metric() 함수를 만들기 위해, Hugging Face 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}
Hugging Face Evaluate 문서에서 다른 평가지표와 전략을 배워보세요.
모델의 head 가 무작위로 초기화되서 평가지표의 결과값이 달라질 수 있습니다.
지금은 validation set 에 대해 모델의 정확도(accuracy)가 85.78%, F1 score 가 89.97로 나오네요.
이 두개의 평가지표는 GLUE 벤치마크에서 MRPC 데이터 셋의 결과를 평가하는 두개의 평가지표입니다.
BERT 논문의 표에서는 기본 모델의 F1 score 를 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", eval_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,
processing_class=tokenizer,
compute_metrics=compute_metrics,
)
eval_strategy 를 "epoch" 로 설정하여 새로운 TrainingArguments 를 생성했다는 것에 주의하세요.
이전에 이미 학습을 진행했던 모델을 그대로 사용해도 됩니다.
하지만 새로 생성한 모델을 학습하려면 학습을 다시 한번 실행하세요:
trainer.train()
이번에는 학습 loss 외에도 validation loss 와 평가지표를 각 epoch 의 끝마다 보고하게 됩니다.
다시 말하지만, 정확한 accuracy/F1 score 는 조금 다를 수 있습니다.
모델 head 를 무작위로 초기화했기 때문입니다.
하지만 대략 비슷한 값일겁니다.
Trainer 는 내장된 기능들을 통해 현대의 딥러닝 모범 사례들을 사용할 수 있습니다:
TrainingArguments에 fp16=True 를 설정해서 더 빠르게 학습하고 메모리를 절약하세요:
training_args = TrainingArguments(
"test-trainer",
eval_strategy="epoch",
fp16=True, # Enable mixed precision
)
GPU 메모리가 제한적일 때 더 큰 batch size 를 위해:
training_args = TrainingArguments(
"test-trainer",
eval_strategy="epoch",
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # Effective batch size = 4 * 4 = 16
)
Trainer 는 linear decay 를 기본으로 사용하지만, 직접 정의할 수 있습니다:
training_args = TrainingArguments(
"test-trainer",
eval_strategy="epoch",
learning_rate=2e-5,
lr_scheduler_type="cosine", # Try different schedulers
)
성능 최적화: 더 많은 고급 학습 기술 예를 들어 분산학습, 메모리 최적화, 특정 하드웨어 최적화를 위해서는 Hugging Face Transformers performance guide 를 확인하세요.
Trainer 는 다중 GPU, TPU 에서 바로 작동하며 분산 학습을 위한 여러가지 옵션을 제공합니다.
10장에서 지원하는 모든 기능을 확인합니다.
지금까지 Trainer API 를 사용해 fine-tunning 하는 방법에 대해 소개했습니다.
일반적인 NLP 작업에 대한 예제는 7장에서 확인해보고, 지금은 순수한 PyTorch 학습 loop 에서의 방법을 살펴봤습니다.
더 많은 예제: 더 많은 예제는 Hugging Face Transformers notebooks 에서 확인하세요.
1. Trainer 의 processing_class 매개변수의 목적은 무엇입까?
a. 사용할 모델 구조를 특정.
b. Trainer 에게 데이터를 처리할 때 어떤 tokenizer 를 사용할지 알려줌.
c. 학습에 사용할 batch size 를 결정.
d. 평가 빈도를 조절.
2. TrainingArguments 의 어떤 매개변수가 평가가 얼마나 자주 일어날지 결정합니까?
a. eval_frequency
b. eval_strategy
c. evaluation_steps
d. do_eval
3. TrainingArguments 의 fp16=True 는 무엇을 활정화합니까?
a. 더 빠른 학습을 위한 16-bit 정수 precision(정밀도).
b. 16-bit 부동소수 를 사용한 mixed precision(복합 정밀도) 학습으로 학습 속도 향상, 메모리 사용량 감소.
c. 정확히 16 epoch 동안 학습.
d. 16개의 GPU 를 사용해 분산학습.
4. Trainer 의 compute_metrics 함수는 역할이 무었입니까?
a. 학습중에 loss 를 계산.
b. logit 을 예측값으로 바꾸고, accuracy 와 F1 같은 평가지표를 계산.
c. 어떤 최적화 방법을 사용할지 결정.
d. 학습 데이터를 전처리.
5. eval_dataset 을 Trainer 에게 제공하지 않으면 어떻게 됩니까?
a. 에러와 함께 학습을 실패.
b. Trainer 가 자동으로 학습 데이터에서 평가 데이터를 분할.
c. 학습중에 평가지표를 제공받지 못하지만, 학습은 동작함.
d. 모델이 학습 데이터를 평가에 사용함.
6. gradient accumulation 은 무엇이며 어떻게 활성화합니까?
a. 디스크에 gradient 를 저장, save_gradients=True 로 활성화.
b. 업데이트 전에 여러 batch 에서 gradient 를 축적, gradient_accumulation_steps 로 활성화.
c. gradient 계산 속도를 높임, fp16 으로 활성화.
d. gradient overflow 를 예방, gradient_clipping=True 로 활성화.
핵심 내용 정리:
TrainerAPI 는 대부분의 학습의 복잡함을 해소해주는 고수준의 인터페이스를 제공합니다.- 적절한 데이터 핸들링을 위해
processing_class를 사용해 tokenizer 를 특정하세요.TrainingArguments는 학습의 모든 것을 제어합니다: 학습 빈도, batch 크기, 평가 전략, 최적화compute_metrics는 학습 loss 이상의 커스텀 평가지표들을 활성화합니다.- mixed precision(fp16=True) 과 gradient accumulation 같은 현대의 기능은 학습 효율을 크게 향상시킵니다.
이제 Trainer 클래스를 사용하지 않고, 현대 PyTorch 모범 사례를 활용하여 처음부터 학습 루프를 구현함으로써 지난 소단원에서와 동일한 결과를 얻는 방법을 살펴보겠습니다.
다시 말해, 여러분이 소단원 2에서 데이터 처리를 완료했다고 가정합니다.
아래는 필요한 모든 내용을 요약한 간단한 정리입니다:
처음부터 학습하기(Training from Scratch): 이 소단원은 이전 내용에서 이어집니다. PyTorch 학습 루프와 모범 사례에 대한 종합적인 안내는 Hugging Face Transformers training 문서와 custom training cookbook 을 참고하세요.
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)
실제로 학습 루프를 작성하기 전에, 몇 가지 객체를 먼저 정의해야합니다.
가장 먼저 필요한 것은 batch 를 반복해서 처리(iterate)하기 위해 사용할 dataloader 입니다.
하지만 이 데이터로더들을 정의하기 전에, 우리의 tokenized_datasets에 약간의 postprocessing(후처리) 을 적용해야합니다.
이는 Training 가 자동으로 처리해주던 일부 작업을 직접 다루기 위함입니다.
구체적으로, 우리는 다음 작업들을 해야합니다:
label 컬럼의 이름을 labels로 바꿉니다. (모델은 인자가 labels라는 이름으로 전달되기를 기대하기 때문입니다.)우리의 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"]
작업이 끝났다면, 이제 우리는 손쉽게 dataloaders 를 정의할 수 있습니다:
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
)
데이터 처리에 실수가 없는지 빠르게 확인하려면, 다음과 같이 batch 를 점검할 수 있습니다:
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를 설정했으며, batch 내에서 최대 길이에 맞춰 padding 을 하고 있기 때문입니다.
이제 데이터 전처리를 완전히 마쳤으니(모든 머신러닝 전문가에게 있어, 성취감은 크지만 좀처럼 잡히지 않는 목표), 모델로 돌아가겠습니다.
우리는 이전 소단원에서 했던 것과 똑같이 모델을 생성합니다:
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
학습이 원활하게 진행되도록 하기 위해, 모델에 우리의 batch 를 전달합니다:
outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)
tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])
모든 Hugging Face Transformers 모델은 labels 가 제공되면 loss 를 반환합니다.
또한 우리는 logits(batch 의 각 입력마다 두 개씩이므로, 크기가 8 * 2인 텐서) 도 얻게 됩니다.
이제 학습 루프를 작성할 준비가 거의 다 되었습니다!
다만 두 가지가 아직 빠져 있습니다:
optimizer 와 learning rate scheduler(학습률 스케쥴러) 입니다.
우리는 Trainer 가 수행하던 것을 직접 구현하려는 것이므로, 동일한 기본값을 사용하겠습니다.
Trainer 가 사용하는 옵티마이저는 AdamW 인데, 이는 Adam 과 거의 같지만 weight decay regularization(가중치 감쇠 정규화) 에 변형이 가해진 방식입니다.
("Decoupled Weight Decay Regularization" by Ilya Loshchilov and Frank Hutter 를 확인):
from torch.optim import AdamW
optimizer = AdamW(model.parameters(), lr=5e-5)
최신 최적화 팁: 더 나은 성능을 위해 다음을 시도할 수 있습니다:
- Weight decay 를 적용한 AdamW:
AdamW(model.parameters(), lr=5e-5, weight_decay=0.01)- 8-bit Adam: 메모리 효율 최적화를 위해
bitsandbytes사용- 다른 learning rate: 대형 모델의 경우 더 낮은 학습률(1e-5 ~ 3e-5)이 더 잘 동작하는 경우가 많음
최적화 자료: 옵티마이저와 학습 전략에 대해 더 알아보려면 Hugging Face Transformers optimization guide 를 확인하세요.
마지막으로, 기본적으로 사용되는 learning rate scheduler 는 최대값(5e-5)에서 0까지의 선형적으로 감소하는(linear decay) 함수입니다.
이를 올바르게 정의하려면, 우리가 수행할 학습 단계 수를 알아야 합니다.
이 값은 우리가 실행하려는 epoch 수와 학습 batch 수(학습용 데이터로더의 길이)를 곱해서 계산합니다.
Trainer 는 기본적으로 3 epoch 을 사용하므로, 우리도 따라하겠습니다:
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
마지막으로 한 가지 더: GPU 가 있다면 그것을 사용하고 싶습니다(CPU에서는 학습이 몇 분 대신 몇 시간이 걸릴 수 있습니다).
이를 위해 우리는 모델과 batch 를 탑제할 device 를 정의합니다:
import torch
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
device
device(type='cuda')
이제 학습을 시작할 준비가 되었습니다!
학습이 언제 끝날지 대략 감을 잡기 위해, 우리는 학습 단계 수에 대해 progress bar(진행 상황 표시줄)를 추가합니다.
이를 위해 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)
현대의 학습 최적화: 학습 루프를 더욱 효율적으로 만들기 위해 다음을 고려해보세요:
- Gradient Clipping:
optimizer.step()전에torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)추가- Mixed Precision: 더 빠른 학습을 위해
torch.cuda.amp.autocast()와GradScaler사용- Gradient Accumulation: 여러 batch에 걸쳐 그래디언트를 누적하여 더 큰 batch 크기를 시뮬레이션
- Checkpointing: 주기적으로 모델 체크포인트를 저장하여, 학습이 중단되었을 때 다시 이어서 할 수 있도록 함
구현 가이드: 최적화에 대한 자세한 예시는 Hugging Face Transformers efficient training.guide 와 range of optimizers 를 확인하세요.
학습 루프의 핵심은 introduction 에서 본 것과 매우 비슷하다는 것을 확인 하셨을겁니다.
우리는 어떠한 보고서도 요청하지 않으므로, 이 학습 루프는 모델이 얼마나 잘 작동하는지에 대해 아무것도 알려주지 않습니다.
확인하고 싶다면 evaluation loop(평가 루프)를 추가해야 합니다.
앞서 했던 것처럼, 우리는 Evaluate 라이브러리에서 제공하는 metric을 사용할 것입니다.
우리는 이미 metric.compute() 메서드를 봤지만, 실제로는 예측 루프를 도는 동안 add_batch() 메서드를 통해 batch들을 누적할 수도 있습니다.
모든 batch를 누적한 뒤에는 metric.compute()로 최종 결과를 얻을 수 있습니다.
다음은 이것들을 평가 루프 (evaluation loop)에 구현하는 방법입니다.
Evaluation(평가) 모범 사례: 더 정교한 평가 전략과 지표(metrics)를 위해는, Hugging Face Evaluate 문서와 comprehensive evaluation cookbook을 확인하세요.
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}
다시 말하지만, 결과는 모델 헤드를 무작위로 초기화했고, 데이터 셔플링 때문에 약간씩 다를 수 있습니다.
그러나 대체로 비슷한 범위 안에 있을 것 입니다.
직접 해보기! 이전 학습 루프를 수정하여 SST-2 데이터셋에 대해 모델을 파인튜닝 해 보세요.
우리가 앞서 정의한 학습 루프는 단일 CPU나 GPU에서만 잘 동작합니다.
하지만 Hugging Face Accelerate 라이브러리를 사용하면, 몇 가지 조정만으로 다중 GPU나 TPU에서의 분산 학습을 활성화 할 수 있습니다.
Huggin Face Accelerate 는 분산 학습, mixed precision, device placement 의 복잡함을 자동 관리로 해소해줍니다.
학습 및 검증 데이터로더를 생성하는 것부터 시작해서, 수동으로 만든 학습 루프는 이렇게 생겼습니다:
Accelerate 심층 탐구: 분산 학습, mixed precision, 하드웨어 최적화에 대해 모든 것을 배우려면 Hugging Face Accelerate 문서를 확인하세요.
또한 실용적인 예시는 Transformers 문서에서 살펴볼 수 있습니다.
from accelerate import Accelerator
from torch.optim import AdamW
from transformers import AutoModelForSequenceClassification, get_scheduler
accelerator = Accelerator()
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)
train_dl, eval_dl, model, optimizer = accelerator.prepare(
train_dataloader, eval_dataloader, model, optimizer
)
num_epochs = 3
num_training_steps = num_epochs * len(train_dl)
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_dl:
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
첫 번쨰로 추가해야 할 것은 import 구문입니다.
두 번째 줄에서는 Accelerator 객체를 생성하는데, 이는 실행 환경을 확인하고 적절한 분산 작업의 설정을 초기화합니다.
Hugging Face Accelerate 는 device placement 를 대신 처리해주므로, 모델을 GPU/CPU에 할당하는 코드를 제거할 수 있습니다(혹은 원한다면, device 대신 accelerator.device를 사용하도록 변경할 수도 있습니다).
그 다음 작업의 핵심은 데이터로더, 모델, 옵티마이저를 accelerator.prepare() 전달하는 코드에서 이루어집니다.
이 과정은 해당 객체들을 적절한 컨테이너로 감싸, 분산 학습이 의도한 대로 동작하도록 보장합니다.
남은 변경 사항은, device 에 batch 를 올리는 코드를 제거하고(다시 말하지만, 그대로 두고 싶다면 accelerator.device 로 바꾸세요), loss.backward() 를 accelerator.backword(loss) 로 변경하는 것입니다.
⚠️ Cloud TPU 가 제공하는 학습 속도 최적화를 제대로 활용하기 위해
padding="max_length"와 토크나이저의max_length인자를 사용하여 샘플을 고정 길이로 패딩할 것을 권장합니다.
복사 붙여넣기로 실험해보고 싶다면, Hugging Face Accelerate 를 사용한 완성된 학습 루프는 다음과 같습니다:
from accelerate import Accelerator
from torch.optim import AdamW
from transformers import AutoModelForSequenceClassification, get_scheduler
accelerator = Accelerator()
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)
train_dl, eval_dl, model, optimizer = accelerator.prepare(
train_dataloader, eval_dataloader, model, optimizer
)
num_epochs = 3
num_training_steps = num_epochs * len(train_dl)
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_dl:
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
이 코드를 train.py 스크립트에 넣으면, 그 스크립트는 어떤 종류의 분산 환경에서도 실행할 수 있게 됩니다.
자신의 분산 환경에서 테스트하려면, 다음 명령어를 실행하세요:
accelerate config
해당 명령어를 실행하면, 몇 가지 질문에 답하도록 프롬프트가 표시되며, 당신의 답변은 다음 명령어에서 사용되는 설정 파일에 저장됩니다.
accelerate launch train.py
이 명령어를 실행하면 분산 학습을 시작합니다.
만약 Notebook(예: Colab 에서 TPU 로 테스트하기 위해) 환경에서 이것을 실행해 보고 싶다면, 코드를 training_function() 안에 붙여 넣고 마지막 셀에서 다음을 실행하면 됩니다:
from accelerate import notebook_launcher
notebook_launcher(training_function)
더 많은 예시는 Hugging Face Accelerate 저장소에서 확인할 수 있습니다.
분산 학습 (Distributed Training): multi-GPU 및 multi-node 학습에 대한 종합적인 내용을 확인하려면,
Hugging Face Transformers distributed training.guid 와 scaling training cookbook 을 확인하세요.
이제 처음부터 학습을 구현하는 방법을 배웠으니, 실무에서 사용할 때 고려해야 할 몇 가지 추가 사항을 소개합니다:
모델 평가 (Model Evaluation): 항상 모델을 평가할 때는 정확도(accuracy) 하나만 보지 말고, 여러 지표(metrics)를 함께 확인하세요.
종합적인 평가를 위해서는 Hugging Face Evaluate 라이브러리를 사용하세요.
하이퍼파라미터 튜닝 (Hyperparameter Tuning): 체계적인 하이퍼파라미터 최적화를 위해 Optuna나 Ray Tune 같은 라이브러리 사용을 고려하세요.
모델 모니터링 (Model Monitoring): 학습 전 과정에서 학습 지표(training metrics), 학습 곡선(learning curves), 검증 성능(validation performance)을 지속적으로 확인하세요.
모델 공유 (Model Sharing): 학습이 끝나면, Hugging Face Hub 에 모델을 공유하여 커뮤니티가 접근할 수 있도록 하세요.
효율성 (Efficiency): 대형 모델의 경우, gradient checkpointing, parameter-efficient fine-tuning(LoRA, AdaLoRA) 또는 quantization method
같은 기술들을 고려하세요.
이것으로 커스텀 학습 루프를 활용한 파인튜닝 심층 탐구를 마칩니다.
여기서 배운 기술들은 학습 과정을 완전히 제어해야 하거나, Trainer API가 제공하는 기능을 넘어서는 커스텀 학습 로직을 구현하고자 할 때 큰 도움이 될 것입니다.
핵심 내용 요약:
- 수동적으로 만든 학습 루프는 모든 제어권이 나에게 있지만 올바른 순서를 이해해야 합니다: forward -> backward -> optimizer step -> zero gradients
- Transformer 모델의 optimizer 는 weight decay 를 사용하는 AdamW 를 추천합니다.
- 평가중에는 올바른 대응과 효율을 위해 항상
model.eval()과torch.no_grad()를 사용하세요.- Hugging Face Accelerate 는 최소한의 코드 수정으로 분산 학습을 가능하게 해줍니다.
- PyTorch 를 실행하기 위해서는 장치 관리(GPU/CPU 에 tensor 를 올리는 것)가 중요합니다.
- 현대의 mixed precision, gradient accumulation, gradient clipping 은 비약적으로 학습 효율을 높여줍니다.
이제 Trainer API와 나만의 학습 루프를 사용하여 fine-tuning 을 구현하는 방법을 배웠으므로, 그 결과를 해석하는 방법을 이해하는 것이 중요합니다. 학습 곡선은 학습 중 모델의 성능을 평가하고, 성능 저하를 일으키기 전에 잠재적인 문제를 식별하는 데 도움이 되는 매우 유용한 도구입니다.
이 소단원에서는, accuracy(정확도) 및 loss curve(손실 곡선) 을 읽고 해석하는 방법, 다양한 곡선 모양이 모델의 동작에 대해 무엇을 알려주는지 이해하고, 일반적으로 학습중에 발생하는 문제를 해결하는 방법을 배웁니다.
학습 곡선은 학습 중 시간 경과에 따른 모델의 성능 지표를 시각적으로 표현한 것입니다. 주목해야 할 가장 중요한 두 가지 곡선은 다음과 같습니다:
이러한 곡선은 모델이 효과적으로 학습하고 있는지 이해하는 데 도움이 되며 성능 향상을 위해 미세 조정하는 방법을 안내해줍니다. 트랜스포머에서는 이러한 평가지표가 각 batch 에 대해 개별적으로 계산된 다음 디스크에 저장됩니다. 그런 다음 Weights & Biases 와 같은 라이브러리를 사용하여 이러한 곡선을 시각화하고 시간에 따라 모델의 성능을 추적할 수 있습니다.
손실 곡선은 시간 경과에 따라 모델의 오류가 어떻게 감소하는지 보여줍니다. 일반적으로 성공적인 학습에서는 아래와 유사한 곡선을 볼 수 있습니다:

2장에서와 마찬가지로 Trainer API를 사용하여 이러한 평가지표를 추적하고 대시보드에서 시각화할 수 있습니다. 다음은 Weights & Biases로 이를 수행하는 방법입니다.
# Example of tracking loss during training with the Trainer
from transformers import Trainer, TrainingArguments
import wandb
# Initialize Weights & Biases for experiment tracking
wandb.init(project="transformer-fine-tuning", name="bert-mrpc-analysis")
training_args = TrainingArguments(
output_dir="./results",
eval_strategy="steps",
eval_steps=50,
save_steps=100,
logging_steps=10, # Log metrics every 10 steps
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
report_to="wandb", # Send logs to Weights & Biases
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
processing_class=tokenizer,
compute_metrics=compute_metrics,
)
# Train and automatically log metrics
trainer.train()
정확도 곡선은 시간에 따른 정확한 예측의 비율을 보여줍니다. 손실 곡선과 달리 정확도 곡선은 모델이 학습함에 따라 일반적으로 증가해야 하며 일반적으로 손실 곡선보다 더 많은 계단모양(steps)이 나타날 수 있습니다.

💡 정확도 곡선이 "계단모양"인 이유: 연속적인 손실과 달리, 정확도는 불연속적인 예측을 실제 레이블과 비교하여 계산됩니다. 모델 신뢰도의 작은 개선은 최종 예측을 변경하지 않을 수 있으며, 이로 인해 임계값을 넘을 때까지 정확도가 평평하게 유지됩니다.
수렴은 모델의 성능이 안정화되고 손실 곡선과 정확도 곡선이 수평을 이룰 때 발생합니다. 이는 모델이 데이터의 패턴을 학습했으며 사용 가능한 상태가 되었음을 나타냅니다. 쉽게 말해, 모델을 학습할 때마다 안정적인 성능으로 수렴하는 것이 목표인 것입니다.

모델이 수렴되면 새로운 데이터에 대해 예측하고 평가지표을 확인하여 모델이 얼마나 잘 작동하는지 확인할 수 있습니다.
다양한 곡선 모양은 모델 학습의 여러 측면을 보여줍니다. 가장 일반적인 패턴과 그 의미를 살펴봅시다.
잘 진행된 학습은 일반적으로 아래와 유사한 곡선 모양을 보여줍니다.

위의 그림을 보면, 손실 곡선(왼쪽)과 정확도 곡선(오른쪽)을 보여주고 있습니다. 이 곡선들은 뚜렷한 특징을 가지고 있습니다.
손실 곡선은 시간에 따른 모델의 loss(손실) 값을 보여줍니다. 처음에는 손실이 높다가 점차 감소하여 모델이 개선되고 있음을 나타냅니다. 손실 값은 예측된 출력과 실제 출력 간의 차이(오류)를 나타내므로 이것이 감소한다는 것은 모델이 더 나은 예측을 하고 있다는 말입니다.
이제 정확도 곡선을 살펴보겠습니다. 정확도 곡선은 시간에 따른 모델의 accuracy(정확도)를 나타냅니다. 정확도 곡선은 낮은 값에서 시작하여 학습이 진행됨에 따라 증가합니다. 정확도는 정답과 똑같이 분류한 것들의 비율입니다. 따라서 정확도 곡선이 상승함에 따라 모델이 더 정확한 예측을 하고 있음을 의미합니다.
두 곡선에서 한 가지 주목할만한 차이점은 얼마나 부드러운 모양인지 봤을 때 정확도 곡선에는 "정체기"가 존재한다는 겁니다. 손실은 부드럽게 감소하지만 정확도 곡선의 정체기에서는 연속적인 증가 대신 불연속적인 점프가 나타납니다. 이 현상은 정확도를 측정하는 방식때문에 나타납니다. 결과적으로 예측이 부정확하더라도 모델의 출력이 정답에 점점 가까워지면 손실이 줄어듭니다. 그러나 정확도는 예측이 정답의 기준이 되는 임계값을 넘을 때만 향상됩니다.
예를 들어, 고양이(0)와 개(1)를 구별하는 binary classifier(이진 분류기)에서 모델이 개 이미지(실제 값 1)에 대해 0.3을 예측하면 이는 0으로 반올림되어 잘못된 분류입니다. 다음 단계에서 0.4를 예측하면 여전히 부정확합니다. 0.4가 0.3보다 1에 더 가깝기 때문에 손실은 감소했지만 정확도는 변경되지 않아 정체기가 생성됩니다. 정확도는 모델이 0.5보다 큰 값을 예측하여 1로 반올림될 때만 점프합니다.
건강한 곡선의 특징:
- 손실의 부드러운 감소: 학습/검증 손실이 꾸준히 감소.
- 가까운 학습/검증 성능: 학습과 검증 평가지표 간의 격차가 작음.
- 수렴: 곡선이 점차 수평을 이루면 모델이 패턴을 학습했다는 뜻.
학습 곡선의 몇 가지 실제 예제를 살펴보겠습니다. 먼저 학습 중 학습 곡선을 모니터링하는 몇 가지 방식을 소개합니다. 그 다음 학습 곡선에서 관찰할 수 있는 다양한 패턴을 분석합니다.
학습 과정 중에(trainer.train()을 실행한 후) 다음과 같은 주요 지표를 모니터링할 수 있습니다:
학습이 완료된 후, 전체 곡선을 분석하여 모델의 성능을 이해할 수 있습니다.
🔍 W&B 대시보드 기능: Weights & Biases는 아름답고, 반응형인 학습 곡선을 자동으로 생성합니다. 이런걸 할 수 있습니다:
- 여러 실행을 나란히 비교
- 나만의 평가지표 추가 및 시각화
- 비정상적인 동작에 대한 경고 설정
- 팀과 결과 공유
Weights & Biases 문서에서 자세히 알아보세요.
과적합은 모델이 학습 데이터를 너무 많이 학습해서 다른 데이터(검증 세트로 표시됨)로 일반화할 수 없을 때 발생합니다.

증상:
과적합 해결책:
아래 샘플에서는 조기 종료를 사용하여 과적합을 방지합니다. early_stopping_patience 를 3으로 설정했는데, 이는 3개의 연속하는 epoch 동안 검증 손실이 개선되지 않으면 학습을 중지한다는 뜻입니다.
# Example of detecting overfitting with early stopping
from transformers import EarlyStoppingCallback
training_args = TrainingArguments(
output_dir="./results",
eval_strategy="steps",
eval_steps=100,
save_strategy="steps",
save_steps=100,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
num_train_epochs=10, # Set high, but we'll stop early
)
# Add early stopping to prevent overfitting
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
processing_class=tokenizer,
compute_metrics=compute_metrics,
callbacks=[EarlyStoppingCallback(early_stopping_patience=3)],
)
과소적합은 모델이 너무 단순해서 데이터의 기본 패턴을 파악할 수 없을 때 발생합니다. 이는 여러 가지 이유로 발생할 수 있습니다:

증상:
과소적합 해결책:
아래 샘플에서는 epoch 를 늘리면 모델이 데이터의 패턴을 학습할 수 있는지 확인해봅니다.
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./results",
-num_train_epochs=5,
+num_train_epochs=10,
)
불규칙한 학습 곡선은 모델이 효과적으로 학습하지 못할 때 나타납니다. 이는 여러 가지 이유로 발생할 수 있습니다:

증상:
학습 및 검증 곡선 모두 불규칙한 동작을 보입니다.

불규칙한 곡선 해결책:
아래 샘플에서는 학습률을 낮추고 batch 크기를 늘립니다.
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./results",
-learning_rate=1e-5,
+learning_rate=1e-4,
-per_device_train_batch_size=16,
+per_device_train_batch_size=32,
)
학습 곡선을 이해하는 것은 효과적인 머신러닝 실무자가 되는 데 중요합니다. 이러한 시각적 도구는 모델의 학습 진행 상황에 대한 즉각적인 피드백을 제공하고 학습을 중단할 시기, 하이퍼파라미터를 조정할 시기 또는 다른 접근 방식을 시도할 시기에 대한 근거있는 결정을 내리는 데 도움이 됩니다. 연습을 통해 건강한 학습 곡선이 어떻게 생겼는지, 문제가 발생했을 때 해결하는 방법에 대한 직관을 키울 수 있습니다.
💡 핵심 내용 정리:
- 학습 곡선은 모델 학습 진행 상황을 이해하는 데 필수적인 도구입니다.
- 손실 및 정확도 곡선을 모두 모니터링하되, 서로 다른 특성을 가지고 있음을 기억하십시오.
- 학습/검증 성능의 발산은 과적합을 의미합니다.
- 학습 및 검증 데이터 모두에서 저조한 성능이 나타나면 과소적합을 의미합니다.
- Weights & Biases와 같은 도구를 사용하면 학습 곡선을 쉽게 추적하고 분석할 수 있습니다.
- 조기 종료 및 적절한 정규화는 일반적인 학습 문제를 대부분 해결할 수 있습니다.
🔬 다음 단계: 여러분의 파인튜닝 실험에서 학습 곡선 분석을 연습해 보세요. 하이퍼파라미터를 다양하게 조절해보고 곡선 모양에 어떤 영향을 미치는지 관찰해보세요. 이 실습 경험은 학습 진행 상황을 파악하는 직관을 키우는 가장 좋은 방법입니다.
학습 곡선 및 학습 분석에 대한 이해도를 테스트해 보세요:
1. 학습 손실은 감소하지만 검증 손실이 증가하기 시작하면 일반적으로 무엇을 의미합니까?
a. 모델이 성공적으로 학습하고 있으며 계속해서 향상될 것입니다.
b. 모델이 학습 데이터에 과적합되고 있습니다.
c. 학습률이 너무 낮습니다.
d. 데이터 세트가 너무 작습니다.
2. 정확도 곡선이 부드러운 증가 대신 "계단식" 또는 정체기 같은 패턴을 보이는 이유는 무엇입니까?
a. 정확도 계산에 오류가 있습니다.
b. 정확도는 예측이 결정 경계를 넘을 때만 변경되는 불연속적인 평가지표입니다.
c. 모델이 효과적으로 학습하지 못하고 있습니다.
d. batch 크기가 너무 작습니다.
3. 불규칙하고 변동이 심한 학습 곡선을 관찰할 때 가장 좋은 접근 방식은 무엇입니까?
a. 수렴 속도를 높이기 위해 학습률을 높입니다.
b. 학습률을 줄이고 batch 크기를 늘릴 수 있습니다.
c. 모델이 개선되지 않으므로 즉시 학습을 중지합니다.
d. 완전히 다른 모델 아키텍처로 전환합니다.
4. 언제 조기 종료 사용을 고려해야 합니까?
a. 모든 형태의 과적합을 방지하므로 항상 사용합니다.
b. 검증 성능이 향상되지 않거나 저하되기 시작할 때.
c. 학습 손실이 여전히 빠르게 감소할 때만.
d. 모델이 잠재력을 최대한 발휘하지 못하게 하므로 절대 사용하지 않습니다.
5. 모델이 과소적합될 수 있음을 나타내는 것은 무엇입니까?
a. 학습 정확도가 검증 정확도보다 훨씬 높습니다.
b. 학습 및 검증 성능이 모두 저조하고 조기에 정체됩니다.
c. 학습 곡선이 변동 없이 매우 부드럽습니다.
d. 검증 손실이 학습 손실보다 빠르게 감소합니다.
알차네요! 1, 2장에서는 모델과 토크나이저를 배웠고, 이제 여러분들은 그것들을 나만의 데이터로 fine-tuning 하는 방법을 익혔습니다.
되짚어 보자면 3장에서 여러분들은 이런 것들을 배웠습니다:
Trainer API 의 최신 기술을 활용하여 fine-tuning 과 evaluation 을 구현했습니다.축하합니다! 당신은 트랜스포머 모델의 fine-tuning 기초를 마스터했습니다. 이제 실제 ML 프로젝트에 도전할 준비가 됐습니다.
다음으로 배울 것: 이 자료들을 살펴보고 지식에 깊이를 더하세요:
- 특수한 NLP 작업을 위한 Hugging Face Transformers task guides
- 종합적인 요약 정리를 위한 Hugging Face Transformers examples
다음 단계:
- 배운 기술들을 활용해 나만의 데이터 셋으로 fine-tuning 하기
- Hugging Face Hub 에 있는 다른 모델 구조로 시험해보기
- Hugging Face community 에 가입해서 프로젝트를 공유하고 도움 받기
이건 그저 Hugging Face Transformer 모험의 시작일 뿐입니다.
다음 장에서는, 모델과 토크나이저를 공유하고 사전 학습 모델 생태계에 기여하는 방법을 알아봅니다.
여기서 익힌 기술들 - 데이터 전처리, 학습 설정, 평가, 최적화 - 은 보편적인 머신러닝 프로젝트에 적용될 수 있는 것들입니다.
Text classification, named entity recognition, question answering 혹은 어떠한 NLP 작업을 하든지 간에, 이 기술들이 많은 도움이 될 것입니다.
성공을 위한 꿀팁:
- 커스텀 학습 루프를 구현하기 전에 항상
TrainerAPI 를 사용해서 튼튼한 기반을 마련하기- 더 좋은 출발점을 위해서 Hugging Face Hub 에서 나의 작업과 가장 가까운 사전 학습 모델을 모색하기
- 커뮤니티를 발전시키기 - 모델과 테이터 셋을 공유해서 다른 사람들을 돕고 내 작업에 피드백 받기