THE長文日記

長文とか短文とかのクレームは一切受け付けません

マルチGPU環境でTensorFlowをなんとなく触ってみた。Chainerとの違いなど

 個人的にはChainer推しでもあるし、国産フレームワークとしてChainerには頑張っていただきたいのだが、GoogleのTensorFlowもなかなかの逸品である。


 

 たぶんできることそのものに大きな違いはないんだろうけど、Chainerの場合マルチGPUにするときには自分でGPUの管理をしなきゃなんないのが、TenrorFlowだともうちょっとラクなのかなあと思ったりする。

http://api.tensorflow.org/system/image/body/1722/Parallelism.png


 ところでマルチGPU環境で実際にテストしてみるといろいろおもしろいことが分かった。

 シングルGPU(TITAN X)の場合、STEP1000まで回してみると

2015-11-22 08:36:43.235397: step 0, loss = 4.67 (3.1 examples/sec; 41.178 sec/batch)
2015-11-22 08:41:10.398625: step 1000, loss = 3.12 (550.1 examples/sec; 0.233 sec/batch)

 と、約5分27秒かかっているが、マルチGPU(TITAN Xx2)にすると・・・

2015-11-22 09:08:55.440036: step 0, loss = 4.67 (6.7 examples/sec; 19.055 sec/batch)
2015-11-22 09:14:25.703258: step 1000, loss = 3.00 (796.7 examples/sec; 0.161 sec/batch)

 計算時間は6分30秒だが、一秒間に処理しているサンプル数は二倍になっており、バッチごとの処理速度もシングルGPUの場合0.233sec/batchだが、デュアルGPUにすると0.161sec/batch、30%ほど高速化している。



 そしてlossも3.00と、デュアルGPUのほうが速く性能を出せてるという結果になった。

 ちなみにStep2000で比較すると、シングルGPUは2.36、デュアルGPUは2.09とかなり差が開いていた。


 このあたりの話はデータ量が多ければ多いほど、効いてくるだろう。


 サンプルにマルチGPUの場合とシングルGPUの場合が両方載ってるので、書き方の違いが実に参考になる。

Error 403 (Forbidden)!!1

Error 403 (Forbidden)!!1


 これによると主な違いはtrainの中で

 シングルGPUだと

def train():
  """Train CIFAR-10 for a number of steps."""
  with tf.Graph().as_default():
    global_step = tf.Variable(0, trainable=False)
    # Get images and labels for CIFAR-10.
    images, labels = cifar10.distorted_inputs()
    # Build a Graph that computes the logits predictions from the
    # inference model.
    logits = cifar10.inference(images)
    # Calculate loss.
    loss = cifar10.loss(logits, labels)

 みたいにロスを計算するのだが、マルチGPUだと


def train():
  """Train CIFAR-10 for a number of steps."""
  with tf.Graph().as_default(), tf.device('/cpu:0'):
    # Create a variable to count the number of train() calls. This equals the
    # number of batches processed * FLAGS.num_gpus.
    global_step = tf.get_variable(
        'global_step', [],
        initializer=tf.constant_initializer(0), trainable=False)
    # Calculate the learning rate schedule.
    num_batches_per_epoch = (cifar10.NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN /
                             FLAGS.batch_size)
    decay_steps = int(num_batches_per_epoch * cifar10.NUM_EPOCHS_PER_DECAY)
    # Decay the learning rate exponentially based on the number of steps.
    lr = tf.train.exponential_decay(cifar10.INITIAL_LEARNING_RATE,
                                    global_step,
                                    decay_steps,
                                    cifar10.LEARNING_RATE_DECAY_FACTOR,
                                    staircase=True)
    # Create an optimizer that performs gradient descent.
    opt = tf.train.GradientDescentOptimizer(lr)
    # Calculate the gradients for each model tower.
    # Calculate the gradients for each model tower.
    tower_grads = []
    for i in xrange(FLAGS.num_gpus):
      with tf.device('/gpu:%d' % i):
        with tf.name_scope('%s_%d' % (cifar10.TOWER_NAME, i)) as scope:
          # Calculate the loss for one tower of the CIFAR model. This function
          # constructs the entire CIFAR model but shares the variables across
          # all towers.
          loss = tower_loss(scope)
          # Reuse variables for the next tower.
          tf.get_variable_scope().reuse_variables()
          # Retain the summaries from the final tower.
          summaries = tf.get_collection(tf.GraphKeys.SUMMARIES, scope)
          # Calculate the gradients for the batch of data on this CIFAR tower.
          grads = opt.compute_gradients(loss)
          # Keep track of the gradients across all towers.
          tower_grads.append(grads)
