Chainerモデルのさらなる高速化、デプロイの簡便化、可搬性の向上に向けた実験的な取り組みについて

Shinichiro Hamaji

2019-01-25 16:06:16

PFN のエンジニアの浜地です。入社以来取り組んできた実験的なプロジェクト Chainer-compiler を github で公開しました。まだ実運用に投入していける段階では無いですが、面白いものになってきているのではないかと思うので、紹介させてもらいたいと思います。

https://github.com/pfnet-research/chainer-compiler

昨年末、 PFN は ChainerX をベータリリースしました。 ChainerX は Chainer の使いやすさを維持しつつ、 Python で実装されていた部分を C++ 実装で置き換え、以下の3つを実現するものでした。

  • モデルの実行を高速化する
  • Python の無い環境でもデプロイ可能にする
  • CPU/GPU以外への移植を容易にする

Chainer-compiler プロジェクトは ChainerX を利用して、上記の ChainerX の目標をさらに押し進めるツールチェインを開発しています。現状、以下のようなコンポーネントからなります。

  1. Python 構文木を解釈して、計算グラフを、拡張された ONNX フォーマットとして取り出す
  2. 拡張 ONNX で表現された計算グラフを変換して、高速化や自動微分をし、コード生成をする
  3. ChainerX の C++ 部分を用いて、生成されたコードを実行する

想定している使い方の例をいくつか紹介すると

  • オペレーションが逐次実行される Chainer/CuPy/ChainerX の計算モデルと違って、いったん複数のオペレーションからなる計算グラフが作られるので、オペレーションをまたいだ命令融合や、データフォーマットの事前変換などの最適化ができる
  • 上記の1,2を事前に実行して、3の部分だけをデプロイ環境で実行することによって Python の無い環境でのデプロイが手軽にできる
  • 2のコード生成部のターゲットを追加することによって、特に静的グラフを前提としたモデル実行系や用途特化チップ(例えば MN-Core)での実行をサポートしていける
  • 2,3の部分のみを使うことによって、 ONNX-chainer や他の ONNX 生成系で作られた ONNX モデルを実行する。また、これに限らず、他のツールとの連携は、色々な組み合わせが考えられると考えています

などがあります。上記以外にも、色々と実験的なことをやっていきたいと考えています。

深層学習用コンパイラは、他の深層学習の領域同様、競争が激化していて、とても面白い領域だと思います。深層学習用コンパイラとひとことで言っても、用途や目標によって色々なものがあるのですが、 chainer-compiler プロジェクトでは Chainer のフレキシビリティをなるべく損なわない形でモデルをコンパイルすることを、重要な目標の一つとして考えています。そのため、次元が固定されていない tensor や Python の条件分岐・ループ、 Python のリストなども表現できるようにしています。多くの深層学習コンパイラは最適化に集中していて次元が静的であることを前提にしているものが多いので、この点は少しユニークな点になりえるのではないかと思っています。

この投稿では、 Chainer モデルをコンパイル・実行する実験的なプロジェクトについて紹介しました。結構広い領域について、色々とやりたいことがあって、楽しく作業しています。こういう作業に興味がある方は、ぜひ PFN への応募をご検討ください。この投稿では説明しきれなかった点も多くあります。技術的な話でも採用の話でも、なんでも疑問点がある方はご連絡いただければ、と思います。

最後に、 chainer-compiler プロジェクトに協力して下さっている全ての方々に感謝します。特に、「Chainer を使った Python コードを ONNX に変換する」というテーマに取り組んでもらった、インターンの佐藤さんの多大な貢献に感謝します。

インターン参加報告:Concolic Testing による SystemVerilog 向けテストパターン生成

Masahiro Sakai

2018-12-25 12:00:56

本記事は、2018年インターンシップに参加された押川さんによる寄稿です。


こんにちは。2018年夏季インターンシップに参加していた東京大学の押川広樹です。大学では定理証明支援系やモデル検査を用いたソフトウェアの検証について研究しています。

インターンでは、PFN で開発中のハードウェアに対して「良い」テストを実行することが目標でした。「良い」テストの基準は色々ありますが、今回は効率的で効果的なテストケースを生成することを目指しました。ここで言う効率的なテストとは、短い時間で実行できるように出来るだけサイズの小さいテストセットのことです。また、効果的なテストとはプログラムのより多くの部分を検査できるカバレッジの高いもののことです。

今回はそのようなテストケースを Concolic Testing と呼ばれる手法に基づいて生成することを試みました。

Concolic Testing

まず、テストというとランダムテストが考えられます。ランダムテストは手軽ですが効果的だとは言えません。例えば以下のようなコードを考えてみます。

int x, y;
if (x == y)
  if (x == 10)
    fail;
  else
    ...
else
  ...

上の例は xy が共に 10 の時だけバグがあるプログラムを表します。ランダムテストによってバグを見つけられる確率は単純計算で (int が 32 ビットの時) 232 × 232 回に一回です。

このためランダムテストはコーナーケースになるような微妙な部分のテストには向いておらず、高いカバレッジをとるためにはもう少し賢くテストケースを作る必要があります。その一つの例が Concolic Testing と呼ばれる手法です。Concolic は Concrete と Symbolic を組み合わせた言葉で、Concolic Testing とはプログラムの実際の実行とシンボリックな実行を交互に行うテスト手法です。

Concolic Testing では次の手順でテストを進めて行きます。

  1. ランダムな入力を生成する
  2. 入力に対して プログラム を実行する
  3. 実行時にプログラムのどの部分が実行されたかを記録しておく
  4. 3. の情報を元にその部分が使われるための条件を計算する
  5. 4. の条件の一部を変更し、プログラムの別の部分が実行されるための制約を得る
  6. 5. の制約を論理ソルバーで解くことで、実際にその部分を使うような入力を得る
  7. 2.-6. を繰り返す

各イテレーションごとにプログラムの新しい部分を実行するような入力が得られるので、理想的にはラインカバレッジ100%のテストケースを生成することが出来ます。

ランダムテストと同じ例でどのように動作するかをみてみます。
プログラムは if 文などで分岐する木だと思えるので、上の例は次のように表せます。

まず、xy の値をランダムに生成します。例えば x = 1163551168y = 1363922006 だとします。この下でプログラムを実行すると以下のようなパスを通ります。

このパスを通ったのは、x == y が成り立たなかったからです。したがって制約としてこの条件の否定、つまり x != y を考えます。これを満たすような xy をソルバーを使って求めます。
例えば x = 0y = 0 はこの制約を満たします。次はこれを入力としてプログラムを実行します。次のようになります。

確かに1度目の実行とは異なる部分が実行されています。先ほどと同様、このパスを通ったことから、「x == y かつ x != 10」が成り立つとわかります。x != 10 を否定した制約、「x == y かつ x == 10」をソルバーを用いて解くことで次の入力 x = 10y = 10 を得ます。そして、x = 10y = 10で実行することでちゃんと fail を見つけることができます。

Concolic Testing は Godefroid らによる DART (Directed Automated Random Testing) [1] によって導入されました。[1] では Concolic Testing の手続きの導入と、Cで書かれたプログラムに対しての実装の実験がなされています。

上で説明した Concolilc Testing の手続きを素朴に行うと、対象のプログラムが大きくなるにつれてたどる必要のあるパスの数が爆発するためスケールしません。また、生成される制約が大きくなりすぎてソルバーで制約が解消できなくなる可能性もあります。

そのため実用上は、できるだけ早くカバレッジを増やすようにパスをたどったり、生成される制約を小さくする最適化を加えることで、実際のアプリケーションにも適応できるように改良されています。実際、DART にこのような最適化を加えたものに SAGE [2] があります。SAGE は X86 アセンブリを対象にしており、Microsoft で Office のバグを見つけるのに使われたようです。

また、対象をハードウェア設計言語にしたものに HYBRO [3] があります。HYBRO は Verilog を対象とします。

やったこと

今回は SystemVerilog で記述されたプログラムに対して Concolic Testing を行うフレームワークを作成しました。実装時間の都合で、受け入れられるプログラムの種類は限られていています。具体的には、ある程度制限された SystemVerilog の機能を用いて書かれた組み合わせ回路になります。例えば、interface が使われていないことなどを仮定しています。

テストケース生成の手続きの全体像は以下の図のようになります。

大まかには上で説明した手順と同じです。

  1. まず、SystemVerilog のソースコードをパース・解析して always 文ごとに Control Flow Graph (CFG) を作ります。この際にプログラムが期待する入力の情報(名前や型)を得ます。
  2. 次に、得た情報を使って最初の入力をランダムに生成し、シミュレータを用いて実行します。シミュレータは実行時の各変数の値の情報を Value Change Dump (VCD) というフォーマットで出力します。
  3. VCD には各タイミングでの変数の値の情報が入っているのでこれを元に CFG のどのパスが実行されたかを計算します。
  4. そこからその実行が行われるための条件を求めて、まだ実行されていない部分を次に実行するような入力を生成するのための制約を求めます。
  5. この制約をSMT ソルバーで解くことで次の入力を得ます。
  6. このプロセスをラインカバレッジが100%になるまで繰り返します。

