TensorFlow:实战Google深度学习框架(三)深层神经网络

时间:2022-12-14 15:25:23

第四章 深层神经网络

本章主要介绍如何设计和优化神经网络,使其能够更好的对未知样本进行预测。

4.1 深度学习与深层神经网络

深度学习的两大特性:多层+非线性

4.1.1 线性模型的局限性

线性模型中,模型的输出就是输入的加权和,其能解决问题的能力是有限的,无法解决非线性问题(至少是无法通过直线或高维空间的平面来划分的),当引入非线性激活函数后,就可以实现更复杂的划分。

4.1.2 激活函数实现非线性化

将每个神经元的输出都经过一个非线性函数,整个模型就是非线性的模型了,通过激活函数去实现。
激活函数实例:阶跃函数、sigmoid函数、tanh函数、ReLU函数等
因为激活函数是非线性的,所以经过激活函数处理之后的每一个节点都将实现复杂的非线性变换。

例子:

a=tf.nn.relu(tf.matmul(x,w1)+biases1)
y=tf.nn.relu(tf.matmul(a,w2)+biases2)
  • TensorFlow提供的激活函数
    1. Traditional: sigmoid(logistic)、tanh
    2. RELU Family: RELU、Leaky RELU、PRELU、RRELU
    3. Exponential Family: ELU、SELU

4.1.3 多层网络解决异或问题

本节主要讲解深度学习的另一重要性质——多层变换

单层感知机无法模拟“异或运算”,加入隐藏层后可以很好的实现异或问题,隐藏层可以被认为从输入特征中抽取了更高维的特征,深层网络实际上有组合特征提取的功能,该特性对解决不易提取特征向量的问题(图像识别、语音识别等)有很大的帮助。

4.2 损失函数

主要介绍如何刻画神经网络模型的效果(神经网络模型的效果以及优化的目标是通过损失函数来定义的)

4.2.1 经典损失函数

神经网络解决多分类问题最常用的方法是设置n个输出节点,n为类别个数,对于每个样例,得到一个n维数组作为输出结果,每个维度对应一个类别,输出为“1”代表判断结果的类别。

判断输出向量和期望向量的距离:交叉熵(cross entropy),刻画了两个概率分布间的距离,较为常用。

