前言

上篇笔记我们利用MNIST数据集训练了一个手写数字识别的模型,但是准确率非常的低,维持在91%左右,我们可以尝试着将准确率提高到96%以上,在实验之前我们需要先了解一些基本的概念,本篇文章可能会有些枯燥,因为大多都是理论知识。

本文重点

  1. 激活函数
  2. 代价函数
  3. 拟合

什么是激活函数?激活函数是干嘛的?

想了解什么是激活函数,就要先了解神经网络的基本模型,下图所示为一单一人工神经网络的基本模型图:

单一人工神经网络的基本模型图

神经网络中的每个神经元节点接受上一层神经元的输出值作为本神经元的输入值,并将输入值传递给下一层,输入层神经元节点会将输入属性值直接传递给下一层(隐藏层或输出层)。在多层神经网络中,上层节点的输出和下层节点的输入之间具有一个函数关系,这个函数称为激活函数(又称激励函数)。

如果我们不运用激活函数的话,则输出信号将仅仅是一个简单的线性函数。线性函数一个一级多项式。现如今,线性方程是很容易解决的,但是它们的复杂性有限,并且从数据中学习复杂函数映射的能力更小。一个没有激活函数的神经网络将只不过是一个线性回归模型(Linear regression Model)罢了,它功率有限,并且大多数情况下执行得并不好。我们希望我们的神经网络不仅仅可以学习和计算线性函数,而且还要比这复杂得多。同样是因为没有激活函数,我们的神经网络将无法学习和模拟其他复杂类型的数据,例如图像、视频、音频、语音等。这就是为什么我们要使用人工神经网络技术,诸如深度学习(Deep learning),来理解一些复杂的事情,一些相互之间具有很多隐藏层的非线性问题,而这也可以帮助我们了解复杂的数据。

那么为什么我们需要非线性函数?

非线性函数是那些一级以上的函数,而且当绘制非线性函数时它们具有曲率。现在我们需要一个可以学习和表示几乎任何东西的神经网络模型,以及可以将输入映射到输出的任意复杂函数。神经网络被认为是通用函数近似器(Universal Function Approximators)。这意味着他们可以计算和学习任何函数。几乎我们可以想到的任何过程都可以表示为神经网络中的函数计算。

而这一切都归结于这一点,我们需要应用激活函数f(x),以便使网络更加强大,增加它的能力,使它可以学习复杂的事物,复杂的表单数据,以及表示输入输出之间非线性的复杂的任意函数映射。因此,使用非线性激活函数,我们便能够从输入输出之间生成非线性映射。

激活函数的另一个重要特征是:它应该是可以区分的。我们需要这样做,以便在网络中向后推进以计算相对于权重的误差(丢失)梯度时执行反向优化策略,然后相应地使用梯度下降或任何其他优化技术优化权重以减少误差。

二次代价函数

二次代价函数的公式如下: $$C=\frac{1}{2n}\sum_{x}^{ }\left | y(x)-{a}^L(x) \right |^2$$

其中,C表示代价,x表示样本,y表示实际值,a表示输出值,n表示样本的总数。为简单起见,以一个样本为例进行说明,此时二次代价函数为: $$C=\frac{(y-a)^2}{2}$$ 其中$a=\delta (z),z=\sum W_j*X_j+b$,$\delta (z)$是激活函数

加入我们使用梯度下降法来调整权值参数的大小,权值w和偏置b的梯度推导如下: $$\frac{\partial C}{\partial w}=(a-y)\sigma'(z)x$$ $$\frac{\partial C}{\partial b}=(a-y)\sigma'(z)$$ 其中,z表示神经元的输入,$\sigma $表示激活函数。从以上公式可以看出,w和b的梯度跟激活函数的梯度成正比,激活函数的梯度越大,$w$和$b$的大小调整得越快,训练收敛得就越快。而神经网络常用的激活函数为sigmoid函数,该函数的曲线如下所示:

