目的
Python で機械学習ッ!!! みたいなことをする時に,割と起こりがちな悲劇を防ぐためにやっておくことをメモしておく.
なおコードはPython3.7.1で実行確認したもの.2系を使用するならraw_inputとか適宜置き換えること.
保存
機械学習系で割とあるのは, データを読み込んで前処理してモデルを定義してパラメータを学習してそれを保存する,という流れ.
なので学習されたモデルはみんなどっかしらのディレクトリに必ず保存するけど,その時にいくつかやっておかないと再実験や見返す際に困ることがある.
結果の出力先ディレクトリ
名前は適宜困らない程度に分かりやすい名前をつける. 実行した日付と時刻やモデルの種類なんかをディレクトリ名にしてたこともあったけど(1人で開発するなら)個人の好みだと思う
絶対にやっておくことはコードを実行する際に最初に出力先予定のディレクトリが既に存在するかを確認すること.
「ふー,実験回し終わったわ〜〜過去のやつと比較するか〜〜!」と思った時に実は今の実験が過去の実験を全部上書きしてしまっていた,という悲劇や「2個同時に実験回して時間を節約ー!!」と思って回した実験が同じディレクトリに結果を書き出していて結局どっちのモデルかわからないという悲劇を見ることになる.見ました.
既にそのディレクトリが存在している場合,必ず実験を行わない,という風にしておきたければ
from pathlib import Path
def main():
out_dir = Path(path_to_output_directory)
out_dir.mkdir(exist_ok=False) # デフォルトでFalseだけど一応
のようなものを書いておけば良い.これだとmkdir
の時点でFileExistsError
が返ってくる.可読性が気になるなら
if out_dir.is_dir():
raise FileExistsError(f'{out_dir} already exists')
のようにハッキリと「出力先ディレクトリが存在していれば実行を許さない」ことをコードレベルで明記してもいい気がする.
どちらにしろexist_ok
はFalse
にしないとダメだけど.
ただこれだと「あ,たった今実行した実験,よく考えるとパラメータ間違ってたわやり直しやり直し」みたいな時に同じディレクトリを再使用できない(消せばいい,それはそう)
なので既にディレクトリが存在するなら本当にそのディレクトリを使用していいかを尋ねるという対策も個人的にはありだと思う.
def check_directory(out_dir: Path) -> bool:
if out_dir.is_dir():
ans = input(f'{out_dir} already exists. Do you continue? (overwrite all files) [y/n] >')
return ans == 'y'
else:
return True
ディレクトリが存在している場合,上書きして良いかを尋ねるコードである.ただし自分はアホなので何度も実験しているうちに手癖で「y」と入力してしまう.それを気にするなら
import random
def check_directory(out_dir: Path) -> bool:
if out_dir.is_dir():
random_code = ''.join(random.choices([str(i) for i in range(10)], k=3))
ans = input(f'{out_dir} already exists. Do you continue? (overwrite all files) [code: {random_code}] >')
return ans == random_code
else:
return True
のように毎回ランダムコードを尋ねれば良い.
[2018-12-30 追記]
先輩から別の提案を頂いたので忘れないうちに追記.
resultsディレクトリについて,実行時にランダムなsuffixをつけてからmkdir
を行うという方法.
これにより同じ名前のディレクトリを指定しても別のディレクトリとして利用することができる.
ランダムなsuffix,もしくは現在時刻を元にしたsuffixをつけたディレクトリ名を生成する一例.
import string
from datetime import datetime
def get_rnd_suffix() -> str:
return '_' + ''.join(random.choices(string.ascii_letters + string.digits, k=8))
def get_time_suffix() -> str:
return '_' + datetime.now().strftime("%Y-%m-%d_%H_%M_%S_%f")
def get_path(out: Path, suffix_type: str) -> Path:
suffix_func = {'random': get_rnd_suffix, 'time': get_time_suffix}
assert suffix_type in suffix_func
return Path(str(out) + suffix_func[suffix_type]())
ハイパーパラメータの保存
機械学習(に限った話ではないけど)ではよくハイパーパラメータなるものが出てくる.
Deep Learning だったら学習率だのバッチサイズだのあるし,SVMでもカーネルやマージンなどこれでもかってくらい色々出てくる.
で,大抵こういうのは引数で指定したり設定ファイルを作ってそこから読み出したりするけどうっかり保存しておくのを忘れると再実験できなくなる.モデルの出力先と同じディレクトリに全部保存しておくと良い.
import sys
def save_args(out_dir: Path):
with (out_dir / 'command.log').open('w') as f:
f.writelines(' '.join(sys.argv))
全引数を保存しておく.オシャレに整形して保存したい人は頑張っていじってほしい.
import shutil
def save_setting_file(setting_file_path: Path, out_dir: Path)
shutil.copy2(setting_file_path, out_dir)
単にコピーしているだけ.
ソースコードの保存
gitで管理してるから大丈夫だよ!は大丈夫じゃないので必ずそのモデルを作成したソースコードを保存する.これもモデルの出力先と同じディレクトリ(できればその子ディレクトリ)に保存する.
単純にsrc
ディレクトリを作っているならsrc
ごと保存するのが手っ取り早い.
とにかくこのモデルをもう一回つくり直せるという状態にしておくのが安全.
import subprocess
def save_src_files(src_dir: Path, out_dir: Path):
src_dir = src_dir.resolve()
out_dir = out_dir.resolve()
if str(src_dir) in str(out_dir):
raise RuntimeError(f'Omg, {out_dir} is in {src_dir} !')
subprocess.call(f'cp -r {src_dir} {out_dir}', shell=True)
結果の出力先ディレクトリをうっかりコピー対象のディレクトリ内に作るとループが発生するので確認しておく.どうせシェル実行時に途中で落ちるけど.os.path.commonpath
とかでオシャレに書いた方が分かりやすいかもしれない.
どうでもいいけどシンタックスハイライト,f-stringには対応してないみたいね.
結果の保存
特にDeep Learning系でよく聞くし自分もやった悲劇が「3日かけて学習を回して結果を保存しようとしたら失敗して落ちた」という話.こまめに保存しておけよという話でもあるがこれは本当に辛い.
対策は簡単で非常に小さなデータセットサイズにするなり,イテレーション数を調整するなりして学習を1,2分程度で終わるようにして最後まで実験を回すというもの.
このとき,適当なダミーデータを作って試す,という案もあるけど個人的にダミーデータは好きじゃない.というのもダミーデータでうまくいったけど実データ読み込み時に失敗したみたいなのは頻繁に起こるから.
その他
他に保存しておくと便利なものはまとめてReadMe.mdとかに書いておいてこれも対象のディレクトリに保存しておく.
保存しておいた方がいいなぁと思ったものを挙げておく.他にあったかなぁ.
- 実験の日付と時刻
- なんのために回した実験か
- どのパラメータに注目しているのか
データ読み込み
学習しているな(していないな),と思ったらそもそもデータが間違っていた.とてもつらいやつ.
データの順序
まじでうっかりミスりやすいやつ...
連番に振ってあるデータを順に読み出すかーと思って
image_paths = list(image_dir.glob('*.jpg'))
ってやって死ぬやつ. 実行時にどの順序でパスが取り出されるかは保証されないので順番が大切なら必ず読み出し順序を固定する.
単純にsorted
で
image_paths = sorted(list(image_dir.glob('*.jpg')))
としてもいいけど,これだと数字的な意味での連番みたいなのは守られないこともあるからkey
をlambda式
かなんかで指定しても良い.
なんにせよ一度printするなりして確認すること.
最も確実なのは読み込むパスをテキストに順に書き出してそれを利用する.ところでglob
ってファイル数が多いと話にならないけどfind
とかだと速いのかな?もっと速いのがあるのかな
目的とするデータを読み込めているか?
「このファイルパスのクラスIDは3です」とか「このファイルIDの正解値は23です」とか「このデータに写っているのは馬と牛です」みたいなメタデータの読み出し.ちゃんと正しく読み込めているか??
必ず適当にデータを10-100個くらいランダムサンプリングして確認する.なるべくならモデルに入れる直前の状態のデータを目で見てあっているかを確認する.めちゃくちゃ大事.
破壊操作 vs 非破壊操作 or 返り値がある vs ない
特にnumpyなんかを使っている時に起こりがち.
非破壊操作だと思って元の変数を使用していたら実は破壊操作をしていたなんてことや,破壊操作によって変数を変更する関数の返り値がNone
であることを忘れてa = hakai(b)
としてしまっていた...なんてことがある.
Manualを読んで返り値はcopyを返すのかviewを返すのかを確認するのはもちろん,手元で最小コードでその関数の挙動を確認することが大切.
単純な話,データセットローダーはtestスクリプトを用意すべきとも言える.
(TODO: データセットローダーのtestの書き方をまとめる)
実験条件の表示
各ハイパーパラメータや指定した実験条件や,出力先などは必ずprint
(pprint
)で表示する
重要なものなどは色付きで表示しておくのが望ましい.
これは,設定値にミスがあってもあとでログを見返せば気付けるが,学習時間が無駄になってしまうのでなるべく早く設定値ミス,引数ミスに気づかせるために行なっているのでコーディングは面倒かもしれないが絶対に丁寧に記述するべき
- 引数全て (列挙するだけでなく1行1行見やすい形に整形して表示する)
- configファイルに書いた設定値 (同上)
- データセットサイズ(
len(train_dataset)
) - 結果の最終的な出力先ディレクトリ
- 一つ目のデータの
shape
[2018-12-23]
なんか他にもあった気がするので随時更新していきたい...