AI

[AI] RAGAS 공식 문서(docs) 파악하기

청귤파파 2024. 11. 10. 23:50

이전 포스트에서 RAGAS 논문에 대해 분석하였는데, 논문 이후에 수정된 내용은 주로 공식 문서에서 확인할 수 있기 때문에 추가적으로 어떤 것이 변경되었나 확인해보려고 한다. 문서에는 Evaluation, Metrics 이외에도 Test Data Generation, Knowledge Graph Building, Scenario Generation 등의 과정이 자세히 설명되어 있었으나, 주로 Metrics 부분에 대해 파악해보고 논문과 어떻게 달라졌는지 비교할 예정이다.

 

[Paper Review] RAGAS: Automated Evaluation of Retrieval Augmented Generation

RAG 자동 평가 중 잘 알려진 RAGAS 에 대하여 논문과 공식문서를 읽고 정리한 바를 기록해두려고 한다. 우선 첫 번째로 23년 9월에 아카이브에 올라온 RAGAS 논문에 대해 파악한 후, 최근까지 업데이

jihan819.tistory.com

1. Metrics

Metrics 은 AI applications 성능을 평가할 때 사용되는 지표이다. 주어진 테스트 데이터에 대하여 각 모듈 별로 성능이 어떻게 되는 지 평가하는 데에 쓰이는데, 정확하고 합리적인 Metrics 이 정의되어야 이를 바탕으로 모델 개발 및 배포 프로세스에 적용이 가능하다. 즉, 다양한 모델 실험에 대하여 어떤 모델이 가장 우수한 지 결정할 수 있는 지표가 되어야 하고, 이 결과를 바탕으로 다음 모델 개선 및 배포를 진행하게 된다.

다양한 유형의 Metrics

RAGAS 에서는 Metrics 을 위 그림처럼 2가지로 나누어서 설명한다.

  • LLM-based metrics: 평가를 위해 LLM 을 사용하는 Metric 이다. 이 방식은 LLM 을 사용하기 때문에 동일한 입력에 대해 다른 결과가 나올 수 있다(non deterministic). 그러나 이러한 Metric 은 사람 평가에 더 가까운 경향성을 보이는 경우가 많고, 경우에 따라서는 정답 데이터를 가지고 있지 않더라도 평가가 가능하다. Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena 방식이 등장한 이래로 자주 채택되는 평가 방식이다. RAGAS 에서는 LLM-based metrics 인 경우에 MetricWithLLM 클래스를 상속하여 LLM 객체를 인자로 받게끔 되어 있다.
  • Non-LLM-based metrics: 평가를 위해 LLM 을 사용하지 않는 Metric 이다. 생성 결과와 정답 간의 Similarity, BLEU Score 등을 주로 사용하는데, 기존에 많이 사용된 방식이지만 사람 평가와 상관 관계가 낮은 것으로 알려져있다.

평가하는 데이터의 유형에 따라 아래 2가지 카테고리로 분류할 수도 있는데,

  • Single-turn metrics: user 와 assistant 간의 single turn 대화를 평가하고, 이러한 Metric 은 SingleTurnMetric 클래스를 상속한다.
  • Multi-turn metrics: user 와 assistant 간의 multi turn 대화를 평가하고, MultiTurnMetric 클래스를 상속한다.

각 metric 별로 개념 설명과 github 구현체에서 확인 가능한 prompts(instruction), input, output 그리고 활용 예시를 작성하였다. 특히, prompts 부분은 metric 을 계산하기 위해 실제 어떻게 LLM 에게 요청하고 있는 지 확인이 가능하기 때문에 체크하면 좋을 것 같다.

1.1. Context Precision

