본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로AI › 언어 모델 › 응용

6. sLLM 학습하기

목차

sLLM 학습하기

작은 언어 모델(sLLM)을 학습한다는 말은 처음부터 모든 지식을 학습시키는 것과, 이미 학습된 모델을 특정 작업에 맞게 미세 조정하는 것을 모두 가리킬 수 있다. 대부분의 작은 팀에는 후자가 현실적인 출발점이다. 이 장에서는 “고객 문의를 한 문장으로 분류하고 이유를 설명하는 모델”을 예로 들어 데이터부터 평가까지 연결한다.

flowchart LR
    A[과제와 성공 기준] --> B[기본 모델·라이선스 확인]
    B --> C[학습/검증/시험 데이터 분리]
    C --> D[베이스라인 측정]
    D --> E[LoRA 기반 SFT]
    E --> F[미사용 시험셋 평가]
    F --> G{기준 충족?}
    G -->|아니오| C
    G -->|예| H[어댑터·토크나이저·설정 보관]

1. 학습보다 먼저 과제를 좁히기

“우리 회사에 맞는 AI”는 평가하기 어렵다. 대신 문의 → 배송/교환/결제/기타 중 하나 + 한 문장 이유처럼 입력과 출력 형식을 정한다. 분류 문제라면 일반 생성 모델을 미세 조정하기 전에 프롬프트만 적용한 기본 모델, 또는 분류 전용 모델을 기준으로 측정한다. 미세 조정이 반드시 이득인 것은 아니다.

기본 모델을 고를 때는 한국어 지원, 문맥 길이, 사용 허가 범위, 추론 장치의 메모리, 채팅 템플릿을 확인한다. 모델 카드는 사용 조건과 알려진 한계를 확인하는 출발점이다. 모델을 바꾸면 같은 학습 코드를 써도 토크나이저와 채팅 형식이 달라질 수 있다.

2. 좋은 데이터가 작은 모델의 범위를 정한다

아래 데이터는 형식을 보여주기 위한 예시다. 실제 학습에는 다양한 표현과 예외 사례를 수집해야 한다.

{
  "messages": [
    {"role": "user", "content": "결제는 됐는데 주문 내역이 보이지 않아요. 범주와 이유를 알려 주세요."},
    {"role": "assistant", "content": "범주: 결제\n이유: 결제 완료 후 주문 내역 확인에 문제가 있습니다."}
  ]
}

학습·검증·시험 데이터는 고객이나 사건 단위로 분리한다. 같은 문의를 표현만 바꿔 양쪽에 넣으면 시험 점수가 부풀려질 수 있다. 레이블 규칙을 문서로 정하고, 배송과 결제가 동시에 등장하는 사례에서 어느 범주를 택할지도 먼저 결정한다. 개인 정보는 제거하고, 학습 데이터 사용 권한도 확인한다.

3. LoRA로 미세 조정하기

LoRA(Low-Rank Adaptation)는 기본 모델의 모든 가중치를 업데이트하는 대신, 일부 선형층에 작은 학습 가능한 행렬을 붙인다. 계산 자원이 제한적일 때 유용하지만, 데이터 품질이 나쁘면 적은 매개변수로도 잘못된 패턴을 배운다. PEFT LoRA 문서와 TRL SFT 문서의 설정을 확인한다.

다음 코드는 이미 train.jsonl과 valid.jsonl을 준비했다는 전제의 구성 예시다. 각 줄은 위와 같은 messages 필드를 갖는다. 모델과 라이브러리 버전에 따라 채팅 템플릿과 지원 옵션을 확인해야 한다.

from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer

model_id = "Qwen/Qwen2.5-0.5B-Instruct"
data = load_dataset("json", data_files={
    "train": "train.jsonl",
    "validation": "valid.jsonl",
})

trainer = SFTTrainer(
    model=model_id,
    train_dataset=data["train"],
    eval_dataset=data["validation"],
    peft_config=LoraConfig(
        r=16,
        lora_alpha=32,
        lora_dropout=0.05,
        target_modules="all-linear",
        task_type="CAUSAL_LM",
    ),
    args=SFTConfig(
        output_dir="./customer-support-adapter",
        per_device_train_batch_size=2,
        gradient_accumulation_steps=8,
        learning_rate=2e-4,
        num_train_epochs=2,
        eval_strategy="epoch",
        save_strategy="epoch",
    ),
)
trainer.train()
trainer.save_model()

target_modules="all-linear"는 모델의 선형층 전반에 어댑터를 붙이는 방법이다. 메모리 예산이 부족하면 학습 대상 층이나 입력 길이를 조정한다. 학습률과 epoch 수는 정답이 아니라 검증셋으로 비교할 시작값이다. 학습이 끝난 어댑터는 기본 모델과 함께 사용하므로, 배포할 때 기본 모델의 정확한 리비전도 기록한다.

