MRC(6) : Retrieval

SeongGyun Hong·2024년 10월 2일

NaverBoostCamp

목록 보기
6/64

1. Passage Retrieval이란?

query를 던졌을 때 이에 맞는 passage(문서)를 찾는 것.
구조화된 문서에서 찾을 수도 있지만, 일반적인 웹상에서 굴러다니는 모든 문서가 그 대상에 해당될 수 있다.

2. 왜 Passage Retrieval이 필요한가?

ODQA 즉 Open-Domain Question Answering에 활용할 수 있기 때문이다.
지금까지 MRC에서 배운 내용과 Retrieval의 기능을 합치면 총 두 단계로 이루어진 ODQA가 가능하다.

3. Query와 Passage가 어떻게 묶이는가?

실제로 어떻게 Passage Retrieval에 접근해야 할까?

일반적으로 Query와 Passage에 대해서 각각을 같은 Vector Space에 Embedding한다.

다만, 이때 Passage에 해당하는 '문서'의 경우 질문이 들어올 때마다 매번 Embedding을 새로 진행하면 매우 비효율적인 바, 미리 Embedding 해놓고 있고

새롭게 Query가 들어올 때 마다 해당 Query를 Passage가 Embedding 된 Vector Space로 같이 Embedding해준다.

그렇게 임베딩된 Query와 임베딩된 각각의 Passage에 대해서 Similarity Score를 측정한다.

일반적으로 많이 쓰는 Similaryity Score로는 Nearest Neighbor, innerproduct 등이 있다.

이렇게 유사도 점수를 구해서 가장 높은 Rank에 위치한 문서(Passage)의 순서대로 내보내주는 방식을 채택하게 된다.

4. Sparse / Dense Embedding

결국은 Passage를 Vector Space로 맵핑하려고 하는 건데, 이 Vector Space라는 것은 고차원의 숫자로 이루어진 포인트들이 모여있는 추상적 공간이며, 이러한 공간에 있는 Passage와 Query는 서로 간의 유사도 등의 알고리즘을 활용할 수 있는 계산 가능한 상태가 됨.
그래서 중요한건 결국은 어떤 방식의 Embedding으로 Vector Space에 Query와 Passage를 끌고올 것인가 라고 할 수있다.

4.1 Sparse Embedding

Sparse Embedding이란?
고차원 벡터로, 대부분의 값이 0인 희소 벡터를 의미한다. 주로 단어의 출현 여부나 빈도를 나타내는데 사용된다.
, Bag-of-Words(BOW) 모델에서는 각 단어의 출현 여부를 1로 표시하므로, 짧은 문서에서는 1의 비율이 많아질 수 있다.
따라서 Sparse Embedding의 핵심은 대부분의 값이 0이라는 것이지만, 이는 대규모 데이터셋이나 긴 문서에서 더 두드러질뿐 Sparse Embedding 자체가 항상 0으로만 이루어진 것은 아니다.

  • 특징
    고차원 벡터(보통 수만 차원이다)
    대부분의 값이 0이다.
    정확한 단어 매칭에 유용하다.
    메모리 효율적이다(0이 아닌 값만 저장한다)

    예시 : TF-IDF 벡터
    문장: "The cat sat on the mat"
    [0, 0, 0, ..., 0.5, 0, ..., 0.3, 0, ..., 0.5, 0, ...]
    ^ ^ ^
    "cat" "sat" "mat"

코드 예시

from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [
    "The cat sat on the mat",
    "The dog ran in the park"
]

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)
print(X.toarray())

Bag-of-Words (BoW)

텍스트 데이터를 수치화하는 간단한 방법을 말한다.
각 문서를 단어의 출현 빈도로 표현한다.

  • 특징
  1. 각 문서에서 단어가 등장하는 횟수를 세어 벡터로 표현한다.
  2. 모든 문서에 등장하는 고유한 단어들의 집합을 기반으로 벡터의 차원이 결정된다.
  3. BoW는 단어의 순서를 고려하지 않으며, 단순히 빈도만을 반영한다.
  4. 대부분의 경우, 특정 문서에 등장하지 않는 단어들이 많아 0이 많이 포함된 희소 벡터가 된다.
  5. 대개 BoW를 구성하는 방법은 unigram이지만, bigram과 같은 n-gram 방식도 가능하다. 하나씩 보는게 아니라 양옆으로 해서 두개씩 보는 방식 또한 가능하다는 것.
  6. Term value의 결정 (단어의 중요성 결정)
    1) Term이 document에 등장하는지 (binary)
    2) Term이 몇번 등장하는지 (term frequency) 등의 방법으로 결정한다.

