포스트

2026-05-20 강의 정리

언어모델 파인튜닝 2

2026-05-20 강의 정리

목차

  1. 문제와 접근
  2. train/validation split
  3. k-fold validation 개념
  4. JSONL 파일 업로드
  5. fine-tuning job 생성
  6. job 조회와 이벤트 확인
  7. payload 검증 함수
  8. 파인튜닝 모델 사용
  9. PoliteRewriter 캐시 래퍼
  10. HuggingFace Qwen LoRA 실습
  11. 핵심 개념 총정리
  12. 자주 나오는 실수/주의사항
  13. 학습 완료 이후에 남겨야 할 비교

1. 문제와 접근

파인튜닝 실행과 효과 확인은 별도의 단계로 나누어 본다. 정제한 데이터를 분리하고 학습 작업을 생성한 뒤, 학습에 사용하지 않은 입력에서 기본 모델과 비교할 준비를 한다. API 기반 학습과 LoRA 예제는 실행 방식이 다르므로 환경과 결과를 각각 기록한다.

1
2
3
4
5
6
7
8
9
import json
import random
from pathlib import Path
from openai import OpenAI
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage

oai = OpenAI()
WORK_DIR = Path("./260519_ft")

2. train/validation split

무작위 분할 전에 원문과 증강본의 관계를 확인한다. 같은 문장에서 파생한 샘플이 train과 validation에 나뉘면 새로운 입력에 대한 성능을 과대평가할 수 있어, 가능한 경우 원본 그룹 단위로 분리한다.

검증 데이터를 분리해서 학습 중 성능을 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
def split_jsonl(in_path, train_path, val_path, val_ratio=0.2, seed=42):
    rows = [json.loads(l) for l in open(in_path, encoding="utf-8")]
    random.Random(seed).shuffle(rows)

    n_val = max(1, int(len(rows) * val_ratio))
    val, train = rows[:n_val], rows[n_val:]

    for path, data in [(train_path, train), (val_path, val)]:
        with open(path, "w", encoding="utf-8") as f:
            for row in data:
                f.write(json.dumps(row, ensure_ascii=False) + "\n")

    return len(train), len(val)
1
2
3
4
state.train_path = WORK_DIR / "train.jsonl"
state.val_path = WORK_DIR / "val.jsonl"

n_tr, n_vl = split_jsonl(clean_path, state.train_path, state.val_path)

3. k-fold validation 개념

데이터가 적을 때는 여러 번 train/validation 분할을 바꿔 평가할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def kfold_indices(n, k, seed=42) -> list[tuple[list[int], list[int]]]:
    if k > n:
        raise ValueError(f"k({k}) > n({n})")

    idx = list(range(n))
    random.Random(seed).shuffle(idx)
    folds = []

    for i in range(k):
        val = idx[i::k]
        train = [x for x in idx if x not in set(val)]
        folds.append((train, val))

    return folds

4. JSONL 파일 업로드

OpenAI fine-tuning용 파일로 업로드한다.

1
2
3
4
def upload_jsonl(path):
    with open(path, "rb") as f:
        result = oai.files.create(file=f, purpose="fine-tune")
    return result.id
1
2
state.train_file_id = upload_jsonl(state.train_path)
state.val_file_id = upload_jsonl(state.val_path)

5. fine-tuning job 생성

기본 모델, train 파일, validation 파일, suffix를 지정해 job을 만든다.

1
2
3
4
5
6
7
8
9
10
11
BASE_MODEL = "gpt-4o-mini-2024-07-18"

job = oai.fine_tuning.jobs.create(
    training_file=state.train_file_id,
    validation_file=state.val_file_id,
    model=BASE_MODEL,
    suffix="polite-email",
    hyperparameters={"n_epochs": "auto"},
)

state.job_id = job.id

6. job 조회와 이벤트 확인

job 상태와 학습 이벤트를 확인한다.

1
2
j = oai.fine_tuning.jobs.retrieve(state.job_id)
j.id, j.status, j.model, j.trained_tokens
1
2
3
4
5
6
7
events = oai.fine_tuning.jobs.list_events(
    fine_tuning_job_id=state.job_id,
    limit=50,
)

for e in events.data:
    print(e.created_at, e.message)

7. payload 검증 함수

job 생성 전 필수 값과 suffix 길이를 검증한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def build_job_payload(state, base_model, suffix, n_epochs="auto") -> dict:
    if not state.train_file_id:
        raise ValueError("train 파일 미업로드")
    if len(suffix) > 20:
        raise ValueError("suffix length")

    payload = {
        "model": base_model,
        "training_file": state.train_file_id,
        "suffix": suffix,
        "hyperparameters": {"n_epochs": n_epochs},
    }
    if state.val_file_id:
        payload["validation_file"] = state.val_file_id

    return payload

