본문 바로가기
HRDI_AI/[인공지능] RAG 검색 성능 최적화와 최신 Retrieval 전략

4. hyde_experiment

by Toddler_AD 2025. 6. 7.

HyDE (Hypothetical Document Embedding) 실험

라이브러리 설치

이 첫 번째 코드 셀에서는 실험에 필요한 다양한 파이썬 라이브러리를 설치합니다. %pip install ... 명령을 사용하여 지정된 패키지들을 설치하거나 이미 설치되어 있는 경우 확인합니다. 설치되는 주요 라이브러리는 다음과 같습니다:

  • python-dotenv: .env 파일에 저장된 환경 변수를 쉽게 불러오기 위한 라이브러리입니다 (예: API 키 등 설정 로드).
  • pandas: 데이터 처리를 위한 라이브러리로, CSV 파일을 읽고 DataFrame 형태로 다루는 데 사용됩니다.
  • eunjeon: 은전한닢이라 불리는 한국어 형태소 분석기 (Mecab의 파이썬 래퍼)로, 한국어 문장을 형태소(단어) 단위로 나누는 데 사용됩니다.
  • rank_bm25: 정보 검색에서 자주 쓰이는 BM25 알고리즘 구현체로, 문서들 간의 유사도를 계산하여 순위를 매기는 데 사용됩니다.
  • pinecone: Pinecone이라는 벡터 데이터베이스 클라우드 서비스와 연동하기 위한 라이브러리입니다. 임베딩된 벡터를 저장하고 유사도 검색을 할 수 있습니다.
  • langchain 및 관련 패키지 (langchain-openai, langchain-core, langchain-pinecone 등): LLM(거대 언어 모델)과 기타 도구를 연결해주는 프레임워크인 LangChain과 그 OpenAI, Pinecone 통합 모듈들입니다. HyDE 파이프라인 구축에 사용됩니다.

명령을 실행하면 해당 패키지들이 설치되거나 이미 설치되어 있음을 보여줍니다. 아래 코드 셀은 이러한 라이브러리를 설치한 후, 각 패키지에 대해 "Requirement already satisfied"라는 메시지를 통해 현재 환경에 이미 설치되어 있음이 확인되고 있습니다. (일부 출력에는 sentence-transformers, scikit-learn 등의 추가 패키지도 보이는데, 이는 위 라이브러리들이 내부적으로 필요로 하는 의존 패키지들로 이미 설치되어 있음을 나타냅니다.)

%pip install python-dotenv pandas eunjeon rank_bm25 pinecone langchain langchain-openai langchain-core langchain-pinecone

이 셀의 출력은 각 라이브러리에 대해 이미 설치가 완료되어 있다는 메시지를 보여줍니다. "Requirement already satisfied"는 해당 패키지가 이미 환경에 존재함을 의미합니다. 따라서 추가 설치 없이도 필요한 라이브러리를 모두 사용할 준비가 되었다는 것을 확인할 수 있습니다:

Requirement already satisfied: python-dotenv in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (1.1.0)
Requirement already satisfied: pandas in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (2.3.0)
Requirement already satisfied: eunjeon in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (0.4.0)
Requirement already satisfied: rank_bm25 in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (0.2.2)
Requirement already satisfied: pinecone in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (7.0.2)
Requirement already satisfied: langchain in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (0.3.25)
Requirement already satisfied: langchain-openai in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (0.3.19)
Requirement already satisfied: langchain-core in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (0.3.64)
Requirement already satisfied: langchain-pinecone in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (0.2.8)
Requirement already satisfied: sentence-transformers in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (4.1.0)
Requirement already satisfied: scikit-learn in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (1.7.0)
... (이하 중략) ...

위 출력에서 볼 수 있듯이, 필요한 모든 패키지가 이미 설치 완료된 상태이며, 추가로 의존 라이브러리들도 만족되고 있음을 알 수 있습니다. 따라서 다음 단계로 넘어가면 됩니다.

환경 변수 로드

이 코드 셀에서는 앞서 설치한 python-dotenv를 사용하여 .env 파일에 저장된 환경 변수들을 불러오고, 이를 파이썬 변수로 설정합니다. 먼저 load_dotenv() 함수를 호출하여 현재 디렉토리의 .env 파일을 찾아 환경 변수들을 메모리에 로드합니다. 그런 다음 os.getenv() 함수를 통해 각 키에 해당하는 값을 가져와 변수에 저장합니다:

  • OPENAI_API_KEY: OpenAI API 키 (예: ChatGPT 등을 호출하기 위한 인증 키).
  • OPENAI_LLM_MODEL: 사용할 OpenAI LLM(대형 언어 모델)의 모델명 (예: "gpt-3.5-turbo" 등).
  • OPENAI_EMBEDDING_MODEL: 문서 임베딩에 사용할 OpenAI 임베딩 모델명 (예: "text-embedding-ada-002" 등).
  • PINECONE_API_KEY: Pinecone 벡터 데이터베이스 API 키.
  • PINECONE_INDEX_NAMEPINECONE_INDEX_REGIONPINECONE_INDEX_CLOUD 등: Pinecone에서 사용할 인덱스의 이름, 지역, 클라우드 및 기타 설정 값들. 이들은 Pinecone 벡터 DB에 이미 구성된 인덱스를 가리키는 정보입니다.
  • PINECONE_INDEX_METRICPINECONE_INDEX_DIMENSION: 벡터 유사도 계산 방식(metric)과 벡터 차원 수 등 Pinecone 인덱스의 사전 정의된 설정값입니다.

이렇게 불러온 값들은 이후 코드에서 사용될 전역 변수로 설정됩니다. 마지막으로 print("환경 변수 로딩 완료")를 통해 환경 변수 로드가 정상적으로 완료되었음을 출력합니다. 이 출력은 사용자가 .env 파일 세팅이 잘 되었는지 확인할 수 있게 해줍니다.

import os
from dotenv import load_dotenv

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_LLM_MODEL = os.getenv("OPENAI_LLM_MODEL")
OPENAI_EMBEDDING_MODEL = os.getenv("OPENAI_EMBEDDING_MODEL")
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
PINECONE_INDEX_REGION = os.getenv("PINECONE_INDEX_REGION")
PINECONE_INDEX_CLOUD = os.getenv("PINECONE_INDEX_CLOUD")
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME")
PINECONE_INDEX_METRIC = os.getenv("PINECONE_INDEX_METRIC")
PINECONE_INDEX_DIMENSION = int(os.getenv("PINECONE_INDEX_DIMENSION"))

print("환경 변수 로딩 완료")

이 셀을 실행하면 아래와 같은 출력이 나타납니다. .env 파일의 내용이 정상적으로 불러와졌다면 미리 코딩된 대로 "환경 변수 로딩 완료"라는 메시지가 표시됩니다:

