LLM Finetuning은 기존의 모델을 특정 분야에서 더 잘하게 만들기 위해서 진행된다. 특히 법률QA나 의료 상담, 사내 문서등에 사용된다.
그럼 RAG랑 LLM Finetuning이랑 비슷한 역할 아닌가❓ 할 수 있지만 엄연히 다르다.
RAG는 모델이 학습되어있는 정보 외에 외부 지식을 제공하여 출처 정확한 답변을 주도록 하는 역할이다. 검색 tools를 추가하여 최근 뉴스 정보를 가져오고 답변을 해주는 역할도 있다.
RAG를 통해 AI Agent를 만들었는데 그게 성능 개선이 필요한데 이미 RAG에서 할 수 있는걸 다했고 리트리버에서 출력된 문서도 이미 너무 잘 찾은 상태이고 모델도 여러가지 바꿔봤고,,, 프롬프트도 개선하면서 진행을 하는데!!!!!!
model이 추측 답변을 한다거나 잘 찾은 문서에대한 요약 답변이 엉망이거나 계속 그런 상황일 때!!! LLM Model Finetuning을 사용하여 성능 개선을 진행한다.
결론! RAG를 개선해서 할 수 있는건 다 해본 후 검색된 LLM Finetuning으로 가야한다. (context에 문제가 없는 경우, 외부 지식과 LLM Finetuning은 관련 없다.)
RAG 부터가 문제인데 LLM Finetuning을 들어가면 비용만 증가하고 성능 개선에 도움이 되지 않는다.
추가 지식 없이(RAG 없이) 기존 모델의 학습된 정보만을 가지고 프롬프트로도 해결이 어려운 상황일 때 LLM Finetuning이 단독으로 진행되기도 한다.
LLM의 Finetuning은 규모가 엄청 커서 파라미터들이 수십억개부터 수천억되는 경우도 있다.
그래서 일반적인 모델 Finetuning과 동일하게 작업할 수 없다.
LLM의 Finetuning은 Base model parameters는 고정으로 하고 일부 파라미터만 학습시킨다. 그걸 PEFT라고 부름.
PEFT는 파라미터를 어떻게 학습할지에 대한 방법이고, 양자화는 모델을 어떻게 표현하고 연산할지에대한 기술이다. 파라미터를 학습할 때 메모리 경량을 위해 양자화 기술을 함께 사용하는 경우가 많다고 한다.
PEFT (Parameter-Efficient Fine-Tuning)
PEFT의 목표는 번역 그대로 파라미터를 효율적으로 파인튜닝하면서 특정 분야에 맞추고 성능을 유지하거나 향상시키면서 계산량과 파라미터의 크기를 줄이는 것이다.
PEFT는 Adapter 기반 메소드들과 Reparameterization 기반 메소드들로 분류된다.
Adapter 기반 메소드에는 Adapter Tuning과 Prompt/Prefix 계열 (Prompt Tuning, P-Tuning, Prefix Tuning)이 있다. Reparameterization 기반 메소드의 대표적으로 LoRA가 있으며, QLoRA는 LoRA에 양자화를 결합한 확장 방식이다.
현재는 PEFT 기법 중 대표적으로 LoRA를 많이 사용한다.
- Adapter Tuning : 기존 모델에 어댑터 레이어를 추가하고, 어댑터 레이어만 학습시키는 방식
- Prompt Tuning : 모델의 입력값 앞에 '학습된 벡터'를 붙혀서 특정 분야에 특화시킬 수 있다.
Prompt Tuning 논문 : https://arxiv.org/pdf/2104.08691
[P1 P2 ... Pk]의 의미는 a분야에 맞게 답변을 유도하는 벡터.
Prompt Tuning은 입력단에서만 적용이 되므로 깊게 들어갈 수록 영향력이 약해진다.
[P1 P2 ... Pk] [CLS] 토큰1 토큰2 토큰3 .. - P-Tuning : 입력의 다양한 위치에 '학습된 벡터'를 삽입하여 학습시키는 방식.
P-Tuning 논문 : https://arxiv.org/pdf/2103.10385
P는 Prompt. GPT-3 이전 모델들은 Prompt Tuning으로 가능했지만, 모델이 너무 커지면서 영향력이 너무 약해지는 현상이 발생.
그래서 Prompt Tuning이지만 업그레이드 된 P-Tuning이 나왔다.
문장 사이사이에 학습된 벡터를 끼워넣으면, 깊은 레이어로 들어가도 더 영향력을 주기때문에 해당 입력에대해서 a분야의 이런 의도로 풀어야 해라는 영향력을 줄 수 있다. - Prefix Tuning : 모델 레이어들에게 Prefix 영향을 준다.
Prefix Tuning 논문 : https://arxiv.org/pdf/2101.00190
LoRA (Low-Rank Adaptation)
LoRA 논문 : https://arxiv.org/pdf/2106.09685

