こんにちは。
クリスマスイブですね。気づけばもうすぐ年の瀬ということで、今年一年頑張った研究活動の棚卸しの一環として、試行回数を増やすための具体的な方法論についてまとめていこうと思います。
この記事は機械学習アドベントカレンダー24日の記事です。
研究における試行回数を増やし無駄を減らす重要性
こちらの資料の冒頭で紹介されているように、研究で結果を出すためには、試行回数を増やす必要があります。
そして試行回数は
試行回数 = 手持ちの時間 / 1回の試行にかける時間
で定義されるとも書かれており、上記の資料は手持ちの時間は不変だから、1回の試行にかける時間を減らそうという発想の元、その減らし方について紹介されています。
しかし、実際には
試行回数 = (手持ちの時間 - 試行以外に掛けた時間) / 1回の施行にかける時間
ではないでしょうか?
この試行以外に掛けた時間というのは例えば、
- アイデア出しや他の論文のサーベイ
- 実験計画を考える
といった研究活動の本質に近い部分もあれば、
- 後輩や生徒の指導(プログラムに関する指導)
- 引き継いだ
クソコードを読む時間 - 実験の結果を図やグラフにしてまとめる時間
- バグによる実験のやり直しなどの出戻り
といった研究活動の本質では無い作業の時間も含まれるのではないでしょうか?
よって本来は
- こういった研究活動の本質から離れた作業の時間を減らし
- なおかつ1回あたりの試行にかける時間を減らす
ことが重要であるはずです。
しかし、このような内容というのは、そもそも研究活動に取り組める期間が短い学生や、期間があっても他業務で忙しい研究者にとって非常に大事な内容であるはずなのに、具体的にまとまった資料やそういった指導や教育の授業や時間というのはほとんど存在しません。
そことで今回は試行回数を増やすための具体的な方法論についてコーディングの面とツールの面で紹介していこうと思います。
執筆の際に気をつけたこと
本記事の執筆時においてはなるべく以下のことを意識して執筆しました。
-
オーバーエンジニアリングにならない
なるべく少ない労力で楽をできるように必要最小限の工夫になるようにポイントを絞りました。私達の目標はあくまで研究で成果を出すことであって、美しいコードやシステムを作ることではありません。 -
なるべく具体的な例をコードやスクリーンショットと共に紹介する。
なるべくわかりやすくなるように実際に私が使っている/書いたコードなどで実例を載せるようにしました。わかりにくい部分や改善点などはコメントや編集リクエストで知らせていただけると幸いです。 -
部分的に適用可能である
本記事で紹介する項目に関しては、個別に適用できるように配慮しました。
全てを完璧にする必要は全くありませんので、真似したいと感じた部分だけでも活かしていただけると幸いです。
もしも、少しでも役に立ったと思われた方はLGTM押していただけますと執筆の励みになりますのでよろしくおねがいします。
対象読者
- これから研究を始める学生
- 研究を効率化したい人
- 実験をたくさん回す人
今回紹介する方法は現在私が大学院の修士課程で半年間研究を行ってきて、試行錯誤しながら生み出した方法や実際に使っているツールになります。後1年以上は研究活動をする予定なので、この記事もなるべく随時アップデートしていきます。
(ツール編においてPytorch LightningとVScodeの部分を加筆する予定です。またgithubにコピペするだけですぐに、今回紹介する処方箋を実践することができるテンプレートも用意する予定ですので、しばらくお待ち下さい。)
コーディング編
debug_mode
を作ろう。
上記の資料でも言及されていますが、試行回数を増やすためにはまず最初に小さなデータセット、単純なモデル、少ないエポックでちゃんと一通りプログラムが動くかどうかを確認することが非常に大事になります。
そのときにオススメなのが、debug_mode
です。
私のプログラムではよく、
python main.py debug_mode=True
とすることでデバッグモードで起動し、実際のデータセットよりも少ないデータセット、少ないエポックでコードを動かして、デバッグをすることができます。
実際のデータをロードするコードの中では、このようにデバッグモードで少ないデータセットでデータをロードできるようにしています。
普段からdebug_mode
を作ること意識しながらコーディングできると後の検証が非常に楽になるのでおすすめです。
def load_file_names(
data_dir: Path, debug_mode: bool
) -> tuple[list[str], list[str], list[str]]:
"""load feature file names.
return train, val, test filenames.
"""
train_file_names = [file_name(case_id) for case_id in load_case_ids(mode="train")]
test_file_names = [file_name(case_id) for case_id in load_case_ids(mode="test")]
val_file_names = [file_name(case_id) for case_id in load_case_ids(mode="val")]
if debug_mode:
train_file_names[:6], val_file_names[:2], test_file_names[:2]
return train_file_names, val_file_names, test_file_names
値エンティティやEnumを使おう
他人のコードを読んでいてあるあるだと思うのですが、意味のある同じ文字列が何回もプログラムに登場してしまうと以下のデメリットがあります。
- 似た名前の場合ミスに気づかない
- 変更することが大変
なので意味のある文字列などは値エンティティとして切り出したり、Enumを使ってあげましょう。例としては以下のようになります。
"""How camera controls work
ref: https://plotly.com/python/3d-camera-controls/
The camera position and direction is determined by three vectors: up, center, eye. Their coordinates refer to the 3-d domain, i.e., (0, 0, 0) is always the center of the domain, no matter data values. The eye vector determines the position of the camera. The default is $(x=1.25, y=1.25, z=1.25)$.
The up vector determines the up direction on the page. The default is $(x=0, y=0, z=1)$, that is, the z-axis points up.
The projection of the center point lies at the center of the view. By default it is $(x=0, y=0, z=0)$.
"""
from enum import Enum
class PredefinedCameraParameters(Enum):
"""In medical field there are some patterns in camera parameters.
ref:https://mrsenpai.com/?p=82
"""
axial = dict(eye=dict(x=0.0, y=0.0, z=2.5), up=dict(x=0, y=1, z=0))
coronal = dict(eye=dict(x=0.0, y=2.5, z=0.0))
sagittal = dict(eye=dict(x=2.5, y=0.0, z=0.0))
私は医療分野におけるAIに関して研究を行っているので、可視化の際にも医療分野における方法論に従って可視化を行う必要があります。可視化の向きはaxial
, coronal
, sagital
と行った3方向があり、それぞれの向きは....
といった内容が上のコードを読むだけで理解できてしまいます。
このようなエンティティを用意してあげることで、型ヒント同様、エディタの保管機能の支援を受けることができるので打ち間違いをなくすことができます。
またエンティティそれぞれにコメントを付けることができるので、後から自分がコードを読んだときに意味を理解しやすくなります。
このように文字列のハードコーディングをなくすで、ミスを減らして自分のコードに動作に自信を持つことができるようになります。また可読性も向上します。
なるべくコメントと型ヒントをつけよう。
これは有名な話ですが、3ヶ月後の自分は赤の他人ですので、将来の自分のためにもコードの可読性を上げておくことは大事です。
例えば、以下のコードはどうでしょうか?
def calculate_feature(input):
x, y, z = input.T[:,:3]
...
あなたはこの関数名を呼んで意味を理解できるでしょうか?
おそらく「なんだ、このfeature」ってとなると思います。しかし、研究を進め実際にコードを書いているときは、自分の世界に入ってしまっていて、このようなコードも意味がわかるように感じてしまうことはよくあることだと思います。
では以下のコードではどうでしょうか?
def calculate_curvature_feature(input: np.ndarray) -> np.ndarray:
""" This function calculates curvature feature of 3d object.
curvanture -> ref: http://ryutai.ninja-web.net/mesh/mesh_2.html
Args:
input (np.ndarray): [N x 3] N is the number of vertices of mesh. [x, y, z] order.
Returns:
np.ndarray: [N x 3] curvanture vector.
"""
x, y, z = input.T[:,:3]
...
関数名が変わったことで、このプログラムは曲率を計算するプログラムだということがすぐに分かるでしょうし、docstring
でnp.ndarray
の詳細な形まで指定しているのでこの関数にどのような配列を入力してあげたらいいのかも一目瞭然です。
研究でコードを書いている際は、これぐらいわかるだろうと考えがちですが、3ヶ月後下手すれば1週間後の自分は赤の他人ですから、その時の自分を苦しめないように普段からコメントを書く癖をつけておきましょう。
また型ヒントを書いたことにより、エディタのインテリセンスによる補完の支援を受けることができます!このことにはたくさんのメリットがあります。
- プログラムにタイポなどのバグが入り込むことを防いでくれる。
- 実行前にバグに気づくことができる。
- エディタが補完してくれるので、コーディング速度が上がる。
- 定義ジャンプを使うことができるので、すぐに定義を参照することができる。
できる部分からで構いませんので少しずつ型ヒントをつけていきましょう!
以上2つはコーディングそのものに関することでしたが、他にも研究のコードの可読性を上げてくれる方法論はたくさん存在します。
以下のリーダブルコードは本当に実践的な例を挙げてくれているのでとても読みやすく、即実践することができると思います。
些細な、本当にちょっとした違和感を大事にしよう
実験をしていると些細な違和感に出会うことがあります。些細な違和感とは、可視化やログなどを見たときに、大きな問題じゃないが想定していた挙動と少し異なる部分があると行ったところです。
こういった違和感は大抵の場合、後々大きな問題として自分に降り掛かってきたり、その違和感が新たな知見につながる大変重要なものです。
しかし、締切に追われていたり、どうにかして結果を早く得たいと思うあまり、こういった違和感はよく見逃されがちです。
こうした違和感を逃さないためにも、普段から無駄を減らしていき、研究活動の本質に時間を割き、研究という営みを楽しみましょう。
可視化とログの記録は自動化しよう
小さな違和感に気づくためにも結果を確認できるように記録すること、可視化することは極めて重要です。
また、発表や論文発表間際になって、結果の可視化ができておらず、泣く泣く可視化のスクリプトを書いて、再実験を行う羽目になっては時間の浪費が甚だしいです。
データの可視化は研究分野においてデファクトスタンダードが存在したり、アンチパターンが存在したりするので、先行研究を読む際には必ず、どのように可視化を行っているかを確認するようにクセづけましょう。
最近はビッグデータが話題になることが多く、tableauのような素晴らしいプロダクトも存在するのでデータの可視化自体で一つの専門分野になりえます。
Amazonで検索するだけでも、多数の本がヒットすることからこの領域の注目度が図りしれます。アカデミアを卒業して、ビジネスの世界に行くときにも必ず役に立つスキルの一つだと思うので、先に勉強しておきましょう。
また、教授や先輩がいる場合はどのような可視化方法が良いのかのアドバイスを求めることで、無駄を減らすことができます。
出力のディレクトリの設計
研究におけるコーディングにおいて、あまりコーディングの設計に頭を使うことは良くないと思いますが、出力のディレクトリに関しては、最初に設計を頑張っておくことをおすすめします。
その理由としては
- 出力のディレクトリは後で思いついて変更すると新旧のディレクトリ構造が交わってしまい、非常に見づらくなる。
- そして旧のディレクトリ構造から新のディレクトリ構造への移行には時間がかかる。(うまくlinuxコマンドの
mv
一つで移行できたとしても、データ数が増えれば遅くなるし、TBオーダー以上のデータになるとそもそも移行できないことも(あるらしい)。移行のスクリプトを書くのも手間) - ディレクトリの切り方を間違えると、不必要なデータを削除する労力が大きくなる。
- (ディレクトリをうまく切れているとDropBoxなどのデータのバックアップが楽)
ディレクトリの設計に完全な正解は無いと思いますが、以下に示すディレクトリの切り方に従うと大きな間違いはなくなると思います。
- 不必要なデータか必要なデータかでまず分ける。
- その次の階層では、処理ごとに分ける(データ生成、前処理、モデルの学習、後処理)
- その次の階層では、自分の研究テーマが何の研究テーマで分ける。
例えばoutputディレクトリに出力を全部貯めていくとすると、以下のようなディレクトリ構造が良いです。
output
├── debug_mode
│ ├── preprocess
│ │ ├── preprocess_A
│ │ │ └── 20221220T031545-6ce16bc5
...
│ └── train_model
│ ├── model_A
│ │ └── 20221221T111235-38dad7b3
...
└── main
├── preprocess
│ ├── preprocess_A
│ │ └── 20221108T031235-3c98a0fc
...
└── train_model
├── model_A
│ └── 20221220T042447-2e585762
...
このようにしておけば、
- debug_mode 配下のデータは不要になればすぐに消せる。
- main配下のディレクトリをDropboxと同期することで簡単にディレクトリを冗長化することができる。
といった恩恵を受けることができます。
え、なになに?
「出力ディレクトリを考えて切るメリットはわかったけど、実際にやるのは難しくて、めんどくさいよ」って?
大丈夫です。
本記事後半のツール編で紹介しているHydraというツールとサンプルコードを組み合わせるだけで、あなたはほとんどコードを書かずにメリットだけを享受できます!
不要なファイルやスクリプトは自信を持ってすぐ消せる状態にしておく
これは先程の出力ディレクトリの設計の話に通じるのですが、研究を行っていると必ず、方針の転換などにより不要な結果やスクリプトが出てきます。
このような不要なゴミが出てきたときに、整理をせずに新たなコードや結果を重ねてしまうことは、後々の生産性を非常に下げてしまいます。
こちらの記事でも示されていますが、一般的なサラリーマンが探しものに費やす時間は年間150時間に上るそうです。このことからも結果を探したりコードを探す時間というのも同様にかかっていそうだということがおわかりいただけると思います。
では、この無駄な時間を極力なくすためにはどうしたらいいか。
それには、不要なファイルやスクリプトは自信を持ってすぐ消せる状態にしておくことが大事です。
ここで消すことが大事なのではなく、すぐ消せる状態にしておくことが大事だということを理解していただきたいです。
すぐに消せるということはどういうことかと言うと、純粋にコマンド一つで消せることを意味します。先程のディレクトリ構成であればdebug_mode
のディレクトリを指定するだけで簡単に消すことができます。
またコーディングであれば後述のgit
を使うことで不要なファイルを(復元可能な状態で)簡単に消すことができます。
研究においては過去に行ってきた取り組みやその結果というのは資産になります。
なのでそれを消してしまうことは資産を捨てる行為に等しいと言えます。
ですが、実験におけるファイルやスクリプトは資産であると同時に負債でもあるのです。
どういった点で負債かといいますと
- ファイルやスクリプトなど量が増えるとディスク容量を圧迫する。
- ファイルやスクリプトなど量が増えると探す時間が増える。
- ファイルやスクリプトなど量が増えると移動や修正が大変になる。
という点です。
よって、負債の面を最小限に抑えつつ、資産の面を享受するためにも
不要なファイルやスクリプトは自信を持ってすぐ消せる状態にしておくことが重要なのです。
動作が怪しいコードはテストを書こう
研究のコードは研究が進むにつれてどんどん膨大になっていきます。
そして、コードが増えるほど処理も増えていきます。
その中で特に計算に関わる部分に関しては比較的簡単にテストが可能なものが多いので単体テストを書くことをおすすめします。
単体テストとは関数やメソッドが正しく動作しているかを確認するためのテストです。
例えばアフィン変換のパラメーターから実際にアフィン変換行列を作成する以下のコードがあるとします。
class AffineBase(nn.Module):
"""ref: https://imagingsolution.net/math/rotation-scaling-translation-3d-matrix"""
num_vertices: int
def __init__(self, num_vertices: int) -> None:
super().__init__()
self.num_vertices = num_vertices
@jit
def forward(self, x: torch.Tensor, affine_x: torch.Tensor) -> torch.Tensor:
"""
Args:
x (torch.Tensor): [B*num_vertices x num_feature in 1 vertex] size tensor
affine_x (torch.Tensor): [5 * batch_size x 3] size tensor
[
[theta_x, theta_y, theta_z],
[scaling_x, scaling_y, scaling_z],
[translation_x, translation_y, translation_z],
[shear_yx, shear_zx, shear_zy],
[shear_xy, shear_xz, shear_yz],
]
Returns:
torch.Tensor: affine transformed tensor
"""
transformed_vertex_positions: list[torch.Tensor] = []
batch_size: int = int(len(x) / self.num_vertices)
ones: torch.Tensor = torch.ones((len(x), 1))
x_homogeneous: torch.Tensor = torch.cat([x[:, :3], ones], dim=1)
for batch_idx in range(batch_size):
affine_matrix = self.extract_affine_matrix(
affine_x=affine_x[batch_idx * 5 : (batch_idx + 1) * 5]
)
for vertex_idx in range(self.num_vertices):
tensor_idx = batch_idx * self.num_vertices + vertex_idx
vertex_position_homogeneous_tensor = x_homogeneous[tensor_idx]
transformed_vertex_positions.append(
torch.mv(affine_matrix, vec=vertex_position_homogeneous_tensor)
)
return torch.vstack(transformed_vertex_positions)[:, :3]
def extract_affine_matrix(self, affine_x: torch.Tensor) -> torch.Tensor:
raise NotImplementedError("You need to Implement")
class XRotAffine(AffineBase):
@jit
def extract_affine_matrix(self, affine_x: torch.Tensor) -> torch.Tensor:
theta_x: torch.Tensor = affine_x[0, 0]
sin = torch.sin(theta_x)
cos = torch.cos(theta_x)
affine_matrix = torch.eye(4)
affine_matrix[1, 1] = cos
affine_matrix[2, 2] = cos
affine_matrix[1, 2] = sin * (-1)
affine_matrix[2, 1] = sin
return affine_matrix
このコードをテストする基本的なユニットテストは以下のようになります。
def test_x_rot() -> None:
x_rot_affine = XRotAffine(num_vertices=4)
mesh = dummy_mesh_generate()
x90_rot_affine_matrix = torch.Tensor(
[
[torch.pi / 2, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
]
)
transformed_mesh = x_rot_affine.forward(mesh, affine_x=x90_rot_affine_matrix)
expect = torch.Tensor(
[
[0, 0, 0],
[1, 0, 0],
[0, 0, 1], # y -> z
[0, -1, 0], # z -> -y
]
).to(dtype=transformed_mesh.dtype)
torch.testing.assert_close(transformed_mesh, expect)
ソフトウェア開発としては本当は様々なテストケースを試すべきですが、完全100%を目指す必要は研究においてはそこまで無いのかなと感じています。些細な部分に関してはなるべく労力は少なくしていきたいですし、このテストケースだけでもある程度プログラムの動作に自信を持つことができます。
また、単体テストというのは関数を最小単位で実行していることになるので、もし間違えていた場合もすぐにバグがあることがわかるのです。
単体テストを書かなければ、例えば水増しのコードであれば実際に学習を回してみないと動作が正しいのかを確認することはできないでしょう。もっと最悪なパターンとしては、一通り実験が終わって論文をまとめるときになって、バグが見つかり、もう一度実験をやり直すパターンです。
それらに比べれば単体テストを書くことがどれだけコスパがいいことかおわかりいただけると思います。
簡単に高速化できる処理は高速化しよう。
高速化と聞くと高度すぎて、私には無理だと思われるかもしれませんが、案外簡単にできてしまう部分もあります。注意する部分としては、
- 順序が関係なくそれぞれの処理が独立して行えるもの(データの水増しや前処理などが最たるもの)は簡単に並列化できる。
- キャッシュはあまり使わない
並列化に関して言えばjoblib
というライブラリが最も使いやすいですし、キャッシュであればlru_cache
デコレーターを関数に付けるだけで結果をキャッシュしてくれて計算時間を大幅に削減してくれます。
ただ、キャッシュに関してはその関数が本当にキャッシュしてよいのかということを、実装の際には毎回考える必要があります。
本当は再計算をしないといけない場合にキャッシュを使ってしまうと正しい結果を得ることができません。
処理がキャッシュできることに自信が持てないようであればキャッシュは使わないことをおすすめします。
共通化をしない
ある程度コーディングに慣れてくると、同じ処理を共通化してシュッとしたスリムなコードを書きたくなるかもしれません。そんな時期が私にもありました。
具体例として、前処理Aというプログラムを書くことを考えましょう。
あなたはすでに前処理Aを実装しているとします。
続いて似た前処理Bのコードを別の部分でもう一度書く必要が出てきたとき、あなたはこう思うでしょう。
「あれ、前処理Bも前処理Aと共通化できるやん!」
そうしてコードを修正し前処理A'を作成し、あなたはこう思うわけです。
「うぉ!めっちゃコードスッキリしたわ。俺天才やわ」
しかし、数日後、新たな特徴量を追加する必要が出てきたとします。
その特徴量を追加するためには前処理Cを実装する必要があります。
「あーなるほどね。前共通化した前処理A'があるから、その処理を呼び出したら完了やわ... あれ、この特徴量って、今までとは違って引数に別の情報も追加で入れなあかんやん...! うーん、ちょっと汚いけど、デフォルト引数とか使って、前処理A'の中で条件分岐して、ちょっと無理やりやけど、前処理CもA'に入れてまおう。」
そうして数ヶ月が経った後、あなたの前処理コードは多数の条件分岐と複雑な引数にまみれた奇々怪々なプログラムへと進化してしまうのでした。
こうしたことは特に研究を行っている人にはよくあるのではないでしょうか?
例えば前処理は、セグメンテーションのタスクにおいて、入力画像に何らかの処理をして、処理された画像を返却する処理だとしましょう。
def preprocessA(image: np.ndarray) -> np.ndarray:
...
しかし後から、新たな特徴量として、セグメンテーションのラベルも一緒に使わないと行けない前処理が出てきたとします。このとき、
def preprocessA(image: np.ndarray, label: Optional[np.ndarray]) -> np.ndarray:
if label is None:
# もともとpreprocessAで行っていた作業
else:
# 今回新規で実装する作業
みたいな形です。
カオスを避ける方法
こうしたカオスを避ける方法の中で私がおすすめできる方法は2つあります。
- 徹底的に重複を許す。実験ごとに前処理から評価まで実験ごとに丸コピする。
- メソッドやクラスを分解して単一責務にする。
徹底的に重複を許す
実験ごとに1から最後まで徹底的にスクリプトファイルをコピー&ペーストするパターン。
一見ヤバすぎるパターンですが、実は状況によってはかなり妥当な選択肢になりそうです。その状況とは
- 期間が短期間である(全体を自分ひとりで把握できる)
- 比較的コードが短い(全体を自分ひとりで把握できる)
- 他の人に再利用される可能性が低い。
という状況が揃ったときです。
Kaggleや学部の研究であればこの条件に当てはまることが多いでしょう。
実際にKagglerの方のZennでもこちらの管理方法を使用されていました。
https://zenn.dev/fkubota/articles/f7efe69fd2044d
この方法であれば実験条件の比較がかなりしやすくなります。
短期的にたくさん実験やアイデアを試したい場合、例えば実験1と実験2の差分において複数条件の変更が含まれることがあります。
こういう場合では、実験ごとにまるっと比較をすることができるので、むしろ効率がよく妥当だと思われます。
一方デメリットとしては、
- 明らかにコードの重複が多く、他人が読んで理解できるかは怪しい。
- 途中からコピペが面倒くさくなって、コピペをサボり破綻する。
などが挙げられます。
ここは本人の意識や性格によっても変わってきそうです。
単一責務を徹底する
共通化をあきらめたくない場合はこちらの方向になります。
そもそも共通化を志してカオスになる1番の理由は関数自体が複数責務を負っており、複数の関数内で共通の責務が発生するからです。
しかし、理想的には1つの関数やメソッドは1つの責務しか負わないので、このような自体は発生しないはずです。後から共通化したくならないように、普段から単一責務の原則を守りましょう。
単一責務に関しては以下の記事がわかりやすいです。
...って言っても結構難しいです。これ。
難しい点は2点あります。
- どこまでを単一原則としてみなすか
- 単一原則に従って作成された関数をどのディレクトリ構造のどこに入れるのか
正直言って、現段階で私もこれらの課題に対する具体的な処方箋は考えられていません。身もふたもないですが、経験をするしか無いのかもしれません。
ただ、何も考えずに書き出すよりは、自分と類似する研究の中でgithubでコードが公開されているものをなるべくたくさん見て、その中で自分のわかりやすかったもの、そのドメインのデファクトスタンダードなディレクトリ構造やファイル名、関数の切り方などを学ぶほうが断然効率も可読性も上がります。
(年内には間に合わないですが、githubにテンプレートを作って公開しようと思います。)
ツール編
ここからは試行回数を増やし無駄を減らしてくれるツールについてご紹介します。
git
gitはプログラムの変更を管理するためのバージョン管理ツールです。
https://backlog.com/ja/git-tutorial/
gitを使うことで、プログラムを管理することができるので、以下のようなファイルの管理が破綻してしまう自体を避けることができます。(画像はサル先生のGit入門から転載)
githubはgitと連携することでオンラインでコード管理をすることができるツールです。
以上は冒頭に紹介した試行回数の増やし方2021verにも記載されています。
ここでは更に具体的な使い方に踏み込んで行こうと思います。
(以下ではgitにおける、commitとbranchがどういったものかを理解している前提でお話します。)
commit hashを実験番号として使う
すべてのcommitには、一意にhashというidみたいなものが割り当てられます。
このhashを使うことで、1年前でも2年前でも一瞬でそのhashのcommit時点のプログラムに戻すことが可能になります。
そして、これを実験番号として使うことで、実験の追跡性を上げることができます。
つまり研究室会やディスカッションで「このときの実験ってどういうプログラム(処理)だった?」という質問をされたときに、commit hashがわかれば実際のコードをみて確認をすることができます。
commitを簡単な実験ノートとして使う
各commitには以下に示すようにコミットメッセージと呼ばれる簡単な説明を付けることができます。
これをつけておくことで、後で「あれ、この実験ってどういうコードで行ったっけ?」となったときに見返すことができますし、「このコードってなんでこういうコードになっているんだけ」というときにもcommitメッセージを見返すことで理解することができるようになります。
粒度の小さいコミットという幻想
上のコミットを簡易な実験ノートとして使ったり実験IDとしてつかうためには、まとまった変更の都度、ある程度細かくコミットをする(=コミットの粒度を小さくする)必要があります。少なくとも実験を回す前にはcommitをしておく必要があります。
しかし、すでにgitをお使いの方はお気づきかもしれませんが、この細かくcommitをするということが意外と難しいのです。
普通のエンジニアリングならまだしも、研究においてはコードを書いている最中に、「あれも試したい、こういう場合にはどういう結果になるんだろう、ここもっとこうした方が良さそうだな...」と行った有象無象のアイデアが頭をめぐります。
なので、気づいたときにはファイルの差分が10以上になっていることも珍しくありません。
こうなってしまうとある実験をしたときのコードはどんなコードだったのかを後から追跡することが不可能になってしまい、実験の再現性を取ることができません。
この問題を解決してくれるのが、以下のHydraというツールです。
Hydra
Hydraは本来はパラメーター管理のツールです。
以下のようなyamlファイルを書いてあげることでそれらを簡単にプログラムに渡すことができます。
hydra:
run:
dir: /work/artifact/${branch_name}/mesh_vae/${model_name}/${feature_name}/${target_organ}/${now:%Y%m%d}T${now:%H%M%S}-${experiment_id}-${commit_hash}
sweep:
dir: /work/artifact/${branch_name}/mesh_vae/${model_name}/${feature_name}/${target_organ}/${now:%Y%m%d}T${now:%H%M%S}-${experiment_id}-${commit_hash}
# common
random_seed: 42
commit_hash: "${commit_hash: ${debug_mode}}"
# hyper-parameter
sigma_max: 2.0
lr: 0.01
max_epochs: 1000
augmentation:
moving_ratio: 0.000
# debugging
debug_mode: false
Hydraの設定で使うyamlファイルでは${hoge}
というふうな変数の埋め込みを使うことができます。
これにより条件を変えて実験を行う際にもyamlファイルの変更を最小限度に抑えることができ、ミスが減ります。
また、Hydraは自動でプログラムの出力ファイルを作成してくれます。
https://hydra.cc/docs/1.1/configure_hydra/workdir/#internaldocs-banner
上記の例で上げたyamlファイルの一番上の行の
hydra:
run:
dir: /work/artifact/${branch_name}/mesh_vae/${model_name}/${feature_name}/${target_organ}/${now:%Y%m%d}T${now:%H%M%S}-${experiment_id}-${commit_hash}
sweep:
dir: /work/artifact/${branch_name}/mesh_vae/${model_name}/${feature_name}/${target_organ}/${now:%Y%m%d}T${now:%H%M%S}-${experiment_id}-${commit_hash}
という部分に設定が書かれています。
これはモデルを学習させる実験でモデルの重みを保存したり、途中結果や画像を出力したりといった実験で頻出の部分を作るときに威力を発揮してくれます。
例えば皆さんは以下のようなコードを書くことはありませんか?
plt.savefig('sample_predict_result.png')
しかしこれでは毎回実験を行うごとにsample_predict_result.png
は上書きされてしまいます。
なので皆さんの中には以下のようにモデルの名前やメモや日付などを入れて、なんとか上書きされないようにといった涙ぐましい努力をしている方もいらっしゃるかもしれません。
plt.savefig(f"{today}_{model_name}_sample_predict_result.png")
一方Hydraを使うことでこのような悩みは解消されます。
Hydraは以下のように実験ごとに自動的に出力用のディレクトリを作成してくれるのです。
そしてHydraにはもう一つプラグインという強力な機能が存在します。
自分でプラグインを書くことでconfigファイルで使える変数を自作することが可能になります。
具体例をお見せします。
src
├── hydra_plugins
│ ├── git_branch.py # プラグイン用のスクリプト
│ └── commit_hash.py # プラグイン用のスクリプト
└── train.py # 実際に学習を回すためのスクリプト
以上のようにhydra_plugins
ディレクトリをスクリプトのあるディレクトリに配置してあげて、その中にプラグイン用のスクリプトを書いてあげます。
# commit_hash.py
import git
from pathlib import Path
from omegaconf import OmegaConf
class UnCommitError(Exception):
pass
class UnStagedError(Exception):
pass
def provide_commit_id(is_debug_mode: bool) -> str:
if is_debug_mode:
return "debug_mode"
repo = git.Repo(Path("ここはgit管理されているディレクトリを指定"))
if repo.untracked_files:
raise UnCommitError("You have to commit before you start to experiment.")
if repo.git.diff():
raise UnStagedError(
"You have to add and commit all changes before you start to experiment."
)
sha = repo.head.commit.hexsha
return sha[:8]
OmegaConf.register_new_resolver("commit_hash", provide_commit_id)
# git_branch.py
import git
from pathlib import Path
from omegaconf import OmegaConf
class UnCommitError(Exception):
pass
class UnStagedError(Exception):
pass
def provide_branch_name(is_debug_mode: bool) -> str:
if is_debug_mode:
return "debug_mode"
repo = git.Repo(Path("/work"))
if repo.untracked_files:
raise UnCommitError("You have to commit before you start to experiment.")
if repo.git.diff():
raise UnStagedError(
"You have to add and commit all changes before you start to experiment."
)
branch = repo.active_branch
return branch.name
OmegaConf.register_new_resolver("branch_name", provide_branch_name)
OmegaConf
というhydraの元になっているライブラリにcommit_hash
という名前のリゾルバを定義してあげることでyamlファイルの中で${commit_hash: ${debug_mode}}
というふうにコミットハッシュを呼び出してあげることが可能になりました。
またこのプラグインの強力なところは、上のコードでも分かる通り、もしもコミットしていないファイルが存在した場合、例外を呼ぶので実験スクリプトを回すことができない点にあります(debug_modeが有効化されている場合はcommit_hash
はdebug_mode
という値になり、コミットしていない差分が存在しても実行可能になります。)
こうすることで、嫌でも実験を回すためにはcommitをしないといけなくなるので、先程ご説明した、
- commit_hashを実験IDとして使う
- commit_hashを簡易な実験ノートとして使う
ことが実現できます。
また、今上で紹介したスクリプトと設定ファイルをコピペして使えば、自動的にコーディング編でお伝えした、ディレクトリ構成が作成されます!
Pytorch Lightning
ごめんなさい。記事の執筆が間に合いませんでした。
また来年加筆します。
大体以下のスライドに書いてあることと一緒です。
Wandb
ごめんなさい。記事の執筆が間に合いませんでした。
また来年加筆します。
VScode
拡張機能
このセクションでは私が使っているオススメの拡張機能を紹介するつもりでした。
ごめんなさい。記事の執筆が間に合いませんでした。
また来年加筆します。
デバッガ
このセクションでは超絶便利なVScodeのデバッガについて、使い方と設定ファイルの紹介をするつもりでした。
ごめんなさい。記事の執筆が間に合いませんでした。
また来年加筆します。
以上になりますがいかがだったでしょうか?
どのTIPSも今日から実践していけるものばかりです。簡単にできるものから少しずつ実践して研究の生産性を高めていきましょう。
もしも、少しでも役に立ったと思われた方はLGTM押していただけますと執筆の励みになりますのでよろしくおねがいします。
では良いお年を。