ディープラーニング/コンピュータービジョンでよくある8つの間違いとその回避方法

ディープラーニング/コンピュータービジョンでよくある8つの間違いとその回避方法

この記事はLeiphone.comから転載したものです。転載する場合は、Leiphone.com公式サイトにアクセスして許可を申請してください。

人間は完璧ではないので、ソフトウェアを作成するときに間違いを犯すことがよくあります。場合によっては、これらのバグは簡単に見つかります。コードが機能せず、アプリがクラッシュするだけです。しかし、一部のバグは隠れていて見つけるのが難しく、さらに危険になります。

ディープラーニングの問題に取り組む場合、特定の不確実性のためにこのような間違いを犯しやすくなります。Web アプリケーションのエンドポイントがリクエストを正しくルーティングしているかどうかを確認するのは簡単ですが、勾配降下法の手順が正しいかどうかを確認するのは簡単ではありません。ただし、ディープラーニングの実践ルーチンには回避できるバグが多数存在します。

過去 2 年間のコンピューター ビジョンの仕事で私が見つけた間違いや犯した間違いに関する経験を皆さんと共有したいと思います。私は会議でこの話題について話しましたが、その後多くの人から「そうだね、私も同じバグにたくさん遭遇したよ」と言われました。この記事が、皆さんがこれらの問題のいくつかを回避するのに役立つことを願っています。

1. 画像とキーポイントを反転する

キーポイント検出問題に取り組んでいるとします。データは、画像と [(0,1),(2,2)] などのキーポイント タプルのシーケンスのペアのように見えます。各キーポイントは、x 座標と y 座標のペアです。

このデータにいくつかの基本的な機能強化を加えてみましょう。

  1. def flip_img_and_keypoints(img: np.ndarray, kpts:
  2.  
  3. シーケンス[シーケンス[ int ]]):
  4.  
  5. 画像 = np.fliplr(画像)
  6.  
  7. h、w、*_ = 画像の形状
  8.  
  9. kpts = [(y, w - x) y、xはkpts内]
  10.  
  11. img、kptsを返す

上記のコードは正しいように見えますね。次に、それを視覚化してみましょう。

  1. 画像 = np.ones(( 10 , 10 ), dtype=np.float32)
  2.  
  3. kpts = [( 0 , 1 ), ( 2 , 2 )]
  4.  
  5. image_flipped、kpts_flipped = flip_img_and_keypoints(画像、kpts)
  6.  
  7. img1 = 画像.コピー()
  8.  
  9. y、x は kpts の場合:
  10.  
  11. 画像1[y, x] = 0   
  12.  
  13. img2 = image_flipped.copy()
  14.  
  15. kpts_flipped の y、xについて:
  16.  
  17. 画像2[y, x] = 0   
  18.  
  19. _ = plt.imshow(np.hstack((img1, img2)))

このグラフは非対称で奇妙に見えます。極端な値をチェックするとどうなるでしょうか?

  1. 画像 = np.ones(( 10 , 10 ), dtype=np.float32)
  2.  
  3. kpts = [( 0 , 0 ), ( 1 , 1 )]
  4.  
  5. image_flipped、kpts_flipped = flip_img_and_keypoints(画像、kpts)
  6.  
  7. img1 = 画像.コピー()
  8.  
  9. y、x は kpts の場合:
  10.  
  11. 画像1[y, x] = 0   
  12.  
  13. img2 = image_flipped.copy()
  14.  
  15. kpts_flipped の y、xについて:
  16.  
  17. 画像2[y, x] = 0   
  18.  
  19. -------------------------------------------------------------------- -------
  20.  
  21. インデックスエラー
  22.  
  23. トレースバック(最新の呼び出しが最後)
  24.  
  25. <ipython-input-5-997162463eae>は <module> にあります
  26.  
  27. 8 img2 = image_flipped.copy()
  28.  
  29. 9   kpts_flipped の y、xについて:
  30.  
  31. ---> 10画像2[y, x] = 0   
  32.  
  33. IndexError: インデックス10 は、サイズ101範囲外です 

