基本概念

基于Tensorflow的NN:张量(Tensor)表示数据,用计算图搭建神经网络,用会话执行计算图,优化线上的权重(参数),得到模型。

张量(tensor)

多维数组(列表);阶:张量的维数;张量可以表示0到n阶的数组。在张量中并没有真正保存数字,保存的是如何得到这些数字的计算过程。

1
2
3
4
5
6
7
8
import tensorflow as tf 
a=tf.constant([1.0,2.0)
b=tf.constant([3.0,4.0)
# result=a+b
result=tf.add(a,b,name="add")
print(result)

Tensor("add:0",shape=(2,),dtype=float32)

输出结果为:

意思是结果是一个名称为add:0的张量,shape=(2,)表示一维数组长度为2,dtype=float32表示数据类型为浮点型。所以,计算图只描述运算过程,不描述运算结果。

计算图(Graph)

搭建神经网络的计算过程,只搭建,不计算。

神经网络的基本模型是神经元,神经元的基本模型其实就是数学中的乘、加运算。我们搭建如下的计算图:

实现上述计算图:

1
2
3
4
5
import tensorflow as tf #引入模块
x = tf.constant([[1.0, 2.0]]) #定义一个 2 阶张量等于[[1.0,2.0]]
w = tf.constant([[3.0], [4.0]]) #定义一个 2 阶张量等于[[3.0],[4.0]]
y = tf.matmul(x, w) #实现 xw 矩阵乘法
print y #打印出结果

可以打印出这样一句话:Tensor(“matmul:0”, shape(1,1), dtype=float32),
从这里我们可以看出,print 的结果显示 y 是一个张量,只搭建承载计算过程的
计算图,并没有运算,如果我们想得到运算结果就要用到“会话 Session()”了。

集合

集合可以在一个计算图中保存一组实体(比如张量)。

会话(Session)

执行计算图中的节点运算。

对于刚刚所述计算图,我们执行 Session()会话可得到矩阵相乘结果:

1
2
3
4
5
6
7
import tensorflow as tf #引入模块
x = tf.constant([[1.0, 2.0]]) #定义一个 2 阶张量等于[[1.0,2.0]]
w = tf.constant([[3.0], [4.0]]) #定义一个 2 阶张量等于[[3.0],[4.0]]
y = tf.matmul(x, w) #实现 xw 矩阵乘法
print y #打印出结果
with tf.Session() as sess:
print sess.run(y) #执行会话并打印出执行后的结果

可以打印出这样的结果:

1
2
Tensor(“matmul:0”, shape(1,1), dtype=float32)
[[11.]]

我们可以看到,运行Session()会话前只打印出y是个张量的提示,运行Session()会话后打印出了 y 的结果 1.0 x 3.0 + 2.0 x 4.0 = 11.0。

神经网络的参数

是指神经元线上的权重 w,用变量表示,一般会先随机生成这些参数。生成参数的方法是让w等于tf.Variable,把生成的方式写在括号里。神经网络中常用的生成随机数/数组的函数有:

函数 功能
tf.random_normal() 生成正太分布的随机数
tf.truncated_normal() 生成去掉多大偏离点的正太分布随机数
tf.random_uniform() 生成均匀分布随机数
tf.zeros 表示生成全 0 数组
tf.ones 表示生成全 1 数组
tf.fill 表示生成全定值数组
tf.constant 表示生成直接给定值的数组

举例

1
w=tf.Variable(tf.random_normal([2,3],stddev=2,mean=0,seed=1))

表示生成正太分布随机数,形状两行三列,标准差是2,均值是0,随机种子是1。

1
w=tf.Variable(tf.Truncated_normal([2,3],stddev=2,mean=0,seed=1))

表示去掉偏离过大的正太分布,也就是如果随机出来的数据偏离平均值超过两个标准差,这个数据将重新生成。

1
w=random_unifrom(shape=7,minval=0,maxval=1,dtype=tf.int32,seed=1)

表示从一个均匀分布[minval,maxval)中随机采用,注意定义域是左闭右开。

注意:随机种子如果去掉每次生成的随机数将不一样;如果没有特殊要求标准差、均值、随机种子是可以不写的。

神经网络的搭建

神经网络的实现过程如下:

  1. 准备数据,提取特征,作为输入喂给神经网络(Neural Network,NN)
  2. 搭建NN结构,从输入到输出(先搭建计算图,再用会话执行)
    NN前向传播算法——计算输出
  3. 大量特征数据喂给NN,迭代优化NN参数。
    NN反向传播算法——优化参数训练模型
  4. 使用训练好的模型预测和分类。

由此可见,基于神经网络的机器学习主要分为两个过程,即训练过程和使用。训练过程是第一步、第二步 、第三步的循环迭代,使用过程是第四步,一旦参数优化完成就可以固定这些参数,实现特定应用了 。
很多实际应用中,我们会先使用现有的成熟网络结构,喂入新的数据,训练相应模型,判断是否能对喂入的从未见过新数据作出正确响应,再适当更改网络结构,反复迭代,让机器自动训练参数 找出最优结构和,以固定专用模型。

前向传播

前向传播就是搭建模型计算的过程,让模型具有推理能力,可以针对一组输入给出相应的输出。

举例
假如生产一批零件,体积为 x1,重量为 x2,体积和重量就是我们选择的特征,把它们喂入神经网络把它们喂入神经网络把它们喂入神经网络,当体积和重量这组数据走过神经网络后会得到一个输出。假如输入特征是:体积0.7,重量0.5

由搭建的神经网络可得,隐藏层节点 a11=x1*w11+x2*w21=0.14+0.15=0.29,同理算得节点a12=0.32,a13=0.38,最终计算得到输出层Y=-0.015,这便实现了前向传播过程。

推导过程:

第一层

X是输入喂1x2矩阵

用x表示输入,是一个1行2列矩阵,表示一次输入一组特征,这组特征包含了体积和重量两个元素。

W前节点编号,后节点编号(层数)为待优化参数

对于第一层的w前面有两个节点,后面有3个节点,w应该是2行3列矩阵。这样表示:

神经网络共有几层(或当前是第几层网络)都是指计算层,输入都不是计算层,所以a为第一层网络,啊是一个一行3列矩阵。
我们这样表示:

a(1)=[a11,a12,a13]=XW(1)

第二层

参数要满足前面3个节点,后面一个节点,所以W(2)是3行1列矩阵。这样表示:

我们把每层输入乘以线上的权重w,这样用矩阵乘法就可以算出输出y了。一下Tensorflow程序实现了上述神经网络的前向传播过程。

1
2
a=tf.matmul(X,W1)
y=tf.matmul(a,W2)

由于需要计算结果,就要用with结构实现,所有变量初始化过程、计算过程都要放到sess.run函数中。对于变量初始化,在sess.run小红写入tf.global_variables_initializer实现对所有变量初始化。对于计算图中的运算,我们直接把运算节点填入sess.run中即可,比如要计算输出y,直接写sess.run(y)。
在实际应用中,我们可以一次喂如多组输入,让神经网络计算输出y,可以先用tf.palceholder给输入占位。如果一次喂一组数据shape的第一维位置写1,第二维位置看有几个输入特征;如果一次想喂如多组数据,shape的第一维位置写None表示先空着,第二维位置写有几个输入特征。这样在feed_dict中可以喂入若干组体积重量了。

前向传播过程Tensorflow描述

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
# 两层神经网络(全连接)
import tensorflow as tf

# 定义输入和参数
# x = tf.constant([[0.7, 0.5]])
# 用placehloder实现输入定义 (sess.run中喂一组数据)
# x = tf.placeholder(tf.float32, shape=(1, 2))
# sess.run喂入多组数据
x = tf.placeholder(tf.float32, shape=(None, 2))
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))

