← 학습 목차 · 텐서 기초 · Day1 · Day2

보충학습 · Chapter 2 Dataset

텍스트에서 학습 데이터까지

문자열이 어떻게 GPT의 입력이 될까요? 토큰화부터 정답 생성, 배치 구성, 임베딩까지 하나의 흐름으로 연결합니다. Day1·Day2의 진도와 별개로, 데이터 처리 과정이 헷갈릴 때 돌아오는 주제별 노트입니다.

기준 자료는 Chapter_2_Exercise_Dataset.ipynb입니다. 아래 ‘셀’ 번호는 노트북 위에서부터 센 순서입니다. 실행 결과의 토큰 ID는 토크나이저에 따라 달라지며, 설명용 숫자는 실제 인코딩 결과가 아닙니다.

먼저 학습의 목적을 연결합니다

GPT는 앞에 주어진 토큰을 보고 다음 토큰을 예측하도록 학습합니다. 이를 위해 원문을 숫자로 바꾸고, 각 위치에서 맞혀야 할 다음 토큰을 정답으로 준비합니다.

텍스트 → 토큰 ID → 입력·정답 쌍 → 배치 → 토큰·위치 임베딩 → Transformer → 어휘별 점수 → 정답과 비교

이 노트북은 임베딩까지의 입력 준비를 다룹니다. 이후 Attention은 문맥을 반영하고, 출력 레이어는 다음 토큰의 점수를 만듭니다. 학습 시에는 그 점수를 여기서 만든 정답 ID와 비교해 파라미터를 갱신합니다.

텐서 shape와 인덱싱이 어렵다면 텐서 기초를 먼저 읽고, 여기서 데이터 흐름을 확인한 뒤 Day1의 Attention·GPT 설명으로 이어가세요. 이후 Day2의 GPT 조립·Loss·학습 루프에서 이 입력과 정답이 실제 학습에 어떻게 쓰이는지 연결하세요.

셀 1

1. 문자열을 토큰 ID로 바꿉니다

import tiktoken
tokenizer = tiktoken.get_encoding("gpt2")
integers = tokenizer.encode(text, allowed_special={"<|endoftext|>"})
strings = tokenizer.decode(integers)

encode는 문자열을 정수 목록으로, decode는 그 목록을 문자열로 복원합니다. 토큰은 단어 전체일 수도, 단어 조각이나 문장부호일 수도 있습니다. 토큰 ID는 식별 번호이므로 ID가 크다고 의미가 더 강하거나 비슷한 의미끼리 번호가 가까운 것은 아닙니다.

allowed_special은 원문 속 <|endoftext|>를 특수 토큰으로 인식하도록 허용합니다. 각 문장 뒤에 자동으로 붙이라는 뜻은 아닙니다.

원본 예제의 "terraces" "of"는 파이썬에서 "terracesof"로 연결됩니다. 띄어쓰기를 의도했다면 두 번째 문자열 앞에 공백을 넣어야 합니다. 공백도 토큰화 결과에 영향을 줍니다.

셀 2

2. 정답은 입력보다 한 토큰 뒤에 있습니다

datas/the-verdict.txt가 없으면 다운로드한 뒤 UTF-8로 읽어 raw_text에 저장합니다. 전체 원문을 인코딩한 enc_text의 길이는 토큰 수입니다. enc_text[50:]는 예시를 위해 처음 50개 토큰을 건너뛴 것으로, 필수 전처리는 아닙니다.

context_size = 4
x = enc_sample[:context_size]
y = enc_sample[1:context_size + 1]

# 설명용 토큰 ID
# 원본: [10, 20, 30, 40, 50, ...]
# x:    [10, 20, 30, 40]
# y:    [20, 30, 40, 50]

슬라이스의 끝 인덱스는 포함되지 않습니다. 입력과 정답은 모두 길이 4이며, 정답의 각 원소는 해당 입력 위치 바로 다음 토큰입니다.

예측 위치모델이 볼 수 있는 입력정답
01020
110, 2030
210, 20, 3040
310, 20, 30, 4050

입력에는 뒤쪽 토큰도 함께 담겨 있지만, 모델의 causal attention이 미래 위치를 가립니다. Dataset은 정답을 정렬하고, Attention 마스크는 정답을 미리 보지 못하게 합니다.

