스터디의 일환으로 정리한 글 입니다.
2장의 주요 내용
- 대규모 언어 모델 훈련을 위한 텍스트를 준비힌다.
- 텍스트를 단어와 부분단어인 토큰으로 나눈다.
- 고급 텍스트 토큰화 방법인 바이트 페어 인코딩을 학습한다.
- 슬라이딩 윈도 방식으로 훈련 데이터를 샘플링한다.
- 대규모 언어 모델에 주입하기 위해 토큰을 벡터로 변환한다.
2.1 단어 임베딩 이해하기
신경망은 문자나 단어와 같은 범주형 데이터를 직접 처리하지 못한다.
따라서 텍스트는 신경망이 다룰 수 있는 실수 벡터 형태로 변환해야 하며, 이 과정을 임베딩이라고 한다.
임베딩은 텍스트뿐 아니라 이미지, 음성과 같은 비정형 데이터도 연속적인 벡터 공간으로 바꾸어 신경망이 처리할 수 있게 한다.
초기 임베딩 기법의 대표적인 예로는 Word2Vec이 있다.
Word2Vec : 타깃 단어가 주어지면 문맥 단어를 예측하거나 그 반대 방식으로 신경망을 훈련하여 단어 임베딩을 생성한다.
단어를 2차원 단어 임베딩 공간에 투영하면 아래와 같이 비슷한 단어는 가깝게 모여있게 된다.

임베딩 벡터의 차원은 모델 크기에 따라 달라진다.
결론적으로 모델이 커질수록 단어의 의미를 더 정교하게 표현할 수 있게 된다.
다음으로 LLM에서 사용하는 임베딩을 준비하는데 필요한 단계를 살펴보자.
텍스트를 단어로 분할하고, 단어를 토큰으로 변환하고, 토큰을 임베딩 벡터로 변환하는 순서이다.
2.2 텍스트 토큰화하기
텍스트 토큰화(tokenization)는 문장을 더 작은 단위인 토큰으로 나누는 과정이다.
모델은 긴 문장을 한 덩어리로 다루기보다 토큰의 연속으로 처리한다.
토큰화 방식은 여러 가지가 있다.
LLM의 텍스트 처리 단계는 아래와 같다.

