https://product.kyobobook.co.kr/detail/S000214934825
한 권으로 끝내는 실전 LLM 파인튜닝 | 강다솔 - 교보문고
한 권으로 끝내는 실전 LLM 파인튜닝 | 실무 현장에서 꼭 필요한 파인튜닝, PEFT, vLLM 서빙 기술을 직접 실습하면서 배워 보자!AI 기술의 최전선에서 배우는 LLM 파인튜닝의 모든 것! 이론적 토대부터
product.kyobobook.co.kr
"한 권으로 끝내는 실전 LLM 파인튜닝" 교재를 활용해 3주(주말 제외) 동안 진행 되는 온라인 스터디
0. PEFT(Parameter-Efficient Fine-Tuning) 란?
PEFT(Parameter-Efficient Fine-Tuning)는 대형 언어 모델의 전체 파라미터를 업데이트하지 않고, 일부 선택된 파라미터만 조정함으로써 Fine Tuning의 효율성을 극대화하기 위한 미세조정(Fine-Tuning) 방법론을 의미
"PEFT와 태스크 적용 사례" 이전 포스팅 참고
https://minhyuk0914.tistory.com/28
한 권으로 LLM 온라인 스터디 1기 Day_4 - 파인튜닝의 모든 것 : PEFT와 태스크 적용 사례
https://product.kyobobook.co.kr/detail/S000214934825 한 권으로 끝내는 실전 LLM 파인튜닝 | 강다솔 - 교보문고한 권으로 끝내는 실전 LLM 파인튜닝 | 실무 현장에서 꼭 필요한 파인튜닝, PEFT, vLLM 서빙 기술을
minhyuk0914.tistory.com
1. LoRA(Low Rank Adaption) 란?
LoRA(Low Rank Adaption)이란 PEFT의 대표적인 방법론 중 하나로, 언어모델을 구성하는 대부분 파라미터(매개변수)의 가중치(weight)를 그대로 유지한 채 학습이 가능한 저차원의 가중치 행렬을 추가하여 모델을 조정하는 방식이다. 새로 추가된 가중치 행렬(LoRA 가중치)은 기존 가중치에 비해 훨씬 낮은 차원을 가지며, 파라미터 수를 크게 줄이면서도 모델의 성능을 유지하거나 향상시킬 수 있다.
LoRA의 작동원리에 대해 알아보자
구성요소
- 파란색 모듈은 모델의 원래 가중치 (PLM의 원래 가중치(W)는 고정되어 학습 중에 수정되지 않는다.)
- 주황색 모듈은 학습하려는 LoRA 가중치 (A, B)
- x : 모델의 입력시 처리할 데이터
- h : 모델의 출력 데이터
- A : d x r 차원의 행렬로, 여기서 d는 원래 가중치 행렬 W의 차원, r은 분해 순위를 나타냄
- B : r x d 차원의 행렬로, A와 곱해져 원래 가중치 행렬 W와 같은 d x d 크기의 행렬을 나타냄
- r(rank) : 행렬 B와 A의 공통된 차원으로, 행렬 분해의 랭크를 결정 (사용자 지정 값)
두 행렬 A와 B의 곱은 원래 가중치 행렬 W와 합산되어 최종 출력에 영향을 미침
- r 이 작으면, 학습되는 파라미터 수가 줄어들어 학습이 빠르고 비용이 효율적이지만, 너무 작으면 모델의 성능 제한 우려 존재
- r 이 크면, 더 복잡한 작업을 처리할 수 있지만, 학습 시간과 비용이 증가
작동 원리
행렬의 차원을 r 만큼 축소하고, 그 후에 다시 원래의 크기로 복원하는 과정을 행렬의 곱으로 나타낼 수 있다. 또한, 레이어 내에 존재하는 여러 은닉층(hidden states) h에 특정 값을 추가하여 모델의 출력을 조절하는 파라미터를 통해 모델의 출력 값을 원하는 타겟 레이블에 맞게 튜닝하는 것이 LoRA의 핵심원리이다
2. 실습 환경 준비
2.1 RunPod 환경 설정
VRAM | 최소 60GB 이상(H100 PCle 권장) |
pytorch 버전 | 2.1 |
GPU 수량 | 1개 |
대여 옵션 | On-Demand |
Container Disk | 200GB |
Volume Disk | 200GB |
2.2 Gemma-2-9b-it 모델 준비
Hugging Face API 토큰을 활용
from huggingface_hub import login
login(
token="Your_Huggingface_API_KEY",
add_to_git_credential=True
)
Gemma-2-9b-it 모델과 토크나이저를 허깅페이스에서 불러오기
import json
import torch
from datasets import Dataset, load_dataset
from trl import (setup_chat_format,
DataCollatorForCompletionOnlyLM,
SFTTrainer)
from peft import AutoPeftModelForCausalLM, LoraConfig, PeftConfig
from transformers import (AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
BitsAndBytesConfig,
pipeline,
StoppingCriteria)
model_id = "google/gemma-2-9b-it"
# 모델과 토크나이저 불러오기
model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map="auto",
torch_dtype=torch.bfloat16,
attn_implementation='eager'
# load_in_8bit=True
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
2.3 데이터 전처리
- wget : 웹에서 파일을 다운로드하게 도와주는 라이브러리
- !wget 명령어를 통해 데이터셋을 노트북 환경에서 실행하여, 데이터셋을 다운로드
- 파이썬의 list comprehension을 통하여 파일의 각 줄을 한 줄씩 읽어 json.loads()함수를 적용
- json.loads() : JSON형식으로 작성된 문자열을 python에서 사용할 수 있는 dictionary형태로 변환
!wget https://raw.githubusercontent.com/MrBananaHuman/CounselGPT/main/total_kor_multiturn_counsel_bot.jsonl
with open('./total_kor_multiturn_counsel_bot.jsonl',
'r',
encoding='utf-8') as file:
original_jsonl_data = [json.loads(line) for line in file]
original_jsonl_data[5085]
"""
[{'speaker': '상담사', 'utterance': '안녕하세요. 심리상담사입니다. 어떤 고민이 있으신가요?'},
{'speaker': '내담자', 'utterance': '요즘 직장에서 너무 힘들어요.'},
{'speaker': '상담사', 'utterance': '정말요? 어떤 점이 힘드신가요? 좀 더 자세히 말해주세요.'},
{'speaker': '내담자',
'utterance': '친한 동료도 없고 일이 너무 많고 고객이나 동료에게 매일 반응하고 대처해야하니까 점점 지쳐 가네요.'},
{'speaker': '상담사',
'utterance': '그러셨군요. 직장생활에서 하나하나 대응하는 일은 많은 에너지를 필요로 합니다. 그리고 이러한 에너지 소모는 급격히 힘들어지게 합니다. 이러한 일상에 적응하며 시간이 지나면 점점 힘들어질 수 있어요.'},
{'speaker': '내담자', 'utterance': '집에 가면 집안일을 하고 나면 무언가를 해야하는데 그게 너무 힘들어요.'},
{'speaker': '상담사',
'utterance': '집에서도 일을 하시는군요. 그러시다보니 집에서의 일도 의무적으로 느껴지는 거 같아요. 이러한 의무감에 의해서 불안감과 힘들어질 수 있죠.'},
{'speaker': '내담자', 'utterance': '이러다 몸이 아플 것 같아요. 이게 계속되면 어떻게 해야할까요?'},
{'speaker': '상담사',
'utterance': '몸이 힘들어지는 건 자신이 지니고 있는 신호입니다. 즉, 몸과 마음에 신호를 주고 있는 거죠. 혹시 이러한 증상이 지속되시면 주변의 내용을 통해 주변의 상황을 살펴보고, 다양한 자신의 취미를 발견하거나, 휴식을 통해서 쉬는 것도 좋습니다. 만약에 몸에 이상을 느끼신다면 병원에 찾아가셔서 다양한 건강상의 문제를 예방할 수 있도록 조치하세요.'},
{'speaker': '상담사', 'utterance': '내담자님, 어떤 생각이 드시나요?'},
{'speaker': '내담자', 'utterance': '생각을 잘 못해서요.'},
{'speaker': '상담사',
'utterance': '그러시면, 우선 이러한 일상에 대해서 고민해보세요. 머리를 비우고 쉬어도 좋고, 진지하게 자신의 일상을 돌아보면서 어떻게 하면 이러한 고민을 줄일 수 있는지 생각해보세요.'},
{'speaker': '상담사', 'utterance': '어떤 생각을 하셨나요?'},
{'speaker': '내담자', 'utterance': '가족이랑 시간을 보내면서 즐겁게 생활해야겠다는 생각이 들었어요.'},
{'speaker': '상담사',
'utterance': '그렇군요. 가족이나 친구와의 소통은 그만큼의 만족감과 편안함을 가져다줄 수 있죠. 다양한 시간과 경험을 나누면서 그 사람들과 더 가까워질 수 있을 거 같아요.'},
{'speaker': '상담사', 'utterance': '더 말씀하실 내용이 있으신가요?'},
{'speaker': '내담자', 'utterance': '없어요. 감사합니다.'}]
"""
데이터 구조 분석
- speaker : 대화에 참여하는 사람을 의미 ('상담사' or '내담자'로 구분)
- utterance : 해당 화자가 실제로 말한 내용
모델을 학습시키기 위하여, 대화 데이터를 일관된 형식으로 변환하는 처리 과정이 필요.
- 내담자와 상담사를 각각 user와 assistant로 변환
- 대화 흐름을 일관되게 user -> assistant 순으로 정리 필요 (assistant로 시작하는 첫 메세지 제거, user로 끝나는 마지막 메세지들도 제거)
- 연속된 동일한 역할의 메세지가 나올 경우 이를 하나로 병합
user -> assistant -> user -> assistant 순으로 데이터를 구성
ex)
기존 : user -> assistant -> assistant -> user -> user -> assistant -> user - > assistant
처리 후 : user -> assistant(assistant + assistant) -> user(user + user) -> assistant -> user -> assistant
speaker_dict = {'내담자': 'user', '상담사': 'assistant'}
def preprocess_conversation(messages):
# speaker를 role로 변환
converted_messages = [{'role': speaker_dict[m['speaker']], 'content': m['utterance']} for m in messages]
# assistant로 시작하는 경우 첫 메시지 제거
if converted_messages and converted_messages[0]['role'] == 'assistant':
converted_messages = converted_messages[1:]
# user로 끝나는 경우 마지막 메시지들 제거
while converted_messages and converted_messages[-1]['role'] == 'user':
converted_messages = converted_messages[:-1]
# 연속된 동일 역할의 메시지 병합
converted_messages = merge_consecutive_messages(converted_messages)
# 대화가 비어있거나 홀수 개의 메시지만 남은 경우 처리
if not converted_messages or len(converted_messages) % 2 != 0:
return []
return converted_messages
def merge_consecutive_messages(messages):
if not messages:
return []
merged = []
current_role = messages[0]['role']
current_content = messages[0]['content']
for message in messages[1:]:
if message['role'] == current_role:
current_content += " " + message['content']
else:
merged.append({'role': current_role, 'content': current_content})
current_role = message['role']
current_content = message['content']
merged.append({'role': current_role, 'content': current_content})
return merged
def transform_to_new_format(original_data):
transformed_data = []
for conversation in original_data:
processed_conversation = preprocess_conversation(conversation)
if processed_conversation:
transformed_data.append(processed_conversation)
return transformed_data
result = transform_to_new_format(original_jsonl_data)
전처리 된 데이터 확인
- user, assistant로 변환 되었는지 확인
- user -> assistant 순으로 일관되게 처리 되었는지 확인
- 처음 시작이 user, 마지막이 assistant로 구성 되었는지 확인
result[0]
"""
[{'role': 'user', 'content': '내가 약간 중2병 같은 걸 증상을 보이고 있어요.'},
{'role': 'assistant', 'content': '중2병 증상이라니, 어떤 증상이신 건가요?'},
{'role': 'user',
'content': '그러니까 공부하기 싫어하고, 공격적이고, 좀 무례하게 말하고 싶은 게 많아져서 그런 거예요.'},
{'role': 'assistant',
'content': '그런 증상이 있으니까 힘드시겠죠. 중2병 같은 것이라고 생각하시는 이유는 무엇인가요?'},
{'role': 'user', 'content': '막 공부 안하고 이것저것 들먹이고 하고 싶은 게 너무 많아서 그런 거 같아요.'},
{'role': 'assistant',
'content': '그런 것도 어쩔 수 없이 찾아오는 시기가 있으니까 무리하지 않도록 해야겠죠. 대학교를 가면서 나아질 것 같았는데, 오히려 더 심해진 것 같다고 하셨죠. 그 원인이 무엇인가요?'},
...
{'role': 'assistant',
'content': '걱정은 당연한 것이죠. 그러나 지나치게 걱정하는 것은 오히려 스트레스를 더 받게 됩니다. 지금 이 곳에 오셔서 이야기를 나누어 봤을 때, 내담자님께서 가진 문제가 그렇게 심각한 것은 아닌 것 같습니다. 하지만, 무엇보다도, 내담자님이 존중하고 사랑하는 마음으로 자신을 바라보며, 나름의 방식으로 살아가는 것이 중요합니다. 무언가를 끼워 맞추어 주려 하지 마시고, 스스로 찾아낼 수 있는 길을 찾아보세요.'},
{'role': 'user', 'content': '그 말씀대로 할게요.'},
{'role': 'assistant',
'content': '그렇게 하셔서 조금이나마 좋아지시길 바라겠습니다. 이후에도 힘든 마음이 계속되면 언제든지 저를 찾아주세요.'}]
"""
전처리를 진행한 데이터를 저장 및 불러오기
with open("./train_dataset.jsonl", "w", encoding="utf-8") as file:
for conversation in result:
json_obj = {"messages": conversation}
json.dump(json_obj, file, ensure_ascii=False)
file.write("\n")
dataset = load_dataset("json", data_files="./train_dataset.jsonl")
dataset
"""
DatasetDict({
train: Dataset({
features: ['messages'],
num_rows: 8731
})
})
"""
3. 파라미터 설정 및 모델 학습
3.1 LoRA 파라미터 설정
주요 파라미터는 rank(랭크), alpha(알파), dropout(드롭아웃) 등이 있다.
peft_config = LoraConfig(
lora_alpha=128,
lora_dropout=0.05,
r=256,
bias="none",
target_modules=[
"q_proj",
"up_proj",
"o_proj",
"k_proj",
"down_proj",
"gate_proj",
"v_proj"],
task_type="CAUSAL_LM",
)
- LoRA 설정 target_modules : 모델의 이 핵심 부분들만 선택적으로 파인튜닝 진행
이로 인해,
메모리 효율을 높이고, 학습 속도를 향상
과적합 위험을 줄이고, 모델의 핵심 기능을 효과적으로 조정하면서도 전체적인 학습 효율성을 크게 높일 수 있다.
- q_proj, k_proj, v_proj : 각각 쿼리, 키, 밸류를 생성하는 데 사용
이 세 요소는 트랜스포머 아키텍처의 셀프 어텐션 메커니즘의 핵심으로, 문장 내 단어들 간의 관계를 파악하는데 중요- 쿼리(q_proj) : 특정 단어에 대한 정보 요청
- 키(k_proj) : 다른 단어들의 정보 나타냄
- 쿼리와 키 둘의 상호작용을 통해 단어 간 관련성을 결정
- 밸류(v_proj) : 각 단어의 실제 정보를 나타냄
- o_proj : 셀프 어텐션 메커니즘의 결과를 종합해 최종 출력을 생성
- up_proj, down_proj : 정보를 더 깊은 층으로 전달하거나 더 표면적인 층으로 전달하는 역할
- gate_proj : 정보의 중요도를 결정하는 게이트 기능을 수행
- q_proj, k_proj, v_proj : 각각 쿼리, 키, 밸류를 생성하는 데 사용
- task_type : 모델의 학습 방식을 지정하는 중요한 파라미터
"CAUSAL_LM"은 '인과적 언어 모델(Causal Language Model)'을 의미
'인과적' 이라는 표현은 모델이 오직 이전에 등장한 단어들만 고려해 예측한다는 특성을 나타냄
-> '지금까지 본 정보만을 바탕으로 다음에 올 내용을 예측하라'고 모델에 지시하는 것과 동일
3.2 학습 파라미터 설정
args = TrainingArguments(
output_dir="./model_output",
num_train_epochs=1,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
gradient_checkpointing=True,
optim="adamw_torch_fused",
logging_steps=100,
save_strategy="epoch",
learning_rate=2e-4,
bf16=True,
tf32=True,
max_grad_norm=0.3,
warmup_ratio=0.03,
lr_scheduler_type="constant",
push_to_hub=True,
report_to="wandb",
)
- output_dir : 학습된 모델과 관련파일(예 : 체크포인트, 설정 파일 등)을 저장할 경로
- num_train_epochs : 학습 데이터셋을 몇 번 반복하여 학습할지 설정
- per_device_train_batch_size : GPU 또는 CPU 하나당 학습에 사용하는 배치 크기
- gradient_accumulation_steps : 배치 크기가 작을 경우, 여러 스텝 동안 기울기를 누적한 뒤 역전파를 수행
- 실제 배치 크기는 per_device_train_batch_size × gradient_accumulation_steps로 계산
- 해당 코드에서는, per_device_train_batch_size(2) × gradient_accumulation_steps(4) = 2 × 4 = 8
- gradient_checkpointing : 메모리 사용량을 줄이기 위해 일부 중간 계산 결과를 저장하지 않고, 필요할 때 다시 계산
- optim : 옵티마이저 설정
- logging_steps : 몇 개의 학습 스텝마다 로그를 출력할지 설정
- save_strategy : 모델 체크포인트 저장 전략을 설정 (epoch로 설정되어 있어, 각 에포크가 끝날 때마다 모델이 저장)
- learning_rate : 학습률 설정
- bf16 : 모델 학습 시 bfloat16(BF16)데이터 형식을 사용
- tf32 : TF32(tensorfloat32)를 활성화
- max_grad_norm : 기울기 클리핑(Gradient Clipping)을 위한 최대 값
- warmup_ratio : 전체 학습 단계 중 처음 몇 퍼센트 동안 학습률을 점진적으로 증가시키는 비율
- lr_scheduler_type : 학습률 스케줄러 설정 (constant는 학습률을 일정하게 유지)
- push_to_hub : 학습이 완료되면 Hugging Face Hub에 모델을 업로드
- report_to : 학습 로그를 보낼 도구를 지정 (wandb 활용)
3.3 모델 학습
SFTTrainer를 사용해 실제 훈련 과정을 설정
trainer = SFTTrainer(
model=model,
args=args,
train_dataset=dataset,
max_seq_length=512,
peft_config=peft_config,
tokenizer=tokenizer,
packing=True,
)
- 모델, tokenizer, args, peft_config 설정
- packing = True : 최대 길이, LoRA의 파라미터, 효율적인 데이터 처리를 위해 여러 개의 짧은 텍스트를 하나의 긴 시퀀스로 묶어 처리하는 방법 활성화
학습 시작
trainer.train()