ディープラーニングのコードを信頼できるのはなぜでしょうか?

ディープラーニングのコードを信頼できるのはなぜでしょうか?

ディープラーニングは、正確性を評価するのが難しい分野です。ランダムな初期化、膨大なデータセット、重みの解釈可能性の限界により、モデルのトレーニングに失敗した正確な問題を見つけるには、ほとんどの場合、試行錯誤が必要でした。従来のソフトウェア開発では、自動化されたユニット テストは、コードが想定どおりに動作するかどうかを判断するための基本的な手段です。これにより、開発者はコードを信頼し、変更を導入する際に自信を持つことができます。破壊的な変更はユニットテストによって検出されます。

[[338647]]

GitHub 上の多くの研究リポジトリの状況から判断すると、ディープラーニングの実践者はまだこのアプローチを好んでいません。実践者は、コードが正しく動作しているかどうかわからなくても大丈夫ですか? 多くの場合、学習システムの各コンポーネントの予想される動作は、上記の 3 つの理由により、簡単に定義できません。しかし、ユニットテストは研究プロセスをよりスムーズに進めるのに役立つ可能性があるため、実践者や研究者はユニットテストに対する嫌悪感を再考する必要があると私は考えています。コードを信頼する方法を学ぶ必要があります。

明らかに、ディープラーニングのユニットテストについて話すのは私が最初でも最後でもありません。このトピックに興味がある場合は、こちらをご覧ください:

  • Andrej Karpathy によるニューラル ネットワークのトレーニング レシピ
  • Sergios Karagiannakos によるディープラーニングのユニットテスト方法

この投稿は、上で述べたものからインスピレーションを得たものですが、おそらく今は思いつかないようなものも数多くあるでしょう。議論に背景を追加するために、「同じことを繰り返さないように」再利用可能な単体テストを記述する方法に焦点を当てます。

この例では、MNIST で変分オートエンコーダー (VAE) をトレーニングする PyTorch で記述されたシステムのコンポーネントをテストします。この記事のすべてのコードはgithub.com/tilman151/unittest_dlで見つかります。

ユニットテストとは何ですか?

ユニット テストに精通している場合は、このセクションをスキップできます。それ以外の人のために、Python でのユニット テストがどのようなものかを説明します。簡単にするために、他の派手なパッケージの代わりに組み込みパッケージ unittest を使用します。

一般的に、ユニット テストの目的は、コードが正しく実行されるかどうかを確認することです。多くの場合 (私も長い間そうでした)、ファイルの末尾に次のような記述が見られます。

  1. __name__ == 'main'の場合:
  2. ネット = ネットワーク()
  3. x = torch.randn(4, 1, 32, 32)
  4. y = ネット(x)
  5. 印刷(y.shape)

ファイルを直接実行すると、コード スニペットはネットワークを構築し、フォワード パスを実行し、出力の形状を出力します。この方法では、順方向伝播でエラーが発生するかどうか、および出力の形状が妥当かどうかを確認できます。コードを複数のファイルに分散する場合は、各ファイルを手動で実行し、コンソールに出力される内容を確認する必要があります。さらに悪いことに、このコード スニペットは実行後に削除され、変更があった場合に書き換えられることがあります。

原則として、これはすでに基本的な単体テストです。簡単に自動化できるように、少し形式化するだけで済みます。次のようになります:

  1. ユニットテストをインポートする
  2.  
  3. クラス MyFirstTest(unittest.TestCase):
  4. def test_shape(self):
  5. ネット = ネットワーク()
  6. x = torch.randn(4, 1, 32, 32)
  7. y = ネット(x)
  8. self.assertEqual( torch.Size ((10,)), y.shape)

unittest パッケージの主なコンポーネントは TestCase クラスです。個々の単体テストは、TestCase サブクラスのメンバー関数です。私たちの場合、パッケージはクラス MyFirstTest を自動的に検出し、関数 'test_shape' を実行します。 assertEqual 呼び出しの条件が満たされた場合、テストは成功します。そうでない場合、またはクラッシュした場合、テストは失敗します。

何をテストする必要がありますか?

ユニット テストの仕組みがわかったので、次の質問は、何をテストすべきかということです。以下に、この例のコード構造を示します。

  1. |- 出典
  2. |- データセット.py
  3. |- モデル.py
  4. |- トレーナー.py
  5. |- 実行.py

run.py はプログラムのエントリ ポイントに過ぎないため、これを除くすべてのファイルで機能をテストします。

データセット

例で使用するデータセットは、torchvisionMNIST クラスです。したがって、画像の読み込みやトレーニング/テストの分割などの基本的な機能は正常に動作すると想定できます。ただし、MNIST クラスには構成のための十分な機会が用意されているため、すべてが正しく構成されていることをテストする必要があります。 dataset.py ファイルには、2 つのメンバー変数を持つ MyMNIST というクラスが含まれています。メンバー train_data には、データのトレーニング部分をロードするように構成された torchvisionMNIST クラスのインスタンスがあり、test_data のインスタンスはテスト部分をロードします。どちらの方法でも、各画像の両側に 2 ピクセルをパディングし、ピクセル値を [-1,1] の間に正規化します。さらに、train_data は各画像にランダムな回転を適用してデータを拡張します。

データの形状