환경 변수 로딩 완료

위 출력은 설정된 환경 변수를 성공적으로 가져와서 해당 파이썬 변수에 잘 담았다는 의미입니다. 이제 OpenAI API나 Pinecone 등을 사용할 준비가 되었다는 것을 확인할 수 있습니다.

데이터 로딩

이 코드 셀에서는 실험에 사용할 데이터셋을 불러옵니다. pandas 라이브러리를 통해 두 개의 CSV 파일을 읽는데, 하나는 문서 목록이고 다른 하나는 질의(query) 목록입니다. 구체적으로 수행되는 작업은 다음과 같습니다:

  • documents_df = pd.read_csv("documents.csv"): 문서 데이터가 들어 있는 CSV 파일을 읽어들여 documents_df라는 DataFrame 객체를 생성합니다. 이 DataFrame에는 각 문서의 ID와 내용 등이 열(column)로 포함되어 있을 것으로 예상됩니다 (예: doc_id, content 등).
  • queries_df = pd.read_csv("queries.csv"): 질의 데이터가 들어 있는 CSV 파일을 읽어 queries_df DataFrame으로 만듭니다. 각 질의에 고유 ID와 질의 텍스트, 그리고 관련 문서 정보(정답 문서 목록)가 들어 있을 것입니다 (예: query_id, query_text, relevant_doc_ids 등).

CSV 파일이 정상적으로 읽혔다면, 이어서 데이터의 크기를 확인하기 위해 각 DataFrame의 행 개수를 출력합니다. len(documents_df)는 문서의 개수를, len(queries_df)는 질의의 개수를 나타냅니다. 이를 print로 출력하여 데이터셋의 규모를 확인합니다.

import pandas as pd

documents_df = pd.read_csv("documents.csv")
queries_df = pd.read_csv("queries.csv")
print("문서 수:", len(documents_df))
print("질의 수:", len(queries_df))

위 코드 실행 결과, 문서와 질의의 개수가 아래처럼 출력됩니다:

문서 수: 30
질의 수: 30

이를 통해 문서 30개와 질의 30개가 데이터셋에 포함되어 있음을 알 수 있습니다. 즉, 이번 실험에서는 30개의 문서를 대상으로 30개의 질의에 대한 검색 실험을 수행하게 됩니다. 데이터가 정상적으로 로드되었으므로, 다음 단계에서 이 데이터를 활용할 수 있습니다.

BM25 기반 검색 세팅

이 셀에서는 BM25 알고리즘을 이용한 전통적인 문서 검색 환경을 설정합니다. BM25는 Bag-of-Words 기반의 정보검색 모델로서 질의와 문서 간의 단순 단어 매칭 빈도로 유사도를 평가합니다. 이를 위해 먼저 문서와 질의의 텍스트를 **토큰화(tokenization)**하는 전처리가 필요합니다. 한국어의 경우 형태소 단위로 나누는 작업이 필요하므로, Mecab 형태소 분석기를 활용합니다. 주요 동작은 다음과 같습니다:

  • Mecab() 객체 초기화: eunjeon 패키지에서 제공하는 Mecab 형태소 분석기를 초기화하여 mecab 변수에 할당합니다. 이를 통해 한국어 문장을 입력하면 명사, 동사 등의 최소 의미 단위로 분리된 단어 리스트를 얻을 수 있습니다.
  • 문서 토큰화: 리스트 컴프리헨션을 사용하여, 모든 문서 (documents_df['content']에 문서 내용들이 담겨 있다고 가정)에 대해 mecab.morphs(content)를 적용합니다. mecab.morphs() 함수는 주어진 문장을 형태소(단어)들의 리스트로 반환합니다. 이렇게 해서 tokenized_docs에는 각 문서의 단어 리스트가 순서대로 저장됩니다.
  • BM25 인덱스 생성: BM25Okapi(tokenized_docs)를 호출하여 BM25 알고리즘에 사용할 인덱스를 생성합니다. tokenized_docs를 입력으로 주면 내부적으로 각 단어의 문서 빈도 등을 계산하여 BM25 점수를 계산할 준비를 마칩니다. bm25 변수에 이 인덱스 객체가 저장됩니다.
  • BM25 검색 함수 정의 (bm25_search): 주어진 질의 문자열을 받아 상위 5개의 관련 문서를 찾아주는 함수를 정의합니다. 함수 내부에서 mecab.morphs(query)로 질의를 토큰화하고, bm25.get_scores(tokens)를 통해 해당 토큰들이 각 문서에 얼마나 잘 매치되는지 BM25 점수를 계산합니다. 그런 다음 scores 리스트를 점수 순으로 정렬하여 가장 점수가 높은 문서 5개의 인덱스를 얻습니다. 마지막으로 그 문서들의 doc_id를 반환하도록 합니다. 이렇게 하면 추후 주어진 질의에 대해 BM25 기반으로 상위 5개 문서 ID 목록을 쉽게 얻을 수 있습니다.

정리하면, 이 셀을 통해 전통적인 BM25 검색이 가능하도록 문서 말뭉치에 대한 토큰화 작업과 BM25 인덱스 준비, 그리고 검색 함수를 구현한 것입니다.

from eunjeon import Mecab
from rank_bm25 import BM25Okapi

# Mecab 형태소 분석기 초기화
mecab = Mecab()
# 문서 토큰화
tokenized_docs = [mecab.morphs(content) for content in documents_df['content']]
bm25 = BM25Okapi(tokenized_docs)

def bm25_search(query, top_k=5):
    tokens = mecab.morphs(query)
    scores = bm25.get_scores(tokens)
    ranked_idx = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
    return [documents_df['doc_id'].iloc[i] for i in ranked_idx[:top_k]]

이 셀을 실행할 때 특별한 print 출력은 없지만, 라이브러리를 불러오면서 시스템에서 경고 메시지가 하나 나타납니다. 출력에 보이는 경고는 다음과 같습니다:

c:\Users\ssampooh\RAG-Retrieval\.conda\Lib\site-packages\eunjeon\__init__.py:11: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  import pkg_resources

이 메시지는 사용 중인 eunjeon 패키지 내부에서 발생한 경고로, pkg_resources 모듈의 사용이 곧 중단(deprecated)될 예정이라는 내용입니다. 우리의 코드에 문제가 있다는 것이 아니라, 패키지 내부 구현에 관한 정보이므로 실험 수행에는 영향을 주지 않습니다. 다시 말해, BM25 인덱스 구축과 함수 정의는 정상적으로 완료되었고, 이 경고는 무시해도 되는 정보입니다. (만약 필요하다면 패키지 버전을 변경하거나 업데이트하라는 취지의 안내입니다.) 경고를 제외하면 이 셀은 성공적으로 실행되었으며, 이제 BM25를 이용한 검색을 수행할 준비가 되어 있습니다.

