홈강좌LAYA 결정 모델 24편 중 19편째
LAYA 쇼핑 검색 3 — v1 결과 읽기: 배운 단어는 80%, 처음 보는 단어는 0%
18편의 v1 을 평가 세트 210건으로 잰다. 숫자는 여섯 칸이 모두 맞은 비율(완전 일치) 을 먼저 본다. 검색에서는 칸 하나만 틀려도 결과가 어긋나기 때문이다.
01 학습 전과 v1
"""검색어 평가 세트로 판 여러 개를 나란히 잰다 — 완전 일치(여섯 칸 모두 맞음) · 어휘 묶음별 · 칸별.
python shop_eval.py base checkpoints/shop-v1
"""
import json
import sys
import time
from collections import Counter
import laya
FIELDS = ["price", "category", "item", "winter", "gender", "color"]
POOLS = ["train", "ops", "unseen"]
def load(target):
if target == "base":
return laya.load("convaiinnovations/laya", subfolder="multilingual")
return laya.load(target)
def filters(answers):
"""LAYA 답을 필터 값으로 — choice 는 고른 값, noul 은 0.5 이상이면 True."""
return {f: (answers[f]["noul"] >= 0.5 if answers[f]["type"] == "noul" else answers[f]["choice"]) for f in FIELDS}
def measure(target, cases):
agent = load(target)
start = time.time()
preds = [filters(agent.predict(c["state"], c["questions"])["answers"]) for c in cases]
ms = (time.time() - start) / len(cases) * 1000
exact = [all(p[f] == c["expected"][f] for f in FIELDS) for c, p in zip(cases, preds)]
row = {"exact match": sum(exact) / len(cases)}
for pool in POOLS:
hits = [e for c, e in zip(cases, exact) if c["pool"] == pool]
row[f" {pool} words"] = sum(hits) / len(hits)
for f in FIELDS:
row[f] = sum(p[f] == c["expected"][f] for c, p in zip(cases, preds)) / len(cases)
row["ms per query"] = ms
misses = Counter(f"{f} {c['expected'][f]} -> {p[f]}" for c, p in zip(cases, preds)
for f in FIELDS if p[f] != c["expected"][f])
return row, misses
if __name__ == "__main__":
targets = sys.argv[1:]
cases = [json.loads(line) for line in open("shop-eval.jsonl", encoding="utf-8")]
results = [measure(t, cases) for t in targets]
names = [t.split("/")[-1] for t in targets]
print(f"{len(cases)} queries · " + " · ".join(f"{p} words {sum(c['pool'] == p for c in cases)}" for p in POOLS))
print(f"{'':<16}" + "".join(f"{n:>10}" for n in names))
for key in results[0][0]:
fmt = "{:>10.0f}" if key == "ms per query" else "{:>10.3f}"
print(f"{key:<16}" + "".join(fmt.format(r[key]) for r, _ in results))
print(f"most common mistakes ({names[-1]}):")
for k, v in results[-1][1].most_common(5):
print(f" {k} ×{v}")python shop_eval.py base checkpoints/shop-v1210 queries · train words 70 · ops words 70 · unseen words 70
base shop-v1
exact match 0.014 0.305
train words 0.029 0.800
ops words 0.014 0.114
unseen words 0.000 0.000
price 0.590 0.800
category 0.138 0.671
item 0.595 0.638
winter 0.457 0.852
gender 0.629 0.743
color 0.505 0.605
ms per query 92 90
most common mistakes (shop-v1):
gender men -> unknown ×33
winter True -> False ×30
gender women -> unknown ×21
item sneakers -> none ×18
color gray -> none ×17- 전체: 0.014 에서 0.305 로 올랐다. 210건 중 64건의 필터가 통째로 맞는다.
- train 단어: 0.800. 학습에서 본 단어를 새로 조합한 검색어는 대부분 맞힌다.
- ops 단어: 0.114. "아빠"·"푸퍼"·"곤색" 처럼 처음 150건에 없던 단어가 들어가면 무너진다.
- unseen 단어: 0.000. 70건 중 하나도 통째로 맞히지 못했다.
학습 전 모델(0.014)은 질문 문구만으로 판단했고, v1 은 150건에서 단어와 값의 짝을 배웠다. 그 짝은 본 단어에만 통한다. 바탕 모델이 "아들" 이 남성이라는 것을 어느 정도 안다 해도, 150건 학습은 그 지식을 끌어내기보다 본 단어에 맞추는 쪽으로 움직였다.
02 칸별로 보면
- 많이 오른 칸: 분류(0.138 → 0.671)와 겨울옷(0.457 → 0.852). 둘 다 사이트 규칙이 필요한 칸이다. 학습 전에는 "패딩 → 상의" 와 "패딩 → 겨울옷" 을 몰랐다.
- 조금 오른 칸: 품목(0.595 → 0.638)과 색(0.505 → 0.605). 값마다 단어가 가장 많이 바뀌는 칸이라, 새 단어가 섞인 ops·unseen 검색어에서 계속 틀린다.
흔한 실수 첫째는 gender men -> unknown ×33 이다. "아빠"·"남편"·"아들" 을 보고도 성별을 모른다고 했다. 둘째 winter True -> False ×30 은 대부분 품목을 못 알아본 탓이다. "다운 점퍼" 를 패딩으로 못 읽으면 분류도, 겨울옷도 같이 틀린다. 칸끼리 실수가 이어진다. 21편에서 이 연결을 표로 끊는 방법을 본다.
03 예문 셋 다시
python first_query.py checkpoints/shop-v1검색어: 50만원이하 패딩 상품중 갈색
price under50 확신 0.99
category top 확신 0.99
item padding 확신 0.99
winter 1.00 확신 1.00
gender unknown 확신 0.95
color brown 확신 0.99
filters: {"price_under50": true, "category_top": true, "item_padding": true, "winter": true, "gender": null, "color_brown": true}
검색어: 아빠 겨울 부츠 브라운으로
price none 확신 0.99
category shoes 확신 0.98
item boots 확신 0.98
winter 1.00 확신 1.00
gender unknown 확신 0.87
color brown 확신 0.98
filters: {"price": null, "category_shoes": true, "item_boots": true, "winter": true, "gender": null, "color_brown": true}
검색어: 언니 선물로 십만 원 이하 밤색 터틀넥
price none 확신 0.81
category top 확신 0.96
item none 확신 0.97
winter 0.00 확신 1.00
gender unknown 확신 0.87
color none 확신 0.89
filters: {"price": null, "category_top": true, "item": null, "winter": false, "gender": null, "color": null}첫 예문은 17편에서 쓴 필터 그대로 나왔다. price_under50 · category_top · item_padding · winter · color_brown 이 걸리고 성별은 null 이다. 둘째 예문은 "아빠" 를 여전히 모른다(ops 단어).
셋째 예문(unseen 단어)이 더 위험하다. 겨울옷을 확신 1.00 으로 아니라고 답했다. 터틀넥은 니트라 겨울옷인데, 모델은 틀리면서도 전혀 망설이지 않았다. 확신 문턱이 막아 주는 것은 모델이 망설일 때뿐이다.
04 확신 문턱을 어디에 둘까
17편의 to_filters 는 확신 0.7 아래인 칸을 비웠다. 문턱마다 필터가 몇 개 걸리고, 그중 몇 개가 틀리는지 쟀다.
"""확신 문턱마다 — 필터가 몇 개 걸리고, 걸린 필터 중 몇 개가 틀리며, 틀린 필터가 하나라도 걸린 검색어가 몇인지.
틀린 필터가 하나라도 걸리면 그 검색은 엉뚱한 결과(대개 0건)를 낸다. 문턱은 이 숫자를 보고 정한다.
python threshold.py checkpoints/shop-v1
"""
import json
import sys
import laya
FIELDS = ["price", "category", "item", "winter", "gender", "color"]
EMPTY = {"none", "unknown", False}
agent = laya.load(sys.argv[1])
cases = [json.loads(line) for line in open("shop-eval.jsonl", encoding="utf-8")]
answers = [] # 검색어마다 [(칸, 값, 확신, 맞았나)]
for c in cases:
a = agent.predict(c["state"], c["questions"])["answers"]
row = []
for f in FIELDS:
value = a[f]["noul"] >= 0.5 if a[f]["type"] == "noul" else a[f]["choice"]
row.append((f, value, a[f]["answer_confidence"], value == c["expected"][f]))
answers.append((c, row))
wanted = sum(c["expected"][f] not in EMPTY for c in cases for f in FIELDS)
print(f"queries {len(cases)} · filters the queries ask for {wanted}")
print(f"{'threshold':>9}{'applied':>9}{'wrong':>7}{'wrong %':>9}{'queries hit':>13}{' unseen hit':>13}")
for t in (0.5, 0.7, 0.9, 0.99):
applied = wrong = hit = unseen_hit = 0
for c, row in answers:
bad = False
for f, value, conf, ok in row:
if value in EMPTY or conf < t:
continue # 걸지 않는 칸
applied += 1
if not ok:
wrong += 1
bad = True
hit += bad
unseen_hit += bad and c["pool"] == "unseen"
print(f"{t:>9}{applied:>9}{wrong:>7}{wrong / applied:>9.1%}{hit:>13}{unseen_hit:>13}")python threshold.py checkpoints/shop-v1queries 210 · filters the queries ask for 868
threshold applied wrong wrong % queries hit unseen hit
0.5 585 75 12.8% 63 34
0.7 543 55 10.1% 51 30
0.9 506 33 6.5% 31 18
0.99 172 1 0.6% 1 1- 걸린 필터
- 틀린 필터
검색어 210건이 요구한 필터는 868개
숫자로 보기
| 항목 | 걸린 필터 | 틀린 필터 |
|---|---|---|
| 0.5 | 585 | 75 |
| 0.7 | 543 | 55 |
| 0.9 | 506 | 33 |
| 0.99 | 172 | 1 |
- 검색어가 요구한 필터는 868개다. 0.5 에서도 585개만 걸린다. 나머지는 모델이
none·unknown으로 비웠다. - 0.9 로 올리면 틀린 필터가 33개(6.5%)로 줄고, 걸리는 필터는 506개로 크게 줄지 않는다. 그래도 31건(15%)은 틀린 필터가 하나 이상 걸린다.
- 0.99 에서는 틀린 필터가 1개뿐이지만, 걸리는 필터도 172개로 줄어 필터 검색이라 부르기 어렵다.
- 틀린 필터가 걸린 검색어의 절반 넘게가 unseen 단어다. 처음 보는 단어에서 틀리면서도 확신하는 경우다.
v1 이라면 0.9 가 출발점이다. 0.9 에서도 15% 는 틀린 필터가 걸리니, 그대로 내보내려면 안전장치가 하나 더 있어야 한다. 예를 들어 필터 결과가 0건이면 필터를 빼고 다시 검색한다.
05 결과 가능성
| 결과 | 이번에 본 것 | 다음에 할 것 |
|---|---|---|
| 좋아짐 | train 단어 0.029 → 0.800 · 분류·겨울옷 칸 | 그대로 쓴다 |
| 조금 좋아짐 | ops 단어 0.114 · 품목·색 칸 | 운영에서 고친 검색어로 지속 학습(20편) |
| 그대로 | unseen 단어 0.000 | 지속 학습으로는 안 고쳐질 수 있다 — 다른 방법과 비교(21편) |
| 과신 | 처음 보는 단어에 확신 1.00 으로 틀림 | 문턱만 믿지 않는다 — 0건이면 필터 빼고 다시 검색 |
| 연쇄 | 품목 하나를 모르면 분류·겨울옷도 틀림 | 품목 → 분류·겨울옷은 표로 정할 수 있는지 본다 |
이 숫자는 한 번 학습한 결과다. 8편에서 본 것처럼 같은 명령도 씨앗에 따라 흔들린다. 20편에서 세 번씩 돌려 폭을 본다.
06 실습
shop_eval.py에 단어 묶음별 칸별 정확도를 더해, unseen 단어에서 어느 칸이 가장 약한지 본다.threshold.py의 문턱에 0.8 과 0.95 를 더해, 내 서비스라면 어디에 둘지 정해 본다. 틀린 필터 한 개와 덜 걸린 필터 한 개 중 무엇이 더 나쁜지가 기준이다.- 셋째 예문의 "터틀넥" 을 "니트" 로 바꿔 돌려 본다. 단어 하나로 겨울옷 답이 뒤집히는지 본다.