코드 예시

from sklearn.feature_extraction.text import CountVectorizer

# 샘플 문서들
documents = [
    'it was the best of times',
    'it was the worst of times'
]

# Bag-of-Words 모델 생성
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(documents)

# 결과를 배열로 변환
bow_array = X.toarray()

# 키워드 리스트 확보
feature_names = vectorizer.get_feature_names_out()

# 출력
print("BoW Array:")
print(bow_array)
print("\nFeature Names:")
print(feature_names)
출력결과
Bow Array :
[[1 1 1 1 1 0]
 [0 1 1 1 1 1]]
 
Feature Names:
['best', 'it', 'of', 'the', 'times', 'was', 'worst']

정리
Sparse Embedding의 경우에 단어장이 커질수록, N-gram의 n이 커질수록 그 크기가 매우 크게 증가한다.
Term overlap 즉, 단어가 들어가 있는지 확인해야할 때 매우 유용하지만, 비슷한 의미를 가졌지만 다른 단어인 경우에는 비교가 불가하다. 단어의 유의성에는 취약하다!

4.2 Dense Embedding

Dense Embedding의 경우 저차원 벡터로, 대부분의 값이 0이 아닌 밀집 벡터이다.
단어나 문장의 의미를 압축해서 표현한다.

  • 특징
    저차원 벡터 (보통 100~1000차원)
    대부분 값이 0이 아니다.
    의미적 유사성 포착에 유용하다.
    신경망 모델로 생성된다.

    예시 : Word2Vec 벡터(300차원)
    단어 : "cat"
    [0.2, -0.1, 0.5, ..., 0.3, -0.4, 0.1]

코드 예시

from gensim.models import Word2Vec

sentences = [["cat", "sat", "on", "the", "mat"],
             ["dog", "ran", "in", "the", "park"]]

model = Word2Vec(sentences, vector_size=100, window=5, min_count=1, workers=4)
print(model.wv["cat"])

4.3 비교

Sparse Embedding의 경우 정확한 키워드 매칭이 필요한 경우에 유용하며, Dense Embedding은 의미적 유사성을 고려한 검색이나 분류 작업에 적합하다.
최근에는 두 방식을 결합하여 사용하는 하이브리드 접근법도 많이 사용되고 있다.

특성Sparse EmbeddingDense Embedding
차원고차원 (수만)저차원 (100~1000)
값 분포대부분 0대부분 0이 아님
장점정확한 단어 매칭, 메모리 효율의미적 유사성 포착, 차원 축소
단점의미적 유사성 포착 어려움정확한 단어 매칭 어려움
대표 기법TF-IDF, BM25Word2Vec, BERT

Hybrid Search란 Sparse Embedding과 Dense Embedding의 장점을 결합하여 검색 성능을 향상시키는 방법을 의미한다.

  • 주요 특징
  1. Sparse Embedding의 정확한 키워드 매칭과 Dense Embedding의 의미적 유사성 포착 능력을 동시에 사용한다.
  2. 희귀 단어나 고유명사가 포함된 Query에서도 우수한 성능을 발휘한다.
  3. 자연어의 미묘한 차이를 처리하면서도 기존 임베딩 매칭 구도에서 추가적인 변환이나 특별한 처리 없이 기존 검색 시스템이나 도구에서 바로 사용 가능.
  • 구현 방식
  1. 단일 Sparse-Dense 인덱스 사용
    Pinecone에서는 단일 Sparse-Dense 인덱스를 사용하여 하이브리드 검색을 구현한다.
  2. 벡터 결합
    Sparse 벡터와 Dense 벡터를 하나의 복합 Dense 벡터로 결합한다.
  3. 가중치 조정
    alpha 파라미터를 통해 Sparse와 Dense 임베딩의 상대적 중요도를 조절할 수 있다.

  • 구현 단계
  1. Pinecone 설치 및 초기화
!pip install pinecone-client
import pinecone
import os

# Pinecone 초기화
pinecone.init(api_key=os.environ["PINECONE_API_KEY"], environment="us-west1-gcp")
  1. 인덱스 생성
