有個事情可能會讓初學(xué)者驚訝:神經(jīng)網(wǎng)絡(luò)模型并不復(fù)雜!『神經(jīng)網(wǎng)絡(luò)』這個詞讓人覺得很高大上,但實際上神經(jīng)網(wǎng)絡(luò)算法要比人們想象的簡單。
這篇文章完全是為新手準(zhǔn)備的。我們會通過用Python從頭實現(xiàn)一個神經(jīng)網(wǎng)絡(luò)來理解神經(jīng)網(wǎng)絡(luò)的原理。本文的脈絡(luò)是:
介紹了神經(jīng)網(wǎng)絡(luò)的基本結(jié)構(gòu)——神經(jīng)元;
在神經(jīng)元中使用S型激活函數(shù);
神經(jīng)網(wǎng)絡(luò)就是連接在一起的神經(jīng)元;
構(gòu)建了一個數(shù)據(jù)集,輸入(或特征)是體重和身高,輸出(或標(biāo)簽)是性別;
學(xué)習(xí)了損失函數(shù)和均方差損失;
訓(xùn)練網(wǎng)絡(luò)就是最小化其損失;
用反向傳播方法計算偏導(dǎo);
用隨機(jī)梯度下降法訓(xùn)練網(wǎng)絡(luò)。 ?
磚塊:神經(jīng)元
首先讓我們看看神經(jīng)網(wǎng)絡(luò)的基本單位,神經(jīng)元。神經(jīng)元接受輸入,對其做一些數(shù)據(jù)操作,然后產(chǎn)生輸出。例如,這是一個2-輸入神經(jīng)元:
這里發(fā)生了三個事情。首先,每個輸入都跟一個權(quán)重相乘(紅色):
然后,加權(quán)后的輸入求和,加上一個偏差b(綠色):
最后,這個結(jié)果傳遞給一個激活函數(shù)f:
激活函數(shù)的用途是將一個無邊界的輸入,轉(zhuǎn)變成一個可預(yù)測的形式。常用的激活函數(shù)就就是S型函數(shù):
S型函數(shù)的值域是(0, 1)。簡單來說,就是把(?∞, +∞)壓縮到(0, 1) ,很大的負(fù)數(shù)約等于0,很大的正數(shù)約等于1。
一個簡單的例子
假設(shè)我們有一個神經(jīng)元,激活函數(shù)就是S型函數(shù),其參數(shù)如下:
就是以向量的形式表示?,F(xiàn)在,我們給這個神經(jīng)元一個輸入。我們用點(diǎn)積來表示:
當(dāng)輸入是[2, 3]時,這個神經(jīng)元的輸出是0.999。給定輸入,得到輸出的過程被稱為前饋(feedforward)。
編碼一個神經(jīng)元
讓我們來實現(xiàn)一個神經(jīng)元!用Python的NumPy庫來完成其中的數(shù)學(xué)計算:
?
?
import?numpy?as?np def?sigmoid(x): ??#?我們的激活函數(shù):?f(x)?=?1?/?(1?+?e^(-x)) ??return?1?/?(1?+?np.exp(-x)) class?Neuron: ??def?__init__(self,?weights,?bias): ????self.weights?=?weights ????self.bias?=?bias ??def?feedforward(self,?inputs): ????#?加權(quán)輸入,加入偏置,然后使用激活函數(shù) ????total?=?np.dot(self.weights,?inputs)?+?self.bias ????return?sigmoid(total) weights?=?np.array([0,?1])?#?w1?=?0,?w2?=?1 bias?=?4???????????????????#?b?=?4 n?=?Neuron(weights,?bias) x?=?np.array([2,?3])???????#?x1?=?2,?x2?=?3 print(n.feedforward(x))????#?0.9990889488055994
?
?
還記得這個數(shù)字嗎?就是我們前面算出來的例子中的0.999。
把神經(jīng)元組裝成網(wǎng)絡(luò)
所謂的神經(jīng)網(wǎng)絡(luò)就是一堆神經(jīng)元。這就是一個簡單的神經(jīng)網(wǎng)絡(luò):
這個網(wǎng)絡(luò)有兩個輸入,一個有兩個神經(jīng)元(?和?)的隱藏層,以及一個有一個神經(jīng)元(?) )的輸出層。要注意,?的輸入就是?和??的輸出,這樣就組成了一個網(wǎng)絡(luò)。
隱藏層就是輸入層和輸出層之間的層,隱藏層可以是多層的。
例子:前饋
我們繼續(xù)用前面圖中的網(wǎng)絡(luò),假設(shè)每個神經(jīng)元的權(quán)重都是??,截距項也相同??,激活函數(shù)也都是S型函數(shù)。分別用?表示相應(yīng)的神經(jīng)元的輸出。
當(dāng)輸入??時,會得到什么結(jié)果?
這個神經(jīng)網(wǎng)絡(luò)對輸入的輸出是0.7216,很簡單。
一個神經(jīng)網(wǎng)絡(luò)的層數(shù)以及每一層中的神經(jīng)元數(shù)量都是任意的?;具壿嫸家粯樱狠斎朐谏窠?jīng)網(wǎng)絡(luò)中向前傳輸,最終得到輸出。接下來,我們會繼續(xù)使用前面的這個網(wǎng)絡(luò)。
編碼神經(jīng)網(wǎng)絡(luò):前饋
接下來我們實現(xiàn)這個神經(jīng)網(wǎng)絡(luò)的前饋機(jī)制,還是這個圖:
?
?
import?numpy?as?np #?...?code?from?previous?section?here class?OurNeuralNetwork: ??''' ??A?neural?network?with: ????-?2?inputs ????-?a?hidden?layer?with?2?neurons?(h1,?h2) ????-?an?output?layer?with?1?neuron?(o1) ??Each?neuron?has?the?same?weights?and?bias: ????-?w?=?[0,?1] ????-?b?=?0 ??''' ??def?__init__(self): ????weights?=?np.array([0,?1]) ????bias?=?0 ????#?這里是來自前一節(jié)的神經(jīng)元類 ????self.h1?=?Neuron(weights,?bias) ????self.h2?=?Neuron(weights,?bias) ????self.o1?=?Neuron(weights,?bias) ??def?feedforward(self,?x): ????out_h1?=?self.h1.feedforward(x) ????out_h2?=?self.h2.feedforward(x) ????#?o1的輸入是h1和h2的輸出 ????out_o1?=?self.o1.feedforward(np.array([out_h1,?out_h2])) ????return?out_o1 network?=?OurNeuralNetwork() x?=?np.array([2,?3]) print(network.feedforward(x))?#?0.7216325609518421
?
?
結(jié)果正確,看上去沒問題。
訓(xùn)練神經(jīng)網(wǎng)絡(luò) 第一部分
現(xiàn)在有這樣的數(shù)據(jù):
?
姓名 | 體重(磅) | 身高 (英寸) | 性別 |
---|---|---|---|
Alice | 133 | 65 | F |
Bob | 160 | 72 | M |
Charlie | 152 | 70 | M |
Diana | 120 | 60 | F |
?
接下來我們用這個數(shù)據(jù)來訓(xùn)練神經(jīng)網(wǎng)絡(luò)的權(quán)重和截距項,從而可以根據(jù)身高體重預(yù)測性別:
我們用0和1分別表示男性(M)和女性(F),并對數(shù)值做了轉(zhuǎn)化:
?
姓名 | 體重 (減 135) | 身高 (減 66) | 性別 |
---|---|---|---|
Alice | -2 | -1 | 1 |
Bob | 25 | 6 | 0 |
Charlie | 17 | 4 | 0 |
Diana | -15 | -6 | 1 |
?
我這里是隨意選取了135和66來標(biāo)準(zhǔn)化數(shù)據(jù),通常會使用平均值。
損失
在訓(xùn)練網(wǎng)絡(luò)之前,我們需要量化當(dāng)前的網(wǎng)絡(luò)是『好』還是『壞』,從而可以尋找更好的網(wǎng)絡(luò)。這就是定義損失的目的。
我們在這里用平均方差(MSE)損失:?,讓我們仔細(xì)看看:
是樣品數(shù),這里等于4(Alice、Bob、Charlie和Diana)。
表示要預(yù)測的變量,這里是性別。
是變量的真實值(『正確答案』)。例如,Alice的??就是1(男性)。
變量的預(yù)測值。這就是我們網(wǎng)絡(luò)的輸出。
被稱為方差(squared error)。我們的損失函數(shù)就是所有方差的平均值。預(yù)測效果越好,損失就越少。
更好的預(yù)測 = 更少的損失!
訓(xùn)練網(wǎng)絡(luò) = 最小化它的損失。
損失計算例子
假設(shè)我們的網(wǎng)絡(luò)總是輸出0,換言之就是認(rèn)為所有人都是男性。損失如何?
?
Name | y_true | y_pred | (y_true - y_pred)^2 |
---|---|---|---|
Alice | 1 | 0 | 1 |
Bob | 0 | 0 | 0 |
Charlie | 0 | 0 | 0 |
Diana | 1 | 0 | 1 |
?
代碼:MSE損失
下面是計算MSE損失的代碼:
?
?
import?numpy?as?np def?mse_loss(y_true,?y_pred): ??#?y_true?and?y_pred?are?numpy?arrays?of?the?same?length. ??return?((y_true?-?y_pred)?**?2).mean() y_true?=?np.array([1,?0,?0,?1]) y_pred?=?np.array([0,?0,?0,?0]) print(mse_loss(y_true,?y_pred))?#?0.5
?
?
如果你不理解這段代碼,可以看看NumPy的快速入門中關(guān)于數(shù)組的操作。
好的,繼續(xù)。
訓(xùn)練神經(jīng)網(wǎng)絡(luò) 第二部分
現(xiàn)在我們有了一個明確的目標(biāo):最小化神經(jīng)網(wǎng)絡(luò)的損失。通過調(diào)整網(wǎng)絡(luò)的權(quán)重和截距項,我們可以改變其預(yù)測結(jié)果,但如何才能逐步地減少損失?
這一段內(nèi)容涉及到多元微積分,如果不熟悉微積分的話,可以跳過這些數(shù)學(xué)內(nèi)容。
為了簡化問題,假設(shè)我們的數(shù)據(jù)集中只有Alice:
假設(shè)我們的網(wǎng)絡(luò)總是輸出0,換言之就是認(rèn)為所有人都是男性。損失如何?
?
姓名 | 體重 (減 135) | 身高 (減 66) | Gender |
---|---|---|---|
Alice | -2 | -1 | 1 |
?
那均方差損失就只是Alice的方差:
也可以把損失看成是權(quán)重和截距項的函數(shù)。讓我們給網(wǎng)絡(luò)標(biāo)上權(quán)重和截距項:
這樣我們就可以把網(wǎng)絡(luò)的損失表示為:
假設(shè)我們要優(yōu)化??,當(dāng)我們改變??時,損失??會怎么變化?可以用??來回答這個問題,怎么計算?
接下來的數(shù)據(jù)稍微有點(diǎn)復(fù)雜,別擔(dān)心,準(zhǔn)備好紙和筆。
首先,讓我們用來改寫這個偏導(dǎo)數(shù):
因為我們已經(jīng)知道??,所以我們可以計算
現(xiàn)在讓我們來搞定??。分別是其所表示的神經(jīng)元的輸出,我們有:
由于??只會影響??(不會影響??),所以:
對??,我們也可以這么做:
在這里,?是身高,?是體重。這是我們第二次看到??(S型函數(shù)的導(dǎo)數(shù))了。求解:
稍后我們會用到這個??。
我們已經(jīng)把?分解成了幾個我們能計算的部分:
這種計算偏導(dǎo)的方法叫『反向傳播算法』(backpropagation)。
好多數(shù)學(xué)符號,如果你還沒搞明白的話,我們來看一個實際例子。
例子:計算偏導(dǎo)數(shù)
我們還是看數(shù)據(jù)集中只有Alice的情況:
?
Name | ? | ? | ? |
---|---|---|---|
Alice | 1 | 0 | 1 |
姓名 | 身高 (minus 135) | 體重 (minus 66) | Gender |
---|---|---|---|
Alice | -2 | -1 | 1 |
?
把所有的權(quán)重和截距項都分別初始化為1和0。在網(wǎng)絡(luò)中做前饋計算:
網(wǎng)絡(luò)的輸出是??,對于Male(0)或者Female(1)都沒有太強(qiáng)的傾向性。算一下
提示:前面已經(jīng)得到了S型激活函數(shù)的導(dǎo)數(shù)??。
搞定!這個結(jié)果的意思就是增加也會隨之輕微上升。
訓(xùn)練:隨機(jī)梯度下降
現(xiàn)在訓(xùn)練神經(jīng)網(wǎng)絡(luò)已經(jīng)萬事俱備了!我們會使用名為隨機(jī)梯度下降法的優(yōu)化算法來優(yōu)化網(wǎng)絡(luò)的權(quán)重和截距項,實現(xiàn)損失的最小化。核心就是這個更新等式:
是一個常數(shù),被稱為學(xué)習(xí)率,用于調(diào)整訓(xùn)練的速度。我們要做的就是用??減去
如果??是正數(shù),??變小,?會下降。
如果???是負(fù)數(shù),??會變大,??會上升。
如果我們對網(wǎng)絡(luò)中的每個權(quán)重和截距項都這樣進(jìn)行優(yōu)化,損失就會不斷下降,網(wǎng)絡(luò)性能會不斷上升。
我們的訓(xùn)練過程是這樣的:
從我們的數(shù)據(jù)集中選擇一個樣本,用隨機(jī)梯度下降法進(jìn)行優(yōu)化——每次我們都只針對一個樣本進(jìn)行優(yōu)化;
計算每個權(quán)重或截距項對損失的偏導(dǎo)(例如?、?等);
用更新等式更新每個權(quán)重和截距項;
重復(fù)第一步;
代碼:一個完整的神經(jīng)網(wǎng)絡(luò)
我們終于可以實現(xiàn)一個完整的神經(jīng)網(wǎng)絡(luò)了:
?
姓名 | 身高 (減 135) | 體重 (減 66) | Gender |
---|---|---|---|
Alice | -2 | -1 | 1 |
Bob | 25 | 6 | 0 |
Charlie | 17 | 4 | 0 |
Diana | -15 | -6 | 1 |
?
?
?
import?numpy?as?np def?sigmoid(x): ??#?Sigmoid?activation?function:?f(x)?=?1?/?(1?+?e^(-x)) ??return?1?/?(1?+?np.exp(-x)) def?deriv_sigmoid(x): ??#?Derivative?of?sigmoid:?f'(x)?=?f(x)?*?(1?-?f(x)) ??fx?=?sigmoid(x) ??return?fx?*?(1?-?fx) def?mse_loss(y_true,?y_pred): ??# y_true和y_pred是相同長度的numpy數(shù)組。 ??return?((y_true?-?y_pred)?**?2).mean() class?OurNeuralNetwork: ??''' ??A?neural?network?with: ????-?2?inputs ????-?a?hidden?layer?with?2?neurons?(h1,?h2) ????-?an?output?layer?with?1?neuron?(o1) ??***?免責(zé)聲明?***: ????下面的代碼是為了簡單和演示,而不是最佳的。 ????真正的神經(jīng)網(wǎng)絡(luò)代碼與此完全不同。不要使用此代碼。 ????相反,讀/運(yùn)行它來理解這個特定的網(wǎng)絡(luò)是如何工作的。 ??''' ??def?__init__(self): ????#?權(quán)重,Weights ????self.w1?=?np.random.normal() ????self.w2?=?np.random.normal() ????self.w3?=?np.random.normal() ????self.w4?=?np.random.normal() ????self.w5?=?np.random.normal() ????self.w6?=?np.random.normal() ????#?截距項,Biases ????self.b1?=?np.random.normal() ????self.b2?=?np.random.normal() ????self.b3?=?np.random.normal() ??def?feedforward(self,?x): ????# X是一個有2個元素的數(shù)字?jǐn)?shù)組。 ????h1?=?sigmoid(self.w1?*?x[0]?+?self.w2?*?x[1]?+?self.b1) ????h2?=?sigmoid(self.w3?*?x[0]?+?self.w4?*?x[1]?+?self.b2) ????o1?=?sigmoid(self.w5?*?h1?+?self.w6?*?h2?+?self.b3) ????return?o1 ??def?train(self,?data,?all_y_trues): ????''' ????-?data?is?a?(n?x?2)?numpy?array,?n?=?#?of?samples?in?the?dataset. ????-?all_y_trues?is?a?numpy?array?with?n?elements. ??????Elements?in?all_y_trues?correspond?to?those?in?data. ????''' ????learn_rate?=?0.1 ????epochs?=?1000?#?遍歷整個數(shù)據(jù)集的次數(shù) ????for?epoch?in?range(epochs): ??????for?x,?y_true?in?zip(data,?all_y_trues): ????????#?---?做一個前饋(稍后我們將需要這些值) ????????sum_h1?=?self.w1?*?x[0]?+?self.w2?*?x[1]?+?self.b1 ????????h1?=?sigmoid(sum_h1) ????????sum_h2?=?self.w3?*?x[0]?+?self.w4?*?x[1]?+?self.b2 ????????h2?=?sigmoid(sum_h2) ????????sum_o1?=?self.w5?*?h1?+?self.w6?*?h2?+?self.b3 ????????o1?=?sigmoid(sum_o1) ????????y_pred?=?o1 ????????#?---?計算偏導(dǎo)數(shù)。 ????????#?---?Naming:?d_L_d_w1?represents?"partial?L?/?partial?w1" ????????d_L_d_ypred?=?-2?*?(y_true?-?y_pred) ????????#?Neuron?o1 ????????d_ypred_d_w5?=?h1?*?deriv_sigmoid(sum_o1) ????????d_ypred_d_w6?=?h2?*?deriv_sigmoid(sum_o1) ????????d_ypred_d_b3?=?deriv_sigmoid(sum_o1) ????????d_ypred_d_h1?=?self.w5?*?deriv_sigmoid(sum_o1) ????????d_ypred_d_h2?=?self.w6?*?deriv_sigmoid(sum_o1) ????????#?Neuron?h1 ????????d_h1_d_w1?=?x[0]?*?deriv_sigmoid(sum_h1) ????????d_h1_d_w2?=?x[1]?*?deriv_sigmoid(sum_h1) ????????d_h1_d_b1?=?deriv_sigmoid(sum_h1) ????????#?Neuron?h2 ????????d_h2_d_w3?=?x[0]?*?deriv_sigmoid(sum_h2) ????????d_h2_d_w4?=?x[1]?*?deriv_sigmoid(sum_h2) ????????d_h2_d_b2?=?deriv_sigmoid(sum_h2) ????????#?---?更新權(quán)重和偏差 ????????#?Neuron?h1 ????????self.w1?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_h1?*?d_h1_d_w1 ????????self.w2?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_h1?*?d_h1_d_w2 ????????self.b1?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_h1?*?d_h1_d_b1 ????????#?Neuron?h2 ????????self.w3?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_h2?*?d_h2_d_w3 ????????self.w4?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_h2?*?d_h2_d_w4 ????????self.b2?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_h2?*?d_h2_d_b2 ????????#?Neuron?o1 ????????self.w5?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_w5 ????????self.w6?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_w6 ????????self.b3?-=?learn_rate?*?d_L_d_ypred?*?d_ypred_d_b3 ??????#?---?在每次epoch結(jié)束時計算總損失? ??????if?epoch?%?10?==?0: ????????y_preds?=?np.apply_along_axis(self.feedforward,?1,?data) ????????loss?=?mse_loss(all_y_trues,?y_preds) ????????print("Epoch?%d?loss:?%.3f"?%?(epoch,?loss)) #?定義數(shù)據(jù)集 data?=?np.array([ ??[-2,?-1],??#?Alice ??[25,?6],???#?Bob ??[17,?4],???#?Charlie ??[-15,?-6],?#?Diana ]) all_y_trues?=?np.array([ ??1,?#?Alice ??0,?#?Bob ??0,?#?Charlie ??1,?#?Diana ]) #?訓(xùn)練我們的神經(jīng)網(wǎng)絡(luò)! network?=?OurNeuralNetwork() network.train(data,?all_y_trues)
?
?
隨著網(wǎng)絡(luò)的學(xué)習(xí),損失在穩(wěn)步下降。
現(xiàn)在我們可以用這個網(wǎng)絡(luò)來預(yù)測性別了:
?
?
#?做一些預(yù)測 emily?=?np.array([-7,?-3])?#?128?磅,?63?英寸 frank?=?np.array([20,?2])??#?155?磅,?68?英寸 print("Emily:?%.3f"?%?network.feedforward(emily))?#?0.951?-?F print("Frank:?%.3f"?%?network.feedforward(frank))?#?0.039?-?M
?
?
接下來?
搞定了一個簡單的神經(jīng)網(wǎng)絡(luò),快速回顧一下:
介紹了神經(jīng)網(wǎng)絡(luò)的基本結(jié)構(gòu)——神經(jīng)元;
在神經(jīng)元中使用S型激活函數(shù);
神經(jīng)網(wǎng)絡(luò)就是連接在一起的神經(jīng)元;
構(gòu)建了一個數(shù)據(jù)集,輸入(或特征)是體重和身高,輸出(或標(biāo)簽)是性別;
學(xué)習(xí)了損失函數(shù)和均方差損失;
訓(xùn)練網(wǎng)絡(luò)就是最小化其損失;
用反向傳播方法計算偏導(dǎo);
用隨機(jī)梯度下降法訓(xùn)練網(wǎng)絡(luò);
接下來你還可以:
用機(jī)器學(xué)習(xí)庫實現(xiàn)更大更好的神經(jīng)網(wǎng)絡(luò),例如TensorFlow、Keras和PyTorch;
其他類型的激活函數(shù);
其他類型的優(yōu)化器;
學(xué)習(xí)卷積神經(jīng)網(wǎng)絡(luò),這給計算機(jī)視覺領(lǐng)域帶來了革命; 學(xué)習(xí)遞歸神經(jīng)網(wǎng)絡(luò),常用于自然語言處理;
作者:Victor Zhou?
編輯:黃飛
評論
查看更多