지난 편에서 쿠키는 방 찾는 법을 두 개로 늘렸다.
목차로 찾기(BM25)랑 느낌으로 찾기(Dense)를 합쳐서 쓰니까 정답 방이 후보 안에 들어올 확률이 확 올라갔다.
근데 문제가 하나 더 있었다.
쿠키 눈은 두 개인데 방은 409개다.
쿠키한테 "월세 지원 있어?" 하고 물어봤다.
쿠키가 마인드팰리스에 가서 방을 찾아야 한다.
근데 방이 409개다.
쿠키가 질문을 들고 1번 방 문 열어보고, "이거 맞나?" 비교한다.
그 다음 2번 방 문 열어보고, 비교한다.
3번, 4번, 5번...
409번 방까지 전부 비교한다.
눈이 두 개밖에 없으니까 질문이랑 문서를 나란히 놓고 한 글자 한 글자 대조하면서 읽는 수밖에 없다. 409번 반복하면...
이게 BERT가 하는 짓이다.
Cross-Encoder라고 부른다.
질문이랑 문서를 한 쌍으로 묶어서 BERT한테 넣으면
"이 둘이 얼마나 관련 있는지" 점수를 준다.
근데... 진짜 느리다.
원 논문(Reimers & Gurevych, 2019)에 나온 숫자:
10,000개 문장 쌍을 비교하는 데 약 65시간.
65시간이다. 65시간이면 파리까지 왕복 두 번에 편도로 또 갈 수 있다.
"아니 이걸 내가 왜 다 봐야하지?"
맞다. 409개 방을 전부 비교할 필요 없다.
먼저 빠르게 후보 10개만 추리고, 그 10개만 꼼꼼하게 비교하면 된다 이말이야.
409번 비교할 걸 10번으로 줄이는 거다.
이걸 하려면 두 종류의 찾는 놈이 필요하다.
이 논문이 만든 게 이거다.
기존 BERT(Cross-Encoder)는 질문이랑 문서를 한 쌍으로 묶어서 넣는데 그거 꽤 정확함.
근데 할 때마다 BERT를 돌려야 한다. 409개면 409번 다 돌려야 함.
SBERT(Sentence-BERT)는 다르다.
질문은 질문대로, 문서는 문서대로 따로따로 벡터로 바꿔놓는다.
쿠키한테 비유하면 이렇다.
Cross-Encoder는 쿠키가 질문이랑 문서를 같이 들고 눈 두 개로 한 글자 한 글자 대조하면서 읽는 거다.
Bi-Encoder는 쿠키가 문서 409개를 미리 다 읽어서 요약 메모를 만들어놓는 거다. 질문이 오면 질문 메모만 만들고, 기존 메모들이랑 비교만 하면 된다.
원본 펼쳐서 대조하는 거랑 메모끼리 비교하는 거. 뭐가 더 빠를지는 뻔하다.
원 논문 숫자:
10,000개 문장 비교: 65시간 → 약 5초.
이거갖고는 파리는 커녕 어디 화장실도 못 감
어떻게 이게 되냐면 Siamese Network(샴 네트워크)라는 구조를 쓴다. 같은 BERT를 두 개 나란히 놓고, 하나는 질문을 넣고 하나는 문서를 넣는다. 일단 임베딩해서 벡터공간에 던져놓고 가까우면 그건 관련 있는 거로 본다.
학습할 때 "이 질문이랑 이 문서는 관련 있다/없다"를 가르치면 BERT가 관련 있는 것끼리 가까운 벡터를 뽑도록 학습된다.
빠르긴 한데 정확도가 떨어진다.
질문이랑 문서를 따로따로 벡터로 바꾸니까 둘 사이의 미세한 관계를 못 잡는다.
Cross-Encoder는 질문이랑 문서를 한 번에 넣어서 둘 사이의 단어 하나하나를 서로 참조한다.
Bi-Encoder는 질문 벡터랑 문서 벡터를 따로 만들어서 거리만 재니까 이런 세밀한 비교가 안 된다.
쿠키가 만들어놓은 요약 메모에 "주거 지원"이라고 적혀 있으면 "월세 보조금"이랑 "전세 대출"이 같은 메모로 묶이긴 한다. 근데 실제로 펼쳐보면 하나는 현금 지원이고 하나는 대출이다. 메모만 보면 이 차이를 못 잡는다.
| Bi-Encoder (SBERT) | Cross-Encoder (BERT) | |
|---|---|---|
| 속도 | 5초 (10,000개) | 65시간 (10,000개) |
| 정확도 | 좀 떨어짐 | 높음 |
| 쿠키가 하는 짓 | 메모끼리 비교 | 눈 두 개로 원본 대조 |
| 용도 | 후보 빠르게 추리기 | 후보 중에서 정답 고르기 |
여기서 나온 게 retrieve-and-rerank.
1단계: Bi-Encoder가 빠르게 후보 10~20개를 추린다 (retrieve)
2단계: Cross-Encoder가 그 10~20개만 꼼꼼하게 다시 줄 세운다 (rerank)
409개를 전부 Cross-Encoder로 비교하면 파리 왕복 두 번 반이지만,
Bi-Encoder로 10개 추리고 Cross-Encoder로 10개만 비교하면 몇 초면 끝난다.
쿠키한테 시키면 이런 느낌이다:
일단 요약보고 후보 추려서 그걸 이제 제대로 보는 그런 느낌.
복지나침반에서 이 패턴을 그대로 썼다.
1단계 (retrieve): BM25 + Dense 앙상블로 후보 20개를 뽑는다.
Bi-Encoder 역할.
2단계 (rerank): BGE Reranker v2-m3로 20개를 다시 줄 세운다.
Cross-Encoder 역할.
리랭커를 고를 때 4종을 비교했다.
| 리랭커 | 결과 |
|---|---|
| 없음 (앙상블만) | Hit@5 50.9% |
| Cohere API | 개선됨, 근데 유료 ($1/1K) |
| ko-reranker | 한국어 특화인데 기대만큼 안 나옴 |
| BGE v2-m3 | Hit@5 84.3%, 로컬 무료 |
채택 기준은 5% 이상 개선이었는데 BGE가 +33.4%p를 찍었다.
Cohere는 성능도 괜찮았지만 무료티어는 너무 오래 걸리고 제한도 있어서 로컬 BGE로 갔다.
최종 결과:
메모로 대충 추리는 놈(BM25+Dense)이 후보를 뽑고, 원본 대조하는 놈(BGE Reranker)이 다시 확인한다.
이 논문이 제안한 retrieve-and-rerank 패턴이 실제로 잘 된다.
위에서 비유를 좀 끼워 맞춘 부분이 있다.
BM25는 Bi-Encoder가 아니다. BM25는 키워드 매칭이고 Bi-Encoder는 벡터 유사도다. "빠르게 후보를 추린다"는 역할이 같아서 retrieve 단계로 묶은 거다.
BGE Reranker도 순수 Cross-Encoder는 아니다. Cross-Encoder 계열이긴 한데 BERT 원본이랑 구조가 좀 다르다. 논문의 개념을 실무에 가져올 때는 이런 변형 모델을 쓴다는 정도.
5초 vs 65시간은 2019년 기준이다. 지금은 하드웨어도 좋아지고 최적화도 많이 됐으니까 비율은 비슷해도 절대 시간은 다를 수 있다.
409개는 우리 복지나침반에 있는 정책수이다. 서비스에서 신청 기간 + 서울 청년 조건으로 좁혀도 이만큼 남는다. "409개쯤이야 다 보면 되지"가 아니라 매 질의마다 이 규모에서 빠르게 판단해야 한다는 얘기다.
그러니까 위에서 쿠키 메모 비유 너무 곧이곧대로 받아들이지 말라는 얘기
쿠키한테 빠른 놈이랑 꼼꼼한 놈을 팀으로 묶어줬다.
근데 이 팀이 진짜 잘 되는지는 좀 더 봐야 안다.
Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks
Reimers & Gurevych, 2019
https://aclanthology.org/D19-1410/