LoRA는 기존(사전학습) 모델의 가중치(W)를 고정하고,
Transformer 일부 선형층에 low-rank 어댑터를 추가해 학습한다.
Figure 1: Our reparametriza-tion. We only train A and B.
왼쪽 이미지에서 주황색 부분인 A와 B 부분의 파라미터들만 Finetuning 시키는데,
여기서 r 값을 작게 하면 학습할 파라미터 숫자가 줄어들고 크게 하면 학습할 파라미터 숫자가 늘어난다.
파라미터 숫자가 늘어난다는 건 표현력이 높아진다는 것.
음,,,,,, 하이퍼파라미터 r을 8로 설정하면,,,,!!!!!!
기존 차원과 별개로 r(8)차원의 새로운 공간을 만들어내고 모델은 8개 차원 공간 안에서 좌표를 학습한다. 결국, r이 크면 좌표를 학습할 공간이 많다는 거고 작으면 학습할 공간이 적다는거라 규제가 강하다는 거고 그렇다!!!
from peft import LoraConfig
peft_config = LoraConfig(
r=8, # 8, 16, 32 등
lora_alpha=32, # 16, 32, 64 등
lora_dropout=0.1, # 0.0 ~ 1.0
bias="none", # "none", "all"
target_modules=["q_proj", "v_proj"], # "q_proj", "k_proj", "v_proj", "o_proj"
task_type="CAUSAL_LM", # "SEQ_CLS"(분류), "SEQ_2_SEQ_LM"(번역/요약), "TOKEN_CLS"(토큰분류), "QUESTION_ANSWERING"(질의응답)
)
'r/d가 작다'라는 건, $ \approx $ 저랭크 라는것.
행렬에서 rank란? 행렬이 표현할 수 있는 서로 독립적인 방향의 개수이다.
low-rank란? 랭크가 전체 차원(d)에 비해 매우 작은 경우를 말한다.
결국 LoRA는 가중치 변화를 low-rank로 재파라미터화하여 학습하는 기법이다.
재파라미터화❓ 같은 모델의 출력은 유지하면서 표현하는 파라미터만 바꾸는 것.
QLoRA (Quantized Low-Rank Adaptation)
LoRA 기법에서, 고정된 파라미터들은 4-bit(NF4)로 양자화해서 메모리를 줄이고 계산할 때만 Float32로 다시 복원하여 사용한다.
LoRA에서 추가된 주황색 영역의 파라미터들은 보통 FB16이나 BF16으로 양자화 해서 학습을 진행한다.
이렇게 하면 제한된 GPU 메모리 안에서도 연산이 가능해진다.
QLoRA 논문 : https://arxiv.org/pdf/2305.14314

PEFT 쪼오오오금 알 것 같아,,
근데,,, NF4로 양자화 한다는게 뭔데...? 싶어서 양자화 공부 시작,,,🥹
양자화 (Quantization)
float32 vs float16 vs bfloat16
딥러닝 모델들은 파라미터를 일반적으로 float32를 사용하여 저장한다. 양자화는 모델 파라미터를 float32 대신 float16이나 INT8 같은 type으로 계산하여 메모리 사용량과 연산 비용을 줄이는 기술이다.

