리랭커란,

Lost in Middle: 질문에 대한 관련 문서가 컨텍스트 중간에 위치할 경우, LLM 응답 정확도가 낮아진다.
(출처: AWS 기술 블로그, 한국어 Reranker를 활용한 검색 증강 생성(RAG) 성능 올리기 / 원 출처: Liu et al., 2023)
💡 가장 관련성 높은 문서를 상위권에 배치(순위 재정렬, Reranking)하기

그림 2: Encoder 종류 (a) Bi-encoder (b) Cross-encoder
(출처: AWS 기술 블로그, 한국어 Reranker를 활용한 검색 증강 생성(RAG) 성능 올리기)
💡 둘 다 의미를 계산한다. 차이는 "질문과 문서가 모델 안에서 언제 서로를 보느냐"이다.
[CLS] 질문 토큰들 [SEP] 문서 토큰들 [SEP]데뷔 토큰이 문서의 2022년, 데뷔하여 토큰에 직접 attention 할 수 있다.[CLS] 출력 → linear layer → 관련도 점수(실수 1개).파이썬: v3.8.16
transformers: v4.41.2
Dongjin-kr/ko-reranker · Hugging Face
BAAI/bge-reranker-large 기반 한국어 데이터에 대한 fine-tuned model
# 1. 모델과 데이터 처리에 필요한 라이브러리 불러오기
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
import numpy as np
# 2. 비교할 문장 쌍 데이터 설정 (질문과 4개의 후보 답변들)
pairs = [
['뉴진스는 몇 년도에 데뷔했나요?', '안녕하세요.'],
['뉴진스는 몇 년도에 데뷔했나요?', '뉴진스의 데뷔 연도는 2022년입니다.'],
['뉴진스는 몇 년도에 데뷔했나요?', '2022년에는 뉴진스, 르세라핌, 엔믹스가 데뷔했습니다.'],
['뉴진스는 몇 년도에 데뷔했나요?', '뉴진스는 2022년에 데뷔하여 Hype boy 등 여러 노래를 발표했습니다.'],
]
# 3. 로짓(모델의 원시 출력값)을 확률 분포(0~1)로 변환하는 정규화 함수 정의
def exp_normalize(x):
b = x.max() # 수치적 안정성을 위해(오버플로우 방지) 최댓값 추출
y = np.exp(x - b) # 각 값에서 최댓값을 뺀 후 자연상수 e의 지수승 계산
return y / y.sum() # 모든 값의 합으로 나누어 총합이 1이 되는 확률 값으로 반환
# 4. 사전 학습된 한국어 Reranker 모델의 Hugging Face 저장소 경로 지정
model_path = "Dongjin-kr/ko-reranker"
# 5. 지정된 경로에서 텍스트를 처리할 토크나이저와 모델 가중치 불러오기
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForSequenceClassification.from_pretrained(model_path)
# 6. 모델을 추론(평가) 모드로 전환 (학습용 기능인 Dropout 등 비활성화)
model.eval()
# 7. 기울기(Gradient) 계산을 비활성화하여 메모리 사용량을 줄이고 속도 향상
with torch.no_grad():
# 문장 쌍을 모델이 이해할 수 있는 토큰 형태로 변환 (패딩 및 절단 적용, 최대 512토큰)
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512)
# 모델에 토큰화된 입력을 통과시켜 연관성 점수(logits)를 계산하고, 1차원 실수 배열로 변환
scores = model(**inputs, return_dict=True).logits.view(-1, ).float()
# 텐서를 NumPy 배열로 변환한 뒤, 직접 정의한 함수를 통해 0~1 사이의 확률값으로 정규화
scores = exp_normalize(scores.numpy())
# 8. 계산된 확률값에 100을 곱해 퍼센트(%)로 바꾸고, 소수점 둘째 자리까지 반올림하여 출력
print(np.round(scores * 100, 2))
pairs의 각 원소가 [질문, 문서]이고, tokenizer(pairs, ...)가 이 둘을 하나의 시퀀스로 합친다. → 4개 쌍 = 시퀀스 4개 = cross-encoder 입력 4개.exp_normalize(softmax)의 의미model(**inputs).logits → shape (4, 1). 쌍 하나당 실수 1개..view(-1) → shape (4,)로 펴기.exp_normalize → 4개 값의 합이 1이 되도록 변환. 이 함수는 softmax와 같은 계산이다.[-5, 5, 3, 4]일 때 softmax → 약 [0.0%, 66.5%, 9.0%, 24.5%]안녕하세요.)와만 비교하면 logit [-5, 5] → 약 [0.0%, 100.0%]질문 → 1단계 Bi-encoder로 top-k 후보 추출 → 2단계 Cross-encoder로 후보 재점수 → 상위 n개를 컨텍스트 앞쪽에 배치 → LLM
top_k(리랭커에 넘길 후보 수)와 top_n(LLM에 넘길 문서 수)은 다른 값이다.top_k가 클수록 1차 검색에서 놓치는 문서가 줄지만, 리랭커 forward 횟수가 늘어난다.top_n이 작을수록 LLM에 전달되는 컨텍스트가 줄어든다.from sentence_transformers import SentenceTransformer
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
import numpy as np
# 문서 저장소
documents = [
'안녕하세요.',
'뉴진스의 데뷔 연도는 2022년입니다.',
'2022년에는 뉴진스, 르세라핌, 엔믹스가 데뷔했습니다.',
'뉴진스는 2022년에 데뷔하여 Hype boy 등 여러 노래를 발표했습니다.',
'르세라핌은 2022년 5월에 데뷔했습니다.',
'아이브는 2021년에 데뷔했습니다.',
]
query = '뉴진스는 몇 년도에 데뷔했나요?'
# 1단계: Bi-encoder 1차 검색
embedder = SentenceTransformer('BAAI/bge-m3')
doc_embs = embedder.encode(documents, normalize_embeddings=True) # 질문과 무관하게 미리 계산
query_emb = embedder.encode(query, normalize_embeddings=True)
sims = doc_embs @ query_emb # 정규화된 벡터의 내적 = 코사인 유사도
top_k = 4
candidate_idx = np.argsort(-sims)[:top_k]
candidates = [documents[i] for i in candidate_idx]
# 2단계: Cross-encoder 리랭킹
model_path = 'Dongjin-kr/ko-reranker'
tokenizer = AutoTokenizer.from_pretrained(model_path)
reranker = AutoModelForSequenceClassification.from_pretrained(model_path)
reranker.eval()
pairs = [[query, doc] for doc in candidates]
with torch.no_grad():
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512)
logits = reranker(**inputs, return_dict=True).logits.view(-1).float().numpy()
top_n = 2
order = np.argsort(-logits) # logit 그대로 내림차순 정렬
reranked = [candidates[i] for i in order[:top_n]]
# 3단계: 관련도 높은 문서를 컨텍스트 앞쪽에 배치하여 LLM에 전달
context = '\n'.join(f'[문서 {i + 1}] {doc}' for i, doc in enumerate(reranked))
prompt = f"""다음 문서를 참고하여 질문에 답하세요.
{context}
질문: {query}
답변:"""
# answer = call_llm(prompt) # 사용하는 LLM API로 대체
print(prompt)
| 방식 | 평가 메커니즘 | 특징 | 장단점 |
|---|---|---|---|
| Pointwise (포인트와이즈) | 하나의 쿼리와 단 하나의 문서를 독립적으로 비교하여 점수를 매김 | 주로 Cross-Encoder 모델 사용 | ➕ 속도가 빠르고 병렬 처리가 쉬움 ➖ 다른 문서들과의 상대적 우위를 반영하지 못함 |
| Pairwise (페어와이즈) | 하나의 쿼리에 대해 두 개의 문서를 짝지어 어느 쪽이 더 우수한지 비교 | 토너먼트 형식의 비교 가능 | ➕ 상대적 비교가 가능해짐 ➖ 문서 수가 많아질수록 비교 횟수가 제곱에 비례하여 증가 |
| Listwise (리스트와이즈) | 하나의 쿼리와 전체 후보 문서 목록을 동시에 입력받아 최종 순열(Permutation)을 한 번에 출력 | 최신 고성능 모델(LLM 기반)에서 주로 차용 | ➕ 문서 간 중복성, 다양성, 미세한 품질 차이를 가장 잘 포착함 ➖ 연산 비용이 크고 실시간 처리 속도(Latency)가 느림 |
Listwise 관련: ZipRerank
💡 처리 방식(병렬이냐 한 번이냐)은 결과로 따라오는 특징이다. 본질적인 차이는 "모델이 다른 후보를 보느냐"이다.
pairs 4개를 tokenizer(pairs, ...)로 한꺼번에 넣은 것이 batch 처리다.