SMTソルバーは Z3 [4] を用い、その他は OCaml によって実装しました。Z3 の API は OCaml から利用することができ便利です。

実装は https://github.com/pfnet-research/ATPG4SV で公開しています。

結果

300行程度の比較的小さめの回路に対して実行したところ10イテレーションほどで100%のカバレッジになるテストケースが得られました。

まだ実装が不完全なので対象に制限がありますが、実際の回路に対して動作させることができました。

まとめ

効率的で効果的なテストケースを生成する手法として Concolic Testing を紹介しました。また、インターンで取り組んだ、SystemVerilog で記述されたハードウェアへの適用例を紹介しました。まだ実用的に役に立つレベルのものではないですが、プロトタイプとして面白いことができたと思います。

最後になりましたが、メンターの酒井さんと Kühn さんには、テーマ決め、自分の知識が少なかったハードウェアに関する質問、発表練習、そして休憩中の雑談、とインターン期間を通して大変お世話になりました。ありがとうございました。

参考文献

  • [1] P. Godefroid, N. Klarlund, and K. Sen. DART: Directed Automated Random Testing
  • [2] P. Godefroid, M. Levin, and D. Molnar. Automated Whitebox Fuzz Testing
  • [3] L. Liu and S. Vasudevan. Efficient validation input generation in RTL using hybridized source code analysis
  • [4] The Z3 Theorem Prover https://github.com/Z3Prover/z3

おまけ:メンターより

押川さんのメンターを担当した、PFNの酒井とKühnです。

PFN ではディープラーニングを高速化する専用プロセッサー MN-Core™(エムエヌ・コア)の開発も行っていますが、チップ設計の検証は大きな課題の一つでした。 今回の取り組みは、この課題への取り組みとして始まった実験的なプロジェクトであり、押川さんには短い期間で、Coqによる部分的な証明などのアプローチも含めた複数のアプローチを検討のうえ、Concolic Testing に基づいたアプローチを選択し、プロトタイプを実装して実際の回路を対象にテストパターン生成のループを回せるところまで開発していただきました。(注: 既存の回路を対象に実験したものであり、現時点でのMN-Coreの実際の開発に適用しているわけではありません。)

PFNは上から下まで様々なことに取り組んでいていますが、まだまだ手が回っていないことも沢山あり、そこに興味のあるインターンの学生さんが来てくれたことをきっかけに新たな取組みが始まることがしばしばあります。今回のプロジェクトはそのような取り組みの一例でもあります。我こそはという学生の皆さんは、ぜひ来年のPFNインターンシップへの応募をご検討ください。 また、もちろん中途・新卒の人材募集も通年で行っていますので、こちらも興味のある方はぜひご検討ください!PFNの人材募集のページはこちら https://www.preferred-networks.jp/ja/jobs です。(FPGA/ASIC開発の人材も募集しています)

分散深層学習とモデル並列性

Kota Uenishi

2018-12-21 15:29:18

(本記事は、2016年インターンシップを経て現在はアルバイトとして勤務されている包さんによる寄稿です)

はじめまして。Preferred Networksの分散深層学習チームでアルバイトをしている包です。私は分散深層学習の中でも主にモデル並列に関する機能実装を行っています。今回はモデル並列性の概要と、ChainerMNにおいてどのようにモデル並列性を実現しているのかについて紹介します。

分散深層学習: データ並列性とモデル並列性

深層学習における各種フレームワークは目覚ましい発展を遂げ続けており、最近では一般ユーザーでも簡単に複数GPUを用いたニューラルネットの訓練ができるようになってきました。たとえば、ChainerMNではoptimizerの定義にほんの数行加えるだけでニューラルネットを複数GPUで訓練できます[1]。これにより1024GPU上でImageNetによるResNet-50の学習を15分で行うなどの実績を上げています[2]。このような複数プロセス、複数ノードを用いた分散深層学習によってニューラルネットの訓練は高速に行えるようになっており、分散深層学習は現在の深層学習の基盤を支えているといえます。

ところで、「分散深層学習」にはデータ並列とモデル並列という2通りのアプローチがあることが知られています[3]。データ並列では、全プロセスに同じモデルのコピーして訓練することでバッチサイズをプロセス数倍し、学習を高速化させる手法です。先程お話したImageNetの並列訓練もデータ並列による高速化の一例です。一方でモデル並列とは、1つのモデルを分割して複数のプロセスに配置し、全プロセスで協調して1つのモデルを訓練する手法です。主なユースケースとしては超解像度を入力とするCNNやMixture of Experts[4]など、1プロセス上に載りきらないサイズのモデルを訓練したい場合に用いられます。最近ではMesh-Tensorflow[5]というTensorflow用のモデル並列ライブラリが公開されましたが、現状ではモデル並列をサポートしているフレームワークは非常に少ないです。

この記事では、ChainerMNに実装されているモデル並列APIを、実例を交えて紹介します。特に、Define-by-Runとともにモデル並列を実現する際に発生する問題と、その解決方法について重点的にお話をします。

ChainerMNにおけるモデル並列性の実現

ChainerMNでは、通信をChainerの関数呼び出しによって定義 します。これにより非常に柔軟な通信パターンを実現することができます。

図1: 関数呼び出しによる通信の定義の例

ChainerMNにおける通信はMPIを用いて実現されており、モデル並列でも基本的にMPIの通信スタイルを踏襲しています。MPIでは大きく分けて MPI_Send を始めとした1対1通信と、 MPI_Bcast のような集団通信向けのAPIが提供されています。ChainerMNでは、これらの通信APIと対応するように chainermn.functions.sendchainermn.functions.bcast のように、Chainerの関数を提供しています。通信用の関数は、それぞれbackwardにおいて「勾配を逆向きに通信」するように設計されています。例えば bcast の場合、forward計算ではmasterからslaveに対して入力変数がbroadcast通信されます。一方で、backward計算ではslaveからmasterに対して勾配をallreduceします。

ChainerMNに実装されているforward通信に対応するbackwardの通信パターンは以下のようになります。

表: forward と backward における通信パターンの対応

forward backward
allgather allgather
alltoall alltoall
bcast allgather
gather scatter
scatter gather
send recv
recv send

 

次に、モデル並列APIの具体的な使い方について見ていきます。まず、データ並列の際と同様に、通信を行うためのコミュニケータを作成します。

comm = chainermn.create_communicator()

例えば、図1のようなモデルの実装イメージは次のようになります(図1のモデルに特に意味はありません)。

class ExampleModel(chainer.Chain):
    def __init__(self):
        self.comm = chainermn.create_communicator()
        self.conv = L.Convolution2D(...)

    def forward(self, x):
        x = chainermn.functions.bcast(self.comm, x)
        h = self.conv(x)
        y = F.relu(h)
        ys = chainermn.functions.gather(self.comm, y)
        ...

この例では、masterからブロードキャストされた変数が Convolution2D の入力になります。一方で、backward計算の際には、 Convolution2D の勾配が自動的に Bcast のbackwardによってmasterへ集約されます。

ChainerMNに用意されているAPIの詳細については、ドキュメントを参照してください[6]。なお、モデル並列関連のAPIに関しては現状では実験段階なので、将来的に後方互換でないAPIの変更が起こる可能性があります。

Define-by-Runにおける注意点(その1)

ChainerをはじめとしたDefine-by-Runによる計算グラフの定義はモデルを直感的に記述することができる点で優れているといえます。backward計算時には、出力変数からグラフのバックトラックを行うことによってパラメータの更新を行うことができます。しかし、モデル並列を実現するために上述のように通信を関数として定義すると、計算グラフが正しくバックトラックできない状況が発生します。

例えば、下記のような2つのプロセス間におけるシンプルな1対1通信の例を考えます。

図2: 1対1通信の例

「Process #0」に注目してみると、出力変数 y からバックトラックを行ったときに、 recv から send へ戻ることができません。その結果、「Process #1」は recv のbackward(すなわち勾配のsend)を呼んでいるにもかかわらず、「Process #0」は send のbackward(すなわち勾配のrecv)を呼ぶことができず、デッドロックが発生します。このような状況は、1つのプロセス上における計算グラフが非連結になっているときに生じます。そのため、 send 関数が戻り値として返す特別な変数を recv に渡すことによって、 計算グラフが連結になるようにモデルの定義を行います

図3: delegate variableによる計算グラフの連結化

