에코프로.AI

[HuggingFace] Fine-tuning - 1 (Processing the data) 본문

AI Tutorial

[HuggingFace] Fine-tuning - 1 (Processing the data)

AI_HitchHiker 2025. 1. 5. 18:38

https://cdn.hashnode.com/res/hashnode/image/upload/v1683376943049/3a0f16db-5fdb-47aa-bcb9-cc024b0de27e.png


Processing the data (데이터 처리)

이전 장의 예를 계속하면 PyTorch에서 하나의 배치에 대한 시퀀스 분류기를 훈련하는 방법은 다음과 같습니다.

import torch
from transformers import AdamW, 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()

 

물론 두 문장으로만 모델을 훈련한다고 해서 좋은 결과를 얻을 수 있는 것은 아닙니다. 더 나은 결과를 얻으려면 더 큰 데이터 세트를 준비해야 합니다.

 

이 섹션에서는 William B. Dolan and Chris Brockett (윌리엄 B. 돌란과 크리스 브로켓)의 논문에서 소개된 MRPC(Microsoft Research Paraphrase Corpus) 데이터 집합을 예로 들어 설명합니다. 이 데이터 집합은 5,801쌍의 문장으로 구성되어 있으며, 의역인지 아닌지(즉, 두 문장이 같은 의미인지 아닌지)를 나타내는 레이블이 있습니다. 이 장에서 이 데이터 세트를 선택한 이유는 작은 데이터 세트이기 때문에 훈련을 실험하기 쉽기 때문입니다.

 

Loading a dataset from the Hub (허브에서 데이터 세트 로드)

허브에는 모델만 있는 것이 아니라 다양한 언어로 된 여러 데이터 세트도 있습니다. 여기에서 데이터 세트를 찾아볼 수 있으며, 이 섹션을 살펴본 후 새 데이터 세트를 로드하고 처리하는 것이 좋습니다(일반 설명서는 여기에서 참조). 하지만 지금은 MRPC 데이터 세트에 집중해 보겠습니다! 이것은 10개의 텍스트 분류 작업에서 ML 모델의 성능을 측정하는 데 사용되는 학술적 벤치마크인 GLUE 벤치마크를 구성하는 10개의 데이터 세트 중 하나입니다 .

 

🤗 Datasets 라이브러리는 허브에 데이터세트를 다운로드하고 캐시하는 매우 간단한 명령을 제공합니다. MRPC 데이터세트는 다음과 같이 다운로드할 수 있습니다.

 

- datasets 라이브러리를 먼저 설치해 줍니다.

pip install datasets

vscode 터미널 실행화면

 

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

vscode 실행화면

 

보시다시피, 우리는 훈련(train) 세트, 검증(validation) 세트, 테스트(test) 세트를 포함하는 DatasetDict  객체를 얻습니다. 각 세트에는 여러 열( sentence1, sentence2, label, idx)과 가변적인 수의 행이 포함되어 있으며, 이는 각 세트의 요소 수입니다(훈련(train) 세트에는 3,668쌍의 문장이 있고, 검증(validation) 세트에는 408쌍이 있고, 테스트(test) 세트에는 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 .'}

vscode 실행화면

 

레이블이 이미 정수로 되어 있으므로 전처리를 할 필요가 없습니다. 어떤 정수가 어떤 레이블에 해당하는지 알기 위해서는 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)}

vscode 실행화면

 

뒤에서 레이블은 클래스 레이블 유형이며, 레이블 이름에 대한 정수의 매핑은 이름 폴더에 저장됩니다. 0은 not_equivalent(의미가 다르다)에 해당하고 1은 equivalent(의미가 같다)에 해당합니다.

 

Preprocessing a dataset (데이터 집합 전처리)

