2016年3月12日土曜日

MySQLではできないことができるデータベース(広義)達

自分は一応暫くMySQLの開発者だったので、MySQLでできることできないことはすぐわかる訳です。現実的な問題と対峙すること1年間、MySQLは使えることにしか使わないわけで、そうすると構築してしまうと、アラートメールが全く来ないので、水や空気のように存在を忘れてしまいます。でも、使えないことには全く使う気がしないわけで…。というわけでMySQLは結局逆にあまり触れていません。限られた範囲では完成を見ているというわけでしょうか。

データを処理して何か貯めて利用できるものをデータベースとするならば、MySQLを適用する気も起きないような領域があって、近年はそのような領域に挑む別の道具が出てきています。

今回は趣向を変えて、いろいろ現状MySQLでは扱えない問題の解決法を模索したことについて少し触れます。MySQLを離れた話題ですが、いつか遠い未来にMySQLの世界に持って帰る事柄かも知れません。それぞれMySQLに比べると私は初心者なので情報が不正確かもしれませんがご容赦ください。

ええ、これはMySQL以外も掘り下げてハッキングする誘いです。
それぞれ、一般的な説明は他にあるので省略します。

(1) Apache Spark

10年くらい前から、RDBMSの性能が解決したら、次はETLのお化けのような道具が必要になると常々考えていました。Apache Sparkはまさにそれに当たるものです。

例えば、物凄い超巨大なデータ同士のJOINをして、それぞれのデータに含まれる結合キー以外のキーの組み合せで集計しなければいけなくて、しかも集計結果も下手したら元のデータよりも巨大になるような処理を、一晩で行わなければいけない場合を考えます。偶々現在は1台の高性能なサーバーで誤魔化しながら処理できているけれども、1年後には収まらなくなるかも知れないような状況としましょう。

クエリの並列実行ができても破綻する問題なので、MySQLを使う余地はありません。多数台サーバーで協調してソート・結合・集計を行うしか手はないわけで、Apache Sparkはまさにそういうことの実現に最も近いフレームワークです。

しかし、Apache Sparkを普通に使うだけでは、前述の問題は解決できないでしょう。バージョン1.5系で ソートマージ結合 が導入されましたが、それだけでは足りません。試したのは昨年末にかけてで、1.5系ベースの話ですが、1.6系でも多分一緒だと思います。Sparkの想定する用途は、「集計したら、データは各ノードのメモリに必ず収まる程度に小さくなる」ことが想定されていて、集計しても尚巨大なことは想定外。メモリ不足で落ちてしまうでしょう。

そこで、利用者が工夫する必要があるわけですが、Apache Sparkの素晴らしいところは、そういった「根本の内部クラスの拡張が本体パッケージのリビルド無しで可能」な点です。さらに、Sparkは、ユーザー側のソースコードはScalaで書かれています。自分で、DataFrameクラスを拡張した、拡張メソッドを持つExDataFrameクラスを作っても、

implicit def DataFrameToExDataFrame(df: DataFrame): ExDataFrame = new ExDataFrame(df)

とソース中に暗黙変換の定義を書くだけで、あたかもDataFrameに新しいメソッドが実装されたかのように利用できるわけです。

で、どう拡張するのかですが、Sparkのソースの中には、
sql/core/src/main/scala/org/apache/spark/sql/ExperimentalMethods.scala
というクラス定義があって、
コメントには「勇者へ」みたいな感じで
 * :: Experimental ::
 * Holder for experimental methods for the bravest. We make NO guarantee about the stability
 * regarding binary compatibility and source compatibility of methods here.
 *
 * {{{
 *   sqlContext.experimental.extraStrategies += ...
 * }}}
自分で実行計画を拡張できるようなことが書かれていますが(実際できるのですが)、 複雑なことをしようとすると丸ごとクラス群を作る必要があり、量が多すぎるので、 結局は同じ package の中に継承するクラスを作る羽目になります。。。

試してみたこととしては、
ソートベースの集計 sortGroupBy() のようなメソッド
使い方は既存の groupBy() と一緒。少なくともメモリ不足で落ちません。堅実に動作します。
とか、
ソートして1個前のレコードと結合 sortMergePrevRec()
キーとなるカラムのリストを指定しますが、最後のカラム以外のキー単位で処理の分散、最後のカラムでソートを行い、1個前レコードが後ろにくっつきます。最後のカラムを日付とかにすると、前レコードの日付を使って経過時間を出せたり便利かも。
のようなものは作れるみたいです。

但し、バージョンが上がってオプティマイザが賢くなってくると、こちらが拡張して差し込んだ実行計画や実行に必要な情報が勝手に壊されちゃったりするので、そういった情報のクラスは継承する別クラスを作ってオプティマイザから隠したりしなければいけないので注意です。

結果、オプティマイザとの戦いを通して、動作に詳しくなることでしょう。興味深い世界です。

実はこれでもまだ、前述の課題に対しては課題が有ります。結合と集計のキーが違うことです。Sparkやhdfsはキーのハッシュで分散処理しますが、キーを変えると分散のキーも変わるので盛大にノード間でデータの持ち替え(Spark用語でシャッフル)が発生するわけですが、これが効率がまだ良くないように見受けられます。
spark.shuffle.manager=tungsten-sort
でマシにはなりましたが、シャッフルの仕組み自体に結構無駄がある疑いがあります。巨大データのキー変更を含むと、処理時間の大部分はこのシャッフル絡みに費やされることがわかると思います。

実際に使うときにはシャッフルの動作も詳しくチェックして、できたら改造の余地を調べたいところです。


(2) TensorFlow

