LLM Day 35 - Unsloth finetuning

Soyee Sung·2025년 4월 1일

LLM

목록 보기
34/34

ETF 데이터로 LLM 파인튜닝하기 (Unsloth 활용)

데이터 품질: 더 많은 ETF 데이터와 다양한 질문-답변 쌍을 추가하면 모델 성능이 향상됩니다.
하이퍼파라미터 튜닝: LoRA 랭크(r), 학습률, 배치 크기 등을 조정하여 최적의 성능을 찾으세요.
평가 지표: 파인튜닝 후 모델 성능을 ETF 관련 질문에 대한 정확도로 평가하세요.
메모리 관리: 가용 VRAM에 맞게 max_seq_length, 배치 크기, LoRA 매개변수를 조정하세요.

  1. Unsloth 환경 설정
  • https://github.com/unslothai/unsloth
    1) 환경 설정 및 필요 라이브러리 설치
    먼저 필요한 라이브러리를 설치합니다.
    Gemma 3는 최신 transformers 라이브러리가 필요합니다.
  1. 한국어 데이터셋 준비
    데이터셋 검토가 파인튜닝의 첫 단계임
    데이터는 파인튜닝에 적합한 형태로 변환 필요함
    1) 데이터셋 로드 및 전처리
import pandas as pd
import numpy as np

# CSV 파일 로드 (한글 인코딩 cp1252 사용)
df = pd.read_csv('etf_info.csv', encoding='cp949')

# 데이터 확인
print(f"데이터 형태: {df.shape}")
print("칼럼 목록:")
for col in df.columns:
    print(f"- {col}")
# 한글 컬럼명 매핑 정의
column_mapping = {
    '표준코드': 'standard_code',
    '단축코드': 'ticker',
    '한글종목명': 'name_kr',
    '한글종목약명': 'short_name_kr',
    '영문종목명': 'name_en',
    '상장일': 'listing_date',
    '기초지수명': 'base_index',
    '지수산출기관': 'index_provider',
    '추적배수': 'tracking_multiplier',
    '복제방법': 'replication_method',
    '기초시장분류': 'base_market_category',
    '기초자산분류': 'base_asset_category',
    '상장좌수': 'listed_shares',
    '운용사': 'manager',
    'CU수량': 'cu_quantity',
    '총보수': 'total_expense_ratio',
    '과세유형': 'tax_type'
}

# 데이터프레임의 컬럼명 변경
df = df.rename(columns=column_mapping)

df.head()

2) 다양한 파인튜닝 형식에 맞게 데이터셋 구성

2.1 Instruction Tuning (Alpaca 형식)
Alpaca 형식은 instruction-input-output 구조를 따름
ETF 정보를 질문-답변 형태로 변환하는 방식임
이 구조는 모델이 지시사항을 이해하고 실행하도록 훈련함
Instruction Tuning은 모델의 응답 정확도를 향상시킴

2.2 Conversation Fine-tuning (ShareGPT 형식)
ShareGPT 형식은 다중 턴 대화 구조를 활용함
ETF 데이터를 연속적인 대화 흐름으로 변환함
이 방식은 모델의 맥락 이해 능력을 강화함
대화형 학습은 자연스러운 응답 생성에 효과적임
사용자-모델 간 상호작용 패턴을 학습할 수 있음

  1. Unsloth를 활용한 파인튜닝 구현
    준비된 데이터셋을 사용하여 다양한 파인튜닝 방법을 구현
    3.1 Alpaca 방식 기본 파인튜닝
# 허깅페이스 데이터셋 다운로드 
import os
from datasets import load_dataset

alpaca_dataset = load_dataset(
    "Soy22/etf-alpaca", 
    token=os.getenv("HUGGINGFACE_TOKEN")
)

alpaca_dataset
# datasets 라이브러리의 train_test_split 메서드 사용
from datasets import Dataset, DatasetDict

# train 분할을 다시 train과 test로 나눔
train_test = alpaca_dataset['train'].train_test_split(test_size=0.1, seed=1207)

# 새로운 DatasetDict 생성
split_datasets = DatasetDict({
    'train': train_test['train'],
    'test': train_test['test']
})


split_datasets
split_datasets['train'][0]
from unsloth import FastLanguageModel
from transformers import TextStreamer

# 기본 모델 로드 (파인튜닝 없이)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen2.5-7B",  # 또는 다른 모델 선택
    max_seq_length = 2048, # 컨텍스트 길이 설정
    load_in_4bit = True, # 4비트 양자화로 메모리 사용 감소
)

