#
데이터 처리

이전 챕터의 예제에 이어서, 한 배치에서 시퀀스 분류기를 훈련하는 방법은 다음과 같습니다.

    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() 을 사용하는 주요 장점은 무엇인가요?

메모리를 덜 사용합니다. 여러 예제를 한 번에 처리하여 토큰화를 훨씬 빠르게 만듭니다. 자동으로 패딩을 처리해줍니다. 데이터를 PyTorch 텐서로 변환합니다.

###
2. 데이터 세트의 최대 길이로 모든 시퀀스를 패딩하는 대신 동적 패딩을 사용하는 이유는 무엇인가요?

동적 패딩이 모델 아키텍처에 의해 요구됩니다. 각 배치의 최대 길이까지만 패딩하여 계산 오버헤드를 줄입니다. 모델 정확도를 향상시킵니다. DataCollatorWithPadding을 사용할 때 필수입니다.

###
3. BERT 토큰화에서 token_type_ids 필드는 무엇을 나타내나요?

시퀀스에서 각 토큰의 위치입니다. 문장 쌍을 처리할 때 각 토큰이 어느 문장에 속하는지를 나타냅니다. 각 토큰의 어텐션 마스크입니다. 각 토큰의 어휘 ID입니다.

###
4. load_dataset('glue', 'mrpc') 로 데이터 세트를 로딩할 때 두 번째 인수는 무엇을 지정하나요?

로딩할 데이터 세트의 버전입니다. GLUE 벤치마크 내의 특정 작업 또는 하위 집합입니다. 데이터 세트의 분할(train/validation/test)입니다. 데이터를 반환할 형식입니다.

###
5. 훈련 전에 ‘sentence1’과 ‘sentence2’ 같은 열을 제거하는 목적은 무엇인가요?

훈련 중 메모리를 절약하기 위해서입니다. 모델이 이러한 원시 텍스트 열을 예상하지 않고 오류가 발생할 수 있기 때문입니다. 이러한 열들이 평가에 필요하지 않기 때문입니다. 훈련 속도를 크게 향상시키기 때문입니다.

> 💡 핵심 요점

> - 전처리를 훨씬 빠르게 하려면 `Dataset.map()`에서 `batched=True`를 사용하세요
> - `DataCollatorWithPadding`을 사용한 동적 패딩이 고정 길이 패딩보다 효율적입니다
> - 모델의 추론 결과물(수치적 텐서, 올바른 열 이름)에 맞게 항상 데이터를 전처리하세요
> - 🤗 Datasets 라이브러리는 대규모 효율적인 데이터 처리를 위한 강력한 도구를 제공합니다

Update on GitHub