AI 모델 구축 (2) QLoRA
gpt-oss 모델을 웹 서비스를 연결을 마치고 Llama 서버를 설치했다. Llama는 파리미터가 더 적어서 경량 파인튜닝이 가능해 파인튜닝도 진행했다.
경량 튜닝은 원본 모델의 가중치를 수정하는 것이 아니다. 원본 모델 가중치는 그대로 두고 모델 위에 작은 레이어(LoRA 행렬)를 붙여서 레이어만 학습하는 방식이다.
우리가 가중치를 하나하나 조정하는 것이 아니라 “어떤 부분을 얼마나 학습 가능하게 할지”만 설정하고 실제 값 업데이트는 코드(Pytorch/Unsloth/TRL)가 자동으로 진행한다. 나는 Pytorch로 진행했다.
💡LoRA (Low-Rank Adaptation)
원리: 기존 모델의 가중치(W)는 고정(Freeze)하고, 가중치의 변화량(Delta W)을 두 개의 작은 행렬($A, B$)로 분해하여 이들만 학습시킵니다. Wupdated = W + Delta W = W + BA
여기서 r(Rank)이라는 아주 작은 차원을 사용하여 학습 파라미터 수를 10,000배 이상 줄일 수 있다. • 장점: ◦ 메모리 절감: 전체 파인튜닝 대비 GPU 메모리 사용량이 약 1/3로 감소한다. ◦ 빠른 속도: 학습해야 할 데이터량이 적어 학습 시간이 대폭 단축된다. ◦ 어댑터 방식: 학습된 결과물(Adapter) 용량이 수십 MB 수준으로 작아, 여러 작업용 어댑터를 쉽게 교체하며 사용할 수 있다.
QLoRA (Quantized LoRA)
QLoRA는 LoRA를 한 단계 더 발전시켜 극단적인 메모리 효율성을 추구한 기술이다. 2025~2026년 기준, 일반 소비자용 GPU 한 장으로도 거대 모델(예: Llama 70B)을 학습시킬 수 있게 한 일등 공신이라고 한다.
핵심 기술:
4-bit NormalFloat (NF4): 모델 가중치를 4비트로 양자화하여 메모리 점유율을 75% 이상 낮추면서도 성능 하락을 최소화한다.
Double Quantization: 양자화에 필요한 상수들조차 다시 양자화하여 추가 메모리를 확보한다.
Paged Optimizers: GPU 메모리가 부족할 때 CPU 메모리를 활용해 메모리 부족(OOM) 오류를 방지한다.
장점: LoRA보다 훨씬 적은 자원으로 유사한 성능을 낼 수 있어, 중소규모 개발 환경에서 필수적으로 사용된다.
LoRA 설정에서 직접하는 것들은 다음과 같다.
r(랭크) : LoRA 레이어의 크기
alpha : 스케일링 계수
target_modules/target_parameters:
어떤 레이어에 LoRA를 붙일지
학습률(learning rate), batch size, max_steps 등 학습 하이퍼파라미터
여기서 예를 들어
from peft import LoraConfig
peft_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj"], # 여기에만 LoRA 붙이겠다
)
이렇게 어디에 어느 정도 크기(r, alpha)만 정해주면 실제 가중치 텐서 값은 훈련 중에 자동으로 학습된다.
정리하면 학습 시 내부적으로는
베이스 모델은 고정되고
LoRA로 추가된 작은 가중치들만 gradient descent(경사 하강법)로 업데이트 되고,
loss가 줄어들면서 우리가 설정한 패턴/스타일을 배우게 된다.
💡Gradeient Descent (경사하강법)
머신러닝과 딥러닝에서 모델을 학습시킬 때 사용하는 가장 기본적인 최적화 알고리즘.
→ 함수의 값이 가장 작아지는 지점을 찾기 위해 경사를 따라 반복적으로 내려가는 방법
1. 직관적인 이해: 안개 낀 산 내려가기
여러분은 지금 안개가 자욱해서 앞이 전혀 보이지 않는 산의 정상에 서 있습니다. 여러분의 목표는 산의 가장 낮은 골짜기(최솟값)로 내려가는 것입니다. 이때 여러분이 할 수 있는 최선의 방법은 다음과 같습니다.
지금 내가 서 있는 지점에서 어느 방향이 가장 가파른 내리막인지 발로 느껴봅니다.
그 방향으로 한 걸음(Step) 내딛습니다.
다시 그 자리에서 어느 쪽이 가장 가파른지 확인하고 이 과정을 반복합니다.
2. 수학적 원리와 공식
컴퓨터는 이 과정을 수학적으로 처리한다. 모델의 예측값과 실제값의 차이를 나타내는 손실 함수(Loss Function)의 값을 최소화하는 것이 목표이다.
업데이트 공식
매개변수(Weight)를 θ라고 할 때, 다음과 같은 식으로 값을 업데이트한다.
θnew=θold−α⋅∇J(θ)
J(θ): 손실 함수 (줄여야 할 대상)
∇J(θ): 그레이디언트(Gradient). 즉, 현재 위치에서의 기울기.
α (알파): 학습률(Learning Rate). 한 번에 얼마나 큰 보폭으로 이동할지를 결정한다.
3. 핵심 변수: 학습률 (Learning Rate)
학습률은 경사 하강법에서 매우 중요한 하이퍼파라미터이다.
학습률이 너무 크면: 최저점을 지나쳐 버리거나(Overshooting), 아예 발산하여 최적의 값을 찾지 못할 수 있다.
학습률이 너무 작으면: 최저점까지 도달하는 데 시간이 너무 오래 걸리고, 지역 최솟값(Local Minimum)이라는 '가짜 골짜기'에 빠져서 빠져나오지 못할 수 있다.
4. 경사 하강법의 종류
데이터를 한 번에 얼마나 사용하느냐에 따라 세 가지로 나뉜다.
배치 경사 하강법 (Batch GD): 전체 데이터를 모두 확인한 뒤 한 걸음 이동한다. 정확하지만 속도가 매우 느림.
확률적 경사 하강법 (SGD): 데이터 하나를 볼 때마다 한 걸음씩 이동한다. 빠르지만 경로가 매우 불규칙(Noisy)하다.
미니배치 경사 하강법 (Mini-batch GD): 데이터를 적당한 묶음(예: 32개, 64개)으로 나누어 이동한다. 현재 딥러닝에서 가장 표준으로 사용되는 방식.
먼저 경량 튜닝을 전체 흐름을 정리했다.
튜닝 목표 정하기(무엇을 잘하게 할 지 정의)
튜닝에 필요한 데이터 구축
데이터 포맷 맞추기
경량 튜닝 방식 LoRA/QLoRA
튜닝을 위한 Phython 환경 구축
1. 튜닝 목표 정하기
“경량 튜닝”은 이미 학습된 모델에 특정 스킬을 덧붙인다에 가깝다. 현재 만들고자 하는 것은 사내 엔진 함수를 보다 쉽게 파악하고 예시도 쉽게 볼 수 있도록 하는 것이다.
목표는 “사내 엔진 함수를 명확하게 파악하기”이다.
2, 3 데이터 구축 및 데이터 포맷 설정
모델에 따라서 특화된 데이터 구조가 있다고 한다. 보통 message 리스트 형식으로 준비하는 게 좋다고 한다.
원본 데이터를 구조화해서 준비한 후 message 형식으로 변환해서 학습을 진행했다. 튜닝 과정에 데이터 포맷 형식 로직을 추가해서 사용했다.
{"engine_function": "...", "file": "...", "language": "javascript", "snippet": "const ...", "context_comment": "...", "tags": ["..."]}
// 위와 같은 형태의 데이터를 아래와 같이 변경
{"messages":[
{"role":"system","content":"너는 코딩 전문 어시스턴트야."},
{"role":"user","content":"..."},
{"role":"assistant","content":"const proxyUrl = ...; ..."}
]}
4. 경량튜닝 방식
메모리 효율을 높이고자 QLoRA 방식으로 진행했다.
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig
1) 사용할 Llama 3.1 8B Instruct 모델
MODEL_ID = "meta-llama/Llama-3.1-8B-Instruct"
# 2) 학습 데이터 경로 (messages 형식 두 파일)
# - 코드 생성용: train_xdworld_codegen_messages.jsonl
# - 문서 설명용: train_xdworld_docs_messages.jsonl
DATA_FILES = [
"...jsonl",
"...jsonl",
]
def main():
print("Loading tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
tokenizer.padding_side = "right"
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token # Llama 계열 pad 설정
print("Loading dataset...")
# 여러 jsonl 파일을 하나의 train split로 합치기
dataset = load_dataset(
"json",
data_files={"train": DATA_FILES},
)["train"]
# messages → text 로 변환: Llama3 chat template 적용
def format_example(example):
messages = example["messages"]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
)
return {"text": text}
dataset = dataset.map(format_example)
print("Loading base model in 4bit (QLoRA)...")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto", # RTX 4060 + CPU에 자동 분산/오프로딩
)
# LoRA 설정 (8GB VRAM 고려)
lora_config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
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()
# 학습 설정 (2차 튜닝, output 디렉터리는 v2로 분리)
training_args = SFTConfig(
output_dir="llama3-8b-xdworld-lora-v2",
num_train_epochs=1.0, # 처음엔 1 epoch로 테스트
per_device_train_batch_size=1, # 8GB VRAM에 안전한 값
gradient_accumulation_steps=8, # 유효 batch size ~= 8
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=10,
save_steps=200,
bf16=True,
report_to=[], # wandb 등 안 쓸 거면 빈 리스트
)
# SFTTrainer 초기화 (설치된 TRL 버전 시그니처에 맞게 최소 인자만 사용)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
eval_dataset=None,
processing_class=tokenizer,
)
print("Start training...")
trainer.train()
print("Training finished. Saving LoRA model...")
trainer.save_model("llama3-8b-xdworld-lora-v2")
print("LoRA weights saved to ./llama3-8b-xdworld-lora-v2")
if __name__ == "__main__":
main()
이렇게 튜닝하니까 학습만 내용에 대해 대답은 잘 하는데, 디테일을 모를 때 같은 내용을 반복해서 출력하는 LLM 폭주가 발생했다. 학습한 데이터가 많지 않고 제약이 부족한 상태일 수 있다. 근본적으로 데이터 형식이 잘 못되었을 수도 있고..
그냥 해결하기 위해 main.py 에서 아래와 같이 조정을 진행했다.
outputs = model.generate(
**inputs,
max_new_tokens=200, # 512 → 200 정도로, 너무 긴 코드는 잘라주기
do_sample=False, # 랜덤 샘플링 끄고, greedy decoding으로
...
repetition_penalty=1.2, # 같은 토큰 반복을 패널티 줘서 줄이기
no_repeat_ngram_size=6, # 6토큰 이상의 구문이 반복되는 걸 막기
💡SFTTrainer (Supervised Fine-Tuning)
지도 학습 기반 미세 조정 : Hugging Face의 TRL(Transformer ReinForcement Learning) 라이브러리에서 제공하는 클래스로, 거대언어 모델(LLM)을 지도 학습 기반 미세 조정하는 데 최적화된 도구이다.
특징
다양한 데이터 형식 지원 : 텍스트 데이터셋을 모델이 이해할 수 있는 형식으로 자동 변환함.
dataset_text_field옵션을 통해 특정 열을 지정하거나, 질문-답변 쌍을 자동으로 처리함.패킹(Packing) 기능 : 여러 개의 짧은 문장을 하나의 고정된 길이로 묶어서 학습 효율을 극대화 함. 이를 통해 GPU 연산 낭비를 줄이고 학습 속도를 높일 수 있음.
PEFT(LoRA/QLoRA) 기본 통합 : 모델을 전체 학습 시키는 대신 일부 파라미터만 학습 시키는 PEFT(Parameter-Efficient Fine-Tuning) 설정을
peft_config파라미터 하나로 쉽게 적용할 수 있음.Chat Template 자동 적용 : 대화형 모델 학습 시 user, assistant 역할을 구분하는 복잡한 템플릿 작업을 자동으로 처리함.
5. 튜닝을 위한 Python 환경 구축
환경은 FastAPI 프레임워크를 사용했다. Llama 사용를 위한 REST API가 주 목적이고 안정성을 위해 이 프레임워크를 선택했다.
결과적으로 원하는 결과가 나오지 않아서.. 데이터 재점검, RAG 도입을 계획하고 있다.
댓글 (0)
첫 댓글을 남겨 대화를 시작해 보세요.