AI & ML/사용하기

서비스별 Fine-Tuning 방법(OpenAI, Google, Qwen-2.5-7B)

Ssul 2025. 1. 22. 15:59

Foundaion Model 학습은, 나와 같은 GPU거지 & 가난한 개발자(연구자)는 엄두를 낼수 없으니,

이론으로 잘 이해하고, 실전에서는 파인튜닝에 집중하자.

 

내가 종사하는 도메인에서도 AI를 적용하려 노력중이다. 그러면 방법은 아래와 같이 몇가지로 좁혀진다.

 

1. 빅테크모델 api에 prompt를 잘 먹여서 사용하기

2. 빅테크모델을 파인튜닝하여 사용하기

3. 적당한 크기의 오픈소스 모델을 full파인튜닝하거나, peft해서 사용하기

 

이 정도이며, 1번은 개발 기술적인 부분이니 생략하고,

2,3번을 공략해보도록 하겠다.

 

2번의 단점은 매번 사용할때마다 api비용이 나간다는 것이고, 당연히 학습할때도 비용이 나간다. 그리고, 내가 사용했던 base모델이 사라질지도 모른다는 리스크가 있다. 간단한 예로, 이번에 4o-mini모델을 기반으로 파인튜닝하였는데, 먼 훗날 ai가 더 발전해서, openai에서 더이상 4o-mini모델에 GPU를 할당할 필요가 없다는 정책을 정한다면, 나의 파인튜닝 모델은 사라지는 구조(?)

 

3번의 단점은 적당한 크기라고 해도... 작은 크기가 아니라는거(뭔 소리야...)

아래가 각 모델의 크기에 따라, 양자화, full이냔 Peft냐에 따른 필요한 vram이다

7B만 풀파인튜닝해도 A100이 4장은 필요하다 ;;;;

그래서 막상 3번을 한다고 해도, 규모나 투자금이 일정규모 이상 되지 않은 스타트업들은 힘든 것이 현실이다.

결국 opensource LLM 튜닝시 클라우스에서 GPU를 열심히 빌려써야 할 것이다.

 

서론이 길었고, 본격적으로 파인튜닝을 해보자.

내가 풀어내는 과업은 6개 응답 중 1개를 선택하는 어찌보면 ML의 classification문제인데, 단순한 다지선다의 느낌과는 다른 분명 인간의 지식(LLM의 능력)이 들어가서 판단하고 선택하는 문제.

R&D과제이기 때문에 보안상 유사한 문제로 대체해보면, 핸드폰에 오는 문자를 "일반문자", "인증문자", "정치문자", "스팸문자", "프로모션문자" 등으로 분류하는 과업이다.

우리가 잘하는 instruction tuning으로 풀어볼 예정이다.

 

 

1. OPEN AI 파인튜닝(생각보다 쉽다)

- openai.com 을 회원가입합니다(chatgpt와는 다른 것)

- 로그인 후 api login으로 입장 > dashboard 클릭

 

- 왼쪽편에 fine-tuning을 클릭

 

- create버튼을 클릭합니다. 파인튜닝 방법이 supervised와 DPO가 있는데, DPO는 데이터셋 구성부터 약간 다르니, Supervised선택

그리고 파인튜닝할 base모델을 선택합니다. "gpt-4o-mini"가 저렴하니, mini를 추천드립니다.

 

- 학습에 사용될 jsonl파일을 업로드 해야 하는데, 해당 파일포맷은 아래와 같습니다. 파일명은 train.jsonl로 하시면 됩니다.

{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]}

보시는것처럼, system/user/assistant로 구성되며

문자를 구분하는 태스크의 경우, 

 

system메세지: 너는 문자를 분류하는 뛰어난 문자분류기야. 다음 문자를 보고, "일반문자", "인증문자", "정치문자", "스팸문자", "프로모션문자" 중 1개로 분류해줘

user메세지: [Web발신]\n★주식투자로 부자되기 프로젝트★\n저희는 주식정보제공 전문업체입니다.~~~~

