1. 程式人生 > >TensorFlow筆記(4)——優化手寫數字識別模型之代價函式和擬合

TensorFlow筆記(4)——優化手寫數字識別模型之代價函式和擬合

前言

上篇筆記我們利用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的梯度跟啟用函式的梯度成正比,啟用函式的梯度越大,wb的大小調整得越快,訓練收斂得就越快。而神經網路常用的啟用函式為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服務加速深度學習的模型訓練。