1. 交叉熵(给定两个概率分布 p q

H ( p , q ) = x p ( x ) l o g q ( x )

  1. 交叉熵不是对称的 H ( p , q ) H ( q , p ) ,刻画的是通过q来表达p的困难程度,p代表的正确答案,q代表的预测值。所以交叉熵值越小,即表示两个概率分布越接近
  2. 交叉熵刻画的是两个概率分布间的距离,然而神经网络的输出并不一定是概率分布,概率分布刻画了不同事件的发生概率,当事件总数有限的情况下,概率分布函数 p ( X = x ) 满足:
    x p ( X = x ) [ 0 , 1 ] , x p ( X = x ) = 1
  3. 即任意事件发生的概率都在[0,1]之间,且所有概率之和为1。
  • s o f t m a x 回归:将前向传播结果变成概率分布的常用方法
    其本身是一个学习算法来优化分类结果,但TensorFlow中只将其作为处理层——将网络输出变为概率分布
  • 示例:
    假设三分类问题,样例的正确答案为 1 0 0 ,softmax回归之后的预测答案是 0.5 , 0.4 , 0.1
    则预测和正确结果的交叉熵为:
    H ( ( 1 , 0 , 0 ) , ( 0.5 , 0.4 , 0.1 ) ) = ( 1 × l o g 0.5 + 0 × l o g 0.4 + 0 × l o g 0.1 ) 0.3

    另外一个模型的预测是 0.8 , 0.1 , 0.1 :
    H ( ( 1 , 0 , 0 ) , ( 0.8 , 0.1 , 0.1 ) ) = ( 1 × l o g 0.8 + 0 × l o g 0.1 + 0 × l o g 0.1 ) 0.1

    可知第二个模型效果更好

TensorFlow实现交叉熵

cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
#其中y代表正确结果,y_代表预测结果

分别解释上述程序的四个运算

1. tf.clip_by_value:将一个张量的数值限定在一个范围内,可以避免一些错误的运算(如log0无效)

clip_by_value(t,clip_value_min,clip_value_max,name=None)

举例:

import tensorflow as tf
v=tf.constant([[1.0,2.0,3.0],[4.0,5.0,6.0]])
with tf.Session() as sess:
    print(tf.clip_by_value(v,2.5,4.5).eval())
#eval(str [,globals [,locals ]])函数将字符串str当成有效Python表达式来求值,并返回计算结果。

[[ 2.5 2.5 3. ] [ 4. 4.5 4.5]]
#小于2.5的都变为了2.5
#大于4.5的都变为了4.5

2. tf.log:对张量中的所有元素依次求对数

import tensorflow as tf
v=tf.constant([1.0,2.0,3.0])
with tf.Session() as sess:
    print(tf.log(v).eval())

[ 0.          0.69314718  1.09861231]

3. tf.matmul:矩阵元素点乘

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

with tf.Session() as sess:
    print((v1*v2).eval())
    #输出:[[ 5. 12.],[ 21. 32.]](矩阵对应位置元素相乘)
    print(tf.matmul(v1, v2).eval())
    #输出:[[ 19. 22.],[ 43. 50.]](矩阵相乘)

通过这三步计算得到每一个样例中的每一个类别的交叉熵,是一个 n × m 矩阵:

n 为一个batch中的样例数量, m 为分类的类别数量。

如何获得交叉熵:将每行的 m 个结果相加得到所有样例的交叉熵,再对 n 行取平均得到一个batch的平均交叉熵

简化:分类问题的类别数量不变,故可以直接对整个矩阵做平均而不改变计算结果的意义,用tf.reduce_mean实现

import tensorflow as tf
v1=tf.constant([[1.0,2.0,3.0],[4.0,5.0,6.0]])
with tf.Session() as sess:
    print(tf.reduce_mean(v1).eval())

output:3.5

交叉熵一般会和softmax回归同时使用,所以TensorFlow对其进行了封装,可以直接获得softmax之后的交叉熵函数

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

2. 均方误差(MSE,Mean Squared Error)

回归问题是对具体数值的预测,解决回归问题的神经网络一般只有一个节点,该节点的值就是预测值,回归问题最常见的的损失函数是均方误差函数,定义如下:

M S E ( y , y ) = i = 1 n ( y i y i ) 2 n

其中, y i 是一个batch中的第 i 个数据的正确结果, y i 为预测结果

TensorFlow实现均方误差函数

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

4.2.2 自定义损失函数

自定义一个损失函数(预测值多于真实值和预测值少于真实值的损失函数不同)

L o s s ( y , y ) = i = 1 n f ( y i , y i ) ; i f x > y , f ( x , y ) = a ( x y ) ; i f x <= y , f ( x , y ) = b ( y x )

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

对其中的两个函数进行说明:

tf.select(condition,a,b)  
# condition:一个张量tensor,类型为bool
# a:一个张量tensor,shape与condition一致,类型一般为float32, float64, int32, int64.
# b:一个张量tensor,类型和shape与a一致。

# 当condition为True时,选择第二个参数a,否则选择第三个参数b(在元素级别进行)
tf.greater(a,b)
# 比较两个值的大小,当a>b时,返回True;当a<b时,返回False
  • 损失函数对模型训练结果的影响
import tensorflow as tf
from numpy.random import RandomState

batch_size=8

#两个输入节点
x=tf.placeholder(tf.float32,shape=(None,2),name='x-input')
#回归问题一般只有一个输出节点
y_=tf.placeholder(tf.float32,shape=(None,1),name='y-input')

#定义了一个单层的神经网络前向传播的过程,这里为简单的加权求和
w1=tf.Variable(tf.random_normal([2,1],stddev=1,seed=1))
y=tf.matmul(x,w1)

#定义预测多了/少了的样本
loss_less=10
loss_more=1
loss=tf.reduce_sum(tf.where(tf.greater(y,y_),(y-y_)*loss_more,(y_-y)*loss_less))

train_step=tf.train.AdamOptimizer(0.001).minimize(loss)

#通过随机数产生一个模拟数据集
rdm=RandomState(1)
dataset_size=128
X=rdm.rand(dataset_size,2)

Y=[[x1+x2+rdm.rand()/10.0-0.05] for (x1,x2) in X]
#训练神经网络
with tf.Session() as sess:
    init_op=tf.global_variables_initializer()
    sess.run(init_op)
    STEPS=5000
    for i in range(STEPS):
        start=(i*batch_size)%dataset_size
        end=min(start+batch_size,dataset_size)
        sess.run(train_step,feed_dict={x: X[start:end],y: Y[start:end]})
        print(sess.run(w1))

使用不同的损失函数会对结果造成不同的影响

4.3 神经网络优化算法

本节将更加具体的介绍如何通过反向传播和梯度下降来优化单个参数的取值

梯度下降:迭代式更新参数,不断沿梯度的反方向让参数朝着总损失更小的方向更新。

损失函数: J ( θ )

梯度: θ J ( θ )

学习率: η (learning rate),定义每次更新的幅度,也就是每次参数移动的幅度。

参数更新的公式:

θ n + 1 = θ n η θ n J ( θ )

  • 梯度下降法的步骤

    1. 随机生成一个参数x的初始值
    2. 通过梯度和学习率来更新参数x的取值
    3. 不断迭代获得更新后的参数
  • 神经网络的优化过程:

    1. 通过前向传播获得预测值,并获得和真实值的差距
    2. 通过反向传播计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降法更新每个参数
  • 梯度下降法的缺点

    1. 不能保证达到全局最优化,初始值的选取很重要,只有当损失函数为凸函数时,才能达到全局最优解
    2. 计算时间太长,因为要在全部训练数据上最小化损失,所以损失函数是在所有训练数据上的损失和,所以在每轮迭代中都需要计算在全部训练数据上的损失函数。
  • 随机梯度下降法:为了加速训练过程

    1. 该算法是在每一轮的迭代中,随机优化某一条训练数据上的损失函数,使得每一轮的更新速度加快。
    2. 存在的问题:在某一条数据上损失函数更小,并不代表在全部数据上的损失函数更小,可能都无法达到局部最优。
  • 综合梯度下降和随机梯度下降的折中方法:

    1. 每次计算一小部分的训练数据(batch)的损失函数,
    2. 优势:
      • 通过矩阵运算,每次在一个batch上优化神经网络的参数并不会比单个数据慢太多;
      • 每次使用一个batch可以大大减小收敛所需要的迭代次数,同时达到接近梯度下降的效果

TensorFlow训练神经网络的过程

batch_size=8

#每次读取一小部分数据作为当前训练数据来执行反向传播过程
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:
    #参数初始化
    ...
    #迭代更新次数
    for i in range(STEPS)
    #准备batch_size个训练数据,一般将所有训练数据随机打乱之后再选取,可以得到更好的优化效果
    current_X,current_Y=...
    sess.run(train_step,feed_dict={...})

4.4 神经网络的进一步优化

4.4.1 学习率的设置

  • 学习率——控制参数更新的速度,决定了参数每次更新的幅度

    1. 幅度过大(学习率过大):可能导致参数在极优秀值附近来回移动
    2. 幅度过小(学习率过小):降低收敛速度
  • TensorFlow提供了更加灵活的学习率设置方法——指数衰减法

tf.train.exponential_decay()

步骤:
1. 首先使用较大学习率(目的:为快速得到一个比较优的解);
2. 然后通过迭代逐步减小学习率(目的:为使模型在训练后期更加稳定);

代码实现:

decayed_learning_rate=learining_rate*decay_rate^(global_step/decay_steps) 

# decayed_learning_rate为每一轮优化时使用的学习率;
# learning_rate: 事先设定的初始学习率;
# decay_rate: 衰减系数;
# decay_steps: 衰减速度。

选择不同的衰减方式:staircase函数

#绘制staircase函数曲线
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

x=np.arange(2)
learning_rate = 0.1
decay_rate = 0.96
global_steps = 1000
decay_steps = 100

global_ = tf.Variable(tf.constant(0))
c = tf.train.exponential_decay(learning_rate, global_, decay_steps, decay_rate, staircase=True)
d = tf.train.exponential_decay(learning_rate, global_, decay_steps, decay_rate, staircase=False)

T_C = []
F_D = []

with tf.Session() as sess:
    for i in range(global_steps):
        T_c = sess.run(c, feed_dict={global_: i})
        T_C.append(T_c)
        F_d = sess.run(d, feed_dict={global_: i})
        F_D.append(F_d)

plt.figure(1)
plt.plot(range(global_steps), F_D, 'r-')
plt.plot(range(global_steps), T_C, 'b-')

plt.show()

TensorFlow:实战Google深度学习框架(三)深层神经网络
初始的学习速率是0.1,总的迭代次数是1000次,如果staircase=True,那就表明每decay_steps次计算学习速率变化,更新原始学习速率,如果是False,那就是每一步都更新学习速率。红色表示False,蓝色表示True。

  • staircase默认为False:连续的指数衰减学习率,不同的训练数据有不同的学习率,学习率减小时,对应的训练数据对模型训练结果的影响也就小了。

  • 当staircase为True时:(global_step/decay_steps)则被转化为整数,使得学习率成为一个阶梯函数。
    此时的decay_steps代表了完整的使用一遍训练数据所需要的迭代次数(即总样本数/每个batch中的样本数)
    效果:每完整的过一遍训练数据,学习率就减小一次,使得训练数据集中的所有数据对模型训练有相等的作用。

使用tf.train.exponential_decay的示例:

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(...my loss...,global_step=global_step

# 代码设定初始学习率为0.1,指定`staircase=True`,所以每训练100轮以后,学习率乘以0.96

4.4.2 过拟合问题

以上的章节都是求解如何在训练数据上优化一个给定的损失函数,但是实际中并不是仅仅希望模型尽量模拟训练数据,而是通过训练得到的模型能够给出对未知数据的判断。

  • 过拟合
    过拟合是指当一个模型过为复杂之后,它可以很好的“记忆”每个训练数据中随机噪声的部分,而忽视了整体的规律。也就是参数过多,使得模型对训练数据有很好的拟合,但是却没有学习到通用的规律,使得其对未知数据并不能很好的预测。

  • 如何避免过拟合:——正则化(regularization):
    在损失函数中加入刻画模型复杂程度的指标
    损失函数: J ( θ ) θ 指一个神经网络的所有参数,包括权重和偏置)
    优化时优化的函数: J ( θ ) + λ R ( w )
    其中:

    1. R ( w ) ——刻画模型的复杂程度;
      • L 1 正则化: R ( w ) = w 1 = i | w i |
        ① 会让参数变得稀疏,也就是将更多的参数变为0,这样可以达到类似的特征选取的功能;
        ② 计算公式不可导,所以优化带 L 1 正则化函数要更加复杂
      • L 2 正则化: R ( w ) = w 2 2 = i | w i 2 |
        ① 不会让参数变得更稀疏,因为当参数很小时(例如0.0001),则该参数的平方基本上就可以忽略了,于是模型不会进一步将其调整为0。
        ② 计算公式可导,因为在优化时需要计算损失函数的偏导数,所以对含有 L 2 正则化损失函数的优化更加简洁
      • L 1 L 2 正则化同时使用: R ( w ) = i α | w i | + ( 1 α ) w i 2
    2. λ ——模型复杂损失在总损失中的比例
  • TensorFlow优化带正则化的损失函数

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.12_regularizer(lambda)(w)

# loss:损失函数
# 第一部分:均方误差函数(刻画了模型在数据上的表现)
# 第二部分:正则化(防止模型过度模拟训练数据的随机噪音,lambda表示正则项的权重)
  • TensorFlow计算给定参数的 L 1 正则化项的值:
tf.contrib.layers.11_regularizer
  • TensorFlow计算给定参数的 L 2 正则化项的值:
tf.contrib.layers.12_regularizer

示例:

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.11_regularizer(0.5)(weights))
    # 输出为(|1|\^2+|-2|\^2+|-3|\^2+|4|\^2)/2*0.5=7.5
    # TensorFlow会将L2正则化损失值/2,使得求导结果更加简洁)
    print sess.run(tf.contrib.layers.11_regularizer(0.5)(weights))
  • 当神经网络参数增多时,这样的方式可能导致损失函数loss的定义很长,可读性差,易于出错,所以可以用集合的方式来计算。

