15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类

使用TensorFlow中的卷积神经网络CNN对于图片进行分类。

简介

CIFAR-10

每张图片: (32,32) 六万张

15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类
mark

十种分类 训练集: 五万张 测试集: 一万张

  • 汽车 手机 鸟 猫 等。

图片 彩色 (32,32)

我们将要使用的卷积神经网络的网络结构:

15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类
mark
15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类
mark

从下往上看: 卷积1 - 池层1 - 归一化1 - 卷积2 - 归一化2 - 池层2 - 全连接层3

  • 全连接层4 - 输出层

每一层的详细解释如1图所示。

使用单gpu和多gpu的代码应该怎么来写。

单gpu版本的代码实现

https://github.com/tensorflow/models.git

models/tutorials/images/cifar10

tutorials/image/cifar10/cifar10_train.py 这个是单gpu的版本

tutorials/image/cifar10/cifar10_multi_gpu_train.py 这个是多gpu的版本

if __name__ == '__main__':
  tf.app.run()

调用app.run运行代码

def main(argv=None):  # pylint: disable=unused-argument
  cifar10.maybe_download_and_extract()
  if tf.gfile.Exists(FLAGS.train_dir):
    tf.gfile.DeleteRecursively(FLAGS.train_dir)
  tf.gfile.MakeDirs(FLAGS.train_dir)
  train()

默认情况下会运行我们定义的main函数。main函数中做了数据的下载,判断traindir是否存在,重新创建。

进入train函数,train函数的上面定义了一些参数

FLAGS = tf.app.flags.FLAGS

tf.app.flags.DEFINE_string('train_dir', '/tmp/cifar10_train',
                           """Directory where to write event logs """
                           """and checkpoint.""")
tf.app.flags.DEFINE_integer('max_steps', 1000000,
                            """Number of batches to run.""")
tf.app.flags.DEFINE_boolean('log_device_placement', False,
                            """Whether to log device placement.""")
tf.app.flags.DEFINE_integer('log_frequency', 10,
                            """How often to log results to the console.""")

训练的目录,最大的步数。在train中开始训练。

def train():
  """Train CIFAR-10 for a number of steps."""
  with tf.Graph().as_default():
    global_step = tf.train.get_or_create_global_step()

在训练之前设置一下现在全局的步数。

    with tf.device('/cpu:0'):
      images, labels = cifar10.distorted_inputs()

通过cifar10封装好的方法获取我们的图片内容和对应的标签。接下来跟我们做TensorFlow做mnist 一样的。使用inference方法传入images来创建网络。

过完网络后得到了网络的预测值。拿到预测结果和真实标签计算loss

    # Calculate loss.
    loss = cifar10.loss(logits, labels)

这时候就可以开始训练

    # Build a Graph that trains the model with one batch of examples and
    # updates the model parameters.
    train_op = cifar10.train(loss, global_step)

训练的时候放入loss和全局的步数,得到一个训练的优化op。

这里的训练和之后有一些区别。

    with tf.train.MonitoredTrainingSession(
        checkpoint_dir=FLAGS.train_dir,
        hooks=[tf.train.StopAtStepHook(last_step=FLAGS.max_steps),
               tf.train.NanTensorHook(loss),
               _LoggerHook()],
        config=tf.ConfigProto(
            log_device_placement=FLAGS.log_device_placement)) as mon_sess:
      while not mon_sess.should_stop():
        mon_sess.run(train_op)

可以设置一个钩子函数,在运行之前和运行之中设置回调函数处理变量,输出你想要的信息。