index_name = "hybrid-search-example"
if index_name not in pinecone.list_indexes():
    pinecone.create_index(
        index_name,
        dimension=384,  # OpenAI ada-002 모델의 차원
        metric="cosine",
        pod_type="p1"
    )

# 인덱스 연결
index = pinecone.Index(index_name)
  1. 데이터 준비 및 임베딩
from sentence_transformers import SentenceTransformer
from sklearn.feature_extraction.text import TfidfVectorizer

# 샘플 데이터
texts = [
    "The quick brown fox jumps over the lazy dog",
    "A fast orange fox leaps above a sleepy canine",
    "The lazy dog sleeps all day long"
]

# Dense 임베딩
model = SentenceTransformer('all-MiniLM-L6-v2')
dense_vectors = model.encode(texts)

# Sparse 임베딩
vectorizer = TfidfVectorizer()
sparse_vectors = vectorizer.fit_transform(texts)
  1. 데이터 삽입
for i, (dense, sparse) in enumerate(zip(dense_vectors, sparse_vectors)):
    sparse_dict = {str(idx): value for idx, value in zip(sparse.indices, sparse.data)}
    index.upsert(
        vectors=[
            {
                "id": f"vec{i}",
                "values": dense.tolist(),
                "sparse_values": sparse_dict,
                "metadata": {"text": texts[i]}
            }
        ]
    )
  1. Hybrid Search 수행
# 검색 쿼리
query = "fast fox"

# Dense 쿼리 벡터
dense_query = model.encode([query])[0].tolist()

# Sparse 쿼리 벡터
sparse_query = vectorizer.transform([query])
sparse_query_dict = {str(idx): value for idx, value in zip(sparse_query.indices, sparse_query.data)}

# Hybrid Search 실행
search_results = index.query(
    vector=dense_query,
    sparse_vector=sparse_query_dict,
    top_k=2,
    include_metadata=True
)

# 결과 출력
for result in search_results['matches']:
    print(f"ID: {result['id']}")
    print(f"Score: {result['score']}")
    print(f"Text: {result['metadata']['text']}")
    print("---")

예상 출력

ID: vec1
Score: 0.9876543
Text: A fast orange fox leaps above a sleepy canine
---
ID: vec0
Score: 0.8765432
Text: The quick brown fox jumps over the lazy dog
---

5. TF-IDF(Term Frequency - Inverse Documnet Frequency)

문서에서 단어의 중요성을 평가하는 통계적 방법이다. 각 단어의 빈도와 문서 내에서의 희소성을 결합하여 가중치를 계산한다.

  • TF
    특정 단어가 문서에 등장하는 빈도를 의미한다.
    TF(t,d)=단어 t의 등장 횟수문서 d의 총 단어 수TF(t, d) = \frac{\text{단어 } t \text{의 등장 횟수}}{\text{문서 } d \text{의 총 단어 수}}
  • IDF
    단어가 전체 문서에서 얼마나 흔하지 않은지를 나타낸다.
    IDF(t)=log(Ndf(t))IDF(t) = \log \left( \frac{N}{df(t)} \right)

    ( N )은 전체 문서의 수
    ( df(t) )는 단어 ( t )가 등장한 문서의 수

  • TF-IDF
    둘을 곱하여 특정 단어의 중요성을 평가한다.
    TF(t,d) X IDF(t,D)

코드예시

from sklearn.feature_extraction.text import TfidfVectorizer

# 샘플 문서들
documents = [
    'it was the best of times',
    'it was the worst of times'
]

# TF-IDF 벡터화
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documents)

# 결과를 배열로 변환
tfidf_array = tfidf_matrix.toarray()

# 키워드 리스트 확보
feature_names = vectorizer.get_feature_names_out()

# 출력
print("TF-IDF Array:")
print(tfidf_array)
print("\nFeature Names:")
print(feature_names)

실행 결과

TF-IDF Array:
[[0.         0.4472136  0.         0.4472136  0.4472136  0.4472136]
 [0.4472136  0.4472136  0.4472136  0.         0.         0.4472136]]
Feature Names:
['best', 'it', 'of', 'the', 'times', 'was', 'worst']

