QA MRC 모델 성능향상을 위해 각종 retrieval 모델을 구현해보며 알게된 점들을 정리하려고 한다.

모든 문서에서 등장하는 단어는 중요도가 낮으며, 특정 문서에서만 자주 등장하는 단어는 중요도가 높다.

위의 예시에서 word1은 doc1, doc2에 모두 10회 등장하지만 doc1에서 상대적 중요도가 높다고 볼 수 있다.
IDF는 DF의 역수(1/DF)이다. DF는 전체 문서들 중에서 해당 문서를 제외한 나머지 문서에서 해당 단어가 몇 번 사용되었는지를 의미한다.

아래 표를 보고 계산해보자.

✔ DF 표
word1 의 경우 Doc1 입장에서 다른 문서(Doc2)에도 사용되었기 때문에 DF=1
word2의 경우 Doc1 입장에서 Doc2에 사용되었기 때문에 DF=1, Doc2 입장에서는 Doc1에 사용되지 않았기 때문에 0.
IDF를 계산할 때에는 분모가 0이 되는 것을 방지하기 위해서 1을 더하고, 로그를 취한다.

최종값은 tf x idf 로 계산된다.

import json
import os
# 더미 데이터 생성
dummy_data = {
"1": {"text": "This is a sample document about AI."},
"2": {"text": "Machine learning is a subset of AI."},
"3": {"text": "Deep learning uses neural networks."},
"4": {"text": "Natural language processing is important in AI."},
"5": {"text": "Computer vision is another field of AI."}
}
# 데이터 저장
os.makedirs("data", exist_ok=True)
with open("data/wikipedia_documents.json", "w") as f:
json.dump(dummy_data, f)
import json
import os
import pickle
import time
import random
from contextlib import contextmanager
from typing import List, NoReturn, Optional, Tuple, Union
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from tqdm.auto import tqdm
seed = 2024
random.seed(seed)
np.random.seed(seed)
@contextmanager
def timer(name):
t0 = time.time()
yield
print(f"[{name}] done in {time.time() - t0:.3f} s")
def tokenize_fn(text):
return text.split()
class TFIDFSparseRetrieval:
def __init__(
self,
tokenize_fn,
data_path: Optional[str] = "data/",
context_path: Optional[str] = "wikipedia_documents.json",
) -> NoReturn:
self.data_path = data_path
with open(os.path.join(data_path, context_path), "r", encoding="utf-8") as f:
wiki = json.load(f)
self.contexts = list(dict.fromkeys([v["text"] for v in wiki.values()]))
print(f"Lengths of unique contexts : {len(self.contexts)}")
self.ids = list(range(len(self.contexts)))
#TF-IDF 벡터라이저 초기화
# tokenizer: 텍스트를 토큰화하는 함수
# ngram_range: (1, 2)는 단일 단어와 두 단어의 조합을 모두 고려
# max_features: 사용할 최대 특성(단어) 수를 50,000개로 제한
self.tfidfv = TfidfVectorizer(
tokenizer=tokenize_fn, ngram_range=(1, 2), max_features=50000,
)
self.p_embedding = None
self.indexer = None
def get_sparse_embedding(self) -> NoReturn:
pickle_name = f"sparse_embedding.bin"
tfidfv_name = f"tfidv.bin"
emd_path = os.path.join(self.data_path, pickle_name)
tfidfv_path = os.path.join(self.data_path, tfidfv_name)
# 이전에 저장된 TF-IDF 임베딩과 벡터라이저가 있으면 로드
if os.path.isfile(emd_path) and os.path.isfile(tfidfv_path):
with open(emd_path, "rb") as file:
self.p_embedding = pickle.load(file)
with open(tfidfv_path, "rb") as file:
self.tfidfv = pickle.load(file)
print("Embedding pickle load.")
else:
# 문서 컬렉션(self.contexts)에 대해 TF-IDF 변환 수행
print("Build passage embedding")
self.p_embedding = self.tfidfv.fit_transform(self.contexts)
print(self.p_embedding.shape)
# TF-IDF 임베딩과 벡터라이저를 파일로 저장
with open(emd_path, "wb") as file:
pickle.dump(self.p_embedding, file)
with open(tfidfv_path, "wb") as file:
pickle.dump(self.tfidfv, file)
print("Embedding pickle saved.")
def retrieve(
self,
query_or_dataset: Union[str, List], # 검색할 쿼리 또는 쿼리 리스트
topk: Optional[int] = 1 # 반환할 상위 문서의 개수
) -> Union[Tuple[List, List], pd.DataFrame]:
assert self.p_embedding is not None, "get_sparse_embedding() 메소드를 먼저 수행해줘야합니다."
if isinstance(query_or_dataset, str):
doc_scores, doc_indices = self.get_relevant_doc(query_or_dataset, k=topk)
print("[Search query]\n", query_or_dataset, "\n")
# 상위 k개의 문서에 대한 정보 출력
for i in range(topk):
print(f"Top-{i+1} passage with score {doc_scores[i]:4f}")
print(self.contexts[doc_indices[i]])
return (doc_scores, [self.contexts[doc_indices[i]] for i in range(topk)])
elif isinstance(query_or_dataset, list):
total = []
with timer("query exhaustive search"):
doc_scores, doc_indices = self.get_relevant_doc_bulk(
query_or_dataset, k=topk
)
for idx, example in enumerate(
tqdm(query_or_dataset, desc="Sparse retrieval: ")
):
tmp = {
"question": example,
"id": idx,
"context": " ".join(
[self.contexts[pid] for pid in doc_indices[idx]]
),
}
total.append(tmp)
return pd.DataFrame(total)
def get_relevant_doc(self, query: str, k: Optional[int] = 1) -> Tuple[List, List]:
# 쿼리를 TF-IDF 벡터로 변환
with timer("transform"):
query_vec = self.tfidfv.transform([query])
assert (
np.sum(query_vec) != 0
), "오류가 발생했습니다. 이 오류는 보통 query에 vectorizer의 vocab에 없는 단어만 존재하는 경우 발생합니다."
with timer("query ex search"):
result = query_vec * self.p_embedding.T
if not isinstance(result, np.ndarray):
result = result.toarray()
# 각 쿼리에 대해 유사도 점수로 문서 정렬
sorted_result = np.argsort(result.squeeze())[::-1]
doc_score = result.squeeze()[sorted_result].tolist()[:k]
doc_indices = sorted_result.tolist()[:k]
return doc_score, doc_indices
def get_relevant_doc_bulk(
self, queries: List, k: Optional[int] = 1
) -> Tuple[List, List]:
query_vec = self.tfidfv.transform(queries)
assert (
np.sum(query_vec) != 0
), "오류가 발생했습니다. 이 오류는 보통 query에 vectorizer의 vocab에 없는 단어만 존재하는 경우 발생합니다."
# 쿼리 벡터와 문서 임베딩 간의 내적 계산
result = query_vec * self.p_embedding.T
if not isinstance(result, np.ndarray):
result = result.toarray()
doc_scores = []
doc_indices = []
for i in range(result.shape[0]):
sorted_result = np.argsort(result[i, :])[::-1]
doc_scores.append(result[i, :][sorted_result].tolist()[:k])
doc_indices.append(sorted_result.tolist()[:k])
return doc_scores, doc_indices
# 실행 예시
retriever = TFIDFSparseRetrieval(tokenize_fn)
retriever.get_sparse_embedding()
# 단일 쿼리 테스트
single_result = retriever.retrieve("What is AI?", topk=2)
print("\nSingle query result:")
print(single_result)
# 다중 쿼리 테스트
multi_result = retriever.retrieve(["What is machine learning?", "Explain deep learning"], topk=2)
print("\nMulti query result:")
print(multi_result)
Lengths of unique contexts : 5
Build passage embedding
(5, 50)
Embedding pickle saved.
[transform] done in 0.001 s
[query ex search] done in 0.001 s
[Search query]
What is AI?
Top-1 passage with score 0.179151
Machine learning is a subset of AI.
Top-2 passage with score 0.170358
Computer vision is another field of AI.
Single query result:
([0.17915084430495923, 0.17035763308351742], ['Machine learning is a subset of AI.', 'Computer vision is another field of AI.'])
[query exhaustive search] done in 0.002 s
/usr/local/lib/python3.10/dist-packages/sklearn/feature_extraction/text.py:521: UserWarning: The parameter 'token_pattern' will not be used since 'tokenizer' is not None'
warnings.warn(
Sparse retrieval: 100%
2/2 [00:00<00:00, 31.56it/s]
Multi query result:
question id \
0 What is machine learning? 0
1 Explain deep learning 1
context
0 Machine learning is a subset of AI. Computer v...
1 Deep learning uses neural networks. Machine le...
TfidfVectorizer
get_sparse_embedding()
retrieve()
get_relevant_doc()
get_relevant_doc_bulk()
TF-IDF는 문서 내 단어의 상대적 중요도를 측정하는 기법이다.
TF(Term Frequency): 특정 단어가 한 문서에서 등장하는 빈도를 의미한다.
DF(Document Frequency): 특정 단어가 여러 문서 중 몇 개의 문서에 등장했는지를 나타낸다.
IDF(Inverse Document Frequency): DF의 역수를 취한 값으로, 특정 단어가 전체 문서에서 얼마나 희소한지를 나타낸다.
TF만 사용할 경우 "그리고", "있다" 같은 단어가 자주 등장하는 문서에서 중요하다고 판단될 수 있다. 반대로, IDF는 특정 문서에서만 등장하는 단어의 가치를 높게 평가하여 불필요한 단어의 영향을 줄인다.
결국, TF-IDF를 사용하면 문서에서 중요한 키워드를 추출하거나, 검색 엔진에서 문서의 연관도를 계산하는 데 유용하게 활용할 수 있다.