良くない!これは典型的な間違いです。正しいコードは次のとおりです。

  1. def flip_img_and_keypoints(img: np.ndarray, kpts: Sequence[Sequence[ int ]]):
  2.  
  3. 画像 = np.fliplr(画像)
  4.  
  5. h、w、*_ = 画像の形状
  6.  
  7. kpts = [(y, w - x - 1 ) y 、x は kpts 内]
  8.  
  9. img、kptsを返す

この問題は視覚化によって検出されましたが、x=0 ポイントを使用した単体テストも役立ちます。面白い事実: チームの 3 人 (私を含む) がそれぞれほぼ同じ間違いを犯しました。

2. 重要なポイントについて話し続ける

上記の機能が修正されたとしても、依然として危険は残ります。以下は単なるコードではなく、セマンティクスに関するものです。

画像を強調するために両手のひらを使う必要があるとします。安全そうです。左から右に回した後も手は手のままです。

でも待ってください!キーポイントのセマンティクスについては何も知りません。重要なポイントが実際には次のことを意味するとしたらどうでしょうか:

  1. kpts = [
  2.  
  3. ( 20 , 20 )、# 左小指
  4.  
  5. ( 20 , 200 ), # 右小指
  6.  
  7. ...
  8.  
  9. ]

つまり、拡張によって実際にセマンティクスが変わります。左が右になり、右が左になりますが、配列内のキーポイントのインデックスは交換されません。トレーニングに大きなノイズが発生し、メトリックが悪化します。

ここで学ぶべき教訓:

  • 拡張機能やその他の機能を適用する前に、データ構造とセマンティクスを理解して考慮します。

  • 実験を独立した状態に保ちます。小さな変更 (新しい変換など) を追加し、そのパフォーマンスを確認し、スコアが向上した場合は再度マージします。

3. カスタム損失関数

セマンティックセグメンテーションの問題に詳しい人は、IoU (intersection over union) メトリックを知っているかもしれません。残念ながら、SGD で直接最適化することはできないため、一般的な方法は微分可能な損失関数で近似することです。これをコーディングしてみましょう!

  1. iou_continuous_loss(y_pred, y_true)を定義します。
  2.  
  3. eps = 1e- 6   
  4.  
  5. _sum(x)を定義します:
  6.  
  7. x.sum(- 1 ).sum(- 1 )を返す
  8.  
  9. 分子 = (_sum(y_true * y_pred) + eps)
  10.  
  11. 分母 = (_sum(y_true ** 2 ) + _sum(y_pred ** 2 ) -
  12.  
  13. _sum(y_true * y_pred) + eps)
  14.  
  15. (分子 / 分母).mean()を返します

良さそうです。少し検査してみましょう。

  1. [ 3 ]の場合: ones = np.ones(( 1 , 3 , 10 , 10 ))
  2.  
  3. ...: x1 = iou_continuous_loss(ones * 0.01 , ones)
  4.  
  5. ...: x2 = iou_continuous_loss(ones * 0.99 , ones)
  6.  
  7. [ 4 ]において: x1, x2
  8.  
  9. アウト[ 4 ] : ( 0.010099999897990103,0.9998990001020204 )

x1 では、真実とはまったく異なる損失を計算しましたが、x2 は真実に非常に近い関数の結果です。予測があまり良くないため、x1 は大きくなり、x2 はゼロに近くなると予想されます。ここで何が起こったのですか?

上記の関数はメトリックの良い近似値です。指標は損失ではありません。高ければ高いほど良いのです。損失を最小限に抑えるために SGD を使用したいので、実際にはその逆を行う必要があります。

  1. v> iou_continuous(y_pred, y_true)を定義します。
  2.  
  3. eps = 1e- 6   
  4.  
  5. _sum(x)を定義します:
  6.  
  7. x.sum(- 1 ).sum(- 1 )を返す
  8.  
  9. 分子 = (_sum(y_true * y_pred) + eps)
  10.  
  11. 分母 = (_sum(y_true ** 2 ) + _sum(y_pred ** 2 )
  12.  
  13. - _sum(y_true * y_pred) + eps)
  14.  
  15. (分子 / 分母).mean()を返します
  16.  
  17. iou_continuous_loss(y_pred, y_true)を定義します。
  18.  
  19. 戻る  1 - iou_continuous(y_pred, y_true)