# 定义前向传播过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)

# 用回话计算结果
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
# print(sess.run(y, feed_dict={x: [[0.7, 0.5]]}))
print(
sess.run(y, feed_dict={x: [[0.7, 0.5], [0.2, 0.3], [0.3, 0.4], [0.4,0.5]]}))
print(sess.run(w1))
print(sess.run(w2))

反向传播

训练模型参数,在所有参数上用梯度下降,使NN模型在训练数据上的损失函数最小。

损失函数(loss):计算得到的预测值y与已知答案y_的差距。

损失函数计算方法有很多,均方误差MSE是比较常用的方法之一。

均方误差MSE:求前向传播计算结果与已知答案之差的平方再求平均。

用Tensroflow函数表示为:

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

反向传播训练方法:以减小loss值为优化目标,有梯度下降、momentum优化器、adam优化器等优化方法。
这3中优化方法用tensorflow的函数可以表示为:

1
2
3
train_step=tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)
train_step=tf.train.MomentumOptimizer(learning_rate,momentum).minimize(loss)
train_step=tf.train.AdamOptimizer(learning_rate).minimize(loss)

三种优化方法区别如下:

  1. tf.train.GradientDescentOptimizer()使用随机梯度下降算法,使参数沿着梯度相反方向,即总损失减小的方向移动,实现更新参数。