设置checkpoint_dir, 设置一个钩子,然后设置了一个配置项。

    class _LoggerHook(tf.train.SessionRunHook):
      """Logs loss and runtime."""

      def begin(self):
        self._step = -1
        self._start_time = time.time()

      def before_run(self, run_context):
        self._step += 1
        return tf.train.SessionRunArgs(loss)  # Asks for loss value.

      def after_run(self, run_context, run_values):
        if self._step % FLAGS.log_frequency == 0:
          current_time = time.time()
          duration = current_time - self._start_time
          self._start_time = current_time

          loss_value = run_values.results
          examples_per_sec = FLAGS.log_frequency * FLAGS.batch_size / duration
          sec_per_batch = float(duration / FLAGS.log_frequency)

          format_str = ('%s: step %d, loss = %.2f (%.1f examples/sec; %.3f '
                        'sec/batch)')
          print (format_str % (datetime.now(), self._step, loss_value,
                               examples_per_sec, sec_per_batch))

设置的回调函数有三个方法。在程序运行之前做一些操作,,每次run之前做一些操作。
run之后计算耗时,得到loss值。每隔多少step打印信息。

这些都是在钩子函数里来完成的。

下面我们来看一下cifar10里面封装的网络构建函数和损失值计算,以及训练优化。

cifar10.py:

 Basic model parameters.
tf.app.flags.DEFINE_integer('batch_size', 128,
                            """Number of images to process in a batch.""")
tf.app.flags.DEFINE_string('data_dir', '/tmp/cifar10_data',
                           """Path to the CIFAR-10 data directory.""")
tf.app.flags.DEFINE_boolean('use_fp16', False,
                            """Train the model using fp16.""")

首先定义了一些变量。batch大小,数据目录,标志位。

# Global constants describing the CIFAR-10 data set.
IMAGE_SIZE = cifar10_input.IMAGE_SIZE
NUM_CLASSES = cifar10_input.NUM_CLASSES
NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = cifar10_input.NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN
NUM_EXAMPLES_PER_EPOCH_FOR_EVAL = cifar10_input.NUM_EXAMPLES_PER_EPOCH_FOR_EVAL

图片大小(32,32) 分类数目(10) 训练集和验证集的每个epoch数量

设置了一些训练时的超参数,每隔多少轮我们的learning_rate下降多少。

# Constants describing the training process.
MOVING_AVERAGE_DECAY = 0.9999     # The decay to use for the moving average.
NUM_EPOCHS_PER_DECAY = 350.0      # Epochs after which learning rate decays.
LEARNING_RATE_DECAY_FACTOR = 0.1  # Learning rate decay factor.
INITIAL_LEARNING_RATE = 0.1       # Initial learning rate.

前三个参数都是learning_rate下降时,第四个是我们初始化的learning_rate

我们初始化的是0.1 随着网络的训练,每隔一定的步数这个learning_rate下降一点点。

我们开始时可以把learning_rate调大一点,使它下降快一点。当loss值下降到一定的水平。

我们的learning_rate会逐渐减小。

tower_name就是我们使用多gpu版本时为op加上前缀。

TOWER_NAME = 'tower'

DATA_URL = 'https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz'

数据的下载地址。程序会帮我们直接下载。

把需要记录的数据用summary记录的封装函数

def _activation_summary(x):
  """Helper to create summaries for activations.

  Creates a summary that provides a histogram of activations.
  Creates a summary that measures the sparsity of activations.

  Args:
    x: Tensor
  Returns:
    nothing
  """
  # Remove 'tower_[0-9]/' from the name in case this is a multi-GPU training
  # session. This helps the clarity of presentation on tensorboard.
  tensor_name = re.sub('%s_[0-9]*/' % TOWER_NAME, '', x.op.name)
  tf.summary.histogram(tensor_name + '/activations', x)
  tf.summary.scalar(tensor_name + '/sparsity',
                                       tf.nn.zero_fraction(x))

98行时创建一个在cpu上运行的变量。

def _variable_on_cpu(name, shape, initializer):
  """Helper to create a Variable stored on CPU memory.

  Args:
    name: name of the variable
    shape: list of ints
    initializer: initializer for Variable

  Returns:
    Variable Tensor
  """
  with tf.device('/cpu:0'):
    dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
    var = tf.get_variable(name, shape, initializer=initializer, dtype=dtype)
  return var