Dense 임베딩 검색 설정

이번 셀에서는 Dense Retrieval을 위한 환경을 설정합니다. Dense Retrieval이란 문서와 질의를 **벡터 임베딩(vector embedding)**으로 표현한 뒤, 벡터 공간에서 유사도를 계산하여 가까운 문서를 찾는 방법을 말합니다. 이를 위해 Pinecone 벡터 데이터베이스와 OpenAI 임베딩 모델을 활용합니다. 주요 동작은 다음과 같습니다:

  • Pinecone 연결 설정: Pinecone(api_key=PINECONE_API_KEY)를 호출하여 Pinecone 서비스에 연결할 객체 pc를 생성합니다. 이후 pc.Index(PINECONE_INDEX_NAME)를 통해 미리 생성된 Pinecone 인덱스에 연결합니다. 이렇게 얻은 index 객체는 해당 인덱스에 대해 검색이나 삽입 등의 작업을 할 수 있습니다. (이때 PINECONE_INDEX_NAME은 .env에서 불러온 인덱스의 이름이며, 해당 인덱스에는 이미 문서 벡터들이 사전에 저장되어 있는 것으로 보입니다.)
  • 임베딩 모델 초기화: OpenAIEmbeddings 클래스를 이용하여 embedding_model을 생성합니다. model=OPENAI_EMBEDDING_MODEL로 지정된 OpenAI의 문서 임베딩 모델 (예: ada 모델 등)을 사용하고, OpenAI API 키를 제공하여 인증합니다. 이 객체를 통해 텍스트를 전달하면 해당 문장의 벡터 표현을 얻을 수 있습니다.
  • 벡터 스토어 초기화: PineconeVectorStore를 생성하여 vector_store 변수에 저장합니다. 이 벡터 스토어는 Pinecone 인덱스(index_name으로 지정)와 임베딩 모델을 결합한 것으로, 이후 vector_store를 통해 질의문을 입력하면 자동으로 임베딩하고 Pinecone에서 유사한 벡터를 찾아주는 통합된 검색 인터페이스를 제공하게 됩니다. 한마디로, 텍스트 질의를 넣으면 Pinecone에서 가장 유사한 문서들을 편리하게 검색할 수 있는 도구가 됩니다.
  • 설정 완료 메시지 출력: 설정이 정상적으로 되었다면 print("Dense Retrieval 설정 완료")를 실행하여 사용자에게 Dense Retrieval 구성이 끝났음을 알립니다.
from pinecone import Pinecone
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore

# Pinecone 연결 (기존 인덱스 사용)
pc = Pinecone(api_key=PINECONE_API_KEY)
index = pc.Index(PINECONE_INDEX_NAME)

# 임베딩 모델 준비 (OpenAI)
embedding_model = OpenAIEmbeddings(model=OPENAI_EMBEDDING_MODEL, openai_api_key=OPENAI_API_KEY)
# Dense 벡터 스토어 초기화
vector_store = PineconeVectorStore(index_name=PINECONE_INDEX_NAME, embedding=embedding_model)

print("Dense Retrieval 설정 완료")

이 셀을 실행하면 두 가지 종류의 출력이 나타납니다. 첫 번째는 tqdm 라이브러리에서 발생한 경고이고, 두 번째는 우리가 지정한 완료 메시지입니다:

c:\Users\ssampooh\RAG-Retrieval\.conda\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update Jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Dense Retrieval 설정 완료

첫 번째 줄은 **진행 막대(progress bar)**를 표시하는 tqdm 패키지에서 Jupyter 위젯을 찾을 수 없다는 경고입니다. 이는 현재 Jupyter 환경에서 인터랙티브한 진행률 표시 기능이 없다는 것을 알려주는 것으로, 검색 기능 자체에는 영향이 없습니다. 두 번째 줄 "Dense Retrieval 설정 완료"는 우리의 코드에서 출력한 것으로, Pinecone 및 임베딩 모델을 이용한 Dense Retrieval 준비가 성공적으로 완료되었음을 알려줍니다. 이제 벡터 기반의 유사도 검색을 수행할 수 있는 환경이 갖추어졌습니다.

HyDE 파이프라인 구성

이 셀에서는 HyDE (Hypothetical Document Embedding) 접근법을 구현하기 위한 LangChain 파이프라인을 설정합니다. HyDE 방법은, 주어진 질의에 대하여 실제 문서가 아닌 **가상의 답변(hypothetical answer)**을 먼저 생성한 후, 그 답변을 질의처럼 취급하여 관련 문서를 검색하는 기법입니다. 이를 통해 질의가 짧거나 모호할 때 더 풍부한 컨텍스트를 가진 답변을 기반으로 검색함으로써 성능 향상을 기대할 수 있습니다. 여기서는 LangChain을 활용해 LLM을 호출하고 그 결과를 받아오는 일련의 작업을 파이프라인으로 구성합니다:

  • ChatOpenAI 모델 초기화: ChatOpenAI 클래스를 이용해 chat_model 객체를 생성합니다. model_name=OPENAI_LLM_MODEL로 지정된 OpenAI의 대형 언어 모델(예: GPT-3.5/4)이 사용되며, OpenAI API 키도 함께 제공됩니다. temperature=0.3은 생성되는 텍스트의 무작위성 정도를 조절하는 파라미터로, 0.3이라 약간의 다양성을 주되 너무 산만하지 않게 응답을 생성하도록 설정한 것입니다. 이 모델 객체를 통해 질문을 입력하면 답변을 생성할 수 있습니다.
  • 프롬프트 템플릿 정의: PromptTemplate을 사용하여 LLM에 전달할 질문 프롬프트 형식을 정의합니다. 여기서는 hyde_prompt라는 템플릿을 만들어 input_variables=["query"]로 질의 문자열을 받을 준비를 합니다. 템플릿 내용은 다음과 같습니다:이 프롬프트는 LLM에게 "질문: [질의]" 형태로 질의를 알려주고, 이어서 "실제 문서가 아니어도 좋으니 짧게 가상의 답변을 생성하세요:"라는 지시를 줌으로써, 주어진 질문에 대한 간략한 가상의 답변을 작성하도록 유도합니다. 즉, HyDE 개념에 맞게 질문만으로 추론한 가상의 답변을 생성해 달라는 요청입니다.
  • 질문: {query} 아래 질문에 대해 실제 문서가 아니어도 좋으니 짧게 가상의 답변을 생성하세요:
  • 출력 파서 설정: StrOutputParser()를 이용해 output_parser를 생성합니다. 이 파서는 LLM의 출력 결과를 문자열로 그대로 받는 역할을 합니다. (LangChain에서는 출력 내용을 구조화하거나 후처리할 때 OutputParser를 사용합니다. 여기서는 특별한 구조화 없이 문자열로 받으면 되므로 기본 파서를 씁니다.)
  • 체인 구성: 마지막으로 hyde_prompt | chat_model | output_parser 형태로 **파이프라인(chain)**을 구성하여 hyde_chain에 저장합니다. 이 구문은 LangChain의 기능으로, 프롬프트 -> LLM -> 출력처리의 일련의 단계를 하나로 묶은 것입니다. 이제 hyde_chain을 하나의 함수처럼 사용하여 질의를 넣으면 내부적으로 우리가 정의한 프롬프트에 질의를 채워 LLM에 전달하고, 그 결과를 받아 문자열로 반환하게 됩니다. 요약하면, hyde_chain은 "질의 -> 가상 답변 생성"의 과정을 캡슐화한 객체입니다.
  • 구성 완료 메시지 출력: HyDE 파이프라인이 정상적으로 만들어지면 print("HyDE LCEL 파이프라인 구성 완료")를 통해 사용자에게 알립니다. (여기 LCEL은 LangChain Experimental or Pipeline의 이름일 수 있으나, 맥락상 HyDE 파이프라인 구축 완료를 나타내는 것으로 보면 됩니다.)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

