《实战Google深度学习框架》第六章 学习代码及笔记

  第六章主要介绍了卷积神经网络的一些知识,卷积神经网络的每个节点都是一个神经元。输入层是图像的原始像素,而输出层的每个节点代表了不同类别的可信度。卷积神经网络与全连接网络的主要区别就是相邻两层的连接方式。全连接层处理图像时的参数太多,除了导致计算速度变慢,还很容易导致过拟合问题。而通过卷积神经网络中的卷积层就可以有效的解决这个问题。
  卷积神经网络一般由输入层、卷积层、池化层、全连接层和Softmax层五种结构组成。其中卷积层是神经网络中最重要的一部分。卷积层中的输入只是上层网络中的一小块,常用大小为3*3或5*5,常称其为卷积核或过滤器。过滤器会将当前层神经网络上的一个子节点矩阵转化为下一层神经网络上的一个长宽为1,但深度不限的单位节点矩阵。卷积层试图将神经网络中的每一小块进行深入分析从而得到抽象程度更高的特征。在卷积神经网络中,每一个卷积层使用的过滤器都相同,这样可以大大减少神经网络上的参数个数。卷积层的参数个数只和过滤器尺寸、深度以及当前层节点矩阵的深度有关。
  在卷积层之间往往会加入一个池化层,池化层可以非常有效地缩小矩阵的尺寸,从而减少最后全连接层的参数。池化操作可以理解为将一张分辨率较高的图片转化为分辨率较低的图片。与卷积层过滤器类似,池化层也需要人工设定过滤器的尺寸、是否使用全0填充以及移动的步长等。唯一的区别是卷积层使用的过滤器是横跨整个深度的,而池化层使用的过滤器只影响一个深度上的节点。所以池化层过滤器除了在长宽两个维度移动外,还需要在深度这个维度移动。输入层、全连接层和softmax层的作用与全连接网络大致相同。这些仅仅是对卷及网络结构的一些简单介绍,前向传播的具体原理及过程还请查阅相关资料。
TensorFlow实现卷积层、池化层及部分Inception-v3模型:

#tensorflow实现卷积层

import tensorflow as tf

#通过tf.get_variable的方式创建过滤器的权重变量和偏执项变量。因为卷积层参数个数只和
#过滤器的尺存、深度以及当前层节点的深度有关,所以声明的参数变量是一个四维矩阵,前两个
#维度代表了过滤器的尺寸,第三个维度代表当前层的深度,第四个维度代表过滤器的深度。
filter_weight = tf.get_variable('weights1',[5,5,3,16],initializer=tf.truncated_normal_initializer(stddev=0.1))

#和卷积层的权重类似,当前层矩阵上不同位置的偏置项也是共享的,所以总共有下一层个数深度的偏置项
#此处过滤器深度为16,也是下一层神经网络中节点矩阵的深度。
biases = tf.get_variable('biases1',[16],initializer=tf.constant_initializer(0.1))

#tf.nn.conv2d提供了一个非常方便的函数来实现卷积层的前向传播算法。这个函数的第一个输入为
#当前层的节点矩阵。注意这是个四维矩阵,后面三个维度对应一个节点矩阵,第一维对应一个输入batch
#比如在输入层,input[0,:,:,:]表示第一张图片,input[1,:,:,:]表示第二张图片,以此类推。
#tf.nn.conv2d第二个参数提供了卷积层的权重,第三个参数为不同维度上的步长。虽然第三个参数
#为一个长度为4的数组,但第一维和最后一维的数字一定为1,这是因为卷积层的步长只对矩阵的长宽
#有效。最后一个参数是填充的方法,TensorFlow提供SAME和VALID两种选择。其中SAME表示全0
#填充,VALID表示不填充
conv = tf.nn.conv2d(input,filter_weight,strides=[1,1,1,1],padding='SAME')

#tf.nn.bias_add提供了一个方便的函数给每一个节点加上偏执项。注意这里不能直接使用加法,
#因为矩阵上不同位置的节点都需要加上同样的偏执项。如下一层神经网络的大小为2*2,偏执项只是
#一个数(因为深度为1),而2*2矩阵中每个值都需要加上这个数。
bias = tf.nn.bias_add(conv,biases)
#将计算结果通过relu激活函数实现去线性化。
actived_conv = tf.nn.relu(bias)


