Kesinの知見置き場

知見を共有していきたいじゃないですか

研究のプログラミングにおける悲劇を無くすためのGitとテスト

大学の研究に役に立った物シリーズ第3弾です

f:id:Kesin:20140227000334p:plain

今回は研究のためのプログラミングのノウハウについてです。 特に、研究におけるプログラミングでの悲劇を防ぐために自分が実践していた方法を紹介をしたいと思います。大学や研究室によっては、このような研究のプログラミングのノウハウの伝承が行われているところもあると思いますが、何かの参考になれば幸いです。

大学の研究で役に立ったものシリーズの記事

研究のためのプログラミングとは

まずは、研究のためのプログラミングに求められる特徴をざっと説明したいと思います。自分の経験からですが、こんなところではないでしょうか。

  • 実験結果が出ないと何も議論できないので、とりあえず速く実装することが求められる
  • コードのモジュール化、速度の最適化は後回しになりがち
  • 計算量が多いタスクでは、24時間実行しても実験が終わらないことがあり得る
  • バグによって実験の結果が変わってしまうことは許されない
  • 突然の再実験を求められることがある
  • 実験ノートの日付に対応したコードが動く形で残っていることが求められる
  • 自分以外の人が再実験できるように、実行時オプションやログが記録されていると良い
  • 後輩への引き継ぎのために可読性、拡張性のあるコードが望ましい

理想は果てしなく高い・・・

現実

大学のプログラミングの授業では、プログラムの基礎やアルゴリズムがメインの内容で、ツールの使い方などはほとんどは授業で教わりませんでした。 このような状態で研究室に配属されて、研究のためのプログラムを書いているとハマりやすいのは、おそらく以下の2つのパターンだと思われます。

1. とりあえず実装を優先
2. コードが肥大化してくると読みにくく、実行速度が遅いコードになる
3. リファクタリング・最適化を試みる
4. 実験結果が以前のものと合わない
5. リファクタリング・最適化を諦めて過去のコードの修正ができなくなる
6. 拡張するのが段々厳しくなっていき、実験が進まなくなる
7. 悲しみ
1. 再実験を求められたときのために、もう使用していない関数や処理も一応コメントアウトして残しておく
2. そのうちコメントアウトした処理が何をしていたのか忘れる。消していいか分からないので、とりあえず残しておく
3. コードが肥大化してくると、大量のコメントアウト行のせいで全体像が把握しにくくなってくる
4. 拡張するのがだんだん厳しくなっていき、実験が進まなくなる
5. 悲しみ

どちらも、時間と共に後戻りできなくなっていくために、前に進めなくなって破綻するパターンです。学部から修士にかけて同じ研究をする場合、実験プログラムを2~3年に渡って拡張し続けるため、よっぽどしっかりとメンテしていないと破綻する可能性が高いと思います。
このような問題は研究に限らずプログラミング全体において言えるため、これを解決するために使われているツールを知ることで、悲しみの連鎖を断ち切ることができます。

Git

おそらく、現在最も勢いのあるバージョン管理システムです。
簡単にGitについて説明すると、今のコードの状態のセーブポイント(コミット)を作っておき、いつでもそのセーブポイントに戻れる、ということが実現できるツールです。 Gitの使い方や詳しいことは、色々なところで解説されているのでここでは紹介しません。しかし、Git自体の概念がとっつきにくく、用語も多いため、入門としては以下のようなグラフィカルに解説してくれている資料がオススメです。

Gitは基本的にコマンドラインから操作するものですが、今ではMacでもWindowsでもGUIからGitを使うためのSourceTreeTortoiseGitなどがありますし、EclipseのようなIDEにもGitのプラグインがあります。とりあえず使ってみないことにはGitのメリットも分からないでしょうから、最初はGUIのツールを使ってとりあえずコミットだけでもしておくといいでしょう。 自分も最初はEclipseプラグインから使い出して、段々とコマンドラインから操作するようになっていきました。


Gitについてある程度理解したところで、研究においてGitでソースコードを管理するメリットを説明します。

簡単に再実験ができる

実験の結果などは通常、研究ノートに日付と共に記録しておきます。再実験が必要になった際には、実験ノートの日付に一番近いコミットに戻ることで、その当時のコードの状態が復元されるので再実験を簡単に行うことができます。

機能拡張・修正が怖くなくなる

簡単に前の状態に戻せるということは、

機能拡張やバグ修正のためにコードを変更する → 何か上手くいかない → 修正した部分を元に戻す → 元に戻したはずなのに何故かプログラムが動かない → 動いているコードを修正することが怖くなる

という負のサイクルを恐れる必要がなくなります。修正した結果、プログラムが動かなくなってしまっても、正常に動いていたときのコミットまでいつでも戻ることが可能なので、動いているコードを修正することを恐れる必要がなくなります。

コメントアウトが不要になる

嬉しいオマケとして、過去のコードをコメントアウトして残すということが不要になります。

コードを修正する前に、修正前のコードをとりあえずコメントアウトして残しておくということは誰でも経験があると思います。しかしそれを続けていると、いずれコメントアウトだらけになってしまってコード全体の見通しが悪くなってしまいます。特に最悪なのが、時間が経ちすぎてコメントアウトした理由を忘れてしまうことです。コメントアウトした部分が必要なのか不要なのかすら分からなくなり、消すに消せなくなってしまいます。
Gitでコメントと共にコミットを残しておくといつでも過去のコードをコメント付きで確認できるので、過去のコードをコメントアウトで残す必要がなくなります。