このような sendrecv を繋ぐような send 関数の戻り値を、便宜的に「delegate variable」と呼ぶことにします。Delegate variableは「Process #0」においてグラフを連結にする役割を果たす他に、「Process #1」でもバックトラックの起点となるダミーの出力変数として振る舞います。図3をコードで記述すると以下のようになります。

class ExampleModel_0(chainer.Chain):
    def forward(self, x):
        # first component
        z = f(x)
        phi = chainermn.functions.send(z, comm, rank=1)

        # second component
        z = chainermn.functions.recv(comm, rank=1, delegate_variable=phi)
        y = h(z)

        return y

class ExampleModel_1(chainer.Chain):
    def forward(self, _):
        z = chainermn.functions.recv(comm, rank=0)
        z = g(z)
        phi = chainermn.functions.send(z, comm, rank=0)
        return phi

Define-by-Runにおける注意点 (その2)

先程の節ではグラフが非連結になると計算グラフのバックトラックができない例を1つ挙げました。このような例は他にも存在します。

 

図4: 1対1通信を2回呼ぶ例

 

図4では、1対1通信が2回発生しています。この場合、「Process #0」における send が返す2つのdelegate variableを適切に処理する必要があります。そこで、以下のように2つの変数を1つにまとめる処理を行います。

 

図5: pseudo_connectを用いた例

 

chainermn.functions.pseudo_connect という関数は、「delegate variableがあたかも別の変数であるかのように振る舞うような変数」を返す関数です。図5の例では、 \( \phi_1 \) というdelegate variableが実際には \( \phi_2 \) という別の変数として振る舞うような変数 \( \psi \) を返します。 \( \psi \) をバックトラックする際には、まず \( \phi_1 \) のバックトラックを行い、次に \( \phi_2 \) のバックトラックを行います。このようにして、backward計算の際に2つのdelegate variableを正しくトラックバックすることができます。図5をコードで記述すると次のようになります。

class ExampleModel_0(chainer.Chain):
    def forward(self, x):
        z1, z2 = f(x)
        phi1 = chainermn.functions.send(z1, comm, rank=1)
        phi2 = chainermn.functions.send(z2, comm, rank=1)
        psi = chainermn.functions.pseudo_connect(phi1, phi2)
        return psi

class ExampleModel_1(chainer.Chain):
    def forward(self, _):
        z1 = chainermn.functions.recv(comm, rank=0)
        z2 = chainermn.functions.recv(comm, rank=0)
        y = g(z1, z2)
        return y

図5では pseudo_connect で2つのdelegate variableをまとめましたが、次の図6のように通常の変数にdelegate variableを結合することも可能です。

図6: delegate variableと通常の変数を結合する例

 

y_ = chainermn.functions.pseudo_connect(phi, y)

以上がChainerMNにおけるモデル並列の概要になります。次に、実際のモデルの例を見てみます。

1対1通信を用いた例: encoder-decoderモデル

Encoder-decoderモデル[7]は可変長の入力を可変長の出力に変換することを目的としたモデルで、自然言語処理をはじめとした応用分野で広く用いられています。
Chainerのexampleにも機械翻訳の例があります[8]。Encoder-decoderモデルの入力や出力に画像を用いるようなモデルの場合、CNNをencoderやdecoderに用いることになりますが、層数やパラメータ数が膨大なencoderやdecoderになると、全体のモデルが1GPUに載らないケースが発生します。その場合、モデルをいくつかに分割して複数プロセスでモデル並列学習を行うことによって学習できます。例えば、下図のようにencoderとdecoderにそれぞれ1プロセスずつ割り当てるような分割が考えられます。

図7: encoder-decoderのモデル並列化

 

ここでは、はじめのプロセスでencodeしたcontext vectorをdecoderへ送信して、decoder側のプロセスでdecodeするように分割を行っています。例えばLSTMの場合はcontext vectorが2つあるので、2回の1対1通信を行うことで実現できます。ただし、図5の例と同様に、encoder側では pseudo_connect を用いてdelegate variableを1つにまとめる必要があることに注意してください。基本的には sendrecvpseudo_connect を用いれば実装することができますが、encoder-decoderモデルの分割は実装が煩雑になるので、専用のLinkを用意しています。

rnn = chainermn.links.create_multi_node_n_step_rnn(
        L.NStepLSTM(n_layers, n_units, n_units, 0.1),
        comm, rank_in=None, rank_out=1)

create_multi_node_n_step_rnn は、Chainerで提供されている NStepRNN [9](可変長系列をまとめて入出力するAPI)をラップして、内部で別のプロセスと自動的にcontext vectorを送受信します。rank_in に指定したプロセスからcontext vectorを受信し、 rank_out に指定したプロセスに対してcontext vectorを送信します。これを用いると、次のようにモデル並列なencoder-decoderモデルを簡単に実装することができます。

class Encoder(chainer.Chain):
    def __init__(self, comm, n_layers, n_units):
        super(Encoder, self).__init__(
            # Corresponding decoder LSTM will be invoked on process 1.
            mn_encoder=chainermn.links.create_multi_node_n_step_rnn(
                L.NStepLSTM(n_layers, n_units, n_units, 0.1),
            comm, rank_in=None, rank_out=1
            ),
        )
        self.comm = comm
        self.n_layers = n_layers
        self.n_units = n_units

    def forward(self, *xs):
        exs = f(xs)
        c, h, _, phi = self.mn_encoder(exs)
        return phi

class Decoder(chainer.Chain):
    def __init__(self, comm, n_layers, n_units):
        super(Decoder, self).__init__(
            # Corresponding encoder LSTM will be invoked on process 0.
            mn_decoder=chainermn.links.create_multi_node_n_step_rnn(
                L.NStepLSTM(n_layers, n_units, n_units, 0.1),
            comm, rank_in=0, rank_out=None),
        )
        self.comm = comm
        self.n_layers = n_layers
        self.n_units = n_units

    def forward(self, *ys):
        c, h, os, _ = self.mn_decoder(ys)
        ...

この例はChainerMNのexampleに公開されています[10]。

集団通信を用いた例: チャネル方向の並列化

集団通信を用いると、下図のようにCNNのチャネル方向の並列化が実現できます。この並列化は高解像度画像を扱う際や、バッチサイズを大きくする際に有用です。

図8: チャネル方向の並列化

 

各プロセスはCNNのチャネルのうち一部だけを入力としてとって畳み込みを行うので、各々のプロセス上のCNNのパラメータ数を減らすことができます。CNNの出力に対して “allgather“ を用いることで、全チャネルを集約することができます。実装のイメージは以下のようになります。

class ParallelConvolution(chainer.Chain):
    def __init__(self, comm, in_channels, out_channels):
        self.comm = comm
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.conv = L.Convolution2D(...)

    @property
    def _indices(self):
        # index % comm.size == comm.rankとなるインデックスのチャネルを担当
        # 例 (size=4, rank=1の場合): _indices = [1, 5, 9, ...]
        idx = numpy.arange(self.in_channels)
        return idx[idx % self.comm.size == self.comm.rank]

    def forward(self, x):
        # 当該プロセスの担当チャネルをスライス
        x = x[:, self._indices, :, :]

        y = self.conv(x)

        # 全チャネルを集約
        ys = chainermn.functions.allgather(self.comm, y)
        return F.concat(ys, axis=1)

この例はchainerMNのexampleに公開されています[11]。

まとめ

本記事では、ChainerMNにおけるモデル並列の実現と、実際の例をいくつか紹介しました。特に、Defined-by-Runの下では計算グラフが連結でなければならないため、delegate variableやpseudo_connectなどのテクニックが必要になります。今回はスペースの都合で紹介がかないませんでしたが、特定のタイプのモデル向けによりシンプルにモデルを定義できるようなAPI( MultiNodeChainList, MultiNodeNStepRNN)も用意されているので、お手軽に試してみたい方はぜひドキュメント[6]をご覧ください。

参考文献

Google Colaboratoryを用いた機械学習・深層学習の入門教材を無料公開(健康・医療向けデータを用いた実践編も含む)

Shunta Saito
リサーチャー

2018-12-20 13:13:56

PFNのリサーチャの齋藤です。今年は色々な仕事に取り組みました。本記事では、日本メディカルAI学会が新しく始める公認資格へ向けたオンライン講義資料について書きます。

昨今、機械学習や深層学習といった技術はIT企業のみならず様々な分野で活用されるようになってきました。その一つに医療分野があります。しかし、忙しい臨床医・研究医・その他医療従事者の方々の中には機械学習や深層学習の可能性を知りつつも、なかなか自ら手を動かして学び、それを医学の研究や医療の現場へ生かしていく時間がとれない方もいらっしゃいます。その大きな理由の一部には、特に深層学習を実践的に用いる方法を学ぶ場合に必要となる計算機環境の用意および環境構築が難しいといった点があります。

