ETF 데이터로 LLM 파인튜닝하기 (Unsloth 활용)
데이터 품질: 더 많은 ETF 데이터와 다양한 질문-답변 쌍을 추가하면 모델 성능이 향상됩니다.
하이퍼파라미터 튜닝: LoRA 랭크(r), 학습률, 배치 크기 등을 조정하여 최적의 성능을 찾으세요.
평가 지표: 파인튜닝 후 모델 성능을 ETF 관련 질문에 대한 정확도로 평가하세요.
메모리 관리: 가용 VRAM에 맞게 max_seq_length, 배치 크기, LoRA 매개변수를 조정하세요.
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 데이터를 연속적인 대화 흐름으로 변환함
이 방식은 모델의 맥락 이해 능력을 강화함
대화형 학습은 자연스러운 응답 생성에 효과적임
사용자-모델 간 상호작용 패턴을 학습할 수 있음
# 허깅페이스 데이터셋 다운로드
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>