ぽよメモ

レガシーシステム考古学専攻

公開さえすればいいってもんじゃない

TL;DR

  • 研究で開発したツールを公開するのはとても良いこと
  • 頼むから必要な依存関係の情報を残してくれ
  • 頼むからREADMEを書いてくれ
  • 頼むからライセンスを明記してくれ
  • 頼むからDockerイメージになんでも突っ込まないでくれ

はじめに

 これはあくあたん工房お盆休みアドベントカレンダー7日目の記事です.

 今日は開発者向けではなく,研究者向けの話をします.主にコンピュータサイエンスの話になりますが,ソフトウェアを書く必要のある分野であればどれもそう変わらないだろうと思います.
 研究でソフトウェアを書いている人,それをGitHubで公開している/しようとしている人へ向けて書いています.僕の専攻分野,研究内容による影響でこの記事の内容には偏りがあります.全ての分野に当てはまるとは言えません.よろしくお願いします.

研究とソフトウェア

 コンピュータサイエンスの多くの研究では,規模の大小こそあれど,ソフトウェア(ないしスクリプト)を書いて実験を回すことになります.その過程でできたものをGitHub等で広く公開することも,最近では見られるようになってきました.論文に掲載された理論と実際の実装があれば,それを拡張することも,検証をおこなうことも容易になり,大変便利でありがたいです.しかし,そうして公開されるソフトウェアの多くは様々な要因が重なり,はっきり言うと使い物にならないことが多いです.
 色々とその原因はあるのでしょうが,僕個人は,研究における開発は研究者当人の環境で動けば問題ないことが多く,他環境での再現性には注意が払われないという事態にしばしば陥りがちであるためと考えています.これは以下の様な事態を引き起こします.

  • それを動かすための依存関係の情報が欠落している
  • 使用方法が書いておらず,入力するデータをどのような形式で用意すれば良いのかわからない.
  • ライセンスの表記がない

特に「依存情報の欠落」と「ライセンスの欠落」は致命的であり,このようなリポジトリは基本的に使用不可能に近くなってしまいます.卒業研究のような短期的に誰かが研究を行い,その後続の研究を誰か他の人が行うような事例の場合でもこれらの問題は顕在化し,しばしば技術/研究の継承を途切れさせてしまいます.
 今回はこれらについて,最近使用されることが多くなってきたPythonにフォーカスし,いくつかの対処法を紹介していこうと思います.

依存関係の管理

どうして依存関係管理が必要なのか

 使用するライブラリだけでなく,Python自体も,定期的な機能更新が行われています.しかし,テキストで記述されているソースコードから,使用されているライブラリ・Pythonのバージョンを推測することは容易ではなく,適していないバージョンのものを用いると,実行時にエラーが発生したり,予期しない結果になる恐れもあります.
 また,使用されているライブラリはスクリプトを解析すれば分かるかも知れませんが,OS側にインストールが必要なものがあるとか,特定のハードウェアが必要であるとか,そういった情報も,開発した当人にとっては当たり前かも知れませんが,利用者にとってはそうではありません.これが依存関係の適切な管理・および把握が必要な理由です.

なぜAnacondaは微妙なのか

 研究開発において,よくAnacondaが使用されています.

www.anaconda.com

はっきり言うとAnacondaは必要な依存関係を隠蔽し,開発環境をガラパゴス化させる大きな要因の一つであると考えています.
 Anacondaには,最初からビルド済みのデータ解析用パッケージが多数含まれており,パッケージのインストールのような特別な操作をわざわざしなくても最初から様々なパッケージの恩恵を受けることが出来ます.特にIntel MKLにリンク済みのnumpyなど,通常のインストールでは面倒なものを簡単にインストールできるため,大変人気です.しかし一方で,

  • どのパッケージを自分は実際に使っているのか
  • そのパッケージのどのバージョンを利用しているのか

といった情報からユーザを隔離してしまいがちです.仮想環境を作って運用していたとしても,その状態は明示的に残さない限りリポジトリには含まれないため,気をつけて運用する必要があります.
 深層学習・機械学習は進歩がめざましく,パッケージの更新も頻繁にあるため,以前のAPIが使用不能になることは多く見られます.特に直近ではTensorflowが2.0への移行を進めており,将来的にデフォルトのバージョン*1が切り替わる事になります.ある実装がどのバージョンに対応しているのか,という情報が必要不可欠であることがわかります.