所以在这种情况下,权值和偏置的变化就会出现如下异常:

假设我们目标是收敛到 1。A 点为 0.82 离目标比较远,梯度比较大,权值调整比较大。B 点为 0.98 离目标比较近,梯度比较小,权值调整比较小。调整方案合理。 假如我们目标是收敛到 0. A 点为 0.82 离目标比较近,梯度比较大,权值调整比较大。B 点为 0.98 离目标比较远,梯度比较小,权值调整比较小。调整方案不合理。

那么可能有人就会说,如果我们想要解决上述问题,选择一个梯度不变化或变化不明显的激活函数不就解决问题了吗?图样图森破,那样虽然简单粗暴地解决了这个问题,但可能会引起其他更多更麻烦的问题。而且,类似sigmoid这样的函数(比如tanh函数)有很多优点,非常适合用来做激活函数,具体请自行google之。

在这里我们不改变激活函数,选择将代价函数改为交叉熵代价函数。

交叉熵代价函数

先放公式:$$C=-\frac{1}{n}\sum_{x}^{ }[ylna+(1-y)ln(1-a)]$$ 其中,C表示代价,x表示样本,y表示实际值,a表示输出值,n表示样本的总数。那么,重新计算参数w的梯度:

其中:$${\sigma }'(z)=\sigma (z)(1-\sigma (z))$$ 因此,w的梯度公式中原来的${\sigma }'(z)$被消掉了;另外,该梯度公式中的$\sigma (z)-y$表示输出值与实际值之间的误差。所以,当误差越大,梯度就越大,参数w调整得越快,训练速度也就越快。同理可得,b的梯度为: $$\frac{\partial C}{\partial b}=\frac{1}{n}\sum_{x}^{ }(\sigma (z)-y)$$ 实际情况证明,交叉熵代价函数带来的训练效果往往比二次代价函数要好。

  • 权值和偏置值的调整与${\sigma }'(z)$无关,另外,梯度公式中的$\sigma (z)-y$表示输出值与实际值的误差。所以当误差越大时,梯度就越大,参数 w 和 b 的调整就越快,训练的速度也就越快。
  • 如果输出神经元是线性的,那么二次代价函数就是一种合适的选择。如果输出神经元是 S 型函数,那么比较适合用交叉熵代价函数。

对数释然代价函数(log-likelihood cost)

  • 对数释然函数常用来作为softmax回归的代价函数,然后输出层神经元是sigmoid函数,可以采用交叉熵代价函数。而深度学习中更普遍的做法是将softmax作为最后一层,此时常用的代价函数是对数释然代价函数。
  • 对数似然代价函数与softmax的组合和交叉熵与sigmoid函数的组合非常相似。对数释然代价函数在二分类时可以化简为交叉熵代价函数的形式。 在tensorflow中用:
tf.nn.sigmoid_cross_entropy_with_logits()来表示跟sigmoid搭配使用的交叉熵。
tf.nn.softmax_cross_entropy_with_logits()来表示跟softmax搭配使用的交叉熵。

使用TensorFlow比较两种代价函数的效果

以上一篇文章手写数字识别的模型为例子,在这给出采用交叉熵函数的模型的代码:

import datetime

# 4.1 交叉熵代价函数
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

start = datetime.datetime.now()

# 载入数据
mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
# 每个批次的大小
batch_size = 50
# 计算一共有多少个批次
n_batch = mnist.train.num_examples // batch_size

# 定义两个placeholder
x = tf.placeholder(tf.float32, [None, 784])
y = tf.placeholder(tf.float32, [None, 10])

# 创建一个简单的神经网络
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
prediction = tf.nn.softmax(tf.matmul(x, W)+b)

# 二次代价函数
# loss = tf.reduce_mean(tf.square(y-prediction))
# 交叉熵代价函数
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(
    labels=y, logits=prediction))
# 使用梯度下降法
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