이전 논문에서 문맥 관련성(Context Relevance) 이라는 이름으로 검색된 context 가 질문에 필요한 정보만을 포함하고 있는 지를 평가하는 metric 을 소개하였는데, 실제 구현체에서는 precision, recall 로 나누어 더 정밀하게 측정하게 된 것 같다. Context Precision 은 retrieved_contexts 에서 관련 chunk 의 비율을 측정하는 지표이다. 일반적인 모델 평가에서 생각하는 precision 개념과 거의 유사하며(모델이 예측한 결과 중 실제 맞은 케이스), 수식은 아래와 같이 정의된다.

$$\text{Context Precision@K} = \frac{\sum_{k=1}^{K} (\text{Precision@k} \times v_k)}{\text{Total number of relevant items in the top } K \text{ results}}$$

$$\text{Precision@k} = \frac{\text{true positives@k}}{(\text{true positives@k} + \text{false positives@k})}$$

여기서 $K$ 는 retrieved_contexts 에 있는 chunk 의 총 개수이며, $v_k$ 는 rank $k$ 에서의 0 또는 1의 관련성 indicator 이다.

LLM 기반 Context Precision 은 아래 prompt(instruction) 를 사용하고, question, context, answer 를 input 으로 사용하여 reason, verdict 값을 return 하는 형태이다. 활용하는 데이터에 reference 가 있는 지 없는 지에 따라 answer 값에 reference 를 넣을 수도 있고(LLMContextPrecisionWithReference), response 를 넣을 수도 있다(LLMContextPrecisionWithoutReference).

class QAC(BaseModel):
    question: str = Field(..., description="Question")
    context: str = Field(..., description="Context")
    answer: str = Field(..., description="Answer")


class Verification(BaseModel):
    reason: str = Field(..., description="Reason for verification")
    verdict: int = Field(..., description="Binary (0/1) verdict of verification")

class ContextPrecisionPrompt(PydanticPrompt[QAC, Verification]):
    instruction: str = (
            'Given question, answer and context verify if the context was useful in arriving at the given answer. Give verdict as "1" if useful and "0" if not with json output.'
        )
    input_model = QAC
    output_model = Verification
  • reference 가 있는 데이터의 활용 예시
from ragas import SingleTurnSample
from ragas.metrics import LLMContextPrecisionWithReference

context_precision = LLMContextPrecisionWithReference()

sample = SingleTurnSample(
    user_input="Where is the Eiffel Tower located?",
    reference="The Eiffel Tower is located in Paris.",
    retrieved_contexts=["The Eiffel Tower is located in Paris."], 
)

await context_precision.single_turn_ascore(sample)

1.2. Context Recall

Context Recall 은 관련 문서 중 얼마나 많은 수가 성공적으로 검색되었는 지를 측정하는 metric 이다. 이는 실제 정답 문서에서 중요한 결과들을 얼마나 놓치지 않았는 지를 확인한다. recall 은 precision 과 다르게 정답 문서에서 얼마나 놓치지 않았는 지를 판단해야 하기 때문에 항상 reference 가 필요하다.

LLM 기반 Context Recall 은 user_input, reference 그리고 retrieved_contexts 를 가지고 계산하며, 그 값은 0과 1사이가 되고 높을 수록 성능이 더 좋다. 이 지표는 reference 를 reference_contexts 대신 사용할 수 있는데, reference_contexts 는 직접 annotate 하는 데에 많은 시간과 비용이 소요되기 때문이다. 지표를 계산하기 위해 우선적으로 reference 를 여러 개의 claims 로 쪼개고, 각 claim 이 검색된 context 에 기인할 수 있는지 여부를 판단한다. 이상적인 시나리오에서, 모든 claims 는 검색된 context 에 기인할 수 있어야 한다(attributable). 수식은 아래와 같이 정의된다.

$$\text{context recall} = \frac{|\text{GT claims that can be attributed to context}|}{|\text{Number of claims in GT}|}$$

아래 구현체에서 확인하면 더욱 명확한데, question, context, answer 를 input 으로 받아 answer 를 sentence 단위로 분석하고, 각 sentence 별로 주어진 context 에 attributed 한 지 아닌 지를 분류하는 방식이다.

class QCA(BaseModel):
    question: str
    context: str
    answer: str