assistant메세지: 스팸문자

{"messages": [{"role": "system", "content": "너는 문자를 분류하는 뛰어난 문자분류기야. 다음 문자를 보고, "일반문자", "인증문자", "정치문자", "스팸문자", "프로모션문자" 중 1개로 분류해줘"}, {"role": "user", "content": "[Web발신]\n★주식투자로 부자되기 프로젝트★\n저희는 주식정보제공 전문업체입니다.~~~~"}, {"role": "assistant", "content": "스팸문자"}]}

위와 같이 구성됩니다.

 

- jsonl파일이 완성되었으면 upload합니다.

- 나머지는 추천해주는대로 하면 됩니다. 이후 조정을 해보셔도 됨

- create버튼 클릭 > 데이터 검증 > 파인튜닝을 시작

아름답게(?) 학습되고 있는 장면(맨날 이랬으면 ㅠㅠ)

 

2. gemini 파인튜닝

google의 gemini도 openai처럼 웹ui로 학습이 가능하다. 근데 내가 잘 못하는 것인지 모르겠는데, 데이터셋 업로드부터 쉽지 않아서, 코드 베이스로 학습을 하였다. 코드와 주석으로 설명.

import os
from dotenv import load_dotenv
import google.generativeai as genai
import pandas as pd

# 환경 변수 로드
load_dotenv()

# Gemini API 키 설정
api_key = os.getenv("GEMINI_API_KEY")
genai.configure(api_key=api_key)

# 현재 사용 가능한 튜닝된 모델 리스트 출력
for i, m in zip(range(5), genai.list_tuned_models()):
    print(m.name)


# 기본 모델 선택
# 파인튜닝이 가능하고 'flash'가 이름에 포함된 모델 중 첫 번째 모델을 선택
# createTunedModel: 파인튜닝 가능 여부 체크
# flash: 빠른 학습이 가능한 모델 체크
base_model = [
    m
    for m in genai.list_models()
    if "createTunedModel" in m.supported_generation_methods and "flash" in m.name
][0]
print("Selected base model:", base_model)


# CSV 파일에서 훈련 데이터를 로드하는 함수
def load_training_data(csv_path):
    """
    CSV 파일을 읽어서 Gemini 파인튜닝에 필요한 형식으로 변환

    Args:
        csv_path (str): CSV 파일 경로

    Returns:
        list: 딕셔너리 리스트 형태의 훈련 데이터
        [{"text_input": "입력텍스트", "output": "출력텍스트"}, ...]
    """
    df = pd.read_csv(csv_path)
    return [
        {"text_input": row["INPUT"], "output": row["OUTPUT"]}
        for _, row in df.iterrows()
    ]


# 훈련 데이터 로드
train_data = load_training_data("../data/google_training_data.csv")

# 데이터 형식 확인을 위한 출력
print("\n데이터 샘플:")
print(train_data[0])
print(f"\n총 데이터 수: {len(train_data)}")

# 모델 파인튜닝 설정 및 시작
name = f"generate-brainai-v2"  # 튜닝된 모델의 이름
operation = genai.create_tuned_model(
    source_model=base_model.name,  # 기본 모델
    training_data=train_data,  # 훈련 데이터
    id=name,  # 모델 ID
    epoch_count=5,  # 학습 에포크 수
    batch_size=4,  # 배치 크기
    learning_rate=0.001,  # 학습률
)

# 파인튜닝 작업의 메타데이터 출력
print("Tuning task metadata:", operation.metadata)

# 파인튜닝 진행 상황 모니터링
import time

for status in operation.wait_bar():
    time.sleep(30)  # 30초마다 상태 체크

# 학습 결과 시각화를 위한 seaborn 임포트
import seaborn as sns

# 파인튜닝된 모델 가져오기
model = operation.result()

# 학습 과정의 스냅샷을 DataFrame으로 변환
snapshots = pd.DataFrame(model.tuning_task.snapshots)

# 에포크별 손실 그래프 그리기
sns.lineplot(data=snapshots, x="epoch", y="mean_loss")

