画像分類器を学習する
フォルダ形式のデータセットを用意し、MNIST160でyolo26n-cls.ptを学習して、確率とtop-1精度を確認し、信頼度の低い予測を人によるレビューに回します。
画像分類は、画像ごとに「これは何か」という1つの問いに答えます。必要なラベルはどのタスクよりも簡単なフォルダ名だけで、最も高速に動作します。そのため、検出器の後段に置く標準的な第2ステージであり、1フレームに1つの部品が映る検査ステーションに最適な最初のモデルです。データセットにYAMLはなく、フォルダ構成が仕様になります。また、分類器は、見たことのない画像に対しても必ず答えを返します。その答えをいつ信頼しないか判断することも、作業の半分です。
実践的
フォルダ形式のデータセット構成#

分類データセットはYAMLファイルではなくディレクトリです。data=にはルートフォルダを指定します。
parts/
├── train/
│ ├── good/ img_0001.jpg ...
│ ├── scratch/ img_0412.jpg ...
│ └── dent/ img_0733.jpg ...
└── val/
├── good/
├── scratch/
└── dent/インストールされたソースコードと分類データセットガイドに基づく、ローダーの動作は次のとおりです。
| 状況 | 動作 |
|---|---|
| クラスインデックス | train/以下のフォルダ名をアルファベット順に並べる → dent=0、good=1、scratch=2 |
| 検証フォルダ | val/、次にvalidation/、次にvalid/を使用します。警告を出してtest/にフォールバックします。 |
train/フォルダはなく、クラスフォルダのみがある場合 | 自動的にtrain/とval/に80/20で分割します |
Ultralytics Platformでは、分類データセットには形状ではなく、画像ごとに画像レベルのラベルが1つ付与されます。

症状: クラスを追加した後、既存のインテグレーションがgoodの部分をdentとして報告します。 原因: bentという名前のフォルダーを追加すると、そのフォルダーが先頭に並び、その後のすべてのインデックスがずれます。 対処: インデックスは常にmodel.names(またはresult.names)を使って名前に対応付けます。インデックスをハードコードしないでください。
MNIST160で学習する#
MNIST160は手書き数字160枚のデータセットです。train/とtest/にそれぞれ80枚あり、各セットは10個の数字フォルダに分かれています。CPUのスモークテストにちょうどよい小ささです。val/フォルダがないため、ローダーが警告を出してtest/にフォールバックすることを確認してください。
from ultralytics import YOLO
model = YOLO("yolo26n-cls.pt")
model.train(data="mnist160", epochs=1, imgsz=64, device="cpu")
metrics = model.val()
print(f"top-1: {metrics.top1:.3f} top-5: {metrics.top5:.3f}")WARNING ⚠️ Dataset 'split=val' not found, using 'split=test' instead.
val: .../datasets/mnist160/test... found 80 images in 10 classes ✅
all 0.162 0.463
top-1: 0.162 top-5: 0.4631エポックでは、新しい10クラスのヘッドはほとんど改善しません。私たちの実行では、ノートPCのCPUで約10秒かけてepochs=10がtop-1 0.55、top-5 0.95に到達しました。処理は動作していますが、学習スケジュールが必要です。Classifyタスクガイドに、事前学習済みモデルのサイズが掲載されています。
Top-1は、最も確率の高いクラスが正解となる画像の割合です。Top-5は、確率上位5クラスのいずれかに正解クラスが含まれていれば、正解として数えます。クラスが大まかなものから非常に細分化されたものまである1,000クラスのImageNetでは、Top-5が参考になります。クラス数が5以下の場合は常に1.0となり、10クラスの場合でも判定は甘めです。top-1精度とクラスごとの混同行列を基準に判定してください。

