この記事は日経Robotics 有料購読者向けの記事ですが
日経Robotics デジタル版(電子版)』のサービス開始を記念して、特別に誰でも閲覧できるようにしています。
本記事はロボットとAI技術の専門誌『日経Robotics』のデジタル版です
著者の岡野原大輔氏
著者の岡野原大輔氏

 ディープラーニングはこれまでに例がない速さで研究開発とその実用化が進んでいる。それを支えているのが、ディープラーニングフレームワークと呼ばれるディープラーニングを実現するためのソフトウェアである。

 ディープラーニングは複雑であり、一から実装するのは非常に大変である。例えば、層の実装一つを取っても、順方向の関数と逆誤差伝搬時の逆方向の関数を正しく実装する必要がある。さらに、これらの層はメモリ消費量も少なく高速に処理できることが望ましい。

 こうしたフレームワークの実装をさらに難しくしているのが、それを動作させるためのハードウエア環境が多様なことである。学習時や大規模な利用時にはGPUを使い、バッチサイズ(訓練データを与える単位)が小さい場合やGPUを備えていないデバイスではCPUを利用する。携帯機器や組み込み機器では特別なハードウエアを利用する場合がある。多くのフレームワークでは、ユーザーは一度実装を書けば、設定を切り替えるだけでハードウエアを切り替え、その特性を活かせるようになっている。

Caffeでは設定ファイルが巨大化

 フレームワークは増大する要求に応じて進化をし続けている。表1に代表的なフレームワークを挙げた。以降でそれぞれの概要を紹介する。

表1 主なディープラーニングフレームワーク
表1 主なディープラーニングフレームワーク
[画像のクリックで拡大表示]

 Caffeは最初期に登場し、広く普及しているフレームワークである。米University of California Berkeleyのグループが開発しており、彼らが画像認識を研究していることから、Caffeも特に画像認識を得意としている。ネットワークや学習の方法は設定ファイルで指定する。これはプログラムと設定を別にすることで、プログラムを全く書かなくても様々な種類のネットワークや問題設定を試すことができるので、実験をする上では便利である。米Microsoft社が提供しているCNTKも、設定ファイルを使ってネットワークを記述する。

 一方で今のディープラーニングが利用するネットワークは非常に複雑になっており、設定ファイル自体が数千行と非常に巨大となってきている。こうした設定ファイルを人手で誤りなく書くのは困難であり、プログラムで設定ファイルを自動生成するようになる。そうなると、設定ファイルをプログラムと分離するというもともとの目的が達成されなくなる。また、問題が発生した場合にどこが間違っているのかを追跡しにくくなる。すべてプログラムで書いていれば、デバッグツールやデバッグ出力が利用でき、問題を追跡しやすい。

構造記述にはシンボル型と手続き型がある

 現在の多くのフレームワークが、プログラムの中でニューラルネットワークの構造や学習方法を記述するスタイルを取っている。プログラムの中でネットワークをどう書くかはシンボル型と手続き型の大きく2つに分かれる。

 シンボル型はシンボルがどのように変換されていくのかを記述することでネットワークを定義し、それをコンパイルした上で利用する。TheanoやTensorFlowがそれに当たる。ネットワーク全体を見てから処理を決められるので計算コストやメモリ消費量を抑え易い。処理をどこでどのように実行するかについても自由度があるため、分散処理と相性が良い。

 しかし、シンボル型はプログラム構文とは異なる構文を利用するため(言語内DSL)、ユーザーは新しい構文を覚える必要がある。また、何か問題が発生した場合、その問題を追求しにくい。例えば、途中の層の値がどうなっているのかを調べる、調べるために変えるといったことは自由にはできない。

 一方で、手続き型はネットワークの構造を前の層から順番に記述し、プログラムはそれを順番に実行し処理をする。TorchやChainerがそれに当たる。プログラミング言語の構文をそのままネットワーク構造の記述に使えるのでプログラマにとって扱いやすく、使いやすい。また、各層が順に実行されるので途中結果を調べて表示したり、変更したりできる自由度がある。一方で、最適化がかけにくく、シンボル型のフレームワークより計算コストが大きくなりがちである。

 各フレームワークはプログラマの生産性と実行性能とのバランスをとって、これらトレードオフの間に存在している。

 今のディープラーニングのもう一つの課題として、分散並列処理の問題がある。ディープラーニングの学習は計算量が大きい。このため並列で計算させることができれば、学習時間を大幅に減らし、結果として試行錯誤回数を増やせる。例えば、100万枚の画像からなるImageNetの一般物体認識タスクの学習は、何も工夫しないとすべてのデータを走査するのに3時間かかり、これを300周させるので900時間、つまり約1カ月強の時間を必要とする。

 一方で分散並列処理をサポートしているTensorFlowやFireCaffe(Caffeの分散対応版)では分散処理を実現することで数十倍の高速化が実現されており、1日以内での学習が可能となる。フレームワークではこうした分散並列処理の詳細をユーザーが意識することなく、利用できるのが望ましい。

 複数ノードでの分散処理は、ネットワークのハードウエアや分散のソフトウェア部分にどのような構成をとるかで場合分けする必要が多くあり、安定して性能を出しにくい。一方で、1ノード内での分散処理は、比較的バリエーションが少ないので分散処理を書きやすい。

 当社が開発しOSSとして公開しているChainerは手続き型として一番極端な立場をとっており、プログラム中で自由にネットワークを記述できるのが特徴である。これは、現在は新しいアイデアを速く試すことが重要であると考えているためだ。以下に、Chainerで3つの結合層から成るニューラルネットの定義と、それを使った学習の例を挙げる。

class MLP(chainer.Chain):
    def _init_(self, n_in, n_units, n_out):
        super(MLP, self)._init_(
            l1=L.Linear(n_in, n_units),
            l2=L.Linear(n_units, n_units),
            l3=L.Linear(n_units, n_out),
        )
    def _call_(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)
optimizer = optimizers.Adam()
optimizer.setup(model)
model.to_gpu()    # もしGPUを使うなら
model = L.Classifier(net.MLP(784, n_units, 10))
x = chainer.Variable(xp.asarray(x_train[i:i + batchsize]))
t = chainer.Variable(xp.asarray(y_train[i:i + batchsize]))
optimizer.update(model, x, t)

 初期化(_init_)ではパラメータを持った層(Link)の定義を行い、呼び出し(_call_)ではどのように入力が変換されるかを記述する。あとは各データをセットし最適化関数を呼び出せばよい。この例は最も簡単なニューラルネットの例だが、深層学習のアルゴリズムは通常、数十行から数百行の記述量で書ける。このため、新しいアイデアがあったとしても、それを数日で試すといったことが可能となってきている。

 今後しばらくはディープラーニングの進化に合わせて、フレームワークも激しく変わっていくことが予想される。

岡野原 大輔(おかのはら・だいすけ)
Preferred Networks 取締役副社長
岡野原 大輔2006年にPreferred Infrastructureを共同創業。2010年、東京大学大学院博士課程修了。博士(情報理工学)。未踏ソフト創造事業スーパークリエータ認定。東京大学総長賞。