そこで、PFNは、Google Colaboratory(以下、colab)を用いた学習教材を作成しました(なお、資料の一部は株式会社キカガクの協力を得て執筆されています)。本資料は、日本メディカルAI学会公認資格:メディカルAI専門コースのオンライン講義資料として作られたものですが、メディカルAI学会所属でない方にも自由にご覧いただけるよう、全ての資料をウェブで無料公開しています。colabを用いると、Googleアカウントさえあれば誰でも無料でGPUが有効な環境の上でPythonコードを実行することができます。従来は、GPUを搭載したコンピュータを自前で用意したり、また用意できたとしても様々な深層学習フレームワークを動作させられるように環境構築を行う部分などでつまづいてしまうケースも多くありましたが、colabを用いればこれらの行程をスキップして本質的に重要なコーディングの部分から学び始めることができます。

今回の学習資料は、全てcolab上で執筆を行い、colabから直接GitHubへ.ipynb形式のファイルをpushし、Pythonパッケージのnbsphinxを用いてそれをHTMLへ変換してドキュメントサイトを構築するというワークフローで作成されました。また、colabで書いたプログラムが実行時に生成したファイルは、簡単にGoogle Driveへコピーすることができるため、学習の結果得られた成果物を再利用するといったことも行いました。具体的には、本資料は1章が2〜3時間で終えられるように作られていますが、中には深いニューラルネットワークの学習に必要な時間だけで数時間かかってしまうものもあります。そういった場合には、あらかじめ筆者らで途中までcolab上で学習を行って学習途中のスナップショット(ある時点のネットワークの重みなどをファイルに保存したもの)を作成しておき、記事中ではそれをダウンロードしてきて途中から学習を再開するという形にすることで、資料中のコードを実行したあと待機する時間を減らしつつ、実際に学習が行われる様子を体験することができるよう工夫しています。

本資料は全8章からなり、機械学習に必要な数学の基礎から深層学習を用いたMRI画像のセグメンテーション、血液の顕微鏡画像からの物体検出、DNA配列解析、心電図の信号波形データの時系列解析といった具体的な応用まで、その背景にある理論の概説から実行可能なコードを用いた実践的な解説まで広く扱っています。目次は次の通りです。

1章:機械学習に必要な数学の基礎

1章では、深層学習に限らず、機械学習の様々な手法を学んでいく際に必須となる微分の知識、線形代数の基礎、そして確率・統計の基礎について、最低限の知識をおさえるために簡潔にまとめています。

2章:機械学習ライブラリの基礎

2章では、機械学習や深層学習の領域では広く用いられているPythonと、その代表的な数値計算ライブラリであるNumPyに慣れるために、重回帰分析をNumPyのみを使って実装する方法をコードを実行しながら学んでいけるようになっています。また、様々な機械学習アルゴリズムを実装しているScikit-learnというライブラリの使い方も紹介しています。

3章:ニューラルネットワークの基礎

3章では、ニューラルネットワークの基礎について、図や動画を用いてできるだけ分かりやすく說明しました。NumPyを用いて誤差逆伝播法(バックプロパゲーション)を実装し、colab上で実行してみることで、具体的な数値を見ながらニューラルネットワークの学習の仕組みを理解することができます。

4章:Deep Learningフレームワークの基礎

4章では、より複雑なニューラルネットワークを扱いやすくするために深層学習フレームワークの一つであるChainerを用いた画像分類の問題に取り組みます。ここからは、colab上でGPUを使ったより実践に近いプログラムを実行することで深層学習を活用していく際のエッセンスを効率よく学びます。

5章:実践編: MRI画像のセグメンテーション

5章では、心臓MRI画像の中から左心室の領域を抽出するセマンティックセグメンテーションのタスクに取り組みます。Chainerを用いて色々なニューラルネットワークを実装し、そのパフォーマンスの違いを見てみます。ChainerCVという画像を取り扱う場合に便利なChainerの追加パッケージの使い方も簡単に說明しています。

6章:実践編: 血液の顕微鏡画像からの細胞検出

6章では、血液の顕微鏡画像の中から赤血球、白血球、血小板の3種類の物体を検出する物体検出タスクに取り組みます。物体検出のためにデザインされたニューラルネットワークの中から代表的なものをいくつか概説し、ここではSingle Shot Multibox Detector (SSD)という手法を使って自ら用意したデータセット(ここでは血液の顕微鏡画像)を用いるモデルの訓練方法を解説しています。本章でもニューラルネットワーク自体の実装コードの転用や評価のためにChainerCVを活用しています。

7章:実践編: ディープラーニングを使った配列解析

7章では、DNA塩基配列を入力として受け取り、配列中の長距離相互作用を考慮した上でDNA塩基配列と特定の転写調節因子の結合可能性を予測するために1次元Dilated Convolutionを使ったニューラルネットワークを訓練する方法を說明しています。

8章:実践編: ディープラーニングを使ったモニタリングデータの時系列解析

8章では、心電図の信号波形データを入力として、不整脈を検出するという時系列解析の問題に取り組んでいます。この章では1次元Convolutionと1次元Dilated Convolutionを使った教師あり学習による時系列データの分類方法について解説しています。

本資料を通じて、機械学習や深層学習の分野について学び始める方が増えることを期待しています。

また、本資料のうち1章〜3章の作成にあたっては株式会社キカガクの吉崎様にご協力をいただきました。この場を借りてお礼申し上げます。そしてGoogle Colaboratoryがなければこのような文章を中心としつつも実行可能なコードを埋め込むことで実践的な資料としても成り立たせることはできませんでした。合わせてお礼申し上げます。

【関連リンク】

日本メディカルAI学会:https://www.japan-medical-ai.org/
株式会社キカガク:https://www.kikagaku.co.jp/
深層学習フレームワークChainer https://chainer.org/

インターン参加報告:確率的なプログラムの型による静的検証

Masahiro Sakai

2018-12-19 12:00:32

本記事は、2018年インターンシップに参加された南條陽史さんによる寄稿です。


はじめまして。PFNの2018夏季インターンシップに参加した筑波大学の南條陽史です。 大学ではプログラム言語について研究しており, 型理論やプログラム検証に興味があります。

今回のインターンで“確率的なプログラムの型による静的検証”というテーマで研究開発を行いましたので、その紹介をいたします。

テーマの説明

確率的なプログラムの検証

確率的プログラム、というと色々な意味がありますがここでは確率分布からのサンプリングといった確率的な挙動を含むプログラムを指します。 研究の目的はこの確率的プログラムに対して“悪いことが起きる確率はいくら以下である”とか“よいことが起きる確率はいくら以上である”といった確率についての仕様検証をすることです。

確率的なプログラムの仕様検証の応用例として以下が挙げられます。

  • 確率的に生じる観測誤差によってシステムが誤った判断をしてしまう確率がしきい値以下であることの検証
  • クライアントに仕事を割り振るシステムについて、あるクライアントに捌き切れないほど多くの仕事を流してしまう確率がしきい値以下であることの検証

このように確率的なプログラムの検証は現実的な問題に直接結びついています。

型による検証

僕のテーマはこの確率的なプログラムの検証を“型”主導で行うというものです。 型というのはintとかcharとかstringとかのあの型です。 プログラムの仕様としての型という考えを推し進めると、例えば以下のように詳細な仕様を型として記述できます。

rand(0, 5) : {x:int|Prob(x%2=0) = 1/2}

これは0から5の整数を一様分布からサンプリングするプログラム (rand(0, 5)) に対して, その値が偶数である確率は1/2だという仕様 (Prob(x%2=0) = 1/2) を与えています。

型主導で検証することの利点として一般に以下が挙げられます

  • 再利用性 : 検証が合成的なのであるプログラムの検証の結果をより大きなプログラムの検証に再利用することができます
  • 網羅性 : 型による検証は数学的な証明に対応するので単体テストなどと違ってカバレッジ100%です
  • 静的 : 検証がプログラムのコンパイル時に行われるので実際にプログラムを実行する必要はありません

既存研究

既存の確率的なプログラム検証のツールに確率的モデル検査器PRISM[1][2] があります。 これは確率的な状態遷移系に対して、悪い状態に到達する確率が十分低いことや良い状態に到達する確率が十分高いことを検証できます。

しかしPRISMを使うためには確率的状態遷移系を書き下す必要があり、人間が読み書きするには不向きでした。 一方で今回のテーマでは検証の対象をプログラムという形で与えることで、関数などのプログラム言語が持つ抽象化の手段を使うことにより検証の対象を人間が読みやすく記述することを可能にしています。

実装

インターンでは確率的なプログラムを記述できる小さなプログラミング言語のインタプリタとその型検査器を実装しました。ソースコードは[3]で公開しています。 この型検査器では型付可能性問題を既存ツールPRISMが扱える妥当性判定問題に帰着させて解いています。

実装したプログラミング言語では例えば次のプログラムの型検査が可能です。

