めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

TensorFlow Tutorialの数学的背景 − TensorFlow Mechanics 101(その3)

注意事項

本記事末尾の「ひとりごと」にあるように、この記事は説明内容に若干(かなり?)の無理があります。参考までにこのままにしておきますが、基本的にこの記事は無視して、改めて書きなおした下記の記事へと読み進めてください m(_ _)m

enakai00.hatenablog.com

何の話かというと

enakai00.hatenablog.com
enakai00.hatenablog.com

上に示した前回と前々回の記事では、「TensorFlow Mechanics 101」で紹介されているコードを簡単化した例として、隠れ層が1層だけのニューラルネットワークによる分類処理を説明しました。しかしながら、実際にチュートリアルで紹介されているコードでは、隠れ層が2層のモデルを使用しています。そこで、今回は、隠れ層を2層にすることの意味を考えてみます。

復習

次のように隠れユニットが2個あるニューラルネットワークを用いると、平面を2本の直線で分割する形での分類が可能になりました。

たとえば、次のような結果が得られます。

ただしこれは、元々、2本の直線でうまく分類できるデータを用意しているだけで、この方法がいつでもうまくいくとは限りません。たとえば、次の例はまだがんばれますが・・・

こうなるともう無理です。

この場合、隠れUnitの数を増やして平面をより細かく分割することもできますが、はたしてそれは本質的な解決策でしょうか? この程度の例であればそれでもうまくいきますが、もっと複雑な問題になると、データが持つ「本質的な構造」を無視して、やみくもにモデルを複雑にするだけでは対処できません。

特徴量の考え方

先ほどの2つの例を見比べると、実は、これらには類似性が見られます。結論から言うと、「がんばれない例」は、「がんばれる例」の上部を「変形」することで得られます。ブラックホールの影響で「空間の歪み」が生じたようなものとでも思ってください。

つまり、(x_0,x_1) というまっすぐな座標の下では、2本の直線で分類することはできませんが、「空間の歪み」を取り入れた特殊な座標を用意すれば、「2本の歪んだ線」で分類することが可能だと分かります。(これは、(x_0,x_1) 座標で見ると歪んでいるだけで、特殊な座標の世界では直線になっていると考えてください。)

しかしながら、実際のところ、どのような「歪んだ座標」を用いればよいのかは簡単には分かりません。そこで、「歪んだ座標」を見つけ出す部分を含めて計算できるようなモデルを考えます。これは、次のようなニューラルネットワークで表現できます。

1層目の隠れユニット (x'_0,x'_1,x'_2,x'_3) が「歪んだ空間の座標」に相当します。この層のユニット数をふやすことで、より複雑に歪んだ空間が表現できることになります。そして、この歪んだ空間の座標を2層目の隠れユニットに入力することで、「2本の歪んだ線」で元の平面を分割することになります。

このように、最初に与えられたデータの座標(変数)をそのまま用いるのではなく、よりデータの本質(特徴)を表す座標である「特徴変数」に変換して利用するというのがポイントになります。データの内容がある程度よくわかっている場合は、データ分析を実施する人間が、経験に基づいて特徴変数を探し出していくということもあります。一方、この例では、特徴変数そのものも機械学習で発見しようとしています。特徴変数を表すパラメーターと最後の分類「直線」を表すパラメーターの両方を変化させていきながら、トレーニングデータの対数尤度が最大になる所を探します。

コードの実行例

先に実行結果を示すと下記になります。(何度か実行して、比較的にうまく分類できた例を示しています。)

上の2つは、隠れ層が1つだけのニューラルネットワークの場合で、先ほどの「がんばれない例」と同じです。一方、下の2つは隠れ層を2つもつニューラルネットワークを同じデータに適用しています。1層目の隠れUnitは4個あるので、4種類の「特徴量」を抽出していることになります。結果としては、「2本の歪んだ線」でうまく分類できていることが分かります。

下記に実際のコードを示しておきます。例によって、突貫で書いたのでいろいろ冗長ですがご勘弁ください。本質的には、関数single_layerと関数double_layerだけ見れば十分です。それぞれ、隠れ層が1つと2つのモデルによる計算を行っています。

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import tensorflow as tf
import math
import numpy as np
from numpy.random import rand, multivariate_normal
import matplotlib.pyplot as plt

