TensorFlow基础教程(1)

主要根据郑泽宇,梁博文,顾思宇编著的《TensorFlow-实战Google深度学习框架》作为一个学习笔记。本篇包括书中的一到五章节的内容,主要是TensorFlow里面的基础概念和在mnist数据集上构建一个神经网络的操作介绍。

TensorFlow里面模块和功能众多,下面只会按照书中顺序进行简单的记录。同时由于书是2017年第一版,经过两年, 书中的部分代码使用的模块在新版本的TensorFlow中已经不建议使用或者被其他的替换,博客中的代码大多都作了修改,但仍有遗留错误的可能。详细的TensorFlow文档可以参考。

w3cschool上的TensorFlow文档

序言——从Google说起

Google公司作为人工智能领域的领头羊,实在是给了我们很多学习的参考,Tensorflow的火爆就是很好的例子。当年AlphaGo和AlphaZero在围棋博弈游戏上带给我们的震撼还没结束,AlphaStar就在星际争霸这种更为复杂的游戏博弈上给我们展示了人工智能的更大潜力。

AlphaGo主要由三部分组成,蒙特卡罗树搜索(MonteCarlo tree search,MCTS),估值网络(value network),走棋网络(policy network)。

关于蒙特卡罗树和AlphaGo的基本特性,在这篇文章中有详细介绍

英文原文地址 机器之心翻译地址

在众多的深度学习工具中,如Caffe,PyTorch等,TensorFlow在Github上的活跃度还是受关注程度都是最高的。

TensorFlow的主要依赖包

Protocol Buffer

这个包是用来处理结构化数据的工具。例如用户信息在Protocol Buffer上表示如下。

name: breeze
id: 1234
email: example@gmail.com

和XML和JSON格式有挺大的区别的

XML:

<user>
    <name>breeze</name>
    <id>1234</id>
    <email>example@gmail.com</email>
</user>

JSON:

{
    "name": "breeze",
    "id": "1234",
    "email": "example.gmail.com",
}

除此之外Pro�tocol Buffer序列化之后得到的数据不是可读的字符串,而是二进制流。其次,XML和JSON格式的数据信息都包含在了序列化后的数据中,不需要额外信息就能还原。Protocol Buffer还需要先定义数据的格式(schema),还原一个序列化的数据时候需要用到这个定义好的数据格式。如对应上面的格式例子,schema如下:

message user{
    optional string name = 1;
    required int32 id = 2;
    repeated string email = 3;
}

Protocol Buffer定义数据格式的文件一般保存在.proto文件中,每个message代表了一类结构化的数据。一个属性可以使必需的(required)也可以是可选的(optional)或者是可重复的(repeated)。在上面的例子中用户id是必需的,用户名可选可以不填写,一个用户可能有多个email地址所以是可重复的。

标签数字1和2则表示不同的字段在序列化后的二进制数据中的布局位置。在该例中,id字段编码后的数据一定位于name之后。需要注意的是该值在同一message中不能重复。在proto2,每个属性前必须加required,optional,repeated。后面跟的数字只要不重复,可以定义为任何数字,不需要总是从1或者0开始。

值得注意的是已经更新了proto3。在proto3语法中显式的 "optional" 关键字被禁止,因为字段默认就是可选的;同时必填字段不再被支持。另外在proto3中枚举值第一个必须是0,其他的随意。

enum Sex
{
    MALE = 0;
    FEMALE = 1;
}

proto3参考链接

Bazel

Bazel是谷歌的自动化构建工具,功能和Makefile差不多,在一个项目工作目录下需要有一个WORKSPACE文件定义对外部资源的依赖。

Bazel通过BUILD文件来找到需要编译的目标和确定编译的方式。Bazel对Python支持的编译方式有三种:py_binary,py_library,py_test三种,分别对应编译成可执行文件,库函数和测试程序。

目前假定工作目录下有两个.py文件,一个是hello_lib.py,一个是hello_main.py

hello_lib.py如下:

def print_hello_world():
    print("Hello World")

hello_main.py如下:

import hello_lib
hello_lib.print_hello_world()

那么BUILD文件应该这么写

py_library(
    name = "hello_lib",
    srcs = [
        "hello_lib.py",
    ]
)

py_binary(
    name = "hello_main",
    srcs = [
        "hello_main.py",
        ]
        deps = [
            ":hello_lib",
        ],
) 

srcs给出了编译所需要的源代码,deps给出了编译所需要的依赖关系。这两项都是一个list。

Tensorflow基础概念和定义

计算图和张量(Tensor)的概念

TensorFlow中每一个变量都是计算图上的一个节点,节点之间的边描述了各个节点之间的运算依赖关系。计算图的概念保证了变量与计算关系的统一管理。

张量(Tensor)一看就是TensorFlow的核心观点。所有的数据的属性都通过张量的形式来表示。从功能上看,张量可以被理解为多维数组。其中零阶张量表示标量(scalar),也就是单独的数;一阶张量就变成向量(vector)表示一纬数组;n阶张量就是表示高纬空间里的n纬数组。

还有值得在意的是张量在TensorFlow里面存储的不是实际的数值,而是保存如何得到这个节点的数字的计算过程。

下面我们用一个简单的向量加法解释下计算图和张量:

<img src = 'http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/Graph.png' width = '500'>

在TensorFlow中系统会维护一个默认的计算图,在终端交互中如果不特意指定,使用的就是默认的计算图。

在图中我们创建了两个变量a和b,两者在计算图中指向计算节点add。在计算图上每一个节点都代表了一个计算,计算的结果就保存在张量中。

result = tf.add(a, b, name="add_demo")tf.add()创建了一个计算,这个计算在计算图上的节点名叫做add_demo计算出的结果保存在result这个张量中。

通过print我们可以看到张量的结构。主要包含三个属性:名字(name)、纬度(shape)和类型(type)。"add_demo:0"表示了result这个张量是计算节点add_demo输出的第一个结果(编号从0开始)。shape = (2,)说明了张量result是个一维数组,数组长度为2。每一个张量会有一个唯一的类型,参与计算的张量的类型需要保持匹配。

对于纬度shape信息里面的2,1和空分别代表什么含义,下面的图作了额外解释:

<img src = 'http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/shape.png' width = '500'>

TensorFlow支持14种类型,包括了实数(tf.float32、tf.float64)、整数(tf.int8、tf.int16、tf.int32、tf.int64、tf.uint8)、布尔型(tf.bool)和复数(tf.complex64、tf.complex128)。

会话(session)的概念

会话用于执行定义好的运算,为计算图的运算提供运算环境。

sess = tf.Session()

sess.run()

sess.close()

或者利用Python里的with,使用上下文管理来管理这个会话,确保会话创建成功和结束后资源的释放。

with tf.Session() as sess:
    sess.run()

在命令行交互界面有直接构建会话的简便方法tf.InteractiveSession(),结合我们上面的例子有:

<img src = 'http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/session.png' width = '500'>

集合(collection)的概念

在一个计算图中,可以通过集合(collection)来管理不同类型的资源。这里的资源可以是张量、变量或者队列资源等。

TensorFlow中维护的集合列表:

