AI 업무 활용

AI 고객지원 티켓 분류 정확도를 혼동행렬로 측정하는 방법

AI는 고객지원 티켓에 빠르게 라벨을 붙일 수 있지만, 실제로 사용하기 전에 사람이 직접 라벨링한 소규모 기준 데이터와 비교해 성능을 측정해야 합니다. 이 글에서는 작은 합성 예제를 사용해 AI 예측을 정답 라벨과 비교하고, Python으로 정확도와 혼동행렬을 계산합니다.

목차 보기

이런 분께AI 채팅 도우미로 고객지원 티켓 같은 짧은 텍스트를 분류하고, 분류 정확도를 간단하게 측정하고 싶은 사람을 위한 내용입니다.

준비사항
  • Python 3.12
  • CSV 파일과 분류 라벨에 대한 기본적인 이해

01몇 개의 예시만 믿지 말고 AI 분류 성능을 측정해야 하는 이유

AI 채팅 도우미는 고객지원 티켓, 이메일, 댓글, 짧은 메모에 카테고리를 빠르게 붙일 수 있습니다. 하지만 몇 개의 예측이 그럴듯해 보인다고 해서 전체 분류 정확도를 알 수 있는 것은 아닙니다. 중요한 것은 개별 예측이 자연스러워 보이는지가 아니라, 사람이 미리 라벨링한 기준 데이터와 AI가 얼마나 자주 일치하는지를 확인하는 것입니다.

작은 수작업 라벨 데이터셋을 사용하면 고정된 정답표를 만들 수 있습니다. 각 AI 예측을 그 정답과 비교하고 정확도를 계산한 뒤, 혼동행렬을 통해 어떤 클래스끼리 자주 혼동되는지 확인할 수 있습니다. 정확도는 전체 성능을 하나의 숫자로 보여주고, 혼동행렬은 오류가 발생한 구체적인 패턴을 보여줍니다.

02작은 합성 라벨 데이터셋 만들기

다음 8개의 고객지원 티켓은 이 튜토리얼을 위해 만든 합성 예제입니다. 라벨은 billing, login, bug 세 개를 사용합니다. billing은 결제나 청구서 문제, login은 인증이나 계정 접근 문제, bug는 소프트웨어가 정상적으로 동작하지 않는 문제를 의미합니다. hand_label 열은 AI에게 분류를 요청하기 전에 사람이 미리 지정한 기준 라벨입니다.

idtickethand_label
T1I was charged twice for the same order.billing
T2My password reset link says it has expired.login
T3The app closes whenever I open the reports page.bug
T4Where can I download last month's invoice?billing
T5I cannot sign in after changing my email address.login
T6The export button does nothing when I click it.bug
T7My card was declined but the bank says it is fine.billing
T8The dashboard shows a blank screen after login.bug

데이터셋이 작기 때문에 예상 라벨을 모두 사람이 직접 검토할 수 있습니다. 따라서 더 큰 라벨 데이터셋에 같은 방법을 적용하기 전에 평가 흐름 자체가 올바른지 확인하기에 적합합니다.

03각 티켓에 정해진 라벨 하나만 반환하도록 AI에 요청하기

분류 프롬프트는 허용되는 라벨과 출력 형식을 고정할수록 평가하기 쉽습니다. 별도의 설명이 필요하지 않다면 자유 형식 설명을 요구하지 않는 편이 좋습니다. 불필요한 문장이 추가되면 결과 비교가 어려워집니다.

  1. 허용되는 라벨을 billing, login, bug로 한정합니다.
  2. 각 라벨의 의미를 짧게 정의합니다.
  3. 각 티켓마다 정확히 하나의 라벨만 반환하도록 요구합니다.
  4. 티켓 ID는 변경하지 않도록 지정합니다.
  5. 애매한 티켓이 있더라도 새로운 카테고리를 만들지 말라고 명시합니다.

04AI 출력과 기준 라벨 비교하기