層状ニューラルネットワークの「学習-判別」の関係は「格納-取り出し」の関係に似ています。ただ、キーとなるデータが大きかったり曖昧だったり複雑だったりするだけで、データベースの延長のようなものだと考えています。人工知能の重要部品だとはおもいますが、個人的にはこれ自体にはまだ知能は感じないので人工知能の枠組みで語ることには抵抗が有ります。

データベースっぽいとは言え、これもMySQLとはまだ無縁な分野でしょう。とは言え、(1)の問題のように物理的な制約ではないので、意外と(1)よりは近くにある課題かも知れません。

一応、例をあげると、画像に写っているのは犬か猫かとか、画像に写っているものに近い特徴の商品は何で誰が買いそうかとか、思い出せない固有名詞について一生懸命説明文を書いてそれを解釈させて近い特徴の単語を調べるとか。

TensorFlowは同分野のニューラルネットワーク向けフレームワークの中でも、特にサービスとして利用することを意識した創りになっています。学習フェーズにせよ、サービスを行うフェーズにせよ、仕組みを理解すれば最適に近い処理で、GPUを無駄なく使うことができます。

という仕組みの話もありますが、チュートリアルと称して提供されているサンプルの質の高さも魅力的です。参考にすることによって、非常に時間のかかる低層の学習(回転拡大縮小)を省略できたりします。

Inception-v3 というサンプルがあって、これはImageNetのILSVRCのコンペティションの「Object localization」をターゲットにしていますが、位置の検出はしないという少しひねたものになっています。写真を1000種類のカテゴリに分類します。使ってみたら判りますが、これは結構面白い精度で位置の特定までしないのが不思議なくらいです。写真から候補領域を切り出して、それぞれInception-v3に入力するだけで反応の高い部分をとりだせばそのまま位置になるんじゃないでしょうか?Googleの中の人がそうした(フル参戦しない)意図は判りませんが、これはハッカーを誘っているのだと思います。

まぁ、候補領域をどうやってあたりをつけるかは今回は置いておいて、効率よくInception-v3で処理することを考えて見ます。

TensorFlowの動作は、実行計画となるGraph(Tensorの演算)を作って、セッションのメソッド run() で実行するのですが、最初の実行時にGraphに従って、GPUのセットアップを行い、2回目以降は同じGraphであれば即処理が始まります。さらに、GPUのコアとメモリに余裕があれば、複数の画像を同時に与えて同時に処理をしたほうがハイスペックなGPUを活かせます。それは難しいことではなく、バッチ処理用の次元を最初から最後までGraphの中で保持しておけば、配列を任意の長さの行列にして run() の feed_dict に与えるだけです。長すぎる場合は勝手に複数回に分けてくれるようです。

run()メソッドでは、Graph内のTensorであれば、入力と出力を任意に指定できます(途中でもいい)。Inception-v3では、バッチ処理に対応していない部分が少しあるのでそこを避けて使ってみます。

ヒントはC++のサンプルプログラムの中にあります。Inception-v3のモデル自体は最初が DecodeJpeg というテンソルで、最後が softmax という1000+αの判別のためのニューロンが並びます。しかし、C++のサンプルは入力には Mul というテンソルの出力を差し替えて行っています。(余談:C++だと、テンソルを入力にできるのですね。Pythonだと配列じゃないと与えられないのです。Operationクラスの_update_input()っていう内部メソッドで無理やり繋げることができるのですが、チェックとか一切入らないので上級者向けです。)

>>> print sess.graph.get_tensor_by_name('Mul:0')
Tensor("Mul:0", shape=TensorShape([Dimension(1), Dimension(299), Dimension(299), Dimension(3)]), dtype=float32)

この出力は、画像をデコードしてfloat32にして299x299の固定サイズにリサイズして、-1.0~+1.0に正規化したものです。 最初のディメンションが Dimension(1) ですね。ここで、バッチの余地が潰されています。この後ろのテンソルはバッチ対応のニューラルネットワークで、 実は、バッチ処理で入力するにはここ以降から行う必要があるわけです。 (しかもgpu対応していないオペレーションも手前にはある。resize_bilinear とか、cpu-gpu間のやりとりも最後まで無いほうがいい)

自分で、リサイズ&正規化して行列にして与えようとして次に問題になるのは結構最後のほうです。

W tensorflow/core/common_runtime/executor.cc:1102] 0x719ce20 Compute status: Invalid argument: Input to reshape is a tensor with 118784 values, but the requested shape has 2048
         [[Node: pool_3/_reshape = Reshape[T=DT_FLOAT, _device="/job:localhost/replica:0/task:0/gpu:0"](pool_3, pool_3/_reshape/shape)]]

softmaxの手前、pool_3 まではちゃんとバッチ処理できます。pool_3 を使いたい人はこれでいいのですが、一応理解を深めるためにsoftmaxまでバッチ処理が通るようにします。

Reshapeで次元整理(x,y座標がもう無いのでカットしている)するときに一緒にバッチで使う次元も1にしてしまっているみたいです。reshape(pool3, [1, 2048]) みたいな感じなのを、reshape(pool3, [-1, 2048]) みたいにすれば通りそうです。

。。。

ロード後に変更するのは面倒くさいから直接もとのファイルを変えることにします(!)。int32なので、"01 00 00 00 00 08 00 00" となっている箇所を "FF FF FF FF 00 08 00 00" とすればOKです。

はい。これで、バンバン299x299の画像をバッチ処理でsoftmaxテンソルにできるようになりました。候補領域を切り出して処理して位置特定もできるかも。

バッチ処理できないと、折角の良いGPUの性能を発揮できなかったので少し調べてみました。
皆さんもいじり倒してみましょう。