TensorFlow入门及简单卷积神经网络实践

一 TensorFlow介绍

    TensorFlow是一个采用数据流图,用于数值计算的开源软件库。节点在图中表示数学操作,图中的线则表示在节点间相互联系的多维数据数组,即张量。它灵活的架构让你可以在多种平台上展开计算,例如台式计算机中的一个或多个CPU,服务器,移动设备等等。TensorFlow最初由Google大脑小组的研究员和工程师们开发出来,用于机器学习和深度神经网络方面的研究,但这个系统的通用性使其也可以广泛用于其他计算领域。其特点如下:

    1.高度的灵活性

     TensorFlow不是一个严格的“神经网络”库。只要你可以将你的计算表示为一个数据流图,你就可以使用TensorFlow。你来构建图,描写驱动计算的内部循环。

    2.真正的可移植性

    TensorFlow可以在CPU和GPU上运行,比如说可以运行在台式机、服务器、手机移动设备等等。想要在没有特殊硬件的前提下,在你的笔记本上跑一下机器学习的新想法?TensorFlow可以办到这点。准备将你的训练模型在多个CPU上规模化运算,又不想修改代码?TensorFlow可以办到这点。

    3.将科研和产品联系在一起

    过去如果要将科研中的机器学习想法应用到产品中去,需要大量的代码重写工作。那样的日子一去不复返了!在Google,科学家们用TensorFlow尝试新的算法,产品团队则用TensorFlow来训练和使用计算模型,并直接提供给在线用户。使用TensorFlow可以让应用型研究者将想法迅速运用到产品中,也可以让学术性研究者更直接地彼此分享代码,从而提高科研产出率。

二 TensorFlow中抽象概念介绍

    1.计算图

    什么是计算图?本质上是一个全局数据结构:是一个有向图,用于捕获有关指令。让我们来看看构建计算图的一个示例。在下图中,上半部分是我们运行的代码及其输出,下半部分是生成的计算图。

        import tensorflow as tf

        计算图:

        

        可见,仅仅导入Tensorflow并不会给我们生成一个计算图。而只是一个单独的、空白的全局变量。但当我们调用一个TensorFlow操作时,会发生什么?

        代码:

        import tensorflow as tf

        two_node = tf.constant(2)

        print (two_node)

        输出:Tensor("Const:0", shape=(), dtype=int32)

    计算图:

    

        我们得到了一个节点。它包含常量2.这来自于一个名为tf.constant的函数。当我们打印这个变量时,我们看到它返回一个tf.Tensor对象,它是一个指向我们刚刚创建的节点的指针。在看一个示例:

        代码:

        import tensorflow as tf

        two_node = tf.constant(2)

        anther_two_node = tf.constant(2)

        two_node = tf.constant(2)

        tf.constant(3)

        计算图:

        

        每次我们调用tf.constant时,我们都会在图中创建一个新的节点。即使该节点的功能与现有节点相同,即使我们将节点重新分配给同一个变量

    或者即使我们根本没有将其分配给一个变量,结果都是一样的。

    代码:

    import tensorflow as tf

    two_node = tf.constant(2)

    two_node = tf.constant(2)

    anther_pointer_at_two_node = two_node

    two_node = Node

    print two_node

    print anthor_pointer_at_two_node

    输出:

    Node

    Tensor("Const:0", shape=(), dtype=int32)

    计算图:

    

     让我们更近一步看看

    代码:

    import tensorflow as tf

    two_node = tf.constant(2)

    three_node = tf.constant(3)

    sum_node = two_node + three_node

    计算图:

    

    现在来讨论什么是真正的计算图,+操作在TensorFlow中过载,因此同时添加两个张量会在图中增加一个节点,尽管它表面上看起来不像是TensoeFlow操作。

    two_node指向包含2的节点,three_node指向包含3的节点,同时sum_node指向包含+的节点?难道不应该包含5吗?

