4장. LLM 을 완성하는 3단계와 Hugging Face
실습 6 — 상담 말투로 SFT 하기
실습 5에서 사전학습한 작은 GPT 에, 가상 쇼핑몰의 질문과 상담 말투의 답 72쌍(sft_shop.jsonl 의 학습용)으로 SFT 를 합니다. SFT 전과 뒤의 답을 견주고, 학습에 쓰지 않은 시험 질문 24개로 맞힌 수를 셉니다. 실습 5를 먼저 실행해 out_pretrained 가 있어야 합니다.
1단계. 데이터 읽고 템플릿 정하기
import json
import torch
from transformers import PreTrainedTokenizerFast, GPT2LMHeadModel, set_seed
from datasets import Dataset
tok = PreTrainedTokenizerFast.from_pretrained("out_pretrained") # 실습 5에서 함께 저장한 토크나이저
# jsonl: 한 줄이 JSON 하나. 의도, 질문, 답변, 정답열쇠, 나눔(train/test)
with open("sft_shop.jsonl", encoding="utf-8") as f:
rows = [json.loads(line) for line in f]
train_rows = [r for r in rows if r["나눔"] == "train"]
test_rows = [r for r in rows if r["나눔"] == "test"]
print("학습용:", len(train_rows), "/ 시험용:", len(test_rows))
print(train_rows[0])
def prompt(q):
"""학습할 때와 물어볼 때 똑같이 쓰는 템플릿."""
return f"### 질문:\n{q}\n### 답변:\n"학습용: 72 / 시험용: 24
{'의도': '배송 기간', '질문': '배송은 얼마나 걸리나요?', '답변': '안녕하세요, 고객님. 결제 후 1~3일 안에 도착합니다. 제주와 섬 지역은 2일 더 걸립니다. 감사합니다.', '정답열쇠': '1~3일', '나눔': 'train'}2단계. 질문은 -100, 답만 채점하는 데이터 만들기
def encode(batch):
input_ids, labels = [], []
for q, a in zip(batch["질문"], batch["답변"]):
p = tok(prompt(q))["input_ids"] # 질문 부분의 토큰
r = tok(a + "<eos>")["input_ids"] # 답 부분의 토큰. 끝 표시를 붙인다
input_ids.append(p + r)
labels.append([-100] * len(p) + r) # 질문 자리는 채점하지 않는다
return {"input_ids": input_ids, "labels": labels}
sft_data = Dataset.from_list(train_rows).map(encode, batched=True, remove_columns=list(train_rows[0].keys()))
def collate(features):
"""길이가 다른 줄을 가장 긴 줄에 맞춰 채운다. 채운 자리의 labels 는 -100."""
L = max(len(f["input_ids"]) for f in features)
ids = torch.full((len(features), L), tok.pad_token_id)
lab = torch.full((len(features), L), -100)
att = torch.zeros((len(features), L), dtype=torch.long) # 1 인 자리만 진짜 토큰
for i, f in enumerate(features):
n = len(f["input_ids"])
ids[i, :n] = torch.tensor(f["input_ids"])
lab[i, :n] = torch.tensor(f["labels"])
att[i, :n] = 1
return {"input_ids": ids, "labels": lab, "attention_mask": att}
ex = sft_data[0]
print("토큰 수:", len(ex["input_ids"]), "/ 채점하는 자리:", sum(x != -100 for x in ex["labels"]))토큰 수: 60 / 채점하는 자리: 273단계. SFT 전의 답 보기
def answer(model, q):
"""템플릿으로 묻고, 답 부분만 돌려준다(탐욕적 선택)."""
enc = tok(prompt(q), return_tensors="pt").to(model.device)
with torch.no_grad():
out = model.generate(**enc, max_new_tokens=60, do_sample=False,
eos_token_id=tok.eos_token_id, pad_token_id=tok.pad_token_id)
return tok.decode(out[0][enc["input_ids"].shape[1]:], skip_special_tokens=True).strip()
pre = GPT2LMHeadModel.from_pretrained("out_pretrained")
if torch.cuda.is_available():
pre = pre.to("cuda")
for q in ["배송비 알려 주세요.", "환불 처리 기간을 알려 주세요."]:
print(f"[SFT 전] {q} → {answer(pre.eval(), q)}")[SFT 전] 배송비 알려 주세요. → 일 안에 신청할 수 있습니다.
[SFT 전] 환불 처리 기간을 알려 주세요. → 일 안에 신청할 수 있습니다.4단계. SFT 하고 시험 질문 채점하기
from transformers import Trainer, TrainingArguments
set_seed(42)
args = TrainingArguments("out_sft_ckpt", per_device_train_batch_size=16, num_train_epochs=30,
learning_rate=5e-4, logging_steps=50, save_strategy="no", report_to="none",
seed=42, fp16=torch.cuda.is_available(), disable_tqdm=True)
trainer = Trainer(model=GPT2LMHeadModel.from_pretrained("out_pretrained"), args=args,
train_dataset=sft_data, data_collator=collate)
trainer.train()
sft = trainer.model.eval()
# 학습에 쓰지 않은 시험 질문 24개: 답에 정답 열쇠가 들어 있는지 센다
hits = 0
for r in test_rows:
a = answer(sft, r["질문"])
ok = r["정답열쇠"] in a
hits += ok
print("O" if ok else "X", r["질문"], "→", a)
print(f"맞힌 수: {hits} / {len(test_rows)}")
sft.save_pretrained("out_sft_shop")
tok.save_pretrained("out_sft_shop"){'loss': '1.681', 'grad_norm': '1.555', 'learning_rate': '0.0003367', 'epoch': '10'}
{'loss': '0.193', 'grad_norm': '0.6793', 'learning_rate': '0.00017', 'epoch': '20'}
{'loss': '0.05335', 'grad_norm': '0.2223', 'learning_rate': '3.333e-06', 'epoch': '30'}
{'train_runtime': '12.42', 'train_samples_per_second': '173.9', 'train_steps_per_second': '12.08', 'train_loss': '0.6424', 'epoch': '30'}
O 배송이 며칠 걸리는지 알려 주세요. → 안녕하세요, 고객님. 결제 후 1~3일 안에 도착합니다. 제주와 섬 지역은 2일 더 걸립니다. 감사합니다.
X 언제 도착하는지 궁금합니다. → 안녕하세요, 고객님. 평일 오전 9시부터 오후 6시까지이고, 점심시간은 낮 12시부터 1시까지입니다. 감사합니다.
O 배송비 알려 주세요. → 안녕하세요, 고객님. 3만원 이상 주문하시면 무료이고, 그보다 적으면 3,000원입니다. 감사합니다.
O 택배비는 얼마예요? → 안녕하세요, 고객님. 3만원 이상 주문하시면 무료이고, 그보다 적으면 3,000원입니다. 감사합니다.
X 언제까지 교환할 수 있어요? → 안녕하세요, 고객님. 신용카드, 계좌이체, 간편결제를 쓰실 수 있습니다. 감사합니다.
O 교환 기간이 며칠이에요? → 안녕하세요, 고객님. 상품을 받은 날부터 7일 안에 신청하시면 됩니다. 감사합니다.
O 교환할 때 돈이 드나요? → 안녕하세요, 고객님. 상품 불량이면 쇼핑몰이 내고, 단순 변심이면 고객님이 왕복 6,000원을 내십니다. 감사합니다.
X 교환 배송비를 알려 주세요. → 안녕하세요, 고객님. 상품을 받은 날부터 7일 안에 신청하시면 됩니다. 감사합니다.
O 환불 언제 들어와요? → 안녕하세요, 고객님. 반품 상품이 창고에 도착한 뒤 3영업일 안에 돌려드립니다. 감사합니다.
O 환불 처리 기간을 알려 주세요. → 안녕하세요, 고객님. 반품 상품이 창고에 도착한 뒤 3영업일 안에 돌려드립니다. 감사합니다.
O 포인트로 결제했는데 환불은요? → 안녕하세요, 고객님. 포인트로 내신 금액은 포인트로 돌려드립니다. 감사합니다.
O 포인트 환불 방법을 알려 주세요. → 안녕하세요, 고객님. 포인트로 내신 금액은 포인트로 돌려드립니다. 감사합니다.
O 주문 취소 가능한가요? → 안녕하세요, 고객님. 배송 준비 중이 되기 전에는 마이페이지에서 바로 취소하실 수 있습니다. 감사합니다.
X 주문 취소를 알려 주세요. → 안녕하세요, 고객님. 결제 후 1~3일 안에 도착합니다. 제주와 섬 지역은 2일 더 걸립니다. 감사합니다.
O 주소 수정은 어떻게 해요? → 안녕하세요, 고객님. 출고 전이라면 고객센터에 연락 주시면 바꿔 드립니다. 감사합니다.
O 배송 주소를 바꾸는 방법을 알려 주세요. → 안녕하세요, 고객님. 출고 전이라면 고객센터에 연락 주시면 바꿔 드립니다. 감사합니다.
O 결제 수단을 알려 주세요. → 안녕하세요, 고객님. 신용카드, 계좌이체, 간편결제를 쓰실 수 있습니다. 감사합니다.
X 무엇으로 결제할 수 있나요? → 안녕하세요, 고객님. 포인트로 내신 금액은 포인트로 돌려드립니다. 감사합니다.
O 쿠폰 여러 개 적용되나요? → 안녕하세요, 고객님. 주문 하나에 쿠폰은 한 장만 쓰실 수 있습니다. 감사합니다.
O 쿠폰 몇 장 쓸 수 있는지 알려 주세요. → 안녕하세요, 고객님. 주문 하나에 쿠폰은 한 장만 쓰실 수 있습니다. 감사합니다.
O 적립금을 알려 주세요. → 안녕하세요, 고객님. 결제 금액의 1%가 쌓이고, 1년 뒤 사라집니다. 감사합니다.
X 얼마나 적립되나요? → 안녕하세요, 고객님. 결제 후 1~3일 안에 도착합니다. 제주와 섬 지역은 2일 더 걸립니다. 감사합니다.
O 상담원 연결 시간을 알려 주세요. → 안녕하세요, 고객님. 평일 오전 9시부터 오후 6시까지이고, 점심시간은 낮 12시부터 1시까지입니다. 감사합니다.
O 고객센터 몇 시에 끝나요? → 안녕하세요, 고객님. 평일 오전 9시부터 오후 6시까지이고, 점심시간은 낮 12시부터 1시까지입니다. 감사합니다.
맞힌 수: 18 / 24이 실습이 뜻하는 것
이 실습은 가게 글을 외운 새 직원에게 "질문 카드와 모범 답" 연습을 시키는 것과 같습니다. 연습 문제집과 답안 양식을 정하고(1단계), 질문 부분은 채점하지 않고 답 부분만 채점하는 답안지를 만들고(2단계), 연습 전에 질문을 던져 보고(3단계), 서른 바퀴 연습시킨 뒤 처음 보는 시험 질문 24개로 채점합니다(4단계). 여기서 답안 양식이 프롬프트 템플릿, 채점하지 않는 칸이 -100, 연습이 SFT 입니다. 비유와 다른 점은, 이 채점이 정답 열쇠 낱말이 답에 들어 있는지만 보는 기계 채점이라 사람 선생님처럼 뜻을 따지지 않는다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 데이터 | 학습용 72, 시험용 24 | 질문과 모범 답 72쌍으로 연습하고, 처음 보는 질문 24개로 시험합니다 |
| 1. 한 줄 | '정답열쇠': '1~3일' | 답에 이 낱말이 들어 있으면 맞힌 것으로 셉니다 |
| 2. 토큰 수 | 60, 채점하는 자리 27 | 한 쌍 60토큰 가운데 답 부분 27토큰만 채점합니다. 질문을 따라 쓰는 연습은 시키지 않습니다 |
| 3. SFT 전 | 두 질문 모두 "일 안에 신청할 수 있습니다." | ### 답변: 표시를 처음 본 모델이 쇼핑몰 글의 아무 조각이나 이어 썼습니다 |
| 4. 학습 손실 | 1.681 → 0.193 → 0.05335 | 10, 20, 30 에포크에서 잰 손실이 내려갔습니다. 연습 문제의 답을 거의 외웠다는 뜻입니다 |
| 4. 시험 답 | "안녕하세요, 고객님. … 감사합니다." | 24개 답이 모두 상담 말투가 되었습니다 |
| 4. 맞힌 수 | 18 / 24 | 처음 보는 질문의 75%를 맞혔습니다 |
| 4. 틀린 6개 | "언제 도착하는지 궁금합니다" → 상담 시간 | 말투는 맞는데 다른 질문의 답을 가져왔습니다. 학습 질문과 낱말이 다른 질문에서 주로 틀렸습니다 |
한 줄로 줄이면, 질문과 답 72쌍의 SFT 로 작은 GPT 가 상담 말투를 익혔고 처음 보는 질문 24개 가운데 18개를 맞혔다는 실습입니다. 틀린 답을 보면 질문의 뜻보다 겉의 낱말로 답을 고르고 있어, 많은 글로 사전학습한 큰 모델이 필요하다는 것을 알 수 있습니다. 같은 데이터를 큰 지시 모델에 LoRA(Low-Rank Adaptation, 낮은 계수 적응)로 가르치는 일은 5장에서 합니다. 맞힌 수는 정답 낱말이 들어 있는지만 본 것이고, 손실은 이 PC 에서 얻은 값입니다.
코드 설명
Dataset.from_list는 사전들의 목록으로 데이터를 만듭니다. jsonl 을 읽은rows를 그대로 넣었습니다.collate는 길이가 다른 줄들을 가장 긴 줄에 맞춰 채웁니다. 채운 자리는attention_mask를 0,labels를 -100 으로 두어 보지도, 채점하지도 않게 합니다.- SFT 전의 모델은 두 질문 모두에 "일 안에 신청할 수 있습니다."라고 답합니다. 사전학습만 한 모델은
### 답변:이라는 표시를 처음 보니, 쇼핑몰 글의 아무 문장 조각이나 이어 쓴 것입니다. SFT 뒤에는 모든 답이 "안녕하세요, 고객님. … 감사합니다."의 상담 말투로 바뀌었습니다. - 학습에 쓰지 않은 시험 질문 24개 가운데 18개를 맞혔습니다. 2단계 출력의 "토큰 60개 가운데 채점하는 자리 27개"가 답 부분입니다.
- 틀린 6개는 모두 말투는 바른데 다른 의도의 답을 그대로 가져왔습니다. "언제 도착하는지 궁금합니다"에 상담 시간을, "무엇으로 결제할 수 있나요"에 포인트 환불을 답했습니다. 학습 질문과 낱말이 크게 다른 질문("도착", "무엇으로")에서 주로 틀렸습니다. 작은 모델이 질문의 뜻보다 겉의 낱말로 답을 고르고 있다는 뜻이고, 수억 배 많은 글로 사전학습한 큰 모델이 필요한 까닭입니다. 실습에는 넣지 않았지만, 사전학습 없이 같은 크기의 빈 모델에 같은 설정으로 SFT 만 해 따로 돌려 보니 17개를 맞혀, 이 작은 실험에서 사전학습의 덕은 크지 않았습니다.