機械学習個人レベルのワークフロー改善


はじめに

この記事は機械学習の実践についての話題ですが、事業レベルでサービスを開発するような華々しさや高度な技術の紹介とは対極的な内容です。

個々のエンジニアの仕事は、やがては大きな成果に集約される源泉です。この個人レベルのスループットを上げて、本質的価値創造により力を注げるようにして、やがて有望な種を生み出す手数を増やしたい…。その個人レベルの環境づくりで、特に繰り返し試作するときの実践で「効いているかな」と思うプラクティスについて、この記事でまとめてみたいと思います。

具体的には下記の内容になります。

  • DRY原則のソフトウェア・エンジニアリング。
  • 繰り返し現れる常套句=クリシェを再利用できる形に。
  • Pythonパッケージの形で継続的成長を。

この記事で紹介しているリポジトリはこちらです。

きっかけ

機械学習に限らずエンジニアの日常として、関連するアイデアを試作して、目的の実現を図る作業があります。この時すべて一から組み立てることはなく、既存の部材を組み合わせ、そのつなぎの作業が細かく発生します。このオーバーヘッド、多くのプログラムで重複して現れ、工数が無視できずコードの煩雑さを生んでいると感じていました。例えば試行錯誤のノートブックの先頭に必ず書かれるこんな行、かといってライブラリにする程でもなく。

%matplotlib inline
%reload_ext autoreload
%autoreload 2

多くのチュートリアルやサンプルで見かけるため、皆さんの手元のお試しコードにもコピペされ、気にせず使われているのではないでしょうか。(なお上記をライブラリ関数化するにはやや面倒なことになりますので、なおさらコピペ対象になると思います。)

見慣れていて気にしていない、一般的にはそういう扱いだと思いますが、①再利用のため毎回コピー元を探すなど時間がかかかり、②利用中には毎回コードに現れるため一瞬でも読むことになり、少なからず集中力が消費されます。このオーバーヘッドにより、クリエイティビティが少しずつ削がれるのが気になっていました

個人的に、仕事の早いクリエーターやミュージシャンの作業を見る機会が時々あるのですが、彼らはすぐに作業できるよう仕事の環境を整えています。打ち合わせの場でさっとアイデアを伝えないと仕事が続かない、そんな厳しい環境に臨む姿勢は学ぶことが多く、自分の手元でも作業性をそのレベルに上げるよう準備すべきと感じています。そのとき、このオーバーヘッドの解決は取り組むべき課題ではないかと思ったのです。

また個人的な話ですが、ソフトウェア・エンジニアとして20年キャリアを積み、機械学習にシフトしました。機械学習は関連ソフトウェアの進歩がとても早くて日々何かが新しくなり、試作コードが山積する状況があります。やれる事はリポジトリでのソースコード構成管理程度、スピードを落とさずソフトウェア品質を保つ事は二の次に。何とか手をつける必要がありました。

2018年はセミナー等でも「機械学習でも真っ当なエンジニアリングが必要」という話題が散見されるようになった年でもあります。

機械学習プログラムでのDRY原則

DRY原則について詳しくは割愛しますが、「同じことを繰り返さない」ことは設計思想としても、プラクティスとしても有効です。手元の機械学習ではどうでしょうか?

  • コードの重複は?フレームワークを再利用し、基本的にはOK。モデルの定義は前回から使いまわしてる?コピーしてない?
  • データの読み込みや可視化でpandasやmatplotlibを使ってる、OK。でもそれを使いこなすコードはコピペしてない?
  • ローカルによく使うデータはまとめてライブラリ化してるならOK、でもそのテストは?

ライブラリやパッケージを使うこと自体はOK。やはりアプリケーションサイドにあるグルーコードやアプリケーションレベルのロジック周辺のコードに、原則的に進めにくい状況がありそうです。極端な話、

df.to_csv(filename, encoding='utf_8_sig')

これはCSV出力する際、Excelで文字化けさせずに日本語の文書を読ませるオプションencoding='utf_8_sig'ですが、類似の場面で付け忘れたり、そもそも何のために付けたか半年後の自分には不明な可能性があります。これを使うとき呪文のような状態でコピペすることも、その結果忘れた意味を再確認することも、DRYではない行為に入ると思います。

ワークフローの改善〜クリシェをまとめる仕組みづくり

そこでライブラリ化して、Pythonパッケージを整備することにしました。
これまでコピペしていた「クリシェ=常套句」をまとめるパッケージ、自分のアカウント名を冠して「dl-cliche」、まさに自分専用です。

  • クリシェなコードをパッケージ化して、簡単に呼び出せるように。
  • オープンなリポジトリで構成管理、インターネットでどこからでもアクセス可能に。
  • ドキュメンテーションの方法を決め、十分な説明を最小限の工数で。
  • テストの方法を決め、最低限な品質担保を持続できる体制を。
  • ロギングの方法も決め、すぐにログ出力可能に。

