この記事はLeiphone.comから転載したものです。転載する場合は、Leiphone.com公式サイトにアクセスして許可を申請してください。 人間は完璧ではないので、ソフトウェアを作成するときに間違いを犯すことがよくあります。場合によっては、これらのバグは簡単に見つかります。コードが機能せず、アプリがクラッシュするだけです。しかし、一部のバグは隠れていて見つけるのが難しく、さらに危険になります。 ディープラーニングの問題に取り組む場合、特定の不確実性のためにこのような間違いを犯しやすくなります。Web アプリケーションのエンドポイントがリクエストを正しくルーティングしているかどうかを確認するのは簡単ですが、勾配降下法の手順が正しいかどうかを確認するのは簡単ではありません。ただし、ディープラーニングの実践ルーチンには回避できるバグが多数存在します。 過去 2 年間のコンピューター ビジョンの仕事で私が見つけた間違いや犯した間違いに関する経験を皆さんと共有したいと思います。私は会議でこの話題について話しましたが、その後多くの人から「そうだね、私も同じバグにたくさん遭遇したよ」と言われました。この記事が、皆さんがこれらの問題のいくつかを回避するのに役立つことを願っています。 1. 画像とキーポイントを反転する キーポイント検出問題に取り組んでいるとします。データは、画像と [(0,1),(2,2)] などのキーポイント タプルのシーケンスのペアのように見えます。各キーポイントは、x 座標と y 座標のペアです。 このデータにいくつかの基本的な機能強化を加えてみましょう。 - def flip_img_and_keypoints(img: np.ndarray, kpts:
-
- シーケンス[シーケンス[ int ]]):
-
- 画像 = np.fliplr(画像)
-
- h、w、*_ = 画像の形状
-
- kpts = [(y, w - x) 、 y、xはkpts内]
-
- img、kptsを返す
上記のコードは正しいように見えますね。次に、それを視覚化してみましょう。 - 画像 = np.ones(( 10 , 10 ), dtype=np.float32)
-
- kpts = [( 0 , 1 ), ( 2 , 2 )]
-
- image_flipped、kpts_flipped = flip_img_and_keypoints(画像、kpts)
-
- img1 = 画像.コピー()
-
- y、x は kpts の場合:
-
- 画像1[y, x] = 0
-
- img2 = image_flipped.copy()
-
- kpts_flipped の y、xについて:
-
- 画像2[y, x] = 0
-
- _ = plt.imshow(np.hstack((img1, img2)))
このグラフは非対称で奇妙に見えます。極端な値をチェックするとどうなるでしょうか? - 画像 = np.ones(( 10 , 10 ), dtype=np.float32)
-
- kpts = [( 0 , 0 ), ( 1 , 1 )]
-
- image_flipped、kpts_flipped = flip_img_and_keypoints(画像、kpts)
-
- img1 = 画像.コピー()
-
- y、x は kpts の場合:
-
- 画像1[y, x] = 0
-
- img2 = image_flipped.copy()
-
- kpts_flipped の y、xについて:
-
- 画像2[y, x] = 0
-
- -------------------------------------------------------------------- -------
-
- インデックスエラー
-
- トレースバック(最新の呼び出しが最後)
-
- <ipython-input-5-997162463eae>は <module> にあります
-
- 8 img2 = image_flipped.copy()
-
- 9 kpts_flipped の y、xについて:
-
- ---> 10画像2[y, x] = 0
-
- IndexError: インデックス10 は、サイズ10の軸1の範囲外です
良くない!これは典型的な間違いです。正しいコードは次のとおりです。 - def flip_img_and_keypoints(img: np.ndarray, kpts: Sequence[Sequence[ int ]]):
-
- 画像 = np.fliplr(画像)
-
- h、w、*_ = 画像の形状
-
- kpts = [(y, w - x - 1 ) y 、x は kpts 内]
-
- img、kptsを返す
この問題は視覚化によって検出されましたが、x=0 ポイントを使用した単体テストも役立ちます。面白い事実: チームの 3 人 (私を含む) がそれぞれほぼ同じ間違いを犯しました。 2. 重要なポイントについて話し続ける 上記の機能が修正されたとしても、依然として危険は残ります。以下は単なるコードではなく、セマンティクスに関するものです。 画像を強調するために両手のひらを使う必要があるとします。安全そうです。左から右に回した後も手は手のままです。 でも待ってください!キーポイントのセマンティクスについては何も知りません。重要なポイントが実際には次のことを意味するとしたらどうでしょうか: - kpts = [
-
- ( 20 , 20 )、# 左小指
-
- ( 20 , 200 ), # 右小指
-
- ...
-
- ]
つまり、拡張によって実際にセマンティクスが変わります。左が右になり、右が左になりますが、配列内のキーポイントのインデックスは交換されません。トレーニングに大きなノイズが発生し、メトリックが悪化します。 ここで学ぶべき教訓: 3. カスタム損失関数 セマンティックセグメンテーションの問題に詳しい人は、IoU (intersection over union) メトリックを知っているかもしれません。残念ながら、SGD で直接最適化することはできないため、一般的な方法は微分可能な損失関数で近似することです。これをコーディングしてみましょう! - iou_continuous_loss(y_pred, y_true)を定義します。
-
- eps = 1e- 6
-
- _sum(x)を定義します:
-
- x.sum(- 1 ).sum(- 1 )を返す
-
- 分子 = (_sum(y_true * y_pred) + eps)
-
- 分母 = (_sum(y_true ** 2 ) + _sum(y_pred ** 2 ) -
-
- _sum(y_true * y_pred) + eps)
-
- (分子 / 分母).mean()を返します。
良さそうです。少し検査してみましょう。 - [ 3 ]の場合: ones = np.ones(( 1 , 3 , 10 , 10 ))
-
- ...: x1 = iou_continuous_loss(ones * 0.01 , ones)
-
- ...: x2 = iou_continuous_loss(ones * 0.99 , ones)
-
- [ 4 ]において: x1, x2
-
- アウト[ 4 ] : ( 0.010099999897990103,0.9998990001020204 )
x1 では、真実とはまったく異なる損失を計算しましたが、x2 は真実に非常に近い関数の結果です。予測があまり良くないため、x1 は大きくなり、x2 はゼロに近くなると予想されます。ここで何が起こったのですか? 上記の関数はメトリックの良い近似値です。指標は損失ではありません。高ければ高いほど良いのです。損失を最小限に抑えるために SGD を使用したいので、実際にはその逆を行う必要があります。 - v> iou_continuous(y_pred, y_true)を定義します。
-
- eps = 1e- 6
-
- _sum(x)を定義します:
-
- x.sum(- 1 ).sum(- 1 )を返す
-
- 分子 = (_sum(y_true * y_pred) + eps)
-
- 分母 = (_sum(y_true ** 2 ) + _sum(y_pred ** 2 )
-
- - _sum(y_true * y_pred) + eps)
-
- (分子 / 分母).mean()を返します。
-
- iou_continuous_loss(y_pred, y_true)を定義します。
-
- 戻る 1 - iou_continuous(y_pred, y_true)
これらの問題は、次の 2 つの方法で特定できます。 4. Pytorchの使用 事前にトレーニングされたモデルがあり、それが時系列モデルであると仮定します。私たちは、ceevee API に基づいて予測クラスを作成します。 - ceevee.base からAbstractPredictorをインポートします
-
- クラスMySuperPredictor(AbstractPredictor):
-
- def __init__(self, weights_path: str, ):
-
- スーパー().__init__()
-
- self.model = self._load_model(weights_path = weights_path) です。
-
- defプロセス(self, x, *kw):
-
- torch.no_grad() の場合:
-
- res = 自己.モデル(x)
-
- 戻り値
-
- @静的メソッド
-
- def _load_model(重みパス):
-
- モデル = モデルクラス()
-
- 重み = torch.load(weights_path, map_location= 'cpu' )
-
- model.load_state_dict(重み)
-
- リターンモデル
このパスワードは正しいですか?多分!確かに、これはいくつかのモデルに当てはまります。たとえば、torch.nn.BatchNorm2d などのモデルにノルム レイヤーがない場合、またはモデルが各画像の実際のノルム統計を使用する必要がある場合 (たとえば、多くの pix2pix ベースのアーキテクチャで必要)。 しかし、ほとんどのコンピューター ビジョン アプリケーションでは、コードに重要な点、つまり評価モードへの切り替えが欠けています。 この問題は、動的な pytorch グラフを静的な pytorch グラフに変換しようとすると簡単に識別できます。この変換には torch.jit モジュールがあります。 簡単な修正方法: - [ 4 ] の場合: model = nn.Sequential(
-
- ...: nn.Linear( 10 , 10 )、
-
- ..: nn.Dropout(. 5 )
-
- ...: )
-
- ...:
-
- ...: traced_model = torch.jit.trace(model.eval(), torch.rand( 10 ))
-
- # 警告はもうありません!
この時点で、torch.jit.trace はモデルを複数回実行し、結果を比較します。ここでは違いはないようです。 ただし、torch.jit.trace はここでは万能薬ではありません。これは知っておいて覚えておくべきニュアンスです。 5. コピー&ペーストの問題 トレーニングと検証、幅と高さ、緯度と経度など、多くのものがペアで存在します。注意深く読むと、ペアのメンバー間でコピー アンド ペーストすることによって発生したエラーを簡単に見つけることができます。 - v> def make_dataloaders(train_cfg、val_cfg、batch_size):
-
- トレーニング = Dataset.from_config(train_cfg)
-
- val = Dataset.from_config(val_cfg)
-
- shared_params = { 'batch_size' : batch_size, 'shuffle' : True,
-
- 'num_workers' : cpu_count()}
-
- トレーニング = DataLoader(トレーニング、**shared_params)
-
- val = DataLoader(train, **shared_params)
-
- 帰りの電車、val
愚かなミスを犯したのは私だけではありませんでした。一般的なライブラリでも同様のエラーが発生します。 - #
-
-
-
- def apply_to_keypoint(self, keypoint, crop_height= 0 , crop_width= 0 , h_start= 0 , w_start= 0 , rows= 0 , cols= 0 , **params):
-
- キーポイント = F.keypoint_random_crop(キーポイント、クロップ高さ、クロップ幅、h_start、w_start、行数、列数)
-
- scale_x = 自己幅 / クロップ高さ
-
- scale_y = self.height / crop_height
-
- keypoint = F.keypoint_scale(keypoint, scale_x, scale_y)キーポイントを返す
心配しないでください。このバグは修正されました。どうすれば回避できるでしょうか?コードをコピーして貼り付けるのではなく、コピーして貼り付ける必要がない方法でコーディングするようにしてください。 - データセット = []
-
- data_a = get_dataset(MyDataset(config[ 'dataset_a' ]), config[ 'shared_param' ], param_a) datasets.append(data_a)
-
- data_b = get_dataset(MyDataset(config[ 'dataset_b' ]), config[ 'shared_param' ], param_b) datasets.append(data_b)
-
- データセット = []
-
- 名前、param in zip(( 'dataset_a' , 'dataset_b' ), (param_a, param_b), ):
-
- データセットの追加(get_dataset(MyDataset(config[name]), config[ 'shared_param' ], param))
6. 適切なデータ型 もう 1 つ機能強化してみましょう。 - def add_noise(img: np.ndarray) -> np.ndarray:
-
- マスク = np.random.rand(*img.shape) + .5
-
- img = img.astype( 'float32' ) * マスク
-
- img.astype( 'uint8' )を返す
画像が変わりました。これは私たちが予想していたことでしょうか?まあ、あまりにも多くのことが変わってしまったのかもしれません。 ここでは、float32 を uint8 に変換するという危険な操作が行われます。これによりオーバーフローが発生する可能性があります: - def add_noise(img: np.ndarray) -> np.ndarray:
-
- マスク = np.random.rand(*img.shape) + .5
-
- img = img.astype( 'float32' ) * マスク
-
- np.clip(img, 0 , 255 ).astype( 'uint8' )を返します。
-
- img = add_noise(cv2.imread( 'two_hands.jpg' )[:, :, ::- 1 ]) _ = plt.imshow(img)
見た目がずっと良くなりましたね? ちなみに、この問題を回避する別の方法があります。車輪の再発明をするのではなく、前任者の作業に基づいてコードを修正することです。たとえば、 albumentations.augmentations.transforms.GaussNoise です。 再び同じバグが発生しました。 ここで何が間違っていたのでしょうか?まず、キュービック補間を使用してマスクのサイズを変更するのは良い考えではありません。 float32 を uint8 に変換する場合も同じ問題が発生します。3 次補間により入力よりも大きな値が出力され、オーバーフローが発生する可能性があります。 問題を見つけました。ループ内にアサーションを入れるのも良い考えです。 7. 入力ミス 完全な畳み込みネットワーク (セマンティックセグメンテーション問題など) と巨大な画像を処理する必要があるとします。画像が大きすぎて GPU に収まらない場合 (たとえば、医療画像や衛星画像など)。 この場合、画像をグリッドに分割し、各パッチに対して個別に推論を実行し、最後にそれらを結合することができます。さらに、いくつかの予測交差を使用して、境界付近のアーティファクトを滑らかにすることができます。 コーディングしましょう! - tqdm からtqdm をインポート
-
- クラスGridPredictor:
-
- "" " このクラスは、GPU メモリ制限がある場合に大きな画像のセグメンテーション マスクを予測するために使用できます。" ""
-
- def __init__(self, predictor: AbstractPredictor, size: int , stride: Optional[ int ] = None): self.predictor = predictor
-
- 自己.size = サイズ
-
- self.stride = stride stride が None でない場合はstride 、そうでない場合はsize
-
- def __call__(self, x: np.ndarray):
-
- h, w, _ = x.shape
-
- マスク = np.zeros((h, w, 1 ), dtype= 'float32' )
-
- 重み = mask.copy()
-
- i が tqdm(range( 0 , h - 1 , self.stride) の場合):
-
- j が範囲( 0 、 w - 1 、 self.stride )内である場合:
-
- a、b、c、d = i、min(h、i + 自己サイズ)、j、min(w、j + 自己サイズ)
-
- パッチ = x[a:b, c:d, :]
-
- マスク[a:b, c:d, :] += np.expand_dims(self.predictor(patch), - 1 ) 重み[a:b, c:d, :] = 1
-
- マスク/重みを返す
シンボルのタイプミスがありますが、コード スニペットは十分に大きいため、簡単に見つけることができます。コードを見るだけですぐに識別できると思います。コードが正しいかどうかは簡単に確認できます。 - クラスModel(nn.Module):
-
- def forward(self, x):
-
- x.mean(axis=- 1 )を返す
-
- モデル = モデル()
-
- grid_predictor = GridPredictor(モデル、サイズ= 128 、ストライド= 64 )
-
- simple_pred = np.expand_dims(モデル(画像), - 1 )
-
- グリッド予測 = グリッド予測子(画像)
-
- np.testing.assert_allclose(シンプル_pred、グリッド_pred、atol = .001 )
call メソッドの正しいバージョンは次のとおりです。 - def __call__(self, x: np.ndarray):
-
- h, w, _ = x.shape
-
- マスク = np.zeros((h, w, 1 ), dtype= 'float32' )
-
- 重み = mask.copy()
-
- i が tqdm(range( 0 , h - 1 , self.stride) の場合):
-
- j が範囲 ( 0 、 w - 1 、 self.stride )内にある場合: a、 b、 c、 d = i、 min(h、 i + self.size)、 j、 min(w、 j + self.size)
-
- パッチ = x[a:b, c:d, :]
-
- マスク[a:b, c:d, :] += np.expand_dims(self.predictor(patch), - 1 )
-
- 重み[a:b, c:d, :] += 1
-
- マスク/重みを返す
それでも問題が解決しない場合は、linewidth[a:b,c:d,:]+=1 であることに注意してください。 8. ImageNet正規化 転移学習を行う必要がある場合は、通常、ImageNet をトレーニングするときに行ったように画像を正規化するのが最適です。 すでに使い慣れている albumentations ライブラリを使用しましょう。 - アルバムからのインポート正規化
-
- ノルム = 正規化()
-
- 画像 = cv2.imread( 'img_small.jpg' )
-
- マスク = cv2.imread( 'mask_small.png' , cv2.IMREAD_GRAYSCALE)
-
- mask = np.expand_dims(mask, - 1 ) # shape ( 64 , 64 ) -> shape ( 64 , 64 , 1 )
-
- normed = norm(画像=img、マスク=mask)
-
- img, mask = [normed[x] for x in [ 'image' , 'mask' ]]
-
- img_to_batch(x)を定義します。
-
- x = np.transpose(x, ( 2 , 0 , 1 )).astype( 'float32' )
-
- torch.from_numpy(np.expand_dims(x, 0 ))を返す
-
- img、マスク = map(img_to_batch、(img、マスク))
-
- 基準 = F.binary_cross_entropy
ここで、ネットワークをトレーニングし、単一の画像にオーバーフィットさせます。前述したように、これは優れたデバッグ手法です。 - モデルa = UNet ( 3,1 )
-
- オプティマイザー = torch.optim.Adam(model_a.parameters(), lr=1e- 3 )
-
- 損失 = []
-
- tqdm(range( 20 ))内のtの場合:
-
- 損失 = 基準(model_a(img), マスク)
-
- 損失.append(損失.item())
-
- オプティマイザ.zero_grad()
-
- 損失.後方()
-
- オプティマイザ.ステップ()
-
- _ = plt.plot(損失)
曲率は問題ないように見えますが、クロスエントロピー損失値が -300 になることは予想されません。どうしたの? 画像の正規化は適切に機能し、手動で [0, 1] にスケーリングする必要があります。 - モデルb = UNet ( 3,1 )
-
- オプティマイザー = torch.optim.Adam(model_b.parameters(), lr=1e- 3 )
-
- 損失 = []
-
- tqdm(range( 20 ))内のtの場合:
-
- 損失 = 基準(model_b(img), マスク / 255 .)
-
- 損失.append(損失.item())
-
- オプティマイザ.zero_grad()
-
- 損失.後方()
-
- オプティマイザ.ステップ()
-
- _ = plt.plot(損失)
トレーニング ループ内の単純なアサーション (例: assert mask.max() <= 1) により、問題はすぐに検出されます。同様に、ユニットテストでは問題を検出できます。 全体として: |