算法实现过程的详细介绍

\quad 首先需要介绍一下AlexNet,2012年Hinton的学生Alex Krizhevsky提出了深度卷积神经网络模型AlexNet,它可以算是LeNet的一种更宽更深的版本。AlexNet中包含了几个技术点,也首次在CNN中成功应用了ReLU,Dropout和LRN等Trick。同时AlexNet也使用了GPU进行运算加速,作者开源了他们在GPU上训练神经网络的CUDA代码。AlexNet包含了6亿3000万个连接,6000万个参数和65万个神经元,拥有5个卷积层,其中3个卷积层后面连接了连接了最大池化层,最后还有3个全连接层。AlexNet以显著的优势赢得了竞争激烈的ILSVRC 2012的比赛,top-5的错误率降低到了16.4%,相比第二名的成绩26.2%错误率有了巨大的提升。AlexNet可以说是神经网络在低谷期后的第一次发声,确立了深度学习在计算机视觉的统治地位,同时也推动了深度学习在语音识别,自然语言处理,强化学习等领域的拓展。
\quad AlexNet将Lenet的思想发扬光大,把CNN的基本原理应用到了很深很宽的网络中。AlexNet主要用到的新技术如下:

  • 使用ReLU作为CNN的激活函数,并验证其效果在比较深的网络中超过了Sigmoid,成功解决了Sigmoid在网络比较深时的梯度弥散问题。虽然ReLU激活函数在不久之前就提出了,但是直到AlexNet的出现才将其发扬光大。
  • 训练时使用Dropout随机忽略一部分神经元,以避免模型过拟合。Dropout虽然有单独的论文论述,但是AlexNet将其实用化,通过实践证明了它的效果。在AlexNet中主要是最后几个全连接层使用了Dropout。
  • 在CNN中使用重叠的最大池化。之前CNN中普遍使用平均池化,AlexNet全部使用最大池化,避免平均池化的模糊化效果。并且AlexNet中提出让步长比池化核的尺寸小,这样池化层的输出之间有重叠和覆盖,提升了特征的丰富性。
  • 提出了LRN层,对局部神经元的活动创建竞争机制,使得其中响应比较大的变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。
  • 使用CUDA加速深度卷积网络的训练,利用GPU强大的并行计算能力,处理神经网络训练时大量的矩阵运算。AlexNet使用了两块GTX 580 GPU进行训练,单个GTX 580只有3G显存,这限制了可训练的网络的最大规模。因此,作者将AlexNet分布在两个GPU上,在每个GPU的显存中存储一半的神经元的参数。因为GPU之间的通信方便,可以互相访问显存,而不需要通过主机内存,所以同时使用多块GPU也是非常高效的。同时,AlexNet的设计让GPU之间的通信只在网络的某些层进行,控制了通信的性能损耗。
  • 数据增强,随机的人从256 × \times × 256的原始图像截取224 × \times × 224大小的区域(以及水平翻转的镜像),相当于增加了(256-224)^2$\times$2=2048倍的数据量,使用数据增强之后可以大大减轻过拟合,提升泛化能力。进行预测时,则是取图片的4个角加中间共5个位置,并进行左右翻转,一共获得10张图片,对他们进行预测并对10次结果求均值。同时,AlexNet论文中提到了会对RGB图像数据进行PCA处理,并对主成分做一个标准差为0.1的高斯扰动,增加一些噪声,这个Trick可以让错误率再下降1%
  1. 首先导入会用到的系统库,包括datetime,math和time,并载入tensorflow。
#coding=utf-8
from datetime import datetime
import math
import time
import tensorflow as tf
  1. 这里设置batch_size为32,num_batches为100,即总共测试100个batch的数据。
#设置batch_size=32,num_batches为100
batch_size = 32
num_batches = 100
  1. 定义一个用来显示网络每一层结构的函数print_actications,展示每个卷积层或池化层输出tensor的尺寸。这个函数接受一个tensor作为输入,并显示其名称(t.op.name)和tensor尺寸(t.get_shape().as_list())。
