深层神经网络
文章结构如下:
1: 激活函数
2: 损失函数定义
- 2.1 经典损失函数
- 2.2 自定义损失函数
3: 神经网络优化算法
4: 神经网络的进一步优化
- 4.1 学习率的设置
- 4.2 过拟合问题
- 4.3 滑动平均模型
1: 激活函数
激活函数可以去线性化。TensorFlow提供了7种不同的非线性激活函数,tf.nn.relu, tf.sigmoid, tf.tanh是比较常用的几个。当然TensorFlow也支持自己定义的激活函数。
a = tf.nn.relu(tf.matmul(x, w1) + biases1)
b = tf.nn.relu(tf.matmul(a, w2) + biases2)
2: 损失函数定义
2.1 经典损失函数
交叉熵刻画了两个概率分布之间的距离,它是分类问题中使用比较广方的一种损失函数。
给定两个概率分布 p 和 q ,通过 q 来表示 p 的交叉熵为:
H(p,q)=-\sum_xp(x)log~q(x)\\
虽神经网络的输出不是一个概率分布,但通过Softmax回归可以将神经网络的输出变成一个概率分布
softmax(y)_i=y_{i}'=\frac{e^{yi}}{\sum_{j=1}^{n}e^{yj}}\\
从交叉熵的公式可以看到交叉熵函数不是对称的 (H(p,q)\neq H(q,p)) ,它刻画的是通过概率分布 q 来表达概率分布 p 的困难程度,因为正确答案是希望得到的结果,所以当作交叉熵作为神经网络的损失函数时, p 代表的是正确答案, q 是代表的是预测值。交叉熵值越小,两个概率分布越近。
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
y_是正确的结果,y代表预测值。
v1 = tf.constant([[1.0, 2.0], [3.0, 4.0]])
v2 = tf.constant([[5.0, 6.0], [7.0, 8.0]])
#元素相乘
print((v1 * v2).eval())
#矩阵相乘
print(tf.matmul(v1, v2).eval())
v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(tf.reduce_mean(v).eval()) #求平均
#softmax回归与交叉熵的一起封装
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(label=y_, logits=y)
在只有一个正确答案的分类问题中,TensorFlow提供了tf.nn.sparse_softmax_croos_entropy_with_logits函数来进一步加速过程。
回归最常用的损失函数为:均方误差(MSE, mean squared error):
MSE(y,y')=\frac{\sum_{i=1}^{n}(y_i-y_i')^2}{n}\\
其中 y_i 为一个batch中第 i 个数据的正确答案,而 y'_i 为神经网络给出的预测值。
mse = tf.reduce_mean(tf.square(y_ - y))
2.2 自定义损失函数
假如损失函数是:
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\le y \end{matrix}\right.\\
loss = tf.reduce_sum(tf.where(tf.greater(v1, v2), (v1 - v2)*a, (v2 - v1)*b))
tf.greater的输入是两个张量,此函数会比较这两个输入张量中每一个元素的大小,返回比较结果。
tf.where函数有三个参数,第一个是选择条件根据,当条件为True时,tf.where函数会选择第二个参数的值,否则选择第三个参数中的值。tf.where函数判断和选择都在元素级别进行。
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])
sess = tf.InteractiveSession()
print(tf.greater(v1, v2).eval())
#输出:[False False True True]
print(tf.where(tf.greater(v1, v2), v1, v2).eval())
#输出:[4. 3. 3. 4.]
我们将 Tensorflow入门 里的4.3节的损失函数换成上面自定义的损失函数,看看损失函数对模型训练结果的影响。
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_mean(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))
#输出:[[1.019347 ]
# [1.0428089]]
从结果看预测的函数是 1.02x_1+1.04x_2 ,这比 x_1+x_2 要大,因为在损失函数中指定预测少了的损失更大,因此模型偏向预测多一点。
3: 神经网络优化算法
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:
#参数优化
...
#迭代的更新参数
for i in range(STEPS):
#准备batch_size来训练数据,一般将所有训练数据随机打乱之后再选取可以得到
#更好的优化结果
current_X, current_Y = ...
sess.run(train_step, feed_dict={x:current_X, y: current_Y})
4: 神经网络的进一步优化
4.1 学习率的设置
TensorFlow提供了指数衰减学习率:tf.train.exponential_decay函数。exponential_decay函数会指数级的减小学习率,它实现了以下代码的功能:
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
其中decayed_learning_rate为每一轮优化时使用的学习率,learning_rate为事先设定的初始学习率,decay_rate为衰减指数,decay_steps为衰减速度,如下图,随着迭代次数的增加,学习率逐渐降低的过程。
tf.train.exponential_decay函数可以通过设置参数staircase选择不同的衰减方式。staircase默认是False,学习率如灰色所示。当staircase为True时,global_step / decay_steps会转化为整数,这使得学习率成为一个阶梯函数(staircase function)。decay_steps通常代表了完整的使用一遍训练数据所需要的迭代论轮数。这个迭代轮数是总训练样本数除以每一个batch中的训练样本数。这种设置使用场景是每完整的过完一遍训练数据,学习率就减少一次。这可以是的训练数据集中的所有数据对模型有相等的作用。
tf.train.exponential_decay(learning_rate, global_step, decay_steps, decay_rate, staircase=True/False)
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_rate = tf.train.GradientDescentOptimizer(learning_rate).minimize(...myloss..., global_step=global_step)
4.2 过拟合问题
为了避免过拟合问题,我们常用的方法是正则化(regularization)。正则化思想是在损失函数中加入刻画模型复杂程度的指标。假设模型的损失函数是 J(\theta) ,那么我们优化 J(\theta)+\lambda R(w) ,其中 R(w) 刻画的是模型的复杂度。
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 \right |^2\\
L1 正则化会让参数变得稀疏,而 L2 正则化不会。所谓参数变得稀疏是指会有更多的参数变为0,这样可以达到类似特征选取的功能。之所以 L2 正则化不会让参数变得稀疏的原因是当参数很小时,比如0.001,这个参数的平方基本上就可以忽略了,于是模型基本上不会进一步将这个参调整为零。 L1 正则化和 L2 正则化也可同时使用:
R(w)=\sum_i\alpha\left |w_i \right |+(1-\alpha)w_i^2\\
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)
#当网络复杂时,需要用到collection
import tensorflow as tf
#获取一层神经网络边上的权重,并将这个权重的L2正则化损失加入名称为'losses'的集合中
def get_weight(shape, lambda_):
#生成一个变量
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(lambda_)(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, 10, 10, 1]
#神经网络的层数
n_layers = len(layer_dimension)
#这个变量维护前向传播时最深层次的节点,开始的时候就是输入层
current_layer = x
#当前的节点
in_dimension = layer_dimension[0]
#通过一个循环来生成5层全连接的神经网络结构
for i in range(1, n_layers):
# layers_dimension[0]为下一层的节点数
out_dimension = layer_dimension[i]
#生成当前层中权重的变量,并将这个变量的L2正则化损失加入这个计算图上的集合
weight = get_variable([in_dimension, out_dimension], 0.001)
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正则化损失加入了图上的集合
#这里只需计算刻画模型在训练数据上表现的损失函数
mes_loss = tf.reduce_mean(tf.square(y_ - cur_layer))
#将均方误差损失函数加入损失集合
tf.add_collection('losses', mse_loss)
#get_collection返回一个列表,这个列表是所有这个集合中的元素,
#这些元素是损失函数的不同部分,将他们加起来就可以得到最终的损失函数
loss = tf.add_n(tf.get_collection('losses'))
4.3 滑动平均模型
在采用随机梯度下降算法训练神经网络的同时,使用滑动平均模型在很多应用中都可以在一定程度提高模型在测试数据集上的表现。在TensorFlow中提供了tf.train.ExponentialMovingAverage来实现滑动平均模型。在初始化ExponentialMovingAverage时需要提供一个衰减率(decay)。这个衰减率将用于控制模型更新的速度。ExponentialMovingAverage对每个变量维护一个影子变量(shadow variable),这个影子变量的初始值就是相应变量的初始值,而每次运行时,影子变量的值会更新为:
shadow_variable = decay\times shadow_variable + (1-decay)\times variable\\
其中shadow_variable为影子变量,variable为待更新的变量,decay为衰减率。decay决定了模型更新的速度,decay越大,模型越稳定。在实际中,decay一般会设成非常接近于1的数(比如0.999或0.9999)。为了使模型在训练前期可以更新的更快,ExponentialMovingAverage提供了num_updates参数来动态设置decay的大小。如果在ExponentialMovingAverage初始化时提供了num_updates参数,那么每次使用的衰减率将是:
min\{decay,\frac{1+num_updates}{10+num_updates}\}\\
我们假设 \{a_1,a_2,\cdots,a_n\} ,其衰减率为decay,对应的影子变量是 \{m_1,m_2,\cdots,m_n\} ,则:
m_n=decay*m_{n-1}+(1-decay)*a_n\\
我们展开来:
a_1=m_1\\
m_2=decay*m_1+(1-decay)*a_2=decay*a_1+(1-decay)*a_2\\
m_3=decay*m_{2}+(1-decay)*a_3=decay^2*a_1+decay(1-decay)a_2+(1-decay)a_3\\ m_4=decay^3*a_1+decay^2(1-decay)a_2+decay(1-decay)a_3+(1-decay)a_4\\
依次递推:
m_n=decay^{n-1}*a_1+decay^{n-2}(1-decay)a_2+decay^{n-3}(1-decay)a_3+\cdots+(1-decay)a_n\\
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:
# 初始化
init_op = tf.global_variables_initializer()
sess.run(init_op)
#通过ema.average(v1)获取滑动平均之后变量的取值,
#在初始化之后的变量v1的值和v1的滑动平均都为零
print(sess.run([v1, ema.average(v1)]))
# 更新变量v1的取值
sess.run(tf.assign(v1, 5))
sess.run(maintain_averages_op)
print(sess.run([v1, ema.average(v1)]))
# 更新step和v1的取值
sess.run(tf.assign(step, 10000))
sess.run(tf.assign(v1, 10))
sess.run(maintain_averages_op)
print(sess.run([v1, ema.average(v1)]))
# 更新一次v1的滑动平均值
sess.run(maintain_averages_op)
print(sess.run([v1, ema.average(v1)]))
#输出
[0.0, 0.0]
[5.0, 4.5]
[10.0, 4.555]
[10.0, 4.60945]