셀 3

3. Dataset은 전체 원문에서 샘플을 만듭니다

한 쌍을 만들었던 슬라이싱을 전체 토큰 목록에 반복합니다. max_length는 샘플 하나의 입력 길이이고, stride는 다음 샘플의 시작점을 이동하는 간격입니다.

class GPTDatasetV1(Dataset):
    def __init__(self, txt, tokenizer, max_length, stride):
        self.input_ids = []
        self.target_ids = []
        token_ids = tokenizer.encode(
            txt, allowed_special={"<|endoftext|>"}
        )
        assert len(token_ids) > 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]

이 코드는 앞서 import torch와 from torch.utils.data import Dataset, DataLoader를 실행한 상태를 전제로 합니다. 입력 길이가 L이면 정답의 마지막 토큰까지 최소 L+1개가 필요합니다. 완전한 쌍을 만들 수 없는 끝부분은 버립니다. max_length와 stride는 양의 정수로 사용합니다.

슬라이딩 윈도우를 직접 움직여 보세요

고정된 설명용 ID 12개를 사용합니다. 원본 노트북을 실행하는 시뮬레이션이 아니라 동일한 슬라이싱 규칙의 시각화입니다.

len(dataset)은 전체 토큰 수가 아니라 샘플 쌍의 수입니다. dataset[0]은 첫 번째 입력·정답 텐서를 반환하며, 각각 shape는 [max_length]입니다. 이 구현은 모든 샘플을 생성할 때 메모리에 저장합니다.

셀 4–5

4. DataLoader는 샘플을 배치로 묶습니다

def create_dataloader_v1(txt, batch_size=4, max_length=256,
                         stride=128, shuffle=True, drop_last=True,
                         num_workers=0):
    tokenizer = tiktoken.get_encoding("gpt2")
    dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
    return DataLoader(
        dataset, batch_size=batch_size, shuffle=shuffle,
        drop_last=drop_last, num_workers=num_workers
    )

dataloader = create_dataloader_v1(
    raw_text, batch_size=1, max_length=4, stride=1, shuffle=False
)
data_iter = iter(dataloader)
first_batch = next(data_iter)
second_batch = next(data_iter)

iter로 배치를 꺼낼 순회자를 만들고, next로 다음 배치를 받습니다. 위 설정에서 첫 두 배치의 입력은 시작 위치가 한 토큰 차이입니다. 배치 크기가 1이어도 배치 차원은 유지되어 입력과 정답 각각 [1, 4]가 됩니다.

설정바뀌는 것
batch_size한 번에 묶어 전달할 샘플 수 B
shuffle샘플을 꺼내는 순서. 샘플 내부 토큰 순서는 유지
drop_last마지막 배치의 샘플 수가 B보다 적으면 버릴지 결정
num_workers데이터 로딩 작업에 사용할 별도 프로세스 수. 0은 현재 프로세스 사용

stride와 batch_size는 역할이 다릅니다. stride는 Dataset에서 샘플을 만드는 간격이고, batch_size는 만들어진 샘플을 묶는 개수입니다. 끝의 짧은 토큰 구간을 제외하는 일과 drop_last로 불완전한 배치를 버리는 일도 서로 다릅니다.

셀 6–7

5. ID를 벡터로 바꾸고 위치 정보를 더합니다

input_ids = torch.tensor([2, 3, 5, 1])
embedding_layer = torch.nn.Embedding(6, 3)
token_embeddings = embedding_layer(input_ids)
# weight: [6, 3] → 선택한 네 행: [4, 3]

Embedding은 학습 가능한 표입니다. 어휘가 6개이고 벡터 차원이 3이면 가중치는 [6, 3]입니다. 각 ID에 대응하는 행을 가져옵니다. 초기값은 무작위이며 학습으로 바뀝니다. 허용되는 ID는 0부터 5까지입니다. 노트북의 torch.manual_seed(123)은 난수 초기화의 재현을 돕습니다.

vocab_size, output_dim = 50257, 256
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
max_length = 4
dataloader = create_dataloader_v1(
    raw_text, batch_size=8, max_length=max_length,
    stride=max_length, shuffle=False
)
inputs, targets = next(iter(dataloader))      # 각각 [8, 4]
token_embeddings = token_embedding_layer(inputs)  # [8, 4, 256]
pos_embedding_layer = torch.nn.Embedding(max_length, output_dim)
positions = torch.arange(max_length)              # [0, 1, 2, 3]
pos_embeddings = pos_embedding_layer(positions)   # [4, 256]
input_embeddings = token_embeddings + pos_embeddings  # [8, 4, 256]

