Tensorflow搭建神经网络基础篇

写在前面

这系列博客文章将跟随莫烦老师学习的Tensorflow建立神经网络系列内容重新进行系统的整理和归纳。本人水平有限,在本文中若有不足之处,还请各位谅解。

另外,感谢莫烦老师出品的深度学习系列教程,我从该教程获益良多。希望越来越多的IT从业者能够分享自己的技术成长经历,也希望自己能够将技术成长路线记录下来和大家交流。

1、神经网络的梯度下降

在神经网络的基本运行机制中,其实最核心的问题就是如何寻找到一个最优解的问题,特别是寻找一个全局最优解。然而,全局最优解是很难得到的,而且在随着数据量的不断增加,通常一些网络会陷入局部最优解的情境中。因此,对于神经网络,我们需要研究其的优化方法,这也就是一种优化问题。

在生活中我们经常会遇到许多优化问题,在实际的生活中我们的大脑进行非线性的思考后,综合了当前一些信息,这些信息可以看作是不同的输入变量。在处理这些信息时,类似赋予了这些信息不同的权重,最后输出基于这些信息的近似最优解。

梯度下降的具体内容在这不做具体的描述,感兴趣的朋友可以查阅一些专业书籍资料。梯度下降最核心的目的就是所谓的快速收敛,得到最小值。

2、What is Tensorflow

Tensorflow是谷歌开发的一款神经网络框架,采用数据流图来进行数值计算的开源软件库。Tensorflow极大的降低了深度学习网络的开发成本和开发难度,对初学者极其友好。目前,Tensorflow已经成为了最主流的开源神经网络框架,并且不断地完善。

3、Tensorflow基础架构之处理结构

前文所述,Tensorflow基本的工作机制是数据流图:

Tensorflow数据流图

首先需要建立一个数据流图,之后将我们的数据放在数据流图里计算,节点代表了一个数学操作,线代表节点输出数据的传递方向,代表两个节点之间的相互联系。在Tensorflow中,数据是以张量(Tensor)存储的,张量可以理解为不同维度组成的数据结构,一维张量代表向量,二维张量代表矩阵,三维和四维……数据从节点flow流向另一个节点,这就是Tensorflow。

Tensorflow很巧妙的将其数据处理机制进行了概括,或许当看到Tensorflow时,也在不断提醒我们读懂其独特的含义。

4、Tensorflow基本内容之会话机制(Session)

由Tensorflow的处理结构可知,对于一个任务,我们所需要的模型已经被定义在数据流图中,模型的节点设置,数据流的传递方向,各节点之间的关系在理论上可以通过一个数据流图清晰的展示出来。

然而,定义好的模型就像一台机器,我们需要给它数据让它运行起来,它才能够按照我们预期设计的功能实现某种预期的结果。Session在这里充当了启动器的角色。定义的数据流图想要运行起来,必须在Session中启动,之后由Session将数据流图定义的节点部署到CPU、GPU上实现 计算,同时应用优化算法进行计算的优化。

在这里附上会话控制的Python代码,由于Tensorflow的版本更新导致的一些代码语句产生问题,还请结合实际情况进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import tensorflow as tf #导入tensorflow
matrixl = tf.constant([[3,3,3]])#矩阵一
matrix2 = tf.constant([[2],[2],[2]])#矩阵二
product = tf.matmul(matrixl, matrix2)#进行矩阵运算

#两种会话控制方法#

#会话控制1
sess = tf.compat.v1.Session()
result =sess.run(product)
print(result)
sess.close()

#会话控制2
with tf.compat.v1.Session() as sess:#运行到Session结束最后自动关闭Session
result2 = sess.run(product)
print(result2)

这里调用了计算机的GPU来进行计算,当计算完成后会话窗口会自动关闭。个人推荐会话控制2,相较于会话控制1可以避免忘记输入关闭会话语句。当然,会话控制1更容易理解。

5、变量:Variable

在Tensorflow中,一串字符串被定义后才可以表示一个变量;变量被定义后必须对其进行初始化操作。

在Tensorflow官方例程中,对变量的解释是“tf.Variable 对象会存储在训练期间访问的可变、类似于 tf.Tensor 的值,以更简单地实现自动微分。”

(1) 初始化值

变量tf.Variable()构建需要一个初始化值,这个初始化值可以是一个任意大小,任意维度的张量。

