前面的14 交叉熵損失函數(shù)——防止學(xué)習(xí)緩慢和15 重新思考神經(jīng)網(wǎng)絡(luò)初始化從學(xué)習(xí)緩慢問題入手,嘗試改進神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)。本篇討論過擬合問題,并引入與之相對的L2正則化(Regularization)方法。

無處不在的過擬合
模型對于已知數(shù)據(jù)的描述適應(yīng)性過高,導(dǎo)致對新數(shù)據(jù)的泛化能力不佳,我們稱模型對于數(shù)據(jù)過擬合(overfitting)。
過擬合無處不在。
羅素的火雞對自己的末日始料未及,曾真理般存在的牛頓力學(xué)淪為狹義相對論在低速情況下的近似,次貸危機破滅了美國買房只漲不跌的神話,血戰(zhàn)鋼鋸嶺的醫(yī)療兵Desmond也并不是懦夫。
凡是基于經(jīng)驗的學(xué)習(xí),都存在過擬合的風(fēng)險。動物、人、機器都不能幸免。

誰存在過擬合?
對于一些離散的二維空間中的樣本點,下面兩條曲線誰存在過擬合?

遵循奧卡姆剃刀的一派,主張“如無必要,勿增實體”。他們相信相對簡單的模型泛化能力更好:上圖中的藍色直線,雖然只有很少的樣本點直接落在它上面,但是不妨認為這些樣本點或多或少包含一些噪聲。基于這種認知,可以預(yù)測新樣本也會在這條直線附近出現(xiàn)。
或許很多時候,傾向簡單會占上風(fēng),但是真實世界的復(fù)雜性深不可測。雖然在自然科學(xué)中,奧卡姆剃刀被作為啟發(fā)性技巧來使用,幫助科學(xué)家發(fā)展理論模型工具,但是它并沒有被當做邏輯上不可辯駁的定理或者科學(xué)結(jié)論??傆泻唵文P捅磉_不了,只能通過復(fù)雜模型來描述的事物存在。很有可能紅色的曲線才是對客觀世界的真實反映。
康德為了對抗奧卡姆剃刀產(chǎn)生的影響,創(chuàng)建了他自己的反剃刀:“存在的多樣性不應(yīng)被粗暴地忽視”。
阿爾伯特·愛因斯坦告誡:“科學(xué)理論應(yīng)該盡可能簡單,但不能過于簡單。”
所以僅從上圖來判斷,一個理性的回答是:不知道。即使是如此簡單的二維空間情況下,在沒有更多的新樣本數(shù)據(jù)做出驗證之前,不能僅通過模型形式的簡單或復(fù)雜來判定誰存在過擬合。
過擬合的判斷
二維、三維的模型,本身可以很容易的繪制出來,當新的樣本出現(xiàn)后,通過觀察即可大致判斷模型是否存在過擬合。
然而現(xiàn)實情況要復(fù)雜的多。對MNIST數(shù)字識別所采用的3層感知器——輸入層784個神經(jīng)元,隱藏層30個神經(jīng)元,輸出層10個神經(jīng)元,包含23860個參數(shù)(23860 = 784 x 30 + 30 x 10 + 30 + 10),靠繪制模型來觀察是不現(xiàn)實的。
最有效的方式是通過識別精度判斷模型是否存在過擬合:比較模型對驗證集和訓(xùn)練集的識別精度,如果驗證集識別精度大幅低于訓(xùn)練集,則可以判斷模型存在過擬合。
至于為什么是驗證集而不是測試集,請復(fù)習(xí)11 74行Python實現(xiàn)手寫體數(shù)字識別中“驗證集與超參數(shù)”一節(jié)。
然而靜態(tài)的比較已訓(xùn)練模型對兩個集合的識別精度無法回答一個問題:過擬合是什么時候發(fā)生的?
要獲得這個信息,就需要在模型訓(xùn)練過程中動態(tài)的監(jiān)測每次迭代(Epoch)后訓(xùn)練集和驗證集的識別精度,一旦出現(xiàn)訓(xùn)練集識別率繼續(xù)上升而驗證集識別率不再提高,就說明過擬合發(fā)生了。
這種方法還會帶來一個額外的收獲:確定作為超參數(shù)之一的迭代數(shù)(Epoch Number)的量級。更進一步,甚至可以不設(shè)置固定的迭代次數(shù),以過擬合為信號,一旦發(fā)生就提前停止(early stopping)訓(xùn)練,避免后續(xù)無效的迭代。
過擬合監(jiān)測
了解了過擬合的概念以及監(jiān)測方法,就可以開始分析我們訓(xùn)練MNIST數(shù)字識別模型是否存在過擬合了。
所用代碼:tf_16_mnist_loss_weight.py。它在12 TensorFlow構(gòu)建3層NN玩轉(zhuǎn)MNIST代碼的基礎(chǔ)上,使用了交叉熵損失,以及1/sqrt(nin)權(quán)重初始化:
- 1個隱藏層,包含30個神經(jīng)元;
- 學(xué)習(xí)率:3.0;
- 迭代數(shù):30次;
- mini batch:10;
訓(xùn)練過程中,分別對訓(xùn)練集和驗證集的識別精度進行了跟蹤,如下圖所示,其中紅線代表訓(xùn)練集識別率,藍線代表測試集識別率。圖中顯示,大約在第15次迭代前后,測試集的識別精度穩(wěn)定在95.5%不再提高,而訓(xùn)練集的識別精度仍然繼續(xù)上升,直到30次迭代全部結(jié)束后達到了98.5%,兩者相差3%。
由此可見,模型存在明顯的過擬合的特征。