토큰 임베딩은 ‘어떤 토큰인가’를, 위치 임베딩은 ‘샘플 안에서 몇 번째인가’를 나타냅니다. 위치 벡터는 모든 배치 샘플에 broadcasting으로 더해집니다. 이어 붙이는 연산이 아니므로 차원이 512로 늘어나지 않습니다.

단계일반 shape노트북 예시
입력·정답 ID[B, T][8, 4]
토큰 임베딩 표[V, D][50257, 256]
토큰 임베딩 결과[B, T, D][8, 4, 256]
위치 임베딩 결과[T, D][4, 256]
Transformer 입력[B, T, D][8, 4, 256]

B는 배치 크기, T는 시퀀스 길이, V는 어휘 수, D는 임베딩 차원입니다. 정답 targets는 이후 손실 계산에 사용할 정수 ID로 유지합니다.

6. 뒤쪽 확장 자료는 어디에 연결될까요?

토크나이저 비교

노트북 후반은 Qwen과 EXAONE으로 여러 언어의 문장을 인코딩합니다. 같은 글도 토크나이저에 따라 ID와 토큰 수가 달라집니다. 토큰 수가 적다는 것은 해당 예제의 표현이 더 짧다는 뜻이며, 모델의 이해력이나 전체 성능을 보장하지 않습니다. 특수 토큰 추가 여부와 더 넓은 테스트 문장도 함께 봐야 합니다.

개별 ID를 하나씩 디코딩하면 문자 바이트가 나뉜 경우 글자가 깨져 보일 수 있습니다. 전체 ID 시퀀스의 디코딩 결과와 구분하세요. 다른 토크나이저로 바꿀 때는 해당 모델의 어휘와 임베딩도 맞아야 합니다.

Hugging Face 파일 구조

파일 이름 암기보다 ‘텍스트 변환 규칙·모델 구조·학습된 가중치’의 역할을 구분하세요. 노트북 마지막 외부 시뮬레이터의 cl100k_base는 본 실습의 gpt2와 다르므로, 같은 문장도 ID와 분할 결과가 다를 수 있습니다.

이유까지 설명할 수 있는지 확인하세요

입력 길이가 4인데 최소 5개 토큰이 필요한 이유는?

네 번째 입력의 다음 토큰을 정답으로 제공해야 하기 때문입니다. 입력과 정답은 각각 4개입니다.

토큰 12개, max_length=4, stride=2일 때 샘플 수는?

시작점은 range(0, 8, 2)의 0, 2, 4, 6이므로 4개입니다. 시작점 8에서는 정답의 마지막 토큰이 부족합니다.

위 샘플 4개를 batch_size=3, drop_last=True로 묶으면?

배치 1개만 나옵니다. 남은 샘플 1개는 버립니다. drop_last=False면 [3, 4]와 [1, 4] 크기의 입력 배치 두 개가 됩니다.

shuffle=True가 문장 안의 단어를 섞을까요?

샘플을 꺼내는 순서를 섞습니다. 샘플 내부의 토큰 순서와 입력·정답 대응은 유지합니다.

토큰 ID와 임베딩, Attention 출력은 어떻게 다른가요?

ID는 식별 번호, 임베딩은 그 ID를 조회한 학습 가능한 벡터, Attention 출력은 볼 수 있는 문맥 정보를 반영한 벡터입니다.

직접 빈칸을 복원해 보세요
input_chunk = token_ids[___ : ___ + max_length]
target_chunk = token_ids[___ + 1 : ___ + max_length + 1]
dataloader = DataLoader(___, batch_size=4)
embedding_layer = torch.nn.Embedding(___, ___)
정답과 이유

순서대로 i, i, i, i, dataset, vocab_size, output_dim입니다. 현재 시작점에서 일정 길이를 잘라내고, 정답은 한 칸 뒤에서 자릅니다. DataLoader에는 샘플을 제공할 객체를, Embedding에는 표의 행 수와 열 수를 전달합니다.