如果一个变量初始化的时候需要调用另外一个变量的初始化值时,需要initialized_value()获得这个被调用的变量的初始化值。

1
2
3
4
5
6
7
8
9
10
import tensorflow as tf
a = tf.Variable(0.06, name='a')
b = tf.Variable(a.initialized_value()*2.0, name='b')
# init = tf.initialize_all_variables()#初始化变量,定义了变量后必须使用
init = tf.global_variables_initializer()

with tf.compat.v1.Session() as sess:
sess.run(init)
print(sess.run(a))
print(sess.run(b))

输出结果:

1
2
3
4
5
a:
0.06
b:
0.12
Process finished with exit code 0

在tf.Variable里有一个name参数,其主要作用是在TensorBoard中查看图的结构时,方便查看变量。

(2)维度变换

变量构建完成后,变量的大小和维度已经固定好了。如果想改变变量的维度大小,可以通过reshape操作进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
import tensorflow as tf
a = tf.Variable([1, 2, 3, 4, 5, 6, 7, 8, 9], name='a')
reshape_a = tf.reshape(a, [3,3])
# init = tf.initialize_all_variables()#初始化变量,定义了变量后必须使用
init = tf.global_variables_initializer()

with tf.compat.v1.Session() as sess:
sess.run(init)
print("a:")
print(sess.run(a))
print("reshape_a:")
print(sess.run(reshape_a))

6、占位符:Placeholder

tf.placeholder表示一个占位符,这是由Tensorflow的运行机制所决定的。Tensorflow将定义和运行分为两部分,定义的内容需要放入运行中才能执行;变量的执行已经非常清晰的反映了Tensorflow的运行机制。placeholder在必要时会分配内存,并且将封装数据的tensor传递到session中。

1
2
3
4
tf.placeholder(dtype,shape=None,name=None)
#dtype:数据类型。例如:tf.float32、tf.float64...
#shape:数据大小,主要描述数据的维度。例如:一维、二维...
#name:数据名称。

tf.placeholder定义的数据在session中run时需要将数据设置在feed_dict中,给定设置值的大小。

示例1:

1
2
3
4
5
6
7
8
9
import tensorflow as tf

input1 = tf.placeholder(tf.float32)#给定数据类型float32
input2 = tf.placeholder(tf.float32)

output = tf.multiply(input1, input2)

with tf.Session() as sees:
print(sees.run(output, feed_dict={input1: [7.0], input2: [2.0]}))

结果1:

1
2
3
[14.]

Process finished with exit code 0

示例2:

1
2
3
4
5
6
7
8
9
10
import tensorflow as tf

x = tf.placeholder(tf.string)

init = tf.global_variables_initializer()

with tf.Session() as sess:
sess.run(init)
output = sess.run(x, feed_dict={x:'Hello World'})
print(output)

结果2:

1
2
3
Hello World

Process finished with exit code 0

示例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import tensorflow as tf

variable_1 = tf.Variable(tf.random_normal([1, 2], stddev=1, seed=1))

x = tf.placeholder(tf.float32, shape=(1, 2))
x1 = tf.constant([0.7, 0.9])

a = x + variable_1
b = x1 + variable_1
init = tf.global_variables_initializer()

with tf.Session() as sess:
sess.run(init)
output_a = sess.run(a, feed_dict={x : [[0.3, 0.6]]})#在这里[0.3, 0.6]是一个整体量
output_b = sess.run(b)
print(sess.run(variable_1))
print(output_a)
print(output_b)

结果3:

1
2
3
4
5
[[-0.8113182  1.4845988]]
[[-0.5113182 2.0845988]]
[[-0.11131823 2.3845987 ]]

Process finished with exit code 0

7、激励函数:Activation Function

(1)什么是激励函数?

定义:神经网络中的每个节点接受输入值,并将输入值传递给下一层。输入节点会将输入属性值直接传递给下一层(隐层或输出层)。在神经网络中,隐层和输出层节点的输入和输出之间具有函数关系,这个函数称为激励函数(Activation Function)。

(2)为什么要使用激励函数?

假设一个神经网络可以看成一个线性的函数y = K*x,对于一个简单的线性问题可以有效的解决。但是,生活中大多数都是一些非线性的场景,显然线性的神经网络模型很难有效的处理这些非线性的输入数据。

如何使神经网络更好的实现对非线性数据的处理呢?答案就是在神经网络中加入激励函数。激励函数在这里充当了这样一个转换器角色。y =AF Kx,利用AF这样一个非线性函数即可实现一个非线性的输出。