Gitは最初こそ取っ付きにくいですが、慣れてしまえばこれほど心強いツールはありません。自分も最初は苦戦しましたが、今ではGit無しでプログラムを書くことはあり得ないと言っても過言ではありません。

テスト

テストと言っても、紙に書いたチェック項目を目視で確認するわけではありません。プログラムにおけるテストと言えば、最近では単体テストユニットテスト)のことを指すことが多いです。テストについては語り切れないので、今までテストの存在も知らなかった人はググるなりして調べてみてください。とりあえずは、自分の得意な言語でユニットテストのやり方を解説されている資料を探してみるといいと思います。

さて、ユニットテストについて何となく分かりましたか?テストコードによって、ある関数の出力と期待する出力が一致していることを自動的に確かめことができる。という雰囲気だけでも分かればOKです。
研究におけるプログラミングでテストを使用することをオススメする理由は2つあります

安全のためのテスト

多くの入門資料ではテストコードの例として関数の出力をチェックする、という使い方がメインであったと思います。もちろん全ての関数についてテストコードが書かれていることがベストですが、既にある程度の規模の実験プログラムを作ってしまった後だとかなりハードルが高くなります。そこで、オススメなのは

  • 関数 → 実験プログラム
  • 関数の出力 → 実験結果

と置き換えた、関数の出力の代わりに実験結果をテストするテストコードを作成することです。
このようなテストコードを作成しておくと以下の様な悲劇を防ぐことができます

リファクタリングや最適化によるバグの発生

リファクタリングや実行速度の最適化といった、実験結果には影響が出ないはずの修正をした数カ月後に、同じパラメータで実験しているのに昔の実験結果と結果が微妙に違ってしまっているという悲劇。

過去の実験プログラムへのバグの混入

ありがちな例として、ある実験Aを行うプログラムを作った後に、実験Aを少し拡張した実験Bを行うプログラムを作るとします。実験Bが無事終了した後に何気なく実験Aを実行してみたら、実験Aの結果が昔と変わってしまっていたという悲劇。

特にAとBが共通の関数やクラスを使っていると、Bにしか影響が無いと思っていた変更が実はAにも影響してしまっていて、実験結果が変わってしまうということが起こりやすいです。

過去の実験プログラムが動かない

新しい手法の実験を行うために新しい実験プログラムを書いていると、ついつい昔の実験プログラムのメンテナンスを忘れがちになってしまいます。そして論文を書く段階になってから、昔の実験プログラムがなぜか動かないために再実験に苦労するという悲劇。

これらの悲劇は、過去の実験プログラムが出力する実験結果が変わっていないことを確認するテストコードを書いておき、定期的にテストを実行することで回避することができます。
実際に実験結果をテストするには、長くても数分でテストの実行が終わるような小規模なデータセットを使って(無ければ自分で用意して)、過去のプログラムが出力する実験結果のXMLCSVを正しく動作していた頃のものと比較するようなテストコードを書くといいでしょう。テストコードの書き方がイマイチ分からなければ、diffコマンドを実行するシェルスクリプトで代用してもいいと思います。重要なのは、過去の実験プログラムの実験結果が変化していないことを継続的に保証して悲劇を回避することです。

開発効率のためのテスト

一般的にTDD(テスト駆動開発)と呼ばれる手法で、実際の機能を実装する前に、テストコードから書いてしまうやり方です。
この手法が大きな効果を発揮するケースはいくらでもあると思いますが、自分の場合は実験データの前処理として、テキストに付けられた複数のタグを正規表現で除去するときにTDDが非常に有効だと感じました。今までは、全てのタグのケースに対応できているかprintデバッグで確認していたのですが、テストコードを先に書いておくとテストが走って一瞬で正しく実装できているかどうかが分かるので、非常に効率的に実装できました*1

まとめ

テストとGitと使う最大のメリットは、動いているコードを修正することが怖くなくなることです。実験結果が昔と変わっていないと保証できることで、リファクタリングや最適化をガンガンできるようになるので、プログラムが肥大化しすぎて研究生活の終盤で破綻してしまったりだとか、後輩にプログラムを引き継げなくなってしまうという悲劇を回避することができます。

Gitとテストは、言葉だけでメリットを伝えることは難しいです。自分も最初にGitやテストを勉強した時には、なぜこれほど人気があるのか分かりませんでした。しかし、どちらもその便利さを知ってしまった今では必須のツールです。ぜひ一度、使ってみてください。



最後に、可能であれば言語処理学会第19回年次大会のチュートリアル

  • 「言語処理研究におけるソフトウェアの開発と公開」

の資料を一読することをオススメします。このチュートリアルは三部構成で、残念ながら全ては公開されていないようですが、岡崎先生が担当された 「研究者流コーディングの極意」だけでも必見です。プログラムを小さく作ること、実験結果の再現性、可視化、最適化・整理は後回し、ソフト公開などの重要性について説かれています。バージョン管理ツールとしてGitの名前も挙げられています。

残りの二部については、言語処理学会第19回年次大会に参加された研究室の方しか手に入れることができませんが

などについての説明されている貴重な資料です。自然言語処理に限らず、プログラミングを伴う情報系の研究には非常に関係する内容だと思うので、閲覧が可能な人は是非一読することをオススメします。

*1:言語処理以外の分野の人には分かりにくい例ですいません