목차
메타 설명
거대한 AI 모델의 모든 파라미터를 다시 학습하지 않고도 원하는 업무에 맞게 Fine-tuning할 수 있을까요? Hugging Face PEFT와 LoRA의 원리부터 Rank, Alpha, Target Module, Adapter 저장·로드·병합, Trainer 연결, QLoRA 4비트 학습, GPU 메모리 절감 방법까지 실습 중심으로 알아봅니다.
지난 시간에는 Hugging Face Trainer를 이용해 사전 학습된 DistilBERT 모델을 영화 리뷰 감정 분석 업무에 맞게 Fine-tuning했습니다.
전체 모델을 학습하는 과정은 다음과 같았습니다.
사전 학습 모델 불러오기
→ 영화 리뷰 데이터 토큰화
→ 모든 모델 파라미터의 Gradient 계산
→ Optimizer가 전체 가중치 업데이트
→ 전체 모델 Checkpoint 저장DistilBERT처럼 비교적 작은 모델에서는 이 방식이 충분히 가능합니다.
하지만 모델 크기가 점점 커진다면 상황이 달라집니다.
1억 개 파라미터
→ 학습 가능
10억 개 파라미터
→ GPU 메모리 고민 시작
70억 개 파라미터
→ 장비 계산기 실행
700억 개 파라미터
→ 예산 담당자와 긴급회의Full Fine-tuning에서는 모델 가중치만 GPU에 올리는 것이 아닙니다.
기본 모델 가중치
+ Gradient
+ Optimizer 상태
+ 중간 Activation
+ 학습 Batch모델 하나를 학습시키려 했는데 GPU 메모리 안에는 모델의 가중치, Gradient, Optimizer가 단체로 입주합니다.
GPU:
“한 분만 들어오시는 줄 알았습니다.”
Optimizer:
“저희는 모델 파라미터보다
짐이 조금 많습니다.”그렇다면 모델 전체를 수정하지 않고 일부 작은 파라미터만 학습할 수는 없을까요?
이 질문에 답하는 기술이 PEFT입니다.
그리고 PEFT의 대표적인 방법이 LoRA입니다.
기존 모델 가중치
→ 동결
작은 LoRA Adapter
→ 학습
결과
→ 거대한 모델을 다시 저장하지 않고
작은 Adapter만 저장이번 시간에는 거대한 AI 모델의 엔진 전체를 뜯어내지 않고 작은 튜닝 부품만 장착하는 방법을 배워보겠습니다. 🔧🤖
1. PEFT란?
PEFT는 다음 표현의 약자입니다.
Parameter-Efficient Fine-Tuning한국어로 표현하면 다음과 같습니다.
파라미터 효율적 미세조정PEFT는 사전 학습 모델의 전체 파라미터를 수정하지 않고, 소수의 파라미터만 학습해 모델을 특정 작업에 적응시키는 방법입니다.
Hugging Face PEFT는 LoRA, IA³, AdaLoRA, Prompt Tuning, Prefix Tuning 등 다양한 파라미터 효율적 학습 방법을 지원합니다. Transformers에도 Adapter를 추가하고 학습·전환·삭제할 수 있는 PEFT 통합 기능이 포함되어 있습니다.
Full Fine-tuning
전체 모델:
학습
PEFT
기본 모델:
동결
작은 추가 파라미터:
학습PEFT에서 학습된 작은 파라미터 묶음을 일반적으로 Adapter라고 부릅니다.
기본 모델
+ 금융 Adapter
→ 금융 상담 모델
+ 법률 Adapter
→ 법률 문서 모델
+ 고객센터 Adapter
→ 고객 응대 모델하나의 기본 모델 위에 업무별 Adapter를 갈아 끼울 수 있습니다.
모델을 업무마다 통째로 복제하지 않아도 되는 셈입니다.
2. Full Fine-tuning의 비용
Full Fine-tuning은 모델의 거의 모든 파라미터를 업데이트합니다.
for parameter in model.parameters():
print(
parameter.requires_grad
)대부분 다음 결과를 가집니다.
True학습에서는 모델 가중치 외에도 추가 메모리가 필요합니다.
모델 가중치
모델 자체의 파라미터입니다.
Gradient
각 파라미터를 어느 방향으로 수정할지 저장합니다.
Optimizer 상태
AdamW 같은 Optimizer는 파라미터별 이동 평균 등을 추가로 저장합니다.
Activation
역전파 계산을 위해 Forward Pass의 중간 결과를 저장합니다.
따라서 다음 계산은 정확하지 않습니다.
7B 모델이 14GB니까
16GB GPU에서 학습할 수 있겠군!추론과 학습의 메모리 요구량은 다릅니다.
추론:
모델 가중치 중심
학습:
모델 가중치
+ Gradient
+ Optimizer 상태
+ ActivationGPU가 모델을 간신히 불러왔다고 학습까지 가능한 것은 아닙니다.
3. PEFT가 줄여주는 것
PEFT는 기본 모델을 동결하고 작은 추가 파라미터만 학습합니다.
따라서 다음 자원을 줄일 수 있습니다.
학습 가능한 파라미터 수
Gradient 메모리
Optimizer 상태 메모리
Checkpoint 저장 공간
업로드·다운로드 용량PEFT Adapter는 전체 모델보다 훨씬 작기 때문에 저장과 공유가 편리합니다. 공식 PEFT 문서의 예시에서는 약 700MB 규모의 OPT-350M 기본 모델에 적용한 LoRA Adapter가 약 6.3MB로 저장됩니다. 실제 크기는 모델 구조, Rank, Target Module에 따라 달라집니다.
Full Fine-tuning 모델:
업무 A 모델 전체 저장
업무 B 모델 전체 저장
업무 C 모델 전체 저장
PEFT:
기본 모델 하나 저장
+ 업무 A Adapter
+ 업무 B Adapter
+ 업무 C AdapterAI 모델 저장소가 대형 창고에서 서랍장으로 줄어들 수 있습니다.
4. PEFT의 주요 방법
PEFT는 하나의 기술 이름이 아니라 여러 학습 방법을 포함하는 개념입니다.
LoRA
기존 가중치 변화량을 작은 두 행렬로 표현합니다.
가장 널리 사용되는 PEFT 방법AdaLoRA
학습 과정에서 Layer별 Rank를 동적으로 조정합니다.
IA³
Attention과 Feed Forward Layer의 Activation에 학습 가능한 Vector를 적용합니다.
Prompt Tuning
입력 앞에 학습 가능한 가상 Token을 추가합니다.
Prefix Tuning
Transformer Layer에 학습 가능한 Prefix 표현을 추가합니다.
DoRA
가중치를 크기와 방향으로 나누고 방향 업데이트에 LoRA를 활용합니다.
이번 편에서는 가장 기본적이고 널리 활용되는 LoRA를 중심으로 알아봅니다.
5. LoRA란?
LoRA는 다음 표현의 약자입니다.
Low-Rank AdaptationLoRA는 2021년 발표된 기법으로, 사전 학습 모델의 가중치를 동결한 뒤 Transformer Layer에 작은 Low-Rank 행렬을 삽입해 이 행렬만 학습합니다. 원 논문은 전체 모델을 다시 학습하는 대신 학습 가능한 Rank Decomposition 행렬을 추가하는 방법을 제안했습니다.
기본 모델의 가중치를 `W`라고 하겠습니다.
Full Fine-tuning은 `W` 자체를 수정합니다.
W
→ 학습
→ 새로운 WLoRA는 `W`를 동결하고 변화량 `ΔW`만 학습합니다.
기본 W:
동결
변화량 ΔW:
학습최종 계산에서는 다음처럼 함께 사용됩니다.
W + ΔW6. LoRA의 핵심 아이디어
LoRA는 Fine-tuning에서 필요한 가중치 변화량이 전체 차원을 모두 사용하지 않을 가능성에 주목합니다.
가중치 행렬이 매우 크더라도 실제 업무 적응에 필요한 변화는 더 낮은 차원으로 표현할 수 있다는 아이디어입니다.
거대한 가중치 변화량 ΔW
→ 작은 행렬 A
× 작은 행렬 B즉, 큰 변화량 행렬을 직접 학습하지 않고 작은 두 행렬의 곱으로 표현합니다.
ΔW = B × A예를 들어 원본 가중치가 다음 크기라고 가정하겠습니다.
768 × 768전체 변화량 행렬을 직접 학습하면 다음 파라미터가 필요합니다.
768 × 768
= 589,824개Rank를 8로 설정한 LoRA는 다음 두 행렬을 학습합니다.
A:
8 × 768
B:
768 × 8전체 파라미터:
8 × 768
+ 768 × 8
= 12,288개한 개의 768×768 가중치 행렬만 비교하면 학습 파라미터가 48분의 1로 줄어듭니다.
실제 모델에서는 LoRA를 어느 Layer에 적용하는지에 따라 전체 비율이 달라집니다.
7. Low-Rank란 무엇인가?
행렬의 Rank는 그 행렬이 표현하는 독립적인 정보 차원의 수와 관련이 있습니다.
다음과 같은 거대한 변화가 있다고 생각해 보겠습니다.
589,824개의 값을
모두 독립적으로 바꾸기하지만 실제 업무 적응에 필요한 변화가 몇 가지 주요 방향으로 압축될 수 있다면 다음처럼 표현할 수 있습니다.
제품 긍정 표현
제품 부정 표현
배송 불만 표현
가격 만족 표현LoRA의 Rank는 이러한 변화 방향을 담는 작은 공간의 크기라고 이해할 수 있습니다.
Rank가 작음
→ 학습 파라미터 적음
→ 메모리 절약
→ 표현 능력 제한 가능
Rank가 큼
→ 학습 파라미터 증가
→ 표현 능력 증가 가능
→ 메모리와 저장 공간 증가Rank가 크다고 항상 성능이 좋아지는 것은 아닙니다.
업무 복잡도와 데이터 크기에 맞는 실험이 필요합니다.
8. LoRA 수식 이해하기
원본 Linear Layer의 계산은 다음과 같습니다.
y = WxLoRA를 적용하면 다음과 같은 형태가 됩니다.
y = Wx + (α / r)BAx각 기호의 의미는 다음과 같습니다.
W
→ 동결된 원본 가중치
A, B
→ 학습하는 Low-Rank 행렬
r
→ LoRA Rank
α
→ LoRA Alpha
α / r
→ LoRA 변화량의 크기를 조절하는 ScalingLoRA 원 논문의 기본 아이디어는 원본 가중치를 고정하고 Low-Rank 행렬로 표현된 업데이트를 추가하는 것입니다. Hugging Face PEFT의 기본 LoRA 구현도 Rank와 Alpha를 이용해 Adapter의 크기와 Scaling을 구성합니다.
기본 모델은 자신의 지식을 유지합니다.
Wx
→ 기존 지식LoRA Adapter는 업무에 맞는 작은 보정값을 더합니다.
BAx
→ 새로운 업무 적응비유하면 다음과 같습니다.
기본 모델:
숙련된 요리사
LoRA Adapter:
새로운 레스토랑의 양념 레시피요리사를 처음부터 다시 교육하지 않고 새로운 양념법만 알려주는 방식입니다.
9. LoRA 파라미터 수 계산
원본 가중치 행렬 크기를 다음과 같이 가정하겠습니다.
출력 차원:
d_out
입력 차원:
d_inFull Fine-tuning 파라미터:
d_out × d_inLoRA 파라미터:
r × d_in
+ d_out × r정리하면 다음과 같습니다.
r × (d_in + d_out)예를 들어:
d_in:
4096
d_out:
4096
r:
16Full Fine-tuning:
4096 × 4096
= 16,777,216개LoRA:
16 × (4096 + 4096)
= 131,072개해당 행렬의 업데이트 파라미터만 비교하면 약 128분의 1입니다.
물론 실제 모델에서는 여러 Attention과 Feed Forward Layer에 LoRA가 들어갑니다.
10. LoRA의 장점
학습 파라미터 감소
전체 모델 대신 Adapter만 학습합니다.
GPU 메모리 절약
동결된 기본 파라미터에는 일반적인 Full Fine-tuning과 같은 Gradient와 Optimizer 상태가 필요하지 않습니다.
작은 Checkpoint
업무별로 전체 모델을 복제할 필요가 없습니다.
Adapter 교체
하나의 기본 모델에 여러 업무용 Adapter를 연결할 수 있습니다.
빠른 공유
수 MB에서 수백 MB 크기의 Adapter만 공유할 수 있습니다.
기본 모델 보존
기본 모델 가중치를 직접 수정하지 않습니다.
병합 가능
필요하면 LoRA 가중치를 기본 모델에 병합해 독립 모델로 만들 수 있습니다.
11. LoRA의 한계
LoRA가 모든 문제를 자동으로 해결하는 것은 아닙니다.
Full Fine-tuning보다 성능이 낮을 수 있음
업무 변화가 복잡하거나 데이터가 충분히 많다면 전체 Fine-tuning이 더 좋은 성능을 낼 수 있습니다.
Rank 선택 필요
Rank가 너무 작으면 학습 능력이 부족할 수 있습니다.
Target Module 선택 필요
잘못된 Layer에 적용하면 학습 효과가 제한됩니다.
기본 모델 필요
Adapter만 저장한 경우 추론할 때 원본 기본 모델도 필요합니다.
메모리 문제가 완전히 사라지지 않음
기본 모델 가중치와 Activation은 여전히 메모리를 사용합니다.
학습 시간 단축이 보장되지는 않음
Gradient와 Optimizer 메모리는 줄지만 Forward·Backward 계산은 기본 모델을 통과합니다.
LoRA:
모델 전체 계산은 수행
하지만
업데이트는 Adapter만 수행PEFT는 GPU를 투명하게 만드는 주문이 아닙니다.
가방을 가볍게 만들어 주지만 산 자체를 없애지는 않습니다.
12. 현재 PEFT 버전
2026년 8월 2일 기준 PyPI의 최신 PEFT 안정 버전은 0.20.0이며, 2026년 7월 28일 공개되었습니다. 현재 PEFT 패키지는 Python 3.10 이상을 요구합니다. 최신 Transformers의 PEFT 통합 기능은 PEFT 0.19.1 이상을 요구합니다.
버전 확인:
import peft
print(
peft.__version__
)터미널:
python -m pip show peft라이브러리 버전에 따라 지원되는 설정과 API가 달라질 수 있습니다.
13. 개발 환경 설치
Windows
python -m venv venv
venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install \
"transformers[torch]" \
datasets \
accelerate \
peft \
scikit-learnWindows 명령 프롬프트에서는 한 줄로 입력합니다.
python -m pip install "transformers[torch]" datasets accelerate peft scikit-learnmacOS·Linux
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
python -m pip install \
"transformers[torch]" \
datasets \
accelerate \
peft \
scikit-learnQLoRA 패키지
python -m pip install \
bitsandbytes \
peft \
accelerate \
transformersBitsandbytes는 8비트와 4비트 양자화 기능을 Transformers에 연결하며, 실제 지원 기능은 운영체제와 GPU·CPU Backend에 따라 달라집니다.
설치 확인
import datasets
import peft
import torch
import transformers
print(
f"Transformers: "
f"{transformers.__version__}"
)
print(
f"PEFT: "
f"{peft.__version__}"
)
print(
f"Datasets: "
f"{datasets.__version__}"
)
print(
f"PyTorch: "
f"{torch.__version__}"
)
print(
f"CUDA 사용 가능: "
f"{torch.cuda.is_available()}"
)14. PEFT의 기본 구성 요소
LoRA를 적용할 때 자주 사용하는 객체는 다음과 같습니다.
from peft import (
LoraConfig,
PeftModel,
TaskType,
get_peft_model
)LoraConfig
LoRA의 Rank, Alpha, Target Module 등을 설정합니다.
get_peft_model
기본 모델에 LoRA Adapter를 삽입합니다.
PeftModel
기본 모델과 Adapter가 결합된 모델입니다.
TaskType
문장 분류, 텍스트 생성, 번역 등 모델의 작업 유형을 지정합니다.
prepare_model_for_kbit_training
양자화된 모델을 QLoRA 학습에 사용할 수 있도록 준비합니다.
15. LoraConfig 이해하기
기본 설정 예:
from peft import (
LoraConfig,
TaskType
)
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
inference_mode=False,
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=[
"q_lin",
"v_lin"
],
bias="none",
modules_to_save=[
"pre_classifier",
"classifier"
]
)주요 옵션:
task_type
→ 모델 작업 종류
inference_mode
→ 학습용인지 추론용인지
r
→ Low-Rank 크기
lora_alpha
→ Adapter Scaling
lora_dropout
→ LoRA 입력 Dropout
target_modules
→ LoRA를 삽입할 Layer
bias
→ Bias 파라미터 학습 여부
modules_to_save
→ LoRA 외에 함께 학습·저장할 Layer현재 PEFT의 `LoraConfig`는 Rank, Alpha, Dropout, Target Module, 추가 저장 Module과 다양한 초기화·LoRA 변형 옵션을 제공합니다.
16. Rank r의 의미
r=8Rank는 LoRA 행렬의 내부 차원입니다.
작은 Rank
r=4
r=8
→ 파라미터 적음
→ 저장 공간 적음
→ 학습 능력 제한 가능
큰 Rank
r=32
r=64
→ 파라미터 많음
→ 표현 능력 증가 가능
→ 메모리와 저장 공간 증가일반적인 시작 후보:
4
8
16
32
64작은 분류 작업에서는 8이나 16부터 실험할 수 있습니다.
대형 생성 모델의 복잡한 Instruction Tuning에서는 더 큰 Rank가 필요할 수도 있습니다.
정답은 데이터와 모델에 따라 달라집니다.
17. lora_alpha의 의미
lora_alpha=16LoRA의 기본 Scaling은 일반적으로 다음과 관련됩니다.
lora_alpha / r예:
r:
8
lora_alpha:
16
Scaling:
2또 다른 예:
r:
16
lora_alpha:
16
Scaling:
1`lora_alpha`는 LoRA 업데이트가 원본 모델 출력에 어느 정도 영향을 주는지 조절합니다.
Alpha가 너무 작음
→ Adapter 영향이 약할 수 있음
Alpha가 너무 큼
→ 학습 불안정 가능Rank를 변경할 때 Alpha도 함께 실험하는 것이 좋습니다.
18. lora_dropout의 의미
lora_dropout=0.05LoRA 경로의 입력 일부를 학습 중 무작위로 비활성화합니다.
0.0
→ Dropout 없음
0.05
→ 약 5% 비활성화
0.1
→ 약 10% 비활성화작은 데이터에서는 과적합을 줄이는 데 도움이 될 수 있습니다.
하지만 너무 높으면 Adapter가 충분히 학습하지 못할 수 있습니다.
LoRA:
“새 업무를 배우겠습니다.”
Dropout 0.9:
“오늘 배운 내용의 대부분을 잠시 가리겠습니다.”규칙적인 휴식도 정도가 있습니다.
19. target_modules의 의미
`target_modules`는 LoRA Adapter를 삽입할 Layer를 지정합니다.
target_modules=[
"q_lin",
"v_lin"
]DistilBERT Attention에는 일반적으로 다음 Linear Layer가 있습니다.
q_lin
→ Query
k_lin
→ Key
v_lin
→ Value
out_lin
→ Attention 출력Llama, Gemma, Qwen 같은 모델에서는 다음 이름을 자주 볼 수 있습니다.
q_proj
k_proj
v_proj
o_proj
gate_proj
up_proj
down_projPEFT는 여러 대표 아키텍처에 기본 Target Module 정보를 제공하지만, 다른 Layer를 대상으로 삼거나 자동 매핑이 없는 모델에서는 `target_modules`를 직접 지정해야 합니다.
모델마다 모듈 이름이 다릅니다.
BERT:
query, value
DistilBERT:
q_lin, v_lin
Llama:
q_proj, v_proj
GPT-2:
c_attn다른 모델의 예제를 그대로 복사하면 다음 오류가 발생할 수 있습니다.
Target modules not found20. modules_to_save가 필요한 이유
문장 분류 모델에는 사전 학습 기본 모델 위에 분류 Head가 추가됩니다.
DistilBERT 본체
→ pre_classifier
→ classifier기본 체크포인트의 분류 Head는 현재 감정 분석 업무를 학습하지 않은 새 Layer일 수 있습니다.
LoRA Layer만 학습하고 분류 Head를 동결하면 분류기가 제대로 학습되지 않을 수 있습니다.
따라서 다음처럼 지정합니다.
modules_to_save=[
"pre_classifier",
"classifier"
]`modules_to_save`에 지정한 Layer는 LoRA Adapter와 함께 학습되고 Checkpoint에 저장됩니다. 문장 분류처럼 새 Classification Head가 추가되는 작업에서 특히 중요합니다.
LoRA Adapter:
학습
분류 Head:
학습
기본 Transformer:
동결21. task_type 설정하기
from peft import TaskType대표 작업 유형:
TaskType.SEQ_CLS
→ 문장 분류
TaskType.TOKEN_CLS
→ Token 분류
TaskType.CAUSAL_LM
→ 다음 Token 생성
TaskType.SEQ_2_SEQ_LM
→ 번역·요약
TaskType.QUESTION_ANS
→ 질문 답변
TaskType.FEATURE_EXTRACTION
→ 특징 추출감정 분석은 문장 분류이므로 다음처럼 설정합니다.
task_type=TaskType.SEQ_CLS대화형 생성 모델은 다음을 사용합니다.
task_type=TaskType.CAUSAL_LM22. 모델의 모듈 이름 확인하기
Target Module을 결정하려면 모델의 구조를 확인해야 합니다.
for name, module in (
base_model.named_modules()
):
print(
name,
module.__class__.__name__
)Linear Layer만 확인:
import torch
for name, module in (
base_model.named_modules()
):
if isinstance(
module,
torch.nn.Linear
):
print(name)DistilBERT에서는 다음과 비슷한 이름을 발견할 수 있습니다.
distilbert.transformer.layer.0.attention.q_lin
distilbert.transformer.layer.0.attention.k_lin
distilbert.transformer.layer.0.attention.v_lin
distilbert.transformer.layer.0.attention.out_lin마지막 모듈 이름만 수집할 수도 있습니다.
linear_module_names = sorted({
name.split(".")[-1]
for name, module
in base_model.named_modules()
if isinstance(
module,
torch.nn.Linear
)
})
print(
linear_module_names
)예상 결과:
[
'classifier',
'ffn.lin1',
'ffn.lin2',
'k_lin',
'out_lin',
'pre_classifier',
'q_lin',
'v_lin'
]실제 출력은 모델 구현과 버전에 따라 달라질 수 있습니다.
23. DistilBERT에 LoRA 적용하기
기본 모델을 불러옵니다.
from transformers import (
AutoModelForSequenceClassification
)
MODEL_ID = (
"distilbert/"
"distilbert-base-uncased"
)
base_model = (
AutoModelForSequenceClassification
.from_pretrained(
MODEL_ID,
num_labels=2
)
)LoRA 설정:
from peft import (
LoraConfig,
TaskType,
get_peft_model
)
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
inference_mode=False,
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=[
"q_lin",
"v_lin"
],
bias="none",
modules_to_save=[
"pre_classifier",
"classifier"
]
)PEFT 모델 생성:
model = get_peft_model(
base_model,
lora_config
)이제 기본 모델의 대부분은 동결되고 LoRA와 분류 Head만 학습할 수 있습니다.
Base Model
→ get_peft_model()
→ PeftModelForSequenceClassification24. 학습 가능한 파라미터 확인하기
PEFT 모델에는 학습 가능한 파라미터를 출력하는 기능이 있습니다.
model.print_trainable_parameters()출력 형태:
trainable params:
...
all params:
...
trainable%:
...`print_trainable_parameters()`와 `get_nb_trainable_parameters()`를 이용하면 Adapter를 포함한 전체 PEFT 모델에서 학습 가능한 파라미터 수와 전체 파라미터 수를 확인할 수 있습니다.
직접 가져오기:
(
trainable_parameters,
total_parameters
) = model.get_nb_trainable_parameters()
trainable_ratio = (
trainable_parameters
/ total_parameters
* 100
)
print(
f"학습 파라미터: "
f"{trainable_parameters:,}"
)
print(
f"전체 파라미터: "
f"{total_parameters:,}"
)
print(
f"학습 비율: "
f"{trainable_ratio:.4f}%"
)실제로 학습되는 이름 확인:
for name, parameter in (
model.named_parameters()
):
if parameter.requires_grad:
print(name)출력에는 다음과 같은 이름이 포함될 수 있습니다.
lora_A
lora_B
pre_classifier
classifier25. Full Fine-tuning과 LoRA 비교하기
Full Fine-tuning
기본 모델 전체:
학습
분류 Head:
학습LoRA
기본 모델 전체:
동결
LoRA A·B:
학습
분류 Head:
학습비교 코드를 작성할 수 있습니다.
def count_parameters(
model
) -> tuple[int, int, float]:
total_count = sum(
parameter.numel()
for parameter
in model.parameters()
)
trainable_count = sum(
parameter.numel()
for parameter
in model.parameters()
if parameter.requires_grad
)
ratio = (
trainable_count
/ total_count
* 100
)
return (
trainable_count,
total_count,
ratio
)사용:
(
trainable_count,
total_count,
ratio
) = count_parameters(
model
)
print(
f"{trainable_count:,} / "
f"{total_count:,}"
)
print(
f"{ratio:.4f}%"
)파라미터 수 감소가 학습 시간과 메모리 사용량에 어느 정도 영향을 주는지는 모델, Batch Size, 입력 길이, Optimizer와 장치에 따라 달라집니다.
26. 영화 리뷰 데이터 불러오기
지난 편과 동일한 영화 리뷰 데이터를 사용합니다.
from datasets import load_dataset
DATASET_ID = (
"cornell-movie-review-data/"
"rotten_tomatoes"
)
dataset = load_dataset(
DATASET_ID
)
print(dataset)라벨 정보:
label_feature = (
dataset["train"]
.features["label"]
)
label_names = (
label_feature.names
)
print(
label_names
)라벨 매핑:
id2label = {
label_id: label_name.upper()
for label_id, label_name
in enumerate(label_names)
}
label2id = {
label_name.upper(): label_id
for label_id, label_name
in enumerate(label_names)
}27. 데이터 토큰화
Tokenizer:
from transformers import (
AutoTokenizer
)
tokenizer = (
AutoTokenizer
.from_pretrained(
MODEL_ID
)
)전처리 함수:
def tokenize_batch(
batch
):
return tokenizer(
batch["text"],
truncation=True,
max_length=256
)전체 Split 처리:
tokenized_dataset = dataset.map(
tokenize_batch,
batched=True,
desc="영화 리뷰 토큰화 중"
)Dynamic Padding:
from transformers import (
DataCollatorWithPadding
)
data_collator = (
DataCollatorWithPadding(
tokenizer=tokenizer,
return_tensors="pt"
)
)28. 평가 지표 작성
import numpy as np
from sklearn.metrics import (
accuracy_score,
precision_recall_fscore_support
)
from transformers import (
EvalPrediction
)
def compute_metrics(
eval_prediction: EvalPrediction
) -> dict[str, float]:
logits = (
eval_prediction.predictions
)
labels = (
eval_prediction.label_ids
)
if isinstance(
logits,
tuple
):
logits = logits[0]
predictions = np.argmax(
logits,
axis=-1
)
accuracy = accuracy_score(
labels,
predictions
)
(
precision,
recall,
f1,
_
) = precision_recall_fscore_support(
labels,
predictions,
average="binary",
zero_division=0
)
return {
"accuracy": float(
accuracy
),
"precision": float(
precision
),
"recall": float(
recall
),
"f1": float(
f1
)
}29. TrainingArguments 구성
LoRA는 학습 파라미터 수가 적기 때문에 Full Fine-tuning보다 조금 높은 Learning Rate를 사용하는 사례가 많습니다.
하지만 모델과 데이터에 따라 반드시 실험해야 합니다.
from transformers import (
TrainingArguments
)
training_args = TrainingArguments(
output_dir=(
"outputs/"
"distilbert-lora"
),
learning_rate=1e-4,
lr_scheduler_type="linear",
warmup_ratio=0.1,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
num_train_epochs=5,
weight_decay=0.01,
eval_strategy="epoch",
save_strategy="epoch",
logging_strategy="steps",
logging_steps=25,
logging_first_step=True,
load_best_model_at_end=True,
metric_for_best_model="f1",
greater_is_better=True,
save_total_limit=2,
report_to="none",
seed=2026,
data_seed=2026
)Full Fine-tuning에서 사용한 `2e-5`보다 높은 `1e-4`로 시작했습니다.
Full Fine-tuning 시작 후보:
1e-5
2e-5
3e-5
LoRA 시작 후보:
5e-5
1e-4
2e-4절대적인 규칙은 아닙니다.
30. Trainer에 PEFT 모델 연결
PEFT 모델은 일반 Transformers 모델처럼 Trainer에 전달할 수 있습니다.
from transformers import (
EarlyStoppingCallback,
Trainer
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=(
tokenized_dataset["train"]
),
eval_dataset=(
tokenized_dataset[
"validation"
]
),
processing_class=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics,
callbacks=[
EarlyStoppingCallback(
early_stopping_patience=2,
early_stopping_threshold=0.001
)
]
)Trainer는 `requires_grad=True`인 Adapter 파라미터와 추가 학습 Module만 업데이트합니다. PEFT 모델의 Trainer Checkpoint에는 기본 모델 전체가 아니라 Adapter 가중치와 설정이 저장됩니다.
31. LoRA Fine-tuning 시작
train_result = trainer.train()학습 중 실제로 업데이트되는 대상:
q_lin LoRA A
q_lin LoRA B
v_lin LoRA A
v_lin LoRA B
pre_classifier
classifier업데이트되지 않는 대상:
Embedding
기본 Attention 가중치
기본 Feed Forward 가중치
기본 Layer Normalization학습 결과:
trainer.log_metrics(
"train",
train_result.metrics
)
trainer.save_metrics(
"train",
train_result.metrics
)
trainer.save_state()32. Validation·Test 평가
Validation 평가:
validation_metrics = (
trainer.evaluate()
)
print(
validation_metrics
)Test 평가:
test_metrics = trainer.evaluate(
eval_dataset=(
tokenized_dataset["test"]
),
metric_key_prefix="test"
)
print(
test_metrics
)예상 출력 구조:
eval_loss
eval_accuracy
eval_precision
eval_recall
eval_f1Full Fine-tuning 결과와 비교할 항목:
Validation F1
Test F1
학습 시간
최대 GPU 메모리
학습 파라미터 수
Checkpoint 크기33. LoRA Adapter 저장하기
from pathlib import Path
ADAPTER_DIR = Path(
"models/"
"sentiment-distilbert-lora"
)
trainer.save_model(
str(
ADAPTER_DIR
)
)
tokenizer.save_pretrained(
ADAPTER_DIR
)또는 모델에서 직접 저장할 수 있습니다.
model.save_pretrained(
ADAPTER_DIR
)PEFT 모델의 `save_pretrained()`는 기본적으로 Adapter 파라미터와 Adapter 설정을 저장합니다.
34. Adapter Checkpoint 구조
저장 폴더 예:
models/sentiment-distilbert-lora/
├─ adapter_config.json
├─ adapter_model.safetensors
├─ README.md
├─ tokenizer.json
├─ tokenizer_config.json
├─ special_tokens_map.json
└─ vocab.txtadapter_model.safetensors
LoRA와 `modules_to_save`로 지정한 학습 파라미터를 저장합니다.
adapter_config.json
다음 정보를 저장합니다.
PEFT 방법
기본 모델 ID
Rank
Alpha
Dropout
Target Module
Task TypeREADME.md
Adapter Model Card 초안입니다.
PEFT Checkpoint는 기본 모델 전체가 아니라 Adapter의 `state_dict`만 저장합니다. 따라서 Adapter를 불러오려면 원본 기본 모델에 접근할 수 있어야 합니다.
Adapter만 있음
+ 기본 모델 없음
→ 완전한 추론 불가부품 설명서는 있지만 자동차 본체가 없는 상태입니다.
35. 저장된 Adapter 불러오기
방법 1: 기본 모델을 직접 불러온 뒤 Adapter 연결
from peft import PeftModel
from transformers import (
AutoModelForSequenceClassification
)
base_model = (
AutoModelForSequenceClassification
.from_pretrained(
MODEL_ID,
num_labels=2,
id2label=id2label,
label2id=label2id
)
)
peft_model = PeftModel.from_pretrained(
base_model,
ADAPTER_DIR
)추론 모드:
peft_model.eval()방법 2: AutoPeftModel 사용
from peft import (
AutoPeftModelForSequenceClassification
)
peft_model = (
AutoPeftModelForSequenceClassification
.from_pretrained(
ADAPTER_DIR
)
)현재 Transformers와 PEFT는 `adapter_config.json`에서 기본 모델 정보를 읽어 기본 모델과 Adapter를 자동으로 연결하는 기능을 제공합니다.
36. Pipeline으로 추론하기
import torch
from transformers import pipeline
def select_pipeline_device():
if torch.cuda.is_available():
return 0
if torch.backends.mps.is_available():
return "mps"
return -1Pipeline:
classifier = pipeline(
task="text-classification",
model=peft_model,
tokenizer=tokenizer,
device=select_pipeline_device()
)추론:
reviews = [
(
"The movie was exciting "
"and beautifully directed."
),
(
"The story was dull "
"and completely predictable."
)
]
results = classifier(
reviews,
batch_size=2,
truncation=True
)
for review, result in zip(
reviews,
results
):
print(
f"리뷰: {review}"
)
print(
f"예측: {result['label']}"
)
print(
f"확률: "
f"{result['score']:.2%}"
)
print(
"-" * 50
)37. Adapter 활성화와 비활성화
Adapter를 비활성화하면 기본 모델의 결과를 확인할 수 있습니다.
Transformers 통합 방식으로 Adapter를 추가한 모델에서는 다음 기능을 사용할 수 있습니다.
model.disable_adapters()다시 활성화:
model.enable_adapters()현재 활성화된 Adapter 확인:
print(
model.active_adapters()
)PEFT 통합 기능은 Adapter 활성화·비활성화·전환·삭제 기능을 제공합니다.
기본 모델과 Adapter 모델 결과를 비교할 수 있습니다.
Adapter 비활성화
→ 기본 모델 결과
Adapter 활성화
→ 업무에 적응한 결과단, 새롭게 학습한 분류 Head가 Adapter에 포함된 구조라면 기본 모델 단독 결과 해석에 주의해야 합니다.
38. 여러 Adapter 관리하기
하나의 기본 모델에 여러 Adapter를 연결할 수 있습니다.
model.load_adapter(
"adapters/movie_sentiment",
adapter_name="movie"
)
model.load_adapter(
"adapters/product_sentiment",
adapter_name="product"
)영화 리뷰 Adapter 활성화:
model.set_adapter(
"movie"
)상품 리뷰 Adapter 활성화:
model.set_adapter(
"product"
)필요 없는 Adapter 삭제:
model.delete_adapter(
"movie"
)하나의 모델이 여러 Adapter를 메모리에 보유하고 필요에 따라 특정 Adapter를 활성화할 수 있습니다.
기본 모델:
하나
Adapter:
영화 감정
상품 감정
고객 문의
스팸 탐지기본 모델은 사무실이고 Adapter는 부서별 업무 매뉴얼입니다.
39. Adapter 병합하기
추론 환경에서 기본 모델과 Adapter를 따로 관리하지 않고 하나의 모델로 만들고 싶을 수 있습니다.
기본 모델
+ LoRA Adapter
→ 병합된 모델병합 전:
기본 모델 필요
Adapter 필요
PEFT 실행 구조 필요병합 후:
일반 Transformers 모델처럼 사용
Adapter 교체 불가
전체 모델 크기로 저장40. merge_and_unload 이해하기
merged_model = (
peft_model.merge_and_unload(
safe_merge=True
)
)저장:
MERGED_MODEL_DIR = Path(
"models/"
"sentiment-distilbert-merged"
)
merged_model.save_pretrained(
MERGED_MODEL_DIR
)
tokenizer.save_pretrained(
MERGED_MODEL_DIR
)`merge_and_unload()`는 활성 Adapter의 가중치를 기본 모델에 병합하고 Adapter Layer를 제거해 독립적인 기본 모델 형태로 반환합니다. `safe_merge=True`는 병합 과정에서 NaN과 같은 이상값을 검사하도록 합니다.
장점
PEFT 없이 일반 모델처럼 배포 가능
Adapter 계산 Layer 제거
단일 모델 폴더로 관리단점
파일 크기가 기본 모델 전체 크기로 증가
Adapter 교체 불가
원본 기본 모델로 쉽게 복귀 불가
여러 업무 Adapter 관리 장점 감소배포 구조에 따라 병합 여부를 결정합니다.
41. QLoRA란?
QLoRA는 다음 두 기술을 결합합니다.
Quantization
+ LoRA
= QLoRA기본 모델 가중치를 4비트로 양자화해 메모리를 줄이고, 그 위에 LoRA Adapter를 추가해 학습합니다.
기본 모델:
FP16·BF16
→ 4비트 양자화
→ 동결
LoRA Adapter:
상대적으로 높은 정밀도
→ 학습Hugging Face 공식 PEFT 문서는 QLoRA를 기본 모델을 4비트로 양자화한 뒤 LoRA를 학습하는 방법으로 설명합니다.
LoRA:
기본 모델 정밀도 유지
+ Adapter 학습
QLoRA:
기본 모델 4비트 압축
+ Adapter 학습LoRA가 작은 가방을 드는 방식이라면 QLoRA는 짐을 압축팩에 넣은 뒤 작은 가방까지 드는 방식입니다.
42. 양자화와 LoRA의 결합
일반적으로 양자화된 모델 전체를 직접 Fine-tuning하는 것은 수치 안정성 문제 때문에 어렵습니다.
하지만 QLoRA에서는 양자화된 기본 가중치를 수정하지 않습니다.
4비트 기본 모델:
읽기 전용
LoRA 파라미터:
학습 가능이 구조 덕분에 대형 모델을 제한된 GPU 메모리에서 학습할 가능성이 커집니다.
공식 PEFT Quantization 가이드는 양자화된 모델 위에 PEFT Adapter를 추가하면 양자화된 기본 모델을 유지하면서 추가 파라미터를 학습할 수 있다고 설명합니다.
43. BitsAndBytesConfig
4비트 양자화 설정:
import torch
from transformers import (
BitsAndBytesConfig
)
compute_dtype = (
torch.bfloat16
if (
torch.cuda.is_available()
and torch.cuda.is_bf16_supported()
)
else torch.float16
)
quantization_config = (
BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=(
compute_dtype
)
)
)load_in_4bit
기본 모델을 4비트로 불러옵니다.
bnb_4bit_quant_type
nf4정규분포 형태의 신경망 가중치에 맞게 설계된 4비트 형식입니다.
bnb_4bit_use_double_quant
양자화에 사용되는 값도 다시 양자화해 메모리를 추가로 줄이는 방식입니다.
bnb_4bit_compute_dtype
가중치는 4비트로 저장하지만 실제 계산에는 BF16이나 FP16을 사용합니다.
공식 문서는 4비트 QLoRA 설정에 `load_in_4bit`, `nf4`, Double Quantization, BF16 계산 자료형을 구성할 수 있다고 안내합니다.
44. prepare_model_for_kbit_training
양자화 모델 로딩:
from transformers import (
AutoModelForCausalLM
)
LLM_MODEL_ID = (
"사용할_생성_모델_ID"
)
base_model = (
AutoModelForCausalLM
.from_pretrained(
LLM_MODEL_ID,
quantization_config=(
quantization_config
),
device_map="auto"
)
)학습 준비:
from peft import (
prepare_model_for_kbit_training
)
base_model = (
prepare_model_for_kbit_training(
base_model
)
)`prepare_model_for_kbit_training()`은 양자화 모델을 PEFT 학습에 적합하도록 전처리합니다. 공식 QLoRA 가이드도 4비트 모델을 불러온 뒤 이 함수를 호출하도록 안내합니다.
Gradient Checkpointing을 사용한다면 다음 옵션을 전달할 수 있습니다.
base_model = (
prepare_model_for_kbit_training(
base_model,
use_gradient_checkpointing=True
)
)45. QLoRA 설정 예제
from peft import (
LoraConfig,
TaskType,
get_peft_model
)
qlora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules="all-linear",
bias="none"
)
qlora_model = get_peft_model(
base_model,
qlora_config
)
qlora_model.print_trainable_parameters()`target_modules="all-linear"`는 모델의 Linear·Conv1D Layer에 폭넓게 LoRA를 적용하는 QLoRA 스타일 설정으로 사용할 수 있습니다. 공식 PEFT Quantization 가이드는 QLoRA와 LoftQ 계열 설정에서 가능한 많은 Linear Layer를 대상으로 삼는 방식을 안내합니다.
다만 모든 프로젝트에서 `all-linear`가 최적이라는 뜻은 아닙니다.
장점:
학습 능력 증가 가능
단점:
Adapter 파라미터 증가
학습 시간 증가
메모리 증가46. LoRA와 QLoRA 비교
| 구분 | LoRA | QLoRA |
|---|---|---|
| 기본 모델 | 일반 정밀도 | 주로 4비트 |
| 기본 모델 학습 | 동결 | 동결 |
| Adapter | 학습 | 학습 |
| 메모리 사용 | Full FT보다 적음 | LoRA보다 더 적을 수 있음 |
| 수치 구성 | 비교적 단순 | 양자화 설정 필요 |
| 설치 | PEFT 중심 | PEFT + bitsandbytes |
| 대표 활용 | 중소형 모델, 분류 | 대형 생성 모델 |
| 병합 | 가능 | 양자화·정밀도 구조 확인 필요 |
| 환경 호환성 | 비교적 넓음 | Backend 호환성 확인 필요 |
Full Fine-tuning:
건물 전체 리모델링
LoRA:
업무 공간에 조립식 방 추가
QLoRA:
건물을 압축 설계하고 조립식 방 추가47. Rank와 Target Module 선택법
작은 Rank부터 시작
r=8
또는
r=16성능이 부족하다면 다음을 비교합니다.
r=32
r=64Attention 일부 적용
target_modules=[
"q_proj",
"v_proj"
]학습 파라미터가 적습니다.
Attention 전체 적용
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj"
]적응 능력이 증가할 수 있습니다.
모든 Linear Layer 적용
target_modules="all-linear"QLoRA에서 자주 검토하는 설정입니다.
실험표 작성
| 실험 | Rank | Target | Learning Rate | F1 | Adapter 크기 |
|---|---|---|---|---|---|
| -- | ---: | ---------- | ------------: | -: | ---------: |
| A | 8 | Q, V | 1e-4 | 측정 | 측정 |
| B | 16 | Q, V | 1e-4 | 측정 | 측정 |
| C | 16 | Q, K, V, O | 1e-4 | 측정 | 측정 |
| D | 32 | All Linear | 5e-5 | 측정 | 측정 |
AI 학습 설정은 느낌보다 실험표가 강합니다.
48. LoRA 학습률 설정
LoRA는 기본 모델을 동결하고 새로 추가된 Adapter를 학습하기 때문에 Full Fine-tuning보다 높은 Learning Rate를 사용하는 경우가 많습니다.
시작 후보:
5e-5
1e-4
2e-4
3e-4하지만 다음 조건에 따라 달라집니다.
모델 크기
Rank
데이터 크기
Batch Size
Target Module 수
분류 작업인지 생성 작업인지
Adapter 초기화 방식학습률이 너무 높으면 다음 현상이 나타날 수 있습니다.
Training Loss 급격한 변동
Validation 성능 하락
NaN Loss
불안정한 생성 결과너무 낮으면 다음 문제가 발생할 수 있습니다.
Loss 감소가 매우 느림
Adapter가 충분히 학습되지 않음
Epoch를 늘려도 개선이 적음49. 메모리 사용량 줄이기
Batch Size 줄이기
per_device_train_batch_size=2Gradient Accumulation
gradient_accumulation_steps=8유효 Batch Size:
2 × 8
= 16입력 길이 줄이기
max_length=128Gradient Checkpointing
gradient_checkpointing=True혼합 정밀도
bf16=True또는:
fp16=TrueQLoRA 적용
load_in_4bit=True적은 Target Module
target_modules=[
"q_proj",
"v_proj"
]작은 Rank
r=8평가 결과 CPU 이동
eval_accumulation_steps=8메모리 최적화는 하나의 버튼이 아니라 여러 손잡이를 함께 조절하는 작업입니다.
50. 실전 프로젝트 전체 코드
다음 내용을 `train_lora.py`로 저장합니다.
from pathlib import Path
import json
import time
import numpy as np
import torch
from datasets import load_dataset
from peft import (
LoraConfig,
PeftModel,
TaskType,
get_peft_model
)
from sklearn.metrics import (
accuracy_score,
confusion_matrix,
precision_recall_fscore_support
)
from transformers import (
AutoModelForSequenceClassification,
AutoTokenizer,
DataCollatorWithPadding,
EarlyStoppingCallback,
EvalPrediction,
Trainer,
TrainingArguments,
pipeline,
set_seed
)
DATASET_ID = (
"cornell-movie-review-data/"
"rotten_tomatoes"
)
MODEL_ID = (
"distilbert/"
"distilbert-base-uncased"
)
OUTPUT_DIR = Path(
"outputs/"
"sentiment-distilbert-lora"
)
ADAPTER_DIR = Path(
"models/"
"sentiment-distilbert-lora"
)
MERGED_MODEL_DIR = Path(
"models/"
"sentiment-distilbert-merged"
)
SEED = 2026
def select_precision() -> tuple[
bool,
bool
]:
"""CUDA 환경에 맞는 혼합 정밀도를 선택합니다."""
if not torch.cuda.is_available():
return (
False,
False
)
use_bf16 = (
torch.cuda.is_bf16_supported()
)
use_fp16 = not use_bf16
return (
use_bf16,
use_fp16
)
def select_pipeline_device():
"""Pipeline 추론 장치를 선택합니다."""
if torch.cuda.is_available():
return 0
if torch.backends.mps.is_available():
return "mps"
return -1
def compute_metrics(
eval_prediction: EvalPrediction
) -> dict[str, float]:
"""분류 평가 지표를 계산합니다."""
logits = (
eval_prediction.predictions
)
labels = (
eval_prediction.label_ids
)
if isinstance(
logits,
tuple
):
logits = logits[0]
predictions = np.argmax(
logits,
axis=-1
)
accuracy = accuracy_score(
labels,
predictions
)
(
precision,
recall,
f1,
_
) = precision_recall_fscore_support(
labels,
predictions,
average="binary",
zero_division=0
)
return {
"accuracy": float(
accuracy
),
"precision": float(
precision
),
"recall": float(
recall
),
"f1": float(
f1
)
}
def print_parameter_report(
model
) -> dict[str, float | int]:
"""전체·학습 파라미터 수를 출력합니다."""
(
trainable_parameters,
total_parameters
) = model.get_nb_trainable_parameters()
trainable_ratio = (
trainable_parameters
/ total_parameters
* 100
)
print(
"\n[파라미터 현황]"
)
print(
f"학습 파라미터: "
f"{trainable_parameters:,}"
)
print(
f"전체 파라미터: "
f"{total_parameters:,}"
)
print(
f"학습 비율: "
f"{trainable_ratio:.6f}%"
)
return {
"trainable_parameters": (
trainable_parameters
),
"total_parameters": (
total_parameters
),
"trainable_ratio": (
trainable_ratio
)
}
def main() -> None:
set_seed(
SEED
)
OUTPUT_DIR.mkdir(
parents=True,
exist_ok=True
)
ADAPTER_DIR.mkdir(
parents=True,
exist_ok=True
)
print(
"[1/12] 영화 리뷰 데이터 로딩"
)
dataset = load_dataset(
DATASET_ID
)
print(dataset)
label_feature = (
dataset["train"]
.features["label"]
)
label_names = (
label_feature.names
)
id2label = {
label_id: label_name.upper()
for label_id, label_name
in enumerate(label_names)
}
label2id = {
label_name.upper(): label_id
for label_id, label_name
in enumerate(label_names)
}
print(
f"라벨: {id2label}"
)
print(
"[2/12] Tokenizer 로딩"
)
tokenizer = (
AutoTokenizer
.from_pretrained(
MODEL_ID
)
)
def tokenize_batch(
batch
):
return tokenizer(
batch["text"],
truncation=True,
max_length=256
)
print(
"[3/12] 데이터 토큰화"
)
tokenized_dataset = (
dataset.map(
tokenize_batch,
batched=True,
desc="영화 리뷰 토큰화 중"
)
)
data_collator = (
DataCollatorWithPadding(
tokenizer=tokenizer,
return_tensors="pt"
)
)
print(
"[4/12] 기본 분류 모델 로딩"
)
base_model = (
AutoModelForSequenceClassification
.from_pretrained(
MODEL_ID,
num_labels=len(
label_names
),
id2label=id2label,
label2id=label2id
)
)
print(
"[5/12] LoRA Adapter 구성"
)
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
inference_mode=False,
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=[
"q_lin",
"v_lin"
],
bias="none",
modules_to_save=[
"pre_classifier",
"classifier"
]
)
model = get_peft_model(
base_model,
lora_config
)
parameter_report = (
print_parameter_report(
model
)
)
parameter_report_path = (
OUTPUT_DIR
/ "parameter_report.json"
)
parameter_report_path.write_text(
json.dumps(
parameter_report,
ensure_ascii=False,
indent=2
),
encoding="utf-8"
)
use_bf16, use_fp16 = (
select_precision()
)
print(
f"CUDA 사용: "
f"{torch.cuda.is_available()}"
)
print(
f"BF16 사용: {use_bf16}"
)
print(
f"FP16 사용: {use_fp16}"
)
print(
"[6/12] 학습 설정 생성"
)
training_args = TrainingArguments(
output_dir=str(
OUTPUT_DIR
),
learning_rate=1e-4,
lr_scheduler_type="linear",
warmup_ratio=0.1,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
gradient_accumulation_steps=1,
num_train_epochs=5,
weight_decay=0.01,
eval_strategy="epoch",
save_strategy="epoch",
logging_strategy="steps",
logging_steps=25,
logging_first_step=True,
load_best_model_at_end=True,
metric_for_best_model="f1",
greater_is_better=True,
save_total_limit=2,
bf16=use_bf16,
fp16=use_fp16,
report_to="none",
seed=SEED,
data_seed=SEED,
run_name=(
"rotten-tomatoes-"
"distilbert-lora"
)
)
print(
"[7/12] Trainer 생성"
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=(
tokenized_dataset["train"]
),
eval_dataset=(
tokenized_dataset[
"validation"
]
),
processing_class=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics,
callbacks=[
EarlyStoppingCallback(
early_stopping_patience=2,
early_stopping_threshold=0.001
)
]
)
print(
"[8/12] LoRA Fine-tuning 시작"
)
training_start_time = (
time.perf_counter()
)
train_result = trainer.train()
training_elapsed = (
time.perf_counter()
- training_start_time
)
train_result.metrics[
"training_elapsed_seconds"
] = training_elapsed
trainer.log_metrics(
"train",
train_result.metrics
)
trainer.save_metrics(
"train",
train_result.metrics
)
trainer.save_state()
print(
"[9/12] Test 데이터 평가"
)
test_output = trainer.predict(
tokenized_dataset["test"]
)
trainer.log_metrics(
"test",
test_output.metrics
)
trainer.save_metrics(
"test",
test_output.metrics
)
test_predictions = np.argmax(
test_output.predictions,
axis=-1
)
test_labels = (
test_output.label_ids
)
matrix = confusion_matrix(
test_labels,
test_predictions
)
print(
"Confusion Matrix:"
)
print(matrix)
(
OUTPUT_DIR
/ "confusion_matrix.json"
).write_text(
json.dumps(
matrix.tolist(),
ensure_ascii=False,
indent=2
),
encoding="utf-8"
)
print(
"[10/12] Adapter 저장"
)
trainer.save_model(
str(
ADAPTER_DIR
)
)
tokenizer.save_pretrained(
ADAPTER_DIR
)
print(
f"Adapter 저장 위치: "
f"{ADAPTER_DIR.resolve()}"
)
print(
f"최적 Checkpoint: "
f"{trainer.state.best_model_checkpoint}"
)
print(
f"최적 지표: "
f"{trainer.state.best_metric}"
)
print(
"[11/12] 저장된 Adapter 재로딩"
)
inference_base_model = (
AutoModelForSequenceClassification
.from_pretrained(
MODEL_ID,
num_labels=len(
label_names
),
id2label=id2label,
label2id=label2id
)
)
inference_model = (
PeftModel.from_pretrained(
inference_base_model,
ADAPTER_DIR
)
)
inference_model.eval()
classifier = pipeline(
task="text-classification",
model=inference_model,
tokenizer=tokenizer,
device=(
select_pipeline_device()
)
)
sample_reviews = [
(
"The acting was excellent "
"and the story was deeply moving."
),
(
"The movie was boring, "
"predictable, and far too long."
),
(
"The visual effects were good, "
"but the plot was difficult "
"to follow."
)
]
results = classifier(
sample_reviews,
batch_size=3,
truncation=True
)
print(
"\n[LoRA 추론 결과]"
)
for review, result in zip(
sample_reviews,
results
):
print(
f"리뷰: {review}"
)
print(
f"예측: {result['label']}"
)
print(
f"확률: "
f"{result['score']:.2%}"
)
print(
"-" * 60
)
print(
"[12/12] Adapter 병합 모델 생성"
)
merged_model = (
inference_model
.merge_and_unload(
safe_merge=True
)
)
MERGED_MODEL_DIR.mkdir(
parents=True,
exist_ok=True
)
merged_model.save_pretrained(
MERGED_MODEL_DIR
)
tokenizer.save_pretrained(
MERGED_MODEL_DIR
)
print(
f"병합 모델 저장 위치: "
f"{MERGED_MODEL_DIR.resolve()}"
)
if __name__ == "__main__":
main()51. 프로젝트 실행 방법
1단계: 프로젝트 폴더
mkdir peft_lora_project
cd peft_lora_project2단계: 가상환경
Windows
python -m venv venv
venv\Scripts\activatemacOS·Linux
python3 -m venv venv
source venv/bin/activate3단계: 설치
python -m pip install \
"transformers[torch]" \
datasets \
accelerate \
peft \
scikit-learn4단계: 코드 저장
전체 코드를 다음 파일에 저장합니다.
train_lora.py5단계: 실행
python train_lora.pyCPU에서도 실행할 수 있지만 GPU보다 학습 시간이 오래 걸릴 수 있습니다.
52. 결과 파일 구조
학습이 완료되면 다음과 비슷한 구조가 만들어집니다.
peft_lora_project/
├─ train_lora.py
│
├─ outputs/
│ └─ sentiment-distilbert-lora/
│ ├─ checkpoint-...
│ ├─ train_results.json
│ ├─ test_results.json
│ ├─ trainer_state.json
│ ├─ parameter_report.json
│ └─ confusion_matrix.json
│
└─ models/
├─ sentiment-distilbert-lora/
│ ├─ adapter_config.json
│ ├─ adapter_model.safetensors
│ └─ tokenizer 관련 파일
│
└─ sentiment-distilbert-merged/
├─ config.json
├─ model.safetensors
└─ tokenizer 관련 파일Adapter 폴더
작고 교체 가능한 업무용 부품입니다.
Merged 폴더
기본 모델과 Adapter를 합친 독립 모델입니다.
53. 학습 중단 후 재개
Trainer에서 Checkpoint를 이용해 학습을 재개할 수 있습니다.
trainer.train(
resume_from_checkpoint=True
)특정 Checkpoint:
trainer.train(
resume_from_checkpoint=(
"outputs/"
"sentiment-distilbert-lora/"
"checkpoint-500"
)
)현재 Transformers의 PEFT 통합은 Trainer가 Adapter Checkpoint를 감지하고 올바른 학습 가능 상태로 다시 불러오도록 지원합니다.
CheckPoint에는 일반적으로 다음 내용이 포함됩니다.
Adapter 가중치
Adapter 설정
Optimizer 상태
Scheduler 상태
Trainer 상태
난수 상태54. Hugging Face Hub 업로드
로그인:
hf auth loginAdapter 업로드:
model.push_to_hub(
"사용자명/"
"sentiment-distilbert-lora",
private=True
)Tokenizer 업로드:
tokenizer.push_to_hub(
"사용자명/"
"sentiment-distilbert-lora",
private=True
)Trainer 사용:
trainer.push_to_hub(
commit_message=(
"Train DistilBERT "
"with LoRA"
)
)업로드되는 주요 파일:
adapter_model.safetensors
adapter_config.json
README.md
Tokenizer 파일Adapter Repository에는 기본 모델 ID가 기록되므로 사용자는 기본 모델과 Adapter를 결합해 추론할 수 있습니다.
55. 모델·Adapter 버전 관리
Adapter는 특정 기본 모델을 기준으로 학습됩니다.
다음 정보가 일치해야 합니다.
기본 모델 ID
기본 모델 Revision
Tokenizer Revision
PEFT 버전
Transformers 버전
Adapter 설정
Target Module 이름Adapter 설정 파일에는 `base_model_name_or_path`와 Revision 정보가 포함될 수 있습니다.
운영 프로젝트에서는 Commit Hash를 고정하는 것이 좋습니다.
MODEL_REVISION = (
"검증한_Commit_Hash"
)기본 모델:
base_model = (
AutoModelForSequenceClassification
.from_pretrained(
MODEL_ID,
revision=MODEL_REVISION
)
)Tokenizer:
tokenizer = (
AutoTokenizer
.from_pretrained(
MODEL_ID,
revision=MODEL_REVISION
)
)기록할 정보:
Base Model ID
Base Model Revision
Adapter ID
Adapter Revision
Dataset Revision
Python 버전
PyTorch 버전
Transformers 버전
PEFT 버전
학습 Seed56. 개인정보와 라이선스
PEFT로 만든 Adapter가 작다고 해서 자유롭게 배포할 수 있는 것은 아닙니다.
확인해야 할 내용:
기본 모델 라이선스
학습 데이터 라이선스
상업적 사용 허용 여부
파생 모델 배포 조건
사용자 개인정보
민감한 업무 데이터
모델 출력 위험Adapter에는 기본 모델 전체가 저장되지 않지만 학습 데이터에서 얻은 정보가 반영될 수 있습니다.
Adapter가 작다
≠
개인정보 위험이 없다특히 다음 데이터는 외부 업로드 전에 검토해야 합니다.
고객 상담 기록
의료 정보
회사 기밀
보안 로그
개인 식별정보
내부 소스코드57. 자주 발생하는 오류
오류 1. peft를 찾을 수 없음
ModuleNotFoundError:
No module named 'peft'설치:
python -m pip install --upgrade peft확인:
python -m pip show peft오류 2. PEFT 버전이 낮음
PEFT version is not compatible업데이트:
python -m pip install \
--upgrade \
transformers \
peft \
accelerate현재 Transformers 통합은 PEFT 0.19.1 이상을 요구합니다.
오류 3. Target Module을 찾지 못함
ValueError:
Target modules not found원인:
Llama 예제의 q_proj를
DistilBERT에 그대로 사용모듈 이름 확인:
for name, module in (
model.named_modules()
):
print(name)DistilBERT 예:
target_modules=[
"q_lin",
"v_lin"
]오류 4. 학습 가능한 파라미터가 0개
trainable params:
0확인:
target_modules가 실제로 존재하는가?
inference_mode=False인가?
Adapter가 모델에 연결되었는가?
분류 Head가 modules_to_save에 포함되었는가?model.print_trainable_parameters()오류 5. 분류 결과가 계속 무작위임
분류 Head가 학습되지 않았을 수 있습니다.
modules_to_save=[
"pre_classifier",
"classifier"
]학습 가능한 파라미터 이름을 확인합니다.
for name, parameter in (
model.named_parameters()
):
if parameter.requires_grad:
print(name)오류 6. Adapter만 불러오려다 실패
Adapter에는 기본 모델 전체가 포함되지 않습니다.
잘못된 접근:
adapter_model.safetensors만 복사
→ 기본 모델 없이 추론기본 모델과 Adapter를 함께 불러옵니다.
base_model = (
AutoModelForSequenceClassification
.from_pretrained(
MODEL_ID
)
)
model = PeftModel.from_pretrained(
base_model,
ADAPTER_DIR
)오류 7. Adapter 설정 파일이 없음
adapter_config.json not foundAdapter 폴더에는 최소한 다음 파일이 필요합니다.
adapter_config.json
adapter_model.safetensors오류 8. Classification Head 크기가 맞지 않음
size mismatch for classifier확인:
num_labels
id2label
label2id
modules_to_save
기본 모델 구성학습할 때와 추론할 때 같은 Label 수를 사용합니다.
오류 9. CUDA 메모리 부족
CUDA out of memory대응 순서:
Batch Size 감소
Gradient Accumulation 증가
입력 길이 감소
Rank 감소
Target Module 감소
Gradient Checkpointing
FP16·BF16
QLoRA오류 10. bitsandbytes 설치 오류
bitsandbytes was compiled
without GPU support또는:
CUDA Setup failed확인:
운영체제
CUDA 버전
GPU Architecture
PyTorch CUDA 버전
bitsandbytes 지원 Backend설치된 PyTorch CUDA 확인:
import torch
print(
torch.version.cuda
)
print(
torch.cuda.is_available()
)오류 11. QLoRA에서 CPU·MPS 오류
Bitsandbytes의 지원 기능은 Backend와 운영체제에 따라 달라집니다.
NVIDIA CUDA
Intel XPU
Intel Gaudi
CPU사용 환경에서 4비트 학습 기능이 지원되는지 공식 호환 정보를 확인해야 합니다.
Apple Silicon에서는 일반 LoRA를 먼저 검토하고 QLoRA Backend 호환성을 별도로 확인하는 편이 안전합니다.
오류 12. merge_and_unload가 없음
모델이 PEFT 모델인지 확인합니다.
print(
type(model)
)다음 형태여야 합니다.
PeftModelAdapter가 이미 병합되었거나 일반 Transformers 모델만 불러온 것은 아닌지 확인합니다.
오류 13. 병합 후 Adapter를 전환할 수 없음
`merge_and_unload()`는 Adapter를 기본 모델에 병합하고 Adapter Layer를 제거합니다.
병합 전:
Adapter 전환 가능
병합 후:
일반 단일 모델전환이 필요하면 병합하지 않은 PEFT 모델을 유지합니다.
오류 14. 저장 파일이 예상보다 큼
다음 중 어떤 것을 저장했는지 확인합니다.
model.save_pretrained()
→ PEFT 모델이면 Adapter 중심
merged_model.save_pretrained()
→ 전체 병합 모델병합 모델은 기본 모델 전체 크기를 가집니다.
오류 15. Adapter 학습 Loss가 감소하지 않음
확인:
Learning Rate
Rank
Target Module
분류 Head 학습 여부
라벨 구조
Tokenizer
데이터 품질시작 실험:
Learning Rate:
5e-5
1e-4
2e-4
Rank:
8
16
32오류 16. Training Loss는 좋지만 Validation 성능이 하락함
과적합 가능성이 있습니다.
Epoch 감소
Early Stopping
LoRA Dropout 증가
Rank 감소
데이터 추가
중복 제거오류 17. Adapter 로딩 후 성능이 다름
확인:
동일한 기본 모델인가?
기본 모델 Revision이 같은가?
Tokenizer가 같은가?
동일한 Label Mapping인가?
Adapter가 활성화되었는가?
eval()을 호출했는가?model.eval()오류 18. Adapter Checkpoint 재개 실패
Checkpoint가 완전한 Trainer Checkpoint인지 확인합니다.
adapter_model.safetensors
adapter_config.json
optimizer.pt
scheduler.pt
trainer_state.json최종 Adapter 저장 폴더와 학습 재개용 Checkpoint 폴더는 목적이 다릅니다.
58. 연습 문제
문제 1
PEFT와 Full Fine-tuning의 차이를 세 문장으로 설명하세요.
문제 2
입력과 출력 차원이 각각 1,024인 Linear Layer에서 다음 파라미터 수를 계산하세요.
Full Fine-tuning 변화량
LoRA r=8
LoRA r=16문제 3
다음 설정으로 LoraConfig를 작성하세요.
작업:
문장 분류
Rank:
16
Alpha:
32
Dropout:
0.1
Target:
q_lin, v_lin문제 4
DistilBERT 모델에서 모든 Linear Layer 이름을 출력하세요.
문제 5
학습 가능한 파라미터 수와 전체 파라미터 수를 출력하세요.
문제 6
다음 Layer만 LoRA Target으로 지정하세요.
q_lin
k_lin
v_lin
out_lin문제 7
분류 Head를 LoRA Adapter와 함께 학습하고 저장하도록 설정하세요.
문제 8
Rank 8과 Rank 32 Adapter의 학습 파라미터 수를 비교하세요.
문제 9
Learning Rate를 다음과 같이 바꾸어 Validation F1을 비교하세요.
5e-5
1e-4
2e-4문제 10
LoRA Dropout을 다음 값으로 비교하세요.
0.0
0.05
0.1문제 11
학습된 Adapter를 로컬 폴더에 저장하세요.
문제 12
기본 모델과 저장된 Adapter를 다시 결합해 추론하세요.
문제 13
두 개의 Adapter를 하나의 기본 모델에 불러오고 번갈아 활성화하세요.
문제 14
Adapter를 기본 모델에 병합한 뒤 독립 모델로 저장하세요.
문제 15
4비트 QLoRA용 BitsAndBytesConfig를 작성하세요.
4비트 로딩
NF4
Double Quantization
BF16 계산문제 16
양자화 모델에 `prepare_model_for_kbit_training()`을 적용하세요.
문제 17
QLoRA에서 모든 Linear Layer를 Target으로 지정하세요.
문제 18
Full Fine-tuning과 LoRA를 다음 기준으로 비교하세요.
Trainable Parameters
학습 시간
최대 GPU 메모리
Validation F1
Checkpoint 크기문제 19
Adapter의 `adapter_config.json`을 열어 다음 값을 확인하세요.
base_model_name_or_path
r
lora_alpha
target_modules
task_type문제 20
실전 프로젝트에 다음 기능을 추가하세요.
Full Fine-tuning과 LoRA 자동 비교
학습 파라미터 비율 JSON 저장
Adapter 파일 크기 출력
Merged 모델 파일 크기 출력
오분류 리뷰 CSV 저장
Hub 비공개 업로드59. 핵심 요약
설치
python -m pip install \
peft \
transformers \
accelerateLoRA 설정
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
inference_mode=False,
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=[
"q_lin",
"v_lin"
],
modules_to_save=[
"pre_classifier",
"classifier"
]
)모델에 Adapter 추가
model = get_peft_model(
base_model,
lora_config
)학습 파라미터 확인
model.print_trainable_parameters()Trainer 학습
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=validation_dataset
)
trainer.train()Adapter 저장
model.save_pretrained(
"saved_adapter"
)Adapter 로딩
base_model = (
AutoModelForSequenceClassification
.from_pretrained(
MODEL_ID
)
)
model = PeftModel.from_pretrained(
base_model,
"saved_adapter"
)Adapter 병합
merged_model = (
model.merge_and_unload(
safe_merge=True
)
)QLoRA 양자화
quantization_config = (
BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=(
torch.bfloat16
)
)
)K-bit 학습 준비
model = (
prepare_model_for_kbit_training(
model
)
)QLoRA Target
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
target_modules="all-linear",
lora_dropout=0.05
)60. 마무리
이번 시간에는 PEFT와 LoRA를 이용해 거대한 사전 학습 모델의 일부 파라미터만 학습하는 방법을 알아보았습니다.
전체 흐름을 다시 정리해 보겠습니다.
사전 학습 모델 로딩
→ 기본 가중치 동결
→ LoraConfig 작성
→ Attention Layer에 LoRA 삽입
→ 학습 가능한 파라미터 확인
→ Trainer로 Adapter 학습
→ Adapter만 저장
→ 기본 모델과 결합해 추론
→ 필요하면 전체 모델로 병합LoRA의 핵심은 큰 가중치 변화량을 작은 두 행렬로 표현하는 것입니다.
거대한 ΔW를 직접 학습
대신
작은 A와 B를 학습이 방식으로 줄어드는 것은 단순히 저장 파일 크기만이 아닙니다.
Gradient 메모리
Optimizer 상태
Checkpoint 용량
업무별 모델 복제 비용
모델 배포·전송 비용기본 모델 하나를 두고 여러 Adapter를 운영할 수도 있습니다.
기본 모델
├─ 금융 상담 Adapter
├─ 법률 문서 Adapter
├─ 상품 리뷰 Adapter
└─ 고객 문의 Adapter업무가 바뀔 때마다 거대한 모델을 새로 저장할 필요가 없습니다.
작은 Adapter를 갈아 끼우면 됩니다.
사용자:
“이번에는 영화 리뷰를 분석해 주세요.”
모델:
“영화 Adapter를 장착하겠습니다.”
사용자:
“이제 상품 리뷰입니다.”
모델:
“상품 Adapter로 교체하겠습니다.”다만 LoRA가 Full Fine-tuning의 완벽한 대체품은 아닙니다.
학습 파라미터가 적다
≠
항상 성능이 같다
GPU 메모리가 줄었다
≠
아무 장비에서나 학습된다
Adapter가 작다
≠
보안과 라이선스를 무시해도 된다좋은 결과를 얻으려면 다음 설정을 실험해야 합니다.
Rank
Alpha
Dropout
Target Module
Learning Rate
Batch Size
입력 길이
데이터 품질모델이 매우 크다면 QLoRA를 이용해 기본 모델을 4비트로 압축하고 LoRA Adapter만 학습할 수 있습니다.
Full Fine-tuning
→ 모델 전체 업데이트
LoRA
→ 기본 모델 동결
→ 작은 Adapter 업데이트
QLoRA
→ 기본 모델 4비트 압축·동결
→ 작은 Adapter 업데이트AI 모델의 크기가 커질수록 모든 것을 직접 수정하는 방식은 점점 부담스러워집니다.
PEFT와 LoRA는 거대한 모델을 움직이는 대신 필요한 변화만 작게 포장합니다.
전체 모델을 다시 만들지 말고
모델이 배워야 할 차이만 저장한다.이것이 LoRA의 가장 중요한 철학입니다. 🔧🤗
다음 편 예고
[Python 완전정복 시리즈 #39] TRL과 SFTTrainer 완벽 이해하기 | 대화형 AI 모델을 지시 데이터로 Fine-tuning하는 방법
다음 시간에는 Hugging Face TRL과 SFTTrainer를 이용해 질문과 답변, Instruction과 Response 형태의 데이터로 생성형 AI 모델을 Fine-tuning하는 방법을 알아봅니다.
Chat Template, Instruction Dataset, Completion Only 학습, Packing, LoRA·QLoRA 연결, 생성 결과 평가, Adapter 저장, Gradio 챗봇 연결까지 실습해 보겠습니다.
#Python #파이썬 #Python강좌 #파이썬기초 #HuggingFace #PEFT #LoRA #QLoRA #FineTuning #파인튜닝 #ParameterEfficientFineTuning #LoraConfig #PeftModel #Adapter #DistilBERT #Transformers #Trainer #머신러닝 #딥러닝 #생성형AI #LLM #양자화 #Quantization #BitsAndBytes #4비트양자화 #GPU메모리 #ModelAdapter #HuggingFaceHub #PyTorch #AI모델학습 #코딩공부 #프로그래밍