dataset(데이터 집합)을 전 처리하려면 텍스트를 모델이 이해할 수 있는 숫자로 변환해야 합니다. 이전 장에서 보았듯이 토큰화 도구를 사용하면 이 작업을 수행할 수 있습니다. 토큰화 도구에 하나의 문장 또는 문장 목록을 공급할 수 있으므로, 이렇게 각 쌍의 첫 번째 문장과 두 번째 문장을 모두 직접 토큰화할 수 있습니다:

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_ids와 attention_mask 키에 대해 설명했지만 token_type_ids에 대해서는 뒤로 미뤘습니다. 이 예제에서는 입력의 어느 부분이 첫 번째 문장이고 어느 부분이 두 번째 문장인지 모델에 알려주는 역할을 합니다.

 

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] 문장1 [SEP]에 해당하는 부분은 모두 token_type_ids 가 0이고, 문장2 [SEP]에 해당하는 다른 부분은 모두 토큰 유형 ID가 1입니다.

 

다른 체크포인트를 선택하면 토큰화된 입력에 token_type_ids 가 반드시 포함되지는 않습니다(예: DistilBERT 모델을 사용하는 경우 반환되지 않음). 모델이 사전 학습 중에 이를 확인했기 때문에 모델이 이를 어떻게 처리할지 알 때만 반환됩니다.

 

여기서 BERT는 token type ID 로 사전 학습되며, 1장에서 설명한 마스크 언어 모델링 목표 외에도 다음 문장 예측이라는 추가 목표가 있습니다. 이 작업의 목표는 문장 쌍 간의 관계를 모델링하는 것입니다.

 

다음 문장 예측에서는 모델에 무작위로 마스킹된 토큰이 포함된 문장 쌍이 제공되고 두 번째 문장이 첫 번째 문장을 따르는지 예측하도록 요청합니다. 이 작업을 어렵지 않게 만들기 위해 절반은 추출된 원본 문서에서 두 문장이 서로 이어지고, 나머지 절반은 두 문장이 서로 다른 두 문서에서 나오도록 합니다.

 

일반적으로 토큰화된 입력값에 token_type_ids가 있는지 여부는 걱정할 필요가 없습니다. 토큰화 도구와 모델에 동일한 체크포인트를 사용하기만 하면 토큰화 도구가 모델에 무엇을 제공할지 알기 때문에 모든 것이 정상적으로 작동할 것입니다.

 

이제 토큰화 도구가 한 쌍의 문장을 어떻게 처리하는지 살펴보았으니, 토큰화 도구를 사용해 전체 데이터셋을 토큰화할 수 있습니다. 이전 장에서처럼 토큰화 도구에 첫 문장 목록과 두 번째 문장 목록을 제공함으로써 문장 쌍의 목록을 제공할 수 있습니다. 이는 2장에서 살펴본 패딩(padding) 및 잘라내기(truncation) 옵션과도 호환됩니다. 따라서 학습 데이터 집합을 사전 처리하는 한 가지 방법은 다음과 같습니다:

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

 

이 방법은 잘 작동하지만 사전(keys, input_ids, attention_mask, token_type_ids 및 목록의 목록인 값)을 반환한다는 단점이 있습니다. 또한 토큰화 중에 전체 데이터셋을 저장할 수 있는 충분한 RAM이 있는 경우에만 작동합니다(반면 🤗 데이터셋 라이브러리의 데이터셋은 디스크에 저장된 Apache Arrow 파일이므로 요청하는 샘플만 메모리에 로드된 상태로 유지합니다).

 

데이터를 데이터셋으로 유지하기 위해 Dataset.map() 메서드를 사용합니다. 이 메서드는 토큰화보다 더 많은 전처리가 필요한 경우 추가적인 유연성을 제공합니다. map() 메서드는 데이터 세트의 각 요소에 함수를 적용하여 작동하므로 입력을 토큰화하는 함수를 정의해 보겠습니다:

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

 

이 함수는 데이터 세트의 항목과 같은 사전을 가져와 input_ids, attention_mask, token_type_ids 키가 포함된 새 사전을 반환합니다. 토큰화기는 앞서 살펴본 것처럼 문장 쌍의 목록에서 작동하므로 예제 사전에 여러 샘플(각 키가 문장 목록)이 포함되어 있는 경우에도 작동한다는 점에 유의하세요. 이렇게 하면 map() 호출에서 batched=True 옵션을 사용할 수 있으므로 토큰화 속도가 크게 빨라집니다. 이 토큰화 도구는 🤗 토큰화 라이브러리에서 Rust( 성능과 안정성에 중점을 둔 시스템 프로그래밍 언어 )로 작성된 토큰화 도구로 지원됩니다. 이 토큰화 도구는 매우 빠를 수 있지만, 한 번에 많은 입력을 제공할 때만 가능합니다.

 

