本章的主題是神經網絡的學習。這里所說的“學習”是指從訓練數據中自動獲取最優權重參數的過程。本章中,為了使神經網絡能進行學習,將導入損失函數這一指標。而學習的目的就是以該損失函數為基准,找出能使它的值達到最小的權重參數。為了找出盡可能小的損失函數的值,本章我們將介紹利用了函數斜率的梯度法。
4.1 從數據中學習
神經網絡的特征就是可以從數據中學習。所謂“從數據中學習”,是指可以由數據自動決定權重參數的值。這是非常了不起的事情!因為如果所有的參數都需要人工決定的話,工作量就太大了。在第2章介紹的感知機的例子中,我們對照着真值表,人工設定了參數的值,但是那時的參數只有3個。而在實際的神經網絡中,參數的數量成千上萬,在層數更深的深度學習中,參數的數量甚至可以上億,想要人工決定這些參數的值是不可能的。本章將介紹神經網絡的學習,即利用數據決定參數值的方法,並用Python實現對MNIST手寫數字數據集的學習。
對於線性可分問題,第2章的感知機是可以利用數據自動學習的。根據“感知機收斂定理”,通過有限次數的學習,線性可分問題是可解的。但是,非線性可分問題則無法通過(自動)學習來解決。
4.1.1 數據驅動
數據是機器學習的命根子。從數據中尋找答案、從數據中發現模式、根據數據講故事……這些機器學習所做的事情,如果沒有數據的話,就無從談起。因此,數據是機器學習的核心。這種數據驅動的方法,也可以說脫離了過往以人為中心的方法。
通常要解決某個問題,特別是需要發現某種模式時,人們一般會綜合考慮各種因素后再給出回答。“這個問題好像有這樣的規律性?”“不對,可能原因在別的地方。”——類似這樣,人們以自己的經驗和直覺為線索,通過反復試驗推進工作。而機器學習的方法則極力避免人為介入,嘗試從收集到的數據中發現答案(模式)。神經網絡或深度學習則比以往的機器學習方法更能避免人為介入。
現在我們來思考一個具體的問題,比如如何實現數字“5”的識別。數字5是圖4-1所示的手寫圖像,我們的目標是實現能區別是否是5的程序。這個問題看起來很簡單,大家能想到什么樣的算法呢?

圖4-1 手寫數字5的例子:寫法因人而異,五花八門
如果讓我們自己來設計一個能將5正確分類的程序,就會意外地發現這是一個很難的問題。人可以簡單地識別出5,但卻很難明確說出是基於何種規律而識別出了5。此外,從圖4-1中也可以看到,每個人都有不同的寫字習慣,要發現其中的規律是一件非常難的工作。
因此,與其絞盡腦汁,從零開始想出一個可以識別5的算法,不如考慮通過有效利用數據來解決這個問題。一種方案是,先從圖像中提取特征量,再用機器學習技術學習這些特征量的模式。這里所說的“特征量”是指可以從輸入數據(輸入圖像)中准確地提取本質數據(重要的數據)的轉換器。圖像的特征量通常表示為向量的形式。在計算機視覺領域,常用的特征量包括SIFT、SURF和HOG等。使用這些特征量將圖像數據轉換為向量,然后對轉換后的向量使用機器學習中的SVM、KNN等分類器進行學習。
機器學習的方法中,由機器從收集到的數據中找出規律性。與從零開始想出算法相比,這種方法可以更高效地解決問題,也能減輕人的負擔。但是需要注意的是,將圖像轉換為向量時使用的特征量仍是由人設計的。對於不同的問題,必須使用合適的特征量(必須設計專門的特征量),才能得到好的結果。比如,為了區分狗的臉部,人們需要考慮與用於識別5的特征量不同的其他特征量。也就是說,即使使用特征量和機器學習的方法,也需要針對不同的問題人工考慮合適的特征量。
到這里,我們介紹了兩種針對機器學習任務的方法。將這兩種方法用圖來表示,如圖4-2 所示。圖中還展示了神經網絡(深度學習)的方法,可以看出該方法不存在人為介入。
如圖4-2 所示,神經網絡直接學習圖像本身。在第2個方法,即利用特征量和機器學習的方法中,特征量仍是由人工設計的,而在神經網絡中,連圖像中包含的重要特征量也都是由機器來學習的。

圖4-2 從人工設計規則轉變為由機器從數據中學習:沒有人為介入的方塊用灰色表示
深度學習有時也稱為端到端機器學習(end-to-end machinelearning)。這里所說的端到端是指從一端到另一端的意思,也就是從原始數據(輸入)中獲得目標結果(輸出)的意思。
神經網絡的優點是對所有的問題都可以用同樣的流程來解決。比如,不管要求解的問題是識別5,還是識別狗,抑或是識別人臉,神經網絡都是通過不斷地學習所提供的數據,嘗試發現待求解的問題的模式。也就是說,與待處理的問題無關,神經網絡可以將數據直接作為原始數據,進行“端對端”的學習。
4.1.2 訓練數據和測試數據
本章主要介紹神經網絡的學習,不過在這之前,我們先來介紹一下機器學習中有關數據處理的一些注意事項。
機器學習中,一般將數據分為訓練數據和測試數據兩部分來進行學習和實驗等。首先,使用訓練數據進行學習,尋找最優的參數;然后,使用測試數據評價訓練得到的模型的實際能力。為什么需要將數據分為訓練數據和測試數據呢?因為我們追求的是模型的泛化能力。為了正確評價模型的泛化能力,就必須划分訓練數據和測試數據。另外,訓練數據也可以稱為監督數據。
泛化能力是指處理未被觀察過的數據(不包含在訓練數據中的數據)的能力。獲得泛化能力是機器學習的最終目標。比如,在識別手寫數字的問題中,泛化能力可能會被用在自動讀取明信片的郵政編碼的系統上。此時,手寫數字識別就必須具備較高的識別“某個人”寫的字的能力。注意這里不是“特定的某個人寫的特定的文字”,而是“任意一個人寫的任意文字”。如果系統只能正確識別已有的訓練數據,那有可能是只學習到了訓練數據中的個人的習慣寫法。
因此,僅僅用一個數據集去學習和評價參數,是無法進行正確評價的。這樣會導致可以順利地處理某個數據集,但無法處理其他數據集的情況。順便說一下,只對某個數據集過度擬合的狀態稱為過擬合(over fitting)。避免過擬合也是機器學習的一個重要課題。
4.2 損失函數
如果有人問你現在有多幸福,你會如何回答呢?一般的人可能會給出諸如“還可以吧”或者“不是那么幸福”等籠統的回答。如果有人回答“我現在的幸福指數是10.23”的話,可能會把人嚇一跳吧。因為他用一個數值指標來評判自己的幸福程度。
這里的幸福指數只是打個比方,實際上神經網絡的學習也在做同樣的事情。神經網絡的學習通過某個指標表示現在的狀態。然后,以這個指標為基准,尋找最優權重參數。和剛剛那位以幸福指數為指引尋找“最優人生”的人一樣,神經網絡以某個指標為線索尋找最優權重參數。神經網絡的學習中所用的指標稱為損失函數(loss function)。這個損失函數可以使用任意函數,但一般用均方誤差和交叉熵誤差等。
損失函數是表示神經網絡性能的“惡劣程度”的指標,即當前的神經網絡對監督數據在多大程度上不擬合,在多大程度上不一致。以“性能的惡劣程度”為指標可能會使人感到不太自然,但是如果給損失函數乘上一個負值,就可以解釋為“在多大程度上不壞”,即“性能有多好”。並且,“使性能的惡劣程度達到最小”和“使性能的優良程度達到最大”是等價的,不管是用“惡劣程度”還是“優良程度”,做的事情本質上都是一樣的。
4.2.1 均方誤差
可以用作損失函數的函數有很多,其中最有名的是均方誤差(mean squared error)。均方誤差如下式所示。

