메트릭과 평가 (Metrics and evaluation)
메트릭과 평가 (Metrics and evaluation)
이 문서가 설명하는 것
DSPy 메트릭은 예측을 옵티마이저가 쫓을 수 있는 숫자로 바꾸는 함수이고, dspy.Evaluate는 그 메트릭을 데이터셋 전체에 병렬로 돌리는 하네스입니다. 이 페이지는 옵티마이저가 기대하는 메트릭 계약, DSPy가 내장하는 문자열·LLM 판사 메트릭, 그리고 Evaluate가 채점 실행을 어떻게 조율하는지 다룹니다.
자신만의 메트릭을 쓰거나, 규칙 기반 메트릭을 LLM-as-judge로 갈아끼우거나, Evaluate의 노브를 고르거나, 옵티마이저가 왜 우리 메트릭을 그런 식으로 읽는지 문제를 해결하고 싶을 때 읽어 보세요.
설계 결정
1. 메트릭은 (gold, pred, ...)를 받아 점수를 돌려주는 어떤 callable이든 된다
계약은 **덕 타이핑(duck-typed)**입니다. 서브클래스할 기본 클래스도, 메트릭을 등록할 데코레이터도, @metric 마커도 없어요. Evaluate는 넘기는 무엇이든 호출하고 옵티마이저도 마찬가지입니다. 장점: NLP나 QA 라이브러리에서 가져온 기존 채점 함수를 한 줄로 DSPy 메트릭으로 감쌀 수 있어요. 대가: 계약이 강제되지 않아서, 잘못된 타입을 돌려주는 callable은 등록 시점이 아니라 평가 시점에 실패합니다.
2. 옵티마이저 지향 전체 시그니처는 (gold, pred, trace=None, pred_name=None, pred_trace=None)이다
대부분의 메트릭은 (gold, pred)만 선언하고, Python의 기본 인자 규칙 덕에 옵티마이저는 나머지를 무해하게 넘길 수 있어요. 옵티마이저는 프로그램이 답에 어떻게 도달했는지 메트릭이 보길 원할 때 trace를 채우고, 예측기별 채점을 원할 때 pred_name/pred_trace를 채웁니다(GEPA는 둘 다 해요). 오늘 (gold, pred)에 맞춰 쓴 메트릭은 미래의 옵티마이저가 더 원해도 계속 동작합니다 — 그래서 추가 인자들이 꼬리 위치 기본값인 거예요.
3. 반환 타입은 bool, float, 또는 dspy.Prediction(score, feedback)일 수 있고, 각각 다르게 처리된다
Evaluate는 불리언을 백분율로, float를 평균으로 집계합니다. Prediction 반환은 풀어집니다. score는 float처럼 평균화되고, feedback은 GEPA만 읽어요. 세 가지 모양의 반환이 중요한 이유는, 가장 단순한 메트릭은 Prediction으로 출력을 의례적으로 감쌀 필요가 없어야 하지만, 더 풍부한 신호를 필요로 하는 옵티마이저도 잠겨서는 안 되기 때문입니다.
4. feedback 채널은 Evaluate의 점수가 아니라 GEPA용이다
메트릭은 모든 점수에 자연어 설명을 붙일 수 있고, GEPA의 반성적(reflective) 루프만 그것을 읽습니다. Evaluate는 무시해요. 이 분리는 두 관심사를 깔끔하게 유지합니다. "이 예측이 얼마나 좋았나"는 하나의 신호이고, "유용한 비평은 무엇인가"는 다른 신호이며, 듣는 옵티마이저가 없을 때 생략해도 아무 비용이 들지 않아요.
5. trace는 메트릭이 옵티마이저 안에서 한 방식, 평가 시점엔 다른 방식으로 동작하게 하는 레버다
SemanticF1과 CompleteAndGrounded 판사는 trace is None(평가)일 때 원시 F1을 돌려주고, trace is not None(최적화)일 때 이진화된 score >= threshold를 돌려줍니다. 패턴: 평가 시점에는 연속 점수를 원하고, 최적화 시점에는 탐색이 쫓을 깔끔한 통과/실패 신호를 원합니다. trace 인자는 두 개의 별도 함수를 강요하지 않고 메트릭이 어느 모드인지 알려줘요.
6. 내장 문자열 메트릭은 normalize_text에 모인다
EM, F1, HotPotF1, 그리고 메트릭 모양 래퍼들은 예측과 참조 양쪽에 normalize_text를 호출합니다. NFD 유니코드, 소문자, 영어 관사(a / an / the) 제거, 구두점 제거, 공백 축소를 해요. 단일 표준 정규화 덕분에 토큰 레벨 F1과 exact match가 동일한 입력에서 동작합니다 — answer_exact_match가 frac 인자에 따라 둘 사이를 전환하기 때문에 중요해요.
7. LLM 판사는 평범한 함수가 아니라 dspy.Module 서브클래스다
SemanticF1과 CompleteAndGrounded는 dspy.Module을 상속해서, 다른 DSPy 프로그램처럼 검사·트레이싱·최적화될 수 있습니다. dspy.Predict를 둘러싼 자유 함수로 판사를 쓸 수도 있지만, Module이 공짜로 주는 트레이스와 history를 잃게 돼요. 패턴: 출력에 score: float(그리고 선택적으로 feedback: str)를 포함한 dspy.Signature를 쓰고, forward가 Prediction(score, feedback)을 돌려주는 Module로 감쌉니다.
8. Evaluate는 자체 스레드 풀이 아니라 ParallelExecutor로 병렬화한다
dspy.Parallel과 Module.batch가 쓰는 것과 같은 실행기예요. 장점: 설정 전파, 에러 카운팅, 지연 탐지, 진행 보고가 하나의 구현입니다. Evaluate 호출을 둘러싼 dspy.context(lm=...) 블록은 모든 워커 안에서 존중되는데, ParallelExecutor가 thread_local_overrides를 스냅샷하고 다시 적용하기 때문입니다.
9. 메트릭 실패는 예외가 아니라 failure_score를 얻는다
메트릭이 (raise하거나 None을 돌려주면) Evaluate는 그 예시에 failure_score(기본 0.0)를 넣고 계속 갑니다. 이유: 긴 평가에서 나쁜 예시 하나로 나머지 999개의 점수를 잃어선 안 되기 때문이에요. max_errors 노브는 하네스가 멈추기 전에 견디는 실패 횟수를 제한합니다.
10. EvaluationResult가 단일 반환 모양이다
.score는 집계(불리언이면 True의 백분율, float와 Prediction.score이면 평균)이고, .results는 (example, prediction, score) 삼중항 목록입니다. 레거시 return_outputs / return_all_scores 생성자 kwargs는 제거됐고, 넘기면 마이그레이션 메시지와 함께 ValueError를 던져요. 단일 반환 모양이라 다운스트림 코드가 호출자가 어떤 키워드를 썼는지 스위칭할 필요가 없습니다.
API 둘러보기
하려는 일 기준으로 묶었어요.
메트릭 해부
DSPy가 우리 메트릭을 어떻게 호출하고 무엇을 기대하는지.
호출 시그니처 — metric(gold, pred, trace=None, pred_name=None, pred_trace=None)
gold는 라벨이 붙은 dspy.Example, pred는 프로그램이 만든 dspy.Prediction입니다. trace는 전체 실행을 담는 (predictor, inputs, outputs) 삼중항 목록으로, 옵티마이저가 프로그램이 어떻게 도달했는지 메트릭이 보길 원할 때 채웁니다. pred_name과 pred_trace는 같은 뷰를 예측기 하나로 좁히는데, GEPA가 다단계 프로그램의 한 단계를 채점할 때 이것들을 써요.
dspy.Prediction(score, feedback) — 점수-동반-피드백 반환 모양
최소한 score 필드가 있고 선택적으로 feedback 필드가 있는 Prediction. Evaluate는 score만 읽고 GEPA는 둘 다 읽습니다. 평범한 메트릭에서 Prediction(score=...)을 돌려줘도 잘 동작해요 — feedback은 선택이고, 점수만 원할 때 래퍼는 거의 비용이 들지 않습니다.
문자열·토큰용 내장 메트릭
규칙 기반 메트릭의 표준 라이브러리. dspy/evaluate/metrics.py에 있어요.
dspy.evaluate.normalize_text(s: str) → str
NFD 유니코드, 소문자, 영어 관사 a / an / the 제거, 구두점 제거, 공백 축소. 아래 모든 문자열 비교 메트릭이 공유하는 단일 표준 정규화예요.
dspy.evaluate.EM(prediction, answers_list) → bool
양쪽에 normalize_text를 적용한 후의 exact match입니다. answers_list의 어떤 참조와도 일치하면 True를 돌려줘요. answers_list가 리스트가 아니면 raise합니다. 가장 작은 메트릭 원시 연산이고, 커스텀 래퍼의 빌딩 블록으로 유용합니다.
dspy.evaluate.answer_exact_match(example, pred, trace=None, frac=1.0) → bool
메트릭 모양 래퍼입니다. pred.answer와 example.answer를 읽고 둘 다 정규화한 뒤, frac >= 1.0이면 EM으로, frac < 1.0이면 F1 임계값 매칭으로 보냅니다. example.answer가 단일 문자열이거나 허용 참조 목록인 경우를 처리해요.
dspy.evaluate.answer_passage_match(example, pred, trace=None) → bool
검색(retrieval) 평가입니다. pred.context의 어떤 패시지가 example.answer의 어떤 참조를 포함하면 True를 돌려줍니다. 패시지에는 DPR 스타일 정규화기(더 많은 텍스트를 보존)를 쓰고, 답에는 여전히 normalize_text를 사용합니다.
F1 / HotPotF1 — 토큰 레벨 채점자 (내부)
F1은 정규화된 문자열에 대해 토큰 레벨 F1을 하고, 참조 중 최대값을 고릅니다. HotPotF1은 HotPotQA 고유 규칙 하나를 추가합니다. 정규화된 양쪽이 yes / no / noanswer이고 둘이 다르면 0을 돌려줘요. 둘 다 answer_exact_match에 공급되고 거의 직접 호출되지 않습니다.
LLM-as-judge 메트릭
규칙 기반 채점이 올바른 정확성 개념을 잡지 못하는 작업용입니다. 둘 다 dspy/evaluate/auto_evaluation.py에 있고 dspy.Module 서브클래스입니다.
dspy.evaluate.SemanticF1(threshold=0.66, decompositional=False)
Module입니다. forward(example, pred, trace=None)는 SemanticRecallPrecision(또는 decompositional 변형) 위에서 ChainOfThought를 돌리고 example.response와 pred.response에 대한 precision·recall을 LM에 요청합니다. f1_score(precision, recall)을 계산합니다. 평가 시점에는 Prediction(score=f1), 최적화 시점에는 Prediction(score=(f1 >= threshold))을 돌려줍니다 — 같은 인스턴스가 두 모드를 다 서비스해요.
dspy.evaluate.CompleteAndGrounded(threshold=0.66)
ChainOfThought 호출 두 개를 실행합니다. 하나는 완전성(pred.response가 example.response를 덮는가?), 하나는 근거(pred.response가 pred.context로 뒷받침되는가?). SemanticF1과 같은 trace 기반 이진화로 Prediction(score=f1_score(groundedness, completeness))을 돌려줍니다. 검색 증강 프로그램용으로 설계됐어요.
일반적인 LLM-judge 패턴. 출력에 score: float(그리고 선택적으로 feedback: str)를 포함한 dspy.Signature를 쓰세요. 그걸 forward(example, pred, trace=None)가 판사 예측기를 호출하고 Prediction(score, feedback)을 돌려주는 dspy.Module로 감쌉니다. 최적화 안에서 다른 동작을 원할 때 trace is None 토글을 쓰세요. 그게 전체 레시피예요 — 두 내장 판사는 이 모양 위에 있는 20줄짜리 모듈입니다.
Evaluate 하네스
프로그램 + 메트릭 + 데이터셋을 받아 점수를 만드는 실행기.
dspy.Evaluate(*, devset, metric=None, num_threads=None, display_progress=False, display_table=False, max_errors=None, provide_traceback=None, failure_score=0.0, save_as_csv=None, save_as_json=None)
키워드 전용 생성자입니다. metric은 여기서 넘기거나 호출로 미룰 수 있어요. display_table=N은 표시된 DataFrame을 N 행으로 자르고, display_table=True는 전체를 보여주며, False는 아무것도 안 보여줍니다. save_as_csv/save_as_json은 나중에 검사하도록 예시별 결과를 디스크에 씁니다. 제거된 return_outputs kwarg를 넘기면 ValueError를 던져요.
Evaluate.__call__(program, metric=None, devset=None, ...) → EvaluationResult
(program, example) 쌍을 ParallelExecutor에 제출합니다. 각 워커는 부모의 thread_local_overrides를 다시 적용하고 program(**example.inputs())를 실행한 뒤 메트릭을 (example, prediction)으로 호출합니다. 집계가 .score가 됩니다. 불리언 반환은 True의 백분율, float 반환은 평균, Prediction 반환은 score의 평균이에요. 예시별 결과는 (example, prediction, score) 삼중항으로 .results에 담깁니다.
dspy.evaluate.EvaluationResult
필드 두 개(집계 .score, 삼중항 목록 .results)를 가진 dspy.Prediction 서브클래스. repr은 전체 목록을 버리지 않고 점수와 결과 개수를 보여줘서, EvaluationResult를 로깅해도 메가바이트 단위 출력을 찍지 않습니다.
관련 문서
- 설정과
context()—Evaluate의 워커가 호출 주변의dspy.context(...)오버라이드를 어떻게 상속하는지. - 내장 모듈 변형 —
dspy.Parallel과Module.batch가 바닥에서 같은ParallelExecutor를 씁니다. - 옵티마이저: 하나 고르기 — 모든 옵티마이저가 여기 정의된 메트릭에 맞춰 컴파일하며, GEPA가 기대하는
Prediction(score, feedback)모양은 위에 문서화돼 있어요.