본문 바로가기

LLM

Gemma 4 파인튜닝 Unsloth로 30분에 끝내기 — API 비용 0원, 도메인 특화 모델

반응형

요즘 사내 챗봇이나 코딩 어시스턴트 만들 때 다들 GPT나 Claude API부터 붙이고 보는데, 막상 매달 청구서 보면 정신이 아득해지죠. 저도 처음엔 프롬프트만 잘 짜면 되는 줄 알았는데, 출력 형식이 자꾸 흔들려서 결국 파인튜닝까지 손댄 케이스예요. 그래서 오늘은 Gemma 4를 Unsloth로 30분 만에 파인튜닝해서 API 없이 도메인 특화 모델 만드는 법을 정리해봤어요.

Gemma 4는 2026년 4월 2일 Google DeepMind가 출시한 오픈소스 모델인데, Apache 2.0 라이선스라서 상업적 사용이나 재배포, 수정까지 전부 자유롭게 할 수 있어요.

파인튜닝이 왜 필요한지부터 짚어볼게요. 프롬프트 엔지니어링만으로 "항상 JSON으로 응답해줘"라고 시켜도 실제로는 30% 정도 실패율이 나오는 게 현실이에요. RAG는 지식을 주입하는 데는 좋지만 스타일이나 출력 형식까지 제어하기는 어렵다는 한계가 있고요. 반면 파인튜닝을 하면 99% 이상 일관된 출력을 얻으면서 동시에 도메인 특화 지식까지 모델에 박아넣을 수 있어요. 그래서 출력 형식이 항상 일정해야 하거나, 특정 도메인 용어와 스타일이 필요하거나, 프롬프트가 너무 길어져서 비용이 부담되거나, API 없이 로컬·온프레미스로 배포해야 하는 상황이라면 파인튜닝이 가장 현실적인 답이 됩니다.

Unsloth가 뭔가

Unsloth는 HuggingFace 기본 방식보다 훨씬 가볍게 파인튜닝할 수 있게 해주는 라이브러리예요. 학습 속도는 2배 빠르고 메모리는 최대 70%까지 절약되는데, 체감 차이가 꽤 커요. 예를 들어 기본 HuggingFace로 E4B 모델을 파인튜닝하려면 VRAM 15GB가 필요한데, Unsloth의 QLoRA 방식을 쓰면 VRAM 8GB로도 충분해서 RTX 3070이나 3060 Ti 같은 보급형 GPU에서도 돌릴 수 있어요.

0단계 — 모델 선택

먼저 어떤 크기의 Gemma 4를 쓸지 골라야 해요. E2B(2B)는 스마트폰이나 라즈베리파이 수준에서 돌아가는 모델이라 매우 가벼운 분류·추출 작업에 적합해요. 이 글에서 다루는 건 E4B(4B)인데, 무료 Google Colab T4 GPU에서도 가능하고 RTX 3060 이상이면 로컬에서도 돌릴 수 있고, 텍스트뿐 아니라 이미지와 오디오까지 지원해요. 26B A4B는 A100 Colab이나 RTX 4090이 필요하고 MoE 구조라서 QLoRA보다는 16bit LoRA가 권장돼요. 31B는 RTX 4090 24GB에 QLoRA를 쓰면 최고 품질을 뽑을 수 있어요.

1단계 — 환경 설치

본격적으로 시작하기 전에 가상환경부터 만들고 필요한 패키지를 설치해야 해요. 아래 명령어를 순서대로 실행하면 됩니다.

# 가상환경 생성
python -m venv gemma4-finetune
source gemma4-finetune/bin/activate  # Windows: .\gemma4-finetune\Scripts\activate

# Unsloth 설치 (의존성 전부 포함)
pip install unsloth

# GPU 없이 CPU만 있는 경우
pip install unsloth[cpu]

# 추가 라이브러리
pip install datasets trl transformers

