前言

  在本部分中,将从深度学习与深层神经网络概念介绍、如何设定神经网络的优化目标、更加详细的介绍神经网络的反向传播算法等方面来进一步介绍如何运用TensorFlow来构建神经网络。

深度学习与深层神经网络

  在维基百科中,深度学习的定义为“一类通过多层非线性变换对高复杂性数据建模算法的合集”。因为神经网络是实现“多层非线性变换”最常用的一种方法,所以在实际中基本可以认为深度学习就是深层神经网络的代名词。从以上可以看出,深度学习的两个重要特性——多层、非线性

线性模型的局限性

  在线性模型中,模型的输出为输入的加权和。假设一个模型的输出y和输入$x_i$满足以下关系,那么这个模型就是一个线性模型。$$y=\sum_i{w_ix_i}+b$$
  其中$w_i,b\in R$为模型的参数。上面的公式就是一个线性变换,即便深层神经网络有着多层结构,也只是多个W进行矩阵乘法,与单层网络没有区别。只通过线性变换,任意层的全连接网络和单层神经网络模型的表达能力没有任何区别,而且他们都是线性模型

激活函数实现去线性化

  一般的线性神经元构成的模型都是线性模型,如果将每一个神经元的输出通过一个非线性函数,那么整个神经网络的模型也就不再是线性的了。这个非线性函数就是激活函数,下图显示了加入激活函数和偏置项之后的神经元结构。

  以下公式给出了加上激活函数和偏置项后的前向传播算法的数学定义$$A_1=[a_{11},a_{12},a_{13}]=f(xW^{(1)}+b)$$
  相对于之前的定义,新的公式增加了偏置项(bias),偏置项是神经网络中非常常用的一种结构;其次就是每个节点的取值不再是单纯的甲醛和。每个节点在加权和的基础上还做了一个非线性变换。下图显示了几种常用的非线性激活函数的函数图像。

  目前TensorFlow提供了7种不同的非线性激活函数,tf.nn.relutf.sigmoidtf.tanh是比较常用的几个,当然TensorFlow也支持使用自己定义的激活函数。以下代码展示了TensorFlow实现神经网络中的前向算法。

1
2
a = tf.nn.relu(tf.matmul(x, w1) + biases1)
y = tf.nn.relu(tf.matmul(a, w2) + biases2)

  TensorFlow可以很好地支持使用了激活函数和偏置项的神经网络。

多层网络解决异或运算

  感知机可以简单的理解为单层的神经网络,这在我之前的博客里也实现了。感知机会先将输入进行加权和,然后使用激活函数最后得到输出。这个结构就是一个没有隐藏层的神经网络。
  深层神经网络其实是有组合特征提取的功能的,这个特性对于解决不易提取特征向量的问题有很大帮助,也是深度学习在这些问题上更加容易取得突破性进展的原因。

损失函数定义

  神经网络的效果以及优化的目标是通过损失函数(loss function)来定义的。

经典损失函数

  分类问题和回归问题是监督学习的两大种类。本节将会分别介绍分类问题和回归问题中使用到的经典损失函数。