class ContextRecallClassification(BaseModel):
    statement: str
    reason: str
    attributed: int


class ContextRecallClassifications(BaseModel):
    classifications: t.List[ContextRecallClassification]

class ContextRecallClassificationPrompt(
    PydanticPrompt[QCA, ContextRecallClassifications]
):
    name: str = "context_recall_classification"
    instruction: str = (
        "Given a context, and an answer, analyze each sentence in the answer and classify if the sentence can be attributed to the given context or not. Use only 'Yes' (1) or 'No' (0) as a binary classification. Output json with reason."
    )
    input_model = QCA
    output_model = ContextRecallClassifications
  • 활용 예시
from ragas.dataset_schema import SingleTurnSample
from ragas.metrics import LLMContextRecall

sample = SingleTurnSample(
    user_input="Where is the Eiffel Tower located?",
    response="The Eiffel Tower is located in Paris.",
    reference="The Eiffel Tower is located in Paris.",
    retrieved_contexts=["Paris is the capital of France."], 
)

context_recall = LLMContextRecall()
await context_recall.single_turn_ascore(sample)

1.3. Context Entities Recall

Context Entities Recall 역시 논문에서는 소개되지 않았던 새로운 지표로, reference 와 retrieved_contexts 에 모두 존재하는 entity 수를 기준으로 retrieved_contexts 의 recall 을 측정하며, 이는 reference 에만 존재하는 entity 수에 상대적인 값이다. 다시 말하면, reference 에서 나온 entities 의 비율을 측정하는 것이다. entity 기반으로 측정하는 metric 이기 때문에 기존 metrics 와 다른 형태로 비교가 가능하여 유용하게 쓰일 수 있다.

이 metric 을 계산하기 위해 $GE$ 와 $CE$ 를 사용하는데, 각각 reference 에 존재하는 entity set 과 retrieved_contexts 에 존재하는 entity set 을 의미한다. 계산은 간단하게 교집합에 해당하는 entities 개수를 구하여 $GE$ 로 나눠준다.

$$\text{context entity recall} = \frac{|\text{CE} \cap \text{GE}|}{|\text{GE}|}$$

prompt 는 생각보다는 간단한 형태로 구성되어 있었다. 특히, entity 를 추출하는 prompt 는 좀 더 정교하게 정의되어야 할 것 같은데, 아래 instruction 부분을 보면 매우 간단한 형태였다. 이 부분은 custom prompt 로 변경하여 추출 조건을 task 에 맞게 명확하게 하는 것이 더 좋을 것 같다.

class EntitiesList(BaseModel):
    entities: t.List[str]


class ExtractEntitiesPrompt(PydanticPrompt[StringIO, EntitiesList]):
    name: str = "text_entity_extraction"
    instruction: str = (
        "Given a text, extract unique entities without repetition. Ensure you consider different forms or mentions of the same entity as a single entity."
    )
    input_model = StringIO
    output_model = EntitiesList
  • 활용 예시
from ragas import SingleTurnSample
from ragas.metrics import ContextEntityRecall

sample = SingleTurnSample(
    reference="The Eiffel Tower is located in Paris.",
    retrieved_contexts=["The Eiffel Tower is located in Paris."], 
)

scorer = ContextEntityRecall()

await scorer.single_turn_ascore(sample)

논문에서 소개할 때는 없던 metric 이라서 좀 더 기대했던 부분인데, 생각보다 활용도가 높지는 않을 것 같다는 생각이 든다. 더 정확히는 이 metric 의 결과를 우리가 신뢰할 수 있을까 싶은 생각이 드는데, 도메인 및 task 에 대한 고려 없이 LLM 으로 간단하게 추출한 entity 이기 때문에 reference 에서 추출된 entity 를 가지고 있는 context 가 더 정확도가 높은 context 라고 보장할 수 있는 지가 의문이다.

1.4. Noise Sensitivity