上記のコード スニペットを引き続き使用するには、まずデータセットが目的の形状を出力するかどうかをテストします。画像のパディングにより、画像のサイズは 32 x 32 ピクセルになります。私たちのテストは次のようになります:

  1. def test_shape(self):
  2. データセット = MyMNIST()
  3. サンプル、_ = dataset.train_data[0]
  4. 自己.assertEqual(torch.Shape((1, 32, 32)), sample.shape)

これで、パディングが希望どおりであることを確認できます。これは些細なことのように思えるかもしれませんし、私がこれをテストすることにこだわりすぎていると思う人もいるかもしれませんが、塗りつぶし機能がどのように機能するかを理解できなかったために、図形を間違えたことが何回あったかわかりません。このような単純なテストはすぐに作成でき、後で多くの頭痛の種を防ぐことができます。

データのスケーリング

次に設定するのは、データのスケーリングです。私たちの場合、これは非常に簡単です。各画像のピクセル値が[-1,1]の範囲内であることを確認します。以前のテストとは異なり、データセット内のすべての画像に対してテストを行います。このようにして、データのスケーリング方法に関する仮定がデータセット全体に対して有効であることを確認できます。

  1. def test_scaling(self):
  2. データセット = MyMNIST()
  3. サンプルの場合 dataset.train_data_:
  4. 自己.assertGreaterEqual(1,サンプル.max ())
  5. self.assertLessEqual(-1、サンプル.min ())
  6. self.assertTrue( torch.any (サンプル < 0))
  7. self.assertTrue( torch.any (サンプル > 0))

ご覧のとおり、各画像の最大値と最小値が範囲内にあるかどうかをテストしているだけではありません。また、値を[0,1]にスケーリングすることをアサートすることで、ゼロより大きい値とゼロより小さい値の存在をテストします。このテストが機能するのは、MNIST のすべての画像が値の範囲全体をカバーしていると想定できるためです。自然画像などのより複雑なデータの場合は、より複雑なテスト条件が必要になります。スケーリングがデータの統計に基づいている場合は、それらの統計を計算するためにトレーニング部分のみを使用しているかどうかをテストすることもお勧めします。

データ拡張

トレーニング データを増やすと、特にデータ量が限られている場合に、モデルのパフォーマンスを大幅に向上させることができます。一方、モデルの評価の確実性を維持したいため、テスト データは追加しません。つまり、トレーニング データは拡張されているが、テスト データは拡張されていないかどうかをテストする必要があります。賢明な読者はこの時点で重要なことに気づくでしょう。これまでのところ、テストはトレーニング データのみを対象としています。これは強調する必要がある点です:

トレーニングデータとテストデータの両方で常にテストを実行する

コードがデータの一部で動作するからといって、別の部分に検出されていないバグがないという保証はありません。データ拡張のために、セクションごとにコードの異なる動作をアサートしたい場合もあります。