4.4.3 滑动平均模型

在使用随机梯度下降法训练神经网络时,该方法可以使得模型在测试数据上更为健壮

  • TensorFlow实现滑动平均模型
tf.train.ExponentialMovingAverage(decay, steps)
# 这个函数用于更新参数,就是采用滑动平均的方法更新参数。
# 这个函数初始化需要提供一个衰减速率(decay):用于控制模型的更新速度。
# 这个函数还会维护一个影子变量(也就是更新参数后的参数值):这个影子变量的初始值就是这个变量的初始值。

1.影子变量

shadow_variable = decay * shadow_variable + (1-decay) * variable
# shadow_variable:影子变量
# variable:待更新的变量,也就是变量被赋予的值
# decay:衰减速率(decay越大模型越稳定,因为decay越大,参数更新的速度就越慢,模型越趋于稳定。 )
# decay一般设为接近于1的数(0.99或0.999等)。

2.动态设置decay的大小:为了使得模型在训练前期可以更新的更快,num_updates参数来实现动态设置

d e c a y = m i n { d e c a y , 1 + n u m _ u p d a t e s 10 + n u m _ u p d a t e s }

代码示例:

import tensorflow as tf

# 定义一个变量用于计算滑动平均模型,该变量的初始值为0,
# 此处手动设定变量的类型为tf.float32,因为所有需要计算滑动平均模型的变量必须是实数型
v1 = tf.Variable(0, dtype=tf.float32)  # 变量初始值为0
# step变量模拟神经网络中迭代的轮数,可用于动态控制衰减率
step = tf.Variable(0, trainable=False)  #train=True 可优化,train=False 不可优化

# 定义一个滑动平均的类,初始化时给定衰减率(0.99)和控制衰减率的变量step
ema = tf.train.ExponentialMovingAverage(0.99, step)
# 定义一个更新变量滑动平均操作
# 需要给定一个列表,每层执行该操作时,列表中的变量都会被更新
# apply()方法添加了训练变量的影子副本,并保持了其影子副本中训练变量的移动平均值操作。在每次训练之后调用此操作,更新移动平均值。
maintain_averages_op = ema.apply([v1])  # v1:更新变量列表

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

    # 更新变量v1的值为5
    sess.run(tf.assign(v1, 5))
    # 更新v1的滑动平均值,衰减率为min{0.99,(1+step)/(10+step)=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](variable,shadow_variable)

    # 更新step的值为10000
    sess.run(tf.assign(step,10000))
    # 更新v1的值为10
    sess.run(tf.assign(v1, 10))
    # 更新v1的滑动平均值,衰减率为min{0.99,(1+step)/(10+step)≈0.999}=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.5559998] 输出[10.0,4.6094499]

    # 再次更新滑动平均值,得到新滑动平均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]

[0.0, 0.0]
[5.0, 4.5]
[10.0, 4.5549998]
[10.0, 4.6094499]