https://aistudio.google.com/ 회원가입하고, api키 생성 후, 자신만의 데이터셋을 만들어 위 코드를 돌리면 된다

- epoch/batch_size/lr은 구글 문서에서 추천하는 값을 넣었다. 학습시켜보며 조정하면 되겠다.

 

 

3. Qwen-2.5-7B 파인튜닝

- 우선 성능좋다는 다른 모델을 사용해 보았지만, 한글이 영 엉망이었다. 그러다 한글을 잘한다는 소문을 듣고 선택한 Qwen

- 확실히 다른 오픈소스 모델보다 한글을 잘하는 것 같다.

파인튜닝 코드로 들어가자~!

 

[config.py]

- 파인튜닝에 사용할 base모델을 지정하고,

- 주요 하이퍼파라미터를 지정합니다.

- instruction tuning에 사용할 템플릿을 설정합니다

import torch

# Model configuration
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"

# Training configuration
CUTOFF_LEN = 4098
TRAIN_ON_INPUTS = False
ADD_EOS_TOKEN = False
VAL_SIZE = 0.005

# LoRA configuration
LORA_CONFIG = {
    "r": 16,
    "lora_alpha": 16,
    # "target_modules": ["q_proj", "k_proj", "v_proj"],
    "target_modules": ["q_proj", "k_proj", "v_proj", "o_proj"],
    "lora_dropout": 0.05,
    "bias": "none",
    "task_type": "CAUSAL_LM",
}

# Training arguments
TRAINING_ARGS = {
    "output_dir": "./Qwen_singleGPU-v1",
    "num_epochs": 1,
    "micro_batch_size": 1,
    "gradient_accumulation_steps": 8,
    "warmup_steps": 100,
    "learning_rate": 5e-8,
    "group_by_length": False,
    "optimizer": "paged_adamw_8bit",
    "beta1": 0.9,
    "beta2": 0.95,
    "lr_scheduler": "cosine",
    "logging_steps": 1,
    "use_wandb": True,
    "wandb_run_name": "Project_v1",
    "use_fp16": False,
    "use_bf_16": True,
    "evaluation_strategy": "steps",
    "eval_steps": 50,
    "save_steps": 50,
    "save_strategy": "steps",
}


INSTRUCT_TEMPLATE = {
    "prompt_input": "여러분의 과업",
    "prompt_no_input": "여러분의 과업",
    "response_split": "여러분의 과업",
}

 

 

[data.py]

- 입력된 텍스트를 base모델의 사전을 활용하여 tokenizing합니다.

- 또한 autoregressive에 맞게, Input_ids, attention_mask, labels를 구성하여 데이터셋을 만드는 함수입니다.

- train 8, test 2의 비율로 데이터셋을 구성합니다.

import pandas as pd
from datasets import Dataset
from typing import Union
from sklearn.model_selection import train_test_split
from transformers import DataCollatorForSeq2Seq
from config import INSTRUCT_TEMPLATE, CUTOFF_LEN, TRAIN_ON_INPUTS, ADD_EOS_TOKEN


class Prompter:
    def __init__(self, verbose: bool = False):
        self.template = INSTRUCT_TEMPLATE

    def generate_prompt(
        self,
        instruction: str,
        input: Union[None, str] = None,
        label: Union[None, str] = None,
        verbose: bool = False,
    ) -> str:
        if input:
            res = self.template["prompt_input"].format(
                instruction=instruction, input=input
            )
        else:
            res = self.template["prompt_no_input"].format(instruction=instruction)

        if label:
            res = f"{res}{label}"

        if verbose:
            print(res)

        return res

    def get_response(self, output: str) -> str:
        return output.split(self.template["response_split"])[1].strip()


def load_dataset(file_path: str):
    dataset = pd.read_excel(file_path)
    dataset = Dataset.from_pandas(dataset)

    train_test_split_data = dataset.train_test_split(
        test_size=0.2, shuffle=True, seed=42
    )
    return train_test_split_data["train"], train_test_split_data["test"]


