3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Jev】商品名のカテゴリ判定をLLMからJevに移したら何が変わるのか100件で確かめてみた

3
Posted at

はじめに

商品マスタで「商品名からカテゴリを付ける」処理を、正規表現・LLM2種・Jevの4手法に同じ100件を通し、正解ラベルを手で付けて採点しました。結論は次の3行です。

  • 正答率は正規表現55%、Opus 5とgpt-5.6-lunaが93%、Jevが85%。Jevは正答率ではLLMに8ポイント負けた
  • それでもJevを選ぶ理由は確信度にある。確信度1.00の43件は98%的中、0.50未満の12件は33%しか当たらない。当たらない行をJev自身が申告してくる
  • 確信度0.5でしきい値を切ると、自動処理に回した88件の的中率は92.0%まで上がる。人が見るのは12件で済む

対象読者: 商品名・品名・問い合わせ文などの短いテキストを、キーワードのルール表で分類している人。

この記事ではJevの基本的な使い方やプリミティブの解説はしません。すでに解説記事が多く出ているので、そちらに譲ります。ここで扱うのは「既存のルールベース処理を置き換えたら何が変わるか」だけです。

参考文献

検証の設計

データ

Open Food Facts から「日本の商品」として登録されているものを取得し、かな・カタカナ・CJK漢字のいずれかを含む商品名だけを残して、乱数シードを固定して100件を抽出しました。母集団は489件です。

この絞り方だと漢字だけの中国語名も通ります。実際に「好侍百梦多咖喱」(ハウス バーモントカレー)が混ざりました。言語判定まではしていません。

有志が登録したデータなので、実際の商品マスタと同じくらい汚れています。抽出された100件にはこういう名前が入っていました。

商品名 何が厄介か
生茶 種別を示す語が「茶」しかなく、ブランド名で完結している
超熟 6枚スライス 食パンだが「パン」の字が無い
淡麗 グリーンラベル〈生〉 ビールだが酒を示す語が無い
マルエフ 同上。しかも略称
好侍百梦多咖喱 中国語表記のカレールウ
グリーンティー 実体は抹茶アイス
究極の水ゼリー 「水」が入るが菓子
ホワイトサワー ノンアルコール 「サワー」が入るが酒ではない

商品マスタを触ったことがある人なら、この時点で嫌な予感がすると思います。

カテゴリ定義

飲料・酒類・菓子デザート・乳製品・パンシリアル穀類・調味料油・加工食品惣菜・その他の8つを定義しました。定義文は1つのPythonモジュールに置いてあります。

LLMとJevにはこの定義文をそのまま渡しています。正規表現だけは定義文を機械的に使えないので、同じ定義を読みながらキーワード表を手で書きました。

揃えたのは「同じ100商品」と「同じカテゴリ定義」の2つだけです。呼び出し形式は揃っていません。LLMには10商品ぶんの一覧と出力形式の指示を加えたプロンプトを送り、Jevには1商品を state に、カテゴリ定義を choicecriteria に入れて送っています。

境界は事前に決めています。牛乳そのものは乳製品、缶コーヒーや乳飲料は飲料、アイスは菓子デザートです。この線引きを曖昧にすると後の突き合わせが成立しません。

4つの手法

手法 実行方法
正規表現 キーワード表を上から順に評価し、最初に当たったカテゴリを採用
Opus 5 claude -p --model opus を10件ずつのバッチで10回
gpt-5.6-luna codex exec -m gpt-5.6-luna を同じバッチ構成で10回
Jev POST https://api.typesafe.ai/v1/systemone を1商品につき1回、計100回

正規表現はわざと弱くしていません。酒類・乳製品・飲料・菓子などに思いつく限りのキーワードを並べ、7カテゴリぶんのルールを書いています。

Jevだけ1件ずつにしたのは、この検証でそう決めたからです。Jevは1リクエストにつき state は1つですが、questions は複数送れます。複数の商品を構造化して state に詰め、商品ごとに質問を立てればバッチ化もできます。今回はLLM側のバッチと条件が混ざるのを避けて、1商品1リクエストに揃えました。

正解ラベル

100件すべてに手でカテゴリを付けました。判定結果を見てから付けると引きずられるので、4手法の出力を横に並べたExcelで、定義シートを見ながら1行ずつ埋めています。この記事の正答率はすべてこのラベルを正としたものです。

正答率は正規表現55%、LLM93%、Jev85%

jev-classification-accuracy.png

誤りの内訳は「その他と答えて外した」ぶんと「別カテゴリに誤爆した」ぶんに分けています。3列の合計が100件です。

手法 正解 「その他」と答えて外した 別カテゴリに誤爆
正規表現 55 32 13
Opus 5 93 0 7
gpt-5.6-luna 93 2 5
Jev 85 4 11

正規表現が「その他」と答えたのは36件で、うち4件は本当に「その他」が正解だったため、外したのは32件です。

正直に書くと、始める前は「判断特化モデルなんだからJevが一番当たるだろう」と思っていました。外れました。 LLM2種が93%で並び、Jevは85%です。8ポイントの差は100件では小さくありません。

一方で正規表現の55%は予想以上にひどい数字でした。半分近くを外しています。

Jevの確信度は実際に当たり外れと対応していた

ここからがJevを使う理由です。Jevは choice で選んだカテゴリと一緒に confidence と全選択肢の確率分布を返します。確信度の帯ごとに的中率を出すとこうなりました。

jev-confidence-calibration.png

確信度 件数 的中率
1.00(0.999以上) 43 98%
0.90以上1.00未満 22 91%
0.70以上0.90未満 17 82%
0.50以上0.70未満 6 83%
0.50未満 12 33%