当神经网络不包含激励函数时,神经网络的每一层节点的输入都是上层输出的线性函数 ,因此无论多少层的神经网络,上一层的输出值都会直接的传递给下一层作为输入,下一层的输出又传递给下下层……每一层的输入和输出都是线性的关系,与没有隐藏层效果相当,因此最后的输出结果相当于多层的线性关系的叠加,这也就是最原始的感知机(Perceptron)。显然这样的一个网络能力是有限的。当我们在每一层的后面添加一个激活函数,就可以使上一层的输出和下一层的输入之间具有一个函数关系,这个函数就是激活函数。

激励函数的作用主要是向神经网络中引入非线性关系,因此激励函数通常是非线性并且是可微分的。引入非线性关系后的神经网络相较于线性关系的表示能力更强。

神经元输入和输出结构图:

神经元输入和输出结构图

(3)常见的激励函数

sigmoid函数:

sigmoid function

sigmoid函数能够将输入的值变换到0和1之间,对于非常大的负数,其输出结果是0;对于非常大的正数,其输出结果是1。

sigmoid函数的不足:

(a)在深度神经网络中梯度反向传播时导致梯度爆炸和梯度消失,其中梯度爆炸发生的概率非常小,而梯度消失发生的概率比较大。

(b)sigmoid函数的输出不是零均值的。这会导致该层经过非线性函数输出的非零均值的输出作为下一层的神经元的输入。在反向传播过程中,很容易出现只向一个方向更新使得收敛缓慢。当然,应用batch训练由于输入多个数据,因此还可以缓解这样的问题。

tanh函数:

tanh function

tanh函数解决了sigmoid函数输出不是零均值的问题,但是仍存在梯度爆炸或者梯度消失的问题和幂运算的问题。

ReLU函数:

ReLU function

ReLU的几大优点:

(a)在正区间解决了梯度消失(gradient vanishing)问题 。

(b)计算速度非常快,只需要判断输入是否大于0。

(c)收敛速度远快于sigmoid和tanh。

ReLU也存在一些问题:

(a)ReLU的输出并不是零均值的。

(b)存在某些神经元可能永远不会被激活导致相应的参数永远无法被更新。

8、定义or添加一个层

导入tensorflow模块。

1
import tensorflow as tf

定义添加神经层的函数def add_layer(),函数包含四个参数:输入值、输入的大小、输出的大小、激励函数。和神经元的输入输出结构图做个对比可以发现二者似乎具有类似的结构,只是神经元的输入可以是多个值,输出只有一个。

1
def add_layer(inputs, in_size, out_size, activation_function=None)

定义权重(weights)和偏移量(biases)。

在生成初始参数时,采用随机变量比全部为零的要更好,因此在这里weights设置为一个in_size行,out_size列的随机变量矩阵。

1
Weights = tf.Variable(tf.random_normal([in_size, out_size]))

在机器学习中,biases不推荐值为0,因此可以增加一个量,例如0.1。

1
biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)

定义Wx_plus_b,即神经网络未激活的值。当activation_function——激励函数为None时,输出就是当前的预测值——Wx_plus_b,不为None时,就把Wx_plus_b传到activation_function()函数中得到输出。

1
2
3
4
5
Wx_plus_b = tf.matmul(inputs, Weights) + biases
if activation_function is None:
outputs = Wx_plus_b
else:
outputs = activation_function(Wx_plus_b)

最后结果返回输出,添加神经层函数def add_layer()定义完成。

1
return  outputs

9、搭建第一个神经网络

导入所需模块,本次导入tensorflow和numpy即可。

1
2
import tensorflow as tf
import numpy as np

构造添加神经层函数。

1
2
3
4
5
6
7
8
9
def add_layer(inputs, in_size, out_size, activation_function=None):
Weights = tf.Variable(tf.random_normal([in_size, out_size]))
biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)
Wx_plus_b = tf.matmul(inputs, Weights) + biases
if activation_function is None:
outputs = Wx_plus_b
else:
outputs = activation_function(Wx_plus_b)
return outputs

构建输入数据,在这里要注意x_data这个输入量,由于使用了np.newaxis函数使得np.linspace函数生成的数据发生了变化。在这里使用了占位符定义了神经网络的输入,None代表输入多少都是可以的,由于输入只有一个特征,因此设置为1。