# ChatOpenAI 인스턴스 생성 (OpenAI LLM 모델 사용)
chat_model = ChatOpenAI(
    model_name=OPENAI_LLM_MODEL,
    openai_api_key=OPENAI_API_KEY,
    temperature=0.3
)

# HyDE용 프롬프트 템플릿 설정
hyde_prompt = PromptTemplate(
    input_variables=["query"],
    template="""
질문: {query}
아래 질문에 대해 실제 문서가 아니어도 좋으니 짧게 가상의 답변을 생성하세요:
"""
)

# 출력 파서 설정
output_parser = StrOutputParser()

# 프롬프트 -> LLM -> 출력파서를 연결하여 체인 생성
hyde_chain = hyde_prompt | chat_model | output_parser

print("HyDE LCEL 파이프라인 구성 완료")

이 셀의 실행 결과로 아래와 같은 출력이 나타납니다:

HyDE LCEL 파이프라인 구성 완료

이 메시지는 HyDE를 수행하기 위한 LangChain 파이프라인이 성공적으로 구축되었음을 의미합니다. 이제 hyde_chain을 사용하여 각 질의에 대한 가상 답변을 생성할 준비가 되었습니다. (이때까지는 실제로 답변을 생성한 것은 아니고, 어떻게 생성할지에 대한 설정만 완료된 상태입니다.)

평가 지표 계산 함수 정의

이 셀에서는 검색 결과를 평가하기 위한 평가 지표(metric) 계산 함수를 정의합니다. 정보 검색 실험에서는 검색 성능을 정량적으로 평가하기 위해 다양한 지표를 사용합니다. 대표적으로 정밀도(Precision)재현율(Recall)MRR (Mean Reciprocal Rank)MAP (Mean Average Precision) 등이 있는데, 이 코드에서는 그 중 몇 가지를 직접 계산하고 있습니다. 각 함수의 역할과 지표의 의미는 아래와 같습니다:

  • parse_relevant(relevant_str) 함수: 개별 질의에 대한 정답(관련 문서) 정보를 파싱하는 함수입니다. queries_df에는 각 질의별로 어떤 문서들이 관련 문서인지 문자열 형태("doc1=1;doc2=1;..." 등)로 저장되어 있다고 가정할 수 있습니다. 이 함수를 호출하면, 그런 문자열을 ';'로 분리하여 "doc_id=grade" 형태의 쌍으로 나눈 뒤, 각 문서 ID를 키로 하고 해당 grade(중요도 점수 혹은 관련 여부)를 값으로 하는 파이썬 사전(dict)을 반환합니다. 예를 들어 relevant_str이 "D1=1;D2=0"라면 {"D1": 1, "D2": 0} 형태로 변환됩니다. 이 실험에서는 grade 값 자체는 크게 활용하지 않고, 단지 문서 ID가 사전에 존재하는지를 통해 해당 문서의 관련 여부를 판단하게 됩니다.
  • compute_metrics(predicted, relevant_dict, k=5) 함수: 하나의 질의에 대해 검색 결과와 정답을 비교하여 여러 평가 지표를 계산합니다. predicted는 검색 시스템이 반환한 문서 ID 리스트이고, relevant_dict는 앞의 함수로 얻은 해당 질의의 정답 문서 사전입니다. k=5는 상위 5개 결과를 고려하겠다는 의미입니다 (이 실험에서는 5개 문서를 검색하므로 top-5 기준 평가). 내부에서 계산되는 지표는 다음과 같습니다:
    • Precision @ k (정밀도@k): 상위 k개 결과 중 관련 문서가 몇 개나 있는지를 나타냅니다. hits는 predicted 상위 k개 중 정답 사전에 존재하는 문서 수를 센 것입니다. Precision@5 = hits / k로 계산됩니다. 예를 들어 상위 5개 결과 중 2개가 정답 문서라면 정밀도는 2/5 = 0.4 (40%)가 됩니다. 정밀도는 검색 결과의 정확성을 나타내며, 값이 높을수록 검색결과 상위에 관련 없는 문서가 적다는 것을 의미합니다.
    • Recall @ k (재현율@k): 관련 문서들 중 상위 k개 결과 안에 포함된 비율입니다. total_relevant는 해당 질의의 전체 관련 문서 수 (사전의 길이)이고, recall = hits / total_relevant로 계산됩니다. 예를 들어 정답 문서가 10개 있는데 그 중 5개가 상위 5개 결과에 포함되었다면 재현율@5 = 5/10 = 0.5가 됩니다. 재현율은 관련 문서를 얼마나 놓치지 않고 찾았는가를 보여주며, 값이 높을수록 관련 문서를 많이 찾아냈음을 의미합니다 (단, top-k로 제한된 맥락에서의 값입니다).
    • RR (Reciprocal Rank, 역순위): 첫 번째로 등장한 관련 문서의 순위에 대한 역수 값입니다. 코드에서 rr은 초기 0으로 두고, 예측 목록을 처음부터 순회하며 관련 문서를 찾으면 1/(해당 순위)로 계산하여 저장하고 반복을 중단합니다. 예를 들어 첫 관련 문서가 검색 결과 리스트의 1번째에 있다면 RR = 1/1 = 1.0, 2번째에 있으면 RR = 1/2 = 0.5, 5번째에 있으면 1/5 = 0.2가 됩니다. 만약 관련 문서를 하나도 못 찾았다면 RR는 0으로 남습니다. Reciprocal Rank는 사용자가 원하는 정보를 얼마나 빨리(상위 몇 번째에) 찾을 수 있는가를 반영하며, 값이 높을수록 첫 관련 문서가 리스트 상위에 있음을 뜻합니다.
    • AP (Average Precision, 평균 정밀도): 검색 결과의 정밀도를 순위별로 누적 고려한 지표입니다. 구현을 보면 상위 k개 결과를 순차적으로 훑으면서, precisions 리스트에 관련 문서를 발견할 때마다의 Precision 값을 기록하고, 마지막에 그 평균을 구하고 있습니다. 구체적으로, i번째(0-indexed, 실제로는 순위 i+1번째) 결과까지 확인했을 때의 Precision (num_correct / (i+1))를 계산하여 관련 문서가 발견될 때마다 리스트에 추가합니다. 그리고 precisions의 평균을 AP로 삼습니다. 예를 들어 상위 5개 중 관련 문서가 3개 있다고 하면, 관련 문서가 나온 시점들의 Precision을 모두 더해 3으로 나누는 식입니다. AP는 랭킹 전반에 걸친 정밀도의 평균적인 가중치로 볼 수 있으며, 관련 문서를 일찍 많이 배치할수록 높은 값을 갖게 됩니다. 관련 문서가 아예 없다면 precisions 리스트가 비어 AP=0으로 처리됩니다.
    • 함수는 최종적으로 (precision, recall, rr, ap) 튜플을 반환합니다.
  • evaluate_all(results_dict, queries_df, k=5) 함수: 전체 질의에 대한 평균 지표를 계산합니다. results_dict에는 여러 질의에 대한 예측 결과(예: {qid: [doc1, doc2, ...], ...} 형태)가 들어있고, queries_df에는 각 질의의 정답 정보가 들어 있습니다. 함수 내부에서는 queries_df.iterrows()로 각 질의를 순회하며:
    • 해당 query_id에 대한 정답 문서 사전을 parse_relevant 함수로 얻습니다.
    • 예측 결과 딕셔너리에서 해당 query_id의 예측 문서 리스트를 가져옵니다.
    • 앞서 정의한 compute_metrics를 호출하여 그 질의의 (precision, recall, rr, ap)를 계산합니다.
    • 모든 질의에 대해 이를 수행하며, prec_list, rec_list, rr_list, ap_list에 각각 값을 누적합니다.
    • 반복이 끝나면 각 리스트의 평균을 구하여 'P@5', 'R@5', 'MRR', 'MAP' 키를 가진 딕셔너리를 반환합니다. 여기서 P@5는 모든 질의에 대한 Precision@5의 평균, R@5는 Recall@5의 평균이며, MRR은 Mean Reciprocal Rank (모든 RR의 평균), MAP은 Mean Average Precision (모든 AP의 평균)입니다. 이 값들은 전체 검색 성능을 요약하여 나타내는 지표들입니다.