分类问题

  通过神经网络解决多分类问题最常用的方法是设置n个输出节点,其中n为类别的个数。对于每一个样例,神经网络可以得到一个n维数组作为输出结果。在理想情况下,如果一个样本输入类别k,那么这个类别所对应的输出节点的值应该为1,而其他节点的输出都为0.
  判断一个输出的向量和期望向量有多接近有什么方法?交叉熵(cross entropy)是常用的评判方法之一,交叉熵刻画了两个概率分布之间的距离,它是分类问题中使用比较广的一种损失函数。
  交叉熵是一个信息论中的概念,它原本是用来估算平均编码长度的。给定两个概率分布p和q,通过q来表示p的交叉熵为:$$H(p,q)=-\sum_x{p(x)log {q(x)}}$$
  注意交叉熵刻画的是两个概率分布之间的距离,然而神经网络的输出却不一定是一个概率分布。概率分布刻画了不同事件发生的概率。因此可以通过Softmax将神经网络的输出值转化为概率:$$softmax(y)_i=y_i`=\frac{e^{y_i}}{\sum_{j=1}^{n}{e^{y_j}}}$$
  从以上公式可以看出,原始神经网络的输出被用作置信度来生成新的输出。这样就把神经网络的输出也变成了一个概率分布,从而可以通过交叉熵来计算预测的概率分布和真实答案的概率分布之间的距离了。
  交叉熵函数不是对称的($H(p,q)\ne H(q,p)$),它刻画的是通过概率分布q来表达概率分布p的困难程度。因为正确答案是希望得到的结果,所以当交叉熵作为神经网络的损失函数时,p代表的是正确答案,q代表的是预测值。
  在TensorFlow中实现交叉熵代码为:

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

  其中y_代表正确结果,y代表预测结果,这一行代码包含了4个不同的TensorFlow运算。通过tf.clip_by_value函数可以将一个张量中的数值限制在一个范围内。如上就是限制在(1e-10,1.0)之内。
  因为交叉熵一般会和softmax回归一起使用,所以TensorFlow对这两个功能进行了封装,并提供了tf.nn.softmax_cross_entropy_with_logits函数。比如可以直接通过以下代码来实现先softmax回归然后交叉熵的损失函数:

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

  其中y代表了原始神经网络的输出结果,而y_给出了标准答案。这样通过一个命令可以得到使用了Softmax回归之后的交叉熵。而在只有一个正确答案的分类问题中,TensorFlow提供了tf.nn.sparse_softmax_cross_entropy_with_logits函数来进一步加速计算过程。

回归问题

  与分类问题不同,回归问题解决的是对具体数值的预测。这些问题需要预测的不是一个事先定义好的类别,而是一个任意实数。解决回归问题的神经网络一般只有一个输出节点,这个节点的输出值就是预测值。对于回归问题,最常用的损失函数是均方误差(MSE)。它的定义如下:$$MSE(y,y’)=\frac{\sum_{i=1}^{n}{(y_i-y_i’)^2}}{n}$$
  其中$y_i$为一个batch中第i个数据的正确答案,而$y_i’$为神经网络给出的预测值,以下代码展示了TensorFlow实现均方误差损失函数:

1
mse = tf.reduce_mean(tf.square(y_ - y))

  其中y代表了神经网络的输出答案,y_代表了标准答案。这里的减法运算符代表两个矩阵中对应元素的减法。

自定义损失函数

  在TensorFlow中也支持自定义损失函数,它可以使得神经网络优化的结果更加接近实际问题的需求。例如如下损失函数:

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

  以上代码用到了tf.greatertf.where来实现选择操作。tf.greater的输入是两个张量,此函数会比较这两个输入张量中的每一个元素的大小,并返回比较结果。当tf.greater的输入张量维度不一样时,TensorFlow会进行类似numpy的广播操作的处理,tf.where函数有三个参数,第一个为选择条件根据,当选择条件为True时,tf.where函数会选择第二个参数中的值,否则使用第三个参数中的值。注意这两个操作都是元素级别进行。

神经网络优化

  本部分将讨论通过反向传播算法梯度下降算法调整神经网络中参数的取值。梯度下降法主要用于优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降法,从而使神经网络模型在训练数据上的损失函数尽可能小。
  反向传播算法是训练神经网络的核心算法,它可以根据定义好的损失函数优化神经网络中参数的取值,从而使神经网络模型在训练数据集上的损失函数达到一个较小值。神经网络模型中参数的优化过程直接决定了模型的质量,是使用神经网络时非常重要的一步。
  假设用$\theta$表示神经网络中的参数,$J(\theta)$表示在给定的参数取值下,训练数据集上损失函数的大小,那么整个优化过程可以抽象为寻找一个参数$\theta$,使得$J(\theta)$最小。梯度下降算法会迭代式更新参数$\theta$,不断沿着梯度的反方向让参数朝着总损失更小的方向更新。
  参数的梯度可以通过求偏导的方式计算,对于参数$\theta$,其梯度为$$\frac{\delta}{\delta\theta}J(\theta)$$
  神经网络的优化过程可以分为两个阶段,第一个阶段先通过前向传播算法计算得到预测值,并将预测值和真实值做对比得出两者之间的差距。然后在第二个阶段通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。
  在训练神经网络时,参数的初始值会很大程度影响最后得到的结果。只有损失函数为凸函数时,梯度下降算法才能保证达到全局最优解。
  除了不一定能达到全局最优,梯度下降算法的另一个问题就是计算时间太长,因为需要计算全部训练数据的损失函数。
  为了加速训练算法,可以使用随机梯度下降算法。这个算法优化的不是在全部训练数据上的损失函数,而是在每一轮迭代中,随机优化某一条训练数据上的损失函数;但是随机梯度下降优化的神经网络可能无法达到局部最优。
  为了综合梯度下降算法和随机梯度下降算法的优缺点,在实际应用中一般采用这两个算法的折中——每次计算一小部分训练数据的损失函数。这一小部分被称之为**batch**。通过矩阵运算,每次在一个batch上优化神经网络的参数并不会比单个数据慢太多。另一方面,每次使用一个batch可以大大减小收敛所需要的迭代次数,同时可以使收敛的结果更加接近梯度下降的效果。以下代码给出了在TensorFlow中如何实现神经网络的训练过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
batch_size = n

# 每次读取一小部分作为当前的训练数据来执行反向传播算法。
x = tf.placeholder(tf.float32, shape=(batch_size, 2),name="x_input")
y_ = tf.placeholder(tf.float32, shape=(batch_size, 1),name="y_input")

# 定义神经网络结构和优化算法
loss = ......
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)

# 训练神经网络
with tf.Session() as sess:
...
# 迭代的更新参数
current_X, current_Y = ...
sess.run(train_step, feed_dict={x: current_X, y_: current_Y})

神经网络进一步优化

  本部分将介绍神经网络优化过程中的可能遇到的问题,比如设置梯度下降法中的学习率过拟合问题滑动平均模型

学习率的设置

  学习率决定了参数每次更新的幅度。如果幅度过大,那么可能导致参数在极优值的两侧来回移动。
  学习率既不能过大,也不能过小。为了解决设定学习率的问题,TensorFlow提供了一种更加灵活的学习率设置方法——指数衰减法。tf.train.exponential_decay函数实现了指数衰减学习率。通过这个函数,可以先使用较大的学习率获得一个比较优的解,然后随着迭代的继续逐步减小学习率,使得模型在训练后期更加稳定。exponential_decay函数会指数级地减小学习率,它实现了以下代码的功能:

1
2
3
# decayed_learning_rate为每一轮batch优化时使用的学习率,learning_rate为事先规定的初始学习率
# decay_rate为衰减系数,decay_steps衰减速度
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)

  在tf.train.exponential_decay函数中提供了staircase这一参数,默认时为False,此时学习率为连续衰减形式,设置为True时为阶梯状衰减学习率。以下就是一段代码示范如何在TensorFlow中使用tf.train.exponential_decay函数:

1
2
3
4
5
6
7
8
9
global_step = tf.Variable(0)

# 通过exponential_decay函数生成学习率
learning_rate = tf.train.exponential_decay(0.1, global_step, 100, 0.96, staircase=True)

# 使用指数衰减的学习率。在minimize函数中传入global_step将自动更新
# global_step参数,从而使得学习率也得到相应更新。
learning_step = tf.train.GradientDescentOptimizer(learning_rate)\
.minimize(myloss,global_step=global_step)

  以上代码设定了初始学习率为0.1,因为制定了staircase=True,所以每训练100轮后学习率乘以0.96。若staircase=False,则训练每条数据的时候学习率都会乘以0.96。

过拟合问题(正则化项)

  之前的博客中也多次讲到了过拟合问题,一般是将loss函数后加上正则化项,常用的刻画模型复杂度的正则化项有两种,分别为L1和L2,这里就不再赘述其数学原理,在TensorFlow中,可以实现如下一个简单的带L2正则化的损失函数定义:

1
2
3
4
5
6
7
# 设置初始参数,2行1列,标准差为1,随机种子1,正态分布
w = tf.Variable(tf.random_normal([2,1],stddev=1,seed=1))
# 设置w为从x到y中间的隐藏层
y = tf.matmul(x,w)

# 设置loss为y_与y的平方差均值加上w的L2范数*lambda系数,tf.square函数是为求平方
loss = tf.reduce_mean(tf.square(y_ - y)) + tf.contrib.layers.l2_regularizer(lambda)(w)

  类似的,tf.contrib.layers.l1_regularizer函数可以计算L1正则化项的值。但是当网络结构复杂之后定义网络结构的部分和计算损失函数的部分可能不在同一个函数中,这样通过变量这种方式计算损失函数就不行了,在TensorFlow中可以是集合(collection),它可以在一个计算图中保存一组实体,下面代码实现了计算一个5层神经网络带L2正则化的损失函数的计算方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np

dataset_size = 200
data = []
label = []
np.random.seed(0)

# 以原点为圆心,半径为1的圆把散点划分成红蓝两部分,并加入随机噪音。
for i in range(dataset_size):
x1 = np.random.uniform(-1, 1)
x2 = np.random.uniform(0, 2)
if x1 ** 2 + x2 ** 2 <= 1:
data.append([np.random.normal(x1, 0.1), np.random.normal(x2, 0.1)])
label.append(0)
else:
data.append([np.random.normal(x1, 0.1), np.random.normal(x2, 0.1)])
label.append(1)

# np.hstack()函数是将list在水平方向上平铺,然后.reshape将维度更改
data = np.hstack(data).reshape(-1, 2)
label = np.hstack(label).reshape(-1, 1)

# # 绘制原散点图
# plt.scatter(data[:, 0], data[:, 1], c=np.squeeze(label),
# cmap="RdBu", vmin=-0.2, vmax=1.2, edgecolor="white")
# plt.show()

# 设置layer参数,入口为矩阵维度和正则化项系数
def get_weight(shape, var_lambda):
# 声明一层网络
w = tf.Variable(tf.random_normal(shape), dtype=tf.float32)
# 添加L2正则化项到losses集合中
tf.add_to_collection('losses', tf.contrib.layers.l2_regularizer(var_lambda)(w))
return w

# 声明batch
x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))

# 每层节点的个数
layer_dimension = [2,10,5,3,1]
# 声明网络结构层数
n_layers = len(layer_dimension)
# 前一个layer
cur_layer = x
# 前一个layer的节点数
in_dimension = layer_dimension[0]

# 循环生成网络结构,输入层X[None, 2],隐藏层W1[2, 10]、W2[10, 5]、W3[5, 3],输出层Y[None, 1]
for i in range(1, n_layers):
out_dimension = layer_dimension[i] # 该layer输出的节点数量
weight = get_weight([in_dimension, out_dimension], 0.003) # 设置layer,并且正则化项系数为0.003
bias = tf.Variable(tf.constant(0.1, shape=[out_dimension])) # 设置偏执项,维度按照输出维度提供
cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight) + bias) # 该层的输出为relu(w_i*x+b)
in_dimension = layer_dimension[i] # 设置下层的输入节点数量

y= cur_layer # 最后的输出层

# 损失函数的定义。
mse_loss = tf.reduce_sum(tf.pow(y_ - y, 2)) / dataset_size
tf.add_to_collection('losses', mse_loss) # 正常的损失函数加入losses集合中
loss = tf.add_n(tf.get_collection('losses')) # 获得losses集合所有项并求和

# 定义训练的目标函数loss,训练次数及训练模型
train_op = tf.train.AdamOptimizer(0.001).minimize(loss)
TRAINING_STEPS = 10000

with tf.Session() as sess:
tf.global_variables_initializer().run()
for i in range(TRAINING_STEPS):
sess.run(train_op, feed_dict={x: data, y_: label})
if i % 1000 == 1000 - 1:
print("After %d steps, loss: %f" % (i, sess.run(loss, feed_dict={x: data, y_: label})))

# 画出训练后的分割曲线
xx, yy = np.mgrid[-1:1:.01, 0:2:.01] # 分别创建两个密集型网格,起始:终点:步长
print("xx.ravel()"+"*"*100,'\n',xx.ravel(),"yy.ravel()"+'*'*100,yy.ravel())
grid = np.c_[xx.ravel(), yy.ravel()] # 形成多个n*2的array,每组里是(x,y)
# np.c_()函数是将两个array按照行连接起来,np.r_()是按照列连接
# array.ravel()函数是将array变成一维,然后返回一维数据
probs = sess.run(y, feed_dict={x:grid}) # 将x作为输入去运行y这个计算图
probs = probs.reshape(xx.shape) # 按照网格重新排列

plt.scatter(data[:,0], data[:,1], c=np.squeeze(label),
cmap="RdBu", vmin=-.2, vmax=1.2, edgecolor="white")
# 填充网格中的等高线
plt.contour(xx, yy, probs, levels=[.5], cmap="Greys", vmin=0, vmax=.1)
plt.show()

分类结果