본문 바로가기
LLM/LLM 개발

Llama 3.1 8B 파인튜닝 하기(1) - 전처리, 파인튜닝

by 컴돌이_예준 2025. 3. 17.

📌 Llama 3.1 8B, 한국어 대화에 약하다.

최근 Llama 3.1 8B 모델을 테스트해본 결과, 한국어 대화에서 문맥이 맞지 않는 답변을 하는 경우가 많았다. 🤔
특히, 감성적인 대화에서 어색한 표현이 종종 등장하여 자연스러운 대화를 이어가기 어려웠음.

그래서 "감성대화 말뭉치" 데이터를 학습시켜, 보다 자연스럽고 문맥에 맞는 대화를 생성할 수 있도록 개선해보았다! 🚀


📂 사용한 데이터셋

📌 데이터셋 이름: 감성대화 말뭉치
📌 출처: AI Hub 감성대화 말뭉치
📌 설명:

  • 5만 건의 대화 데이터 포함 💬
  • 사람과 시스템의 응답이 기록되어 있음 📝

🛠️ 전처리 과정

1. 먼저 Data를 csv 파일로 변환하고 파일구조를 변경시킴.

db/emotional_data/train/test.CSV

db/emotional_data/test/train.CSV

 

2. CSV 파일을 Jsonl 형태로 변환

# data_preprocess.py
import pandas as pd
import json

# CSV 파일 불러오기
csv_file = "db/emotional_data/train/train.CSV"
df = pd.read_csv(csv_file)

# JSONL 변환을 위한 리스트
jsonl_data = []

for _, row in df.iterrows():
    messages = [{"role": "system", "content": "당신은 감정을 공감해주는 챗봇입니다."}]
    
    # 대화 쌍을 순서대로 추가
    for i in range(1, 4):  # 사람문장1~3, 시스템문장1~3
        # NaN을 안전하게 문자열로 처리
        user_text = str(row.get(f"사람문장{i}", "")).strip()
        assistant_text = str(row.get(f"시스템문장{i}", "")).strip()

        # 유효한 텍스트만 추가
        if user_text and user_text != "nan":  # 빈 문자열, 'nan' 제외
            messages.append({"role": "user", "content": user_text})
        if assistant_text and assistant_text != "nan":  # 빈 문자열, 'nan' 제외
            messages.append({"role": "assistant", "content": assistant_text})

    # 메시지가 시스템 메시지만 있는 경우 제외
    if len(messages) > 1:
        jsonl_data.append({"messages": messages})

# JSONL 파일 저장
jsonl_file = "db/emotional_data/train/train.jsonl"
with open(jsonl_file, "w", encoding="utf-8") as f:
    for entry in jsonl_data:
        json.dump(entry, f, ensure_ascii=False)
        f.write("\n")

print(f"✅ 변환 완료! {jsonl_file} 파일을 확인하세요. (총 {len(jsonl_data)}개 대화)")

# Output:
# ✅ 변환 완료! db/emotional_data/train/train.jsonl 파일을 확인하세요. (총 51630 개 대화)

🔬 Finetuning 과정

첫번째 시도 (로컬에서 돌리기)

로컬 GPU: RTX 3060 Ti (VRAM 8GB)

=> 학습시간이 약 21시간이 걸리고, GPU가 버티질 못해 Colab에서 자원을 할당받기로 결정했다.

🚫 실패 

더보기
# finetune_model.py
# Description: LoRA를 적용한 Meta-Llama 모델을 감정 대화 데이터로 파인튜닝하는 코드

import torch
import pandas as pd
from transformers import AutoTokenizer, LlamaForCausalLM, BitsAndBytesConfig, TrainingArguments, DataCollatorForLanguageModeling
from peft import LoraConfig, get_peft_model
from datasets import load_dataset
from trl import SFTTrainer

# GPU 메모리 캐시 정리
torch.cuda.empty_cache()
import gc
gc.collect()