显式的指定了这个变量在cpu0上运行,指定了你需要创建的变量的数据类型。

    var = tf.get_variable(name, shape, initializer=initializer, dtype=dtype)

使用get_variable创建一个变量。变量的名字,变量的shape,变量的初始化方法。

TensorFlow中有很多的变量初始化方法。正态分布等。这个函数主要的目的是在cpu0上创建变量。

def _variable_with_weight_decay(name, shape, stddev, wd):
  """Helper to create an initialized Variable with weight decay.

  Note that the Variable is initialized with a truncated normal distribution.
  A weight decay is added only if one is specified.

  Args:
    name: name of the variable
    shape: list of ints
    stddev: standard deviation of a truncated Gaussian
    wd: add L2Loss weight decay multiplied by this float. If None, weight
        decay is not added for this Variable.

  Returns:
    Variable Tensor
  """
  dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
  var = _variable_on_cpu(
      name,
      shape,
      tf.truncated_normal_initializer(stddev=stddev, dtype=dtype))
  if wd is not None:
    weight_decay = tf.multiply(tf.nn.l2_loss(var), wd, name='weight_loss')
    tf.add_to_collection('losses', weight_decay)
  return var

正则化,代码中使用的是L2的正则化。 变量的类型。

调用我们之前的在cpu创建变量的方法。这里使用的是截断的正态分布truncated_normal_initializer。输入数据类型,名称。数据的形状。

如果使用L2正则化。就是w的平方加和。TensorFlow已经提供了一个函数就是l2_loss

https://www.tensorflow.org/versions/master/api_docs/python/tf/nn/l2_loss

L2损失。

计算一个张量的L2范数的一半,没有sqrt:

output = sum(t ** 2) / 2

w的平方加和除以2。查看之前我们介绍正则化的时候的公式。

def distorted_inputs():
  """Construct distorted input for CIFAR training using the Reader ops.

  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.

  Raises:
    ValueError: If no data_dir
  """
  if not FLAGS.data_dir:
    raise ValueError('Please supply a data_dir')
  data_dir = os.path.join(FLAGS.data_dir, 'cifar-10-batches-bin')
  images, labels = cifar10_input.distorted_inputs(data_dir=data_dir,
                                                  batch_size=FLAGS.batch_size)
  if FLAGS.use_fp16:
    images = tf.cast(images, tf.float16)
    labels = tf.cast(labels, tf.float16)
  return images, labels

获取图片和每个图片对应的标签。如果是多gpu,会把每个batch的图片和标签获取出来,分散到多个gpu上。

单gpu版本就使用input函数来获取样本

def inputs(eval_data):
  """Construct input for CIFAR evaluation using the Reader ops.

  Args:
    eval_data: bool, indicating if one should use the train or eval data set.

  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.

  Raises:
    ValueError: If no data_dir
  """
  if not FLAGS.data_dir:
    raise ValueError('Please supply a data_dir')
  data_dir = os.path.join(FLAGS.data_dir, 'cifar-10-batches-bin')
  images, labels = cifar10_input.inputs(eval_data=eval_data,
                                        data_dir=data_dir,
                                        batch_size=FLAGS.batch_size)
  if FLAGS.use_fp16:
    images = tf.cast(images, tf.float16)
    labels = tf.cast(labels, tf.float16)
  return images, labels

网络的构建