コードで確率を読み取る#
from ultralytics import YOLO
model = YOLO("yolo26n-cls.pt") # ImageNet, 1,000 classes
result = model("https://ultralytics.com/images/bus.jpg")[0]
probs = result.probs
print(f"top-1: {result.names[probs.top1]} ({float(probs.top1conf):.2f})")
for i, conf in zip(probs.top5, probs.top5conf.tolist()):
print(f" {result.names[i]:20s} {conf:.3f}")top-1: minibus (0.52)
minibus 0.516
police_van 0.369
trolleybus 0.096
minivan 0.006
ambulance 0.003probs.top1はインデックス、probs.top5はインデックスのリスト、probs.dataは確率ベクトル全体です。top-1の信頼度に注目してください。0.52はminibusとpolice_vanのどちらかを選ぶ際、コイントスで決めるのとほとんど変わりません。この値を基準に振り分けます。
確信度の低い予測を人に回す#
本番環境の分類器では、すべての画像で正解する必要はほとんどありません。分類器だけで判断すると決めた画像で正解する必要があります。このスクリプトは10エポックのモデルを学習し、異なる信頼度しきい値を適用した場合に、保留した80枚の数字画像でどうなるかを測定します。
from pathlib import Path
from ultralytics import YOLO
model = YOLO("yolo26n-cls.pt")
model.train(data="mnist160", epochs=10, imgsz=64, device="cpu", plots=False) # model now holds best.pt
print(model.names) # folder names, sorted, became class indices
test = Path(model.trainer.data["test"]) # mnist160/test, the split the loader validated on
images = sorted(test.rglob("*.png"))
results = model(images, imgsz=64, verbose=False)
truth = [p.parent.name for p in images]
for threshold in (0.0, 0.3, 0.6):
kept = [(r, t) for r, t in zip(results, truth) if r.probs.top1conf >= threshold]
correct = sum(r.names[r.probs.top1] == t for r, t in kept)
print(f"threshold {threshold:.1f}: auto-accept {len(kept):2d}/{len(images)}, accuracy on accepted {correct / max(len(kept), 1):.0%}"){0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9'}
threshold 0.0: auto-accept 80/80, accuracy on accepted 55%
threshold 0.3: auto-accept 41/80, accuracy on accepted 68%
threshold 0.6: auto-accept 10/80, accuracy on accepted 80%各行はトレードオフを示しています。しきい値を上げると、採用する予測の精度は高まりますが、人による確認に回す画像も増えます。0.6の場合、このモデルだけで判断できる数字画像は80枚中10枚にすぎません。まだ何も自動化できる段階ではなく、必要なのは巧妙なしきい値ではなく、追加の学習です。学習ラウンドごとに同じ表を再計算してください。実現できる精度と、各しきい値で発生するレビュー作業量の両方がわかります。
保留データで測定し、受け入れ画像の精度が要件を満たす範囲で最も低いしきい値を選びます。次に、そのしきい値によって発生するレビュー量をチームが処理できることを確認します。難しい場合は、モデルを改善します。
よくある落とし穴#
症状: 空のステーション、間違った部品、またはレンズを覆う手に対して、確信度の高いクラスラベルが付与されます。 原因: softmaxの確率の合計は常に1になるため、分類器は必ずいずれかのクラスを選びます。 対処: 該当する画像だけを入れたother/フォルダーを追加し、確信度の低いtop1conf予測はレビューに回します。
症状: 学習前の事前学習済みモデルをデータセットで評価すると、スコアがほぼ0になります。 原因: yolo26n-cls.ptは1,000個のImageNetインデックスを予測しますが、データセットのフォルダーではインデックス0~9を異なる意味で再利用しています。MNIST160では、フォルダー0がインデックス0ですが、事前学習済みヘッドはこれをtenchと判定します。この方法ではtop-1が0.000になることを確認しました。 対処: まずファインチューニングを行い、その後で検証します。
症状: 検証時のtop-1は1.0近くなのに、本番環境では大幅に低下します。 原因: train/とval/に、同じ部品を撮影したほぼ同一の写真が含まれています。 対処: データセットを正しく分割するの例のように、部品単位またはセッション単位で分割します。
分類器の事前学習時の入力サイズは640ではなくimgsz=224です。大きなフレーム内の小さな物体には、より大きなimgszを使うよりも、検出してから分類する方法(次のレッスン)のほうが適しています。
解答を表示
from ultralytics import YOLO
for epochs in (1, 10):
model = YOLO("yolo26n-cls.pt")
model.train(data="mnist160", epochs=epochs, imgsz=64, device="cpu")
metrics = model.val()
print(f"{epochs:2d} epochs top-1: {metrics.top1:.3f} top-5: {metrics.top5:.3f}")
print(model.names) # {0: '0', 1: '1', ..., 9: '9'}実行結果は、1エポックでtop-1 0.162、top-5 0.463、10エポックでtop-1 0.550、top-5 0.950でした。5クラスの場合、正解は必ず上位5クラスに含まれるため、top-5は定義上1.0となり、何の情報も得られません。
自動採用した画像で75%の精度を得るには、route.pyによると0.6(採用10枚で80%)がしきい値となり、80枚中70枚をレビューに回すことになります。これが正直な答えです。しきい値は精度要件を満たしますが、モデルはまだ実用的ではありません。実運用のしきい値を選ぶ前に、さらに学習して表を再計算してください。