학습 가이드 (내 데이터로 모델 기르기)

검증에서 나온 확정 정답으로 자체 모델을 파인튜닝해서, 파이프라인이 회사 도메인에 점점 더 정확해지게 만드는 방법. 왜 학습하는지 → 어떤 자리를 학습하는지 → 데이터를 어디에 넣는지 → 학습된 모델을 어떻게 다시 꽂는지, 전체 흐름을 다룹니다.

① 검증 운영→② 확정 정답 수집→③ 데이터셋 준비 → ④ 학습 →⑤ 자가평가 →⑥ 모델 주입→↺ ①로

01 왜 학습하나

StructVerify는 학습 없이도 클라우드 LLM으로 동작합니다. 그런데도 학습 루프를 두는 이유는 세 가지:

회사 도메인 정확도범용 모델은 "순매출=할인 반영", "1,500만 = 15,000,000", 우리 테이블 스키마 같은 회사 고유의 용어·표기·구조를 모릅니다. 확정 정답으로 학습하면 추출·SQL·판정이 그 도메인에 특화됩니다.
비용·속도파이프라인은 문서 하나에 LLM을 수십 번 호출합니다. 특화된 작은(7B) 자체 모델이 그 자리를 대신하면 호출당 비용이 0이 되고, 온프렘 GPU에서 즉시 응답합니다.
데이터 주권 + 자가 개선회사 데이터를 클라우드로 안 보내고 검증하고, 검증할수록 정답이 쌓여 모델이 스스로 좋아지는 루프가 됩니다. 이 라이브러리만의 구조적 이점.

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·스파이크 리포트
감독 3종이 학습 방법과 무관하게 항상 붙습니다, DataCurator(데이터 품질) · TrainDoctor(실시간 이상 감지 + 조기 종료) · EvalGate(학습 전/후 자가평가). "그냥 파인튜닝 스크립트"가 아니라 감독되는 학습 루프인 게 차별점입니다.

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_datasetGPU 불필요
준비 품질검사 + 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 절을, 단계별 실행 흐름은 사용법의 학습 루프 절을 참조하십시오.