集合名词 集合内容
tf.GraphKeys.VARIABLES 所有变量
tf.GraphKeys.TRAINABLE_VARIABLES 可学习的变量(神经网络中的参数)
tf.GraphKeys.SUMMARIES 日志生成的相关张量
tf.GraphKeys.QUEUE_RUNNERS 处理输入的QueueRunner
tf.GraphKeys.MOVING_AVERAGE_VARIABLES 计算了滑动平均值的变量

关于QueueRunner,需要谈到TensorFlow中的Queue,Queue是TF队列和缓存机制的实现,QueueRunner就是对操作Queue的线程的封装。
Tensorflow的计算主要在使用CPU/GPU和内存,而数据读取涉及磁盘操作,速度远低于前者操作。因此通常会使用多个线程读取数据,然后使用一个线程处理数据。QueueRunner就是来管理这些读写队列的线程的。Queue相关更多参考

滑动平均值可以看作是变量的过去一段时间取值的均值,相比对变量直接赋值而言,滑动平均得到的值在图像上更加平缓光滑,抖动性更小,不会因为某次的异常取值而使得滑动平均值波动很大。滑动平均模型常常用于提高神经网络模型在测试数据上的表现,这在后文会说。

TensorFlow中的常见指令

书中介绍的神经网络原理部分在另一篇博文神经网络与人工智能中介绍了,在此略过,着重整理书中出现的关于TensorFlow的功能命令。会按照不同任务阶段进行划分。

变量初始化

1.使用随机数生成器初始化变量

weights = tf.Variable(tf.random_normal([2,3], stddev=2))

这里产生了声明了一个矩阵,矩阵中的元素是均值为0,标准差为2的随机数。

tf.random_normal函数用来来产生满足正态分布的随机数,可以通过参数mean来确定平均值,默认为0。

tf.random_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None) 正太分布随机数,均值mean,标准差stddev。

tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None) 截断正态分布随机数,均值mean,标准差stddev,不过只保留[mean-2stddev,mean+2stddev]范围内的随机数。

TensorFlow里面的各种随机数生成器:

函数名称 随机数分布 主要参数
tf.random_normal 正态分布 平均值,标准差,取值类型
tf.random_uniform 均匀分布 最小、最大取值,取值类型
tf.random_gamma Gamma分布 形状参数alpha,尺度参数beta,取值类型

使用random_uniform()的示例

<img src = 'http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/random_uniform.png'>

对于Gamma分布的初始化,统计学没学好,不是很懂,给出两个链接
w3cschool上的理解
Gamma分布的知乎讨论

2.使用常量来初始化变量

函数名称 功能 样例
tf.zeros 产生全0的数组 tf.zeros([2,3],int32)->[[0,0,0],[0,0,0]]
tf.ones 产生全1的数组 tf.ones([2,3],int32)->[[1,1,1],[1,1,1]]
tf.fill 产生一个全部为给定数字的数组 tf.fill([2,3],9)->[[9,9,9],[9,9,9]]
tf.constant 产生一个给定值的常量 tf.constant([1,2,3])->[1,2,3]

3.通过其他变量的初始值来初始化新的变量

w2 = tf.Variable(weights.initialized_value())
w3 = tf.Variable(weights.initialized_value() * 2.0)

需要注意的是前面三种变量初始化方法相当于只是表明了初始化的格式,还没有真正赋值。所以对于每个使用了tf.Variable()指明为变量的张量,每次使用前需要对这个变量正式初始化。

weights = tf.Variable(tf.random_normal([2,3], stddev=2))为例:
初始化的方法有weights.initializer,后来发现weights.initialized_value()也相当于对变量进行初始化后返回初始化数值

<img src= 'http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/initialized_value.png'>

面对数目众多的变量需要初始化时不可能一个个初始化,这时候使用tf.global_variables_initializer()并行对训练模型中的众多变量进行统一初始化

import tensorflow as tf

weights = tf.Variable(tf.random_uniform([2, 3], minval = 1.0, maxval = 3.0))

w1 = tf.Variable(weights.initialized_value() * 2.0)

with tf.Session() as sess:
    init_op = tf.global_variables_initializer()
    sess.run(init_op)
    print(sess.run(weights))
    print(sess.run(w1))

结果如下:

<img src = 'http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/global_initalize.png'>

结合上面的集合的概念,在变量创建之后所有的变量都会自动地加入到GraphKeys.VARIABLES这个集合中,通过tf.global_variables()函数可以拿到当前计算图上所有的变量。如果声明变量时候参数trainable为True,那么这个变量将会被加入到GraphKeys.TRAINABLE_VARIABLES集合,可以通过tf.trainable_variables()函数得到这些变量。变量的类型在确定之后不能改变,但是纬度参数如果在初始设置时候加入额外参数validate_shape=False,就可以改变,不过一般来讲也不会这么干。

输入训练数据

多次迭代训练所需要的训练数据不可能全部以常量形式保存在计算图中,TensorFlow提供了placeholder机制用于提供输入数据,往往搭配feed_dict使用。

下面是对n个样例计算前向传播结果的代码示例。

import tensorflow as tf

w1 = tf.Variable(tf.random_normal([2,3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3,1], stddev=1, seed=1))

x = tf.placeholder(tf.float32, shape=(n,2), name="input")
#placeholder作为存放输入数据的地方,纬度不一定需要定义,如果输入数据的纬度是确定的,定义可以保证程序的稳定。

a = tf.matmul(x, w1)
y = tf.matmul(a, w2) #tf.matmul()矩阵乘法

sess = tf.Session()
init_op = tf.global_variables_initializer()
sess.run(init_op)

