機械学習の分野でコードを書くと、当初思った以上に入力データの数を増やして使うようになることが多い。
最初は書き捨てのコード
- アルゴリズムを作っていく初期の段階では、書き捨てのコードになるのは必然だ。
- いま注力しているのはそこじゃないからだ
- 着目すべきポイントは、それが目的の機能を果たしているかかどうかだ。
- Githubにある機械学習コードのサンプル実装のAPIが、シンプルであるのはそれが理由だ。
- クラスにして再利用性を良くすることよりも、単純であることを優先していることの帰結だ。
機械学習は、データの数が山ほど増える。
役割を全うしてくれないソースコード
- 壊れた画像ファイルが入力に加わると、その時点でエラーで死ぬ。
- リストの大きさが大きくなりすぎて、メモリーが足らなくなって死ぬ。
- 進行状況がわからないので、フリーズしているのか、動作しているのかがわからなくなる。
遅すぎて実用にならないソースコード
- GPUでの推論を1枚の画像に対して実行しており、GPUのバッチサイズをまったく使っていない。
- O(N^2)のロジックの処理を無駄に使ってしまう。
このことで、処理が無駄に遅くなる。 - set型を使うと充分なところにlist型を使って遅くしてしまう。
Pythonのlistとsetでの値の含み判定の所要時間について
マルチコアを使っていないことで無駄に遅くしてしまっているソースコード
ファイル単位の処理が独立しているときには、1つづつ、単一のスレッドの中で動作させるよりは、複数のスレッドにで並列処理したほうがいい。スレッドを2つにするだけで、2倍速くなる。
単一責任の原理を満たすコードに書き換える。
例:画像ファイルの読み込み
- 画像ファイルが壊れているかをチェックし、目的の画像だけ処理するためには、山ほどある imread() に対して
img = imread(image_path)
if img is None:
continue
とするのでは不十分だ。
やまほど、別な場所にimread() が潜んでいて、それが同様の問題を生じうる。
だから、画像ファイルが壊れていないことを確認して、次の処理に画像とそのファイルパスを与えるコードは、
目的の実装の中で1箇所にあって、それが全ての責任を果たすべきだ。これは単一責任の原理で解決されなきゃならない問題だ。
GPUでの推論時のバッチ入力
- 機械学習屋は、GPUによる推論のコードを多数書く。
- そのたび、1枚の画像ごとに推論しているコードを所定のバッチ数ごとに書き換えるコードに書き換えるのはめんどうだ。
- だから、所定のバッチ数ごとに処理できるように書き換える。
結果をlistにしない
- 多数の入力があって、その結果を返すような関数・メソッドがあると、結果をメモリ上に保持することになる。
- このことは、メモリを浪費して、異常終了するコードをになる。
- だから、結果をlistにしない。listにすると望ましくない結果: データの生成自体に時間がかかる場合、全体の計算が終わるまで次の処理に進めず、プログラムの初動(レスポンス)が著しく遅れます。イテレータであれば、準備ができた最初の1個目から即座に処理を開始できます
- 1. つど結果をyield する。そうすれば、処理が済んだ分だけ、次の工程が作業に着手できる。
- 2. 結果をcsvファイルに書き込んでゆき、メモリ上に保持しない。
- 結果をlistとして保持するのは、多数のデータがあるときにはしてはいけない。
安全なコードにすることを忘れない。
- 単純なコードと違って、高速化を意識した書き換えは、バグを埋め込みやすい。
- 例:
- 不十分なキャッシュ操作、過去のキャッシュが残って実行結果を汚染する可能性
- 所定のバッチ数ごとの処理の最後に、残った分を処理し忘れる。
- for item in イテレータ: の処理は、イテレータの中身を空っぽにしてしまうことを忘れてしまう。
テストコードを書こう
- 高速化を意識したコードには、必ず自動化テストを書く。
手順
- まずは、目的の役割を果たすコードを簡潔に書く。
- どういう状況で、そのコードが破綻するのかを考える。
- どういう書き換えをしたいのか、満たすべきことを明確にする。
- vscode のcopilotなどで、対応する書き換え版を書き加えさせる。
- テストコードを書き、単純なバグが入りにくい実装と高速の書き換え版とを比較させる。
- 従来の単純な実装には@deprecated 扱いに変更する。
- 利用する実装側が書き換わったら、@deprecated の実装を取り除く。
付記:
-「動くようにする、正しくする、速くする」というように、コードが価値を持つのは動くのが前提だ。最初の時点では、コードの単純さ、わかりやすさ、バグの入りにくさが重要になる。
- 初期の段階のコードのレビュー時点で重視しべきポイントは、そのコードが目的の機能を発揮できるかであって、コーディングの上の揚げ足をとることじゃない。
- 初期の段階で速くすることをめざすと、バグの温床になる。計算結果をキャッシュして再利用としようとする実装などはバグが入りやすい。
- 初期の段階で、クラス設計をすると、どういう使い方をするものなのかが明確になっていないので、公開するインターフェースと公開しないインターフェースの設計を間違える。
- C++はそういう意味で設計が難しい。C++ 自体のフレームワークの中にテストのフレームワークがなかったこともあって、テスト用のコードが、そのプライベートメソッドをテストするのがとても大変だった(今はどうなっているかな。)