対処法1:Minicondaと仮想環境を使う

 Windowsユーザは実質的にこれを選ぶしかありません.MinicondaはAnacondaの軽量版であり,自分でパッケージを選択してインストールする必要があります. まずは1つの研究プロジェクト,ないしソフトウェアリポジトリごとに1つの仮想環境を作成してください.これにより,研究ごとのPython環境が隔離されるため,異なるプロジェクト間で異なるバージョンのパッケージを要求されても対応できるようになったり,どのパッケージが必要となっているのかが明白になったりします.
condaコマンドを使って仮想環境を作成できます.

# 仮想環境の作成
$ conda create -n 仮想環境名 "python==使うバージョン"
$ conda install -n 仮想環境名 numpy
# 環境へ入る
$ conda activate 仮想環境名
# 環境から抜ける
$ conda deactivate

このように作成した仮想環境の情報を明示的に残すためにはconda env exportというコマンドを使用できます.

# env.ymlとして環境の情報を出力する
$ conda env export -n 仮想環境名 -f env.yml

こうして出力されたenv.ymlには以下の様なフォーマットでパッケージが記録されています.

name: 仮想環境名
channels:
  - defaults
dependencies:
  - パッケージ名=バージョン=ビルド番号
  - pip:
    - パッケージ名==バージョン
prefix: /path/to/仮想環境名

このファイルを共有しておけば,OS,アーキテクチャを揃えれば他の環境でもconda env create -f env.ymlで同等の環境を復元することが出来ます.ただし,新しいパッケージをインストールしても自動的には反映されないため,変更したら再びexportする必要があります.

対処法2:pipenvで自動的に管理する

 Pipenvはrequestsなどの有名パッケージの作者であるKenneth Reitz氏による,新しい依存関係管理ツールです.pipと似たインターフェースを備え,PipfileおよびPipfile.lockにより依存関係のバージョンを固定しています.

# pipenvのインストール
$ pip install --user pipenv
# 仮想環境の作成.PipfileおよびPipfile.lockが作成される
# 明示的に使用するPythonを指定する場合
# pipenv install --python /path/to/python
# または
# pipenv install --python バージョン
# ただし,pyenv等でこれらのバージョンが使用可能になっている必要がある
$ pipenv install
# パッケージのインストール
$ pipenv install numpy
# 仮想環境に入ることなく,仮想環境内のコマンドを呼ぶ
$ pipenv run python main.py
# 仮想環境に入る
$ pipenv shell

今後はpipenv installuninstallを行うごとにPipfileおよびPipfile.lockが更新されるため,これらをリポジトリに含めておけば良いです.Anaconda/Minicondaと違い,pipと同じパッケージを参照するため,MKL等は自分で導入する必要があります.

詳細な使い方は以下の参考リンクをご覧ください.

narito.ninja

対処法3:setup.pyを書く

 これは上記対処法1・2と共存できます.完璧にバージョンを指定するのではなく緩いバージョン制約を設けること,そしてそのパッケージをその仮想環境へインストールする事が出来るようになります.

 まずプロジェクトのディレクトリ構造を以下の様にします.今回作るプロジェクトで使用するPythonモジュール群をpackage_aディレクトリ以下に置くものとします.

.
├── LICENSE
├── README.md
├── setup.cfg
├── setup.py
└── package_a
    ├── __init__.py
    └── hoge.py

setup.pyを書きます.以下の内容だけで良いです.

from setuptools import setup

setup()

setup.cfgを書きます.

[metadata]
name = パッケージ名
version = バージョン
# バージョンをPythonスクリプト内の変数から取ることも出来る
# package_aの__init__.pyに`version = "1.0.0"`などと宣言しておけば以下で参照できる
# version = attr: package_a.version
author = 著作者
author_email = 著作者の連絡先
description = 説明
url = https://github.com/yourname/yourrepo
# ライセンスファイルへのパスを指定
license_file = LICENSE

[options]
# Pythonバージョンの制約.以下だと3.5以上4.0未満
pythonrequire = >=3.5,4.0>
# 含めるパッケージの選択.この場合は自動で探索
packages = find:
# 列挙する場合は普通にインストールするパッケージを羅列すれば良い
# ただしトップレベルパッケージのみ書いてもそれ以下のサブパッケージは含まれない
# packages =
#    package_a

# 依存関係の列挙
install_requires =
    # 以下なら1.0以上,2.0未満のhogeを要求する
    hoge>=1.0,<2.0
    fuga

[options.packages.find]
# 自動探索から外すものを列挙
exclude =
    tests
    piyo