AI가 다음과 같은 합성 예측을 만들었다고 가정합니다. 8개 중 6개가 정답입니다. T7은 기준 라벨이 billing이지만 AI는 login으로 예측했고, T8은 기준 라벨이 bug지만 AI는 login으로 예측했습니다.

idhand_labelai_labelcorrect
T1billingbillingyes
T2loginloginyes
T3bugbugyes
T4billingbillingyes
T5loginloginyes
T6bugbugyes
T7billingloginno
T8bugloginno

따라서 전체 정확도는 8개 티켓 중 6개 정답입니다. 손으로 계산하면 6 ÷ 8 = 0.75이므로 정확도는 75%입니다.

05정확도만 보지 말고 혼동행렬 읽기

혼동행렬은 기준 라벨과 예측 라벨의 조합별 개수를 세는 표입니다. 이 예제에서는 행을 hand_label, 열을 AI 예측 라벨로 둡니다. 대각선에 있는 값은 정답이고, 대각선 밖의 값은 잘못 분류된 사례입니다.

hand \ predictedbillingloginbug
billing210
login020
bug012

이 행렬을 보면 두 오류의 형태를 바로 확인할 수 있습니다. billing 티켓 하나가 login으로 잘못 분류되었고, bug 티켓 하나도 login으로 잘못 분류되었습니다. 반대로 이 작은 예제에서는 기준 라벨이 login인 티켓을 billing이나 bug로 잘못 예측한 사례는 없습니다. 이것이 login 클래스가 일반적으로 가장 잘 분류된다는 의미는 아니며, 단지 이 8개의 합성 사례에서 나타난 결과일 뿐입니다.

06Python으로 정확도와 혼동행렬 계산하기

비교 데이터를 id, hand_label, ai_label 열이 있는 ticket_labels.csv에 저장합니다. 아래 스크립트는 라벨이 허용된 값인지 확인하고, 정답 개수를 세고, 혼동행렬을 만든 뒤 결과를 outputs/classification_report.txt에 저장합니다. Python 3.12 표준 라이브러리만 사용하고 입력 파일은 변경하지 않으며, 출력 파일이 이미 존재하면 중단합니다.

python
import csv
from collections import Counter
from pathlib import Path

INPUT_FILE = Path("ticket_labels.csv")
OUTPUT_DIR = Path("outputs")
OUTPUT_FILE = OUTPUT_DIR / "classification_report.txt"
LABELS = ["billing", "login", "bug"]


def main():
    if not INPUT_FILE.is_file():
        raise SystemExit(f"Input file not found: {INPUT_FILE}")

    if OUTPUT_FILE.exists():
        raise SystemExit(f"Output already exists: {OUTPUT_FILE}")

    rows = []
    with INPUT_FILE.open("r", encoding="utf-8", newline="") as file:
        reader = csv.DictReader(file)
        required = {"id", "hand_label", "ai_label"}
        if reader.fieldnames is None or not required.issubset(reader.fieldnames):
            raise SystemExit("ticket_labels.csv is missing required columns.")

        for row_number, row in enumerate(reader, start=2):
            hand = row["hand_label"].strip()
            predicted = row["ai_label"].strip()

            if hand not in LABELS:
                raise SystemExit(f"Invalid hand_label on row {row_number}: {hand}")
            if predicted not in LABELS:
                raise SystemExit(f"Invalid ai_label on row {row_number}: {predicted}")

            rows.append((row["id"].strip(), hand, predicted))

    if not rows:
        raise SystemExit("No labeled rows found.")

    correct = sum(1 for _, hand, predicted in rows if hand == predicted)
    accuracy = correct / len(rows)

    counts = Counter((hand, predicted) for _, hand, predicted in rows)

    report = []
    report.append(f"tickets={len(rows)}")
    report.append(f"correct={correct}")
    report.append(f"accuracy={accuracy:.3f}")
    report.append("")
    report.append("confusion_matrix")
    report.append("hand\\predicted\t" + "\t".join(LABELS))

    for hand in LABELS:
        values = [str(counts[(hand, predicted)]) for predicted in LABELS]
        report.append(hand + "\t" + "\t".join(values))

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    OUTPUT_FILE.write_text("\n".join(report) + "\n", encoding="utf-8")
    print(f"Wrote: {OUTPUT_FILE}")