def print_activations(t):
    print(t.op.name, ' ', t.get_shape().as_list())
  1. 接下来设计AlexNet网络的结构。我们先定义函数inference,它接受images作为输入,返回最后一层pool5(第5个池化层)及parameters(AlexNet中所有需要训练的模型参数)。这个inference函数将会很大,包括多个卷积层和池化层,下面会拆分成几个小段讲解。
    \quad 首先是第一个卷积层conv1,这里使用Tensorflow中的name_scope,通过with tf.name_scope(‘conv1’) as scope可以将scope内生成的Variabler自动命名为conv1/xxx,便于区分不同卷积层之间的组件。然后定义第一个卷积层,和之前一样使用tf.truncated_normal截断的正态分布函数(标准差为0.1)初始化卷积核的参数kernel。卷积核尺寸为11 × \times × 11,颜色通道为3,卷积核尺寸为64.准备好了kernel,再使用tf.nn.conv2d对输入images完成卷积操作,我们将strides步长设置为4*4(即在图片上4 × \times × 4区域只取样一次,横向间隔是4,纵向间隔也是4,每次取样的卷积核大小都为11 × \times × 11),padding模式设为SAME。将卷积层的biases全部初始化为0,再使用tf.nn.bias_add将conv和bias加起来,并使用激活函数tf.nn.relu对结果进行非线性处理。最后使用print_activations将这一层最后输出的tensor_conv1的结构打印出来,再将这一层可训练的参数kernel,biases添加到parameters。
