⚡ AI 인텔리전스 레이더
🧪 전문가반 · 코드로 만드는 AI 엔지니어

🏋️ 파트 8 · 모델을 직접 만든다

23회차 · 자체 모델을 만든다 — 파인튜닝·post-training (트랙 마무리) ★

백두산 (Doosan Baek) · 게시 2026-06-13 · 최종 수정 2026-07-10

이 회차에서 배우는 것

🎯 목표'기성 모델 쓰기'에서 '내 모델 만들기'로 — 자체 LLM 학습의 지도
🧱 데이터 구축서비스 로그 정제 + 합성 데이터로 학습셋 만들기
🎓 SFT우리 도메인·말투를 지도학습으로 주입
🧭 정렬(DPO)'좋은 답/나쁜 답' 쌍으로 취향 맞추기
🏆 RL(GRPO)검증가능 보상으로 추론력 끌어올리기
💾 LoRA·양자화작은 GPU로 큰 모델 튜닝(QLoRA)·서빙 경량화
🌐 분산학습FSDP·DeepSpeed·Megatron '맛보기'(선택)
📊 평가·벤치마크도메인+일반 지표(KMMLU 등)로 before/after
✅ 완료 기준작은 모델을 LoRA로 튜닝 → 평가 점수 X%→Y%

전문가반의 진짜 마지막 봉우리입니다. 22회차까지는 '완성된 남의 모델(Claude·GPT·오픈모델)을 코드로 잘 쓰는' 엔지니어였어요. 여기서부터는 그 모델 자체를 '내 데이터로 만드는' 쪽입니다 — 비싼 상용 모델을 우리말·우리 도메인에 특화된 자체 모델로 대체하는 일이죠. 이 회차의 목표는 두 가지: (1) AI 뉴스·기술 글에 자주 나오는 SFT·DPO·LoRA·양자화·분산학습이 각각 뭔지 '지도'를 그리고, (2) 작은 모델 하나를 실제로 내 데이터로 튜닝해 점수를 숫자로 확인하는 것.

🧭 언제 '모델을 직접 만드나' — 대부분은 안 만들어도 된다

모델 학습은 비싸고 오래 걸립니다. 그래서 순서가 중요해요 — 11회차의 'RAG vs 파인튜닝'을 다시 떠올리면, 항상 프롬프트 → RAG → 파인튜닝 순으로 시도하고, 앞 둘로 안 될 때만 모델을 건드립니다.

🗺️ 큰 그림 — 이미 똑똑한 모델에 '세 번 더' 가르친다 (post-training)

바닥부터 학습(pretraining, 인터넷을 통째로 읽히기)은 수조 토큰·수백~수천 장 GPU라 스타트업이 처음부터 하지 않습니다. 우리가 하는 일의 대부분은 '이미 똑똑한 오픈 모델(Llama·Qwen·Exaone 등)에 우리 것만 추가로 가르치는' post-training이에요. 2025~2026 실무는 이걸 세 개의 모듈로 쌓습니다.

🎓 ① SFT — 지도학습 파인튜닝 (가장 기본)

'입력 → 모범답' 쌍을 잔뜩 보여주며 따라하게 하는 것. 우리 도메인의 말투·형식·지식을 '추가 과외'시키는 단계입니다. 자체 모델을 만들 때 '데이터 구축 → 모델 학습'의 첫 칸이에요. 데이터 질이 결과의 8할 — 예시 500개가 잘 정제됐으면 5만 개 잡음보다 낫습니다.

말보다 예시가 빠릅니다. SFT 학습 데이터는 결국 아래처럼 '사용자 질문 → 우리가 원하는 모범답' 쌍의 모음이에요(JSONL — 한 줄이 예시 하나). 이런 걸 수백 개 모아 보여주면, 모델이 '아, 이 서비스는 이런 말투·형식으로 답하는구나'를 흉내 내게 됩니다.

# sft_data.jsonl — '입력→모범답' 쌍 모음 (한 줄 = 예시 하나)
{"messages": [
  {"role": "user",      "content": "AI Radar에 새 툴 'Sora'를 한 줄로 소개해줘."},
  {"role": "assistant", "content": "Sora — OpenAI의 텍스트→영상 생성 AI. 프롬프트만으로 최대 1분 영상 제작. [카테고리: 영상 생성]"}
]}
{"messages": [
  {"role": "user",      "content": "'fal.ai'는 어떤 도구야?"},
  {"role": "assistant", "content": "fal.ai — 이미지·영상 생성 모델을 API로 빠르게 돌리는 서버리스 추론 플랫폼. [카테고리: 인프라·애그리게이터]"}
]}
# 핵심은 '형식의 일관성' — 모든 모범답을 똑같은 틀(한 줄 요약 + [카테고리])로 맞추면
# 모델이 그 틀을 그대로 학습한다. (실제 학습 코드는 아래 '🔧 만들 것'의 SFTTrainer 참고)

