의도 분류기 (Intent Classifier) 평가하기: 정확도(Accuracy) 너머로 확인해야 할 것들
요약
의도 분류기(Intent Classifier) 평가 시 단순 정확도(Accuracy) 수치에 의존할 때 발생하는 위험성을 경고합니다. 실제 운영 환경의 불균형한 트래픽을 반영하지 못하는 테스트 세트의 한계를 지적하며, 효과적인 평가를 위한 두 가지 테스트 세트 전략을 제안합니다.
핵심 포인트
- 단순 정확도는 빈도가 높은 클래스에 의해 왜곡될 수 있음
- 희귀하지만 중요한 의도(Intent)의 성능 저하를 놓칠 위험이 있음
- 실제 트래픽을 반영한 불균형 세트와 균등 오버샘플링 세트 병행 필요
- 두 테스트 세트 간의 성능 격차를 모니터링하여 장애 예방
저는 우리 지원 에이전트 (Support Agent)의 전면에 위치하는 의도 라우터 (Intent Router)를 구축했습니다. 이 라우터는 사용자가 입력한 내용을 읽고 어떤 파이프라인 (Pipeline)이 이를 처리할지 결정합니다. 저는 잘 균형 잡힌 데이터 세트로 테스트를 진행하여 92%의 정확도 (Accuracy)를 얻었고, 기분 좋게 배포했습니다.
2주 후, 당직 엔지니어 (On-call engineer)는 해지 요청 (Cancellation requests)이 결제 큐 (Billing queue)로 들어가고 있다는 사실을 발견했습니다. 사기 보고 (Fraud-report) 큐는 비어 있는 반면, 실제 사기 불만 사항들은 제품 피드백 (Product feedback)에 머물러 있었습니다. 그리고 우리가 출시했을 당시에는 존재하지 않았던 새로운 요청 유형이 대부분의 경우 결제 질문으로 조용히 분류되고 있었습니다.
모델이 고장 난 것은 아니었습니다. 제 테스트 세트 (Test set)가 실제 운영 환경 (Production)에서 발생하는 실패 사례들을 단순히 숨기고 있었을 뿐입니다. 92%라는 수치는 실제였지만, 동시에 쓸모도 없었습니다.
제가 고생하며 깨달은 것들을 쉬운 용어로 정리해 드립니다. 여러분은 제가 시행착오를 겪으며 보낸 2주라는 시간을 건너뛸 수 있을 것입니다.
헤드라인 정확도 수치가 저를 속인 이유
실제 지원 트래픽 (Support traffic)은 불균형합니다. 5~6개의 흔한 의도 (Intents) (제품 질문, 가격, 계정 접속, 배송, 환불)가 대부분의 볼륨을 차지하며, 이들은 처리하기 쉽습니다. 드문 사례들 (구독 해지, 사기 보고, 정책 분쟁, 완전히 새로운 요청 유형)은 트래픽의 아주 작은 부분만을 차지하지만, 바로 이 지점에 돈, 당직 호출 (On-call pages), 그리고 신뢰 및 안전 (Trust-and-safety) 사고가 몰려 있습니다.
균형 잡힌 테스트 세트 (Balanced test set)는 모든 의도에 동일한 가중치를 부여합니다. 따라서 모델은 모든 의도에 대해 고르게 잘 수행하도록 학습되며, 저의 단 하나의 큰 정확도 수치는 쉽고 볼륨이 큰 클래스 (Classes)들에 의해 지배되었습니다. 드문 클래스들이 조용히 30%까지 무너져도 평균에서는 반올림 오차 수준에 불과했기 때문에 헤드라인 수치는 거의 움직이지 않았습니다.
그리고 그 드문 클래스들 하나하나가 그 볼륨이 시사하는 것보다 훨씬 더 중요합니다. 라우터가 무엇을 선택하느냐에 따라 다음에 실행될 지식 베이스 (Knowledge base), 도구 (Tools), 그리고 에스컬레이션 경로 (Escalation path)가 결정됩니다. 라우팅 (Routing)을 잘못하면 그 하위의 모든 과정이 확신을 가지고 틀리게 됩니다.
하나가 아닌, 두 개의 테스트 세트
저에게 모든 것을 바꿔 놓은 해결책은 두 개의 별도 테스트 세트를 유지하는 것이었습니다:
- 실제 트래픽을 반영하는 세트. 운영 환경(Production)과 동일한 불균형한 혼합 비율을 가집니다. 이것이 사용자가 실제로 경험하는 것이기에, 제가 보고하는 수치는 바로 이것입니다.
- 모든 의도(Intent)를 균등하게 오버샘플링(Oversampling)한 세트. 클래스가 아무리 희귀하더라도 클래스당 100개 정도의 예시를 포함합니다. 모델이 희귀 클래스를 아예 수행할 수 있는지 확인할 수 있는 유일한 방법이기에, 저는 이 세트를 기준으로 디버깅을 수행합니다.
이 두 세트 사이의 격차는 경고 신호입니다. 만약 모델이 실제 트래픽 세트에서는 훌륭하지만 균등 세트의 희귀 클래스에서 취약하다면, 해당 클래스는 트래픽 변화(Traffic shift) 한 번에 바로 다음 장애(Incident)로 이어질 수 있습니다.
평균이 아닌, 각 의도(Intent)를 살펴보세요
하나의 수치만 보는 것을 멈추고 각 의도를 개별적으로 살펴보기 시작하자, 문제들이 명확해졌습니다. 이제 저는 모든 의도에 대해 다음 두 가지를 확인합니다:
- 해당 의도가 실행되었을 때 얼마나 자주 맞습니까? 이 수치가 낮다면, 모델이 확신이 없을 때 해당 의도를 일종의 쓰레기통(Dumping ground)처럼 남용하고 있다는 뜻입니다.
- 실제 사례를 얼마나 자주 잡아냅니까? 이 수치가 낮다면, 모델이 해당 사례들을 조용히 인접한 다른 의도로 보내버리고 있다는 뜻입니다.
가장 유용한 단일 관점은 무엇이 무엇과 혼동되었는지를 보여주는 그리드(Grid)입니다. 그 그리드는 "취소(Cancel)" 요청이 대부분 "결제(Billing)"로 읽히고 있다는 사실을 명확하게 알려주었습니다. 충돌이 발생하는 정확한 쌍을 파악하고 나면, 해결책은 대개 거대한 모델 변경이 아니라 모델에게 해당 두 의도를 설명하는 방식을 두세 번 수정하는 것입니다.
어떤 의도가 정답률도 낮고 포착률도 낮다면, 그곳이 바로 위험 구역(Danger zone)입니다. 설명이 모호하거나, 레이블(Label) 자체가 잘못 정의되었거나, 혹은 모델이 진정으로 인접한 의도와 구분할 수 없는 상태인 것입니다. 그리드는 그중 무엇인지 알려줍니다.
신뢰도 점수(Confidence score)는 보정(Calibrate)하기 전까지 아무런 의미가 없습니다
제 라우터(Router) 또한 신뢰도 수치를 출력했고, 저는 특정 임계값(Cutoff) 미만일 경우 사람에게 넘기도록 에스컬레이션(Escalation) 로직을 설계해 두었습니다. 문제는 가공되지 않은 원시 신뢰도(Raw confidence)가 거의 무의미했다는 점입니다.
두 가지 사실이 저를 놀라게 했습니다. 점수들이 양 극단(매우 높거나 매우 낮은 쪽)에 몰려 있었고, 임계값(Cutoff)을 설정할 수 있는 중간 영역에는 거의 아무것도 없었습니다. 또한, 동일한 점수라도 의도(Intent)에 따라 의미하는 바가 달랐습니다. 일반적인 제품 질문에 대한 0.7점은 거의 매번 정답이었지만, 사기 보고(Fraud report)에 대한 0.7점은 동전 던지기보다 나을 것이 없었습니다.
따라서 하나의 전역 임계값(Global cutoff)을 사용하는 것은 의도별로 완전히 다른 수준의 주의력을 적용하는 것과 같았습니다. 실제로 효과적인 방법은 다음과 같습니다:
- 구독 취소나 사기 신고와 같이 되돌릴 수 없는 작업(Irreversible actions)의 경우, 기준을 높게 설정하고 그 미만의 모든 입력은 확인 질문(Clarifying question)이나 상담원에게 전달하십시오.
- 포괄적인 안전 의도(Catch-all safety intents)의 경우, 시스템이 추측하는 대신 안전한 경로로 과잉 라우팅(Over-routes)되도록 기준을 낮게 설정하십시오.
그리고 이러한 임계값의 변경을 모델 자체의 변경과 동일하게 취급하십시오. 함께 버전 관리(Version)를 해야 합니다.
모델에게 "해당 사항 없음" 옵션을 제공하십시오
이것은 제가 완전히 놓쳤던 부분이었습니다. 모델에게 명시적인 "해당 사항 없음(None of these)" 클래스를 제공하지 않으면, 모델은 모든 입력을 알려진 레이블 중 하나로 강제로 끼워 맞추게 됩니다. 횡설수설, 주제를 벗어난 질문, 그리고 노골적인 탈옥(Jailbreak) 시도까지 모두 실제 의도로 분류되어 실제 파이프라인(Pipeline)으로 라우팅됩니다.
그래서 저는 명시적인 "어떤 의도에도 해당하지 않음" 클래스를 추가하고, 적대적 프롬프트(Adversarial prompts), 모호한 다중 주제 메시지, 단순한 헛소리 등 지저분한 데이터들로 이를 학습시켰습니다. 두 가지 규칙이 이를 작동하게 만들었습니다:
- 신뢰도 기준(Confidence bar)을 통과하는 것이 없다면, 최선의 추측을 선택하는 대신 이 클래스로 넘어가도록(Fall through) 하십시오.
- 이 클래스가 막다른 길(Dead-end)이 되게 하지 마십시오. 파이프라인으로 보내지 말고, "다음 중 의도하신 것이 있나요?"라는 질문이나 상담원에게 라우팅하십시오.
또한 저는 이 클래스의 포착률(Catch rate)을 면밀히 모니터링하며 결코 악화되도록 두지 않습니다. 포착률이 떨어진다는 것은 실제 쓰레기 데이터가 다시 실제 흐름(Flows)으로 유출되고 있다는 의미이기 때문입니다.
배포된 분류 체계(Taxonomy)는 노후화됩니다
제가 처음 출시했던 의도(Intents) 목록은 3개월 후 프로덕션(Production)에서 필요로 했던 목록과 달랐습니다. 사용자의 언어는 표류(Drift)하고, 신제품 출시가 완전히 새로운 요청 유형을 만들어내며, 정책 변경이 새로운 종류의 불만을 생성합니다. 결제(Billing)로 잘못 분류된 그 새로운 요청 유형이 바로 그런 사례였습니다.
현재 제가 주시하는 두 가지 사항은 다음과 같습니다:
- 기존 의도의 성능 저하 (Slipping). 만약 주 단위로 수치가 변동된다면, 모델은 괜찮지만 유입되는 언어가 모델의 기준 아래에서 변화한 것입니다. 예시(Examples)를 새로고침하고 다시 베이스라인(Re-baseline)을 잡으세요.
- 새로운 의도의 형성. "이 중 어느 것도 아님 (None of these)" 버킷에 담긴 항목들과 신뢰도(Confidence)가 낮은 미분류 항목들을 그룹화하여, 지속적으로 나타나는 클러스터(Cluster)가 있는지 확인합니다. 동일한 새로운 패턴이 2주 연속으로 나타난다면, 그것은 독자적인 의도를 가질 자격이 있습니다. 몇 가지 예시에 라벨(Label)을 붙이고, 정의를 작성한 뒤, 추가하세요.
첫날에 알았더라면 좋았을 두 가지
- 의도를 시스템이 무엇을 하는가가 아니라, 사용자가 무엇을 원하는가로 정의하세요. "사용자가 환불을 원함"은 의도입니다. "환불 플로우(Flow)를 실행함"은 도구(Tool)입니다. 저는 이 두 가지를 혼동했고, 그것이 라벨을 흐릿하게 만들었습니다.
- 모델을 탓하기 전에 사람이 라벨에 동의하는지 확인하세요. 몇 명의 사람에게 동일한 100개의 예시에 라벨을 붙이게 해보세요. 만약 사람들이 의도에 대해 합의하지 못한다면, 모델에게는 가망이 없습니다. 그것은 모델의 문제가 아니라 먼저 해결해야 할 분류 체계(Taxonomy)의 문제입니다.
제가 계속 되새기는 사실은, 의도 라우터(Intent Router)가 에이전트(Agent)가 내리는 첫 번째 결정이며, 저는 그동안 실제 실패를 보여줄 가능성이 가장 낮은 단 하나의 수치로 라우터를 평가해 왔다는 점입니다. "이 중 어느 것도 아님"이라는 탈출구(Escape hatch)를 두고, 실제 트래픽에 대해 의도별로 살펴보는 순간, 2주 동안 숨어있던 버그들이 그리드(Grid) 안에 바로 자리 잡고 있었습니다.
만약 여러분이 이와 같은 라우터를 운영하고 있다면, 어떤 의도가 조용히 무너졌었는지 듣고 싶습니다. 저의 경우에는 항상 결제 질문인 척하는 해지(Cancellations) 요청이었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기