# 初始化变量
init = tf.global_variables_initializer()

# 结果存放在一个布尔型列表中
# argmax返回一维张量中最大的值所在的位置
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))
# 求准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

with tf.Session() as sess:
    sess.run(init)
    for epoch in range(30):
        for batch in range(n_batch):
            batch_xs, batch_ys = mnist.train.next_batch(batch_size)
            sess.run(train_step, feed_dict={x: batch_xs, y: batch_ys})
        acc = sess.run(accuracy, feed_dict={
                       x: mnist.test.images, y: mnist.test.labels})
        print("Iter "+str(epoch)+",Testing Accuracy "+str(acc))

end = datetime.datetime.now()
print((end-start).seconds)

在这里我们将二次代价函数更改为了交叉熵代价函数:

# 二次代价函数
# loss = tf.reduce_mean(tf.square(y-prediction))
# 交叉熵代价函数
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(
    labels=y, logits=prediction))

接下来我们来对比下训练的结果: 使用二次代价函数的训练结果 使用交叉熵代价函数的训练结果

由上图可知,使用二次代价函数训练第10次的精确度为0.9063,而使用交叉熵代价函数训练到第2次的精确度就已经超过0.9了,结果显而易见。

拟合

拟合分为三种:1.欠拟合(underfitting);2. 正确拟合(just right);3. 过拟合(overfitting);如下图所示: 拟合 其中每个x表示的是样本,每条曲线代表的是模型。 下图是分类问题中的拟合情况,和上述情况类似。 拟合

在这里介绍过拟合,下面是wikipedia对于overfitting的解释。 在统计学和机器学习中,overfitting一般在描述统计学模型随机误差或噪音时用到。它通常发生在模型过于复杂的情况下,如参数过多等。overfitting会使得模型的预测性能变弱,并且增加数据的波动性。

发生overfitting是因为评判训练模型的标准不适用于作为评判该模型好坏的标准,模型通常会增强模型在训练模型的预测性能。但是模型的性能并不是由模型在训练集的表现好坏而决定,它是由模型在未知数据集上的表现确定的。当模型开始“memorize”训练数据而不是从训练数据中“learning”时,overfitting就出现了。比如,如果模型的parameters大于或等于观测值的个数,这种模型会显得过于简单,虽然模型在训练时的效果可以表现的很完美,基本上记住了数据的全部特点,但这种模型在未知数据的表现能力会大减折扣,因为简单的模型泛化能力通常都是很弱的。

上面这个图,是通过线性函数和多项式函数来拟合这些数据点,显然多项式函数拟合效果很完美,包含了所有的点,而线性函数丢失了大部分点。但实际上,线性函数有一个很好的泛化能力,如果用这些点来做一个回归线,多项式函数过拟合的情况更糟糕。

过拟合不仅和参数的个数以及数据有关,也和数据形状模型结构的一致性有关。

为了避免过拟合,有必要使用一些额外的技术(如交叉验证、正则化、early stopping、贝斯信息量准则、赤池信息量准则或model comparison),以指出何时会有更多训练而没有导致更好的一般化。

Overfitting的概念在机器学习中很重要。通常一个学习算法是借由训练样本来训练的,在训练时会伴随着训练误差。当把该模型用到未知数据的测试时,就会相应的带来一个validation error。下面通过训练误差和验证误差来详细分析一下overfitting。如下图:

在上图总,蓝色表示训练误差training error,红色表示validation error。当训练误差达到中间的那条垂直线的点时,模型应该是最优的,如果继续减少模型的训练误差,这时就会发生过拟合。

其实你可以这样来理解overfitting:数据集中信息分为两部分,一部分是和预测未来数据有关的数据,另一部分是无关的,两者地位是平等的。用来作为预测的评判标准越不精确,表明噪声数据就越多,需要忽略掉的数据也就越多,而关键就是究竟那一部分应该忽略掉。所以我们把一个学习算法对噪声的削减能力就叫做它的鲁棒性。我们需要的就是鲁棒性很强的学习算法

