AIの業務活用

AIの問い合わせ分類精度を混同行列で測る方法

AIは問い合わせチケットを素早く分類できますが、実際に使う前に人手でラベル付けした小さな基準データと比較して精度を測る必要があります。このガイドでは、小さな合成例を使い、AI予測と正解ラベルを比較してPythonで正解率と混同行列を確認します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者AIチャットアシスタントで問い合わせチケットなどの短い文章を分類し、その精度を簡単に測定したい人向けです。

準備するもの
  • Python 3.12
  • CSVファイルと分類ラベルに関する基本的な理解

01数件の例だけを信じず分類精度を測る理由

AIチャットアシスタントは、問い合わせチケット、メール、コメント、短いメモにカテゴリを付けられます。しかし、数件の予測がもっともらしく見えても、全体の分類精度は分かりません。重要なのは個々の予測が自然に見えるかではなく、あらかじめ人がラベル付けした基準データとAIの予測がどの程度一致するかです。

小さな人手ラベル付きテストセットを用意すると、固定された正解表として使えます。各AI予測をその正解と比較し、正解率を計算し、さらに混同行列を使ってどのクラス同士が混同されているか確認できます。正解率は全体を1つの数字で示し、混同行列は誤りのパターンを示します。

02小さな合成ラベルデータを作る

以下の8件の問い合わせチケットは、このチュートリアル用に作成した合成例です。ラベルは billing、login、bug の3種類を使います。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各チケットに決められたラベルを1つだけ返すよう依頼する

分類プロンプトは、許可するラベルと出力形式を固定すると評価しやすくなります。説明が別途必要でなければ、自由記述を求めない方が比較しやすくなります。

  1. 許可するラベルを billing、login、bug のみに限定します。
  2. 各ラベルの意味を短く定義します。
  3. 各チケットに対して正確に1つのラベルだけ返すよう求めます。
  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

この行列を見ると、2つの誤りの内容がすぐ分かります。billing のチケット1件が login に分類され、bug のチケット1件も 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%という数値だけを見るより、2件の誤分類を直接確認する方が重要です。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ロジックは標準ライブラリの csv、collections、pathlib のみを使い、既存の出力ファイルを上書きしないよう構成されています。
検証範囲の限界
  • この記事のPythonコードは実行していません。記載した動作は手作業による確認に基づいています。
  • 8件の合成データでは実運用環境の分類精度を信頼できる形で推定するには小さすぎます。
  • 特にクラス分布が不均衡な場合、正解率だけではクラス別の問題を隠す可能性があります。
  • 測定結果は人手で作成した基準ラベルの品質と一貫性に依存します。

サイト全体の執筆・検証方針

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。