hyunsu blog

홈강좌LAYA 결정 모델 24편 중 19편째

LAYA 쇼핑 검색 3 — v1 결과 읽기: 배운 단어는 80%, 처음 보는 단어는 0%

목차 · 6
  1. 01학습 전과 v1
  2. 02칸별로 보면
  3. 03예문 셋 다시
  4. 04확신 문턱을 어디에 둘까
  5. 05결과 가능성
  6. 06실습

18편의 v1 을 평가 세트 210건으로 잰다. 숫자는 여섯 칸이 모두 맞은 비율(완전 일치) 을 먼저 본다. 검색에서는 칸 하나만 틀려도 결과가 어긋나기 때문이다.

01 학습 전과 v1

python
"""검색어 평가 세트로 판 여러 개를 나란히 잰다 — 완전 일치(여섯 칸 모두 맞음) · 어휘 묶음별 · 칸별.

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}")
bash
python shop_eval.py base checkpoints/shop-v1
text
210 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
그림 1.완전 일치 — 단어 묶음별
0.00.20.40.60.81.0전체train 단어ops 단어unseen 단어전체 · 학습 전: 0.014전체 · v1: 0.305train 단어 · 학습 전: 0.029train 단어 · v1: 0.800ops 단어 · 학습 전: 0.014ops 단어 · v1: 0.114unseen 단어 · 학습 전: 0.000unseen 단어 · v1: 0.0000.0140.3050.0290.8000.0140.1140.0000.0000.00.20.40.60.81.0전체train 단어ops 단어unseen 단어전체 · 학습 전: 0.014전체 · v1: 0.305train 단어 · 학습 전: 0.029train 단어 · v1: 0.800ops 단어 · 학습 전: 0.014ops 단어 · v1: 0.114unseen 단어 · 학습 전: 0.000unseen 단어 · v1: 0.0000.0140.3050.0290.8000.0140.1140.0000.000
  • 학습 전
  • v1

검색어 210건, 묶음마다 70건. 여섯 칸이 모두 맞아야 1건

숫자로 보기
항목학습 전v1
전체0.0140.305
train 단어0.0290.800
ops 단어0.0140.114
unseen 단어0.0000.000
  • 전체: 0.014 에서 0.305 로 올랐다. 210건 중 64건의 필터가 통째로 맞는다.
  • train 단어: 0.800. 학습에서 본 단어를 새로 조합한 검색어는 대부분 맞힌다.
  • ops 단어: 0.114. "아빠"·"푸퍼"·"곤색" 처럼 처음 150건에 없던 단어가 들어가면 무너진다.
  • unseen 단어: 0.000. 70건 중 하나도 통째로 맞히지 못했다.

학습 전 모델(0.014)은 질문 문구만으로 판단했고, v1 은 150건에서 단어와 값의 짝을 배웠다. 그 짝은 본 단어에만 통한다. 바탕 모델이 "아들" 이 남성이라는 것을 어느 정도 안다 해도, 150건 학습은 그 지식을 끌어내기보다 본 단어에 맞추는 쪽으로 움직였다.

02 칸별로 보면

그림 2.칸별 정확도 — 학습 전과 v1
0.00.20.40.60.81.0pricecategoryitemwintergendercolorprice · 학습 전: 0.590price · v1: 0.800category · 학습 전: 0.138category · v1: 0.671item · 학습 전: 0.595item · v1: 0.638winter · 학습 전: 0.457winter · v1: 0.852gender · 학습 전: 0.629gender · v1: 0.743color · 학습 전: 0.505color · v1: 0.6050.5900.8000.1380.6710.5950.6380.4570.8520.6290.7430.5050.6050.00.20.40.60.81.0pricecategoryitemwintergendercolorprice · 학습 전: 0.590price · v1: 0.800category · 학습 전: 0.138category · v1: 0.671item · 학습 전: 0.595item · v1: 0.638winter · 학습 전: 0.457winter · v1: 0.852gender · 학습 전: 0.629gender · v1: 0.743color · 학습 전: 0.505color · v1: 0.6050.5900.8000.1380.6710.5950.6380.4570.8520.6290.7430.5050.605
  • 학습 전
  • v1

검색어 210건

숫자로 보기
항목학습 전v1
price0.5900.800
category0.1380.671
item0.5950.638
winter0.4570.852
gender0.6290.743
color0.5050.605
  • 많이 오른 칸: 분류(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 예문 셋 다시

bash
python first_query.py checkpoints/shop-v1
text
검색어: 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 아래인 칸을 비웠다. 문턱마다 필터가 몇 개 걸리고, 그중 몇 개가 틀리는지 쟀다.

python
"""확신 문턱마다 — 필터가 몇 개 걸리고, 걸린 필터 중 몇 개가 틀리며, 틀린 필터가 하나라도 걸린 검색어가 몇인지.

틀린 필터가 하나라도 걸리면 그 검색은 엉뚱한 결과(대개 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}")
bash
python threshold.py checkpoints/shop-v1
text
queries 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
그림 3.확신 문턱별 — 걸린 필터와 틀린 필터
개01002003004005006000.50.70.90.990.5 · 걸린 필터: 5850.7 · 걸린 필터: 5430.9 · 걸린 필터: 5060.99 · 걸린 필터: 1720.5 · 틀린 필터: 750.7 · 틀린 필터: 550.9 · 틀린 필터: 330.99 · 틀린 필터: 11721개02004006000.50.70.90.990.5 · 걸린 필터: 5850.7 · 걸린 필터: 5430.9 · 걸린 필터: 5060.99 · 걸린 필터: 1720.5 · 틀린 필터: 750.7 · 틀린 필터: 550.9 · 틀린 필터: 330.99 · 틀린 필터: 11721
  • 걸린 필터
  • 틀린 필터

검색어 210건이 요구한 필터는 868개

숫자로 보기
항목걸린 필터틀린 필터
0.558575
0.754355
0.950633
0.991721
  • 검색어가 요구한 필터는 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 실습

  1. shop_eval.py 에 단어 묶음별 칸별 정확도를 더해, unseen 단어에서 어느 칸이 가장 약한지 본다.
  2. threshold.py 의 문턱에 0.8 과 0.95 를 더해, 내 서비스라면 어디에 둘지 정해 본다. 틀린 필터 한 개와 덜 걸린 필터 한 개 중 무엇이 더 나쁜지가 기준이다.
  3. 셋째 예문의 "터틀넥" 을 "니트" 로 바꿔 돌려 본다. 단어 하나로 겨울옷 답이 뒤집히는지 본다.