LoRA 완전 정복 — 7B 모델을 노트북 하나로 파인튜닝하기

LoRA 완전 정복 — 7B 모델을 노트북 하나로 파인튜닝하기
GPU 하나로 70억 파라미터 모델을 파인튜닝할 수 있다면 믿으시겠습니까?
2년 전만 해도 LLM 파인튜닝은 A100 8장, 수백 GB 메모리가 필요한 대기업 전용 기술이었습니다. LoRA(Low-Rank Adaptation)가 이 판을 완전히 뒤집었습니다. 7B 모델 기준 학습 파라미터를 0.1%로 줄이면서도 풀 파인튜닝에 근접한 성능을 냅니다.
이 시리즈에서는 Qwen 2.5 7B를 대상으로 LoRA → QLoRA → 평가/배포까지 전 과정을 실습합니다.
- Part 1 (이 글): LoRA 이론 + 첫 파인튜닝
- Part 2: QLoRA + 한국어 데이터셋 구축
- Part 3: 평가 + 배포 + 실전 팁
파인튜닝이 왜 필요한가?
GPT-4, Claude, Qwen 같은 범용 LLM은 모든 걸 "적당히" 잘 합니다. 하지만 특정 도메인에서는 "적당히"로는 부족합니다.
프롬프트 엔지니어링으로 어느 정도 커버할 수 있지만, 한계가 명확합니다:
- 토큰 비용: 매번 긴 시스템 프롬프트를 보내야 합니다
- 일관성: 프롬프트가 길어질수록 지시를 놓치는 확률이 높아집니다
- 전문성 한계: 프롬프트만으로는 모델이 "모르는" 지식을 주입할 수 없습니다
파인튜닝은 모델의 가중치 자체를 수정하므로 이 세 가지를 근본적으로 해결합니다.
풀 파인튜닝의 문제: 메모리
7B 모델을 풀 파인튜닝하려면 얼마나 필요할까요?
모델 파라미터: 7B × 4 bytes (FP32) = 28 GB
Optimizer states (Adam): 7B × 8 bytes = 56 GB
Gradients: 7B × 4 bytes = 28 GB
Activations: ~20-40 GB (배치 크기에 따라)
──────────────────────────────
총 필요 VRAM: ~130-150 GBA100 80GB 2장이 필요합니다. 클라우드 비용으로 시간당 $6-8.
여기서 핵심 질문: 7B개 파라미터를 전부 업데이트해야 할까?
LoRA: 핵심 아이디어
LoRA의 핵심 인사이트는 놀라울 정도로 단순합니다:
파인튜닝으로 변하는 가중치의 변화량 $\Delta W$는 낮은 랭크(low-rank)를 가진다.수학적으로:
$$W' = W_0 + \Delta W = W_0 + BA$$
여기서:
$W_0$: 사전학습된 원본 가중치 (고정, 학습 안 함)$B \in \mathbb{R}^{d \times r}$: 작은 행렬$A \in \mathbb{R}^{r \times k}$: 작은 행렬$r$: 랭크 (보통 8~64, 원래 차원$d$보다 훨씬 작음)
예를 들어 $d = 4096$, $k = 4096$인 어텐션 레이어에서:
16.7M개 대신 131K개만 학습합니다. 99.2% 감소.
왜 이게 작동하는가?
직관적으로 이해해봅시다. 사전학습된 LLM은 이미 언어의 기본 구조를 알고 있습니다. 파인튜닝은 이 지식을 "미세 조정"하는 것이지, 처음부터 다시 배우는 게 아닙니다.
비유하자면: 영어를 유창하게 하는 사람이 의학 용어를 배우는 것과 같습니다. 언어 체계 전체를 다시 배울 필요 없이, 새로운 어휘와 표현 패턴만 추가하면 됩니다. 이 "추가분"이 low-rank라는 겁니다.
LoRA의 핵심 하이퍼파라미터
lora_alpha / r이 실질적인 스케일링 비율입니다. alpha=32, r=16이면 스케일 = 2, 즉 LoRA의 영향력이 2배로 증폭됩니다.
실습: Qwen 2.5 7B LoRA 파인튜닝
이제 이론을 코드로 옮깁니다. 전체 코드는 함께 제공되는 Jupyter 노트북에서 실행할 수 있습니다.
환경 설정
!pip install -q transformers peft datasets accelerate bitsandbytes trlimport torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
from trl import SFTTrainer모델 로드
model_name = "Qwen/Qwen2.5-7B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
)Qwen 2.5 7B Instruct를 bfloat16으로 로드합니다. 이것만으로 약 14GB VRAM을 사용합니다.
LoRA 설정
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 13,631,488 || all params: 7,628,554,240 || trainable%: 0.17877.6B개 중 1,360만 개(0.18%)만 학습합니다.
target_modules에 어텐션 레이어(q/k/v/o_proj)뿐 아니라 FFN 레이어(gate/up/down_proj)도 포함했습니다. 최근 연구에 따르면 FFN도 포함하는 게 성능이 더 좋습니다.
데이터셋 준비
실습용으로 Open-Orca/OpenOrca의 일부를 사용합니다. 실전에서는 도메인 데이터로 교체하세요.
dataset = load_dataset("Open-Orca/OpenOrca", split="train[:5000]")
def format_chat(example):
messages = [
{"role": "system", "content": example["system_prompt"]},
{"role": "user", "content": example["question"]},
{"role": "assistant", "content": example["response"]},
]
text = tokenizer.apply_chat_template(messages, tokenize=False)
return {"text": text}
dataset = dataset.map(format_chat)Qwen의 chat template을 적용해서 <|im_start|>system\n...<|im_end|> 형식으로 변환합니다.
학습
training_args = TrainingArguments(
output_dir="./qwen25-lora",
num_train_epochs=1,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=2e-4,
warmup_ratio=0.1,
logging_steps=10,
save_strategy="epoch",
bf16=True,
gradient_checkpointing=True,
optim="adamw_8bit",
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=1024,
)
trainer.train()핵심 설정 설명:
gradient_accumulation_steps=8: 배치 2 × 8 = 실질 배치 16gradient_checkpointing=True: 메모리를 시간으로 교환 (VRAM ~30% 절약)optim="adamw_8bit": 8-bit Adam으로 optimizer 메모리 절반 절약bf16=True: BFloat16 학습 (A100/RTX 3090+ 지원)
저장 및 추론
# LoRA 어댑터만 저장 (원본 모델 X)
model.save_pretrained("./qwen25-lora-adapter")
# 추론
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype=torch.bfloat16, device_map="auto"
)
model = PeftModel.from_pretrained(base_model, "./qwen25-lora-adapter")
messages = [
{"role": "user", "content": "양자역학을 5살 아이에게 설명해줘"}
]
inputs = tokenizer.apply_chat_template(messages, return_tensors="pt").to("cuda")
outputs = model.generate(inputs, max_new_tokens=256)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))저장된 어댑터 크기는 약 52MB입니다. 원본 모델(14GB)과 비교하면 0.4%.
메모리 비교: Full FT vs LoRA
같은 Qwen 2.5 7B, 같은 데이터셋으로 측정한 결과입니다.
LoRA가 압도적으로 효율적입니다. 특히 "추론 성능 ~98%"에 주목하세요. 2%의 성능 차이를 위해 10배의 비용을 쓸 이유가 없습니다.
자주 묻는 질문
Q: rank를 높이면 무조건 좋은 건가요?
아닙니다. rank=8과 rank=64의 차이는 대부분의 태스크에서 미미합니다. 오히려 rank가 너무 높으면 과적합 위험이 커집니다. 경험적으로:
- 간단한 태스크 (톤 변경, 형식 맞추기): r=8
- 중간 태스크 (도메인 적응): r=16-32
- 복잡한 태스크 (새로운 지식 주입): r=32-64
Q: 어떤 레이어에 LoRA를 적용해야 하나요?
모든 어텐션 + FFN 레이어에 적용하는 게 가장 안정적입니다. 논문에서는 q_proj, v_proj만 사용했지만, 이후 연구에서 더 많은 레이어에 적용할수록 성능이 좋아진다는 결과가 나왔습니다.
Q: 기존 모델의 성능이 떨어지지 않나요?
LoRA는 원본 가중치를 건드리지 않습니다. 어댑터를 떼면 원래 모델로 돌아갑니다. 이게 LoRA의 가장 큰 장점 중 하나입니다.
다음 편 예고: QLoRA + 한국어
Part 1에서는 LoRA의 이론과 기본 파인튜닝을 다뤘습니다. 하지만 RTX 3090(24GB)으로도 Qwen 2.5 7B LoRA는 빠듯합니다.
Part 2에서는 QLoRA(4-bit 양자화 + LoRA)로 T4 16GB에서도 7B 모델을 파인튜닝하는 방법을 다룹니다. 그리고 한국어 데이터셋을 구축해서 실제로 한국어 성능을 개선합니다.