2. 데이터 샘플링

Reading time: 6 minutes

데이터 샘플링

데이터 샘플링은 GPT와 같은 대형 언어 모델(LLM)을 훈련하기 위한 데이터 준비 과정에서 중요한 단계입니다. 이는 모델이 이전 단어를 기반으로 다음 단어(또는 토큰)를 예측하는 방법을 학습하는 데 사용하는 입력 및 목표 시퀀스로 텍스트 데이터를 구성하는 것을 포함합니다. 적절한 데이터 샘플링은 모델이 언어 패턴과 의존성을 효과적으로 포착하도록 보장합니다.

tip

이 두 번째 단계의 목표는 매우 간단합니다: 입력 데이터를 샘플링하고 훈련 단계에 맞게 준비하는 것으로, 일반적으로 데이터셋을 특정 길이의 문장으로 분리하고 예상 응답도 생성하는 것입니다.

데이터 샘플링의 중요성

GPT와 같은 LLM은 이전 단어가 제공하는 맥락을 이해하여 텍스트를 생성하거나 예측하도록 훈련됩니다. 이를 달성하기 위해 훈련 데이터는 모델이 단어 시퀀스와 그 후속 단어 간의 관계를 학습할 수 있는 방식으로 구조화되어야 합니다. 이러한 구조화된 접근 방식은 모델이 일반화하고 일관되며 맥락에 맞는 텍스트를 생성할 수 있도록 합니다.

데이터 샘플링의 주요 개념

  1. 토큰화: 텍스트를 토큰(예: 단어, 하위 단어 또는 문자)이라고 하는 더 작은 단위로 나누는 과정.
  2. 시퀀스 길이 (max_length): 각 입력 시퀀스의 토큰 수.
  3. 슬라이딩 윈도우: 토큰화된 텍스트 위에 창을 이동시켜 겹치는 입력 시퀀스를 생성하는 방법.
  4. 스트라이드: 슬라이딩 윈도우가 다음 시퀀스를 생성하기 위해 앞으로 이동하는 토큰 수.

단계별 예제

데이터 샘플링을 설명하기 위해 예제를 살펴보겠습니다.

예제 텍스트

arduino
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."

토큰화

우리가 텍스트를 단어와 구두점으로 나누는 기본 토크나이저를 사용한다고 가정해 보겠습니다:

vbnet
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]

매개변수

  • 최대 시퀀스 길이 (max_length): 4 토큰
  • 슬라이딩 윈도우 보폭: 1 토큰

입력 및 타겟 시퀀스 생성

  1. 슬라이딩 윈도우 접근법:
  • 입력 시퀀스: 각 입력 시퀀스는 max_length 토큰으로 구성됩니다.
  • 타겟 시퀀스: 각 타겟 시퀀스는 해당 입력 시퀀스에 바로 뒤따르는 토큰으로 구성됩니다.
  1. 시퀀스 생성:
윈도우 위치입력 시퀀스타겟 시퀀스
1["Lorem", "ipsum", "dolor", "sit"]["ipsum", "dolor", "sit", "amet,"]
2["ipsum", "dolor", "sit", "amet,"]["dolor", "sit", "amet,", "consectetur"]
3["dolor", "sit", "amet,", "consectetur"]["sit", "amet,", "consectetur", "adipiscing"]
4["sit", "amet,", "consectetur", "adipiscing"]["amet,", "consectetur", "adipiscing", "elit."]
  1. 결과 입력 및 타겟 배열:
  • 입력:
python
[
["Lorem", "ipsum", "dolor", "sit"],
["ipsum", "dolor", "sit", "amet,"],
["dolor", "sit", "amet,", "consectetur"],
["sit", "amet,", "consectetur", "adipiscing"],
]
  • 타겟:
python
[
["ipsum", "dolor", "sit", "amet,"],
["dolor", "sit", "amet,", "consectetur"],
["sit", "amet,", "consectetur", "adipiscing"],
["amet,", "consectetur", "adipiscing", "elit."],
]

시각적 표현

토큰 위치토큰
1Lorem
2ipsum
3dolor
4sit
5amet,
6consectetur
7adipiscing
8elit.

보폭 1의 슬라이딩 윈도우:

  • 첫 번째 윈도우 (위치 1-4): ["Lorem", "ipsum", "dolor", "sit"] → 타겟: ["ipsum", "dolor", "sit", "amet,"]
  • 두 번째 윈도우 (위치 2-5): ["ipsum", "dolor", "sit", "amet,"] → 타겟: ["dolor", "sit", "amet,", "consectetur"]
  • 세 번째 윈도우 (위치 3-6): ["dolor", "sit", "amet,", "consectetur"] → 타겟: ["sit", "amet,", "consectetur", "adipiscing"]
  • 네 번째 윈도우 (위치 4-7): ["sit", "amet,", "consectetur", "adipiscing"] → 타겟: ["amet,", "consectetur", "adipiscing", "elit."]