이렇게 설치만 끝내면 로컬 환경에서 파인튜닝할 준비가 끝나요. GPU가 없거나 그냥 가볍게 테스트해보고 싶다면 로컬 설치 대신 Colab을 쓰는 게 훨씬 편해요.

# Colab 첫 셀에 실행
!pip install unsloth
!pip install datasets trl

Colab에서는 셀 하나만 실행하면 끝이라서, 환경 설정에 시간 쓰기 싫은 분들은 이 방법을 추천해요.

2단계 — 학습 데이터 만들기

파인튜닝의 핵심은 사실 코드가 아니라 데이터예요. 아무리 코드를 잘 짜도 데이터 품질이 낮으면 결과물도 낮을 수밖에 없어요.

데이터 형식

학습 데이터는 아래처럼 user와 assistant 역할이 담긴 messages 형식의 JSONL로 준비하면 됩니다.

{"messages": [
  {"role": "user", "content": "FastAPI에서 JWT 인증 미들웨어 만들어줘"},
  {"role": "assistant", "content": "```python\nfrom fastapi import Request, HTTPException\nfrom jose import jwt\n...\n```\n\n위 코드는 Bearer 토큰을 검증합니다..."}
]}
{"messages": [
  {"role": "user", "content": "PostgreSQL 연결 풀 설정하는 법"},
  {"role": "assistant", "content": "```python\nfrom sqlalchemy.ext.asyncio import create_async_engine\n...\n```"}
]}

이 형식만 지키면 어떤 도메인이든 자유롭게 데이터를 채워넣을 수 있어요. 실제 사용자가 물어볼 법한 질문과 정확한 답변 쌍으로 구성하는 게 핵심이에요.

데이터 수집 방법

데이터를 직접 작성하면 품질은 가장 좋지만 시간이 많이 들어요. 아래처럼 파이썬 리스트로 작성한 다음 JSONL로 저장하는 방식이 가장 기본적이에요.

# 방법 1: 직접 작성 (품질 최고)
training_data = [
    {
        "messages": [
            {"role": "user", "content": "질문"},
            {"role": "assistant", "content": "답변"}
        ]
    },
    # ...
]

# JSONL로 저장
import json
with open("training_data.jsonl", "w", encoding="utf-8") as f:
    for item in training_data:
        f.write(json.dumps(item, ensure_ascii=False) + "\n")

직접 작성이 부담스럽다면 기존 문서나 로그에서 Claude 같은 모델을 이용해 Q&A 쌍을 자동으로 뽑아내는 방법도 있어요. 회사 문서가 많을 때 특히 유용한 방법이에요.

# 방법 2: 기존 문서/로그에서 자동 생성
import anthropic

client = anthropic.Anthropic()

def generate_qa_pairs(document: str, n: int = 10) -> list:
    """문서에서 Q&A 쌍 자동 생성"""

    response = client.messages.create(
        model="claude-haiku-4-5",  # 생성은 Haiku로 절약
        max_tokens=2000,
        messages=[{
            "role": "user",
            "content": f"""
다음 문서를 기반으로 Q&A 쌍 {n}개를 만들어줘.
실제 사용자가 물어볼 법한 질문과 정확한 답변으로.
JSON 배열로만 응답해줘:
[{{"user": "질문", "assistant": "답변"}}, ...]

문서:
{document}
"""
        }]
    )

    pairs = json.loads(response.content[0].text)
    return [
        {"messages": [
            {"role": "user", "content": p["user"]},
            {"role": "assistant", "content": p["assistant"]}
        ]}
        for p in pairs
    ]

# 내 회사 문서에서 생성
docs = ["API 문서 내용", "코딩 가이드라인", "도메인 지식 문서"]
all_pairs = []
for doc in docs:
    pairs = generate_qa_pairs(doc, n=20)
    all_pairs.extend(pairs)

print(f"총 {len(all_pairs)}개 학습 데이터 생성")

이렇게 자동 생성한 데이터는 검증 없이 그대로 쓰면 위험하니까, 일부는 직접 검토하고 다듬는 과정을 거치는 게 좋아요.

