自分のコードを書き換えて直る画像認識エンジンをC++で作った
はじめに
認識精度が落ちたら、原因を自分で調べ、自分のソースコードを書き換え、g++ で自分自身を再ビルドして新しい世代に入れ替わる画像認識エンジンを作りました。外部ライブラリは使わず、C++20 の標準ライブラリだけで動きます。
リポジトリ: https://github.com/purplecatman2005-dev/self-healing-vision
この記事について
コードと記事は、AI(Claude)と対話しながら作りました。数値はすべて手元で実行した結果です。
何が起きるのか
600 フレームの映像(合成画像)を流し、途中で「外乱」を混ぜます。
| フレーム | 入力 |
|---|---|
| 0〜149 | きれいな図形(○□△) |
| 150〜299 | 鏡面反射(グレア: 画像の一部が真っ白に飛ぶ) |
| 300〜449 | ごま塩ノイズ(白黒の点が散らばる) |
| 450〜599 | 全部まぜ |
最初のエンジン(v1)は外乱への対策を何も持っていません。外乱が来ると精度が落ち、エンジンは自分で原因を突き止めて対策を自分のコードに書き足します。
グラフの縦線が「自己修復」のタイミングです。
- f167: グレアを検知 → グレア除去の処理を追加した v2 に入れ替わり
- f318: ごま塩ノイズを検知 → メディアンフィルタを追加した v3 に入れ替わり
最終的な精度は次のとおりです。
| 区間 | 精度 |
|---|---|
| クリーン | 150/150 (100.0%) |
| グレア混入 | 128/150 (85.3%) |
| ノイズ混入 | 140/150 (93.3%) |
| 全外乱ミックス | 142/150 (94.7%) |
自己修復の4ステップ
認識ループ ─→ ウォッチドッグ(直近25フレームでエラー6件以上で発火)
▼
① 数理診断 : 失敗したフレームの統計から原因を判定
▼
② 修正の最適化 : 原因に合った処理とパラメータを探す
▼
③ 自己書き換え : 設定ヘッダを書き出し、g++ で自分を再ビルド
▼
④ 回帰ゲート : 新しい exe をテストし、合格したときだけ交代
① 数理診断 — 「なぜ間違えたか」を統計で当てる
失敗したフレームを集めて、次の統計量を計算します。
| 統計量 | 意味 |
|---|---|
| 飽和画素率 | 真っ白(輝度 0.95 以上)の画素の割合 |
| 飽和画素の凝集度 | 真っ白な画素が「1つの塊」か「散らばっている」か |
| 孤立極値率 | 周囲 3×3 の中央値から大きく外れた点の割合 |
| KL ダイバージェンス | 輝度の分布が、起動時に測った正常時の分布からどれだけずれたか |
グレアは「真っ白な塊」、ごま塩は「白黒の点がバラバラ」なので、凝集度を見れば区別できます。判定部分はこれだけです。
// 飽和が校正時より有意に増え、かつ空間的に凝集 → 鏡面反射
if (ev.satRatio > calib.satMean * 5.0 + 0.01 && ev.satCluster > 0.5)
return Cause::Glare;
// 極値が孤立して散在 → インパルスノイズ
if (ev.impulseFrac > 0.02 && ev.satCluster < 0.5)
return Cause::Impulse;
実際のログでは、1 回目は凝集度 1.00(完全な塊)でグレア、2 回目は凝集度 0.05(散在)でごま塩と判定しています。
[v1] │ 数理診断: 飽和率=0.118(校正時0.0000) 飽和凝集度=1.00 孤立極値率=0.001 KL=3.74
[v1] │ → 根本原因の判定: 鏡面反射(飽和ブロブ)
...
[v2] │ 数理診断: 飽和率=0.051(校正時0.0000) 飽和凝集度=0.05 孤立極値率=0.049 KL=0.89
[v2] │ → 根本原因の判定: インパルス性ノイズ(ごま塩)
② 修正の最適化 — 勾配法ではなくグリッド探索
グレア対策では「どの明るさ以上を白飛びとみなして補修するか」という閾値を決める必要があります。
ここで工夫したのは、勾配法や黄金分割探索を使わないことです。認識精度は「正解した枚数 ÷ 全体」なので、閾値を少し動かしても精度は変わらず、ある点で突然変わる階段状になります。階段には傾きがないので、勾配法は使えません。
そこで閾値を 0.50〜0.99 の範囲で細かく全部試し、最高精度が続く区間(プラトー)の真ん中を採用しました。端ではなく真ん中を選ぶことで、少し条件が変わっても精度が落ちにくくなります。ログでは「プラトー [0.92, 0.99] の中点 0.96」が選ばれています。
ごま塩ノイズ対策は数値の調整ではなく、メディアンフィルタという処理段そのものを計算グラフに追加するかどうかを事前に検証します。
③ 自己書き換え — 自分のソースを書いて自分をビルド
修正内容は generated_params.hpp というヘッダファイルとして書き出します。エンジンはこのファイルを #include しているので、書き換えて再ビルドすると処理の構成そのものが変わります。
// 新しい計算グラフ定義ヘッダを書き出す(自己書き換えの本体)
out << "inline constexpr bool STAGE_GLARE_CLIP = " << (gp.glareClip ? "true" : "false") << ";\n";
out << "inline constexpr float GLARE_CLIP_THRESH = " << th << ";\n";
out << "inline constexpr bool STAGE_MEDIAN_FILTER = " << (gp.medianFilter ? "true" : "false") << ";\n";
// 自分自身のソースを再コンパイル
std::string cmd = "g++ -O2 -std=c++20 -static engine.cpp -o " + exeName(newVer) + ...;
v3 になったあとのヘッダには、進化の履歴がコメントとして残ります。
// version: 3
// history:
// v1: baseline (defensive preprocessing stages: none)
// v2: glare-clip段を有効化 thresh=0.96 (グリッド探索: 最大精度95.5%のプラトー[0.92,0.99]の中点)
// v3: median3x3段をグラフに追加 (失敗フレーム+回帰セットで精度100.0%を確認)
Windows では実行中の exe を上書きできないため、engine_v1.exe が engine_v2.exe を作って起動し、自分は終了する、という世代交代の形にしました。組み込み機器のファームウェア更新で使われる A/B パーティション方式と同じ考え方です。映像はフレーム番号から決定的に生成しているので、新しい世代は中断したフレームの続きから処理を再開します。
④ 回帰ゲート — 直したつもりで壊すのを防ぐ
自分を書き換えるプログラムで一番怖いのは、ある問題を直したつもりで、正常に動いていた部分を壊すことです。
そこで新しい exe には、交代の前に試験を課します。
[v1] │ 回帰ゲート実行中 (engine_v2.exe):
[gate] selftest clean : acc=100.0% (下限95%) → PASS
[gate] selftest glare : acc= 81.0% (下限75%) → PASS
[v1] └─ ゲート合格。状態をf167で保存し engine_v2.exe へ制御を移譲 ── 再起動
- clean: きれいな画像で 95% 以上(正常系を壊していないか)
- 修正対象: 直したかった外乱で基準以上か
不合格なら設定ファイルを元に戻し、作った exe を捨てます。
さらに、修正候補を採用する条件として「絶対精度 90% 以上かつ現在のエラー率を半分以下にする」を課しています。実際に v2 は、2 回目のグレア修正候補(閾値 0.71)を自分で棄却しました。
[v2] │ 修正候補: glare-clip段を有効化 thresh=0.71 (グリッド探索: 最大精度93.9%のプラトー[0.69,0.72]の中点)
[v2] └─ 棄却: 候補エラー率6.1%が基準(絶対精度90%以上かつ現行9.1%の半減)を満たさない → 縮退運転を継続
「直せるなら何でも直す」ではなく、効果が小さい変更は入れないことで、書き換えが積み重なって不安定になるのを防いでいます。
動かし方
MinGW-w64 の g++(13 以上)が必要です。実行中に自分で g++ を呼ぶので、g++ に PATH が通っている必要があります。
git clone https://github.com/purplecatman2005-dev/self-healing-vision
cd self-healing-vision
build.bat
engine_v1.exe
600 フレームのシナリオが自動で流れ、engine_v2.exe、engine_v3.exe が順に生まれます。build.bat を実行し直すと v1 の状態に戻ります。
限界
デモとして単純化している部分があります。
- 入力は合成画像で、実際のカメラではありません
- 認識器は 2 つの特徴量(充填率・コンパクト度)による最近傍分類で、CNN ではありません。ただ「計算グラフの処理段を差し替える」という仕組み自体は CNN でも同じ形で使えます
- 正解ラベルはシミュレータから得ています。実機では、別のセンサや後から確定する情報で代用する必要があります
- 複数のプロセスが同時に書き換える状況には対応していません
おわりに
「エラーを検知 → 原因を特定 → 直す → 直したものを検証 → 入れ替える」という流れを、プログラムが自分自身に対して行う仕組みを、小さく一通り作ってみました。
感想や改善案があれば、コメントや GitHub の Issue で教えてください。
リポジトリ: https://github.com/purplecatman2005-dev/self-healing-vision