def inference(images):
  """Build the CIFAR-10 model.

卷积层1的创建

  # conv1
  with tf.variable_scope('conv1') as scope:
    kernel = _variable_with_weight_decay('weights',
                                         shape=[5, 5, 3, 64],
                                         stddev=5e-2,
                                         wd=None)
    conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
    biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))
    pre_activation = tf.nn.bias_add(conv, biases)
    conv1 = tf.nn.relu(pre_activation, name=scope.name)
    _activation_summary(conv1)

把代码块创建的所有变量前面再加一个前缀。

调用tf的卷积函数,我们自己实现神经网络时使用的theano。输入的图片,kernel(卷积的过滤器) 步长,然后padding

在cpu上创建了biases偏置。使用了常量的初始化方式。

卷积加上biases得到了预测值。将预测值过了一遍relu函数。

_activation_summary将第一层的信息保存至磁盘,以便于TensorBoard观察

池层:

把卷积层的输出结果放到池层,池层里面的过滤器形状,步长,padding,起一个别名

归一化方法:

对池层输出的结果做了归一化。

  • 第二个卷积层:

  • 第二层归一化 & 池层

  • 过一个全连接层

  • 将batch上的数据做了平铺

 # local3
  with tf.variable_scope('local3') as scope:
    # Move everything into depth so we can perform a single matrix multiply.
    # 变成了一个一维的向量
    reshape = tf.reshape(pool2, [images.get_shape().as_list()[0], -1])
    dim = reshape.get_shape()[1].value
    # 创建了weights 和biases
    weights = _variable_with_weight_decay('weights', shape=[dim, 384],
                                          stddev=0.04, wd=0.004)
    biases = _variable_on_cpu('biases', [384], tf.constant_initializer(0.1))
    # wx+b线性部分,经过relu函数
    local3 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
    _activation_summary(local3)

接下来又过一个全连接层: 创建w和b 过线性过relu激励。 需要数据写入磁盘

最后一个是输出层

w b 线性部分,总共分类10类

  with tf.variable_scope('softmax_linear') as scope:
    weights = _variable_with_weight_decay('weights', [192, NUM_CLASSES],
                                          stddev=1/192.0, wd=None)
    biases = _variable_on_cpu('biases', [NUM_CLASSES],
                              tf.constant_initializer(0.0))
    softmax_linear = tf.add(tf.matmul(local4, weights), biases, name=scope.name)
    _activation_summary(softmax_linear)

  return softmax_linear

过一个线性,直接得到十种分类每个类别上的概率。

loss函数

def loss(logits, labels):
  """Add L2Loss to all the trainable variables.

  Add summary for "Loss" and "Loss/avg".
  Args:
    logits: Logits from inference().
    labels: Labels from distorted_inputs or inputs(). 1-D tensor
            of shape [batch_size]

  Returns:
    Loss tensor of type float.
  """
  # Calculate the average cross entropy loss across the batch.
  labels = tf.cast(labels, tf.int64)
  cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
      labels=labels, logits=logits, name='cross_entropy_per_example')
  cross_entropy_mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
  tf.add_to_collection('losses', cross_entropy_mean)

  # The total loss is defined as the cross entropy loss plus all of the weight
  # decay terms (L2 loss).
  return tf.add_n(tf.get_collection('losses'), name='total_loss')

标签做一下类型转换。sparse_softmax_cross_entropy_with_logits方法。
得到了最终的损失值。将最终的损失值保存在了一个列表里面

凡是带有summary都是往磁盘写数据的

train函数

lr = tf.train.exponential_decay(INITIAL_LEARNING_RATE,
                                  global_step,
                                  decay_steps,
                                  LEARNING_RATE_DECAY_FACTOR,
                                  staircase=True)
  tf.summary.scalar('learning_rate', lr)

exponential_decay函数做learningrate的指数下降得到每个epoch中有多少个batch。

15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类
mark

利用下面这个公式变小。

TensorFlow帮你控制学习率的下降

  # Compute gradients.
  with tf.control_dependencies([loss_averages_op]):
    opt = tf.train.GradientDescentOptimizer(lr)
    grads = opt.compute_gradients(total_loss)

使用梯度下降算法,输入learning_rate.不同的是直接显式的计算梯度。

介绍theano中也有一个计算梯度的函数。应用梯度。

maybe_download_and_extract函数作用是下载数据集。

什么时候停止:

 with tf.train.MonitoredTrainingSession(
        checkpoint_dir=FLAGS.train_dir,
        hooks=[tf.train.StopAtStepHook(last_step=FLAGS.max_steps),
               tf.train.NanTensorHook(loss),
               _LoggerHook()],
        config=tf.ConfigProto(
            log_device_placement=FLAGS.log_device_placement)) as mon_sess:
      while not mon_sess.should_stop():
        mon_sess.run(train_op)

StopAtStepHook到达你设定的最大的步数的时候。调用should_stop停止

15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类
mark

多gpu版本代码实现

cifar10_multi_gpu_train.py

整个代码的区别与单gpu版本区别不大。首先还是

if __name__ == '__main__':
  tf.app.run()

然后进入main函数,跟单gpu版本一模一样

train函数前面的代码也和之前是一样的。

# Get images and labels for CIFAR-10.
    images, labels = cifar10.distorted_inputs()
    batch_queue = tf.contrib.slim.prefetch_queue.prefetch_queue(
          [images, labels], capacity=2 * FLAGS.num_gpus)
    # Calculate the gradients for each model tower.
    tower_grads = []
    with tf.variable_scope(tf.get_variable_scope()):
      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:

取有多少块gpu。然后循环每个gpu,把数据平均分到每个gpu上。

放在gpu上计算。把每个训练数据的batch分到多个gpu上。

 image_batch, label_batch = batch_queue.dequeue()
            # 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, image_batch, label_batch)

            # 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)

每个gpu上的网络其实完全是一样的。每个GPU上分别取计算各自的loss。计算每个gpu上的梯度。然后将梯度保存到列表里面

    grads = average_gradients(tower_grads)

将每个gpu上的梯度计算一个平均梯度。

显式的计算梯度:

 # Apply the gradients to adjust the shared variables.
    apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)

把训练数据写入到磁盘

    # Create a saver.
    saver = tf.train.Saver(tf.global_variables())

保存了一下模型

 # Build an initialization operation to run below.
    init = tf.global_variables_initializer()

    # Start running operations on the Graph. allow_soft_placement must be set to
    # True to build towers on GPU, as some of the ops do not have GPU
    # implementations.
    sess = tf.Session(config=tf.ConfigProto(
        allow_soft_placement=True,
        log_device_placement=FLAGS.log_device_placement))
    sess.run(init)

初始化全局变量,调用session.run方法

  for step in xrange(FLAGS.max_steps):
      start_time = time.time()
      _, loss_value = sess.run([train_op, loss])
      duration = time.time() - start_time

开始训练每一步,记录一下训练的耗时。每隔一定步数,打印记录到磁盘。

15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类
mark

代码如图: 初始化所有的w和b,这里假设有两块gpu。初始化好的w和b分别拷贝到了两块gpu上。
接着把数据一个batch的数据平均分配到了这两块gpu上。

然后在这两块gpu上去训练我们的模型。计算loss,计算梯度。可以看到这两块gpu上的模型以及初始化的w和b是一模一样的。

这两个gpu分别得到了一个梯度值和一个loss值, 又把这两块gpu的loss值和梯度值拷贝到了cpu中进行平均梯度的计算。

计算完平均梯度就来更新神经网络。

Wn = Wn-1 - 一塔(学习率) 乘以 梯度值

更新网络中的所有w和b。所有的w和b更新完之后又是同样的操作。又把更新好的w和b拷到gpu上,更新这两块gpu上的参数。然后又把一个batch平均分配给两个gpu。

计算loss计算梯度。然后循环。

多gpu版本, 阿里云等云服务商租两块gpu。

这个版本的代码没有gpu跑不起来。因为显式指定了GPU。

15- 深度学习之神经网络核心原理与算法-多gpu实现CNN图片分类
mark