그럼 이 데이터로 '학습한다'는 게 정확히 무슨 일일까요? LLM은 본질이 '다음 단어(토큰) 맞추기' 기계라는 걸 떠올리면(1회차), SFT는 그 성질을 그대로 이용하는 겁니다.

  1. ① 토큰화 — 각 대화(질문+모범답)를 토큰 열로 변환한다.
  2. ② 다음 토큰 예측 — 문장을 앞에서부터 보여주며, 모범답(assistant) 부분을 한 토큰씩 예측하게 한다.
  3. ③ 손실 계산 — 예측이 모범답과 다를수록 오차가 크다. 질문(user) 부분은 채점에서 제외 — '답하는 법'만 배우게.
  4. ④ 가중치 업데이트 — 오차가 줄어드는 방향으로 파라미터를 아주 조금씩 조정한다(LoRA면 어댑터만).
  5. ⑤ 반복(epoch) — 수백 개 예시를 몇 바퀴 돌면 '우리 형식으로 답하는 습관'이 가중치에 새겨진다.
# 위 5단계를 '개념 코드'로 — SFTTrainer.train() 안에서 벌어지는 일 (형태만 감 잡기용)
for epoch in range(3):                          # ⑤ 반복 — 데이터셋을 3바퀴
    for example in dataset:                     # 예시(대화) 하나씩
        tokens = tokenizer(example)             # ① 토큰화
        pred = model(tokens[:-1])               # ② 다음 토큰 예측
        loss = cross_entropy(pred, tokens[1:],  # ③ 오차 계산 —
                 mask=assistant_only)           #    질문(user) 부분은 채점 제외
        loss.backward()                         # ④ 오차를 거꾸로 전파해
        optimizer.step()                        #    가중치를 조금씩 조정

# 실전에선 이 루프 전체가 SFTTrainer.train() 한 줄. 그래도 '안에서 뭐가 도는지'는 이제 안다.

🧭 ② 선호 정렬 — DPO(그리고 RLHF)

SFT는 '맞는 답'은 가르쳐도 '더 나은 답 vs 못한 답'의 취향은 못 가르칩니다. RLHF는 사람 피드백으로 '보상 모델'을 만들어 강화학습으로 정렬하는데 — 강력하지만 무겁습니다(모델을 네 개나 동시에 메모리에 올림). DPO는 보상 모델 없이 '좋은 답/나쁜 답 쌍'만으로 비슷한 효과의 대부분을 절반 비용에 냅니다. 그래서 2025 실무의 사실상 기본값이에요.

예시로 보면 확실합니다. SFT 데이터가 '정답 하나'였다면, DPO 데이터는 같은 질문에 대한 '더 나은 답(chosen) / 못한 답(rejected)' 한 쌍이에요. 사람은 둘 중 나은 쪽만 골라주면 됩니다.

# dpo_pairs.jsonl — 같은 질문에 'chosen(더 나은 답)'과 'rejected(못한 답)'를 쌍으로
{"prompt": "AI Radar에 'Claude'를 한 줄로 소개해줘.",
 "chosen":   "Claude — Anthropic의 대화형 AI. 긴 문서 분석·코딩에 강함. [카테고리: 대화형 AI]",
 "rejected": "Claude는 그냥 챗봇이에요. 좋습니다."}
# chosen은 우리 형식([카테고리] 포함)을 지키고, rejected는 성의 없는 답.
# 이런 쌍 수백 개로 '무엇이 더 나은 답인지'를 각인 — 보상 모델 없이 바로 학습(DPO).

🏆 ③ RL — 검증가능 보상 (GRPO/RLVR)

수학·코드처럼 '정답을 기계가 채점할 수 있는' 문제는, 사람 취향 대신 '맞았나?'를 보상으로 씁니다(RLVR=검증가능 보상). GRPO가 추론에서 특히 강해요(DeepSeek-R1이 이 방식으로 유명). 2025의 가장 큰 변화가 바로 이것 — 값비싼 '사람 라벨'에서 자동 채점되는 '검증가능 보상'으로의 이동입니다.

여기선 '데이터'가 아니라 '채점 규칙(보상 함수)'이 핵심입니다. 정답을 기계로 매길 수 있으면, 사람 라벨 없이 모델이 스스로 점수를 높이는 방향으로 학습해요.

# GRPO/RLVR의 핵심은 '보상 함수' — 사람 취향 대신 기계가 점수를 매긴다
def reward(answer: str, expected_category: str) -> float:
    score = 0.0
    if expected_category in answer:                      # ① 카테고리를 맞혔나
        score += 0.7
    if answer.count('[') == 1 and answer.endswith(']'):  # ② 우리 형식([카테고리])을 지켰나
        score += 0.3
    return score          # 0.0~1.0 — 모델은 이 점수가 커지는 방향으로 스스로 학습

# 채점이 자동이라(형식 준수·수학 정답·코드 실행 결과 등) 사람 라벨 없이 대량 반복 가능.

💾 현실적으로 — LoRA·QLoRA·양자화 (작은 GPU로)