print(sess.run(y, feed_dict={x: [[0.7,0.2], [0.1,0.4], [0.6,0.5]...})) #n个样例输入

计算y时候需要把一个3\times2的矩阵传入placeholder,否则x的数值不明确无法计算。相当于placeholder是给需要的输入数据在计算图里面先预留了一个坑,真正运行时候需要把需要的值填入。

设置代价函数

经典代价函数

交叉熵函数

C = - \frac{1}{n}\sum_{x}[y\ln{a} + (1-y)\ln{(1-a)}]

其中y是正确的结果,a是模型预测的结果。

如果面对的是一个分类的问题y这个张量的值非0即1,那么上面的交叉熵函数退化为

C = - \frac{1}{n}\sum_{x}y\ln{a}

表示的代码如下:

cross_entropy = -tf.reduce_mean(y * tf.log(tf.clip_by_value(a, 1e-10, 1.0)))

均方代价函数

C(w,b)\equiv\frac{1}{2n}\sum_{x}\|y(x)-a\|^2

代码表示:

mse = tf.reduce_mean(tf.square(y-a))


1.*运算

代表元素之间直接相乘,也就是Hadamard乘积

v1 = tf.constant([[1.0, 2.0], [3.0, 4.0]])
v2 = tf.constant([[5.0, 6.0], [7.0, 8.0]])

print((v1 * v2).eval()

# 输出
[[ 5. 12.]
 [21. 32.]]

2.reduce_mean

这个函数用于计算张量tensor沿着指定的数轴(tensor的某一维度)上的的平均值,主要用作降维或者计算tensor(图像)的平均值。

  • 第一个参数input_tensor: 输入的待降维的tensor;
  • 第二个参数axis: 指定的轴,如果不指定,则计算所有元素的均值;
  • 第三个参数keep_dims:是否降维度,设置为True,输出的结果保持输入tensor的形状,设置为False,输出结果会降低维度;

简单的例子如下

#’x' is [[1, 1, 1]
#        [1, 1, 1]]

tf.reduce_sum(x) ==> 6
tf.reduce_sum(x, 0) ==> [2, 2, 2]
tf.reduce_sum(x, 1) ==> [3, 3]
tf.reduce_sum(x, 1, keep_dims=True) ==> [[3], [3]]
tf.reduce_sum(x, [0, 1]) ==> 6

其余类似的函数还有

  • tf.reduce_sum :计算tensor指定轴方向上的所有元素的累加和;
  • tf.reduce_max : 计算tensor指定轴方向上的各个元素的最大值;
  • tf.reduce_all : 计算tensor指定轴方向上的各个元素的逻辑和(and运算);
  • tf.reduce_any: 计算tensor指定轴方向上的各个元素的逻辑或(or运算);

3.tf.clip_by_value

这个函数可以将张量中的数值限制在一个范围内,避免一些运算的错误,比如上面的例子中把log0看成是无效的(1的-10次方近似看成0)

因为交叉熵函数一般会与softmax回归一起使用,所以TensorFlow对这两个部分进行的封装:

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y, logits=a)

在只有一个正确答案的分类问题中,TensorFlow提供了

tf.nn.sparse_softmax_cross_entropy_with_logits函数加速计算过程。

自定义代价函数

对于一些特殊的问题可能需要自定义代价函数,譬如想要定义的损失函数如下:

Loss(y,y^\prime)=\sum_{i=1}^nf(y_i,y_i^\prime)

f(x,y)= \bigg\{\begin{aligned} a(x-y)\qquad x>y\\\ b(y-x)\qquad x\leq y\end{aligned}

代码表示:

loss = tf.reduce_sum(tf.where(tf.greater(v1, v2),(v1-v2) * a, (v2-v1) * b))

tf.greater的输入是两个张量,此函数会比较这两个输入张量中每一个元素的大小,并返回比较结果。

tf.where函数有三个参数。第一个为选择条件根据,当选择条件为True时,选择第二个参数,否则使用第三个参数的值。值得注意的是tf.where函数判断和选择都是在元素级别进行的。

<img src='http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/tf.where.png'>

使用L2和L1规范化

L2规范化和L1规范化TensorFlow都提供了对应的函数。
tf.contrib.layers.l2_regularizer
tf.contrib.layers.l1_regularizer

如果是简单的网络里把loss变化下就行:

loss = tf.reduce_mean(tf.square(y_ - y) + tf.contrib.layers.l2_regularizer(lambda)(w)

这里的lambda就是\lambda代表规范化参数。

不过重新定义loss的方式在面对多层神经网络时候loss的表达式过长,可读性也差。

下面利用集合(collection),完整写一下5层神经网络带L2规范化的损失函数的计算方法。

import tensorflow as tf

#获取一层神经网络边上的权重,并将这个权重的L2正则化损失加入名为'losses'的集合中,输入两个参数,一个是里面定义的var的纬度,一个lambda就是规范化参数
def get_weight(shape, lambda):
    # 生成一个初始变量,协助完成权重初始化
    var = tf.Variable(tf.random_normal(shape), dtype = tf.float32)
    # add_to_collection 把这一层新生成变量的L2规范化损失项加入集合
    tf.add_to_collection(
        'losses',tf.contrib.layers.l2_regularizer(lambda)(var))
    return var

x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))
batch_size = 8

#定义每一层网络中节点的个数
layer_dimension = [2, 10, 10, 10, 1]

#神经网络的层数

n_layers = len(layer_dimension)

#这个变量维护前向传播是最深层次的节点,最开始的时候为输入层,所以初始值为x
cur_layer = x

in_dimension = layer_dimension[0]

#一个循环建立一个5层全连接网络

for i in range(1, n_layers):
    out_dimension = layer_dimension[i]
    weight = get_weight([in_dimension, out_dimension], 0.001)
    bias = tf.Variable(tf.constant(0.1, shape=[out_dimension]))
    #使用ReLU激活函数
    cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight) + bias)
    in_dimension = layer_dimension[i]
    
mse_loss = tf.reduce_mean(tf.square(y_ - cur_layer)

#将均方误差也加入'losses'集合

tf.add_to_collection('losses', mse_loss)

#把'losses'这个集合的元素加起来就是最后loss的表达式
loss = tf.add_n(tf.get_collection('losses')

这里值得注意的是:结合计算图的概念,'losses'集合里存的都不是具体的数值,都是一个个节点元素。并且上面的代码只是完成了结构的定义,还没有进行真正的初始化和优化器优化。

设定学习速率

较小的学习率虽然能保证收敛的效果,但会导致收敛速度过慢。较大的学习率能快速收敛但效果不一定好。所以基本不同的迭代轮次设置不同的学习率是必要的。

TensorFlow提供了使用指数衰减法设置学习率的函数 tf.train.exponential_decay

这个函数的定义如下:

decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)

decay_learning_rate为每一轮优化时使用的学习率,learning_rate为事先设定的初始学习率,decay_rate为衰减系数,decay_steps为衰减速度。在这个函数中还有一个参数staircase, 默认为False, 当staircase设置为True的时候,global_step/decay_steps会被转化成整数,使得学习率变成一个阶梯函数。

global_step在滑动平均、优化器、指数衰减学习率等方面都有用到,这个变量的实际意义非常好理解:代表全局步数,比如在多少步该进行什么操作,现在神经网络训练到多少轮等等,类似于一个钟表。

大致的用法如下:

import tensorflow as tf
import numpy as np
 
x = tf.placeholder(tf.float32, shape=[None, 1], name='x')
y = tf.placeholder(tf.float32, shape=[None, 1], name='y')
w = tf.Variable(tf.constant(0.0))
 
global_steps = tf.Variable(0, trainable=False)
 
 
learning_rate = tf.train.exponential_decay(0.1, global_steps, 10, 2, staircase=False)
loss = tf.pow(w*x-y, 2)
 
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss,global_step=global_steps)
 
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for i in range(10):
        sess.run(train_step, feed_dict={x:np.linspace(1,2,10).reshape([10,1]),
            y:np.linspace(1,2,10).reshape([10,1])})
        print(sess.run(learning_rate))
        print(sess.run(global_steps))

可以注意到global_steps被初始化为0,但是运行上面的代码我们会发现global_steps每轮迭代都会自动加1。如果把设定优化器的那一行代码中的global_step=global_steps去掉,global_step的自动加一就会失效。
综上所述:损失函数优化器的minimize()global_step=global_steps能够提供global_step自动加一的操作。参考自

使用滑动平均模型

TensorFlow里面提供了tf.train.ExponentialMovingAverage也就是上文已经提到过的滑动平均值的概念来实现滑动平均模型。