이 셀에서는 함수 정의만 이루어지고, 바로 값을 출력하지는 않습니다. 따라서 실행해도 눈에 보이는 출력은 없습니다. 그러나 뒤이어 이 함수를 호출하여 Dense vs HyDE 성능을 비교할 것이므로, 여기서 정의한 지표들의 의미를 이해하고 넘어가는 것이 중요합니다. 요약하면, 정밀도는 검색 결과의 정확성, 재현율은 정답 문서 누락 없이 찾아냈는지에 대한 비율, MRR은 얼마나 상위에 정답을 보여주는지, MAP은 정밀도를 순위 전체에 걸쳐 고려한 종합 점수라고 이해하면 됩니다.

import numpy as np

def parse_relevant(relevant_str):
    pairs = relevant_str.split(';')
    rel_dict = { }
    for pair in pairs:
        doc_id, grade = pair.split('=')
        rel_dict[doc_id] = int(grade)
    return rel_dict

def compute_metrics(predicted, relevant_dict, k=5):
    hits = sum(1 for doc in predicted[:k] if doc in relevant_dict)
    precision = hits / k
    total_relevant = len(relevant_dict)
    recall = hits / total_relevant if total_relevant > 0 else 0
    rr = 0
    for idx, doc in enumerate(predicted):
        if doc in relevant_dict:
            rr = 1 / (idx + 1)
            break
    num_correct = 0
    precisions = []
    for i, doc in enumerate(predicted[:k]):
        if doc in relevant_dict:
            num_correct += 1
            precisions.append(num_correct / (i + 1))
    ap = np.mean(precisions) if precisions else 0
    return precision, recall, rr, ap

def evaluate_all(results_dict, queries_df, k=5):
    prec_list, rec_list, rr_list, ap_list = [], [], [], []
    for idx, row in queries_df.iterrows():
        qid = row['query_id']
        relevant = parse_relevant(row['relevant_doc_ids'])
        predicted = results_dict[qid]
        p, r, rr, ap = compute_metrics(predicted, relevant, k)
        prec_list.append(p)
        rec_list.append(r)
        rr_list.append(rr)
        ap_list.append(ap)
    return {
        'P@5': np.mean(prec_list),
        'R@5': np.mean(rec_list),
        'MRR': np.mean(rr_list),
        'MAP': np.mean(ap_list)
    }

이 셀은 출력이 없으며, 함수 정의를 끝마치는 역할을 합니다. 이제 이 함수를 이용해 곧 검색 성능을 계산해볼 것입니다.

HyDE 가상 답변 생성

이제 준비된 HyDE 체인을 사용하여 각 **질의에 대한 가상의 답변(pseudo answer)**을 생성합니다. 이 셀에서는 질의 데이터프레임 queries_df를 순회하면서 HyDE를 적용하고 결과를 저장합니다. 구체적인 동작은 다음과 같습니다:

  • hyde_pseudo = {}: 우선 빈 딕셔너리를 만들어, 각 질의의 가상 답변을 저장할 공간을 마련합니다. 키는 query_id, 값은 생성된 가상 답변 문자열이 될 것입니다.
  • for idx, row in queries_df.iterrows():: 판다스 DataFrame의 각 행을 순회하며 질의 하나씩 처리합니다. row에는 해당 행(질의)의 데이터가 Series 형태로 담기고, row['query_id']와 row['query_text']를 통해 질의 ID와 질의 내용을 얻습니다.
  • HyDE 체인 호출: pseudo_answer = hyde_chain.invoke({"query": query_text}) 부분에서, 미리 구성한 hyde_chain에 현재 질의 텍스트를 입력으로 주어 실행합니다. 앞서 체인이 PromptTemplate -> ChatOpenAI -> OutputParser로 연결돼 있으므로, 내부적으로 OpenAI LLM이 query_text에 대한 가상 답변을 생성하여 pseudo_answer 변수에 문자열로 반환하게 됩니다. 이 한 줄이 핵심으로, 실제로 OpenAI API를 호출하여 가상 답변을 얻는 부분입니다.
  • 생성된 pseudo_answer를 hyde_pseudo[qid]에 저장합니다. 이렇게 하면 각 질의 ID 별로 HyDE 답변이 딕셔너리에 누적됩니다.
  • time.sleep(0.5): 이 함수 호출은 **잠깐 대기(0.5초)**하는 것으로, OpenAI API 호출 시 Rate Limit(초당 요청 제한)을 피하기 위한 조치입니다. 30개의 질의에 대해 연속으로 API를 호출하면 제한에 걸릴 수 있으므로, 호출 사이에 0.5초의 지연을 넣어 과부하를 방지합니다.
  • 모든 질의 처리 후 print("HyDE 가상 답변 생성 완료")로 완료 메시지를 출력합니다.

