Non-Rank Based Metrics: Accuracy, Precision, Recall@k 등을 통해 관련성의 이진적 평가를 수행
Rank-Based Metrics: MRR(Mean Reciprocal Rank), MAP(Mean Average Precision)를 통해 검색 결과의 순위를 고려한 평가를 수행
RAG 특화 지표: 기존 검색 평가 방식의 한계를 보완하는 LLM-as-judge 방식 도입
포괄적 평가: 정확도, 관련성, 다양성, 강건성을 통합적으로 측정
전통적 평가: ROUGE(요약), BLEU(번역), BertScore(의미 유사도) 지표 활용
LLM 기반 평가: 응집성, 관련성, 유창성을 종합적으로 판단하는 새로운 접근법 도입 (전통적인 참조 비교가 어려운 상황에서 유용)
다차원 평가: 품질, 일관성, 사실성, 가독성, 사용자 만족도를 포괄적 측정
상세 프롬프트와 사용자 선호도 기준으로 생성 텍스트 품질 평가
2개의 검색 쿼리에 대한 정답(실제 문서)와 검색 결과(예측 문서)를 준비
실제 문서(actual_docs):
예측 문서(predicted_docs):
랭체인 문서 객체로 구현된 각 문서는 다음 정보를 포함:
from langchain_core.documents import Document
from textwrap import dedent
# 실제 문서 데이터 (정답)
actual_docs = [
[
Document(
page_content=dedent("""
고객 문의: 제품 배송 지연
문의 일시: 2024-01-15 14:23
고객명: 김지안
문의 내용: 지난주 주문한 상품이 아직도 배송이 안 왔습니다.
언제쯤 받을 수 있을까요?
처리 상태: 답변 완료
담당자: 이수진
""").strip(),
metadata={"id": "doc1", "category": "배송", "priority": "높음"}
),
Document(
page_content=dedent("""
고객 문의: 결제 오류
문의 일시: 2024-01-15 15:30
고객명: 이동현
문의 내용: 결제 시도 중 오류가 발생했습니다.
카드 결제가 되지 않아요.
처리 상태: 답변 완료
담당자: 김태호
""").strip(),
metadata={"id": "doc2", "category": "결제", "priority": "높음"}
),
Document(
page_content=dedent("""
고객 문의: 포인트 적립 문의
문의 일시: 2024-01-17 16:10
고객명: 최서연
문의 내용: 지난 구매 건에 대한 포인트가 적립이 안 되었습니다.
확인 부탁드립니다.
처리 상태: 답변 대기
담당자: 미배정
""").strip(),
metadata={"id": "doc5", "category": "포인트/적립", "priority": "낮음"}
),
],
[
Document(
page_content=dedent("""
고객 문의: 제품 교환 요청
문의 일시: 2024-01-16 09:45
고객명: 박현우
문의 내용: 받은 제품의 사이즈가 맞지 않아 교환하고 싶습니다.
교환 절차를 알려주세요.
처리 상태: 진행중
담당자: 정미영
""").strip(),
metadata={"id": "doc3", "category": "교환/반품", "priority": "중간"}
),
Document(
page_content=dedent("""
고객 문의: 취소 환불 문의
문의 일시: 2024-01-16 11:20
고객명: 장민서
문의 내용: 주문을 취소하고 싶습니다.
환불은 얼마나 걸리나요?
처리 상태: 답변 완료
담당자: 홍길동
""").strip(),
metadata={"id": "doc4", "category": "취소/환불", "priority": "중간"}
),
]
]
# 예측 문서 데이터 (검색 결과)
predicted_docs = [
[
Document(
page_content=dedent("""
고객 문의: 제품 배송 지연
문의 일시: 2024-01-15 14:23
고객명: 김지안
문의 내용: 지난주 주문한 상품이 아직도 배송이 안 왔습니다.
언제쯤 받을 수 있을까요?
처리 상태: 답변 완료
담당자: 이수진
""").strip(),
metadata={"id": "doc1", "category": "배송", "priority": "높음"}
),
Document(
page_content=dedent("""
고객 문의: 결제 오류
문의 일시: 2024-01-15 15:30
고객명: 이동현
문의 내용: 결제 시도 중 오류가 발생했습니다.
카드 결제가 되지 않아요.
처리 상태: 답변 완료
담당자: 김태호
""").strip(),
metadata={"id": "doc2", "category": "결제", "priority": "높음"}
),
Document(
page_content=dedent("""
고객 문의: 포인트 적립 문의
문의 일시: 2024-01-17 16:10
고객명: 최서연
문의 내용: 지난 구매 건에 대한 포인트가 적립이 안 되었습니다.
확인 부탁드립니다.
처리 상태: 답변 대기
담당자: 미배정
""").strip(),
metadata={"id": "doc5", "category": "포인트/적립", "priority": "낮음"}
),
],
[
Document(
page_content=dedent("""
고객 문의: 상품 재입고 문의
문의 일시: 2024-01-17 17:45
고객명: 한승우
문의 내용: 품절된 상품의 재입고 일정이 궁금합니다.
알림 신청은 어떻게 하나요?
처리 상태: 답변 대기
담당자: 미배정
""").strip(),
metadata={"id": "doc6", "category": "재고", "priority": "중간"}
),
Document(
page_content=dedent("""
고객 문의: 취소 환불 문의
문의 일시: 2024-01-16 11:20
고객명: 장민서
문의 내용: 주문을 취소하고 싶습니다.
환불은 얼마나 걸리나요?
처리 상태: 답변 완료
담당자: 홍길동
""").strip(),
metadata={"id": "doc4", "category": "취소/환불", "priority": "중간"}
),
Document(
page_content=dedent("""
고객 문의: 포인트 적립 문의
문의 일시: 2024-01-17 16:10
고객명: 최서연
문의 내용: 지난 구매 건에 대한 포인트가 적립이 안 되었습니다.
확인 부탁드립니다.
처리 상태: 답변 대기
담당자: 미배정
""").strip(),
metadata={"id": "doc5", "category": "포인트/적립", "priority": "낮음"}
),
]
]
# 문서 ID 출력
actual_ids = [[doc.metadata["id"] for doc in docs] for docs in actual_docs]
predicted_ids = [[doc.metadata["id"] for doc in docs] for docs in predicted_docs]
print("실제 문서 ID:", actual_ids)
print("예측 문서 ID:", predicted_ids)
- 출력
실제 문서 ID: [['doc1', 'doc2', 'doc5'], ['doc3', 'doc4']]
예측 문서 ID: [['doc1', 'doc2', 'doc5'], ['doc6', 'doc4', 'doc5']]
문서 검색 시스템의 성능은 정확도와 재현율을 통해 평가
정밀도(Precision) 지표는 검색된 문서 중 관련 문서의 비율을 측정
재현율(Recall) 지표는 전체 관련 문서 중 실제로 검색된 문서의 비율을 나타냄
F1 점수는 정밀도와 재현율의 조화 평균으로 종합적 성능을 평가
효과적인 검색 시스템은 정확도와 재현율 간의 최적의 균형을 찾는 것이 중요
True Positive (TP):
False Positive (FP):
False Negative (FN):
# True Positive - 예측 문서 중 실제 문서와 일치하는 문서
true_positives = [
[doc.metadata["id"] for doc in actual if doc in predicted]
for actual, predicted in zip(actual_docs, predicted_docs)
]
# False Positive - 예측 문서 중 실제 문서와 일치하지 않는 문서
false_positives = [
[doc.metadata["id"] for doc in predicted if doc not in actual]
for actual, predicted in zip(actual_docs, predicted_docs)
]
# False Negative - 실제 문서 중 예측 문서와 일치하지 않는 문서
false_negatives = [
[doc.metadata["id"] for doc in actual if doc not in predicted]
for actual, predicted in zip(actual_docs, predicted_docs)
]
print("True Positive:", true_positives)
print("False Positive:", false_positives)
print("False Negative:", false_negatives)
- 출력
True Positive: [['doc1', 'doc2', 'doc5'], ['doc4']]
False Positive: [[], ['doc6', 'doc5']]
False Negative: [[], ['doc3']]
정밀도(Precision) = (검색된 문서 중 관련 문서 수) / (검색된 총 문서 수)
재현율(Recall) = (검색된 관련 문서 수) / (전체 관련 문서 수)
F1 Score = 2 × (정밀도 × 재현율) / (정밀도 + 재현율)
성능 평가는 문서셋과 검색 쿼리에 대해 각각 계산하여 평균값 사용
# 각 쿼리별 성능 분석
for i, (tp, fp, fn) in enumerate(zip(true_positives, false_positives, false_negatives)):
print(f"쿼리 {i+1} 분석:")
print(f"정확하게 검색된 문서 (TP): {tp}")
print(f"잘못 검색된 문서 (FP): {fp}")
print(f"놓친 문서 (FN): {fn}")
# 정밀도와 재현율 계산
precision = len(tp) / (len(tp) + len(fp)) if len(tp) + len(fp) > 0 else 0
recall = len(tp) / (len(tp) + len(fn)) if len(tp) + len(fn) > 0 else 0
f1 = 2 * (precision * recall) / (precision + recall) if precision + recall > 0 else 0
print(f"정밀도 (Precision): {precision:.2f}")
print(f"재현율 (Recall): {recall:.2f}")
print(f"F1 Score: {f1:.2f}")
print('')
- 출력
쿼리 1 분석:
정확하게 검색된 문서 (TP): ['doc1', 'doc2', 'doc5']
잘못 검색된 문서 (FP): []
놓친 문서 (FN): []
정밀도 (Precision): 1.00
재현율 (Recall): 1.00
F1 Score: 1.00
쿼리 2 분석:
정확하게 검색된 문서 (TP): ['doc4']
잘못 검색된 문서 (FP): ['doc6', 'doc5']
놓친 문서 (FN): ['doc3']
정밀도 (Precision): 0.33
재현율 (Recall): 0.50
F1 Score: 0.40
Micro Average는 전체 데이터셋의 개별 결과를 합산하여 계산
Macro Average는 각 클래스(쿼리)별 성능 지표를 먼저 계산한 후 평균을 구함
Micro는 데이터 불균형에 덜 민감하며 전체적 성능 평가에 적합
Macro는 각 클래스(쿼리)의 성능을 동등하게 고려하여 소수 클래스 성능도 중요하게 반영
매크로 평균은 각 클래스(쿼리)별 성능 지표(Precision, Recall, F1)을 독립적으로 계산 후 평균값 산출
수식: Macro-Precision =
수식: Macro-Recall =
각 클래스(쿼리)는 동일한 가중치로 평가되어 소수 클래스의 성능도 중요하게 반영됨
from typing import List, Tuple
import numpy as np
def calculate_macro_metrics(
true_positives: List[List],
false_positives: List[List],
false_negatives: List[List]
) -> Tuple[float, float, float]:
"""
Macro-average 방식으로 Precision, Recall, F1 Score를 계산합니다.
각 클래스별 메트릭을 먼저 계산한 후 평균을 냅니다.
Args:
true_positives (List[List]): 각 클래스별 true positive 케이스들의 리스트
false_positives (List[List]): 각 클래스별 false positive 케이스들의 리스트
false_negatives (List[List]): 각 클래스별 false negative 케이스들의 리스트
Returns:
Tuple[float, float, float]: (macro_precision, macro_recall, macro_f1) 값을 반환
"""
n_classes = len(true_positives)
precisions = []
recalls = []
f1_scores = []
for i in range(n_classes):
tp = len(true_positives[i])
fp = len(false_positives[i])
fn = len(false_negatives[i])
# Precision 계산
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
precisions.append(precision)
# Recall 계산
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
recalls.append(recall)
# F1 Score 계산
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
f1_scores.append(f1)
print(f"Class {i+1}: Precision={precision:.3f}, Recall={recall:.3f}, F1 Score={f1:.3f}")
# 각 메트릭의 평균을 계산
macro_precision = np.mean(precisions)
macro_recall = np.mean(recalls)
macro_f1 = np.mean(f1_scores)
return macro_precision, macro_recall, macro_f1 # type: ignore
# Macro-average 방식으로 Precision, Recall, F1 Score 계산
macro_precision, macro_recall, macro_f1 = calculate_macro_metrics(true_positives, false_positives, false_negatives)
print("\nMacro-average Metrics:")
print(f"Macro-average Precision: {macro_precision:.2f}")
print(f"Macro-average Recall: {macro_recall:.2f}")
print(f"Macro-average F1 Score: {macro_f1:.2f}")
- 출력
Class 1: Precision=1.000, Recall=1.000, F1 Score=1.000
Class 2: Precision=0.333, Recall=0.500, F1 Score=0.400
Macro-average Metrics:
Macro-average Precision: 0.67
Macro-average Recall: 0.75
Macro-average F1 Score: 0.70
마이크로 평균은 전체 데이터셋의 TP, FP, FN을 먼저 통합하여 계산
수식: Micro-Precision =
수식: Micro-Recall =
데이터 수가 많은 클래스가 전체 성능에 더 큰 영향을 미침
from typing import List, Tuple
def calculate_micro_metrics(
true_positives: List[List],
false_positives: List[List],
false_negatives: List[List]
) -> Tuple[float, float, float]:
"""
Micro-average 방식으로 Precision, Recall, F1 Score를 계산합니다.
모든 클래스의 TP, FP, FN을 합산한 후 메트릭을 계산합니다.
Args:
true_positives (List[List]): 각 클래스별 true positive 케이스들의 리스트
false_positives (List[List]): 각 클래스별 false positive 케이스들의 리스트
false_negatives (List[List]): 각 클래스별 false negative 케이스들의 리스트
Returns:
Tuple[float, float, float]: (micro_precision, micro_recall, micro_f1) 값을 반환
"""
# 전체 TP, FP, FN 합계 계산
total_tp = sum(len(tp) for tp in true_positives)
total_fp = sum(len(fp) for fp in false_positives)
total_fn = sum(len(fn) for fn in false_negatives)
# Micro Precision
micro_precision = total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0
# Micro Recall
micro_recall = total_tp / (total_tp + total_fn) if (total_tp + total_fn) > 0 else 0
# Micro F1 Score
micro_f1 = 2 * micro_precision * micro_recall / (micro_precision + micro_recall) \
if (micro_precision + micro_recall) > 0 else 0
return micro_precision, micro_recall, micro_f1
# Micro-average 방식으로 Precision, Recall, F1 Score 계산
micro_precision, micro_recall, micro_f1 = calculate_micro_metrics(true_positives, false_positives, false_negatives)
print("Micro-average Metrics:")
print(f"Micro-average Precision: {micro_precision:.2f}")
print(f"Micro-average Recall: {micro_recall:.2f}")
print(f"Micro-average F1 Score: {micro_f1:.2f}")
- 출력
Micro-average Metrics:
Micro-average Precision: 0.67
Micro-average Recall: 0.80
Micro-average F1 Score: 0.73
이전에 합성한 테스트 데이터셋 활용
이전에 생성해둔 벡터저장소를 로드
유사도 기준 검색기(retriever)를 설정 : k=3
검색된 문서의 텍스트가 참조(reference_contexts)에 포함되는지 여부를 평가
테스트셋의 모든 쿼리에 대해서 계산 (Precision, Recall, F1 Score)
import pandas as pd
testset = pd.read_excel('data/testset.xlsx')
# 벡터 저장소 로드
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
vector_store = Chroma(
collection_name='db_korean_cosine',
embedding_function=embedding_model,
persist_directory='./chroma_db'
)
retriever = vector_store.as_retriever(search_kwargs={'k': 3})
# 모든 쿼리에 대해서 검색을 수행하고 결과를 저장
retrieved_results = []
for i, row in testset.iterrows():
query = row["user_input"]
context = eval(row["reference_contexts"])
# 검색 수행
retrieved_docs = retriever.invoke(query)
# 검색 결과 저장
retrieved_results.append({
"query": query,
"context": context,
"retrieved": [doc.page_content for doc in retrieved_docs]
})
# 결과 확인
pprint(retrieved_results[:2])
- 출력
[{'context': ['Tesla, Inc.는 미국의 다국적 자동차 및 청정 에너지 회사입니다. 이 회사는 전기 자동차(BEV), 고정형 '
'배터리 에너지 저장 장치, 태양 전지판, 태양광 지붕널 및 관련 제품/서비스를 설계, 제조 및 판매합니다. '
'2003년 7월 Martin Eberhard와 Marc Tarpenning이 Tesla Motors로 설립했으며, '
'Nikola Tesla를 기리기 위해 명명되었습니다. Elon Musk는 2004년 Tesla의 초기 자금 조달을 '
'주도하여 2008년에 회장 겸 CEO가 되었습니다.'],
'query': 'Tesla, Inc.는 미국에서 어떤 역할을 하고 있으며, 이 회사의 주요 제품과 서비스는 무엇인가요?',
'retrieved': ['Tesla, Inc.는 미국의 다국적 자동차 및 청정 에너지 회사입니다. 이 회사는 전기 자동차(BEV), ',
...
]}]
# 검색 결과 저장
with open("data/retrieved_results.json", "w", encoding="utf-8") as f:
json.dump(retrieved_results, f, ensure_ascii=False, indent=2)
# 검색 결과 로드
with open("data/retrieved_results.json", "r", encoding='utf-8') as f:
retrieved_results = json.load(f)
# 검색 결과 확인
pprint(retrieved_results[:2])
# 각 쿼리별 성능 분석
# TP 계산 (True Positive) - 예측 문서 중 실제 문서와 일치하는 문서
true_positives = [
[doc for doc in result["retrieved"] if doc in result["context"]]
for result in retrieved_results
]
# FP 계산 (False Positive) - 예측 문서 중 실제 문서와 일치하지 않는 문서
false_positives = [
[doc for doc in result["retrieved"] if doc not in result["context"]]
for result in retrieved_results
]
# FN 계산 (False Negative) - 실제 문서 중 예측 문서와 일치하지 않는 문서
false_negatives = [
[doc for doc in result["context"] if doc not in result["retrieved"]]
for result in retrieved_results
]
# 각 쿼리별 성능 분석
for i, (tp, fp, fn) in enumerate(zip(true_positives, false_positives, false_negatives)):
print(f"\n쿼리 {i+1} 분석:")
print(f"정확하게 검색된 문서 (TP): {len(tp)}")
print(f"잘못 검색된 문서 (FP): {len(fp)}")
print(f"놓친 문서 (FN): {len(fn)}")
- 출력
쿼리 1 분석:
정확하게 검색된 문서 (TP): 1
잘못 검색된 문서 (FP): 2
놓친 문서 (FN): 0
쿼리 2 분석:
...
# Macro-average 방식으로 Precision, Recall, F1 Score 계산
macro_precision, macro_recall, macro_f1 = calculate_macro_metrics(true_positives, false_positives, false_negatives)
print("Macro-average Metrics:")
print(f"Macro-average Precision: {macro_precision:.2f}")
print(f"Macro-average Recall: {macro_recall:.2f}")
print(f"Macro-average F1 Score: {macro_f1:.2f}")
# Micro-average 방식으로 Precision, Recall, F1 Score 계산
micro_precision, micro_recall, micro_f1 = calculate_micro_metrics(true_positives, false_positives, false_negatives)
print("Micro-average Metrics:")
print(f"Micro-average Precision: {micro_precision:.2f}")
print(f"Micro-average Recall: {micro_recall:.2f}")
print(f"Micro-average F1 Score: {micro_f1:.2f}")