這里,yk是表示神經網絡的輸出,tk表示監督數據,k表示數據的維數。比如,在3.6 節手寫數字識別的例子中,yk、tk是由如下10 個元素構成的數據。
1 >>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] 2 >>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
數組元素的索引從第一個開始依次對應數字“0”“1”“2”…… 這里,神經網絡的輸出y是softmax函數的輸出。由於softmax函數的輸出可以理解為概率,因此上例表示“0”的概率是0.1,“1”的概率是0.05,“2”的概率是0.6等。t是監督數據,將正確解標簽設為1,其他均設為0。這里,標簽“2”為1,表示正確解是“2”。將正確解標簽表示為1,其他標簽表示為0的表示方法稱為one-hot表示。
如式(4.1)所示,均方誤差會計算神經網絡的輸出和正確解監督數據的各個元素之差的平方,再求總和。現在,我們用Python來實現這個均方誤差,實現方式如下所示。
1 def mean_squared_error(y, t): 2 return 0.5 * np.sum((y-t)**2)
這里,參數y 和t 是NumPy數組。代碼實現完全遵照式(4.1),因此不再具體說明。現在,我們使用這個函數,來實際地計算一下。
1 >>> # 設“2”為正確解 2 >>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] 3 >>> 4 >>> # 例1:“2”的概率最高的情況(0.6) 5 >>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] 6 >>> mean_squared_error(np.array(y), np.array(t)) 7 0.097500000000000031 8 >>> 9 >>> # 例2:“7”的概率最高的情況(0.6) 10 >>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] 11 >>> mean_squared_error(np.array(y), np.array(t)) 12 0.59750000000000003
這里舉了兩個例子。第一個例子中,正確解是“2”,神經網絡的輸出的最大值是“2”;第二個例子中,正確解是“2”,神經網絡的輸出的最大值是“7”。如實驗結果所示,我們發現第一個例子的損失函數的值更小,和監督數據之間的誤差較小。也就是說,均方誤差顯示第一個例子的輸出結果與監督數據更加吻合。
4.2.2 交叉熵誤差
除了均方誤差之外,交叉熵誤差(cross entropy error)也經常被用作損失函數。交叉熵誤差如下式所示。
這里,log表示以e為底數的自然對數(loge)。yk是神經網絡的輸出,tk是正確解標簽。並且,tk中只有正確解標簽的索引為1,其他均為0(one-hot表示)。因此,式(4.2)實際上只計算對應正確解標簽的輸出的自然對數。比如,假設正確解標簽的索引是“2”,與之對應的神經網絡的輸出是0.6,則交叉熵誤差是−log0.6 = 0.51;若“2”對應的輸出是0.1,則交叉熵誤差為−log0.1 = 2.30。也就是說,交叉熵誤差的值是由正確解標簽所對應的輸出結果決定的。
自然對數的圖像如圖4-3 所示。

圖4-3 自然對數y = log x的圖像
如圖4-3 所示,x等於1 時,y 為0;隨着x向0 靠近,y 逐漸變小。因此,正確解標簽對應的輸出越大,式(4.2)的值越接近0;當輸出為1 時,交叉熵誤差為0。此外,如果正確解標簽對應的輸出較小,則式(4.2)的值較大。
下面,我們來用代碼實現交叉熵誤差。
1 def cross_entropy_error(y, t): 2 delta = 1e-7 3 return -np.sum(t * np.log(y + delta))
這里,參數y和t是NumPy數組。函數內部在計算np.log時,加上了一個微小值delta。這是因為,當出現np.log(0)時,np.log(0)會變為負無限大的-inf,這樣一來就會導致后續計算無法進行。作為保護性對策,添加一個微小值可以防止負無限大的發生。下面,我們使用cross_entropy_error(y, t)進行一些簡單的計算。
1 >>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] 2 >>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] 3 >>> cross_entropy_error(np.array(y), np.array(t)) 4 0.51082545709933802 5 >>> 6 >>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] 7 >>> cross_entropy_error(np.array(y), np.array(t)) 8 2.3025840929945458
第一個例子中,正確解標簽對應的輸出為0.6,此時的交叉熵誤差大約為0.51。第二個例子中,正確解標簽對應的輸出為0.1 的低值,此時的交叉熵誤差大約為2.3。由此可以看出,這些結果與我們前面討論的內容是一致的。
4.2.3 mini-batch學習
機器學習使用訓練數據進行學習。使用訓練數據進行學習,嚴格來說,就是針對訓練數據計算損失函數的值,找出使該值盡可能小的參數。因此,計算損失函數時必須將所有的訓練數據作為對象。也就是說,如果訓練數據有100個的話,我們就要把這100個損失函數的總和作為學習的指標。
前面介紹的損失函數的例子中考慮的都是針對單個數據的損失函數。如果要求所有訓練數據的損失函數的總和,以交叉熵誤差為例,可以寫成下面的式(4.3)。

