Developer's Development
3.3.17 [LLM] 파인튜닝 기법: PEFT 본문
PEFT (Parameter Efficient Fine-Tuning)
PEFT는 모델 전체를 재학습하지 않고, 일부 파라미터만 조정하여 학습 비용을 줄이는 방법이다. 대규모 언어 모델 재학습 시 학습해야 할 파라미터 수가 줄어들어 메모리와 계산 비용이 크게 감소하여 효율성을 높이는 데 활용된다.
👉🏻 주요 특징
- 효율성: 학습 시간과 비용 감소
- 일관성: 기존 모델의 강점 유지
- 적용성: 다양한 도메인에 쉽게 적용 가능
- PEFT 기법 계열 구조 요약
| 계열 | 기법 | 주요 특징 |
| 파라미터 삽입형 | LoRA, DoRA, AdaLoRA | 모델 내부 weight 일부에 저랭크 행렬 삽입 |
| 양자화 결합형 | QLoRA | 4bit 양자화 + LoRA로 저비용 고효율 학습 |
| 입력 확장형 | Prompt Tuning, Prefix Tuning | 임베딩 앞에 가상 벡터 삽입, 모델 수정 없이 학습 |
| 블록 삽입형 | Adapter | Transformer 사이에 작은 모듈 삽입, 모듈만 학습 |
- 주요 PEFT 기법
1. LoRA (Low-Rank Adaption)
모델의 큰 파라미터 매트릭스를 조정하지 않고, 저랭크(Low-Rank) 매트릭스를 추가해 학습한다.
(기존 모델의 가중치를 고정한 상태에서, 새로운 저랭크 매트릭스를 학습한다.)
👉🏻 작동 원리
W가 모델의 기존 가중치 매트릭스일 때, LoRA는 W + Δ W를 사용한다.
Δ W = A · B (A: 저랭크 행렬, B: 저랭크 행렬)
👉🏻 장점
- 메모리 사용량 감소 (전체 파라미터 수의 1~3%만 학습)
- 기존 모델 성능 유지
- Hugging Face [ peft ]로 간편 구현 가능
2. Q-LoRA (Quantized LoRA)
LoRA 기법을 양자화(Quantization)하여 GPU 메모리 사용량을 줄이고 학습 비용을 절감하는 방법이다.
(4-bit 정밀도를 사용하여 모델 가중치를 저장하고, LoRA를 통해 미세 조정한다.)
👉🏻 작동 원리
- 4-bit NormalFloat (NF4) 양자화를 적용하여 모델의 가중치를 압축한다.
- LoRA와 결합하여 저비용으로 고효율 파인튜닝을 한다.
👉🏻 장점
- 적은 메모리 사용량으로 대형 모델 학습 가능
- 학습 비용 최소화 (기존 LoRA보다 연산 비용이 더 낮음)
3. DoRA (Decomposed LoRA)
LoRA 기법을 확장하여 더 정밀한 파인튜닝을 수행하는 방법으로, 기존 LoRA의 저랭크 행렬을 더 세분화하여 학습 가능하도록 설계되었다.
👉🏻 작동 원리
- 기존 LoRA에서 사용하는 Δ W = A · B를 더욱 세분화하여 다층 업데이트를 수행한다.
- 다중 저랭크 행렬을 결합하여 모델 성능을 최적화한다.
👉🏻 장점
- 기존 LoRA보다 세밀한 조정 가능
- 도메인별 맞춤형 파인튜닝에 최적화
4. Prompt Tuning (Soft Prompts)
입력 데이터에 명시적인 프롬프트 대신, 학습 가능한 임베딩 벡터를 사용하는 것으로, 기존 모델을 수정하지 않고, 학습 가능한 "소프트" 프롬프트로 모델을 조정하는 기법이다.
👉🏻 작동 원리
- 텍스트 입력 대신 학습 가능한 벡터를 추가해 모델 입력으로 사용
- 모델은 이 벡터를 기반으로 특정 작업에 적응
👉🏻 장점
- 프롬프트 엔지니어링의 한계를 극복
- 모델의 파라미터를 고정한 상태에서 작업 학습 가능 (완전한 비침습 튜닝 == 원본 모델 손상 없음)
- 빠른 task 전환에 유리
실습 (LoRA)
- LoRA를 활용한 GPT-2 감성 분석 모델 튜닝
!pip install peft datasets transformers
from huggingface_hub import login
login(token="hf_xxx")
import torch
import os
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device) # cuda
os.environ["TOKENIZERS_PARALLELISM"] = "false"
1. 기반 모델 load
from transformers import AutoTokenizer, AutoModelForSequenceClassification
model_name = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
base_model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
base_model.config.pad_token_id = tokenizer.pad_token_id
2. LoRA 설정 (LoraConfig)
# LoRA 설정
from peft import LoraConfig
lora_config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["c_attn", "c_fc", "c_proj"],
lora_dropout=0.1
)
# 기반 모델 + LoRA 설정 == 학습할 모델
from peft import get_peft_model
model = get_peft_model(base_model, lora_config).to(device)
3. 데이터 준비
from datasets import load_dataset
dataset = load_dataset("imdb")
pos_sample = [data for data in dataset["train"] if data["label"] == 1][:500]
neg_sample = [data for data in dataset["train"] if data["label"] == 0][:500]
train_texts = [data["text"] for data in pos_sample + neg_sample]
train_labels = [data["label"] for data in pos_sample + neg_sample]
pos_eval = [data for data in dataset["test"] if data["label"] == 1][:100]
neg_eval = [data for data in dataset["test"] if data["label"] == 0][:100]
eval_texts = [data["text"] for data in pos_eval + neg_eval]
eval_labels = [data["label"] for data in pos_eval + neg_eval]
# 토큰화 함수
def preprocess_data(texts, labels):
encodings = tokenizer(
texts,
truncation=True,
padding="max_length",
max_length=512,
return_tensors="pt"
)
encodings["labels"] = torch.tensor(labels, dtype=torch.long)
return encodings
# 토큰화 함수 적용 -> 전처리
train_encodings = preprocess_data(train_texts, train_labels)
eval_encodings = preprocess_data(eval_texts, eval_labels)
# 데이터셋 변환을 위한 클래스
class IMDBDataset(torch.utils.data.Dataset):
def __init__(self, encodings):
self.encodings = encodings
def __len__(self):
return len(self.encodings["input_ids"])
def __getitem__(self, idx):
return {
key: val[idx] for key, val in self.encodings.items()
}
# 데이터셋 형태로 변환
train_dataset = IMDBDataset(train_encodings)
eval_dataset = IMDBDataset(eval_encodings)
# 배치를 만들어 줄 함수
def collate_fn(batch):
batch = {
key: torch.stack([item[key] for item in batch]) for key in batch[0]
}
return batch
4. 학습 준비
# 학습 설정
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./results",
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
num_train_epochs=5,
save_steps=100,
save_total_limit=2,
eval_strategy="epoch",
logging_dir="./logs",
logging_steps=10,
fp16=True
)
# Trainer 설정 (학습할 모델 + 학습 설정 + 데이터셋)
from transformers import Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
data_collator=collate_fn
)
trainer.train()
5. 추론
def predict_sentiment(text):
inputs = tokenizer(
text,
return_tensors="pt",
padding=True,
truncation=True,
max_length=512
).to(device)
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
prediction = torch.argmax(logits, dim=-1).item()
return "긍정" if prediction == 1 else "부정"
# test_review = "I enjoyed watching the movie!"
test_review = "It was boring!"
result = predict_sentiment(test_review)
result # '부정'
# base model 추론 결과와 비교
reload_base_model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
def predict_sentiment_origin(text):
inputs = tokenizer(
text,
return_tensors="pt",
padding=True,
truncation=True,
max_length=512
)
with torch.no_grad():
outputs = reload_base_model(**inputs)
logits = outputs.logits
prediction = torch.argmax(logits, dim=-1).item()
return prediction
# test_review = "I enjoyed watching the movie!"
test_review = "It was boring!"
result = predict_sentiment_origin(test_review)
result # 0
실습 (Q-LoRA)
- 한국어 QA 시스템 최적화
0. 환경 설정
!pip install peft datasets transformers bitsandbytes
!python -m pip install --upgrade pip
!pip install typing_extensions==4.7.1 --upgrade
from huggingface_hub import login
login(token="hf_xxx")
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
device
1. 모델 로드
model_name = 'NCSOFT/Llama-VARCO-8B-Instruct'
# 4-bit 양자화된 모델 로드를 위한 설정
from transformers import BitsAndBytesConfig
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type='nf4',
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
# 토크나이저 및 모델 로드
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained(model_name)
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
device_map='auto'
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
2. 데이터 준비
# 데이터셋 로드
from datasets import load_dataset
dataset = load_dataset('KorQuAD/squad_kor_v1')
dataset['train'][0]
# 데이터 전처리 함수
def preprocess_data(examples):
inputs = ["질문: " + q + "\n문맥: " + c for q, c in zip(examples["question"], examples["context"])]
answer_texts = [a["text"][0] if len(a["text"]) > 0 else "" for a in examples["answers"]]
model_inputs = tokenizer(
inputs,
truncation=True,
padding="max_length",
max_length=512,
return_tensors="pt"
)
labels = tokenizer(
answer_texts,
truncation=True,
padding="max_length",
max_length=512,
return_tensors="pt"
)["input_ids"]
max_length = model_inputs["input_ids"].shape[1]
labels = labels[:, :max_length]
labels[labels == tokenizer.pad_token_id] = -100
model_inputs["labels"] = labels
return model_inputs
# 데이터셋 전처리 적용
train_dataset = dataset["train"].map(preprocess_data, batched=True, remove_columns=dataset["train"].column_names)
val_dataset = dataset["validation"].map(preprocess_data, batched=True, remove_columns=dataset["validation"].column_names)
print(tokenizer.decode(train_dataset[0]["input_ids"], skip_special_tokens=True))
3. 모델 학습 준비
# LoRA 설정
from peft import LoraConfig
lora_config = LoraConfig(
r=8,
lora_alpha=32,
lora_dropout=0.1,
bias='none',
target_modules=['q_proj', 'v_proj'],
task_type='CAUSAL_LM'
)
# LoRA 적용
from peft import get_peft_model
model = get_peft_model(base_model, lora_config)
model.enable_input_require_grads()
model.gradient_checkpointing_enable()
model.print_trainable_parameters()
from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir='./q_lora_korqa',
save_strategy='epoch',
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
gradient_accumulation_steps=8,
learning_rate=2e-4,
weight_decay=0.01,
num_train_epochs=3,
# num_train_epochs=1,
logging_dir='./logs',
logging_steps=100,
save_total_limit=2,
fp16=True,
push_to_hub=False,
report_to='none'
)
from transformers import Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=val_dataset,
tokenizer=tokenizer,
data_collator=data_collator
)
4. 모델 학습 (Fine-tuning)
# 시간 오래 걸림
trainer.train()
5. 학습된 모델 활용
(1) 모델 추론
# 설정 저장
from transformers import AutoConfig
trained_model_path = "./q_lora_korqa/checkpoint-xxxx"
trained_model_path = "./q_lora_korqa/checkpoint-5664"
config = AutoConfig.from_pretrained(model_name)
config.save_pretrained(trained_model_path)
# Base model + 학습 결과
from peft import PeftModel
from transformers import pipeline
base_model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto", device_map="auto")
model = PeftModel.from_pretrained(base_model, trained_model_path)
qa_pipeline = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer
)
question = "바그너는 괴테의 파우스트를 읽고 무엇을 쓰고자 했는가?"
context = "1839년 바그너는 괴테의 파우스트을 처음 읽고 그 내용에 마음이 끌려 이를 소재로 해서 하나의 교향곡을 쓰려는 뜻을 갖는다. 이 시기 바그너는 1838년에 빛 독촉으로 산전수전을 다 걲은 상황이라 좌절과 실망에 가득했으며 메피스토펠레스를 만나는 파우스트의 심경에 공감했다고 한다. 또한 파리에서 아브네크의 지휘로 파리 음악원 관현악단이 연주하는 베토벤의 교향곡 9번을 듣고 깊은 감명을 받았는데, 이것이 이듬해 1월에 파우스트의 서곡으로 쓰여진 이 작품에 조금이라도 영향을 끼쳤으리라는 것은 의심할 여지가 없다. "
input_text = f"질문: {question}\n문맥: {context}"
output = qa_pipeline(input_text, max_new_tokens=50, temperature=0.7, top_p=0.8)
print(output)
(2) HuggingFace Hub
# LoRA 어댑터 or 병합 모델 저장
model.save_pretrained("./q_lora_korqa")
tokenizer.save_pretrained("./q_lora_korqa")
# HuggingFace 업로드
model.push_to_hub("squirreln/q_lora_korqa")
tokenizer.push_to_hub("squirreln/q_lora_korqa")
# HuggingFace에서 로드해서 사용 (Base Model, Adapter 각각 load)
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# 베이스 모델 로드
base_model = AutoModelForCausalLM.from_pretrained(
"NCSOFT/Llama-VARCO-8B-Instruct", # 원래 쓰던 base model
torch_dtype="auto",
device_map="auto"
)
# LoRA 어댑터 로드
model = PeftModel.from_pretrained(
base_model,
"squirreln/q_lora_korqa_" # HuggingFace repo id
)
# 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained("squirreln/q_lora_korqa_")
# 예시
inputs = tokenizer("바그너는 괴테의 파우스트를 읽고 무엇을 쓰고자 했는가?", return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
# 바그너는 괴테의 파우스트를 읽고 무엇을 쓰고자 했는가?)))))))))))))))))) )))) )) 의 의 일일
실습 (PEFT: Soft Prompts)
0. 환경설정
from google.colab import userdata
HF_TOKEN = userdata.get('HF_TOKEN')
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
1. Tokenizer 로드 + 스페셜 토큰 추가
from transformers import AutoTokenizer, AutoModelForCausalLM
# 프롬프트 튜닝을 위한 스페셜 토큰
emotion_tokens = ['<happy>', '<sad>']
tokenizer = AutoTokenizer.from_pretrained('gpt2', token=HF_TOKEN)
tokenizer.add_special_tokens({"additional_special_tokens": emotion_tokens})
train_data = [
('<happy> Once upon a time, there was a dragon who,',
'The dragon breathed colorful fireworks that lit up the sky.'),
('<sad> In a dark forest, a lonely knight',
'The knight knelt by the withered tree, tears falling on his rusted armor.')
]
2. Model 로드 및 Tuning 준비
model = AutoModelForCausalLM.from_pretrained('gpt2', token=HF_TOKEN).to(device)
model.resize_token_embeddings(len(tokenizer))
from peft import PromptTuningConfig, get_peft_model
peft_config = PromptTuningConfig(
task_type="CAUSAL_LM",
num_virtual_tokens=10,
token_dim=model.config.hidden_size
)
peft_model = get_peft_model(model, peft_config)
optimizer = torch.optim.AdamW(peft_model.parameters(), lr=2e-5)
# 학습 진행
for epoch in range(30):
for prompt, continuation in train_data:
inputs = tokenizer(prompt, return_tensors="pt").to(device)
labels = tokenizer(continuation, return_tensors="pt").input_ids.to(device)
full_inputs = torch.cat([inputs.input_ids, labels], dim=1)
outputs = peft_model(full_inputs, labels=full_inputs)
loss = outputs.loss
loss.backward()
optimizer.step()
optimizer.zero_grad()
3. 모델 추론
def generate_story(emotion, prompt):
inputs = tokenizer(f'{emotion} {prompt}', return_tensors="pt").to(device)
output = peft_model.generate(
**inputs,
max_new_tokens=50,
temperature=0.9,
top_k=40,
repetition_penalty=1.5,
do_sample=True
)
return tokenizer.decode(output[0], skip_special_tokens=False)
generate_story('<happy>', 'In a magical kingdom')
# "<happy> In a magical kingdom. and in\n - A (3D's!-, to the or I can have no special needs one year on which he is not all of those who are going over just as well)\n\n 'The more so that every man up by"
generate_story('<sad>', 'In a magical kingdom')
# '<sad> In a magical kingdom that in the only.\n or "It is as and more, (to not to do it on you are all of his by an end- she was made for other I\'mm their place at home when he\'s got up who were very'
'LLM' 카테고리의 다른 글
| 3.3.19 [LLM] 파인튜닝 기법: DPO (0) | 2025.09.15 |
|---|---|
| 3.3.18 [LLM] 파인튜닝 기법: RLHF (0) | 2025.09.14 |
| 3.3.16 [LLM] 파인튜닝 (0) | 2025.09.14 |
| 3.3.15 [LLM] 프롬프트 엔지니어링 응용: CoT (0) | 2025.09.14 |
| 3.3.14 [LLM] 프롬프트 엔지니어링 응용: RAG (1) | 2025.09.13 |