LLM 엔지니어링4장 · LLM 을 완성하는 3단계와 Hugging Face19 / 23← 이전목차다음 →MetaContents

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와 같은 준비

Python
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 사용: True

2단계. 작은 GPT 만들기

Python
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,792

3단계. Trainer 로 사전학습하기

Python
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.70

4단계. 이어 쓰기와 저장

Python
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_loss5.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 를 그대로 복사해 줍니다. "사전학습" 절의 한 칸 밀기는 모델 안에서 합니다.
← 이전 절4장 · 실습 4 — 사전학습 데이터 만들기다음 절 →4장 · 실습 6 — 상담 말투로 SFT 하기