讀《Python神經網絡編程》總結
一些歸納
- 計算機編程語言可以理解矩陣計算,並認識到潛在的計算方法的相似性,這允許計算機高速高效地進行這些計算,算出X = W • I ,而無需我們對每一層的每個節點給出單獨的計算指令。
- 爲什麼讓誤差反向傳播到網絡的每一層呢?原因是,我們使用誤差來指導如何調整鏈接權重,從而改進神經網絡輸出的總體答案。
- 爲了避免終止於錯誤的山谷或錯誤的函數最小值,我們從山上的不同點開始,多次訓練神經網絡,確保並不總是終止於錯誤的山谷。不同的起始點意味着選擇不同的起始參數,在神經網絡的情況下,這意味着選擇不同的起始鏈接權重。
- 我們更喜歡使用差的平方作爲誤差函數,而不喜歡使用差的絕對值作爲誤差函數,原因有以下幾點:
(1)使用誤差的平方,我們可以很容易使用代數計算出梯度下降的斜率。
(2)誤差函數平滑連續,這使得梯度下降法很好地發揮作用——沒有間斷,也沒有突然的跳躍。
(3)越接近最小值,梯度越小,這意味着,如果我們使用這個函數調節步長,超調的風險就會變得較小。 - 選擇使用100個隱藏層節點並不是通過使用科學的方法得到的。我們認爲,神經網絡應該可以發現在輸入中的特徵或模式,這些模式或特徵可以使用比輸入本身更簡短的形式表達,因此沒有選擇比784大的數字。通過選擇使用比輸入節點的數量小的值,強制網絡嘗試總結輸入的主要特點。但是,如果選擇太少的隱藏層節點,那麼就限制了網絡的能力,使網絡難以找到足夠的特徵或模式,也就會剝奪神經網絡表達其對MNIST數據理解的能力。給定的輸出層需要10個標籤,對應於10個輸出層節點,因此,選擇100這個中間值作爲中間隱藏層的節點數量,似乎有點道理。
- 這裏應該強調一點。對於一個問題,應該選擇多少個隱藏層節點,並不存在一個最佳方法。同時,我們也沒有最佳方法選擇需要幾層隱藏層。就目前而言,最好的辦法是進行實驗,直到找到適合你要解決的問題的一個數字。
如何更新權重
目標函數是E,也稱爲代價函數,損失函數。
這個表達式表示了當權重改變時,誤差E是如何改變的。這是誤差函數的斜率,也就是我們希望使用梯度下降的方法到達最小值的方向。
在節點n的輸出只取決於連接到這個節點的鏈接,因此我們可以直接簡化這個表達式。。這意味着,由於這些權重是鏈接到節點k的權重,因此節點k的輸出只取決於權重。
將這個微積分任務分解成更多易於管理的小塊
我們對平方函數進行簡單的微分,就很容易擊破了第一個簡單的項。對於第二項,我們需要仔細考慮一下,但是無需考慮過久。是節點k的輸出,如果你還記得,這是在連接輸入信號上進行加權求和,在所得到結果上應用S函數得到的結果。
對S函數求微分,這對我們而言是一種非常艱辛的方法,但是,其他人已經完成了這項工作。我們可以只使用衆所周知的答案,就像全世界的數學家每天都在做的事情一樣。
在微分後,一些函數變成了非常可怕的表達式。S函數微分後,可以得到一個非常簡單、易於使用的結果。在神經網絡中,這是S函數成爲大受歡迎的激活函數的一個重要原因。
在寫下最後的答案之前,讓我們把在前面的2去掉。我們只對誤差函數的斜率方向感興趣,這樣我們就可以使用梯度下降的方法,因此可以去掉2。只要我們牢牢記住需要什麼,在表達式前面的常數,無論是2、3還是100,都無關緊要。因此,去掉這個常數,讓事情變得簡單。
我們所得到的這個表達式,是爲了優化隱藏層和輸出層之間的權重。現在,我們需要完成工作,爲輸入層和隱藏層之間的權重找到類似的誤差斜率。這是我們所得到誤差函數斜率,用於輸入層和隱藏層之間權重調整。
權重改變的方向與梯度方向相反。我們使用學習因子,調節變化,我們可以根據特定的問題,調整這個學習因子。當我們建立線性分類器,作爲避免被錯誤的訓練樣本拉得太遠的一種方式,同時也爲了保證權重不會由於持續的超調而在最小值附近來回擺動,我們都發現了這個學習因子。讓我們用數學的形式來表達這個因子。
正如我們先前所看到的,如果斜率爲正,我們希望減小權重,如果斜率爲負,我們希望增加權重,因此,我們要對斜率取反。符號α是一個因子,這個因子可以調節這些變化的強度,確保不會超調。我們通常稱這個因子爲學習率。
權重更新矩陣有如下的矩陣形式,這種形式可以讓我們通過計算機編程語言高效地實現矩陣運算。
使用Python製作神經網絡
1.框架代碼
有三個函數:
(1)初始化函數——設定輸入層節點、隱藏層節點和輸出層節點的數量。
(2)訓練——學習給定訓練集樣本後,優化權重。
(3)查詢——給定輸入,從輸出節點給出答案。
# neural network class definition
class neuralNetwork:
# initialise the neural network
def __init__():
pass
# train the neural network
def train():
pass
# query the neural network
def query():
pass
2.初始化網絡
優秀的程序員、計算機科學家和數學家,只要可能,都盡力創建一般代碼,而不是具體的代碼。如果能做到這點,就意味着我們的解決方案可以適用於不同的場景。讓我們看看__init __()函數是什麼樣子的:
# neural network class definition
class neuralNetwork:
# initialise the neural network
def __init__(self , inputnodes, hiddennodes, outputnodes, learningrate):
# set number of nodes in each input, hidden, output layer
self . inodes = inputnodes
self . hnodes = hiddennodes
self . onodes = outputnodes
# learning rate
self . lr = learningrate
pass
3.權重—-網絡的核心
網絡中最重要的部分是鏈接權重,我們使用這些權重來計算前饋信號、反向傳播誤差,並且在試圖改進網絡時優化鏈接權重本身。使用正態概率分佈採樣權重,其中平均值爲0,標準方差爲節點傳入鏈接數目的開方,即下面是實現了神經網絡的心臟——鏈接權重矩陣。
# neural network class definition
class neuralNetwork:
# initialise the neural network
def __init__(self , inputnodes, hiddennodes, outputnodes, learningrate):
# set number of nodes in each input, hidden, output layer
self . inodes = inputnodes
self . hnodes = hiddennodes
self . onodes = outputnodes
# link weight matrices, wih and who
# weights inside the arrays are w_i_j, where link is from node i to node j in the next layer
# w11 w21
# w12 w22 etc
# self.wih = (numpy.random.rand(self.hnodes, self.inodes) - 0.5)
# self.who = (numpy.random.rand(self.onodes, self.hnodes) - 0.5)
self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
self.who = np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
# learning rate
self . lr = learningrate
pass
4.查詢網絡
# initialise the neural network
def __init__(self , inputnodes, hiddennodes, outputnodes, learningrate):
# activation function is the sigmoid function
self.activation_function = lambda x: np.array([1/(1 + math.exp(-z)) for z in x], ndmin=2).T
pass
# query the neural network
def query(self, inputs_list):
# convert inputs list to 2d array
inputs = np.array(inputs_list, ndmin=2).T
# calculate signals into hidden layer
hidden_inputs = np.dot(self.wih, inputs)
# calculate the signals emerging from hidden layer
hidden_outputs = self.activation_function(hidden_inputs)
# calculate signals into final output layer
final_inputs = np.dot(self.who, hidden_outputs)
# calculate the signals emerging from final output layer
final_outputs = self.activation_function(final_inputs)
return final_outputs
5.訓練網絡
# train the neural network
def train(self, inputs_list, targets_list):
# convert inputs list to 2d array
inputs = numpy.array(inputs_list, ndmin=2).T
targets = numpy.array(targets_list, ndmin=2).T
# calculate signals into hidden layer
hidden_inputs = numpy.dot(self.wih, inputs)
# calculate the signals emerging from hidden layer
hidden_outputs = self.activation_function(hidden_inputs)
# calculate signals into final output layer
final_inputs = numpy.dot(self.who, hidden_outputs)
# calculate the signals emerging from final output layer
final_outputs = self.activation_function(final_inputs)
pass
現在,接近神經網絡工作的核心,即基於所計算輸出與目標輸出之間的誤差,改進權重。首先需要計算誤差,這個值等於訓練樣本所提供的預期目標輸出值與實際計算得到的輸出值之差。這個差也就是將矩陣targets和矩陣final_outputs中每個對應元素相減得到的。
# error is the (target - actual)
output_errors = targets - final_outputs
根據所連接的權重分割誤差,爲每個隱藏層節點重組這些誤差
# hidden layer error is the output_errors, split by weights, recombined at hidden nodes
hidden_errors = numpy.dot(self.who.T, output_errors)
這樣,我們就擁有了所需要的一切,可以優化各個層之間的權重了。對於在隱蔽層和最終層之間的權重,我們使用output_errors進行優化。對於輸入層和隱藏層之間的權重,我們使用剛纔計算得到的hidden_errors進行優化。
先前,我們得到了用於更新節點j與其下一層節點k之間鏈接權重的矩陣形式的表達式:
α是學習率,*乘法是正常的對應元素的乘法,•點乘是矩陣點積。最後一點要注意,來自上一層的輸出矩陣被轉置了。
首先爲隱藏層和最終層之間的權重進行編碼。
# update the weights for the links between the hidden and output layers
self.who += self.lr * np.dot((output_errors * final_outputs * (1.0 - final_outputs)), np.transpose(hidden_outputs))
用於輸入層和隱藏層之間權重的代碼也是類似的。我們只是利用對稱性,重寫代碼,更換名字,這樣它們指的就是神經網絡的前一層了。
# update the weights for the links between the input and hidden layers
self. wih += self.lr * numpy.dot(( hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
6.完整的神經網絡代碼
import numpy as np
import math
# neural network class definition
class neuralNetwork:
# initialise the neural network
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
# set number of nodes in each input, hidden, output layer
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
# link weight matrices, wih and who
# weights inside the arrays are w_i_j, where link is from node i to node j in the next layer
# w11 w21
# w12 w22 etc
self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
self.who = np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
# learning rate
self.lr = learningrate
# activation function is the sigmoid function
self.activation_function = lambda x: np.array([1/(1 + math.exp(-z)) for z in x], ndmin=2).T
pass
# training the neural network
def train(self, inputs_list, targets_list):
# convert inputs list to 2d array
inputs = np.array(inputs_list, ndmin=2).T
targets = np.array(targets_list, ndmin=2).T
# calculate signals into hidden layer
hidden_inputs = np.dot(self.wih, inputs)
# calculate the signals emerging from hidden layer
hidden_outputs = self.activation_function(hidden_inputs)
# calculate signals into final output layer
final_inputs = np.dot(self.who, hidden_outputs)
# calculate the signals emerging from final output layer
final_outputs = self.activation_function(final_inputs)
# error is the (target - actual)
output_errors = targets - final_outputs
# hidden layer error is the output_errors, split by weights, recombined at hidden nodes
hidden_errors = np.dot(self.who.T, output_errors)
# update the weights for the links between the hidden and output layers
self.who += self.lr * np.dot((output_errors * final_outputs * (1.0 - final_outputs)),
np.transpose(hidden_outputs))
# update the weights for the links between the input and hidden layers
self.wih += self.lr * np.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)),
np.transpose(inputs))
pass
# query the neural network
def query(self, inputs_list):
# convert inputs list to 2d array
inputs = np.array(inputs_list, ndmin=2).T
# calculate signals into hidden layer
hidden_inputs = np.dot(self.wih, inputs)
# calculate the signals emerging from hidden layer
hidden_outputs = self.activation_function(hidden_inputs)
# calculate signals into final output layer
final_inputs = np.dot(self.who, hidden_outputs)
# calculate the signals emerging from final output layer
final_outputs = self.activation_function(final_inputs)
return final_outputs
7.在手寫數字的數據集MNIST上運行神經網絡
fileintrain = r"H:\深度學習\data\mnist_train.csv"
fileintest = r"H:\深度學習\data\mnist_test.csv"
# number of input, hidden and output nodes
input_nodes = 784
hidden_nodes = 100
output_nodes = 10
# learning rate is 0.3
learning_rate = 0.2
# create instance of neural network
n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
# load the mnist training data CSV file into a list
training_data_file = open(fileintrain)
training_data_list = training_data_file.readlines()
training_data_file.close()
# train the neural network
# go through all records in the training data set
for record in training_data_list:
# split the record by the ',' commas
all_values = record.split(',')
# scale and shift the inputs
inputs = (np.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# create the target output values (all 0.01, except the desired label which is 0.99)
targets = np.zeros(output_nodes) + 0.01
# all_values[0] is the target label for this record
targets[int(all_values[0])] = 0.99
n.train(inputs, targets)
pass
# load the mnist test data CSV file into a list
test_data_file = open(fileintest)
test_data_list = test_data_file.readlines()
test_data_file.close()
# test the neural network
# scorecard for how well the network performs, initially empty
scorecard = []
# go through all the records in the test data set
for record in test_data_list:
# split the record by the ',' commas
all_values = record.split(',')
# correct answer is first value
correct_label = int(all_values[0])
print(correct_label, "correct label")
# scale and shift the inputs
inputs = (np.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# query the network
outputs = n.query(inputs)
# the index of the highest value corresponds to the label
label = np.argmax(outputs)
print(label, "network's answer")
# append correct or incorrect to list
if label == correct_label:
# network's answer matches correct answer, add 1 toscorecard
scorecard.append(1)
else:
# network's answer doesn't match correct answer, add 0 to scorecard
scorecard.append(0)
pass
pass
# calculate the performance score, the fraction of correct answers
scorecard_array = np.asarray(scorecard)
print("performance = ", scorecard_array.sum() / scorecard_array.size)
爲什麼選擇784個輸入節點呢?請記住,這是28×28的結果,即組成手寫數字圖像的像素個數。
選擇使用100個隱藏層節點並不是通過使用科學的方法得到的。我們認爲,神經網絡應該可以發現在輸入中的特徵或模式,這些模式或特徵可以使用比輸入本身更簡短的形式表達,因此沒有選擇比784大的數字。通過選擇使用比輸入節點的數量小的值,強制網絡嘗試總結輸入的主要特點。但是,如果選擇太少的隱藏層節點,那麼就限制了網絡的能力,使網絡難以找到足夠的特徵或模式,也就會剝奪神經網絡表達其對MNIST數據理解的能力。給定的輸出層需要10個標籤,對應於10個輸出層節點,因此,選擇100這個中間值作爲中間隱藏層的節點數量,似乎有點道理。
這裏應該強調一點。對於一個問題,應該選擇多少個隱藏層節點,並不存在一個最佳方法。同時,我們也沒有最佳方法選擇需要幾層隱藏層。就目前而言,最好的辦法是進行實驗,直到找到適合你要解決的問題的一個數字。
7.1 一些改進:調整學習率
使用的步長太小了,限制了梯度下降發生的速度,對性能造成了損害;使用步長太大,無法到達谷底;所以這個結果也是有道理的。對於學習率存在一個甜蜜點。
7.2 一些改進:多次運行
使用數據集,重複多次進行訓練。
有些人把訓練一次稱爲一個世代。因此,具有10個世代的訓練,意味着使用整個訓練數據集運行程序10次。爲什麼要這麼做呢?特別是,如果這次計算機花的時間增加到10或20甚至30分鐘呢?這是值得的,原因是通過提供更多爬下斜坡的機會,有助於在梯度下降過程中進行權重更新。
直覺告訴我們,所做的訓練越多,所得到的性能越好。有人可能會注意到,太多的訓練實際上會過猶不及,這是由於網絡過度擬合訓練數據,因此網絡在先前沒有見到過的新數據上表現不佳。不僅是神經網絡,在各種類型的機器學習中,這種過度擬合也是需要注意的。
結果呈現出不可預測性。在大約5或7個世代時,有一個甜蜜點。在此之後,性能會下降,這可能是過度擬合的效果。性能在6個世代的情況下下降,這可能是運行中出了問題,導致網絡在梯度下降過程中被卡在了一個局部的最小值中。事實上,由於沒有對每個數據點進行多次實驗,無法減小隨機過程的影響,因此我們已經預見到結果會有各種變化。這就是爲什麼保留了6個世代這個奇怪的點,這是爲了提醒我們,神經網絡的學習過程其核心是隨機過程,有時候工作得不錯,有時候工作得很糟。
另一個可能的原因是,在較大數目的世代情況下,學習率可能設置過高了。繼續這個實驗,將學習率從0.2減小到0.1,看看會發生什麼情況。
下圖顯示了在學習率爲0.1情況下,得到的新性能與前一幅圖疊加的情況。
可以看到,在更多世代的情況下,減小學習率確實能夠得到更好的性能。0.9689的峯值表示誤差率接近3%,這可以與Yann LeCun網站上的神經網絡標準相媲美了。
直觀上,如果你打算使用更長的時間(多個世代)探索梯度下降,那麼你可以承受採用較短的步長(學習率),並且在總體上可以找到更好的路徑,這是有道理的。確實,對於MNIST學習任務,我們的神經網絡的甜蜜點看起來是5個世代。請再次記住,我們在使用一種相當不科學的方式來進行實驗。要正確、科學地做到這一點,就必須爲每個學習率和世代組合進行多次實驗,儘量減少在梯度下降過程中隨機性的影響。
7.3 一些改進:改變網絡形狀
我們還沒有嘗試過改變神經網絡的形狀,也許應該更早嘗試這件事。讓我們試着改變中間隱藏層節點的數目。一直以來,我們將它們設置爲100!
在嘗試使用不同數目的隱藏層節點進行實驗之前,讓我們思考一下,如果這樣做可能會發生什麼情況。隱藏層是發生學習過程的層次。請記住,輸入節點只需引入輸入信號,輸出節點只要送出神經網絡的答案,是隱藏層(可以多層)進行學習,將輸入轉變爲答案。這是學習發生的場所。事實上,隱藏層節點前後的鏈接權重具有學習能力。
如果隱藏層節點太少,比如說3個,那麼你可以想象,這不可能有足夠的空間讓網絡學習任何知識,並將所有輸入轉換爲正確的輸出。這就像要5座車去載10個人。你不可能將那麼多人塞進去。計算機科學家稱這種限制爲學習容量。雖然學習能力不可能超過學習容量,但是可以通過改變車輛或網絡形狀來增加容量。
如果有10 000個隱藏層節點,會發生什麼情況呢?雖然我們不會缺少學習容量,但是由於目前有太多的路徑供學習選擇,因此可能難以訓練網絡。這也許需要使用10 000個世代來訓練這樣的網絡。
可以看到,比起較多的隱藏層節點,隱藏層節點數量少,其效果不是很理想,這是我們預期的結果。但是,只有5個隱藏層節點的神經網絡,其性能得分就可以達到0.7001,鑑於只給瞭如此少的學習場所,而網絡仍有70%的正確率,這已經相當驚人了。
請記住,迄今爲止,程序運行的是100個隱藏層節點。只用10個隱藏層節點,網絡就得到了0.8998的準確性,這同樣讓人側目。只使用我們曾經用過的節點數目的1/10,網絡的性能就跳到90%。只使用如此少的隱藏層節點或學習場所,神經網絡就能夠得到如此好的結果。這也證明了神經網絡的力量。這一點值得我們讚賞。
隨着增加隱藏層節點的數量,結果有所改善,但是不顯著。由於增加一個隱藏層節點意味着增加了到前後層的每個節點的新網絡鏈接,這一切都會產生額外較多的計算,因此訓練網絡所用的時間也顯著增加了!因此,必須在可容忍的運行時間內選擇某個數目的隱藏層節點。