はじめに
Pythonで解析スクリプトを大量に作成し始めてから、学習途中の状態を任意のタイミングで保存して、いつでもリストアできるようにしたいという要求が多く生じた。ある程度のコード量になると、学習に必要なデータセット、パラメータ群と操作を合わせたクラスを作成し、クラスのインスタンスベースで処理を実行する形になる。この時、いつも思うのが、このクラスインスタンスごとdumpできないだろうか・・ということだ。この要求はpickleなどのオブジェクトのシリアライズパッケージで叶い、オブジェクトごとdump, loadすることができる。
しかし、だんだんとこれでは満足できなくなる。
- 複数のマシンで並列に処理を実行した結果を集約・管理したい
- 解析の条件で、解析結果をソートしたい
- 解析の条件で、解析結果を検索したい
- 必要に応じて、任意のマシンで解析を再開したい
という具合にやりたいことがどんどん贅沢になってくるのだ。ここまでになるとpickleなどの標準的なシリアライザでは足りなくなり、データベースで管理したくなってくる。しかし、これを簡単におこなう仕組みは簡単には見つからない。
dump, restore程度なら自分で作れるが、検索とかソートもつくるとなると、本来の解析のコードよりdump部分のコードの方が大きくなるといった本末転倒な事態に陥ることになる。
今までもこのような課題に対して、ユーティリティを作りたいと考えた時期があったが、かなり大規模なフレームワークを作る必要があることに気づき、躊躇していた。実装上の問題のいくつかを挙げる。
- データ保存のためにクラスのアトリビュートを保持するデータベーステーブルを作る必要があるが、既存のORMの仕組みは、Modelを動的に生成しにくい。といってsql文に近いAPIを使っていたらいつまでたっても終わりそうにない
- SQL系のデータベースは大容量のデータ(例えば、バイナリ)を保持するようにできていない。別の仕組みで保持するとなると、MySQL + WebDAVといったようにいくつもサービスを立ち上げる必要がある。やはり本末転倒な事態になりかねない
- listやdict、datetimeなどのPythonの標準クラスはそのまま扱えるようにしたい。けど、ORMで対応していない型も多い。カスタムフィールドとか作り始めると・・(以下同文・・)
いろいろと考慮すると片手間(1日強で実装、1週間くらいでリリース)につくるレベルをはるかに超えてしまうため、やりたいけど手が出せない日々が続いた。
ところが、最近、mongodbもそろそろちゃんと勉強しなくてはと思って勉強していたら、pythonのパッケージとの組み合わせで、上記の問題があらかた片付くことに気づいた。
- mongoengine.DynamicDocument, mongoengine.DynamicFieldでアトリビュートの動的追加が可能
- GridFSの仕組みで大容量のバイナリもmongodbの仕組みだけで扱える
- mongoengineはsqlalchemyやdjangoよりもpythonの型に合わせたフィールドセットを提供してくれている
など、必要とされている機能が気づくとだいたい提供されているのだ。あとは軽量wrapperのみ作れば、実現できそうなので、やってみた(そこまでこないとやらないとか、腰が重すぎる・・)
パッケージ化
実装したソースコードはdbarchiveという名前で、すでにgithub上に置いてあるので、見ることができる。
setup.pyも用意したので、インストールも可能だ。
設計
解析用のクラスを使う人たちが望む設計とはなんだろう?なるべくdump/restoreなど意識の外から追い出したい。そう・・・できれば、スーパークラスをひとつ継承するだけで全てが解決するそんな仕組みは作れないだろうか・・。クエリ検索はなるべくオリジナルのではなく、既存の仕組みを踏襲したい(ドキュメント書く必要ないし・・)ということで、以下のような仕様とした
- Baseクラスを継承すれば必要な機能が全部ついてくる
- 基本的なAPIはmongoengineを踏襲
上記を満たすように作った結果、以下のような使い方ができるパッケージになった。
使い方
以下がdbarchiveを使ったサンプルコードとなる。
import numpy
import logging
from datetime import datetime
from dbarchive import Base
class Sample(Base):
def __init__(self, maxval=10):
self.base = "hoge"
self.bin = numpy.arange(maxval)
self.created = datetime.now()
print 'create sample instance'
sample01 = Sample(10)
sample01.save()
sample02 = Sample(3)
sample02.save()
for sample in Sample.objects.all():
print 'sample: ', type(sample)
print '\tbase: ', sample.base
print '\tbin: ', sample.bin
print '\tcreated: ', sample.created
sample01.bin = numpy.arange(20)
sample01.save()
for sample in Sample.objects.all():
print 'sample: ', type(sample)
print '\tbase: ', sample.base
print '\tbin: ', sample.bin
print '\tcreated: ', sample.created
print "all task completed"
ソースを細かく追っていこう。
まず、データベースで管理したいクラスに、dbarchive.Baseクラスを継承させる・・以上。
class Sample(Base):
def __init__(self, maxval=10):
self.base = "hoge"
self.bin = numpy.arange(maxval)
self.created = datetime.now()
dbarchive.Baseクラスを継承することで、データベース保存に必要なユーティリティを持ったクラスを作ることができる。あとは、インスタンスをsave関数で保存するだけ。
なお、Baseクラスを継承するクラスの__init__メソッドは、引数なしで実行できるように設計されていないといけない。これは、データベースからインスタンスを自動作成するときに引数なしでインスタンス化できる必要があるためで、仕方なくこういう制約が必要となる。
print 'create sample instance'
sample01 = Sample(10)
sample01.save()
sample02 = Sample(3)
sample02.save()
save関数が呼び出されると、クラスは<class名>_tableというテーブル(collection)をデータベース内に作成してその値を格納する。
検索はクラスが持つobjectsというハンドラを通じて行う。objectsを通じたクエリの発行は基本的にdjango準拠の仕様になっているため、慣れている人にとってはそのまま違和感なく使うことが可能だ。
for sample in Sample.objects.all():
print 'sample: ', type(sample)
print '\tbase: ', sample.base
print '\tbin: ', sample.bin
print '\tcreated: ', sample.created
上記は、これまで保存した全てのインスタンスを取得し、表示するコード。objectsハンドラによるクエリセットの作成の詳細については以下のドキュメントを参考のこと。
より、巨大なバイナリも扱えるか確認するため、chainerによる深層学習への応用するサンプルコードも用意した。
詳しい他の使い方については、dbarchiveのreadmeを参照のこと。
インタフェース
mongodbに保存した値を確認する時には、以下のツールが便利だ。
mongohubはパーソナルユースに最適だ。既存のデータベースクライアントツールと同じ感覚で使うことができる。ウェブインタフェースを使って、複数人で確認したい場合はmongo-expressを使うと良い。