1
2
3
4
5
6
x_data = np.linspace(-1, 1, 300)[:, np.newaxis]
noise = np.random.normal(0, 0.05, x_data.shape)
y_data = np.square(x_data) - 0.5 + noise

xs = tf.placeholder(tf.float32, [None, 1])
ys = tf.placeholder(tf.float32, [None, 1])

接下来定义神经层。通常神经层由输入层、隐藏层和输出层组成。在这里由于输入层只有1个输入,即x_data,因此只设置1个输入;隐藏层假设有10个神经元;输出层也只有1个输出,因此设置1个输出。所以,我么构建了输入层包含1个神经元,隐含层包含10个神经元,输出层包含1个神经元的神经网络。

定义隐含层,隐含层的输入是输入层的输出,这一点一定要明确。在这里我们虽然假设定义了包含1个神经元的输入层,实际上我们是默认了输入数据即代表一个输入层神经元,因此并没有直接定义输入层。在这里,激活函数采用了Tensorflow自带的tf,nn.relu函数,这也是使用Tensorflow这个框架的便捷之处。

1
l1 = add_layer(xs, 1, 10, activation_function = tf.nn.relu)

定义输出层,输出层的输入是隐含层的输出,因此要承接隐含层神经元个数。输出层设置为1。另外在这里并没有设置激活函数。

1
prediction = add_layer(l1, 10, 1, activation_function = None)

计算预测值和真实值的误差,对二者差的平方求和再取平均。

1
loss = tf.reduce_mean(tf.reduce_sum(tf.square(ys - preduction), reduction_indices = [1]))

让神经网络的学习率有效提高的方法之一是采用优化器。TensorFlow优化器GradientDescentOptimizer是一个实现梯度下降算法的优化器。在这里取值是0.1表示以0.1的效率来最小化误差loss。

1
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

初始化变量。

1
init = tf.global_variables_initializer()

初始化会话控制Session。

1
sess = tf.Session()

初始化变量。

1
sess.run(init)

开始进行数据的训练。

在这里让机器学习1000次。机器学习的内容是train_step,用Session来run每一次的training数据,逐步提升神经网络的准确性。当运算到placeholder时,需要feed_dict这个字典来指定输入。

在这里为什么要使用feed_dict呢?我们需要注意到train_step里包含loss,而其主要作用是利用梯度下降算法来优化loss函数。loss函数的基本思想是计算预测值和真实值的误差,之后对二者的差求平方再求和。在loss函数定义时,并没有直接将数据放入式中,而是采用了占位符的方式,等待数据放入。因此必须在这里使用feed_dict{}这个字典进行数据的存放的读取。

1
2
for i in range(1000):
sess.run(train_step, feed_dict={xs: x_data, ys: y_data})

设置每50步输出一次机器学习过程中的误差loss。

1
2
if i % 50 == 0:
print(sess.run(loss, feed_dict={xs:x_data, ys: y_data}))

电脑代码运行结果:

搭建第一个神经网络代码运行结果

可以很明显的看到,误差函数的值是逐渐减小的,这说明搭建的神经网络是有效的。

10、数据结果可视化

将9中搭建的神经网络的数据进行显示。

导入matplotlib.pyplot模块。

1
import matplotlib.pyplot as plt

在Session后面加入如下代码。

1
2
3
4
5
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter(x_data, y_data)
plt.ion()#只执行散点图必须注释,执行全局运行则不需要注释
plt.show()

在这里plt.ion()代表打开交互模式,plt.ioff()代表关闭交互模式。在matplotlib中画图有两种显示模式:

(1)阻塞模式,利用plt.show()显示图片,且图片关闭之前将代码阻塞在该行。

(2)交互模式,plt.plot()直接显示图片,并且不阻塞代码的继续运行。

运行结果如图所示:

散点图

显示神经网络预测数据,每50次刷新预测图形,用红色、宽度为5的线显示,并且暂停0.1s。

1
2
3
4
5
6
7
8
try:#异常处理内容
ax.lines.remove(lines[0])#清除初始图线
except Exception:
pass
prediction_value = sess.run(prediction, feed_dict={xs: x_data})
lines = ax.plot(x_data, prediction_value, 'r-', lw=5)
plt.pause(0.1)#暂停功能,让程序暂停0.1秒
plt.show()