在初始化模型时候,需要提供一个衰减率,这个衰减率将会控制模型更新的速度。同时滑动平均模型对每一个变量都会维护一个影子变量(shadow variable),这个影子变量的初始值就是对应变量的初始值。但是每次运行变量更新时候,影子变量的更新规则为:

shadow\_variable = decay \times shadow\_variable + (1-decay) \times variable

在实际中decay的值会设定成十分靠近1的数值(0.99,0.999)。为了使模型前期训练地更快,ExponentialMovingAverage还提供了num_updates参数来动态控制decay的大小,如果提供了这个参数,每次使用的衰减率会从下面两个数值中取较小值。

min\left\{decay,\frac{1+num\_updates}{10+num\_updates}\right\}

mnist数据集上的实例展示

这个实例是书本第五章在mnist数据集上进行训练的例子。主要为了展示上面提到的滑动平均模型,动态学习率和规范化方法在TensorFlow中的使用方法。

在书中使用了tensorflow.examples.tutorials.mnist这个工具包中的input_data来自动导入mnist数据集并分类。但是实际运行过程中TensorFlow警告这个功能在不久的版本中就会被移除,建议自己写程序导入mnist数据集并且划分训练集,验证集和测试集。加上终端运行起来一开始一直提示报错,所以直接又创建了一个load_mnist.py来导入数据并对书中的代码改动了一部分。

load_mnist.py

第一部分代码是载入mnist的代码,参考自 这里面详细介绍了mnist数据集的内部格式和Python3中的导入方法。

import numpy as np
from struct import unpack

def __read_image(path):
    with open(path, 'rb') as f:
        magic, num, rows, cols = unpack('>4I', f.read(16))
        img = np.fromfile(f, dtype=np.uint8).reshape(num, 784)
    return img

def __read_label(path):
    with open(path, 'rb') as f:
        magic, num = unpack('>2I', f.read(8))
        lab = np.fromfile(f, dtype=np.uint8)
    return lab
    
def __normalize_image(image):
    img = image.astype(np.float32) / 255.0
    return img

def __one_hot_label(label):
    lab = np.zeros((label.size, 10))
    for i, row in enumerate(lab):
        row[label[i]] = 1
    return lab

def load_mnist(train_image_path, train_label_path, test_image_path, test_label_path, normalize=True, one_hot=True):
    '''读入MNIST数据集
    Parameters
    ----------
    normalize : 将图像的像素值正规化为0.0~1.0
    one_hot_label : 
        one_hot为True的情况下,标签作为one-hot数组返回
        one-hot数组是指[0,0,1,0,0,0,0,0,0,0]这样的数组
    Returns
    ----------
    (训练图像, 训练标签), (测试图像, 测试标签)
    '''
    image_train = __read_image(train_image_path)
    image_test = __read_image(test_image_path)

    label_train = __read_label(train_label_path)
    label_test = __read_label(test_label_path)
    
    
    if normalize:
        image_train = __normalize_image(image_train)
        image_test = __normalize_image(image_test)

    if one_hot:
        label_train = __one_hot_label(label_train)
        label_test = __one_hot_label(label_test)

    return image_train, label_train, image_test, label_test

mnist_tf.py

这部分基本和书中代码没有区别,修改了几个训练数据量的参数,和因为不使用tensorflow.examples.tutorials.mnist进行的改动。

import tensorflow as tf
import load_mnist
# MNIST数据集相关常数

INPUT_NODE = 784 # 输入层的节点数,对于MNIST数据集,就是28*28的图片
OUTPUT_NODE = 10 # 输出层的节点数,因为需要用0-9标注手写数字的结果

# 神经网络的一些配置参数

LAYER1_NODE = 500 # 本例子使用只有一层500个节点的隐藏层的网络结构作为样例

BATCH_SIZE = 100 # 一个batch中的数据量,越小越接近随机梯度下降,越大越接近梯度下降

LEARNING_RATE_BASE = 0.8 # 基础学习速率
LEARNING_RATE_DECAY = 0.99 # 使用动态学习率的衰减率

REGULARIZATION_RATE = 0.0001 # 规范化参数
TRAINING_STEPS = 400 #训练轮数
MOVING_AVERAGE_DECAY = 0.99 #滑动平均衰减率

# 下面定义了一个辅助函数给定神经网络的输入和所有参数,计算出神经网络的前向传播结果。
# 定义了一个ReLU激活函数,设定了输入层,一层隐层,一层输出层的最简单的神经网络结构

def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
    # avg_class是决定用不用滑动平均模型的参数,当没有提供滑动平均类时,直接使用参数当前的值。
    # weights1 和 biases1 是隐层参数,weights2 和 biases2 是输出层参数
    # 这里没有加上softmax层是因为后面定义代价函数的时候用的是sparse_softmax_cross_entropy_with_logits函数已经附带了softmax
    if avg_class == None:
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
        return tf.matmul(layer1, weights2) + biases2
    else:
        # 首先使用avg_class.average函数来计算得出变量的滑动平均值
        # avg_class 的申明会在后面申明,这里作为已知的滑动平均类参数使用
        layer1 = tf.nn.relu(tf.matmul(input_tensor, avg_class.average(weights1)) + avg_class.average(biases1))
        return tf.matmul(layer1, avg_class.average(weights2)) + avg_class.average(biases2)