以上Pythonでの開発がほとんどなので、Pythonプログラムについて問題を解決を図りました。

dl-clicheを使ったプログラムのイメージ

ノートブックの場合、先頭行にこの二行を書くと、様々な準備が完了します。

from dlcliche.notebook import *
from dlcliche.utils import *
  • ノートブックのリロード、matplitlibのインライン指定。
  • numpy, pandas, matplotlib, IPythonなど定番パッケージの読み込み。
  • クリシェな常套句ライブラリの読み込み。

例として、IoTで取得した日々のセンサーデータを5日分(11/17から11/21まで)まとめ、1/10に間引いて保存する前処理は以下のとおりです。

from datetime import date
# 「df_load_excel_like()」で日々のデータをロード。その後'created'列の日時データをindexに設定。
dfs = [df_load_excel_like(f'data/{date(2018, 11, d)}.csv').set_index('created')
                          for d in range(17, 22)]
# 「df_merge_update()」で一つのDataFrameにまとめた上、重複があればまとめる。
df = df_merge_update(dfs)
# 1/10に間引く処理。
df = df[::10]
# 「df_to_csv_excel_friendly」でファイルに保存。Excelで開いても問題ない。
df_to_csv_excel_friendly(df, 'data/ref_merge_resampled.csv')

元のファイルのうち、一つのファイルの温度をJupyter Notebook内で表示してみましょう。

plt_japanese_font_ready() # dl-cliche: 日本語フォントの使用を設定
plt_looks_good() # dl-cliche: グラフを幅いっぱいに表示
df = df_load_excel_like(Path('data')/'2018-11-17.csv', preserve_dtype=False)
df.温度.plot(title='11/17の温度グラフ')
plt.show()

(表示例)

以上こういう時系列データをまとめたり、可視化するときのクリシェの利用例でした。
※ この例でもし日本語が化ける場合、下記を実行してキャッシュをクリアしてみてください。「matplotlibで日本語」より。

import matplotlib
matplotlib.font_manager._rebuild()

改善内容: パッケージ化

Pythonはパッケージ化の仕組みが用意されています。これを利用することで下記の利点があります。

  • dl-clicheパッケージに関数を蓄積する。
  • cd dl-cliche & pip install . で開発環境のすべての場所から使えるようになる。
  • 新しい環境では、パッケージファイルをコピーしてきて、pip install .でインストールできる。

ライブラリファイルをコピーしてきたり、そこへのパスを考えたりすることからDRYに開放されます。

改善内容: リポジトリ管理とオープン化

例えばgithubで構成管理すれば、ソースコードのバージョンが管理できて、インターネットどこからでもアクセスできます。

オープンにすることに問題があるケースもあると思います。しかしクリシェなコードの成り立ちに立ち返ったとき、検索してBlogやStack overflowなどのQ&Aサイトから得られた情報がほとんどではないでしょうか。一般に公知なコードの集合であれば、たとえ仕事で使うとしてもオープンに運用する方が適切に思われることもあるんです…本当? 例えばこの「pandasでread_csv時にUnicodeDecodeErrorが起きた時の対処 (pd.read_table())」で公開していただいた情報は、下記のようにまとめました。

import codecs
def df_read_sjis_csv(filename, **args):
    """Read shift jis Japanese csv file.
    Thanks to https://qiita.com/niwaringo/items/d2a30e04e08da8eaa643
    """
    with codecs.open(filename, 'r', 'Shift-JIS', 'ignore') as file:
        return pd.read_table(file, delimiter=',', **args)

外部にライブラリで公開されることもないこのような小さなことは、自分たちの非公開ライブラリにライセンスを明記しながら外部情報として登録するのも似合わず、githubのgistに登録したいサイズですが、gistでは使い回しが不便です。

もともとオープンな環境から得られた情報であれば、まとめて再利用しやすくする社会還元、という見方もあると思います。

(追加補足) github公開により、この一行でインストールできます。

pip install git+https://github.com/daisukelab/dl-cliche.git@master

改善内容: ドキュメンテーションの方法を決める

ソフトウェアの文書化は色んな場面で不十分さ&維持の困難さが指摘されますが、解決の一つは書き方を定式化して、それを習慣化することかと思います。今回、標準的な方法を真似て、基本的に下記の簡単なルールを決めました。

def convert_foo_to_xxx(foo):
    """Write something to explain.

    Arguments:
        foo: Explain what is supposed to be.

    Returns:
        Explain what will be returned as type xxx.
    """

