튜토리얼: 분류 파인튜닝

튜토리얼: 분류 파인튜닝 (Classification Fine-tuning)

DSPy 프로그램 내의 LM 가중치를 파인튜닝하는 quick example을 함께 살펴볼게요. 간단한 77-way 분류 작업에 적용해 볼 거예요.

파인튜닝된 프로그램은 GPU에 로컬로 호스팅되는 아주 작은 Llama-3.2-1B 언어 모델을 사용할 거예요. 더 흥미롭게 만들기 위해 (i) 학습 라벨이 전혀 없다고 가정하지만 (ii) 라벨이 없는 학습 예제 500개는 있다고 가정할게요.

출처: 문서

본문

의존성 설치 및 데이터 다운로드 (Install dependencies and download data)

최신 DSPy를 pip install -U dspy로 설치하고 따라와 보세요(원하면 uv pip도 가능). 이 튜토리얼은 DSPy >= 2.6.0에 의존해요. 또한 pip install datasets도 실행해야 합니다.

이 튜토리얼은 현재 추론을 위해 로컬 GPU가 필요하며, 파인튜닝된 모델을 위한 ollama 서빙도 지원할 계획입니다.

또한 다음 의존성도 필요해요:

  1. 추론: 로컬 추론 서버를 실행하기 위해 SGLang을 사용해요. 최신 버전은 여기 지침을 따라 설치할 수 있어요: https://docs.sglang.ai/start/install.html 아래는 2025/04/02 기준 가장 최신 설치 명령이지만, 설치 링크로 이동해 가장 최신 버전의 지침을 따르는 것을 권장해요. 이렇게 하면 파인튜닝 패키지와 sglang 패키지가 동기화됩니다.
    > pip install --upgrade pip
    > pip install uv
    > uv pip install "sglang[all]>=0.4.4.post3" --find-links https://flashinfer.ai/whl/cu124/torch2.5/flashinfer-python
    
  2. 파인튜닝: 다음 패키지를 사용해요. transformers 패키지 버전은 최근 이슈에 대한 임시 수정으로 지정한다는 점에 유의하세요: https://github.com/huggingface/trl/issues/2338
    > uv pip install -U torch transformers==4.48.3 accelerate trl peft
    

설치 속도를 높이기 위해 uv 패키지 관리자를 사용하는 것을 권장해요.

데이터셋 (Dataset)

이 튜토리얼에서는 Banking77 데이터셋을 사용할 거예요.

import dspy
import random
from dspy.datasets import DataLoader
from datasets import load_dataset

# Load the Banking77 dataset.
CLASSES = load_dataset("PolyAI/banking77", split="train", trust_remote_code=True).features['label'].names
kwargs = dict(fields=("text", "label"), input_keys=("text",), split="train", trust_remote_code=True)

# Load the first 2000 examples from the dataset, and assign a hint to each *training* example.
raw_data = [
    dspy.Example(x, label=CLASSES[x.label]).with_inputs("text")
    for x in DataLoader().from_huggingface(dataset_name="PolyAI/banking77", **kwargs)[:1000]
]

random.Random(0).shuffle(raw_data)

Banking77에서 (라벨이 없는) 쿼리 500개를 샘플링해 볼게요. 부트스트랩 파인튜닝에 이것을 사용할 거예요.

unlabeled_trainset = [dspy.Example(text=x.text).with_inputs("text") for x in raw_data[:500]]

unlabeled_trainset[0]
Example({'text': 'What if there is an error on the exchange rate?'}) (input_keys={'text'})

부트스트랩 파인튜닝 (Bootstrapped finetuning)

이를 수행하는 방법은 여러 가지가 있어요. 예를 들어 모델이 스스로 가르치게 하거나, 라벨 없이 높은 신뢰도의 경우를 식별하기 위해 추론 시점 계산(예: 앙상블)을 사용하는 것이죠.

아마 가장 간단한 방법은 이 작업에서 합리적으로 잘할 것으로 기대되는 모델을 추론·분류의 선생님으로 사용하고, 그것을 우리의 작은 모델로 증류(distill)하는 것입니다. 이 모든 패턴은 몇 줄의 코드로 표현할 수 있어요.

작은 Llama-3.2-1B-Instruct를 학생(student) LM으로 설정해 볼게요. GPT-4o-mini를 선생님(teacher) LM으로 사용할 거예요.

from dspy.clients.lm_local import LocalProvider

student_lm_name = "meta-llama/Llama-3.2-1B-Instruct"
student_lm = dspy.LM(model=f"openai/local:{student_lm_name}", provider=LocalProvider(), max_tokens=2000)
teacher_lm = dspy.LM('openai/gpt-4o-mini', max_tokens=3000)

학생 프로그램과 선생님 프로그램을 만들고 각각의 LM을 설정해 볼게요:

student_classify = classify.deepcopy()
student_classify.set_lm(student_lm)

teacher_classify = classify.deepcopy()
teacher_classify.set_lm(teacher_lm)

