TensorFlow深层神经网络

学习笔记,内容和代码来自书《TensorFlow实战Google深度学习框架》

目录

深度学习与深层神经网络

使用激活函数实现去线性化

如何选择激活函数

多层网络解决异或运算

损失函数

经典损失函数

自定义损失函数

神经网络优化算法

梯度下降算法

神经网络进一步优化

学习率的设置

过拟合问题

滑动平均模型


深度学习与深层神经网络

深度学习是一类通过多层非线性变换对高复杂性数据建模算法的合集。                          ——维基百科

 而深层神经网络是实现“多层非线性变换”的最常用方法,所以基本上可以认为深度学习是深层神经网络的代名词。

在之前的对于神经网络的介绍中,采用了前向传播算法,是一个线性模型,线性模型的特点是任意线性模型的组合都是一个线性变换,这样即使神经网络有多层,他和单层神经网络也没有什么区别。这也正是线性模型的局限之处,也是为什么深度学习要强调非线性。

使用线性模型解决线性可分问题的效果:

使用激活函数实现去线性化

神经网络游乐场中,激活函数为上面的Activation选项,有Linear,ReLU,Tanh,Sigmoid几个选项。

如果在神经元中的输出通过一个非线性函数,那么神经网络模型也不再是线性的了,这个非线性函数就是激活函数。

函数图像和介绍来自博客:TensorFlow神经网络中的激活函数 from Jean_V

常见的几种激活函数:

                                                                                             Sigmoid函数

                                                                                               Tanh函数

  • tanh与sigmoid非常接近,且与后者具有类似的优缺点,
  • sigmoid和tanh的主要区别在于tanh的值为[-1.0,1.0]优点在于在一些特定的网络架构中,能够输出负值的能力十分有用。
  • 缺点在于注意tanh值域的中间点为0.0,当网络中的下一层期待输入为负值或者为0.0时,这将引发一系列问题。

                                                               Relu(Rectified Linear Units修正线性单元)函数

                                                                                          线性函数

线性函数即为y=kx+b

通过激活函数后,每一个神经元都不再是线性变换。目前TensorFlow提供7种不同的非线性激活函数,tf.nn.relu、tf.sigmoid和tf.tanh是比较常用的几个,调用语法如下:

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

如何选择激活函数

转自博客:TensorFlow神经网络中的激活函数 from Jean_V

激活函数好或坏,不能凭感觉定论。然而,根据问题的性质,我们可以为神经网络更快更方便地收敛作出更好的选择。

用于分类器时,Sigmoid函数及其组合通常效果更好。

由于梯度消失问题,有时要避免使用sigmoid和tanh函数。

ReLU函数是一个通用的激活函数,目前在大多数情况下使用。

如果神经网络中出现死神经元,那么PReLU函数就是最好的选择。

请记住,ReLU函数只能在隐藏层中使用。

一点经验:你可以从ReLU函数开始,如果ReLU函数没有提供最优结果,再尝试其他激活函数。

多层网络解决异或运算

感知机(perceptron)模型从数学上完成了对神经网络的建模,感知机可以理解为单层神经网络。感知机会先将输入进行加权和,然后通过激活函数得到输出,这就是一个没有隐藏层的神经网络。根据相关论文的数学推导,感知机是无法模拟异或运算的,如下图。

加入隐藏层后疑惑问题得到解决,如下图:

隐藏层的四个节点中,每个节点都有一个角是黑色的,这四个隐藏节点可以被认为代表了从输入特征中抽取更高维的特征。

损失函数

经典损失函数

分类问题和回归问题是监督学习的两大种类,这一部分重点介绍分类问题和回归问题的经典损失函数。

分类问题希望解决的是将不同的样本分到事先定义好的类别中。通过神经网络解决多分类问题通常方法是设置n个输出节点,n为类别个数。对于每一个样例,神经网络得到一个n维数组作为输出结果,如果一个样本属于类别k,那么对应输出节点的输出值应该为1,如识别数字1,结果应该越接近[0,1,0,0,0,0,0,0,0,0]越好。如何判断识别输出向量和期望向量有多接近呢?交叉熵是常用的(corss entropy)是常用的评判方法之一。交叉熵刻画了两个概率分布之间的距离,是常用的一种损失函数。

交叉熵是一个信息论中的概念,原本用于计算平均编码长度,计算:

给定两个概率p,q,通过q来表示p的交叉熵为

H\left ( p,q \right )=-\sum p\left ( x \right )\log q\left ( x \right )

交叉熵刻画的是两个概率分布之间的距离,然而神经网络输出却不一定是一个概率分布(即要求概率均大于0且概率之和必须是1)。所以经常使用Softmax回归是一个常用的方法,将神经网络前向传播的结果变成概率分布。

