메트릭 만들기: 평가와 최적화의 잣대

메트릭 만들기: 평가와 최적화의 잣대

DSPy로 프로그램을 자동으로 개선하려면, 그 전에 "더 낫다"가 정확히 뭔지 알려줘야 해요. 이 잣대가 바로 메트릭(metric)이에요. 이번 글에서는 왜 옵티마이저가 메트릭을 필요로 하는지, 하이쿠 생성 프로그램을 위한 정량적 메트릭을 어떻게 설계하는지, 그리고 기준 점수(baseline)를 어떻게 찍어 두는지 살펴볼게요.

출처: 공식 문서 — Building metrics for evaluation and optimization

옵티마이저는 왜 메트릭이 필요할까

지금까지 우리는 DSPy 프로그램을 손으로 작성해 왔어요. 시그니처와 지시문은 우리가 타이핑한 만큼만 좋을 뿐이고, 학습할 예시도 없어요. 하이쿠의 품질은 모델이 장소와 하이쿠 작성에 대해 원래 갖고 있는 기반 지식에 달려 있죠.

*옵티마이저(optimizer)*가 바로 이 격차를 메꿔 줍니다. 옵티마이저는 프로그램을 자동으로 개선하는 알고리즘인데, DSPy는 여러 가지를 제공해요. 어떤 것들은 프롬프트에 데모로 넣을 작은 입출력 쌍(few-shot 예시)을 골라내고, 다른 것들은 시그니처의 자연어 지시문을 다시 써요. 더 나아가 기반 모델을 파인튜닝하는 것도 있죠. 공통점은 하나예요. 프로그램을 여러 번 실행하고, 가장 점수가 좋은 버전을 유지하는 루프를 돈다는 거예요.

"더 낫다"가 뭔지 판단하기 위해 옵티마이저는 메트릭을 필요로 해요. 메트릭은 단일 예측을 점수로 환산하는 파이썬 함수예요.

DSPy 밖에서는 평가 코드가 구조적 검사에 상당한 공을 들이곤 해요. 모델이 유효한 JSON을 반환했는지, 올바른 필드가 채워졌는지, 출력이 파싱되는지 같은 것들이요. 그런데 DSPy에서는 시그니처가 이미 구조를 처리해 주기 때문에, 우리 메트릭은 과제의 목표에 집중할 수 있어요. 하이쿠가 그 장소를 떠올리게 했는지, 답이 올바른 결론에 도달했는지, 에이전트가 요청한 대로 행동했는지 같은 것들이죠. 루브릭(rubric), 판정자(judge), 데이터셋 비교 모두 이 목표를 겨냥해요.

메트릭을 정의하는 방법은 여러 가지지만, 대부분 아래 패턴 중 하나 이상을 써요.

  • 라벨 데이터 비교(Labeled data comparisons): 예측을 보통 사람 전문가가 라벨링한 "정답(gold)" 출력과 비교해요. 예를 들어 다섯 개 하이쿠 배열마다 이상적인 선택지 하나가 짝지어진 데이터셋이 있다면, 앙상블 판정자를 평가·최적화할 수 있어요.
  • 규칙 기반 검사(Rule-based checks): 검증 가능한 속성을 확인하는 코드로 평가해요. 하이쿠 프로그램이라면 하이쿠의 줄 수와 음절 수를 세는 방식이 여기 해당돼요.
  • LLM 판정자(LLM judges): 보통은 더 큰 다른 모델로 평가해요. 소형 모델의 하이쿠가 장소와 계절을 얼마나 잘 떠올리게 하는지 프론티어 모델에게 묻는 식이에요. 이 방식은 앙상블의 판정자와 비슷한데, 차이는 옵티마이저가 그 판단을 소형 모델의 지시문에 한 번 녹여 넣는다는 점이에요. 매번 추론할 때마다 판정자를 호출하지 않는 대신 말이죠.

이 패턴들로 참신하고 영리한 메트릭을 만들 수도 있어요. 예를 들어 다양한 장소 입력에서 하이쿠 집합을 생성한 뒤, LLM 판정자에게 배열을 보여주고 특정 장소에 맞는 구절을 고르게 하는 방식으로 "그 장소를 떠올리게 하는 능력"을 측정할 수 있어요.

오늘은 다만 간단하게 갈게요. 하이쿠의 음절 수, 줄 수, 시제, 1인칭 사용, 그리고 입력 용어를 그대로 반영했는지를 확인하는 정량적 메트릭을 쓸 거예요.

하이쿠 데이터셋에서 Example 준비하기

우리는 장소, 계절, 무드 문자열을 무작위로 조합해 예시 입력 데이터셋을 만들었어요. 이 JSONL 파일을 다운로드하면 800행이 담겨 있어요.

평가나 최적화에 쓸 수 있게, 각 레코드를 dspy.Example 객체로 변환해 볼게요.

import json

examples = []
with open("haiku_examples.jsonl") as f:
    for line in f:
        row = json.loads(line)
        examples.append(
            dspy.Example(
                location=row["location"],
                season=row["season"],
                mood=row["mood"],
            ).with_inputs("location", "season", "mood")
        ) 

n = len(examples)
train_end = int(n * 0.75)
val_end = int(n * 0.875)
train, val, test = examples[:train_end], examples[train_end:val_end], examples[val_end:]

.with_inputsExample의 어떤 필드를 호출 시점에 프로그램으로 넘길지 알려주는 메서드예요. 이 데이터셋에서는 모든 필드가 입력이에요. 왜냐하면 우리 메트릭이 라벨링된 답변 대신 하이쿠 자체의 규칙으로 점수를 매기기 때문이죠. 질의응답 데이터셋이라면 더 전형적인 모양이 돼요. 각 행이 (question, answer)를 담고, .with_inputs("question")을 호출해 프로그램에는 질문만 넘기고 answer는 메트릭을 위해 보관해 두는 식입니다.

평가 메트릭 만들기

dspy.Evaluate용 메트릭 함수는 원본 example와 프로그램의 prediction을 받아 단일 float를 반환해요. 관례상 점수는 0.0과 1.0 사이이고, 높을수록 좋아요.

평가 메트릭은 아주 단순하게, 계절이나 무드 입력이 하이쿠에 그대로 사용됐는지만 확인해 볼게요.

def haiku_score(example, prediction) -> float:
    """
    Penalize verbatim use of the input season string.
    A haiku should evoke the season through imagery, not name it
    directly.
    """
    text = prediction.haiku.lower()
    if example.season.strip().lower() in text:
        return 0.0
    return 1.0

그다음 Evaluate를 호출해요.

evaluate = dspy.Evaluate(devset=val, metric=haiku_score)
baseline_score = evaluate(haiku_bot)
print(baseline_score)

우리의 dspy.ReAct 하이쿠 작성 프로그램은 gpt-5.4-nano로 구동되는데, 이 메트릭에서 고작 30%를 기록해요. 말이 되는 결과예요. 우리 규칙은 하이쿠의 절대적인 법칙이 아니기 때문에, 가중치만으로는 모델이 따르지 않을 테니까요. 게다가 시그니처에도 이게 목표라는 단서가 전혀 없었어요.

복합 점수, LLM 판정자, 테스트셋 위생에 대해서는 메트릭과 평가 심화를 참고하세요.

더 알아보기 (Learn more)