創(chuàng)建了神經(jīng)網(wǎng)絡(luò)后,我們需要進(jìn)行權(quán)重和偏差的初始化。到現(xiàn)在,我們一直是根據(jù)在第一章中介紹的那樣進(jìn)行初始化。提醒你一下,之前的方式就是根據(jù)獨(dú)立的均值為 $$0$$,標(biāo)準(zhǔn)差為 $$1$$ 的高斯隨機(jī)變量隨機(jī)采樣作為權(quán)重和偏差的初始值。這個(gè)方法工作的還不錯(cuò),但是非常 ad hoc,所以我們需要尋找一些更好的方式來(lái)設(shè)置我們網(wǎng)絡(luò)的初始化權(quán)重和偏差,這對(duì)于幫助網(wǎng)絡(luò)學(xué)習(xí)速度的提升很有價(jià)值。
結(jié)果表明,我們可以比使用正規(guī)化的高斯分布效果更好。為什么?假設(shè)我們使用一個(gè)很多的輸入神經(jīng)元,比如說(shuō) $$1000$$。假設(shè),我們已經(jīng)使用正規(guī)化的高斯分布初始化了連接第一隱藏層的權(quán)重?,F(xiàn)在我將注意力集中在這一層的連接權(quán)重上,忽略網(wǎng)絡(luò)其他部分:
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/123.png" alt="" />
我們?yōu)榱撕?jiǎn)化,假設(shè),我們使用訓(xùn)練樣本 x 其中一半的神經(jīng)元值為 $$0$$,另一半為 $$1$$。下面的觀點(diǎn)也是可以更加廣泛地應(yīng)用,但是你可以從特例中獲得背后的思想。讓我們考慮帶權(quán)和 $$z=\sum_j w_j x_j + b$$ 的隱藏元輸入。其中 $$500$$ 個(gè)項(xiàng)消去了,因?yàn)閷?duì)應(yīng)的輸入 $$x_j=0$$。所以 $$z$$ 是 $$501$$ 個(gè)正規(guī)化的高斯隨機(jī)變量的和,包含 $$500$$ 個(gè)權(quán)重項(xiàng)和額外的 $$1$$ 個(gè)偏差項(xiàng)。因此 $$z$$ 本身是一個(gè)均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$\sqrt{501}\approx 22.4$$ 的分布。$$z$$ 其實(shí)有一個(gè)非常寬的高斯分布,不是非常尖的形狀:
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/124.png" alt="" />
尤其是,我們可以從這幅圖中看出 $$|z|$$ 會(huì)變得非常的大,比如說(shuō) $$z\gg1$$ 或者 $$z\ll 1$$。如果是這樣,輸出 $$\sigma(z)$$ 就會(huì)接近 $$1$$ 或者 $$0$$。也就表示我們的隱藏元會(huì)飽和。所以當(dāng)出現(xiàn)這樣的情況時(shí),在權(quán)重中進(jìn)行微小的調(diào)整僅僅會(huì)給隱藏元的激活值帶來(lái)極其微弱的改變。而這種微弱的改變也會(huì)影響網(wǎng)絡(luò)中剩下的神經(jīng)元,然后會(huì)帶來(lái)相應(yīng)的代價(jià)函數(shù)的改變。結(jié)果就是,這些權(quán)重在我們進(jìn)行梯度下降算法時(shí)會(huì)學(xué)習(xí)得非常緩慢。這其實(shí)和我們前面討論的問(wèn)題差不多,前面的情況是輸出神經(jīng)元在錯(cuò)誤的值上飽和導(dǎo)致學(xué)習(xí)的下降。我們之前通過(guò)代價(jià)函數(shù)的選擇解決了前面的問(wèn)題。不幸的是,盡管那種方式在輸出神經(jīng)元上有效,但對(duì)于隱藏元的飽和卻一點(diǎn)作用都沒(méi)有。
我已經(jīng)研究了第一隱藏層的權(quán)重輸入。當(dāng)然,類(lèi)似的論斷也對(duì)后面的隱藏層有效:如果權(quán)重也是用正規(guī)化的高斯分布進(jìn)行初始化,那么激活值將會(huì)接近 $$0$$ 或者 $$1$$,學(xué)習(xí)速度也會(huì)相當(dāng)緩慢。
還有可以幫助我們進(jìn)行更好地初始化么,能夠避免這種類(lèi)型的飽和,最終避免學(xué)習(xí)速度的下降?假設(shè)我們有一個(gè)有 $$n{in}$$ 個(gè)輸入權(quán)重的神經(jīng)元。我們會(huì)使用均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$1/\sqrt{n{in}}$$ 的高斯分布初始化這些權(quán)重。也就是說(shuō),我們會(huì)向下擠壓高斯分布,讓我們的神經(jīng)元更不可能飽和。我們會(huì)繼續(xù)使用均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$1$$ 的高斯分布來(lái)對(duì)偏差進(jìn)行初始化,后面會(huì)告訴你原因。有了這些設(shè)定,帶權(quán)和 $$z=\sum_j w_j x_j + b$$ 仍然是一個(gè)均值為 $$0$$ 不過(guò)有很陡的峰頂?shù)母咚狗植?。假設(shè),我們有 $$500$$ 個(gè)值為 $$0$$ 的輸入和$$500$$ 個(gè)值為 $$1$$ 的輸入。那么很容證明 $$z$$ 是服從均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$\sqrt{3/2} = 1.22$$ 的高斯分布。這圖像要比以前陡得多,所以即使我已經(jīng)對(duì)橫坐標(biāo)進(jìn)行壓縮為了進(jìn)行更直觀的比較:
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/125.png" alt="" />
這樣的一個(gè)神經(jīng)元更不可能飽和,因此也不大可能遇到學(xué)習(xí)速度下降的問(wèn)題。
我在上面提到,我們使用同樣的方式對(duì)偏差進(jìn)行初始化,就是使用均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$1$$ 的高斯分布來(lái)對(duì)偏差進(jìn)行初始化。這其實(shí)是可行的,因?yàn)檫@樣并不會(huì)讓我們的神經(jīng)網(wǎng)絡(luò)更容易飽和。實(shí)際上,其實(shí)已經(jīng)避免了飽和的問(wèn)題的話,如何初始化偏差影響不大。有些人將所有的偏差初始化為 $$0$$,依賴梯度下降來(lái)學(xué)習(xí)合適的偏差。但是因?yàn)椴顒e不是很大,我們后面還會(huì)按照前面的方式來(lái)進(jìn)行初始化。
讓我們?cè)?MNIST 數(shù)字分類(lèi)任務(wù)上比較一下新舊兩種權(quán)重初始化方式。同樣,還是使用 $$30$$ 個(gè)隱藏元,minibatch 的大小為 $$30$$,規(guī)范化參數(shù) $$\lambda=5.0$$,然后是交叉熵代價(jià)函數(shù)。我們將學(xué)習(xí)率從 $$\eta=0.5$$ 調(diào)整到 $$0.1$$,因?yàn)檫@樣會(huì)讓結(jié)果在圖像中表現(xiàn)得更加明顯。我們先使用舊的初始化方法訓(xùn)練:
>>> import mnist_loader
>>> training_data, validation_data, test_data = \
... mnist_loader.load_data_wrapper()
>>> import network2
>>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost)
>>> net.large_weight_initializer()
>>> net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0,
... evaluation_data=validation_data,
... monitor_evaluation_accuracy=True)
我們也使用新方法來(lái)進(jìn)行權(quán)重的初始化。這實(shí)際上還要更簡(jiǎn)單,因?yàn)?network2's 默認(rèn)方式就是使用新的方法。這意味著我們可以丟掉 net.large_weight_initializer() 調(diào)用:
>>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost)
>>> net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0,
... evaluation_data=validation_data,
... monitor_evaluation_accuracy=True)
將結(jié)果用圖展示出來(lái),就是:
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/126.png" alt="" />
兩種情形下,我們?cè)?96% 的準(zhǔn)確度上重合了。最終的分類(lèi)準(zhǔn)確度幾乎完全一樣。但是新的初始化技術(shù)帶來(lái)了速度的提升。在第一種初始化方式的分類(lèi)準(zhǔn)確度在 87% 一下,而新的方法已經(jīng)幾乎達(dá)到了 93%??雌饋?lái)的情況就是我們新的關(guān)于權(quán)重初始化的方式將訓(xùn)練帶到了一個(gè)新的境界,讓我們能夠更加快速地得到好的結(jié)果。同樣的情況在 $$100$$ 個(gè)神經(jīng)元的設(shè)定中也出現(xiàn)了:
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/127.png" alt="" />
在這個(gè)情況下,兩個(gè)曲線并沒(méi)有重合。然而,我做的實(shí)驗(yàn)發(fā)現(xiàn)了其實(shí)就在一些額外的回合后(這里沒(méi)有展示)準(zhǔn)確度其實(shí)也是幾乎相同的。所以,基于這些實(shí)驗(yàn),看起來(lái)提升的權(quán)重初始化僅僅會(huì)加快訓(xùn)練,不會(huì)改變網(wǎng)絡(luò)的性能。然而,在第四張,我們會(huì)看到一些例子里面使用 $$1/\sqrt{n_{in}}$$ 權(quán)重初始化的長(zhǎng)期運(yùn)行的結(jié)果要顯著更優(yōu)。因此,不僅僅能夠帶來(lái)訓(xùn)練速度的加快,有時(shí)候在最終性能上也有很大的提升。
$$1/\sqrt{n{in}}$$ 的權(quán)重初始化方法幫助我們提升了神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)的方式。其他的權(quán)重初始化技術(shù)同樣也有,很多都是基于這個(gè)基本的思想。我不會(huì)在這里給出其他的方法,因?yàn)?$$1/\sqrt{n{in}}$$ 已經(jīng)可以工作得很好了。如果你對(duì)另外的思想感興趣,我推薦你看看在 $$2012$$ 年的 Yoshua Bengio 的論文的 $$14$$ 和 $$15$$ 頁(yè),以及相關(guān)的參考文獻(xiàn)。
Practical Recommendations for Gradient-Based Training of Deep Architectures, by Yoshua Bengio (2012).
讓我們實(shí)現(xiàn)本章討論過(guò)的這些想法。我們將寫(xiě)出一個(gè)新的程序,network2.py,這是一個(gè)對(duì)第一章中開(kāi)發(fā)的 network.py 的改進(jìn)版本。如果你沒(méi)有仔細(xì)看過(guò) network.py,那你可能會(huì)需要重讀前面關(guān)于這段代碼的討論。僅僅 $$74$$ 行代碼,也很易懂。
和 network.py 一樣,主要部分就是 Network 類(lèi)了,我們用這個(gè)來(lái)表示神經(jīng)網(wǎng)絡(luò)。使用一個(gè) sizes 的列表來(lái)對(duì)每個(gè)對(duì)應(yīng)層進(jìn)行初始化,默認(rèn)使用交叉熵作為代價(jià) cost 參數(shù):
class Network(object):
def __init__(self, sizes, cost=CrossEntropyCost):
self.num_layers = len(sizes)
self.sizes = sizes
self.default_weight_initializer()
self.cost=cost
__init__ 方法的和 network.py 中一樣,可以輕易弄懂。但是下面兩行是新的,我們需要知道他們到底做了什么。
我們先看看 default_weight_initializer 方法,使用了我們新式改進(jìn)后的初始化權(quán)重方法。如我們已經(jīng)看到的,使用了均值為 $$0$$ 而標(biāo)準(zhǔn)差為 $$1/\sqrt{n}$$,$$n$$ 為對(duì)應(yīng)的輸入連接個(gè)數(shù)。我們使用均值為 $$0$$ 而標(biāo)準(zhǔn)差為 $$1$$ 的高斯分布來(lái)初始化偏差。下面是代碼:
def default_weight_initializer(self):
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
self.weights = [np.random.randn(y, x)/np.sqrt(x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]
為了理解這段代碼,需要知道 np 就是進(jìn)行線性代數(shù)運(yùn)算的 Numpy 庫(kù)。我們?cè)诔绦虻拈_(kāi)頭會(huì) import Numpy。同樣我們沒(méi)有對(duì)第一層的神經(jīng)元的偏差進(jìn)行初始化。因?yàn)榈谝粚悠鋵?shí)是輸入層,所以不需要引入任何的偏差。我們?cè)?network.py 中做了完全一樣的事情。
作為 default_weight_initializer 的補(bǔ)充,我們同樣包含了一個(gè) large_weight_initializer 方法。這個(gè)方法使用了第一章中的觀點(diǎn)初始化了權(quán)重和偏差。代碼也就僅僅是和default_weight_initializer差了一點(diǎn)點(diǎn)了:
def large_weight_initializer(self):
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]
我將 larger_weight_initializer 方法包含進(jìn)來(lái)的原因也就是使得跟第一章的結(jié)果更容易比較。我并沒(méi)有考慮太多的推薦使用這個(gè)方法的實(shí)際情景。
初始化方法 __init__ 中的第二個(gè)新的東西就是我們初始化了 cost 屬性。為了理解這個(gè)工作的原理,讓我們看一下用來(lái)表示交叉熵代價(jià)的類(lèi):
class CrossEntropyCost(object):
@staticmethod
def fn(a, y):
return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a)))
@staticmethod
def delta(z, a, y):
return (a-y)
讓我們分解一下。第一個(gè)看到的是:即使使用的是交叉熵,數(shù)學(xué)上看,就是一個(gè)函數(shù),這里我們用 Python 的類(lèi)而不是 Python 函數(shù)實(shí)現(xiàn)了它。為什么這樣做呢?答案就是代價(jià)函數(shù)在我們的網(wǎng)絡(luò)中扮演了兩種不同的角色。明顯的角色就是代價(jià)是輸出激活值 $$a$$ 和目標(biāo)輸出 $$y$$ 差距優(yōu)劣的度量。這個(gè)角色通過(guò) CrossEntropyCost.fn 方法來(lái)扮演。(注意,np.nan_to_num 調(diào)用確保了 Numpy 正確處理接近 $$0$$ 的對(duì)數(shù)值)但是代價(jià)函數(shù)其實(shí)還有另一個(gè)角色?;叵氲诙轮羞\(yùn)行反向傳播算法時(shí),我們需要計(jì)算網(wǎng)絡(luò)輸出誤差,$$\delta^L$$。這種形式的輸出誤差依賴于代價(jià)函數(shù)的選擇:不同的代價(jià)函數(shù),輸出誤差的形式就不同。對(duì)于交叉熵函數(shù),輸出誤差就如公式(66)所示:
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/128.png" alt="" />
所以,我們定義了第二個(gè)方法,CrossEntropyCost.delta,目的就是讓網(wǎng)絡(luò)知道如何進(jìn)行輸出誤差的計(jì)算。然后我們將這兩個(gè)組合在一個(gè)包含所有需要知道的有關(guān)代價(jià)函數(shù)信息的類(lèi)中。
類(lèi)似地,network2.py 還包含了一個(gè)表示二次代價(jià)函數(shù)的類(lèi)。這個(gè)是用來(lái)和第一章的結(jié)果進(jìn)行對(duì)比的,因?yàn)楹竺嫖覀儙缀醵荚谑褂媒徊婧瘮?shù)。代碼如下。QuadraticCost.fn 方法是關(guān)于網(wǎng)絡(luò)輸出 $$a$$ 和目標(biāo)輸出 $$y$$ 的二次代價(jià)函數(shù)的直接計(jì)算結(jié)果。由 QuadraticCost.delta 返回的值就是二次代價(jià)函數(shù)的誤差。
class QuadraticCost(object):
@staticmethod
def fn(a, y):
return 0.5*np.linalg.norm(a-y)**2
@staticmethod
def delta(z, a, y):
return (a-y) * sigmoid_prime(z)
現(xiàn)在,我們理解了 network2.py 和 network.py 兩個(gè)實(shí)現(xiàn)之間的主要差別。都是很簡(jiǎn)單的東西。還有一些更小的變動(dòng),下面我們會(huì)進(jìn)行介紹,包含 L2 規(guī)范化的實(shí)現(xiàn)。在講述規(guī)范化之前,我們看看 network2.py 完整的實(shí)現(xiàn)代碼。你不需要太仔細(xì)地讀遍這些代碼,但是對(duì)整個(gè)結(jié)構(gòu)尤其是文檔中的內(nèi)容的理解是非常重要的,這樣,你就可以理解每段程序所做的工作。當(dāng)然,你也可以隨自己意愿去深入研究!如果你迷失了理解,那么請(qǐng)讀讀下面的講解,然后再回到代碼中。不多說(shuō)了,給代碼:
"""network2.py
~~~~~~~~~~~~~~
An improved version of network.py, implementing the stochastic
gradient descent learning algorithm for a feedforward neural network.
Improvements include the addition of the cross-entropy cost function,
regularization, and better initialization of network weights. Note
that I have focused on making the code simple, easily readable, and
easily modifiable. It is not optimized, and omits many desirable
features.
"""
#### Libraries
# Standard library
import json
import random
import sys
# Third-party libraries
import numpy as np
#### Define the quadratic and cross-entropy cost functions
class QuadraticCost(object):
@staticmethod
def fn(a, y):
"""Return the cost associated with an output ``a`` and desired output
``y``.
"""
return 0.5*np.linalg.norm(a-y)**2
@staticmethod
def delta(z, a, y):
"""Return the error delta from the output layer."""
return (a-y) * sigmoid_prime(z)
class CrossEntropyCost(object):
@staticmethod
def fn(a, y):
"""Return the cost associated with an output ``a`` and desired output
``y``. Note that np.nan_to_num is used to ensure numerical
stability. In particular, if both ``a`` and ``y`` have a 1.0
in the same slot, then the expression (1-y)*np.log(1-a)
returns nan. The np.nan_to_num ensures that that is converted
to the correct value (0.0).
"""
return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a)))
@staticmethod
def delta(z, a, y):
"""Return the error delta from the output layer. Note that the
parameter ``z`` is not used by the method. It is included in
the method's parameters in order to make the interface
consistent with the delta method for other cost classes.
"""
return (a-y)
#### Main Network class
class Network(object):
def __init__(self, sizes, cost=CrossEntropyCost):
"""The list ``sizes`` contains the number of neurons in the respective
layers of the network. For example, if the list was [2, 3, 1]
then it would be a three-layer network, with the first layer
containing 2 neurons, the second layer 3 neurons, and the
third layer 1 neuron. The biases and weights for the network
are initialized randomly, using
``self.default_weight_initializer`` (see docstring for that
method).
"""
self.num_layers = len(sizes)
self.sizes = sizes
self.default_weight_initializer()
self.cost=cost
def default_weight_initializer(self):
"""Initialize each weight using a Gaussian distribution with mean 0
and standard deviation 1 over the square root of the number of
weights connecting to the same neuron. Initialize the biases
using a Gaussian distribution with mean 0 and standard
deviation 1.
Note that the first layer is assumed to be an input layer, and
by convention we won't set any biases for those neurons, since
biases are only ever used in computing the outputs from later
layers.
"""
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
self.weights = [np.random.randn(y, x)/np.sqrt(x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]
def large_weight_initializer(self):
"""Initialize the weights using a Gaussian distribution with mean 0
and standard deviation 1. Initialize the biases using a
Gaussian distribution with mean 0 and standard deviation 1.
Note that the first layer is assumed to be an input layer, and
by convention we won't set any biases for those neurons, since
biases are only ever used in computing the outputs from later
layers.
This weight and bias initializer uses the same approach as in
Chapter 1, and is included for purposes of comparison. It
will usually be better to use the default weight initializer
instead.
"""
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]
def feedforward(self, a):
"""Return the output of the network if ``a`` is input."""
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a
def SGD(self, training_data, epochs, mini_batch_size, eta,
lmbda = 0.0,
evaluation_data=None,
monitor_evaluation_cost=False,
monitor_evaluation_accuracy=False,
monitor_training_cost=False,
monitor_training_accuracy=False):
"""Train the neural network using mini-batch stochastic gradient
descent. The ``training_data`` is a list of tuples ``(x, y)``
representing the training inputs and the desired outputs. The
other non-optional parameters are self-explanatory, as is the
regularization parameter ``lmbda``. The method also accepts
``evaluation_data``, usually either the validation or test
data. We can monitor the cost and accuracy on either the
evaluation data or the training data, by setting the
appropriate flags. The method returns a tuple containing four
lists: the (per-epoch) costs on the evaluation data, the
accuracies on the evaluation data, the costs on the training
data, and the accuracies on the training data. All values are
evaluated at the end of each training epoch. So, for example,
if we train for 30 epochs, then the first element of the tuple
will be a 30-element list containing the cost on the
evaluation data at the end of each epoch. Note that the lists
are empty if the corresponding flag is not set.
"""
if evaluation_data: n_data = len(evaluation_data)
n = len(training_data)
evaluation_cost, evaluation_accuracy = [], []
training_cost, training_accuracy = [], []
for j in xrange(epochs):
random.shuffle(training_data)
mini_batches = [
training_data[k:k+mini_batch_size]
for k in xrange(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(
mini_batch, eta, lmbda, len(training_data))
print "Epoch %s training complete" % j
if monitor_training_cost:
cost = self.total_cost(training_data, lmbda)
training_cost.append(cost)
print "Cost on training data: {}".format(cost)
if monitor_training_accuracy:
accuracy = self.accuracy(training_data, convert=True)
training_accuracy.append(accuracy)
print "Accuracy on training data: {} / {}".format(
accuracy, n)
if monitor_evaluation_cost:
cost = self.total_cost(evaluation_data, lmbda, convert=True)
evaluation_cost.append(cost)
print "Cost on evaluation data: {}".format(cost)
if monitor_evaluation_accuracy:
accuracy = self.accuracy(evaluation_data)
evaluation_accuracy.append(accuracy)
print "Accuracy on evaluation data: {} / {}".format(
self.accuracy(evaluation_data), n_data)
print
return evaluation_cost, evaluation_accuracy, \
training_cost, training_accuracy
def update_mini_batch(self, mini_batch, eta, lmbda, n):
"""Update the network's weights and biases by applying gradient
descent using backpropagation to a single mini batch. The
``mini_batch`` is a list of tuples ``(x, y)``, ``eta`` is the
learning rate, ``lmbda`` is the regularization parameter, and
``n`` is the total size of the training data set.
"""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [(1-eta*(lmbda/n))*w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]
def backprop(self, x, y):
"""Return a tuple ``(nabla_b, nabla_w)`` representing the
gradient for the cost function C_x. ``nabla_b`` and
``nabla_w`` are layer-by-layer lists of numpy arrays, similar
to ``self.biases`` and ``self.weights``."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x] # list to store all the activations, layer by layer
zs = [] # list to store all the z vectors, layer by layer
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
delta = (self.cost).delta(zs[-1], activations[-1], y)
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# Note that the variable l in the loop below is used a little
# differently to the notation in Chapter 2 of the book. Here,
# l = 1 means the last layer of neurons, l = 2 is the
# second-last layer, and so on. It's a renumbering of the
# scheme in the book, used here to take advantage of the fact
# that Python can use negative indices in lists.
for l in xrange(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)
def accuracy(self, data, convert=False):
"""Return the number of inputs in ``data`` for which the neural
network outputs the correct result. The neural network's
output is assumed to be the index of whichever neuron in the
final layer has the highest activation.
The flag ``convert`` should be set to False if the data set is
validation or test data (the usual case), and to True if the
data set is the training data. The need for this flag arises
due to differences in the way the results ``y`` are
represented in the different data sets. In particular, it
flags whether we need to convert between the different
representations. It may seem strange to use different
representations for the different data sets. Why not use the
same representation for all three data sets? It's done for
efficiency reasons -- the program usually evaluates the cost
on the training data and the accuracy on other data sets.
These are different types of computations, and using different
representations speeds things up. More details on the
representations can be found in
mnist_loader.load_data_wrapper.
"""
if convert:
results = [(np.argmax(self.feedforward(x)), np.argmax(y))
for (x, y) in data]
else:
results = [(np.argmax(self.feedforward(x)), y)
for (x, y) in data]
return sum(int(x == y) for (x, y) in results)
def total_cost(self, data, lmbda, convert=False):
"""Return the total cost for the data set ``data``. The flag
``convert`` should be set to False if the data set is the
training data (the usual case), and to True if the data set is
the validation or test data. See comments on the similar (but
reversed) convention for the ``accuracy`` method, above.
"""
cost = 0.0
for x, y in data:
a = self.feedforward(x)
if convert: y = vectorized_result(y)
cost += self.cost.fn(a, y)/len(data)
cost += 0.5*(lmbda/len(data))*sum(
np.linalg.norm(w)**2 for w in self.weights)
return cost
def save(self, filename):
"""Save the neural network to the file ``filename``."""
data = {"sizes": self.sizes,
"weights": [w.tolist() for w in self.weights],
"biases": [b.tolist() for b in self.biases],
"cost": str(self.cost.__name__)}
f = open(filename, "w")
json.dump(data, f)
f.close()
#### Loading a Network
def load(filename):
"""Load a neural network from the file ``filename``. Returns an
instance of Network.
"""
f = open(filename, "r")
data = json.load(f)
f.close()
cost = getattr(sys.modules[__name__], data["cost"])
net = Network(data["sizes"], cost=cost)
net.weights = [np.array(w) for w in data["weights"]]
net.biases = [np.array(b) for b in data["biases"]]
return net
#### Miscellaneous functions
def vectorized_result(j):
"""Return a 10-dimensional unit vector with a 1.0 in the j'th position
and zeroes elsewhere. This is used to convert a digit (0...9)
into a corresponding desired output from the neural network.
"""
e = np.zeros((10, 1))
e[j] = 1.0
return e
def sigmoid(z):
"""The sigmoid function."""
return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
"""Derivative of the sigmoid function."""
return sigmoid(z)*(1-sigmoid(z))
有個(gè)更加有趣的變動(dòng)就是在代碼中增加了 L2 規(guī)范化。盡管這是一個(gè)主要的概念上的變動(dòng),在實(shí)現(xiàn)中其實(shí)相當(dāng)簡(jiǎn)單。對(duì)大部分情況,僅僅需要傳遞參數(shù) lmbda 到不同的方法中,主要是 Network.SGD 方法。實(shí)際上的工作就是一行代碼的事在 Network.update_mini_batch 的倒數(shù)第四行。這就是我們改動(dòng)梯度下降規(guī)則來(lái)進(jìn)行權(quán)重下降的地方。盡管改動(dòng)很小,但其對(duì)結(jié)果影響卻很大!
其實(shí)這種情況在神經(jīng)網(wǎng)絡(luò)中實(shí)現(xiàn)一些新技術(shù)的常見(jiàn)現(xiàn)象。我們花費(fèi)了近千字的篇幅來(lái)討論規(guī)范化。概念的理解非常微妙困難。但是添加到程序中的時(shí)候卻如此簡(jiǎn)單。精妙復(fù)雜的技術(shù)可以通過(guò)微小的代碼改動(dòng)就可以實(shí)現(xiàn)了。
另一個(gè)微小卻重要的改動(dòng)是隨機(jī)梯度下降方法的幾個(gè)標(biāo)志位的增加。這些標(biāo)志位讓我們可以對(duì)在代價(jià)和準(zhǔn)確度的監(jiān)控變得可能。這些標(biāo)志位默認(rèn)是 False 的,但是在我們例子中,已經(jīng)被置為 True 來(lái)監(jiān)控 Network 的性能。另外,network2.py 中的 Network.SGD 方法返回了一個(gè)四元組來(lái)表示監(jiān)控的結(jié)果。我們可以這樣使用:
>>> evaluation_cost, evaluation_accuracy,
... training_cost, training_accuracy = net.SGD(training_data, 30, 10, 0.5,
... lmbda = 5.0,
... evaluation_data=validation_data,
... monitor_evaluation_accuracy=True,
... monitor_evaluation_cost=True,
... monitor_training_accuracy=True,
... monitor_training_cost=True)
所以,比如 evaluation_cost 將會(huì)是一個(gè) $$30$$ 個(gè)元素的列表其中包含了每個(gè)回合在驗(yàn)證集合上的代價(jià)函數(shù)值。這種類(lèi)型的信息在理解網(wǎng)絡(luò)行為的過(guò)程中特別有用。比如,它可以用來(lái)畫(huà)出展示網(wǎng)絡(luò)隨時(shí)間學(xué)習(xí)的狀態(tài)。其實(shí),這也是我在前面的章節(jié)中展示性能的方式。然而要注意的是如果任何標(biāo)志位都沒(méi)有設(shè)置的話,對(duì)應(yīng)的元組中的元素就是空列表。
另一個(gè)增加項(xiàng)就是在 Network.save 方法中的代碼,用來(lái)將 Network 對(duì)象保存在磁盤(pán)上,還有一個(gè)載回內(nèi)存的函數(shù)。這兩個(gè)方法都是使用 JSON 進(jìn)行的,而非 Python 的 pickle 或者 cPickle 模塊——這些通常是 Python 中常見(jiàn)的保存和裝載對(duì)象的方法。使用 JSON 的原因是,假設(shè)在未來(lái)某天,我們想改變 Network 類(lèi)來(lái)允許非 sigmoid 的神經(jīng)元。對(duì)這個(gè)改變的實(shí)現(xiàn),我們最可能是改變?cè)?Network.__init__ 方法中定義的屬性。如果我們簡(jiǎn)單地 pickle 對(duì)象,會(huì)導(dǎo)致 load 函數(shù)出錯(cuò)。使用 JSON 進(jìn)行序列化可以顯式地讓老的 Network 仍然能夠 load。
其他也還有一些微小的變動(dòng)。但是那些只是 network.py 的微調(diào)。結(jié)果就是把程序從 $$74$$ 行增長(zhǎng)到了 $$152$$ 行。
network.py 中的 Network.cost_derivative 方法。這個(gè)方法是為二次代價(jià)函數(shù)寫(xiě)的。怎樣修改可以用于交叉熵代價(jià)函數(shù)上?你能不能想到可能在交叉熵函數(shù)上遇到的問(wèn)題?在 network2.py 中,我們已經(jīng)去掉了 Network.cost_derivative 方法,將其集成進(jìn)了 CrossEntropyCost.delta 方法中。請(qǐng)問(wèn),這樣是如何解決你已經(jīng)發(fā)現(xiàn)的問(wèn)題的?直到現(xiàn)在,我們還沒(méi)有解釋對(duì)諸如學(xué)習(xí)率 $$\eta$$,規(guī)范化參數(shù) $$\lambda$$ 等等超參數(shù)選擇的方法。我只是給出那些效果很好的值而已。實(shí)踐中,當(dāng)你使用神經(jīng)網(wǎng)絡(luò)解決問(wèn)題時(shí),尋找好的超參數(shù)其實(shí)是很困難的一件事。例如,我們要解決 MNIST 問(wèn)題,開(kāi)始時(shí)對(duì)于選擇什么樣的超參數(shù)一無(wú)所知。假設(shè),剛開(kāi)始的實(shí)驗(yàn)中選擇前面章節(jié)的參數(shù)都是運(yùn)氣較好。但在使用學(xué)習(xí)率 $$\eta=10.0$$ 而規(guī)范化參數(shù) $$\lambda=1000.0$$。下面是我們的一個(gè)嘗試:
>>> import mnist_loader
>>> training_data, validation_data, test_data = \
... mnist_loader.load_data_wrapper()
>>> import network2
>>> net = network2.Network([784, 30, 10])
>>> net.SGD(training_data, 30, 10, 10.0, lmbda = 1000.0,
... evaluation_data=validation_data, monitor_evaluation_accuracy=True)
Epoch 0 training complete
Accuracy on evaluation data: 1030 / 10000
Epoch 1 training complete
Accuracy on evaluation data: 990 / 10000
Epoch 2 training complete
Accuracy on evaluation data: 1009 / 10000
...
Epoch 27 training complete
Accuracy on evaluation data: 1009 / 10000
Epoch 28 training complete
Accuracy on evaluation data: 983 / 10000
Epoch 29 training complete
Accuracy on evaluation data: 967 / 10000
我們分類(lèi)準(zhǔn)確度并不比隨機(jī)選擇更好。網(wǎng)絡(luò)就像隨機(jī)噪聲產(chǎn)生器一樣。
你可能會(huì)說(shuō),“這好辦,降低學(xué)習(xí)率和規(guī)范化參數(shù)就好了。”不幸的是,你并不先驗(yàn)地知道這些就是需要調(diào)整的超參數(shù)??赡苷嬲膯?wèn)題出在 $$30$$ 個(gè)隱藏元中,本身就不能很有效,不管我們?nèi)绾握{(diào)整其他的超參數(shù)都沒(méi)有作用的?可能我們真的需要至少 $$100$$ 個(gè)隱藏神經(jīng)元?或者是 $$300$$ 個(gè)隱藏神經(jīng)元?或者更多層的網(wǎng)絡(luò)?或者不同輸出編碼方式?可能我們的網(wǎng)絡(luò)一直在學(xué)習(xí),只是學(xué)習(xí)的回合還不夠?可能 minibatch 的太小了?可能我們需要切換成二次代價(jià)函數(shù)?可能我們需要嘗試不同的權(quán)重初始化方法?等等。很容易就在超參數(shù)的選擇中迷失了方向。如果你的網(wǎng)絡(luò)規(guī)模很大,或者使用了很多的訓(xùn)練數(shù)據(jù),這種情況就很令人失望了,因?yàn)橐淮斡?xùn)練可能就要幾個(gè)小時(shí)甚至幾天乃至幾周,最終什么都沒(méi)有獲得。如果這種情況一直發(fā)生,就會(huì)打擊你的自信心??赡苣銜?huì)懷疑神經(jīng)網(wǎng)絡(luò)是不是適合你所遇到的問(wèn)題?可能就應(yīng)該放棄這種嘗試了?
本節(jié),我會(huì)給出一些用于設(shè)定超參數(shù)的啟發(fā)式想法。目的是幫你發(fā)展出一套工作流來(lái)確保很好地設(shè)置超參數(shù)。當(dāng)然,我不會(huì)覆蓋超參數(shù)優(yōu)化的每個(gè)方法。那是太繁重的問(wèn)題,而且也不會(huì)是一個(gè)能夠完全解決的問(wèn)題,也不存在一種通用的關(guān)于正確策略的共同認(rèn)知。總是會(huì)有一些新的技巧可以幫助你提高一點(diǎn)性能。但是本節(jié)的啟發(fā)式想法能幫你開(kāi)個(gè)好頭。
寬的策略:在使用神經(jīng)網(wǎng)絡(luò)來(lái)解決新的問(wèn)題時(shí),一個(gè)挑戰(zhàn)就是獲得任何一種非尋常的學(xué)習(xí),也就是說(shuō),達(dá)到比隨機(jī)的情況更好的結(jié)果。這個(gè)實(shí)際上會(huì)很困難,尤其是遇到一種新類(lèi)型的問(wèn)題時(shí)。讓我們看看有哪些策略可以在面臨這類(lèi)困難時(shí)候嘗試。
假設(shè),我們第一次遇到 MNIST 分類(lèi)問(wèn)題。剛開(kāi)始,你很有激情,但是當(dāng)?shù)谝粋€(gè)神經(jīng)網(wǎng)絡(luò)完全失效時(shí),你會(huì)就得有些沮喪。此時(shí)就可以將問(wèn)題簡(jiǎn)化。丟開(kāi)訓(xùn)練和驗(yàn)證集合中的那些除了 $$0$$ 和 $$1$$ 的那些圖像。然后試著訓(xùn)練一個(gè)網(wǎng)絡(luò)來(lái)區(qū)分 $$0$$ 和 $$1$$。不僅僅問(wèn)題比 $$10$$ 個(gè)分類(lèi)的情況簡(jiǎn)化了,同樣也會(huì)減少 80% 的訓(xùn)練數(shù)據(jù),這樣就給出了 $$5$$ 倍的加速。這樣可以保證更快的實(shí)驗(yàn),也能給予你關(guān)于如何構(gòu)建好的網(wǎng)絡(luò)更快的洞察。
你通過(guò)簡(jiǎn)化網(wǎng)絡(luò)來(lái)加速實(shí)驗(yàn)進(jìn)行更有意義的學(xué)習(xí)。如果你相信 $$[784, 10]$$ 的網(wǎng)絡(luò)更可能比隨機(jī)更加好的分類(lèi)效果,那么就從這個(gè)網(wǎng)絡(luò)開(kāi)始實(shí)驗(yàn)。這會(huì)比訓(xùn)練一個(gè) $$[784, 30 ,10]$$ 的網(wǎng)絡(luò)更快,你可以進(jìn)一步嘗試后一個(gè)。
你可以通過(guò)提高監(jiān)控的頻率來(lái)在試驗(yàn)中獲得另一個(gè)加速了。在 network2.py 中,我們?cè)诿總€(gè)訓(xùn)練的回合的最后進(jìn)行監(jiān)控。每回合 $$50,000$$,在接受到網(wǎng)絡(luò)學(xué)習(xí)狀況的反饋前需要等上一會(huì)兒——在我的筆記本上訓(xùn)練 $$[784, 30, 10]$$ 網(wǎng)絡(luò)基本上每回合 $$10$$ 秒。當(dāng)然,$$10$$ 秒并不太長(zhǎng),不過(guò)你希望嘗試幾十種超參數(shù)就很麻煩了,如果你想再嘗試更多地選擇,那就相當(dāng)棘手了。我們可以通過(guò)更加頻繁地監(jiān)控驗(yàn)證準(zhǔn)確度來(lái)獲得反饋,比如說(shuō)在每 $$1,000$$ 次訓(xùn)練圖像后。而且,與其使用整個(gè) $$10,000$$ 幅圖像的驗(yàn)證集來(lái)監(jiān)控性能,我們可以使用 $$100$$ 幅圖像來(lái)進(jìn)行驗(yàn)證。真正重要的是網(wǎng)絡(luò)看到足夠多的圖像來(lái)做真正的學(xué)習(xí),獲得足夠優(yōu)秀的估計(jì)性能。當(dāng)然,我們的程序 network2.py 并沒(méi)有做這樣的監(jiān)控。但是作為一個(gè)湊合的能夠獲得類(lèi)似效果的方案,我們將訓(xùn)練數(shù)據(jù)減少到前 $$1,000$$ 幅 MNIST 訓(xùn)練圖像。讓我們嘗試一下,看看結(jié)果。(為了讓代碼更加簡(jiǎn)單,我并沒(méi)有取僅僅是 0 和 1 的圖像。當(dāng)然,那樣也是很容易就可以實(shí)現(xiàn))。
>>> net = network2.Network([784, 10])
>>> net.SGD(training_data[:1000], 30, 10, 10.0, lmbda = 1000.0, \
... evaluation_data=validation_data[:100], \
... monitor_evaluation_accuracy=True)
Epoch 0 training complete
Accuracy on evaluation data: 10 / 100
Epoch 1 training complete
Accuracy on evaluation data: 10 / 100
Epoch 2 training complete
Accuracy on evaluation data: 10 / 100
...
我們?nèi)匀猾@得完全的噪聲!但是有一個(gè)進(jìn)步:現(xiàn)在我們每一秒鐘可以得到反饋,而不是之前每 10 秒鐘才可以。這意味著你可以更加快速地實(shí)驗(yàn)其他的超參數(shù),或者甚至近同步地進(jìn)行不同參數(shù)的組合的評(píng)比。
在上面的例子中,我設(shè)置 $$\lambda=1000.0$$,跟我們之前一樣。但是因?yàn)檫@里改變了訓(xùn)練樣本的個(gè)數(shù),我們必須對(duì) $$\lambda$$ 進(jìn)行調(diào)整以保證權(quán)重下降的同步性。這意味著改變 $$\lambda = 20.0$$。如果我們這樣設(shè)置,則有:
>>> net = network2.Network([784, 10])
>>> net.SGD(training_data[:1000], 30, 10, 10.0, lmbda = 20.0, \
... evaluation_data=validation_data[:100], \
... monitor_evaluation_accuracy=True)
Epoch 0 training complete
Accuracy on evaluation data: 12 / 100
Epoch 1 training complete
Accuracy on evaluation data: 14 / 100
Epoch 2 training complete
Accuracy on evaluation data: 25 / 100
Epoch 3 training complete
Accuracy on evaluation data: 18 / 100
...
哦也!現(xiàn)在有了信號(hào)了。不是非常糟糕的信號(hào),卻真是一個(gè)信號(hào)。我們可以基于這點(diǎn),來(lái)改變超參數(shù)從而獲得更多的提升??赡芪覀儾聹y(cè)學(xué)習(xí)率需要增加(你可以能會(huì)發(fā)現(xiàn),這只是一個(gè)不大好的猜測(cè),原因后面會(huì)講,但是相信我)所以為了測(cè)試我們的猜測(cè)就將 $$\eta$$ 調(diào)整至 $$100.0$$:
>>> net = network2.Network([784, 10])
>>> net.SGD(training_data[:1000], 30, 10, 100.0, lmbda = 20.0, \
... evaluation_data=validation_data[:100], \
... monitor_evaluation_accuracy=True)
Epoch 0 training complete
Accuracy on evaluation data: 10 / 100
Epoch 1 training complete
Accuracy on evaluation data: 10 / 100
Epoch 2 training complete
Accuracy on evaluation data: 10 / 100
Epoch 3 training complete
Accuracy on evaluation data: 10 / 100
...
這并不好!告訴我們之前的猜測(cè)是錯(cuò)誤的,問(wèn)題并不是學(xué)習(xí)率太低了。所以,我們?cè)囍鴮?$$\eta$$ 將至 $$\eta=1.0$$:
>>> net = network2.Network([784, 10])
>>> net.SGD(training_data[:1000], 30, 10, 1.0, lmbda = 20.0, \
... evaluation_data=validation_data[:100], \
... monitor_evaluation_accuracy=True)
Epoch 0 training complete
Accuracy on evaluation data: 62 / 100
Epoch 1 training complete
Accuracy on evaluation data: 42 / 100
Epoch 2 training complete
Accuracy on evaluation data: 43 / 100
Epoch 3 training complete
Accuracy on evaluation data: 61 / 100
...
這樣好點(diǎn)了!所以我們可以繼續(xù),逐個(gè)調(diào)整每個(gè)超參數(shù),慢慢提升性能。一旦我們找到一種提升性能的 $$\eta$$ 值,我們就可以嘗試尋找好的值。然后按照一個(gè)更加復(fù)雜的網(wǎng)絡(luò)架構(gòu)進(jìn)行實(shí)驗(yàn),假設(shè)是一個(gè)有 $$10$$ 個(gè)隱藏元的網(wǎng)絡(luò)。然后繼續(xù)調(diào)整 $$\eta$$ 和 $$\lambda$$。接著調(diào)整成 $$20$$ 個(gè)隱藏元。然后將其他的超參數(shù)調(diào)整再調(diào)整。如此進(jìn)行,在每一步使用我們 hold out 驗(yàn)證數(shù)據(jù)集來(lái)評(píng)價(jià)性能,使用這些度量來(lái)找到越來(lái)越好的超參數(shù)。當(dāng)我們這么做的時(shí)候,一般都需要花費(fèi)更多時(shí)間來(lái)發(fā)現(xiàn)由于超參數(shù)改變帶來(lái)的影響,這樣就可以一步步減少監(jiān)控的頻率。
所有這些作為一種寬泛的策略看起來(lái)很有前途。然而,我想要回到尋找超參數(shù)的原點(diǎn)。實(shí)際上,即使是上面的討論也傳達(dá)出過(guò)于樂(lè)觀的觀點(diǎn)。實(shí)際上,很容易會(huì)遇到神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)不到任何知識(shí)的情況。你可能要花費(fèi)若干天在調(diào)整參數(shù)上,仍然沒(méi)有進(jìn)展。所以我想要再重申一下在前期你應(yīng)該從實(shí)驗(yàn)中盡可能早的獲得快速反饋。直覺(jué)上看,這看起來(lái)簡(jiǎn)化問(wèn)題和架構(gòu)僅僅會(huì)降低你的效率。實(shí)際上,這樣能夠?qū)⑦M(jìn)度加快,因?yàn)槟隳軌蚋斓卣业絺鬟_(dá)出有意義的信號(hào)的網(wǎng)絡(luò)。一旦你獲得這些信號(hào),你可以嘗嘗通過(guò)微調(diào)超參數(shù)獲得快速的性能提升。這和人生中很多情況一樣——萬(wàn)事開(kāi)頭難。
好了,上面就是寬泛的策略。現(xiàn)在我們看看一些具體的設(shè)置超參數(shù)的推薦。我會(huì)聚焦在學(xué)習(xí)率 $$\eta$$,L2 規(guī)范化參數(shù) $$\lambda$$,和 minibatch 大小。然而,很多的觀點(diǎn)同樣可以應(yīng)用在其他的超參數(shù)的選擇上,包括一些關(guān)于網(wǎng)絡(luò)架構(gòu)的、其他類(lèi)型的規(guī)范化和一些本書(shū)后面遇到的如 momentum co-efficient 這樣的超參數(shù)。
學(xué)習(xí)率:假設(shè)我們運(yùn)行了三個(gè)不同學(xué)習(xí)率($$\eta=0.025$$、$$\eta=0.25$$、$$\eta=2.5$$)的 MNIST 網(wǎng)絡(luò)。我們會(huì)像前面介紹的實(shí)驗(yàn)?zāi)菢釉O(shè)置其他的超參數(shù),進(jìn)行 $$30$$ 回合,minibatch 大小為 $$10$$,然后 $$\lambda = 5.0$$。我們同樣會(huì)使用整個(gè) $$50,000$$ 幅訓(xùn)練圖像。下面是一副展示了訓(xùn)練代價(jià)的變化情況的圖:
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/129.png" alt="" />
使用 $$\eta=0.025$$,代價(jià)函數(shù)平滑下降到最后的回合。使用 $$\eta=0.25$$,代價(jià)剛開(kāi)始下降,在大約 $$20$$ 回合后接近飽和狀態(tài),后面就是微小的震蕩和隨機(jī)抖動(dòng)。最終使用 $$\eta=2.5$$ 代價(jià)從始至終都震蕩得非常明顯。為了理解震蕩的原因,回想一下隨機(jī)梯度下降其實(shí)是期望我們能夠逐漸地抵達(dá)代價(jià)函數(shù)的谷底的,
http://wiki.jikexueyuan.com/project/neural-networks-and-deep-learning-zh-cn/images/130.png" alt="" />
然而,如果 $$\eta$$ 太大的話,步長(zhǎng)也會(huì)變大可能會(huì)使得算法在接近最小值時(shí)候又越過(guò)了谷底。這在 $$\eta=2.5$$ 時(shí)非??赡馨l(fā)生。當(dāng)我們選擇 $$\eta=0.25$$ 時(shí),初始幾步將我們帶到了谷底附近,但一旦到達(dá)了谷底,又很容易跨越過(guò)去。而在我們選擇 $$\eta=0.025$$ 時(shí),在前 $$30$$ 回合的訓(xùn)練中不再受到這個(gè)情況的影響。當(dāng)然,選擇太小的學(xué)習(xí)率,也會(huì)帶來(lái)另一個(gè)問(wèn)題——隨機(jī)梯度下降算法變慢了。一種更加好的策略其實(shí)是,在開(kāi)始時(shí)使用 $$\eta=0.25$$,隨著越來(lái)越接近谷底,就換成 $$\eta=0.025$$。這種可變學(xué)習(xí)率的方法我們后面會(huì)介紹?,F(xiàn)在,我們就聚焦在找出一個(gè)單獨(dú)的好的學(xué)習(xí)率的選擇,$$\eta$$。
所以,有了這樣的想法,我們可以如下設(shè)置 $$\eta$$。首先,我們選擇在訓(xùn)練數(shù)據(jù)上的代價(jià)立即開(kāi)始下降而非震蕩或者增加時(shí)作為 $$\eta$$ 的閾值的估計(jì)。這個(gè)估計(jì)并不需要太過(guò)精確。你可以估計(jì)這個(gè)值的量級(jí),比如說(shuō)從 $$\eta=0.01$$ 開(kāi)始。如果代價(jià)在訓(xùn)練的前面若干回合開(kāi)始下降,你就可以逐步地嘗試 $$\eta=0.1, 1.0,...$$,直到你找到一個(gè) $$\eta$$ 的值使得在開(kāi)始若干回合代價(jià)就開(kāi)始震蕩或者增加。相反,如果代價(jià)在 $$\eta=0.01$$ 時(shí)就開(kāi)始震蕩或者增加,那就嘗試 $$\eta=0.001, 0.0001,...$$ 直到你找到代價(jià)在開(kāi)始回合就下降的設(shè)定。按照這樣的方法,我們可以掌握學(xué)習(xí)率的閾值的量級(jí)的估計(jì)。你可以選擇性地優(yōu)化估計(jì),選擇那些最大的 $$\eta$$,比方說(shuō) $$\eta=0.5$$ 或者 $$\eta=0.2$$(這里也不需要過(guò)于精確)。
顯然,$$\eta$$ 實(shí)際值不應(yīng)該比閾值大。實(shí)際上,如果 $$\eta$$ 的值重復(fù)使用很多回合的話,你更應(yīng)該使用稍微小一點(diǎn)的值,例如,閾值的一半這樣的選擇。這樣的選擇能夠允許你訓(xùn)練更多的回合,不會(huì)減慢學(xué)習(xí)的速度。
在 MNIST 數(shù)據(jù)中,使用這樣的策略會(huì)給出一個(gè)關(guān)于學(xué)習(xí)率 $$\eta$$ 的一個(gè)量級(jí)的估計(jì),大概是 $$0.1$$。在一些改良后,我們得到了閾值 $$\eta=0.5$$。所以,我們按照剛剛的取一半的策略就確定了學(xué)習(xí)率為 $$\eta=0.25$$。實(shí)際上,我發(fā)現(xiàn)使用 $$\eta=0.5$$ 在 $$30$$ 回合內(nèi)表現(xiàn)是很好的,所以選擇更低的學(xué)習(xí)率,也沒(méi)有什么問(wèn)題。
這看起來(lái)相當(dāng)直接。然而,使用訓(xùn)練代價(jià)函數(shù)來(lái)選擇 $$\eta$$ 看起來(lái)和我們之前提到的通過(guò)驗(yàn)證集來(lái)確定超參數(shù)的觀點(diǎn)有點(diǎn)矛盾。實(shí)際上,我們會(huì)使用驗(yàn)證準(zhǔn)確度來(lái)選擇規(guī)范化超參數(shù),minibatch 大小,和層數(shù)及隱藏元個(gè)數(shù)這些網(wǎng)絡(luò)參數(shù),等等。為何對(duì)學(xué)習(xí)率要用不同的方法呢?坦白地說(shuō),這些選擇其實(shí)是我個(gè)人美學(xué)偏好,個(gè)人習(xí)慣罷了。原因就是其他的超參數(shù)傾向于提升最終的測(cè)試集上的分類(lèi)準(zhǔn)確度,所以將他們通過(guò)驗(yàn)證準(zhǔn)確度來(lái)選擇更合理一些。然而,學(xué)習(xí)率僅僅是偶然地影響最終的分類(lèi)準(zhǔn)確度的。學(xué)習(xí)率主要的目的是控制梯度下降的步長(zhǎng),監(jiān)控訓(xùn)練代價(jià)是最好的檢測(cè)步長(zhǎng)過(guò)大的方法。所以,這其實(shí)就是個(gè)人的偏好。在學(xué)習(xí)的前期,如果驗(yàn)證準(zhǔn)確度提升,訓(xùn)練代價(jià)通常都在下降。所以在實(shí)踐中使用那種衡量方式并不會(huì)對(duì)判斷的影響太大。
使用 Early stopping 來(lái)確定訓(xùn)練的回合數(shù):正如我們?cè)诒菊虑懊嬗懻摰哪菢樱珽arly stopping 表示在每個(gè)回合的最后,我們都要計(jì)算驗(yàn)證集上的分類(lèi)準(zhǔn)確度。當(dāng)準(zhǔn)確度不再提升,就終止它。這讓選擇回合數(shù)變得很簡(jiǎn)單。特別地,也意味著我們不再需要擔(dān)心顯式地掌握回合數(shù)和其他超參數(shù)的關(guān)聯(lián)。而且,這個(gè)過(guò)程還是自動(dòng)的。另外,Early stopping 也能夠幫助我們避免過(guò)匹配。盡管在實(shí)驗(yàn)前期不采用 Early stopping,這樣可以看到任何過(guò)匹配的信號(hào),使用這些來(lái)選擇規(guī)范化方法,但 early stopping 仍然是一件很棒的事。
我們需要再明確一下什么叫做分類(lèi)準(zhǔn)確度不再提升,這樣方可實(shí)現(xiàn) Early stopping。正如我們已經(jīng)看到的,分類(lèi)準(zhǔn)確度在整體趨勢(shì)下降的時(shí)候仍舊會(huì)抖動(dòng)或者震蕩。如果我們?cè)跍?zhǔn)確度剛開(kāi)始下降的時(shí)候就停止,那么肯定會(huì)錯(cuò)過(guò)更好的選擇。一種不錯(cuò)的解決方案是如果分類(lèi)準(zhǔn)確度在一段時(shí)間內(nèi)不再提升的時(shí)候終止。例如,我們要解決 MNIST 問(wèn)題。如果分類(lèi)準(zhǔn)確度在近 $$10$$ 個(gè)回合都沒(méi)有提升的時(shí)候,我們將其終止。這樣不僅可以