標準的なパッケージで使われている書式を参考にしたので、いくつかある標準的な書式のどれかに相当するはずです。
Sphinxたぶん文書整形できることを期待しますが、文書化までは考えないリソース配分のバランス感覚です。

  • 基本は「Self explanatory」に名前をつける。そもそも自己説明的にして、それだけで完結させたい。
  • 書式に則り、関数やクラスは必ず標準的にドキュメントを書く。
  • しかし書きすぎない。書かなくて良いものは、あえて書かない。

あまり自信はありませんが、Pythonicなコードであればそもそも説明しなくても形でわかるものが多いのではないでしょうか。
また関数等の命名もだいたい一般的なパッケージと同じようにすれば、関数名以上の説明が不要になることも多いです。

※ 繰り返しますがPythonicに書けているかどうか、自信はないのですが…

改善内容: テストの方法を決める

Pythonのパッケージには、unittestパッケージを使った自動テストの仕組みが用意されているので、これを整備しました。
このようにすることで、何か変更したあとに

($ cd dl-cliche)
$ pytest

を実行することでユニットテストを実施できます。また、テストを絞りたいときも

$ python -m unittest test/test_utils.py

のように一つのファイルだけ実施できます。これで変更しても手戻りの心配が少なくなり、安心です
持続的で、健全なノウハウが育っていってくれます。

改善内容: ロギングの方法を決める

ログはprint()で行われることが多くないでしょうか。これは…後で手戻りになります。
特にdockerコンテナで動作させたり、サーバー上で定期実行するときに障害になりやすいと思います。

ですので、特にログの方法を決めてしまうことにしました。
その際、コンソールへの出力の他にファイルに出力したいことも多いものです。
dl-clicheでは、手軽さと最低限必要なプラクティスをまとめ、下記のように使えるようにしました。

log = get_logger()
 :
log.info('something informative')
log.debug('debug message 1, 2, 3...')

以上でコンソールに出力、ファイル出力のみの場合下記のように使います。

log = get_logger('foo', level=logging.INFO, print=False, output_file='foo/bar/test.txt')
 :
log.info('something informative')
log.debug('debug message 1, 2, 3...')

Pythonのloggingパッケージは素晴らしいのですが、出力書式の設定が必要になったりと、心理的障壁(=クリエイティビティの消費)がやや大きい気がしています。それらを隠蔽した内容です。

その他の試み

  • test.py 階層構造の変数間の比較、2つのデータフレームの一致確認、といったテストが面倒な件をまとめています。
  • projectml.py 機械学習の小さなプロジェクトの設計を「コードで自己説明的」にする枠組みの試みです。「目的」「データの更新方法」「パラメーターの更新方法」「何を試行錯誤して繰り返すか」など、何をどうしたいプロジェクトなのかを明確にできるような試みです。あまり成果が出ていないのですが、重点を置いて取り組んでいます。
  • excel.py 手作業になりがちなExcel(CSVではなくExcelファイルそのもの)を自動化するためのライブラリ化をまとめています。これは手元の業務ではとても具体的な省力化につながっています。
  • nlp_xxx.py NLPに使用するものをまとめ、直ぐにテキストを処理する仕事にかかれるようにしています。

今後も画像処理などまとめるべきことは多く、特に面倒なデータセットの取りまとめクリシェなどを成長させていく見込みです。

最後に

ここまでご説明した内容で dl-cliche はこれまで成長してきました。新しいアイデアを思いついたとき、

  • 与えるデータの検討に集中して、整形作業はクリシェコードで省力化し、
  • モデル構造の検討や動作の分析に集中して、メッセージはなどは簡単にログ出力、
  • ノートブックを開いて、先頭にfrom dlcliche.notebook …を書いてすぐにスタート、
  • 新しいクリシェが発生したら、dl-clicheにまとめて発行、

このような作業イメージで日々スピードを上げています。

リファクタリングして見直すことも多いので、決めすぎないことも意識して、時間や労力のリソースを切り詰め、持続できる状態を模索してきました。ただ、必ずしもプロフェッショナルに進められていないと思いますし、最善手ではないかもしれません。

とはいえ、「やればできる事はわかっている」「試してみるべき」だけど「面倒で進めていない」ことが楽になり、スピードが上がったと実感しています。

このようにご紹介したプラクティスは、それぞれ個人レベル、またはグループ専用のclicheパッケージに展開することをお勧めできると思います。
私に必要なことと、人それぞれまとめるべき内容は違うでしょうし、大切にしているプラクティスはまちまちだと思います。
無理に共通ライブラリにならないときは、それぞれに分かれてクリシェをまとめるくらいが、スピードとDRYな品質担保のバランスを取りやすいかもしれません。

〜 〜 〜

これまでまとめたクリシェのほとんどは、インターネット上で公開された情報をまとめ直したものです。オープンな環境だからこそ得られたショートカット、先人たちの努力とオープンな環境に改めて感謝します。

dl-cliche はそれなりにまとまっていますので、こちらをforkしていただくのも歓迎です。