지금은 토큰화 함수에서 패딩 인수를 제외했습니다. 이는 모든 샘플을 최대 길이로 패딩하는 것은 효율적이지 않기 때문입니다. 일괄 처리할 때 샘플을 패딩하면 전체 데이터 세트의 최대 길이가 아닌 해당 일괄 처리의 최대 길이로만 패딩하면 되므로 더 좋습니다. 이렇게 하면 입력 길이가 매우 가변적인 경우 많은 시간과 처리 능력을 절약할 수 있습니다!

 

다음은 모든 데이터 세트에 토큰화 함수를 한 번에 적용하는 방법입니다. 이 함수가 각 요소에 개별적으로 적용되지 않고 데이터 집합의 여러 요소에 한 번에 적용되도록 매핑 호출에 batched=True를 사용하고 있습니다. 이렇게 하면 전처리를 더 빠르게 처리할 수 있습니다.

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_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 인수를 전달하여 멀티프로세싱을 사용할 수도 있습니다. 여기서는 🤗 토큰화 라이브러리가 이미 여러 스레드를 사용하여 샘플을 더 빠르게 토큰화하기 때문에 이 방법을 사용하지 않았지만, 이 라이브러리가 지원하는 빠른 토큰화 도구를 사용하지 않는다면 전처리 속도를 높일 수 있습니다.

 

토큰화 함수는 input_ids, attention_mask, token_type_ids 키가 포함된 사전을 반환하므로 이 세 필드가 데이터 세트의 모든 분할에 추가됩니다. 전처리 함수가 map()을 적용한 데이터 세트의 기존 키에 대해 새 값을 반환하는 경우 기존 필드가 변경되었을 수도 있습니다.

 

마지막으로 해야 할 일은 요소를 일괄 처리할 때 모든 예제를 가장 긴 요소의 길이에 맞춰 패딩하는 것인데, 이를 동적 패딩이라고 합니다.

 

Dynamic padding (동적 패딩)

배치 내에서 샘플을 조합하는 함수를 collate function (콜레이트 함수)라고 합니다. DataLoader 를 빌드할 때 전달할 수 있는 인자로, 기본값은 샘플을 PyTorch 텐서로 변환하고 연결(요소가 목록, 튜플 또는 사전인 경우 재귀적으로)하는 함수입니다. 우리의 경우 입력값이 모두 같은 크기가 아니기 때문에 이 기능은 불가능합니다. 각 배치에 필요한 만큼만 패딩을 적용하고 패딩이 많은 입력이 지나치게 길어지는 것을 방지하기 위해 의도적으로 패딩을 연기했습니다. 이렇게 하면 훈련 속도가 상당히 빨라지지만, TPU로 훈련하는 경우 문제가 발생할 수 있습니다. TPU는 추가 패딩이 필요한 경우에도 고정된 모양을 선호합니다.

 

실제로 이 작업을 수행하려면 일괄 처리하려는 데이터 세트의 항목에 정확한 양의 패딩을 적용하는 collate function (콜레이트 함수)를 정의해야 합니다. 다행히도 🤗 트랜스포머 라이브러리는 DataCollatorWithPadding 을 통해 이러한 함수를 제공합니다. 이 함수는 인스턴스화할 때 tokenizer를 필요로 하며(어떤 패딩 토큰을 사용할지, 모델이 입력의 왼쪽에 패딩을 기대하는지 오른쪽에 패딩을 기대하는지 알기 위해), 필요한 모든 작업을 수행합니다:

from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

 

이 새로운 장난감을 테스트하기 위해 훈련(train) 세트에서 함께 일괄 처리할 몇 가지 샘플을 가져와 보겠습니다. 여기에서는 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])}

vscode 실행화면

 

좋아 보이네요! 이제 원시 텍스트에서 모델이 처리할 수 있는 배치로 전환되었으므로 이제 미세 조정할 준비가 되었습니다!

 


참고사이트 : https://huggingface.co/learn/nlp-course/chapter3/2?fw=pt

 

끝~