# 추론 모드로 전환 (2배 빠른 추론 속도)
FastLanguageModel.for_inference(model)

# Fine-tuning 전에 예시 프롬프트 테스트 (한국어 ETF 질문)
def test_model_with_prompt(prompt):
    # 토크나이징
    inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
    
    # 텍스트 스트리머 설정 (생성 과정을 실시간으로 볼 수 있음)
    text_streamer = TextStreamer(tokenizer)
    
    # 생성
    _ = model.generate(
        **inputs, 
        streamer=text_streamer, 
        max_new_tokens=512,  # 최대 생성 토큰 수
        temperature=0.7,     # 창의성 조절 (낮을수록 보수적인 응답)
        min_p=0.1            # 최소 확률 임계값
    )
    
    print("\n" + "-"*50 + "\n")

# 질문 전달하여 답변 생성
test_model_with_prompt("한국투자 ACE 레버리지 ETF의 상장일은 언제인가요?")
split_datasets['train'][1]
# 질문 전달하여 답변 생성
test_model_with_prompt("미래에셋TIGER KOFR금리액티브증권상장지수투자신탁 ETF의 과세유형은 무엇인가요?")
from unsloth import FastLanguageModel
import torch
from trl import SFTTrainer
from transformers import TrainingArguments

# LoRA 어댑터 추가
model = FastLanguageModel.get_peft_model(
    model,
    r = 16,   # LoRA 랭크 (8, 16, 32, 64, 128 등 권장)
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",],
    lora_alpha = 16,    # r과 동일한 값 권장
    lora_dropout = 0,   # 최적화를 위해 0 권장
    bias = "none",      # 최적화를 위해 "none" 권장
    use_gradient_checkpointing = "unsloth",   # 메모리 사용 30% 감소
    random_state = 1207,
)

# Alpaca 형식의 프롬프트 템플릿 정의
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""

# 데이터셋 전처리 함수
def formatting_prompts_func(examples):
    instructions = examples["instruction"]
    inputs = examples["input"]
    outputs = examples["output"]
    texts = []

    for instruction, input, output in zip(instructions, inputs, outputs):
        # 토큰화 시 EOS 토큰 추가
        text = alpaca_prompt.format(instruction, input, output) + tokenizer.eos_token
        texts.append(text)

    return {"text": texts}

# 데이터셋 전처리
processed_dataset = split_datasets.map(formatting_prompts_func, batched=True)

# 데이터셋 확인
processed_dataset

[활용 팁]

메모리 부족(OOM): batch_size 줄이기, gradient_accumulation_steps 늘리기
낮은 품질: 학습률 낮추기, 더 많은 데이터 사용
느린 학습: packing=True 시도 (짧은 시퀀스만)
과적합: weight_decay 늘리기, dropout 추가, 데이터 다양화

# 트레이너 설정 및 학습
trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = processed_dataset['train'],  # 최소 100개 이상의 고품질 데이터 사용 권장, 300개 이상이 최적
    eval_dataset = processed_dataset['test'], # None으로 설정하면 평가 미진행 가능
    dataset_text_field = "text",
    max_seq_length = 2048,
    dataset_num_proc = 2,
    packing = False,  # 짧은 시퀀스의 경우 True로 설정하면 훈련속도 5배 향상 가능
    args = TrainingArguments(
        per_device_train_batch_size = 4,
        gradient_accumulation_steps = 8,  # 더 큰 배치 크기 효과
        warmup_steps = 5,
        num_train_epochs = 3,   # 1 ~ 3 범위의 값을 설정
        # max_steps = 100,  
        learning_rate = 2e-4,  # epoch를 늘린 경우, 긴 훈련 실행은 2e-5로 줄이세요
        fp16 = not torch.cuda.is_bf16_supported(),
        bf16 = torch.cuda.is_bf16_supported(),
        logging_steps = 1,
        optim = "adamw_8bit",
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 1207,
        output_dir = "outputs",
    ),
)

# 학습 실행
trainer_stats = trainer.train()
from unsloth import FastLanguageModel
from transformers import TextStreamer

# 추론 모드로 전환 (2배 빠른 추론)
FastLanguageModel.for_inference(model)

# 알파카 형식 추론 (일반 텍스트 입력)
def generate_alpaca_response(instruction, input_text=""):
    # 알파카 프롬프트 형식 적용
    alpaca_prompt = f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Input:
{input_text}