def tokenize(prompt, tokenizer, add_eos_token=True):
    result = tokenizer(
        prompt,
        truncation=True,
        max_length=CUTOFF_LEN,
        padding=False,
        return_tensors=None,
    )

    if (
        result["input_ids"][-1] != tokenizer.eos_token_id
        and len(result["input_ids"]) < CUTOFF_LEN
        and add_eos_token
    ):
        result["input_ids"].append(tokenizer.eos_token_id)
        result["attention_mask"].append(1)

    result["labels"] = result["input_ids"].copy()

    return result


def generate_and_tokenize_prompt(data_point, prompter, tokenizer):
    full_prompt = prompter.generate_prompt(
        data_point["비밀"], label=data_point["비밀2"]
    )
    tokenized_full_prompt = tokenize(full_prompt, tokenizer)

    if not TRAIN_ON_INPUTS:
        user_prompt = prompter.generate_prompt(data_point["비밀"])
        tokenized_user_prompt = tokenize(
            user_prompt, tokenizer, add_eos_token=ADD_EOS_TOKEN
        )
        user_prompt_len = len(tokenized_user_prompt["input_ids"])

        if ADD_EOS_TOKEN:
            user_prompt_len -= 1

        tokenized_full_prompt["labels"] = [
            -100
        ] * user_prompt_len + tokenized_full_prompt["labels"][user_prompt_len:]

    return tokenized_full_prompt

 

 

[model.py]

- 모델을 양자화 하여 가져옵니다.

- Lora도 붙입니다 (Qlora)

- Lora를 붙여서 기존 7B의 파라미터는 고정하고, 약 0.1%에 해당하는 Lora의 파라미터만 수정가능하게 됩니다.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import get_peft_model, prepare_model_for_kbit_training, LoraConfig
from config import MODEL_ID, LORA_CONFIG
import logging


def setup_tokenizer():
    """Initialize and configure the tokenizer"""
    logging.info(f"Loading tokenizer from {MODEL_ID}")
    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
    tokenizer.pad_token_id = tokenizer.eos_token_id
    tokenizer.padding_side = "right"
    return tokenizer


def setup_model():
    """Initialize and configure the model with LoRA"""
    logging.info("Configuring model quantization...")
    quantization_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_use_double_quant=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_quant_storage=torch.bfloat16,
    )

    logging.info(f"Loading base model from {MODEL_ID}")
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        quantization_config=quantization_config,
        torch_dtype=torch.bfloat16,
        device_map={"": 0},
    )

    logging.info("Preparing model for k-bit training...")
    model = prepare_model_for_kbit_training(model)

    logging.info("Applying LoRA configuration...")
    lora_config = LoraConfig(**LORA_CONFIG)
    model = get_peft_model(model, lora_config)

    trainable_params = model.print_trainable_parameters()
    logging.info(f"Model setup complete. Trainable parameters: {trainable_params}")

    return model

 

 

[trainer.py]

- trainercallback함수를 만들어서, 학습중에 모델의 예측을 볼수 있게 하였습니다

(원래는 그냥 했는데... 다 학습을 한 이후에 저의 실수를 확인하는 경우가 많아서... 무조건 중간에 모니터링 추천)

- config.py에서 설정한 하이퍼파라미터를 trainer에 넣어줍니다.

from transformers import (
    Trainer,
    TrainingArguments,
    DataCollatorForSeq2Seq,
    TrainerCallback,
)
from config import TRAINING_ARGS
import logging
import torch


