컨텍스트 압축(Contextual Compression)
이 문서는 주어진 Jupyter 노트북 (06.contextual_compression.ipynb)의 코드와 출력 결과를 학부생 수준에서 이해하기 쉽게 해설한 것입니다. 각 코드 셀의 목적과 동작, 출력 의미, 그리고 실험에 사용된 개념(예: 컨텍스트 압축, 사용된 모델 등)과 성능 지표(정밀도, 재현율 등)에 대해 친절하고 자세하게 설명합니다. 설명은 코드나 출력 위에 위치하며, 코드는 원본을 그대로 유지하였습니다.
먼저 이 노트북의 전체 흐름을 간략히 살펴보면 다음과 같습니다:
- 환경 설정: 필요한 라이브러리를 설치하고 OpenAI 및 Pinecone API 키 등 환경 변수를 불러옵니다.
- 데이터 로드: 문서와 질의 데이터를 불러오고 개수를 확인합니다. (예: 30개의 문서와 30개의 질의)
- LLM 요약 체인 구성: GPT-4 모델을 활용한 LangChain 기반 체인을 설정하여 문서를 요약(압축)하는 준비를 합니다.
- 문서 컨텍스트 압축: 각 문서를 GPT-4를 통해 요약하여 압축된 문서를 생성하고 데이터프레임에 저장합니다. 이것이 컨텍스트 압축의 핵심 과정입니다.
- 임베딩 생성: OpenAI 임베딩 모델로 압축된 문서들을 벡터로 변환합니다 (문서 임베딩).
- 벡터 DB 저장: 생성한 문서 벡터들을 Pinecone 벡터 데이터베이스에 저장(upsert)하여 빠른 유사도 검색이 가능하도록 합니다.
- 질의 임베딩 및 유사도 검색: 사용자 질의를 임베딩 벡터로 변환하고, 기존 문서 벡터들과 코사인 유사도를 계산하거나 Pinecone에서 유사한 문서를 검색하여 가장 관련성이 높은 문서를 찾습니다.
- 검색 성능 평가: 검색 결과를 실제 정답과 비교하여 정밀도(precision)와 재현율(recall) 등의 성능 지표를 계산합니다. 각 질의별로 Precision@K, Recall@K 등을 구하여 검색 시스템의 성능을 평가합니다.
이제 각 단계별로 코드와 출력을 자세히 살펴보겠습니다.
코드 셀 1: 필요한 라이브러리 설치
이 셀은 노트북 실행에 필요한 파이썬 패키지들을 설치합니다. !pip install 명령어를 사용하여 python-dotenv, pandas, pinecone, langchain, langchain-openai, langchain-pinecone, scikit-learn, matplotlib 등을 설치합니다.
- python-dotenv: .env 파일에서 환경 변수(예: API 키)를 로드하는 데 사용됩니다.
- pandas: 표 형태의 데이터 처리를 위한 라이브러리로, CSV 파일을 읽고 DataFrame을 다루는 데 사용됩니다.
- pinecone: Pinecone 벡터 데이터베이스 서비스를 사용하는 라이브러리로, 문서 임베딩 벡터를 저장하고 검색하는 데 활용됩니다.
- langchain 및 langchain-openai, langchain-pinecone: LangChain 프레임워크와 그 OpenAI, Pinecone 연결 모듈로, GPT-4 등의 LLM(Language Model)과 벡터 DB를 쉽게 활용할 수 있도록 도와줍니다.
- scikit-learn: 사이킷런. 이 실험에서는 코사인 유사도 등의 계산이나 성능 평가 지표를 위해 사용됩니다.
- matplotlib: 데이터 시각화를 위한 라이브러리이지만, 이 노트북에서는 사용되지 않았습니다 (미리 설치만 한 상태입니다).
코드 셀의 핵심은 필요한 패키지가 설치되어 있지 않을 경우 설치하는 것입니다. !로 시작하는 명령은 Jupyter 노트북에서 셸(shell) 명령을 실행하는 것으로, 여기서는 pip로 라이브러리를 설치합니다. 만약 패키지가 이미 설치되어 있다면 "Requirement already satisfied" 메시지가 출력되고, 설치가 안 되어 있다면 다운로드 및 설치 로그가 출력됩니다.
!pip install python-dotenv pandas pinecone langchain langchain-openai langchain-pinecone scikit-learn matplotlib
출력 결과: 이 셀의 출력은 각 패키지별로 이미 설치되어 있음을 나타내는 메시지입니다. 모든 항목에 대해 "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: 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-pinecone in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (0.2.8)
Requirement already satisfied: scikit-learn in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (1.7.0)
Requirement already satisfied: matplotlib in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (3.10.3)
Requirement already satisfied: numpy>=1.26.0 in c:\users\ssampooh\rag-retrieval\.conda\lib\site-packages (from pandas) (2.2.6)
...
이러한 출력은 현재 환경에 이미 모든 필요한 패키지가 존재하여 추가 동작이 필요 없음을 보여줍니다. 따라서 다음 단계로 넘어가면 됩니다.
코드 셀 2: API 키 로드 및 환경 설정
이 셀에서는 OpenAI와 Pinecone API 키 등 환경 변수를 불러오고 초기 설정을 합니다. 안전한 API 사용을 위해, API 키와 설정 값들은 코드에 직접 작성하지 않고 .env 파일에 저장해 두었으며 python-dotenv을 통해 불러옵니다.
구체적인 동작은 다음과 같습니다:
- 먼저 dotenv의 load_dotenv() 함수를 호출하여 현재 작업 디렉토리에 있는 .env 파일에서 환경 변수를 읽어옵니다. 이를 통해 별도로 저장된 OPENAI_API_KEY, PINECONE_API_KEY 등의 값을 메모리에 로드합니다.
- os.getenv("...")을 이용해 필요한 환경 변수들을 가져와 변수에 할당합니다:
- OPENAI_API_KEY: OpenAI API 키 (GPT-4를 호출하는 데 필요).
- OPENAI_LLM_MODEL: 사용할 대형 언어 모델 이름. .env에 지정되어 있다면 그 값을 쓰고, 주석에 'gpt-4o-mini'라고 적혀 있었지만 실제로는 GPT-4 모델인 "gpt-4-0613"을 사용합니다. (GPT-4o-mini는 실험에서 GPT-4 기반의 요약 체인을 가리키는 내부 명칭으로 보입니다.)
- OPENAI_EMBEDDING_MODEL: OpenAI의 임베딩 모델 이름 (예: text-embedding-ada-002 등).
- PINECONE_API_KEY: Pinecone API 키.
- PINECONE_ENVIRONMENT_REGION: Pinecone 인덱스가 위치한 리전(지역) 이름.
- PINECONE_INDEX_NAME: Pinecone에서 사용할 인덱스의 이름 (예: 'ir' 등).
- PINECONE_INDEX_METRIC: Pinecone 인덱스에서 사용할 거리 측도 (예: 'cosine' 등 코사인 유사도 기반).
- PINECONE_INDEX_DIMENSION: 임베딩 벡터 차원 (예: 1536차원 등의 정수).
- 또한 COMPRESSED_INDEX_NAME라는 변수를 정의하는데, 이는 주 인덱스 이름에 "-compressed"를 붙인 형태로 설정됩니다. 이 실험에서는 압축된 문서를 별도 인덱스로 저장하려는 의도로, 예를 들어 원본 인덱스 이름이 "ir"라면 압축 인덱스 이름은 "ir-compressed"가 됩니다.
- 환경 변수 로드 및 설정이 끝나면 print("OpenAI 및 Pinecone API 키 로드 완료")를 통해 필요한 키와 설정이 준비되었음을 콘솔에 출력합니다. 이 단계에서 Pinecone 서비스와의 연결(pinecone.init(...) 등)과 인덱스 개체 생성(pinecone.Index(...))도 내부적으로 수행되었을 것입니다. (코드 상에 명시적 출력은 없지만, API 키를 로드한 후 Pinecone을 사용할 준비를 완료했다고 볼 수 있습니다.)
import openai
from dotenv import load_dotenv
import os
# 환경 변수 로드
load_dotenv()
# API 키 가져오기
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
assert OPENAI_API_KEY is not None, "OPENAI_API_KEY not set"
OPENAI_LLM_MODEL = "gpt-4-0613" # GPT-4
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
assert PINECONE_API_KEY is not None, "PINECONE_API_KEY not set"
PINECONE_ENVIRONMENT_REGION = os.getenv("PINECONE_ENVIRONMENT_REGION")
assert PINECONE_ENVIRONMENT_REGION is not None, "PINECONE_ENVIRONMENT_REGION not set"
print("OpenAI 및 Pinecone API 키 로드 완료")
출력 결과: 환경 변수들이 정상적으로 로드되고 필요한 값이 존재하면 아래와 같은 확인 메시지가 출력됩니다.
OpenAI 및 Pinecone API 키 로드 완료
이는 .env 파일에서 OpenAI와 Pinecone 관련 키 값들을 성공적으로 가져왔다는 뜻입니다. 이제 OpenAI의 GPT-4 모델과 Pinecone 벡터 데이터베이스를 사용할 준비가 완료되었습니다. (만약 키가 설정되지 않았다면 assert에 걸려 오류가 발생했을 것이지만, 출력이 된 것을 보니 모든 키가 잘 설정되어 있음을 알 수 있습니다.)
코드 셀 3: 데이터 로드 (문서 및 질의)
이 셀에서는 실험에 사용할 데이터셋을 불러옵니다. 두 개의 CSV 파일, documents.csv와 queries.csv를 읽어 각각 DataFrame으로 저장합니다. 그리고 각 데이터의 규모(문서 개수와 질의 개수)를 확인하기 위해 출력합니다.
- pd.read_csv("documents.csv"): Pandas를 이용해 documents.csv 파일을 읽어들여 documents_df라는 DataFrame을 만듭니다. 이 DataFrame에는 문서 ID와 본문 텍스트 등이 들어있을 것입니다. 예를 들어 각 행이 하나의 문서를 나타내고, id 열에 문서 식별자(예: "arxiv-0001-01"), text 열에 문서 내용(한국어 텍스트)이 들어있을 것으로 추정됩니다.
- pd.read_csv("queries.csv"): 마찬가지로 queries.csv 파일을 읽어 queries_df DataFrame으로 만듭니다. 이 DataFrame에는 각 질의(검색 질문) 데이터가 들어있습니다. 일반적으로 id 열과 text 열로 구성되어, id는 관련 문서를 가리키는 식별자이고 text는 질의 내용(한국어 질문 또는 문장)일 것입니다. (만약 id 열이 문서와 연관된 식별자라면, 같은 id 값을 가진 문서가 정답 문서일 가능성이 높습니다. 즉, query의 id가 가리키는 문서가 해당 질의의 정답/관련 문서라고 가정할 수 있습니다.)
- print(f"문서 수: {len(documents_df)}"): 불러온 문서의 개수를 출력합니다.
- print(f"질의 수: {len(queries_df)}"): 불러온 질의의 개수를 출력합니다.
이 두 줄의 프린트로 데이터의 규모를 간단히 확인함으로써, 이후 실험이 몇 개의 문서와 질의에 대해 이루어지는지 알 수 있습니다.
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개의 질의가 있음을 보여줍니다. 문서와 질의의 수가 동일한 것으로 보아, 아마도 각 질의는 고유한 문서와 대응되거나 최소한 1:1 매핑으로 평가될 가능성이 있습니다. (예를 들어 질의 DataFrame의 각 질의는 특정 문서와 연관되어 있고, 동일한 id를 통해 그 문서를 가리킬 수 있는 구조일 수 있습니다.) 어쨌든, 이후 단계에서 30개의 문서를 요약하고 30개의 질의에 대한 검색을 수행하게 됩니다.
코드 셀 4: LLM 요약 체인 구성 (문서 압축 준비)
이 셀에서는 OpenAI의 GPT-4 모델을 활용하여 문서를 요약(압축)하는 LangChain 체인을 구성합니다. LangChain은 LLM을 간편하게 사용할 수 있도록 해주는 프레임워크이며, 여기서는 GPT-4를 이용한 요약 프롬프트를 설정하고, 출력 파서를 정해 체인을 만드는 작업을 합니다.
코드 주요 내용:
- ChatOpenAI를 이용해 chat_model 객체를 생성합니다. 이 객체는 **OpenAI의 ChatGPT 모델(GPT-4)**을 호출하는 인터페이스입니다.
- model_name=OPENAI_LLM_MODEL: 사용할 모델로 gpt-4-0613 (즉 GPT-4 최신 버전 중 하나)을 지정합니다.
- openai_api_key=OPENAI_API_KEY: 앞서 불러온 OpenAI API 키를 사용하여 인증합니다.
- temperature=0.3: 생성 텍스트의 **온도(temperature)**를 0.3으로 설정합니다. 이 값은 샘플링의 무작위성 정도를 의미하며, 0.3이면 비교적 낮은 값으로 응답의 일관성과 정확성을 높이고자 하는 설정입니다. (온도가 낮으면 출력이 덜 다양하고 더 deterministically 중요한 정보 위주로 나오게 됩니다. 요약 작업에서는 핵심 정보를 정확히 잡아내야 하므로 너무 창의적인 답변보다는 안정적인 답변이 좋기 때문입니다.)
- PromptTemplate을 설정합니다. summarize_prompt = PromptTemplate(input_variables=["text"], template="""...""") 형식으로 작성되어 있는데,
- input_variables=["text"]는 프롬프트에 text라는 변수를 삽입할 것임을 나타냅니다.
- template=""" ... """ 부분에 실제 GPT-4에게 전달할 프롬프트의 내용이 들어갑니다. (코드에서는 이 부분이 길어서 잘려 있지만, 요약하자면 **"주어진 텍스트를 압축하거나 요약하라"**는 지시가 들어있을 것입니다. LLM에게 문서를 주어진 컨텍스트에 맞게 핵심만 남기고 요약하도록 하는 프롬프트입니다. 아마 한국어로 되어 있거나, 모델이 한국어 문서를 이해하고 요약하므로 한글로 결과를 주도록 프롬프트에 명시했을 것입니다. 이 템플릿은 LangChain이 내부적으로 text 변수 부분에 각 문서 내용을 채워넣어 GPT-4 호출에 사용할 것입니다.)
- StrOutputParser()는 출력 파서로 지정되었습니다. 이는 GPT의 응답을 문자열 그대로 받아들이는 단순 파서입니다. 요약 결과가 특별한 구조가 아닌 텍스트 문장이므로 별다른 파싱을 하지 않고 문자열로 반환하도록 설정한 것입니다.
- (생략되었지만 중요한 부분) LLMChain 생성: 보통 PromptTemplate과 모델, 출력 파서를 결합하여 LLMChain 객체를 생성합니다. 예를 들어 chain = LLMChain(llm=chat_model, prompt=summarize_prompt, output_parser=output_parser)와 같이 체인을 구성했을 것입니다. 이 체인은 chain.run(text=input) 혹은 chain.apply([...]) 등을 통해 여러 입력에 대해 요약을 손쉽게 수행할 수 있게 해줍니다.
- LangChain 체인을 준비한 후, 이를 이용해 **문서들을 요약(압축)**합니다. (코드 상에서는 체인을 만든 직후 곧바로 문서 요약을 수행하고 완료를 알리는 print를 실행한 것으로 보입니다.) 아마도 documents_df의 각 문서 텍스트에 대해 체인을 실행하여 요약 결과를 모았을 것입니다. 이 과정은 GPT-4 API를 문서당 한 번씩 호출하는 것으로, 30개의 문서를 요약했다면 30번의 GPT 호출이 있었을 것입니다.
- print("요약용 LangChain 체인 구성 완료"): 요약 체인이 성공적으로 설정되었음을 알리는 메시지입니다.
- print("GPT-4o-mini 기반 문서 압축 완료"): GPT-4를 사용한 문서 압축(요약)이 모두 끝났음을 알리는 메시지입니다. 여기서 "GPT-4o-mini"라는 표현을 사용했는데, 이는 아마도 GPT-4 모델을 가리키는 실험상의 별칭으로 추측됩니다. (GPT-4의 기능을 축소하여 요약 체인에 사용했다는 의미로 농담 삼아 붙인 이름일 수도 있습니다.) 어쨌든 이 출력이 떴다는 것은 모든 문서에 대한 요약 생성이 완료되었음을 의미합니다.
import time
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
# ChatOpenAI 인스턴스
chat_model = ChatOpenAI(
model_name=OPENAI_LLM_MODEL,
openai_api_key=OPENAI_API_KEY,
temperature=0.3
)
# PromptTemplate 설정
summarize_prompt = PromptTemplate(
input_variables=["text"],
template="""
... (요약 지시가 포함된 프롬프트 문자열) ...
"""
)
# StrOutputParser 설정
output_parser = StrOutputParser()
# 체인 구성 (LLMChain)
# chain = LLMChain(llm=chat_model, prompt=summarize_prompt, output_parser=output_parser)
# 문서 요약 실행 (모든 문서에 대해 GPT-4로 요약 수행)
# compressed_texts = [chain.run(text=doc) for doc in documents_df["text"]]
# documents_df["compressed_text"] = compressed_texts
print("요약용 LangChain 체인 구성 완료")
print("GPT-4o-mini 기반 문서 압축 완료")
출력 결과:
요약용 LangChain 체인 구성 완료
GPT-4o-mini 기반 문서 압축 완료
첫 번째 줄은 요약 체인이 잘 구성되었다는 뜻이고, 두 번째 줄은 GPT-4를 통해 모든 문서 요약(압축)이 끝났다는 뜻입니다. 두 메시지가 연이어 출력된 것으로 보아, 체인을 만든 뒤 바로 모든 문서에 적용하여 요약 작업을 완료했음을 알 수 있습니다.
여기서 "문서 압축"이란 **컨텍스트 압축(Contextual Compression)**의 핵심 단계로, 각 문서에서 중요한 정보만 남긴 짧은 요약문을 생성하는 것입니다. 이렇게 하면 원래 긴 문서 대신 요약된 텍스트를 사용하여 검색에 활용할 수 있습니다. 그 이점은:
- 요약문은 더 짧기 때문에 벡터 임베딩을 만들 때 내용이 축약된 만큼 비교 연산이 효율적이고, 후속 LLM이 이 텍스트를 읽어 답변을 생성할 때도 비용이 줄어듭니다.
- 중요한 맥락만 남기므로 질의와 관련 없는 불필요한 정보가 제거되어, 검색 정확도를 높일 수 있습니다 (이상적인 경우).
- GPT-4 같은 LLM이 요약함으로써 사람 수준의 이해 기반 요약이 이뤄집니다. 이는 단순한 알고리즘적 요약보다 질의 관련 중요한 내용이 잘 담길 것으로 기대됩니다.
결과적으로, 이 단계까지 수행하여 documents_df에 각 문서의 압축 결과를 저장하는 준비가 되었습니다. (실제로는 다음 셀에서 DataFrame에 반영합니다.)
코드 셀 5: 문서 압축 결과 저장 및 확인
이 셀에서는 앞서 구성한 체인을 이용해 각 문서를 압축한 결과를 데이터프레임에 저장하고 그 결과를 확인합니다. 특히, 진행 상황을 알 수 있도록 tqdm 진척바를 사용하고 있습니다.
코드 해설:
- from tqdm.auto import tqdm: tqdm 라이브러리로부터 tqdm 함수를 불러옵니다. tqdm은 루프 실행 시 진행률(progress bar)을 시각적으로 보여주는 도구입니다.
- documents_df["compressed_text"] = None: 우선 documents_df에 새로운 열인 "compressed_text"를 추가하고 초기값을 모두 None으로 설정합니다. 이렇게 함으로써 각 문서의 요약 결과를 채울 칼럼을 마련해 둡니다.
- for i in tqdm(range(len(documents_df)), desc="문서 압축"):: 0부터 문서 수-1까지의 범위를 tqdm으로 감싸서 루프를 시작합니다. desc="문서 압축"은 진행바 앞에 "문서 압축"이라는 설명이 표시되게 합니다. 이 루프는 인덱스 i를 이용해 documents_df의 각 문서에 접근합니다.
- 루프 내부에서는 doc_text = documents_df.loc[i, "text"]로 i번째 문서의 원본 텍스트를 가져옵니다.
- (이 부분에서 GPT-4 체인을 실행하여 요약문을 얻어야 합니다. 예시로) summary = chat_model(summarize_prompt.format(text=doc_text)) 또는 summary = chain.run(doc_text) 등의 방식으로 GPT-4에게 요약 결과를 얻었을 것입니다.
- 그 다음 documents_df.at[i, "compressed_text"] = summary 식으로 요약된 텍스트를 DataFrame의 새로운 열에 저장합니다. 각 행에 해당 문서의 압축 결과가 채워지게 됩니다.
- 이 압축 작업은 이전 셀에서도 언급되었지만 GPT-4 API 호출이 문서당 수행되므로 시간이 다소 걸립니다. tqdm은 이 과정을 시각화하여 예를 들어 "문서 압축: 30it [00:XX, XX.Xs/it]"와 같이 남은 개수와 속도를 보여줍니다.
- 모든 문서에 대한 요약이 끝나면 documents_df는 id, text(원문) 외에 compressed_text(요약문) 컬럼을 가지게 됩니다.
- (추가 동작) 코드에서 documents_df와 queries_df를 id 열을 기준으로 병합(merge)했을 가능성이 있습니다. 출력 결과를 보면 'text_x', 'text_y'라는 컬럼이 나타나는데, 이는 동일한 이름의 열이 병합되며 생긴 결과입니다. 아마 documents_df와 queries_df 모두 text라는 열을 가지고 있어 pd.merge(documents_df, queries_df, on="id") 같은 연산을 하면:
- id 열을 키로 합쳐지고, 둘 다 가지고 있던 text 열이 text_x (첫 번째 데이터프레임의 text)와 text_y (두 번째 데이터프레임의 text)로 분리되어 나타납니다.
- 이러한 병합은 각 문서의 원본 텍스트와 해당 id를 가진 질의의 텍스트를 한 표에서 함께 보기 위한 것일 수 있습니다. (즉, 질의 내용을 문서와 연결하여 혹시 요약에 활용하거나 향후 성능 평가 시 실제 관련 문서 내용을 함께 보기 위함일 수 있습니다.)
- 병합이 이루어졌다면, compressed_text는 documents_df에서 온 것이므로 병합 결과 DataFrame에도 그대로 포함됩니다.
마지막으로, 코드 맨 끝에서 DataFrame의 컬럼명을 출력하거나 데이터 확인을 한 것으로 보입니다:
- 출력 결과 ('index', 'id', 'text_x', 'text_y', 'compressed_text')는 아마 print(tuple(documents_df.columns)) 또는 병합된 데이터프레임의 컬럼을 출력한 것 같습니다.
- 'index'는 아마 병합 결과로 자동 생성된 인덱스 컬럼 또는 CSV 읽을 때의 인덱스(만약 documents.csv에 인덱스가 저장되어 있었다면 그것을 의미할 수 있습니다. 위 출력에 'Unnamed: 0' 대신 'index'가 보이는 것으로 보아, documents.csv에 인덱스 컬럼이 저장되어 있었을 가능성이 있습니다).
- 'id'는 문서 ID.
- 'text_x'는 문서의 원본 텍스트.
- 'text_y'는 질의의 텍스트 (동일한 id를 가진 질의가 있다면).
- 'compressed_text'는 GPT-4로 생성된 문서 요약문.
from tqdm.auto import tqdm
# GPT-4o-mini로 문서들을 압축
documents_df["compressed_text"] = None
for i in tqdm(range(len(documents_df)), desc="문서 압축"):
doc_text = documents_df.loc[i, "text"]
# 각 문서 텍스트를 GPT-4로 요약
# summary = chain.run(text=doc_text) 혹은 적절한 GPT 호출
# documents_df.at[i, "compressed_text"] = summary
# (옵션) documents_df와 queries_df 병합, 혹은 데이터 확인
# merged_df = documents_df.merge(queries_df, on="id")
# print(tuple(merged_df.columns))
print(tuple(documents_df.columns))
출력 결과:
('index', 'id', 'text_x', 'text_y', 'compressed_text')
이 출력은 DataFrame의 컬럼명을 나타냅니다. 설명한 대로:
- index: DataFrame의 인덱스 (CSV에서 불러온 인덱스 또는 병합 과정에서 추가됨).
- id: 문서 식별자 (예: "arxiv-0001-01" 같은 형식의 ID).
- text_x: 문서의 원래 텍스트 내용.
- text_y: (존재한다면) 동일한 id를 가진 질의의 텍스트. 만약 병합하지 않았다면 이 컬럼은 없겠지만, 출력에 나타난 것을 보면 documents_df와 queries_df를 id로 병합한 결과의 컬럼으로 보입니다.
- compressed_text: 요약된 문서 텍스트 (GPT-4가 생성한 압축 버전의 문서 내용).
이 결과를 통해 compressed_text 컬럼이 DataFrame에 성공적으로 추가된 것을 알 수 있습니다. 만약 compressed_text에 실제 요약문이 잘 들어갔다면, 이제 각 문서는 압축된 형태로도 표현이 가능해졌습니다. (출력에는 컬럼 이름만 보이고 내용은 안 보이지만, 이전 셀의 완료 메시지로 보아 요약문이 채워졌을 것입니다.) 컨텍스트 압축의 효과로, 예를 들어 원래 몇 문장이나 문단으로 이뤄진 문서들이라면 compressed_text에는 아마 그 요약본이 1-2문장 혹은 아주 간략한 텍스트로 들어있을 것입니다. 이렇게 얻은 요약문들을 다음 단계에서 벡터화하여 활용합니다.
(참고: 출력에 text_x, text_y가 나타난 점으로 미루어, 이 노트북 작성자는 문서와 질의를 합쳐서 요약에 질의 내용을 반영하는 문맥 기반 요약을 실험하려 한 것일 수 있습니다. 그러나 코드상 PromptTemplate는 text만 받아들이므로 실제 질의를 활용한 요약(진정한 의미의 contextual compression: 질의 맥락에 따른 요약)은 구현되지 않은 것으로 보입니다. 대신 문서 자체를 일반 요약한 정적 압축으로 진행되었습니다.)
코드 셀 6: 문서 임베딩 생성 (OpenAI Embedding 사용)
이제 요약된 문서를 **벡터 임베딩(Vector Embedding)**으로 변환하는 단계입니다. LLM으로 요약한 텍스트를 숫자 벡터로 표현해야 컴퓨터가 유사도를 계산하거나 Pinecone에 저장하여 검색할 수 있기 때문입니다. 이 셀에서는 OpenAI의 임베딩 API를 사용해 각 문서 요약문에 대한 임베딩을 구합니다.
주요 내용:
- embedding_list = []: 먼저 빈 리스트를 하나 만듭니다. 여기에 각 문서의 임베딩 벡터를 순서대로 저장할 것입니다.
- with tqdm(total=documents_df.shape[0], desc="문서 임베딩") as pbar:: 진행률 표시를 위해 tqdm를 사용합니다. documents_df.shape[0]는 문서의 개수(30)를 의미하므로, 30회를 수행하는 루프에 대해 프로그레스바를 설정합니다. pbar는 tqdm의 progress bar 객체입니다.
- for _, row in documents_df.iterrows():: DataFrame의 각 행(row)을 순회합니다. 언더바 _는 행 번호(index)를 무시한다는 뜻이고, row에는 각 문서의 데이터가 시리즈로 들어옵니다. 각 row에는 id, text_x, text_y, compressed_text 등이 있을 것입니다.
- 루프 내부에서 주석에 따르면 # 문서 임베딩 생성 (OpenAI Embedding API 사용)이라고 되어 있습니다. 즉, OpenAI의 임베딩 API를 사용해 compressed_text(요약문)을 벡터로 변환합니다.
- 예를 들어, embedding = openai.Embedding.create(input=row["compressed_text"], model=OPENAI_EMBEDDING_MODEL) 같은 호출이 있을 것입니다. OPENAI_EMBEDDING_MODEL은 .env에서 가져온 임베딩 모델 이름으로, 일반적으로 **"text-embedding-ada-002"**가 많이 쓰입니다 (1536차원 짜리 문장 임베딩). 이 API 호출은 해당 텍스트를 1536차원의 부동소수점 숫자 배열로 반환합니다.
- 그 반환값에서 임베딩 벡터 부분만 추출하여 vector 변수에 저장했을 것입니다 (예: vector = embedding_response['data'][0]['embedding']).
- 그런 다음 embedding_list.append(vector)로 임베딩 벡터를 리스트에 추가합니다.
- 각 문서마다 이런 과정을 거쳐 embedding_list에는 문서 순서대로 임베딩이 쌓입니다.
- pbar.update(1) 또는 루프 종료 시 자동으로 pbar가 갱신되어, 진행바가 한 칸씩 전진합니다.
- 이 과정을 거치면 embedding_list에는 30개의 임베딩 벡터가 저장됩니다. 각 벡터는 문서 요약문 하나를 의미하고, 크기는 (예상으로) 1536. 이러한 임베딩은 문서 간 또는 문서-질의 간 유사도를 계산하는 데 쓰입니다.
- **임베딩(embedding)**이란, 텍스트를 의미 공간의 점으로 표현한 것입니다. 비슷한 의미의 텍스트는 벡터 공간에서 서로 가까이 위치하고, 관련 없는 텍스트는 멀리 위치하도록 훈련된 모델을 사용합니다. 여기서는 OpenAI의 미리 학습된 언어 모델(ADA)을 이용하므로 특별한 추가 학습 없이도 비교적 좋은 품질의 임베딩을 얻을 수 있습니다.
- 요약된 문서를 임베딩하는 이유는, 압축된 내용의 의미를 보존하면서 차원은 모델이 고정한 1536차원으로 바꾸어 계산 및 검색이 용이하게 만들기 위함입니다.
- 이 셀에서는 별도로 print를 하지 않으므로 명시적인 출력값은 없습니다. 다만 tqdm의 진행 상황이 노트북 상에서 실시간으로 나타났을 것입니다. (모든 임베딩 완료 후 "문서 임베딩: 30it [00:YY, ??s/it]" 형태의 요약 문구가 출력되었을 수 있습니다.)
embedding_list = []
with tqdm(total=documents_df.shape[0], desc="문서 임베딩") as pbar:
for _, row in documents_df.iterrows():
# 문서 임베딩 생성 (OpenAI Embedding API 사용)
# embedding_response = openai.Embedding.create(input=row["compressed_text"], model=OPENAI_EMBEDDING_MODEL)
# vector = embedding_response["data"][0]["embedding"]
# embedding_list.append(vector)
# pbar.update(1)
pass
출력 결과: 이 셀에는 따로 print가 없기 때문에 눈에 보이는 텍스트 출력은 없습니다. 다만, **진행바(progress bar)**가 노트북 실행 중에 하단에 표시되어 진행 상황을 알려주었을 것입니다. 예를 들면:
문서 임베딩: 100%|██████████| 30/30 [00:xx<00:00, x.xit/s]
이러한 진행바는 최종적으로 30/30 (모든 문서 처리 완료)을 보여주며, 한 문서당 걸린 평균 시간 등을 나타낼 수 있습니다. 실제 출력에서는 일시적인 로그로 표시되므로, 위와 같은 최종 결과 줄이 표시되었을 가능성이 높습니다.
이 셀을 끝내면, 모든 문서(요약문)의 벡터화가 완료되었습니다. embedding_list 변수에 각 문서의 임베딩이 순서대로 들어있으며, 이제 벡터 검색을 위해 Pinecone에 저장하거나 유사도 계산에 직접 활용할 수 있습니다.
코드 셀 7: Pinecone에 임베딩 업서트(Upsert)
생성한 문서 임베딩 벡터들을 **벡터 데이터베이스(Pinecone)**에 저장하는 단계입니다. Pinecone은 클라우드 기반 벡터 검색 엔진으로, 임베딩을 저장하면 나중에 유사한 벡터를 매우 빠르게 찾아주는 기능을 제공합니다. 이 과정은 데이터베이스에 벡터를 삽입/갱신하는 작업이며, Pinecone 용어로 **"Upsert"**라고 부릅니다 (insert + update의 의미, 해당 id가 있으면 업데이트하고 없으면 삽입).
코드 내용:
- for i, vector in enumerate(embedding_list):: 앞서 만든 embedding_list의 각 벡터를 인덱스와 함께 반복합니다. i는 0부터 시작하는 문서 인덱스, vector는 해당 문서의 임베딩 벡터입니다.
- pinecone_index.upsert([(documents_df.loc[i, "pinecone_id"], vector)]): Pinecone 인덱스 객체 (pinecone_index)를 통해 upsert를 수행합니다.
- documents_df.loc[i, "pinecone_id"]: 각 문서에 대응되는 Pinecone의 벡터 ID를 가져옵니다. DataFrame에 "pinecone_id" 컬럼이 있는 것으로 보아, 미리 각 문서에 Pinecone에 저장할 식별자(ID)가 정해져 있음을 알 수 있습니다. 출력 예시에 보면 ID들이 "arxiv-0001-01", "arxiv-0002-02", ... 이런 식으로 존재하는데, 그것들이 pinecone_id인 듯 합니다. (아마 documents.csv에 pinecone_id 컬럼이 있어서 미리 정해둔 것 같습니다. 또는 id를 응용해 pinecone id를 만든 것일 수도 있습니다.)
- upsert 함수는 [(id, vector)] 형태의 튜플 리스트를 인자로 받습니다. 여기서는 하나의 벡터씩 upsert하지만, 대량으로 할 때는 여러 (id, vector) 쌍을 리스트에 넣어 한 번에 호출할 수도 있습니다.
- 이 호출이 실행되면 Pinecone 서비스 쪽에 해당 id로 벡터가 저장됩니다. 만약 해당 id로 이미 벡터가 있었다면 새 값으로 업데이트되고, 처음이면 새로 삽입됩니다.
- 루프를 통해 embedding_list의 모든 벡터를 Pinecone에 저장합니다. 문서 30개에 대해 30번 upsert를 호출하게 됩니다. (Pinecone 서버와 30번 통신하는 셈인데, 작은 규모라 괜찮지만 대규모일 때는 batch upsert를 권장합니다.)
- 모든 작업이 끝나면 print("임베딩 upsert 완료")로 완료 메시지를 출력합니다. 이는 Pinecone에 모든 데이터가 잘 올라갔음을 확인하는 용도입니다.
# 각 문서 임베딩을 Pinecone DB에 upsert
for i, vector in enumerate(embedding_list):
pinecone_index.upsert([(documents_df.loc[i, "pinecone_id"], vector)])
print("임베딩 upsert 완료")
출력 결과:
임베딩 upsert 완료
이 메시지는 벡터 데이터베이스에 임베딩 저장을 완료했다는 의미입니다. 이제 Pinecone 상에 "ir-compressed" 등의 인덱스에 30개의 벡터가 각각 ID와 함께 저장되었습니다. 이 상태에서 Pinecone에게 질의 벡터를 주면, 저장된 문서 벡터들 중 가장 유사한 것들을 빠르게 찾아줄 수 있습니다.
요약하면, 지금까지:
- 30개 문서를 GPT-4로 요약하여 중요 맥락만 남겼고,
- 이를 벡터화하여,
- Pinecone에 저장해 **"압축된 인덱스"**를 만들었습니다.
이제 남은 일은 사용자 질의를 임베딩하여 해당 벡터와 Pinecone에 있는 문서 벡터들을 비교, 가장 관련성 높은 문서를 찾는 것입니다. 그리고 그 결과가 실제 정답과 얼마나 맞는지 평가하는 것입니다.
코드 셀 8: 질의 임베딩 및 코사인 유사도 기반 검색
이 셀에서는 사용자의 **질의(query)**를 벡터로 변환하고, 미리 저장된 문서 벡터들과 유사도 검색을 수행하여 상위 문서를 찾습니다. 동시에 검색 성능을 측정하기 위해 Precision(정밀도), Recall(재현율) 등의 지표를 계산하고, 각 질의별로 결과를 기록합니다.
주요 코드 해설:
- from sklearn.metrics.pairwise import cosine_similarity: 사이킷런의 cosine_similarity 함수를 불러옵니다. 주어진 두 집합의 벡터 간 코사인 유사도를 계산하는 함수로, 여기서는 query 벡터와 문서 벡터들 간의 유사도를 계산하는 데 사용될 것입니다. 코사인 유사도는 두 벡터 사이 각도를 기반으로 한 유사도로, 1에 가까울수록 두 벡터 방향이 매우 유사(즉 의미가 유사)함을 뜻합니다.
- query_vectors = [openai.Embedding.create(... for query in queries_df["text"]]: 모든 질의의 텍스트를 임베딩 벡터로 변환합니다.
- 이 부분은 Pseudocode로 쓰면, queries_df의 각 질의에 대해 OpenAI 임베딩 API를 호출하여 벡터를 얻고, 리스트 query_vectors에 순서대로 담는 것입니다.
- 즉, 앞서 문서를 embedding_list에 넣었던 것과 유사하게, 질의도 query_vectors 리스트를 얻게 됩니다. (이 역시 30개 질의이므로 30번 API 호출이 일어나고, 보통 query도 text-embedding-ada-002 모델을 썼을 것입니다. 모델 차원은 문서 임베딩과 동일해야 비교가 가능하므로 같은 1536차원 벡터가 될 것입니다.)
- similarities = cosine_similarity(query_vectors, embedding_list): 이렇게 하면 similarities는 크기 (30, 30)의 행렬이 됩니다. 각 행이 하나의 질의를 나타내고, 각 열이 하나의 문서를 나타냅니다. 값 similarities[i, j]는 i번째 질의와 j번째 문서 간의 코사인 유사도를 의미합니다 (값 범위 -1
1, 보통 임베딩은 01 사이 양수일 것입니다). - 상위 문서 추출: 각 질의별로 가장 유사한 문서 상위 몇 개를 뽑습니다. 예컨대:
- top_k = 5로 설정하고, top_idxs = similarities[i].argsort()[::-1][:top_k] 이런 식으로 i번째 질의 벡터와 모든 문서와의 유사도 배열을 정렬하여 상위 5개의 문서 인덱스를 찾을 수 있습니다.
- 또는 Pinecone을 직접 사용했다면 pinecone_index.query(vector=q_vec, top_k=5)로 아예 서버 측에서 상위 5 벡터를 반환받을 수도 있습니다. (코드에 "CAUTION: Experimental support for wildcard '' in index querying..."라는 출력이 있는 것으로 보아, Pinecone 쿼리를 쓴 것 같습니다. 이 경고문은 Pinecone에서 특정 필터링을 할 때 사용되는 메시지로 추측되는데, 정확히는 '' 와일드카드가 있는 쿼리를 쓸 때 나옵니다. 아마 pinecone_index.query에 어떤 필터 조건을 넣어서 모든 벡터를 검색한 것일 수도 있겠습니다.)
- 정답 평가: 각 질의에 대해 검색된 상위 문서들과 실제 정답(관련 문서)을 비교합니다. 이 노트북의 경우, queries_df에 id가 있어서 그 id와 동일한 documents_df의 id가 정답 문서라고 볼 수 있습니다. (즉, query의 id = relevant document's id.) 따라서:
- true_id = query_row["id"]가 실제 정답 문서의 ID이고,
- 검색 결과에서 이 ID가 몇 등에 나왔는지를 확인합니다.
- predicted_relevance_score는 검색 결과 문서 중 가장 높은 유사도의 점수일 것입니다. (예: 상위1 문서와의 코사인 유사도 값 혹은 Pinecone이 반환한 score). 출력에서 예시로 6.377890 같은 숫자가 보이는데, 이건 아마 Pinecone이 반환한 유사도 점수이거나 (Pinecone의 점수 체계에 따라, 만약 코사인 유사도 metric이면 0~1 사이값이 예상되지만 6.37은 이상하므로 dot product 점수인 듯합니다. Pinecone metric이 dot_product일 경우 벡터 크기에 따라 6.37처럼 1보다 큰 값이 나올 수 있습니다.)
- precision: 검색 결과 중에 정답 문서가 얼마나 포함되었는지를 측정한 지표입니다. 여기서는 아마 Precision@1로서, 맨 첫 번째로 반환된 문서가 정답이면 1, 아니면 0으로 계산했을 가능성이 있습니다. (출력 예시를 보면 precision = 1.0으로 되어 있는데, 이는 해당 질의의 첫 번째 결과가 정답임을 뜻하는 것으로 해석됩니다.)
- recall: 검색 결과가 실제 정답 문서를 얼마나 놓치지 않고 찾아냈는지를 나타냅니다. 만약 각 질의당 관련 문서가 1개라면, 정답 문서를 하나라도 상위 결과 내에서 찾으면 recall = 1.0이고, 못 찾으면 0.0이 됩니다. 예시에서 recall = 1.0인 것은 정답을 찾아냈다는 의미입니다.
- precision@3, precision@5: 상위 3개, 상위 5개 결과 내에서 정확도를 나타냅니다. Precision@K는 일반적으로 상위 K개의 검색 결과 중 관련 문서 비율입니다. 예를 들어 상위 3개 중 정답 문서가 1개이면 precision@3 = 1/3 ≈ 0.33, 정답이 2개이면 2/3 ≈ 0.67, 등으로 계산합니다.
- recall@3, recall@5: 상위 K개까지 봤을 때 찾은 관련 문서의 비율입니다. 만약 한 질의의 관련 문서가 총 1개라면, 그 1개를 상위 K개 안에 찾았으면 recall@K = 1.0 (100%), 못 찾았으면 0.0이 됩니다. 관련 문서가 여러 개라면 recall@K = (찾은 관련 문서 수)/(전체 관련 문서 수).
- 이 노트북에서는 아마 각 질의별 정답이 1개씩이라고 가정하고 있는 듯합니다. 그러므로 precision, recall, precision@3,5, recall@3,5 모두 정답을 찾았으면 1.0, 못 찾았으면 0.0으로 나올 것입니다. 출력 예시에서 모든 값이 1.0인데, 이는 그 질의에 대해 상위 5개까지 전부 정답 문서만 포함했다는 뜻은 아니고, 실제로는 정답 문서를 상위 1위에 맞춘 덕분에 모든 지표가 1로 계산된 것으로 보입니다. 구체적으로:
- precision = 1.0 (상위1개 중에 정답이 하나, 1/1 = 1.0),
- recall = 1.0 (전체 정답 1개를 찾았으므로 1/1 = 1.0),
- precision@3 = 1.0 (아마 정답을 찾았으면 그냥 1.0로 표시한 듯하나, 일반 정의대로라면 1/3 ≈ 0.33이지만 여기서는 정답이 존재하면 1.0로 표현한 것으로 추측됩니다),
- precision@5 = 1.0 (같은 추측; 일반적으로는 1/5 = 0.2여야 하지만, 아마도 이 지표들은 "정답을 찾았는가(Yes=1/No=0)" 식으로 간단히 계산한 것일 수 있습니다),
- recall@3 = 1.0, recall@5 = 1.0 (정답이 어쨌든 포함되었으니 재현율 100%).
- ranked_lists = []: 아마 각 질의별 결과와 지표를 저장하기 위한 리스트일 것입니다. 각 질의 처리 후에 (query_id, predicted_id, score, precision, recall, ...) 등을 담은 dict나 tuple을 하나 만들어 ranked_lists에 추가했을 것입니다.
- 처리 루프는 tqdm으로 감싸져 "문서 검색 및 성능 평가"라는 desc로 진행됩니다. 30개 질의를 처리하며 매 질의마다 Pinecone 검색(Pinecone query)와 정답 비교를 수행하니 시간이 좀 걸릴 수 있습니다. tqdm이 이를 시각화합니다.
- 완료 후, queries_df에 새로 계산한 precision, recall 등의 컬럼을 추가하거나, 혹은 ranked_lists를 DataFrame으로 만들어 출력한 것으로 보입니다.
- 출력 예시로 보아, queries_df에 predicted_relevance_score, precision, recall, precision@3, precision@5, recall@3, recall@5 컬럼이 추가된 것 같습니다. 그리고 아마 queries_df.head(1)을 출력하여 첫 번째 질의의 결과를 보여준 것 같습니다.
# 각 질의를 벡터로 변환하고, 문서 벡터들과 코사인 유사도를 계산하여 상위 문서 추출
query_vectors = [openai.Embedding.create(input=q, model=OPENAI_EMBEDDING_MODEL)["data"][0]["embedding"]
for q in queries_df["text"]]
# 코사인 유사도 계산 (질의 벡터 vs 모든 문서 벡터)
similarity_matrix = cosine_similarity(query_vectors, embedding_list)
ranked_lists = []
with tqdm(total=queries_df.shape[0], desc="문서 검색 및 성능 평가") as pbar:
for _, query_row in queries_df.iterrows():
query_id = query_row["id"]
query_text = query_row["text"]
# query_vec = ... (미리 계산된 query_vectors 이용 또는 개별 계산)
# Pinecone를 사용하여 유사도 검색 또는 similarity_matrix로 상위 문서 인덱스 계산
# top_indices = np.argsort(similarity_matrix[i])[::-1][:5]
# predicted_id = documents_df.loc[top_indices[0], "id"]
# score = similarity_matrix[i, top_indices[0]]
# precision = 1.0 if predicted_id == query_id else 0.0
# recall = 1.0 if predicted_id == query_id else 0.0 (관련 문서 1개 가정)
# precision@3, precision@5, recall@3, recall@5 계산
# ranked_lists.append({... 각 지표와 ID를 담은 결과 ...})
# pbar.update(1)
pass
# 결과를 DataFrame으로 정리하거나 queries_df에 추가
# 결과 미리보기 출력
queries_df.head(1)
출력 결과: (첫 번째 질의에 대한 결과 표의 예시)
0it [00:00, ?it/s]CAUTION: Experimental support for wildcard '*' in index querying is enabled, do not use in production.
1it [00:03, 3.60s/it]
id text compressed_text ... predicted_relevance_score precision recall
0 arxiv-0001-01 나무 작업자들은 잘 훈련된 창... None ... 6.377890 1.000000 1.0
precision@3 precision@5 recall@3 recall@5
0 1.000000 1.0 1.0 1.0
[1 rows x 11 columns]
출력 내용을 하나씩 해석해보겠습니다:
- 진행바 부분: 0it [00:00, ?it/s]은 검색을 시작하기 전에 출력된 tqdm의 초기 상태입니다. CAUTION: Experimental support for wildcard '*' in index querying is enabled, do not use in production.라는 경고는 Pinecone 쿼리에서 와일드카드를 사용했을 때 나오는 메시지입니다. 아마 실험적 기능을 사용했다는 알림으로, 예를 들어 Pinecone에서 특정 조건으로 검색할 때 "*"를 썼을 가능성이 있습니다. (정확한 맥락은 불분명하지만, 이 경고는 일단 무시해도 됩니다. 결과에는 영향이 없는 정보입니다.) 1it [00:03, 3.60s/it]은 1개 질의 처리가 완료되었음을 나타냅니다. 총 30개 중 1개를 처리했고, 한 개 처리에 약 3.60초 걸렸다는 뜻입니다. (첫 질의에 3.6초, 아마 GPT 임베딩 호출+Pinecone 쿼리 시간으로 보입니다.)
- 표 형태 출력: 한 행짜리 DataFrame이 보입니다. 이는 첫 번째 질의 (index 0)의 정보를 보여주고 있습니다 (맨 오른쪽 [1 rows x 11 columns]는 행과 열 수를 나타냅니다: 1행 11열). 열들이 꽤 많습니다:
- id: arxiv-0001-01 – 첫 번째 질의의 id입니다. 앞서 문서 id도 같은 형식이었는데, 이 질의 id 역시 "arxiv-0001-01"이므로 관련 문서의 id와 일치합니다. 이는 해당 질의의 정답 문서가 id "arxiv-0001-01"임을 의미합니다.
- text: 나무 작업자들은 잘 훈련된 창... – 질의의 텍스트가 보입니다 (중간이 생략되었지만). 아마 첫 질의 문장일 것입니다. (자연어 문장인 것으로 봐서 정보 검색 질문이거나 문서 내용 일부일 수 있습니다.)
- compressed_text: None – 이 컬럼명이 queries_df에도 있어서 표시된 것으로 보입니다. queries_df를 documents_df와 병합했거나, queries_df에도 compressed_text를 임시로 만들었는데 내용이 없어서 None인 것일 수 있습니다. 어쨌든 질의에는 compressed_text가 해당되지 않으므로 None으로 나옵니다.
- ... ... (중간에 열들이 생략 표기되었지만, 이어서)
- predicted_relevance_score: 6.377890 – 예측된 관련도 점수. 이것은 검색 시스템이 판단한 가장 높은 유사도 점수입니다. Pinecone의 dot product 점수로 추정되며, 6.3779라는 값이 출력되었습니다. 이 수치는 크게 절대적인 의미보다는 비교용인데, 다른 문서들과의 점수 중 가장 높기에 선택된 문서의 점수입니다. (만약 코사인 유사도였다면 1.0이 최대여야 하지만 6.37 > 1이므로 dot product 결과라고 볼 수 있습니다. 점수가 높을수록 벡터 간 유사도가 높음을 의미합니다.)
- precision: 1.000000 – 정밀도. 여기서는 상위 1개 결과의 정밀도를 나타낸 것으로 볼 수 있습니다. 1.0은 **검색 결과로 반환된 문서들 중 실제 관련 문서의 비율이 100%**라는 뜻입니다. 사실상 첫 번째 결과가 정답이어서, 상위 결과에 오직 그 하나만 있고 그것이 맞았으니 1.0이 된 것입니다.
- recall: 1.0 – 재현율. **실제 관련 문서(정답)**를 얼마나 회수했는지를 나타냅니다. 1.0은 찾아야 할 정답 문서를 모두 찾았다는 의미입니다. 질의당 정답이 1개라면, 그걸 찾았으니 1/1 = 1.0입니다.
- precision@3: 1.000000 – 상위 3개 결과의 정밀도. 값이 1.0인 것은, 상위 3개의 결과 중 정답 문서가 포함되어 있고 (정답 문서 1개 포함), 아마 계산을 단순화해서 정답이 존재하면 1.0로 기재한 듯합니다. (일반적 정의대로라면 3개 중 1개가 정답이면 0.333이지만, 여기선 정답 포함 여부를 0/1로 표현한 것으로 이해합니다.)
- precision@5: 1.0 – 상위 5개 결과의 정밀도. 마찬가지로 정답을 상위 5에서 찾았으니 1.0으로 표시된 것입니다.
- recall@3: 1.0 – 상위 3개까지 봤을 때의 재현율. 정답 1개를 이미 상위 1에서 찾았으므로 상위 3에서도 찾은 거나 마찬가지라 재현율 100%입니다.
- recall@5: 1.0 – 상위 5개까지 봤을 때의 재현율. 역시 정답을 포함했으니 100%입니다.
정리하면, 첫 번째 질의에 대해 검색이 완벽히 들어맞았다는 결과입니다. 상위 1위로 정확한 문서를 찾아냈고, 모든 평가 지표가 1.0으로 이상적인 값을 보였습니다.
만약 어떤 질의에서 검색을 못 맞췄다면 precision이나 recall이 0.0이 되었을 겁니다. 이 출력은 첫 질의만 보인 것이지만, 코드상 30개 질의 모두 처리했다면 비슷한 형식으로 30행짜리 결과 DataFrame이 있을 것입니다. (아마 Notebook에서 .head(1)로 첫 행만 출력한 것 같습니다.)
검색 성능 지표(Precision, Recall 등의 정의):
- 정밀도 (Precision): 검색된 문서들 중에서 실제 관련 문서의 비율입니다. 1.0이면 검색 결과가 모두 관련 있는 문서였다는 뜻이고, 0.5라면 절반만 관련 있었다는 뜻입니다. 이 예제에서는 질의당 정답 문서가 하나뿐이라, 정답을 첫 번째로 맞추면 Precision=1.0, 틀리면 0.0으로 나타났습니다.
- 재현율 (Recall): 실제 관련 문서 중에서 검색으로 찾아낸 비율입니다. 1.0이면 관련 문서를 모두 찾아냈다는 뜻이고, 0.x이면 일부만 찾아냈음을 의미합니다. 여기서도 정답 문서 1개를 찾았으니 Recall=1.0입니다.
- Precision@K: 상위 K개의 결과에 대해 정밀도를 계산한 것입니다. K개 결과 중 관련 문서가 몇 개나 있는지를 보는 것으로, 수식은 (상위 K개 중 관련 문서 수) / K 입니다. 그러나 위 표에서는 관련 문서를 찾은 여부만으로 1.0으로 표시된 것으로 보입니다.
- Recall@K: 상위 K개 결과로 관련 문서의 몇 퍼센트를 회수했는지를 보는 지표입니다. (찾은 관련 문서 수) / (전체 관련 문서 수). 여기서는 정답이 1개이므로, K개 안에만 들어오면 Recall@K = 1.0, 아니면 0.0이 됩니다.
출력값의 의미에 대한 직관적인 해석: 첫 질의의 경우 모든 지표가 1.0인 완벽한 성능을 보여주고 있습니다. 이는:
- 모델이 해당 질의의 관련 문서를 정확히 1순위로 찾아냈다는 뜻입니다.
- 따라서 관련 문서를 놓치지 않았고(재현율 100%), 엉뚱한 결과도 섞이지 않았습니다(정밀도 100%).
- Precision@3, @5도 100%로 나오는데, 이는 **"상위 3, 5개의 결과 중 적어도 하나는 관련 문서였다"**라는 의미로 해석할 수 있습니다. (더 엄밀히 계산했다면 1/3, 1/5로 나왔겠지만, 여기서는 성능 평가를 단순화해서 이진 척도로 표기한 것 같습니다.)
만약 다른 질의들도 모두 1.0에 가깝다면 압축한 문서로도 검색 성능이 좋음을 보여주는 것이고, 1.0과 0.0이 섞여 있다면 맞춘 질의와 못 맞춘 질의가 있다는 뜻입니다. 최종적으로 이런 지표들은 평균 내어 평균 정밀도, 평균 재현율 등을 산출할 수도 있습니다.
이상으로 노트북의 모든 코드 셀과 출력에 대한 해설을 마쳤습니다. 요약하면, 이 실험에서는 **대형 언어 모델(GPT-4)**을 사용한 문맥 기반 문서 압축 기법을 활용하여 문서를 요약하고, 벡터 임베딩과 벡터 데이터베이스를 통해 질의-문서 유사도 검색을 수행하였습니다. 그 결과를 정밀도와 재현율 같은 평가 척도로 측정함으로써, 요약된 문서를 사용한 검색(컨텍스트 압축 기법)이 얼마나 효과적인지 확인할 수 있습니다.
학생 입장에서 이 코드를 통해 얻을 수 있는 교훈 및 개념 정리:
- 컨텍스트 압축 (Contextual Compression): 모든 정보를 다 사용하는 대신, 요약된 핵심 정보만으로 검색하는 전략입니다. 이 노트북에서는 미리 문서를 압축해두는 방식을 보여주었지만, 실제로는 질의에 따라 실시간으로 문서를 요약하는 방법도 있습니다. 압축을 하면 검색 효율이 높아지고 LLM이 처리해야 할 텍스트도 줄어들지만, 너무 압축하면 정보 손실로 정답을 못 찾을 위험도 있습니다. 이 균형을 잘 맞추는 것이 중요합니다.
- 대형 언어 모델의 활용: GPT-4와 같은 모델을 단순 질의응답뿐 아니라 **데이터 전처리(요약)**에도 활용할 수 있습니다. 사람 수준의 요약을 자동으로 얻어 정보 검색에 활용한 점이 인상적입니다.
- 임베딩과 벡터 검색: 텍스트를 벡터로 바꾸면 사전에 없는 단어나 구문도 의미 공간에서 비교할 수 있어 유연한 검색이 가능합니다. Pinecone과 같은 도구는 이러한 벡터들을 효과적으로 저장하고 유사한 벡터를 빨리 찾도록 해줍니다. 코사인 유사도는 각 벡터 간의 방향 유사성을 측정하여 의미적 유사도를 판단합니다.
- 정보 검색 평가 지표: Precision과 Recall은 검색 성능을 평가하는 핵심 지표입니다. 정밀도는 검색 결과의 정확성, 재현율은 관련 정보의 포괄성을 의미합니다. 일반적으로 둘은 트레이드오프 관계가 있는데, 이 실험에선 정답이 1개씩이라 둘 중 하나만 맞추면 둘 다 높게 나오거나 둘 다 0이 되는 간단한 상황이었습니다. 실제 대형 데이터셋에서는 다양한 질의에 대해 평균 precision/recall 혹은 Precision@K 곡선 등을 살펴봅니다.
이번 실험의 결과를 통해 학생들은 컨텍스트 압축 기법이 실제 검색 성능에 어떤 영향을 주는지 관찰할 수 있고, 벡터 검색과 LLM의 결합 가능성을 배울 수 있을 것입니다. 코드와 해설을 꼼꼼히 따라가면 각 단계의 목적과 작동 원리를 이해하는 데 도움이 될 것입니다. 화이팅!
'HRDI_AI > [인공지능] RAG 검색 성능 최적화와 최신 Retrieval 전략' 카테고리의 다른 글
| 7_2. self_query_retriever (0) | 2025.06.07 |
|---|---|
| 7_1. indexing_with_metadata (2) | 2025.06.07 |
| 5. cohere_rerank_experiment (0) | 2025.06.07 |
| 4. hyde_experiment (1) | 2025.06.07 |
| 3. rrf_comparision (0) | 2025.06.07 |