これらの問題は、次の 2 つの方法で特定できます。

  • 損失の方向を確認するための単体テストを作成します。現実に近いものほど損失が低くなるという期待を形式化します。

  • 健全性チェックとして、モデルをより小さなバッチ サイズにオーバーフィットしてみてください。

4. Pytorchの使用

事前にトレーニングされたモデルがあり、それが時系列モデルであると仮定します。私たちは、ceevee API に基づいて予測クラスを作成します。

  1. ceevee.base からAbstractPredictorをインポートします
  2.  
  3. クラスMySuperPredictor(AbstractPredictor):
  4.  
  5. def __init__(self, weights_path: str, ):
  6.  
  7. スーパー().__init__()
  8.  
  9. self.model = self._load_model(weights_path = weights_path) です。
  10.  
  11. defプロセス(self, x, *kw):
  12.  
  13. torch.no_grad() の場合:
  14.  
  15. res = 自己.モデル(x)
  16.  
  17. 戻り
  18.  
  19. @静的メソッド  
  20.  
  21. def _load_model(重みパス):
  22.  
  23. モデル = モデルクラス()
  24.  
  25. 重み = torch.load(weights_path, map_location= 'cpu' )
  26.  
  27. model.load_state_dict(重み)
  28.  
  29. リターンモデル

このパスワードは正しいですか?多分!確かに、これはいくつかのモデルに当てはまります。たとえば、torch.nn.BatchNorm2d などのモデルにノルム レイヤーがない場合、またはモデルが各画像の実際のノルム統計を使用する必要がある場合 (たとえば、多くの pix2pix ベースのアーキテクチャで必要)。

しかし、ほとんどのコンピューター ビジョン アプリケーションでは、コードに重要な点、つまり評価モードへの切り替えが欠けています。

この問題は、動的な pytorch グラフを静的な pytorch グラフに変換しようとすると簡単に識別できます。この変換には torch.jit モジュールがあります。

簡単な修正方法:

  1. [ 4 ] の場合: ​​model = nn.Sequential(
  2.  
  3. ...: nn.Linear( 10 , 10 )、
  4.  
  5. ..: nn.Dropout(. 5 )
  6.  
  7. ...: )
  8.  
  9. ...:
  10.  
  11. ...: traced_model = torch.jit.trace(model.eval(), torch.rand( 10 ))
  12.  
  13. # 警告はもうありません!

この時点で、torch.jit.trace はモデルを複数回実行し、結果を比較します。ここでは違いはないようです。

ただし、torch.jit.trace はここでは万能薬ではありません。これは知っておいて覚えておくべきニュアンスです。

5. コピー&ペーストの問題

トレーニングと検証、幅と高さ、緯度と経度など、多くのものがペアで存在します。注意深く読むと、ペアのメンバー間でコピー アンド ペーストすることによって発生したエラーを簡単に見つけることができます。

  1. v> def make_dataloaders(train_cfg、val_cfg、batch_size):
  2.  
  3. トレーニング = Dataset.from_config(train_cfg)
  4.  
  5. val = Dataset.from_config(val_cfg)
  6.  
  7. shared_pa​​rams = { 'batch_size' : batch_size, 'shuffle' : True,
  8.  
  9. 'num_workers' : cpu_count()}
  10.  
  11. トレーニング = DataLoader(トレーニング、**shared_pa​​rams)
  12.  
  13. val = DataLoader(train, **shared_pa​​rams)
  14.  
  15. 帰りの電車、val

