리서치

LAB / 24번째 글

「답 없음」을 내는 유사도 임계값은 어디인가: 거짓 응답 5%에 답할 질의 63%를 버렸다

KorQuAD에서 정답 문단을 빼 답 없는 질의를 만들고 1위 코사인 점수에 임계값을 걸었다. 거짓 응답률을 5%로 누르면 답할 수 있는 질의의 63%도 함께 거절됐고, 같은 5%를 맞추는 임계값은 답 없는 질의를 어떻게 만드느냐에 따라 0.882와 0.855로 갈렸다.

PALDYN Team27 MIN READ

앞 글까지는 정답이 코퍼스 안에 반드시 있다는 전제로 순위만 쟀습니다. 실제 서비스에서는 그 전제가 자주 깨집니다. 사용자가 묻는 것이 우리 문서에 아예 없을 때도 검색은 무언가를 1위로 돌려주고, 생성 모델은 그 1위를 근거 삼아 그럴듯한 답을 씁니다. 이 글은 그 앞에서 「답 없음」을 내는 가장 흔한 장치, 곧 1위 유사도 점수에 임계값을 거는 방법이 실제로 얼마나 가르는지를 잽니다. 답할 수 없는 질문을 평가 셋에 왜 섞어야 하는지는 RAG 평가가 맡으므로 여기서는 임계값의 숫자만 봅니다.

결론부터 적습니다. 한 번에 갈리지 않았습니다. 답 없는 질의가 1위로 받는 점수와 답 있는 질의가 받는 점수가 크게 겹쳐서, 거짓 응답을 5%로 누르는 임계값을 걸면 답할 수 있는 질의의 63%도 함께 거절됩니다. 그리고 그 임계값 자체가 답 없는 질의를 어떻게 만들었느냐에 따라 0.882와 0.855로 달라졌습니다.

실험 설계

답 없는 질의

CPU만으로 세우는 검색 실험대의 한국어 쪽을 그대로 씁니다. KorQuAD 검증 분할의 문단 960개와 질의 5,774개이고, 질의마다 정답 문단이 하나씩 붙어 있습니다. 임베딩 모델은 intfloat/multilingual-e5-small이고 질의 앞에 query: , 문단 앞에 passage: 를 붙입니다.

정답이 코퍼스에 없는 질의는 따로 구할 수 없으므로 만듭니다. 문단 960개 중 절반을 코퍼스에서 빼면, 정답 문단이 빠진 질의는 그 순간 답이 코퍼스에 없는 질의가 됩니다. 이렇게 만든 것을 이 글은 답 없는 질의라고 부르고, 정답 문단이 남은 질의를 답 있는 질의라고 부릅니다. 질의와 문단은 한 번만 인코딩해 두고, 빼는 일은 유사도 행렬에서 빠진 문단의 열을 지우는 것으로 합니다. 그래서 시드를 바꿔 빼는 문단을 열 번 다시 골라도 인코딩은 한 번이면 됩니다.

두 가지 빼는 법

KorQuAD 문단은 위키백과 글 140편에서 왔고, 한 글에서 문단이 평균 일곱 개쯤 나옵니다. 그래서 무엇을 빼느냐가 두 갈래로 갈립니다.

  • 문단 단위로 뺀다. 960개 중 480개를 무작위로 뺍니다. 정답 문단이 빠져도 같은 위키백과 글의 다른 문단은 남아 있을 수 있습니다.
  • 글 단위로 뺀다. 140편 중 70편을 무작위로 골라 그 글의 문단을 통째로 뺍니다. 답 없는 질의의 주제가 코퍼스 어디에도 없습니다.

계획은 앞쪽만 적어 두었습니다. 뒤쪽을 더한 것은 답 없는 질의가 1위로 무엇을 받는지가 판정을 좌우할 것 같아서였고, 실제로 그랬습니다. 앞쪽은 「우리 문서에 그 주제는 있는데 그 질문의 답은 없다」에, 뒤쪽은 「아예 다른 주제를 물었다」에 가깝습니다.

판정 지표