這里, 假設數據有N個,tnk表示第n個數據的第k個元素的值(ynk是神經網絡的輸出,tnk是監督數據)。式子雖然看起來有一些復雜,其實只是把求單個數據的損失函數的式(4.2)擴大到了N份數據,不過最后還要除以N進行正規化。通過除以N,可以求單個數據的“平均損失函數”。通過這樣的平均化,可以獲得和訓練數據的數量無關的統一指標。比如,即便訓練數據有1000個或10000個,也可以求得單個數據的平均損失函數。
另外,MNIST數據集的訓練數據有60000個,如果以全部數據為對象求損失函數的和,則計算過程需要花費較長的時間。再者,如果遇到大數據,數據量會有幾百萬、幾千萬之多,這種情況下以全部數據為對象計算損失函數是不現實的。因此,我們從全部數據中選出一部分,作為全部數據的“近似”。神經網絡的學習也是從訓練數據中選出一批數據(稱為mini-batch, 小批量),然后對每個mini-batch進行學習。比如,從60000個訓練數據中隨機選擇100筆,再用這100筆數據進行學習。這種學習方式稱為mini-batch學習。
下面我們來編寫從訓練數據中隨機選擇指定個數的數據的代碼,以進行mini-batch學習。在這之前,先來看一下用於讀入MNIST數據集的代碼。
1 import sys, os 2 sys.path.append(os.pardir) 3 import numpy as np 4 from dataset.mnist import load_mnist 5 6 (x_train, t_train), (x_test, t_test) = \ 7 load_mnist(normalize=True, one_hot_label=True) 8 9 print(x_train.shape) # (60000, 784) 10 print(t_train.shape) # (60000, 10)
第3章介紹過,load_mnist函數是用於讀入MNIST數據集的函數。這個函數在本書提供的腳本dataset/mnist.py中,它會讀入訓練數據和測試數據。讀入數據時,通過設定參數one_hot_label=True,可以得到one-hot表示(即僅正確解標簽為1,其余為0的數據結構)。
讀入上面的MNIST數據后,訓練數據有60000個,輸入數據是784維(28 × 28)的圖像數據,監督數據是10維的數據。因此,上面的x_train、t_train的形狀分別是(60000, 784)和(60000, 10)。
那么,如何從這個訓練數據中隨機抽取10筆數據呢?我們可以使用NumPy的np.random.choice(),寫成如下形式。
1 train_size = x_train.shape[0] 2 batch_size = 10 3 batch_mask = np.random.choice(train_size, batch_size) 4 x_batch = x_train[batch_mask] 5 t_batch = t_train[batch_mask]
使用np.random.choice()可以從指定的數字中隨機選擇想要的數字。比如,np.random.choice(60000, 10)會從0到59999之間隨機選擇10個數字。如下面的實際代碼所示,我們可以得到一個包含被選數據的索引的數組。
1 >>> np.random.choice(60000, 10) 2 array([ 8013, 14666, 58210, 23832, 52091, 10153, 8107, 19410, 27260, 3 21411])
之后,我們只需指定這些隨機選出的索引,取出mini-batch,然后使用這個mini-batch 計算損失函數即可。
計算電視收視率時,並不會統計所有家庭的電視機,而是僅以那些被選中的家庭為統計對象。比如,通過從關東地區隨機選擇1000個家庭計算收視率,可以近似地求得關東地區整體的收視率。這1000個家庭的收視率,雖然嚴格上不等於整體的收視率,但可以作為整體的一個近似值。和收視率一樣,mini-batch的損失函數也是利用一部分樣本數據來近似地計算整體。也就是說,用隨機選擇的小批量數據(mini-batch)作為全體訓練數據的近似值。
4.2.4 mini-batch版交叉熵誤差的實現
如何實現對應mini-batch的交叉熵誤差呢?只要改良一下之前實現的對應單個數據的交叉熵誤差就可以了。這里,我們來實現一個可以同時處理單個數據和批量數據(數據作為batch集中輸入)兩種情況的函數。
1 def cross_entropy_error(y, t): 2 if y.ndim == 1: 3 t = t.reshape(1, t.size) 4 y = y.reshape(1, y.size) 5 6 batch_size = y.shape[0] 7 return -np.sum(t * np.log(y + 1e-7)) / batch_size
這里,y是神經網絡的輸出,t是監督數據。y的維度為1 時,即求單個數據的交叉熵誤差時,需要改變數據的形狀。並且,當輸入為mini-batch時,要用batch的個數進行正規化,計算單個數據的平均交叉熵誤差。
此外,當監督數據是標簽形式(非one-hot表示,而是像“2”“7”這樣的標簽)時,交叉熵誤差可通過如下代碼實現。
1 def cross_entropy_error(y, t): 2 if y.ndim == 1: 3 t = t.reshape(1, t.size) 4 y = y.reshape(1, y.size) 5 6 batch_size = y.shape[0] 7 return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
實現的要點是,由於one-hot表示中t為0的元素的交叉熵誤差也為0,因此針對這些元素的計算可以忽略。換言之,如果可以獲得神經網絡在正確解標簽處的輸出,就可以計算交叉熵誤差。因此,t為one-hot表示時通過t * np.log(y) 計算的地方,在t 為標簽形式時,可用np.log( y[np.arange(batch_size), t] )實現相同的處理(為了便於觀察,這里省略了微小值1e-7)。
作為參考,簡單介紹一下np.log( y[np.arange(batch_size), t] )。np.arange(batch_size)會生成一個從0到batch_size-1的數組。比如當batch_size為5
時,np.arange(batch_size) 會生成一個NumPy 數組[0, 1, 2, 3, 4]。因為t中標簽是以[2, 7, 0, 9, 4]的形式存儲的,所以y[np.arange(batch_size),t]能抽出各個數據的正確解標簽對應的神經網絡的輸出(在這個例子中,y[np.arange(batch_size), t] 會生成NumPy數組[y[0,2], y[1,7], y[2,0],y[3,9], y[4,4]])。
4.2.5 為何要設定損失函數
上面我們討論了損失函數,可能有人要問:“為什么要導入損失函數呢?”以數字識別任務為例,我們想獲得的是能提高識別精度的參數,特意再導入一個損失函數不是有些重復勞動嗎?也就是說,既然我們的目標是獲得使識別精度盡可能高的神經網絡,那不是應該把識別精度作為指標嗎?
對於這一疑問,我們可以根據“導數”在神經網絡學習中的作用來回答。下一節中會詳細說到,在神經網絡的學習中,尋找最優參數(權重和偏置)時,要尋找使損失函數的值盡可能小的參數。為了找到使損失函數的值盡可能小的地方,需要計算參數的導數(確切地講是梯度),然后以這個導數為指引,逐步更新參數的值。
假設有一個神經網絡,現在我們來關注這個神經網絡中的某一個權重參數。此時,對該權重參數的損失函數求導,表示的是“如果稍微改變這個權重參數的值,損失函數的值會如何變化”。如果導數的值為負,通過使該權重參數向正方向改變,可以減小損失函數的值;反過來,如果導數的值為正,則通過使該權重參數向負方向改變,可以減小損失函數的值。不過,當導數的值為0 時,無論權重參數向哪個方向變化,損失函數的值都不會改變,此時該權重參數的更新會停在此處。
之所以不能用識別精度作為指標,是因為這樣一來絕大多數地方的導數都會變為0,導致參數無法更新。話說得有點多了,我們來總結一下上面的內容。
在進行神經網絡的學習時,不能將識別精度作為指標。因為如果以識別精度為指標,則參數的導數在絕大多數地方都會變為0。
為什么用識別精度作為指標時,參數的導數在絕大多數地方都會變成0呢?為了回答這個問題,我們來思考另一個具體例子。假設某個神經網絡正確識別出了100筆訓練數據中的32筆,此時識別精度為32%。如果以識別精度為指標,即使稍微改變權重參數的值,識別精度也仍將保持在32%,不會出現變化。也就是說,僅僅微調參數,是無法改善識別精度的。即便識別精度有所改善,它的值也不會像32.0123 . . .%這樣連續變化,而是變為33%、34%這樣的不連續的、離散的值。而如果把損失函數作為指標,則當前損失函數的值可以表示為0.92543 . . . 這樣的值。並且,如果稍微改變一下參數的值,對應的損失函數也會像0.93432 . . . 這樣發生連續性的變化。
識別精度對微小的參數變化基本上沒有什么反應,即便有反應,它的值也是不連續地、突然地變化。作為激活函數的階躍函數也有同樣的情況。出於相同的原因,如果使用階躍函數作為激活函數,神經網絡的學習將無法進行。如圖4-4所示,階躍函數的導數在絕大多數地方(除了0 以外的地方)均為0。也就是說,如果使用了階躍函數,那么即便將損失函數作為指標,參數的微小變化也會被階躍函數抹殺,導致損失函數的值不會產生任何變化。
階躍函數就像“竹筒敲石”一樣,只在某個瞬間產生變化。而sigmoid函數,如圖4-4所示,不僅函數的輸出(豎軸的值)是連續變化的,曲線的斜率(導數)也是連續變化的。也就是說,sigmoid函數的導數在任何地方都不為0。這對神經網絡的學習非常重要。得益於這個斜率不會為0的性質,神經網絡的學習得以正確進行。