if __name__ == "__main__":
    main()

07분류기를 사용하기 전에 오류 사례와 한계 검토하기

75%라는 정확도 숫자보다 두 개의 오분류 사례를 직접 확인하는 것이 더 중요할 수 있습니다. T7에는 card라는 단어가 포함되어 있으므로 정의가 불명확한 프롬프트에서는 계정 접근 문제와 혼동될 수 있지만, 이 예제의 기준 분류 체계에서는 billing입니다. T8에는 after login이라는 표현이 있지만 실제로 보고된 문제는 빈 대시보드 화면이므로 기준 체계에서는 bug로 분류됩니다. 이런 사례는 개별 키워드보다 실제 문제의 유형을 기준으로 라벨 정의를 작성해야 한다는 점을 보여줍니다.

  • 프롬프트나 라벨 정의를 수정하기 전에 모든 불일치 사례를 사람이 직접 확인합니다.
  • 다른 프롬프트나 AI 실행 결과를 비교할 때는 동일한 라벨 정의를 유지합니다.
  • 독립적인 평가를 유지하려면 테스트 데이터에 들어 있는 예시를 학습용 프롬프트 예제로 다시 사용하지 않습니다.
  • 클래스 비율이 불균형하거나 특정 오류가 더 중요하다면 전체 정확도만 사용하지 않습니다.
  • 실제 운영 성능을 판단하기 전에 사람이 라벨링한 데이터의 규모를 늘립니다.

혼동행렬은 테스트한 표본에서 나타난 결과를 설명하는 도구이지 미래 성능을 증명하는 것은 아닙니다. 작은 균형 예제는 평가 방법을 익히는 데는 유용하지만, 실제 평가에서는 실제 티켓의 표현 방식, 애매한 사례, 클래스 빈도를 충분히 반영할 수 있는 규모의 수작업 라벨 데이터가 필요합니다.

실행·검증 기록

2026-09-21 · hand-checked example · Python 3.12

  • 이 글의 8개 고객지원 티켓과 라벨은 모두 합성 예제입니다.
  • 예제에는 티켓이 8개 있고 AI 예측 중 6개가 정답입니다.
  • 정확도는 손으로 확인했습니다: 6 ÷ 8 = 0.75, 즉 75%입니다.
  • 혼동행렬의 billing 행은 손으로 확인했습니다: billing으로 예측 2개, login으로 예측 1개, bug로 예측 0개입니다.
  • login 행은 손으로 확인했습니다: billing으로 예측 0개, login으로 예측 2개, bug로 예측 0개입니다.
  • bug 행은 손으로 확인했습니다: billing으로 예측 0개, login으로 예측 1개, bug로 예측 2개입니다.
  • 혼동행렬의 전체 합은 8로 예제 티켓 수와 일치합니다.
  • 제시된 Python 로직은 Python 표준 라이브러리의 csv, collections, pathlib만 사용하며 기존 출력 파일을 덮어쓰지 않도록 구성되어 있습니다.
검증 범위의 한계
  • 이 글의 Python 코드는 직접 실행하지 않았으며 동작은 수동으로 검토했습니다.
  • 8개 티켓으로 구성된 이 합성 데이터셋은 실제 운영 환경의 분류 정확도를 신뢰성 있게 추정하기에는 너무 작습니다.
  • 특히 클래스 비율이 불균형하면 전체 정확도만으로 클래스별 문제를 파악하기 어려울 수 있습니다.
  • 측정 결과는 사람이 만든 기준 라벨의 품질과 일관성에 영향을 받습니다.

사이트 전체의 작성·검증 원칙

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.