こうしておけば,setup.pyと同じ階層でpip install -e .(pipenvの場合はpipenv install -e .)でEditableモードとしてインストールできます.package_aからのパスでのimportができ,コードを編集すればそれがそのまま反映されるため,何度もインストールする必要がありません.

# インポートする
from package_a import hoge

hoge.~~~

対処法4:Dockerイメージ化する

 最近ではDockerが使用されることも多いです.この場合,OSレベルから依存関係を構築出来るため,より高い再現性を得られます.また,Dockerイメージを直接配布できるため,他環境で何度も環境を作るといった手間を省けるというメリットもあります.一方で,Linuxのコンテナ技術を利用するため,そのほかのプラットフォーム特有の機能を利用していたり,特定ハードウェアにロックインされている場合,GUIが必須の場合は注意してください.

CUDAを利用する場合はnvidia/cudaのイメージを使うと良いです.下記の様なDockerfileと呼ばれるフォーマットのファイルを記述します.

FROM nvidia/cuda:10.0-cudnn7-devel-ubuntu18.04

RUN apt-get update -y && \
    apt-get install \
        python3 \
        python3-pip

RUN pip install \
    tensorflow==1.14 \
    numpy==1.16

COPY . /opt/hoge

CMD ["python3", "/opt/hoge/main.py"]

ピュアなPythonイメージ(FROM python:バージョン)を利用することもできます.CUDAが不要な場合はその方がイメージサイズが小さく,オススメです.Debianベースなので同じようにapt-getが使えます.

これも詳しくは参考リンクをご覧ください

moritomo7315.hatenablog.com

Dockerアンチパターン

やってほしくない例です.

バージョン指定していないイメージ
FROM python

RUN pip install tensorflow

このようなものはビルドごとに最新のものを使ってしまうため,異なるイメージが作成されてしまいます.バージョンはしっかりと指定してほしいです.

FROM python:3.6

RUN pip install tensorflow==1.14
なんでもイメージ

あらゆるライブラリ全部網羅!これさえあればOK!みたいなイメージは,結局のところ無駄にリソースを消費し,依存関係をややこしくするだけです.

  • 初心者がとっかかりに使う
  • Kaggleなどで汎用的に使える環境のために使う

のは良いかも知れませんが,再現性のための環境の配布でこういったことを行うのは明らかに悪手です.Web上ではよく見かけますが,こうしたイメージを使うならDockerでなくてもいいじゃんという気持ちです.

データ自体が入ったイメージ

大容量データセットをイメージ自体に含めるのは最悪なのでやめてください.イメージをローカルに展開しただけでシステムの容量を一気に持って行かれるのは大変困ります.
Dockerにはホストのディレクトリをマウントする機能があるため,データセットとイメージの配布は別々に行うべきです.

# カレントディレクトリのdatasetディレクトリをコンテナの/datasetにマウントして起動する
$ docker run -v $PWD/dataset:/dataset hoge python ~~
docker commitして作ったイメージ

dockerには,起動したコンテナ内での作業を永続化してイメージを作成するためのcommitという機能があります.いくつかのWebサイトには

  1. dockerコンテナを起動
  2. 何か作業
  3. git commit代わりにdocker commit
  4. Dockerhubへpush

みたいな例を見かけますが,いたずらにイメージの肥大化を招き,再現性のある環境を得ることが難しくなるだけなので辞めてください.
Dockerfileは再現性を得るためにわざわざビルド時に実行する処理を記述しているのであって,これを無視して実行結果だけを保存していくだけでは,目的を達成できません.

対処法5:READMEに書く

結局上記のどの対処法を使っても,これが必要になるという話ではあります.詳しくは次節に書きます.

その他の依存関係

 対処法1~3では,OSにインストールする必要のあるライブラリについては一部を除きサポートできません*2.READMEなどに記述しておくべきでしょう.Dockerではこれらもまとめて管理できるため,出来るならDockerfileを用意することをオススメします.

使い方を残す

 せっかく公開されていて動かすこともできるのに,どのように使用して良いか分からない!みたいな事例もあります.わかりやすい使い方が載っているかどうかだけでも,そのリポジトリが活用される可能性は大きく向上すると思います.

対処法1:READMEを書く

 一番単純な方法ですが,意外とやらない人は多く居るという気がしています.書くのが面倒,情報を更新していくのが面倒,など事情は理解出来ますが,後々の自分のためにもある程度書いておいた方が良いと思います.

  • 書いておいて欲しいこと
    • 概要:このソフトウェアを使って何を達成できるのか
    • 環境:どのOS・ライブラリを使うのか,その構築方法
    • 使い方:どういう風に使うのか
    • 設定:どういう設定項目があるのか
    • ライセンス
    • 引用のためのbibtexフォーマットのテキスト
  • 書いていなくても良いなと思うこと
    • 連絡先(何かあればissueに書けば良い)
    • Known issue(issueに書いておけば良い)

