Post

아무튼 RRF보다 좋다는 CC

Information Retrieval에 있어서, 여러 Retrieval 기법들을 합쳐서 사용하는 Hybrid Retrieval이 존재한다. 마치 여러 모델에서 나온 결과를 합치고 합치는 Ensemble 기법과 뭔가 유사하다.

아무튼 이런 Hybrid Retrieval을 하기 위해서는 각 Retrieval들에게서 나온 점수들을 적절한 방식으로 합쳐야 하는데, 그 합치는 방식 중 가장 유명한 것이 RRF이다.

다른 분께서 RRF에 대해 여기에 엄청 잘 정리를 해 주셨으니, 깊은 이해를 원한다면 참고하길 바란다.

근데 아무튼 RRF보다 CC가 더 좋다는 Pinecone의 논문이 있어서 가져와 보았다. Pinecone은 유명한 벡터 DB인데, 나름 믿음이 가는 출처이다.

CC가 뭔데?

CC는 진짜 쉽다. 간단하게 가중합이라고 생각하면 된다. 그렇다. weighted sum이다.

\[f_{CC}(q,d) = \alpha\pi_{LEX}(q,d) + (1 - \alpha)\pi_{SEM}(q,d)\]

갑자기 복잡한 수식이 튀어나왔는데, 사실 엄청 쉽다. 먼저, $f_{CC}(q,d)$는 두 가지 방법의 점수를 더해서 구한 최종 점수이다. 이 점수를 통해서 Hybrid Retrieval의 순위를 정하면 된다. 그 순위의 top-k개를 return하면 끝이다. $\pi_{LEX}(q,d)$는 lexical score인데, 그냥 BM25 같은 TF-IDF 방법으로 구한 점수다. $\pi_{SEM}(q,d)$는 semantic score로, 그냥 임베딩해서 similarity score 구한 거라고 생각하면 쉽다. $\alpha$는 그냥 임의의 가중치이다.

그러면 CC가 왜 가중합인지 알겠는가? 그냥 BM25 점수에 가중치 곱하고, similarity 점수에 가중치 곱해서 더한 것이다. 이 때 가중치의 합은 1이 되어야 하고.

쉬운 예시는, BM25점수가 1이고, similarity 점수가 3이라고 하자. 가중치가 대충 0.3, 0.7이면 최종 점수는 이렇다. \(1 \times 0.3 + 3 \times 0.7 = 0.3 + 2.1 = 2.4\) 참 쉽죠?

Normalize

근데 당연히 normalize를 꼭 해야 한다. BM25랑 similarity score가 비슷비슷한 점수 범위일 보장이 1도 없으니깐! 사실 이 부분에 대한 논문은 조금 어려워서 대충 읽었는데, 각각 min-max normalization 같은 방법으로 맞춘 다음에 CC를 하면 된다고 생각하면 편하다.

결론은?

그래서 RRF보다 CC가 더 성능이 좋다고 한다. 나는 뭐, 좋다니깐 쓰는 입장이니 왜 좋은가? 를 이해하는 것보다는 빠르게 가져다 쓰기로 했다.

그래서 RAGchain의 Hybrid Retrieval에는 RRF와 CC 모두가 담기게 되었다. 만약 코드를 구경해보고 싶다면 아래 코드를 넣어두겠다. 사실 그냥 RAGchain에서 Hybrid Retrieval을 바로 쓰는 것이 편할 것이다. 머리 아프게 구현하지 말고 그냥 RAGchain을 쓰자!

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from typing import List, Union
from uuid import UUID
import concurrent
import pandas as pd

def retrieve_id_with_scores(self, query: str, top_k: int = 5, *args, **kwargs) -> tuple[
    List[Union[str, UUID]], List[float]]:
    with concurrent.futures.ThreadPoolExecutor() as executor:
        futures = [executor.submit(self.retrieve_id_with_scores_parallel, retrieval, query, self.p, *args, **kwargs)
                   for retrieval in self.retrievals]

    if self.method == 'cc':
        scores_df = pd.concat([future.result() for future in futures], axis=1, join="inner")
        normalized_scores = (scores_df - scores_df.min()) / (scores_df.max() - scores_df.min())
        normalized_scores['weighted_sum'] = normalized_scores.mul(self.weights).sum(axis=1)
        normalized_scores = normalized_scores.sort_values(by='weighted_sum', ascending=False)
        return (list(map(self.__str_to_uuid, normalized_scores.index[:top_k].tolist())),
                normalized_scores['weighted_sum'][:top_k].tolist())
    elif self.method == 'rrf':
        scores_df = pd.concat([future.result() for future in futures], axis=1)
        rank_df = scores_df.rank(ascending=False, method='min')
        rank_df = rank_df.fillna(0)
        rank_df['rrf'] = rank_df.apply(self.__rrf_calculate, axis=1)
        rank_df = rank_df.sort_values(by='rrf', ascending=False)
        return (list(map(self.__str_to_uuid, rank_df.index[:top_k].tolist())),
                rank_df['rrf'][:top_k].tolist())
    else:
        raise ValueError("method should be either 'cc' or 'rrf'")

풀 코드는 여기에서!

This post is licensed under CC BY 4.0 by the author.