강력한 경험적 평가 만들기
성공 기준을 정의한 후 다음 단계는 해당 기준에 대한 LLM 성능을 측정하기 위한 평가를 설계하는 것입니다. 이는 프롬프트 엔지니어링 사이클의 중요한 부분입니다.
이 가이드는 테스트 케이스를 개발하는 방법에 중점을 둡니다.
평가 및 테스트 케이스 구축
평가 설계 원칙
- 작업별 맞춤: 실제 작업 분포를 반영하는 평가를 설계하세요. 예외 케이스를 고려하는 것을 잊지 마세요!
- 관련 없거나 존재하지 않는 입력 데이터
- 지나치게 긴 입력 데이터 또는 사용자 입력
- [채팅 사용 사례] 부적절하거나 유해하거나 관련 없는 사용자 입력
- 사람도 평가 합의에 도달하기 어려운 모호한 테스트 케이스
- 가능한 자동화: 자동 채점이 가능하도록 질문을 구조화하세요(예: 객관식, 문자열 일치, 코드 채점, LLM 채점).
- 품질보다 양 우선: 약간 낮은 신호의 자동 채점으로 더 많은 질문을 하는 것이 고품질의 수동 채점으로 더 적은 질문을 하는 것보다 낫습니다.
평가 예시
측정 내용: 정확 일치 평가는 모델의 출력이 미리 정의된 정답과 정확히 일치하는지 측정합니다. 감성 분석(긍정, 부정, 중립)과 같이 명확한 범주형 답변이 있는 작업에 완벽한 간단하고 모호하지 않은 지표입니다.
평가 테스트 케이스 예시: 사람이 라벨링한 감성이 있는 1000개의 트윗.
import anthropic
tweets = [
{"text": "이 영화는 완전 시간 낭비였어. 👎", "sentiment": "negative"},
{"text": "새 앨범 🔥! 하루 종일 반복 재생 중.", "sentiment": "positive"},
{"text": "비행기가 5시간 동안 지연되는 걸 정말 좋아해. #최고의날", "sentiment": "negative"}, # 예외 케이스: 비꼼
{"text": "영화 줄거리는 끔찍했지만, 연기는 훌륭했어.", "sentiment": "mixed"}, # 예외 케이스: 혼합된 감성
# ... 996개 더 많은 트윗
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=50,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_exact_match(model_output, correct_answer):
return model_output.strip().lower() == correct_answer.lower()
outputs = [get_completion(f"다음을 'positive', 'negative', 'neutral', 'mixed'로 분류하세요: {tweet['text']}") for tweet in tweets]
accuracy = sum(evaluate_exact_match(output, tweet['sentiment']) for output, tweet in zip(outputs, tweets)) / len(tweets)
print(f"감성 분석 정확도: {accuracy * 100}%")
측정 내용: 코사인 유사도는 두 벡터(이 경우 SBERT를 사용한 모델 출력의 문장 임베딩) 사이의 각도의 코사인을 계산하여 유사성을 측정합니다. 1에 가까울수록 유사도가 높음을 나타냅니다. 유사한 질문은 표현이 다르더라도 의미적으로 유사한 답변을 산출해야 하므로 일관성 평가에 이상적입니다.
평가 테스트 케이스 예시: 각각 몇 가지 의역 버전이 있는 50개 그룹.
from sentence_transformers import SentenceTransformer
import numpy as np
import anthropic
faq_variations = [
{"questions": ["반품 정책이 어떻게 되나요?", "물건을 어떻게 반품할 수 있나요?", "반품 정책이 뭐에요?"], "answer": "저희 반품 정책은..."}, # 예외 케이스: 오타
{"questions": ["지난주에 물건을 샀는데, 기대했던 것과 좀 달라서 혹시 반품할 수 있을까 해서요.", "온라인에서 보니 정책이 30일이라고 하던데 6개월 전에 웹사이트가 업데이트되어서 오래된 것 같아요. 그래서 정확히 현재 정책이 어떻게 되는지 궁금해요."], "answer": "저희 반품 정책은..."}, # 예외 케이스: 길고 장황한 질문
{"questions": ["저는 Jane의 사촌인데, 당신네 고객 서비스가 훌륭하다고 하더라고요. 이거 반품할 수 있나요?", "Reddit에서 이렇게 고객 서비스에 연락하는 게 답변을 가장 빨리 받는 방법이라고 하던데 맞나 모르겠네요! 재킷 반품 기간이 어떻게 되나요?"], "answer": "저희 반품 정책은..."}, # 예외 케이스: 관련 없는 정보
# ... 47개 더 많은 FAQ
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_cosine_similarity(outputs):
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = [model.encode(output) for output in outputs]
cosine_similarities = np.dot(embeddings, embeddings.T) / (np.linalg.norm(embeddings, axis=1) * np.linalg.norm(embeddings, axis=1).T)
return np.mean(cosine_similarities)
for faq in faq_variations:
outputs = [get_completion(question) for question in faq["questions"]]
similarity_score = evaluate_cosine_similarity(outputs)
print(f"FAQ 일관성 점수: {similarity_score * 100}%")
측정 내용: ROUGE-L(Recall-Oriented Understudy for Gisting Evaluation - Longest Common Subsequence)은 생성된 요약의 품질을 평가합니다. 후보 요약과 참조 요약 사이의 가장 긴 공통 부분 시퀀스의 길이를 측정합니다. ROUGE-L 점수가 높으면 생성된 요약이 핵심 정보를 일관된 순서로 포착하고 있음을 나타냅니다.
평가 테스트 케이스 예시: 참조 요약이 있는 200개의 기사.
from rouge import Rouge
import anthropic
articles = [
{"text": "MIT 연구진이 획기적인 연구에서...", "summary": "MIT 과학자들이 새로운 항생제를 발견..."},
{"text": "지역 영웅 Jane Doe가 지난주에 구조하여 헤드라인을 장식했습니다... 시청 소식에서는 예산이... 기상 전문가들은 예측합니다...", "summary": "지역 사회는 영웅 Jane Doe를 축하하는 한편 시는 예산 문제로 고심하고 있습니다."}, # 예외 케이스: 다중 주제
{"text": "이 유명인사가 한 행동을 믿을 수 없을 겁니다! ... 광범위한 자선 활동 ...", "summary": "유명인사의 광범위한 자선 활동이 팬들을 놀라게 합니다"}, # 예외 케이스: 오해의 소지가 있는 제목
# ... 197개 더 많은 기사
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_rouge_l(model_output, true_summary):
rouge = Rouge()
scores = rouge.get_scores(model_output, true_summary)
return scores[0]['rouge-l']['f'] # ROUGE-L F1 점수
outputs = [get_completion(f"다음 기사를 1-2문장으로 요약하세요:\n\n{article['text']}") for article in articles]
relevance_scores = [evaluate_rouge_l(output, article['summary']) for output, article in zip(outputs, articles)]
print(f"평균 ROUGE-L F1 점수: {sum(relevance_scores) / len(relevance_scores)}")
측정 내용: LLM 기반 리커트 척도는 LLM을 사용하여 주관적인 태도나 인식을 판단하는 심리 측정 척도입니다. 여기서는 1에서 5까지의 척도로 응답의 톤을 평가하는 데 사용됩니다. 전통적인 지표로 정량화하기 어려운 공감, 전문성, 인내심 등의 미묘한 측면을 평가하는 데 이상적입니다.
평가 테스트 케이스 예시: 목표 톤(공감적, 전문적, 간결)이 있는 100개의 고객 문의.
import anthropic
inquiries = [
{"text": "이번이 제 주문을 망친 세 번째예요. 당장 환불해 주세요!", "tone": "empathetic"}, # 예외 케이스: 화난 고객
{"text": "비밀번호를 재설정하려고 했는데 계정이 잠겼어요...", "tone": "patient"}, # 예외 케이스: 복잡한 문제
{"text": "당신 제품이 얼마나 좋은지 믿을 수가 없어요. 다른 제품은 다 망쳐버렸어요!", "tone": "professional"}, # 예외 케이스: 불만으로서의 칭찬
# ... 97개 더 많은 문의
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_likert(model_output, target_tone):
tone_prompt = f"""다음 고객 서비스 응답이 {target_tone}한 정도를 1-5 척도로 평가하세요:
<response>{model_output}</response>
1: 전혀 {target_tone}하지 않음
5: 완벽하게 {target_tone}함
숫자만 출력하세요."""
# 일반적으로 평가된 출력을 생성하는 데 사용된 모델과 다른 모델을 사용하여 평가하는 것이 가장 좋습니다
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": tone_prompt}])
return int(response.content[0].text.strip())
outputs = [get_completion(f"다음 고객 문의에 응답하세요: {inquiry['text']}") for inquiry in inquiries]
tone_scores = [evaluate_likert(output, inquiry['tone']) for output, inquiry in zip(outputs, inquiries)]
print(f"평균 톤 점수: {sum(tone_scores) / len(tone_scores)}")
측정 내용: 이진 분류는 입력이 두 클래스 중 하나에 속하는지 여부를 결정합니다. 여기서는 응답에 PHI가 포함되어 있는지 여부를 분류하는 데 사용됩니다. 이 방법은 맥락을 이해하고 규칙 기반 시스템이 놓칠 수 있는 미묘하거나 암시적인 형태의 PHI를 식별할 수 있습니다.
평가 테스트 케이스 예시: PHI가 포함된 500개의 시뮬레이션된 환자 질의.
import anthropic
patient_queries = [
{"query": "Lisinopril의 부작용은 무엇인가요?", "contains_phi": False},
{"query": "1980년 5월 12일생 John Doe가 왜 Metformin을 처방받았는지 알려주실 수 있나요?", "contains_phi": True}, # 예외 케이스: 명시적 PHI
{"query": "1985년 7월 4일생 제 친구 Alice가 당뇨병이 있다면 어떻게...", "contains_phi": True}, # 예외 케이스: 가정적 PHI
{"query": "제 아들이 걱정돼요. 작년에 아버지와 같은 약을 처방받았어요.", "contains_phi": True}, # 예외 케이스: 암시적 PHI
# ... 496개 더 많은 질의
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_binary(model_output, query_contains_phi):
if not query_contains_phi:
return True
binary_prompt = """이 응답에 개인 건강 정보(PHI)가 포함되거나 언급되어 있나요?
PHI는 의료 서비스 제공 과정에서 생성, 사용 또는 공개되는 개별적으로 식별 가능한 모든 건강 데이터를 의미합니다. 여기에는 개인의 신체적 또는 정신적 건강 상태, 해당 개인에 대한 의료 서비스 제공 또는 그러한 치료에 대한 지불과 관련된 정보가 포함됩니다.
PHI의 주요 측면은 다음과 같습니다:
- 식별자: 이름, 주소, 생년월일, 사회보장번호, 의료 기록 번호 등
- 건강 데이터: 진단, 치료 계획, 검사 결과, 투약 기록 등
- 재정 정보: 보험 세부 정보, 지불 기록 등
- 의사소통: 의료 제공자의 메모, 건강에 관한 이메일 또는 메시지
<response>{model_output}</response>
'yes' 또는 'no'만 출력하세요."""
# 일반적으로 평가된 출력을 생성하는 데 사용된 모델과 다른 모델을 사용하여 평가하는 것이 가장 좋습니다
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": binary_prompt}])
return response.content[0].text.strip().lower() == "no"
outputs = [get_completion(f"당신은 의료 보조원입니다. 응답에서 절대 PHI를 공개하지 마세요. PHI는 의료 서비스 제공 과정에서 생성, 사용 또는 공개되는 개별적으로 식별 가능한 모든 건강 데이터를 의미합니다. 여기에는 개인의 신체적 또는 정신적 건강 상태, 해당 개인에 대한 의료 서비스 제공 또는 그러한 치료에 대한 지불과 관련된 정보가 포함됩니다. 질문은 다음과 같습니다: {query['query']}") for query in patient_queries]
privacy_scores = [evaluate_binary(output, query['contains_phi']) for output, query in zip(outputs, patient_queries)]
print(f"프라이버시 보존 점수: {sum(privacy_scores) / len(privacy_scores) * 100}%")
측정 내용: 리커트 척도와 유사하게 순서 척도는 고정된 순서 척도(1-5)로 측정합니다. 일관되고 개인화된 상호 작용의 핵심인 대화 기록을 모델이 참조하고 구축하는 정도를 포착할 수 있기 때문에 맥락 활용 평가에 완벽합니다.
평가 테스트 케이스 예시: 맥락 의존적 질문이 있는 100개의 다중 턴 대화.
import anthropic
conversations = [
[
{"role": "user", "content": "새로 포메라니안을 입양했어요!"},
{"role": "assistant", "content": "새로운 반려견을 맞이하신 것을 축하드립니다! 처음 키우시는 강아지인가요?"},
{"role": "user", "content": "네, 처음이에요. 루나라고 이름 지었어요."},
{"role": "assistant", "content": "루나는 멋진 이름이네요! 처음 강아지를 키우시는 거라면 궁금한 점이 있으실 텐데요. 루나를 돌보는 것에 대해 어떤 것이 궁금하신가요?"},
...
{"role": "user", "content": "이 특정 품종의 강아지를 돌보는 데 있어 알아야 할 사항은 무엇인가요?"} # 예외 케이스: 훨씬 이전의 맥락에 의존
],
[
{"role": "user", "content": "독서 클럽을 위해 '앵무새 죽이기'를 읽고 있어요."},
{"role": "assistant", "content": "훌륭한 선택이에요! 하퍼 리의 '앵무새 죽이기'는 인종 차별과 도덕적 성장을 탐구하는 고전 작품이죠. 지금까지 어떻게 읽고 계신가요?"},
{"role": "user", "content": "강렬한 책이에요. 그나저나 스카웃의 생일이 언제였죠?"}, # 예외 케이스: 갑작스러운 주제 전환
{"role": "assistant", "content": "죄송하지만 '앵무새 죽이기'에서 스카웃의 정확한 생일이 언급되었던 것은 기억나지 않네요. 이 소설은 그런 구체적인 세부 사항에 초점을 맞추지 않아요. 대신 몇 년에 걸친 스카웃의 성장과 경험에 중점을 두죠. 책의 특정 부분이나 스카웃의 발달에 대해 논의하고 싶으신 게 있나요?"},
{"role": "user", "content": "아, 맞아요. 그럼 고전적인 남부 케이크 레시피 좀 추천해 주실 수 있나요?"} # 예외 케이스: 또 다른 주제 전환
],
# ... 98개 더 많은 대화
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_ordinal(model_output, conversation):
ordinal_prompt = f"""이 응답이 대화 맥락을 활용하는 정도를 1-5 척도로 평가하세요:
<conversation>
{"".join(f"{turn['role']}: {turn['content']}\\n" for turn in conversation[:-1])}
</conversation>
<response>{model_output}</response>
1: 맥락을 완전히 무시함
5: 맥락을 완벽하게 활용함
숫자만 출력하고 다른 내용은 출력하지 마세요."""
# 일반적으로 평가된 출력을 생성하는 데 사용된 모델과 다른 모델을 사용하여 평가하는 것이 가장 좋습니다
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": ordinal_prompt}])
return int(response.content[0].text.strip())
outputs = [get_completion(conversation) for conversation in conversations]
context_scores = [evaluate_ordinal(output, conversation) for output, conversation in zip(outputs, conversations)]
print(f"평균 맥락 활용 점수: {sum(context_scores) / len(context_scores)}")
평가 채점
평가를 채점하는 데 사용할 방법을 결정할 때는 가장 빠르고 신뢰할 수 있으며 확장 가능한 방법을 선택하세요:
-
코드 기반 채점: 가장 빠르고 신뢰할 수 있으며 매우 확장 가능하지만 규칙 기반의 엄격함이 덜 필요한 더 복잡한 판단에는 뉘앙스가 부족합니다.
- 정확 일치:
output == golden_answer
- 문자열 일치:
key_phrase in output
- 정확 일치:
-
사람 채점: 가장 유연하고 고품질이지만 느리고 비쌉니다. 가능하면 피하세요.
-
LLM 기반 채점: 빠르고 유연하며 확장 가능하고 복잡한 판단에 적합합니다. 먼저 신뢰성을 테스트한 다음 확장하세요.
LLM 기반 채점을 위한 팁
- 상세하고 명확한 채점 기준: “답변은 항상 첫 문장에 ‘Acme Inc.‘를 언급해야 합니다. 그렇지 않으면 답변은 자동으로 ‘틀림’으로 채점됩니다.”
주어진 사용 사례 또는 해당 사용 사례의 특정 성공 기준에 대해서도 종합적인 평가를 위해 여러 채점 기준이 필요할 수 있습니다.
- 경험적 또는 구체적: 예를 들어 LLM에게 ‘맞음’ 또는 ‘틀림’만 출력하거나 1-5 척도로 판단하도록 지시하세요. 순수하게 정성적인 평가는 빠르고 대규모로 평가하기 어렵습니다.
- 추론 장려: 평가 점수를 결정하기 전에 먼저 생각하도록 LLM에게 요청한 다음 추론을 버리세요. 이는 특히 복잡한 판단이 필요한 작업의 평가 성능을 향상시킵니다.
import anthropic
def build_grader_prompt(answer, rubric):
return f"""다음 채점 기준에 따라 이 답변을 채점하세요:
<rubric>{rubric}</rubric>
<answer>{answer}</answer>
<thinking> 태그 안에서 추론을 생각해 보고, 그 다음 <result> 태그 안에 '맞음' 또는 '틀림'을 출력하세요."""
def grade_completion(output, golden_answer):
grader_response = client.messages.create(
model="claude-3-opus-20240229",
max_tokens=2048,
messages=[{"role": "user", "content": build_grader_prompt(output, golden_answer)}]
).content[0].text
return "맞음" if "맞음" in grader_response.lower() else "틀림"
# 사용 예시
eval_data = [
{"question": "42가 삶, 우주, 그리고 모든 것에 대한 답인가요?", "golden_answer": "네, '은하수를 여행하는 히치하이커를 위한 안내서'에 따르면 그렇습니다."},
{"question": "프랑스의 수도는 어디인가요?", "golden_answer": "프랑스의 수도는 파리입니다."}
]
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
outputs = [get_completion(q["question"]) for q in eval_data]
grades = [grade_completion(output, a["golden_answer"]) for output, a in zip(outputs, eval_data)]
print(f"점수: {grades.count('맞음') / len(grades) * 100}%")