이제 부트스트랩 파인튜닝을 실행해 볼게요. "부트스트랩된"이라는 단어는 프로그램 자체가 학습 입력에 대해 호출되고, 모든 모듈에 걸쳐 보이는 결과 트레이스가 기록되어 파인튜닝에 사용된다는 뜻이에요. 이는 DSPy의 다양한 BootstrapFewShot 메서드의 가중치 최적화 변형입니다.

(라벨이 없는) 학습 세트의 모든 질문에 대해, 이는 선생님 프로그램을 호출해 추론을 생성하고 클래스를 선택하도록 합니다. 이는 추적되고 나서 학생 프로그램의 모든 모듈(이 경우 단 하나의 CoT 모듈)의 학습 세트를 구성합니다.

compile 메서드가 호출되면 BootstrapFinetune 옵티마이저는 전달된 선생님 프로그램(프로그램들 — 리스트를 전달할 수도 있어요!)을 사용해 학습 데이터셋을 만듭니다. 그런 다음 이 학습 데이터셋을 사용해 student 프로그램에 설정된 LM의 파인튜닝 버전을 만들고, 이를 훈련된 LM으로 교체합니다. 훈련된 LM은 새 LM 인스턴스가 된다는 점에 유의하세요(여기서 인스턴스화한 student_lm 객체는 건드리지 않습니다!)

참고: 라벨이 있으면 BootstrapFinetune 생성자에 metric을 전달할 수 있어요. 실제로 적용하려면 생성자에 train_kwargs를 전달해 로컬 LM 학습 설정(device, use_peft, num_train_epochs, per_device_train_batch_size, gradient_accumulation_steps, learning_rate, max_seq_length, packing, bf16, output_dir)을 제어할 수 있어요.

# Optional:
# [1] You can set `DSPY_FINETUNEDIR` environment variable to control where the directory that will be used to store the
#     checkpoints and fine-tuning data. If this is not set, `DSPY_CACHEDIR` is used by default.
# [2] You can set the `CUDA_VISIBLE_DEVICES` environment variable to control the GPU that will be used for fine-tuning
#     and inference. If this is not set and the default GPU that's used by HuggingFace's `transformers` library is
#     occupied, an OutOfMemoryError might be raised.
#
# import os
# os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# os.environ["DSPY_FINETUNEDIR"] = "/path/to/dir"
dspy.settings.experimental = True  # fine-tuning is an experimental feature, so we set a flag to enable it

optimizer = dspy.BootstrapFinetune(num_threads=16)  # if you *do* have labels, pass metric=your_metric here!
classify_ft = optimizer.compile(student_classify, teacher=teacher_classify, trainset=unlabeled_trainset)
classify_ft.get_lm().launch()

파인튜닝된 프로그램 검증 (Validating the finetuned program)

이제 성공했는지 알아볼게요. 시스템에 한 가지 질문하고 그 동작을 검사할 수 있어요.

classify_ft(text="I didn't receive my money earlier and it says the transaction is still in progress. Can you fix it?")
Prediction(
    reasoning='The user is inquiring about a specific issue, which they did not receive and is still showing as a pending transaction. This situation typically indicates a problem with the cash withdrawal process, as the user is not receiving the money they attempted to withdraw. The appropriate label for this scenario is "pending_cash_withdrawal," as it directly relates to the status of the cash withdrawal transaction.',
    label='pending_cash_withdrawal'
)

이 작은 dev 세트에 대한 평가자를 정의해 볼게요. 여기서 지표는 추론을 무시하고 라벨이 정확히 일치하는지 확인합니다.

metric = (lambda x, y, trace=None: x.label == y.label)
evaluate = dspy.Evaluate(devset=devset, metric=metric, display_progress=True, display_table=5, num_threads=16)
MLflow 실험에서 평가 결과 추적하기

시간 경과에 따른 평가 결과를 추적하고 시각화하려면 결과를 MLflow 실험에 기록할 수 있어요.

import mlflow

with mlflow.start_run(run_name="classification_ft_evaluation"):
    metric = (lambda x, y, trace=None: x.label == y.label)
    evaluate = dspy.Evaluate(devset=devset, metric=metric, display_progress=True, num_threads=16)

    # Evaluate the program as usual
    result = evaluate(classify_ft)

    # Log the aggregated score
    mlflow.log_metric("accuracy", result.score)
    # Log the detailed evaluation results as a table
    mlflow.log_table(result.results, artifact_file="eval_results.json")

통합에 대해 더 알아보려면 MLflow DSPy 문서도 방문하세요.

파인튜닝을 마친 후 로컬 추론 서버를 정리해 주세요:

classify_ft.get_lm().kill()

라벨을 사용한 파인튜닝 (Fine-tuning with labels)

라벨이 있다면 metric을 사용할 수 있어요. 이는 라벨이 있는 학습 예제가 있을 때 더 나은 결과를 이끌어낼 수 있어요:

optimizer = dspy.BootstrapFinetune(num_threads=16, metric=metric)
classify_ft = optimizer.compile(student_classify, teacher=teacher_classify, trainset=raw_data[:500])
classify_ft.get_lm().launch()
evaluate(classify_ft)

더 알아보기 (Learn more)