圖4-4 階躍函數和sigmoid函數:階躍函數的斜率在絕大多數地方都為0,而sigmoid函數的斜率(切線)不會為0
4.3 數值微分
梯度法使用梯度的信息決定前進的方向。本節將介紹梯度是什么、有什么性質等內容。在這之前,我們先來介紹一下導數。
4.3.1 導數
假如你是全程馬拉松選手,在開始的10分鍾內跑了2千米。如果要計算此時的奔跑速度,則為2/10 = 0.2[千米/分]。也就是說,你以1分鍾前進0.2千米的速度(變化)奔跑。
在這個馬拉松的例子中,我們計算了“奔跑的距離”相對於“時間”發生了多大變化。不過,這個10分鍾跑2千米的計算方式,嚴格地講,計算的是10分鍾內的平均速度。而導數表示的是某個瞬間的變化量。因此,將10分鍾這一時間段盡可能地縮短,比如計算前1分鍾奔跑的距離、前1秒鍾奔跑的距離、前0.1秒鍾奔跑的距離……這樣就可以獲得某個瞬間的變化量(某個瞬時速度)。
綜上,導數就是表示某個瞬間的變化量。它可以定義成下面的式子。
![]()
式(4.4)表示的是函數的導數。左邊的符號
表示f(x)關於x的導數,即f(x)相對於x的變化程度。式(4.4)表示的導數的含義是,x的“微小變化”將導致函數f(x)的值在多大程度上發生變化。其中,表示微小變化的h無限趨近0,表示為
。
接下來,我們參考式(4.4),來實現求函數的導數的程序。如果直接實現式(4.4)的話,向h中賦入一個微小值,就可以計算出來了。比如,下面的實現如何?
1 # 不好的實現示例 2 def numerical_diff(f, x): 3 h = 10e-50 4 return (f(x+h) - f(x)) / h
函數numerical_diff(f, x) 的名稱來源於數值微分①的英文numerical differentiation。這個函數有兩個參數,即“函數f”和“傳給函數f的參數x”。乍一看這個實現沒有問題,但是實際上這段代碼有兩處需要改進的地方。
在上面的實現中,因為想把盡可能小的值賦給h(可以話,想讓h無限接近0),所以h使用了10e-50(有50 個連續的0 的“0.00 . . . 1”)這個微小值。但是,這樣反而產生了舍入誤差(rounding error)。所謂舍入誤差,是指因省略小數的精細部分的數值(比如,小數點第8位以后的數值)而造成最終的計算結果上的誤差。比如,在Python中,舍入誤差可如下表示。
1 >>> np.float32(1e-50) 2 0.0
如上所示,如果用float32類型(32 位的浮點數)來表示1e-50,就會變成0.0,無法正確表示出來。也就是說,使用過小的值會造成計算機出現計算上的問題。這是第一個需要改進的地方,即將微小值h改為10−4。使用10−4就可以得到正確的結果。
第二個需要改進的地方與函數f的差分有關。雖然上述實現中計算了函數f在x+h和x之間的差分,但是必須注意到,這個計算從一開始就有誤差。如圖4-5 所示,“真的導數”對應函數在x處的斜率(稱為切線),但上述實現中計算的導數對應的是(x + h)和x之間的斜率。因此,真的導數(真的切線)和上述實現中得到的導數的值在嚴格意義上並不一致。這個差異的出現是因為h不可能無限接近0。
如圖4-5所示,數值微分含有誤差。為了減小這個誤差,我們可以計算函數f 在(x + h) 和(x − h) 之間的差分。因為這種計算方法以x 為中心,計算它左右兩邊的差分,所以也稱為中心差分(而(x + h) 和x之間的差分稱為前向差分)。下面,我們基於上述兩個要改進的點來實現數值微分(數值梯度)。

圖4-5 真的導數(真的切線)和數值微分(近似切線)的值不同
1 def numerical_diff(f, x): 2 h = 1e-4 # 0.0001 3 return (f(x+h) - f(x-h)) / (2*h)
如上所示,利用微小的差分求導數的過程稱為數值微分(numerical differentiation)。而基於數學式的推導求導數的過程,則用“解析性”(analytic)一詞,稱為“解析性求解”或者“解析性求導”。比如,y = x2 的導數,可以通過
解析性地求解出來。因此,當x = 2時,y的導數為4。解析性求導得到的導數是不含誤差的“真的導數”。
4.3.2 數值微分的例子
現在我們試着用上述的數值微分對簡單函數進行求導。先來看一個由下式表示的2次函數。
![]()
用Python來實現式(4.5),如下所示。
1 def function_1(x): 2 return 0.01*x**2 + 0.1*x
接下來,我們來繪制這個函數的圖像。畫圖所用的代碼如下,生成的圖像如圖4-6 所示。
1 import numpy as np 2 import matplotlib.pylab as plt 3 4 x = np.arange(0.0, 20.0, 0.1) # 以0.1為單位,從0到20的數組x 5 y = function_1(x) 6 plt.xlabel("x") 7 plt.ylabel("f(x)") 8 plt.plot(x, y) 9 plt.show()

圖4-6 f(x) = 0.01x2 + 0.1x的圖像
我們來計算一下這個函數在x = 5 和x = 10 處的導數。
>>> numerical_diff(function_1, 5) 0.1999999999990898 >>> numerical_diff(function_1, 10) 0.2999999999986347
這里計算的導數是f(x) 相對於x 的變化量,對應函數的斜率。另外,f(x) = 0.01x2 + 0.1x 的解析解是
。因此,在x = 5 和x = 10 處,“真的導數”分別為0.2 和0.3。和上面的結果相比,我們發現雖然嚴格意義上它們並不一致,但誤差非常小。實際上,誤差小到基本上可以認為它們是相等的。
現在,我們用上面的數值微分的值作為斜率,畫一條直線。結果如圖4-7所示,可以確認這些直線確實對應函數的切線。

圖4-7 x = 5、x = 10 處的切線:直線的斜率使用數值微分的值
4.3.3 偏導數
接下來,我們看一下式(4.6) 表示的函數。雖然它只是一個計算參數的平方和的簡單函數,但是請注意和上例不同的是,這里有兩個變量。
![]()
這個式子可以用Python來實現,如下所示。
1 def function_2(x): 2 return x[0]**2 + x[1]**2 3 # 或者return np.sum(x**2)
這里,我們假定向參數輸入了一個NumPy數組。函數的內部實現比較簡單,先計算NumPy數組中各個元素的平方,再求它們的和(np.sum(x**2)也可以實現同樣的處理)。我們來畫一下這個函數的圖像。結果如圖4-8 所示,是一個三維圖像。

