7장. 자체 임베딩과 벡터DB, 검색 품질
재정렬
서류 심사로 지원자 열 명을 추린 뒤, 면접으로 한 사람씩 자세히 보고 순서를 다시 정합니다. 수백 명을 모두 면접할 수는 없으니 빠른 심사로 줄이고, 줄인 후보만 꼼꼼히 봅니다. 재정렬이 이 면접입니다.
왼쪽 바이 인코더(이 장의 e5)는 문서 임베딩을 미리 만들어 두고 질문만 새로 임베딩하므로 빠릅니다. 오른쪽 크로스 인코더는 질문과 문서를 한 덩어리로 읽어 관련 점수를 내므로, 둘 사이의 낱말 관계를 꼼꼼히 봅니다. 대신 후보마다 모델을 한 번씩 돌려야 해 느립니다. 여기서 서류 심사가 바이 인코더, 면접이 크로스 인코더입니다.
두 짜임 견주기
| 견줄 것 | 바이 인코더 | 크로스 인코더 |
|---|---|---|
| 입력 | 질문, 문서를 따로 | (질문, 문서) 쌍 |
| 미리 해 둘 수 있나 | 문서 임베딩을 미리 만든다 | 못 한다. 질문이 와야 계산한다 |
| 속도 | 수만 개도 빠르다 | 후보 수십 개가 알맞다 |
| 정확도 | 좋다 | 대체로 더 좋다 |
| 쓰는 곳 | 1단계: 후보 찾기 | 2단계: 후보 다시 줄 세우기 |
크로스 인코더 쓰는 꼴
sentence-transformers 의 CrossEncoder 는 (질문, 문서) 쌍의 목록을 받아 점수 목록을 돌려줍니다. 아래 줄은 꼴만 보이고, 실제로는 실습 6에서 돌립니다.
question = "로그인을 여러 번 실패해서 들어갈 수가 없어요."
candidates = ["## 계정 잠김\n비밀번호를 5번 틀리면 계정이 30분 동안 잠깁니다.",
"## 2단계 인증\n로그인할 때는 휴대폰 인증 앱으로 2단계 인증을 합니다."]
pairs = [(question, c) for c in candidates] # 크로스 인코더에는 쌍으로 넣는다
print(len(pairs), "쌍:", pairs[0][1].split("\n")[0], "/", pairs[1][1].split("\n")[0])
# scores = CrossEncoder("cross-encoder/mmarco-mMiniLMv2-L12-H384-v1").predict(pairs) ← 실습 62 쌍: ## 계정 잠김 / ## 2단계 인증 이 나옵니다. 크로스 인코더의 점수는 확률이 아니라 크기만 견주는 수(음수도 나온다)입니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
candidates | 1단계 검색(바이 인코더)이 찾아 온 후보 조각들이다 |
[(question, c) for c in candidates] | 질문 하나를 후보마다 짝지어 (질문, 조각) 쌍의 목록을 만든다 |
pairs[0][1].split("\n")[0] | 첫 쌍의 둘째 칸(조각)에서 첫 줄(제목)만 꺼낸다 |
CrossEncoder("cross-encoder/mmarco-mMiniLMv2-L12-H384-v1") | 재정렬 모델을 불러온다. 이름의 mmarco 는 여러 언어로 옮긴 MS MARCO 검색 데이터, L12 는 층 12개, H384 는 좌표 384개라는 뜻이다. 여기서는 주석으로만 보였다 |
.predict(pairs) | 쌍마다 관련 점수 하나를 돌려준다. predict 는 "예측하다"라는 뜻이다 |
이 코드가 뜻하는 것
면접장에 지원자를 들여보내기 전에 "면접관(질문)과 지원자(조각)"를 한 조씩 짝짓는 단계입니다. 크로스 인코더는 이 한 조를 함께 보고 점수를 매기므로, 질문을 조각마다 하나씩 붙여 넣어야 합니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
question = ... | 사용자의 질문 | 면접관의 질문지 |
candidates = [...] | 1단계가 찾아 온 후보 둘 | 서류 심사를 통과한 지원자 둘 |
pairs = [...] | 질문과 후보를 하나씩 짝짓는다 | 면접 조를 짠다 |
print(len(pairs), ...) | 쌍의 수와 각 쌍의 제목을 확인한다 | 면접 순서표를 본다 |
# scores = ...predict(pairs) | 쌍마다 점수를 매긴다(실습 6) | 면접 점수 |
질문 "로그인을 여러 번 실패해서"와 "비밀번호를 5번 틀리면"은 낱말이 다릅니다. 크로스 인코더는 두 글을 한 덩어리로 읽기 때문에 이런 관계를 바이 인코더보다 꼼꼼히 볼 수 있습니다.
점수로 다시 줄 세우기
predict 가 점수를 돌려준 뒤에 하는 일은 "점수가 큰 순서로 다시 줄 세우기"뿐입니다. 아래 점수는 모델을 돌린 값이 아니라 설명을 위해 손으로 정한 숫자입니다.
first = ["2단계 인증", "계정 잠김", "비밀번호 주기", "보안 인쇄"] # 1단계(바이 인코더)가 찾은 순서
cross = [1.2, 6.5, 0.4, -3.1] # 쌍마다 크로스 인코더 점수(설명용 숫자)
pairs = sorted(zip(cross, first), reverse=True) # 점수가 큰 순서로 다시 줄 세운다
for new_rank, (s, title) in enumerate(pairs, start=1):
old_rank = first.index(title) + 1
print(f"{new_rank}등 {title} / 점수 {s} / 전에는 {old_rank}등")1등 계정 잠김 / 점수 6.5 / 전에는 2등
2등 2단계 인증 / 점수 1.2 / 전에는 1등
3등 비밀번호 주기 / 점수 0.4 / 전에는 3등
4등 보안 인쇄 / 점수 -3.1 / 전에는 4등| 결과 | 읽는 법 |
|---|---|
| 1등 계정 잠김 | 1단계에서 2등이던 조각이 가장 높은 점수(6.5)를 받아 1등으로 올라섰습니다 |
| 2등 2단계 인증 | 1단계의 1등은 점수 1.2 로 한 칸 내려갔습니다 |
| 4등 보안 인쇄 점수 -3.1 | 점수가 음수여도 오류가 아닙니다. 다른 후보보다 관련이 적다는 뜻일 뿐입니다 |
후보 넷은 그대로이고 순서만 바뀌었습니다. 이것이 "재정렬은 후보 안에서만 순서를 바꾼다"는 말의 뜻입니다.
주의할 점
- 재정렬은 1단계가 찾아 온 후보 안에서만 순서를 바꿉니다. 정답이 후보 10개 안에 없으면 재정렬로도 찾지 못합니다.
- 후보를 늘리면 정답이 들어올 가능성은 커지지만 그만큼 느려집니다. 이 장은 후보 10개를 다시 줄 세웁니다.