큰 모델 '전체'를 다시 학습하면 GPU가 수십 장 필요합니다. LoRA는 큰 가중치는 얼려두고 '작은 어댑터 두 장'만 학습해요 — 저장·학습 비용이 수백 배 쌉니다. QLoRA는 여기에 4비트 양자화를 더해, 65B급 모델을 48GB GPU 한 장에서도 튜닝할 수 있게 합니다. 양자화(GPTQ·AWQ)는 '16비트를 4비트로 줄여' 서빙 메모리·속도를 확보하는 기술이고요(품질은 거의 유지).

# 왜 싼가 — 모델 '전체'가 아니라 '작은 어댑터'만 학습하기 때문
#   full fine-tuning : 학습 대상 ≈ 70억 개 파라미터 전부   (GPU 수십 장)
#   LoRA (r=16)      : 학습 대상 ≈ 수백만 개, 전체의 0.1~0.5%  (GPU 1장)

from transformers import BitsAndBytesConfig      # QLoRA = 4비트 로드 + LoRA
bnb = BitsAndBytesConfig(load_in_4bit=True)       # 원본을 4비트로 압축해 GPU에 올림
# → 결과물(어댑터)은 수 MB라 갈아끼우기 쉽고, 48GB GPU 한 장에 65B급도 튜닝 가능.

🧱 데이터가 8할 — 구축·합성·품질검증

모델 종류보다 데이터 품질이 결과를 가릅니다(자체 모델을 만드는 일에서 '데이터 구축'이 가장 앞에 오는 이유예요). 서비스 로그를 정제해 학습셋으로 만들고, 부족하면 강한 teacher 모델로 '합성 데이터'를 찍어 채웁니다. 단, 합성 데이터는 품질 검증이 생명 — 나쁜 합성은 모델을 조용히 망칩니다.

📊 평가·벤치마크 — 22회차 eval의 '모델판'

튜닝이 진짜 나아졌는지 숫자로 봐야 합니다. 22회차에서 만든 eval 하니스를 그대로 확장하면 돼요 — 도메인 지표(우리 문제) + 일반 능력(한국어는 KMMLU·KMMLU-Hard 등)을 같이 재서, '우리 문제는 올랐는데 일반 능력은 까먹지 않았나'까지 봅니다. 자체 모델을 만드는 일의 세 번째 축이 바로 이 '평가 벤치마킹'이에요.

🌐 규모를 키울 때 — 분산학습 '맛보기'(선택)

GPU 한 장을 넘어서면 여러 장·여러 머신에 나눠 학습합니다. 지금은 세 도구의 '역할'만 알면 충분해요.

🔧 만들 것 — 작은 모델을 내 데이터로 LoRA 튜닝 (캡스톤)

이 트랙 전체의 마지막 실습입니다. 작은 오픈 모델(1~3B급) + 우리 도메인 예시 수백 개 + LoRA로 튜닝하고, 22회차 eval로 before/after 점수를 냅니다. 로컬이 버거우면 클라우드 GPU 1장(Colab 등)이나 관리형 파인튜닝 API로 대체해도 '데이터→학습→평가' 경험은 똑같이 남습니다.

  1. ① 데이터 — '입력→모범답' 50~500쌍을 JSONL로(20~22회차에서 모은 도메인 예시 재활용).
  2. ② 학습 — HF Transformers + PEFT(LoRA)로 SFT 1 epoch. 작게 시작해 과적합 감시.
  3. ③ 로드·추론 — 어댑터를 저장하고 원본 위에 얹어 몇 문장 돌려본다.
  4. ④ 평가 — 22회차 골든셋으로 튜닝 전/후 점수표(before X% → after Y%)를 만든다.
  5. ⑤ (선택) DPO 한 스텝 — '좋은 답/나쁜 답' 쌍 몇 개로 선호 정렬을 맛본다.
# 개념 스켈레톤 — TRL의 SFTTrainer + LoRA (전체가 아니라 '형태'만 감 잡기용)
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTTrainer, SFTConfig

# 1) 데이터: {"prompt": "...", "completion": "..."} 형태의 JSONL
data = load_dataset('json', data_files='train.jsonl', split='train')

# 2) LoRA: 원본은 얼리고 '작은 어댑터'만 학습
lora = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, task_type='CAUSAL_LM')

# 3) 학습: 작은 오픈 모델 위에 SFT 1 epoch
trainer = SFTTrainer(
    model='Qwen/Qwen2.5-1.5B-Instruct',   # 작게 시작 (1~3B급)
    train_dataset=data,
    peft_config=lora,
    args=SFTConfig(num_train_epochs=1, per_device_train_batch_size=2,
                   learning_rate=2e-4, output_dir='out'),
)
trainer.train()          # → out/ 에 LoRA 어댑터 저장
# 이후: 원본+어댑터 로드해 추론 → 22회차 eval로 before/after 채점

✅ 완료 기준 (숫자로 닫기)

🧭 이 회차 ↔ 실제 현업 — 자체 모델을 만드는 일

📚 자료

📌 23회차 핵심

🗺️ 전문가반 한 바퀴 — 코드로 쓰고, 이제 모델까지