### Response:
"""
    
    # 모델 추론
    inputs = tokenizer(alpaca_prompt, return_tensors="pt").to("cuda")
    text_streamer = TextStreamer(tokenizer)
    _ = model.generate(
        **inputs, 
        streamer=text_streamer, 
        max_new_tokens=512,
        temperature=0.7,
        min_p=0.1
    )

# 예제 추론
generate_alpaca_response(
    "다음 ETF에 대한 기본 정보를 제공해주세요.",
    "한국투자 ACE 레버리지 ETF(종목코드: 152500)"
)
# 예제 추론
generate_alpaca_response(
    "다음 ETF에 대한 기본 정보를 제공해주세요.",
    "미래에셋TIGER KOFR금리 ETF"  # 종목코드: 449170
)
# 모델 저장
model.save_pretrained("etf_alpaca_model")
tokenizer.save_pretrained("etf_alpaca_model")
# Hugging Face에 업로드
model.push_to_hub("username/Qwen2.5-7B-etf", token = os.getenv("HUGGINGFACE_TOKEN"))
tokenizer.push_to_hub("username/Qwen2.5-7B-etf", token = os.getenv("HUGGINGFACE_TOKEN"))

3.2 대화 파인튜닝 (Conversational Fine-tuning)

# 허깅페이스 데이터셋 다운로드 
import os
from datasets import load_dataset

sharegpt_dataset = load_dataset(
    "Soy22/etf-sharegpt", 
    token=os.getenv("HUGGINGFACE_TOKEN")
)

sharegpt_dataset
# datasets 라이브러리의 train_test_split 메서드 사용
from datasets import Dataset, DatasetDict

# train 분할을 다시 train과 test로 나눔
train_test = sharegpt_dataset['train'].train_test_split(test_size=0.1, seed=1207)

# 새로운 DatasetDict 생성
split_datasets = DatasetDict({
    'train': train_test['train'],
    'test': train_test['test']
})


split_datasets
from unsloth import FastModel
from transformers import TextStreamer

# 기본 모델 로드 (파인튜닝 없이)
model, tokenizer = FastModel.from_pretrained(
    model_name = "unsloth/gemma-3-12b-it-unsloth-bnb-4bit",  # 4비트 양자화
    max_seq_length = 2048, # 긴 컨텍스트를 위해 원하는 값 선택!
    load_in_4bit = True,  # 메모리 감소를 위한 4비트 양자화
    load_in_8bit = False, # 조금 더 정확하지만 2배의 메모리 사용
    full_finetuning = False, # 전체 미세조정 여부
)

# 추론 모드로 전환 (2배 빠른 추론 속도)
FastModel.for_inference(model)

# Fine-tuning 전에 예시 프롬프트 테스트 (한국어 ETF 질문)
def test_model_with_prompt(prompt):
    # 토크나이징
    inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
    
    # 텍스트 스트리머 설정 (생성 과정을 실시간으로 볼 수 있음)
    text_streamer = TextStreamer(tokenizer)
    
    # 생성
    _ = model.generate(
        **inputs, 
        streamer=text_streamer, 
        max_new_tokens=512,  # 최대 생성 토큰 수
        temperature=0.7,     # 창의성 조절 (낮을수록 보수적인 응답)
        min_p=0.1            # 최소 확률 임계값
    )
    
    print("\n" + "-"*50 + "\n")

# 질문 전달하여 답변 생성
test_model_with_prompt("한국투자 ACE 레버리지 ETF의 상장일은 언제인가요?")
from unsloth.chat_templates import get_chat_template, standardize_sharegpt

# 토크나이저에 채팅 템플릿 적용
tokenizer = get_chat_template(
    tokenizer,
    "gemma-3",  # Gemma 3 템플릿 사용
)

# ShareGPT 형식 표준화
standardized_dataset = standardize_sharegpt(sharegpt_dataset['train'])

# 대화 형식으로 포맷팅하는 함수
def formatting_prompts_func(examples):
    convos = examples["conversations"]
    texts = [tokenizer.apply_chat_template(convo, tokenize=False, add_generation_prompt=False) for convo in convos]
    return {"text": texts}

# 데이터셋 전처리
processed_dataset = standardized_dataset.map(formatting_prompts_func, batched=True)

# 첫 번째 데이터 확인
processed_dataset[0]
Gemma-3: 다중 턴 대화 포맷 필요

<bos><start_of_turn>user
Hello!<end_of_turn>
<start_of_turn>model
Hey there!<end_of_turn>

0개의 댓글