# dataset
class Dataset():
  def __init__(self):
    n1, n2, n3, n4 = 30, 50, 30, 30
    mu1, mu2, mu3, mu4 = [10,-5], [10,25], [-15, -12], [35,-8]
    cov1 = np.array([[20,0],[0,180]])
    cov2 = np.array([[220,0],[0,30]])
    cov3 = np.array([[20,0],[0,40]])
    cov4 = np.array([[20,0],[0,40]])
    data1 = multivariate_normal(mu1,cov1,n1)
    data2 = multivariate_normal(mu2,cov2,n2)
    data3 = multivariate_normal(mu3,cov3,n3)
    data4 = multivariate_normal(mu4,cov4,n4)
    data = np.r_[np.c_[data1,np.ones(n1)],
                 np.c_[data2,np.ones(n2)],
                 np.c_[data3,np.zeros(n3)],
                 np.c_[data4,np.zeros(n4)]]
    np.random.shuffle(data)

    self.test_data, self.test_label = np.hsplit(data[0:40],[2])
    self.train_data, self.train_label = np.hsplit(data[40:],[2])
    self.index = 0

  def next_batch(self, n):
    if self.index + n > len(self.train_data):
        self.index = 0
    data = self.train_data[self.index:self.index+n]
    label = self.train_label[self.index:self.index+n]
    self.index += n
    return data, label

def plot_result(sess, dataset, x, y, subplot1, subplot2):
  data0_x, data0_y, data1_x, data1_y = [], [], [], []
  for i in range(len(dataset.train_data)):
    if dataset.train_label[i][0] == 0:
      data0_x.append(dataset.train_data[i][0])
      data0_y.append(dataset.train_data[i][1])
    else:
      data1_x.append(dataset.train_data[i][0])
      data1_y.append(dataset.train_data[i][1])
  subplot1.scatter(data0_x, data0_y, marker='x', color='blue')
  subplot1.scatter(data1_x, data1_y, marker='o', color='blue')
  subplot2.scatter(data0_x, data0_y, marker='x', color='blue')
  subplot2.scatter(data1_x, data1_y, marker='o', color='blue')

  data0_x, data0_y, data1_x, data1_y = [], [], [], []
  for i in range(len(dataset.test_data)):
    if dataset.test_label[i][0] == 0:
      data0_x.append(dataset.test_data[i][0])
      data0_y.append(dataset.test_data[i][1])
    else:
      data1_x.append(dataset.test_data[i][0])
      data1_y.append(dataset.test_data[i][1])
  subplot1.scatter(data0_x, data0_y, marker='x', color='red')
  subplot1.scatter(data1_x, data1_y, marker='o', color='red')
  subplot2.scatter(data0_x, data0_y, marker='x', color='red')
  subplot2.scatter(data1_x, data1_y, marker='o', color='red')

  xs, ys = np.hsplit(dataset.train_data,[1])
  xmin, xmax = xs.min(), xs.max()
  ymin, ymax = ys.min(), ys.max()
  subplot1.set_ylim([ymin-1, ymax+1])
  subplot1.set_xlim([xmin-1, xmax+1])
  subplot2.set_ylim([ymin-1, ymax+1])
  subplot2.set_xlim([xmin-1, xmax+1])

  RES = 80
  X = np.linspace(xmin-1,xmax+1,RES)
  Y = np.linspace(ymin-1,ymax+1,RES)
  field = [[xx,yy] for yy in Y for xx in X]
  a_k = y.eval(session=sess, feed_dict={x: field})
  a_k = a_k.reshape(len(Y), len(X))
  subplot1.imshow(np.sign(a_k-0.5), extent=(xmin-1,xmax+1,ymax+1,ymin-1),
                  alpha=0.4)
  subplot2.imshow(a_k, extent=(xmin-1,xmax+1,ymax+1,ymin-1),
                  alpha=0.4, vmin=0, vmax=1, cmap=plt.cm.gray_r)

  linex = np.arange(xmin-1, xmax+1)

