학습 가이드 (내 데이터로 모델 기르기)
검증에서 나온 확정 정답으로 자체 모델을 파인튜닝해서, 파이프라인이 회사 도메인에 점점 더 정확해지게 만드는 방법. 왜 학습하는지 → 어떤 자리를 학습하는지 → 데이터를 어디에 넣는지 → 학습된 모델을 어떻게 다시 꽂는지, 전체 흐름을 다룹니다.
01 왜 학습하나
StructVerify는 학습 없이도 클라우드 LLM으로 동작합니다. 그런데도 학습 루프를 두는 이유는 세 가지:
02 모델이 쓰이는 자리 지도
파이프라인에서 LLM이 호출되는 자리는 7곳이고, 각 자리는 성격에 따라 모델 티어
(heavy 복잡 추론 · light 빠른 분류 · structured JSON 구조화)에 매핑돼 있습니다.
이 중 학습 대상은 3곳, 회사 도메인에 특화될수록 이득이 커지는 자리들입니다.
| 자리 | 하는 일 | 티어 | 학습 |
|---|---|---|---|
| 도메인 분류 | 문서가 어느 도메인인지 | light | 범용 능력으로 충분 |
| 주장 탐지 | 검증할 가치가 있는 수치 문장 선별 | light/heavy | 프로파일 주입으로 적응 |
| 스키마 추출 | 주장 → {지표, 값, 단위, 시점} 구조화 | structured | 학습 task: schema 회사 용어·한국어 숫자 표기 각인 |
| 검색·랭킹·판별 | 후보 검색어 재구성, 관련도 판별, 행 매칭 | light | 임베딩+중립 프롬프트로 커버 |
| 에이전틱 SQL | 스키마 조사 → 검증용 집계 SQL 생성 | heavy | 학습 task: sql 회사 테이블 구조·지표 공식 각인 |
| 판정 | 주장 vs 근거 → match/mismatch + 근거문 | structured | 학습 task: verdict 허용오차·판정 톤 각인 |
| 설명 생성 | 판정 근거를 자연어 리포트로 | heavy | 범용 작문 능력 |
task 필드(schema/sql/verdict)가 붙어 있고, 시스템 프롬프트가 태스크별로 달라서 한 모델이
세 역할을 구분해 수행합니다. 자리별로 어댑터를 따로 만들 필요가 없습니다.03 학습 데이터 (어디서 얻고 어디에 넣나)
데이터는 네 군데서 나오고, 전부 LearningLoop에 체이닝으로 넣습니다:
from structverify.training import LearningLoop loop = LearningLoop(engine, base_model="unsloth/Qwen2.5-7B-Instruct") loop.add_seed() # ① 내장 시드 — 한국어 숫자 파싱·SQL 패턴·판정 톤 (29건) loop.add_reports(reports) # ② 검증 운영에서 나온 Report들 — 확정 정답이 핵심 재료 loop.add_jsonl("corrections.jsonl") # ③ 사람 교정·직접 만든 예시 (아래 포맷) # ④ 합성(증류): 강한 클라우드 모델로 회사 지표에 맞는 예시 대량 생성 from structverify.training import generate_dataset generate_dataset(cfg["llm"], out="gen.jsonl", domain="이커머스", indicators=["매출","고객수","평균주문금액"], n=3000) loop.add_jsonl("gen.jsonl")
파일 포맷 (chat jsonl (한 줄 = 예시 하나))
표준 chat 포맷이라 TRL·unsloth·axolotl 등 어디에나 그대로 먹일 수 있습니다.
경로는 아무데나, add_jsonl()에 경로만 주면 됩니다:
{"task": "verdict", "messages": [
{"role": "system", "content": "너는 주장과 근거 데이터를 대조해 사실 여부를 판정하는 검증관이다."},
{"role": "user", "content": "주장: \"누적 고객 수는 150만 명입니다.\"\n근거 데이터: 1500000 명 …"},
{"role": "assistant", "content": "{\"verdict\": \"match\", \"reason\": \"주장 150만 명이 공식 수치와 일치합니다.\"}"}
]}
직접 만들 땐 build_example(task, ...) 헬퍼가 이 포맷을 만들어 주고,
write_jsonl(rows, path)로 저장합니다. 정답 표(CSV/DB)가 있으면 match/mismatch 쌍을
프로그램으로 대량 생성하는 것도 흔한 패턴입니다.
넣은 다음: DataCurator가 거릅니다
ds, curation = loop.prepare("train.jsonl") # [DataCurator] 총 10038 → 통과 9987 · 격리 51 # 태스크 분포(통과): schema 3302, sql 7, verdict 6678 # 격리(중복): "시장 점유율은 16.0%입니다." … ← 왜 뺐는지 사유가 남음
중복·라벨 오류·편중 샘플은 자동 격리되고 사유가 주석으로 남습니다. 통과분만
train.jsonl로 저장, 이 파일이 학습 입력입니다.
04 어떻게 학습되나
LoRA 어댑터 방식입니다. 7B 베이스 모델은 그대로 두고, 위에 작은 어댑터(전체 파라미터의 ~0.5%)만 회사 데이터로 학습합니다. 그래서 RTX 3060 12GB / Colab T4급이면 충분하고, 산출물도 어댑터 폴더 하나(수십 MB)입니다.
# backend 자동 감지: NVIDIA → QLoRA(unsloth) · Apple Silicon → MLX loop.train("train.jsonl", "./adapter", backend="auto", run=True) # 학습 중 — 실시간 감독이 터미널 한 줄로 상태를 보여주고, 수렴/발산이면 스스로 멈춤 # 336/2496 ▓░░░░░░░ 13% · loss 0.108 ▆█▄▄▁▁ · ETA 2:29 · 1 · 수렴 완료 # step 336: 200스텝 동안 이동평균 개선 없음. 더 학습해도 이득이 없어 조기 종료합니다. loop.diagnose("./adapter/trainer_state.json") # 사후 진단 — 발산·overfit·스파이크 리포트
05 학습된 모델 주입 (어댑터를 파이프라인에 꽂기)
학습 결과(./adapter)를 실제 검증에 쓰는 순서는 서빙 → 주입 → 채택 검증 3단계입니다.
① 어댑터 서빙 (OpenAI 호환 엔드포인트로)
# Ollama (간단) — 베이스에 어댑터를 얹은 모델 등록 # Modelfile: FROM qwen2.5:7b-instruct / ADAPTER ./adapter ollama create sv-tuned -f Modelfile && ollama serve # vLLM (운영) — LoRA 동적 장착 vllm serve Qwen/Qwen2.5-7B-Instruct --enable-lora --lora-modules sv-tuned=./adapter
② config 주입: base_url + 모델 이름
import structverify as sv cfg = sv.build_config(provider="upstage", api_key="none", data=data) # OpenAI 호환 경로 재사용 cfg["llm"]["base_url"] = "http://192.168.0.10:11434/v1" # 회사 GPU 서버 cfg["llm"]["models"] = {"structured": "sv-tuned"} # 티어별 부분 주입 tuned_engine = sv.Verifier(config=cfg)
llm.models는 티어 단위로 덮어쓸 수 있으므로,
학습 태스크가 커버하는 자리(structured = 스키마 추출·판정, heavy = 에이전틱 SQL)에만
자체 모델을 꽂고, 나머지 자리는 기존 클라우드 모델을 유지하는 점진 전환이 됩니다.
전부 자체 모델로 전환하려면 세 티어를 모두 sv-tuned로 지정하면 됩니다.③ EvalGate (채택은 증명 후에)
gate = loop.evaluate(eval_set, tuned_engine=tuned_engine) # EvalGate — 채택 # before: 정확도 82.0% (41/50) ← 기존 클라우드 엔진 # after : 정확도 91.0% (45/50) ← 학습 모델 주입 엔진 # Δ정확도 +9.0% — 개선 확인, 채택 if gate.accepted: engine = tuned_engine # 다음 검증부터 학습된 모델 사용. 나빠졌으면 자동 거부(회귀 방지)
평가셋은 라벨이 있는 문장 목록([{"text": "...", "expected": "match"}, ...])이며,
검증 엔진 자체가 채점자라 별도 벤치마크 구축이 필요 없습니다. "학습했더니 검증이 실제로 좋아졌나"를
자기 자신으로 증명하는 구조.
06 전체 루프 정리
| 단계 | 하는 일 | API | 필요 자원 |
|---|---|---|---|
| 수집 | 검증 Report·교정·시드·합성 데이터 모으기 | add_reports / add_jsonl / add_seed / generate_dataset | GPU 불필요 |
| 준비 | 품질검사 + clean 데이터셋 저장 | loop.prepare() | GPU 불필요 |
| 학습 | LoRA 어댑터 학습 + 실시간 감독·조기 종료 | loop.train(backend="auto") | NVIDIA([training]) 또는 Apple Silicon([training-mac]) |
| 진단 | loss 곡선 사후 진단 + 처방 | loop.diagnose() | GPU 불필요 |
| 주입 | 어댑터 서빙 → 티어별 config 주입 | cfg["llm"]["base_url" / "models"] | 서빙 GPU (7B: 12GB급) |
| 채택 | 학습 전/후 비교 → 채택/거부 | loop.evaluate(tuned_engine=) | GPU 불필요 |
StructVerify v0.3.4 · MIT License. 학습 API의 상세 시그니처는 레퍼런스의 학습 API 절을, 단계별 실행 흐름은 사용법의 학습 루프 절을 참조하십시오.