이 과정을 통해 최종적으로 hyde_pseudo 딕셔너리에 30개의 질의 각각에 대한 ChatGPT가 생성한 가상의 답변이 저장됩니다. 이 답변들은 실제 문서로부터 나온 것이 아니지만, 질의를 보강하는 추가 정보로 간주되어 이후 벡터 검색에 활용될 것입니다.

import time

# 질의마다 가상 답변 생성 (HyDE 체인 사용)
hyde_pseudo = {}
for idx, row in queries_df.iterrows():
    qid = row['query_id']
    query_text = row['query_text']
    # HyDE 체인으로 가상 답변 생성
    pseudo_answer = hyde_chain.invoke({"query": query_text})
    hyde_pseudo[qid] = pseudo_answer
    time.sleep(0.5)  # API rate limit 대비 약간의 지연

print("HyDE 가상 답변 생성 완료")

실행 결과, 모든 질의에 대한 가상 답변 생성이 완료되면 아래와 같은 메시지가 출력됩니다:

HyDE 가상 답변 생성 완료

이로써 30개의 질의 각각에 대하여 ChatGPT 모델이 만들어낸 요약적인 가상 답변이 준비되었습니다. 예를 들어 "질의: OOO"에 대해 짧은 답변 문장이 생성되었을 것입니다. 이러한 가상 답변들은 이후 실제 문서 검색 단계에서 질의 대신 사용될 예정입니다. (중간 단계인 가상 답변 생성은 출력으로 해당 답변 내용을 직접 보여주지는 않지만, hyde_pseudo 변수에 잘 저장되었음을 위 메시지를 통해 알 수 있습니다.)

검색 실행 및 결과 저장

이 셀에서는 본격적인 문서 검색을 수행하고, 그 결과를 저장합니다. 앞서 우리는 두 가지 검색 방법을 준비해두었습니다: Dense 임베딩 기반 검색 (vector_store)과 HyDE를 활용한 검색 (hyde_pseudo를 이용). 이제 각 질의에 대해 이 두 가지 방법으로 검색을 수행하고 비교해볼 것입니다. 동작 과정은 다음과 같습니다:

  1. Dense Retrieval (원본 질의 기반):
    • dense_results = {}: 빈 딕셔너리를 만들어 Dense 검색 결과를 저장할 구조를 준비합니다. 키는 query_id, 값은 해당 질의의 상위 5개 문서 ID 리스트가 될 것입니다.
    • for idx, row in queries_df.iterrows():로 질의 DataFrame을 순회하며 각 질의를 처리합니다.
    • 각 질의에 대해 query_text를 가져와 vector_store.similarity_search(query_text, k=5)를 호출합니다. vector_store는 이전에 Pinecone 인덱스와 임베딩 모델이 결합된 객체로, similarity_search 메서드는 내부적으로 query_text를 임베딩하고 Pinecone에서 비슷한 벡터를 가진 상위 5개 문서를 찾아줍니다. 결과로 docs에는 상위 5개 문서에 대한 정보(문서 내용, 메타데이터 등)가 반환됩니다.
    • docs 리스트의 각 원소는 LangChain의 Document 객체일 가능성이 높으며, 여기에 문서의 메타데이터로 doc_id를 우리가 저장해 두었을 것입니다. 코드에서 [doc.metadata['doc_id'] for doc in docs] 부분이 바로 각 문서의 메타데이터에서 문서 ID를 추출하는 부분입니다. 이렇게 추출한 다섯 개의 문서 ID 리스트를 dense_results[qid]에 저장합니다.
    • 이 반복을 통해 dense_results 딕셔너리에 모든 질의에 대한 Dense 임베딩 검색 상위5 문서ID가 채워집니다.
  2. HyDE Retrieval (가상 답변 기반):
    • hyde_results = {}: 빈 딕셔너리를 만들어 HyDE 기반 검색 결과를 저장합니다.
    • for qid, pseudo in hyde_pseudo.items():로 앞서 생성한 hyde_pseudo 딕셔너리를 순회합니다. 각 항목은 질의 ID와 그에 대응하는 가상 답변 텍스트입니다.
    • 각 가상 답변 pseudo에 대해 다시 vector_store.similarity_search(pseudo, k=5)를 호출합니다. 즉, 원본 질의 대신 해당 질의의 가상 답변을 Pinecone에 질의하여 상위 5개 문서를 찾습니다. Dense 검색 절차는 동일하지만 입력 문장이 다를 뿐입니다.
    • 결과로 얻은 문서 리스트 docs에서 마찬가지로 doc.metadata['doc_id']를 추출하여 리스트로 만들고, 이를 hyde_results[qid]에 저장합니다.
    • 이 과정을 통해 hyde_results에는 HyDE 방법을 사용했을 때 각 질의별 상위5 문서 ID 목록이 저장됩니다.
  3. 완료 메시지 출력: 모든 질의에 대해 두 가지 검색이 끝나면 print("Dense 및 HyDE 검색 결과 저장 완료")를 출력하여 처리 완료를 알립니다.

요약하면, 이 셀은 **(질의 -> Pinecone 검색)**을 두 번 수행한 셈입니다: 한 번은 그냥 질의 그대로 임베딩하여 검색한 결과(dense_results), 또 한 번은 질의로 생성한 가상 답변을 임베딩하여 검색한 결과(hyde_results)를 얻었습니다. 이렇게 얻은 두 가지 결과를 이후에 성능 평가에 활용하게 됩니다.

# Dense Retrieval (원본 질의 사용)
dense_results = {}
for idx, row in queries_df.iterrows():
    qid = row['query_id']
    query_text = row['query_text']
    docs = vector_store.similarity_search(query_text, k=5)
    dense_results[qid] = [doc.metadata['doc_id'] for doc in docs]

# HyDE Retrieval (가상 답변 사용)
hyde_results = {}
for qid, pseudo in hyde_pseudo.items():
    docs = vector_store.similarity_search(pseudo, k=5)
    hyde_results[qid] = [doc.metadata['doc_id'] for doc in docs]