def train(train_images, train_labels, test_images, test_labels):
    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-output')

    #生成隐藏层的参数
    weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
    biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))
    #生成输出层的参数
    weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
    biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))

    #计算不使用滑动平均值的前向传播的值

    y = inference(x, None, weights1, biases1, weights2, biases2)

    #定义存储训练轮数的变量,这个变量就算在使用滑动平均模型的情况下也不需要滑动平均所以设置为不可训练变量
    #把这个global_step变量初始化为0,之后这个变量每轮迭代会自动加一
    global_step = tf.Variable(0, trainable=False)

    #给定滑动平均衰减率和训练轮数的变量,初始化滑动平均类。
    variable_average = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)

    #在所有需要的变量进行滑动平均。tf_trainable_variables返回的就是在GraphKeys.TRAINABLE_VARIABLES中的元素。
    variables_average_op = variable_average.apply(tf.trainable_variables())

    #计算使用了滑动平均之后的前向传播的结果
    average_y = inference(x, variable_average, weights1, biases1, weights2, biases2)

    #使用交叉熵函数作为损失函数,然后在计算代价之前使用softmax
    #这里使用TensorFlow里的sparse_softmax_cross_entropy_with_logits函数,值得注意的是这边使用的是不经过滑动平均的前向传播值计算的代价函数值
    #因为mnist给出的标准答案是一个长度为10的一维数组,而该函数需要返回的是一个正确答案的数字,所以需要使用tf.argmax函数进行一下转化
    #tf.argmax的第二个参数为‘1’时候表示选取最大值的操作仅在第一个纬度中进行,相当于按行找。为‘0’时候相当一按列找最大值。返回值都是最大值的下标。
    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)
    
    #定义一个regularizer
    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    #计算模型的规范化
    regularization = regularizer(weights1) + regularizer(weights2)

    #总损失等于规范化损失加上原本的交叉熵损失
    loss = cross_entropy_mean + regularization 

    #设置动态指数衰减的学习率
    learning_rate = tf.train.exponential_decay(
            LEARNING_RATE_BASE,  #基础学习率
            global_step,
            40000 / BATCH_SIZE, #总训练数据量除以batch大小等于所需要的迭代轮数
            LEARNING_RATE_DECAY)

    #使用tf.train.GradientDescentOptimizer优化算法来优化损失
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)

    #在训练神经网络模型的时候,需要用反向传播不断更新神经网络的参数,同时需要更新每一个变量的滑动平均值。
    #为了一次完成这几个操作, TensorFlow提供了tf.group机制,相当于是把两个操作符分组组成一个操作符, 这里把优化器的动态修改操作和滑动平均操作修改操作合并
    train_op = tf.group(train_step, variables_average_op)
    
     #判断使用了滑动平均模型的神经网络的前向传播结果是否和标准结果一致
    correct_prediction = tf.equal(tf.argmax(average_y, 1),tf.argmax(y_, 1))
    #tf.cast(x, dtype, name=None) 转化数据格式的函数,这里把一个布尔型的数值转化为实数型,方便均值的操作
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    #初始化会话并开始训练
    with tf.Session() as sess:
    
        tf.global_variables_initializer().run() #变量初始化
        #准备验证数据
        validate_feed = {x: train_images[55000:], y_: train_labels[55000:]}
        #准备测试数据
        test_feed = {x: test_images, y_: test_labels}
        #迭代地开始训练
        
        for i in range(TRAINING_STEPS):
            #每100轮输出一次在验证数据集上的测试结果。
            if i % 100 == 0:
                validate_acc = sess.run(accuracy, feed_dict=validate_feed)
                print("After %d training steps, validation accuracy using average model is %g" % (i, validate_acc))
            #产生这一轮使用的一个batch的训练数据
            xs = train_images[i*BATCH_SIZE:(i+1)*BATCH_SIZE]
            ys = train_labels[i*BATCH_SIZE:(i+1)*BATCH_SIZE]    
           
            sess.run(train_op, feed_dict={x: xs, y_: ys})        
        #在训练结束后输出最终正确率
        test_acc = sess.run(accuracy, feed_dict=test_feed)
        print("After %d training steps, test accuracy using average model is %g" % (TRAINING_STEPS, test_acc))

def main(argv=None):
    train_image_path = './train-images.idx3-ubyte'
    train_label_path = './train-labels.idx1-ubyte'
    test_image_path = './t10k-images.idx3-ubyte'
    test_label_path = './t10k-labels.idx1-ubyte'
    train_images, train_labels, test_images, test_labels = load_mnist.load_mnist(train_image_path, train_label_path, test_image_path, test_label_path, normalize=True, one_hot=True)
    train(train_images, train_labels, test_images, test_labels)

# TensorFlow提供的一个主程序入口,tf.app.run会调用上面的这个main函数
if __name__ == '__main__':
    tf.app.run()


运行结果

<img src='http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/outcome.png'>

变量管理

在上面的演示代码中有个计算前向传播的函数

def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2)

可以看到我们每次使用的时候都要输入众多的参数。对此改进的话,变量的管理可以使用tf.get_variable()tf.variable_scope()

首先来看tf.get_variable()tf.Variable()的区别。tf.get_variable()也可以用来创建新的变量,除此之外还可以配合tf.variable_scope获取变量。

#下面的两种变量的命名是等价的

v = tf.get_variable("v", shape=[1], initializer=tf.constant_initializer(1.0))

v = tf.Variable(tf.constant(1.0, shape=[1]), name="v")

值得注意的是在tf.Variable()里面name="v"这个指定变量名称的参数不是一个必要的参数,但在tf.get_variable()里面就是一个必填的参数,因为这个函数有时候还需要去根据函数名字获取变量。

如果需要tf.get_variable()获取一个已经创建的变量,需要通过tf.variable_scope()函数生成一个上下文管理器。这个函数相当于生成了一个命名空间,具有变量的管理功能。

值得注意的基本有下面几点:

  • 同一个命名空间中不可以有同名变量
  • tf.variable_scope有一个参数reuse,设置为True的话可以获取其他命名空间或者自己空间里已经命名的变量。
  • 创建命名空间可以嵌套
  • 在一个命名空间里面创建的变量名称都会带上这个命名空间名作为前缀。
#在名字为foo的命名空间内创建名字为v的变量

with tf.variable_scope("test"):
    v1 = tf.get_variable("v", shape=[1], initializer=tf.constant_initializer(1.0))
    print(v.name) #输出test/v:0, 'test/v'表示是test这个命名空间下名为v的变量,':0'表示是v1这个运算下输出的第一个结果
    
#如果你再想在test这个命名空间内创建一个命名为"v"的同名变量会报错:
#Variable test/v already exists

with tf.variable_scope("test"):
    v2 = tf.get_variable("v", [1])
    
#命名空间的嵌套
with tf.variable_scope("test"):
    with tf.variable_scope("foo"):
        v3 = tf.get_variable("v", [1])
        print(v3.name) # 输出test/foo/v:0
        
    v4 = tf.get_variable("v1", [1])
    print(v4.name) # 输出test/v1:0。当一个命名空间退出后,前缀变化。
    
#创建一个新的命名空间,调用test里的变量。可以直接通过带命名空间名称的变量名来获取其他命名空间下的变量

with tf.variable_scope("reference", reuse=True)
    v5 = tf.get_variable("test/foo/v", [1])
    print(v5 == v3) #输出True
    v6 = tf.get_variable("test/v1", [1])
    print(v6 == v4) #输出True

这时候我们对之前提到inference函数进行优化修改

def inference(input_tensor, reuse=False):
    #定义第一层神经网络的变量和前向传播的过程
    with tf.variable_scope('layer1', reuse=reuse):
        #根据传进来的reuse参数来决定是创建新变量还是使用已经创建好的。
        #在第一次构造网络时候必然需要创建新的变量,以后每次调用这个函数时候把reuse这个变量设置为True就可以
        weights = tf.get_variable("weights", [INPUT_NODE, LAYER1_NODE], initializer=tf.truncated_normal_initializer(stddev=0.1))
        biases = tf.get_variable("biases", [LAYER1_NODE], initializer=tf.constant_initializer(0.0))
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)
    #类似的定义第二层神经网络的变量和前向传播过程
    with tf.variable_scope('layer2', reuse=reuse):
        weights = tf.get_variable("weights", [LAYER1_NODE, OUTPUT_NODE], initializer=tf.truncated_normal_initializer(stddev=0.1))
        biases = tf.get_variable("biases", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))
        layer2 = tf.matmul(layer1, weights) + biases
    return layer2

这样写的话就不用将所有变量都作为参数传递到函数中了

def train(train_images, train_labels, test_images, test_labels):
    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-output')

    #生成隐藏层的参数
    weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
    biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))
    #生成输出层的参数
    weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
    biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))
    
    #计算不使用滑动平均值的前向传播的值

    y = inference(x, None, weights1, biases1, weights2, biases2)

这么一长串提前生成变量的操作被整合到inference中,方便复用