实际上并没有,计算图只包含计算步骤,不包含结果。至少现在还没有!

    2.会话

    会话的作用是处理内存分配和优化,使我们能够实际执行由计算图指定的计算。你可以将计算图想象为我们想要执行的计算的模版:它列出了所有步骤。为了使用计算图,我们需要启动一个会话,它使我们能够实际地完成任务;例如,遍历模版的所有节点来分配一堆用于存储计算输出的存储器。为了使用 TensorFlow 进行各种计算,你既需要计算图也需要会话

    会话包含一个指向全局图的指针,该指针通过指向所有节点的指针不断更新。这意味着在创建节点之前还是之后创建会话都无所谓。

    创建会话对象后,可以使用 sess.run(node) 返回节点的值,并且 TensorFlow 将执行确定该值所需的所有计算。

    代码:

    import tensorflow as tf

    two_node = tf.constant(2)

    three_node = tf.constant(3)

    sum_node = two_node + three_node

    sess = tf.Session()

    print sess.run(sum_node)

    输出:5

    计算图:

    

    我们也可以传递一个列表,sess.run([node1, node2, ...]),并让它返回多个输出。

    一般来说,sess.run() 的调用往往是 TensorFlow 最大的瓶颈之一,因此调用它的次数越少越好。如果可以的话,在一个 sess.run() 的调用中返回多个项目,而不是进行多个调用。

    3.占位符和feed_dict

    我们所做的计算一直很乏味:没有机会获得输入,所以它们总是输出相同的东西。一个更有价值的应用可能涉及构建一个计算图,它接受输入,以某种(一致)方式处理它,并返回一个输出。

    最直接的方法是使用占位符。占位符是一种用于接受外部输入的节点。

    代码:

    import tensorflow as tf

    input = tf.placeholder(tf.int32)

    sess = tf.Session()

    print sess.run(input)

    输出:

    

    计算图:

    

    这是一个糟糕的例子,因为它引发了一个异常。占位符预计会被赋予一个值。但我们没有提供一个值,所以 TensorFlow 崩溃了。

    为了提供一个值,我们使用 sess.run() 的 feed_dixt 属性。

    代码:

    import tensorflow as tf

    input_placeholder = tf.placeholder(tf.int32)

    sess = tf.Session()

    print sess.run(input_placeholder, feed_dict={input_placeholder: 2})

    输出:2

    计算图:

    

    这就好多了。注意传递给 feed_dict 的 dict 格式,其关键应该是与图中的占位符节点相对应的变量(如前所述,它实际上意味着指向图中占位符节点的指针)。相应的值是要分配给每个占位符的数据元素——通常是标量或 Numpy 数组。

    4.计算路径

    来看看另一个使用占位符的例子。

    代码:

    import tensorflow as tf

    input_placeholder = tf.placeholder(tf.int32)

    three_node = tf.constant(3)

    sum_node = input_placeholder + three_node

    sess = tf.Session()

    print sess.run(three_node)

    print sess.run(sum_node)

    输出:

    

    计算图:

    

    

    为什么第二次调用 sess.run() 会失败?即使我们没有评估 input_placeholder,为什么仍会引发与 input_placeholder 相关

的错误?答案在于最终的关键 TensorFlow 抽象:计算路径。幸运的是,这个抽象非常直观。

    当我们在依赖于图中其他节点的节点上调用 sess.run() 时,我们也需要计算那些节点的值。如果这些节点具有依赖关系,那么

我们需要计算这些值(依此类推……),直到达到计算图的「顶端」,即节点没有父节点时。

    sum_node的计算路径:

    

    所有三个节点都需要进行求值以计算 sum_node 的值。最重要的是,这包含了我们未填充的占位符,并解释了异常!

    Three_node的计算路径:

    

    根据图结构,我们不需要计算所有节点才能评估我们想要的节点!因为我们在评估 three_node 时不需要评估

placehoolder_node,所以运行 sess.run(three_node) 不会引发异常。

    5.变量&副作用

    至此,我们已经看到两种类型的「无祖先」节点(no-ancestor node):每次运行都一样的 tf.constant 和每次运行都不一样的 tf.placeholder。我们常常要考虑第三种情况:一个通常在运行时保持值不变的节点也可以被更新为新值,这时就需要引入变量。

    变量对于使用 TensorFlow 进行深度学习是至关重要的,因为模型的参数就是变量。在训练期间,你希望通过梯度下降在每个步骤更新参数;但在评估时,你希望保持参数不变,并将大量不同的测试集输入模型。

通常,模型所有可训练参数都是变量。

    要创建变量,就需要使用 tf.get_variable()。tf.get_variable() 的前两个参数是必需的,其余参数是可选的。(关于各个函数的介绍在下文)它们是 tf.get_variable(name,shape)。name 是一个唯一标识这个变量对象的字符串。

