LLM Course documentation
데이터 처리
데이터 처리
이전 챕터의 예제에 이어서, 한 배치에서 시퀀스 분류기를 훈련하는 방법은 다음과 같습니다.
import torch
from torch.optim import AdamW
from transformers import 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()
물론 두 문장만으로 모델을 훈련하는 것으로는 매우 좋은 결과를 얻을 수 없습니다. 더 나은 결과를 얻으려면 더 큰 데이터 세트를 준비해야 합니다.
이 섹션에서는 William B. Dolan과 Chris Brockett의 논문에서 소개된 MRPC(Microsoft Research Paraphrase Corpus) 데이터 세트를 예제로 사용하겠습니다. 이 데이터 세트는 5,801개의 문장 쌍으로 구성되어 있으며, 각 쌍이 패러프레이즈인지 아닌지를 나타내는 레이블이 있습니다(즉, 두 문장이 같은 의미인지). 이 챕터에서 이 데이터 세트를 선택한 이유는 작은 데이터 세트이므로 훈련 실험을 하기에 쉽기 때문입니다.
Hub에서 데이터 세트 가져오기
Hub에는 모델뿐만 아니라 다양한 언어로 된 여러 데이터 세트도 있습니다. 여기에서 데이터 세트를 찾아볼 수 있으며, 이 섹션을 완료한 후에는 새로운 데이터 세트를 로드하고 처리해보는 것을 권장합니다(여기에서 일반적인 문서를 참조하세요). 하지만 지금은 MRPC 데이터 세트에 집중해보겠습니다! 이것은 GLUE 벤치마크를 구성하는 10개 데이터 세트 중 하나로, 10개의 서로 다른 텍스트 분류 작업에 걸쳐 ML 모델의 성능을 측정하는 데 사용되는 학술적 벤치마크입니다.
🤗 Datasets 라이브러리는 Hub에서 데이터 세트를 다운로드하고 캐시하는 매우 간단한 명령을 제공합니다. MRPC 데이터 세트를 다음과 같이 다운로드할 수 있습니다.
💡 추가 자료: 더 많은 데이터 세트 로딩 기법과 예제를 보려면 🤗 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
})
})
보시다시피, 훈련 세트, 검증 세트, 테스트 세트가 포함된 DatasetDict
객체를 얻습니다. 각각은 여러 열(sentence1
, sentence2
, label
, idx
)과 가변적인 행 수를 포함하며, 이는 각 세트의 요소 수입니다(따라서 훈련 세트에는 3,668개의 문장 쌍이, 검증 세트에는 408개가, 테스트 세트에는 1,725개가 있습니다).
이 명령은 기본적으로 ~/.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번째 요소를 살펴보세요. 그들의 레이블은 무엇인가요?
데이터 세트 전처리
데이터 세트를 전처리하려면 텍스트를 모델이 이해할 수 있는 숫자로 변환해야 합니다. 이전 챕터에서 보았듯이, 이는 토크나이저로 수행됩니다. 토크나이저에 한 문장이나 문장 목록을 입력할 수 있으므로, 다음과 같이 각 쌍의 모든 첫 번째 문장과 모든 두 번째 문장을 직접 토큰화할 수 있습니다.
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"])
💡 심화 학습: 더 고급 토큰화 기법과 다양한 토크나이저가 작동하는 방식을 이해하려면 🤗 Tokenizers 문서와 쿡북의 토큰화 가이드를 살펴보세요.
하지만 두 시퀀스를 모델에 전달하기만 해서는 두 문장이 패러프레이즈인지 아닌지 예측할 수 없습니다. 두 시퀀스를 쌍으로 처리하고 적절한 전처리를 적용해야 합니다. 다행히 토크나이저는 한 쌍의 시퀀스를 받아서 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
에 대한 이야기는 미뤄두었습니다. 이 예제에서 이것은 입력의 어느 부분이 첫 번째 문장이고 어느 부분이 두 번째 문장인지 모델에 알려주는 역할을 합니다.
✏️ 직접 해보기! 훈련 세트의 15번째 요소를 가져와서 두 문장을 따로따로 토큰화하고 쌍으로도 토큰화해보세요. 두 결과의 차이점은 무엇인가요?
input_ids
안의 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]
에 해당하는 입력 부분은 모두 토큰 타입 ID가 0
이고, sentence2 [SEP]
에 해당하는 다른 부분들은 모두 토큰 타입 ID가 1
입니다.
다른 체크포인트(checkpoint)를 선택하면 토큰화된 입력에 token_type_ids
가 반드시 있지는 않다는 점에 주의하세요(예를 들어, DistilBERT 모델을 사용하면 반환되지 않습니다). 모델이 사전 훈련 중에 이를 본 적이 있어서 무엇을 해야 할지 알 때만 반환됩니다.
여기서 BERT는 토큰 타입 ID로 사전 훈련되었으며, 1장에서 이야기한 마스크드 언어 모델링 목표 외에도 다음 문장 예측이라는 추가 목표를 가지고 있습니다. 이 작업의 목표는 문장 쌍 간의 관계를 모델링하는 것입니다.
다음 문장 예측에서는 모델에 문장 쌍(무작위로 마스킹된 토큰과 함께)이 제공되고 두 번째 문장이 첫 번째 문장을 따르는지 예측하도록 요청받습니다. 작업을 쉽지 않게 만들기 위해, 절반의 경우에는 문장들이 추출된 원본 문서에서 서로를 따르고, 나머지 절반의 경우에는 두 문장이 서로 다른 문서에서 나옵니다.
일반적으로 토큰화된 입력에 token_type_ids
가 있는지 여부에 대해 걱정할 필요는 없습니다. 토크나이저와 모델에 동일한 체크포인트(checkpoint)를 사용하는 한, 토크나이저가 모델에 제공해야 할 것을 알고 있으므로 모든 것이 잘 될 것입니다.
이제 토크나이저가 한 쌍의 문장을 어떻게 처리할 수 있는지 보았으므로, 이를 사용하여 전체 데이터 세트를 토큰화할 수 있습니다: 이전 챕터에서처럼, 첫 번째 문장 목록과 두 번째 문장 목록을 제공하여 토크나이저에 문장 쌍 목록을 입력할 수 있습니다. 이는 2장에서 본 패딩과 생략 옵션과도 호환됩니다. 따라서 훈련 데이터 세트를 전처리하는 한 가지 방법은
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"],
raw_datasets["train"]["sentence2"],
padding=True,
truncation=True,
)
이것은 잘 작동하지만, 딕셔너리(키는 input_ids
, attention_mask
, token_type_ids
이고 값은 목록의 목록)를 반환한다는 단점이 있습니다. 또한 토큰화 중에 전체 데이터 세트를 메모리에 저장할 수 있는 충분한 RAM이 있는 경우에만 작동합니다(🤗 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
키가 있는 새 딕셔너리를 반환합니다. example
딕셔너리에 여러 샘플이 포함되어 있어도(각 키가 문장 목록으로) 작동한다는 점에 주목하세요. 앞서 본 것처럼 tokenizer
는 문장 쌍의 목록에서 작동하기 때문입니다. 이를 통해 map()
호출에서 batched=True
옵션을 사용할 수 있으며, 이는 토큰화를 크게 가속화할 것입니다. tokenizer
는 🤗 Tokenizers 라이브러리의 Rust로 작성된 토크나이저로 뒷받침됩니다. 이 토크나이저는 매우 빠를 수 있지만, 한 번에 많은 입력을 제공해야만 그렇습니다.
지금은 토큰화 함수에서 padding
인수를 빼둔 것에 주목하세요. 모든 샘플을 최대 길이로 패딩하는 것은 효율적이지 않기 때문입니다. 배치를 만들 때 샘플을 패딩하는 것이 더 좋습니다. 그러면 해당 배치의 최대 길이까지만 패딩하면 되고, 전체 데이터 세트의 최대 길이까지 패딩할 필요가 없기 때문입니다. 입력의 길이가 매우 다양할 때 많은 시간과 처리 능력을 절약할 수 있습니다!
📚 성능 팁: 효율적인 데이터 처리 기법에 대한 자세한 내용은 🤗 Datasets 성능 가이드에서 배울 수 있습니다.
다음은 모든 데이터 세트에 토큰화 함수를 한 번에 적용하는 방법입니다. map
호출에서 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
})
})
num_proc
인수를 전달하여 map()
으로 전처리 함수를 적용할 때 멀티프로세싱을 사용할 수도 있습니다. 🤗 Tokenizers 라이브러리가 이미 여러 스레드를 사용하여 샘플을 더 빠르게 토큰화하므로 여기서는 이를 사용하지 않았지만, 이 라이브러리가 뒷받침하는 빠른 토크나이저를 사용하지 않는다면 전처리 속도를 높일 수 있습니다.
우리의 tokenize_function
은 input_ids
, attention_mask
, token_type_ids
키가 있는 딕셔너리를 반환하므로, 이 세 필드가 데이터 세트의 모든 분할에 추가됩니다. 전처리 함수가 map()
을 적용한 데이터 세트의 기존 키에 대한 새 값을 반환한다면 기존 필드를 변경할 수도 있었을 것입니다.
마지막으로 해야 할 일은 요소들을 배치로 묶을 때 모든 예제를 가장 긴 요소의 길이로 패딩하는 것입니다 — 이 기법을 동적 패딩이라고 합니다.
동적 패딩
배치 내에서 샘플들을 함께 배치하는 역할을 하는 함수를 collate function이라고 합니다. 이는 DataLoader
를 구축할 때 전달할 수 있는 인수로, 기본값은 샘플을 PyTorch 텐서로 변환하고 연결하는 함수입니다(요소가 목록, 튜플 또는 딕셔너리인 경우 재귀적으로). 우리의 경우 입력이 모두 같은 크기가 아니므로 이것은 불가능할 것입니다. 우리는 의도적으로 패딩을 연기하여 각 배치에서만 필요에 따라 적용하고 많은 패딩이 있는 지나치게 긴 입력을 피했습니다. 이것은 훈련을 상당히 가속화할 것이지만, TPU에서 훈련하는 경우 문제를 일으킬 수 있다는 점에 주의하세요 — TPU는 추가 패딩이 필요하더라도 고정된 모양을 선호합니다.
🚀 최적화 가이드: 패딩 전략과 TPU 고려사항을 포함한 훈련 성능 최적화에 대한 자세한 내용은 🤗 Transformers 성능 문서를 참조하세요.
실제로 이를 수행하려면 함께 배치하려는 데이터 세트 항목에 적절한 양의 패딩을 적용할 collate function을 정의해야 합니다. 다행히 🤗 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])}
좋아 보입니다! 이제 원시 텍스트에서 모델이 처리할 수 있는 배치까지 만들었으므로, 미세 조정할 준비가 되었습니다!
✏️ 직접 해보기! GLUE SST-2 데이터 세트에서 전처리를 복제해보세요. 쌍이 아닌 단일 문장으로 구성되어 있어 약간 다르지만, 나머지는 동일하게 보일 것입니다. 더 어려운 도전을 위해서는 GLUE 작업 중 어떤 것에서도 작동하는 전처리 함수를 작성해보세요.
📖 추가 연습: 🤗 Transformers 예제에서 이러한 실습 예제들을 확인해보세요.
완벽합니다! 이제 🤗 Datasets 라이브러리의 최신 모범 사례로 데이터를 전처리했으므로, 최신 Trainer API를 사용하여 모델을 훈련할 준비가 되었습니다. 다음 섹션에서는 Hugging Face 생태계에서 사용할 수 있는 최신 기능과 최적화를 사용하여 모델을 효과적으로 미세 조정하는 방법을 보여드리겠습니다.
섹션 퀴즈
데이터 처리 개념에 대한 이해도를 테스트해보세요.
1. batched=True 와 함께 Dataset.map() 을 사용하는 주요 장점은 무엇인가요?
2. 데이터 세트의 최대 길이로 모든 시퀀스를 패딩하는 대신 동적 패딩을 사용하는 이유는 무엇인가요?
3. BERT 토큰화에서 token_type_ids 필드는 무엇을 나타내나요?
4. load_dataset('glue', 'mrpc') 로 데이터 세트를 로딩할 때 두 번째 인수는 무엇을 지정하나요?
5. 훈련 전에 ‘sentence1’과 ‘sentence2’ 같은 열을 제거하는 목적은 무엇인가요?
Update on GitHub💡 핵심 요점
- 전처리를 훨씬 빠르게 하려면
Dataset.map()
에서batched=True
를 사용하세요DataCollatorWithPadding
을 사용한 동적 패딩이 고정 길이 패딩보다 효율적입니다- 모델의 추론 결과물(수치적 텐서, 올바른 열 이름)에 맞게 항상 데이터를 전처리하세요
- 🤗 Datasets 라이브러리는 대규모 효율적인 데이터 처리를 위한 강력한 도구를 제공합니다