愚かなミスを犯したのは私だけではありませんでした。一般的なライブラリでも同様のエラーが発生します。

  1. #
  2.  
  3. https://github.com/albu/albumentations/blob/0.3.0/albumentations/aug mentations/transforms.py  
  4.  
  5. def apply_to_keypoint(self, keypoint, crop_height= 0 , crop_width= 0 , h_start= 0 , w_start= 0 , rows= 0 , cols= 0 , **params):
  6.  
  7. キーポイント = F.keypoint_random_crop(キーポイント、クロップ高さ、クロップ幅、h_start、w_start、行数、列数)
  8.  
  9. scale_x = 自己幅 / クロップ高さ
  10.  
  11. scale_y = self.height / crop_height
  12.  
  13. keypoint = F.keypoint_scale(keypoint, scale_x, scale_y)キーポイントを返す

心配しないでください。このバグは修正されました。どうすれば回避できるでしょうか?コードをコピーして貼り付けるのではなく、コピーして貼り付ける必要がない方法でコーディングするようにしてください。

  1. データセット = []
  2.  
  3. data_a = get_dataset(MyDataset(config[ 'dataset_a' ]), config[ 'shared_pa​​ram' ], param_a) datasets.append(data_a)
  4.  
  5. data_b = get_dataset(MyDataset(config[ 'dataset_b' ]), config[ 'shared_pa​​ram' ], param_b) datasets.append(data_b)
  6.  
  7. データセット = []
  8.  
  9. 名前、param in zip(( 'dataset_a' , 'dataset_b' ), (param_a, param_b), ):
  10.  
  11. データセットの追加(get_dataset(MyDataset(config[name]), config[ 'shared_pa​​ram' ], param))

6. 適切なデータ型

もう 1 つ機能強化してみましょう。

  1. def add_noise(img: np.ndarray) -> np.ndarray:
  2.  
  3. マスク = np.random.rand(*img.shape) + .5   
  4.  
  5. img = img.astype( 'float32' ) * マスク
  6.  
  7. img.astype( 'uint8' )を返す

画像が変わりました。これは私たちが予想していたことでしょうか?まあ、あまりにも多くのことが変わってしまったのかもしれません。

ここでは、float32 を uint8 に変換するという危険な操作が行われます。これによりオーバーフローが発生する可能性があります:

  1. def add_noise(img: np.ndarray) -> np.ndarray:
  2.  
  3. マスク = np.random.rand(*img.shape) + .5   
  4.  
  5. img = img.astype( 'float32' ) * マスク
  6.  
  7. np.clip(img, 0 , 255 ).astype( 'uint8' )を返します
  8.  
  9. img = add_noise(cv2.imread( 'two_hands.jpg' )[:, :, ::- 1 ]) _ = plt.imshow(img)

見た目がずっと良くなりましたね?

ちなみに、この問題を回避する別の方法があります。車輪の再発明をするのではなく、前任者の作業に基づいてコードを修正することです。たとえば、 albumentations.augmentations.transforms.GaussNoise です。

再び同じバグが発生しました。

ここで何が間違っていたのでしょうか?まず、キュービック補間を使用してマスクのサイズを変更するのは良い考えではありません。 float32 を uint8 に変換する場合も同じ問題が発生します。3 次補間により入力よりも大きな値が出力され、オーバーフローが発生する可能性があります。

問題を見つけました。ループ内にアサーションを入れるのも良い考えです。

7. 入力ミス

完全な畳み込みネットワーク (セマンティックセグメンテーション問題など) と巨大な画像を処理する必要があるとします。画像が大きすぎて GPU に収まらない場合 (たとえば、医療画像や衛星画像など)。

この場合、画像をグリッドに分割し、各パッチに対して個別に推論を実行し、最後にそれらを結合することができます。さらに、いくつかの予測交差を使用して、境界付近のアーティファクトを滑らかにすることができます。