임계값 하나를 정하고, 1위 점수가 그 값 이상이면 답하고 아래면 「답 없음」을 냅니다. 이때 세 비율을 셉니다.

  • 거짓 응답률: 답 없는 질의 가운데 임계값을 넘어 답해 버린 비율입니다. 코퍼스에 없는 것을 찾았다고 말한 비율이고, 이 글이 누르려는 값입니다.
  • 응답률: 답 있는 질의 가운데 임계값을 넘어 답한 비율입니다. 임계값을 올리면 함께 떨어집니다.
  • 정답 응답률: 답 있는 질의 가운데 답했고 그 1위가 정답 문단이기까지 한 비율입니다. 임계값이 없을 때는 Recall@1과 같습니다.

계획이 정한 판정은 「거짓 응답률 5% 이하로 누를 때 잃는 재현율」 한 줄입니다. 답 없는 질의 점수의 95번째 백분위수를 임계값으로 잡으면 거짓 응답률이 5%가 되고, 그때의 응답률과 정답 응답률을 읽습니다. 두 분포가 얼마나 겹치는지는 AUC로 따로 셉니다. 답 있는 질의 하나와 답 없는 질의 하나를 무작위로 뽑았을 때 앞쪽 점수가 더 높을 확률이고, 1이면 어떤 임계값으로 완벽히 갈리고 0.5면 동전 던지기입니다.

재현과 출력

재현 블록