举一个简单的例子,一个零售购物的数据库包括购买项、购买人、日期、和购买时间。根据这个数据可以很容易的建立一个模型,并且在训练集上的拟合效果也会很好,通过使用日期、购买时间来预测其它属性列的值,但是这个模型对于新数据的泛化能力很弱,因为这些过去的数据不会再次发生。

防止过拟合的几种方式

这里推荐阅读机器学习中用来防止过拟合的方法有哪些?,说的比较详细。

  1. 增加数据集 你的模型可以存储很多很多的信息,这意味着你输入模型的训练数据越多,模型就越不可能发生过拟合。原因是随着你添加更多数据,模型会无法过拟合所有的数据样本,被迫产生泛化以取得进步。 收集更多的数据样本应该是所有数据科学任务的第一步,数据越多会让模型的准确率更高,这样也就能降低发生过拟合的概率。

2. 正则化方法 $C=C_0+\frac{\lambda }{2n}\sum_{w}^{ }w^2$ 正则化是指约束模型的学习以减少过拟合的过程。它可以有多种形式,推荐阅读机器学习中用来防止过拟合的方法有哪些?,说的比较详细。 3. Dropout 由于深度学习依赖神经网络处理从一个层到下一个层的信息,因而从这两方面着手比较有效。其理念就是在训练中随机让神经元无效(即dropout)或让网络中的连接无效(即dropconnect)。 droppout

这样就让神经网络变得冗长和重复,因为它无法再依赖具体的神经元或连接来提取具体的特征。等完成模型训练后,所有的神经元和连接会被保存下来。试验显示这种方法能起到和神经网络集成方法一样的效果,可以帮助模型泛化,这样就能减少过拟合的问题。

我们来用代码体验下dropout:

import datetime
# 4.2 Dropout
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

start = datetime.datetime.now()

# 载入数据
mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
# 每个批次的大小
batch_size = 50
# 计算一共有多少个批次
n_batch = mnist.train.num_examples // batch_size

# 定义两个placeholder
x = tf.placeholder(tf.float32, [None, 784])
y = tf.placeholder(tf.float32, [None, 10])
keep_prob = tf.placeholder(tf.float32)

# 创建一个神经网络
W1 = tf.Variable(tf.truncated_normal([784, 2000], stddev=0.1))
b1 = tf.Variable(tf.zeros([2000])+0.1)
L1 = tf.nn.tanh(tf.matmul(x, W1)+b1)
L1_drop = tf.nn.dropout(L1, keep_prob)

W2 = tf.Variable(tf.truncated_normal([2000, 2000], stddev=0.1))
b2 = tf.Variable(tf.zeros([2000])+0.1)
L2 = tf.nn.tanh(tf.matmul(L1_drop, W2)+b2)
L2_drop = tf.nn.dropout(L2, keep_prob)

W3 = tf.Variable(tf.truncated_normal([2000, 1000], stddev=0.1))
b3 = tf.Variable(tf.zeros([1000])+0.1)
L3 = tf.nn.tanh(tf.matmul(L2_drop, W3)+b3)
L3_drop = tf.nn.dropout(L3, keep_prob)

W4 = tf.Variable(tf.truncated_normal([1000, 10], stddev=0.1))
b4 = tf.Variable(tf.zeros([10])+0.1)

prediction = tf.nn.softmax(tf.matmul(L3_drop, W4)+b4)

# 二次代价函数
# loss = tf.reduce_mean(tf.square(y-prediction))
# 交叉熵代价函数
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(
    labels=y, logits=prediction))
# 使用梯度下降法
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

# 初始化变量
init = tf.global_variables_initializer()

# 结果存放在一个布尔型列表中
# argmax返回一维张量中最大的值所在的位置
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))
# 求准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