参数更新公式为:

  1. tf.train.MomentumOptimizer()在更新参数时,利用了超参数,参数更新公式为:
  1. tf.train.AdamOptimizer()是利用自适应学习率的优化算法,Adam算法和随机梯度下降算法不同。随机梯度下降算法保持单一的学习率更新所有的参数,学习率在训练过程中并不会改变。而Adam算法通过计算梯度的一阶矩阵估计和二阶矩阵估计而为不同的参数设计独立的自适应学习率。

学习率:决定每次参数更新的幅度。

优化器中都需要一个叫做学习率的参数,使用时,如果学习率过大会出现震荡不收敛的情况,如果过小,会出现收敛速度慢的情况。可以选择比较小的值填入,比如0.01、0.001。

反向传播参数更新推导过程:

符号说明:

zl表示第l层隐藏层和输出层的输入值;

al表示第l层隐藏层和输出层的输出值;

f(z)表示激活函数;

最后输出层为第L层;

搭建神经网络的八股

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
import tensorflow as tf
import numpy as np

BATCH_SIZE = 8
seed = 23455

# 基于seed产生随机数
rng = np.random.RandomState(seed)
# 随机数返回32行2列的矩阵 表示32组 体积和重量 作为输入数据集
X = rng.rand(32, 2)
# 从X这个32行2列的矩阵中取出一行 如果和小于1 给Y赋值1 如果和不小于1 给Y赋值0 作为输入数据集的标签(正确答案)
Y = [[int(x0 + x1 < 1)] for (x0, x1) in X]
print("X:", X)
print("Y:", Y)
# 1定义神经网络的输入、参数和输出 定义前向传播过程
x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))

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

a = tf.matmul(x, w1)
y = tf.matmul(a,w2)

# 2定义损失函数及反向传播方法
loss = tf.reduce_mean(tf.square(y - y_))
train_step = tf.train.GradientDescentOptimizer(0.001).minimize(loss)
# train_step = tf.train.MomentumOptimizer(0.001, 0.9).minimize(loss)
# trian_step=tf.train.AdamOptimizer(0.001).minimize(loss)

# 生成会话 训练steps轮

with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
# 输出目前的参数取值
print("w1", sess.run(w1))
print("w2", sess.run(w2))

# 训练模型
STEPS = 3000

for i in range(STEPS):
start = (i * BATCH_SIZE) % 32
end = start + BATCH_SIZE
sess.run(train_step, feed_dict={x: X[start:end], y_: Y[start:end]})
if i % 500 == 0:
total_loss = sess.run(loss, feed_dict={x: X, y_: Y})
print("After %d training steps,loss on all data is %g" %
(i, total_loss))
# 输出训练后的参数取值
print("w1", sess.run(w1))
print("w2", sess.run(w2))

神经网络优化

神经元模型

激活函数

引入非线性激活因素,解决线性模型的表达、分类能力不足的问题,提高模型的表达力。将线性组合转换为非线性结果。

  1. Sigmoid

    变化区间[0,1],如果是非常大的负数,输出就是0,如果是非常大的正数,输出就是1,这样使得数据在传递过程中不容易发散。优点是整个区间上可导,缺点一是容易过饱和,丢失梯度。从Sigmoid的示意图上可以看到,神经元的活跃度在0和1处饱和,梯度接近于0,这样在反向传播时,很容易出现梯度消失的情况,导致训练无法完整;二是Sigmoid的输出均值不是0。

  2. tanh

    tanh是Sigmoid函数的变形,tanh的均值是0