圖4-8
的圖像
現在我們來求式(4.6)的導數。這里需要注意的是,式(4.6)有兩個變量,所以有必要區分對哪個變量求導數,即對x0 和x1 兩個變量中的哪一個求導數。另外,我們把這里討論的有多個變量的函數的導數稱為偏導數。用數學式表示的話,可以寫成
。
怎么求偏導數呢?我們先試着解一下下面兩個關於偏導數的問題。
問題1:求x0 = 3, x1 = 4 時,關於x0的偏導數
。
1 >>> def function_tmp1(x0): 2 ... return x0*x0 + 4.0**2.0 3 ... 4 >>> numerical_diff(function_tmp1, 3.0) 5 6.00000000000378
問題2:求x0 = 3, x1 = 4 時,關於x1 的偏導數
。
>>> def function_tmp2(x1): ... return 3.0**2.0 + x1*x1 ... >>> numerical_diff(function_tmp2, 4.0) 7.999999999999119
在這些問題中,我們定義了一個只有一個變量的函數,並對這個函數進行了求導。例如,問題1 中,我們定義了一個固定x1 = 4的新函數,然后對只有變量x0的函數應用了求數值微分的函數。從上面的計算結果可知,問題1的答案是6.00000000000378,問題2的答案是7.999999999999119,和解析解的導數基本一致。
像這樣,偏導數和單變量的導數一樣,都是求某個地方的斜率。不過,偏導數需要將多個變量中的某一個變量定為目標變量,並將其他變量固定為某個值。在上例的代碼中,為了將目標變量以外的變量固定到某些特定的值上,我們定義了新函數。然后,對新定義的函數應用了之前的求數值微分的函數,得到偏導數。
4.4 梯度
在剛才的例子中,我們按變量分別計算了x0和x1的偏導數。現在,我們希望一起計算x0 和x1的偏導數。比如,我們來考慮求x0 = 3, x1 = 4時(x0, x1)的偏導數
。另外,像
這樣的由全部變量的偏導數匯總而成的向量稱為梯度(gradient)。梯度可以像下面這樣來實現。
1 def numerical_gradient(f, x): 2 h = 1e-4 # 0.0001 3 grad = np.zeros_like(x) # 生成和x形狀相同的數組 4 5 for idx in range(x.size): 6 tmp_val = x[idx] 7 # f(x+h)的計算 8 x[idx] = tmp_val + h 9 fxh1 = f(x) 10 11 # f(x-h)的計算 12 x[idx] = tmp_val - h 13 fxh2 = f(x) 14 15 grad[idx] = (fxh1 - fxh2) / (2*h) 16 x[idx] = tmp_val # 還原值 17 return grad
函數numerical_gradient(f, x)的實現看上去有些復雜,但它執行的處理和求單變量的數值微分基本沒有區別。需要補充說明一下的是,np.zeros_like(x)會生成一個形狀和x相同、所有元素都為0的數組。
函數numerical_gradient(f, x)中,參數f為函數,x為NumPy數組,該函數對NumPy數組x的各個元素求數值微分。現在,我們用這個函數實際計算一下梯度。這里我們求點(3, 4)、(0, 2)、(3, 0) 處的梯度。
1 >>> numerical_gradient(function_2, np.array([3.0, 4.0])) 2 array([ 6., 8.])A 3 >>> numerical_gradient(function_2, np.array([0.0, 2.0])) 4 array([ 0., 4.]) 5 >>> numerical_gradient(function_2, np.array([3.0, 0.0])) 6 array([ 6., 0.])
像這樣,我們可以計算(x0, x1) 在各點處的梯度。上例中,點(3, 4)處的梯度是(6, 8)、點(0, 2)處的梯度是(0, 4)、點(3, 0) 處的梯度是(6, 0)。這個梯度意味着什么呢?為了更好地理解,我們把的梯度畫在圖上。不過,這里我們畫的是元素值為負梯度(后面我們將會看到,負梯度方向是梯度法中變量的更新方向)的向量。
如圖4-9 所示,
的梯度呈現為有向向量(箭頭)。觀察圖4-9,我們發現梯度指向函數f(x0,x1) 的“最低處”(最小值),就像指南針一樣,所有的箭頭都指向同一點。其次,我們發現離“最低處”越遠,箭頭越大。

圖4-9
的梯度
雖然圖4-9 中的梯度指向了最低處,但並非任何時候都這樣。實際上,梯度會指向各點處的函數值降低的方向。更嚴格地講,梯度指示的方向是各點處的函數值減小最多的方向。這是一個非常重要的性質,請一定牢記!(高等數學告訴我們,方向導數= cos(θ) × 梯度(θ是方向導數的方向與梯度方向的夾角)。因此,所有的下降方向中,梯度方向下降最多)
4.4.1 梯度法
機器學習的主要任務是在學習時尋找最優參數。同樣地,神經網絡也必須在學習時找到最優參數(權重和偏置)。這里所說的最優參數是指損失函數取最小值時的參數。但是,一般而言,損失函數很復雜,參數空間龐大,我們不知道它在何處能取得最小值。而通過巧妙地使用梯度來尋找函數最小值(或者盡可能小的值)的方法就是梯度法。
這里需要注意的是,梯度表示的是各點處的函數值減小最多的方向。因此,無法保證梯度所指的方向就是函數的最小值或者真正應該前進的方向。實際上,在復雜的函數中,梯度指示的方向基本上都不是函數值最小處。
函數的極小值、最小值以及被稱為鞍點(saddle point)的地方,梯度為0。極小值是局部最小值,也就是限定在某個范圍內的最小值。鞍點是從某個方向上看是極大值,從另一個方向上看則是極小值的點。雖然梯度法是要尋找梯度為0的地方,但是那個地方不一定就是最小值(也有可能是極小值或者鞍點)。此外,當函數很復雜且呈扁平狀時,學習可能會進入一個(幾乎)平坦的地區,陷入被稱為“學習高原”的無法前進的停滯期。
雖然梯度的方向並不一定指向最小值,但沿着它的方向能夠最大限度地減小函數的值。因此,在尋找函數的最小值(或者盡可能小的值)的位置的任務中,要以梯度的信息為線索,決定前進的方向。
此時梯度法就派上用場了。在梯度法中,函數的取值從當前位置沿着梯度方向前進一定距離,然后在新的地方重新求梯度,再沿着新梯度方向前進,如此反復,不斷地沿梯度方向前進。像這樣,通過不斷地沿梯度方向前進,逐漸減小函數值的過程就是梯度法(gradient method)。梯度法是解決機器學習中最優化問題的常用方法,特別是在神經網絡的學習中經常被使用。
根據目的是尋找最小值還是最大值,梯度法的叫法有所不同。嚴格地講,尋找最小值的梯度法稱為梯度下降法(gradient descent method),尋找最大值的梯度法稱為梯度上升法(gradient ascent method)。但是通過反轉損失函數的符號,求最小值的問題和求最大值的問題會變成相同的問題,因此“下降”還是“上升”的差異本質上並不重要。一般來說,神經網絡(深度學習)中,梯度法主要是指梯度下降法。
現在,我們嘗試用數學式來表示梯度法,如式(4.7)所示。