(let a = rand(-10, 10) in
let b = a + rand(0, 10) in
 not (b - a >= 10) /\ ((b + rand(0, 5)) - (a + rand(0, 5)) >= 10))
 : {x:bool | Prob(x) <= 1/10}

このプログラムは“GPSに基づいてスピード違反を検出するシステムが観測誤差によって誤った判断をする確率がしきい値以下”であることの検証を簡単にモデル化したものです。

詳細に説明すると、 a はある時点での自動車の真の座標で b は微小時間後の自動車の真の座標として、 実際にはスピード違反していない (not (b - a >= 10)) のに観測誤差によってスピード違反と判定される ((a + rand(0, 5)) - (b + rand(0, 5)) >= 10) 確率が1/10以下である (Prob(x) <= 1/10) と言っています。

このプログラムと同等な検証をPRISMを使って行うと確率的状態遷移系は 1460 bytes ほどの非直感的なコードになり、確かに実装した言語上の方が簡潔にわかりやすく記述できることがわかります。

実装したプログラミング言語は一様な離散分布からのサンプリングのみを考えており言語機能も大きく制限されているので、現実的な問題を考えるためにはいくつかの機能拡張をする必要があります。 つまり

  • 連続分布からのサンプリング
  • 高階関数
  • 条件付き確率

などです。 考えるべきことはまだまだ残っているので今後も開発を進めていきたいと考えています。

謝辞

インターンが始まった頃は機械学習について全く詳しくなかったのですが、メンターの方が僕にレベルを合わせてかなり初歩的なことから丁寧に説明をしてくださったのでとても助かりました。 メンターの方々以外にも多くの社員さんと交流する機会が用意されており、どの方のお話もとても楽しかったです。 社員の方々だけでなくインターン生にも優秀な方が多くとても刺激的で心地よい夏休みを過ごせました。 皆様に心から感謝いたします。貴重な経験をありがとうございました。

参考文献


おまけ:メンターより

南條さんのメンターを担当したPFNの吉川と酒井です。

PFNは機械学習/深層学習そのものの研究開発のイメージが強いとは思いますが、フレームワークの開発などにおいてはプログラミング言語理論も含め幅広い分野の知識が必要となります。 今回南條さんに取り組んでもらったのも、現時点では非常に簡単なものではありますが、プログラミング言語理論的な知見を確率モデリング等に活かしていけないかと考えての試みです。お互いのバックグラウンドとなる知識の違いから苦労した部分もありましたが、最終的には簡単な言語の設計と実装までこぎつけることができました。

これに限らず、機械学習・深層学習にはプログラミング言語理論などで解決できる課題がまだまだあるのではないかと考えています。それらに興味をお持ちの皆さんも、ぜひ来年のPFNインターンシップへの応募をご検討ください。 また、もちろん中途・新卒の人材募集も通年で行っています。興味のある方はぜひご検討ください!PFNの人材募集のページはこちら https://www.preferred-networks.jp/ja/jobs です。

ChainerとTensorRTの間をつなぐchainer-trtの公開

Daichi Suzuo
エンジニア

2018-12-13 17:00:28

この度、Chainerで開発したモデルをNVIDIAの推論エンジンTensorRTに変換しNVIDIA GPU上で高速に推論するための実験的ツールchainer-trtをOSSで公開しました。この記事ではその概要、開発の背景と位置づけを簡単に紹介したいと思います。

https://github.com/pfnet-research/chainer-trt.git

はじめまして。PFNエンジニアの鈴尾(すずお)です。いつも同じアバターを使っているので、社内や社外でお魚さんと呼んでいただけることもあります。

深層学習技術の急速な発展に伴って、いよいよエッジへの推論器のデプロイという形での実用化が進んできました。学習時の速度がモデル開発の効率に直結するように、推論時の速度は製品に載せるハードウェアのコストに直結するため、高速に推論できることは極めて重要です。ここで推論とは、ニューラルネット(NN)の順伝搬処理のみを実行し解釈可能な出力(例えば、物体検出の結果など)を得るプロセスを指します。Chainerは一般に非常に高速な深層学習フレームワークとして知られ、また学習に用いたコードをほとんどそのまま推論に用いることができます。しかしながら、特定の種類のデバイス上で、入力の大きさやアーキテクチャおよびパラメータが固定されたNNの順伝搬処理のみに注目した場合、最適化の余地はまだまだあります。

NVIDIAのTensorRTはこのような需要に対応するため開発された推論エンジンの一つです。
Deploying Deep Neural Networks with NVIDIA TensorRT
How to Speed Up Deep Learning Inference Using TensorRT | NVIDIA Developer Blog

TensorRTは完全に静的なNNと固定のデバイス(GPU)を前提とし、あらかじめ深さ方向ないしは幅方向に隣接する層を可能な限り統合するなどの計算グラフレベルの最適化、指定されたGPU上で最も実性能の良いCUDAカーネルを計測に基づいて自動選択するなどの実装レベルの最適化、計算グラフ中の破棄可能なメモリ領域を同定し再使用するなどのメモリレベルの最適化、および16bit浮動小数点数や8bit整数を用いた低精度計算へのpost-training変換などを施します。これをビルド段階と呼びます。

推論時は、このビルド段階で構築した実行計画に基づいて処理を行うだけであるため、NVIDIA GPU上で極めて高速に順伝搬処理を行うことができます。

これまでChainerで開発・学習したモデルをTensorRT推論器に変換する仕組みはONNXを介する方法しかありませんでした。この場合少々複雑なTensorRTのC++ APIを理解し、CPUおよびGPU上の生ポインタを責任持って管理するなどの作業が生じるため、これらを可能な限り吸収しかつChainerからTensorRT化をスムーズに行うことができないかを検証するべく、実験的ツールchainer-trtを開発しました。

一言でまとめるとchainer-trtは、TensorRTのC++ APIを用いてTensorRTにNNの構造とパラメータを教え最適化を走らせることによってChainerのモデルをTensorRT推論器に変換し、またそれを実行する、といった作業を行うための薄いラッパーです。

chainer-trtの仕組み

chainer-trtを使う上では、大きく3段階の作業が必要です。

1段階目は、PythonによってChainerの流儀で書かれた順伝搬コードをもとに、計算グラフをchainer-trt独自の中間形式に書き出すプロセスです。chainer-trtでは、これをModelRetrieverと呼ぶPythonパートが担当します。

計算グラフの全ての情報を得るのは、ちょうどChainerの機能の一つであるComputationalGraphと同じ仕組みで行われています。すなわち、順伝搬後に得られる出力Variable(chainer.VariableNode)から計算グラフを終端(入力側)まで順にたどっていく方法です。多くの場合、順伝搬のコードそのものに対するchainer-trt対応のための特別な変更は必要ありません。

中間形式の実体は1つのディレクトリであり、これは計算グラフの構造を表すmodel.jsonと各層の重みを表す*.weightsファイルを含みます。

下記のスニペットは、ImageNet学習済みのResNet50モデルを中間形式に書き出す処理です。見ての通り、通常の推論コードに少しchainer-trtの要素を外付けしているだけです。

import numpy as np
import chainer
import chainer_trt

# NNを用意
net = chainer.links.ResNet50Layers()

# ダミー入力を作成
x = chainer.Variable(np.random.random((1, 3, 224, 224)).astype(np.float32))

# ModelRetrieverを作成し、入力に名前をつける(任意)
retriever = chainer_trt.ModelRetriever("resnet50")
retriever.register_inputs(x, name="input")

# ダミー入力を用いて順伝搬を実行
with chainer.using_config('train', False):
    with chainer_trt.RetainHook():    # おまじない
        y = net(x)['prob']

# 計算グラフを取得し、全ての情報を保存する
retriever(y, name="prob")
retriever.save()

2段階目は、先程書き出した中間形式の情報を読み込みTensorRTのC++ APIを用いて推論器のビルドを走らせるプロセスで、chainer-trtのC++パートがこれを行います。推論を実行させたい対象デバイスの上でchainer-trtのビルド実行関数を呼ぶと、まもなくそのデバイス専用の推論器(実体は1つのファイル)ができあがります。

#include <chainer_trt/chainer_trt.hpp>
...

// ModelRetrieverの出力ディレクトリを指定し、推論エンジンを構築・保存する
auto m = chainer_trt::model::build_fp32("resnet50");
m->serialize("resnet50/fp32.trt");

上記のスニペットはユーザの書いたC++からこのビルド処理を行う例ですが、TensorRTの機能であるINT8量子化を行わない場合については標準でコンパイルされる小さなツールを下記のように呼ぶだけでも全く等価な推論器が構築できます。
(INT8量子化を伴う場合は、キャリブレーションと呼ばれるプロセスをNNごとに行う必要があるため、専用のビルドツールをユーザが実装する必要があります。)

% tensorrt_builder -i resnet50 -o resnet50/fp32.trt