Sigmiod和tanh的缺点是当数值很大或很小时,结果变化比较平缓,大网络下学习效率低。

  1. RelU
  • 非线性的激活函数使得模型能够处理更加复杂的非线性数据集问题,提高了模型的学习能力。
  • 仿照生物神经元的思想,通过激活函数的处理后,神经元被划分为激活态和抑制态,因而,在训练过程中,能够吧起终点作用的神经元置为激活态,而把相对无关的神经元置为抑制态,起到自动特征提取的作用。

神经网络的复杂度

可用神经网络的层数和神经网络中待优化参数个数表示。

  • 神经网络的层数:一般不计入输入层,层数=n个隐藏层+1个输出层
  • 神经网络待优化参数:神经网络中所有参数w的个数+所有参数b的个数

在该神经网络中,包含1个输入层、 1个隐藏层和1个输出层,该神经网络的数为2层。在该神经网络中,参数的个数是所有参数w的个数加上所有参b的总数,第一层参数用三行四列的二阶张量表示(即12个线上的权重w)再加上4个偏置 b;第二层参数是四行两列的二阶张量(即 8个线上的权重 w)再加上 2个偏置 b。总参数 =3x4+4+4x2+2=26。

损失函数

用来表示预测值(y)与已知答案(y_)的差距。在训练神经网络时,通过 不断改变神经网络中所有参数,使损失函数不断减小,从而训练出更高准确度的神经网络模型。

  • 常用的损失函数有:均方误差(Mean Squared Error)、自定义和交叉熵(Cross Entropy)等。

  • 均方误差(Mean Squared Error):n个样本的预测值y与已知答案y_之差的平方和,再求平均值。

    在Tensorflow中用 loss_mse=tf.reduce_mean(tf.square(y_-y)) 表示。

举例

预测酸奶日销量y。x1、x2是影响销量的因素。应提前采集的数据有:一段时间内,每日的x1、x2和销量y_。用销量预测产量,最优的产量应等于销量。
利用 Tensorflow中函数 随机生成 x1、x2,制造标准答案 y_ = x1 + x2,为了更真实 ,求和后还加了正负0.05的随机噪声。

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
import tensorflow as tf
import numpy as np
BATCH_SIZE = 8
SEED = 23455 # 保证每次生成的数据集一样

# 生成32行2列的数据集
rdm = np.random.RandomState(SEED)
X = rdm.rand(32, 2)
Y_ = [[x1 + x2 + (rdm.rand() / 10.0 - 0.05)] for (x1, x2) in X]

# 1 定义神经网络的输入、参数和输出,定义前向传播过程
x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))
w1 = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w1)

# 2 定义损失函数及反向传播方法
# 损失函数为MSE 反向传播方法为梯度下降
loss_mse = tf.reduce_mean(tf.square(y_ - y))
trian_step = tf.train.GradientDescentOptimizer(0.001).minimize(loss_mse)

# 3 生成会话 训练steps轮
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
STEPS = 20000
for i in range(STEPS):
start = (i * BATCH_SIZE) % 32
end = (i * BATCH_SIZE) % 32 + BATCH_SIZE
sess.run(trian_step, feed_dict={x: X[start:end], y_: Y_[start:end]})

if i % 500 == 0:
print("After %d training steps,w1=%s" % (i, sess.run(w1)))
print("Final w1=", sess.run(w1))

自定义损失函数

根据问题的实际情况,定制合理的损失函数。

对于预测酸奶日销量问题,如果预测销量大于实际销量则会损失成本;如果预测销量小于实际销量,则会损失利润。在实际生活中,往往制造一盒酸奶的成本和销售一盒酸奶的利润是不等价的。因此,需要使用符合该问题的自定义损失函数。

  1. 若酸奶的成本为1元,酸奶的销售利润为9元,则制造成本小于酸奶利润,因此希望预测的结果y多一些。
    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
    import tensorflow as tf
    import numpy as np

    BATCH_SIZE = 8
    SEED = 23455
    COST = 1
    PROFIT = 9

    rdm = np.random.RandomState(SEED)
    X = rdm.rand(32, 2)
    Y = [[x1 + x2 + (rdm.rand() / 10.0 - 0.05)] for (x1, x2) in X]

    # 定义神经网络的输入、参数和输出,定义前向传播过程
    x = tf.placeholder(tf.float32, shape=(None, 2))
    y_ = tf.placeholder(tf.float32, shape=(None, 1))

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

    # 定义损失函数及反向传播方法
    # 定义损失函数使得预测少了的损失大,于是模型应该偏向多的放心预测。
    loss = tf.reduce_sum(
    tf.where(tf.greater(y, y_), (y - y_) * COST, (y_ - y) * PROFIT))

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

    # 生成会话 训练steps轮
    with tf.Session() as sess:
    init_op = tf.global_variables_initializer()
    sess.run(init_op)
    STEPS = 20000
    for i in range(STEPS):
    start = (i * BATCH_SIZE) % 32
    end = (i * BATCH_SIZE) % 32 + BATCH_SIZE
    sess.run(train_step, feed_dict={x: X[start:end], y_: Y[start:end]})

    if i % 500 == 0:
    print("After %d training steps,w1=%s" % (i, sess.run(w1)))
    print("Final w1=", sess.run(w1))