def single_layer(dataset, subplot1, subplot2):
  with tf.Graph().as_default():
    mult = dataset.train_data.flatten().mean()
    hidden1_units = 2
    
    # Create the model
    with tf.name_scope('input'):
      x = tf.placeholder(tf.float32, [None, 2])
  
    with tf.name_scope('hidden1'):
      w1 = tf.Variable(
        tf.truncated_normal([2, hidden1_units],stddev=1.0/math.sqrt(2.0)),
        name='weights')
      b1 = tf.Variable(tf.zeros([1, hidden1_units]), name='biases')
      hidden1 = tf.nn.tanh(tf.matmul(x, w1) + b1*mult)

      # Logging data for TensorBoard
      _ = tf.histogram_summary('hidden1_weight', w1)
      _ = tf.histogram_summary('hidden1_bias', b1)
  
    with tf.name_scope('output'):
      w = tf.Variable(tf.zeros([hidden1_units,1]), name='weights')
      b = tf.Variable(tf.zeros([1]), name='biases')
      y = tf.nn.sigmoid(tf.matmul(hidden1, w) + b*mult)
  
      # Logging data for TensorBoard
      _ = tf.histogram_summary('output_weight', w)
      _ = tf.histogram_summary('output_bias', b)
  
    # Define loss and optimizer
    with tf.name_scope('optimizer'):
      y_ = tf.placeholder(tf.float32, [None, 1])
      log_probability = tf.reduce_sum(y_*tf.log(y) + (1-y_)*tf.log(1-y))
      train_step = tf.train.GradientDescentOptimizer(0.001).minimize(-log_probability)
      correct_prediction = tf.equal(tf.sign(y-0.5), tf.sign(y_-0.5))
      accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    
    # Logging data for TensorBoard
    _ = tf.histogram_summary('probability of training data', y)
    _ = tf.scalar_summary('log_probability', log_probability)
    _ = tf.scalar_summary('accuracy', accuracy)
    
    # Train
    with tf.Session() as sess:
      writer = tf.train.SummaryWriter('/tmp/nn_single',
                                      graph_def=sess.graph_def)
      sess.run(tf.initialize_all_variables())
      for i in range(500):
        batch_xs, batch_ys = dataset.next_batch(100)
        feed = {x: batch_xs, y_: batch_ys}
        sess.run(train_step, feed_dict=feed)
      
        feed = {x: dataset.test_data, y_: dataset.test_label}
        summary_str, lp, acc = sess.run(
          [tf.merge_all_summaries(), log_probability, accuracy], feed_dict=feed)
        writer.add_summary(summary_str, i)
        print('LogProbability and Accuracy at step %s: %s, %s' % (i, lp, acc))

      plot_result(sess, dataset, x, y, subplot1, subplot2)