class TrainingMonitorCallback(TrainerCallback):
    """Callback to monitor training progress and sample predictions"""

    def __init__(self, tokenizer, prompter, model, train_dataset):
        self.tokenizer = tokenizer
        self.prompter = prompter
        self.model = model  # 직접 모델 저장
        self.train_dataset = train_dataset  # 직접 데이터셋 저장

    def on_step_end(self, args, state, control, **kwargs):
        """Log sample predictions during training"""
        # 처음 100스텝은 10스텝마다, 이후 100스텝마다 로깅
        if state.global_step <= 100:
            if state.global_step % 10 != 0:
                return
        elif state.global_step % 100 != 0:
            return

        try:
            # Get a sample (using step number to rotate through dataset)
            sample_idx = state.global_step % len(self.train_dataset)
            sample = self.train_dataset[sample_idx]

            # Original input and label
            input_text = sample["비밀"]
            true_label = sample["비밀2"]

            # Generate prompt and get model inputs
            prompt = self.prompter.generate_prompt(input_text)
            model_inputs = self.tokenizer(
                prompt, return_tensors="pt", truncation=True, max_length=512
            ).to(self.model.device)

            # Get model prediction
            self.model.eval()
            with torch.no_grad():
                outputs = self.model.generate(
                    input_ids=model_inputs["input_ids"],
                    attention_mask=model_inputs["attention_mask"],
                    max_new_tokens=3,
                    eos_token_id=3,
                    pad_token_id=self.tokenizer.pad_token_id,
                )
            self.model.train()

            # Decode prediction
            generated_text = outputs[0][model_inputs["input_ids"].size(1) :]
            pred_text = self.tokenizer.decode(generated_text, skip_special_tokens=True)

            # Log the results
            logging.info("\n" + "=" * 50)
            logging.info(f"Training Progress - Step {state.global_step}")
            logging.info(
                f"Loss: {state.log_history[-1].get('loss', 'N/A') if state.log_history else 'N/A'}"
            )
            logging.info(f"Input text: {input_text[:100]}...")  # First 100 chars
            logging.info(f"True label: {true_label}")
            logging.info(f"Model prediction: {pred_text}")
            logging.info("=" * 50)

        except Exception as e:
            logging.warning(
                f"Error in monitoring callback at step {state.global_step}: {str(e)}"
            )


def setup_trainer(model, tokenizer, train_data, val_data):
    training_args = TrainingArguments(
        per_device_train_batch_size=TRAINING_ARGS["micro_batch_size"],
        per_device_eval_batch_size=TRAINING_ARGS["micro_batch_size"],
        gradient_accumulation_steps=TRAINING_ARGS["gradient_accumulation_steps"],
        warmup_steps=TRAINING_ARGS["warmup_steps"],
        num_train_epochs=TRAINING_ARGS["num_epochs"],
        learning_rate=TRAINING_ARGS["learning_rate"],
        adam_beta1=TRAINING_ARGS["beta1"],
        adam_beta2=TRAINING_ARGS["beta2"],
        fp16=TRAINING_ARGS["use_fp16"],
        bf16=TRAINING_ARGS["use_bf_16"],
        logging_steps=TRAINING_ARGS["logging_steps"],
        optim=TRAINING_ARGS["optimizer"],
        evaluation_strategy=TRAINING_ARGS["evaluation_strategy"],
        save_strategy=TRAINING_ARGS["save_strategy"],
        eval_steps=TRAINING_ARGS["eval_steps"],
        save_steps=TRAINING_ARGS["save_steps"],
        output_dir=TRAINING_ARGS["output_dir"],
        load_best_model_at_end=True,
        group_by_length=TRAINING_ARGS["group_by_length"],
        report_to="wandb" if TRAINING_ARGS["use_wandb"] else None,
        run_name=(
            TRAINING_ARGS["wandb_run_name"] if TRAINING_ARGS["use_wandb"] else None
        ),
    )

    # Create Prompter instance for the callback
    from data import Prompter

    prompter = Prompter()

    # Initialize the callback with model and dataset
    monitor_callback = TrainingMonitorCallback(
        tokenizer=tokenizer, prompter=prompter, model=model, train_dataset=train_data
    )

    trainer = Trainer(
        model=model,
        train_dataset=train_data,
        eval_dataset=val_data,
        args=training_args,
        data_collator=DataCollatorForSeq2Seq(
            tokenizer, pad_to_multiple_of=8, return_tensors="pt", padding=True
        ),
        callbacks=[monitor_callback],
    )

    return trainer

 

 

[main.py]

- 데이터셋을 구성하고

