무엇을 판단하고, 어떤 실수가 비싼가: 『작은 AI로 판단 시스템 만들기』 1장
무료로 공개하는 1장입니다. 아무것도 학습하기 전에 의도마다 데이터가 얼마나 되는지 세고, 헷갈리기 쉬운 의도를 찾고, 실수마다 비용을 적어 둡니다. 코드는 1초 안에 돌아갑니다.

무엇을 판단하고, 어떤 실수가 비싼가: 『작은 AI로 판단 시스템 만들기』 1장
쓰고 있는 책 『작은 AI로 판단 시스템 만들기』의 1장입니다. 책은 예제 하나를 처음부터 끝까지 따라갑니다. 은행에 들어온 문의를 의도 77개로 나누고, 시스템이 확신하는 문의는 자동으로 답하고 나머지는 사람에게 넘깁니다. 2장은 샘플로 먼저 공개했습니다. 두 장의 코드는 11KB짜리 파일 하나로 받을 수 있습니다: decision-book-code-ch01.zip. 책 전체 목차는 글 끝에 있습니다.
아무것도 학습하기 전에, 이 장에서는 책 전체가 채점 기준으로 삼을 세 가지를 적어 둡니다.
- 시스템이 무엇을 정하는가: 의도 77개 중 하나, 아니면 "사람에게 넘김"
- 어떤 의도끼리 헷갈리기 쉬운가: 학습 문장만 보고 찾습니다
- 실수마다 비용이 얼마인가:
cost.py의 작은 표로 적고, 뒤의 모든 장이 이 파일을 불러 씁니다
이 장에서는 모델을 학습하지 않고 GPU도 필요 없습니다. 데이터를 받은 뒤라면 스크립트는 1초 안에 끝납니다(제 컴퓨터에서 0.8초).
1.1 정할 것은 하나가 아니라 둘입니다
은행에 "I am still waiting on my card?"라는 문의가 들어오면 어디로 보낼지 정해야 합니다. 눈에 띄는 결정은 77개 의도 중 무엇이냐입니다. 이 문의는 card_arrival(카드 도착)입니다. 놓치기 쉬운 결정이 하나 더 있습니다. 애초에 답을 할 것인가입니다. 사람에게 넘길 수 있는 시스템은 두 가지 방식으로 틀리고, 둘의 비용은 다릅니다.
- 답했는데 틀렸습니다.
- 답할 수 있었는데 사람에게 넘겼습니다.
분류기의 정확도는 틀린 의도를 모두 똑같이 세고, 넘기는 선택은 아예 모릅니다. 그래서 모델을 비교하기 전에 셈법부터 정합니다. 이 책은 사람에게 넘긴 건수를 단위로 셉니다.
| 문의가 어떻게 끝났나 | 비용 |
|---|---|
| 자동으로 답했고 맞음 | 0 |
| 사람에게 넘김 | 1 |
| 자동으로 답했는데 틀림 | r × 심각도 |
r은 오답 한 건이 사람 이관 몇 건과 맞먹는지입니다. 심각도는 어떤 오답을 다른 오답보다 무겁게 칩니다(1.4절). r을 정확히 알 필요는 없습니다. 책에서는 r을 여러 값으로 바꿔 가며 채점하고, 어디서 결정이 바뀌는지를 봅니다.
1.2 재기 전에 먼저 셉니다
README대로 환경을 만듭니다(모든 장이 같은 환경을 씁니다). 그다음 실행합니다.
python ch01_define.py처음 몇 줄은 건수입니다.
train 9000 val 1003 test 3080 intents 77
messages per intent
train 31 to 168
val 4 to 19
test 40 to 40분할은 bank.py가 만들고 모든 장이 같은 분할을 씁니다(왜 학습 전에 고정하는지는 2장에서 설명합니다). 여기서 볼 것은 가운데 줄입니다. 검증 문장이 4건뿐인 의도가 있습니다. 이 책의 선택은 모두 검증 세트에서 하므로, 4건이나 40건으로 무엇을 알 수 있는지 알아 둘 필요가 있습니다. 스크립트는 정확도 90%를 표본 크기별로 쟀을 때의 95% 구간을 출력합니다.
| 문장 수 | 90% 주변의 95% 구간 | 폭 |
|---|---|---|
| 5 | 46%에서 99% | 53%p |
| 13 | 64%에서 98% | 34%p |
| 40 | 77%에서 96% | 19%p |
| 100 | 83%에서 94% | 12%p |
| 400 | 87%에서 93% | 6%p |
(Wilson 구간입니다. 책 전체에서 비율에는 이 구간을 씁니다.) 테스트 세트는 의도마다 40건이라 의도별 정확도가 위아래로 10%p 안팎 흔들리고, 검증 세트에서는 거의 잡음입니다. 그래서 이 책은 의도 하나하나가 아니라 세트 전체나 의도 묶음으로 비교합니다.
1.3 어떤 의도끼리 헷갈릴까
가까운 의도가 있습니다. card_arrival("카드가 어디쯤 왔나요?")과 card_delivery_estimate("카드가 오는 데 얼마나 걸리나요?")는 라벨은 다르지만 문장이 비슷하게 읽힐 때가 많습니다. 이런 후보는 모델이 없어도 찾을 수 있습니다. 의도마다 학습 문장의 평균을 내고, 평균끼리 가까운 쌍을 봅니다.
스크립트는 2장의 첫 기준선과 비슷한 TF-IDF(단어와 글자 단위로 센 값)로 이 일을 합니다. 2장은 글자를 3개에서 5개씩 묶고, 여기서는 2개에서 5개씩 묶습니다.
features = make_union(TfidfVectorizer(ngram_range=(1, 2), sublinear_tf=True),
TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5), sublinear_tf=True))
X = features.fit_transform([r["text"] for r in train])
centroids = np.vstack([np.asarray(X[y == lab].mean(axis=0)) for lab in labels])
centroids /= np.linalg.norm(centroids, axis=1, keepdims=True)
sim = centroids @ centroids.T # 의도 쌍마다 코사인 유사도학습 문장만 씁니다. 가장 닮은 다섯 쌍입니다.
| 유사도 | 의도 |
|---|---|
| 0.854 | verify_my_identity / why_verify_identity |
| 0.828 | card_payment_wrong_exchange_rate / wrong_exchange_rate_for_cash_withdrawal |
| 0.782 | disposable_card_limits / get_disposable_virtual_card |
| 0.760 | unable_to_verify_identity / why_verify_identity |
| 0.760 | getting_virtual_card / virtual_card_not_working |
이 목록은 답이 아니라 확인할 질문으로 읽습니다. 사람끼리도 의견이 갈릴 만큼 거의 같은 쌍이 있고, 한 단어("not working")가 결정적인 쌍도 있습니다. 이런 목록을 학습한 분류기가 테스트 세트에서 실제로 틀린 것과 맞춰 본 적이 있습니다(어떤 실수가 비싼가). 가장 닮은 77쌍이 오답 219건의 절반 정도를 덮었고, 가장 비싼 쌍(고객이 모르는 결제 대 카드 정보 유출)은 2,926쌍 중 177위였습니다. 문장만으로 만든 목록은 출발점입니다. 실제로 헷갈린 쌍은 2장에서 나옵니다.
1.4 어떤 실수가 비싼가
오답이라고 다 같지 않습니다. cost.py를 열어 보세요. 코드가 대신 정해 줄 수 없는 판단 두 가지가 들어 있습니다.
고객이 지금 위험한 의도. 돈이나 계정이 걸린 경우입니다. 카드 분실·도난, 카드 정보 유출, 본인이 하지 않은 카드 결제·현금 인출·자동이체, 두 번 빠져나간 결제, ATM이 삼킨 카드, 잘못 나온 현금, 받는 사람에게 도착하지 않은 송금, 취소하고 싶은 송금. 모두 11개 의도입니다. 이 의도를 틀리면 다섯 배로 칩니다.
담당 팀. 의도마다 처리할 팀을 하나씩 정했습니다. 카드 발급·배송, 카드 문제, 카드 결제, 현금·ATM, 충전, 송금, 환전, 계정·본인 확인, 모두 여덟 팀입니다. 틀렸어도 답이 맞는 팀에 떨어지면 5분의 1로 칩니다. 고객이 처음부터 다시 설명하지 않고 동료가 넘겨 주면 되기 때문입니다.
여기서 시나리오 네 개가 나오고, 뒤의 장들은 넷을 나란히 보고합니다.
| 시나리오 | 오답 한 건의 비용 |
|---|---|
uniform (균일) | 언제나 r |
at-risk (위험 의도) | 위험 의도의 문의면 5r, 아니면 r |
same-team (같은 팀) | 답이 맞는 팀 안이면 0.2r, 아니면 r |
both (둘 다) | 위험 의도를 먼저, 그다음 같은 팀 |
5와 0.2는 제가 정한 숫자입니다. 여러분의 숫자는 다를 것이고, 위험 의도 목록과 팀 나누기도 다를 것입니다. 중요한 것은 적는 시점입니다. 어떤 시스템이든 채점하기 전에 적습니다. 결과를 본 다음에는 마음에 드는 시스템이 1등이 되는 비용표를 고르기 쉽습니다. 먼저 적고, 값 하나가 아니라 범위로 채점하면 그 유혹이 사라집니다.
스크립트는 무엇보다 먼저 이 표를 데이터와 맞춰 봅니다(cost.check(labels)). 모든 의도에 팀이 있어야 하고, 위험 의도는 모두 실제 라벨이어야 합니다. 그다음 위험 의도마다 가장 가까운 의도를 출력합니다.
cancel_transfer -> transfer_not_received_by_recipient 0.535 same team
card_swallowed -> declined_cash_withdrawal 0.527 OTHER TEAM
compromised_card -> lost_or_stolen_card 0.533 same team
...11개 중 10개는 가장 가까운 의도가 같은 팀에 있습니다. 그렇지 않은 하나, card_swallowed(ATM이 삼킨 카드)는 지켜볼 쌍입니다.
1.5 결정이 뒤집히는 점
비용표를 실제로 써 봅니다. 5장에서는 확신하면 답하고 나머지는 넘기는 시스템을 만들고, 기준값을 정해야 합니다. 저는 같은 종류의 시스템을 이 블로그에서 의도 77개를 모두 학습시켜 쟀습니다(어떤 문의를 사람에게 넘겨야 할까). 테스트 3,080건에서 기준값 두 개가 낸 결과는 이렇습니다.
| 설정 | 오답 | 사람에게 넘김 |
|---|---|---|
| 더 많이 답함 | 109 | 209 |
| 덜 답함 | 50 | 526 |
어느 쪽이 나을까요? 균일 비용이면 각자의 합계는 오답 × r + 넘김이고, 두 합계는 다음에서 같아집니다.
109r + 209 = 50r + 526, 곧 r = 317 ÷ 59 = 5.37
cost.break_even()이 이 계산을 하고, 스크립트는 몇 가지 r에서 두 비용을 나란히 출력합니다.
r = 2.00: 0.139 vs 0.203 hand-offs per message -> answers more
r = 5.00: 0.245 vs 0.252 hand-offs per message -> answers more
r = 5.37: 0.258 vs 0.258 hand-offs per message -> same
r = 10.00: 0.422 vs 0.333 hand-offs per message -> answers less오답 한 건이 이관 다섯 건 남짓보다 비싸면 엄격한 쪽이 쌉니다. 사실 첫 번째 설정은 "자동으로 답한 것의 95%가 맞도록" 고른 것입니다. 정확도 목표가 감추는 것이 바로 이것입니다. 어떤 r을 골랐는지 말하지 않고 r을 정해 버립니다.
이 계산을 쓰는 데 자기 r을 정확히 알 필요는 없습니다. 5.37보다 큰지 작은지만 알면 됩니다.