8. 파인튜닝 모델 사용

학습이 완료된 모델을 호출할 수 있다는 사실만으로 개선을 확인한 것은 아니다. 동일한 입력을 기본 모델과 학습 모델에 전달하고, 정중함뿐 아니라 원문 조건의 보존과 출력 형식도 같은 기준으로 비교한다.

학습이 끝나면 ft:로 시작하는 모델명을 사용한다.

1
2
3
4
5
6
7
8
state.ft_model = "ft:gpt-4o-mini-2024-07-18:ai-data-short-term:polite-email:Dh4tr9SQ"

llm_ft = ChatOpenAI(model=state.ft_model)

llm_ft.invoke([
    SystemMessage(content=SYSTEM_MSG),
    HumanMessage(content="회의 내일로 미뤄"),
])

기본 모델과 파인튜닝 모델의 답변을 같은 입력으로 비교한다.

1
2
3
4
llm.invoke([
    SystemMessage(content=SYSTEM_MSG),
    HumanMessage(content="회의 내일로 미뤄"),
])

9. PoliteRewriter 캐시 래퍼

같은 입력이 반복되면 캐시에서 재사용하는 래퍼를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PoliteRewriter:
    def __init__(self, model_name):
        self.model_name = model_name
        self._cache: dict[str, str] = {}
        self.hits = 0
        self.misses = 0

    def rewrite(self, text: str) -> str:
        if text in self._cache:
            self.hits += 1
            return self._cache[text]

        self.misses += 1
        llm = ChatOpenAI(model=self.model_name)
        out = llm.invoke([
            SystemMessage(content=SYSTEM_MSG),
            HumanMessage(content=text),
        ]).content
        self._cache[text] = out
        return out

    def batch(self, texts: list[str]) -> list[str]:
        return [self.rewrite(t) for t in texts]

10. HuggingFace Qwen LoRA 실습

보조 노트북에서는 Qwen instruct 모델을 LoRA로 학습한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model
from datasets import load_dataset
import torch

MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    dtype=torch.bfloat16,
    device_map="auto",
)
1
2
3
4
5
6
7
8
9
10
ds = load_dataset("json", data_files={
    "train": str(TRAIN_PATH),
    "validation": str(VAL_PATH),
})

print(tokenizer.apply_chat_template(
    ds["train"][0]["messages"],
    tokenize=False,
    add_generation_prompt=False,
))
1
2
3
4
5
6
7
8
9
10
11
lora_cfg = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)

model = get_peft_model(model, lora_cfg)
model.print_trainable_parameters()

11. 핵심 개념 총정리

단계역할
split_jsonltrain/validation 분리
k-fold데이터가 적을 때 반복 평가
files.createfine-tuning용 파일 업로드
jobs.create파인튜닝 job 생성
jobs.retrievejob 상태 조회
jobs.list_events학습 이벤트 확인
ft model학습 완료 모델 사용
LoRA일부 adapter 파라미터만 학습

12. 자주 나오는 실수/주의사항

실수 1: 파일 목록 필터 변수명 오타

1
2
3
4
5
# 틀린 코드
getattr(F, "purpose", "")

# 올바른 코드
getattr(f, "purpose", "")

실수 2: LangChain 메시지 클래스만 넘기기

1
2
3
4
5
6
7
8
# 틀린 코드
llm.invoke([SystemMessage, HumanMessage])

# 올바른 코드
llm.invoke([
    SystemMessage(content=SYSTEM_MSG),
    HumanMessage(content=text),
])

실수 3: suffix 길이 초과

1
2
OpenAI fine-tuning suffix는 짧고 식별 가능하게 둔다.
예: polite-email

실수 4: HuggingFace 학습에서 device 미정의

1
device = "cuda" if torch.cuda.is_available() else "cpu"

13. 학습 완료 이후에 남겨야 할 비교

작업 ID와 모델 ID를 학습 데이터 버전 및 설정에 연결해 둔다. 어떤 결과가 어느 데이터에서 나왔는지 확인할 수 있어야 데이터 정제나 학습 설정을 바꾼 효과를 설명할 수 있다.

개선 여부는 익숙한 표현과 처음 보는 표현을 나누어 확인한다. 검증 손실과 실제 생성 문장의 평가를 함께 보고, 기존 프롬프트만 사용했을 때와 비교해 학습 과정을 유지할 이유가 있는지 판단한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그