由代码执行结果可知,神经网络的最终参数为w1=1.02,w2=1.04,销量预测结果为y=1.02*x1+1.04*x2,由此可见,自定义损失函数的结果大于采用均方误差预测结果,更符合实际需求。

交叉熵

表示两个概率分布之间的距离。交叉熵越大,两个概率分布距离越远,两个概率分布越相异;交叉熵越小,两个概率分布越接近,两个概率分布越相似。

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

其中 tf.clip_by_value 函数可以将一个张量中的数值限制在一个范围内,避免一些运算错误(比如log0是无效的)。tf.log 函数对张量中所有元素依次求对数的功能。

使用softmax函数回归之后的交叉熵

softmax函数应用:在n分类中,模型会有n个输出(y1,y2…yn),其中yi表示第i中情况出现的可能性大小。将n个输出经过softmax函数,可得到符合概率分布的分类结果。

在Tensorflow中,一般让模型的输出经过softmax函数,以获得输出分类的概率分布,再与标准答案对比,求出交叉熵,得到损失函数,用如下函数实现:

1
2
ce=tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y,labels=tf.argmax(y_,1))
cem=tf.reduce_mean(ce)

神经网络优化算法

  梯度下降算法主要用于优化单个参数的取值;反向传播算法给出了一种高效的方式在所有参数上用梯度下降,使NN模型在训练数据上的损失函数最小,它可以根据定义好的损失函数优化神经网络中参数的取值,从而使NN在训练数据集上的损失函数达到一个较小值。

神经网络的优化过程可以分为两个阶段,第一阶段是先通过前向传播算法计算得到预测值,并将预测值和真实值做对比得出两者的差距。然后第二阶段通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。

梯度下降有两个不足,一个是不一定能达到全局最优,在一个就是计算时间太长,因为要在全部训练数据上最小化损失,所以损失函数J(θ)是在所有训练数据上的损失和。为了加速训练过程,可以使用随机梯度下降算法。该算法的优化不是在全部训练数据上的损失函数,而是在每一轮迭代中,随机优化某一条训练数据上的损失函数。所以,它的问题也非常明显,在某一条数据上损失函数更小并不代表在全部数据上损失函数更小,使用随机梯度下降优化得到的神经网络甚至无法达到局部最优。

在实际应用中会采用这两个算法的折中,每次计算一小部分训练数据的损失函数,这一小部分称之为一个 batch。通过矩阵运算,每次在一个 batch 上优化神经网络的参数并不会比单个数据慢太多。另一方面,每次使用一个 batch 可以大大减小收敛所需的迭代次数,同时可以使收敛到的结果更加接近梯度下降的效果。

神经网络的进一步优化

学习率的设置

  1. 恒定学习率

    表示每次参数更新的幅度大小。学习率过大,会导致待优化参数在最小值附近波动;学习率过小,会导致待优化的参数收敛缓慢。
    在训练过程中,参数更新向着损失函数梯度下降的方向。

参数的更新公式:

由图可知,损失函数loss的最小值会在(-1,0)处得到,此时损失函数的导数为0,得到最终参数w=-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import tensorflow as tf
# 定义待优化参数w初值赋5
w = tf.Variable(tf.constant(5, dtype=tf.float32))
# 定义损失函数loss
loss = tf.square(w + 1)
# 定义反向传播的方法
train_step = tf.train.GradientDescentOptimizer(0.2).minimize(loss)
# 生成回话 训练40轮
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
for i in range(40):
sess.run(train_step)
w_val = sess.run(w)
loss_val = sess.run(loss)
print("After %s setps:w= %f, loss= %f" % (i, w_val, loss_val))