3段階目は、その推論器を用いて推論を実行するプロセスで、chainer-trtのC++パートがこれを行います。ユーザはまず入力データと出力先バッファをCPU上ないしGPU上に用意し、それぞれがNNのどの入力・出力に対応するか(1つのNNは複数の入力や出力を持つことができ、これを名前で識別できます)などの情報とともにchainer-trtの推論実行関数を呼び出します。すると推論処理が走り、出力結果が返ってきます。

厳密にはビルドされた推論器のファイルはchainer-trtに依存するものではなく、chainer-trtを経由せず直接TensorRTのC++ APIを呼んで利用することができます。しかしながらchainer-trtは典型的なユースケースに関してメモリ管理や入出力の指定方法などを簡易化しCUDAの煩雑なコーディングをある程度隠蔽しているため、chainer-trtでビルドした推論器はchainer-trtで実行するのが便利でしょう。

#include <chainer_trt/chainer_trt.hpp>
...

// 構築した実行エンジンを読み込み、ランタイムを作成
auto m = chainer_trt::model::deserialize("renet50/fp32.trt");
chainer_trt::infer rt(m);

// CPU上の入力・出力バッファを用意し、入力バッファに入力データを読み込んでおく
std::vector<float> x(...);
std::vector<float> y(...);
load_input(x, ...);

rt.infer_from_cpu(1, {{"input"s, x.data}}, {{"prob"s, y.data}});

// 出力がyに入っている

同梱されているImageNetおよびYOLOv2のサンプルを用いた場合の1バッチ推論の平均実効時間、すなわち画像を入力して結果が得られるまでの推論レイテンシを測定してみました。

モデル Chainer FP32 (ms/img) TensorRT FP32 (ms/img) TensorRT INT8 (ms/img)
VGG16 4.713 2.259 1.384
GoogLeNet 13.809 0.974 0.624
ResNet50 19.062 2.145 0.851
YOLO v2 20.749 (12月14日訂正) 6.151 4.579

環境: GeForce GTX 1080Ti, i7 7700K, CUDA 10, TensorRT 5.0, Ubuntu 18.04, Chainer 5.1.0, ChainerCV 0.11.0

GoogLeNetなどの細かな層が多数積み重なったようなNNでは演算量に対して計算グラフ構築のオーバヘッドが相対的に多くなるため、特にTensorRTの活用による高速化の恩恵を受けやすい傾向があります。ただしChainerベースの推論であっても、バッチ数を大きくし複数のCUDAストリームで推論を並列実行するなどの工夫によってスループットはかなりTensorRTベースの推論に近づけることができます。推論のレイテンシが特に重要な場合は、TensorRTで推論器を構成することがきわめて有効です。

コードに同梱しているREADMEドキュメントでは、導入方法(現在のところ、手動でのコンパイル作業が必要です)、より詳細な動作原理、並列化・バッチ化による高スループット化の方法、INT8量子化の使い方、numpy/cupy arrayから直接推論を走らせることのできるPythonインタフェースの使い方など詳細を解説しています。

社内での活用

社内ではいくつかのプロジェクトでchainer-trtを活用して推論を高速化しています。

最も典型的な活用例としては、実製品となる組み込み機器上での画像認識器の開発が挙げられます。計算能力上の制約が多い現場では、TensorRTの活用による高速化が必要不可欠でした。

面白いところでは、探索問題における評価関数を近似するNNを学習しこれをTensorRTで高速化することで全体の処理時間を大幅に短縮するという活用例がありました。別の例としては、強化学習のような問題設定において正確だが計算時間かかる物理シミュレータを同様にNNで近似後TensorRTで高速化することで学習全体を高速化するという例もあります。

Menohとの関係

次に、Menohとの関係についても述べておきたいと思います。

PFNでは、ONNXベースの推論ライブラリMenoh(R)を今年6月にリリースし、現在も活発に開発しています。MenohはONNXとして書き出したNNをもとに推論を行うライブラリで、主に以下のような極めて優れた特長を持っています。

  • Python/C++に限らない豊富な言語バインディング
  • ONNXを用いることで、Chainerのみに必ずしも依存しない汎用性
  • プラガブルなバックエンド

特にバックエンドとして、もともとMenohはIntel製CPU上で深層学習のための演算を高速に実行するためのライブラリMKL-DNNを用いていましたが、現在TensorRTバックエンドを選択できるようにするべく開発を進めています。

これに対してchainer-trtは、

  • ChainerのモデルをTensorRTに迅速に変換することのみを目的とする
  • ONNXに依存しない

といった思想で設計されています。

2点目は特に重要で、ONNXに依存する選択をしている限りは現在のONNXで表現できないオペレータを含むようなNNは原理的に扱えないという困難があります(※1)。TensorRTの場合はプラグインという仕組みにより、TensorRTさえも標準サポートしていないような任意のオペレータをユーザが自らCUDA実装しNN内で使うことができますが、ONNXを中間形式とした場合この自由度がONNXの表現能力によって制約されてしまいます。chainer-trtはONNX非依存の独自中間形式をとるという選択をすることで、実装さえすれば提案されたばかりのオペレータなどを迅速に利用可能にできるような作りにしました(※2)。
※1: ONNXに任意の拡張を施せるようにする構想はいくつか検討されているそうですが、当面の間は使えるオペレータの種類に制約がある状態は続くと考えられます。
※2: もちろんこのように実装されたプラグインオペレータに関してはTensorRTによる自動的かつ高度な最適化の対象とはなりませんが、そのようなオペレータの存在のみによってNNのTensorRT化自体を諦める必要がなくなります。

これらのことから、ごく一般的なオペレータのみで構成されるほとんどのNNは、Menohによって十分にその超高速推論の恩恵に与れることと思います。ONNXで表現できないオペレータを使いたい、またそのオペレータの独自実装を自ら行うことができる方にはchainer-trtが役に立つでしょう。

歴史的には、実はchainer-trtはONNXが昨秋発表される前より社内で開発しています。したがってONNX非依存という選択をしたというよりは、そうするより他になかった当初の設計のまま今に至っていると言う方が正確な表現です。技術的には、Menohとchainer-trtは全く異なるコードベースから始まっており、情報交換をしつつ独立に開発が進んでいます。

Menohは今後も汎用的な推論ライブラリとして最新の高度な需要に追従するため大規模に開発が継続されていく見込みです。chainer-trtはChainerとTensorRTの間をとりもつ実験的プロジェクトとして、TensorRT本体の各種新機能への追従やカスタムオペレータの拡張などを中心に、またインタフェースやドキュメント改善なども含めた開発をしていく見込みです。

ぜひ実機での推論に課題を抱えている皆様のお役に立てればと考えております。また、皆様からのフィードバックをお待ちしています。

ハイパーパラメータ自動最適化ツール「Optuna」公開

秋葉 拓哉
リサーチャー

2018-12-03 13:45:42

ハイパーパラメータ自動最適化フレームワーク「Optuna」のベータ版を OSS として公開しました。この記事では、Optuna の開発に至った動機や特徴を紹介します。

 

 

ハイパーパラメータとは?

ハイパーパラメータとは、機械学習アルゴリズムの挙動を制御するパラメータのことです。特に深層学習では勾配法によって最適化できない・しないパラメータに相当します。例えば、学習率やバッチサイズ、学習イテレーション数といったようなものがハイパーパラメータとなります。また、ニューラルネットワークの層数やチャンネル数といったようなものもハイパーパラメータです。更に、そのような数値だけでなく、学習に Momentum SGD を用いるかそれとも Adam を用いるか、といったような選択もハイパーパラメータと言えます。

ハイパーパラメータの調整は機械学習アルゴリズムが力を発揮するためにほぼ不可欠と言えます。特に、深層学習はハイパーパラメータの数が多い傾向がある上に、その調整が性能を大きく左右すると言われています。深層学習を用いる多くの研究者・エンジニアは、ハイパーパラメータの調整を手動で行っており、ハイパーパラメータの調整にかなりの時間が費やされてしまっています。

Optuna とは?

Optuna はハイパーパラメータの最適化を自動化するためのソフトウェアフレームワークです。ハイパーパラメータの値に関する試行錯誤を自動的に行いながら、優れた性能を発揮するハイパーパラメータの値を自動的に発見します。現在は Python で利用できます。

Optuna は次の試行で試すべきハイパーパラメータの値を決めるために、完了している試行の履歴を用いています。そこまでで完了している試行の履歴に基づき、有望そうな領域を推定し、その領域の値を実際に試すということを繰り返します。そして、新たに得られた結果に基づき、更に有望そうな領域を推定します。具体的には、Tree-structured Parzen Estimator というベイズ最適化アルゴリズムの一種を用いています。

Chainer との関係は?

Optuna は Chainer を含む様々な機械学習ソフトウェアと一緒に使うことができます。

