はじめに
『Data-centric AI入門(技術評論社)』を読み、考え方は理解したものの、実践のイメージが持てずにいました。そこで、モデルを固定(YOLOv8n・スクラッチ・640px)し、データだけを改善して精度を競う 3LC Multi Vehicle Detection Challenge に Late Submission で取り組みました。本記事は、その実践の記録です。
結論
先に結論を示します。
| 試行 | 評価 | 結果 |
|---|---|---|
| コンペスターター + 3LC ワークフロー(=疑似ラベリング) | Public LB | +0.013 |
| epoch を 10→100(データ不変、学習を収束) | Public LB | +0.216 |
| out-of-fold(OOF) + データのクリーニング一式 | Public LB | +0.024 |
- データ改善の効果を測る前に、まず比較の足場となるモデルを収束させる必要があった。
- train のラベル誤りを調べるには、k-fold の out-of-fold(OOF)予測が要る。暗記の影響を避け、「ラベルの誤り」と「モデルの誤検出」を見分ける根拠になる。
- ラベル誤りは件数こそ多くないが、van/car の取り違えには、追跡アノテーションによって同じ誤りが連続フレームへ伝播する、局所的だが系統的なパターンがあった。加えて分割・冗長・分布といった構造の問題も目立った。
- 「ラベルを直す」「難例を増やす」といった手法をやみくもに試しても意味がない。「データのどこに問題があるか仮説を立て、学習に使っていないデータで確かめ、効かなければ次の仮説に進む」の繰り返しが重要。銀の弾丸は存在しない。
以下、この過程を順に追います。
想定読者
- Data-Centric AI の実践に迷っている方(初心者)
- 3LC / Cleanlab / FiftyOne / LightlyStudio といった各種Data-Centric AIツールの使用方法を知りたい方
3LC Multi Vehicle Detection Challenge
3LC.AI が 2026年4月10日〜6月9日に Kaggle で開催した車両検出コンペです。UA-DETRAC ベースの画像に truck / car / van / bus の4クラスのバウンディングボックスを付け、テストセットの mAP@0.5 を競います。300人以上・97チームが参加しました。
最大の特徴は、モデル側を完全に固定するルールです。
- モデルは YOLOv8n のみ・スクラッチ学習(事前学習重み禁止)
- 入力 640px 固定・アンサンブル / TTA / 疑似ラベリング禁止
改善できるのは「データ」だけ。ラベルを直して再学習するループを回すこと自体が競技になっています。
※ 2021 年の Data-Centric AI Competition とは題材こそ異なりますが、データセットの質で精度を競う点では共通しています。
Data-Centric AI とは
Model-Centric AI(データを固定してモデルを工夫する)の対義語です。モデルとコードを固定し、学習データの質(ラベルの正しさ、カバレッジ、クラスバランス、難例の充実度)を改善して精度を上げます。Andrew Ng が提唱し、「同じモデルのままでもラベルを直せばスコアが動く」という主張です。
- Data-Centric AI (YouTube)
- Data-Centric AI: A Data-Driven Machine Learning Approach(Landing AI)
- Data-Centric AIの紹介(kzykmyzw)
- Data-Centric AI - 関連する学術分野と実践例 -(kzykmyzw)
Ng が示す Data-Centric AI の反復は「学習 → 誤り分析 → データ改善 → 再学習」です。本コンペでは、主に ②誤り分析 と ③データ改善 を試します。
① 学習(固定モデル)
↓
② 誤り分析(ラベル誤り・偏り・重複の検出)
↓
③ データ改善(ラベル修正・難例追加・間引き…)
↓
④ 再学習(①へ戻る)
ここで肝心なのが「データ改善」の中身です。Ng のループでは1つの箱ですが、実際は多岐にわたります。これを体系化したのがサーベイ Data-centric AI: A Survey(Zha et al., ACM Computing Surveys, 2023)で、Data-Centric AI を 3つの目標に整理しています。
| 目標 | 内容 | 本コンペでの扱い |
|---|---|---|
| 学習データの構築 | 収集・ラベリング・前処理・削減・拡張 | 主戦場。ラベル誤り検出(ラベリング)・間引き(削減)・オーバーサンプル(拡張)を実施 |
| 推論データの構築 | モデルを評価・診断するための評価データの設計(分布内・分布外評価など) | テストセットは Kaggle 固定で触れない。余地はリークしない val/OOF を組むことだけ |
| データの維持 | データ理解・品質保証・保管(運用での継続的な維持) | 一発勝負のコンペではほぼ対象外。可視化・バージョニング(FiftyOne / 3LC)が「データ理解」に当たる程度 |
本コンペで扱えるのは、ほぼ「学習データの構築」です。本記事ではラベル誤りの検出を中心に、間引き(削減)とオーバーサンプル(拡張)も試します。Data-Centric AI はラベル修正だけではなく、データの収集・選定・品質評価・構成の見直しまで含めた取り組みです。本記事ではその中でも、コンペで実際に試せるラベル品質の改善、データ削減、データ拡張を扱います。
(参考)Data-Centric AI 向けの主なツール
いくつかデータ改善を行うためのツールが存在します。ループ上の位置は「①学習 → ②誤り分析 → ③データ改善 → ④再学習」のどこを担うかで示します。
| ツール | ループ上の位置 | 一言 | 規模感 |
|---|---|---|---|
| Label Studio / CVAT / Roboflow | ③(ラベリング) | 人手ラベリングと管理の定番 | 大(数万★級) |
| 3LC | ①〜④(全体) | Table のバージョニング+学習連携+Dashboard。本コンペ必須 | 新興 |
| Cleanlab | ②(検出) | confident learning でラベル誤りをスコア化。ラベル誤り検出でよく使われる | 中(1万★級) |
| FiftyOne | ②(可視化) | 画像・予測・GT・embedding を可視化して探索・評価。OSS では最大手級 | 中(1万★級) |
| Lightly | ③(選定) | SSL ベースのデータ選定・重複除去 | 小(数千★級) |
| Renumics Spotlight | ②(可視化) | embedding + メタデータのインタラクティブ可視化 | 小 |
| DataMapPlot | ②(可視化) | 大量データの埋め込みマップを綺麗に描くプロット専用 | 小 |
| Lilac | ②(テキスト向け) | テキスト・LLM データの重複検出・意味検索・PII 検出。CV の本コンペでは使わず | 中 |
★は執筆時点のおおよその GitHub Star 規模です(正確な値ではありません)。用途が違うので「どれがデファクト」は一概には言えませんが、ラベル誤り検出なら Cleanlab、可視化なら FiftyOne が代表格という印象でした。本記事で実際に使うのは太字の 3LC / Cleanlab / FiftyOne / Lightly の4つです。
上位解法
最初に上位勢の工夫を見ておきます。主催の 3LC.AI から結果発表がありました。
- 🥇 1位: Danh Nguyen Duc — Table 登録・Run 連携・Dashboard でのラベル修正まで含めた、完全にスクラッチからの YOLOv8n パイプライン
- 🥈 2位: Kodjo Josué AYITEY — よくドキュメント化されたData-Centricな取り組み
- 🥉 3位: Muhammad Haaris — スクラッチ・ラベル作業も丁寧な提出
- 🏅 Most Innovative Use of 3LC: Anand Koirala — Dashboard を使った深いラベル修正と、プロセス全体の徹底したドキュメント化
中でも2位チームの write-up が公開されていて、施策とスコアの対応が明確です。
| 施策 | 学び | スコア推移 |
|---|---|---|
| ラベル品質の改善(高信頼度の自動承認は逆効果。IoU==0 の予測だけを人間が目視レビュー) | confidence だけでは安全信号にならない | 0.543 → 0.755 |
| split leakage の解消(シーケンス単位で train/val を分割+サンプリング重み補正) | 隣接フレームのリークが評価を歪める | 0.755 → 0.927 |
| HDBSCAN で「逆光・夜間・遮蔽」など難例をオーバーサンプル | クラス単位より視覚クラスタ単位が効く | — |
| van の単純オーバーサンプルは逆効果(negative result) | 曖昧クラスを増やすと FP が増え LB 悪化 | — |
「どの施策も触れば良くなるわけではない。結果を検証しながら進める必要がある」というのが write-up からの学びでした。
実践 Data-Centric AI
ここからは実際に手を動かした記録です。使うツールは、ルール上使用必須の 3LC に加え、ラベル品質評価の Cleanlab、データ可視化・探索の FiftyOne、データキュレーションの LightlyStudio です。
1.ベースライン作成
3LC はコンペのフローに組み込まれたツールです。register_tables.py(Table 登録)→ train.py(YOLOv8n 学習)→ predict.py(submission 生成)→ 3LC Dashboard でデータ改善 → 再学習、という流れになっています。
まずスターターをそのまま実行したベースライン(10 epoch)の提出スコアは Public 0.53982 / Private 0.52867。10 epoch なので Loss はまだ下がりそうです。
3LC Dashboard では GT と予測を見比べられ、truck を bus、car を van と誤るなどの弱点が見えました。
2.3LCワークフローによる改善
まずは 3LC のビルトイン機能「ワークフロー」を使ってみました。
「Object detection basic insights tour」をStartすると、自動的にIoU計算や各種フィルタ適用がなされ、ベースラインモデルの出力をGTとする疑似ラベリング処理が実行されます。
このデータセットを使って再学習した結果は Public 0.55310(+0.013)。ほぼ変わりませんでした。この処理は「ベースラインモデル自身の予測を新しい GT に採用する」=疑似ラベリングなわけで、10 epoch の未熟なモデルの予測でラベルを上書きしても意味がなかったわけです(そもそも疑似ラベリングはこのコンペではルール違反でもあります)。
参考リンク:
- Part 1 - Competition Details and Environment setup
- Part 2 - Dataset Registration, Train baseline and First Kaggle submission
- Part 3 - Dashboard: inspect predictions, fix labels, retrain and iterate
- 3LC - Getting Started
3.まず学習を収束
疑似ラベリングが効かず、次は「ラベル誤りを見つけて直す」王道に進もうとして、足を止めました。
ここまでの分析は全部 10 epoch の未収束モデルを参照にしていたからです。
ラベルの怪しさをモデルの予測で測るのに、そのモデルが下手では話になりません。そこでデータは変えず、epoch だけ 10→100 に増やして再学習しました。
| Run | Public | Private |
|---|---|---|
| 10 epoch | 0.53982 | 0.52867 |
| 100 epoch(データは同一) | 0.75568 | 0.77858 |
データを変えず、収束させただけで Public +0.216。 これはデータ改善ではなく、以後の仮説を測るための基準線づくりです。未収束モデルの予測でラベルを直すと、モデルの弱さをデータの問題と取り違えます。
4.王道のラベルクレンジング
学習はある程度収束したので、ラベル誤りの検出に進みます。前述の通り Data-Centric AI はデータの収集・ラベリング・削減・拡張と幅広いのですが、まずはラベル誤りの検出から試しました。
4.1.誤りの「型」
やみくもに試す前に、誤りの種類を整理します。2026年に、物体検出のアノテーション誤りを標準化したサーベイ Quality over quantity: a data-centric survey of annotation errors in object detection datasets(Hussain et al., Artificial Intelligence Review, 2026)が出ており、これを棚卸しの枠組みに使いました。
| # | 型 | 内容 |
|---|---|---|
| 1 | Missed(見落とし) | 物体があるのに GT が無い |
| 2 | Incorrect(誤ラベル) | クラス取り違え/物体が無いのに GT |
| 3 | Localization(位置ズレ) | box が大きすぎ・小さすぎ・ずれ |
| 4 | Duplicate(重複) | 同一物体に複数 box |
| 5 | Inconsistent(方針の不整合) | 画像間でラベル方針がバラつく |
| 6 | Debatable(曖昧) | 人間でも判断が割れる。van/car 境界はここに当たり、OOF 不一致だけで修正は決めない |
| 7 | Group(まとめ囲い) | 複数物体を1つの box で囲う |
1〜3 は 後述する Cleanlab の ObjectLab: Automated Diagnosis of Mislabeled Images in Object Detection Data(Tkachenko et al., ICML DMLR Workshop, 2023)が返す3スコア(overlooked / swapped / badly located)とほぼ一致します。同サーベイの COCO 実測では Missed が最頻で、この順序は自分の結果とも合致しました。
4.2.k-fold の out-of-fold(OOF)予測の必要性
train のラベルを、その train で学習したモデルの予測で評価しても、まず正しくは測れません。
提供される train.py では、train テーブルで学習、val テーブルで評価を行います。収束した OOF なしの 100 epoch モデルで train を評価してしまうと、Cleanlab の swap / overlooked スコアがほぼ全画像で「問題なし」に張り付きました。モデルがラベルを丸暗記しているので、GT をそのまま返すだけになるのです。実際、下で見る「牽引車の付け忘れ」は、10 epoch では検出信頼度 0.98 で拾えていたのに、100 epoch では 0.19 まで下がって消えました。GT に無いものを毎エポック「背景」と教え込まれ、正しい検出を忘れてしまったわけです。
正攻法は k-fold cross-validation です。train を分割し、各 fold を「それを学習に使っていない別モデル」で予測すれば、暗記のかからない予測(OOF)が得られます。ただし UA-DETRAC はシーケンス動画なので、ランダム分割だと隣接フレームがリークして、また暗記でマスクされます。そこで シーケンスを丸ごと fold に割り当てる(GroupKFold) 必要がありました。3-fold で train 全体の OOF 予測を作り、以降の分析はこれを使います。
参考までに、OOF 予測で既知のケースがどう変わるかを見ます。
下表の値は Cleanlab の overlook スコアで、0 に近いほど怪しい(検出信頼度とは別物)です。
| ケース | 10ep(in-sample) | 100ep(in-sample) | 100ep(OOF) | 判定 |
|---|---|---|---|---|
| 駐車中の牽引車 | 0.00 | 1.00(丸暗記) | 0.00(復活) | 本物の付け忘れ |
牽引車は、弱い 10ep モデルも暗記のかからない OOF モデルも「truck がいる」と検出するのに GT に無い。100 epoch だけが暗記で握りつぶしていました。