在这里利用了异常处理算法,检测图中是否有其它图线,若存在这些图线则清除。由于每50步绘制一次图线,因此当下一个50步输出结果进行绘图时为了防止上一次执行的结果对其的影响,需要进行图线的清除。

总体程序执行结果:

神经网络学习结果

11、加速神经网络的训练or提高神经网络的收敛速度or优化算法

对于神经网络的训练,假如只是将数据放入其中,可想而知其训练会持续相当长的时间。因此,在训练过程中必须使用一些优化算法来帮助神经网络提高训练速度。这个过程也被称为神经网络的优化过程。

以下是部分优化算法的介绍。

(1)梯度下降算法

梯度下降的核心思想是通过寻找最小值,控制方差,更新模型参数,最终使模型收敛。常用的梯度下降算法包括:批量梯度下降算法(Batch Gradient Descent,BGD)、随机梯度下降算法(Stochastic Gradient Descent,SGD)、小批量梯度下降算法(Mini-Batch Gradient Descent,MBGD)。

批量梯度下降算法(Batch Gradient Descent,BGD):批量梯度算法是梯度算法最原始的形式,它的具体思路是当更新每一个参数时都是用所有的样本来进行更新。当然,批量梯度下降算法得到的是一个全局最优解。但是由于它在每一次迭代过程中都必须使用训练集中的所有数据,如果样本数很大,这样的迭代方式就会变得很慢。

随机梯度下降算法(Stochastic Gradient Descent,SGD):由于批量梯度算法需要所有的训练样本导致训练速度变得极其缓慢,随机梯度下降是每次迭代使用一个样本来对参数进行更新,使得训练速度加快。。但是,随机梯度下降算法得到的并不是全局最优解,而是一个局部最小值。

小批量梯度下降算法(Mini-Batch Gradient Descent,MBGD):批量梯度下降,是对批量梯度下降以及随机梯度下降的一个折中办法。其思想是每次迭代使用batch_size个样本来对参数进行更新。每次迭代采用batch_size数量的样本进行运算,假设训练集有30万个样本,设置batch_size=100,迭代次数就变成了3000次,相较于SGD迭代30万次,大大降低了迭代的次数。

假设数据集包含1000个样本,BGD算法每次迭代需要计算全部的样本才能对一个参数进行更新,假设迭代10次即可将所有参数更新完毕,则需要计算10000次;对于SGD算法来说,每次迭代只需要计算一个样本,因此对于这个数据集来说,很大概率在这1000次迭代期间就能够完成所有参数的更新,因此最多计算1000次;而对于MBGD算法来说,假设batch_size=100,每次运算采用100个样本,这样一个数据集仅需要10次即可迭代完成,虽然和SGD算法的计算量相同,但是迭代次数远远小于SGD算法的迭代次数。

(2)Momentum算法

Momentum算法相当于对原始梯度做了一个平滑,之后在进行梯度下降。通常的梯度下降算法,其下降路径是曲折的;而Momentum算法通过对原始梯度做了一个平滑,正好将纵轴方向的梯度抹平了,使得参数更新方向更多地沿着横轴进行,因此速度更快。

Momentum算法梯度下降平滑示意图

(3)AdaGrad算法

AdaGrad算法主要是改变学习率,使得每一个参数更新都会有对应的学习率。

(4)RMSProp算法

AdaGrad算法的改进。RMSProp算法结合了部分Momentum算法和AdaGrad算法。

(5)Adam算法

Adam算法即自适应时刻估计方法(Adaptive Moment Estimation),能计算每个参数的自适应学习率。它吸取了RMSProp最大的优点,将动量优化的概念相结合,使得策略可以做出快速高效的优化。

12、Tensorflow中封装完成的优化器

Tensorflow官方文档中提供了许多封装完成的优化器,在使用时可以直接根据参数要求进行调用即可。

以下列出了Tensorflow1.14.0中的优化器:

(1)tf.train.AdadeltaOptimizer

(2)tf.train.AdagradDAOptimizer

(3)tf.train.AdagradOptimizer

(4)tf.train.AdamOptimizer

(5)tf.train.FtrlOptimizer

(6)tf.train.GradientDescentOptimizer

(7)tf.train.MomentumOptimizer

(8)tf.train.Optimizer

(9)tf.train.ProximalAdagradOptimizer

(10)tf.train.ProximalGradientDescentOptimizer

(11)tf.train.RMSPropOptimizer

(12)tf.train.SyncReplicasOptimizer