- 학습을 합니다.

- 학습을 마치면 추론을 확인해봅니다.

from huggingface_hub import login
from data import Prompter, load_dataset, generate_and_tokenize_prompt
from model import setup_tokenizer, setup_model
from trainer import setup_trainer
from inference import load_trained_model, run_inference
from config import HF_TOKEN, MODEL_ID
import logging
import torch
from transformers import AutoModelForCausalLM

# Configure logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)


def test_model_generation(model, tokenizer, text):
    """Test model's generation with a sample input"""
    inputs = tokenizer(text, return_tensors="pt").to("cuda")

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=100,
            pad_token_id=tokenizer.pad_token_id,
            eos_token_id=tokenizer.eos_token_id,
        )

    return tokenizer.decode(outputs[0], skip_special_tokens=True)


def main():
    """
    Main function to run the training and inference pipeline.
    """
    logging.info("Starting the training pipeline...")

    # Login to Hugging Face
    logging.info("Logging into Hugging Face...")
    login(HF_TOKEN)

    # Setup tokenizer and prompter
    logging.info("Setting up tokenizer and prompter...")
    tokenizer = setup_tokenizer()
    prompter = Prompter()

    # Load and process dataset
    logging.info("Loading and processing dataset...")
    train_data, val_data = load_dataset("./data/preprocessed_data.xlsx")
    logging.info(
        f"Loaded {len(train_data)} training samples and {len(val_data)} validation samples"
    )

    # Tokenize datasets
    logging.info("Tokenizing datasets...")
    train_data = train_data.map(
        lambda x: generate_and_tokenize_prompt(x, prompter, tokenizer)
    )
    val_data = val_data.map(
        lambda x: generate_and_tokenize_prompt(x, prompter, tokenizer)
    )

    # Setup model
    logging.info("Setting up model...")

    # Test original model before applying LoRA
    logging.info("\n" + "=" * 50)
    logging.info("Testing original model before applying LoRA...")
    test_input = "[Web발신] 부자가 되고 싶으세요...."
    test_prompt = prompter.generate_prompt(test_input)

    base_model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        torch_dtype=torch.bfloat16,
        device_map={"": 0},
    )
    base_output = test_model_generation(base_model, tokenizer, test_prompt)
    logging.info(f"Original Model Input:\n{test_prompt}")
    logging.info(f"Original Model Output:\n{base_output}")
    logging.info("=" * 50 + "\n")

    # Free up memory
    del base_model
    torch.cuda.empty_cache()

    # Setup model with LoRA
    model = setup_model()
    model.config.use_cache = False

    # Setup trainer and train
    logging.info("Starting training...")
    trainer = setup_trainer(model, tokenizer, train_data, val_data)
    trainer.train()
    logging.info("Training completed!")

    # Save model to Hub
    logging.info("Saving model to Hugging Face Hub...")
    model_name = "HackerCIS/Pong_BrainAI_Qwen2.5-72B-Instruct_v1"
    model.push_to_hub(model_name, use_auth_token=True)
    logging.info(f"Model saved as {model_name}")

    # Load trained model and run inference
    logging.info("Running inference on validation data...")
    trained_model = load_trained_model(model_name, tokenizer)
    run_inference(trained_model, tokenizer, prompter, val_data)
    logging.info("Pipeline completed successfully!")


if __name__ == "__main__":
    main()


# pip install torch transformers peft bitsandbytes accelerate pandas datasets openpyxl huggingface-hub wandb scikit-learn tqdm

 

[Inference.py]

- val_data를 활용하여, 학습이 끝난 모델의 예측과 Label을 비교하여 성능을 체크합니다

import torch
from peft import PeftModel, PeftConfig
from transformers import AutoModelForCausalLM
import logging
import json
from tqdm import tqdm


def load_trained_model(peft_model_id, tokenizer):
    """Load the trained model for inference"""
    logging.info(f"Loading trained model from {peft_model_id}")
    config = PeftConfig.from_pretrained(peft_model_id)
    model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path)
    model.resize_token_embeddings(len(tokenizer))
    model = PeftModel.from_pretrained(model, peft_model_id)
    model.to("cuda:0")
    return model