#第一次调用
x = tf.placeholder(tf.float32, [None, INPUT_NODE], name="x-input')
y = inference(x)

#复用
new_x = ...
new_y = inference(new_x, True)

模型持久化

模型持久化指的是每次训练完的模型保存下来方便下次使用,这样训练结果可以复用。对此,TensorFlow提供了一个tf.train.Saver类。

saver=tf.train.Saver(max_to_keep=1)

在创建这个Saver对象的时候,有一个参数我们经常会用到,就是max_to_keep参数,这个是用来设置保存模型的个数,默认为5,即max_to_keep=5,保存最近的5个模型。
如果你想每训练一代(epoch)就想保存一次模型,则可以将max_to_keep设置为None或者0。

保存

saver.save(sess,'ckpt/mnist.ckpt',global_step=step)

第一个参数sess, 第二个参数设定保存的路径和名字,第三个参数将训练的次数作为后缀加入到模型名字中。

import tensorflow as tf
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2

init_op = tf.global_variables_initializer()
#声明tf.train.Saver()类用来保存模型
saver = tf.train.Saver(max_to_keep=3) 

with tf.Session() as sess:
    sess.run(init_op)
    #将模型保存到ckpt文件夹下
    saver.save(sess, "ckpt/model.ckpt")

虽然上面只指定了一个文件路径,但是在目录里面会出现三个文件。上面代码生成model.ckpt.meta保存计算图结构,model.ckpt保存每个变量的取值,最后一个文件为checkpoint保存了目录下所有模型文件的列表。

加载

saver.restore(sess, "ckpt/model.ckpt")

import tensorflow as tf
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2

saver = tf.train.Saver(max_to_keep=3) 

with tf.Session() as sess:
    #加载已经保存的模型,并通过已经保存的模型中变量的值来计算加法
    saver.restore(sess, "ckpt/model.ckpt")
    print(sess.run(result))

在加载模型的代码中没有运行变量初始化,而是直接将变量的值通过已经保存的模型加载进来。

除了上面保存和加载了整个模型全部变量的例子,也可以选择保存或者加载部分变量。可以提供一个列表来指定需要保存或者加载的变量。
如只加载上面的"v1"变量:
saver = tf.train.Saver([v1])

变量重命名

tf.train.Saver类也支持在保存或者加载时候给变量重命名,用字典将模型保存时的变量名和需要加载的变量联系起来。这样做的主要目的之一是方便使用变量的滑动平均值,因为我们在使用了滑动平均模型之后更关注的其实是各个变量经过滑动平均之后的值,我们只想用这个值代替原本的变量保存下来。

import tensorflow as tf

v = tf.Variable(0, dtype=tf.float32, name="v")
ema = tf.train.ExponentialMovingAverage(0.99)
maintain_average_op = ema.apply(tf.global_variables())

#在申明滑动平均模型之后,TensorFlow会自动生成一个影子变量
#v/ExponentialMoving Average

#可以通过变量重命名来将原来的变量v的滑动平均值直接复制给v。
saver = tf.train.Saver({"v/ExponentialMovingAverage":v})
with tf.Session() as sess:
    saver.restore(sess, "ckpt/model.ckpt")
    print(sess.run(v)) #输出模型中v的滑动平均值

当变量多的时候为了方便,tf.train.ExponentialMovingAverage类提供了variables_to_restore函数来生成tf.train.Saver类所需要的重命名字典。

import tensorflow as tf

v = tf.Variable(0, dtype=tf.float32, name="v")
ema = tf.train.ExponentialMovingAverage(0.99)

#通过使用variables_to_restore函数可以直接生成上面代码中提供的字典
#下面的print会输出{'v/ExponentialMovingAverage':<tensorflow.Variable 'v:0' shape=() dtype=float32_ref>}

print(ema.variables_to_restore())

saver = tf.train.Saver(ema.variables_to_restore())

with tf.Session() as sess:
    saver.restore(sess, "ckpt/model.ckpt")
    print(sess.run(v)) #输出模型中v的滑动平均值

变量转常量

使用tf.train.Saver会保存运行TensorFlow需要的全部信息,然而在模型测试或者离线预测的时候,只需要知道如何从输入层经过前向传播到输出层,并不需要知道变量初始化或者模型保存等辅助节点的信息。通过convert_variables_to_constants函数,可以将计算图中的变量和取值方式通过常量的方式保存,这样整个TensorFlow计算图可以统一存放在一个文件中,而不是像之前用tf.train.Saver一样需要分别存放在三个文件中。参考

存储

import tensorflow as tf
from tensorflow.python.framework import graph_util

v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2

init_op = tf.global_variables_initializer()

with tf.Session() as sess:
    sess.run(init_op)
    #导出当前计算图的GraphDef部分,主需要这部分就能完成从输入层到输出层的计算过程
    graph_def = tf.get_default_graph().as_graph_def()
    output_graph_def = graph_util.convert_variables_to_constants(sess, graph_def, ['add'])
    with tf.gfile.GFile("ckpt/combined_model.pb", "wb") as f:
        f.write(output_graph_def.SerializeToString())

如果只关心程序中定义的某些运算的时候,其他的节点就没有必要导出并保存了,上面就只是保存了两个变量相加的计算节点:add。在上面的代码中['add']参数给出了需要保存的节点的名称,这里给出的是计算节点的名称,所以后面不带':0'。
(在前面介绍过张量的名称后面有:0,表示是某个计算节点的第一个输出,计算节点本身的名称后面是不带0的)

读取

import tensorflow as tf
from tensorflow.python.platform import gfile

with tf.Session() as sess:
    model_filename = "ckpt/combined_model.pb"
    #读取保存的模型文件,并将文件解析成对应的GraphDef Protocol Buffer
    with open(model_filename, 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
        
    # 将graph_def 中保存的图加载到当前的图中。return_elements=["add:0"]给出了返回的张量的名称。
    #在保存的时候给出的是计算节点的名称,所以为"add"。在加载的时候给出的是张量的名称,所以是"add:0"。
    result = tf.import_graph_def(graph_def, return_elements=["add:0"])
    print(sess.run(result))

结果:

<img src='http://breezepicture.oss-cn-beijing.aliyuncs.com/Tensorflow/saver_pb.png'>

  • graph_util模块是操作张量图用的。
  • gfile模块的介绍 值得注意的是open操作使用范围很广,还有就是tf.gfile.FastGFile()官方建议换成tf.gfile.GFile

实例展示

在定义 saver 的时候一般会定义最多保存模型的数量,一般来说,如果模型本身很大,我们需要考虑到硬盘大小。如果你需要在当前训练好的模型的基础上进行 fine-tune,那么尽可能多的保存模型,后继 fine-tune 不一定从最好的 ckpt 进行,因为有可能一下子就过拟合了。但是如果保存太多,硬盘也有压力。如果只想保留最好的模型,方法就是每次迭代到一定步数就在验证集上计算一次 accuracy ,如果本次结果比上次好才保存新的模型,否则没必要保存。

下面展示保存准确度最高的三代,并且在验证集上测试的代码。参考

saver=tf.train.Saver(max_to_keep=3)

#训练阶段
if is_train:
    max_acc=0
    f=open('ckpt/acc.txt','w')
    for i in range(100):
      batch_xs, batch_ys = mnist.train.next_batch(100)
      sess.run(train_op, feed_dict={x: batch_xs, y_: batch_ys})
      val_loss,val_acc=sess.run([loss,acc], feed_dict={x: mnist.test.images, y_: mnist.test.labels})
      print('epoch:%d, val_loss:%f, val_acc:%f'%(i,val_loss,val_acc))
      f.write(str(i+1)+', val_acc: '+str(val_acc)+'\n')
      if val_acc>max_acc:
          max_acc=val_acc
          saver.save(sess,'ckpt/mnist.ckpt',global_step=i+1)
    f.close()

#验证阶段
else:
    model_file=tf.train.latest_checkpoint('ckpt/')
    saver.restore(sess,model_file)
    val_loss,val_acc=sess.run([loss,acc], feed_dict={x: mnist.test.images, y_: mnist.test.labels})
    print('val_loss:%f, val_acc:%f'%(val_loss,val_acc))
sess.close()

mnist示例展示的改进版本

在上文中我们给出了mnist数据集上一个简单的样例展示,在介绍了变量管理和模型持久化之后,我们再对这个样例进行改进,从这两方面增加程序的可扩展性。

重构后的代码将会分为三个程序,对应三个部分:神经网络结构与参数的设置,网络的训练,网络的测试。

mnist_inference.py

定义神经网络的结构和前向传播方式

import tensorflow as tf

#定义结构参数
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500

def inference(input_tensor, regularizer):
    #如果一个程序多次调用了这个函数,第一次调用之后就需要把reuse设置为True
    with tf.variable_scope('fc1'):
        fc1_weights = tf.get_variable(
            "weight", [INPUT_NODE, LAYER1_NODE], 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", [LAYER1_NODE], initializer=tf.constant_initializer(0.0)
        )
        fc1 = tf.nn.relu(tf.matmul(input_tensor, fc1_weights) + fc1_biases)
        

    with tf.variable_scope('fc2'):
        fc2_weights = tf.get_variable(
            "weight", [LAYER1_NODE, OUTPUT_NODE], 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", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0)
        )
        logit = tf.matmul(fc1, fc2_weights) + fc2_biases
    return logit

mnist_train.py

模型训练部分的代码

import tensorflow as tf
import os
import load_mnist
import mnist_inference

#设置超参数

BATCH_SIZE = 10 # 一个batch中的数据量,越小越接近随机梯度下降,越大越接近梯度下降

LEARNING_RATE_BASE = 0.8 # 基础学习速率
LEARNING_RATE_DECAY = 0.99 # 使用动态学习率的衰减率

REGULARIZATION_RATE = 0.0001 # 规范化参数
TRAINING_STEPS = 5000 #训练轮数
MOVING_AVERAGE_DECAY = 0.99 #滑动平均衰减率

#模型保存的路径和文件名
MODEL_SAVE_PATH = "model"
MODEL_NAME = "model.ckpt"

def train(train_images, train_labels, test_images, test_labels):
    x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-output')

    #定义regularizer
    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    
    y = mnist_inference.inference(x, regularizer)
    #定义存储训练轮数的变量,这个变量就算在使用滑动平均模型的情况下也不需要滑动平均所以设置为不可训练变量
    #把这个global_step变量初始化为0,之后这个变量每轮迭代会自动加一
    global_step = tf.Variable(0, trainable=False)

    #给定滑动平均衰减率和训练轮数的变量,初始化滑动平均类。
    variable_average = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)

    #在所有需要的变量进行滑动平均。tf_trainable_variables返回的就是在GraphKeys.TRAINABLE_VARIABLES中的元素。
    variables_average_op = variable_average.apply(tf.trainable_variables())
    #使用交叉熵函数作为损失函数,然后在计算代价之前使用softmax
    #这里使用TensorFlow里的sparse_softmax_cross_entropy_with_logits函数,值得注意的是这边使用的是不经过滑动平均的前向传播值计算的代价函数值
    #因为mnist给出的标准答案是一个长度为10的一维数组,而该函数需要返回的是一个正确答案的数字,所以需要使用tf.argmax函数进行一下转化
    #tf.argmax的第二个参数为‘1’时候表示选取最大值的操作仅在第一个纬度中进行,相当于按行找。为‘0’时候相当一按列找最大值。返回值都是最大值的下标。
    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)
    #总损失等于规范化损失加上原本的交叉熵损失
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses')) 

    #设置动态指数衰减的学习率
    learning_rate = tf.train.exponential_decay(
            LEARNING_RATE_BASE,  #基础学习率
            global_step,
            50000 / BATCH_SIZE, #总训练数据量除以batch大小等于所需要的迭代轮数
            LEARNING_RATE_DECAY)

    #使用tf.train.GradientDescentOptimizer优化算法来优化损失
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)

    #在训练神经网络模型的时候,需要用反向传播不断更新神经网络的参数,同时需要更新每一个变量的滑动平均值。
    
    with tf.control_dependencies([train_step, variables_average_op]):
        train_op = tf.no_op(name='train')

    #初始化TensorFlow持久化类

    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        #准备验证数据
        #在训练过程中不再测试模型在验证数据集上的表现,验证和测试在另一个独立的程序中完成。
        for i in range(TRAINING_STEPS):
            #产生这一轮使用的一个batch的训练数据
            xs = train_images[i*BATCH_SIZE:(i+1)*BATCH_SIZE]
            ys = train_labels[i*BATCH_SIZE:(i+1)*BATCH_SIZE]  
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys}) 
            #每10轮输出一次
            if step % 100 == 0:
                print("After %d training steps, loss on training batch is %g" % (step, loss_value))
                #保存当前的模型,这里添加了global_step参数,这样可以让每个保存模型的文件名末尾加上训练的轮数
                saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME),global_step=global_step)
            
              
