https://product.kyobobook.co.kr/detail/S000214934825
한 권으로 끝내는 실전 LLM 파인튜닝 | 강다솔 - 교보문고
한 권으로 끝내는 실전 LLM 파인튜닝 | 실무 현장에서 꼭 필요한 파인튜닝, PEFT, vLLM 서빙 기술을 직접 실습하면서 배워 보자!AI 기술의 최전선에서 배우는 LLM 파인튜닝의 모든 것! 이론적 토대부터
product.kyobobook.co.kr
"한 권으로 끝내는 실전 LLM 파인튜닝" 교재를 활용해 3주(주말 제외) 동안 진행 되는 온라인 스터디
QLoRA 이론 : https://minhyuk0914.tistory.com/39
한 권으로 LLM 온라인 스터디 1기 Day_13 - QLoRA 이론
https://product.kyobobook.co.kr/detail/S000214934825 한 권으로 끝내는 실전 LLM 파인튜닝 | 강다솔 - 교보문고한 권으로 끝내는 실전 LLM 파인튜닝 | 실무 현장에서 꼭 필요한 파인튜닝, PEFT, vLLM 서빙 기술을
minhyuk0914.tistory.com
QLoRA 실습
QLoRA를 활용하여 Text-to-SQL 문제를 해결하는 과정을 실습해보자. 양자화를 활용하여 대규모 모델을 효율적으로 학습하고, OpenAI API를 활용하여 모델을 평가해보자.
- 목표 : QLoRA기반 한국어 특화 모델 Llama3를 활용해 자연어 질의로부터 SQL 쿼리를 생성하는 Text-to-SQL모델을 학습하고 평가
- 기술 스택 :
- 모델 : Llama-3-Alpha-Ko-8B-Instruct
- 데이터셋 : KoText-to-SQL
- QLoRA
- 평가 : OpenAI API 활용
1. RunPod 환경설정
- GPU : H100 (VRAM 80GB)
- pytorch version : 2.2
- GPU 수량 : 1개
- Container Disk : 400GB
- Volume Disk : 400GB
2. 데이터셋 준비
2.1 데이터셋 로드
# 텍스트 - SQL로 연결된 데이터이다.
dataset = datasets.load_dataset("daje/kotext-to-sql-v1")
2.2 문장 길이에 따른 난이도 분류 및 샘플링
- 문장 길이 = ko_instruction + input + response
- 문장 길이가 10 ~ 100 : 쉬움(easy)
- 문장 길이가 101 ~ 300 : 보통(moderate)
- 문장 길이가 301 ~ 1000 : 어려움(difficult)
샘플링 : 데이터의 일부만을 추출하여 진행하는 방식
랜덤 샘플링 : 데이터의 일부만을 무작위로 추출하여 진행하는 방식(샘플링의 방법 중 하나)
랜덤 샘플링의 효과 : 데이터의 일부만을 학습을 진행하기에 학습 시간 감소, 무작위 추출로 인한 데이터의 다양성을 유지
def add_length_column(dataset):
df = dataset.to_pandas()
df["total_length"] = 0
for column_name in ["ko_instruction", "input", "response"]:
num_words = df[column_name].astype(str).str.split().apply(len)
df["total_length"] += num_words
return df
df = add_length_column(dataset["train"])
def filter_by_total_length(df, difficulty, number_of_samples):
if difficulty == "easy":
return df[df["total_length"].between(10, 100)].iloc[:number_of_samples]
elif difficulty == "moderate":
return df[df["total_length"].between(101, 300)].iloc[:number_of_samples]
elif difficulty == "difficult":
return df[df["total_length"].between(301, 1000)].iloc[:number_of_samples
easy = filter_by_total_length(df, "easy", 5000)
medium = filter_by_total_length(df, "moderate", 5000)
hard = filter_by_total_length(df, "difficult", 5000)
dataset = pd.concat([easy, medium, hard])
dataset = dataset.sample(frac=1)
dataset = Dataset.from_pandas(dataset)
easy.shape, medium.shape, hard.shape, dataset.shape
"""
((5000, 7), (5000, 7), (5000, 7), (15000, 8))
"""
2.3 채팅 형식 데이터 변환
모델 학습에 적합한 대화 형식으로 데이터를 변환
# trl docs에 보면 이와 같은 방식으로 SFT Trainer용 데이터를 만들 수 있습니다.
# docs에서는 eos_token을 별도로 추가하라는 안내는 없지만, 저자는 습관적으로 eos_token을 붙혀줍니다.
def get_chat_format(element):
system_prompt = "You are a helpful programmer assistant that excels at SQL."
user_prompt = "Task: {ko_instruction}\nSQL table: {input}\nSQL query: "
return {
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt.format_map(element)},
{"role": "assistant", "content": element["response"]+tokenizer.eos_token},
]
}
# 데이터 전처리를 위해 먼저 도크나이저를 불러옵니다.
# 자세한 설명은 ###4.2.4 섹션에서 설명합니다.
tokenizer = AutoTokenizer.from_pretrained("allganize/Llama-3-Alpha-Ko-8B-Instruct")
# 시퀀스의 길이를 맞추기 위해 추가되는 특별한 토큰으로, 일반적으로 pad_token_id에 해당합니다.
# 패딩 방향을 설정함으로써 모델이 일관된 입력 형식을 받을 수 있도록 합니다.
tokenizer.padding_side = 'right'
# 데이터를 일괄적으로 대화형식으로 변경합니다.
dataset = dataset.map(get_chat_format, remove_columns=dataset.features, batched=False)
# train과 test 데이터를 0.9와 0.1로 분할합니다.
dataset = dataset.train_test_split(test_size=0.05)
# json으로 저장합니다.
dataset["train"].to_json("train_dataset.json", orient="records")
dataset["test"].to_json("test_dataset.json", orient="records")
# 정성적으로 변환되었는지 확인합니다.
dataset["train"], dataset["test"]
"""
(Dataset({
features: ['messages'],
num_rows: 249097
}),
Dataset({
features: ['messages'],
num_rows: 13111
}))
"""
dataset["train"][0]
"""
{'messages': [{'content': 'You are a helpful programmer assistant that excels at SQL.',
'role': 'system'},
{'content': 'Task: 각 국적의 호스트 수와 함께 다양한 국적을 보여줍니다. 원형 차트를 보여주세요.\nSQL table: CREATE TABLE party (\n Party_ID int,\n Party_Theme text,\n Location text,\n First_year text,\n Last_year text,\n Number_of_hosts int\n)\n\nCREATE TABLE host (\n Host_ID int,\n Name text,\n Nationality text,\n Age text\n)\n\nCREATE TABLE party_host (\n Party_ID int,\n Host_ID int,\n Is_Main_in_Charge bool\n)\nSQL query: ',
'role': 'user'},
{'content': 'SELECT Nationality, COUNT(*) FROM host GROUP BY Nationality<|end_of_text|>',
'role': 'assistant'}]}
"""
저장된 train 데이터를 학습데이터로 사용하기위해 불러오기
# 저장된 train 데이터를 불러옵니다.
dataset = load_dataset("json", data_files="train_dataset.json", split="train")
3. 양자화 및 모델 준비
양자화 파라미터 설정
- NF4
- Double Quantization
- BF16
모델 준비
- Llama-3-Alpha-Ko-8B-Instruct
- attn_implementation = "flash_attention_2" : 더 빠르고 메모리 효율적인 어텐션을 수행
- 양자화 파라미터 설정 적용
# Quantization config 세팅 -> 모델이 사용하는 vram을 최소화하기
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
# double quantization으로 양자화 오류를 줄입니다.
bnb_4bit_use_double_quant=True,
# 다양한 양자화 종류 중 nf4를 선택
bnb_4bit_quant_type="nf4",
# llama는 16비트 부동 소수를 사용해 학습됐습니다.
bnb_4bit_compute_dtype=torch.bfloat16
)
# 이번 프로젝트에서 사용할 모델로 LLama2를 Base Model로 사용하여 코드를 전용으로 만든 모델
model_id = "allganize/Llama-3-Alpha-Ko-8B-Instruct"
# 모델과 토크나이저 불러오기
model = AutoModelForCausalLM.from_pretrained(
# 앞서 정의한 모델을 불러옵니다.
model_id,
# 모델을 사용할 디바이스를 자동으로 설정합니다.
device_map="auto",
# 더 빠르고 메모리 효율적인 어텐션 구현 방식입니다.
attn_implementation="flash_attention_2",
torch_dtype=torch.bfloat16,
# 양자화 설정을 적용합니다.
quantization_config=bnb_config
)
# 토크나이저 불러옵니다.
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 시퀀스의 길이를 맞추기 위해 추가되는 특별한 토큰으로, 일반적으로 pad_token_id에 해당합니다.
# 패딩 방향을 설정함으로써 모델이 일관된 입력 형식을 받을 수 있도록 합니다.
tokenizer.padding_side = 'right'
# setup_chat_format 함수를 사용하는 주요 이유는 모델과 토크나이저가 대화형 AI 시스템에서 요구하는 형식에 맞게 추가 설정을 적용하기 위함입니다.
# 이 함수는 특별 토큰 추가, 입력 형식 포맷팅, 토큰 임베딩 조정 등의 작업을 수행하여 모델이 대화형 응답을 보다 효과적으로 생성할 수 있도록 준비합니다.
model, tokenizer = setup_chat_format(model, tokenizer)
4. 학습
4.1 LoRA 구성
LoRA를 통해 특정 모듈에만 학습을 집중하여 메모리 사용량을 줄인다
peft_config = LoraConfig(
lora_alpha=128,
lora_dropout=0.05, # Lora 학습 때 사용할 dropout 확률을 지정합니다. 드롭아웃 확률은 과적합 방지를 위해 학습 중 무작위로 일부 뉴런을 비활성화하는 비율을 지정합니다.
r=256, # Lora의 저차원 공간의 랭크를 지정합니다. 랭크가 높을수록 모델의 표현력이 증가하지만, 계산 비용도 증가합니다.
bias="none", # Lora 적용 시 바이어스를 사용할지 여부를 설정합니다. "none"으로 설정하면 바이어스를 사용하지 않습니다.
target_modules=["q_proj", "o_proj", # Lora를 적용할 모델의 모듈 리스트입니다.
"k_proj", "v_proj"
"up_proj", "down_proj",
"gate_proj",
],
task_type="CAUSAL_LM", # 미세 조정 작업 유형을 CAUSAL_LM으로 지정하여 언어 모델링 작업을 수행합니다.
)
4.2 학습 파라미터 설정
데이터 전체에 대한 학습이 약 9.8시간 걸리므로 해당 주석처리된 부분인 max_steps를 지정하여 코드를 수행하여서 시간 및 요금에 알맞은 선택 권장
args = TrainingArguments(
output_dir="code-llama-7b-text-to-sql", # 모델 저장 및 허브 업로드를 위한 디렉토리 지정 합니다.
num_train_epochs=1, # number of training epochs
# max_steps=100, # 100스텝 동안 훈련 수행합니다.
per_device_train_batch_size=1, # 배치 사이즈 설정 합니다.
gradient_accumulation_steps=2, # 4스텝마다 역전파 및 가중치 업데이트합니다.
gradient_checkpointing=True, # 메모리 절약을 위해 그래디언트 체크포인팅 사용합니다.
optim="adamw_torch_fused", # 메모리 효율화할 수 있는 fused AdamW 옵티마이저 사용합니다.
logging_steps=10, # 10스텝마다 로그 기록합니다.
save_strategy="epoch", # 매 에폭마다 체크포인트 저장합니다.
learning_rate=2e-4, # 학습률 2e-4로 설정 (QLoRA 논문 기반)합니다.
bf16=True, # 정밀도 설정으로 학습 속도 향상합니다.
tf32=True,
max_grad_norm=0.3, # 그래디언트 클리핑 값 0.3으로 설정합니다.
warmup_ratio=0.03, # 워밍업 비율 0.03으로 설정 (QLoRA 논문 기반)합니다.
lr_scheduler_type="constant", # 일정한 학습률 스케줄러 사용합니다.
push_to_hub=True, # 훈련된 모델을 Hugging Face Hub에 업로드합니다.
report_to="wandb", # wandb로 매트릭 관찰합니다.
)
4.3 학습 실행
# trainer를 학습합니다.
trainer.train()
5. OpenAI API를 활용한 평가
OpenAI API에 gpt-4o-mini를 활용한 모델 평가 방법 중 일부
client = OpenAI()
def compare_sql_semantics(idx):
save_path = f"/content/drive/MyDrive/text_to_sql_result_ver0.1/result_{idx}.json"
if Path(save_path).exists():
print("이미 처리된 파일입니다.")
pass
else:
item = generated_result[idx]
problem_description, generated_query, ground_truth_query = item
# ChatGPT에게 물어볼 프롬프트 작성
prompt = f"""다음 문제와 두 SQL 쿼리가 의미적으로 동일한 결과를 반환하는지 판단해주세요:
문제 설명: {problem_description}
생성된 쿼리:
{generated_query}
정답 쿼리:
{ground_truth_query}
두 쿼리가 문제에 대해 의미적으로 동일한 결과를 반환한다면 answer에 "1"라고 대답하고,
그렇지 않다면 "0"라고 대답한 후 차이점을 explanation에 적으세요.
쿼리의 구조나 사용된 함수가 다르더라도 결과가 같다면 의미적으로 동일하다고 판단해주세요."""
# ChatGPT API 호출
response = client.chat.completions.create(
model="gpt-4o-mini", # 또는 사용 가능한 최신 모델
response_format={ "type": "json_object" },
messages=[
{"role": "system", "content": """You are a helpful assistant that compares the semantic meaning of SQL queries in the context of a given problem.
return json format below:
{
"answer": "...",
"explanation": "..."
}
"""},
{"role": "user", "content": prompt}
]
)
# ChatGPT의 응답 추출
answer = response.choices[0].message.content.strip()
# 결과를 JSON 파일로 저장
with open(save_path, "w", encoding="utf-8") as f:
json.dump(answer, f, ensure_ascii=False, indent=4)
return answer
# generated_result에 인덱스 추가
indexed_openai_evaluation = list(range(len((openai_evaluation))))
# pqdm을 사용하여 병렬 처리
results = pqdm(indexed_openai_evaluation, compare_sql_semantics, n_jobs=40)
Llama3을 통한 결과가 정답 쿼리인 SQL과 의미적으로 동일한지 판단하는 방식
json_result = []
for result in results:
json_result.append(json.loads(result))
df = pd.DataFrame(json_result)
df["answer"] = df["answer"].map(lambda x : int(x))
after_accuracy = df["answer"].sum() / len(df["answer"])
print(f"Accuracy: {after_accuracy*100:.2f}%")
"""
Accuracy: 62.80%
"""
의미적 동등성 판단에서의 정확도 : 62.80으로, 모델이 쿼리의 의미를 이해하였다고 판단할 수 있다.
'DL > LLM&RAG' 카테고리의 다른 글
한 권으로 LLM 온라인 스터디 1기 Day_15 - vLLM (0) | 2025.01.21 |
---|---|
한 권으로 LLM 온라인 스터디 1기 Day_13 - QLoRA 이론 (0) | 2025.01.21 |
한 권으로 LLM 온라인 스터디 1기 Day_12 - 효율적인 파라미터 튜닝(PEFT) - LoRA 개념 및 실습 2장 (1) | 2025.01.21 |
한 권으로 LLM 온라인 스터디 1기 Day_11 - 효율적인 파라미터 튜닝(PEFT) - LoRA 개념 및 실습 1장 (0) | 2025.01.20 |
한 권으로 LLM 온라인 스터디 1기 Day_10 - 다중 GPU를 활용한 Llama3.1-8B-instruct 파인튜닝 (2) | 2025.01.20 |