コーディングしましょう!

  1. tqdm からtqdm をインポート
  2.  
  3. クラスGridPredictor:
  4.  
  5. "" " このクラスは、GPU メモリ制限がある場合に大きな画像のセグメンテーション マスクを予測するために使用できます。" ""   
  6.  
  7. def __init__(self, predictor: AbstractPredictor, size: int , stride: Optional[ int ] = None): self.predictor = predictor
  8.  
  9. 自己.size = サイズ
  10.  
  11. self.stride = stride stride が None でない場合はstride 、そうでない場合はsize // 2  
  12.  
  13. def __call__(self, x: np.ndarray):
  14.  
  15. h, w, _ = x.shape
  16.  
  17. マスク = np.zeros((h, w, 1 ), dtype= 'float32' )
  18.  
  19. 重み = mask.copy()
  20.  
  21. i が tqdm(range( 0 , h - 1 , self.stride) の場合):
  22.  
  23. j が範囲( 0 、 w - 1 、 self.stride )内である場合:
  24.  
  25. a、b、c、d = i、min(h、i + 自己サイズ)、j、min(w、j + 自己サイズ)
  26.  
  27. パッチ = x[a:b, c:d, :]
  28.  
  29. マスク[a:b, c:d, :] += np.expand_dims(self.predictor(patch), - 1 ) 重み[a:b, c:d, :] = 1   
  30.  
  31. マスク/重みを返す

シンボルのタイプミスがありますが、コード スニペットは十分に大きいため、簡単に見つけることができます。コードを見るだけですぐに識別できると思います。コードが正しいかどうかは簡単に確認できます。

  1. クラスModel(nn.Module):
  2.  
  3. def forward(self, x):
  4.  
  5. x.mean(axis=- 1 )を返す
  6.  
  7. モデル = モデル()
  8.  
  9. grid_predictor = GridPredictor(モデル、サイズ= 128 、ストライド= 64 )
  10.  
  11. simple_pred = np.expand_dims(モデル(画像), - 1 )
  12.  
  13. グリッド予測 = グリッド予測子(画像)
  14.  
  15. np.testing.assert_allclose(シンプル_pred、グリッド_pred、atol = .001 )

call メソッドの正しいバージョンは次のとおりです。

  1. def __call__(self, x: np.ndarray):
  2.  
  3. h, w, _ = x.shape
  4.  
  5. マスク = np.zeros((h, w, 1 ), dtype= 'float32' )
  6.  
  7. 重み = mask.copy()
  8.  
  9. i が tqdm(range( 0 , h - 1 , self.stride) の場合):
  10.  
  11. j が範囲 ( 0 、 w - 1 、 self.stride )内にある場合: a、 b、 c、 d = i、 min(h、 i + self.size)、 j、 min(w、 j + self.size)
  12.  
  13. パッチ = x[a:b, c:d, :]
  14.  
  15. マスク[a:b, c:d, :] += np.expand_dims(self.predictor(patch), - 1 )
  16.  
  17. 重み[a:b, c:d, :] += 1   
  18.  
  19. マスク/重みを返す

それでも問題が解決しない場合は、linewidth[a:b,c:d,:]+=1 であることに注意してください。

8. ImageNet正規化

転移学習を行う必要がある場合は、通常、ImageNet をトレーニングするときに行ったように画像を正規化するのが最適です。

すでに使い慣れている albumentations ライブラリを使用しましょう。

  1. アルバムからのインポート正規化
  2.  
  3. ノルム = 正規化()
  4.  
  5. 画像 = cv2.imread( 'img_small.jpg' )
  6.  
  7. マスク = cv2.imread( 'mask_small.png' , cv2.IMREAD_GRAYSCALE)
  8.  
  9. mask = np.expand_dims(mask, - 1 ) # shape ( 64 , 64 ) -> shape ( 64 , 64 , 1 )
  10.  
  11. normed = norm(画像=img、マスク=mask)
  12.  
  13. img, mask = [normed[x] for x in [ 'image' , 'mask' ]]
  14.  
  15. img_to_batch(x)を定義します。
  16.  
  17. x = np.transpose(x, ( 2 , 0 , 1 )).astype( 'float32' )
  18.  
  19. torch.from_numpy(np.expand_dims(x, 0 ))を返す
  20.  
  21. img、マスク = map(img_to_batch、(img、マスク))
  22.  
  23. 基準 = F.binary_cross_entropy

