hyunsu blog

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

LAYA 쇼핑 검색 1 — "50만원이하 패딩 갈색" 을 필터로: 질문 여섯 개 설계

목차 · 4
  1. 01필터 칸마다 질문 하나
  2. 02답을 필터로 — 모르면 걸지 않는다
  3. 03학습 전 모델로 예문 셋
  4. 04실습

16편까지는 고객 문의를 팀에 나누는 사례였다. 여기부터 다섯 편은 다른 사례로 같은 과정(내 모델 만들기 → 지속 학습)을 한 번 더 한다. 이번 일은 쇼핑몰 검색이다.

검색창에 "50만원이하 패딩 상품중 갈색" 이 들어온다. 키워드 검색은 "50만원이하"·"상품중" 까지 단어로 찾으려 한다. 사람이 원하는 것은 이런 필터다.

필터값
가격 구간50만원 이하
대분류상의
겨울옷예
품목패딩
성별모름
색갈색

필터 칸은 모두 정해진 값 중 하나를 고르는 일이다. 문장을 새로 지어낼 필요가 없으니, 고르기만 하는 결정 모델에 맞는 일로 보인다. 정말 맞는지는 21편에서 다른 방법과 나란히 재 보고 판단한다.

01 필터 칸마다 질문 하나

python
"""검색 문장 하나를 질문 여섯 개로 묻고, 답을 검색엔진 필터로 바꾼다.

python first_query.py base                  # 학습 전 다국어 체크포인트
python first_query.py checkpoints/shop-v1   # 내가 학습한 판
"""
import json
import sys

import laya

QUESTIONS = {
    "price": {"type": "choice", "instructions": "검색어의 가격 조건에 맞는 가격 구간은? 구간 경계와 다르면 그 금액을 넘는 가장 작은 구간",
              "criteria": {"under10": "10만원 이하", "under30": "30만원 이하", "under50": "50만원 이하",
                           "under100": "100만원 이하", "none": "가격 조건 없음"}},
    "category": {"type": "choice", "instructions": "찾는 상품의 대분류는?",
                 "criteria": {"top": "상의·아우터", "bottom": "하의", "shoes": "신발", "bag": "가방", "none": "분류를 알 수 없음"}},
    "item": {"type": "choice", "instructions": "찾는 상품의 품목은?",
             "criteria": {"padding": "패딩", "coat": "코트", "knit": "니트", "tshirt": "티셔츠", "jeans": "청바지",
                          "slacks": "슬랙스", "sneakers": "운동화", "boots": "부츠", "bag": "가방", "none": "품목 언급 없음"}},
    "winter": {"type": "noul", "instructions": "겨울옷(패딩·코트·니트·부츠, 또는 겨울에 쓸 상품)을 찾는가?"},
    "gender": {"type": "choice", "instructions": "누가 쓸 상품인가?",
               "criteria": {"men": "남성", "women": "여성", "unknown": "알 수 없음"}},
    "color": {"type": "choice", "instructions": "원하는 색은?",
              "criteria": {"brown": "갈색", "black": "검정", "white": "흰색", "gray": "회색", "navy": "남색",
                           "beige": "베이지", "none": "색 언급 없음"}},
}
EMPTY = {"none", "unknown"}   # "언급 없음"·"알 수 없음" 은 필터를 걸지 않는다
MIN_CONFIDENCE = 0.7          # 이보다 자신 없는 칸도 걸지 않는다 — 틀린 필터는 결과 0건을 만든다


def to_filters(answers):
    """LAYA 답 → 검색엔진 필터. 걸 칸은 true, 모르는 칸은 null 로 남긴다."""
    out = {}
    for qid, a in answers.items():
        if a["type"] == "noul":
            out["winter"] = (a["noul"] >= 0.5) if a["answer_confidence"] >= MIN_CONFIDENCE else None
        elif a["choice"] in EMPTY or a["answer_confidence"] < MIN_CONFIDENCE:
            out[qid] = None
        else:
            out[f"{qid}_{a['choice']}"] = True
    return out


target = sys.argv[1]
agent = laya.load("convaiinnovations/laya", subfolder="multilingual") if target == "base" else laya.load(target)
for query in ["50만원이하 패딩 상품중 갈색", "아빠 겨울 부츠 브라운으로", "언니 선물로 십만 원 이하 밤색 터틀넥"]:
    answers = agent.predict(query, QUESTIONS)["answers"]
    print(f"검색어: {query}")
    for qid, a in answers.items():
        value = f"{a['noul']:.2f}" if a["type"] == "noul" else a["choice"]
        print(f"  {qid:<9}{value:<10}확신 {a['answer_confidence']:.2f}")
    print("  filters:", json.dumps(to_filters(answers), ensure_ascii=False))