get_memory_footprint() : 현재 모델이 사용하고 있는 메모리양을 바이트 단위로 반환
import torch
from transformers import AutoModelForCausalLM
model_id = 'gpt2'
###### FP32
model = AutoModelForCausalLM.from_pretrained(model_id)
print(f"float32 Model size: {model.get_memory_footprint():,} bytes, {model.get_memory_footprint()/1024/1024:,} MB")
###### FP16
model = AutoModelForCausalLM.from_pretrained(
model_id,
dtype=torch.float16
)
print(f"float16 Model size: {model.get_memory_footprint():,} bytes, {model.get_memory_footprint()/1024/1024:,} MB")
###### BF16
model = AutoModelForCausalLM.from_pretrained(
model_id,
dtype=torch.bfloat16
)
print(f"bfloat16 Model size: {model.get_memory_footprint():,} bytes, {model.get_memory_footprint()/1024/1024:,} MB")
# float32 Model size: 510,342,192 bytes, 486.7002410888672 MB
# float16 Model size: 261,462,552 bytes, 249.3501205444336 MB
# bfloat16 Model size: 261,462,552 bytes, 249.3501205444336 MB
📌 Int8 Quantization
Int8 양자화는 float32/float16 type인 파라미터를 Int8 (8-bit)로 표현하여 메모리와 연산량을 크게 줄이는 양자화 기법이다.
float에서 int8로 직접 변환 하면 소실되는 값들이 너무 많으므로 양자화 공식과 복원하는 공식이 존재한다.
양자화 기법 : $ q = round( \frac {x} {s}) + (z) $
복원화 기법 : $ x \approx s \cdot (q-z) $
- x : 원본값. float값.
- q : int8 값 (-128 ~ 127)
- s : scale
- z : zero-point (only Asymmetric)
Int8 양자화 기법에도 Symmetric Quantization과 Asymmetric Quantization이 존재한다.
Symmetric Quantization(Absmax)은 0을 중심으로 대칭적으로 양자화를 하고,
Asymmetric Quantization(Zero-point)은 0의 위치를 이동시켜 비대칭적으로 양자화를 한다.
Asymmetric Quantization은 데이터 분포가 비대칭적이거나 평균값이 0이 아닌 경우 사용한다. (예: ReLU, 양수값만 사용하기 때문)
Symmetric Quantization 실습 코드 (대칭)
import numpy as np
x = np.array([0.2, 1.5, -0.7, 3.9, -2.3, 0.0], dtype=np.float32)
print("x : ", x)
int_bit = 8
# Symmetric Quantization
sym_qmin = -(2**(int_bit - 1) -1)
sym_qmax = (2**(int_bit - 1) -1)
# 스케일 팩터(Scale Factor) 계산
# 1. 주어진 x의 절대값의 최대값을 찾는다.
# 2. 변경하려는 데이터타입(int8)의 최대값을 찾는다. 그리고 1번에서 찾은 최대값을 나눠준다.
sym_scale = np.max(np.abs(x)) / sym_qmax
# 양자화
# 각 원소에 스케일 팩터를 곱하고 반올림하여 정수형으로 변환해준다.
sym_q = np.round(x/sym_scale).astype(np.int8)
# 데이터 복원
deq_sym_q = sym_q.astype(np.float32) * sym_scale
print("Scale:", sym_scale)
print("Quantized (int8):", sym_q)
print("Dequantized:", deq_sym_q)
# x : [ 0.2 1.5 -0.7 3.9 -2.3 0. ]
# Scale : 0.030708661
# Quantized : [ 7 49 -23 127 -75 0]
# Dequantized : [ 0.21496063 1.5047244 -0.7062992 3.9 -2.3031497 0. ]
Asymmetric Quantization (비대칭)
import numpy as np
x = np.array([0.2, 1.5, -8.7, 3.9, -2.3, -3.3], dtype=np.float32)
print("x : ", x)
int_bit = 8
asy_qmin = -(2 ** (int_bit - 1))
asy_qmax = (2 ** (int_bit - 1) - 1)
asy_x_min, asy_x_max = x.min(), x.max()
asy_scale = (asy_x_max - asy_x_min) / (asy_qmax - asy_qmin)
asy_zero_point = np.round(asy_qmin - asy_x_min / asy_scale)
asy_zero_point = np.clip(asy_zero_point, asy_qmin, asy_qmax)
asy_q = np.round(x / asy_scale + asy_zero_point).astype(np.int8)
deq_asy_q = (asy_q.astype(np.float32) - asy_zero_point) * asy_scale
print("Scale:", asy_scale)
print("Asymmetric Quantized (int8):", asy_q)
print("Dequantized:", deq_asy_q)
# x : [ 0.2 1.5 -8.7 3.9 -2.3 -3.3]
# Scale: 0.049411766
# Asymmetric Quantized (int8): [ 52 78 -128 127 1 -19]
# Dequantized: [ 0.19764706 1.482353 -8.696471 3.9035296 -2.3223531 -3.3105884 ]
4-bit NormalFloat Quantization (NF4)
NF4 양자화 기법은 QLoRA Tuning에 나왔던 양자화 기법으로 사전학습된 모델의 가중치가 정규분포에 가깝다고 가정하고 정규분포를 기준으로 설계된 16개의 실수 대표값으로 가중치를 4-bit로 표현하는 비균등 양자화 방식이다. (사전학습된 딥러닝 모델의 가중치는 일반적으로 평균 0 근처의 정규분포를 따른다.)
1. 양자화 구간 설정 : NF4는 16등분한다. 각 구간에 동일한 개수의 파라미터들이 포함되도록 한다.
평균 0과 가까워질 수록 구간은 좁아지고 (파라미터들이 모여있기때문) 평균 0과 멀어질수록 구간은 넓어진다.