它必须相对于全局图是唯一的,

    所以要明了你使用过的所有命名,确保没有重复。shape 是与张量形状对应的整数数组,它的语法非常直观:按顺序,每个维度只有一个整数。例如,一个 3x8 矩阵形状是 [3, 8]。要创建一个标量,就需要使用形状为 [] 的空列表。

    代码:

    import tensorflow as tf    

    count_variable = tf.get_variable("count", [])

    sess = tf.Session()

    print sess.run(count_variable)

    输出:

    

    计算图:

    

    另一个异常。当首次创建变量节点时,它的值基本上为「null」,并且任何试图对它求值的操作都会引发这个异常。

我们只能在将值放入变量之后才能对其求值。主要有两种将值放入变量的方法:初始化器和 tf.assign()。我们先看看 tf.assign():

    代码:

    import tensorflow as tf

    count_variable = tf.get_variable("count", [])

    zero_node = tf.constant(0.)

    assign_node = tf.assign(count_variable, zero_node)

    sess = tf.Session()

    sess.run(assign_node)

    print sess.run(count_variable)

    输出:0

    计算图:

    

    与我们迄今为止见过的节点相比,tf.assign(target, value) 是具备一些独特属性:

    恒等运算:tf.assign(target, value) 不做任何有趣的运算,通常与 value 相等。

    副作用:当计算流经assign_node 时,副作用发生在图中的其他节点上。此时,副作用是用存储在 zero_node 中的值替换 count_variable 的值。

     非依赖边:即使 count_variable 节点和 assign_node 在图中是相连的,但它们彼此独立。这意味着计算任一节点时,计算不会通过边回流。然而,assign_node 依赖于 zero_node,它需要知道分配了什么。

    副作用节点支撑着大部分 Tensorflow 深度学习工作流程,所以请确保自己真正理解了在该节点发生的事情。当我们调用 sess.run(assign_node) 时,计算路径会通过 assign_node 和 zero_node。

    计算图:

    

    

    当计算流经图中的任何节点时,它还会执行由该节点控制的任何副作用,如图中绿色所示。由于 tf.assign 的特殊副作用,

与 count_variable(之前为「null」)关联的内存现在被永久设置为 0。这意味着当我们下一次调用 sess.run(count_variable) 时,不会引发任何异常。相反,我们会得到 0 值。

    让我们看看初始化器:

    代码:

    import tensorflow as tf

    const_init_node = tf.constant_initializer(0.)

    count_variable = tf.get_variable("count", [], initializer=const_init_node)

    sess = tf.Session()

    print sess.run([count_variable])

    输出:

    

    计算图:

    

    为什么初始化器不工作?

    问题出现在会话和图之间的分离。我们已将 get_variable 的 initializer 属性设置为指向 const_init_node,但它只是在图中的

节点之间添加了一个新的连接。我们还没有做任何解决异常根源的事:与变量节点(存储在会话中,而不是计算图中)相关联的内

存仍然设置为「null」。我们需要通过会话使 const_init_node 去更新变量。

    代码:

    import tensorflow as tf

    const_init_node = tf.constant_initializer(0.)

    count_variable = tf.get_variable("count", [], initializer=const_init_node)

    init = tf.global_variables_initializer()

    sess = tf.Session()

    sess.run(init)

    print sess.run(count_variable)

    输出:0

    计算图:

    

    为此,我们添加另一个特殊的节点:init = tf.global_variables_initializer()。与 tf.assign() 类似,这是一个带有副作用的节点。

与 tf.assign() 相反,实际上我们不需要指定它的输入是什么!

    tf.global_variables_initializer() 将在其创建时查看全局图并自动将依赖关系添加到图中的每个 tf.initializer。当我们在之后使用 sess.run(init) 对它求值时,

它会告诉每个初始化程序执行变量初始化,并允许我们运行 sess.run(count_variable) 而不出错。

    6.总结

    计算图就是一个管道。编写网络就是搭建一个管道结构。在投入实际使用前,不会有任何液体进入管道。而神经网络中的权重和偏移就是管道中的阀门,可以控制液体的流动强弱和方向。

在神经网络的训练中,阀门会根据数据进行自我调节、更新。但是使用之前至少要给所有阀门一个初始的状态才能形成结构。用计算图的好处是它允许我们可以从任意一个节点处取出液体。

三 利用卷积神经网络训练Mnist数据集

    1.Mnist介绍

    Mnist是一个入门级的计算机视觉数据集,它包含各种手写数字图片:

            

    它也包含每一张图片对应的标签,告诉我们这个是数字几。比如,上面这四张图片的标签分别是5,0,4,1。

    Mnist数据集下载网址:http://yann.lecun.com/exdb/mnist/ 下载完成后解压到自己电脑相应的文件夹中:

    

    这样就完成了数据集的准备。

    2.前期准备

    系统:Win10家庭中文版64位

    python版本:

    

    TensorFlow:可以在cmd窗口下安装 pip install tensorflow (多试几次 前几次也没成功 可能和实验室的网有关系 下东西真不行)

    IDE:pycharm (安装最新的版本就可以了)

    其他:提示你少什么去网上下载就可以了

    3.代码实现

    导入相应的包

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

    将Mnist数据集导入, 路径填你自己存放位置。