#TensorFlow实现池化层
#tf.nn.max_pool实现了最大处化层的前向传播过程,它的参数和tf.nn.conv2d函数类似,第一个参数为
#当前层的节点矩阵,这是一个四维矩阵。ksize提供了过滤器的尺寸,四维数组的第一个和最后一个数必须为1
#,这意味着池化层过滤器是不可以跨不同输入样例或者节点矩阵深度的。strides提供了步长信息,第一维和
#最后一维也只能是1,表明池化层不能减少节点矩阵深度或输入样例个数。padding是否全0填充。
pool = tf.nn.max_pool(actived_conv,ksize=[1,3,3,1],strides=[1,2,2,1],padding='SAME')



##Inception-v3模型

#通过TensorFlow-slim实现卷积层,通过TensorFlow-slim可以在一行中实现一个卷积层的前向传播
#算法。slim.conv2d函数有3个参数是必填的,第一个参数为输入节点矩阵,第二个参数为当前卷积层过滤器
#的深度,第三个参数为过滤器的尺寸。可选的参数有过滤器移动的步长,是否使用全0填充、激活函数的选择
#及命名空间等。
net = slim.conv2d(input,32,[3,3])


##inception-v3模型中的部分代码实现。

slim =tf.contrib.slim
#slim.arg_scope函数可用于设置默认的参数值。slim.arg_scope函数的第一个参数为一个函数列表
#在这个列表中的函数将使用默认的参数取值。比如通过下面的定义,调用slim.conv2d(net,320,[1,1]])
#函数时会自动加上stride=1和padding=‘SAME’的参数。如果函数调用时指定了stride,那么这里设置
#的默认值就不会在使用。只有用@slim.add_arg_scope修饰过的方法才能使用arg_scope.
with slim.arg_scope([slim.conv2d,slim.max_pool2d,slim.avg_pool2d],
                    stride=1,padding='SAME'):
    #假设之前神经网络前向传播结果保存在变量net中
    #为一个Inception模块声明一个统一的变量命名空间。
    with tf.variable_scope('Mixed_7c'):
        #给Inception模块中每一条路径声明一个命名空间。
        with tf.variable_scope('Branch_0'):
            #实现一个过滤器边长为1,深度为320的卷积层。
            branch_0 = slim.conv2d(net,320,[1,1],scope='Conv2d_0a_1x1')
            
        #inception模块中的第二条路径。
        with tf.variable_scope('Branch_1'):
            
            branch_1 = slim.conv2d(net,384,[1,1],scope='Conv2d_0a_1x1')
            #tf.concat函数可以将多个矩阵拼接起来。第一个参数指定了拼接的维度,这里给出
            #的‘3’代表了矩阵是在深度这个维度上进行拼接。此处两层卷积层的输入都是branch_1.
            #ps:拼接的维度应该是1,2,3???
            branch_1 = tf.concat(3,[
                    slim.conv2d(branch_1,384,[1,3],scope='Conv2d_0b_1x3'),
                    slim.conv2d(branch_1,384,[3,1],scope='Conv2d_0c_3x1')])            ])

#t1 = [[[1, 2], [2, 3]], [[4, 4], [5, 3]]]
#t2 = [[[7, 4], [8, 4]], [[2, 10], [15, 11]]]
#tf.concat([t1, t2], 0)
        #inception模块中的第三条线
        with tf.variable_scope('Branch_2'):
            branch_2 = slim.conv2d(net,448,[1,1],scope='Conv2d_0a_1x1')
            branch_2 = slim.conv2d(branch_2,384,[3,3],scope='Conv2d_0b_3x3')
            
            branch_1 = tf.concat(3,[
                    slim.conv2d(branch_2,384,[1,3],scope='Conv2d_0c_1x3'),
                    slim.conv2d(branch_2,384,[3,1],scope='Conv2d_0d_3x1')])
        #第四条线
        with tf.variable_scope('Branch_3'):
            branch_3 = slim.avg_pool2d(net,[3,3],scope='AvgPool_oa_3x3')
            branch_3 = slim.conv2d(branch_3,192,[1,1],scope='Conv2d_0b_1x1')
        #最后输出是由上面四个计算结果拼接到的。    
        net = tf.concat(3,[branch_0,branch_1,branch_2,branch_3])

卷积神经网络实现数字识别问题,mnist_inference_cnn.py定义了网络的结构和前向传播的过程,其中使用了dropout方法来减小过拟合:

import tensorflow as tf

