Cohere 재랭킹 실험
환경 설정 및 라이브러리 설치
이 첫 번째 코드 셀은 실험에 필요한 파이썬 패키지들을 설치하는 역할을 합니다. Jupyter 노트북의 매직 명령어 %pip install ...을 사용하여 python-dotenv, pandas, eunjeon(Mecab 형태소 분석기), rank_bm25(BM25 알고리즘 구현), pinecone(벡터 데이터베이스 클라이언트), langchain 및 관련 패키지(langchain-openai, langchain-pinecone), sentence-transformers(문장 임베딩 모델), scikit-learn(머신러닝 평가 도구), matplotlib(시각화) 그리고 Cohere API 클라이언트(cohere) 등을 설치합니다. 이 셀을 실행함으로써 이후 코드 실행에 필요한 환경이 갖추어지며, 출력 로그를 통해 각 패키지가 정상적으로 설치되었거나 이미 설치되어 있음을 확인할 수 있습니다.
%pip install python-dotenv pandas eunjeon rank_bm25 pinecone langchain langchain-openai langchain-pinecone sentence-transformers scikit-learn matplotlib cohere
이 셀을 실행한 출력 결과는 패키지 설치 로그입니다. 각 패키지에 대해 "Requirement already satisfied"라는 메시지가 나오면 이미 설치되어 있는 것이고, 없는 경우 새로 다운로드하여 설치합니다. 아래 출력에서 대부분의 패키지는 이미 설치되어 있으며, Cohere 패키지 등이 새로 설치된 것을 볼 수 있습니다. 마지막 줄에는 Cohere 패키지와 그 의존성들이 성공적으로 설치되었다는 메시지와 함께, 변경된 패키지를 사용하려면 커널 재시작이 필요할 수도 있다는 안내가 나타납니다.
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)
... (중간 설치 로그 다수 출력 생략) ...
Successfully installed cohere-5.15.0 fastavro-1.11.1 httpx-sse-0.4.0 types-requests-2.32.0.20250602
Note: you may need to restart the kernel to use updated packages.
환경 변수 로드
두 번째 코드 셀에서는 운영 체제 환경 변수들을 불러와서 코드에서 사용하기 위한 설정을 합니다. os 모듈과 dotenv 패키지의 load_dotenv 함수를 이용하여 .env 파일에 저장된 API 키 등의 환경 변수를 메모리에 로드합니다. 그런 다음 os.getenv를 통해 Cohere API 키, OpenAI API 키 및 임베딩 모델 이름, Pinecone 서비스의 API 키와 인덱스 정보(지역, 클라우드, 이름, 거리 측정 지표, 차원 수)를 변수에 할당합니다. 마지막 줄에서는 환경 변수를 성공적으로 불러왔음을 알리는 확인 메시지를 출력합니다.
import os
from dotenv import load_dotenv
load_dotenv()
COHERE_API_KEY = os.getenv("COHERE_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
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 파일로부터 변수들을 읽어온 후, **"환경 변수 로딩 완료"**라는 메시지를 출력합니다. 이는 필요한 API 키와 설정값들이 모두 정상적으로 불러와졌다는 뜻입니다. 아래는 해당 확인 메시지 출력 결과입니다.
환경 변수 로딩 완료
데이터 불러오기 및 개수 확인
이 셀에서는 실험에 사용할 데이터셋을 불러오고 그 규모를 확인합니다. pandas를 사용하여 documents.csv 파일과 queries.csv 파일을 읽어 각각 documents_df (문서 데이터프레임)와 queries_df (질의 데이터프레임)에 저장합니다. 이어서 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(f"문서 수: {len(documents_df)}")
print(f"질의 수: {len(queries_df)}")
데이터 파일을 성공적으로 불러오면 각 데이터프레임의 행 수를 세어서 문서 수와 질의 수를 출력합니다. 아래 출력에서 예시로 "문서 수: 30"과 "질의 수: 30"이라고 표시되어 있다면, 이는 불러온 말뭉치가 30개의 문서와 30개의 질의로 구성되어 있음을 의미합니다.
문서 수: 30
질의 수: 30
BM25 검색기 초기화 및 예시
이 셀에서는 BM25 알고리즘 기반의 문서 검색기를 초기화하고 예시 질의를 통해 동작을 확인합니다. 먼저 eunjeon 패키지에서 제공하는 Mecab 형태소 분석기를 불러와 한국어 토큰화를 수행합니다. BM25Okapi 클래스를 이용해 BM25 모델을 초기화하는데, 이를 위해 앞서 불러온 documents_df의 각 문서 내용(content 열)을 Mecab으로 형태소 분해하여 tokenized_docs 리스트에 저장합니다. 이렇게 얻은 토큰 리스트들을 BM25Okapi에 전달하여 BM25 인덱스를 구축합니다.
이어서 BM25 인덱스를 사용해 질의에 대한 상위 문서를 검색하는 함수 bm25_search(query, top_k=20)를 정의합니다. 이 함수는 주어진 질의를 형태소 단위로 토큰화한 후 BM25 인덱스로부터 모든 문서의 점수를 계산하고, 그 중 상위 top_k개의 문서 인덱스를 순서대로 반환합니다. 반환 시에는 문서의 고유 ID(doc_id)를 이용하여 결과를 식별합니다.
마지막으로, 예시 질의 **"제주도 관광 명소"**에 대해 bm25_search 함수를 호출하고 상위 5개의 결과 문서 ID를 출력합니다. 이를 통해 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=20):
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("BM25 예시 상위 5:", bm25_search("제주도 관광 명소", top_k=5))
예시 질의에 대한 BM25 검색 결과가 출력되어, BM25가 선택한 상위 5개 문서의 ID 목록을 확인할 수 있습니다. 아래 출력 예시를 보면 *"BM25 예시 상위 5: ['D1', 'D12', 'D2', 'D3', 'D4']"*와 같이 표시되어 있는데, 이는 질의 "제주도 관광 명소"에 대해 BM25 알고리즘이 D1, D12, D2, D3, D4 문서를 가장 관련성이 높다고 판단하여 상위 5위로 반환했음을 의미합니다. 이를 통해 BM25 검색이 제대로 동작하며, 해당 질의에 대해 어떤 문서들이 우선순위에 놓였는지 알 수 있습니다.
BM25 예시 상위 5: ['D1', 'D12', 'D2', 'D3', 'D4']
Dense 벡터 검색 설정
다음으로, Dense Retrieval라 불리는 임베딩 기반 벡터 검색을 설정하는 코드 셀입니다. Pinecone이라는 벡터 데이터베이스 서비스를 사용하여 미리 임베딩된 문서 벡터들을 검색하는 환경을 구성합니다.
우선 Pinecone Python 클라이언트를 Pinecone 클래스로부터 생성하고, 이전에 환경 변수로 설정된 API 키를 이용해 Pinecone에 연결합니다. pc.Index(PINECONE_INDEX_NAME)를 통해 미리 만들어진 인덱스에 접근하여 해당 인덱스를 사용할 준비를 합니다.
이후 OpenAI의 임베딩 모델을 활용하기 위해 OpenAIEmbeddings를 생성합니다. OPENAI_EMBEDDING_MODEL 이름을 사용하고 OpenAI API 키를 제공하여 임베딩 모델을 초기화합니다.
다음으로, Pinecone 인덱스와 OpenAI 임베딩 모델을 결합한 PineconeVectorStore 객체를 생성합니다. 이 객체는 주어진 인덱스에서 임베딩 기반의 유사도 검색을 수행할 수 있도록 해주며, vector_store.similarity_search(query, k)와 같은 메서드로 질의에 대한 상위 k개의 유사 문서를 찾을 수 있습니다.
마지막으로 **"Dense Retrieval 설정 완료"**라는 메시지를 출력하여, 임베딩 모델 및 벡터 스토어를 이용한 Dense 방식의 검색 환경 구성이 완료되었음을 알립니다.
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)
# 임베딩 모델 생성
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 설정 완료")
위 코드 실행 결과로 **"Dense Retrieval 설정 완료"**라는 문자열이 출력됩니다. 이는 Pinecone 인덱스와 임베딩 모델의 연결이 성공적으로 이루어져, 이제 Dense 임베딩을 활용한 유사도 검색을 수행할 준비가 되었음을 나타냅니다.
Dense Retrieval 설정 완료
평가 지표 계산 함수 정의
이번 셀에서는 검색 성능을 평가하기 위한 지표 계산용 함수들을 정의합니다. 출력은 없지만, 이후 단계에서 BM25, Dense, Cohere 방법의 성능을 정량적으로 비교하기 위해 필요한 함수들입니다:
- NumPy 불러오기: import numpy as np로 수치 계산에 사용할 NumPy 라이브러리를 불러옵니다.
- parse_relevant(relevant_str): 질의 데이터프레임 내의 정답(relevant) 문서 정보를 파싱하는 함수입니다. 각 질의에는 정답 문서 ID와 관련도 점수 쌍이 "doc_id=grade" 형태로 문자열로 저장되어 있는데, 이를 세미콜론(;)으로 구분해 개별 쌍으로 나눈 뒤 '='로 다시 나눠 {문서ID: 등급}의 딕셔너리로 변환합니다. 등급은 정수로 변환하여 저장합니다.
- compute_metrics(predicted, relevant_dict, k=5): 검색 결과에 대한 평가 지표를 계산하는 함수입니다. predicted는 한 질의에 대해 모델이 반환한 문서 ID 리스트이고, relevant_dict는 해당 질의의 정답 문서 딕셔너리입니다. 이 함수는 다음을 계산합니다:
- hits: 상위 k (k=5)개의 예측 목록 중 정답 문서에 해당하는 것이 몇 개인지 (히트 수)를 셉니다.
- 정밀도(Precision@5) precision: hits를 k로 나눈 값입니다. 즉, 모델이 반환한 상위 5개 중 몇 퍼센트가 실제 정답인지 나타냅니다.
- 해당 질의의 전체 정답 문서 개수 total_relevant를 구하고, 재현율(Recall@5) recall을 계산합니다. recall은 hits를 total_relevant로 나눈 값이며, 해당 질의의 정답 문서 중 얼마나 많은 비율이 상위 5개 안에 포함되었는지를 뜻합니다 (정답 문서가 없다면 0으로 처리).
- RR (Reciprocal Rank) rr: 첫 번째 정답 문서의 순위에 대한 역수입니다. predicted 리스트의 앞에서부터 순서대로 정답 문서를 찾고, 첫 정답 문서의 위치가 $i$번째라면 RR = 1/(i)로 계산합니다 (순위는 1부터 시작하며, 정답이 없으면 0).
- AP (Average Precision) ap: 상위 k 문서에서 정답 문서를 찾을 때마다의 정밀도를 누적하여 평균낸 값입니다. 코드에서는 상위 5개를 순회하면서 정답이 발견될 때마다 현재까지의 정밀도(정답 누적 수 / 현재 순위)를 precisions 리스트에 추가하고, 끝나면 그 평균을 AP로 계산합니다. 만약 상위 5개 내에 정답이 전혀 없다면 AP는 0이 됩니다.
- 함수는 최종적으로 (precision, recall, rr, ap) 튜플을 반환합니다. 이는 개별 질의에 대한 P@5, R@5, RR, AP 값입니다.
- evaluate_all(results_dict, queries_df, k=5): 여러 질의에 대한 평균 지표를 계산하는 함수입니다. results_dict에는 각 질의 ID에 대한 모델의 예측 결과 리스트(문서 ID들이 들어있는 리스트)가 저장되어 있다고 가정합니다. 이 함수는 queries_df의 각 질의에 대해:
- 해당 질의 ID와 질의에 대응되는 정답 문서 문자열을 가져와 parse_relevant로 딕셔너리로 변환합니다.
- results_dict에서 해당 질의의 예측 결과 리스트를 받아옵니다.
- 앞서 정의한 compute_metrics를 호출하여 그 질의의 (precision, recall, rr, ap)를 계산합니다.
- 모든 질의에 대해 계산된 precision, recall, rr, ap 값들을 각각 리스트 (prec_list, rec_list, rr_list, ap_list)에 누적합니다.
- 루프가 끝나면 이들 리스트의 평균을 내어 {'P@5': ..., 'R@5': ..., 'MRR': ..., 'MAP': ...} 딕셔너리를 반환합니다. 여기서 MRR은 rr_list의 평균 (Mean Reciprocal Rank), MAP은 ap_list의 평균 (Mean Average Precision)을 의미합니다.
이 셀은 함수 정의만을 포함하고 있기 때문에 별도의 출력은 없습니다. 이후 단계에서 이 함수들을 호출하여 세 가지 검색 방법(BM25, Dense, Cohere)의 성능 지표를 계산하게 됩니다.
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)
}
후보 문서 집합 생성 (BM25 & Dense)
이 셀에서는 앞서 구축한 BM25와 Dense 검색기를 활용하여 질의별 후보 문서 목록을 생성합니다. 각 질의에 대해 BM25와 Dense가 반환하는 상위 20개 문서를 수집하여 나중에 Cohere 모델로 재랭킹할 기반을 마련합니다:
- BM25 후보 생성: bm25_candidates = {} 딕셔너리를 만들고, queries_df의 모든 질의를 순회하면서 각 질의 텍스트에 대해 bm25_search(query_text, top_k=20)을 호출합니다. 반환된 상위 20개 문서 ID 리스트를 bm25_candidates[qid]에 저장합니다 (qid는 질의 ID).
- Dense 후보 생성: 비슷한 방식으로 dense_candidates = {} 딕셔너리를 만들고 각 질의마다 vector_store.similarity_search(query_text, k=20)를 호출합니다. 이 함수는 Pinecone 벡터 DB에서 주어진 질의와 임베딩 공간에서 가장 가까운 20개 문서를 찾아줍니다. 그 결과는 문서 객체 리스트로 반환되므로, 각 객체의 메타데이터에서 문서 ID를 추출하여 리스트를 만들고 dense_candidates[qid]에 저장합니다.
두 방식 모두 루프를 마친 후, **"BM25 & Dense 후보 생성 완료"**라는 메시지를 출력하여 모든 질의에 대한 상위 20개 후보 생성이 완료되었음을 알립니다. 이 단계까지 얻어진 bm25_candidates와 dense_candidates 딕셔너리에는 이후 Cohere 재랭킹에 사용할 후보 문서 ID들이 들어 있습니다.
# BM25 상위 20 후보 생성
bm25_candidates = {}
for idx, row in queries_df.iterrows():
qid = row['query_id']
query_text = row['query_text']
bm25_candidates[qid] = bm25_search(query_text, top_k=20)
# Dense Retrieval 상위 20 후보 생성
dense_candidates = {}
for idx, row in queries_df.iterrows():
qid = row['query_id']
query_text = row['query_text']
docs = vector_store.similarity_search(query_text, k=20)
dense_candidates[qid] = [doc.metadata['doc_id'] for doc in docs]
print("BM25 & Dense 후보 생성 완료")
위 셀을 실행하면 **"BM25 & Dense 후보 생성 완료"**라는 문구가 출력됩니다. 이는 모든 질의에 대해 BM25와 Dense 방법으로 각각 상위 20개의 후보 문서 리스트를 성공적으로 생성했음을 의미합니다. 이 출력이 나오면 다음 단계인 Cohere 모델을 통한 재랭킹을 수행할 준비가 된 것입니다.
BM25 & Dense 후보 생성 완료
Cohere 모델을 사용한 재랭킹
이 셀에서는 앞 단계에서 확보한 BM25와 Dense 후보 문서들을 결합하여 Cohere의 재랭킹 모델로 최종 순위를 매기는 작업을 수행합니다.
- 먼저 Cohere의 Python SDK를 사용하기 위해 cohere 모듈을 임포트하고, API 호출 제한을 다루기 위한 time 모듈과 예외 클래스 TooManyRequestsError를 불러옵니다.
- 환경 변수에 저장된 Cohere API 키로 co = cohere.Client(COHERE_API_KEY)를 초기화하여 Cohere API 클라이언트를 생성합니다.
- cohere_rerank(query, candidate_ids) 함수 정의: 이 함수는 질의 문자열과 해당 질의에 대한 후보 문서 ID 리스트를 입력으로 받아, Cohere의 재랭킹 모델을 사용하여 가장 관련도 높은 순서로 문서 ID 리스트를 재정렬합니다. 구체적인 동작은 다음과 같습니다:
- candidate_ids의 각 문서 ID에 대해 원문 내용을 documents_df에서 찾아 texts 리스트에 모읍니다. (즉, 후보 문서들의 실제 텍스트를 준비)
- co.rerank() 메서드를 호출하여 Cohere의 사전 학습된 다국어 재랭킹 모델 ('rerank-multilingual-v3.0')에 질의와 후보 문서들의 텍스트를 전달합니다. Cohere API는 각 문서에 대해 relevance_score를 반환합니다.
- response.results에 담긴 문서들과 점수들을 relevance_score 기준 내림차순으로 정렬하고, 정렬된 순서대로 원래 candidate_ids의 문서 ID를 재배열하여 반환합니다.
- 만약 Cohere API 호출이 너무 빈번하여 TooManyRequestsError (HTTP 429) 오류가 발생하면, 예외를 잡아 10초간 대기(time.sleep(10))한 후 동일한 요청을 한 번 더 시도합니다. (재시도 시에도 같은 오류가 나올 경우 추가 처리는 없지만, 주어진 제한 내에서는 대부분 한 번 대기로 해결됨)
- 재랭킹 실행: 빈 딕셔너리 rerank_results = {}를 만들고, queries_df의 각 질의를 순회하며 다음을 수행합니다:
- 해당 qid의 BM25 후보와 Dense 후보 리스트를 결합합니다. 여기서는 bm25_candidates[qid] + dense_candidates[qid]로 리스트를 합친 뒤 dict.fromkeys(...).keys()를 이용하여 중복을 제거하면서 원래 순서를 유지합니다. (이 트릭으로 후보들의 중복 문서 ID는 하나로 정리됨)
- query_text와 결합된 후보 ID 리스트를 cohere_rerank(query_text, candidates)에 전달하여 Cohere 모델이 문서들을 재정렬하도록 합니다.
- Cohere 모델이 반환한 정렬된 문서 ID 리스트 중 상위 5개만 취하여 rerank_results[qid]에 저장합니다. (재랭킹 결과를 5개로 제한)
- API 속도 제한을 준수하기 위해 각 질의 처리 후 time.sleep(6)으로 6초씩 지연시켜 줍니다 (분당 최대 10회 호출).
- 모든 질의에 대한 재랭킹이 끝나면 **"Cohere Reranking 완료 (상위 5 저장됨)"**이라는 메시지를 출력합니다. 이로써 각 질의에 대해 Cohere 모델이 선정한 최종 상위 5개 문서가 rerank_results에 저장되었습니다.
import cohere
import time
from cohere import TooManyRequestsError
# Cohere 클라이언트 초기화
co = cohere.Client(COHERE_API_KEY)
def cohere_rerank(query, candidate_ids):
texts = [
documents_df.loc[documents_df['doc_id'] == cid, 'content'].values[0]
for cid in candidate_ids
]
try:
response = co.rerank(
model='rerank-multilingual-v3.0',
query=query,
documents=texts
)
ranked = sorted(response.results, key=lambda x: x.relevance_score, reverse=True)
return [candidate_ids[r.index] for r in ranked]
except TooManyRequestsError:
# 429 에러 발생 시 잠깐 대기 후 재시도
print("TooManyRequestsError 발생, 10초 대기 후 재시도합니다.")
time.sleep(10)
response = co.rerank(
model='rerank-multilingual-v3.0',
query=query,
documents=texts
)
ranked = sorted(response.results, key=lambda x: x.relevance_score, reverse=True)
return [candidate_ids[r.index] for r in ranked]
# Reranked results 저장 (상위 5)
rerank_results = {}
for idx, row in queries_df.iterrows():
qid = row['query_id']
candidates = list(dict.fromkeys(bm25_candidates[qid] + dense_candidates[qid]))
query_text = row['query_text']
rerank_results[qid] = cohere_rerank(query_text, candidates)[:5]
time.sleep(6) # 호출 간 최소 6초 대기 → 분당 약 10회 이하로 제한
print("Cohere Reranking 완료 (상위 5 저장됨)")
모든 질의에 대한 Cohere 재랭킹이 완료되면 아래와 같이 **"Cohere Reranking 완료 (상위 5 저장됨)"**이라는 메시지가 출력됩니다. 이는 Cohere 모델을 통해 각 질의마다 최종적으로 선택된 5개의 문서 리스트가 성공적으로 확보되었음을 의미합니다. 이제 이 결과를 이용하여 성능 평가를 수행할 수 있습니다.
Cohere Reranking 완료 (상위 5 저장됨)
검색 성능 평가 결과 (표)
이 셀에서는 앞서 정의한 평가 함수를 활용하여 BM25, Dense, Cohere 세 가지 방법의 검색 성능 지표를 계산하고 비교합니다.
우선 각 방법별로 상위 5개 결과만을 고려하도록 결과 딕셔너리를 준비합니다:
- bm25_results_5는 bm25_candidates에서 각 질의별 상위 5개 문서만 취한 딕셔너리입니다.
- dense_results_5는 dense_candidates에서 상위 5개만 취한 딕셔너리입니다. (Cohere의 경우 이미 rerank_results가 상위 5개로 구성되어 있으므로 그대로 사용합니다.)
그 다음, evaluate_all 함수를 이용해 각 딕셔너리에 대한 평균 Precision@5 (P@5), Recall@5 (R@5), Mean Reciprocal Rank (MRR), Mean Average Precision (MAP) 값을 계산합니다:
- bm25_metrics = evaluate_all(bm25_results_5, queries_df, k=5)
- dense_metrics = evaluate_all(dense_results_5, queries_df, k=5)
- rerank_metrics = evaluate_all(rerank_results, queries_df, k=5)
이렇게 얻은 세 개의 사전에는 각 메트릭 이름별로 해당 평균값이 들어 있습니다. 이어서 이 결과를 표 형태로 보기 위해 pd.DataFrame을 생성합니다. DataFrame df_metrics는 Metric 이름을 행(index)으로, BM25, Dense, Cohere를 열로 하여 위에서 구한 값들을 배치합니다. 마지막 줄에 df_metrics를 입력하여 이 DataFrame을 출력하면, 각 방법의 성능 지표를 한눈에 비교할 수 있는 표가 표시됩니다.
import pandas as pd
# BM25 상위 5, Dense 상위 5
bm25_results_5 = {qid: lst[:5] for qid, lst in bm25_candidates.items()}
dense_results_5 = {qid: lst[:5] for qid, lst in dense_candidates.items()}
# 평가
bm25_metrics = evaluate_all(bm25_results_5, queries_df, k=5)
dense_metrics = evaluate_all(dense_results_5, queries_df, k=5)
rerank_metrics = evaluate_all(rerank_results, queries_df, k=5)
df_metrics = pd.DataFrame({
'Metric': ['P@5', 'R@5', 'MRR', 'MAP'],
'BM25': [bm25_metrics['P@5'], bm25_metrics['R@5'], bm25_metrics['MRR'], bm25_metrics['MAP']],
'Dense': [dense_metrics['P@5'], dense_metrics['R@5'], dense_metrics['MRR'], dense_metrics['MAP']],
'Cohere': [rerank_metrics['P@5'], rerank_metrics['R@5'], rerank_metrics['MRR'], rerank_metrics['MAP']]
})
df_metrics
위 코드 실행 결과로 BM25, Dense, Cohere 방법의 주요 성능 지표를 정리한 표가 출력됩니다. 표의 각 행은 평가 지표를 나타내며, 열은 방법별 점수를 보여줍니다. 아래는 해당 출력 표의 내용이며, 각 지표의 의미와 수치를 분석하면 다음과 같습니다:
- P@5 (Precision@5): 상위 5개 결과 중 실제 정답 문서의 비율을 의미합니다. 값이 1.0에 가까울수록 상위 결과가 모두 정답인 이상적인 상황입니다. 표에서 BM25의 P@5는 약 0.2533 (25.33%), Dense는 0.2600 (26.00%), Cohere 역시 0.2600 (26.00%)입니다. 이는 BM25는 상위 5개 중 약 1.27개가 정답인 반면, Dense와 Cohere는 5개 중 평균 1.3개 정도가 정답임을 의미합니다. Dense와 Cohere의 정밀도가 거의 같고 BM25보다 약간 높아, 임베딩 기반 방법들이 BM25보다 상위 결과의 정확성이 높음을 알 수 있습니다.
- R@5 (Recall@5): 각 질의에 대해 정답 문서들 중 상위 5위 내에 포함된 비율입니다. BM25의 R@5는 0.8944 (89.44%), Dense는 0.9167 (91.67%), Cohere는 0.9222 (92.22%)로 모두 높은 편입니다. 이는 대부분의 질의에서 정답 문서들이 상위 5개 내에 존재함을 보여줍니다. 특히 Cohere가 가장 높은 재현율을 보이는데, Cohere 재랭킹이 Dense보다 약간 더 많은 정답을 상위권에 포함시켰음을 의미합니다 (예: 어떤 질의에서 Dense는 한 개의 정답을 5위 밖에 놓쳤지만 Cohere는 잡아냈을 수 있음).
- MRR (Mean Reciprocal Rank): 첫 번째 정답 문서의 순위에 대한 평균 역수입니다. 이 값이 1에 가까울수록 모든 질의에 대해 첫 번째 결과가 곧 정답임을 뜻합니다. BM25의 MRR은 0.9444, Dense는 0.9833, Cohere는 0.9667입니다. 세 방법 모두 첫 정답이 매우 상위에 위치하지만, Dense의 MRR이 0.9833으로 가장 높아 거의 모든 질의에서 1위 결과가 정답임을 보여줍니다. Cohere의 MRR은 0.9667로 Dense보다 조금 낮은데, 이는 Cohere 재랭킹으로 인해 일부 질의에서 정답이 1위가 아닌 2위나 그 이하로 밀린 경우가 있었음을 시사합니다. BM25의 MRR이 0.94인 것과 비교하면 임베딩을 사용한 Dense 방법이 초기 순위에서 정답을 맨 위로 올리는 데 특히 효과적임을 알 수 있습니다.
- MAP (Mean Average Precision): 평균 정밀도를 모든 질의에 대해 평균낸 값입니다. 검색 결과의 전반적인 순위 품질을 나타내는 지표로, 값이 높을수록 관련 문서를 순서대로 잘 배치했음을 의미합니다. BM25의 MAP은 0.937963, Dense는 0.959444, Cohere는 0.962407으로 나타났습니다. Cohere의 MAP이 가장 높으며 Dense가 근소하게 그 뒤를 따르고, BM25는 두 방법보다 낮습니다. 이는 Cohere 재랭킹이 Dense 대비 전체 순위의 정밀도를 약간 향상시켰음을 보여줍니다. 예를 들어, Dense 방법에서는 일부 정답 문서가 5위 내에 있더라도 순위가 조금 낮았던 것을 Cohere가 좀 더 상위로 재배치하여 평균 정밀도를 개선한 것으로 해석할 수 있습니다.
종합하면, BM25 기반 검색보다 Dense 임베딩 기반 검색의 성능이 전반적으로 우수하며, Cohere 모델을 사용한 재랭킹은 Dense 검색 결과를 약간 더 향상시켜 특히 Recall과 MAP 측면에서 가장 좋은 성능을 달성하였습니다. 다만 MRR의 경우 Dense가 가장 높게 나타나, 첫 번째 결과의 정확도 면에서는 Dense도 이미 매우 뛰어난 성능을 보여줍니다. 전체적인 추세는 전문 검색에서 전통적인 BM25에 비해 신경망 임베딩과 재랭킹 기법이 유의미한 성능 향상을 가져온다는 것을 시사합니다.
Metric BM25 Dense Cohere
0 P@5 0.253333 0.260000 0.260000
1 R@5 0.894444 0.916667 0.922222
2 MRR 0.944444 0.983333 0.966667
3 MAP 0.937963 0.959444 0.962407
검색 성능 비교 시각화
마지막 셀에서는 위에서 계산한 성능 지표들을 시각적으로 비교하기 위해 그래프를 그립니다. matplotlib를 사용하여 라인 차트를 생성하며, 한국어 텍스트가 그래프에 포함되므로 한글 폰트 설정을 진행합니다.
우선 Windows 환경에 설치된 malgun.ttf (말굽고딕 폰트)를 지정하여 한글 폰트를 설정하고, plt.rcParams['axes.unicode_minus'] = False로 그래프에서 음수 기호가 깨지는 것을 방지합니다.
그 다음, methods 리스트에 방법 이름들('BM25', 'Dense', 'Cohere'), metrics_list에 지표 이름들('P@5', 'R@5', 'MRR', 'MAP')을 정의합니다. 앞서 계산된 bm25_metrics, dense_metrics, rerank_metrics 딕셔너리에서 각 지표 값을 추출하여 bm25_vals, dense_vals, cohere_vals 리스트에 순서대로 저장합니다. 예를 들어 bm25_vals = [bm25_metrics['P@5'], bm25_metrics['R@5'], ...] 형태로 각 메트릭의 값을 가져옵니다.
plt.figure(figsize=(8,5))로 그래프 크기를 설정한 뒤, plt.plot을 이용해 지표 인덱스 (x = range(len(metrics_list))로 0,1,2,3에 해당) 대비 각 방법의 점수 리스트를 그립니다. 각각 다른 마커(marker)를 사용하여:
- BM25 점수는 원형 마커 o로 표시,
- Dense 점수는 사각형 마커 s,
- Cohere 점수는 삼각형 마커 ^로 표시하고, 각 선에 레이블(label)을 붙여 범례에 나타나도록 합니다.
plt.xticks(x, metrics_list)로 x축 눈금을 'P@5', 'R@5', 'MRR', 'MAP'으로 설정하고, plt.ylim(0,1)로 y축 범위를 0부터 1까지로 제한하여 지표 값들의 범위에 맞춥니다. 또한 x축 라벨을 "지표", y축 라벨을 "점수"로 지정하고, 제목을 "BM25 vs Dense vs Cohere Reranking 성능 비교"로 달았습니다. 범례(plt.legend())와 격자(plt.grid(True))를 추가하여 그래프를 더 읽기 쉽게 한 후, plt.show()를 호출하여 그래프를 출력합니다.
이 셀은 시각화된 그래프를 출력할 뿐 별도의 텍스트를 출력하지는 않습니다. 그래프를 통해 앞서 표로 본 성능 수치를 한눈에 비교할 수 있습니다.
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 = ['BM25', 'Dense', 'Cohere']
metrics_list = ['P@5', 'R@5', 'MRR', 'MAP']
bm25_vals = [bm25_metrics['P@5'], bm25_metrics['R@5'], bm25_metrics['MRR'], bm25_metrics['MAP']]
dense_vals = [dense_metrics['P@5'], dense_metrics['R@5'], dense_metrics['MRR'], dense_metrics['MAP']]
cohere_vals = [rerank_metrics['P@5'], rerank_metrics['R@5'], rerank_metrics['MRR'], rerank_metrics['MAP']]
x = range(len(metrics_list))
plt.figure(figsize=(8,5))
plt.plot(x, bm25_vals, marker='o', label='BM25')
plt.plot(x, dense_vals, marker='s', label='Dense')
plt.plot(x, cohere_vals, marker='^', label='Cohere')
plt.xticks(x, metrics_list)
plt.ylim(0,1)
plt.xlabel('지표')
plt.ylabel('점수')
plt.title('BM25 vs Dense vs Cohere Reranking 성능 비교')
plt.legend()
plt.grid(True)
plt.show()
위 그래프는 앞서 표로 제시된 P@5, R@5, MRR, MAP 지표에서 BM25, Dense, Cohere 세 가지 방법의 성능을 시각화한 것입니다. 가로축은 평가 지표 종류를 나타내고 세로축은 해당 지표 값(0부터 1 사이의 점수)을 표현합니다. 각각의 선은 하나의 검색 방법을 의미하며, 원형 마커(●)는 BM25, 사각형 마커(■)는 Dense, 삼각형 마커(▲)는 Cohere 재랭킹을 나타냅니다.
그래프를 통해 BM25의 성능이 다른 방법들에 비해 전반적으로 낮음을 한눈에 알 수 있습니다. BM25의 선은 전체적으로 아래쪽에 위치하여, 모든 지표(P@5, R@5, MRR, MAP)에서 점수가 더 낮게 나타납니다. 반면 Dense와 Cohere의 선은 상단에 가깝게 몰려 있으며 서로 비슷한 수준을 보여줍니다. 두 방법 모두 높은 정밀도와 재현율을 보이고, 특히 MRR 지표에서는 Dense가 약간 더 높고, Recall과 MAP 지표에서는 Cohere가 약간 더 높은 차이를 그래프에서 확인할 수 있습니다.
즉, Dense 임베딩 기반 검색과 Cohere 재랭킹 방법 모두 BM25보다 우수한 성능을 보이며, Cohere 재랭킹은 Dense 대비 일부 지표(R@5, MAP)에서 소폭 개선된 결과를 나타냅니다. 이 시각화는 표에 제시된 수치를 그래프로 나타냄으로써 각 방법의 상대적인 성능 격차를 직관적으로 파악할 수 있게 해줍니다.
'HRDI_AI > [인공지능] RAG 검색 성능 최적화와 최신 Retrieval 전략' 카테고리의 다른 글
| 7_1. indexing_with_metadata (2) | 2025.06.07 |
|---|---|
| 6. contextual_compression (1) | 2025.06.07 |
| 4. hyde_experiment (1) | 2025.06.07 |
| 3. rrf_comparision (0) | 2025.06.07 |
| 2. bm25_dense_comparision (0) | 2025.06.07 |