RAGAS 는 RAG 의 평가지표 중 특히 Rule Base 지표들을 자동으로 계산하기 위해 만들어진 프레임워크입니다.
쉽게 말해 RAG 파이프라인을 구축한 후,
파이프라인의 input / output 을 이용해 평가 지표 계산에 필요한 데이터셋 (question, answer, context, ground truth) 을 만들고
RAGAS 는 이 데이터셋을 이용하여 함수 단 한줄로 각 평가 지표를 빠르게 자동으로 계산해 줍니다.
RAGAS 에 대한 자세한 설명은 [우아한 스터디] RAGAS : RAG 파이프라인 평가 프레임워크 을,
이 포스팅의 전체 코드 및 샘플은 GitHub 을 참고해 주세요.
데이터는 총 4가지를 준비합니다.
question
: 사용자의 질문ground truth
: 질문에 맞는 정답 (= 모범 답안)answer
: LLM이 생성한 답변context
: LLM이 답변을 생성하기 위해 참고한 정보 (=context)보편적으로 다음 4개의 평가 지표를 이용합니다.
Faithfulness
: 주어진 context에 대한 생성된 답변의 일관성에 대한 값
(context - answer)
Context Recall
: ground_truth의 문장 중 contex로부터 추론할 수 있는 문장의 비율을 측정한 값
(ground truth - context)
Context Precision
: contexts에 존재하는 ground_truth와 관련된 항목을 높은 순위로 잘 검색해 왔는지의 여부를 평가하는 지표
(grund truth - context 및 참조 순위와 지표)
Answer Relevance
: 생성된 답변이 주어진 질문과 얼마나 관련성이 있는지에 대한 값
(answer - question)
Langchain 을 사용하지 않고, 데이터셋이 모두 구축된 경우 스코어만 계산하는 간단한 튜토리얼 코드입니다.
RAGAS 는 주어진 데이터에 각 평가지표별 수식을 계산하기 때문에
질문과 실제 답안뿐만이 아니라 LLM 으로 생성한 답변과 답변을 생성하는 데 참고한 context 만 준비한다면 RAGAS 를 이용해 품질 평가를 할 수가 있습니다.
from datasets import Dataset
questions = ["보험가격지수란 무엇인가요?"]
ground_truths = ["해당상품의 보험료총액(보험금 지급을 위한 보험료 및 보험회사의 사업경비 등을 위한 보험료)을 참조순보험료 총액*과 평균사업비 총액**을 합한 금액으로 나눈 비율을 “보험가격지수”라고 합니다.보험가격지수란 고객이 납입하는 보험료 중 사업비로 사용되는 금액을 수준에 대하여 상품군별 생명보험상품 전체의 평균 사업비율을 반영하여 계산한 값입니다."]
answers = ["보험가격지수는 해당 상품의 보험료 총액을 참조순보험료 총액과 평균사업비 총액을 합한 금액으로 나눈 비율을 말합니다. 보험료 총액은 보험금 지급을 위한 보험료와 보험회사의 사업경비 등을 위한 보험료를 포함합니다. 참조순보험료 총액은 감독원장이 정하는 바에 따라 산정한 전체 보험회사 공시이율의 평균(평균공시이율) 및 참조순보험요율을 적용하여 산출한 보험금 지급을 위한 보험료입니다. 평균사업비 총액은 상품군별 생명보험상품 전체의 평균 사업비율을 반영하여 계산(역산)한 값입니다."]
contexts = [['A : 해당상품의 보험료총액(보험금 지급을 위한 보험료 및 보험회사의 사업경비 등을 위한 보험료)을 참조순보험료 총액*과 평균사업비 총액**을 합한 금액으로 나눈 비율을 “보험가격지수”라고 합니다.* 감독원장이 정하는 바에 따라 산정한 전체 보험회사 공시이율의 평균(평균공시이율) 및 참조순보험요율을 적용하여 산출한 보험금 지급을 위한 보험료 ** 상품군별 생명보험상품 전체의 평균 사업비율을 반영하여 계산(역산)한 값(기준 : 40세) | 상품명 | 상품명 | 상품명 | 보험기간 | 납입기간 | 보험가격지수 | 보험가격지수 | 가입금액 (만원) | | --- | --- | --- | --- | --- | --- | --- | --- | | 상품명 | 상품명 | 상품명 | 보험기간 | 납입기간 | 남자 | 여자 | 가입금액 (만원) | | 보험상품명 | 1종 | 1종 | 종신 | 20년 | 110.9% | 113.6% | 10,000 | | 보험상품명 | 2종(1% 지급형) | 은퇴나이 55세 | 종신 | 20년 | 110.7% | 113.6% | 10,000 || 보험상품명 | 2종(1% 지급형) | 은퇴나이 60세 | 종신 | 20년 | 110.6% | 113.4% | 10,000 | | 보험상품명 | 2종(1% 지급형) | 은퇴나이 65세 | 종신 | 20년 | 110.4% | 113.3% | 10,000 | | 보험상품명 | 2종(2% 지급형) | 은퇴나이 55세 | 종신 | 20년 | 110.3% | 113.2% | 10,000 | | 보험상품명 | 2종(2% 지급형) | 은퇴나이 60세 | 종신 | 20년 | 110.0% | 113.0% | 10,000 | | 보험상품명 | 2종(2% 지급형) | 은퇴나이 65세 | 종신 | 20년 | 109.6% | 112.8% | 10,000 || 보험상품명 | 3종 | 3종 | 종신 | 20년 | 108.7% | 111.1% | 10,000 |']]
# To dict
data = {
"user_input": questions,
"response": answers,
"retrieved_contexts": contexts,
"reference": ground_truths
}
# Convert dict to dataset
dataset = Dataset.from_dict(data)
참고
기존(2024년 여름 기준) 의 data dict 포맷은 다음과 같았으나, 현재는 해당 포맷을 그대로 사용하면 에러가 납니다.
data_samples = {
'question': ...
'answer': ...
'contexts' : ...
'ground_truth': ...
}
다음과 같이 최신 dict 포맷으로 고쳐 주세요.
data = {
"user_input": questions,
"response": answers,
"retrieved_contexts": contexts,
"reference": ground_truths
}
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision,
)
result = evaluate(
dataset = dataset,
metrics=[
context_precision,
context_recall,
faithfulness,
answer_relevancy,
],
)
# score 출력
print(result)
# DataFrame 생성
df = result.to_pandas()
df
실전에서는 사용자의 질문과(question), 질문에 맞는 정답(ground truth) 만 엑셀 파일 등에 미리 만들어 두고
RAG Langchain 에 입력하고, 자동으로 RAG 실행 후 RAGAS 를 적용할 수 있습니다.
RAG Langchain 을 구축한 후 RAGAS 까지 한번에 적용하는 코드는 다음과 같습니다.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4-0125-preview", temperature=0)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.messages import SystemMessage
system_prompt = SystemMessage(content=
"""당신은 전문 상담원입니다. 아래 지침에 따라 사용자의 질문에 답변을 제공하세요.
---------------------
1. 주어진 정보만 활용하여 답변을 제공하세요. 주어진 정보로 답변을 할 수 없는 경우, 정중하게 답변을 제공할 수 없다고 설명합니다.
2. 답변은 정제된 형식과 문어체로 작성하며, 친절하고 자세한 내용을 제공합니다.
---------------------
"""
)
template = (
"Below is the context information.\n"
"---------------------\n"
"{context}"
"\n---------------------\n"
"Given the context information, provide a most relevant chunk to {query}."
"If there is no title that matches, output '해당정보 존재하지 않음'."
"Do not include the title on your final output."
)
prompt = ChatPromptTemplate.from_messages([system_prompt, template])
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={"k": 10, "fetch_k": 3, "lambda_mult": 0.5},
)
chain = (
{"context": retriever, "query": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
Langchain 구축 및 RAGAS 적용에 집중하기 위해 우선 1개 데이터셋만 만들어서 실행해 봅니다.
from datasets import Dataset
questions = ["보험금의 지급사유"]
ground_truths = ["회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보험금을 지급합니다. 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다): 사망보험금 2. 보험기간 중 상해로 장해분류표(<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해상태가 되었을 때: 후유장해보험금(장해분류표에서 정한 지급률을 보험가입금액에 곱하여 산출한 금액)"]
answer 에는 질문에 따른 RAG 의 답변을,
context 에는 RAG가 질문을 생성하는 데 참고한 context (=Retreiver 가 검색해 온 Document) 를 넣어 줍니다.
answers = []
contexts = []
# Inference
for query in questions:
answers.append(chain.invoke(query))
contexts.append([docs.page_content for docs in retriever.get_relevant_documents(query)])
# To dict
data = {
"user_input": questions,
"response": answers,
"retrieved_contexts": contexts,
"reference": ground_truths
}
# Convert dict to dataset
dataset = Dataset.from_dict(data)
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision,
)
result = evaluate(
dataset = dataset,
metrics=[
context_precision,
context_recall,
faithfulness,
answer_relevancy,
],
)
# score 출력
print(result)
# DataFrame 생성
df = result.to_pandas()
df
실전에서는 미리 질문 - 답안 세트를 여러 개 만들어 엑셀파일에 정리한 뒤
자동으로 엑셀 파일을 읽어서 RAG 결과를 얻고, RAGAS 를 적용하여 효율성을 높일 수 있습니다.
import pandas as pd
# .xlsx 파일 경로 지정
file_path = 'input.xlsx'
# .xlsx 파일을 읽어서 DataFrame으로 저장
data = pd.read_excel(file_path)
# 첫 번째 행의 'question'과 'ground_truths' 열 데이터를 리스트로 저장
questions = data['questions'].tolist()
ground_truths = data['ground_truths'].tolist()
# question과 ground_truths를 새로운 DataFrame으로 결합
result = pd.DataFrame({
'questions': questions,
'ground_truths': ground_truths
})
result
# # 만약 왼쪽 정렬로 데이터를 보고 싶다면 아래 코드 이용
# # DataFrame의 각 열을 왼쪽 정렬로 스타일링
# result_styled = result.style.set_properties(**{'text-align': 'left'})
# # 결과 출력 (Jupyter 노트북 환경에서 스타일이 반영됨)
# result_styled
from tqdm import tqdm
answers = []
contexts = []
# Inference
for query in tqdm(questions):
answers.append(chain.invoke(query))
contexts.append([docs.page_content for docs in retriever.get_relevant_documents(query)])
# To dict
data = {
"user_input": questions,
"response": answers,
"retrieved_contexts": contexts,
"reference": ground_truths
}
# Convert dict to dataset
dataset = Dataset.from_dict(data)
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision,
)
result = evaluate(
dataset = dataset,
metrics=[
context_precision,
context_recall,
faithfulness,
answer_relevancy,
],
)
# score 출력
print(result)
# DataFrame 생성
df = result.to_pandas()
df.to_excel('output.xlsx', index=False)
df
3.3 실전 : N개 데이터셋에 RAGAS 적용
의 결과만 분석합니다.
주어진 context에 대한 생성된 답변의 일관성에 대한 값 (context - answer)
Faithfullness == 1 인 경우
question
: 보험금의 지급사유context
: ...제3조(보험금의 지급사유 )\n회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보\n험금을 지급합니다 .\n 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다 ): 사\n망보험금\n 2. 보험기간 중 상해로 장해분류표 (<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해\n상태가 되었을 때: 후유장해보험금 (장해분류표에서 정한 지급률을 보험가입금액에 곱하\n여 산출한 금액...answer
: 회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보험금을 지급합니다. 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다 ): 사망보험금 2. 보험기간 중 상해로 장해분류표 (<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해상태가 되었을 때: 후유장해보험금 (장해분류표에서 정한 지급률을 보험가입금액에 곱하여 산출한 금액)ground truth
: 회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보험금을 지급합니다. 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다): 사망보험금 2. 보험기간 중 상해로 장해분류표(<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해상태가 되었을 때: 후유장해보험금(장해분류표에서 정한 지급률을 보험가입금액에 곱하여 산출한 금액)Faithfullness == 0 인 경우
question
: 보험료 할증이란 무엇인가요?context
: ...(context 내에 ground truth 와 연관된 키워드조차 없음)...answer
: 해당정보 존재하지 않음ground truth
: 일반적인 경우보다 위험이 높은 피보험자가 가입하기 위한 방법의 하나로, 보험가입 후 기간이 경과함에 따라 위험의 크기 및 정도가 점차 증가하는 위험 또는 기간의 경과에 상관없이 일정한 상태를 유지하는 위험에 적용하는 방법으로 위험 정도에 따라 해당 보험료 이외에 특별보험료를 부가하는 방법입니다.ground_truth의 문장 중 contex로부터 추론할 수 있는 문장의 비율을 측정한 값 (ground truth - context)
Context Recall == 1 인 경우
Faithfullness == 1 인 경우
와 동일.Context Recall == 0 인 경우
question
: 보험가액이란 무엇인가요?context
: ...(보험가액의 설명과 상관없는 내용)...answer
: 해당정보 존재하지 않음ground truth
: 재산보험에 있어 피보험 이익을 금전으로 평가한 금액으로 보험의 목적에 발생할 수 있는 최대 손해액을 말합니다.(회사가 실제 지급하는 보험금은 보험가액을 초과할 수 없습니다)Context Precision
: contexts에 존재하는 ground_truth와 관련된 항목을 높은 순위로 잘 검색해 왔는지의 여부를 평가하는 지표 (context - question - ground truth)
Context Precision == 1 인 경우
Faithfullness == 1 인 경우
와 동일.Context Precision == 0 에 가까운 경우
Context Recall == 0 인 경우
와 동일.생성된 답변이 주어진 질문과 얼마나 관련성이 있는지에 대한 값 (answer - question)
Answer Relevance == 0.81
Faithfullness == 1 인 경우
와 동일.Answer Relevance == 0 인 경우
Faithfullness == 1 인 경우
와 동일.Context Precision
, Context Recall
점수가 낮은 경우문제 : 표 형식의 텍스트가 VectorDB에 저장될 때 검색에 적합한 형태로 저장되지 않아 context 검색 불가
개선방안 : PDF Loader 를 Fitz 등 테이블을 인식할 수 있는 라이브러리로 교체하여 가급적 Markdown 형태로 변환하여 VectorDB 에 문서를 적재한다.
문제 : Retriever 가 질문에 맞는 context 를 검색하지 못하거나 불필요한 context 를 너무 많이 가져옴 (ex : Faithfullness == 0 인 경우
)
개선방안 : Retreiver 를 개선하기 위해 옵션 조절 (top k 개 축소, threshold 상향), 프롬프트 개선
문제 : LLM이 질문의 정확한 의도를 파악하지 못하여 적합하지 않은 context 를 가져옴
question
: KB개인상해보험에 가입한 후에 재해로 인해 골절이 되었는데 보험금 청구가 가능한가요?context
: ['KB개인상해보험', '제31조(회사의 파산선고와 해지) ························································································· ··················································································································· 65\n골절발생위로금 (I) 특별약관 ··················································································································· 67\n골절발생위로금 (II) 특별약관 ················································································································· 68\n골절수술위로금 특별약관 ...]개선방안 :
Faithfullness
, Answer Relevancy
값이 낮다 : 답변이 생성되지 않았거나 이상함Context Precision
, Context Recall
값이 낮다 : Context 를 제대로 못 가져온다 -> Retriever 를 조지거나 Prompt 를 변경해 본다Answer Relevancy
만 높고 나머지 지표 값은 낮다 : context 에 정확한 답(ground truth) 가 없는데 answer 는 생성되는 경우임.문서 안에 검색할 내용이 포함되어 있고, VectorDB에 깨지지 않고 잘 적재되었는데
이상하게 RAG 답변이 부정확하게 나오거나 아예 생성되지 않는 경우가 많다. 🤔
튜토리얼에 사용한 embedding model 의 성능이 강력하게 의심되는데..
현재 회사에서는 보험 용어로 따로 학습한 embedding model 을 쓰고 있어서인지 검색 성능이 준수하다.
기회가 되면 embedding 모델별 품질 평가도 해보는 것으로...!