최소 데이터 기준

데이터가 50개만 있어도 기본적인 스타일이나 형식은 학습이 가능해요. 100개 정도면 도메인 용어를 익히는 수준까지 올라가고, 200개면 대부분의 도메인 특화 작업에 충분한 수준이 돼요. 전문가급 도메인 모델을 원한다면 500개 이상을 목표로 하는 게 좋아요.

3단계 — 파인튜닝 실행

데이터가 준비됐다면 이제 실제로 모델을 학습시킬 차례예요. 모델 로드부터 LoRA 어댑터 추가, 데이터셋 적용, 학습 설정까지 한 번에 묶어서 실행하는 코드예요.

from unsloth import FastLanguageModel
import torch
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig

# ── 1. 모델 로드 ──────────────────────────────────
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/gemma-4-E4B-it",  # E4B 권장
    max_seq_length=2048,   # 보수적으로 시작
    load_in_4bit=True,     # QLoRA — VRAM 절반으로
    dtype=None,            # 자동 감지
)

# ── 2. LoRA 어댑터 추가 ───────────────────────────
model = FastLanguageModel.get_peft_model(
    model,
    r=16,                    # LoRA 랭크 (8~32 권장)
    lora_alpha=16,           # 스케일링 파라미터
    lora_dropout=0,          # Unsloth 최적화 — 0 유지
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    use_gradient_checkpointing="unsloth",  # VRAM 30% 추가 절약
    bias="none",
)

# ── 3. 데이터셋 로드 ──────────────────────────────
dataset = load_dataset(
    "json",
    data_files="training_data.jsonl",
    split="train"
)

# 채팅 템플릿 적용
def apply_chat_template(examples):
    texts = []
    for messages in examples["messages"]:
        text = tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=False
        )
        texts.append(text)
    return {"text": texts}

dataset = dataset.map(apply_chat_template, batched=True)

# ── 4. 학습 설정 ──────────────────────────────────
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    args=SFTConfig(
        output_dir="./gemma4-finetuned",

        # 핵심 설정
        num_train_epochs=3,              # 데이터 200개 이하면 3, 이상이면 1~2
        per_device_train_batch_size=2,   # VRAM 부족하면 1로
        gradient_accumulation_steps=4,   # 사실상 배치 8

        # 학습률
        learning_rate=2e-4,
        warmup_ratio=0.1,
        lr_scheduler_type="cosine",

        # 정밀도
        bf16=True,                       # RTX 30xx 이상 지원
        fp16=False,

        # 로깅
        logging_steps=10,
        save_strategy="epoch",

        # 최적화
        dataset_text_field="text",
        max_seq_length=2048,
        packing=True,                    # 짧은 샘플 묶어서 효율 향상
    ),
)

# ── 5. 학습 시작 ──────────────────────────────────
print("학습 시작...")
trainer.train()
print("완료!")

이 코드를 그대로 돌리면 데이터 200개 기준으로 GPU에 따라 10분에서 35분 사이에 학습이 끝나요. 학습 중간에 loss가 떨어지는지 로그를 확인하면서 진행 상황을 체크하는 게 좋아요.

4단계 — 결과 확인

학습이 끝났으면 파인튜닝 전후 모델이 실제로 어떻게 다르게 답하는지 비교해봐야 해요. 추론 모드로 전환하는 걸 빠뜨리면 안 되는데, 이 한 줄을 빼먹으면 학습 모드로 동작해서 속도도 느리고 출력도 이상하게 나와요.

# 학습 전후 비교
from unsloth import FastLanguageModel

# 추론 모드로 전환 (필수!)
FastLanguageModel.for_inference(model)