Noise Sensitivity 는 관련성이 있거나 혹은 없는 문서를 활용하여 응답할 때 시스템이 잘못된 응답을 제공하여 에러가 발생하는 빈도를 측정한다. 측정하는 점수의 범위는 0 ~ 1 값이며, 값이 낮을수록 성능이 더 좋다. 이 metric 을 측정하기 위해 생성된 응답의 각 claim 을 검토하여 실제 ground truth 에 기반하여 정확한지, 검색된 context (relevant or irrelevant) 에 기인(attributed)할 수 있는 지 여부를 체크한다. 이상적으로는 답변의 모든 claims 는 관련 retrieved_contexts 에서 검증이 되어야 한다. 수식은 아래와 같으며, 0에 가까울 수록 성능이 좋기 때문에 분자에는 incorrect claims 의 수가 들어가게 되었다.

$$\text{noise sensitivity (relevant)} = \frac{|\text{Total number of incorrect claims in response}|}{|\text{Total number of claims in the response}|}$$

이 metric 은 관련 문서와 관련 없는 문서의 noise sensitivity 를 각각 구할 수 있도록 focus 라는 파라미터를 지원한다.

# focus: t.Literal["relevant", "irrelevant"] = "relevant"
scorer = NoiseSensitivity(focus="irrelevant")
await scorer.single_turn_ascore(sample)
  • 활용 예시
from ragas.dataset_schema import SingleTurnSample
from ragas.metrics import NoiseSensitivity

sample = SingleTurnSample(
    user_input="What is the Life Insurance Corporation of India (LIC) known for?",
    response="The Life Insurance Corporation of India (LIC) is the largest insurance company in India, known for its vast portfolio of investments. LIC contributes to the financial stability of the country.",
    reference="The Life Insurance Corporation of India (LIC) is the largest insurance company in India, established in 1956 through the nationalization of the insurance industry. It is known for managing a large portfolio of investments.",
    retrieved_contexts=[
        "The Life Insurance Corporation of India (LIC) was established in 1956 following the nationalization of the insurance industry in India.",
        "LIC is the largest insurance company in India, with a vast network of policyholders and huge investments.",
        "As the largest institutional investor in India, LIC manages substantial funds, contributing to the financial stability of the country.",
        "The Indian economy is one of the fastest-growing major economies in the world, thanks to sectors like finance, technology, manufacturing etc."
    ]
)

scorer = NoiseSensitivity()
await scorer.single_turn_ascore(sample)

1.5. Response Relevancy

Response Relevancy 는 이전 논문에서 제안한 답변 관련성(Answer Relevance) 과 같은 개념의 metric 으로, 생성된 답변이 주어진 질문에 얼마나 적합한 지를 평가한다. 불완전하거나 중복된 정보를 포함하는 답변에는 낮은 점수가 부여되고, 질문과 관련성이 높은 답변일 때 더 높은 점수가 부여된다. 이 지표는 원래의 user_input(질문) 과 답변을 기반으로 한 여러 개의 생성된(reverse engineered) 질문들 간의 cosine similarity 를 계산하여 평가한다. 수식은 아래와 같다.

$$\text{answer relevancy} = \frac{1}{N} \sum_{i=1}^{N} \cos(E_{g_i}, E_o)$$

$$\text{answer relevancy} = \frac{1}{N} \sum_{i=1}^{N} \frac{E_{g_i} \cdot E_o}{|E_{g_i}| |E_o|}$$

여기서 $E_{g_i}$ 는 생성된 질문 $i$ 의 embeddings 결과이고, $E_o$ 는 원래의 user_input 질문의 embeddings 값이다. 디폴트로는 3개의 질문을 생성한다. 이 지표에서 중요한 점은 답변의 관련성에 대한 평가이기 때문에 사실성을 고려하지는 않는다는 것이며, 기본적으로 LLM 이 답변으로부터 원래 질문과 최대한 유사한 질문을 생성할 수 있어야 관련성이 높은 답변이라는 것을 가정한다.

