外観検査AIの内製開発
精度より先に、評価の設計をやり直した
冷間鍛造部品の外観検査を自動化するプロジェクト。2年間実用水準に届かなかった状態から、評価指標の作り直しと教師なし異常検知への転換で、見逃しゼロを保ったまま過検出を44.2%から5.8%まで下げました。同時に「実験室では勝てるが現場では負ける」ドメインシフトの壁にも突き当たっています。
44.2%→5.8%
過検出率(見逃しゼロを維持)
Technologies
プロジェクト概要
冷間鍛造でつくられる金属部品の外観検査を、AIで自動化するプロジェクトです。人の目で行っている検査を機械に置き換えることを目指しています。
このプロジェクトには、私が参加する前に約2年の経緯がありました。社外から提供された市販のAI検査プラットフォームを使い、教師あり分類でおよそ80バージョンの試行が重ねられていましたが、実用水準には届いていませんでした。私はそこから、内製でのモデル開発を担当しています。
要求は明確でした。不良品の見逃しはゼロ。これは必達です。その上で、良品を誤って不良と判定してしまう過検出を20%以下に抑えることが努力目標でした。
引き継いだときの状況
なぜ2年かかっても届かなかったのか
まず、それまでの記録を全部読むところから始めました。約80バージョンの試行のうち、最良は検出率97.6%。しかしその後の20バージョンは、一度もその値を超えていません。さらに、撮像するアングルを変えて検証したときには、検出率が0%になっていました。 学習した条件から少しでも外れると、まったく機能しなくなる状態です。
記録を追っていて、技術以前の問題に気づきました。
評価指標が「不良品の検出率」だけだったのです。この指標には決定的な欠陥があります。すべての製品を不良と判定すれば、検出率100%・見逃しゼロが達成できてしまう。つまり、過検出を無視すれば、いくらでも良い数字がつくれる指標でした。
混同行列の4象限もROC曲線もF1スコアも使われていなかったため、80バージョンの比較が公正に行われていたかどうか、事後に検証することすらできませんでした。加えて、「これ以上続けても改善しないから手法を変える」という撤退の判断基準もありませんでした。最良バージョンの後に続いた20バージョン、3〜4ヶ月分の試行は、そのまま損失になっています。
技術的な問題より先に、AI検査プロジェクトの進め方そのものを誰も知らなかったことが最大の原因だと結論づけました。
最初にやったこと
モデルではなく評価をつくる
そこで、モデルに手を付ける前に評価フレームワークを自作しました。
やったことはシンプルです。混同行列、ROC曲線、F1スコアを出力する。そして見逃しゼロという制約のもとで閾値を自動決定する。さらに、誤判定した画像のファイル名を全部リストで吐き出す。
最後の点が地味に効きました。「どの画像で間違えたか」が毎回わかるので、失敗の原因を画像そのものに戻って確認できます。これがないと、数字が動いた理由がいつまでも推測のままになります。
データの棚卸しも同時に行いました。手元にあった756枚の画像について、どれが良品でどれが不良品なのか、ファイル名のパターン、旧データセットとの照合、番号の重複の有無など複数の根拠を突き合わせて確定させました。ここを曖昧にしたまま学習を始めると、後の数字が全部信用できなくなります。
教師ありから、教師なしへ
手法は教師あり分類から、教師なしの異常検知(PatchCore)に切り替えました。
理由は、不良品のデータが圧倒的に足りないからです。手元の不良サンプルは、多い種類で158枚、少ない種類だと3枚しかありませんでした。この偏りで教師あり分類を学習させても、少数の欠陥種は学習されません。
一方の異常検知は、良品だけを学習して、そこから外れたものを異常とみなすアプローチです。良品なら現場にいくらでもあります。不良品の種類を事前に列挙しなくていいという点も、実際の不良が何種類あるか誰も断言できない状況に合っていました。
そもそも、この検査対象は目視でも判別が難しいものでした。良品と不良品の画像を並べても、人の目で違いを指摘するのが困難です。数値でも確認しました。良品同士の平均ピクセル差が8.7に対し、良品と不良品の差は13.2〜14.9。差の大半は欠陥ではなく、位置ずれによるものです。単純な画素比較では原理的に届きません。
失敗が教えてくれたこと
同じ処理でも、かける場所で逆になる
最初のベースラインは、過検出率44.2%でした。目標の20%には遠い。
異常マップ(モデルがどこを異常と見ているかのヒートマップ)を確認したところ、原因はすぐわかりました。画像の四隅、つまり背景を異常として検出していたのです。ワークが写っていない領域に反応していました。
そこで検査対象の領域だけを残すマスク処理を入れることにしました。ここで最初に試したのが、入力画像にマスクをかける方法です。結果は72.4%への悪化。目標から遠ざかりました。
悪化の理由は、記録を見返してはっきりしました。外周を塗りつぶすマスクが、端のほうにあった欠陥まで一緒に消していたのです。欠陥が消えれば、その不良品の異常スコアが下がる。見逃しゼロを守るには、閾値をその最低スコアまで下げるしかない。閾値が下がれば、それまで良品と判定できていた製品まで不良側に落ちる。背景を消すつもりの処理が、まわりまわって過検出を倍近くに増やしていました。
そこで発想を変えて、入力画像ではなく、推論後の異常マップにマスクをかける方式に切り替えました。モデルには元の画像をそのまま見せて、出てきた結果から背景の反応だけを取り除く。これで20.8%まで改善しました。
同じマスク処理でも、パイプラインのどこに置くかで結果が正反対になる。この経験は、以降の実験の組み立て方を変えました。
なおマスクの生成は、ぼかし → 二値化 → モルフォロジー処理 → 最大輪郭の抽出 → 凸包、という古典的な画像処理の組み合わせです。461枚すべてで生成に成功し、フォールバックは一度も発生しませんでした。ディープラーニングの手前で、こうした基礎的な処理が効く場面はまだ多いと感じています。
数字の推移
そこから、特徴抽出のバックボーン変更と学習データの拡充を重ねました。
| 段階 | 打ち手 | 過検出率 | 誤検出枚数 |
|---|---|---|---|
| ベースライン | ― | 44.2% | 125枚 |
| 出力側マスク | 異常マップにマスク適用 | 20.8% | 59枚 |
| 特徴抽出の変更 | バックボーンを大型化 | 17.0% | 48枚 |
| 学習データ拡充 | 良品を追加 | 7.9% | 20枚 |
| 最終 | 追加学習+パラメータ調整 | 5.8% | 14枚 |
全段階を通じて、見逃しはゼロを維持しています。最終的にAUCは0.9988、F1スコアは0.962。過検出は当初から87%削減できました。
ただし、この過程が一直線だったわけではありません。学習データを増やした段階では、ある区分の誤検出が減る一方で別の区分の誤検出が増える、いわゆるモグラ叩きが起きています。追加した12枚で逆に悪化した試行もありました。誤検出画像の番号帯を分析して、どの区分のデータが不足しているのかを特定してから追加する、という手順に変えてようやく抜けられました。
一番大きな壁
実験室では勝てるが、現場では負ける
ここまでは順調でした。問題はその次です。
別の日に撮影した無選別の良品100枚を、学習済みモデルに通したら、100枚すべてが誤検出になりました。
異常スコアの最小値が0.5146。テストで使っていた良品の最大値が0.3636ですから、完全に上回っています。閾値をどう動かしても分離できません。
当時のスコアを1枚ずつ集計し直すと、事態はもう少し悪いことが分かります。**同じ日に撮った不良品178枚のうち107枚は、別の日に撮った良品の最小値0.5146より低いスコアでした。**良品のほうが、6割の不良品より「不良らしく」出ている。撮影した日が違うというだけで、良品と不良品の並び順そのものが壊れていました。
平均輝度を確認すると、学習データが140に対して新しいデータは167でした。ならばと輝度を正規化して再学習しましたが、それでも100枚中100枚が誤検出のまま。ヒートマップを見ると、特定の箇所ではなく画面全体にぼんやりと反応していました。
これがドメインシフトです。照明、カメラ、治具の状態がわずかに変われば、モデルにとっては別の世界になる。実験室のデータセットで0.999のAUCを出しても、それは「同じ日に撮った画像の中では区別できる」という意味でしかありませんでした。
対策として、無選別の良品を学習データに追加する実験を3通り行いました。誤検出率は100%→62%→52%まで下がりましたが、完全には吸収できません。しかも同一セッションでの過検出は11.6%から17.4%へ悪化する、というトレードオフも出ました。
ここで方針を切り替えました。データ側でドメインシフトを吸収しようとするのをやめて、本番の検査機の条件で良品を撮り直して学習し、撮像条件そのものを固定する。そして運用中は異常スコアを常時監視して、分布がずれたら検知する。条件を固定できないなら、固定する仕組みごと設計する、という方向です。
環境が壊す精度
技術的に厄介だった問題を2つ記録しておきます。
GPUで学習したモデルをCPUで推論したら、AUCが0.999から0.674に崩壊しました。 最初はONNXへの変換で解決しようとしましたが、変換自体は正確(誤差0.01未満)なのに精度は戻りません。原因は、この手法が持つメモリバンク(8,627個の特徴ベクトル)がGPU上の特徴量で構築されていて、CPUで計算した特徴量と噛み合っていないことでした。結論は単純で、学習と推論は同じデバイス種別で行う。CPUでメモリバンクを再構築したところ、AUC 0.9984で実用域に戻り、学習12分・推論0.26秒/枚という速度も得られました。
もう一つは、依存ライブラリの自動更新です。データ処理ライブラリがメジャーバージョンアップした結果、数ヶ月前まで動いていた実験スクリプトが全滅しました。以降はバージョンを固定したロックファイルを整備し、本番運用する環境では安易な一括更新を禁止、再学習の前には差分を確認する運用にしています。研究用のコードと運用するコードでは、必要な作法が違うということを実感しました。
現在地と、次にやること
推論を回すループの骨格は実装済みで、トリガー・カメラ・モデル・信号出力の4つを分離した構成になっています。品質テスト83項目は全て通過しました。設定ファイルの切り替えだけで、モックから実機に移行できる状態です。
次のステップは、本番の検査機で撮像条件を確定させ、その条件で良品を撮り直して再学習することです。あわせて、再学習をワンコマンドで回せるようにすること、閾値を決めるデータと検証するデータを分離すること、異常スコアの監視でドリフトを検知する仕組みを入れることを進めています。
導入にあたっては、既存のシステムを置き換えるのではなく、しばらく併走させる想定です。いきなり切り替えるにはリスクが大きすぎますし、併走期間のデータそのものが次の改善材料になります。
このプロジェクトで得たもの
一番大きかったのは、モデルの精度を上げる前にやるべきことがあると身をもって理解したことです。
評価が公正でなければ、どれだけ実験を重ねても正しい方向に進んでいるかわかりません。実際、2年分の試行が「比較できていたかどうかも検証できない」状態になっていました。
そして、実験室で出た数字は現場の数字ではない。良品100枚が全滅した瞬間は正直こたえましたが、あれを運用開始後に発見していたらもっと悲惨でした。負ける条件を早めに見つけるのも成果のうちだと考えるようにしています。
Outcomes & Results
- ✓見逃しゼロを保ったまま、過検出率を44.2%から5.8%へ(87%削減)
- ✓誤検出枚数を125枚から14枚へ削減
- ✓評価フレームワークを自作し、混同行列・ROC曲線・F1で公正に比較できる状態を整備
- ✓別ポジションの検査でも同じ手法が成立することを確認(見逃しゼロ、過検出10.6%)
- ✓CPU単体でも学習12分・推論0.26秒/枚で実用域に到達
Challenges Overcome
- 評価指標が検出率のみで、「全品を不良と判定すれば100%」になる設計になっていた
- 位置ずれのノイズが欠陥の信号より大きく、画素の単純比較では判別できない
- 撮像セッションが変わると良品100枚すべてを誤検出する、ドメインシフトの壁
- 学習時と推論時でデバイスが違うと精度が崩壊する(AUC 0.999→0.674)
- 依存ライブラリの自動更新で、動いていた実験スクリプトが一斉に起動不能になった
Key Learnings
- モデルを触る前に、評価が公正かどうかを疑う。指標の設計ミスは全ての比較を無意味にする
- 前処理は「入力に効かせる」のが常に正しいとは限らない。同じ処理でも適用する場所で結果が逆転する
- 実験室の精度は現場の精度ではない。条件を固定できないなら、固定する仕組みごと設計する
- 「やめる判断基準」を先に決めておかないと、改善しない試行を延々と続けてしまう