pip install torch sentence-transformers datasets numpy
import time, numpy as np, torch
from datasets import load_dataset
from sentence_transformers import SentenceTransformer
torch.manual_seed(0); torch.set_num_threads(4)
val = load_dataset("KorQuAD/squad_kor_v1")["validation"]
paras = sorted({r["context"] for r in val}); pidx = {p: i for i, p in enumerate(paras)}
ptitle = {pidx[r["context"]]: r["title"] for r in val}
qs, gold = [r["question"] for r in val], np.array([pidx[r["context"]] for r in val])
qtitle = np.array([ptitle[g] for g in gold])
m = SentenceTransformer("intfloat/multilingual-e5-small"); t = time.perf_counter()
P = m.encode(["passage: " + p for p in paras], batch_size=32, normalize_embeddings=True, show_progress_bar=False)
Q = m.encode(["query: " + q for q in qs], batch_size=64, normalize_embeddings=True, show_progress_bar=False)
S = Q @ P.T
print(f"paragraphs={len(paras)} queries={len(qs)} encode={time.perf_counter()-t:.1f}s")
TS = np.round(np.arange(0.80, 0.921, 0.005), 3)
titles = sorted(set(ptitle.values()))
def run(seed, by_title=False, show=False):
    rng = np.random.default_rng(seed)
    if by_title: gone = set(rng.permutation(titles)[: len(titles) // 2]); keep = np.array([ptitle[i] not in gone for i in range(len(paras))])
    else: keep = np.ones(len(paras), bool); keep[rng.permutation(len(paras))[: len(paras) // 2]] = False
    Sk = np.where(keep, S, -9); top = np.argsort(-Sk, 1)[:, :2]
    s1 = Sk[np.arange(len(qs)), top[:, 0]]; mg = s1 - Sk[np.arange(len(qs)), top[:, 1]]
    ans, hit = keep[gold], top[:, 0] == gold
    auc = lambda x: (x[ans][:, None] > x[~ans][None, :]).mean()
    if show:
        print(f"split={'title' if by_title else 'paragraph'} seed={seed} answerable={ans.sum()} no-answer={(~ans).sum()} R@1(answerable)={hit[ans].mean():.4f}")
        print(f"top1 score  answerable: mean={s1[ans].mean():.4f} p5={np.percentile(s1[ans],5):.4f} p50={np.median(s1[ans]):.4f}"
              f" | no-answer: mean={s1[~ans].mean():.4f} p50={np.median(s1[~ans]):.4f} p95={np.percentile(s1[~ans],95):.4f}")
        same = np.array([ptitle[p] for p in top[~ans, 0]]) == qtitle[~ans]
        print(f"no-answer top1 from same article: {same.mean():.4f}")
        print(f"{'thr':>6} {'FAR':>7} {'answered':>9} {'correct':>8} {'wrong':>7}")
        for th in TS:
            a = s1 >= th
            print(f"{th:6.3f} {a[~ans].mean():7.4f} {a[ans].mean():9.4f} {(a & hit)[ans].mean():8.4f} {(a & ~hit)[ans].mean():7.4f}")
    th5 = np.percentile(s1[~ans], 95)
    return auc(s1), auc(mg), th5, (s1[ans] >= th5).mean(), ((s1 >= th5) & hit)[ans].mean(), hit[ans].mean()
run(0, show=True); run(0, by_title=True, show=True)
for bt in (False, True):
  R = np.array([run(s, bt) for s in range(10)])
  print(f"split={'title' if bt else 'paragraph'}: 10 seeds")
  print(f"{'seed':>4} {'AUC(top1)':>10} {'AUC(margin)':>12} {'thr@FAR5%':>10} {'answered':>9} {'correct':>8} {'R@1 ans':>8}")
  for s, r in enumerate(R): print(f"{s:>4} " + " ".join(f"{v:{w}.4f}" for v, w in zip(r, (10, 12, 10, 9, 8, 8))))
  print("mean " + " ".join(f"{v:{w}.4f}" for v, w in zip(R.mean(0), (10, 12, 10, 9, 8, 8))))
  print(" std " + " ".join(f"{v:{w}.4f}" for v, w in zip(R.std(0), (10, 12, 10, 9, 8, 8))))
python3 threshold.py

실제 출력

paragraphs=960 queries=5774 encode=81.2s
split=paragraph seed=0 answerable=2826 no-answer=2948 R@1(answerable)=0.8415
top1 score  answerable: mean=0.8732 p5=0.8307 p50=0.8737 | no-answer: mean=0.8440 p50=0.8423 p95=0.8825
no-answer top1 from same article: 0.6218
   thr     FAR  answered  correct   wrong
 0.800  0.9878    0.9996   0.8411  0.1585
 0.805  0.9729    0.9979   0.8401  0.1578
 0.810  0.9508    0.9961   0.8401  0.1561
 0.815  0.9162    0.9933   0.8379  0.1553
 0.820  0.8636    0.9827   0.8333  0.1493
 0.825  0.7995    0.9713   0.8277  0.1437
 0.830  0.7171    0.9536   0.8213  0.1323
 0.835  0.6286    0.9342   0.8096  0.1246
 0.840  0.5448    0.9076   0.7916  0.1161
 0.845  0.4583    0.8712   0.7640  0.1072
 0.850  0.3742    0.8323   0.7353  0.0970
 0.855  0.3043    0.7693   0.6829  0.0863
 0.860  0.2429    0.7098   0.6327  0.0771
 0.865  0.1852    0.6380   0.5722  0.0658
 0.870  0.1262    0.5633   0.5081  0.0552
 0.875  0.0882    0.4781   0.4352  0.0428
 0.880  0.0621    0.4045   0.3730  0.0315
 0.885  0.0407    0.3234   0.3008  0.0226
 0.890  0.0251    0.2519   0.2392  0.0127
 0.895  0.0132    0.1875   0.1783  0.0092
 0.900  0.0071    0.1384   0.1323  0.0060
 0.905  0.0044    0.0927   0.0895  0.0032
 0.910  0.0027    0.0573   0.0566  0.0007
 0.915  0.0017    0.0382   0.0375  0.0007
 0.920  0.0010    0.0237   0.0237  0.0000
split=title seed=0 answerable=3225 no-answer=2549 R@1(answerable)=0.7811
top1 score  answerable: mean=0.8737 p5=0.8315 p50=0.8745 | no-answer: mean=0.8282 p50=0.8262 p95=0.8624
no-answer top1 from same article: 0.0000
   thr     FAR  answered  correct   wrong
 0.800  0.9659    0.9991   0.7811  0.2180
 0.805  0.9231    0.9978   0.7808  0.2171
 0.810  0.8509    0.9960   0.7802  0.2158
 0.815  0.7552    0.9910   0.7777  0.2133
 0.820  0.6457    0.9817   0.7733  0.2084
 0.825  0.5288    0.9749   0.7702  0.2047
 0.830  0.4213    0.9563   0.7597  0.1966
 0.835  0.3095    0.9377   0.7498  0.1879
 0.840  0.2146    0.9110   0.7321  0.1789
 0.845  0.1577    0.8794   0.7122  0.1671
 0.850  0.1110    0.8369   0.6847  0.1522
 0.855  0.0769    0.7783   0.6440  0.1343
 0.860  0.0569    0.7200   0.5984  0.1216
 0.865  0.0420    0.6518   0.5448  0.1070
 0.870  0.0314    0.5823   0.4936  0.0887
 0.875  0.0231    0.4905   0.4214  0.0691
 0.880  0.0184    0.4093   0.3597  0.0496
 0.885  0.0110    0.3262   0.2912  0.0350
 0.890  0.0071    0.2546   0.2316  0.0229
 0.895  0.0043    0.1860   0.1721  0.0140
 0.900  0.0031    0.1349   0.1274  0.0074
 0.905  0.0024    0.0933   0.0893  0.0040
 0.910  0.0012    0.0605   0.0592  0.0012
 0.915  0.0012    0.0384   0.0378  0.0006
 0.920  0.0004    0.0217   0.0217  0.0000
split=paragraph: 10 seeds
seed  AUC(top1)  AUC(margin)  thr@FAR5%  answered  correct  R@1 ans
   0     0.8110       0.7688     0.8825    0.3652   0.3393   0.8415
   1     0.7960       0.7669     0.8832    0.3476   0.3217   0.8385
   2     0.8040       0.7685     0.8828    0.3360   0.3145   0.8389
   3     0.7981       0.7845     0.8832    0.3551   0.3321   0.8481
   4     0.8064       0.7776     0.8830    0.3653   0.3451   0.8552
   5     0.8034       0.7809     0.8828    0.3561   0.3275   0.8397
   6     0.8216       0.7741     0.8821    0.3804   0.3568   0.8293
   7     0.8162       0.7926     0.8801    0.4062   0.3798   0.8485
   8     0.8183       0.7753     0.8808    0.3866   0.3620   0.8410
   9     0.8120       0.7869     0.8817    0.3784   0.3520   0.8536
mean     0.8087       0.7776     0.8822    0.3677   0.3431   0.8434
 std     0.0081       0.0081     0.0010    0.0196   0.0190   0.0075
split=title: 10 seeds
seed  AUC(top1)  AUC(margin)  thr@FAR5%  answered  correct  R@1 ans
   0     0.9260       0.8060     0.8624    0.6884   0.5746   0.7811
   1     0.9552       0.8257     0.8500    0.8388   0.6868   0.7856
   2     0.9466       0.8254     0.8519    0.8105   0.6726   0.7936
   3     0.9314       0.8151     0.8603    0.7266   0.6115   0.7945
   4     0.9458       0.8094     0.8531    0.7966   0.6610   0.7783
   5     0.9456       0.8218     0.8553    0.7847   0.6462   0.7891
   6     0.9426       0.8151     0.8553    0.7769   0.6457   0.7792
   7     0.9500       0.8313     0.8520    0.8085   0.6579   0.7740
   8     0.9535       0.8067     0.8487    0.8579   0.6915   0.7728
   9     0.9370       0.8221     0.8588    0.7372   0.6302   0.7996
mean     0.9434       0.8179     0.8548    0.7826   0.6478   0.7848
 std     0.0089       0.0083     0.0043    0.0496   0.0336   0.0087

인코딩이 1분 남짓이고 나머지 열 번씩의 분할은 행렬 연산이라 몇 초에 끝납니다. 임계값 없이 잰 답 있는 질의의 R@1이 0.84 안팎으로, 실험대의 전체 5,774질의 기준선 0.7785보다 높은 것은 코퍼스가 절반으로 줄어 경쟁 문단이 줄었기 때문입니다.

점수 분포

겹치는 두 무리

계획은 「코사인 점수 분포가 두 무리로 갈리는지부터 확인하고, 갈리지 않으면 그것이 결론」이라고 적어 두었습니다. 문단 단위로 뺀 첫 분할의 요약이 이렇습니다.

질의 평균 5번째 백분위 중앙값 95번째 백분위
답 있음 (2,826개) 0.8732 0.8307 0.8737 —
답 없음 (2,948개) 0.8440 — 0.8423 0.8825

답 없는 질의의 중앙값 0.8423이 답 있는 질의의 5번째 백분위 0.8307보다 높고, 답 없는 질의의 95번째 백분위 0.8825는 답 있는 질의의 중앙값 0.8737보다 높습니다. 답 없는 질의 가운데 점수가 높은 5%만 걸러 내려 해도 답 있는 질의 절반 넘게가 같이 걸린다는 뜻입니다. 평균 차이는 0.029이고, 두 무리의 폭이 그 차이보다 훨씬 넓습니다. AUC는 열 번 분할의 평균이 0.8087(표준편차 0.0081)입니다. 무작위로 뽑은 한 쌍에서 다섯 번 중 한 번은 답 없는 질의가 더 높은 점수를 받습니다.

점수가 모두 0.80~0.92 사이에 몰려 있다는 것도 짚어 둡니다. 이 모델에서 코사인 0.85는 「꽤 비슷하다」가 아니라 이 코퍼스의 거의 아무 질의-문단 쌍이 받는 값입니다. 임계값을 0.7이나 0.8 같은 둥근 수로 잡으면 아무것도 거르지 못합니다. 첫 줄 0.800에서 거짓 응답률이 98.78%인 것이 그 증거이고, 이 범위는 모델마다 달라서 다른 모델의 임계값을 옮겨 쓸 수도 없습니다.

같은 글의 이웃 문단

왜 이렇게 겹치는지는 답 없는 질의가 1위로 무엇을 받는지 보면 드러납니다. 문단 단위로 뺀 분할에서 답 없는 질의의 62.18%는 1위로 같은 위키백과 글의 다른 문단을 받았습니다. 질문이 묻는 인물·사건·개념이 그 문단에도 나오므로 점수가 높게 나옵니다. 단지 그 문단에 질문의 답이 되는 문장이 없을 뿐입니다.

유사도 점수는 주제가 같은지를 재지, 답이 들어 있는지를 재지 않습니다. 임계값으로 「답 없음」을 내려는 시도는 이 둘이 같다고 가정하는 것이고, 주제가 남아 있는 코퍼스에서는 그 가정이 깨집니다.

점수 차이

1위 점수 대신 1위와 2위의 점수 차이(margin)로 가르는 방법도 함께 쟀습니다. 정답이 있으면 1위가 2위보다 뚜렷이 높고, 없으면 비슷한 문단 여럿이 고만고만하게 설 것이라는 생각입니다. 결과는 반대 방향이었습니다. 열 번 평균 AUC가 문단 단위에서 0.7776, 글 단위에서 0.8179로, 1위 점수(0.8087·0.9434)보다 두 경우 모두 낮았습니다. 이 코퍼스에서는 1위 점수 하나가 점수 차이보다 낫습니다.

임계값 곡선

거짓 응답률 5%

첫 분할의 표에서 거짓 응답률이 5% 근처를 지나는 자리는 0.880(6.21%)과 0.885(4.07%) 사이입니다. 열 번 분할에서 95번째 백분위로 정확히 5%를 맞춘 임계값은 평균 0.8822이고, 분할이 바뀌어도 표준편차 0.0010으로 거의 움직이지 않았습니다.

문단 단위로 뺐을 때 (10회) 평균 표준편차
거짓 응답률 5% 임계값 0.8822 0.0010
응답률 36.77% 1.96%p
정답 응답률 34.31% 1.90%p
임계값 없을 때 R@1 84.34% 0.75%p

임계값 없이 답하면 답 있는 질의의 84.34%에 정답 문단을 내놓습니다. 거짓 응답률을 5%로 누르면 그 값이 34.31%로 떨어집니다. 정답을 낼 수 있던 질의 10개 중 6개를 「답 없음」으로 돌려보내는 값을 치르고 거짓 응답 95%를 막는 셈입니다. 응답률로 적으면 답 있는 질의의 63.23%를 거절합니다.

공짜 구간과 1:1 구간

곡선 전체를 보면 모양이 둘로 나뉩니다. 임계값 0.800에서 0.830까지는 거짓 응답률이 98.78%에서 71.71%로 27%p 내려가는 동안 응답률은 99.96%에서 95.36%로 4.6%p밖에 안 내려갑니다. 답 없는 질의의 점수 아래쪽 꼬리만 잘라 내는 구간이라 거의 공짜입니다.

그 뒤가 비쌉니다. 0.830에서 0.885까지 거짓 응답률을 67.64%p 더 내리는 동안 응답률이 63.02%p 떨어집니다. 거짓 응답 1%p를 줄일 때마다 답할 수 있는 질의도 0.93%p씩 잃는 셈이라, 거의 1:1로 맞바꾸는 구간입니다. 두 분포가 가장 많이 겹치는 자리를 지나고 있기 때문입니다.

부수 효과도 하나 있습니다. 표의 wrong 열은 답 있는 질의 가운데 답은 했는데 1위가 정답 문단이 아닌 비율입니다. 0.800에서 15.85%이던 것이 0.885에서 2.26%로 줄어듭니다. 임계값은 답 없는 질의만 거르는 것이 아니라, 답이 있는데 엉뚱한 문단을 1위로 받은 질의도 함께 거릅니다. 1위 점수가 낮은 질의는 답이 있든 없든 검색이 헤맨 질의이기 때문입니다.

글째 빠진 경우

글 단위로 빼면 그림이 달라집니다. 답 없는 질의가 1위로 같은 글의 문단을 받을 길이 없어지고(첫 분할에서 0.0000), 점수가 아래로 내려갑니다.

열 번 평균 (표준편차) 문단 단위로 뺐을 때 글 단위로 뺐을 때
AUC (1위 점수) 0.8087 (0.0081) 0.9434 (0.0089)
거짓 응답률 5% 임계값 0.8822 (0.0010) 0.8548 (0.0043)
응답률 36.77% (1.96%p) 78.26% (4.96%p)
정답 응답률 34.31% (1.90%p) 64.78% (3.36%p)
임계값 없을 때 R@1 84.34% (0.75%p) 78.48% (0.87%p)

주제째 없는 질문이라면 임계값이 제법 일합니다. 그래도 공짜는 아닙니다. 임계값 없이 78.48%이던 정답 응답률이 64.78%로 13.70%p 내려가므로, 정답을 낼 수 있던 질의 여섯 개 중 하나는 여전히 「답 없음」으로 돌아갑니다. 응답률(78.26%)과 정답 응답률(64.78%)의 차이 13.48%p는 임계값을 넘었는데도 1위가 정답이 아닌 질의라, 이 경우에도 남은 응답 여섯 개 중 하나꼴은 엉뚱한 문단을 근거로 삼습니다. 열 번 분할 사이의 흔들림도 문단 단위보다 커서, 응답률의 표준편차가 1.96%p에서 4.96%p로 늘었습니다. 어느 글 70편이 빠지느냐에 따라 답 없는 질의의 점수가 달라지기 때문입니다. 거짓 응답률 5%에서 답 있는 질의의 78.26%가 살아남고, AUC 0.94는 꽤 잘 갈린다는 뜻입니다. 같은 모델, 같은 질의, 같은 점수인데 「무엇이 없는가」만 바꿨을 때 거절당하는 답 있는 질의가 63%에서 22%로 바뀌었습니다.

임계값 없을 때 R@1이 글 단위 쪽에서 오히려 낮은 것은 남은 글의 문단이 전부 남아서입니다. 문단 단위로 빼면 같은 글의 형제 문단도 절반쯤 빠져 답 있는 질의의 경쟁자가 줄지만, 글 단위로 빼면 남은 70편의 문단이 하나도 안 빠집니다. 정답과 가장 헷갈리는 문단이 같은 글의 형제라는 것을 거꾸로 보여 주는 숫자입니다.

결정 규칙

옮겨 쓰면 안 되는 임계값

두 경우를 맞대면 실무에서 가장 위험한 자리가 보입니다. 글 단위로 만든 답 없는 질의로 임계값을 맞추면 0.855 근처가 나옵니다. 그 임계값을 문단 단위의 질의에 그대로 걸면, 첫 분할의 표에서 0.855의 거짓 응답률은 5%가 아니라 30.43%입니다. 평가 셋에 「엉뚱한 주제의 질문」만 넣어 임계값을 정하면, 운영에서 들어오는 「주제는 맞는데 답이 없는 질문」 셋 중 하나에 답해 버립니다.

반대 방향도 손해입니다. 문단 단위로 맞춘 0.882를 글 단위 질의에 걸면 거짓 응답률은 1.84%(0.880)와 1.10%(0.885) 사이로 목표보다 낮지만, 응답률이 40.93%와 32.62% 사이로 떨어져 답할 수 있던 질의 절반 넘게를 괜히 거절합니다. 임계값은 모델의 성질이 아니라 그 서비스로 들어오는 답 없는 질문의 성질로 정해집니다.

세 규칙

  1. 임계값은 운영과 같은 종류의 답 없는 질의로 맞춘다. 이 실험에서 답 없는 질의를 만드는 방식만 바꿔도 거짓 응답률 5%의 임계값이 0.882와 0.855로 갈렸고, 엇갈려 쓰면 거짓 응답률이 30%까지 올라갔습니다.
  2. 코퍼스에 주제가 남아 있는 질문(같은 문서의 다른 대목을 묻는 것)이 주된 실패라면 유사도 임계값 하나로 막으려 하지 않는다. 이 경우 AUC가 0.81이고, 거짓 응답 5%를 지키려면 답할 수 있는 질의 63%를 버려야 했습니다. 문단에 답이 들어 있는지를 따로 묻는 단계가 필요한 자리입니다.
  3. 임계값은 그 모델의 점수 분포를 보고 정한다. e5-small의 코사인은 이 코퍼스에서 0.80~0.92에 몰려 있었고, 0.8 같은 둥근 수로는 답 없는 질의를 1.22%밖에 거르지 못했습니다.

꺾이는 지점

한 줄로 줄이면 이렇습니다 — 거짓 응답률을 99%에서 72%까지 내리는 데는 답할 질의 4.6%p만 들고(임계값 0.830까지), 거기서부터는 거짓 응답 1%p를 줄일 때마다 답할 질의 0.93%p를 함께 잃습니다. 5%에 닿을 때쯤에는 답할 질의의 63%를 버린 뒤입니다.

한계와 측정 환경

한계

  • 답 없는 질의를 인위로 만들었습니다. 사람이 실제로 던지는 「우리 문서에 없는 질문」은 이 두 방식의 어느 사이에 있거나 둘 다와 다를 수 있습니다. 이 실험이 보인 것은 그 섞임에 따라 임계값이 크게 달라진다는 것까지입니다.
  • KorQuAD 질문은 문단을 읽고 만든 것이라 정답 문단과 낱말이 많이 겹칩니다. 그래서 답 있는 질의의 점수가 실제 서비스보다 높게 나왔을 수 있고, 그렇다면 실제 서비스에서는 두 분포가 이보다 더 겹칩니다.
  • 모델 하나(multilingual-e5-small), 점수 둘(1위 점수와 점수 차이)만 쟀습니다. 점수 범위가 넓게 퍼지는 모델이나 cross-encoder 점수는 다르게 갈릴 수 있습니다.
  • 「답 없음」 판정을 검색 점수에서 끝냈습니다. 생성 모델이 문단을 읽고 「이 문단에는 답이 없다」고 거절하는 단계를 붙이면 결과가 달라지지만, 이 실험은 그 단계를 재지 않았습니다.
  • 코퍼스를 절반으로 줄인 상태에서 쟀습니다. 코퍼스가 커지면 답 없는 질의가 받는 1위 점수도 올라가므로 임계값이 위로 밀릴 것입니다. 얼마나 밀리는지는 재지 않았습니다.

측정 환경

항목 값
OS Linux 6.18.44 x86_64 (glibc 2.39)
CPU Intel Xeon @ 2.10GHz, 4코어 (torch.set_num_threads(4))
Python 3.11.17
패키지 torch 2.14.1, sentence-transformers 6.1.0, transformers 5.19.0, datasets 5.1.0, numpy 2.4.6
모델 intfloat/multilingual-e5-small (리비전 614241f6)
데이터 KorQuAD squad_kor_v1 검증 분할, 문단 960개 · 질의 5,774개 · 글 140편 (01aad238)
분할 문단 단위 · 글 단위 각 10회 (시드 0~9)
측정일 2026-10-08
실행 시간 1분 37초 (인코딩 81초)

읽어주셔서 감사합니다. 😊

LATEST

과학·실험의 최신 글