def main(argv=None):
    train_image_path = './train-images.idx3-ubyte'
    train_label_path = './train-labels.idx1-ubyte'
    test_image_path = './t10k-images.idx3-ubyte'
    test_label_path = './t10k-labels.idx1-ubyte'
    train_images, train_labels, test_images, test_labels = load_mnist.load_mnist(train_image_path, train_label_path, test_image_path, test_label_path, normalize=True, one_hot=True)
    train(train_images, train_labels, test_images, test_labels)

# TensorFlow提供的一个主程序入口,tf.app.run会调用上面的这个main函数
if __name__ == '__main__':
    tf.app.run()
            

解释:tf.control_dependencies(control_inputs)
此函数指定某些操作执行的依赖关系,返回一个控制依赖的上下文管理器,使用 with 关键字可以让在这个上下文环境中的操作都在 control_inputs 执行之后才执行。

with tf.control_dependencies([a, b]):
    c = ....
    d = ...

在执行完 a,b 操作之后,才能执行 c,d 操作。意思就是 c,d 操作依赖 a,b 操作。
tf.no_op()表示执行完 train_step, variable_averages_op 操作之后什么都不做。

mnist_eval.py

模型的评测部分代码

import time
import tensorflow as tf

import mnist_inference
import mnist_train
import load_mnist

EVAL_INTERVAL_SECS = 10