Chainer は深層学習フレームワークであり、Optuna はハイパーパラメータの自動最適化フレームワークです。例えば、Chainer を用いたニューラルネットの学習に関するハイパーパラメータを最適化する場合、Chainer を用いるユーザーコードの一部に Optuna からハイパーパラメータを受け取るコードを書くことになります。それを Optuna に渡すことによって、Optuna が自動的に何度もそのユーザーコードを呼び出し、異なるハイパーパラメータによりニューラルネットの学習が何度も行われ、優れたハイパーパラメータが自動的に発見されます。

社内では Chainer と共に用いられているユースケースがほとんどですが、Optuna と Chainer は密結合しているわけではなく、Chainer の以外の機械学習ソフトウェアとも一緒に使うことができます。サンプルとして、Chainer の他に scikit-learn, XGBoost, LightGBM を用いたものを用意しています。また、実際には機械学習に限らず、高速化など、ハイパーパラメータを受け取って評価値を返すようなインターフェースを用意できる幅広いユースケースで利用可能です。

なぜ Optuna を開発したのか?

ハイパーパラメータの自動最適化フレームワークとして、Hyperopt, Spearmint, SMAC といった有名なソフトウェアが既に存在しています。そんな中でなぜ Optuna を開発したのでしょうか?

複数の理由やきっかけがありますが、一言で言うと、我々の要求を満たすフレームワークが存在せず、そして既存のものよりも優れたものを作るアイディアがあったからです。また、実際には、機能面だけではなく品質面でも、既存のフレームワークにはレガシーなものが多く、不安定であったり環境によって動作しなかったり修正が必要だったりという状況でした。

Optuna の特徴

Define-by-Run スタイルの API

Optuna は Define-by-Run スタイルの API を提供しており、既存のフレームワークと比較し、対象のユーザーコードが複雑であっても高いモジュール性を保ったまま最適化を行うことを可能とし、またこれまでのフレームワークでは表現出来なかったような複雑な空間の中でハイパーパラメータを最適化することもできます。

深層学習フレームワークには Define-and-Run と Define-by-Run という 2 つのパラダイムが存在します。黎明期は Caffe など Define-and-Run のフレームワークが中心でしたが、PFN の開発した Chainer は Define-by-Run のパラダイムを提唱し先駆けとなり、その後 PyTorch が公開され、TensorFlow も 2.0 では eager mode がデフォルトになるなど、今では Define-by-Run のパラダイムは非常に高く評価されており、標準的にすらなろうとする勢いです。

Define-by-Run のパラダイムの有用性は、深層学習フレームワークの世界に限られたものなのでしょうか?我々は、ハイパーパラメータ自動最適化フレームワークの世界でも同様の考え方を適用できることに気づきました。この考え方の下では、全ての既存のハイパーパラメータ自動最適化フレームワークは Define-and-Run に分類されます。そして Optuna は Define-by-Run の考え方に基づき、既存のフレームワークと大きく異なるスタイルの API をユーザに提供しています。これにより、ユーザプログラムに高いモジュール性を持たせたり複雑なハイパーパラメータ空間を表現したりといったことが可能になりました。

学習曲線を用いた試行の枝刈り

深層学習や勾配ブースティングなど、反復アルゴリズムが学習に用いられる場合、学習曲線から、最終的な結果がどのぐらいうまくいきそうかを大まかに予測することができます。この予測を用いて、良い結果を残すことが見込まれない試行は、最後まで行うことなく早期に終了させてしまうことができます。これが、Optuna のもつ枝刈りの機能になります。

Hyperopt, Spearmint, SMAC 等のレガシーなフレームワークはこの機能を持ちません。学習曲線を用いた枝刈りは、近年の研究で、非常に効果的であることが分かっています。下図はある深層学習タスクでの例です。最適化エンジン自体は Optuna も Hyperopt も TPE を用いており同一であるものの、枝刈りの機能の貢献により、Optuna の方が最適化が効率的になっています。

並列分散最適化

深層学習は計算量が大きく一度の学習に時間がかかるため、実用的なユースケースでのハイパーパラメータの自動最適化のためには、性能が高く安定した並列分散処理を簡単に使えることが必要不可欠です。Optuna は複数ワーカーを用いて複数の試行を同時に行う非同期分散最適化をサポートします。下図のように、並列化を用いることで最適化は更に加速します。下図はワーカー数を 1, 2, 4, 8 と変化させた場合の例ですが、並列化により最適化がさらに高速化されていることが確認できます。

また、Chainer の分散並列化拡張である ChainerMN との連携を容易にする機能も用意されており、最適化対象の学習自体が分散処理を用いるような場合にも Optuna を簡単に使うことができます。これらの組み合わせにより、分散処理が含まれた目的関数を並列に分散実行するようなこともできます。

ダッシュボードによる可視化(実装中)

最適化の過程を見たり、実験結果から有用な知見を得たりするために、ダッシュボードを用意しています。1 コマンドで HTTP サーバが立ち上がり、そこにブラウザで接続することで見ることができます。また、最適化過程を pandas の dataframe 等で export する機能もあり、それらを用いてユーザがシステマチックに解析を行うこともできます。

終わりに

Optuna は既に複数の社内プロジェクトで活用されています。例えば、今夏準優勝を果たした Open Images Challenge 2018 でも用いられました。今後も活発に開発は続けられ、完成度の向上と先進的な機能の試作・実装の両方を精力的に進めていきます。現段階でも他のフレームワークと比較し Optuna を利用する理由は十分存在すると我々は考えています。お試し頂きお気づきの点があれば忌憚のないフィードバックを頂ければ幸いです。

先日開催された第 21 回情報論的学習理論ワークショップ (IBIS’18) では、弊社でのインターンシップにおける成果であるハイパーパラメータ自動最適化に関する研究を 2 件発表しました。これらは Optuna を実際に利用している中で出てきた問題意識に基づいており、成果はいち早く Optuna に組み込むことを目指して取り組んでいます。こういった技術により Optuna を更に優れたものとしていければと考えています。

我々の目標は、深層学習関連の研究開発をできるだけ加速することです。ハイパーパラメータの自動最適化はそのための重要なステップとして取り組んでいますが、他にも既にニューラルアーキテクチャー探索や特徴量の自動抽出といった技術に関しても取り組みを開始しています。PFN では、こういった領域や活動に興味を持ち一緒に取り組んでくれるメンバーをフルタイム・インターンで募集しています

HCIグループの発足、UISTおよびISS 2018での論文発表・デモ実施のお知らせ

Fabrice Matulic

2018-10-15 08:56:12

新たにHCI グループが発足しました

PFNでは最先端のAI技術を駆使して「インテリジェントな」次世代システムとサービスの実現を目指しています。しかし、システムの本質的な部分の開発や運用を担うのは依然として人間であるため、人間とマシンの対話を考える事は非常に重要です。ヒューマンコンピュータインタラクション(HCI)のアプローチは、人間とマシンの隔たりを埋め、機械学習においても人間の介入を要する複雑なプロセスの改善に大きく貢献します。この度PFNでは、「humans-in-the-loop(人間参加型)」の考えを採り入れながら、ユーザー中心のAI設計を推し進めるべく、新たにHCI専門のグループを立ち上げました。

HCIチームが探求する研究は大まかに以下の3分野です。

  • 機械学習のためのHCI: 機械学習には複雑で面倒なプロセスがあり、人間の関与が必要な部分がありますが、HCIの手法を利用する事でこれらの作業を容易にします(例えば、データ収集、ラベル付け、前処理、オーグメンテーション、ニューラルネットワークエンジニアリング、デプロイメントやマネージメントなど)
  • HCIのための機械学習: 深層学習を使って既存のインタラクション手法を強化したり、新たなインタラクション手法を実現します(例えば、高度なジェスチャー認識、行動認識、マルチモーダル入力、センサーフュージョン、身体的インタラクション、AIと人間のコラボレーション、インタラクティブなコンテンツを作成する生成モデルなど)
  • ヒューマンロボットインタラクション(HRI): 未来の賢いロボットとユーザーが、効果的かつ直感的に、さらには楽しくコミュニケーションやインタラクションできる事を目指します。

また、HCIグループの外部コンサルタントとして、HCIとHRI分野で豊富な経験をお持ちである東京大学の五十嵐健夫教授からアドバイスをいただく事になりました。五十嵐教授は、研究科学技術振興機構(JST)の戦略的創造研究推進事業(CREST)として、「機械学習のためのHCI」の研究にも取り組まれています。まさに私たちが注力する研究分野であり、今後の長期的な共同研究から実りある成果が生まれる事を大いに期待しています。

今年のUIST ISSで論文発表とデモを行います

HCIグループはまだ正式に立ち上げて間もないグループですが、すでに本格的な研究活動に着手し、直近の研究成果を二本の論文にまとめています。これらは今週開催されるUISTと来月開催のISSで個別に発表する予定です。

