8장. 최종 프로젝트
평가표: 적중률과 모르면 모른다
시험을 앞둔 학생은 모의고사를 봅니다. 모의고사는 실제 시험과 같은 꼴의 문제와 정답지가 있어서, 몇 개를 맞혔는지와 어느 단원에서 틀렸는지를 알려 줍니다. 모르는 문제에 아무 번호나 찍는 것보다 비워 두는 편이 나은 시험도 있습니다. 챗봇의 평가도 모의고사입니다. 평가 질문과 정답 위치를 미리 정해 두고, 정답 조각을 찾았는지, 근거를 보여 주었는지, 문서에 없는 질문에 모른다고 했는지를 셉니다.
세 상자가 이 장의 평가 항목입니다. 파랑 상자의 검색 적중은 hit@1 과 hit@3 으로, 보라 상자의 근거 표시는 답 아래에 조항과 쪽을 붙였는지로, 하늘 상자의 "모르면 모른다"는 범위 밖 질문에 "찾지 못했습니다"라고 했는지로 잽니다. 여기서 모의고사 문제가 평가 질문, 정답지가 정답 위치, 비워 둔 답이 "찾지 못했습니다"입니다. 비유와 다른 점은, 모의고사는 문제를 바꾸지 않지만 챗봇의 평가 질문은 시연에서 새로 들은 말투를 더해 가며 늘린다는 것입니다.
평가 항목
| 항목 | 재는 법 | 어디서 배웠나 |
|---|---|---|
| 검색 적중 | 정답 조각이 위 1개(hit@1), 위 3개(hit@3) 안에 든 질문의 비율 | 6장 실습 3 |
| 핵심어 | 답이나 근거 조각에 정답의 핵심어(예: 8만 원)가 있나 | 6장 프로젝트 |
| 근거 표시 | 답 아래에 조항과 쪽을 코드가 붙였나 | 6·7장 |
| 모르면 모른다 | 범위 밖 질문에 정해 둔 말로 답했나, 있는 질문을 지나치게 거절하지 않았나 | 6장 |
| 키 노출 | 코드와 설정 파일에 키 꼴의 글이 없나 | 7장 프로젝트 |
문턱으로 "모르면 모른다" 정하기
FAISS 의 점수는 거리라서 작을수록 가깝습니다(6장). 범위 밖 질문도 검색기는 무언가를 가장 가까운 조각으로 내놓으므로, 1위 거리가 어떤 값보다 멀면 모델을 부르지 않고 "찾지 못했습니다"라고 답하게 합니다. 그 값이 문턱입니다. 문턱은 평가 질문에서 맞게 찾은 거리들과 범위 밖 질문의 거리들 사이에 둡니다.
| 문턱을 | 생기는 일 |
|---|---|
| 너무 작게(엄격하게) | 있는 질문도 "찾지 못했습니다"라고 한다. 쓰는 사람이 답답하다 |
| 너무 크게(느슨하게) | 범위 밖 질문에도 엉뚱한 조항으로 답한다. 틀린 답을 믿게 된다 |
| 두 무리 사이에 | 맞게 찾은 질문은 답하고 범위 밖 질문은 넘긴다 |
짧은 예시
프로젝트 1의 2·3단계 결과에서 다섯 질문의 검색 기록과 거리 몇 개를 옮겨 적어 hit@k 와 문턱을 셈해 봅니다.
# 질문 다섯의 검색 기록: (정답 조항, 찾은 조항을 가까운 순서로). 프로젝트 1의 결과에서 옮겨 적었다
records = [
("제2조", ["제2조", "제3조", "제1조"]),
("제3조", ["제7조", "제5조", "제11조"]),
("제7조", ["제3조", "제8조", "제2조"]),
("제11조", ["제9조", "제11조", "제2조"]),
("제12조", ["제12조", "제6조", "제2조"]),
]
def hit_at(records, k):
"""정답이 위 k 개 안에 든 질문의 비율"""
return sum(answer in found[:k] for answer, found in records) / len(records)
for k in (1, 3):
print(f"hit@{k}:", hit_at(records, k))
# 문턱 고르기: 맞게 찾은 질문의 1위 거리와, 문서에 없는 질문의 1위 거리
inside = [1.592, 1.381, 1.664]
outside = [1.689, 1.84, 1.68]
threshold = round((max(inside) + min(outside)) / 2, 2)
print("문턱:", threshold, "/ 두 무리 사이의 틈:", round(min(outside) - max(inside), 3))hit@1: 0.4
hit@3: 0.6
문턱: 1.67 / 두 무리 사이의 틈: 0.016| 결과 | 읽는 법 |
|---|---|
hit@1: 0.4 | 다섯 질문 가운데 첫째 조각이 정답인 것은 제2조와 제12조의 둘, 5개 가운데 2개다 |
hit@3: 0.6 | 셋째 안까지 보면 둘째에 있던 제11조가 더해져 3개다. 제3조와 제7조는 셋 안에도 없다 |
문턱: 1.67 | 맞게 찾은 가장 먼 거리 1.664 와 범위 밖의 가장 가까운 거리 1.68 의 한가운데다 |
틈: 0.016 | 두 무리 사이가 아주 좁다. 새 질문 하나가 들어오면 어느 쪽으로든 넘어갈 수 있다 |
answer in found[:k] 는 True 나 False 이고, sum 은 True 를 1 로 셉니다. 틈이 좁다는 것은 연습용 임베딩이 "가까움"과 "멂"을 또렷하게 가르지 못한다는 뜻입니다. 낱말의 뜻을 아는 진짜 임베딩은 이 틈이 더 벌어지리라 기대하지만, 이 교안을 만든 PC 에는 키가 없어 재 보지 못했습니다.
코드가 의미하는 것
이 코드는 모의고사 답안지를 채점하고 합격선을 정하는 일을 옮긴 것입니다. 검색 기록으로 hit@k 를 세고, 두 무리의 거리 사이에 문턱을 두는 두 동작입니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
records = [("제2조", [...]), ...] | 질문마다 정답과 찾은 순서를 짝짓는다 | 평가 질문 한 줄이 정답지 한 줄입니다. 정답 위치를 미리 적어 두어야 채점할 수 있습니다 |
answer in found[:k] | 정답이 위 k 개 안에 있는지 본다 | 가까운 조각을 몇 개 모델에게 넘겨야 근거에 정답이 들어가는지를 잽니다 |
sum(...) / len(records) | 맞힌 수를 질문 수로 나눈다 | 비율로 두어야 질문 수가 다른 평가끼리도 견줄 수 있습니다 |
(max(inside) + min(outside)) / 2 | 두 무리의 경계 한가운데를 고른다 | "모르면 모른다"의 경계(문턱)를 감이 아니라 평가 결과로 정합니다 |
결과에서 읽을 것은 두 가지입니다.
- hit@1 과 hit@3 의 차이가 넘길 조각 수를 정합니다. 첫째 조각만 근거로 쓰면 다섯 가운데 둘(0.4), 셋을 함께 넘기면 셋(0.6)에서 정답 조항이 모델에게 갑니다.
- 문턱은 정했지만 틈이 좁습니다. 문턱 1.67 양쪽의 틈이 0.016 이라, 이 문턱이 새 질문에서도 맞으리라고 믿기는 어렵습니다.
주의할 점
- 평가 질문에 맞춰 고친 것(별칭 사전, 문턱)은 그 질문에서 좋아 보이기 마련입니다. 고친 뒤에는 평가 질문에 없던 새 말투의 질문으로 따로 잽니다. 프로젝트 1의 3단계가 그렇게 합니다.
- 자동 채점은 글자만 봅니다. 핵심어가 "8만 원"인데 답이 "80,000원"이면 틀렸다고 셉니다. 자동으로 틀린 줄은 눈으로 한 번 읽습니다.