目錄
- 1 從數據中學習
- 1.1 數據驅動
- 1.2 訓練數據和測試數據
- 2損失函數
- 2.1 均方誤差
- 2.2 交叉熵誤差
- 2.3 mini-batch學習
- 2.4 mini-batch版交叉熵誤差的實現
- 2.5 為何要設定損失函數
- 3 數值微分
- 3.1 數值微分
- 3.3 偏導數
- 4 梯度
- 4.1 梯度法
- 4.2 神經網絡的梯度
- 5 學習算法的實現
- 5.1 2層神經網絡的類
- 5.2 mini-batch的實現與基于測試數據的評價
1 從數據中學習
1.1 數據驅動
神經網絡的特征就是可以從數據中學習。所謂“從數據中學習”,是指可以由數據自動決定權重參數的值。
1.2 訓練數據和測試數據
機器學習中,一般將數據分為訓練數據和測試數據兩部分來進行學習和實驗等。首先,使用訓練數據進行學習,尋找最優的參數;然后,使用測試數據評價訓練得到的模型的實際能力。為什么需要將數據分為訓練數據和測試數據呢?因為我們追求的是模型的泛化能力。為了正確評價模型的泛化能力,就必須劃分訓練數據和測試數據。另外,訓練數據也可以稱為監督數據。泛化能力是指處理未被觀察過的數據(不包含在訓練數據中的數據)的能力。獲得泛化能力是機器學習的最終目標。只對某個數據集過度擬合的狀態稱為過擬合(over fitting)。避免過擬合也是機器學習的一個重要課題。
2損失函數
神經網絡的學習通過某個指標表示現在的狀態。然后,以這個指標為基準,尋找最優權重參數。神經網絡以某個指標為線索尋找最優權重參數。神經網絡的學習中所用的指標稱為損失函數(loss function)。這個損失函數可以使用任意函數,但一般用均方誤差和交叉熵誤差等。
損失函數是表示神經網絡性能的“惡劣程度”的指標,即當前的神經網絡對監督數據在多大程度上不擬合,在多大程度上不一致。以“性能的惡劣程度”為指標可能會使人感到不太自然,但是如果給損失函數乘上一個負值,就可以解釋為“在多大程度上不壞”,即“性能有多好”。并且,“使性能的惡劣程度達到最小”和“使性能的優良程度達到最大”是等價的,不管是用“惡劣程度”還是“優良程度”,做的事情本質上都是一樣的。
2.1 均方誤差
這里,yk是表示神經網絡的輸出,tk表示監督數據,k表示數據的維數。
def mean_squared_error(y, t):return 0.5 * np.sum((y-t)**2)
比如,在3.6節手寫數字識別的例子中,yk、tk是由如下10個元素構成的數據。
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> 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表示。
2.2 交叉熵誤差
def cross_entropy_error(y, t):delta = 1e-7return -np.sum(t * np.log(y + delta))
這里,參數y和t是NumPy數組。函數內部在計算np.log時,加上了一個微小值delta。這是因為,當出現np.log(0)時,np.log(0)會變為負無限大的-inf,這樣一來就會導致后續計算無法進行。作為保護性對策,添加一個微小值可以防止負無限大的發生。
2.3 mini-batch學習
前面介紹的損失函數的例子中考慮的都是針對單個數據的損失函數。如果要求所有訓練數據的損失函數的總和,以交叉熵誤差為例
這里,假設數據有N個,tnk表示第n個數據的第k個元素的值(ynk是神經網絡的輸出,tnk是監督數據)。式子雖然看起來有一些復雜,其實只是把求單個數據的損失函數的式(4.2)擴大到了N份數據,不過最后還要除以N進行正規化。通過除以N,可以求單個數據的“平均損失函數”。通過這樣的平均化,可以獲得和訓練數據的數量無關的統一指標。比如,即便訓練數據有1000個或10000個,也可以求得單個數據的平均損失函數。
2.4 mini-batch版交叉熵誤差的實現
def cross_entropy_error(y, t):if y.ndim == 1:t = t.reshape(1, t.size) #reshape為一行n(t.size / y.size)列的形狀是為了batch_size可以取1y = y.reshape(1, y.size) batch_size = y.shape[0]return -np.sum(t * np.log(y + 1e-7)) / batch_size
這里,y是神經網絡的輸出,t是監督數據。y的維度為1時,即求單個數據的交叉熵誤差時,需要改變數據的形狀。并且,當輸入為mini-batch時,要用batch的個數進行正規化,計算單個數據的平均交叉熵誤差。
此外,當監督數據是標簽形式(非one-hot表示,而是像“2”“7”這樣的標簽)時,交叉熵誤差可通過如下代碼實現。
def cross_entropy_error(y, t):if y.ndim == 1:t = t.reshape(1, t.size)y = y.reshape(1, y.size)batch_size = y.shape[0]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]])。(取第一條數據索引位置為2的輸出,第二條數據索引位置為7的輸出…)
2.5 為何要設定損失函數
在進行神經網絡的學習時,不能將識別精度作為指標。因為如果以識別精度為指標,則參數的導數在絕大多數地方都會變為0。
假設某個神經網絡正確識別出了100筆訓練數據中的32筆,此時識別精度為32 %。如果以識別精度為指標,即使稍微改變權重參數的值,識別精度也仍將保持在32 %,不會出現變化。也就是說,僅僅微調參數,是無法改善識別精度的。即便識別精度有所改善,它的值也不會像32.0123 . . . %這樣連續變化,而是變為33 %、34 %這樣的不連續的、離散的值。而如果把損失函數作為指標,則當前損失函數的值可以表示為0.92543 . . . 這樣的值。并且,如果稍微改變一下參數的值,對應的損失函數也會像0.93432 . . . 這樣發生連續性的變化。識別精度對微小的參數變化基本上沒有什么反應,即便有反應,它的值也是不連續地、突然地變化。作為激活函數的階躍函數也有同樣的情況。出于相同的原因,如果使用階躍函數作為激活函數,神經網絡的學習將無法進行。而sigmoid函數,不僅函數的輸出(豎軸的值)是連續變化的,曲線的斜率(導數)也是連續變化的。也就是說,sigmoid函數的導數在任何地方都不為0。這對神經網絡的學習非常重要。得益于這個斜率不會為0的性質,神經網絡的學習得以正確進行。
3 數值微分
3.1 數值微分
數值微分(導數定義法)含有誤差。為了減小這個誤差,我們可以計算函數f在(x + h)和(x ? h)之間的差分。因為這種計算方法以x為中心,計算它左右兩邊的差分,所以也稱為中心差分(而(x + h)和x之間的差分稱為前向差分)。
def numerical_diff(f, x):h = 1e-4 # 0.0001return (f(x+h) - f(x-h)) / (2*h)
3.3 偏導數
def function(x):return x[0]**2 + x[1]**2# 或者return np.sum(x**2)
問題1:求x0 = 3, x1 = 4時,關于x0的偏導數 。
>>> def function_tmp1(x0):
return x0*x0 + 4.0**2.0
>>> numerical_diff(function_tmp1, 3.0)
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 梯度
由全部變量的偏導數匯總而成的向量稱為梯度(gradient)。
def numerical_gradient(f, x):h = 1e-4 # 0.0001grad = np.zeros_like(x) # 生成和x形狀相同的數組for idx in range(x.size):tmp_val = x[idx]# f(x+h)的計算x[idx] = tmp_val + hfxh1 = f(x)# f(x-h)的計算x[idx] = tmp_val - hfxh2 = f(x)grad[idx] = (fxh1 - fxh2) / (2*h)x[idx] = tmp_val # 還原值return grad
與上面例子類似,計算fxh1,fxh2時另外一個x不變,則為計算x[idx]的偏導數
>>> numerical_gradient(function_2, np.array([3.0, 4.0]))
array([ 6., 8.])A
>>> numerical_gradient(function_2, np.array([0.0, 2.0]))
array([ 0., 4.])
>>> numerical_gradient(function_2, np.array([3.0, 0.0]))
array([ 6., 0.])
4.1 梯度法
這里需要注意的是,梯度表示的是各點處的函數值減小最多的方向。因此,無法保證梯度所指的方向就是函數的最小值或者真正應該前進的方向。實際上,在復雜的函數中,梯度指示的方向基本上都不是函數值最小處。
函數的極小值、最小值以及被稱為鞍點(saddle?point)的地方,梯度為 0。極小值是局部最小值,也就是限定在某個范圍內的最小值。鞍點是從某個方向上看是極大值,從另一個方向上看則是極小值的點。雖然梯度法是要尋找梯度為 0的地方,但是那個地方不一定就是最小值(也有可能是極小值或者鞍點)。此外,當函數很復雜且呈扁平狀時,學習可能會進入一個(幾乎)平坦的地區,陷入被稱為“學習高原”的無法前進的停滯期。
雖然梯度的方向并不一定指向最小值,但沿著它的方向能夠最大限度地減小函數的值。因此,在尋找函數的最小值(或者盡可能小的值)的位置的任務中,要以梯度的信息為線索,決定前進的方向。像這樣,通過不斷地沿梯度方向前進,逐漸減小函數值的過程就是梯度法(gradient method)。
根據目的是尋找最小值還是最大值,梯度法的叫法有所不同。嚴格地講,尋找最小值的梯度法稱為梯度下降法(gradient?descent?method),尋找最大值的梯度法稱為梯度上升法(gradient?ascent?method)。但是通過反轉損失函數的符號,求最小值的問題和求最大值的問題會變成相同的問題,因此“下降”還是“上升”的差異本質上并不重要。一般來說,神經網絡(深度學習)中,梯度法主要是指梯度下降法。
η表示更新量,在神經網絡的學習中,稱為學習率(learning rate)。學習率決定在一次學習中,應該學習多少,以及在多大程度上更新參數。
學習率需要事先確定為某個值,比如0.01或0.001。一般而言,這個值過大或過小,都無法抵達一個“好的位置”。在神經網絡的學習中,一般會一邊改變學習率的值,一邊確認學習是否正確進行了。
def gradient_descent(f, init_x, lr=0.01, step_num=100):x = init_xfor i in range(step_num):grad = numerical_gradient(f, x)x -= lr * gradreturn x
問題:請用梯度法求 的最小值。
>>> def function_2(x):
… return x[0]**2 + x[1]**2
…
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
array([ -6.11110793e-10, 8.14814391e-10])
學習率過大的例子:lr=10.0
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
array([ -2.58983747e+13, -1.29524862e+12])
學習率過小的例子:lr=1e-10
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)
array([-2.99999994, 3.99999992])
實驗結果表明,學習率過大的話,會發散成一個很大的值;反過來,學習率過小的話,基本上沒怎么更新就結束了。也就是說,設定合適的學習率是一個很重要的問題。
像學習率這樣的參數稱為超參數。這是一種和神經網絡的參數(權重和偏置)性質不同的參數。相對于神經網絡的權重參數是通過訓練數據和學習算法自動獲得的,學習率這樣的超參數則是人工設定的。一般來說,超參數需要嘗試多個值,以便找到一種可以使學習順利進行的設定。
4.2 神經網絡的梯度
神經網絡的學習也要求梯度。這里所說的梯度是指損失函數關于權重參數的梯度。比如,有一個只有一個形狀為2 × 3的權重W的神經網絡,損失函數用L表示。此時,梯度可以表示為:
我們以一個簡單的神經網絡為例,來實現求梯度的代碼。為此,我們要實現一個名為simpleNet的類
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient
class simpleNet:def __init__(self):self.W = np.random.randn(2,3) # 用高斯分布進行初始化def predict(self, x):return np.dot(x, self.W)def loss(self, x, t):z = self.predict(x)y = softmax(z)loss = cross_entropy_error(y, t)return loss
它有兩個方法,一個是用于預測的predict(x),另一個是用于求損失函數值的loss(x,t)。這里參數x接收輸入數據,t接收正確解標簽。
>>> net = simpleNet()
>>> print(net.W) # 權重參數
[[ 0.47355232 0.9977393 0.84668094],
[ 0.85557411 0.03563661 0.69422093]])
>>>
>>> x = np.array([0.6, 0.9])
>>> p = net.predict(x)
>>> print§
[ 1.05414809 0.63071653 1.1328074]
>>> np.argmax§ # 最大值的索引
2
>>>
>>> t = np.array([0, 0, 1]) # 正確解標簽
>>> net.loss(x, t)
0.92806853663411326
接下來求梯度。和前面一樣,我們使用numerical_gradient(f, x)求梯度(這里定義的函數f(W)的參數W是一個偽參數。因為numerical_gradient(f, x)會在內部執行f(x),為了與之兼容而定義了f(W))。
>>> def f(W):
… return net.loss(x, t)
…
>>> dW = numerical_gradient(f, net.W)
>>> print(dW)
[[ 0.21924763 0.14356247 -0.36281009]
[ 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的貢獻要大。
5 學習算法的實現
前提
神經網絡存在合適的權重和偏置,調整權重和偏置以便擬合訓練數據的過程稱為“學習”。神經網絡的學習分成下面4個步驟。
步驟1(mini-batch)
從訓練數據中隨機選出一部分數據,這部分數據稱為mini-batch。我們的目標是減小mini-batch的損失函數的值。
步驟2(計算梯度)
為了減小mini-batch的損失函數的值,需要求出各個權重參數的梯度。梯度表示損失函數的值減小最多的方向。
步驟3(更新參數)
將權重參數沿梯度方向進行微小更新。
步驟4(重復)
重復步驟1、步驟2、步驟3。
經網絡的學習按照上面4個步驟進行。這個方法通過梯度下降法更新參數,不過因為這里使用的數據是隨機選擇的mini batch數據,所以又稱為隨機梯度下降法(stochastic gradient descent)。“隨機”指的是“隨機選擇的”的意思,因此,隨機梯度下降法是“對隨機選擇的數據進行的梯度下降法”。深度學習的很多框架中,隨機梯度下降法一般由一個名為SGD的函數來實現。SGD來源于隨機梯度下降法的英文名稱的首字母。
5.1 2層神經網絡的類
下面,我們來實現手寫數字識別的神經網絡。這里以2層神經網絡(隱藏層為1層的網絡)為對象,使用MNIST數據集進行學習。首先,我們將這個2層神經網絡實現為一個名為TwoLayerNet的類,實現過程如下所示。
import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient
class TwoLayerNet:def __init__(self, input_size, hidden_size, output_size,weight_init_std=0.01):# 初始化權重self.params = {}self.params['W1'] = weight_init_std * \np.random.randn(input_size, hidden_size)self.params['b1'] = np.zeros(hidden_size)self.params['W2'] = weight_init_std * \np.random.randn(hidden_size, output_size)self.params['b2'] = np.zeros(output_size)def predict(self, x):W1, W2 = self.params['W1'], self.params['W2']b1, b2 = self.params['b1'], self.params['b2']a1 = np.dot(x, W1) + b1z1 = sigmoid(a1)a2 = np.dot(z1, W2) + b2y = softmax(a2)return y# x:輸入數據, t:監督數據def loss(self, x, t):y = self.predict(x)return cross_entropy_error(y, t)def accuracy(self, x, t):y = self.predict(x)y = np.argmax(y, axis=1)t = np.argmax(t, axis=1)accuracy = np.sum(y == t) / float(x.shape[0])return accuracy# x:輸入數據, t:監督數據def numerical_gradient(self, x, t):loss_W = lambda W: self.loss(x, t)grads = {}grads['W1'] = numerical_gradient(loss_W, self.params['W1'])grads['b1'] = numerical_gradient(loss_W, self.params['b1'])grads['W2'] = numerical_gradient(loss_W, self.params['W2'])grads['b2'] = numerical_gradient(loss_W, self.params['b2'])return grads
5.2 mini-batch的實現與基于測試數據的評價
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
(x_train, t_train), (x_test, t_test) = \ load_mnist(normalize=True, one_hot_
laobel = True)
train_loss_list = []train_acc_list = []
test_acc_list = []
# 平均每個epoch的重復次數iter_per_epoch = max(train_size / batch_size, 1)# 超參數iters_num = 10000
batch_size = 100
learning_rate = 0.1
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
for i in range(iters_num):# 獲取mini-batchbatch_mask = np.random.choice(train_size, batch_size)x_batch = x_train[batch_mask]t_batch = t_train[batch_mask]# 計算梯度grad = network.numerical_gradient(x_batch, t_batch)# 更新參數for key in ('W1', 'b1', 'W2', 'b2'):network.params[key] -= learning_rate * grad[key]loss = network.loss(x_batch, t_batch)train_loss_list.append(loss)# 計算每個epoch的識別精度if i % iter_per_epoch == 0:train_acc = network.accuracy(x_train, t_train)test_acc = network.accuracy(x_test, t_test)train_acc_list.append(train_acc)test_acc_list.append(test_acc)print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
(x_train, t_train), (x_test, t_test) = \ load_mnist(normalize=True, one_hot_
laobel = True)
train_loss_list = []train_acc_list = []
test_acc_list = []
# 平均每個epoch的重復次數iter_per_epoch = max(train_size / batch_size, 1)# 超參數iters_num = 10000
batch_size = 100
learning_rate = 0.1
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
for i in range(iters_num):# 獲取mini-batchbatch_mask = np.random.choice(train_size, batch_size)x_batch = x_train[batch_mask]t_batch = t_train[batch_mask]# 計算梯度grad = network.numerical_gradient(x_batch, t_batch)# grad = network.gradient(x_batch, t_batch) # 高速版!# 更新參數for key in ('W1', 'b1', 'W2', 'b2'):network.params[key] -= learning_rate * grad[key]loss = network.loss(x_batch, t_batch)train_loss_list.append(loss)# 計算每個epoch的識別精度if i % iter_per_epoch == 0:train_acc = network.accuracy(x_train, t_train)test_acc = network.accuracy(x_test, t_test)train_acc_list.append(train_acc)test_acc_list.append(test_acc)print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))
epoch是一個單位。一個 epoch表示學習中所有訓練數據均被使用過一次時的更新次數。比如,對于 10000筆訓練數據,用大小為 100筆數據的mini-batch進行學習時,重復隨機梯度下降法 100次,所有的訓練數據就都被“看過”了A。此時,100次就是一個 epoch。
在上面的例子中,每經過一個epoch,就對所有的訓練數據和測試數據計算識別精度,并記錄結果。之所以要計算每一個epoch的識別精度,是因為如果在for語句的循環中一直計算識別精度,會花費太多時間。并且,也沒有必要那么頻繁地記錄識別精度(只要從大方向上大致把握識別精度的推移就可以了)。因此,我們才會每經過一個epoch就記錄一次訓練數據的識別精度。