0.50〜0.70と0.70〜0.90がほぼ並んでいる以外は、確信度が下がるほど的中率も下がります。とくに0.50未満の12件は3分の1しか当たっていません。この100件では、Jevが外す行は低い確信度に寄っていました。

実際の誤り15件のうち、確信度0.50未満だったものが8件を占めます。たとえば北海道の焼き菓子「元祖 山親爺」は確信度0.15で酒類と答えていました。外していますが、0.15という数字が「これは信用するな」と言っています。

逆に確信度1.00で外したのは1件だけで、「カロリーメイト ブロック チョコレート味」でした。これは後述の境界ケースです。

ここは同じ100件の中で見た相関であって、別のデータで同じ傾向が出る保証はありません。しきい値を実務で決めるなら、自分のデータで同じ表を作り直す必要があります。

しきい値を切ると自動処理の精度が上がる

確信度でふるいにかけたときの数字です。

しきい値 自動処理する件数 その的中率 人が見る件数
0.5以上 88 92.0% 12
0.7以上 82 92.7% 18
0.9以上 65 95.4% 35

Jev全体の正答率は85%ですが、確信度0.5で切るだけで自動処理分は92.0%に上がります。人が見るのは12件だけです。0.9まで上げれば95.4%ですが、人が見る件数は35件に増えます。このトレードオフを数字で置けるのが大きい。

LLMは93%と単体では強いものの、今回のCLI経由の出力には確信度がありません。どの7件が間違っているか分からないので、93%を受け入れて全件流すか、全件を人が見るかになります。

ただしこれはLLMの限界ではなく、今回の測り方の問題です。OpenAI互換APIの logprobs を使えばトークンの確率は取れますし、プロンプトで自己申告の確信度を出させることもできます。前者は「選択肢の確率」そのものではなく、後者は較正されている保証がありません。同じ土俵で比べるならここを揃えて測り直す必要があります。Jevの利点は、確率を返すことが仕様として最初から入っている点です。

一方で今回の正規表現実装には「自信がない」という出力そのものが無いので、間違いを間違いとして検出する手段がありません。ルールごとに信頼度を持たせる作りにすれば近いことはできますが、そのスコアが較正されているかは別途測る必要があります。

今回の検証の限界

数字をそのまま信じないでほしい点が3つあります。

1. 100件は少ない。 正答率の差が8ポイントあっても、100件では偶然の幅に収まる可能性があります。差を主張するなら件数を増やす必要があります。

2. 食品に偏っている。 Open Food Factsは食品データベースなので、日用品や生鮮が入っていません。実際のPOSマスタはもっと雑多です。

3. 各手法とも1回しか流していない。 LLMは温度パラメータを指定していないので、同じプロンプトでも実行のたびに結果が変わり得ます。Jevについても、同じ入力を繰り返して結果が一致するかは確認していません。決定的に見えるという印象だけで、この検証では裏が取れていません。確実に決定的なのは正規表現だけです。

リクエストで引っかかった2つのところ

criteria の形が質問タイプによって違う

Jevの questions は配列ではなくオブジェクトで、キーが質問IDになります。ここまでは公式ドキュメントどおりなのですが、criteria の形が質問タイプごとに違います。

# choice: 選択肢名をキーにしたマップ
"criteria": {"飲料": "そのまま飲む液体。...", "酒類": "アルコールを含む飲料。..."}

# score: 配列。順にレベル0から割り当てられる
"criteria": ["まったく当てはまらない", "少し当てはまる", "強く当てはまる"]

# noul: true / false をキーにしたオブジェクト
"criteria": {"true": "...", "false": "..."}

自分は最初 choice にも配列を渡してエラーを踏みました。3つとも「criteria」という同じ名前なので、1つ覚えると他も同じだと思い込みます。

カテゴリ定義をそのままマップとして渡せるのは、実装としてはきれいでした。Pythonの辞書をそのままJSONに載せるだけで済みます。

subprocess から CLI を呼ぶと標準入力で止まる

これはJevの話ではなく、比較対象のLLMを回すときに踏んだものです。codex exec をPythonの subprocess.run から呼んだら、10分待っても返ってきませんでした。

シェルで同じコマンドを < /dev/null 付きで直接叩くと5秒で終わります。そこで標準入力を疑いました。

subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL)

stdin=subprocess.DEVNULL を足したら通りました。ただしプロンプトは位置引数で渡しているので、CLIが標準入力を読む理由はコード上ありません。「標準入力待ちだった」と断定はできず、原因は特定できていないというのが正確なところです。

CLIをバッチから叩くときは、読む理由が無くても stdin を塞いでおくのが安全だと学びました。タイムアウトまで何も出力されないので、原因にたどり着くのに時間を使います。

まとめ

商品名のカテゴリ判定という、小売の現場でよくある処理に4手法を当てて採点しました。

正規表現は55%でした。誤りの内訳は、名前に種別語が無くて手が出ない取りこぼしが32件、文字列は当たるが商品が違う誤爆が13件です。後者はルールの優先順位を入れ替えても別の商品が壊れるので、根本的に解けません。

Jevは85%で、LLMの93%に負けました。判断特化モデルだから精度で勝つ、という期待は外れています。それでもJevに価値があるのは、**確信度1.00の43件が98%、0.50未満の12件が33%**という形で、外す行が低い確信度に寄っていたからです。確信度0.5で切れば自動処理88件が92.0%になり、人が見るのは12件で済みます。

正答率という1つの数字だけで選ぶとLLMになります。運用まで含めて設計するなら話が変わります。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?