# 모델과 토크나이저 로드
model = "meta-llama/Meta-Llama-3.1-8B"
tokenizer = AutoTokenizer.from_pretrained(model)
tokenizer.pad_token = tokenizer.eos_token  # 패딩 토큰 설정

# 🛠 chat_template 수동 설정
tokenizer.chat_template = "{% for message in messages %}\n{{ message['role'] }}: {{ message['content'] }}{% endfor %}"


# QLoRA 설정 적용
quant_config = BitsAndBytesConfig(
    load_in_4bit=True,                    
    bnb_4bit_compute_dtype=torch.float16,  # VRAM 절약을 위해 float16 사용
    bnb_4bit_quant_type="nf4",             # QLoRA에서 일반적으로 사용하는 양자화 방식
    bnb_4bit_use_double_quant=True,        # 2단계 양자화 적용하여 VRAM 절약
)

# 모델 로드
model = LlamaForCausalLM.from_pretrained(
    model,                              # 모델 이름
    quantization_config=quant_config,   # 양자화 설정
    device_map="auto",                  # 모델을 어떤 장치에 할당할지 지정 (GPU 또는 CPU 자동 선택)
)

# 입력 텐서에 대한 gradient 계산 활성화
model.enable_input_require_grads()
model.train()  # 학습 모드 활성화

# LoRA 설정
lora_config = LoraConfig(
    r=32,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)

# JSONL 데이터 로드 (ChatML 포맷 사용)
data_path = "db/emotional_data/train/train.jsonl"
dataset = load_dataset("json", data_files={"train": data_path}, split="train")

# 데이터 전처리 함수 정의
def preprocess_function(examples):
    # 여러 개의 샘플을 처리하기 위해 리스트 생성
    formatted_texts = []

    for messages in examples["messages"]:
        # 개별 메시지를 하나의 텍스트로 변환
        text = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in messages])
        formatted_texts.append(text)

    # 토크나이징
    encodings = tokenizer(
        formatted_texts,
        truncation=True,
        padding="max_length",
        max_length=256,
        return_tensors="pt"
    )
    encodings["labels"] = encodings["input_ids"].clone()  # Causal LM 라벨 설정
    return encodings


tokenized_dataset = dataset.map(preprocess_function, batched=True)

# 데이터 컬레이터 설정 (MLM 비활성화)
data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=False  # Causal LM에서는 MLM 사용하지 않음
)

# 학습 설정
training_args = TrainingArguments(
    output_dir="./lora_finetuned_model",    # 출력 디렉토리
    per_device_train_batch_size=1,          # 장치 당 배치 크기
    gradient_accumulation_steps=16,          # 그래디언트 누적 스텝
    num_train_epochs=1,                     # 학습 에폭 수
    learning_rate=1e-4,                     # 학습률
    fp16=True,                              # 혼합 정밀도 학습
    save_steps=250,                         # 모델 저장 주기
    logging_steps=50,                       # 로그 출력 주기
     optim="adamw_torch",                   # 옵티마이저
    lr_scheduler_type="cosine",             # 학습률 스케줄러
)

# SFTTrainer로 파인튜닝
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    processing_class=tokenizer,
)

# 학습 시작
trainer.train()

# 학습된 모델 저장
model.save_pretrained("./lora_finetuned_model")
tokenizer.save_pretrained("./lora_finetuned_model")

 

두번째 시도 (Colab에서 무료 T4 GPU 할당)
=> GPU 할당량이 모자라서, 9.99 달러를 지불하기로 결정했다.

🚫 실패 

 

세번째 시도 (Colab에서 유로 A100 GPU 할당, 배치사이즈 8)

성공

 