由结果可知,随着损失函数值减小,w无限趋近于-1,模型计算推测出最优参数w=-1。
学习率过大,会导致待优化的参数在最小值附近波动,不收敛;学习率过小,会导致待优化参数的参数收敛缓慢。

  1. 指数衰减学习率

    学习率随着训练轮数变化而动态更新。计算公式如下:

1
decayed_learning_rate=learning_rate * decay_rate ^ (global_step/decay_steps)

用Tensorflow函数表示为:

1
2
3
4
5
6
7
8
# 计数器  计数当前运行了多少轮 非训练参数,标注为不可训练
global_step=tf.Variable(0,trainable=False)
leraning_rate=tf.train.exponential_decay(
LEARNING_RATE_BASE,
global_step,
LEARNING_RATE_STEP,LEARNING_RATE_DECAY,
staircase=True/False
)

其中LEARNING_RATE_BASE为学习率的初始值,LEARNING_RATE_DECAY为学习率的衰减率,global_step记录了当前训练轮数,为不可训练型参数。需息率learning_rate更新频率为输入数据集总样本数除以每次喂入样本数。若staircase为True,表示global_step/learning_rate_step取整数,学习率阶梯型衰减;若staircase为False,学习率会是一条平滑下降的曲线。

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
import tensorflow as tf

LEARNING_RATE_BASE = 0.1 # 最初学习率
LEARNING_RATE_DECAY = 0.99 # 学习率衰减率
LEARNING_RATE_STEP = 1 # 喂入多少轮BATCH_SIZE后,更新一次学习率,一般设为:总样本数/BATCH_SIZE

# 运行了几轮BATCH_SIZE的计数器,初始值给0,设为不被训练
global_step = tf.Variable(0, trainable=False)
# 定义指数下降学习率
learning_rate = tf.train.exponential_decay(
LEARNING_RATE_BASE,
global_step,
LEARNING_RATE_STEP,
LEARNING_RATE_DECAY,
staircase=True)
# 定义待优化参数,初始值为10
w = tf.Variable(tf.constant(5, dtype=tf.float32))
# 定义损失函数loss
loss = tf.square(w + 1)
# 定义反向传播方法
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(
loss, global_step=global_step)
# 生成会话 训练40轮
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
for i in range(40):
sess.run(train_step)
learning_rate_val = sess.run(learning_rate)
global_step_val = sess.run(global_step)
w_val = sess.run(w)
loss_val = sess.run(loss)
print("After %s steps: global_step=%f,w=%f,learning_rate=%f,loss=%f" %
(i, global_step_val, w_val, learning_rate_val, loss_val))

滑动平均模型

记录了一段时间内模型中所有参数w和b各自的平均值。利用滑动平均值可以增强模型的泛化能力。
滑动平均值(影子)计算公式:

用Tensorflow函数表示为:

1
ema=tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY,global_step)

其中,MOVING_AVERAGE_DECAY表示滑动平均衰减率,用于控制模型更新的速度,一般会赋接近1的值,global_step表示当前训练了多少轮。ExponentialMovingAverage 对每一个变量会维护一个影子变量,这个影子变量的初始值就是相应变量的初始值,而每次运行变量更新时,影子变量的值会更新为:

1
shadow_variable=decay * shadow_variable+(1-decay) * variable

其中 shadow_variable 为影子变量,variable 为待更新的变量,decay 为衰减率。从式中可看出 decay 决定了模型更新速度,decay 越大模型越趋于稳定。在实际应用中,一般设置成非常接近1的数,如0.999。

1
ema_op=ema.apply(tf.trainable_variables())

其中,ema.apply()函数实现对括号内参数求滑动平均,tf.trainable_variables()函数实现把所有待训练参数汇总为列表。

1
2
with tf.control_dependencies([train_step,ema_op]):
train_op=tf.no_op(name='train')