print("Dense 및 HyDE 검색 결과 저장 완료")

이 셀의 실행 결과로 아래와 같은 출력이 나타납니다:

Dense 및 HyDE 검색 결과 저장 완료

이 메시지는 모든 질의에 대해 두 가지 방법의 검색이 완료되고 결과가 저장되었다는 것을 의미합니다. 이제 dense_results와 hyde_results 딕셔너리에는 각각 30개의 질의에 대한 검색 상위 5개 문서 ID 리스트가 담겨 있게 됩니다. 다음 단계는 이 결과를 토대로 앞서 정의한 평가 지표를 계산하고 두 방법의 성능을 비교하는 것입니다.

검색 성능 평가

이 셀에서는 앞에서 수행한 Dense vs HyDE 검색 결과를 평가하여 그 성능 지표를 계산하고 비교합니다. 구체적으로, 이전에 정의한 evaluate_all 함수를 이용해서 두 결과에 대한 평균 Precision@5, Recall@5, MRR, MAP 값을 구하고, 이를 하나의 데이터프레임으로 정리합니다. 실행 흐름은 다음과 같습니다:

  • dense_metrics = evaluate_all(dense_results, queries_df, k=5): Dense 검색 결과에 대해 평가 지표를 계산합니다. dense_results는 각 질의별 top-5 결과, queries_df에는 정답이 들어 있으므로, 이들을 대조하여 함수가 앞서 설명한 딕셔너리를 반환합니다. 반환되는 dense_metrics는 예를 들어 {'P@5': 0.26, 'R@5': 0.91..., 'MRR': 0.98..., 'MAP': 0.95...} 와 같이 Dense 방법의 평균 정밀도@5, 재현율@5, MRR, MAP 값을 담고 있을 것입니다.
  • hyde_metrics = evaluate_all(hyde_results, queries_df, k=5): HyDE 검색 결과에 대해서도 같은 방식으로 지표를 계산합니다. 반환되는 hyde_metrics는 Dense와 동일한 키를 가지며 HyDE 방법의 성능 수치를 담고 있습니다.
  • 결과 비교를 위해 판다스 DataFrame으로 정리합니다. pd.DataFrame({...})을 사용하는데, 'Metric': dense_metrics.keys()로 행 이름(지표 이름)들을 가져오고, 'Dense': dense_metrics.values(), 'HyDE': hyde_metrics.values()로 각 지표의 값을 열로 가지는 데이터프레임을 생성합니다. 이렇게 하면 4행 3열 (Metric, Dense, HyDE) 형태의 표가 만들어집니다.
  • df를 마지막 줄에 적어서 Jupyter Notebook에서 표 형태로 출력되도록 합니다. (print(df)로 해도 되지만 그냥 변수만 놓으면 이쁘게 테이블 형태로 렌더링됩니다.)

이를 통해 Dense 대 HyDE의 평균 성능 비교표를 얻게 됩니다.

import pandas as pd

# 평가 수행 (각각 P@5, R@5, MRR, MAP 계산)
dense_metrics = evaluate_all(dense_results, queries_df, k=5)
hyde_metrics = evaluate_all(hyde_results, queries_df, k=5)

# 데이터프레임으로 결과 정리
df = pd.DataFrame({
    'Metric': dense_metrics.keys(),
    'Dense': dense_metrics.values(),
    'HyDE': hyde_metrics.values()
})
df

이 코드를 실행하면 아래와 같이 두 방법의 평가 지표를 비교한 표가 출력됩니다:

  Metric     Dense      HyDE
0    P@5  0.260000  0.280000
1    R@5  0.916667  0.961111
2    MRR  0.983333  0.973333
3    MAP  0.959444  0.931852

표의 각 행은 하나의 성능 지표를 나타내며, Dense 열은 기존 Dense 임베딩 검색의 결과, HyDE 열은 HyDE 가상 답변을 활용한 검색의 결과를 보여줍니다. 이를 해석해보면:

  • P@5 (Precision@5): Dense = 0.260000, HyDE = 0.280000으로, HyDE 쪽이 약간 더 높습니다. 수치 0.26은 26%, 0.28은 28%로 볼 수 있습니다. 이는 상위 5개 결과 중 관련 문서의 비율이 HyDE 방법에서 조금 더 높았다는 의미입니다. 즉 HyDE를 쓴 경우 검색 결과 상위권에 약간 더 많은 관련 문서가 포함되었다고 해석할 수 있습니다 (정밀도 향상).
  • R@5 (Recall@5): Dense = 0.916667 (약 91.67%), HyDE = 0.961111 (약 96.11%)로, HyDE가 더 높습니다. 두 값 모두 100%에 가깝게 높은데, HyDE는 거의 96%의 관련 문서를 top5 안에 찾은 반면 Dense는 91.7% 정도입니다. HyDE를 사용함으로써 재현율이 향상되었음을 보여주며, 이는 관련 문서를 더 놓치지 않고 찾아낸다는 뜻입니다. (30개의 질의 전체 평균이므로 HyDE 덕분에 몇몇 질의에서 추가로 관련 문서를 더 찾았다고 볼 수 있습니다.)
  • MRR (Mean Reciprocal Rank): Dense = 0.983333, HyDE = 0.973333입니다. 둘 다 매우 높은 값으로 (1.0에 가까움) 대부분의 첫 번째 관련 문서가 검색 결과 최상위에 있음을 의미합니다. Dense의 MRR이 약간 더 높다는 것은 Dense 검색에서는 거의 모든 질의에 대해 1순위에 관련 문서가 있었고, HyDE에서는 일부 질의에서 1순위가 아닌 2순위에 처음 관련 문서가 나타난 경우가 있었음을 시사합니다. 그러나 차이가 0.9833 vs 0.9733로 아주 작으므로, 1위 결과의 관련성은 두 방법 모두 우수하며 거의 비슷한 수준입니다.
  • MAP (Mean Average Precision): Dense = 0.959444, HyDE = 0.931852로, 이 역시 두 값 모두 높지만 Dense가 다소 우수합니다. MAP는 검색 결과 전체의 순위 품질을 보는 지표인데, Dense 방법의 평균 정밀도가 95.94%로 HyDE의 93.18%보다 높습니다. 이는 HyDE 방법이 전체 순위를 고려했을 때 약간의 정확도 저하가 있었음을 의미합니다. 예를 들어 HyDE에서는 관련 문서들이 15위 내에 모두 있긴 하지만 Dense에 비해 더 하위(rank 45 등)에 위치한다거나, 또는 불필요한 문서가 하나 더 포함되는 등의 영향으로 평균 정밀도가 떨어졌을 수 있습니다.