1.6 평가 세트 점검표
스크립트는 마지막으로 데이터를 규칙 네 개에 비춰 봅니다.
[x] every intent has at least 40 test messages
[ ] every intent has at least 20 validation messages
[ ] the test set holds questions outside the intents
[x] the at-risk intents have at least 100 test messages together두 개가 통과하지 못했고, 둘 다 뒤에서 중요해집니다. 검증 세트는 의도마다 문장이 적습니다. 그래서 5장의 기준값은 세트 전체로 정하고, 아주 비싼 실수에 맞춘 기준값은 몇 안 되는 오답에 기대게 됩니다. 그리고 BANKING77에는 77개 의도 밖의 문장이 없지만, 실제로 들어오는 문의에는 있습니다. 5장에서는 의도 10개를 학습에서 빼서, 그 문장들이 시스템이 처음 보는 질문 역할을 하게 합니다.
자기 데이터라면 네 줄 뒤의 질문은 이렇습니다.
- 선택지마다 문장이 몇 건 있나요? 고를 때 쓰는 세트와 보고할 때 쓰는 세트 각각에서 셉니다.
- 비싼 선택지가 잴 수 있을 만큼 들어 있나요?
- 어느 선택지에도 맞지 않는 문장이, 실제로 들어올 만한 비율로 들어 있나요?
- 무엇이든 채점하기 전에 비용표를 적었나요?
연습문제
- 내 r. 내 시스템이 낼 수 있는 비싼 오답 하나와 싼 오답 하나를 떠올려 보세요. 각각 사람 이관 몇 건쯤일까요? 5.37이 그 사이에 있나요?
- 위험 의도 바꾸기.
cost.py의AT_RISK에pin_blocked와passcode_forgotten(계정에 들어가지 못하는 고객)을 넣어 보세요.card_swallowed는 여전히 다른 팀에 가까운 의도가 있나요? 이제 어떤 위험 의도가 그런가요? - 다른 유사도. TF-IDF 특징을 글자 단위만으로 바꿔 보세요(
analyzer="char_wb",ngram_range=(2, 5)). 상위 10쌍 중 몇 쌍이 남나요?
실행이 안 될 때
cost table does not match the labels.cost.py를 고치다가TEAMS에서 의도가 빠졌거나 이름에 오타가 있습니다. 메시지에 어느 것인지 나옵니다.unexpected contents (sha256 …). 데이터 파일이 덜 받아졌거나 바뀌었습니다.data/폴더를 지우고 다시 실행합니다.ModuleNotFoundError: sklearn. 가상환경이 켜져 있지 않습니다. 책 폴더에서 먼저source .venv/bin/activate(Windows는.venv\Scripts\activate)를 실행합니다.
측정 환경: 이 장의 코드, Python 3.11.4, scikit-learn 1.9.1, numpy 2.4.6. 이 장의 숫자는 모두 measured/ch01.json에 있습니다. 다만 1.5절의 두 설정과 1.3절의 오답 대조 결과는 거기 링크한 글에서 가져왔습니다. 데이터는 BANKING77 커밋 57ec275, 분할 시드는 20261001입니다.
책의 나머지 장
| 장 | 답하는 질문 | 장을 마치면 손에 남는 것 |
|---|---|---|
| 1. 무엇을 판단하는가 | 선택지는 무엇이고, 어떤 실수가 비싼가? | 라벨 정의, 평가용 데이터, 실수 종류마다 매긴 비용 |
| 2. 첫 기준선 | 단순한 분류기로 어디까지 되는가? | 고정한 학습·검증·테스트 분할, 열에 아홉을 맞히는 CPU 분류기 두 개 |
| 3. 언어 모델 활용 (API) | LLM에 예시와 설명을 어떻게 줄까? | 가까운 예시를 찾아 붙이는 LLM 분류기와, 같은 문장으로 비교한 결과 |
| 4. 판단 모델 (API, GPU가 있으면 로컬) | 전용 판단 모델은 언제 쓸 만한가? | 내 분류기와 같은 문장으로 비교한 보고서 |
| 5. 모르는 것은 사람에게 | 어떤 답을 자동 처리하고 어떤 답을 넘길까? | 검증 세트로 기준값을 고르고 테스트로 한 번 확인한 라우팅 시스템. BANKING77에는 77개 의도 밖의 질문이 없어서, 의도 10개를 학습에서 빼고 그 문장들을 "모르는 질문"으로 씁니다 |
| 6. 운영하기 | 들어오는 문장과 의도가 바뀌면 무엇을 해야 하나? | 새 의도가 들어올 때의 반응을 기록한 로그와, 다시 확인할 항목과 시점(5장에서 뺀 의도 10개가 새 의도로 들어옵니다) |
1, 2, 5, 6장은 GPU 없이 컴퓨터 한 대에서 돌아가고, 3장과 4장은 호스팅 API를 부릅니다(API 키와 몇 달러). GPU가 있다면 4장은 Ollama로 로컬에서 돌릴 수도 있습니다.
책이 나오면 알려 드립니다
나머지 네 장도 같은 방식으로 쓰고 있습니다. 예제 하나, 직접 돌려 볼 수 있는 코드, 그 코드에서 나온 숫자입니다. 이메일을 남겨 주시면 책이 나올 때 한 번 알려 드립니다.
이 글과 이어지는 강좌
SOTAAZ 강좌강좌는 하나씩 살 수 있습니다. 13개 전체가 필요하면 $199 평생 번들도 있습니다.
이메일로 받아보기
관련 포스트