5.1 TF-IDF 점수 특징

  1. 'a', 'the'와 같은 관사의 경우
    TF는 높을 수 있으나, IDF가 0에 가깝기에 TF-IDF 점수가 낮다
  2. 고유 명사의 경우(사람, 지명)
    IDF가 커지면서 전체적인 TF-IDF 값이 증가해서 TF-IDF 점수가 높다.

5.2 TF-IDF를 활용하여 Query에 맞는 문서 파싱해주기

TF-IDF와 코사인 유사도를 이용하여 Query와 가장 유사한 문서를 찾는 예시코드

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# 샘플 문서 집합
documents = [
    "The quick brown fox jumps over the lazy dog",
    "A fast brown dog outpaces a quick fox",
    "The lazy cat sleeps all day",
    "A brown fox is quick and cunning"
]

# 쿼리 문장
query = "quick brown fox"

# TF-IDF 벡터라이저 초기화
vectorizer = TfidfVectorizer()

# 문서 집합에 대해 TF-IDF 행렬 생성
tfidf_matrix = vectorizer.fit_transform(documents)

# 쿼리에 대해 TF-IDF 벡터 생성
query_vector = vectorizer.transform([query])

# 코사인 유사도 계산
cosine_similarities = cosine_similarity(query_vector, tfidf_matrix).flatten()

# 유사도가 높은 순서대로 문서 인덱스 정렬
sorted_indices = np.argsort(cosine_similarities)[::-1]

# 결과 출력
print("Query:", query)
print("\nRanked documents by similarity:")
for idx in sorted_indices:
    print(f"Document {idx}: {documents[idx]}")
    print(f"Similarity: {cosine_similarities[idx]:.4f}")
    print()
  • TfidfVectorizer : 문서를 TF-IDF 벡터로 변환한다.
  • vectorizer.transform(documnets)를 사용하여 문서 집합에 대한 TF-IDF 행렬을 생성한다.
  • vectorizer.transform([query])를 사용하여 query에 대한 TF-IDF 벡터를 구한다.

6. BM25

BM25는 정보 검색 분야에서 널리 사용되는 랭킹 알고리즘이며, TF-IDF와 마찬가지로 Sparse Embedding 방법이다.
document와 query 간의 유사도를 계산하는데 사용되며, BM25를 이용하여 문서를 파싱하고 유사도를 계산할 수 있다.

  • 특징
    BM25는 TF-IDF의 개선된 버전으로, 다음과 같은 특징을 가진다.
    1. 문서 길이를 고려하여 평균 길이보다 더 작은 문서에서 단어가 찾아진 경우에는 그 문서에 대해 높은 가중치를 줌.
    2. 단어 빈도(TF)에 상한을 두어 일정 범위 안에서 값을 가지도록 함.
    3. IDF 계산 방식을 개선.
  • 계산식
    score(D,Q)=i=1nIDF(qi)f(qi,D)(k1+1)f(qi,D)+k1(1b+bDavgdl)\text{score}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{\text{avgdl}})}

  • f(qi,D)f(q_i, D)는 문서 D에서 쿼리 단어 qiq_i의 빈도수

  • D|D|는 문서 D의 길이

  • avgdlavgdl은 평균 문서 길이

  • k1k_1bb는 조정 가능한 매개변수 (일반적으로 k1=1.2,b=0.75k_1 = 1.2, b = 0.75)

  • BM25 이용하여 유사한 문서 파싱하기
from rank_bm25 import BM25Okapi
import numpy as np

# 샘플 문서 집합
corpus = [
    "The quick brown fox jumps over the lazy dog",
    "A fast brown dog outpaces a quick fox",
    "The lazy cat sleeps all day",
    "A brown fox is quick and cunning"
]

# 문서 토큰화
tokenized_corpus = [doc.lower().split() for doc in corpus]

# BM25 모델 생성
bm25 = BM25Okapi(tokenized_corpus)

# 쿼리 설정
query = "quick brown fox"
tokenized_query = query.lower().split()

# 문서 스코어 계산
doc_scores = bm25.get_scores(tokenized_query)

# 상위 N개 문서 가져오기
top_n = 2
top_docs = bm25.get_top_n(tokenized_query, corpus, n=top_n)

print("문서 스코어:", doc_scores)
print(f"\n상위 {top_n}개 문서:")
for doc in top_docs:
    print(doc)
profile
헤매는 만큼 자기 땅이다.

0개의 댓글