보폭 이해하기

  • 보폭 1: 윈도우가 매번 한 토큰씩 앞으로 이동하여 매우 겹치는 시퀀스를 생성합니다. 이는 맥락적 관계를 더 잘 학습할 수 있지만, 유사한 데이터 포인트가 반복되므로 과적합의 위험이 증가할 수 있습니다.
  • 보폭 2: 윈도우가 매번 두 토큰씩 앞으로 이동하여 겹침을 줄입니다. 이는 중복성과 계산 부하를 감소시키지만, 일부 맥락적 뉘앙스를 놓칠 수 있습니다.
  • max_length와 같은 보폭: 윈도우가 전체 윈도우 크기만큼 앞으로 이동하여 겹치지 않는 시퀀스를 생성합니다. 이는 데이터 중복성을 최소화하지만, 시퀀스 간의 의존성을 학습하는 모델의 능력을 제한할 수 있습니다.

보폭 2의 예시:

같은 토큰화된 텍스트와 max_length 4를 사용하여:

  • 첫 번째 윈도우 (위치 1-4): ["Lorem", "ipsum", "dolor", "sit"] → 타겟: ["ipsum", "dolor", "sit", "amet,"]
  • 두 번째 윈도우 (위치 3-6): ["dolor", "sit", "amet,", "consectetur"] → 타겟: ["sit", "amet,", "consectetur", "adipiscing"]
  • 세 번째 윈도우 (위치 5-8): ["amet,", "consectetur", "adipiscing", "elit."] → 타겟: ["consectetur", "adipiscing", "elit.", "sed"] (계속된다고 가정)

코드 예시

https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb에서 코드 예시를 통해 이를 더 잘 이해해 봅시다.

python
# Download the text to pre-train the LLM
import urllib.request
url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt")
file_path = "the-verdict.txt"
urllib.request.urlretrieve(url, file_path)

with open("the-verdict.txt", "r", encoding="utf-8") as f:
raw_text = f.read()

"""
Create a class that will receive some params lie tokenizer and text
and will prepare the input chunks and the target chunks to prepare
the LLM to learn which next token to generate
"""
import torch
from torch.utils.data import Dataset, DataLoader

class GPTDatasetV1(Dataset):
def __init__(self, txt, tokenizer, max_length, stride):
self.input_ids = []
self.target_ids = []

# Tokenize the entire text
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})

# Use a sliding window to chunk the book into overlapping sequences of max_length
for i in range(0, len(token_ids) - max_length, stride):
input_chunk = token_ids[i:i + max_length]
target_chunk = token_ids[i + 1: i + max_length + 1]
self.input_ids.append(torch.tensor(input_chunk))
self.target_ids.append(torch.tensor(target_chunk))

def __len__(self):
return len(self.input_ids)

def __getitem__(self, idx):
return self.input_ids[idx], self.target_ids[idx]


"""
Create a data loader which given the text and some params will
prepare the inputs and targets with the previous class and
then create a torch DataLoader with the info
"""

import tiktoken

def create_dataloader_v1(txt, batch_size=4, max_length=256,
stride=128, shuffle=True, drop_last=True,
num_workers=0):

# Initialize the tokenizer
tokenizer = tiktoken.get_encoding("gpt2")

# Create dataset
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)

# Create dataloader
dataloader = DataLoader(
dataset,
batch_size=batch_size,
shuffle=shuffle,
drop_last=drop_last,
num_workers=num_workers
)

return dataloader


"""
Finally, create the data loader with the params we want:
- The used text for training
- batch_size: The size of each batch
- max_length: The size of each entry on each batch
- stride: The sliding window (how many tokens should the next entry advance compared to the previous one). The smaller the more overfitting, usually this is equals to the max_length so the same tokens aren't repeated.
- shuffle: Re-order randomly
"""
dataloader = create_dataloader_v1(
raw_text, batch_size=8, max_length=4, stride=1, shuffle=False
)

data_iter = iter(dataloader)
first_batch = next(data_iter)
print(first_batch)

# Note the batch_size of 8, the max_length of 4 and the stride of 1
[
# Input
tensor([[   40,   367,  2885,  1464],
[  367,  2885,  1464,  1807],
[ 2885,  1464,  1807,  3619],
[ 1464,  1807,  3619,   402],
[ 1807,  3619,   402,   271],
[ 3619,   402,   271, 10899],
[  402,   271, 10899,  2138],
[  271, 10899,  2138,   257]]),
# Target
tensor([[  367,  2885,  1464,  1807],
[ 2885,  1464,  1807,  3619],
[ 1464,  1807,  3619,   402],
[ 1807,  3619,   402,   271],
[ 3619,   402,   271, 10899],
[  402,   271, 10899,  2138],
[  271, 10899,  2138,   257],
[10899,  2138,   257,  7026]])
]

# With stride=4 this will be the result:
[
# Input
tensor([[   40,   367,  2885,  1464],
[ 1807,  3619,   402,   271],
[10899,  2138,   257,  7026],
[15632,   438,  2016,   257],
[  922,  5891,  1576,   438],
[  568,   340,   373,   645],
[ 1049,  5975,   284,   502],
[  284,  3285,   326,    11]]),
# Target
tensor([[  367,  2885,  1464,  1807],
[ 3619,   402,   271, 10899],
[ 2138,   257,  7026, 15632],
[  438,  2016,   257,   922],
[ 5891,  1576,   438,   568],
[  340,   373,   645,  1049],
[ 5975,   284,   502,   284],
[ 3285,   326,    11,   287]])
]

References