1 深度神经网络
深度学习具有两个重要的性质:非线性和多层变换。
1.1 非线性操作 —— 激活函数
1.1.1 线性模型
线性模型可以表示为:
y=∑(wi⋅xi+bi)(12)(12)y=∑(wi⋅xi+bi)线性模型的最大特点是任意线性模型的组合仍然还是线性模型。如果没有激活函数,多层神经网络就相当于多个矩阵的直接相乘。线性模型能够解决的问题是有限的。这是线性模型最大的局限,这也是为什么深度学习要强调非线性。
1.2 激活函数
因为上面非线性模型的缺点,线性模型只能解决线性可分问题。而对于复杂问题 ——至少是无法通过直线或者高维空间的平面划分的问题,线性模型通常都无法解决。在现实世界中,绝大部门的问题都是无法线性分割的。通过激活函数完成非线性操作。
TensorFlow 提供了 7 种不同的非线性激活函数。集中常见的激活函数:tf.nn.relu, tf.sigmoid, tf.tanh。
1.3 多层网络解决异或运算
在神经网络的发展史上,一个很重要的问题就是异或问题。
神经网络的理论模型由 Warren McCulloch 和 Walter Pitts 在 1943 年首次提出,并在 1958 年由 Frank Rosenblatt 提出了感知机(Perceptron)模型,从数学上完成了对神经网络的精确建模。感知机可以简单地理解为单层的神经网络。然而,1969年,Marvin Minsky 和 Seymour Papert 在《Perceptrons: An Introduction to Computational Gemetry》一书中提出感知机是无法模拟异或运算的。
加入隐藏层之后,异或问题就可以很好地解决。
此外,根据 TensorFlow 游乐场实验的例子,可以看出 深层神经网络实际上有组合特征提取的功能。
1.4 损失函数
神经网络模型的效果以及优化目标都是通过损失函数来完成的。损失函数刻画了当前的预测值和真实答案之间的差距。根据这个差距,通过反向传播机制来调整神经网络的取值,使得差距可以被缩小,也就达到了学习的目的。
1.4.1 两种经典的损失函数
分类问题和回归问题是监督学习的两大种类。
1.4.1.1 分类问题损失函数
分类问题中,可以分为二分类问题和多分类问题。逻辑回归(Logistic Regression)是一种非常常用的求解二分类问题的算法。二分类问题通常使用 0.5 作为阈值。然而这种做法却很难推广到多分类问题中,虽然理论上可以通过设置多个阈值解决,但通常都不会这么处理。
多分类问题,最常用的方式是设置 n 个输出节点,n 表示类别的个数。每个样例,神经网络都会输出一个 n 维数组。其中只有一个维度是1,其余都是0。
对于分类问题,通常使用交叉熵作为损失函数。形式如下:
通常情况下会使用:
注意 交叉熵刻画的是两个概率分布之间的距离,然而神经网络的输出却不一定是一个概率分布。所以问题是:如何将神经网络前向传播得到的结果变成概率分布呢?Softmax 回归就是一个非常常用的方法。
Softmax 回归本身可以作为一个学习算法来优化分类结果,但在 TensorFlow 中,Softmax 回归的参数被去掉了,它只是一层额外的处理层,将神经网络的输出变成一个概率分布。经过 Softmax 回归处理之后的输出为:
注意交叉熵是不对称的,即 H(p, q) <> H(q, p). 交叉熵刻画的是两个概率分布的距离,也就是说交叉熵值越小,两个概率分布越接近。
交叉熵的实现:
# tf.clip_by_value 是一种防止 y 过大或过小的函数,相当于 max(min(y, 1.0), 1e-10)
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
交叉熵一般会与 Softmax 回归一起使用,所以 TensorFlow 对这两个功能进行了封装,提供了 tf.nn.softmax_cross_entropy_with_logits 函数。使用方式:
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(y, y_)
在只有一个正确答案的分类问题中,TensorFlow 提供了 tf.nn.sparse_softmax_cross_entropy_with_logits 函数来进一步加速计算过程。
1.4.1.2 回归问题损失函数
对于回归问题,通常使用 MSE 作为损失函数。形式如下:
实现方式:
mse = tf.reduce_mean(tf.square(y_ - y))
1.5 神经网络优化算法
本小节主要讲述反向传播(backpropagation)和梯度下降算法(gradient decent)。梯度下降算法主要用于优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法。
1.5.1 反向传播
通过参数的梯度和学习率,参数更新的公式为:
1.5.2 梯度下降
梯度下降算法的第一步需要随机产生一个参数 x 的初始值,然后在没通过梯度和学习率来更新参数 x 的取值。
神经网络的优化过程可以分为两个阶段,第一个阶段先通过前向传播算法计算得到预测值,并将预测值和真实值做对比得出两者之间的差距。然后在第二个阶段通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。
注意,梯度下降算法有两个局限:
第一,梯度下降算法并不能保证被优化的函数达到全局最优解。
第二,计算时间太长。因为要在全部训练数据上最小化损失,所以损失函数是在所有训练数据上的损失和。这样每一轮迭代都需要计算在全部训练数据上的损失函数。解决的办法是使用随机梯度下降算法(stochastic gradient descent, SGD)。
SGD 并不是 在全部训练数据上的损失函数,而是在每一轮迭代中,随机优化某一条训练数据上的损失函数。因为只是优化某一条数据上的损失函数,所以问题也非常明显:在某一条数据上损失函数更小并不代表在全部数据上损失函数更小,于是使用随机梯度下降优化得到的神经网络甚至可能无法达到局部最优。
基于梯度下降和随机梯度下降算法的优缺点,在实际引用中,一般采用这两个算法的折中——每次计算一小部分训练数据的损失函数。这一小部分数据称为一个 batch。
1.5.3 学习率
学习率(learning rate)决定了参数每次更新的幅度。如果幅度过大,那么可能导致参数在极优值的两侧来回移动。
TensorFlow 提供了指数衰减法,tf.train.exponential_decay, 可以动态更新学习率。通过这个函数,可以先使用较大的学习率快速得到一个比较优的解,然后随着迭代的继续逐步减小学习率。实现代码:
# decay_rate 为衰减系数,decay_steps 为衰减速度
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
tf.train.exponential_decay 函数可以通过设置参数 staircase 选择不同的衰减方式。staircase 的默认值为 False,global_step / decay_steps 表示的是数学中的除,不是整除,称这种方式为连续衰减学习率。当为 TRUE 时,global_step / decay_steps 会转化成整除,称之为阶梯状衰减学习率。
TRAINING_STEPS = 100
global_step = tf.Variable(0)
LEARNING_RATE = tf.train.exponential_decay(0.1, global_step, 1, 0.96, staircase=True)
x = tf.Variable(tf.constant(5, dtype=tf.float32), name="x")
y = tf.square(x)
train_op = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(y, global_step=global_step)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(TRAINING_STEPS):
sess.run(train_op)
if i % 10 == 0:
LEARNING_RATE_value = sess.run(LEARNING_RATE)
x_value = sess.run(x)
print("After %s iteration(s): x%s is %f, learning rate is %f."% (i+1, i+1, x_value, LEARNING_RATE_value))
1.5.4 正则化
为了解决过拟合问题,一个非常常用的方法是正则化(Regularization)。
正则化的思想是在损失函数中加入刻画模型复杂程度的指标。
此时优化的损失函数为:
其中,R(w) 刻画的是模型的复杂程度,而λλ表示模型复杂损失在总损失中的比例。θθ 表示的是一个神经网络中的所有参数,包括边上的权重 w 和偏置项 b。
1.5.4.1 L1 正则化
计算公式为:
TensorFlow 实现函数:tf.contrib.layers.l1_regularizer.
1.5.4.2 L2 正则化
计算公式为:
TensorFlow 实现函数:tf.contrib.layers.l2_regularizer.
w = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w)
# lambda 参数表示了正则化项的权重,也就是 \lambda
loss = tf.reduce_mean(tf.square(y_ - y)) + tf.contrib.layers.l2_regularizer(lambda)(w)
1.5.4.3 L1 与 L2 的区别
无论是哪一种正则化方式,基本的思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪音。但是两者的方法有很大区别。
第一,L1 正则化会让参数变得更稀疏,而 L2正则化不会。所谓参数变得更稀疏是指会有更多的参数变为 0,这样可以达到类似特征选取的功能。L2之所以不会让参数变得稀疏的原因是当参数很小时,比如 0.001,这个参数的平方基本上就可以忽略了,于是模型不会进一步将在这参数调整为 0。
第二,L1 正则化的计算公式不可导,而 L2 正则化公式可导。因为在优化时需要计算损失函数的偏导数,所以对含有 L2 正则化损失函数的优化更加简洁。优化带 L1正则化的损失函数要更加复杂,而且优化方法也有很多种。
1.5.4.4 L1 与 L2 结合使用
在实践中,有时将 L1 与 L2 结合使用。公式为:
1.6 滑动平均模型
滑动平均模型对每一个变量会维护一个影子变量,每个变量不再是原来的值,而是动态更新的值,每个值动态更新的策略和学习率的更新方式非常相似,只是滑动平均模型更新的是针对所有可训练的变量。影子变量的值会更新为:
shadow_variable = decay * shadow_variable + (1 - decay) * variable
Shadow_variable 为影子变量,variable 为待更新的变量,decay 为衰减率。decay 决定了模型更新的速度,decay 越大模型越趋于稳定。在实际引用中,decay 一般会设置为非常接近 1 的数,比如 0.999 或 0.9999.
TensorFlow 提供了 tf.train.ExponentialMovingAverage 来实现滑动平均模型,这个类还提供了 num_updates 参数来动态设置 decay 的大小,此时每次使用的衰减率将是:
1.7 一个重要的例子
import tensorflow as tf
# 定义变量及滑动平均类
v1 = tf.Variable(0, dtype=tf.float32)
step = tf.Variable(0, trainable=False)
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)
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)]))
1.8 数据集划分
机器学习的很多算法(不包括深度学习算法)的数据集都划分为训练数据和测试数据两种,而深度学习算法中,数据集通常划分为训练数据、验证数据和测试数据。原因如下:
神经网络通常含有非常多的参数,比如学习率、学习率衰减率、隐层层数、隐层结点数等,配置这些参数都需要在训练过程中根据实验结果动态进行调整。但是在训练过程中,却不能拿测试数据去验证,因为使用测试数据进行验证就相当于让神经网络拟合了测试数据,等于是拿测试数据的效果好坏去调整神经网络的参数,这就失去了测试数据作为评测实验结果好坏的意义了。使用验证数据作为训练时的评测标准,以此来评价当时神经网络的好坏,再调整神经网络。在测试时,使用测试数据检验神经网络在未知的测试数据上的效果。这样才能保证模型的效果是接近真实的使用场景。
在传统的统计机器学习算法中,常常会使用交叉验证(Cross Validation)的方式来验证模型效果,但是因为深度学习的训练通常都会非常耗时,使用这种方式并不可取。海量数据下,一般采用验证数据集的形式来评测模型的效果。