式(4.7)的η 表示更新量,在神經網絡的學習中,稱為學習率(learning rate)。學習率決定在一次學習中,應該學習多少,以及在多大程度上更新參數。
式(4.7)是表示更新一次的式子,這個步驟會反復執行。也就是說,每一步都按式(4.7)更新變量的值,通過反復執行此步驟,逐漸減小函數值。雖然這里只展示了有兩個變量時的更新過程,但是即便增加變量的數量,也可以通過類似的式子(各個變量的偏導數)進行更新。
學習率需要事先確定為某個值,比如0.01 或0.001。一般而言,這個值過大或過小,都無法抵達一個“好的位置”。在神經網絡的學習中,一般會一邊改變學習率的值,一邊確認學習是否正確進行了。
下面,我們用Python來實現梯度下降法。如下所示,這個實現很簡單。
1 def gradient_descent(f, init_x, lr=0.01, step_num=100): 2 x = init_x 3 4 for i in range(step_num): 5 grad = numerical_gradient(f, x) 6 x -= lr * grad 7 8 return x
參數f是要進行最優化的函數,init_x 是初始值,lr 是學習率learning rate,step_num 是梯度法的重復次數。numerical_gradient(f,x) 會求函數的梯度,用該梯度乘以學習率得到的值進行更新操作,由step_num 指定重復的次數。
使用這個函數可以求函數的極小值,順利的話,還可以求函數的最小值。下面,我們就來嘗試解決下面這個問題。
問題:請用梯度法求
的最小值。
1 >>> def function_2(x): 2 ... return x[0]**2 + x[1]**2 3 ... 4 >>> init_x = np.array([-3.0, 4.0]) 5 >>> gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100) 6 array([ -6.11110793e-10, 8.14814391e-10])
這里,設初始值為(-3.0, 4.0),開始使用梯度法尋找最小值。最終的結果是(-6.1e-10, 8.1e-10),非常接近(0,0)。實際上,真的最小值就是(0,0),所以說通過梯度法我們基本得到了正確結果。如果用圖來表示梯度法的更新過程,則如圖4-10 所示。可以發現,原點處是最低的地方,函數的取值一點點在向其靠近。這個圖的源代碼在ch04/gradient_method.py 中(但ch04/gradient_method.py不顯示表示等高線的虛線)。

圖4-10
的梯度法的更新過程:虛線是函數的等高線
前面說過,學習率過大或者過小都無法得到好的結果。我們來做個實驗驗證一下。
1 # 學習率過大的例子:lr=10.0 2 >>> init_x = np.array([-3.0, 4.0]) 3 >>> gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100) 4 array([ -2.58983747e+13, -1.29524862e+12]) 5 6 # 學習率過小的例子:lr=1e-10 7 >>> init_x = np.array([-3.0, 4.0]) 8 >>> gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100) 9 array([-2.99999994, 3.99999992])
實驗結果表明,學習率過大的話,會發散成一個很大的值;反過來,學習率過小的話,基本上沒怎么更新就結束了。也就是說,設定合適的學習率是一個很重要的問題。
像學習率這樣的參數稱為超參數。這是一種和神經網絡的參數(權重和偏置)性質不同的參數。相對於神經網絡的權重參數是通過訓練數據和學習算法自動獲得的,學習率這樣的超參數則是人工設定的。一般來說,超參數需要嘗試多個值,以便找到一種可以使學習順利進行的設定。
4.4.2 神經網絡的梯度
神經網絡的學習也要求梯度。這里所說的梯度是指損失函數關於權重參數的梯度。比如,有一個只有一個形狀為2 ×3的權重W的神經網絡,損失函數用L表示。此時,梯度可以用
表示。用數學式表示的話,如下所示。