def evaluate(train_images, train_labels, test_images, test_labels):
    with tf.Graph().as_default() as g:
        x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')
        y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
        validate_feed = {x: train_images[55000:], y_: train_labels[55000:]}
        y = mnist_inference.inference(x, None)

        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        variable_averages = tf.train.ExponentialMovingAverage(mnist_train.MOVING_AVERAGE_DECAY)
        saver = tf.train.Saver(variable_averages.variables_to_restore())

        while True:
            with tf.Session() as sess:
                # tf.train.get_checkpoint_state 函数会通过checkpoint 文件自动找到目录中最新模型的文件名
                ckpt = tf.train.get_checkpoint_state(mnist_train.MODEL_SAVE_PATH)
                if ckpt and ckpt.model_checkpoint_path:
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
                    accuracy_score = sess.run(accuracy, feed_dict=validate_feed)
                    print("After %s training steps, validation accuracy = %g" % (global_step, accuracy_score))
                else:
                    print('No checkpoint file found')
                    return 
                time.sleep(EVAL_INTERVAL_SECS)

def main(argv=None):
    train_image_path = './train-images.idx3-ubyte'
    train_label_path = './train-labels.idx1-ubyte'
    test_image_path = './t10k-images.idx3-ubyte'
    test_label_path = './t10k-labels.idx1-ubyte'
    train_images, train_labels, test_images, test_labels = load_mnist.load_mnist(train_image_path, train_label_path, test_image_path, test_label_path, normalize=True, one_hot=True)
    evaluate(train_images, train_labels, test_images, test_labels)

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

人数检测的样例代码

在毕业设计进行的无线信号电梯人数检测工作中,对电梯内的无线信号进行特征提取后构建了测试集与训练集。下面给出使用三层全连接层进行神经网络训练的样例代码,其中并没有使用滑动平均模型,优化器使用的是Adam。

import scipy.io as sio
import numpy as np
import tensorflow as tf
from sklearn.preprocessing import OneHotEncoder

data = []
label = []

#从mat文件中导入做好的数据格式。
dataset_train = sio.loadmat('train_data.mat')
dataset_test = sio.loadmat('test_data.mat')

#实验采集的电梯内人数情况分为空载到电梯内七人
kind = ['outcome_empty','outcome_one','outcome_two','outcome_three','outcome_four','outcome_five','outcome_six','outcome_seven']

#训练集和测试集里面每个场景测试了几次
num_train = 7
num_test = 15
#选取的子载波数
num_subcarrier = 50

#标注标签
for k in range(8): #八种人数场景
    for i in range(num_train):
        for j in range(num_subcarrier):
            data.append(dataset_train[kind[k]][:,:,i][:,j])
            label.append(k)

for k in range(8):
    for i in range(num_test):
        for j in range(num_subcarrier):
            data.append(dataset_test[kind[k]][:,:,i][:,j])
            label.append(k)

temp = np.array([data,label])
temp = temp.transpose()
#shuffle the samples
np.random.shuffle(temp)
#after transpose, images is in dimension 0 and label in dimension 1
#print(temp.shape)
train_data = list(temp[:8000,0])
train_label = list(temp[:8000,1])
train_data = np.array(train_data)
train_label = np.array(train_label)

test_data = list(temp[8000:,0])
test_label = list(temp[8000:,1])
test_data = np.array(test_data)
test_label = np.array(test_label)

ohe_tri = OneHotEncoder(sparse=False).fit(train_label.reshape(-1, 1))
ohe_tes = OneHotEncoder(sparse=False).fit(test_label.reshape(-1, 1))

train_label = ohe_tri.transform(train_label.reshape(-1, 1))
test_label = ohe_tes.transform(test_label.reshape(-1, 1))

#print(test_label[1])
#print(test_label.shape)

#constants
features = 6
hl_1 = 200
hl_2 = 400
hl_3 = 200
output_nodes = 8
epochs = 50
batch_size = 300

#hyperparameters
lr = 0.01 #learning rate,其实使用Adam不是固定学习速率这个超参不需要

# placholders
x = tf.placeholder('float', [None, features])
y = tf.placeholder('float', [None, output_nodes])

# return an object with weights and biases
def layer_setup(inputs, outputs):
    layer = {
        'weights': tf.Variable(tf.truncated_normal([inputs, outputs], stddev=0.1)),
        'biases': tf.constant(0.1, shape=[outputs])
    }
    return layer

def network_setup(x):
    # setup each layer
    hidden_layer_1 = layer_setup(features, hl_1)
    hidden_layer_2 = layer_setup(hl_1, hl_2)
    hidden_layer_3 = layer_setup(hl_2, hl_3)
    output = layer_setup(hl_3, output_nodes)
    # forward prop
    hl_1_result = tf.matmul(x, hidden_layer_1['weights']) + hidden_layer_1['biases']
    hl_1_result = tf.nn.sigmoid(hl_1_result)
    hl_2_result = tf.matmul(hl_1_result, hidden_layer_2['weights']) + hidden_layer_2['biases']
    hl_2_result = tf.nn.sigmoid(hl_2_result)
    hl_3_result = tf.matmul(hl_2_result, hidden_layer_3['weights']) + hidden_layer_3['biases']
    hl_3_result = tf.nn.sigmoid(hl_3_result)
    result = tf.matmul(hl_3_result, output['weights']) + output['biases']
    result = tf.nn.softmax(result) # reduce to value between 0 and 1
    #print(result[1])
    return result

def losses(logits, labels):
    with tf.variable_scope('loss') as scope:
        cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels,
                                                                       name='xentropy_per_example')
        loss = tf.reduce_mean(cross_entropy, name='loss')
        tf.summary.scalar(scope.name + '/loss', loss)
    return loss

def train_network(x):
    prediction = network_setup(x)
    with tf.name_scope("Optimization"):
        #cost = tf.reduce_mean( tf.squared_difference(y, prediction))
        #cost = losses(y, prediction)
        cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y, logits=prediction))
        optimizer = tf.train.AdamOptimizer().minimize(cost)
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        for epoch in range(epochs):
            epoch_loss = 0
            for i in range(int(len(train_data) / batch_size)):
                epoch_x = train_data[i * batch_size: i * batch_size + batch_size]
                epoch_y = train_label[i * batch_size: i * batch_size + batch_size]
                _, c = sess.run([optimizer, cost], feed_dict={x: epoch_x, y: epoch_y})
                epoch_loss += c
                #print(i,':',c)
            print('Epoch', epoch, 'completed out of',epochs,'loss:',epoch_loss)
            #Test epoch
            # Compare the predicted outcome against the expected outcome

            correct = tf.equal(tf.round(prediction), y)
            # Use the comparison to generate the accuracy
            accuracy = tf.reduce_mean(tf.cast(correct, 'float'))
            test_batch_amount = int(len(test_data) / batch_size)
            final_accuracy = 0
            for i in range(test_batch_amount):
                epoch_x = test_data[i * batch_size: i * batch_size + batch_size]
                epoch_y = test_label[i * batch_size: i * batch_size + batch_size]
                final_accuracy += accuracy.eval(feed_dict={x: epoch_x, y: epoch_y})
            #print(final_accuracy)
            print("test accuracy %", final_accuracy / test_batch_amount * 100)

train_network(x)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350