4장. LLM 을 완성하는 3단계와 Hugging Face
실습 5 — 작은 GPT 사전학습하기
실습 4의 데이터로, transformers 의 GPT2Config 로 만든 작은 GPT(블록 4개, 좌표 256개)를 처음부터 사전학습합니다. 학습 전과 뒤의 퍼플렉서티를 견주고, 이어 쓰기를 해 본 뒤 out_pretrained 폴더에 저장합니다. GPU 가 있으면 Trainer 가 알아서 GPU 와 혼합 정밀도를 씁니다. 이 PC(GTX 1650)에서 30초쯤 걸렸고, GPU 가 없으면 몇 분 걸립니다.
1단계. 실습 4와 같은 준비
import math
import torch
from transformers import PreTrainedTokenizerFast
from datasets import Dataset
# 실습 4와 똑같이 데이터를 만든다
tok = PreTrainedTokenizerFast(tokenizer_file="out_tok/tokenizer.json", pad_token="<pad>", eos_token="<eos>")
with open("shop_corpus.txt", encoding="utf-8") as f:
lines = [line for line in f.read().splitlines() if line]
ds = Dataset.from_dict({"text": lines}).train_test_split(test_size=0.05, seed=42)
BLOCK = 128
def tokenize(batch):
return tok([t + "<eos>" for t in batch["text"]])
def group(batch):
ids = sum(batch["input_ids"], [])
n = len(ids) // BLOCK * BLOCK
return {"input_ids": [ids[i:i + BLOCK] for i in range(0, n, BLOCK)]}
lm_data = ds.map(tokenize, batched=True, remove_columns=["text"]).map(group, batched=True, remove_columns=["attention_mask"])
print("학습용 묶음:", len(lm_data["train"]), "/ 검증용 묶음:", len(lm_data["test"]))
print("GPU 사용:", torch.cuda.is_available())학습용 묶음: 360 / 검증용 묶음: 18
GPU 사용: True2단계. 작은 GPT 만들기
from transformers import GPT2Config, GPT2LMHeadModel, set_seed
set_seed(42) # 파이썬, NumPy, PyTorch 의 씨앗을 한꺼번에 고정한다
config = GPT2Config(
vocab_size=len(tok), # 우리 토크나이저의 어휘 크기
n_positions=BLOCK, # 한 번에 보는 최대 토큰 수
n_embd=256, # 좌표 수
n_layer=4, # 블록 수
n_head=4, # 헤드 수
bos_token_id=tok.eos_token_id, eos_token_id=tok.eos_token_id, pad_token_id=tok.pad_token_id,
)
model = GPT2LMHeadModel(config) # 설정대로 빈 모델을 만든다(가중치는 난수)
print(f"파라미터 수: {sum(p.numel() for p in model.parameters()):,}")파라미터 수: 3,489,7923단계. Trainer 로 사전학습하기
from transformers import Trainer, TrainingArguments, DataCollatorForLanguageModeling
# mlm=False: 가리고 맞히기가 아니라 다음 토큰 맞히기. labels 를 input_ids 로 채워 준다
collator = DataCollatorForLanguageModeling(tok, mlm=False)
args = TrainingArguments(
"out_pretrain_ckpt",
per_device_train_batch_size=32,
num_train_epochs=8,
learning_rate=1e-3,
warmup_steps=50, # 처음 50걸음은 학습률을 천천히 올린다
eval_strategy="epoch", # 에포크마다 검증 손실을 잰다
logging_steps=50,
save_strategy="no", # 중간 저장은 하지 않는다
report_to="none",
seed=42,
fp16=torch.cuda.is_available(), # GPU 가 있을 때만 혼합 정밀도
disable_tqdm=True, # 진행 막대를 끈다
)
trainer = Trainer(model=model, args=args, train_dataset=lm_data["train"],
eval_dataset=lm_data["test"], data_collator=collator)
before = trainer.evaluate()["eval_loss"] # 학습 전 검증 손실
trainer.train()
after = trainer.evaluate()["eval_loss"] # 학습 뒤 검증 손실
print(f"학습 전: 손실 {before:.3f}, 퍼플렉서티 {math.exp(before):.1f}")
print(f"학습 뒤: 손실 {after:.3f}, 퍼플렉서티 {math.exp(after):.2f}"){'eval_loss': '7.052', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '1.462', 'eval_samples_per_second': '12.31', 'eval_steps_per_second': '2.052', 'epoch': 0}
{'eval_loss': '5.205', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0586', 'eval_samples_per_second': '307.2', 'eval_steps_per_second': '51.19', 'epoch': '1'}
{'eval_loss': '3.491', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0586', 'eval_samples_per_second': '307', 'eval_steps_per_second': '51.16', 'epoch': '2'}
{'eval_loss': '1.881', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0599', 'eval_samples_per_second': '300.7', 'eval_steps_per_second': '50.12', 'epoch': '3'}
{'eval_loss': '1.005', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0598', 'eval_samples_per_second': '301.1', 'eval_steps_per_second': '50.19', 'epoch': '4'}
{'loss': '3.666', 'grad_norm': '0.504', 'learning_rate': '0.00098', 'epoch': '4.167'}
{'eval_loss': '0.7918', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0599', 'eval_samples_per_second': '300.6', 'eval_steps_per_second': '50.09', 'epoch': '5'}
{'eval_loss': '0.6629', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0601', 'eval_samples_per_second': '299.3', 'eval_steps_per_second': '49.89', 'epoch': '6'}
{'eval_loss': '0.562', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0597', 'eval_samples_per_second': '301.7', 'eval_steps_per_second': '50.29', 'epoch': '7'}
{'eval_loss': '0.5297', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0601', 'eval_samples_per_second': '299.3', 'eval_steps_per_second': '49.88', 'epoch': '8'}
{'train_runtime': '25.79', 'train_samples_per_second': '111.7', 'train_steps_per_second': '3.722', 'train_loss': '2.236', 'epoch': '8'}
{'eval_loss': '0.5297', 'eval_model_preparation_time': '0.0011', 'eval_runtime': '0.0595', 'eval_samples_per_second': '302.3', 'eval_steps_per_second': '50.39', 'epoch': '8'}
학습 전: 손실 7.052, 퍼플렉서티 1154.7
학습 뒤: 손실 0.530, 퍼플렉서티 1.704단계. 이어 쓰기와 저장
model = trainer.model.eval()
for start in ["[안내] 교환은", "[상품] 텀블러는", "[공지]"]:
enc = tok(start, return_tensors="pt").to(model.device) # 모델이 있는 장치(GPU/CPU)로
with torch.no_grad():
out = model.generate(**enc, max_new_tokens=30, do_sample=False,
eos_token_id=tok.eos_token_id, pad_token_id=tok.pad_token_id)
print(tok.decode(out[0], skip_special_tokens=True))
# 모델과 토크나이저를 한 폴더에 저장한다. 실습 6과 프로젝트에서 from_pretrained 로 불러온다
model.save_pretrained("out_pretrained")
tok.save_pretrained("out_pretrained")
print("저장: out_pretrained")[안내] 교환은 상품을 받은 날부터 7일 안에 신청할 수 있습니다.
[상품] 텀블러는 이중 스테인리스로 만들어 보온이 오래갑니다. 가격은 45,000원입니다.
[공지] 7월 6일부터 봄맞이 할인 행사를 엽니다.
저장: out_pretrained이 실습이 뜻하는 것
이 실습은 아무것도 모르는 새 직원에게 가게 안내문 카드 묶음을 여러 번 통째로 읽히는 것과 같습니다. 실습 4의 카드를 준비하고(1단계), 머릿속이 빈 새 직원을 들이고(2단계), 카드를 여덟 바퀴 읽히며 바퀴마다 시험 카드로 다음 낱말 맞히기 시험을 보고(3단계), 문장 첫머리를 주고 이어 말하게 한 뒤 저장합니다(4단계). 여기서 빈 새 직원이 가중치가 난수인 작은 GPT, 여덟 바퀴가 8 에포크(epoch, 데이터를 한 번 다 도는 단위), 시험 점수가 검증 손실과 퍼플렉서티입니다. 비유와 다른 점은, 사람은 뜻을 이해하며 읽지만 이 모델은 다음 토큰의 확률만 배워 글을 거의 외운다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 준비 | 학습용 360, 검증용 18, GPU True | 실습 4와 같은 카드가 준비됐고, 이 PC 에서는 GPU(Graphics Processing Unit, 그래픽 처리 장치)를 썼습니다 |
| 2. 파라미터 수 | 3,489,792 | 약 350만 개로, 실습 1의 Qwen2.5-0.5B 의 140분의 1쯤입니다. 값은 아직 난수입니다 |
| 3. 학습 전 | 손실 7.052, 퍼플렉서티 1154.7 | 다음 토큰을 어휘 1,162개 가운데 거의 아무렇게나 찍는 수준입니다 |
| 3. 에포크별 eval_loss | 5.205 → 3.491 → … → 0.5297 | 바퀴를 돌 때마다 시험 손실이 꾸준히 내려갔고, 앞쪽 바퀴에서 가장 많이 내려갔습니다 |
| 3. 학습 시간 | train_runtime 25.79 | 이 PC 의 GTX 1650 에서 26초쯤 걸렸습니다. 기계마다 다릅니다 |
| 3. 학습 뒤 | 손실 0.530, 퍼플렉서티 1.70 | 다음 토큰의 후보가 평균 두 개도 안 될 만큼 확신하게 되었습니다 |
| 4. 이어 쓰기 | "[안내] 교환은 상품을 받은 날부터 7일 안에…" | 데이터의 문장을 거의 그대로 이어 씁니다. 가격 45,000원처럼 데이터에서 바뀌던 자리만 숫자가 그때그때 다릅니다 |
| 4. 저장 | 저장: out_pretrained | 모델과 토크나이저를 한 폴더에 담았습니다. 실습 6이 이 폴더를 불러 씁니다 |
한 줄로 줄이면, 난수로 시작한 350만 파라미터 모델이 쇼핑몰 글 4만 6천 토큰을 여덟 바퀴 읽고 그 글을 거의 외워 이어 쓰게 된 실습입니다. 퍼플렉서티 1.70 은 같은 틀로 지어낸 글 안에서의 점수라, 이 모델이 한국어를 잘한다는 뜻은 아닙니다. 질문에 답하는 법은 아직 모르며, 그것은 실습 6의 SFT 에서 가르칩니다. 학습 시간과 손실은 이 PC 에서 얻은 값입니다.
코드 설명
GPT2Config로 크기만 정하면GPT2LMHeadModel(config)가 3장에서 손으로 쓴 것과 같은 짜임(임베딩, 블록, 출력층)을 만들어 줍니다.from_pretrained가 아니라 설정으로 만들었으니 가중치는 난수입니다.- 파라미터는 3,489,792개(약 350만)로 Qwen2.5-0.5B 의 140분의 1쯤입니다. 학습 전 퍼플렉서티 1154.7 은 어휘 수 1,162 와 거의 같습니다. "손실과 퍼플렉서티" 절에서 본 대로, 아무것도 모르는 모델은 어휘 전체 가운데 찍는 셈입니다. 학습 뒤에는 1.70 까지 내려가 다음 토큰을 거의 확신합니다.
- "[안내] 교환은" 뒤로 "상품을 받은 날부터 7일 안에 신청할 수 있습니다"를, "[상품] 텀블러는" 뒤로 데이터의 상품 설명을 그대로 이어 씁니다. 글이 문장 틀로 지어져 있어 거의 외운 것입니다. 가격(45,000원)처럼 데이터에서 무작위로 바뀌던 자리만 그때그때 다른 숫자가 나옵니다.
DataCollatorForLanguageModeling(tok, mlm=False)는 배치를 만들 때labels에input_ids를 그대로 복사해 줍니다. "사전학습" 절의 한 칸 밀기는 모델 안에서 합니다.