概率论领域中,Softmax函数又称归一化函数。假设神经网络原始输出为y_1,y_2,...,y_n,那么经过Softmax回归处理之后输出为:

softmax\left ( y \right )_i=y_i'=\frac{e^{y_{i}}}{\sum_{j=1}^{n}e_{j}}

这样就可以通过交叉熵来计算预测的概率分布和真实答案的概率分布之间的距离了,也就是说交叉熵越小,两个概率分布越接近。

TensorFlow中实现交叉熵代码如下:

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

其中,y_代表正确结果,y代表预测结果。

因为交叉熵一般和Softmax回归一起使用,所以TensorFlow对这两个功能统一封装,提供了tf.nn.softmax_cross_entropy_with_logits() 

例如使用softmax回归后的交叉熵损失函数:

cross_entropy=tf.nn.softmax_cross_entropy_with_logits(y,y_)

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

与分类问题不同,回归问题需要解决的是对具体数值的预测。这些问题需要预测的不是一个实现定义好的类别,而是一个任意实数。解决回归问题的神经网络一般只有一个输出节点,这个节点的输出值就是预测值。对于回归问题,最常用的损失函数是均方误差(MSE,mean squared error),定义如下:

MSE\left ( y,y_{\_} \right )=\frac{\sum_{i=1}^{n}(y_i-y_i')^2}{n}

其中y_i为一个batch中第i个数据的正确答案,而y_i'为神经网络给出的预测值。

使用TensorFlow实现均方误差损失函数:

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

其中,y代表了神经网络的输出答案,y_代表了标准答案。 

自定义损失函数

tf不仅支持经典的损失函数,还可以优化任意的自定义损失函数,使得神经网络优化的结果更接近实际需求。

为了最大化预期利润,需要将损失函数和利润直接联系起来,损失函数定义的是损失。要将利润最大化,定义的损失函数应该刻画成本或者代价。 

例如,一个产品成本一元,利润10元,那么少预测一个就少赚10元,多预测一个少赚1元,如果神经网络模型是最小化的均方误差,就无法预测最大化收益,我们可以定义下列函数:

Loss(y,y')=\sum_{i=1}^{n}f(y_i,y_i'),f(x,y)=\left\{\begin{matrix} a(x-y) &x>y\\ b(y-x) &x\leq y \end{matrix}\right.

TensorFlow代码实现:

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

tf.greater的输入是两个张量,此函数会比较这两个输入张量中每一个元素的大小,并返回比较结果。当张量维度不一样时,TensorFlow会进行类似NumPy广播(broadcasting)操作。第一个为选择条件依据,True时,tf.where函数会选择第二个参数中的值,False使用第三个参数中的值。 

用法:

import tensorflow as tf
v1=tf.constant([1.0, 2.0, 3.0, 4.0])
v2=tf.constant([4.0, 3.0, 2.0, 1.0])

sees = tf.InteractiveSession()
print tf.greater(v1, v2).eval()
#输出[false flase true true]

print tf.where(tf.greater(v1, v2), v1, v2).eval()
#输出[4. 3. 3. 4.],类似?=运算符
sees.close

神经网络优化算法

具体介绍通过反向传播算法(backpropagation)梯度下降算法(gradient decent)调整参数取值。梯度下降算法主要用于优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法,使模型在训练数据上损失函数尽可能小。

梯度下降算法

梯度下降算法是最常用的神经网络优化方法。参数的梯度可以通过求偏导实现,对于参数\theta,梯度为\frac{\partial J\left ( \theta \right )}{\partial \theta }。有了梯度,还需要定义一个学习率\eta(learning rate)来定义每次参数更新的幅度。

从直观上理解,可以认为学习率定义的就是每次参数移动的幅度。通过参数的梯度和学习率,参数的新公式为\theta _{n+1}=\theta _n-\eta \frac{\partial J(\theta _n)}{\partial \theta _n}

神经网络的优化过程可以分为两个阶段:

  1. 通过前向传播算法得到预测值,并将预测值和真实值做对比得到两者之间的差距
  2. 通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降法更新每一个参数。

注意:梯度下降算法并不能保证被优化的函数达到全局最优解。在训练神经网络时,参数的初始值会很大程度影响最后的结果。只有当损失函数是凸函数时,梯度下降算法才能达到全局最优解。

另外,梯度下降算法还有一个问题是训练时间过长。为了加速训练过程,可以使用随机梯度下降算法(stochastic gradient descent)。这个算法不是在全部训练数据上的损失函数,而是在每一轮迭代中,随机优化某一条训练数据上的损失函数,这样速度就大大加快了。它的问题在于,某条数据上的损失函数更小并不代表全部数据上的损失函数最小,因此使用这种方法可能甚至达不到局部最优。

所以,一般采用这两种方法的折中——每次计算一小部分训练数据的损失函数,称之为batch。

下面是TensorFlow中实现神经网络的训练过程:

import tensorflow as tf

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:
    tf.global_variables_initializer().run() # 参数初始化
    # 迭代更新参数
    for i in range(Steps):
        # 准备batch_size个训练数据,一般将所有训练数据随机打乱后再选取可以得到
        # 更好的优化效果
        sess.run(train_step,feed_dict={x:X[start:end],y_:Y[start:end]})

神经网络进一步优化

  • 指数衰减的方法设置梯度下降算法的学习率(调整的步长)
  • 过拟合问题
  • 滑动平均模型

学习率的设置

学习率决定了参数每次更新的幅度,幅度如果过大会导致参数在极优值两侧来回移动。相反,学习率过小会大大降低优化速度。因此学习率不能过大也不能太小。

为了解决学习率的问题,TensorFlow提供了一种更加灵活的学习率设置方法——指数衰减法。tf.train.exponential_decay函数实现了指数衰减学习率。它可以先使用较大的学习率来快速获得一个比较优的解,然后随着迭代的继续逐步减小学习率,使得模型在训练后期更加稳定。

tf.train.exponential_decay可以通过设置参数staircase选择不同的衰减方式。staircase默认值False,设置为True时,学习率会成为一个阶梯函数(staircase function)。 

代码实现:

decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)

其中,decayed_learning_rate为每一轮优化时使用的学习率,learning_rate为事先的学习率,decay_steps为衰减速度。

示例代码:TensorFlow中使用tf.train.exponential_decay函数:

global_step=tf.Variable(0)

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

# 使用指数衰减的学习率。在,minmize函数中传入global_step将自动更新global_step参数,使学习率额更新
learning_step = tf.train.GradientDecentOptimizer(learning_rate).minmize(...my loss..., global_step=global_step)

上面设置初始学习率为0.1,因为staircase=True,即学习率为阶梯函数,所以每训练100轮后学习率乘以0.96。一般,学习率的初始值、衰减系数和衰减速度都是根据经验设置。而且,损失函数下降速度和迭代结束后总损失没必然联系,不能通过前几轮损失函数下降的速度来比较不同神经网络的效果。

过拟合问题

在真实的应用中,希望通过训练出来的模型对未知的数据给出判断。过拟合指的是模型很好的记忆了每一个训练数据中随机噪声的部分而忘记了要去学习训练数据中通用的趋势。 

即:过度拟合训练数据中的随机噪声虽然可以得到非常小的损失函数,但是对于未知数据可能无法做出可靠的判断 。

为了避免过拟合问题,一个常用方法是正则化(regularization)。正则化的思想就是在损失函数中加入刻画模型复杂度的指标。刻画模型复杂度的函数有两种:

L1正则化:

R(w)=\left \| w \right \|_1=\sum_{i}^{ }\left | w_i \right |

L2正则化:

R(w)=\left \| w \right \| _2 ^2=\sum_{i}^{ }\left | w_i ^2 \right |

两种正则化方法都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪音。实践中,常将两种正则化方法结合起来使用:

R(w)=\sum_{i}^{ }\alpha \left | w_i \right |+(1-\alpha )w_i^2

TensorFlow带L2正则化的损失函数定义:

w=tf.Variable(tf.random_normal([2,1],stddev=1,seed=1))
y=tf.matmul(x,w)

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

loss为定义的损失函数,有两个部分组成。第一个部分是均方误差损失函数;第二个部分是正则化,防止模型过度模拟训练数据中的随机噪声。lambda参数表示了正则化项的权重,也就是\lambdaw为需要计算正则化损失的参数。

TensorFlow提供了tf.contrib.layers.l2_regularizer函数,这个函数可以计算一个给定参数的L2正则化项的值。tf.contrib.layers.l1_regularizer函数可以计算L1正则化项的值。 

样例:

import tensorflow as tf

weights=tf.constant([[1.0,2.0],[3.0,4.0]])
with tf.Session() as sess:
    # 输出为(|1|+|-2|+|-3|+|4|)×0.5=5。0.5为正则化项的权重。
    print(sess.run(tf.contrib.layers.l1_regularizer(.5)(weights)))
    # 输出为(1^2+(-2)^2+(-3)^2+4^2)/2*0.5=7.5
    print(sess.run(tf.contrib.layers.l2_regularizer(.5)(weights)))

上述过程可以计算简单的神经网络中的正则化的损失函数。但是当神经网络中的参数增多之后,可能会导致损失函数loss的定义很长,可读性差而且容易出错。更糟糕的是,当网络结构变复杂后,定义网络结构的部分和计算损失函数的部分可能不在同一个函数中,这样通过变量这种方式计算损失函数就不方便了。 
此时,可以通过tf提供的集合。集合可以在一个计算图(tf.Graph)中保存一组实体(比如张量)。
样例:通过集合计算一个5层神经网络带L2正则化的损失函数的计算方法:

import tensorflow as tf

#获取一层神经网络边上的权重,并将这个权重的L2正则化损失加入名称为'losses'的集合中
def get_weight(shape,lambda1):
    #生成一个变量
    var=tf.Variable(tf.random_normal(shape),dtype=tf.float32)
    #add_to_collection 函数将这个新生成变量的L2正则化损失项加入集合
    #这个函数第一个参数losses是集合的名字,第二个参数是要加入这个集合的内容
    tf.add_to_collection('losses',tf.contrib.layers.l2_regularizer(lambda1)(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,5,3,1]
n_layers=len(layer_dimension)

#这个变量维护前向传播时最深层的节点,开始的时候就是输入层
cur_layer=x
# 当前层的节点个数
in_dimension=layer_dimension[0]

#通过一个循环来生成5层全连接的神经网络结构
for i in range(1,n_layers):
    # layer_demension[i]为下一层的节点数
    out_dimension=layer_dimension[i]
    #生成当前层中权重的变量,并将这个变量的L2正则化损失加入计算图上的集合
    weight=get_weight([in_dimension,out_dimension],0.003)
    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]

#在定义神经网络前向传播的同时已经将所有的L2正则化损失加入了图上的集合,
#这里只需要计算刻画模型在训练数据上表现的损失函数
mse_loss=tf.reduce_mean(tf.square(y_-cur_layer))

#将均方误差损失函数加入损失集合
tf.add_to_collection('losses',mse_loss)

#get_collection返回一个列表,这个列表是所有这个集合中的元素。在这个样例中,
#这些元素就是损失函数的不同部分,将它们加起来就可以得到最终的损失函数
loss=tf.add_n(tf.get_collection('losses'))

滑动平均模型

在采用随机梯度下降算法训练神经网络时,使用滑动平均模型在很多应用中都可以提高性能。 
tf中提供了tf.train.ExponentialMovingAverage来实现滑动平均模型。初始化这个函数时,需要提供一个衰减率。衰减率决定了模型更新的速度,衰减率越大模型越稳定。一般设为0.999或者0.9999 (非常接近于1的数)。
样例:ExponentialMovingAverage如何被使用。

import tensorflow as tf

# 定义一个变量用于计算滑动平均,这个变量的初始值为0.
# 所有滑动平均的变量都必须为实数型
v1 = tf.Variable(0, dtype=tf.float32)
# step变量模拟神经网络中迭代的轮数,可以用于动态控制衰减率
step = tf.Variable(0, trainable=False)

# 定义一个滑动平均的类,初始化时给定了衰减率(0.99)和控制衰减率的变量step
ema = tf.train.ExponentialMovingAverage(0.99, step)
# 定义一个更新变量滑动平均的操作。这里需要给定一个列表,每次执行这个操作时,这个列表中的变量都会被更新
maintain_averages_op = ema.apply([v1])

with tf.Session() as sess:
    tf.global_variables_initializer().run()
    #通过ema.average(v1)获取滑动平均之后变量的取值。在初始化之后变量v1的值和v1的滑动平均都为0
    print(sess.run([v1,ema.average(v1)]))

    #更新变量v1的值为5
    sess.run(tf.assign(v1, 5))
    #更新v1的滑动平均值。衰减率为min(0.99,0.1)=0.1
    #v1的滑动平均会被更新为 0.1*0+0.9*5=4.5
    sess.run(maintain_averages_op)
    print(sess.run([v1,ema.average(v1)]))
    #[5.0,4.5]

    #更新step的值为10000
    sess.run(tf.assign(step, 10000))
    #更新v1的值为10
    sess.run(tf.assign(v1, 10))
    #更新v1的滑动平均值。衰减率为min(0.99,(1+10000)/(10+10000))=0.99
    #v1的滑动平均会被更新为0.99*4.5+0.01*10=4.555
    sess.run(maintain_averages_op)
    print(sess.run([v1,ema.average(v1)]))
    #[10.0, 4.5549998]

    #再次更新滑动平均值,得到的新滑动平均值为 0.99*4.555+0.01*10=4.60945
    sess.run(maintain_averages_op)
    print(sess.run([v1,ema.average(v1)]))
    #[10.0, 4.6094499]

后面会给出真实使用样例。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章