1. 입력된 문장을 토큰 단위로 나눈다.
2. 각 토큰을 정수 ID로 매핑한다.
3. 그 ID를 임베딩 벡터로 변환한다.
실습)
import re
# 파일 읽기 - with 문을 사용하면 파일이 자동으로 닫혀 메모리 누수 방지
with open('the-verdict.txt','r', encoding='utf-8') as f:
raw_text = f.read()
print('총 문자 개수:', len(raw_text))
raw_text[:99]
# 정규식으로 토큰 분리
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)' , raw_text)
# 후처리 - 공백 제거 및 필터링(각 조각의 양끝 공백 제거/공백만 있는 항목 제거)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
print(len(preprocessed))
print(preprocessed[:30])
출력 예시
총 문자 개수: 20479
4690
['I', 'HAD', 'always', 'thought', 'Jack', 'Gisburn', 'rather', 'a', 'cheap', 'genius', '--', 'though', 'a', 'good', 'fellow', 'enough', '--', 'so', 'it', 'was', 'no', 'great', 'surprise', 'to', 'me', 'to', 'hear', 'that', ',', 'in']
2.3 토큰을 토큰 ID로 변환하기
앞에서 텍스트를 단어 단위로 나누는 토큰화 작업을 진행했다.
이후에는 토큰을 정수 ID로 변환하고, 이를 다시 텍스트로 복원하는 토크나이저를 만들어보고자 한다.
모델은 문자열이 아니라 숫자를 처리하므로, 각 토큰을 정수 ID로 바꾸는 과정이 필요하다.
이 단계에서는 먼저 어휘 사전(vocabulary) 을 만든다. 어휘 사전에는 사용 가능한 토큰과 그에 대응하는 ID가 저장된다. 예를 들어 특정 토큰이 사전에 등록되어 있다면, 그 토큰은 하나의 고유한 정수 값으로 표현된다.
이 과정의 핵심은 다음과 같다.
- 토큰마다 고유한 ID가 부여된다.
- 입력 문장은 토큰 ID 시퀀스로 표현된다.
- 모델은 이 숫자 시퀀스를 바탕으로 계산을 수행한다.
즉, 토큰 ID 변환은 텍스트를 수치화된 시퀀스 데이터로 바꾸는 단계다.
어휘사전 만들기
all_words = sorted(set(preprocessed))
vocab = {token: integer for integer, token in enumerate(all_words)}
vocab_size = len(all_words)
print(vocab_size)
for i, item in enumerate(vocab.items()):
print(item)
if i > 50:
break
출력 예시
1130
('!', 0)
('"', 1)
("'", 2)
('(', 3)
...
('Her', 49)
('Hermia', 50)
# 총 1,130개의 고유 토큰이 존재하며, 각 토큰이 정수형 ID로 매핑되어있다.
# 텍스트를 정수 배열로 바꿀 수 있게 된다.
파이썬으로 양방향 토크나이저 구현
class SimpleTokenizerV1:
def __init__(self, vocab):
self.str_to_int = vocab
self.int_to_str = {i:s for s, i in vocab.items()} # 역어휘 사전 (id → 단어)
# 텍스트를 토큰 ID로 변환
def encode(self, text):
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)' , text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
ids = [self.str_to_int[s] for s in preprocessed]
return ids
# 토큰 ID를 텍스트로 복원
def decode(self, ids):
text = ' '.join([self.int_to_str[i] for i in ids])
text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)
return text
예제: 소설 한 구절 토큰화
tokenizer = SimpleTokenizerV1(vocab)
text = """"It's the last he painted, you know,"
Mrs. Gisburn said with pardonable pride."""
print('원본:', text)
ids = tokenizer.encode(text)
print('인코딩:', ids)
print('디코딩:', tokenizer.decode(ids))
출력 결과
원본: "It's the last he painted, you know,"
Mrs. Gisburn said with pardonable pride.
인코딩: [1, 56, 2, 850, 988, 602, 533, 746, 5, 1126, 596, 5, 1, 67, 7, 38, 851, 1108, 754, 793, 7]
디코딩: " It' s the last he painted, you know," Mrs. Gisburn said with pardonable pride.
위와 같이 토큰화, 역 토큰화를 확인했다. 그러나 한가지 문제점이 존재한다.
Vocabulary에 없는 단어가 나오면 오류가 발생한다는 점이다.
2.4 특수 문맥 토큰 추가하기
실제 모델 입력에서는 일반 단어 토큰 외에도 특별한 기능을 가진 토큰들이 필요하다. 이를 특수 토큰(special tokens)이라고 한다.
대표적인 특수 토큰은 다음과 같다.
- 문장 시작을 나타내는 토큰
- 문장 끝을 나타내는 토큰
- 패딩을 위한 토큰
- 문장 또는 구간 구분을 위한 토큰
이러한 특수 토큰은 모델에게 단순한 텍스트 이상의 구조적 정보를 제공한다. 예를 들어 입력의 시작과 끝, 문장 간 구분, 빈 자리를 명확히 알려줄 수 있다.
즉, 특수 토큰은 모델이 문맥과 입력 구조를 더 정확하게 해석하도록 돕는 역할을 한다.
특수 토큰 추가
all_tokens = sorted(list(set(preprocessed)))
all_tokens.extend(['<|endoftext|>', '<|unk|>'])
vocab = {token: integer for integer, token in enumerate(all_tokens)}
위 코드에서 새로운 토큰은 아래와 같은 의미를 가진다.
- |unk|> → 모르는 단어(Unknown)
- <|endoftext|> → 문장의 끝(End of Text)
GPT 모델의 경우 <|endoftext|> 하나로 문장 경계를 처리한다.
2.5 바이트 페어 인코딩
BPE 알고리즘은 어휘사전에 없는 단어를 개별 문자로 나누어 처음본 단어를 처리한다.
문자를 바이트 단위로 변환하여 어휘사전에 추가하고, 다음 단계에서 등장하는 문자 조합을 부분단어로 합치는 방식입니다.
이 방식의 장점은 다음과 같다.
- 희귀 단어를 완전히 모르는 단어로 처리하지 않아도 된다.
- 어휘 사전 크기를 지나치게 키우지 않으면서도 다양한 단어를 표현할 수 있다.
- 자주 등장하는 패턴을 효율적으로 압축할 수 있다.
즉, BPE는 단어 전체와 문자 단위 사이의 균형을 잡아 주는 토큰화 방식이라고 볼 수 있다.
BPE 알고리즘
1. 반복적으로 자주 등장하는 문자를 부분 단어로 합친다.
2. 다시 자주 등장하는 부분단어를 단어로 합쳐서 어휘사전을 구축한다.
2.6 슬라이딩 윈도로 데이터 샘플링하기
LLM은 보통 긴 텍스트 전체를 한 번에 처리하지 않고, 일정 길이의 입력 구간을 잘라 학습에 사용한다.
이때 활용되는 방법이 슬라이딩 윈도(sliding window)다.
슬라이딩 윈도는 말 그대로 일정 크기의 창(window)을 텍스트 위에서 조금씩 이동시키며 입력 샘플을 만드는 방식이다.
즉, 모델이 앞의 문맥을 보고, 다음 단어를 예측하도록 학습하는 방식이다.
일정 길이의 토큰 시퀀스를 입력으로 주고, 그 다음 토큰을 정답으로 두는 식으로 학습 데이터를 구성할 수 있다.
이 방법의 장점은 다음과 같다.
- 긴 텍스트를 여러 학습 샘플로 나눌 수 있다.
- 문맥의 연속성을 어느 정도 유지할 수 있다.
- 데이터 활용 효율을 높일 수 있다.
1. 입력과 정답 쌍 생성하기
with open('the-verdict.txt','r',encoding='utf-8') as f:
raw_text = f.read()
enc_text = tokenizer.encode(raw_text)
print("총 토큰 개수 :", len(enc_text))
enc_sample = enc_text[50:] # 처음 50개 토큰은 테스트용으로 제외
context_size = 4 # 문맥의 길이(입력 토큰 개수)
x = enc_sample[:context_size]
y = enc_sample[1:context_size+1]
print(f"x: {x}")
print(f"y: {y}")
문맥의 크기가 4일때,
x: [290, 4920, 2241, 287]
y: [4920, 2241, 287, 257]
위와 같은 형태로 입력(x)과 타깃(y)가 한 칸씩 밀린 형태로 구성된다.
이해하기 쉽게 텍스트 형태로 변환하면 아래와 같다.
and ---> established
and established ---> himself
and established himself ---> in
and established himself in ---> a
즉, 모델은 “and established himself in” 다음에 “a”가 나와야 함을 학습하게 됩니다.
2. Pytorch Dataset으로 데이터 구성하기
입력과 타깃 쌍을 자동으로 생성해주는 데이터 로더를 구현한다.
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 = []
token_ids = tokenizer.encode(txt)
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]
이 과정을 반복하면서 전체 텍스트를 작은 학습 샘플로 분리한다.
3. 데이터 로더 생성 및 샘플 확인
def create_dataloader_v1(txt, batch_size=4, max_length=256, stride=128, shuffle=True):
tokenizer = tiktoken.get_encoding('gpt2')
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, drop_last=True)
return dataloader
이때 문맥 크기는 4, 배치 크기는 1로 설정하여 확인한다.
with open('the-verdict.txt','r',encoding='utf-8') as f:
raw_text = f.read()
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)
출력 결과는 아래와 같다.
[tensor([[ 40, 367, 2885, 1464]]), tensor([[ 367, 2885, 1464, 1807]])]
[tensor([[ 367, 2885, 1464, 1807]]), tensor([[2885, 1464, 1807, 3619]])]
배치 크기를 늘리면 여러 샘플을 동시에 불러올 수 있다.
LLM의 사전 학습은 생성된 쌍을 반복적으로 학습하면서,
문맥으로부터 다음 단어를 정확히 예측하는 능력을 키우게 된다.
2.7 토큰 임베딩 만들기
토큰 ID는 정수일 뿐이므로, 그대로는 의미 있는 계산을 하기에 부족하다.
따라서 각 토큰 ID를 벡터로 변환하는 단계가 필요한데, 이것이 토큰 임베딩이다.
토큰 임베딩은 다음과 같이 이해할 수 있다.
- 각 토큰 ID에 대해 하나의 벡터를 대응시킨다.
- 이 벡터는 학습 가능한 파라미터다.
- 학습이 진행되면서 각 토큰은 문맥에 맞는 표현을 얻게 된다.
즉, 토큰 임베딩은 토큰을 모델이 처리할 수 있는 의미 있는 연속 벡터 표현으로 바꾸는 과정이다.
여기서 중요한 점은 2.1에서 다룬 단어 임베딩 개념이, 실제 모델 구현에서는 2.7의 토큰 임베딩 층으로 구체화된다는 것이다.
2.8 단어 위치 인코딩하기
트랜스포머는 입력 토큰들을 병렬적으로 처리하기 때문에, 순서 정보를 별도로 제공하지 않으면 어떤 단어가 앞에 있고 뒤에 있는지 알기 어렵다. 이를 해결하기 위해 사용하는 것이 위치 인코딩이다.
위치 인코딩은 각 토큰이 문장 안에서 몇 번째 위치에 있는지를 나타내는 정보다. 이 정보가 있어야 모델은 같은 단어라도 위치에 따라 다른 의미를 가질 수 있다는 점을 반영할 수 있다.
예를 들어 “나는 학교에 간다”와 “학교에 나는 간다”는 포함된 단어가 같더라도 순서가 다르기 때문에 의미와 자연스러움이 달라진다. 모델이 이러한 차이를 반영하려면 토큰 자체의 정보뿐 아니라 위치 정보도 함께 받아야 한다.
따라서 실제 입력 표현은 단순한 토큰 임베딩만이 아니라, 토큰 임베딩 + 위치 정보의 조합으로 구성된다.
위치 인코딩 방식
1. 절대 위치 임베딩
- 각 위치(0번째, 1번째, 2번째…)마다 고유한 벡터를 학습한다.
- 입력 토큰 임베딩에 위치 임베딩을 단순히 더하는(add) 방식으로 사용된다.
- 예를 들어 첫 번째 단어에는 “position_0” 임베딩이, 두 번째 단어에는 “position_1” 임베딩이 더해진다.
- GPT 시리즈(OpenAI의 GPT-2, GPT-3)는 이 방식을 사용한다.
2. 상대 위치 임베딩
- 위치 자체보다는 토큰 간의 거리(distance) 에 초점을 맞춘다.
- 즉, “이전 단어로부터 몇 칸 떨어져 있는가”와 같은 거리를 학습한다.
- 시퀀스 길이가 달라지더라도 관계적 패턴을 유지할 수 있기 때문에 긴 문장이나 다양한 입력 길이에 더 잘 일반화되는 장점이 있습니다.
2.9 요약
2장은 텍스트를 LLM의 입력으로 바꾸는 전처리 과정을 다루었다. 핵심 내용을 정리하면 다음과 같다.
- 단어 임베딩은 단어를 벡터 공간에 표현하는 개념이다.
- 텍스트 토큰화는 문장을 토큰 단위로 나누는 과정이다.
- 각 토큰은 어휘 사전을 통해 고유한 토큰 ID로 변환된다.
- 특수 문맥 토큰은 입력 구조와 문맥 경계를 명확히 하는 데 사용된다.
- 바이트 페어 인코딩은 서브워드 단위 토큰화를 구현하는 대표적인 방법이다.
- 슬라이딩 윈도는 긴 텍스트로부터 학습 샘플을 생성하는 방식이다.
- 토큰 임베딩은 토큰 ID를 학습 가능한 벡터로 변환한다.
- 위치 인코딩은 토큰 순서 정보를 모델에 제공한다.
2장의 핵심은 텍스트가 바로 모델에 입력되는 것이 아니라, 토큰화 → ID 변환 → 임베딩 → 위치 정보 추가라는 단계를 거쳐 LLM이 처리할 수 있는 형태로 변환된다는 점이다.
'Study' 카테고리의 다른 글
| [밑바닥부터 만들면서 배우는 LLM] 1장 - 대규모 언어 모델 이해하기 (0) | 2026.03.30 |
|---|---|
| [강화학습] Introduction_RL_2 (0) | 2025.04.08 |
| [강화학습] 1.Introduction_RL_1 (0) | 2025.04.05 |
| [Git/Github] 협업을 위한 Github_간단 정리 (0) | 2025.03.15 |
| [소프트웨어 공학] 애자일(Agile) 방법론 (0) | 2025.03.03 |