其中,该函数实现将滑动平均和训练过程同步运行。查看模型中参数的平均值,可以用ema.average(参数名)函数。

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
import tensorflow as tf
# 1定义变量及滑动平均类
# 定义一个32喂浮点变量 初始值为0.0 这个代码就是不断更新w1参数,优化w1参数,滑动平均做了个w1的影子
w1 = tf.Variable(0, dtype=tf.float32)
# 定义num_updates(NN的迭代轮数),初值为0,不可被优化(训练),这个参数不训练
global_step = tf.Variable(0, trainable=False)
# 实例滑动平均类 衰减率为0.99 当前轮数global_step
MOVING_AVERAGE_DECAY = 0.99
ema = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
# ema.apply后的括号里是更新列表,每次运行sess.run(ema_op)时,对更新列表中的元素求滑动平均值
# 在实际应用中会使用tf.trainable_variables()自动讲所有待训练参数汇总为列表
ema_op = ema.apply(tf.trainable_variables())
# 2查看不同迭代中变量取值的变化
with tf.Session() as sess:
# 初始化
init_op = tf.global_variables_initializer()
sess.run(init_op)
# 用ema.average(w1)获取w1滑动平均值(要运行多个节点,作为列表中的元素列出,写在sess.run中)
# 打印出当前参数w1和w1滑动平均值
print(sess.run([w1, ema.average(w1)]))
# 参数w1的值赋为1
sess.run(tf.assign(w1, 1))
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))
# 更新step和w1的值,模拟出100轮迭代后,参数w1变为10
sess.run(tf.assign(global_step, 100))
sess.run(tf.assign(w1, 10))
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))
# 更新step和w1的值,模拟出100轮迭代后,参数w1变为10
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))
sess.run(ema_op)
print(sess.run([w1, ema.average(w1)]))

过拟合问题

  • 过拟合:神经网络模型在训练数据集上的准确率较高,在新的数据进行预测或分类时准确率较低,说明模型泛华能力差。
  • 正则化:在损失函数中给每个参数w加上权重,引入模型复杂度指标,从而抑制模型噪声,减小过拟合。

假设用于刻画模型在训练数据上表现的损失函数为J(θ),那么在优化时不时直接优化 J(θ),而是优化 J(θ)+λR(w),其中 R(w) 刻画的是模型的复杂程度,而 λ 表示模型复杂损失在总损失中的比例。θ 表示一个神经网络中所有的参数,包括边上的权重 w 和偏置项b,一般来说,模型复杂度只由 w 决定。常用刻画模型复杂度R(w)有两种,L1 正则化和 L2 正则化。

使用正则化后,损失函数loss变为两项之和:

1
los=loss(y与y_)+REGULARIZER * loss(w)

其中,第一项是预测结果与标准啊答案之间的差距,如交叉熵、均方误差等;第二项是正则化计算结果。

  • 正则化计算方法:
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

BATCH_SIZE = 30
seed = 2
# 基于seed产生随机数
rdm = np.random.RandomState(seed)
# 随机数返回300行2列的矩阵,表示300组坐标点(x0,x1)作为输入数据集
X = rdm.randn(300, 2)
# 从X这个300行2列的矩阵中取出一行,判断如果两个坐标的平方和小于2,给Y赋值1,其余赋值0
# 作为输入数据集的标签(正确答案)
Y_ = [int(x0 * x0 + x1 * x1 < 2) for (x0, x1) in X]
# 遍历Y中的每个元素,1赋值red其余赋值blue
Y_c = [['red' if y else 'blue'] for y in Y_]
# 对数据集X和标签Y进行shape整理,把第一个元素为-1表示,随第二个参数计算得到,第二个元素表示多少列,把X整理为n行2列,把Y整理成n行1列
X = np.vstack(X).reshape(-1, 2)
Y_ = np.vstack(Y_).reshape(-1, 1)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
plt.scatter(X[:, 0], X[:, 1], c=np.squeeze(Y_c))
plt.show()
# 定义神经网络的输入、参数和输出,定义前向传播过程
def get_weight(shape, regularizer):
w = tf.Variable(tf.random_normal(shape), dtype=tf.float32)
tf.add_to_collection('losses',
tf.contrib.layers.l2_regularizer(regularizer)(w))
return w
def get_bias(shape):
b = tf.Variable(tf.constant(0.01, shape=shape))
return b
x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))

w1 = get_weight([2, 11], 0.01)
b1 = get_bias([11])
y1 = tf.nn.relu(tf.matmul(x, w1) + b1)