prompt 를 살펴보면 response 를 바탕으로 question 과 noncommittal 여부를 생성하게 되어 있으며, 생성한 질문이 모호할 때(혹은 response 로부터 알 수 없을 때) noncommittal answer 라고 정의한다.

class ResponseRelevanceOutput(BaseModel):
    question: str
    noncommittal: int


class ResponseRelevanceInput(BaseModel):
    response: str


class ResponseRelevancePrompt(
    PydanticPrompt[ResponseRelevanceInput, ResponseRelevanceOutput]
):
    instruction = """Generate a question for the given answer and Identify if answer is noncommittal. Give noncommittal as 1 if the answer is noncommittal and 0 if the answer is committal. A noncommittal answer is one that is evasive, vague, or ambiguous. For example, "I don't know" or "I'm not sure" are noncommittal answers"""
    input_model = ResponseRelevanceInput
    output_model = ResponseRelevanceOutput
  • 활용 예시
from ragas import SingleTurnSample 
from ragas.metrics import ResponseRelevancy

sample = SingleTurnSample(
        user_input="When was the first super bowl?",
        response="The first superbowl was held on Jan 15, 1967",
        retrieved_contexts=[
            "The First AFL–NFL World Championship Game was an American football game played on January 15, 1967, at the Los Angeles Memorial Coliseum in Los Angeles."
        ]
    )

scorer = ResponseRelevancy()
await scorer.single_turn_ascore(sample)

이 때 ResponseRelevancy 의 인자로 embeddings 을 넘겨주는 형태로 사용할 embedding model 을 결정할 수 있다.

1.6. Faithfulness

Faithfulness 역시 기존 논문에서 신뢰성(Faithfulness) 으로 소개된 metric 이며, 논문에서 사용한 형태와 같은 모습으로 구현되어 있다. 이 지표는 (0, 1) 사이의 값을 갖고, 높을 수록 좋으며, 생성된 답변의 사실적인 일관성을 주어진 context 과 비교한다. 이를 계산하기 위해 우선적으로 생성된 답변의 claims 셋을 정의한다. 그리고 이러한 각각의 claim 에 대하여 주어진 context 와 cross-check 하여 context 로부터 추론이 가능한 지를 확인한다. 계산하는 수식은 아래와 같다.

$$\text{score} = \frac{|\text{Number of claims in the generated answer that can be inferred}|}{|\text{Total number of claims in the generated answer}|}$$

Faithfulness 는 2개의 prompts 로 나누어져 있는데, LongFormAnswerPrompt 를 사용하여 answer 를 여러개의 claims 로 분해하고, NLIStatementPrompt 를 사용하여 context 로부터 statements 가 신뢰할 수 있는 지를 판단한다. 결국 신뢰할 수 있다고 판단된 claims 의 개수를 카운트하여 위 수식대로 Faithfulness 를 계산한다.

class LongFormAnswerPrompt(PydanticPrompt[FaithfulnessStatements, SentencesSimplified]):
    instruction = "Given a question, an answer, and sentences from the answer analyze the complexity of each sentence given under 'sentences' and break down each sentence into one or more fully understandable statements while also ensuring no pronouns are used in each statement. Format the outputs in JSON."
    input_model = FaithfulnessStatements
    output_model = SentencesSimplified

class NLIStatementOutput(BaseModel):
    statements: t.List[StatementFaithfulnessAnswer]


class NLIStatementInput(BaseModel):
    context: str = Field(..., description="The context of the question")
    statements: t.List[str] = Field(..., description="The statements to judge")


class NLIStatementPrompt(PydanticPrompt[NLIStatementInput, NLIStatementOutput]):
    instruction = "Your task is to judge the faithfulness of a series of statements based on a given context. For each statement you must return verdict as 1 if the statement can be directly inferred based on the context or 0 if the statement can not be directly inferred based on the context."
    input_model = NLIStatementInput
    output_model = NLIStatementOutput