mnist = input_data.read_data_sets("E:/Mnist_data", one_hot=True)

    权重初始化函数

    为了创建这个模型,我们需要创建大量的权重和偏置项。这个模型中的权重在初始化时应该加入少量的噪声来打破对称性以及避免0梯度。

由于我们使用的是ReLU神经元,因此比较好的做法是用一个较小的正数来初始化偏置项,以避免神经元节点输出恒为0的问题(dead neurons)。为了不在建立

模型的时候反复做初始化操作,我们定义两个函数用于初始化。

def weight_variable(shape):
    initial = tf.truncated_normal(shape,stddev=0.1)
    return tf.Variable(initial)

    偏置初始换函数

def bias_variable(shape):
    initial= tf.constant(0.1,shape=shape)
    return tf.Variable(initial)

    卷积层操作函数

    TensorFlow在卷积和池化上有很强的灵活性。我们怎么处理边界?步长应该设多大?在这个实例里,我们会一直使用vanilla版本。我们的卷积使用1步长(stride size),

0边距(padding size)的模板,保证输出和输入是同一个大小。我们的池化用简单传统的2x2大小的模板做max pooling。为了代码更简洁,我们把这部分抽象成一个函数。

def conv2d(x,w):
    return tf.nn.conv2d(x,w,strides=[1,1,1,1],padding='SAME')

   池化层操作函数

def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])

    每一个MNIST数据单元有两部分组成:一张包含手写数字的图片和一个对应的标签。x 代表输入的图片信息,一张图片包含28*28的

像素点,可以用一个数组来表示。将数字张开成一个向量,长度是28*28=784。如何展开这个数字并不重要,只要保证各个图片采用相同的方式展开。

    y_代表10种特征对应0到9。  

W_conv1 = weight_variable([5,5,1,32])
b_conv1 = bias_variable([32])

    第一层卷积的实现:它由一个卷积接一个max pooling完成。卷积在每个5x5的patch中算出32个特征。卷积的权重张量形状是[5, 5, 1, 32],前两个维度是patch的大小,

接着是输入的通道数目,最后是输出的通道数目。 而对于每一个输出通道都有一个对应的偏置量。

    为了用这一层,我们把x变成一个4d向量,其第2、第3维对应图片的宽、高,最后一维代表图片的颜色通道数(因为是灰度图所以这里的通道数为1,如果是rgb彩色图,则为3)。

x_image = tf.reshape(x,[-1,28,28,1])

    我们把X_image和权值向量进行卷积,加上偏置项,然后应用ReLU激活函数,最后进行max pooling。

h_conv1 = tf.nn.relu(conv2d(x_image,W_conv1) + b_conv1)
h_pool1= max_pool_2x2(h_conv1)

    第二层卷积的实现:为了构建一个更深的网络,我们会把几个类似的层堆叠起来。第二层中,每个5x5的patch会得到64个特征。

W_conv2 = weight_variable([5,5,32,64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1,W_conv2))
h_pool2 = max_pool_2x2(h_conv2)

    密集连接层:现在,图片尺寸减小到7x7,我们加入一个有1024个神经元的全连接层,用于处理整个图片。我们把池化层输出的张量reshape成一些向量,乘上权重矩阵,加上偏置,然后对其使用ReLU。    

    