더보기
# 학습 설정
training_args = TrainingArguments(
    output_dir="/content/drive/MyDrive/Colab Notebooks/Llama3.1 8B/lora_finetuned_model",    # 출력 디렉토리
    per_device_train_batch_size=8,          # 장치 당 배치 크기
    gradient_accumulation_steps=2,         # 그래디언트 누적 스텝
    num_train_epochs=1,                          # 학습 에폭 수
    learning_rate=2e-4,                             # 학습률
    fp16=True,                                           # 혼합 정밀도 학습
    save_steps=250,                                 # 모델 저장 주기
    logging_steps=50,                               # 로그 출력 주기
    optim="adamw_torch_fused",              # 옵티마이저
    lr_scheduler_type="cosine",                # 학습률 스케줄러
    dataloader_num_workers=4,               # 데이터 로딩 병렬화
    report_to="tensorboard",                     # 학습 모니터링
)
 
 

 

📊 파인튜닝 평가 - TensorBoard를 이용해 정확도, 손실, 학습률 시각화

 

종합평가

  • 성공 여부: 3200스텝(1 에포크)을 약 54분 만에 완료했으며, 손실 감소와 정확도 향상은 학습이 잘 진행되었음을 보여줍니다. 평균 손실 1.4135과 토큰 정확도 0.6552는 초기 미세 조정 결과로 만족할 만합니다.
 

 

더보기

1. 손실 (Training Loss)

  • 그래프 분석: 손실 곡선(train/loss)은 0 스텝에서 약 1.6 이상으로 시작해 3227스텝(1epoch 완료)에서 약 1.41 으로 수렴했습니다. 초기에는 급격히 감소하다가 중반 이후에는 완만한 감소 추세를 보이며 안정화되었습니다.
  • 의미: 손실이 감소했다는 것은 모델이 훈련 데이터를 점점 더 잘 학습하고 있음을 나타냅니다. 약 1.41 수준의 손실은 감정 대화 태스크에서 모델이 적절히 적응했음을 시사하지만, 실제 성능(예: 감정 공감 품질)은 검증 데이터로 평가해야 합니다.
  • 해석: 손실 감소가 부드럽게 이루어졌으므로 과적합 없이 학습이 잘 진행된 것으로 보입니다. 그러나 더 낮은 손실(예: 1.3 이하)을 목표로 한다면 에포크를 추가하거나 학습률을 조정할 수 있습니다.

2. 토큰 정확도 (Mean Token Accuracy)

  • 그래프 분석: 토큰 정확도(train/mean_token_accuracy)는 0 스텝에서 약 0.62로 시작해 3227 스텝에서 약 0.655로 증가했습니다. 초기에는 변동이 컸으나, 중반 이후에는 안정적인 상승세를 보이며 약 0.655 수준에서 수렴했습니다.
  • 의미: 토큰 정확도는 모델이 예측한 토큰이 실제 레이블과 얼마나 일치하는지를 나타냅니다. 0.655는 모든 토큰의 65%를 정확히 예측했음을 의미하며, 감정 대화 데이터의 복잡성을 고려하면 괜찮은 수준입니다. 하지만 대화 생성 모델에서는 정확도만으로 성능을 판단하기 어렵습니다.
  • 해석: 정확도가 0.65로 비교적 낮게 수렴한 것은 모델이 아직 학습 데이터에 완전히 적응하지 못했을 수 있음을 시사합니다.

3. 학습률 (Learning Rate)

  • 그래프 분석: 학습률(train/learning_rate)은 0 스텝에서 약 2e-4(0.0002)로 시작해 3227 스텝에서 0으로 수렴했습니다. cosine 스케줄러에 따라 부드럽게 감소하는 곡선을 그리며, 중반 이후에는 급격히 줄어드는 경향을 보였습니다.
  • 의미: 학습률이 점진적으로 감소하며 모델이 초기에는 크게 업데이트되고, 후반에는 미세 조정을 한다는 것을 나타냅니다. 이는 과적합을 방지하고 수렴을 돕는 일반적인 전략입니다.
  • 해석: 학습률이 적절히 조정되어 손실 감소와 정확도 증가에 기여한 것으로 보입니다. 그러나 마지막 단계에서 너무 빠르게 0에 도달했다면, 더 긴 학습이나 다른 스케줄러(예: linear)를 고려할 수 있습니다.

글이 길어져서 테스트는 다음 글에 적겠습니다!