@dataclass
class Faithfulness(MetricWithLLM, SingleTurnMetric):
    name: str = "faithfulness"
    _required_columns: t.Dict[MetricType, t.Set[str]] = field(
        default_factory=lambda: {
            MetricType.SINGLE_TURN: {
                "user_input",
                "response",
                "retrieved_contexts",
            }
        }
    )
    nli_statements_message: PydanticPrompt = field(default_factory=NLIStatementPrompt)
    statement_prompt: PydanticPrompt = field(default_factory=LongFormAnswerPrompt)
  • 활용 예시
from ragas.database_schema import SingleTurnSample 
from ragas.metrics import Faithfulness

sample = SingleTurnSample(
        user_input="When was the first super bowl?",
        response="The first superbowl was held on Jan 15, 1967",
        retrieved_contexts=[
            "The First AFL–NFL World Championship Game was an American football game played on January 15, 1967, at the Los Angeles Memorial Coliseum in Los Angeles."
        ]
    )
scorer = Faithfulness()
await scorer.single_turn_ascore(sample)

2. Evaluate Using Metrics

여기에서는 huggingface hub 에 업로드 된 Amnesty QA RAG datasets 을 사용하지만, 일반적으로 평가에 사용하는 데이터셋은 직접 구축해야 하며 RAGAS 에서 제공하는 Dataset 클래스에 맞게 전처리하여야 한다. 나만의 datasets 을 구성하는 방법에 대해 간단하게 작성된 코드는 아래와 같다.

# Sample 1
sample1 = SingleTurnSample(
    user_input="What is the capital of Germany?",
    retrieved_contexts=["Berlin is the capital and largest city of Germany."],
    response="The capital of Germany is Berlin.",
    reference="Berlin",
)

# Sample 2
sample2 = SingleTurnSample(
    user_input="Who wrote 'Pride and Prejudice'?",
    retrieved_contexts=["'Pride and Prejudice' is a novel by Jane Austen."],
    response="'Pride and Prejudice' was written by Jane Austen.",
    reference="Jane Austen",
)

# Sample 3
sample3 = SingleTurnSample(
    user_input="What's the chemical formula for water?",
    retrieved_contexts=["Water has the chemical formula H2O."],
    response="The chemical formula for water is H2O.",
    reference="H2O",
)

dataset = EvaluationDataset(samples=[sample1, sample2, sample3])

이러한 방식으로 사전에 구축된 datasets 을 huggingface hub 에서 가져올 수도 있는데, 데이터를 가져와서 EvaluationDataset 객체에 로드하는 방법은 아래와 같다.

from datasets import load_dataset
from ragas import EvaluationDataset

dataset = load_dataset("explodinggradients/amnesty_qa","english_v3")
eval_dataset = EvaluationDataset.from_hf_dataset(dataset["eval"])

다음으로는 사용할 metrics 을 결정하여야 하는데, LLM 사용 metric 은 모두 MetricWithLLM 클래스를 상속하고 있기 때문에 동일한 레벨로 import 하여 활용하는 것이 가능하다. 예를 들어, LLMContextRecall, Faithfulness, FactualCorrectness, SemanticSimilarity 를 사용하고자 한다면 아래와 같이 쓸 수 있다. 여기서 llm 객체는 RAGAS 에서 제공하고 있는 다양한 LLM Wrapper 를 사용할 수 있으며, LangChain 과 연동하여 여러 LLM 중에 선택하여 사용할 수 있다.

from ragas.metrics import LLMContextRecall, Faithfulness, FactualCorrectness, SemanticSimilarity
from ragas import evaluate

metrics = [
    LLMContextRecall(llm=evaluator_llm), 
    FactualCorrectness(llm=evaluator_llm), 
    Faithfulness(llm=evaluator_llm),
    SemanticSimilarity(embeddings=evaluator_embeddings)
]
results = evaluate(dataset=eval_dataset, metrics=metrics)

df = results.to_pandas()
df.head()
  • 평가 결과 예시

Evaluate 결과