引入
今天我們就繼續(xù)上次的那篇文章,談一談神經網絡中比較基礎也比較核心的部分甜无。按順序分別是扛点,前向傳播陵究、反向傳播铜邮、權重更新以及代碼實現(xiàn)寨蹋。
在進入正題之前已旧,還是有必要介紹一些事情,方便對于下文的理解惊楼。
第一檀咙、本文,乃至本系列的文章璃诀,其主體都是全連接神經網絡攀芯。全連接神經網絡應該是神經網絡中最簡單的一種了。顧名思義文虏,全連接指的是對于任意一層的某個神經元來說,它與前后兩層的所有神經元之間都存在連接殖演。如圖所示:
這個神經網絡有三層氧秘,從左往右分別是之前提到過的輸入層、隱藏層趴久、輸出層丸相。每一層都有三個神經元,一般情況下彼棍,這代表著它的輸入應該是一個三維數據灭忠,也就是三維向量膳算,例如三維空間中任意一個坐標。隱藏層的三個神經元弛作,把它當成處理數據的黑盒就好了涕蜂。輸出層的三個神經元,則代表神經網絡的輸出也是一個三維的數據映琳,例如它的輸出也是一個坐標机隙。
之前我們也提到了偏置項,使用偏置項來表示輸入層之外的神經元的閾值葱跋,并且提到了,可以在每一層神經元(除了輸出層)上方添加一個偏置神經元,形式更加統(tǒng)一的表示神經網絡僵朗。上方的圖片是不包含偏置神經元的神經網絡結構圖,我們可以再看一個包含偏置神經元的神經網絡粪薛。
諾熟空,對于這個神經網絡而言掂咒,它的前兩層最上方的神經元都是偏置神經元,其輸入恒為1岁歉,它與下一層的神經元連接的權重,則表示著相應神經元的閾值。
當然啦努潘,倘若我們不把最上面兩個神經元看作偏置神經元,這就是一個非全連接的神經網絡压怠。這不是我們研究的對象~
第二、本文提到的神經網絡都是前饋的,也就是輸入層接受外界輸入, 隱含層與輸出層神經元對信號進行加工, 最終結果由輸出層神經元輸出栖忠。數據在其中是從輸入到輸出單向傳遞的,不會從輸出層又向輸入層傳回來。實際的應用中,自然也是存在這種可以反饋的冷溶,也就是信號回傳的神經網絡纯衍,但這也不是我們的研究對象。
所以歌亲,我們的研究對象就是全連接的,前饋的神經網絡悍缠,也就是最簡單的一種神經網絡。它是之后神經網絡各種變形的基礎趴拧,所以還是很有必要了解掌握的。
接下來進入正題。
前向傳播
前向傳播其實我們在上一篇文章中提過挂谍,所謂的前向傳播,其實就是神經網絡進行預測輸出時妄田,所進行的操作脚曾。我們以一個簡單的神經網絡為例再次說明這一過程,作為接下來的鋪墊拷沸。
上圖的神經網絡就是我們用于介紹前向傳播與反向傳播的主體啦勤庐。它的輸入層是一個二維向量,輸出也是一個二維向量丈探,隱藏層有三個神經元。一般來說,前向傳播過程中一個神經元對應一個一維數據爪幻。
對于權重挨稿,我們使用來表示,代表這一層第個神經元與下一層第個神經元之間的權重疲陕,是一個具體的數值。這里為了簡化邑蒋,我們暫時不把偏置項加進去钱慢,換句話說草描,我們將所有神經元的閾值都置為0。因此怀各,這是一個只有權重,閾值為0的全連接前饋神經網絡硕蛹。
除了權重和閾值鲤孵,激活函數也是神經網絡的核心之一琉兜。這里我們采用較為傳統(tǒng)的sigmoid函數作為激活函數,記為。因此幌氮,激活函數
接下來我們來敘述一下前向傳播的過程:
給定一個輸入向量宇智,我們可以計算出隱藏層神經元的輸入向量,記為
是一個列向量(本文以分號為分隔符的向量均為列向量),其中每一個分量的計算公式為
這個過程在上一篇文章中已經提到了,為了方便表示以及計算,我們可以使用矩陣來表示這一過程嬉挡。(沒學過線性代數的自己去學一下哦)
這個其實很簡單,我們將輸入向量(列向量)記為
這其實也是一個的矩陣汇恤,兩行一列庞钢。類似的,隱藏層的輸入向量是一個的矩陣因谎。之后基括,我們記輸入層與隱藏層之間的矩陣為风皿,所以
這是一個的矩陣侨艾。
所以
不過僅僅是隱藏層的輸入磺芭,也就是隱藏層每一個神經元接收到的數據间螟。由于我們并沒有考慮閾值同波,所以這里直接對隱藏層的輸入數據進行激活悲雳,就得到了隱藏層每一個神經元的輸出税弃,即
這樣赋访,數據就由最原始的輸入铣除,先轉化為隱藏層接收的輸入數據盔腔,又轉化為了隱藏層的輸出厉斟。
同理堂鲤,我們將隱藏層的輸出再傳遞到輸出層去译打。記隱藏層與輸出層之間的權重矩陣為
其中表示隱藏層第個神經元與輸出層第個神經元之間的權重香到。
接著瑞妇,我們計算出輸出層接收到的二維向量為
最后聚谁,再經過激活函數,得到最終的輸出
就這樣,我們實現(xiàn)了上述神經網絡前向傳播的過程,及其矩陣表示的止,實現(xiàn)了從輸入到輸出的轉化檩坚。即
神經網絡的前向傳播,就是這么一個從的過程诅福。當然匾委,這里的都可以是任一維度的數據。
誤差反向傳播
前向傳播的過程氓润,是我們已知權重矩陣赂乐,實現(xiàn)從輸入到輸出的過程。而誤差的反向傳播咖气,解決的則是已知輸入和輸出挨措,求解合適的權重矩陣的過程。
之前提到過崩溪,機器學習三要素是模型浅役、策略和算法。放在當前語境下伶唯,模型就是上文中那個簡單的神經網絡觉既。策略,則是我們衡量誤差的手段乳幸,這里我們使用最經典的平方損失函數作為損失函數瞪讼,即我們的策略。記對于某個樣本粹断,該神經網絡模型進行預測時的誤差為
其中符欠,表示樣本的實際輸出值的第個分量,表示的是將樣本的實際輸入值輸入神經網絡后瓶埋,輸出層第個神經元的輸出值希柿,也就是的第個分量的預測值诊沪。對于上述的神經網絡,顯然曾撤,娄徊。
問題來了,我們現(xiàn)在表示出了誤差盾戴,也知道了我們希望通過調整每一層的權重矩陣來縮小這個誤差寄锐,那如何進行調整呢?
這里就不再引入了尖啡,直說吧橄仆,我們可以使用梯度下降法進行權重的調整。如果對于梯度下降法不太了解衅斩,可以看看我之前寫的那篇關于梯度下降的文章盆顾,或者自行搜索一下。
我們之前使用梯度下降法畏梆,是基于
這里就是損失函數您宪,就是參數,而則是關于進行的移動奠涌。對于一般的問題宪巨,比如一元線性回歸,可以直接構造出這樣一個等式溜畅,并且發(fā)現(xiàn)誤差下降的方向捏卓,就是負梯度的方向。之后一步步進行迭代慈格,對于參數進行調整怠晴。
但是對于神經網絡,問題就沒有這么簡單了浴捆。神經網絡是一層層疊加的蒜田,每兩層之間都有一個權重矩陣。但是我們可以觀察到的誤差选泻,僅僅是最后輸出的誤差冲粤,也就是預測值與實際值之間的誤差。雖然這一層的誤差是由之前的誤差層層傳遞而來滔金,但是我們很難直接通過最后一層的誤差色解,對最前幾層的權重矩陣進行調整茂嗓。假設一個神經網絡有10層餐茵,那么我們或許可以使用梯度下降,基于最后一層的誤差述吸,對最后一層的權重矩陣進行調整忿族,但是對于第一層和第二層之間的權重矩陣锣笨,又要怎么構建“誤差與權重”之間的關系呢?
當然道批,肯定是可以一步步構建的错英,但一定會很麻煩……因為中間涉及到的每一個權重矩陣,對于最后的誤差都有影響隆豹,所以如果想直接一步步推導回來椭岩,那么誤差與第一個權重矩陣的關系等式中,必然包含著璃赡。感興趣的判哥,可以以一個三層的神經網絡,自己嘗試一下碉考。
因此塌计,我們的想法就是,將最終可以觀察到的誤差侯谁,一步步往回進行反向傳播锌仅,得到每一層的輸入值與輸出值之間的誤差,并基于此墙贱,使用梯度下降的方式對于相應的權重矩陣進行調整热芹。可以理解為惨撇,每兩層之間的權重矩陣都對應著一個誤差剿吻,這個誤差與相應的權重矩陣是直接相關的。這樣就不用一層層跋山涉水將距離很遠的誤差與權重直接構建關系啦串纺。
如上圖所示丽旅,是我們可以直接觀察到的誤差(=實際值-預測值),它由這兩個分量組成纺棺,代表輸出層兩個神經元與實際值的誤差榄笙。與有著直接的關系,因此我們可以通過對進行修正祷蝌。同理茅撞,我們通過分別對進行調整。所以問題就轉化為巨朦,我們如何通過米丘,求解得到呢?
這就涉及到誤差的反向傳播啦糊啡,回到我們的那個模型上
對于這個三層的神經網絡拄查,我們可以觀察到的誤差是,并且棚蓄,我們知道這兩層之間的權重堕扶,那如何求解第二層的誤差呢碍脏?
簡單理解的話,我們可以把誤差的反向傳播過程稍算,看成誤差的分配過程典尾。對于誤差,與其直接有關的誤差分別是糊探。在神經網絡前向傳播的過程中钾埂,也通過權重傳遞了過來,最終匯總形成了科平。
我們用一個簡單的方式理解一下:不妨假設激活函數為勃教,則,那么由全微分公式
即
如果我們將看成相應的誤差匠抗,那么的誤差其實就是誤差的加權和故源。事實上誤差的傳播公式也是這么回事,感興趣的可以自行搜索一下。
因此,如果我們現(xiàn)在知道了呆奕,想要求解嘱朽,很自然的一個想法,就是根據權重再將分解下去。即
其實這樣的分解應該不是正確的,因為此時
并不一定成立。但這是一個很合理的分配方式奶是,相信你也是這么認為的。
讓我們回到上述模型竣灌,則從那里分到的誤差為
同理聂沙,它從那里分到的誤差為
由于和本身并不存在直接關系,因此這兩者分給的誤差直接相加初嘹,就可以得到
以此類推
舉個例子看一下
事實上及汉,我們更加關心的不是誤差本身,而是對于誤差的分配屯烦,即將后一層的誤差按照權重向前一層進行傳遞坷随。所以,為了進一步的簡化計算驻龟,我們直接將上式中的分子去掉温眉,即
有些同學會對此感到迷惑,因為他們的分母并不完全一樣啊翁狐,怎么能說刪就刪呢类溢?可以這么想,來自于兩個部分谴蔑,分別是和豌骏,這兩者首先不是直接相關的,因此我們可以對于誤差直接相加隐锭,得到窃躲。而其中來自于的部分,其分母是對于的歸一化處理钦睡,我們可以將其轉化為蒂窒,這并不改變按權重分配的本質。同樣的荞怒,可以將轉化為洒琢。最后如之前一般再將二者直接相加,就得到了
明白了嗎褐桌?其實應該是似懂非懂衰抑,因為整個過程在數學上有些缺乏嚴謹性,更多的是從直觀理解的角度進行處理荧嵌,比如誤差直接按權分配呛踊,去除歸一化的因子等等。如果覺得不太能接受啦撮,那可以去看看更加數學化的誤差反向傳播方法的推導谭网。本文關于誤差反向傳播的說明,好像確實有種邪·教的感覺hhh赃春。但是本質上的思想應該是一樣的愉择。
最后,為了方便表示织中,我們記
則由上文锥涕,我們知道
即
權重更新
以上我們(或許?)知道了誤差是如何進行反向傳播的狭吼,并進行了矩陣的表示站楚,接下來就可以根據誤差對權重進行調整啦。
首先搏嗡,對于神經網絡任意一層的誤差
所以對于相應的權重矩陣中的任一個參數
再看一眼下圖窿春,我們會發(fā)現(xiàn),中與有關的部分采盒,僅僅由后一層的第個神經元有關旧乞,相應的誤差部分,也僅僅與第個神經元的誤差有關磅氨。(例如下圖中只與有關)所以
根據鏈式法則(微積分中的求導法則都忘了尺栖?)
解釋一下這部分。首先烦租,是后一層第個輸出延赌,則是前一層第個節(jié)點的輸出除盏,中則是固定的。例如上圖挫以,對于者蠕,它僅與有關,而相應神經元的輸出掐松,則與前一層的三個神經元都有關系踱侣,就是前文提到的前向傳播啦。(說明:大磺,表示神經網絡第層)
對于激活函數
所以
最后的抡句,自然是上一層的第個輸出啦,剛剛講過了杠愧。(說明:若待榔,表示神經網絡第層)
考慮到在進行迭代時,導數前面的系數意義不大(還有學習率支撐嘛)流济,所以我們可以把式子中的刪掉究抓,另外,就得到了誤差關于權重的導數
相應的參數更新公式則為
其中為學習率袭灯。
但是是一個權重矩陣刺下,所以我們有必要把上式也轉化成矩陣的形式,便于計算稽荧。以上文的那個三層神經網絡為例橘茉,第二層的權重更新公式為
其中均是上文提到過的前向傳播過程中的列向量。
相信你可以自己寫出的迭代公式了…
以上姨丈,參數的更新部分就全部結束了
MNIST數據集
接下來介紹一下MNIST手寫數字圖像集畅卓。MNIST是機器學習領域最有名的數據集之一,被應用于從簡單的實驗到發(fā)表的論文研究等各種場合蟋恬。MNIST數據集是由0到9的數字圖像構成的翁潘。訓練圖像有6萬張,測試圖像有1萬張歼争,主要分別用于訓練和評價拜马。
MNIST數據集的一般使用方法是,先用訓練圖像進行學習沐绒,再用學習到的模型度量能在多大程度上對測試圖像進行正確的分類俩莽。
MNIST的圖像數據是28像素 × 28像素的灰度圖像(1通道),各個像素的取值在0到255之間乔遮。每個圖像數據都相應地標有“2”扮超、“3”、“9”等標簽。因此出刷,這些圖像在進行分類時的輸入特征就是28*28=784個位置的像素灰度取值璧疗,輸出特征就是相應的最可能的數字標簽。
由于這類圖像是比較簡單的灰度圖馁龟,所以完全可以自己手寫一些數字崩侠,將其轉化成28×28的灰度圖像,進而進行驗證屁柏。更加直接地看到神經網絡的力量啦膜。
原始數據集網址:http://yann.lecun.com/exdb/mnist/
代碼實現(xiàn)
原始的數據集應該是無法直接使用的……這里提供一個網址:https://pjreddie.com/projects/mnist-in-csv/
該網站提供了將原始數據集轉化為csv格式的代碼有送,以及csv的文件淌喻。同時也是接下來的代碼使用到的數據集文件,如果需要的話自行下載吧雀摘。
代碼也放在下面了裸删,沒有依賴現(xiàn)成的第三方庫,完全是把上文的過程用代碼重述了一遍阵赠。如果你已經對于本文的內容很理解了涯塔,那么看這段代碼應該沒有任何問題。如果你看代碼還是有問題清蚀,不如再把文章讀一讀匕荸?
# -*- coding: utf-8 -*-
import numpy as np
import scipy.special
import matplotlib.pyplot as plt
class neuralNetwork:
def __init__(self,inputnodes,hiddennodes,outputnodes,learningRate):
#定義一個三層的神經網絡
#初始化輸入層節(jié)點數量,隱藏層節(jié)點數量枷邪,輸出層節(jié)點數量
self.inodes=inputnodes
self.hnodes=hiddennodes
self.onodes=outputnodes
#初始化兩層之間的權重矩陣
self.wih=(np.random.rand(self.hnodes,self.inodes)-0.5)
self.who=(np.random.rand(self.onodes,self.hnodes)-0.5)
#初始化激活函數
self.activation_function=lambda x:scipy.special.expit(x)
#初始化學習率
self.lr=learningRate
pass
#訓練函數
def train(self,inputs_list,targets_list):
#將輸入的列表轉化為list便于計算
inputs = np.array(inputs_list,ndmin=2).T
targets=np.array(targets_list,ndmin=2).T
#前向傳播一次榛搔,計算隱藏層的輸入和輸出
hidden_inputs=np.dot(self.wih,inputs)
hidden_outputs=self.activation_function(hidden_inputs)
#再前向傳播一次,計算輸出層的輸入和輸出
final_inputs=np.dot(self.who,hidden_outputs)
final_outputs=self.activation_function(final_inputs)
#確定輸出層的誤差與隱藏層的誤差
output_errors=targets-final_outputs
hidden_errors=np.dot(self.who.T,output_errors)
#根據誤差东揣,使用梯度下降法調整權重
self.who+=self.lr*np.dot((output_errors*final_outputs*(1.0-final_outputs)),np.transpose(hidden_outputs))
self.wih+=self.lr*np.dot((hidden_errors*hidden_outputs*(1.0-hidden_outputs)),np.transpose(inputs))
pass
#查詢函數
def query(self,inputs_list):
#將列表轉化成矩陣
inputs=np.array(inputs_list,ndmin=2).T
#進行兩次前向傳播得到最終解
hidden_inputs=np.dot(self.wih,inputs)
hidden_outputs=self.activation_function(hidden_inputs)
final_inputs=np.dot(self.who,hidden_outputs)
final_outputs=self.activation_function(final_inputs)
return final_outputs
input_nodes=784
hidden_nodes=100
output_nodes=10
lr=0.25
n=neuralNetwork(input_nodes,hidden_nodes,output_nodes,lr)
training_data_file=open("mnist_train.csv","r")
training_data_list=training_data_file.readlines()
training_data_file.close()
'''
for record in training_data_list:
all_values=record.split(",")
#ia=np.asfarray(all_values[1:]).reshape((28,28))
#plt.imshow(ia,cmap="Greys",interpolation="None") 畫出圖像
inputs=(np.asfarray(all_values[1:])/255.0*0.99)+0.01 #標準化處理
targets=np.zeros(output_nodes)+0.01
targets[int(all_values[0])]=0.99 #處理輸出向量
n.train(inputs,targets)
pass
'''
epochs=4
for e in range(epochs):
for record in training_data_list:
all_values=record.split(",")
#ia=np.asfarray(all_values[1:]).reshape((28,28))
#plt.imshow(ia,cmap="Greys",interpolation="None") 畫出圖像
inputs=(np.asfarray(all_values[1:])/255.0*0.99)+0.01 #標準化處理
targets=np.zeros(output_nodes)+0.01
targets[int(all_values[0])]=0.99 #處理輸出向量
n.train(inputs,targets)
pass
test_data_file=open("mnist_test.csv","r")
test_data_list=test_data_file.readlines()
test_data_file.close()
score=[]
for record in test_data_list:
test_value=record.split(",")
correct_label=int(test_value[0])
#print(correct_label," correct_label")
inputs=(np.asfarray(test_value[1:])/255.0*0.99)+0.01
outputs=n.query(inputs)
label=np.argmax(outputs)
#print(label," network's answer")
if(label==correct_label):
score.append(1)
else:
score.append(0)
pass
pass
print("performance = ",sum(score)/len(score))
寫得累死我了……其實除了反向傳播那里有些邪·教外践惑,其他部分都還好,當然我的敘述可能是有一些啰嗦嘶卧。這篇文章比較適合完全不了解神經網絡的朋友了解一下神經網絡尔觉,其實并沒有想象的那么困難,幾十行代碼就可以實現(xiàn)一個簡單的神經網絡了芥吟。
如果深入學習的話侦铜,不妨去聽一聽吳恩達的深度學習,會更專業(yè)一些……不钟鸵,會專業(yè)很多泵额。畢竟這個神經網絡連偏置都沒有。
最后携添,不管怎樣嫁盲,感謝你看到這里,寫的不容易,看的就更不容易了……是不是應該把它拆成兩篇羞秤?
不管了缸托,就醬,bye~