# 输出层不过激活
w2 = get_weight([11, 1], 0.01)
b2 = get_bias([1])
y = tf.matmul(y1, w2) + b2

# 定义损失函数
loss_mse = tf.reduce_mean(tf.square(y - y_)) # 均方误差损失函数
loss_total = loss_mse + tf.add_n(tf.get_collection('losses'))

# 定义反向传播方法:不含正则化
train_step = tf.train.AdamOptimizer(0.0001).minimize(loss_mse)

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

STEPS = 40000
for i in range(STEPS):
start = (i * BATCH_SIZE) % 300
end = start + BATCH_SIZE
sess.run(train_step, feed_dict={x: X[start:end], y_: Y_[start:end]})
if i % 2000 == 0:
loss_mse_v = sess.run(loss_mse, feed_dict={x: X, y_: Y_})
print("After %d steps,loss=%f" % (i, loss_mse_v))
# xx在-3到3之间以步长为0.01,yy在-3到3之间以步长0.01,生成二维网格坐标点
xx, yy = np.mgrid[-3:3:.01, -3:3:.01]
# 将xx,yy拉直,并合并成一个2列的矩阵,得到一个网格坐标点的集合
grid = np.c_[xx.ravel(), yy.ravel()]
# 将网格点坐标喂入神经网络,probs为输出
probs = sess.run(y, feed_dict={x: grid})
# 将probs的shape调整成xx的样子
probs = probs.reshape(xx.shape)
# print("w1:", sess.run(w1))
# print("b1:", sess.run(b1))
# print("w2:", sess.run(w2))
# print("b2:", sess.run(b2))

plt.scatter(X[:, 0], X[:, 1], c=np.squeeze(Y_c))
plt.contour(xx, yy, probs, levels=[0.5])
plt.show()

# 定义反向传播方法:含正则化
train_step = tf.train.AdamOptimizer(0.0001).minimize(loss_total)

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

STEPS = 40000
for i in range(STEPS):
start = (i * BATCH_SIZE) % 300
end = start + BATCH_SIZE
sess.run(train_step, feed_dict={x: X[start:end], y_: Y_[start:end]})
if i % 2000 == 0:
loss_mse_v = sess.run(loss_total, feed_dict={x: X, y_: Y_})
print("After %d steps,loss=%f" % (i, loss_mse_v))
# xx在-3到3之间以步长为0.01,yy在-3到3之间以步长0.01,生成二维网格坐标点
xx, yy = np.mgrid[-3:3:.01, -3:3:.01]
# 将xx,yy拉直,并合并成一个2列的矩阵,得到一个网格坐标点的集合
grid = np.c_[xx.ravel(), yy.ravel()]
# 将网格点坐标喂入神经网络,probs为输出
probs = sess.run(y, feed_dict={x: grid})
# 将probs的shape调整成xx的样子
probs = probs.reshape(xx.shape)
# print("w1:", sess.run(w1))
# print("b1:", sess.run(b1))
# print("w2:", sess.run(w2))
# print("b2:", sess.run(b2))

plt.scatter(X[:, 0], X[:, 1], c=np.squeeze(Y_c))
plt.contour(xx, yy, probs, levels=[0.5])
plt.show()

对比无正则化和有正则化模型的训练结果,可以看出有正则化模型的拟合曲线平滑,模型具有更好的泛化能力。

特点总结:

  • 正则化基本原理是通过限制权重大小,使得模型不能任意拟合训练数据中的随机噪声。
  • L1正则化会让参数变得更稀疏,而L2正则化不会。参数变得更稀疏原因是会有更多的参数变为0,这样可以达到类似特征选取的功能。之所以L2正则化不会是因为当参数很小时,比如0.001,这个参数的平方基本就可以忽略了,于是模型不会进一步将这个参数调整为0。
  • L1正则化的计算公式不可导,而L2正则化公式可导。因为在优化时需要计算损失函数的偏导数,所以对含有L2正则化损失函数的优化更简洁,优化带L1正则化的损失函数更复杂。
    在实践中也可以将L1和L2正则化同时使用:

TensorBoard可视化

名词解释

全连接:之所以称之为全连接神经网络是因为相邻两层之间任意两个节点之间都有连接。
特征向量:用于描述实体的数字组合就是一个实体的特征向量。