W_fc1 = weight_variable([7*7*64,1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2,[-1,7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat,W_fc1) + b_fc1)

    Dropout:为了减少过拟合,我们在输出层之前加入dropout。我们用一个placeholder来代表一个神经元的输出在dropout中保持不变的概率。这样我们可以在训练过程中启用dropout,

在测试过程中关闭dropout。 TensorFlow的tf.nn.dropout操作除了可以屏蔽神经元的输出外,还会自动处理神经元输出值的scale。所以用dropout的时候可以不用考虑scale。

keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    输出层:最后,我们添加一个softmax层,就像前面的单层softmax regression一样。 

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

    训练和评估模型:为了进行训练和评估,我们使用与之前简单的单层SoftMax神经网络模型几乎相同的一套代码,只是我们会用更加复杂的ADAM优化器来做梯度最速下降,

在feed_dict中加入额外的参数keep_prob来控制dropout比例。然后每100次迭代输出一次日志。

cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
sess.run(tf.initialize_all_variables())
for i in range(2000):
    batch = mnist.train.next_batch(50)
    if i%100 == 0:
        train_accuracy = accuracy.eval(feed_dict={
        x:batch[0], y_: batch[1], keep_prob: 1.0})
        print ("step %d, training accuracy %g"%(i, train_accuracy))
    train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print ("test accuracy %g"%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

       

    为了训练我们的模型,我们首先需要定义一个指标来评估这个模型是好的。其实,在机器学习,我们通常定义指标来表示一个模型是坏的,这个指标称为成本(cost)或损失(loss),

然后尽量最小化这个指标。但是,这两种方式是相同的。一个非常常见的,非常漂亮的成本函数是“交叉熵”(cross-entropy)。交叉熵产生于信息论里面的信息压缩编码技术,

但是它后来演变成为从博弈论到机器学习等其他领域里的重要技术手段。它的定义如下:

    

    y 是我们预测的概率分布, y' 是实际的分布(我们输入的one-hot vector)。比较粗糙的理解是,交叉熵是用来衡量我们的预测用于描述真相的低效性。

    首先,用 tf.log 计算 y 的每个元素的对数。接下来,我们把 y_ 的每一个元素和 tf.log(y) 的对应元素相乘。最后,用 tf.reduce_sum 计算张量的所有元素的总和。(注意,这里的交叉熵不仅仅用来衡量单一的一对预测和真实值,

而是所有100幅图片的交叉熵的总和。对于100个数据点的预测表现比单一数据点的表现能更好地描述我们的模型的性能。

train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

    此函数是Adam优化算法:是一个寻找全局最优点的优化算法,引入了二次方梯度校正。

    TensorFlow在这里实际上所做的是,它会在后台给描述你的计算的那张图里面增加一系列新的计算操作单元用于实现反向传播算法和梯度下降算法。然后,它返回给你的只是一个单一的操作,当运行这个操作时,它用梯度下降算法训练你的模型,微调你的变量,不断减少成本。

现在,我们已经设置好了我们的模型。在运行计算之前,我们需要添加一个操作来初始化我们创建的变量:

sess.run(tf.initialize_all_variables())

    

    那么我们的模型性能如何呢?

    首先让我们找出那些预测正确的标签。tf.argmax 是一个非常有用的函数,它能给出某个tensor对象在某一维上的其数据最大值所在

的索引值。由于标签向量是由0,1组成,因此最大值1所在的索引位置就是类别标签,比如tf.argmax(y,1)返回的是模型对于任一输入x预测到的标签值,

而 tf.argmax(y_,1) 代表正确的标签,我们可以用 tf.equal 来检测我们的预测是否真实标签匹配(索引位置一样表示匹配)。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

    这行代码会给我们一组布尔值。为了确定正确预测项的比例,我们可以把布尔值转换成浮点数,然后取平均值。例如,[True, False, True, True] 会变成 [1,0,1,1] ,取平均值后得到 0.75。

  测试结果:

    刚开始训练准确率只有0.1:

    

    接下来的训练结果:

    

    最后测试的准确率有0.9775。(不知道是不是电脑原因,跑了很久才训练完)

四 总结

    大概了解了TensorFlow的运作机制,以及一些常用函数的用法。以后要深入了解TensorFlow如何导入数据,及数据处理。也看了点图像处理入门的视频就直接贴链接了https://v.youku.com/v_show/id_XMjk0MjkxNDA0.html?spm=a2h0j.11185381.listitem_page1.5!16~A(好像年代挺久远的视频)。

    代码来自极客公园TensorFlow教程。

    关于最后图片变成7*7的尺寸,

    

def conv2d(x,w):
    return tf.nn.conv2d(x,w,strides=[1,1,1,1],padding='SAME')
def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')

    卷积操作中padding=SMAE,处理完的图片尺寸大小不变,尽管W是[5,5,1,32]的张量。最大池化操作ksize=[1,2,2,1],strides[1,2,2,1]。尽管参数padding=SAME,但是步长为2。所以经过池化层图片大小为p=f/2。经过一次卷积和池化操作,图片尺寸变为原来的一半,经过两次这样的操作,图片尺寸变成原来的四分之一,原先为28*28,现在就变成了7*7。

    

      

    

    

    

   

    

猜你喜欢

转载自blog.csdn.net/paopaovae/article/details/81059451