grads = average_gradients(tower_grads)

 という感じで当たり前だがGPUごとにlossを計算するようにスコープが変わる。

tower_grads.appendでtower_grads(これはCPU側の変数)にそれぞれのGPUによる計算結果を足してる感じだ。

 最後にgrads = average_gradients(tower_grads)で平均化してる。

 つまり一種のアンサンブル学習をやってるらしい。


 このあたりがたぶんオーバーヘッドにはなってるんだろうな

 実際に学習させるところは完全に一緒


for step in xrange(FLAGS.max_steps):
      start_time = time.time()
      _, loss_value = sess.run([train_op, loss])
      duration = time.time() - start_time
      assert not np.isnan(loss_value), 'Model diverged with loss = NaN'
      if step % 10 == 0:
        num_examples_per_step = FLAGS.batch_size * FLAGS.num_gpus
        examples_per_sec = num_examples_per_step / duration
        sec_per_batch = duration / FLAGS.num_gpus
        format_str = ('%s: step %d, loss = %.2f (%.1f examples/sec; %.3f '
                      'sec/batch)')
        print (format_str % (datetime.now(), step, loss_value,
                             examples_per_sec, sec_per_batch))
      if step % 100 == 0:
        summary_str = sess.run(summary_op)
        summary_writer.add_summary(summary_str, step)
      # Save the model checkpoint periodically.
      if step % 1000 == 0 or (step + 1) == FLAGS.max_steps:
        checkpoint_path = os.path.join(FLAGS.train_dir, 'model.ckpt')
        saver.save(sess, checkpoint_path, global_step=step)

 

 TensorFlowはその名の通りテンソルの流れを記述すると、あとでまとめて実行される、という点ではChainerと思想が似ているが、実行させるセッションが分散しやすくなっているのがさすがという気がする。


 まあでもたぶんChainerでも似たようなことができるはずである。

 

 ChainerのimagenetをマルチGPU化するサンプルを誰かが書くだけで「なあんだChainerでいいや」ということになりかねない。


 TensorFlowだとマルチノードでも簡単に分散できるという話を誰かがしていたが、いまのところチュートリアルだけを追いかけてもそれが簡単にできる方法は示されていない。


 実際、マルチGPUで運用されてるノードなんてAmazonのg2.largeくらいなものだから、イマイマこの瞬間にマルチGPUに対応しないと死ぬというわけではないが、我々もTITAN Xの4枚SLIのマシンなんかを売り出している(http://markezine.jp/release/detail/554154)以上、自動的に分散できることに越したことはない。


 ただ、チュートリアルはTensorFlowのほうができが良いが、サンプルはChainerのほうが解りやすい。

 しかしChainerのimagenetのtrain_imagenet.py(https://github.com/pfnet/chainer/blob/master/examples/imagenet/train_imagenet.py)を見てみると

if args.gpu >= 0:
    cuda.get_device(args.gpu).use()
    model.to_gpu()

 となっていてGPUは一個しか使用しない感じだったり

        if model.train:
            optimizer.update(model, x, t)
            if not graph_generated:
                with open('graph.dot', 'w') as o:
                    o.write(computational_graph.build_computational_graph(
                        (model.loss,)).dump())
                print('generated graph', file=sys.stderr)
                graph_generated = True
        else:
            model(x, t)

 こんな感じでmodelを徹底的に使い回すことしか考えてないにゃーと思ったのだが、

 よく考えたら、別にこれって毎エポック平均化する必要はなくて、それぞれ個別に学習した同じ構造のNNを平均化して混ぜ混ぜすればOKって話?


 どうも俺は平均化すると上手くいくイメージが湧かないんだが、TensorFlowのサンプルみると2つのGPUに分散させた結果を混ぜてるし、まあどうせミニバッチに分割して学習というのを繰り返すからクラスとモデルが同じなら平均化しても性能上がるのかなあ。このあたりは実験してみたいところだなあ。


 期待のTensorBoardですが、僕的にはGUIテンソルの流れを弄ったりできるのかなあと思ったんだけど、NVIDIA Digitsについてるgraphviz以上のことはできなそう。これだったらChainerでもできるしね


 アイドル雑誌を長年担当している知り合いに「日本のアイドルを何でも見分けちゃうAI作ろうぜ」と持ちかけたら、500GBに及ぶ巨大なデータセットが送られてきて愕然としている今日このごろ。アイドリアンの真髄を見た気分です。



参考

TensorFlowでアニメゆるゆりの制作会社を識別する - kivantium活動日記

http://kivantium.hateblo.jp/entry/2015/11/18/233834