def inference(images):
    parameters = []
    with tf.name_scope('conv1') as scope:
        #第一个卷积层
        kernel = tf.Variable(tf.truncated_normal([11, 11, 3, 64], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(images, kernel, [1, 4, 4, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[64], dtype=tf.float32), trainable=True, name='bias')
        bias = tf.nn.bias_add(conv, biases)
        conv1 = tf.nn.relu(bias, name=scope)
        print_activations(conv1)
        parameters += [kernel, biases]

\quad 在第一个卷积层后面再添加LRN层和最大池化层。先使用tf.nn.lrn对前面输出的tensor conv1进行LRN处理,这里使用的depth_radius设为4,bias设为1,alpha为0.001/9,beta为0.75,基本都是AlexNet的论文中的推荐值,不过目前除了AlexNet,其他经典的卷积神经网络模型基本都放弃了LRN(主要是效果不明显),而我们使用LRN也会让前馈,反馈的速度大大下降(整体速度下降到1/3),读者可以自主选择是否使用LRN。下面使用tf.nn.max_pool对前面的输出lrn1进行最大池化处理,这里的池化尺寸为3 × \times × 3,即将3 × \times × 3大小的像素块降为1 × \times × 1的像素,取样的步长为2 × \times × 2,padding模式为VALID,即取样时不能超过边框,不像SAME模式那样可以填充边界外的点。最后将输出结果pool1的结构打印出来。

    #添加LRN和最大池化层
    lrn1 = tf.nn.lrn(conv1, 4, bias=1.0, alpha=0.001/9, beta=0.75, name='lrn1')
    pool1 = tf.nn.max_pool(lrn1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool1')
    print_activations(pool1)

\quad 接下来设计第2个卷积层,卷积核的尺寸为5 × \times × 5,输入通道数(即上一层的输出通道数,也就是上一层的卷积核数量)为64,卷积核数量为192。同时,卷积的步长也全部设为1,即扫描全图像素。

#第二个卷积层
    with tf.name_scope('conv2') as scope:
        kernel = tf.Variable(tf.truncated_normal([5, 5, 64, 192], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(pool1, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[192], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv2 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv2)

\quad 接下来对第2个卷积层的输出conv2进行处理,同样是先做LRN处理,再进行最大池化处理,参数和之前完全一样

 #对conv2处理
    lrn2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001/9, beta=0.75, name='lrn2')
    pool2 = tf.nn.max_pool(lrn2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool2')
    print_activations(pool2)

\quad 下面创建第3个卷积层,基本结构和前面两个类似,也只是参数不同。这一层的卷积核尺寸为3 × \times × 3,输入的通道数为192,卷积核数量继续扩大为384,同时卷积的步长全为1,其他地方和前面保持一致。

#第3个卷积层
    with tf.name_scope('conv3') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 192, 384], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[384], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv3 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv3)

\quad 第4个卷积层和之前也类似,这一层的卷积核尺寸为3 × \times × 3,输入通道数384,但是卷积核尺寸数量将为256。

	#第4个卷积层
    with tf.name_scope('conv4') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 256], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(conv3, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv4 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv4)

\quad 最后的第5个卷积层同样是3 × \times × 3大小的卷积核,输入通道数为256,卷积核数量也为256。

	#第5个卷积层
    with tf.name_scope('conv5') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 256, 256], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(conv4, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv5 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv5)

\quad 在第5个卷积层之后,还有一个最大池化层,这个池化层和前两个卷积层一致,最后我们返回这个池化层的输出pool5。至此,inference函数就完成了,它可以创建AlexNet的卷积部分。在正式使用AlexNet来训练或预测时,还需要添加3个全连接层,隐含节点分别为4096,4096和1000。由于最后3个全连接层的计算量很小,所以没放到速度测评中,他们对耗时的影响很小。

 #最大池化层pool5
    pool5 = tf.nn.max_pool(conv5, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool5')
    print_activations(pool5)
    #未添加全连接层,因为对计算耗时影响小
    return pool5, parameters
  1. 开始实现一个评估AlexNet每轮计算时间的函数time_tensorflow_rn。这个函数的第一个输入是Tensorflow的Session,第二个变量是需要评测的运算因子,第三个变量是测试的名称。先定义预热轮数num_steps_burn_in=10,它的作用是给程序热身,头几轮迭代有显存加载,cache的命中等问题因此可以跳动,我们只需要考量10轮迭代之后的计算时间。同时,也记录总时间total_duratiuon和平方和total_duration_squared用以计算方差。我们进行num_batchs+num_steps_burn_in次迭代计算,使用time.time()记录时间,每次迭代通过session.run(target)执行。在初始热身的num_steps_burn_in次迭代后,每10轮迭代显示当前迭代需要的时间。同时每轮将total_duration和total_duration_squared累加,以便后面计算每轮耗时的均值和标准差。在循环结束后,计算每轮迭代的平均耗时nm和标准差sd,最后将结果显示出来。这样就完成了计算每轮迭代耗时的评测函数。
#定义AlexNet的每轮时间评估函数
def time_tensorflow_run(session, target, info_string):
    num_steps_burn_in = 10 #程序预热
    total_durations = 0.0
    total_duration_squared = 0.0
    for i in range(num_batches + num_steps_burn_in):
        start_time = time.time()
        _ = session.run(target)
        duration = time.time() - start_time
        if i >= num_steps_burn_in:
            if not i % 10:
                print('%s: step %d, duration = %.3f'%(datetime.now(), i - num_steps_burn_in, duration))
            total_durations += duration
            total_duration_squared += duration * duration
    #计算每轮迭代的平均耗时和标准差sd,最后将结果显示出来
    mn = total_durations / num_batches
    vr = total_duration_squared / num_batches - mn * mn
    sd = math.sqrt(vr)
    print('%s: %s across %d steps, %.3f +/- %.3f sec / batch'%(datetime.now(), info_string, num_batches, mn, sd))
  1. 接下来是主函数run_benchmark。首先使用with tf.Graph().as_default()定义默认的Graph方便后面使用。如前面所说,我们并不使用ImageNet数据集来训练,只使用随机图片数据测试前馈和反馈计算的耗时。我们使用tf.random_normal函数构造正态分布(标准差为0.1)的随机tensor,第一个维度是batch_size,即每轮迭代的样本数,第二个和第三个维度是图片的尺寸image_size=224,第4个维度是图片的颜色通道数。接下来,使用前面定义的inference函数构建整个AlexNet网络,得到最后一个池化层的输出pool5和网络中需要训练的参数的集合parameters。接下来,我们使用tf.Session()创建新的Session并通过tf.global_variables_initializer()初始化所有参数。再进行AlexNet的foward计算的评测,这里直接使用time_tensorflow_run统计运算时间,传入的target就是pool5,即卷积网络的最后一个池化层的输出。然后进行backward即训练过程的评测,这里和forward有些不同,我们需要给最后的输出pool5设置一个优化目标Loss,我们使用tf.nn.l2_loss计算pool5的loss,再使用tf.gradients求相对于loss的所有模型参数的梯度,这样就模拟了一个训练的过程。当然,训练时还有一个根据梯度更新参数的过程,不过这个计算量很小,就不统计在评测程序里了。最后我们使用time_tensorflow_run统计Backward的运算时间,这里的target就是求整个网络梯度grad的操作。
#定义主函数run_benchmark
def run_benchmark():
    with tf.Graph().as_default():
        image_size = 224
        images = tf.Variable(tf.random_normal([batch_size, image_size, image_size, 3], dtype=tf.float32, stddev=1e-1))
        pool5, parameters = inference(images)
        init = tf.global_variables_initializer()
        sess = tf.Session()
        sess.run(init)
        time_tensorflow_run(sess, pool5, "Foward")
        objective = tf.nn.l2_loss(pool5)
        grad = tf.gradients(objective, parameters)
        time_tensorflow_run(sess, grad, "Foward-backward")

run_benchmark()

算法实现的完整代码

#coding=utf-8
from datetime import datetime
import math
import time
import tensorflow as tf
#设置batch_size=32,num_batches为100
batch_size = 32
num_batches = 100
#定义一个现实网络每一层结构的函数print_actications,展示每一个卷积层或池化层输出tensor的尺寸。
def print_activations(t):
    print(t.op.name, ' ', t.get_shape().as_list())
#设计Alexnet的网络结构
def inference(images):
    parameters = []
    with tf.name_scope('conv1') as scope:
        #第一个卷积层
        kernel = tf.Variable(tf.truncated_normal([11, 11, 3, 64], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(images, kernel, [1, 4, 4, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[64], dtype=tf.float32), trainable=True, name='bias')
        bias = tf.nn.bias_add(conv, biases)
        conv1 = tf.nn.relu(bias, name=scope)
        print_activations(conv1)
        parameters += [kernel, biases]
    #添加LRN和最大池化层
    lrn1 = tf.nn.lrn(conv1, 4, bias=1.0, alpha=0.001/9, beta=0.75, name='lrn1')
    pool1 = tf.nn.max_pool(lrn1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool1')
    print_activations(pool1)
    #第二个卷积层
    with tf.name_scope('conv2') as scope:
        kernel = tf.Variable(tf.truncated_normal([5, 5, 64, 192], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(pool1, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[192], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv2 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv2)
    #对conv2处理
    lrn2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001/9, beta=0.75, name='lrn2')
    pool2 = tf.nn.max_pool(lrn2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool2')
    print_activations(pool2)
    #第3个卷积层
    with tf.name_scope('conv3') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 192, 384], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[384], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv3 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv3)
    #第4个卷积层
    with tf.name_scope('conv4') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 256], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(conv3, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv4 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv4)
    #第5个卷积层
    with tf.name_scope('conv5') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 256, 256], dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(conv4, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv5 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv5)
    #最大池化层pool5
    pool5 = tf.nn.max_pool(conv5, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool5')
    print_activations(pool5)
    #未添加全连接层,因为对计算耗时影响小
    return pool5, parameters

#定义AlexNet的每轮时间评估函数
def time_tensorflow_run(session, target, info_string):
    num_steps_burn_in = 10 #程序预热
    total_durations = 0.0
    total_duration_squared = 0.0
    for i in range(num_batches + num_steps_burn_in):
        start_time = time.time()
        _ = session.run(target)
        duration = time.time() - start_time
        if i >= num_steps_burn_in:
            if not i % 10:
                print('%s: step %d, duration = %.3f'%(datetime.now(), i - num_steps_burn_in, duration))
            total_durations += duration
            total_duration_squared += duration * duration
    #计算每轮迭代的平均耗时和标准差sd,最后将结果显示出来
    mn = total_durations / num_batches
    vr = total_duration_squared / num_batches - mn * mn
    sd = math.sqrt(vr)
    print('%s: %s across %d steps, %.3f +/- %.3f sec / batch'%(datetime.now(), info_string, num_batches, mn, sd))
#定义主函数run_benchmark
def run_benchmark():
    with tf.Graph().as_default():
        image_size = 224
        images = tf.Variable(tf.random_normal([batch_size, image_size, image_size, 3], dtype=tf.float32, stddev=1e-1))
        pool5, parameters = inference(images)
        init = tf.global_variables_initializer()
        sess = tf.Session()
        sess.run(init)
        time_tensorflow_run(sess, pool5, "Foward")
        objective = tf.nn.l2_loss(pool5)
        grad = tf.gradients(objective, parameters)
        time_tensorflow_run(sess, grad, "Foward-backward")

run_benchmark()