def run_inference(model, tokenizer, prompter, val_data):
    """Run inference on validation data and save results"""
    model.eval()
    device = "cuda:0"
    total_samples = len(val_data)
    results = []
    correct_predictions = 0

    logging.info(f"Starting inference on {total_samples} samples...")

    for idx in tqdm(range(total_samples), desc="Processing validation data"):
        sample = val_data[idx]
        input_text = sample["비밀"]
        true_label = sample["비밀2"]

        # Generate prompt and get prediction
        prompt = prompter.generate_prompt(input_text)
        inputs = tokenizer(
            prompt, return_tensors="pt", truncation=True, max_length=512
        ).to(device)

        with torch.no_grad():
            outputs = model.generate(
                input_ids=inputs["input_ids"],
                attention_mask=inputs["attention_mask"],
                max_new_tokens=3,
                eos_token_id=3,
            )

        generated_text = outputs[:, inputs["input_ids"].size(1) :]
        prediction = tokenizer.decode(
            generated_text[0], skip_special_tokens=True
        ).strip()

        # Check if prediction is correct
        is_correct = prediction == true_label
        if is_correct:
            correct_predictions += 1

        # Store result
        results.append(
            {
                "input": input_text,
                "true_label": true_label,
                "prediction": prediction,
                "is_correct": is_correct,
            }
        )

        # Log progress
        if idx % 10 == 0:
            logging.info(f"\nSample {idx}:")
            logging.info(f"Input: {input_text}")
            logging.info(f"True: {true_label}")
            logging.info(f"Pred: {prediction}")
            logging.info(f"Correct: {is_correct}")

    # Calculate accuracy
    accuracy = correct_predictions / total_samples

    # Prepare final results
    evaluation_results = {
        "accuracy": accuracy,
        "total_samples": total_samples,
        "correct_predictions": correct_predictions,
        "detailed_results": results,
    }

    # Save results to file
    output_file = "qwen_evaluation_results.json"
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(evaluation_results, f, ensure_ascii=False, indent=2)

    logging.info(f"\nEvaluation Summary:")
    logging.info(f"Total samples: {total_samples}")
    logging.info(f"Correct predictions: {correct_predictions}")
    logging.info(f"Accuracy: {accuracy:.2%}")
    logging.info(f"Detailed results saved to {output_file}")

    return evaluation_results

 

4. 파인튜닝에 대한 회고

- 자세한 그래프를 공유하지는 못하지만, 몇가지 시사점을 확인할 수 있었다.

- loss그래프를 보면, 초반부에 확 줄어들다가, 중반부터 진동하면서 수렴하지 않는 상황을 확인할 수 있었다.

- 이것의 이유는 데이터셋인것 같다. 사람인 내가 판단할때도, 5가지 선택지 중 두가지 모두 정답이 되는 것 같았다. 당연히 학습한 AI모델 역시 비슷한 판단을 반복하였고, 그 결과는 당연히 loss가 진동일수 밖에 없었다. 그래서 데이터셋의 labeling을 명확화 하고, 라벨링 분류체계를 명확화 하는 방향으로 보완하였다.

- 빅테크 모델과 opensource의 성능비교

- 우선 결과적으로 봤을때(gemini는 지금 돌리고 있다) val_data기준으로

파인튜닝모델의 정답률은 openai는 50%, Qwen은 25%

모델의 사이즈 차이가 존재하겠지만, gpt-4o-mini가 확실히 똑똑한게 느껴졌다. 물론 Qwen이 7B밖에 안되는 이유도 있는 것 같다.

- 학습이 가능함은 확인하였기 때문에,

#1. 데이터를 보완하고

#2. 모델사이즈를 변경해보고

#3. 하이퍼파라미터를 조정해서

80%로 성능을 높여볼 예정이다.

 

80%가 넘어가면, 사례와 노하우를 가지고 다시 돌아오겠다.