機械学習の実験サイクルを２倍にする３つの要素と工夫たち

この記事は朝日新聞社 [Advent Calendar 2022](https://qiita.com/advent-calendar/2022/asahi)の第１４日目の記事です。
毎日、朝日新聞社のエンジニアがバラエティに富んだテック記事をお届けしております！

本日はメディア研究開発センターの松山が、機械学習の実験サイクルを早める工夫についてお届けします！

---

### 機械学習の実験は、時間がかかりますよね？
初めまして、９月から機械学習に本格入門した松山です。
機械学習を行う際、学習にかかる時間を有効活用できなかったり、コーディングに時間がかかってしまうといった悩みはありませんか？私はそうでした。
そんな私が先輩に相談しながら試行錯誤して辿り着いた、__２倍ほど実験のサイクルが早まった方法__ をお伝えします。

本文は、以下の人を対象とします。

- 機械学習の待ち時間を有効活用できていない
- 行き当たりばったりで学習のプログラムを書いている
- 実験が増えた際のファイル管理が困難になっている

これらの悩みを持つ人の役に立てれば幸いです。

## なぜ機械学習の実験は効率が下がるのか？
試行錯誤するうちに、実験効率が悪くなる理由として、待ち時間、ソースコード、知識量の３つに起因することがわかりました。
ボトルネックとなるこれら３つの要因を改善することで、実験効率を改善できます。

### 待ち時間の要因
機械学習は学習を回すのにどうしても時間がかかってしまいます。一回の学習に１０時間以上かかることもあり、その待ち時間があるため時間を余してしまいがちです。

### ソースコードの要因
機械学習のモデル作成においては、極論、結果となるモデルのみが重要であり、ソースコードは副産物です。同じ設定のコードを二度実行しても出てくるモデルは同じなので、製作したソースコードはそのままの形では二度と使いません。なので、意識しないとソースコードの品質を重視するという考えになりにくく、長期的には効率を落とす要因になります。

### 知識量の要因
精度向上に関する知識は膨大で、分散しています。機械学習の方法やテクニックは日々更新されており、その量は膨大です。有用な情報にアクセスできなければ、車輪の再発明的な実験をして、無駄に時間を使ってしまいます。

# 効率を下げるそれぞれの要因への対処法
では、それぞれの要因へ、どのような対処をしたかお伝えします。
## 待ち時間の要因
待ち時間に関しては、GPUを増やして計算速度を上げるという方法もありますが、今回は限られた計算容量を最大活用する方法について検討します。
#### なぜ待ち時間が効率を下げるか？
待ち時間に何もやることがないというのが実情ですが、その原因として、学習を回している間は完全に自由にはなれないというのがあります。
例えば、一つの学習が終わったら次の学習を動かしたいとすると、その間を監視しなければなりません。そうなると、別な作業に100%集中できなくて、効率も落ちてしまいます。


### 学習を自動で連続して実行するプログラムを組む
解決策として、バックグラウンドであらかじめスケジューリングした学習を次々と自動で行うという対処をしました。
そのために、シェルスクリプトを利用します。シェルスクリプトを利用すると、１つ目のpythonファイルが実行され終わると自動的に次のpythonファイルを実行することができます。これにより、一つずつ手動で実行する必要がなくなります。
例えば、土日に１０個の実験を仕込んでおいて、土日に遊んで、月曜日に結果を確認することもできます。

方法は簡単です。例えば、２つのpythonファイル「train1.py」と「train2.py」を連続して実行し、ログを「train_1_2.log」に記録する場合は、次のように実行します。

```bash
nohup sh -c 'python3 train1.py; python3 train2.py' > train_1_2.log &
```
この短いコードを実行するだけで、学習中は完全に頭を切り替えて、他のタスクに集中できるようになります。
これを実行すると、私は開放感に包まれます。
コード一行あたりの嬉しさで言えば、これを超えるものに出会ったことがありません。


## ソースコードの要因
次に、ソースコードの要因への対処です。
ソースコードに関しては、なるべくコードを書かずに実験することで、コードに起因する問題をそもそも起こさない、という考え方で対処しました。

コードを書かないためには２つの方策があります。
__1\. コードの行数を減らす__
__2\. コードを書かなくても実験できるようにする__

### コードの行数を減らす
まず、1のコードの行数を減らすための方針から始めます。

#### それはきっと必要にならない(YANGIの法則)を意識
YANGIの法則とは、コードは「多分必要になるだろう」「必要になるかもしれない」ではなく、本当に必要になったとき、必要なものだけを書く、というプログラミングの方針です。この方針を意識することで、意識するだけでコードがシンプルに保たれ、わかりやすくなります。

しかし、それに反して自然に任せると、行数が増えるごとにコードの複雑度が増していきます。そのため、実験を繰り返してコードを増やすたびに手元のコードベースは複雑になります。そして、いつか複雑さが認知力の限界を越えると、自分でさえ扱いきれなくなってしまいます。

例えば私の場合は、実験のプログラミングにあたって、いろいろな実験データのデータ形式に対応させたり、もしかしたら使うかもしれないパラメータを引数に入れてしまいがちでしたが、実際にそうした機能はほとんど使用していませんでした。その後、YANGIの法則を意識して、本当に必要なものをだけを書くことで、プログラムを単純化でき、多くの実験ができるようになりました。

コードを書かないことで、却ってより多くの実験をできるようになりました。

この辺りは、先輩にお薦めされて読んだ「[プリンシプルオブプログラミング](https://www.shuwasystem.co.jp/book/9784798046143.html)」に明るいです。お薦めです。

### コードを書かなくても実験できるようにする
次に、2の、コードを書かなくても実験できるようにする方法についてです。
こちらは二つあります。

__1\. ベストプラクティスを活用する__
__2\. 実験の設定を別ファイルで管理する__
__3\. プログラムと実験のディレクトリを分離する__

#### ベストプラクティスを活用する
コードを書かないためには、十分整備された先人たちの「ベストプラクティス」なコードを拝借する手があります。たいていの機械学習では、「データの前処理→学習→評価→モデルの保存」という順序でほぼ共通しています。そのため、このプログラムは世界中で何度も書かれており、ベストプラクティスと言えるものがあるはずです。特に初心者の場合は、自分で書くよりそっちを使う方が確実でしょう。

私の場合、はじめは自前のプログラムで学習させていましたが、無駄に複雑で、実験ごとの改修に時間がかかっていました。そこで、一度白紙にしてベストプラクティスをもとに組み直しました。その結果、実験ごとの改修箇所を最小限に抑えられる柔軟なプログラムを作ることができました。ベストプラクティスは、時間と共に多くの人の手によって磨き上げられたコードで、シンプルで柔軟性の高い構造になっています。複数の実験をすれば、じわじわとその効果が出てきます。

例えば自然言語処理であれば、以下のリポジトリがおすすめです。先輩に教えていただき、自分も使用しています。
https://github.com/huggingface/transformers/tree/main/examples/pytorch

#### 実験の設定を別ファイルで管理する
ハイパーパラメータなどの設定を変えて実験を繰り返す場合、pythonの実行ファイルに記述した設定値を変更しながら実行すると思います。
これには二点の問題があります。

まず一点目の問題は、再現性が失われることです。
実験の設定を変えるたびにパラメータが上書きされるため、「実験当時のプログラム」が消えてしまい、後から同じ状況を作れなくなります。その結果、結果の再現性が失われます。

二つ目の問題は、実験結果の管理が煩雑になることです。
実験の設定を変えるたびにパラメータが上書きされるため、実験ごとに変更した設定をメモなどに移し替えて、結果と一緒に保管する必要があります。これは手作業で、生産性が低下します。

上記の問題を解消するために、pythonの実行ファイルに設定を直接記述せず、外部に実験の設定を管理する必要があります。そのために、私はjsonを使った設定管理をお勧めいたします。

例えば、機械学習の設定を記述したjsonファイルを次のように作成します。
```json:setting1.json
{
    "model_name_or_path": "cl-tohoku/bert-base-japanese",
    "learning_rate": 2e-5,
}
```
これを引数に渡してpythonファイルに渡して、pythonの中で展開します。
```shell
python3 run.py setting1.json
```
こうすることで、パラメータをpythonプログラムから切り離して管理でき、実験の再現性を保証しつつ、管理も楽にできます。
これにより、jsonにパラメータの記録とプログラムの実行という２つの役割を共通に負わせることができます。記録と実行が同じファイルで完結するので、パラメータが複数の箇所で保管されるという状況が防がれます。結果として、実験の再現性を保証しつつ、管理も楽にできます。


この方法は、前章「学習を自動で連続して実行するプログラムを組む」で紹介した、バックグラウンドで学習を連続的に実行するスクリプトと組み合わせることで、より利便性を発揮します。例えば、「setting1.json」と「setting2.json」という２つの設定で連続して学習させたい場合は、次のようにして連続でプログラムを実行できます。
```bash
nohup sh -c 'python3 run.py setting1.json; python3 run.py setting2.json;' > setting1_2_3.log &
```
極論、１０個の設定でも自動で順番に実行できるので、一度設定してしまえば、寝て待つだけで比較実験が終わります。

さらに利便性を高めるために、jsonnetというjsonの進化形を利用する方法があります。実験の設定の一部を継承して新しい設定を作ることができるので、対照実験や性能比較の際に便利です。こちらについてはまた別な機会に紹介します。

### プログラムと実験のディレクトリを分離する
機械学習に用いるファイルは、性質の違う２つの種類に分けることができます。

１つ目は、実験に使用するpythonプログラムです。学習方法や評価手法など、データが通過するロジックが記述されたファイルが含まれます。
２つ目は、実験データです。学習データやハイパーパラメータ、出力されたモデル、評価指標が記録されたファイルなどが含まれます。

１つ目のプログラムは、データが乗っかる工場のレーンのようなものです。その上を、２つ目の学習データが通り、モデルなどが出力されます。これら２つの要素は相互に依存せず、排他的です（現実世界でも、工場から生産された製品がその工場を壊す、なんてことはありませんよね）。そのため、もともと別なディレクトリに保管すれば、分離を明確にできます。それにより、はっきりと分けることができ、無駄な複雑性を排除することができます。

例えば、私の場合は次のようなディレクトリ構造で実験を管理することで、２つを分離しています。
```
root
├ 実験データ
│ ├ 実験１
│ │ ├ 設定ファイル
│ │ │ ├ setting1.json
│ │ │ └ setting2.json
│ │ ├ 出力モデル
│ │ │ ├ model_setting1
│ │ │ └ model_setting2
│ │ ├ ログ
│ │ │ ├ setting1.log
│ │ │ └ setting2.log
│ │ └ 実験１のレポートや実行コマンド.md
│ └ 実験２
│ 　 └ ..
└ pythonプログラム
　 ├ run.py
　 └ ..
```

## 知識量の要因
知識不足によって、誤った方向性の実験をしたり、車輪の再発明をしてしまい、効率を落とすことへの対処についてです。
私はまだ機械学習の専門性が全然身に付いていないのですが、勉強するたびに問題に対する解決策のオプションが増えていくと感じています。従って、知識量が増えれば、目の前のタスクへ洗練されたアプローチができるようになり、車輪の再発明的な実験を減らしつつ、よりよい解決策に素早く辿り着けるはずです。
知識量を向上させる方法は私も模索中ですが、なるべく早く身につけるために、次の３つの工夫をしています。

__1\. 情報の質を上げる__
__2\. 情報の収集速度を上げる__
__3\. 発信する__

###  情報の質を上げる
目に入る情報の質が上がれば、正しい知識をより多く吸収できます。なので、質にこだわることは重要です。
私は、目に入る情報の質を上げるために、論文をキュレートしてくれるTwitterアカウントをフォローしたり、youtubeを見たりしています。また、私の所属するメディア研究開発センターの先輩方は、専門的な知識を持っており、質問や論文読み会を通して学ばせていただいています。（ありがたいことです..）

機械学習の勉強で感じたことですが、進化の早い分野では、序盤は闇雲に情報収集をするより、自分のレベルに適した有益な発信をしてくれる方を探す方が、長期的には多くのことを学べそうです。有益なTwitterやYoutubeをフォローすることで、自動的に良質な情報が入ってくるようになります。フォロー欄を洗練することで、情報収集を効率化できます。なので、有益な発信者を探すことは、効率的に情報を収集するための仕組みづくりとも言えそうです。

例えば、Twitterで特におすすめしたい方がいます。この方は機械学習のホットなトピックを毎週紹介しています。この方のツイートをチェックすることで、キャッチアップが早くなりました。
https://twitter.com/AlphaSignalAI/status/1602325052799827968?cxt=HHwWgIC-rayJzbwsAAAA
まだ深くないので、みなさんのおすすめも教えてください。

### 情報の収集速度を上げる

情報の収集速度を上げるために、論文を読むときは論文リーダーを使っています。また、英語をスムーズに読めれば速度も上がるので、勉強したり、お助けツールを入れたりしています。

論文を読むときは、[Explainpaperというリーダー](https://www.explainpaper.com/)を介して読んでいます。このツールに論文を読み込めば、ChatBotが論文についての質問に答えてくれます。ChatBotに質問すれば大体解決するので、論文でわからないことをググる、ということが減ります。
また、英語の翻訳にDeepLを使ってます。Google Chromeのエクステンションを使って、英語でのブラウジングの際に、わからない部分を翻訳してもらっています。

### 発信する
発信を前提として勉強することで、伝わるレベルに勉強しないといけなくなるので、理解が進みます。たとえ内容が稚拙だったりして、誰の役に立たなくても、少なくとも自分は成長します。誰かに伝わればラッキーだと思います。
また、発信すればそれが自分のドキュメントになるので、将来の自分を助けてくれます。
この考え方は、趣味の作曲での友人に教えてもらった考え方です。その方は発信することで圧倒的に成長して成果を出しています。

なので、メディア研究開発センターのslackに学んだことを発信しています。結果として、理解も深まり、たまに誰かの勉強になったりして、いい効果が得られていると感じます。

### とは言いつつ知識量に関しては...
知識量を増やすために、以上三点の方法を書きましたが、正直まだ確証の持てる方法論は見つけられておらず、模索しているところです。
みなさんのお気に入りの方法も教えていただけたら嬉しいです。

# まとめ
先輩に相談しながら試行錯誤した機械学習の実験効率を向上させるための方法を紹介しました。
効率の向上を妨げる要因として「待ち時間の要因」「ソースコードの要因」「知識量の要因」をあげ、それぞれに対する対処法を紹介しました。
どれも私が今使っている方法で、組み合わせて実践することで、本当にサイクルが速くなりました。
もし参考になる方法がございましたら、ぜひご活用ください！

(メディア研究開発センター・松山莞太)