질문을 만들며 정한 것들이다.

  • 값이 정해진 칸은 choice, 예/아니오는 noul. 여섯 칸 가운데 겨울옷만 noul 이다. 가격은 금액이 아니라 사이트의 가격 구간 넷 중 하나를 고른다. 필터가 구간으로 되어 있기 때문이다.
  • "모름" 을 선택지로 넣는다. 성별을 말하지 않는 검색어가 많다. noul 은 예/아니오뿐이라 "말하지 않음" 을 담을 수 없어서, 성별은 men·women·unknown 셋 중에 고르는 choice 로 했다. 가격·분류·품목·색에도 none(언급 없음)을 둔다.
  • 회사 규칙은 문구에 적는다. 이 사이트에서 겨울옷은 "패딩·코트·니트·부츠" 다. 검색어에 "겨울" 이 없어도 패딩이면 겨울옷 필터를 건다. 가격이 "40만원 이하" 처럼 구간 경계와 다르면 그 금액을 넘는 가장 작은 구간(50만원 이하)을 고른다. 4편에서 본 것처럼, 모델이 문구를 읽으므로 규칙을 문구에 담는다.
  • 대분류와 품목을 둘 다 묻는다. "겨울 아우터 추천" 처럼 품목 없이 분류만 말하는 검색어가 있다.

여섯 질문은 predict 한 번에 함께 묻는다(3편).

02 답을 필터로 — 모르면 걸지 않는다

to_filters 는 LAYA 의 답을 검색엔진에 넘길 필터로 바꾼다. 원칙은 하나다. 틀린 필터는 결과를 0건으로 만든다. 필터 하나를 덜 걸면 결과가 조금 넓어질 뿐이지만, 갈색을 찾는데 검정 필터가 걸리면 사용자는 아무것도 못 본다. 그래서 두 경우에는 그 칸을 null 로 비운다.

  • 답이 none·unknown 일 때 — 검색어에 그 조건이 없다.
  • 답의 확신(answer_confidence)이 0.7 보다 낮을 때 — 모델이 자신 없다.

0.7 은 출발점이다. 문턱을 올리면 필터가 덜 걸리고(넓은 결과), 내리면 틀린 필터가 는다. 이 숫자를 정하는 법은 19편에서 결과를 보며 다시 다룬다.

03 학습 전 모델로 예문 셋

bash
python first_query.py base
text
검색어: 50만원이하 패딩 상품중 갈색
  price    under100  확신 0.51
  category none      확신 0.76
  item     padding   확신 1.00
  winter   0.92      확신 0.92
  gender   unknown   확신 0.63
  color    brown     확신 0.99
  filters: {"price": null, "category": null, "item_padding": true, "winter": true, "gender": null, "color_brown": true}
검색어: 아빠 겨울 부츠 브라운으로
  price    none      확신 0.54
  category none      확신 0.83
  item     boots     확신 1.00
  winter   0.89      확신 0.89
  gender   unknown   확신 0.49
  color    none      확신 0.88
  filters: {"price": null, "category": null, "item_boots": true, "winter": true, "gender": null, "color": null}
검색어: 언니 선물로 십만 원 이하 밤색 터틀넥
  price    under10   확신 0.38
  category none      확신 0.45
  item     knit      확신 0.51
  winter   0.74      확신 0.74
  gender   unknown   확신 0.38
  color    none      확신 0.54
  filters: {"price": null, "category": null, "item": null, "winter": true, "gender": null, "color": null}
  • 첫 예문: 품목(패딩)·색(갈색)·겨울옷은 맞혔다. 가격은 under100 을 0.51 확신으로 골라 틀렸는데, 문턱 아래라 필터에서 빠졌다. 문턱이 틀린 필터 하나를 막은 것이다. 분류는 none 으로 비웠다.
  • 둘째 예문: "아빠" 를 보고도 성별을 unknown 으로 두었고, "브라운으로" 의 색도 놓쳤다.
  • 셋째 예문: "십만 원"·"밤색"·"터틀넥"·"언니" 처럼 사람에게는 쉬운 말인데, 대부분 확신이 0.5 언저리다. 필터는 겨울옷 하나만 남았다.

학습 전 모델은 질문 문구만 보고 판단한다. 품목처럼 선택지 이름(패딩·부츠)이 검색어에 그대로 있으면 잘 맞히고, "아빠 → 남성" 처럼 한 번 건너 생각해야 하거나 사이트 규칙(패딩 → 상의)이 필요하면 놓친다. 그 차이를 메우는 것이 학습이다.

04 실습

  1. first_query.py 에 내가 쓰는 쇼핑몰의 검색어를 다섯 개 넣어 본다. 어느 칸이 가장 자주 null 이 되는지 본다.
  2. MIN_CONFIDENCE 를 0.5 와 0.9 로 바꿔 필터가 어떻게 달라지는지 본다.
  3. 성별 질문을 noul("남성용을 찾는가?")로 바꾸면 "말하지 않은" 검색어에서 무엇이 나오는지 본다. 왜 choice 로 했는지가 보인다.

다음 편에서 학습 데이터를 만들고 v1 을 학습한다.