的元素由各個元素關於W的偏導數構成。比如,第1行第1列的元素
表示當w11稍微變化時,損失函數L會發生多大變化。這里的重點是,
的形狀和W相同。實際上,式(4.8)中的W和
都是2 × 3 的形狀。
下面,我們以一個簡單的神經網絡為例,來實現求梯度的代碼。為此,我們要實現一個名為simpleNet 的類(源代碼在ch04/gradient_simplenet.py中)。
1 import sys, os 2 sys.path.append(os.pardir) 3 import numpy as np 4 from common.functions import softmax, cross_entropy_error 5 from common.gradient import numerical_gradient 6 7 class simpleNet: 8 def __init__(self): 9 self.W = np.random.randn(2,3) # 用高斯分布進行初始化 10 11 def predict(self, x): 12 return np.dot(x, self.W) 13 14 def loss(self, x, t): 15 z = self.predict(x) 16 y = softmax(z) 17 loss = cross_entropy_error(y, t) 18 19 return loss
這里使用了common/functions.py 中的softmax和cross_entropy_error 方法,以及common/gradient.py中的numerical_gradient方法。simpleNet類只有一個實例變量,即形狀為2×3 的權重參數。它有兩個方法,一個是用於預測的predict(x),另一個是用於求損失函數值的loss(x,t)。這里參數x接收輸入數據,t接收正確解標簽。現在我們來試着用一下這個simpleNet。
1 >>> net = simpleNet() 2 >>> print(net.W) # 權重參數 3 [[ 0.47355232 0.9977393 0.84668094], 4 [ 0.85557411 0.03563661 0.69422093]]) 5 >>> 6 >>> x = np.array([0.6, 0.9]) 7 >>> p = net.predict(x) 8 >>> print(p) 9 [ 1.05414809 0.63071653 1.1328074] 10 >>> np.argmax(p) # 最大值的索引 11 2 12 >>> 13 >>> t = np.array([0, 0, 1]) # 正確解標簽 14 >>> net.loss(x, t) 15 0.92806853663411326
接下來求梯度。和前面一樣,我們使用numerical_gradient(f, x)求梯度(這里定義的函數f(W)的參數W是一個偽參數。因為numerical_gradient(f,x)會在內部執行f(x), 為了與之兼容而定義了f(W))。
1 >>> def f(W): 2 ... return net.loss(x, t) 3 ... 4 >>> dW = numerical_gradient(f, net.W) 5 >>> print(dW) 6 [[ 0.21924763 0.14356247 -0.36281009] 7 [ 0.32887144 0.2153437 -0.54421514]]
numerical_gradient(f, x) 的參數f是函數,x是傳給函數f的參數。因此,這里參數x取net.W,並定義一個計算損失函數的新函數f,然后把這個新定義的函數傳遞給numerical_gradient(f, x)。
numerical_gradient(f, net.W)的結果是dW,一個形狀為2 × 3的二維數組。觀察一下dW的內容,例如,會發現
中的
的值大約是0.2,這表示如果將w11增加h,那么損失函數的值會增加0.2h。再如,
對應的值大約是−0.5,這表示如果將w23增加h,損失函數的值將減小0.5h。因此,從減小損失函數值的觀點來看,w23應向正方向更新,w11應向負方向更新。至於更新的程度,w23比w11的貢獻要大。
另外,在上面的代碼中,定義新函數時使用了“def f(x):···”的形式。實際上,Python中如果定義的是簡單的函數,可以使用lambda表示法。使用lambda的情況下,上述代碼可以如下實現。
1 >>> f = lambda w: net.loss(x, t) 2 >>> dW = numerical_gradient(f, net.W)
求出神經網絡的梯度后,接下來只需根據梯度法,更新權重參數即可。在下一節中,我們會以2 層神經網絡為例,實現整個學習過程。
為了對應形狀為多維數組的權重參數W,這里使用的numerical_gradient()和之前的實現稍有不同。不過,改動只是為了對應多維數組,所以改動並不大。這里省略了對代碼的說明,想知道細節的讀者請參考源代碼(common/gradient.py)。
4.5 學習算法的實現
關於神經網絡學習的基礎知識,到這里就全部介紹完了。“損失函數”“mini-batch”“梯度”“梯度下降法”等關鍵詞已經陸續登場,這里我們來確認一下神經網絡的學習步驟,順便復習一下這些內容。神經網絡的學習步驟如下所示。
前提
神經網絡存在合適的權重和偏置,調整權重和偏置以便擬合訓練數據的過程稱為“學習”。神經網絡的學習分成下面4 個步驟。
步驟1(mini-batch)
從訓練數據中隨機選出一部分數據,這部分數據稱為mini-batch。我們的目標是減小mini-batch 的損失函數的值。
步驟2(計算梯度)
為了減小mini-batch 的損失函數的值,需要求出各個權重參數的梯度。梯度表示損失函數的值減小最多的方向。
步驟3(更新參數)
將權重參數沿梯度方向進行微小更新。
步驟4(重復)
重復步驟1、步驟2、步驟3。
神經網絡的學習按照上面4個步驟進行。這個方法通過梯度下降法更新參數,不過因為這里使用的數據是隨機選擇的mini batch數據,所以又稱為隨機梯度下降法(stochastic gradient descent)。“隨機”指的是“隨機選擇的”的意思,因此,隨機梯度下降法是“對隨機選擇的數據進行的梯度下降法”。深度學習的很多框架中,隨機梯度下降法一般由一個名為SGD的函數來實現。SGD來源於隨機梯度下降法的英文名稱的首字母。
下面,我們來實現手寫數字識別的神經網絡。這里以2層神經網絡(隱藏層為1層的網絡)為對象,使用MNIST數據集進行學習。
4.5.1 2層神經網絡的類
首先,我們將這個2 層神經網絡實現為一個名為TwoLayerNet的類,實現過程如下所示。源代碼在ch04/two_layer_net.py中。
1 import sys, os 2 sys.path.append(os.pardir) 3 from common.functions import * 4 from common.gradient import numerical_gradient 5 6 class TwoLayerNet: 7 8 def __init__(self, input_size, hidden_size, output_size, 9 weight_init_std=0.01): 10 # 初始化權重 11 self.params = {} 12 self.params['W1'] = weight_init_std * \ 13 np.random.randn(input_size, hidden_size) 14 self.params['b1'] = np.zeros(hidden_size) 15 self.params['W2'] = weight_init_std * \ 16 np.random.randn(hidden_size, output_size) 17 self.params['b2'] = np.zeros(output_size) 18 19 def predict(self, x): 20 W1, W2 = self.params['W1'], self.params['W2'] 21 b1, b2 = self.params['b1'], self.params['b2'] 22 23 a1 = np.dot(x, W1) + b1 24 z1 = sigmoid(a1) 25 a2 = np.dot(z1, W2) + b2 26 y = softmax(a2) 27 28 return y 29 30 # x:輸入數據, t:監督數據 31 def loss(self, x, t): 32 y = self.predict(x) 33 34 return cross_entropy_error(y, t) 35 36 def accuracy(self, x, t): 37 y = self.predict(x) 38 y = np.argmax(y, axis=1) 39 t = np.argmax(t, axis=1) 40 41 accuracy = np.sum(y == t) / float(x.shape[0]) 42 return accuracy 43 44 # x:輸入數據, t:監督數據 45 def numerical_gradient(self, x, t): 46 loss_W = lambda W: self.loss(x, t) 47 48 grads = {} 49 grads['W1'] = numerical_gradient(loss_W, self.params['W1']) 50 grads['b1'] = numerical_gradient(loss_W, self.params['b1']) 51 grads['W2'] = numerical_gradient(loss_W, self.params['W2']) 52 grads['b2'] = numerical_gradient(loss_W, self.params['b2']) 53 54 return grads 55
雖然這個類的實現稍微有點長,但是因為和上一章的神經網絡的前向處理的實現有許多共通之處,所以並沒有太多新東西。我們先把這個類中用到的變量和方法整理一下。表4-1 中只羅列了重要的變量,表4-2 中則羅列了所有的方法。


TwoLayerNet類有params和grads兩個字典型實例變量。params變量中保存了權重參數,比如params['W1']以NumPy數組的形式保存了第1層的權重參數。此外,第1層的偏置可以通過param['b1']進行訪問。這里來看一個例子。
1 net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10) 2 net.params['W1'].shape # (784, 100) 3 net.params['b1'].shape # (100,) 4 net.params['W2'].shape # (100, 10) 5 net.params['b2'].shape # (10,)
如上所示,params 變量中保存了該神經網絡所需的全部參數。並且,params變量中保存的權重參數會用在推理處理(前向處理)中。順便說一下,推理處理的實現如下所示。
1 x = np.random.rand(100, 784) # 偽輸入數據(100筆) 2 y = net.predict(x)
此外,與params變量對應,grads變量中保存了各個參數的梯度。如下所示,使用numerical_gradient()方法計算梯度后,梯度的信息將保存在grads變量中。
1 x = np.random.rand(100, 784) # 偽輸入數據(100筆) 2 t = np.random.rand(100, 10) # 偽正確解標簽(100筆) 3 4 grads = net.numerical_gradient(x, t) # 計算梯度 5 6 grads['W1'].shape # (784, 100) 7 grads['b1'].shape # (100,) 8 grads['W2'].shape # (100, 10) 9 grads['b2'].shape # (10,)
接着,我們來看一下TwoLayerNet的方法的實現。首先是__init__(self,input_size, hidden_size, output_size)方法,它是類的初始化方法(所謂初始化方法,就是生成TwoLayerNet實例時被調用的方法)。從第1 個參數開始,依次表示輸入層的神經元數、隱藏層的神經元數、輸出層的神經元數。另外,因為進行手寫數字識別時,輸入圖像的大小是784(28 × 28),輸出為10 個類別,所以指定參數input_size=784、output_size=10,將隱藏層的個數hidden_size設置為一個合適的值即可。
此外,這個初始化方法會對權重參數進行初始化。如何設置權重參數的初始值這個問題是關系到神經網絡能否成功學習的重要問題。后面我們會詳細討論權重參數的初始化,這里只需要知道,權重使用符合高斯分布的隨機數進行初始化,偏置使用0 進行初始化。predict(self, x) 和accuracy(self, x, t)的實現和上一章的神經網絡的推理處理基本一樣。如果仍有不明白的地方,請再回顧一下上一章的內容。另外,loss(self, x, t)是計算損失函數值的方法。這個方法會基於predict()的結果和正確解標簽,計算交叉熵誤差。
剩下的numerical_gradient(self, x, t)方法會計算各個參數的梯度。根據數值微分,計算各個參數相對於損失函數的梯度。另外,gradient(self, x, t)是下一章要實現的方法,該方法使用誤差反向傳播法高效地計算梯度。
numerical_gradient(self, x, t)基於數值微分計算參數的梯度。下一章,我們會介紹一個高速計算梯度的方法,稱為誤差反向傳播法。用誤差反向傳播法求到的梯度和數值微分的結果基本一致,但可以高速地進行處理。使用誤差反向傳播法計算梯度的gradient(self,x, t)方法會在下一章實現,不過考慮到神經網絡的學習比較花時間,想節約學習時間的讀者可以替換掉這里的numerical_gradient(self,x, t),搶先使用gradient(self, x, t)!
4.5.2 mini-batch的實現
神經網絡的學習的實現使用的是前面介紹過的mini-batch 學習。所謂mini-batch學習,就是從訓練數據中隨機選擇一部分數據(稱為mini-batch),再以這些mini-batch為對象,使用梯度法更新參數的過程。下面,我們就以TwoLayerNet類為對象,使用MNIST數據集進行學習(源代碼在ch04/train_neuralnet.py中)。
1 import numpy as np 2 from dataset.mnist import load_mnist 3 from two_layer_net import TwoLayerNet 4 5 (x_train, t_train), (x_test, t_test) = \ load_mnist(normalize=True, one_hot_ 6 laobel = True) 7 8 train_loss_list = [] 9 10 # 超參數 11 iters_num = 10000 12 train_size = x_train.shape[0] 13 batch_size = 100 14 learning_rate = 0.1 15 16 network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) 17 18 for i in range(iters_num): 19 # 獲取mini-batch 20 batch_mask = np.random.choice(train_size, batch_size) 21 x_batch = x_train[batch_mask] 22 t_batch = t_train[batch_mask] 23 24 # 計算梯度 25 grad = network.numerical_gradient(x_batch, t_batch) 26 # grad = network.gradient(x_batch, t_batch) # 高速版! 27 28 # 更新參數 29 for key in ('W1', 'b1', 'W2', 'b2'): 30 network.params[key] -= learning_rate * grad[key] 31 32 # 記錄學習過程 33 loss = network.loss(x_batch, t_batch) 34 train_loss_list.append(loss)
這里,mini-batch的大小為100,需要每次從60000個訓練數據中隨機取出100個數據(圖像數據和正確解標簽數據)。然后,對這個包含100筆數據mini-batch求梯度,使用隨機梯度下降法(SGD)更新參數。這里,梯度法的更新次數(循環的次數)為10000。每更新一次,都對訓練數據計算損失函數的值,並把該值添加到數組中。用圖像來表示這個損失函數的值的推移,如圖4-11 所示。