駐車中の牽引車。周囲の車には緑の GT があるのに、中央の牽引車にだけ GT が無い(青枠=OOF が検出した見落とし位置)。
こうして OOF 予測は、「ラベルが間違っている」と「モデルが間違えた」を見分けるための強い追加根拠になります。もちろん OOF モデル自身も誤るため、これだけでラベル誤りを断定はしません。本記事では不一致の向き、連続フレームでの再現、目視も合わせて判断します。サーベイも「アノテーション誤りとモデル予測誤りは絡み合って分離が難しい」と課題提起していますが、k-fold OOF はその交絡を減らす具体的な手段になりました。
4.3.棚卸しの結果
OOF 予測を上記7分類で分析した結果を先に示します。
| 型 | 結果 |
|---|---|
| #1 Missed | 牽引車(MVI_40902、53/53 フレーム)は明確な付け忘れを検出。ただし他候補の多くは路肩の駐車車で #5 寄り |
| #2 Incorrect | van/car に局所的だが系統的な取り違えあり。特に GT=car なのに OOF が van と高信頼で予測する=本物の誤りが濃厚。逆向き(GT=van→car 予測)は多くがモデルのバイアスで、誤りとは言い切れない |
| #3 Localization | badloc は 22% も発火するが、モデルの box 精度との交絡で信頼できない |
| #4 Duplicate | 0 件(自動追跡データなので二重ラベル無し) |
| #5 Inconsistent | 路肩の駐車車を付けたり付けなかったり。ROI 方針の問題で誤りと断定しづらい |
| #6 Debatable | van/car 境界は人間の判断も割れうる。ただし #2 のように、GT=car→高信頼 OOF=van の一部は、連続フレームと目視も合わせると単なる曖昧さを越える |
| #7 Group | 検出を試みたが、連節バスの過分割による偽陽性ばかりで確定なし |
件数だけ見れば大量に崩れているわけではありません(重複ゼロ、明確な付け忘れも数個)。ただし van/car のクラス取り違えは系統的で、これは次節で掘り下げます。信頼できたのは #1 Missed と #2 Incorrect(GT=car→OOF=van)。他はモデル挙動との交絡で判定が難しい、という手応えでした。
以下では、この結果を得る過程(Cleanlab・FiftyOne・LightlyStudio を使った検証)を示します。
4.3.1.Cleanlabによるデータ品質チェック
Cleanlab は、Confident Learning を中核アルゴリズムとして、モデルの予測結果からデータセット中の label issues(ラベル誤りやデータ品質の問題)を推定する Data-Centric AI ライブラリです。物体検出向けの cleanlab.object_detection は、前節の7分類の #1〜#3 に対応した3つのサブスコアに誤りを分解します。
| Cleanlab の関数 | 検出する誤り | サーベイの型 |
|---|---|---|
compute_overlooked_box_scores |
見落とし | #1 Missed |
compute_swap_box_scores |
クラス取り違え | #2 Incorrect |
compute_badloc_box_scores |
box の位置ズレ | #3 Localization |
前節の通り、渡すのは OOF 予測です。推論時の confidence 閾値は極端に低く(conf=0.001)します。高信頼度に絞ると、モデルが自信なく捉えた正しい検出まで「予測なし」扱いになり、正しい GT を誤って怪しいと判定しかねないためです(公式チュートリアルもほぼ生の候補を渡す方針)。
以下が OOF 予測(train 7,852枚)での結果です(スコアは低いほど怪しい)。
| サブスコア | フラグ枚数(閾値0.2) | 所感 |
|---|---|---|
| overlooked(見落とし) | 83 / 7,852 | 一番信頼できた。牽引車の付け忘れ等 |
| swap(クラス誤り) | 18 / 7,852 | 少数だが本物を含む。上位20枚中15枚が GT=car → van の取り違え |
| badloc(位置ズレ) | 1,748 / 7,852(約22%) | 発火しすぎ。box 精度との交絡で信頼できない |
前節のアンマスキングで見た通り、overlooked は牽引車の付け忘れをクリーンに拾えました。swap は数こそ少ないですが、不一致の向きを読むと本物が混じります(GT=car なのにモデルが van と断言する側)。一方 badloc はモデルの box 精度と交絡して発火しすぎ。overlooked と「向き付き」の swap は効くが、badloc は交絡する——これが Cleanlab を使う勘所でした。
標準的な使い方は、公式チュートリアル通りこの数行です。
from cleanlab.object_detection.rank import get_label_quality_scores, issues_from_scores
scores = get_label_quality_scores(labels, predictions) # 画像ごとの総合ラベル品質スコア(低いほど怪しい)
issues = issues_from_scores(scores, threshold=0.2) # 閾値で怪しい画像を抽出
ただしこの総合スコアは、3種の誤り(見落とし / クラス誤り / 位置ズレ)を1つに束ねたものです。本記事は どの型が信頼できるか(overlooked はクリーン、badloc は交絡)を見たいので、下のコードでは型ごとの compute_*_box_scores を個別に使っています。
Code:OOF 予測を整形してサブスコアを計算(run_cleanlab.py)
import json
import numpy as np
from cleanlab.object_detection.rank import (
compute_overlooked_box_scores, compute_swap_box_scores, compute_badloc_box_scores,
pool_box_scores_per_image, issues_from_scores,
)
# train_dump_oos.json = 各画像の GT と「その画像を学習に使っていない fold モデル」の
# conf=0.001 予測(k-fold, シーケンス単位 GroupKFold)
data = json.loads(open("train_dump_oos.json").read())
class_names = data["class_names"]; K = len(class_names)
records = data["records"]
labels, predictions = [], []
for r in records:
labels.append({
"bboxes": np.array(r["gt_boxes"], dtype=np.float32).reshape(-1, 4),
"labels": np.array(r["gt_labels"], dtype=np.int64),
})
per_class = [[] for _ in range(K)]
for p in r["pred"]:
per_class[p["cls"]].append(p["bbox"] + [p["score"]])
predictions.append([
np.array(pc, dtype=np.float32).reshape(-1, 5) if pc else np.zeros((0, 5), dtype=np.float32)
for pc in per_class
])
for name, fn in [("overlooked", compute_overlooked_box_scores),
("swap", compute_swap_box_scores),
("badloc", compute_badloc_box_scores)]:
img_scores = pool_box_scores_per_image(fn(labels=labels, predictions=predictions))
n = len(issues_from_scores(img_scores, threshold=0.2))
print(f"{name}: flagged {n}/{len(img_scores)}")
参考リンク:
- CleanLab - Quickstart
- CleanLab - Finding Label Errors in Object Detection Datasets
- CleanLab - The Workflows of Data-centric AI for Classification with Noisy Labels
- CleanLab - Computing Out-of-Sample Predicted Probabilities with Cross-Validation
4.3.2.OOF の不一致は「向き」で読む——van/car の実ラベル誤り
前節の swap が指した van/car を掘り下げます。まず前提。car と van は本当に紛らわしく、コンペのディスカッションでも参加者が「人間でも van と car の区別が難しい」と質問し、主催者(3LC)は 「train のラベルは元データそのままで messy。val/test は綺麗にしてあり、そちらが正解の基準」 と回答しています。つまり train には van/car のクラス誤りが実在する——問題はどれが誤りか、です。
OOF 予測で GT とのマッチを集計すると、非対称でした(検出できた GT に限った割合)。van の 29.6% が car と予測される一方、car が van と予測されるのは 1.1% だけ。ここで効くのが、不一致の 向き を読むことです。
- van → car(多い):モデルは多数派の car(car : van ≈ 11:1)に引っ張られる。バイアス通りの不一致なので、GT が誤りとは言い切れない(多くは正しい van を car に倒しているだけ)。
-
car → van(少ないが高信頼):car 寄りのモデルが、自分のバイアスに逆らって van を conf 0.9 超で断言する。よほど van らしくないと起きない=GT=car の方が誤りの濃厚なシグナル。該当は約 50 箱(conf≥0.9)、追跡の重複を除くと約24台分で、
MVI_40903など特定シーケンスに固まっていました。自動追跡なので、1台の van の誤ラベルがトラック全体に伝播したものです。数は多くないが系統的なのがポイントです。