전체적으로 HyDE를 도입함으로써 Precision@5와 Recall@5는 향상되었고, MRR과 MAP는 소폭 하락하는 결과가 나타났습니다. 이는 HyDE가 더 많은 관련 문서를 top5 내에 포함하도록 도와주긴 했지만, 관련 문서들의 순위가 완전히 1순위에 오도록 하는 데에는 오히려 방해가 된 경우도 있음을 의미합니다. 한 가지 가능성은, HyDE가 생성한 가상 답변이 질의의 맥락을 풍부하게 만들어 더 많은 관련 문서를 찾게 해주었지만, 동시에 일부 노이즈(관련 없는 내용)도 섞여 미세하게 순위 품질에 영향을 준 것일 수 있습니다. 혹은 이미 Dense만으로도 충분히 성능이 높아서 HyDE의 효과가 크지 않거나, 오히려 불필요한 변화로 인한 약간의 손해가 나타났을 수도 있습니다. 그럼에도 불구하고 큰 그림에서 두 방법 모두 높은 성능을 보이며, HyDE 적용시 재현율 개선이 두드러진 것은 긍정적인 결과로 볼 수 있습니다.

검색 성능 비교 시각화

마지막으로, 위의 성능 평가 결과를 시각화하여 비교합니다. 이 셀에서는 matplotlib을 사용하여 Dense와 HyDE의 성능을 한눈에 볼 수 있는 그래프로 그립니다. 주요 내용은 다음과 같습니다:

  • 한글 폰트 설정: 그래프 축이나 제목에 한글이 포함되므로, matplotlib에서 한글이 깨지지 않도록 폰트를 설정합니다. font_manager.FontProperties를 이용해 Windows의 "맑은 고딕(malgun.ttf)" 폰트를 불러와 적용하고, 또한 plt.rcParams['axes.unicode_minus'] = False로 설정하여 마이너스 기호도 깨지지 않게 처리합니다.
  • 데이터 준비: methods = ['Dense', 'HyDE']는 방법 이름 리스트 (실제 그래프에서는 legend에 사용). metrics_list = ['P@5', 'R@5', 'MRR', 'MAP']는 x축에 표시할 지표들의 목록입니다. dense_vals와 hyde_vals에는 앞서 계산된 dense_metrics와 hyde_metrics에서 각 지표 값을 순서대로 추출하여 리스트로 만듭니다. 예를 들어 dense_vals = [0.26, 0.916667, 0.983333, 0.959444]가 될 것입니다.
  • 그래프 그리기: plt.figure(figsize=(8,5))로 그림 크기를 설정하고, plt.plot(x, dense_vals, marker='o', label='Dense')와 plt.plot(x, hyde_vals, marker='s', label='HyDE')로 두 방법의 성능을 선 그래프로 그립니다. 여기서 x = range(len(metrics_list)) 즉 x좌표는 0,1,2,3에 해당하며 각각 P@5, R@5, MRR, MAP에 대응됩니다. marker 옵션은 Dense는 원형 'o', HyDE는 사각형 's'으로 구분했습니다.
  • 축과 제목 설정: plt.xticks(x, metrics_list)로 x축 눈금을 지표 이름으로 표시하고, plt.ylim(0,1)로 y축 범위를 0부터 1까지로 설정 (모든 지표 값이 0~1 사이이므로). plt.xlabel('지표'), plt.ylabel('점수')로 축 레이블을, plt.title('Dense vs HyDE 성능 비교')로 그래프 제목을 지정합니다. plt.legend()로 범례 표시, plt.grid(True)로 그래프에 격자선을 추가하여 보기 편하게 합니다.
  • plt.show()를 호출하면 Jupyter Notebook 상에서 그래프가 출력됩니다.

이렇게 만들어진 그래프는 x축에 P@5, R@5, MRR, MAP 지표를 놓고, 두 가지 방법의 점수를 선으로 연결하여 비교한 것입니다. 이 그래프를 통해 각 지표별로 어느 방법이 우수한지 시각적으로 쉽게 파악할 수 있습니다.

from matplotlib import font_manager, rc
import matplotlib.pyplot as plt

# 한글 폰트 설정 (맑은 고딕)
font_path = "C:/Windows/Fonts/malgun.ttf"
font_prop = font_manager.FontProperties(fname=font_path).get_name()
rc('font', family=font_prop)
plt.rcParams['axes.unicode_minus'] = False

methods = ['Dense', 'HyDE']
metrics_list = ['P@5', 'R@5', 'MRR', 'MAP']
dense_vals = [dense_metrics['P@5'], dense_metrics['R@5'], dense_metrics['MRR'], dense_metrics['MAP']]
hyde_vals = [hyde_metrics['P@5'], hyde_metrics['R@5'], hyde_metrics['MRR'], hyde_metrics['MAP']]

x = range(len(metrics_list))
plt.figure(figsize=(8,5))
plt.plot(x, dense_vals, marker='o', label='Dense')
plt.plot(x, hyde_vals, marker='s', label='HyDE')
plt.xticks(x, metrics_list)
plt.ylim(0,1)
plt.xlabel('지표')
plt.ylabel('점수')
plt.title('Dense vs HyDE 성능 비교')
plt.legend()
plt.grid(True)
plt.show()

그림 1: Dense 방식과 HyDE 방식의 검색 성능 지표 비교 그래프입니다. 가로축은 네 가지 성능 지표(P@5, R@5, MRR, MAP)를 나타내고, 세로축은 해당 지표 값(0부터 1 사이)을 표시합니다. 파란색 원형 마커 선은 Dense 방법의 점수이고, 주황색 사각형 마커 선은 HyDE 방법의 점수입니다. 그래프를 보면 P@5와 R@5에서는 HyDE가 Dense보다 약간 높게 나타나 두 방법 간 큰 차이는 아니지만 HyDE 적용으로 정밀도와 재현율이 향상된 것을 알 수 있습니다. 반면 MRR과 MAP에서는 HyDE의 점수가 Dense보다 조금 낮게 나타나는데, 이는 HyDE를 사용했을 때 첫 번째 관련 문서의 순위 및 전체 평균 정밀도가 소폭 감소했음을 시사합니다. 종합하면, HyDE 기법은 관련 문서를 더 많이 찾도록 도와줘 정밀도와 재현율 측면에서 이득을 주었지만, 순위의 품질(특히 최상위 결과의 정확성 면)에서는 약간의 손해가 있음을 그래프를 통해 한눈에 확인할 수 있습니다.

'HRDI_AI > [인공지능] RAG 검색 성능 최적화와 최신 Retrieval 전략' 카테고리의 다른 글

6. contextual_compression  (1) 2025.06.07
5. cohere_rerank_experiment  (0) 2025.06.07
3. rrf_comparision  (0) 2025.06.07
2. bm25_dense_comparision  (0) 2025.06.07
1. indexing  (0) 2025.06.07