4. 무엇을 평가할까

  • 분류 정확도와 범주별 재현율: 기타만 잘 맞히는 모델을 가려낸다.
  • 형식 준수율: 범주:와 이유:가 파싱 가능한 형태로 나오는가?
  • 근거 충실도: 이유가 입력에 없는 사실을 덧붙이지 않는가?
  • 회귀 사례: 기본 모델이 맞혔던 사례를 학습 후 틀리는가?
  • 운영 비용: 예상 입력 길이에서 지연 시간과 메모리가 허용 범위인가?

시험셋은 모델과 설정을 고르는 동안 손대지 않는 편이 좋다. 오류를 분석할 때는 “데이터가 부족하다”로만 끝내지 않고, 레이블 충돌·전처리 오류·모델 용량·프롬프트 불일치 중 어느 문제인지 사례를 모아 확인한다. 이 과정을 거쳐야 추가 학습이 필요한지, 단순히 출력 형식을 고치면 되는지 판단할 수 있다.

한 건이 학습 입력으로 바뀌는 과정

앞의 messages 예시를 모델에 그대로 전달하는 것은 아니다. 토크나이저의 채팅 템플릿은 user와 assistant 역할, 메시지 종료 표시를 포함한 문자열을 만들고, 다시 토큰 ID로 변환한다. 학습에서 어떤 토큰에 손실을 줄지도 결정해야 한다. 전체 대화에 손실을 주면 모델이 사용자 질문까지 생성하도록 학습한다. 답변 형식을 배우게 하려면 assistant 응답 토큰에 집중하는 편이 자연스럽다. 다만 TRL의 assistant_only_loss=True는 채팅 템플릿이 assistant 영역을 식별할 수 있어야 작동한다. 지원 여부를 확인하지 않고 옵션만 켜면 기대와 다른 학습이 될 수 있다. TRL의 데이터 형식과 손실 설명을 참고한다.

첫 학습에서는 긴 문서와 짧은 문의를 한꺼번에 넣기보다 토큰 길이 분포를 본다. 설정한 최대 길이보다 긴 사례가 많다면 답변 말미의 레이블이 잘릴 수 있다. 이때 epoch을 늘려도 잘린 정답은 배울 수 없다. 문장 길이별 잘림 비율을 기록하고, 지나치게 긴 사례는 요약 과제로 분리하거나 최대 길이를 조정한다.

저장된 어댑터로 추론하기

학습 코드의 save_model()은 LoRA 어댑터를 저장한다. 추론할 때 기본 모델과 어댑터가 연결돼야 하며, 학습 때 사용한 채팅 형식을 다시 적용한다. 다음 코드는 학습 결과를 확인하는 호출 형태다. 모델을 실행해 얻은 답변을 이 글에서 주장하는 것은 아니다.

from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer

adapter_dir = "./customer-support-adapter"
model = AutoPeftModelForCausalLM.from_pretrained(adapter_dir, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")

messages = [{
    "role": "user",
    "content": "결제는 됐는데 주문 내역이 보이지 않아요. 범주와 이유를 알려 주세요.",
}]
inputs = tokenizer.apply_chat_template(
    messages, add_generation_prompt=True, return_tensors="pt"
).to(model.device)
outputs = model.generate(inputs, max_new_tokens=80, do_sample=False)
answer = tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True)
print(answer)

outputs[0]에는 입력 토큰도 포함될 수 있어 입력 길이만큼 잘라 새로 생성한 부분을 읽는다. do_sample=False로 같은 설정의 비교를 쉽게 만들었다. 배포에서는 요청마다 모델을 새로 불러오지 않고 프로세스가 모델을 유지한다. PEFT 빠른 시작은 어댑터 저장과 AutoPeftModelForCausalLM로 다시 불러오는 방식을 설명한다.

평가 숫자의 의미를 해석하기

가상의 시험셋 100건에서 배송 70건, 결제 10건, 교환 10건, 기타 10건이라고 하자. 모든 사례를 배송으로 답하면 정확도가 70%다. 이 숫자만 보면 준수하지만 결제·교환·기타는 한 건도 해결하지 못한다. 따라서 범주별 정밀도·재현율, macro F1을 함께 기록한다. macro F1은 각 범주의 F1을 동일한 비중으로 평균 내어 적은 범주의 실패를 드러낸다.

또한 범주: 결제가 맞아도 이유: 카드 한도 초과처럼 입력에 없는 원인을 덧붙이면 실패다. 분류 라벨과 이유의 근거 충실도를 별도 칸으로 평가한다. 모델이 틀린 사례 20건을 읽어보니 12건에서 원래 레이블 자체가 일관되지 않았다면, 더 큰 모델보다 레이블 기준표를 고치는 것이 먼저다. 품질이 같다면 작은 모델의 지연·메모리 이점을 택할 수 있지만, 민감한 문의에서 특정 범주의 누락 비용이 큰 경우에는 그 범주의 재현율에 별도 기준을 둔다.

같은 카테고리의 글