어떤 문의를 사람에게 넘겨야 할까: 분류기, 판단 모델, 사람 순서로 넘기는 시스템 실측
확실하면 분류기가 답하고, 애매하면 판단 모델이, 그래도 애매하면 사람이 받는 시스템을 만들어 쟀습니다. BANKING77에서는 판단 모델이 사람 몫을 줄이지 못했고, CLINC150에서는 모르는 질문이 기준값을 무너뜨렸습니다.

분류기, LLM, 판단 모델 중 무엇을 쓸까: 직접 잰 결과로 정리한 텍스트 분류 안내
이 블로그의 텍스트 분류 실측을 한 흐름으로 묶었습니다. 같은 은행 문의 154건에서 CPU 분류기는 90.3%, 가까운 사례 다섯 개를 붙인 LLM은 94.8%, Jev는 76.0%였습니다. 무엇을 언제 쓸지 순서대로 정리합니다.

Ollama 판단 모델을 Jev와 같은 문항으로 재 봤습니다: Nimble과 Tev1
Ollama 0.35가 Jev식 판단 모델을 로컬에서 돌립니다. 같은 문항에서 Nimble 9B는 TREC 95.6%로 Jev(89.0%)를 앞섰고, 추론 위주 4,599문항에서는 76.0 대 85.7로 뒤졌습니다. 선택지가 26개를 넘는 질문은 Ollama 엔드포인트가 세 모델 모두 거절했습니다.