with tf.Session() as sess:
    sess.run(init)
    for epoch in range(20):
        for batch in range(n_batch):
            batch_xs, batch_ys = mnist.train.next_batch(batch_size)
            sess.run(train_step, feed_dict={
                     x: batch_xs, y: batch_ys, keep_prob: 1.0})

        test_acc = sess.run(accuracy, feed_dict={
            x: mnist.test.images, y: mnist.test.labels, keep_prob: 1.0})

        train_acc = sess.run(accuracy, feed_dict={
            x: mnist.train.images, y: mnist.train.labels, keep_prob: 1.0})

        print("Iter "+str(epoch)+",Testing Accuracy " +
              str(test_acc)+",Train Accuracy"+str(train_acc))

end = datetime.datetime.now()
print((end-start).seconds)

相较于之前的代码我们更改了以下一些地方:

# 创建一个神经网络
W1 = tf.Variable(tf.truncated_normal([784, 2000], stddev=0.1))
b1 = tf.Variable(tf.zeros([2000])+0.1)
L1 = tf.nn.tanh(tf.matmul(x, W1)+b1)
L1_drop = tf.nn.dropout(L1, keep_prob)

W2 = tf.Variable(tf.truncated_normal([2000, 2000], stddev=0.1))
b2 = tf.Variable(tf.zeros([2000])+0.1)
L2 = tf.nn.tanh(tf.matmul(L1_drop, W2)+b2)
L2_drop = tf.nn.dropout(L2, keep_prob)

W3 = tf.Variable(tf.truncated_normal([2000, 1000], stddev=0.1))
b3 = tf.Variable(tf.zeros([1000])+0.1)
L3 = tf.nn.tanh(tf.matmul(L2_drop, W3)+b3)
L3_drop = tf.nn.dropout(L3, keep_prob)

W4 = tf.Variable(tf.truncated_normal([1000, 10], stddev=0.1))
b4 = tf.Variable(tf.zeros([10])+0.1)

prediction = tf.nn.softmax(tf.matmul(L3_drop, W4)+b4)

我额外的为神经网络添加了两个隐藏层,为了方便体现出差异,我将每个隐藏层的神经元数量设置的比较多。 然后在训练过程中,

for epoch in range(10):
        for batch in range(n_batch):
            batch_xs, batch_ys = mnist.train.next_batch(batch_size)
            sess.run(train_step, feed_dict={
                     x: batch_xs, y: batch_ys, keep_prob: 1.0})

        test_acc = sess.run(accuracy, feed_dict={
            x: mnist.test.images, y: mnist.test.labels, keep_prob: 1.0})

        train_acc = sess.run(accuracy, feed_dict={
            x: mnist.train.images, y: mnist.train.labels, keep_prob: 1.0})

        print("Iter "+str(epoch)+",Testing Accuracy " +
              str(test_acc)+",Train Accuracy"+str(train_acc))

其中keep_prob表示启用神经元占神经元总数的百分比(1.0表示全部使用),train_acc表示用训练样本来测试训练出来的模型的精确度,test_acc表示用测试样本来测试训练出来的模型的精确度,用这两个数据来反映出拟合程度。训练结果如下图所示: 在这里我们总共就训练了10次,而且数据量并不大,此时test_acc和train_acc就已经差了两个百分点,如果应用到其他项目中,数据量变大之后就不是2个百分点的事情了,所以说如果神经元数量过多是会造成过度拟合的。

总结

在本文中为了提高精确度,引入了代价函数这个概念,为了更好的理解代价函数因此提前介绍了什么是激活函数以及为什么需要激活函数。在只用一层神经网络的时候通过更改代价函数,我们可以使精确度达到93%左右(训练次数较多时),但这还是不够,所以我们尝试多添加几层神经元,但是这时候就会出现“过拟合”这个新的问题了,通常有三种方式解决过拟合的问题。之后的文章将会在此基础上介绍以下优化器,并且告知大家如何使用谷歌免费的GPU服务加速深度学习的模型训练。