#定义网络结构,输入、输出和隐藏层的节点数
#28*28,相当于图片的像素
INPUT_NODE = 784
#输出分类的个数
OUTPUT_NODE = 10
#图片的像素大小
IMAGE_SIZE = 28
#输入图片的深度
NUM_CHANNELS = 1
#输出结果的分类数
NUM_LABELS = 10

#第一层卷积层的尺寸和深度
CONV1_DEEP = 32
CONV1_SIZE = 5

#第二层卷积层的尺寸和深度
CONV2_DEEP = 64
CONV2_SIZE = 5

#全连接层节点个数
FC_SIZE = 512

#重置default graph计算图以及nodes节点        
tf.reset_default_graph()  
#定义神经网络的前向传播过程,定义了一个新参数train,用于区分训练过程和测试过程。在这个程序中将用到
#dropout方法,用来进一步提升模型的可靠性并防止过拟合,dropout过程只在训练时使用。
def inference(input_tensor,train,regularizer):
    #声明第一层神经网络的变量并完成前向传播过程。tf.variable_scope生成了一个上下文管理器,来管理已经创建的变量。
    #使用不同的命名变量来隔离不同层的变量,可以让每一层中的变量命名只需要考虑在当前层的作用,而不需要担心重名
    #的问题。这里定义的卷积层输入为28*28*1,因为使用了全0填充,所以输出为28*28*32的矩阵。
    with tf.variable_scope('layer1-conv1'):
        #这里通过tf.get_variable和tf.Variable没有本质区别,因为在训练或是测试中没有在同一个程序中多次调用这个
        #函数。如果在同一个程序中多次调用,第一次调用后要将reuse设置为True。
        #声明了输入到隐藏层的权重变量和偏执项
        conv1_weights = tf.get_variable('weight',[CONV1_SIZE,CONV1_SIZE,NUM_CHANNELS,CONV1_DEEP],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv1_biases = tf.get_variable('bias',[CONV1_DEEP],initializer=tf.constant_initializer(0.0))
        #使用边长为5,深度为32的过滤器,过滤器移动的步长为1,且使用全0填充。全0填充可以使卷积层
        #前向传播结果矩阵的大小和当前层一致。
        conv1 = tf.nn.conv2d(input_tensor,conv1_weights,strides=[1,1,1,1],padding='SAME')
        
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1,conv1_biases))
        
    #实现第二层池化层的前向传播过程,选用最大池化层,过滤器边长为2,使用全0填充且移动步长
    #为2.这一层的输入是上一层的输出,即输入为28*28*32,输出为14*14*32的矩阵。
    with tf.name_scope('layer2-pool1'):
        #第一个参数为当前层的节点矩阵,这是一个四维矩阵。ksize提供了过滤器的尺寸,四维数组的第一个和最后一个数必须为1
        #,这意味着池化层过滤器是不可以跨不同输入样例或者节点矩阵深度的。strides提供了步长信息,第一维和
        #最后一维也只能是1,表明池化层不能减少节点矩阵深度或输入样例个数。padding是否全0填充。
        pool1 = tf.nn.max_pool(relu1,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
        
    #声明第三层卷积层的变量并实现前向传播过程。这一层输入为14*14*32的矩阵,输出为14*14*64的矩阵。    
    with tf.variable_scope('layer3-conv2'):
        #这里通过tf.get_variable和tf.Variable没有本质区别,因为在训练或是测试中没有在同一个程序中多次调用这个
        #函数。如果在同一个程序中多次调用,第一次调用后要将reuse设置为True。
        #声明了输入到隐藏层的权重变量和偏执项
        conv2_weights = tf.get_variable('weight',[CONV2_SIZE,CONV2_SIZE,CONV1_DEEP,CONV2_DEEP],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv2_biases = tf.get_variable('bias',[CONV2_DEEP],initializer=tf.constant_initializer(0.0))
        #使用边长为5,深度为64的过滤器,过滤器移动的步长为1,且使用全0填充。
        conv2 = tf.nn.conv2d(pool1,conv2_weights,strides=[1,1,1,1],padding='SAME')
        
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2,conv2_biases))
    #实现第四层池化层的前向传播过程,和第二层的结构一样,输入为14*14*64的矩阵,输出为7*7*64的矩阵。
    with tf.name_scope('layer4-pool2'):
        
        pool2 = tf.nn.max_pool(relu2,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
        
    #将第四层池化层的输出转化为第五层全连接层的输入。第四层输出为7*7*64的矩阵,然而全连接层
    #需要输入的格式为向量,所以在这里需要将这个7*7*64的矩阵拉直成一个向量。pool2.get_shape()
    #函数可以得到第四层输出矩阵的维度而不需要手工计算。注意每一层神经网络的输入输出都为一个
    #batch的矩阵,所以这里得到的维度也包含了一个batch中的数据个数。
    pool_shape = pool2.get_shape().as_list()
#    print(pool_shape) 结果为[100,7,7,64]
    #计算矩阵拉直成向量后的长度,是矩阵长宽及深度的乘积,pool_shape[0]是一个batch中的数据个数。
    nodes = pool_shape[1]*pool_shape[2]*pool_shape[3]
#    print(pool_shape[1],pool_shape[2],pool_shape[3],pool_shape[0])
    #通过tf.reshape函数将第四层得输出变为一个batch的向量。
    reshaped = tf.reshape(pool2,[pool_shape[0],nodes])
#    print(reshaped)
    
    #声明第五层的变量并实现前向传播过程。这一层的输入为拉直后的一组向量。长度为7*7*64,输出
    #的长度为512的向量。这一层引入了dropout的概念,训练时会随机将部分节点输出改为0,,避免
    #过拟合,从而使模型在测试数据上的表现更好,一般只用在全连接层。
    with tf.variable_scope('layer5-fc1'):
        fc1_weights = tf.get_variable("weight",[nodes,FC_SIZE],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        #只有全连接层的权重需要加入正则化
        if regularizer != None:
            tf.add_to_collection('losses',regularizer(fc1_weights))
            
        fc1_biases = tf.get_variable('bias',[FC_SIZE],initializer=tf.constant_initializer(0.1))
        
        fc1 = tf.nn.relu(tf.matmul(reshaped,fc1_weights)+fc1_biases)
        if train:fc1 = tf.nn.dropout(fc1,0.5)

    #声明第六层全连接层的变量并实现前向传播,这一层的输入为长度为512的向量,输出为一组长度
    #为10的向量,这一层的输出通过softmax之后就得到了最后的分类结果。    
    with tf.variable_scope('layer6-fc2'):
        fc2_weights = tf.get_variable("weight",[FC_SIZE,NUM_LABELS],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        
        if regularizer != None:
            tf.add_to_collection('losses',regularizer(fc2_weights))
            
        fc2_biases = tf.get_variable('bias',[NUM_LABELS],initializer=tf.constant_initializer(0.1))
        
        logit = tf.matmul(fc1,fc2_weights) + fc2_biases
    #返回第六层的输出。    
    return logit

训练过程mnist_train_cnn.py,其中的大多数代码都和全连接网络相同,只是改变了输入数据的格式:

import os
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import numpy as np

#加载mnist_inference中定义的前向传播函数和参数
import mnist_infrernce_cnn

#一个训练batch中训练数据的个数,数字越小,训练过程越接近随机梯度下降,越大越接近梯度下降
BATCH_SIZE = 100
#基础的学习率,定义了参数每次更新的幅度
LEARNNING_RATE_BASE = 0.8
#学习率衰减率,每训练指定轮数后,学习率乘以0.99,可以使模型在前期迅速接近最优解,又保证模型在训练后期
#不会有太大波动,从而更加接近局部最优解
LEARNING_RATE_DECAY = 0.99
#描述模型复杂度的正则化项在损失函数中的系数,希望通过对模型的
#复杂程度施加一个惩罚,避免模型任意拟合训练数据中的随机噪音
REGULARAZTION_RATE = 0.0001
#训练轮数
TRAINING_STEPS = 30000
#滑动平均衰减率,可以使模型在训练数据上更加robust,一般为非常接近于1的数
MOVING_AVERAGE_DECAY = 0.99

#模型的存储位置和名称
MODE_SAVE_PATH = R"C:\Users\Administrator\Desktop\学校事务\论文资料\MNIST"
MODEL_NAME = "cnn_model.ckpt"

#定义模型的训练过程,
def train(mnist):
    #测试数据的输入与结果值,tf.placeholder为形式参数,定义了过程,输入x为一个四维矩阵,第一维
    #表示一个batch中的样例个数,第二维和第三维表示图片的尺寸,第四维表示图片的深度。
    x = tf.placeholder(tf.float32,[BATCH_SIZE,mnist_infrernce_cnn.IMAGE_SIZE,mnist_infrernce_cnn.IMAGE_SIZE,mnist_infrernce_cnn.NUM_CHANNELS],
                       name='x-input')
    y_ = tf.placeholder(tf.float32,[None,mnist_infrernce_cnn.OUTPUT_NODE],name='y-input')
    
    #计算L2正则化损失函数
    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    #调用函数,计算模型的前向传播结果
    y = mnist_infrernce_cnn.inference(x,train,regularizer)
    #定义存储训练轮数的变量,用于动态控制衰减率。
    #这个变量不需要计算滑动平均值,所以指定为不可训练的变量。
    global_step = tf.Variable(0,trainable=False)
    #给定滑动平均衰减率和训练轮数的变量,初始化滑动平均类。
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY,global_step)
    #在所有代表神经网络参数的变量上使用滑动平均,tf.trainable_variables()返回的就是图上所有可训练的变量。
    variable_averages_op = variable_averages.apply(tf.trainable_variables())
    #因为是分类问题,所以计算交叉熵作为刻画预测值与真实值之间差距的损失函数。当分类问题只有一个
    #正确答案时,可以使用这个函数加速交叉熵的计算。第一个参数为不包含softmax层的前向传播结果,
    #第二个是训练数据的正确答案。比较每个数组内的值,返回y_中最大的值的下标。
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y,labels=tf.argmax(y_,1))
    #计算在当前batch中所有样例的交叉熵平均值。
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    print(cross_entropy_mean)
    print(tf.get_collection('losses'))
    #计算总的损失函数,为交叉熵损失与正则化损失之和。tf.add_n()函数是实现一个列表的元素的相加。
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    #设定指数衰减学习率,第三个参数为过完所有训练数据需要的迭代次数。
    learning_rate = tf.train.exponential_decay(LEARNNING_RATE_BASE,global_step,
                                               mnist.train.num_examples/BATCH_SIZE,LEARNING_RATE_DECAY)
    #使用tf.train.GradientDescentOptimizer优化算法来优化损失函数,这里的损失函数为总损失函数。
    #设置global_step=global_step使学习率得到自动更新
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss,global_step=global_step)
    #tf.group定义了两个操作,即通过反向传播来更新神经网络中的参数,又使每个参数的滑动平均值得到更新。
    train_op = tf.group(train_step,variable_averages_op)
    #初始化TensorFlow持久化类
    saver = tf.train.Saver()
    #初始化所有变量
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        
        #在训练过程中不再测试模型在验证数据上的表现,测试和验证将有一个独立的程序来完成。
        #迭代的训练神经网络,产生这一轮使用的一个batch的训练数据
        for i in range(TRAINING_STEPS):
            xs,ys = mnist.train.next_batch(BATCH_SIZE)
            #将训练数据的格式调整为一个四维矩阵,并将这个调整后的数据传入sess.run过程。
            reshaped_xs = np.reshape(xs,(BATCH_SIZE,mnist_infrernce_cnn.IMAGE_SIZE,
                                         mnist_infrernce_cnn.IMAGE_SIZE,mnist_infrernce_cnn.NUM_CHANNELS))
            #模型更新时的训练轮数和总的损失大小
            _,loss_value,step = sess.run([train_op,loss,global_step],feed_dict={x:reshaped_xs,y_:ys})
            
            #每1000轮保存一次模型
            if i % 1000 == 0:
                #输出当前的训练情况。这里只输出了模型在当前训练batch上的损失函数大小。通过损失函数大小可以
                #大概了解训练的情况,在验证数据集上的正确率信息会有一个单独的程序来生成。
                print("在经过%d轮训练后,在训练数据上的损失为%g."%(step,loss_value))
                #保存当前模型,注意这里给出了global_step参数,可以让每个被保存模型的末尾加上训练的轮数。
                saver.save(sess,os.path.join(MODE_SAVE_PATH,MODEL_NAME),global_step=global_step)
#主程序入口,                
def main(argv=None):
    #声明处理mnist数据集的类
    mnist = input_data.read_data_sets(r'C:\Users\Administrator\Desktop\学校事务\论文资料\MNIST',one_hot=True)
    train(mnist)
#TensorFlow提供一个主程序入口,tf.app.run()会调用上面定义的main函数    
if __name__ == '__main__':
    tf.app.run()

训练过程比较长,对内存的占用也很大,而且我每次训练结果都不相同。
浅谈薄见,如有疏漏,还望指正。

猜你喜欢

转载自blog.csdn.net/qq_40739970/article/details/85264343