過擬合的對策:L2正則化
對抗過擬合最有效的方法就是增加訓(xùn)練數(shù)據(jù)的完備性,但它昂貴且有限。另一種思路是減小網(wǎng)絡(luò)的規(guī)模,但它可能會因為限制了模型的表達潛力而導(dǎo)致識別精度整體下降。
本篇引入L2正則化(Regularization),可以在原有的訓(xùn)練數(shù)據(jù),以及網(wǎng)絡(luò)架構(gòu)不縮減的情況下,有效避免過擬合。L2正則化即在損失函數(shù)C的表達式上追加L2正則化項:

上式中的C0代表原損失函數(shù),可以替換成均方誤差、交叉熵等任何一種損失函數(shù)表達式。
關(guān)于L2正則化項的幾點說明:
- 求和∑是對網(wǎng)絡(luò)中的所有權(quán)重進行的;
- λ(lambda)為自定義參數(shù)(超參數(shù));
- n是訓(xùn)練樣本的數(shù)量(注意不是所有權(quán)重的數(shù)量!);
- L2正則化并沒有偏置參與;
該如何理解正則化呢?
對于使網(wǎng)絡(luò)達到最小損失的權(quán)重w,很可能有非常多不同分布的解:有的均值偏大、有的偏小,有的分布均勻,有的稀疏。那么在這個w的解空間里,該如何挑選相對更好的呢?正則化通過添加約束的方式,幫我們找到一個方向。
L2正則化表達式暗示著一種傾向:訓(xùn)練盡可能的小的權(quán)重,較大的權(quán)重需要保證能顯著降低原有損失C0才能保留。
至于正則化為何能有效的緩解過擬合,這方面數(shù)學(xué)解釋其實并不充分,更多是基于經(jīng)驗的認知。
L2正則化的實現(xiàn)
因為在原有損失函數(shù)中追加了L2正則化項,那么是不是得修改現(xiàn)有反向傳播算法(BP1中有用到C的表達式)?答案是不需要。
C對w求偏導(dǎo)數(shù),可以拆分成原有C0對w求偏導(dǎo),以及L2正則項對w求偏導(dǎo)。前者繼續(xù)利用原有的反向傳播計算方法,而后者可以直接計算得到:

C對于偏置b求偏導(dǎo)保持不變:

基于上述,就可以得到權(quán)重w和偏置b的更新方法:


TensorFlow實現(xiàn)L2正則化
TensorFlow的最優(yōu)化方法tf.train.GradientDescentOptimizer包辦了梯度下降、反向傳播,所以基于TensorFlow實現(xiàn)L2正則化,并不能按照上節(jié)的算法直接干預(yù)權(quán)重的更新,而要使用TensorFlow方式:
tf.add_to_collection(tf.GraphKeys.WEIGHTS, W_2)
tf.add_to_collection(tf.GraphKeys.WEIGHTS, W_3)
regularizer = tf.contrib.layers.l2_regularizer(scale=5.0/50000)
reg_term = tf.contrib.layers.apply_regularization(regularizer)
loss = (tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(labels=y_, logits=z_3)) +
reg_term)
對上述代碼的一些說明:
- 將網(wǎng)絡(luò)中所有層中的權(quán)重,依次通過
tf.add_to_collectio加入到tf.GraphKeys.WEIGHTS中; - 調(diào)用
tf.contrib.layers.l2_regularizer生成L2正則化方法,注意所傳參數(shù)scale=λ/n(n為訓(xùn)練樣本的數(shù)量); - 調(diào)用
tf.contrib.layers.apply_regularization來生成損失函數(shù)的L2正則化項reg_term,所傳第一個參數(shù)為上面生成的正則化方法,第二個參數(shù)為none時默認值為tf.GraphKeys.WEIGHTS; - 最后將L2正則化
reg_term項追加到損失函數(shù)表達式;
向原有損失函數(shù)追加L2正則化項,模型和訓(xùn)練設(shè)置略作調(diào)整:
- 1個隱藏層,包含100個神經(jīng)元;
- 學(xué)習(xí)率:0.5;
- 迭代數(shù):30次;
- mini batch:10;
重新運行訓(xùn)練,跟蹤訓(xùn)練集和驗證集的識別精度,如下圖所示。圖中顯示,在整個30次迭代中,訓(xùn)練集和驗證集的識別率均持續(xù)上升(都超過95%),最終兩者的差距控制在0.5%,過擬合程度顯著的減輕了。
需要注意的是,盡管正則化有效降低了驗證集上過擬合程度,但是也降低了訓(xùn)練集的識別精度。所以在實現(xiàn)L2正則化時增加了隱藏層的神經(jīng)元數(shù)量(從30到100)來抵消識別精度的下降。