ここで、ネットワークをトレーニングし、単一の画像にオーバーフィットさせます。前述したように、これは優れたデバッグ手法です。

  1. モデルa = UNet ( 3,1 )
  2.  
  3. オプティマイザー = torch.optim.Adam(model_a.parameters(), lr=1e- 3 )
  4.  
  5. 損失 = []
  6.  
  7. tqdm(range( 20 ))内のtの場合:
  8.  
  9. 損失 = 基準(model_a(img), マスク)
  10.  
  11. 損失.append(損失.item())
  12.  
  13. オプティマイザ.zero_grad()
  14.  
  15. 損失.後方()
  16.  
  17. オプティマイザ.ステップ()
  18.  
  19. _ = plt.plot(損失)

曲率は問題ないように見えますが、クロスエントロピー損失値が -300 になることは予想されません。どうしたの?

画像の正規化は適切に機能し、手動で [0, 1] にスケーリングする必要があります。

  1. モデルb = UNet ( 3,1 )
  2.  
  3. オプティマイザー = torch.optim.Adam(model_b.parameters(), lr=1e- 3 )
  4.  
  5. 損失 = []
  6.  
  7. tqdm(range( 20 ))内のtの場合:
  8.  
  9. 損失 = 基準(model_b(img), マスク / 255 .)
  10.  
  11. 損失.append(損失.item())
  12.  
  13. オプティマイザ.zero_grad()
  14.  
  15. 損失.後方()
  16.  
  17. オプティマイザ.ステップ()
  18.  
  19. _ = plt.plot(損失)

トレーニング ループ内の単純なアサーション (例: assert mask.max() <= 1) により、問題はすぐに検出されます。同様に、ユニットテストでは問題を検出できます。

全体として:

  • テストは重要です。

  • 実行中のアサーションはパイプラインのトレーニングに使用できます。

  • 視覚化は優れたツールです。

  • 盗作は呪いである。

  • 特効薬は存在せず、機械学習エンジニアは常に注意しなければなりません。

<<:  商品受け取り時の顔認証システムを小学生が“クラック”!鳳超が緊急オフラインに

>>:  AIアートがブームになっていますが、今後はどうなるのでしょうか?

ブログ    
ブログ    

推薦する

2021 年に AIOps は企業にどのような新たな変化をもたらすでしょうか?

AIOps は人工知能と IT 管理を組み合わせた技術として、近年大企業から大きな注目を集めていま...

...

すべてがAI+になる新しい形の人工知能があなたを待っています

人工知能技術は急速に発展し、人々の生活に微妙な影響を与えています。掃除ロボット、調理ロボット、配達ロ...

人工知能が VPS と共有ホスティング オプションの議論を再構築

人工知能は数え切れないほど多くの業界を前例のない形で変えています。ウェブホスティングは人工知能が関与...

AIはあなたより年上かもしれない

[[349378]]現在、ほとんどの調査会社は、人工知能が近い将来ますます重要な役割を果たすと予測し...

機械学習情報工場になるためには、企業はリーン製造からこれらの6つの基本を学ぶ必要がある

【51CTO.com クイック翻訳】調査機関Forrester Researchが最近発表した調査レ...

専門家レベルの機械学習ツールの推奨事項。専門家と同じものを入手しましょう。

この記事は公開アカウント「Reading Core Technique」(ID: AI_Discov...

1990年代生まれの中国人教授が、1年間でネイチャー誌に3本の論文を発表した。最初の量子ニューラルネットワークQuantumFlowはオープンソースです

[[432543]]ニューラル ネットワークは、現在のコンピューティング アプリケーションで最も急速...

CIO が AI を活用して地位を向上させる 3 つの方法

組織内の利害関係者の視点から IT の役割を理解することは、IT がどのように変革する必要があるかを...

...

Dropbox のエンジニアがロスレス圧縮アルゴリズム「Pied Piper」を開発

Dropbox のエンジニアたちは世界をより良い場所にするために取り組んでおり、HBO のコメディー...

...

...

顔認識だけでなく、「心を読む」こともできます。このような AI は好きですか?

世界はとてもカラフルです。この美しい地球に住むすべての人は、理解されることを望み、注目されることを待...