def ask(prompt: str) -> str:
    messages = [{"role": "user", "content": prompt}]

    inputs = tokenizer.apply_chat_template(
        messages,
        tokenize=True,
        add_generation_prompt=True,
        return_tensors="pt"
    ).to("cuda")

    outputs = model.generate(
        input_ids=inputs,
        max_new_tokens=512,
        temperature=0.7,
        do_sample=True,
    )

    # 입력 부분 제거하고 응답만 추출
    response = tokenizer.decode(
        outputs[0][inputs.shape[-1]:],
        skip_special_tokens=True
    )
    return response

# 테스트
print(ask("FastAPI에서 JWT 인증 어떻게 구현해?"))

이렇게 같은 질문을 던져보면 파인튜닝 전과 후의 답변 스타일이 확실히 달라진 걸 체감할 수 있어요.

5단계 — GGUF로 내보내서 Ollama에 배포

모델 품질이 만족스럽다면 이제 실제로 쓸 수 있는 형태로 내보내야 해요. GGUF 포맷으로 변환하면 Ollama 같은 로컬 런타임에서 바로 돌릴 수 있어요.

# GGUF 변환 + 저장
model.save_pretrained_gguf(
    "gemma4-custom",
    tokenizer,
    quantization_method="q4_k_m"  # 권장: q4_k_m (품질/크기 균형)
    # 옵션: q8_0 (고품질), q4_0 (소형), q2_k (초소형)
)

변환이 끝나면 Modelfile을 작성해서 Ollama에 등록하면 바로 사용할 수 있어요.

# Modelfile 생성
cat > Modelfile << 'EOF'
FROM ./gemma4-custom-Q4_K_M.gguf

SYSTEM """
당신은 우리 회사 FastAPI/Python 전문 코딩 어시스턴트입니다.
항상 실행 가능한 코드를 제공하고, 우리 프로젝트 구조를 따릅니다.
"""
EOF

# Ollama에 등록
ollama create my-gemma4 -f Modelfile

# 실행
ollama run my-gemma4

이렇게 등록까지 마치면 더 이상 API 비용 걱정 없이 로컬에서 우리 회사 전용 모델을 계속 사용할 수 있어요.

GPU별 예상 시간

데이터 200개를 기준으로 했을 때, 무료 Colab T4(16GB)는 약 25분에서 35분 정도 걸리고, RTX 3060(12GB)은 40분에서 50분 정도 걸려요. RTX 3090(24GB)이라면 15분에서 20분, RTX 4090(24GB)이라면 10분에서 15분이면 끝나고, A100(40GB)을 쓴다면 5분에서 10분 안에 학습이 마무리돼요. 즉 보급형 GPU로도 충분히 시도해볼 만한 작업이라는 거예요.

흔한 실수와 해결법

메모리 부족(OOM) 에러가 뜬다면 배치 크기를 1로 줄이거나 max_seq_length를 1024로 낮추고, 대신 gradient_accumulation_steps를 8로 늘려서 보완하면 돼요. loss가 잘 안 떨어진다면 learning_rate를 5e-4로 높여보고, 데이터 형식이 일관적인지 다시 확인하고, 필요하면 epoch 수를 5로 늘려보세요. 반대로 파인튜닝 후 오히려 성능이 떨어졌다면 r 값을 8로 낮춰서 과적합을 줄이고, 데이터에 일반 지식 샘플을 20~30% 섞어주고, epoch 3에서 멈추는 게 안전해요. 마지막으로 추론할 때 결과가 이상하게 나온다면 FastLanguageModel.for_inference(model) 호출을 빠뜨렸는지부터 확인해보세요.

전체 과정을 정리하면, 데이터 준비에 15분, 학습 실행에 GPU에 따라 10분에서 35분, 결과 확인에 5분, GGUF 내보내기에 5분 정도가 걸려요. 즉 데이터만 미리 준비해두면 한 시간 안에 도메인 특화 모델 하나를 완성할 수 있다는 뜻이에요. API 비용 걱정 없이 우리 회사 전용 어시스턴트를 만들고 싶다면, Gemma 4와 Unsloth 조합이 지금 가장 현실적인 선택지인 것 같아요.

 

반응형