圖4-11 損失函數的推移:左圖是10000次循環的推移,右圖是1000次循環的推移
觀察圖4-11,可以發現隨着學習的進行,損失函數的值在不斷減小。這是學習正常進行的信號,表示神經網絡的權重參數在逐漸擬合數據。也就是說,神經網絡的確在學習!通過反復地向它澆灌(輸入)數據,神經網絡正在逐漸向最優參數靠近。
4.5.3 基於測試數據的評價
根據圖4-11呈現的結果,我們確認了通過反復學習可以使損失函數的值逐漸減小這一事實。不過這個損失函數的值,嚴格地講是“對訓練數據的某個mini-batch的損失函數”的值。訓練數據的損失函數值減小,雖說是神經網絡的學習正常進行的一個信號,但光看這個結果還不能說明該神經網絡在其他數據集上也一定能有同等程度的表現。
神經網絡的學習中,必須確認是否能夠正確識別訓練數據以外的其他數據,即確認是否會發生過擬合。過擬合是指,雖然訓練數據中的數字圖像能被正確辨別,但是不在訓練數據中的數字圖像卻無法被識別的現象。
神經網絡學習的最初目標是掌握泛化能力,因此,要評價神經網絡的泛化能力,就必須使用不包含在訓練數據中的數據。下面的代碼在進行學習的過程中,會定期地對訓練數據和測試數據記錄識別精度。這里,每經過一個epoch,我們都會記錄下訓練數據和測試數據的識別精度。
epoch是一個單位。一個epoch表示學習中所有訓練數據均被使用過一次時的更新次數。比如,對於10000 筆訓練數據,用大小為100筆數據的mini-batch進行學習時,重復隨機梯度下降法100次,所有的訓練數據就都被“看過”了①。此時,100次就是一個epoch。
①實際上,一般做法是事先將所有訓練數據隨機打亂,然后按指定的批次大小,按序生成mini-batch。這樣每個mini-batch 均有一個索引號,比如此例可以是0, 1, 2, . . . , 99,然后用索引號可以遍歷所有的mini-batch。遍歷一次所有數據,就稱為一個epoch。請注意,本節中的mini-batch每次都是隨機選擇的,所以不一定每個數據都會被看到。
為了正確進行評價,我們來稍稍修改一下前面的代碼。與前面的代碼不同的地方,我們用粗體來表示。
1 import numpy as np 2 from dataset.mnist import load_mnist 3 from two_layer_net import TwoLayerNet 4 # 讀入數據 5 (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True) 6 7 train_loss_list = [] 8 train_acc_list = [] 9 test_acc_list = [] 10 # 平均每個epoch的重復次數 11 iter_per_epoch = max(train_size / batch_size, 1) 12 13 # 超參數 14 iters_num = 10000 15 batch_size = 100 16 learning_rate = 0.1 17 18 network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) 19 20 for i in range(iters_num): 21 # 獲取mini-batch 22 batch_mask = np.random.choice(train_size, batch_size) 23 x_batch = x_train[batch_mask] 24 t_batch = t_train[batch_mask] 25 26 # 計算梯度 27 #grad = network.numerical_gradient(x_batch, t_batch) 28 grad = network.gradient(x_batch, t_batch) 29 30 # 更新參數 31 for key in ('W1', 'b1', 'W2', 'b2'): 32 network.params[key] -= learning_rate * grad[key] 33 34 loss = network.loss(x_batch, t_batch) 35 train_loss_list.append(loss) 36 # 計算每個epoch的識別精度 37 if i % iter_per_epoch == 0: 38 train_acc = network.accuracy(x_train, t_train) 39 test_acc = network.accuracy(x_test, t_test) 40 train_acc_list.append(train_acc) 41 test_acc_list.append(test_acc) 42 print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))
在上面的例子中,每經過一個epoch,就對所有的訓練數據和測試數據計算識別精度,並記錄結果。之所以要計算每一個epoch 的識別精度,是因為如果在for語句的循環中一直計算識別精度,會花費太多時間。並且,也沒有必要那么頻繁地記錄識別精度(只要從大方向上大致把握識別精度的推移就可以了)。因此,我們才會每經過一個epoch就記錄一次訓練數據的識別精度。
把從上面的代碼中得到的結果用圖表示的話,如圖4-12 所示。

圖4-12 訓練數據和測試數據的識別精度的推移(橫軸的單位是epoch)
圖4-12 中,實線表示訓練數據的識別精度,虛線表示測試數據的識別精度。如圖所示,隨着epoch的前進(學習的進行),我們發現使用訓練數據和測試數據評價的識別精度都提高了,並且,這兩個識別精度基本上沒有差異(兩條線基本重疊在一起)。因此,可以說這次的學習中沒有發生過擬合的現象。