2. 각 구간의 대표값을 계산한다.
대표값들을 [-1, 1] 범위로 정규화 하여 NF4 룩테이블(code book)을 생성한다.
3. 실제 파라미터 양자화
3-1. absmax : 각 구간의 절대값의 최대값 구하기
3-2. $ \frac {w_{\text {b, i}}} {a_b} $ : 각 구간의 원소를 absmax로 나눠서 [-1, 1] 정규화 작업
3-3. [3-2]에서 정규화된 각 원소들 중 NF4 룩테이블에서 가장 가까운 대표값을 찾는다.
3-4. 복원을 위해 NF4 룩테이블과 absmax값을 저장한다.
✨ Finetuning할때 중요한 건!!!! 데이터셋을 잘 만드는 것이 매우매우 중요하다!!! ✨
❓이유는❓
Finetuning은 "지식 학습"이 아닌 "행동 학습"이라,, 파라미터들이 데이터셋 기준으로 모든 값을 학습한다.
그래서 데이터셋의 정답이 애매하면 Finetuning된 모델도 애매하게 답변한다.
데이터셋의 정답이 명확하면 Finetuning된 모델도 명확하게 답변한다.
'AI > AI TECH' 카테고리의 다른 글
| [AWS] VPC - EC2 초기 설정 (0) | 2026.01.26 |
|---|---|
| [플레이데이터 SK네트웍스 Family AI 캠프 21기] 1월 2주차 회고 - 3차 프로젝트 (1) | 2026.01.19 |
| [RAG] Advanced RAG (0) | 2025.12.29 |
| [플레이데이터 SK네트웍스 Family AI 캠프 21기] 12월 1주차 회고 (0) | 2025.12.08 |
| [플레이데이터 SK네트웍스 Family AI 캠프 21기] 11월 4주차 회고 - 2차 단위프로젝트 (0) | 2025.12.01 |