一つ目はウォータールー大学でDrini Cami氏とDan Vogel教授との共同研究ですが、タブレット画面に文字を書く際のスタイラスペンの握り方を変える事で様々な機能を呼び出すシステムです。本手法では機械学習を活用し、タッチ入力の生データにもとづいて、ユーザーの手が画面に触れた際の握り方を検知します。これにより、面倒で扱いにくいUIウィジットに頼らず、文字を書いている方の手でペンのモードを素早く変える事が可能です。詳しくは以下の動画をご覧ください。

UISTでは論文発表に加えてDrini Cami 氏が本手法のデモを行います。

二つ目の研究は、昨年12月のコミックマーケットに出展したPaintsChainerに用いたプロジェクションマッピングシステム(論文ではColourAIzeと呼んでいます)で、紙に描いた線画に自動で色を付けます。コミケに行けなかった方のために具体的に説明すると、PaintsChainerが自動的に判断した着色イメージを、線画に重ねるように投影して着色します。その結果、アナログとデジタルが融合した興味深い作品が出来上がります。Web版PaintsChainerと同様に、ヒントとなる色を指定してお好みの自動着色に仕上げる機能もサポートしており、任意の箇所をペンでなぞるだけで本機能が使用可能です。

最初にご紹介したペンの異なる持ち方の研究と同様、11月に東京で開催されるISSでは論文発表とデモの両方を行います。ご自身の線画やマンガにAIが自動着色する楽しい体験をしてみたい方は、カンファレンス期間中にぜひ私たちのデモにお越しください!

最後に、私たちは優秀なHCIリサーチャーを募集しています。前述の研究分野で貢献できる方は、弊社ウェブサイトの採用ページで募集要項をご確認いただき、ぜひご応募ください!お待ちしております。

[BoF] How to choose programming language for product/in-house software development

kashihara
エンジニア

2018-08-24 15:35:26

Preferred Networksでエンジニアをしている柏原です。PFN Dayでは “How to choose programming language for product/in-house software development” という題でBoFのセッションを開きました。PFN Dayとはトビアスのブログエントリ「[PFN Day] BoF session: How to Improve Sharing of Software Components and Best Practices」にもあるように、社内向けの技術カンファレンスです。

ソフトウェア開発において、プログラミング言語は開発環境をはじめとして、開発チームやサポート体制などに大きな影響を与えます。 PFNの中でもたくさんのプログラミング言語が使われていると思います。 今回は社内で何が使われているかという現状については言及せず、社外にリリースする製品/社内製品を開発することを想定して、どうやってプログラミング言語を選択するか、どのような要素がプログラミング言語の選択に影響を与えるのか議論したいと考えました。

まず、参加メンバーのバックグラウンドを共有するため、どういったソフトウェア開発・プログラミング言語の経験があるか自己紹介をしました。 その後、過去にどのような点を重視してプログラミング言語を選んだのか、プログラミング言語を選ぶときの重要な点についての項目を議論の中であげていきました。

結論としては必要としているものを正しく選ぶ、ということになりますが、以下の優先順位がプログラミング言語の決定に大きく依存しているということになりました。

  • Priority 1: Real world restrictions (E.g. frameworks, platforms)
  • Priority 2: Real world needs (E.g. stability, production readiness, concurrency, distributed computed)
  • Priority 3: Real world benefits (E.g. productivity factors)

1番目のrestrictionsでは、実行環境(OS、モバイル端末、組込)や、目的を実現するためのフレームワークが優先されます。 近年ではたくさんのプログラミング言語が増えてきたとはいえ、その言語が利用できるかは環境に大きく依存します。

2番目は、ソフトウェアで求められている機能・非機能要件を満たすことが、当然ながらソフトウェアの開発で求められます。 プログラミング言語やランタイム環境は、適材適所であるべきといえるでしょう。 ソフトウェアの安定性が求められるのはもちろんのこと、近年ではCPUのマルチコア環境を活かすことも必要とされてきています。 プログラミング言語の機能や特性によって、ソフトウェアの要求を実現できるというのはとても心強いです。

3番前は、プログラマーがプログラミングするにあたって、あると嬉しい部分です。 例えば、テキストエディタやIDEによる、プログラミング言語を書くことをサポートする機能(プラグイン)があげられます。

BoFを開催する前は極端な意見に偏るかもしれないと少し不安でしたが、最終的には現実的な結論に落ち着いたと思います。 その他、興味深いトピックとして、ソフトウェアの正しさを検証するものとして、モデル検査やHDLのSystemVerilog(言語)といったものも話題にあがりました。 80分と長いような短い時間の議論でしたが、興味深い会話ができたと思います。BoFのメモがもし公開されたら、そちらも是非ご覧ください。

2018年 PFN夏季インターンシップのコーディング課題公開

楠本充
エンジニア

2018-07-18 11:32:38

PFN 2018夏季インターンシップの選考で用いたコーディング課題を github 上で公開しました。

https://github.com/pfnet/intern-coding-tasks

PFN の楠本です。PFN では毎年8,9月前後に2ヶ月間の長期インターンシップを行っています。コーディング課題はその選考で応募者のプログラミング能力や問題解決能力を見るために出題させて頂いているものです。PFN のインターンシップでは機械学習をはじめとする幅広い分野で応募を行っているため、今年は「機械学習・数理」「バックエンド」「フロントエンド」「プロセッサ/コンパイラ」「Chainer」の5種類のコーディング課題を用意し、応募者の希望するテーマに応じてこのうちのいずれかを解いていただく形にしていました。

今年は去年を大きく上回る数の応募を国内外双方からいただくことができました。それに伴い、インターン生の受け入れ人数も去年よりもさらに拡充する形になりました。

今年の問題は以下のような構成になっています。

  • 機械学習・数理課題: ニューラルネットワークの敵対的入力(Adversarial Example)のアルゴリズムを実装し、性能を報告するためのレポートを記す課題。
  • バックエンド課題: 与えられたログファイルを分析するツールを作る課題。
  • フロントエンド課題: セミナー発表のような動画に対して、発表内容のアノテーションを行うウェブサービスのプロトタイプを作る課題。
  • プロセッサ/コンパイラ課題: 行列積コードの最適化と、行列積回路の設計を行う課題。
  • Chainer 課題: モデルの学習を行うコードを Chainer で実装する課題。

コーディング課題では毎年、出題者が趣向を凝らした問題を作成しています。これらの課題が、興味のある分野を実践的に学ぶための練習問題になれば幸いです。

私は今年の機械学習・数理課題の出題に携わりました。少し余談になりますが、課題を作る際に意識していたことについて書きたいと思います。他の課題ではまた話が違ってくるかもしれませんが、共通しているところもありそうです。

  • 前提知識があまり無くても解けるようにする: PFN では幅広い分野の方々を募集しています。そのため、機械学習そのものの経験や知識が無くても課題を一通り解けるように問題を設定したり、問題文を記述するようにしています。また、特定の知識を持っている人が有利になりすぎるということがあまりないようにも配慮しているつもりです。
  • 実際の研究に近いような設定にする: 深層学習のような分野の研究では「何か良いテーマを見つけて手法を考える → 実装する → 出てきた結果をまとめ、考察を与える」という過程を繰り返しますが、このうち「実装して考察する」という流れを短期間で一通り辿れるような設定にしています。大学の授業の課題のような感じに近いかもしれません。
  • できるだけ興味深いテーマを問う: 機械学習・深層学習の分野では日々研究が進んで面白い結果が次々に出ているので、それに少しでも触れられるような課題を設定しているつもりです。今回の課題である Fast Gradient Signed Method という手法は、シンプルな手法でありながらランダムよりも遥かに強い攻撃手法であるという点で興味深いものだったと思います。
  • 時間が掛かりすぎないようにする: 学業に支障が出ると良くないので、実力が十分あれば1~2日程度で終わるような分量にすることを目標にしています。

提出されたコードは様々な観点から評価するようにしています。単に実装されたコードが正しいのかどうかだけではなく、コードが読みやすいものになっているか、単体テストなどの検証のためのコードが適切に書かれているか、他人がコードの追試をしやすいようになっているか、といった要素も考慮するようにしています。
実験ではコードを書いて動かしたら終わりではなく、手法がどの程度うまくいったのかを評価し、なぜそのような結果になったのかを考察するのが重要になります。特に、複数人で一つの課題に取り組む際にはそれら(評価・考察)を他のチームメンバーに共有することも大事になるでしょう。レポートでは結果の評価と考察ができているかを評価するようにしています。

これらの課題を見て PFN に興味を持っていただけた方は、ぜひ来年のインターンシップへ応募することを検討していただければ幸いです。また、PFN ではフルタイムの採用も通年で行っておりますので、こちらもご検討をよろしくお願いします。