特に,ユーザが知りたいのは

  • 何を入力して
  • 何が得られるのか

という情報です.この2点がしっかり分かるだけでも,そのリポジトリに対する評価は大きく変わってくると思います.

対処法2:コマンドラインオプションを作り込む

 python main.py -hなどでヘルプメッセージを表示できるようにする,という意味です.これをしっかりと書いておけばそれ自体がドキュメントとなるため,その出力をREADMEに貼るだけでマシになります.
 Pythonではargparseというモジュールによって実装されています.簡単な使い方は下記で紹介されています.

qiita.com

より複雑なコマンドを作成する場合は拙作のCLIフレームワークもご検討ください.

poyo.hatenablog.jp

対処法3:exampleを作っておく

 どういうデータを用意すれば良いのか,その形式はどのようにすれば良いのか,などを簡単なサンプルを使って説明できるとより良いと思います.が,正直面倒だと思うので,ライブラリとして開発した上で,それを使用する実験スクリプトを全てexampleに突っ込むという構成がオススメかなと思っています.

$ tree -L 3
.
├── LICENSE
├── README.md
├── package_a
│   ├── __init__.py
│   └── hoge.py
└── examples
    └── ex1
        ├── __init__.py
        └── main.py

実際の実験スクリプトを分離しつつ,使用例を示せるので楽です.ただし,あくまでこれはオプションであり,exampleだから分かりやすくしないと!と必死になっては時間が溶けるだけなので注意しましょう.
 また,実験の再現性に重点を置いて,論文中の図表と実行されるスクリプトとを対応付けるとよりわかりやすいかも知れません.他にも,どの順で何を実行するかを示したシェルスクリプトなどがあると,どの順でコードを追いかけていけば良いかも分かってくるため,実験実施中に使用したスクリプトを併記するのも良いかも知れません.

ライセンスを明記する

ライセンスが無いとどうなる?

 GitHubの規約では,明示的にライセンスが示されていないリポジトリの扱いは以下の様になっています*3

You're under no obligation to choose a license. However, without a license, the default copyright laws apply, meaning that you retain all rights to your source code and no one may reproduce, distribute, or create derivative works from your work.

つまり,ライセンスを一切選択しない場合,

  • 通常通り著作権法が適用される
  • 全ての権利は著作者に帰属し,複製や再配布は許可されない
  • そのような状態のコードを勝手に利用することは著作権法違反となる恐れがある

ということです.当然,このような状態のリポジトリを第三者が扱うことは限り無く難しくなってしまい,公開されていて見れるのに使えない,という状況に陥ってしまいます.

OSSライセンスの種類

大雑把に分けてコピーレフトかそうでないか,という2種類があります.代表的な物をいくつかあげます.

利用者に対して「このコードを組み込んで作ったりこれ自体を改変したソフトウェアは同じく公開してください」という通知をするのが概ねコピーレフトライセンスの概要であり,そういったものを要求せず.著作者の表示のみなど軽い条件を求めるのがそれ以外のものです.詳しいことは各ライセンスについて調べてみてください.
 難しいことを考えたくない,いくらでも使ってくれ,というならMITやApache 2.0を採用すれば良いのでは無いかと思います.

ライセンスってどうやって書けば良いのか分からない

全てのファイルの先頭にコメントでライセンスを埋め込むことも出来ますが,より簡潔にするならリポジトリのルートにLICCENSEファイルを置くと良いです*4

詳細は以下のhelpをご覧ください.

help.github.com

まとめ

 OSS開発になじみのある人ならば,この辺りのことはほとんど呼吸のように行っている可能性がありますが,研究と開発は違います.その辺りは理解しているつもりですが,あまりにも使えないリポジトリと遭遇するので……
 上記の内容が守られていれば,より利用されやすくなり,引用も増えて嬉しいのではないでしょうか.よろしくお願いします.


*1:pip install tensorflowで入るバージョンを指して言っています

*2:condaはそれらも一部サポートしていますが完全ではありません

*3:ただし,Forkする権利はGitHubの規約によって依然として存在します. help.github.com

*4:パッケージングする場合はLICENSEファイルを忘れず含めるようにしてください