機械学習で二度と同じ過ちを繰り返さないためのメモ

# 目的

Python で機械学習ッ!!! みたいなことをする時に，割と起こりがちな悲劇を防ぐためにやっておくことをメモしておく．
なおコードはPython3.7.1で実行確認したもの．2系を使用するならraw_inputとか適宜置き換えること．

# 保存

機械学習系で割とあるのは， データを読み込んで前処理してモデルを定義してパラメータを学習してそれを保存する，という流れ．
なので学習されたモデルはみんなどっかしらのディレクトリに必ず保存するけど，その時にいくつかやっておかないと再実験や見返す際に困ることがある．

## 結果の出力先ディレクトリ
名前は適宜困らない程度に分かりやすい名前をつける． 実行した日付と時刻やモデルの種類なんかをディレクトリ名にしてたこともあったけど（1人で開発するなら）個人の好みだと思う
絶対にやっておくことは**コードを実行する際に最初に出力先予定のディレクトリが既に存在するかを確認する**こと．

「ふー，実験回し終わったわ〜〜過去のやつと比較するか〜〜！」と思った時に実は今の実験が過去の実験を全部上書きしてしまっていた，という悲劇や「2個同時に実験回して時間を節約ー！！」と思って回した実験が同じディレクトリに結果を書き出していて結局どっちのモデルかわからないという悲劇を見ることになる．見ました．

既にそのディレクトリが存在している場合，必ず実験を行わない，という風にしておきたければ

```python:mkdirの引数を使ったディレクトリ確認
from pathlib import Path

def main():
    out_dir = Path(path_to_output_directory)
    out_dir.mkdir(exist_ok=False)  # デフォルトでFalseだけど一応
```

のようなものを書いておけば良い．これだと`mkdir`の時点で`FileExistsError`が返ってくる．可読性が気になるなら

```python:ファイルが存在してはいけないことを明記
if out_dir.is_dir():
    raise FileExistsError(f'{out_dir} already exists')
```

のようにハッキリと「出力先ディレクトリが存在していれば実行を許さない」ことをコードレベルで明記してもいい気がする．
どちらにしろ`exist_ok`は`False`にしないとダメだけど．

ただこれだと「あ，たった今実行した実験，よく考えるとパラメータ間違ってたわやり直しやり直し」みたいな時に同じディレクトリを再使用できない（消せばいい，それはそう）

なので**既にディレクトリが存在するなら本当にそのディレクトリを使用していいかを尋ねる**という対策も個人的にはありだと思う．

```python:ファイルが存在していた場合上書きして良いかをyes/noで尋ねる
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」と入力してしまう．それを気にするなら

```python:ファイルが存在していた場合上書きして良いかを乱数で尋ねる
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をつけたディレクトリ名を生成する一例．

```python: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でもカーネルやマージンなどこれでもかってくらい色々出てくる．

で，大抵こういうのは引数で指定したり設定ファイルを作ってそこから読み出したりするけどうっかり保存しておくのを忘れると再実験できなくなる．モデルの出力先と同じディレクトリに全部保存しておくと良い．

```python:引数を保存する
import sys

def save_args(out_dir: Path):
    with (out_dir / 'command.log').open('w') as f:
        f.writelines(' '.join(sys.argv))
```
全引数を保存しておく．オシャレに整形して保存したい人は頑張っていじってほしい．

```python:設定ファイルを保存する
import shutil

def save_setting_file(setting_file_path: Path, out_dir: Path)
    shutil.copy2(setting_file_path, out_dir)
```

単にコピーしているだけ．

## ソースコードの保存

gitで管理してるから大丈夫だよ！は大丈夫じゃないので必ず**そのモデルを作成したソースコード**を保存する．これもモデルの出力先と同じディレクトリ(できればその子ディレクトリ)に保存する．
単純に`src`ディレクトリを作っているなら`src`ごと保存するのが手っ取り早い．

とにかく**このモデルをもう一回つくり直せる**という状態にしておくのが安全．

```python:ディレクトリ丸ごと
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とかに書いておいてこれも対象のディレクトリに保存しておく．
保存しておいた方がいいなぁと思ったものを挙げておく．他にあったかなぁ．

- 実験の日付と時刻
- なんのために回した実験か
- どのパラメータに注目しているのか

# データ読み込み

学習しているな(していないな)，と思ったらそもそもデータが間違っていた．とてもつらいやつ．

## データの順序
まじでうっかりミスりやすいやつ...
連番に振ってあるデータを順に読み出すかーと思って

```python:データの読み出し
image_paths = list(image_dir.glob('*.jpg'))
```

ってやって死ぬやつ． 実行時にどの順序でパスが取り出されるかは保証されないので**順番が大切なら必ず読み出し順序を固定する**．
単純に`sorted`で

```python:データの読み出し順序固定
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]
なんか他にもあった気がするので随時更新していきたい...