GT=car とラベルされているが、実際は van/MPV の車両(別シーケンスから1台ずつ抜粋、各セルの数字は OOF の van 信頼度)。1台の誤ラベルが追跡で全フレームに伝播している。主催者の言う「messy な train ラベル」の実例。
牽引車(付け忘れ=#1)に続く、2つ目の確定寄りの GT 誤り(#2 クラス誤り)です。得られた勘所は再利用できます——OOF の不一致は、モデルのバイアスと同じ向きなら根拠として弱く、逆向きこそラベル誤りの強いシグナル。
※ なおこの van/car は、今回のスコア用クリーニングでは直していません。2位チームも「van の単純オーバーサンプルは逆効果」と報告しており、曖昧クラスを触るのは避けたためです。
4.3.3.FiftyOneによるデータ品質チェック
FiftyOne は Voxel51 が開発する Data-Centric AI 向けのデータセット管理・可視化ツールです。モデル予測、GT、埋め込み特徴量、各種品質指標を横断的に探索できるため、「なぜこのデータが問題なのか」を対話的に調べられます。Cleanlab がラベル品質をスコアとして返すのに対し、FiftyOne はその結果を可視化し、データセット全体を探索する役割を担います。
まず evaluate_detections でクラス別 mAP を見ると、弱いのは truck と van でした。
| class | mAP@0.5 |
|---|---|
| car | 0.874 |
| bus | 0.856 |
| truck | 0.798 |
| van | 0.797 |
同じ「弱い」でも性質は逆です。truck は「予測すればほぼ当たるが検出漏れが多い(high precision / low recall)」希少クラスで、境界はクリーン。van は FP・FN とも多く、car との境界が曖昧です(#6 Debatable)。この違いから、後のクリーニングで「truck は増やすが van は触らない」と判断しました。
compute_mistakenness(OOF 予測で計算)で食い違いの大きい画像を並べると、Cleanlab の swap ワースト20と18枚が一致しました。Cleanlab と FiftyOne という独立な2手法が同じ画像に収束するのは良い兆候です。先ほどの van/car クラス誤りもこの上位に並び、最大の食い違いは bus を truck と誤検出したケースでした。

FiftyOne App で mistakenness の高い順に並べたビュー。

mistakennessが最大のサンプル(bus を truck と conf 0.99 で誤検出)。GT と予測の食い違いが大きい画像が上に来る。
embedding も可視化できます。compute_visualization で特徴量を UMAP に落とすと、App の Embeddings パネルで散布図として見られます。外れ値=ユニーク/希少シーン、中心の密集=冗長シーンが一目で分かります。

Embeddings パネル(UMAP)。外れ値=ユニーク/希少シーン、中心の密集=冗長シーン。
compute_uniqueness / compute_representativeness / compute_near_duplicates はモデル予測に依存しない(画像の中身だけ)ので、過学習マスクと無縁に「データセット構成の質」を測れます。2つの指標は向きが逆で、uniqueness が高い=他と似ていない外れ値、representativeness が高い=典型シーンを代表するフレームです。uniqueness は train 全体で平均 0.27 と低く、下位(=near-duplicate)は静止した定点シーン MVI_40863 に集中していました。
間引きの素直な方針は、near-duplicate(低 uniqueness)を削り、各シーンを代表するフレーム(高 representativeness)を残すことです。注意したいのは「representativeness が低い=非典型だから貴重、残すべき」とは限らない点です。低 representativeness/高 uniqueness の外れ値には、"貴重な希少シーン"と"見るからにゴミなフレーム"が混在します。ここは指標任せにできず、目視で取捨する領域でした。

FiftyOne に保存した品質スライス(mistakenness / swap / 見落とし / 冗長 / 希少 / ユニーク / 代表)。目的別にワンクリックで切り替えられます。

「most redundant(低 uniqueness)」ビュー。ほぼ同一のバスのフレームが延々と並ぶ=冗長で、間引きの第一候補。

「most representative(高 representativeness)」ビュー。多様だが典型的なシーンが並ぶ=残すべき代表。
補足として、FiftyOne のデフォルト埋め込みは画像のみでテキスト検索できませんが、CLIP(clip-vit-base32-torch)で類似インデックスを作れば「a white van」のような自然言語での画像検索もできます。

CLIP 類似インデックスで「a white van」を自然言語検索した結果。
import fiftyone.brain as fob
# CLIP でテキスト検索できる類似インデックスを張る(CLIP はテキスト塔つき)
fob.compute_similarity(dataset, model="clip-vit-base32-torch", brain_key="clip_sim")
view = dataset.sort_by_similarity("a white van", k=25, brain_key="clip_sim")
Code:OOF 予測を読み込み mistakenness を計算(run_fiftyone.py)
import json
from pathlib import Path
import fiftyone as fo
import fiftyone.brain as fob
IMG_DIR = Path("/path/to/competition_starter/data/train/images")
data = json.loads(Path("train_dump_oos.json").read_text()) # Cleanlab と同じ OOF ダンプ
class_names = data["class_names"]
cl = json.loads(Path("cleanlab_per_file.json").read_text()) # run_cleanlab.py で保存した per-file スコア {file: {overlooked, swap, badloc}}
dataset = fo.Dataset(name="ua_detrac_train_oos", overwrite=True, persistent=True)
samples = []
for r in data["records"]:
w, h = r["width"], r["height"]
s = fo.Sample(filepath=str(IMG_DIR / r["file"]))
s["ground_truth"] = fo.Detections(detections=[
fo.Detection(label=class_names[c], bounding_box=[x1/w, y1/h, (x2-x1)/w, (y2-y1)/h])
for (x1, y1, x2, y2), c in zip(r["gt_boxes"], r["gt_labels"])])
# conf=0.001 の生予測は1枚約300箱で add_samples が遅い。0.05 でフィルタ。
s["predictions"] = fo.Detections(detections=[
fo.Detection(label=class_names[p["cls"]],
bounding_box=[p["bbox"][0]/w, p["bbox"][1]/h,
(p["bbox"][2]-p["bbox"][0])/w, (p["bbox"][3]-p["bbox"][1])/h],
confidence=p["score"])
for p in r["pred"] if p["score"] >= 0.05])
# Cleanlab のスコアを sample フィールドに載せる(App でフィルタ・並べ替え・保存ビュー化できる)
sc = cl.get(r["file"], {})
s["cl_overlooked"], s["cl_swap"], s["cl_badloc"] = sc.get("overlooked"), sc.get("swap"), sc.get("badloc")
samples.append(s)
dataset.add_samples(samples)
fob.compute_mistakenness(dataset, "predictions", label_field="ground_truth")
fob.compute_uniqueness(dataset)
fob.compute_visualization(dataset, method="umap", brain_key="img_viz")
# Cleanlab スコアで保存ビューを作る(例:swap が怪しいものだけ集めて並べる)
from fiftyone import ViewField as F
dataset.save_view("cleanlab class-error", dataset.match(F("cl_swap") < 0.2).sort_by("cl_swap"))
fo.launch_app(dataset)
参考リンク:
- FiftyOne - Getting Started
- FiftyOne - FiftyOne Tutorials
- FiftyOne - FiftyOne Recipes
- FiftyOne - FiftyOne Brain
4.3.4.LightlyStudioによるデータ品質チェック
ここまでの 3LC・Cleanlab・FiftyOne は学習後のラベルや予測を分析するツールでした。これに対し LightlyStudio は、データキュレーションやデータセット管理を支援するツールです。本コンペでは embedding を利用した類似検索や diversity / deduplicate サンプリングを試しました。
dataset.yaml を渡すと自動で MobileCLIP の embedding が計算されます。MobileCLIP は CLIP 系(テキストと画像を同じ空間に埋め込む)なので、下の画面のように「a white van」で自然言語検索できるのが便利でした。diversity / deduplicate によるサンプリングも可能です(コード例参照)。

LightlyStudio の GUI。「a white van」を自然言語検索した画面。
Code:YOLO 形式を読み込んで diversity サンプリング(run_lightlystudio.py)
import lightly_studio as ls
dataset = ls.ImageDataset.load_or_create()
dataset.add_samples_from_yolo(data_yaml="dataset.yaml", input_split="train") # MobileCLIP embedding 自動計算
dataset.query().sampling().diverse(n_samples_to_select=50, sampling_result_tag_name="diverse_50")
ls.start_gui()
参考リンク:
4.4.結局、データの問題はどこにあったのか
棚卸しの結果、van/car を除く per-label の誤りは多くありませんでした。続けて、分け方・重複・分布というデータの構造を見てみました。
まず train/val リーク。動画データは、同じシーケンスのフレームが train と val に散らばると、ほぼ同じ画像で「答え合わせ」してしまいます。FiftyOne の compute_leaky_splits(train と val で酷似する画像を洗い出す機能)で見ると、UA-DETRAC は 100 シーケンス中 99 が train/val 両方に跨り、17.3% がリーク判定。ローカル val が構造的に甘く出るわけで、2位チームが突いたのもここです。次いで、動画特有の冗長性(uniqueness が低く、静止シーンが似たフレームだらけ)、そしてクラス不均衡(car 59,385 に対し truck 1,226 と約48倍差)が続きます。

Embeddings パネルを uniqueness で色分け。密集=冗長、外れ値=希少シーン。
ただし、構造を直せばスコアが伸びるとは限りません。リークが歪めるのはローカル val であり、Kaggle のテスト採点には直接関係しません。間引きとオーバーサンプルは束ねて提出したため、個別効果も分かりません。
4.5.クリーニングして再提出
収束済みの構成(100 epoch)のまま、以下のようにデータだけを変え、効果を測りました。
- 付け忘れの修正 → 確定した牽引車に GT を追加。
- 冗長の削減 → near-duplicate(uniqueness 下位)を 15% 間引き。ただし truck を含む画像と希少シーンは保護。
- 不均衡の是正 → 最弱かつ希少な truck を 3 倍にオーバーサンプル。van は曖昧なので、オーバーサンプルも取り違え修正も見送り。
学習・提出条件はベースラインと完全に同一で、変えたのはデータだけです。
| Run | Public | Private |
|---|---|---|
| 100 epoch ベースライン | 0.75568 | 0.77858 |
| 100 epoch クリーニング版 | 0.77939 | 0.78441 |
Public +0.0237 / Private +0.0058 とプラスに効きました。心配していた「牽引車ラベルを足すとテスト側で FP 減点になる」懸念は、束ね全体としては顕在化しませんでした。
3つの変更を束ねたため、個別の寄与は分かりません。ここでの truck の3倍化は、前節の難例3倍化とは別のクラス不均衡是正です。+0.024 は収束による +0.216 には遠く及びません。収束 ≫ クリーニング一式 > 疑似ラベリングという順序は、モデル固定・小規模・スクラッチという本コンペの条件に依存します。
4.6.難例強調は効果なし
OOF の再現率が低い画像を「実物体を取りこぼしやすい難例」と定義しました。クラス不一致を除き、ラベル誤りを混ぜないようにしています。該当は学習側の 1,295 枚(上位25%)で、truck の38%、bus の53%が含まれました。
そこでこの1,295枚を物理的に3倍化し、シーケンス分離した fold0 で評価しました。比較を公平にするため、素の学習データ 5,183枚は75 epoch、難例3倍化後の 7,773枚は50 epochとし、どちらも 24,300 optimizer step に揃えています。
| 条件 | mAP@0.5 | truck | car | van | bus |
|---|---|---|---|---|---|
| baseline | 0.7968 | 0.760 | 0.852 | 0.647 | 0.929 |
| 難例を3倍化 | 0.7764 | 0.742 | 0.852 | 0.620 | 0.891 |
結果は mAP@0.5 -0.0204。狙った truck / van も改善せず、bus は大きく下がりました。弱いスライスを特定できても、同じ画像を繰り返し見せれば汎化するとは限りません。難例強調一般を否定する結果ではありませんが、少なくとも今回の「低 OOF-recall な画像を一括で3倍化する」定義と実装では効果が確認できませんでした。
その他データを眺めて気づいた点
スコアには直接つなげていませんが、データを眺めて気づいた点を「観察」として残します。「アノテーションの妥当性は、採点に使う方針との整合で決まる」というこのコンペの性質を象徴する例でした。
バスの陰の車と single-shot 検出器
GT を見ていると、バスやトラックに完全に隠れた対向車まで律儀にラベルされているケースがありました。単発ではなく、train 全体で car/van の GT のうち 1,820個(2.8%)が bus/truck の箱に9割以上埋もれており、そのうち 1,201個は OOF モデルでも検出できません(ほぼ不可視)。ランダムなミスではなく、「遠方の遮蔽車も一貫してラベルする」という方針だと分かります。

バスの箱(緑)の中に、完全に隠れた car の GT が複数ある。
気になるのは、YOLO が時系列を持たず1フレームを単発で判定する点です。バスの箱に100%入った car を GT として与えると、モデルにとってその領域はバスの車体・窓のピクセルでしかありません。「奥に車がいる」文脈は知りようがないので、バスの一部を car として学習しかねません。実際、これらの画像で学習した in-sample モデルはその領域に car を conf 0.2〜0.35 で予測していました(OOF モデルは ~0)。主に暗記で汎化はしていないものの、学習信号としてノイズを注入しています。念のため「NMS が重なりを消しているだけでは?」も疑い、NMS をほぼ無効化・agnostic 化しても結果は同じで、後処理の影響ではないと確認しました。
とはいえ「明確な誤り」として消すのは早計です。実在の遮蔽車を一貫してラベルした方針なら、テストも同じはずで、消すとテストと乖離します。single-shot の検出器には筋の悪い信号だが、採点はアノテーション方針に従う——その緊張が最も露骨に出る例でした。
今後の課題
- van/car の系統ラベル誤りの修正: GT=car→OOF=van の取り違え(追跡で伝播)を、主催者が正解とする val/test の定義に合わせて直す。今回は保守的に見送ったが、伸びしろの可能性がある。
- アブレーション: クリーニング一式を束ねたので、個々の寄与は切り分けられていない。
- VLM を QA 補助に: 候補出しを強力な VLM に置き換えると賢くなりそう。ただし全データ再ラベルは疑似ラベリング=ルール違反なので、「候補を出して人間が判断」の枠内で。
- 能動学習・自己教師あり: 不確実性ベースの難例選別や SSL 埋め込みでのコアセット選択。SSL をモデル初期化に使うのは scratch 規定違反だが、データ選別の埋め込みとしてなら合法。
なお、これらの一部はコンペのルール上は制限されますが、実業務では標準的に有効な手法です。「コンペで使えない=役に立たない」ではありません。
おわりに
モデルを固定し、データだけで勝負するルールだったからこそ、Data-Centric AI を実感を伴って理解できました。教訓は3つです。
- 先にモデルを収束させる: これはデータ改善ではなく、データ上の仮説を測るための足場です。
- OOF で暗記を避ける: 未収束モデルは誤検出が多く、学習に使ったモデルは誤り候補を出しにくくなります。
- 仮説ごとに検証する: van/car の誤りは見つかった一方、もっともらしかった難例強調はこの定義では失敗しました。
Data-Centric AI は、何も考えず手を動かせば必ず上手くいく手法集・銀の弾丸ではありません。データの問題について仮説を立て、ツールで候補を出して人が確認、リークのない評価で確かめ、効かなければまた次の仮説に進む、この反復が本質と感じました。
ただし、ここでの「効いた/効かなかった」はこのコンペの制約下の話です。実業務では前提が違います。大量の未ラベルがあれば疑似ラベリング(半教師あり学習)は有効ですし、データが増え続ける環境ではクリーニングの比重はもっと大きくなります。アノテーション方針を自分で決められるなら、今回「曖昧だから触らない」とした van/car の定義を整理し直すのが効く場面もあるでしょう。コンペで NG とされた手法(疑似ラベリングなど)が、実務でも無用とは限りません。
今後は、より体系的に学ぶべく以下を見てみようと思っています。
- Introduction to Data-Centric AI (MIT)
- Data-Centric AI Workshop (NeurIPS '24)
- Data-Centric AI Summit 2022
また、各ツールの設計思想や各種メソッドについても整理したいです。