附完整代碼
import argparse
import sys
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
FLAGS = None
def main(_):
# Import data
mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True,
validation_size=10000)
# Create the model
x = tf.placeholder(tf.float32, [None, 784])
W_2 = tf.Variable(tf.random_normal([784, 100]) / tf.sqrt(784.0))
'''W_2 = tf.get_variable(
name="W_2",
regularizer=regularizer,
initializer=tf.random_normal([784, 30], stddev=1 / tf.sqrt(784.0)))'''
b_2 = tf.Variable(tf.random_normal([100]))
z_2 = tf.matmul(x, W_2) + b_2
a_2 = tf.sigmoid(z_2)
W_3 = tf.Variable(tf.random_normal([100, 10]) / tf.sqrt(100.0))
'''W_3 = tf.get_variable(
name="W_3",
regularizer=regularizer,
initializer=tf.random_normal([30, 10], stddev=1 / tf.sqrt(30.0)))'''
b_3 = tf.Variable(tf.random_normal([10]))
z_3 = tf.matmul(a_2, W_3) + b_3
a_3 = tf.sigmoid(z_3)
# Define loss and optimizer
y_ = tf.placeholder(tf.float32, [None, 10])
tf.add_to_collection(tf.GraphKeys.WEIGHTS, W_2)
tf.add_to_collection(tf.GraphKeys.WEIGHTS, W_3)
regularizer = tf.contrib.layers.l2_regularizer(scale=5.0 / 50000)
reg_term = tf.contrib.layers.apply_regularization(regularizer)
loss = (tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(labels=y_, logits=z_3)) +
reg_term)
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(loss)
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()
correct_prediction = tf.equal(tf.argmax(a_3, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
scalar_accuracy = tf.summary.scalar('accuracy', accuracy)
train_writer = tf.summary.FileWriter(
'MNIST/logs/tf16_reg/train', sess.graph)
validation_writer = tf.summary.FileWriter(
'MNIST/logs/tf16_reg/validation')
# Train
best = 0
for epoch in range(30):
for _ in range(5000):
batch_xs, batch_ys = mnist.train.next_batch(10)
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
# Test trained model
accuracy_currut_train = sess.run(accuracy,
feed_dict={x: mnist.train.images,
y_: mnist.train.labels})
accuracy_currut_validation = sess.run(
accuracy,
feed_dict={x: mnist.validation.images,
y_: mnist.validation.labels})
sum_accuracy_train = sess.run(
scalar_accuracy,
feed_dict={x: mnist.train.images,
y_: mnist.train.labels})
sum_accuracy_validation = sess.run(
scalar_accuracy,
feed_dict={x: mnist.validation.images,
y_: mnist.validation.labels})
train_writer.add_summary(sum_accuracy_train, epoch)
validation_writer.add_summary(sum_accuracy_validation, epoch)
print("Epoch %s: train: %s validation: %s"
% (epoch, accuracy_currut_train, accuracy_currut_validation))
best = (best, accuracy_currut_validation)[
best <= accuracy_currut_validation]
# Test trained model
print("best: %s" % best)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--data_dir', type=str, default='../MNIST/',
help='Directory for storing input data')
FLAGS, unparsed = parser.parse_known_args()
tf.app.run(main=main, argv=[sys.argv[0]] + unparsed)
下載 tf_16_mnist_loss_weight_reg.py
上一篇 15 重新思考神經(jīng)網(wǎng)絡(luò)初始化
下一篇 17 Step By Step上手TensorBoard
共享協(xié)議:署名-非商業(yè)性使用-禁止演繹(CC BY-NC-ND 3.0 CN)
轉(zhuǎn)載請注明:作者黑猿大叔(簡書)