def double_layer(dataset, subplot1, subplot2):
  with tf.Graph().as_default():
    mult = dataset.train_data.flatten().mean()
    hidden1_units = 4
    hidden2_units = 2
    
    # Create the model
    with tf.name_scope('input'):
      x = tf.placeholder(tf.float32, [None, 2])
  
    with tf.name_scope('hidden1'):
      w1 = tf.Variable(
        tf.truncated_normal([2, hidden1_units],stddev=1.0/math.sqrt(2.0)),
        name='weights')
      b1 = tf.Variable(tf.zeros([1, hidden1_units]), name='biases')
      hidden1 = tf.nn.tanh(tf.matmul(x, w1) + b1*mult)

    with tf.name_scope('hidden2'):
      w2 = tf.Variable(
        tf.truncated_normal([hidden1_units, hidden2_units],
                            stddev=1.0/math.sqrt(2.0)),
                            name='weights')
      b2 = tf.Variable(tf.zeros([1, hidden2_units]), name='biases')
      hidden2 = tf.nn.tanh(tf.matmul(hidden1, w2) + b2)
  
      # Logging data for TensorBoard
      _ = tf.histogram_summary('hidden1_weight', w1)
      _ = tf.histogram_summary('hidden1_bias', b1)
      _ = tf.histogram_summary('hidden2_weight', w2)
      _ = tf.histogram_summary('hidden2_bias', b2)
  
    with tf.name_scope('output'):
      w = tf.Variable(tf.zeros([hidden2_units,1]), name='weights')
      b = tf.Variable(tf.zeros([1]), name='biases')
      y = tf.nn.sigmoid(tf.matmul(hidden2, w) + b*mult)
  
      # Logging data for TensorBoard
      _ = tf.histogram_summary('output_weight', w)
      _ = tf.histogram_summary('output_bias', b)
  
    # Define loss and optimizer
    with tf.name_scope('optimizer'):
      y_ = tf.placeholder(tf.float32, [None, 1])
      log_probability = tf.reduce_sum(y_*tf.log(y) + (1-y_)*tf.log(1-y))
      train_step = tf.train.GradientDescentOptimizer(0.001).minimize(-log_probability)
      correct_prediction = tf.equal(tf.sign(y-0.5), tf.sign(y_-0.5))
      accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    
    # Logging data for TensorBoard
    _ = tf.histogram_summary('probability of training data', y)
    _ = tf.scalar_summary('log_probability', log_probability)
    _ = tf.scalar_summary('accuracy', accuracy)
    
    # Train
    with tf.Session() as sess:
      writer = tf.train.SummaryWriter('/tmp/nn_double',
                                      graph_def=sess.graph_def)
      sess.run(tf.initialize_all_variables())
      for i in range(500):
        batch_xs, batch_ys = dataset.next_batch(100)
        feed = {x: batch_xs, y_: batch_ys}
        sess.run(train_step, feed_dict=feed)
      
        feed = {x: dataset.test_data, y_: dataset.test_label}
        summary_str, lp, acc = sess.run(
          [tf.merge_all_summaries(), log_probability, accuracy], feed_dict=feed)
        writer.add_summary(summary_str, i)
        print('LogProbability and Accuracy at step %s: %s, %s' % (i, lp, acc))
    
      plot_result(sess, dataset, x, y, subplot1, subplot2)

# Main
if __name__ == '__main__':
  fig = plt.figure()
  dataset = Dataset()

  subplot1 = fig.add_subplot(2,2,1)
  subplot2 = fig.add_subplot(2,2,2)
  single_layer(dataset, subplot1, subplot2)

  subplot1 = fig.add_subplot(2,2,3)
  subplot2 = fig.add_subplot(2,2,4)
  double_layer(dataset, subplot1, subplot2)

  plt.show()

TensorBoardで見た学習曲線

TensorBoardで見た学習曲線は次のとおりです。隠れ層が1つの場合、正解率は0.7程度ですぐに飽和して、それ以上はうまく学習できていません。

隠れ層が2つの場合、しばらく頑張っていると、正解率が0.9以上にジャンプしています。

また、今回のコードでは、対数尤度や正解率の計算を行う部分を「with tf.name_scope('optimizer')」というセクションに入れてあるので、ネットワークのグラフが次のように見やすくなっています。2層の隠れ層を持つことが一目で分かります。

「hidden1」の箱を開くと、中の詳細が確認できます。


次回予告

今回は、隠れ層で特徴量を抽出するという話をしました。特徴量を人間が判断して見つけるのではなく、特徴量の発見そのものを機械学習のプロセスに組み込んだところがポイントです。ただし、この方法もいつでもうまく行くとは限りません。たとえば、画像データの分類問題のように、対象データの種類(この例で言えば「画像データである」という事実)がわかっている場合、対象データの特徴量をうまく抽出できるように、「隠れ層をうまく設計」することで、判別精度を上げていきます。いわゆるCNN(Convolutional Neural Network/畳み込みニューラルネットワーク)では、画像の特徴抽出に古くから用いられてきた「畳込み演算」を隠れ層で実施することにより、画像判別の精度を上げようというアプローチになります。

ひとりごと

・・・・・・・・・・・・・と言いながら、今回の2層NNの場合、本質的には1層目のレイヤーで平面を(4本の直線により)直線分割して、「2層目+Output層」で各領域の◯×を判定しているだけだったりもします。つまり、2層目が冗長。2層目が冗長な分だけ、最適解へのパスが広くて学習効率が高いというメリットはありそうですが、CNNの畳み込みによる特徴抽出の話につなげるには無理矢理感があるかも知れません。もうちょっとうまい説明できないかしら?