私たちの拡張問題に対する簡単なテストは、サンプルを 2 回ロードし、2 つのバージョンが等しいかどうかを確認することです。簡単な解決策は、各パーツごとにテスト関数を記述することです。

  1. def test_augmentation_active_train_data(self):
  2. データセット = MyMNIST()
  3. 同じ = []
  4. i範囲(len(dataset.train_data))内である場合:
  5. sample_1, _ = データセット.train_data[i]
  6. sample_2, _ = データセット.train_data[i]
  7. are_same.append(0 == torch.sum (sample_1 - sample_2))
  8.  
  9. self.assertTrue(ではない すべて(同じ)
  10.  
  11. def test_augmentation_inactive_test_data(自己):
  12. データセット = MyMNIST()
  13. 同じ = []
  14. i範囲(len(dataset.test_data))内である場合:
  15. sample_1, _ = データセット.テストデータ[i]
  16. sample_2, _ = データセット.テストデータ[i]
  17. are_same.append(0 == torch.sum (sample_1 - sample_2))
  18.  
  19. self.assertTrue(すべて(are_same))

これらの関数はテストしたいものをテストしますが、ご覧のとおり、ほとんど繰り返しになります。これには主に 2 つの欠点があります。まず、テストで何かを変更する必要がある場合は、両方の関数で変更することを忘れないようにする必要があります。次に、検証セクションなどの別のセクションを追加する場合は、テストを 3 回複製する必要があります。これを修正するには、テスト機能を別の関数に抽出し、実際のテスト関数によって 2 回呼び出されるようにする必要があります。リファクタリングされたテストは次のようになります。

  1. def test_augmentation(自己):
  2. データセット = MyMNIST()
  3. self._check_augmentation(dataset.train_data、アクティブ= True )
  4. self._check_augmentation(dataset.test_data、アクティブ= False )
  5.  
  6. def _check_augmentation(自己、データ、アクティブ):
  7. 同じ = []
  8. iが範囲(len(データ))内にある場合:
  9. sample_1, _ = データ[i]
  10. sample_2, _ = データ[i]
  11. are_same.append(0 == torch.sum (sample_1 - sample_2))
  12.  
  13. アクティブな場合:
  14. self.assertTrue(ではない すべて(同じ)
  15. それ以外
  16. self.assertTrue(すべて(are_same))

_check_augmentation 関数は、指定されたデータセットが拡張されているかどうかをアサートし、コード内の重複を効果的に削除します。関数自体は test_ で始まっていないため、unittest パッケージによって自動的に実行されません。テスト関数がかなり短くなったので、これらを 1 つの結合関数に結合してみましょう。これらは、拡張がどのように機能するかという単一の概念をテストするため、同じテスト関数に属する必要があります。しかし、この組み合わせによって別の問題が発生します。テストが失敗した場合、どの部分が失敗したかを直接確認することが難しくなります。このパッケージは、組み合わせ関数の名前のみを伝えます。 subTest 関数を入力します。 TestCase クラスにはメンバー関数 subTest があり、テスト関数内のさまざまなテスト コンポーネントをマークできます。この方法により、パッケージはテストのどの部分が失敗したかを正確に知ることができます。最終的な関数は次のようになります。

  1. def test_augmentation(自己):
  2. データセット = MyMNIST()
  3. self.subTest(split= 'train' )を使用する場合:
  4. self._check_augmentation(dataset.train_data、アクティブ= True )
  5. self.subTest(split= 'test' )の場合:
  6. self._check_augmentation(dataset.test_data、アクティブ= False )

これで、重複がなく、正確にターゲットを絞った、再利用可能なテスト関数ができました。ここで使用する基本原則は、前のセクションで記述する他のすべての単体テストに適用できます。結果のテストは添付のリポジトリで確認できます。

データの読み込み

データセットの最後のタイプの単体テストは、組み込みデータセットを使用しているため、この例にはまったく関係ありません。これは私たちの学習システムの重要な部分をカバーしているため、とにかく含めます。通常、バッチ処理を処理し、読み込みを並列化できる DataLoader クラスで Dataset を使用します。したがって、データセットがデータローダーでシングルプロセス モードとマルチプロセス モードの両方で動作するかどうかをテストすることをお勧めします。拡張テストについて学んだことを考慮すると、テスト関数は次のようになります。

  1. def test_single_process_dataloader(self):
  2. データセット = MyMNIST()
  3. self.subTest(split= 'train' )を使用する場合:
  4. self._check_dataloader(dataset.train_data、num_workers=0) を実行します。
  5. self.subTest(split= 'test' )の場合:
  6. self._check_dataloader(データセット.test_data、num_workers=0)
  7.  
  8. def test_multi_process_dataloader(self):
  9. データセット = MyMNIST()
  10. self.subTest(split= 'train' )を使用する場合:
  11. self._check_dataloader(データセット.train_data、num_workers=2)
  12. self.subTest(split= 'test' )の場合:
  13. self._check_dataloader(データセット.test_data、num_workers=2)
  14.  
  15. def _check_dataloader(self, data, num_workers):
  16. ローダー = DataLoader(データ、バッチサイズ = 4、ワーカー数 = ワーカー数)
  17. ローダー内の_の場合:
  18. 合格

関数 _check_dataloader は、ロードされたデータに対してテストを実行しません。読み込みプロセスでエラーが発生しなかったことを確認したいだけです。理論的には、正しいバッチ サイズや、シーケンス データのさまざまな長さのパディングなどを確認することもできます。データローダーには最も基本的な構成を使用しているため、これらのチェックは省略できます。

繰り返しになりますが、このテストは些細で不必要に思えるかもしれませんが、この簡単なチェックによって私が救われた例を挙げてみましょう。このプロジェクトでは、pandas データフレームからシーケンス データを読み込み、これらのデータフレームのスライディング ウィンドウからサンプルを構築する必要があります。データセットはメモリに収まらないほど大きいため、オンデマンドでデータ モデルをロードし、要求されたシーケンスをそこから切り取る必要があります。読み込み速度を向上させるために、一部のデータ ファイルをキャッシュするために LRU キャッシュを使用することにしました。初期の単一プロセス実験では期待どおりに動作したため、コード ベースに組み込むことにしました。このキャッシュは複数のプロセスではうまく動作しないことが判明しましたが、ユニット テストでこの問題は早期に検出されました。マルチプロセスを使用する場合、後で不快な驚きを避けるためにキャッシュを無効にしました。

最終ノート

ユニット テストで別の繰り返しパターンを見たことがある人もいるかもしれません。各テストはトレーニング データに対して 1 回、テスト データに対して 1 回実行され、結果として同じ 4 行のコードが生成されます。

  1. self.subTest(split= 'train' )を使用する場合:
  2. self._check_something(データセット.train_data)
  3. self.subTest(split= 'test' )の場合:
  4. self._check_dataloader(データセット.test_data)

この重複を排除する理由も十分にあります。残念ながら、これには function_check_something を引数として受け取る高階関数の作成が必要になります。場合によっては、たとえば拡張テストのために、_check_something 関数に追加のパラメータを渡す必要があることもあります。最後に、必要なプログラミング構造によって複雑さが増し、テスト対象の概念がわかりにくくなります。一般的なルールとして、読みやすさと再利用性を考慮して、テストは必要に応じて複雑にしてください。

モデル

モデルは学習システムの中核コンポーネントであると言えるため、通常は完全に構成可能である必要があります。つまり、まだテストすべきことがたくさんあるということです。幸いなことに、ニューラル ネットワーク モデル用の PyTorch の API は非常に簡潔であり、ほとんどの実践者はそれを厳密に使用しています。これにより、モデルの再利用可能な単体テストを非常に簡単に記述できるようになります。

私たちのモデルは、完全に接続されたエンコーダーとデコーダーで構成されるシンプルな VAE です。 forward 関数は入力画像を受け取り、それをエンコードし、再パラメータ化操作を実行してから、潜在コードを画像にデコードします。この変換は比較的単純ですが、ユニット テストする価値のあるいくつかの側面を示しています。

モデルの出力形状

この記事の冒頭で紹介した最初のコードは、ほとんどすべての人が行うテストです。このテストが単体テストとしてどのように記述されるかもすでにわかっています。私たちがしなければならないことは、テストするために正しい形状を追加することだけです。オートエンコーダの場合、形状が入力と同じかどうかを判断するのは簡単です。

  1. 翻訳:
  2. def test_shape(self):
  3. ネット = model.MLPVAE(入力形状=(1, 32, 32)、ボトルネック寸法=16)
  4. 入力 = torch.randn(4, 1, 32, 32)
  5. 出力 = ネット(x)
  6. self.assertEqual(入力.shape、出力.shape)

これも単純ですが、最も厄介なバグのいくつかを見つけるのに役立ちます。たとえば、平坦化された表現からモデル出力を再形成するときに、チャネル ディメンションを追加し忘れた場合などです。

最後に追加したテストは torch.nograd です。これは、この関数が勾配を記録する必要がないことを PyTorch に伝え、わずかな速度向上をもたらします。各テストの量は多くないかもしれませんが、どれくらい書く必要があるかはわかりません。ここでも、ユニットテストに関する知恵の引用をもう一つ紹介します。

テストをより速くします。そうでなければ、誰もそれを実行したくないでしょう。

開発中はユニット テストを頻繁に実行する必要があります。テストの実行に長い時間がかかる場合は、テストをスキップできます。

モデルの移動

CPU 上でディープ ニューラル ネットワークをトレーニングすると、ほとんどの場合、非常に時間がかかります。そのため、GPU を使用して高速化します。これを実現するには、すべてのモデル パラメータが GPU 上に存在している必要があります。したがって、モデルはデバイス間 (CPU と複数の GPU) で正しく移動できると主張する必要があります。

よくある間違いのある VAE の例でこの問題を説明することができます。ここでは、再パラメータ化トリックを実行するボトルネック関数を確認できます。

  1. ボトルネックを定義します(self、mu、log_sigma):
  2. ノイズ = torch.randn(mu.shape)
  3. latent_code = log_sigma.exp() * ノイズ + mu
  4.  
  5. 潜在コードを返す

潜在事前分布のパラメータを取得し、標準ガウス分布からノイズテンソルをサンプリングし、パラメータを使用して変換します。これは CPU 上では問題なく実行されますが、モデルを GPU に移動すると失敗します。問題は、ノイズ テンソルがデフォルトで CPU メモリ内に作成され、モデルが存在するデバイスに移動されないことです。単純な間違いと単純な解決策。問題のあるコード行を noise = torch.randn_like(mu) に置き換えました。これにより、テンソル mu と同じ形状のノイズ テンソルが同じデバイス上に作成されます。

これらのバグを早期に発見するのに役立つテスト:

  1. 翻訳: 翻訳者: トーチ
  2. @unittest.skipUnless(torch.cuda.is_available(), 'GPUが検出されませんでした' )
  3. テストデバイス移動の定義:
  4. ネット = model.MLPVAE(入力形状=(1, 32, 32)、ボトルネック寸法=16)
  5. net_on_gpu = net.to ( 'cuda:0' )
  6. net_back_on_cpu = net_on_gpu.cpu()
  7.      
  8. 入力 = torch.randn(4, 1, 32, 32)
  9.  
  10. トーチ.マニュアル_シード(42)
  11. 出力CPU = ネット(入力)
  12. トーチ.マニュアル_シード(42)
  13. 出力gpu = net_on_gpu(入力.to ( 'cuda:0' ))
  14. トーチ.マニュアル_シード(42)
  15. 出力_back_on_cpu = net_back_on_cpu(入力)
  16.  
  17. self.assertAlmostEqual(0., torch.sum (outputs_cpu - outputs_gpu.cpu())) をアサートします。
  18. self.assertAlmostEqual(0., torch.sum (outputs_cpu - outputs_back_on_cpu)) を返します。

正しいことを確認するために、ネットワークをある CPU から別の CPU に移動し、また元に戻しました。これで、ネットワークのコピーが 3 つ作成され (ネットワークを移動すると複製されます)、フォワード パスに同じ入力テンソルが使用されます。ネットワークが正しくシフトされていれば、フォワード パスはエラーをスローせずに実行され、毎回同じ出力が生成されるはずです。

このテストを実行するには、もちろん GPU が必要ですが、ラップトップで簡単なテストを実行したい場合もあります。 unittest.skipUnless は、PyTorch が GPU を検出しない場合にテストをスキップできます。これにより、テスト結果と失敗したテストの混同が回避されます。

また、各パスの前にトーチのランダム シードを固定していることもわかります。 VAE は非決定論的であるため、これを行う必要があります。そうしないと、異なる結果が得られます。これは、ディープラーニング コードの単体テストにおけるもう 1 つの重要な概念を示しています。

テストにおけるランダム性を制御します。

モデルがエッジ ケースに該当することを保証できない場合、モデルのまれなエッジ ケースをどのようにテストしますか? モデルの出力が決定論的であることをどのように保証しますか? 失敗したテストが偶然によるものか、自分で導入したバグによるものかをどのように判断しますか? ディープラーニング フレームワークのシード値を手動で設定することで、関数からランダム性を排除できます。さらに、CuDNN を決定論的モードに設定する必要があります。これは主に畳み込みに影響しますが、とにかく良いアイデアです。

使用中のすべてのフレームワークのシードを決定するように注意してください。 Numpy と組み込みの Python 乱数ジェネレーターには独自のシードがあり、個別に設定する必要があります。次のような関数があると便利です。

  1. def make_deterministic(シード=42):
  2. # パイトーチ
  3. torch.manual_seed(シード)
  4. torch.cuda.is_available() の場合:
  5. torch.backends.cudnn.deterministic = True  
  6. torch.backends.cudnn.benchmark = False  
  7.      
  8. # ナンピー
  9. np.random.seed(シード)
  10.      
  11. #組み込みPython
  12. ランダムシード(シード)

モデルとサンプルの独立性

99.99% のケースでは、確率的勾配降下法を使用してモデルをトレーニングする必要があります。モデルにサンプルのミニバッチを入力し、それらの平均損失を計算します。トレーニング サンプルをバッチ処理する場合、モデルが各サンプルを処理できること、つまり、サンプルをモデルに個別に供給できることが前提となります。つまり、バッチ内の例は、モデルによって処理されるときに互いに影響を及ぼしません。この仮定は脆弱であり、間違ったテンソル次元に対して誤った再形成または集約が実行されると破られる可能性があります。

次のテストでは、入力に対して前方パスと後方パスを実行して、サンプルの独立性をチェックします。バッチ全体の損失を平均化する前に、損失にゼロを掛けます。モデルがサンプルの独立性を維持する場合、勾配はゼロになります。唯一主張しなければならないことは、マスクされたサンプルが次の場合にのみ勾配がゼロになるということです。

  1. def test_batch_independence(self):
  2. 入力 = torch.randn(4, 1, 32, 32)
  3. inputs.requires_grad = True  
  4. ネット = model.MLPVAE(入力形状=(1, 32, 32)、ボトルネック寸法=16)
  5.  
  6. #バッチノルムを無効にするために評価モードフォワードパスを計算する
  7. ネット評価()
  8. 出力 = ネット(入力)
  9. ネット.トレイン()
  10.  
  11. #バッチ内の特定のサンプルマスク損失
  12. バッチサイズ = 入力[0].形状[0]
  13. mask_idx = torch.randint(0, batch_size, ())
  14. マスク = torch.ones_like(出力)
  15. マスク[mask_idx] = 0
  16. 出力 = 出力 * マスク
  17.  
  18. # 逆方向パスを計算する
  19. 損失 = 出力.平均()
  20. 損失.後方()
  21.  
  22. #勾配が存在するかどうかを確認し  マスクさたサンプルの場合はゼロ
  23. i , gradを enumerate(inputs.grad)指定します:
  24. i == mask_idxの場合:
  25. 自己.assertTrue( torch.all (grad == 0).item())
  26. それ以外
  27. self.assertTrue( torch.all (grad == 0)ではない)

コード スニペットを注意深く読むと、モデルを評価モードに設定していることがわかります。これは、バッチ正規化が上記の仮定に違反するためです。プロセス平均と標準偏差の処理により、バッチ内のサンプルが相互汚染されたため、評価モードによるサンプルの更新を停止しました。これが可能なのは、モデルがトレーニング モードと評価モードで同じように動作するからです。モデルがこのように動作しない場合は、テスト用に無効にする別の方法を見つける必要があります。 1 つのオプションは、一時的にインスタンス正規化に置き換えることです。

上記のテスト関数は非常に一般的なので、そのままコピーできます。ただし、モデルが複数の入力を受け入れる場合は例外です。この問題を処理するには追加のコードが必要です。

モデルパラメータの更新

次のテストも勾配に関するものです。初期化のようにネットワーク アーキテクチャが複雑になると、無効なサブグラフが簡単に構築されます。デッド サブグラフは、学習可能なパラメータを含み、フォワード パス、バックワード パス、またはその両方で使用されないネットワークの一部です。これは、コンストラクターでレイヤーを構築し、それを forward 関数に適用するのを忘れるのと同じくらい簡単です。

これらのデッド サブグラフは、最適化ステップを実行し、ネットワーク パラメータの勾配を調べることで見つけることができます。

  1. def test_all_parameters_updated(自己):
  2. ネット = model.MLPVAE(入力形状=(1, 32, 32)、ボトルネック寸法=16)
  3. 最適化 = torch.optim.SGD(net.parameters(), lr=0.1)
  4.  
  5. 出力 = net(torch.randn(4, 1, 32, 32))
  6. 損失 = 出力.平均()
  7. 損失.後方()
  8. 最適化ステップ()
  9.  
  10. param_name 、param はself.net.named_pa​​rameters()内にあります:
  11. param.requires_gradの場合:
  12. self.subTest( name =param_name )の場合:
  13. 自己アサートIsNotNone(param.grad)
  14. self.assertNotEqual(0., torch.sum (param.grad**2))

パラメータ関数によって返されるモデルのすべてのパラメータには、最適化ステップ後に勾配テンソルが必要です。また、使用している損失はゼロであってはなりません。テストでは、モデル内のすべてのパラメータに勾配が必要であると想定します。更新すべきでないパラメータであっても、最初に requires_grad フラグがチェックされます。テストでいずれかのパラメータが失敗した場合、サブテストの名前からどこを調べればよいかのヒントが得られます。

再利用性の向上

モデルのすべてのテストを記述したので、それらを全体として分析できます。これらのテストには 2 つの共通点があることに注意してください。すべてのテストは、モデルを作成し、サンプル入力のバッチを定義することから始まります。いつものように、このレベルの冗長性はタイプミスや矛盾につながる可能性があります。さらに、モデルのコンストラクターを変更するときに、各テストを個別に更新する必要はありません。

幸いなことに、unittest は setUp 関数という簡単な解決策を提供します。この関数は、TestCase 内の各テスト関数を実行する前に呼び出され、通常は空です。 setUp でモデルと入力を TestCase のメンバー変数として定義することで、テストのコンポーネントを 1 か所で初期化できます。

  1. クラス TestVAE(unittest.TestCase):
  2. def setUp(self):
  3. self.net = model.MLPVAE(input_shape=(1, 32, 32)、ボトルネック次元=16)
  4. 自己テスト入力 = torch.random(4, 1, 32, 32)
  5.  
  6. ... # テスト関数

ここで、ネットと入力の出現をそれぞれのメンバー変数に置き換えれば完了です。さらに一歩進んで、すべてのテストに同じモデル インスタンスを使用する場合は、setUpClass を使用できます。この関数は、TestCase が構築されるときに 1 回呼び出されます。これは、ビルドが遅く、ビルドを複数回実行したくない場合に便利です。

この時点で、VAE モデルをテストするための優れたシステムが完成しました。テストを簡単に追加して、毎回同じバージョンのモデルをテストしていることを確認できます。しかし、新しい種類の畳み込み層を導入したい場合はどうなるでしょうか? 同じデータに対して実行され、同じ動作をするため、同じテストが適用されます。

TestCase 全体を単純にコピーするのは明らかに推奨される解決策ではありませんが、setUp を使用することで、すでに正しい方向に進んでいます。すべてのテスト関数を基本クラスに移動し、setUp を抽象関数として保持します。

  1. クラスAbstractTestVAE(unittest.TestCase):
  2. def setUp(self):
  3. NotImplementedError を発生させる
  4.  
  5. ... # テスト関数

IDE は、クラスにメンバー変数 net と test_inputs がないと警告しますが、Python は気にしません。サブクラスがそれらを追加する限り、動作します。テストするモデルごとに、この抽象クラスのサブクラスを作成し、その中に setUp を実装します。複数のモデルまたは同じモデルの複数の構成の TestCase を作成するのは、次のように簡単です。

  1. クラス TestCNNVAE(AbstractTestVAE):
  2. def setUp(self):
  3. 自己テスト入力 = torch.randn(4, 1, 32, 32)
  4. self.net = model.CNNVAE(input_shape=(1, 32, 32)、ボトルネックの寸法=16)
  5.  
  6. クラス TestMLPVAE(AbstractTestVAE):
  7. def setUp(self):
  8. 自己テスト入力 = torch.randn(4, 1, 32, 32)
  9. self.net = model.MLPVAE(input_shape=(1, 32, 32)、ボトルネック_dim=16)

残る質問は1つだけです。 unittest パッケージは、unittest.TestCase のすべての子要素を検出して実行します。これにはインスタンス化できない抽象基本クラスが含まれるため、常に失敗するテストが発生します。

解決策は、一般的なデザイン パターンによって提案されます。 AbstractTestVAE の親クラスとして TestCase を削除すると、検出されなくなります。代わりに、具体的なテストに TestCase と AbstractTestVAE という 2 つの親クラスを持たせます。抽象クラスと具象クラスの関係は、親クラスと子クラスの関係ではなくなりました。代わりに、具象クラスは抽象クラスによって提供される共有機能を使用します。このモードは MixIn と呼ばれます。

  1. クラスAbstractTestVAE:
  2. ...
  3.  
  4. クラス TestCNNVAE(unittest.TestCase、AbstractTestVAE):
  5. ...
  6.  
  7. クラス TestMLPVAE(unittest.TestCase、AbstractTestVAE):
  8. ...

メソッドの検索は左から右に行われるため、親クラスの順序は重要です。これは、TestCase が AbstractTestVAE の共有メソッドをオーバーライドすることを意味します。私たちの場合、これは問題ではありませんでしたが、とにかく知っておいてよかったです。

トレーナー

私たちの学習システムの最後の部分はトレーナークラスです。すべてのコンポーネント (データセット、オプティマイザー、モデル) をまとめて、それらを使用してモデルをトレーニングします。さらに、テスト データの平均損失を出力する評価関数を実装します。トレーニング中、すべての損失とメトリックは視覚化のために TensorBoard イベント ファイルに書き込まれます。

再利用可能なテストの作成は、実装の自由度が最大限に高まるため、これらの部分の中で最も困難です。トレーニング用にスクリプト ファイル内の単純なコードを使用する人もいれば、関数にラップする人もいれば、よりオブジェクト指向のスタイルを維持しようとする人もいます。どちらの方法を好むかは判断しません。私が言える唯一のことは、私の経験では、きちんとカプセル化されたトレーナー クラスによってユニット テストが最も快適になるということです。

ただし、以前に学んだ原則のいくつかはここでも当てはまることがわかります。

トレーナーの損失

ほとんどの場合、Torch から事前に実装された損失関数を選択するだけで済みます。しかし、選択した損失関数が達成できない可能性もあります。これは、実装が比較的単純であるか、機能がニッチすぎるか、または新しすぎることが原因である可能性があります。いずれにせよ、自分で実装する場合には、テストも行う必要があります。

私たちの例では、全体的な損失関数の一部として Kulback-Leibler (KL) ダイバージェンスを使用していますが、これは PyTorch では利用できませんでした (現在のバージョンでは利用できます)。私たちの実装は次のとおりです。

  1. _kl_divergence(log_sigma, mu)を定義します。
  2. 0.5 * torch.sum ( (2 * log_sigma).exp() + mu ** 2 - 1 - 2 * log_sigma)を返します

この関数は、多変量ガウス分布の標準偏差と平均の対数を取り、閉じた形式で標準ガウス分布の KL ダイバージェンスを計算します。

この損失を確認する 1 つの方法は、手動で計算し、比較のためにハードコードすることです。より良いアプローチとしては、別のパッケージでリファレンス実装を見つけて、その出力に対してコードをチェックすることです。幸いなことに、scipy パッケージには離散 KL ダイバージェンスの実装があり、それを使用できます。

  1. 翻訳:
  2. def test_kl_divergence(自己):
  3. mu = np.random.randn(10) * 0.25 # は0付近を意味します。
  4. sigma = np.random.randn(10) * 0.1 + 1. # 1付近の標準偏差。
  5. 標準正規サンプル = np.random.randn(100000, 10)
  6. 変換された正規分布サンプル = 標準正規分布サンプル * シグマ + ミュー
  7.  
  8. ビン = 1000
  9. ビン範囲 = [-2, 2]
  10. 予想されるkl_div = 0
  11. iが範囲(10)内にある場合:
  12. standard_normal_dist, _ = np.histogram(standard_normal_samples[:, i], bins, bin_range)
  13. 変換された正規分布、_ = np.ヒストグラム(変換された正規分布サンプル[:, i]、ビン、ビン範囲)
  14. 期待kl_div += scipy.stats.entropy(変換された正規分布、標準正規分布)
  15.  
  16. 実際の kl_div は self.vae_trainer._kl_divergence(torch.tensor(sigma).log(), torch.tensor(mu)) です。
  17.  
  18. self.assertAlmostEqual(期待されるkl_div、実際のkl_div.numpy()、デルタ=0.05)

まず、標準ガウス関数と、平均と標準偏差が異なるガウス関数から十分な大きさのサンプルを抽出します。次に、np.histogram 関数を使用して、基礎となる pdf の離散近似値を取得します。これらを入手したら、scipy.stats.entropy を使用して比較用の KL ダイバージェンスを取得できます。 scipy.stats.entropy は近似値に過ぎないため、比較には比較的大きなデルタを使用します。

Trainer オブジェクトを作成せず、代わりに TestCase のメンバーを使用したことに気づいたかもしれません。ここでは、モデル テストで使用したのと同じトリックを使用し、setUp 関数で作成しました。 PyTorch と NumPy のシードも修正しました。ここでは勾配は必要ないので、関数を @torch.no_grad で装飾します。

トレーナーのログ

TensorBoard を使用して、トレーニング プロセスの損失とメトリックを記録します。これを実現するには、すべてのログが期待どおりに書き込まれることを確認する必要があります。 1 つの方法は、トレーニング後にイベント ファイルを開き、正しいイベントを探すことです。これも有効なオプションですが、unittest パッケージの興味深い機能を別の方法で見てみましょう。それは、モックです。

Mock を使用すると、関数またはオブジェクトを、それがどのように呼び出されるかを監視する関数でラップできます。サマリー ライターの add_scalar 関数を置き換えて、重要な損失とメトリックがすべてこの方法で記録されるようにします。

  1. def test_logging(自己):
  2. mock.patch.object(self.vae_trainer.summary, 'add_scalar' )add_scalar_mockとして使用します:
  3. 自己.vae_trainer.train(1)
  4.  
  5. expected_calls = [mock.call( 'train/recon_loss' , mock.ANY , 0) ,
  6. mock.call( 'train/kl_div_loss' , mock.ANY , 0) ,
  7. mock.call( 'train/loss' , mock.ANY , 0) ,
  8. mock.call( 'test/loss' , mock.ANY , 0) ]
  9. add_scalar_mock.assert_has_calls(期待される呼び出し)

ASSERT_HAS_CALLS関数は、予想通話のリストと実際の記録された呼び出しと一致します。 Mock.は、とにかくそれを知らないので、ログスカラーの値を気にしないことを示しています。

データセット全体でエポックを実行する必要はないため、セットアップにバッチが1つだけあるようにトレーニングデータを構成します。これにより、テストを大幅に高速化できます。

トレーナーフィッティング

最後の質問も答えるのが最も難しいです。私のトレーニングは最終的にこの質問に決定的に答えますか?

これは非常に時間がかかるため、より高速な方法を使用します。私たちのトレーニングにより、モデルが単一のデータのバッチに過剰に触れているかどうかがわかります。テスト機能は非常に簡単です:

  1. def test_overfit_on_one_batch(self):
  2. self.vae_trainer.train(500)
  3. self.assertgreaterequal(30、self.vae_trainer.eval())

前のセクションで説明したように、セットアップ関数は、1つのバッチのみを含むデータセットを備えたトレーナーを作成します。さらに、トレーニングデータもテストデータとして使用します。このようにして、評価機能からトレーニングバッチの損失を取得し、予想される損失と比較できます。

分類の問題の場合、完全に過剰に過剰になった場合、損失がゼロになると予想されます。 「vae」の問題は、それが非決定的生成モデルであり、ゼロ損失が非現実的であることです。そのため、予想される損失は30であり、ピクセルあたり0.04の誤差に等しい。

これはこれまでで最も長い実行テストであり、500エポックに対して実行されます。最終的に、ラップトップで約1.5分かかりましたが、これはまだ合理的です。 GPUなしでマシンのサポートを減らすことなくさらにスピードアップするには、このラインをセットアップに追加するだけです。

  1. device = 'cuda:0' if torch.cuda.is_available() else   'CPU'  

このようにして、GPUを持っている場合は、それを利用できます。そうでない場合は、トレーニングにCPUを使用できます。

注意を払う最後のこと

ロギングを行うと、トレーナーに対するユニットテストがイベントファイルでフォルダーを入力する傾向があることに気付くかもしれません。これを回避するために、TempFileパッケージを使用して、トレーナー用の一時的なログディレクトリを作成します。テストが終了した後、再度テストとその内容を削除する必要があります。これを行うには、Twin Functionsのセットアップと分解を使用します。この関数を呼び出して、各テスト関数の後、クリーニングプロセスは次のとおりです。

  1. Def Teardown(self):
  2. shutil.rmtree(self.log_dir)

要約する

この記事を読み終えました。試練全体から私たちが持っているものを評価しましょう。

小さな例で書いたテストスイートには58の単体テストが含まれており、ラン全体に約3.5分かかります。これらの58のテストでは、20の関数のみを書きました。すべてのテストは、明確かつ独立して実行できます。 GPUがある場合は、追加のテストを実行できます。データセットやモデルテストなどのほとんどのテストは、他のプロジェクトで簡単に再利用できます。使用できます:

  • サブテストは、データセットの複数の構成のテストを実行します
  • セットアップと分解機能は、一貫してテストを初期化してクリーンアップします
  • VAEのさまざまな実装をテストするための抽象テストクラス
  • torch.no_gradデコレーターは、可能であれば勾配計算を無効にします
  • Mockモジュールは、関数が正しく呼ばれているかどうかを確認します

最後に、少なくとも誰かが深い学習プロジェクトで単体テストを使用するように説得できることを願っています。この記事のサポートGIT倉庫は、出発点として使用できます。

<<:  コンピュータービジョン: 画像検出と画像セグメンテーションの違いは何ですか?

>>:  暑い天候で火災が続発、消防ロボットが救助活動に活躍

ブログ    
ブログ    
ブログ    

推薦する

Reverse Midjourneyがオンラインになりました!デジタルアーティストがスティーブ・ジョブズに魅了され、写真がボルヘスの精神世界に入る

ブラウザに住むアーティストが開発した、ニューヨーク発のAIカメラアプリが人気を集めている。もしスティ...

SFが現実になる?偉大な劉慈欣がAI企業に入社

[[411067]]サイエンスフィクションと現実がこれほど近づいたことはかつてありませんでした。 「...

AIチャットボットがコロナウイルスによる人員不足の問題を緩和する方法

人工知能 (AI) の最も魅力的な利点の 1 つは、人々がより多くのタスクを達成できるように支援でき...

北京地下鉄は顔認識技術を使用して機密のセキュリティチェックを実施する予定

[[280913]] Jiwei.comニュース(文/Jimmy)によると、北京軌道交通指揮センター...

ドローンのパフォーマンスはどんどん標準化されつつありますが、この4つの点はまだ改善が必要です。

近年、飛行制御、ナビゲーション、通信などの技術が継続的に進歩し、私たちの生産や生活におけるドローンの...

ラスベガスの「チャイナナイト」:中国の人工知能が外国人に人生への疑問を抱かせ始める!

CES は世界最大かつ最も影響力のある消費者向け電子機器展示会です。米国時間1月8日、ラスベガスで...

金融業界がAI自動化を採用すべき理由

ガートナーによると、「ロボティック・プロセス・オートメーション(RPA)ソフトウェア市場は2020年...

...

銀行業界の「退化」の原因は人工知能なのか?

公開データによると、商業銀行は2021年も支店の閉鎖を続けた。11月までに商業銀行は2,100以上の...

デアデビルが来た!バットセンスAIは、スマートフォンが音を聞いて3D画像を生成できるようにする

英国の科学者たちは、スマートフォンやノートパソコンなどの日常的な物に、デアデビルと同じくらい強力なコ...

...

8 月の Github のトップ 10 ディープラーニング プロジェクト、あなたはどれを選びますか?

ビッグデータダイジェスト制作編集者: CoolBoyみなさん、こんにちは! 先月のトップ 10 の機...

...

一目でわかるアルゴリズム「配列と連結リスト」

データ構造はソフトウェア開発の最も基本的な部分であり、プログラミングの内部的な強さを反映しています。...

「コーチ」はとても優しくて合格率も高いです!上海に「無人運転訓練」が登場。試してみませんか?

運転免許試験を受けるとき、2番目の科目で行き詰まっていませんか?コーチに「支配される」ことへの恐怖は...