小白都能看懂的神經(jīng)網(wǎng)絡(luò)教程:從原理到優(yōu)化如此簡(jiǎn)單
程序員們紛紛夸贊這篇文章的代碼寫得很好,變量名很規(guī)范,讓人一目了然。
曉查 發(fā)自 凹非寺
量子位 報(bào)道 | 公眾號(hào) QbitAI
“我在網(wǎng)上看到過(guò)很多神經(jīng)網(wǎng)絡(luò)的實(shí)現(xiàn)方法,但這一篇是最簡(jiǎn)單、最清晰的。”
一位來(lái)自普林斯頓的華人小哥Victor Zhou,寫了篇神經(jīng)網(wǎng)絡(luò)入門教程,在線代碼網(wǎng)站Repl.it聯(lián)合創(chuàng)始人Amjad Masad看完以后,給予如是評(píng)價(jià)。
這篇教程發(fā)布僅天時(shí)間,就在Hacker News論壇上收獲了574贊。程序員們紛紛夸贊這篇文章的代碼寫得很好,變量名很規(guī)范,讓人一目了然。
下面就讓我們一起從零開始學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)吧。
實(shí)現(xiàn)方法
搭建基本模塊——神經(jīng)元
在說(shuō)神經(jīng)網(wǎng)絡(luò)之前,我們討論一下神經(jīng)元(Neurons),它是神經(jīng)網(wǎng)絡(luò)的基本單元。神經(jīng)元先獲得輸入,然后執(zhí)行某些數(shù)學(xué)運(yùn)算后,再產(chǎn)生一個(gè)輸出。比如一個(gè)2輸入神經(jīng)元的例子:
在這個(gè)神經(jīng)元中,輸入總共經(jīng)歷了3步數(shù)學(xué)運(yùn)算,
先將兩個(gè)輸入乘以權(quán)重(weight):
x1→x1 × w1
x2→x2 × w2
把兩個(gè)結(jié)果想加,再加上一個(gè)偏置(bias):
(x1 × w1)+(x2 × w2)+ b
最后將它們經(jīng)過(guò)激活函數(shù)(activation function)處理得到輸出:
y = f(x1 × w1 + x2 × w2 + b)
激活函數(shù)的作用是將無(wú)限制的輸入轉(zhuǎn)換為可預(yù)測(cè)形式的輸出。一種常用的激活函數(shù)是sigmoid函數(shù):
sigmoid函數(shù)的輸出介于0和1,我們可以理解為它把 (?∞,+∞) 范圍內(nèi)的數(shù)壓縮到 (0, 1)以內(nèi)。正值越大輸出越接近1,負(fù)向數(shù)值越大輸出越接近0。
舉個(gè)例子,上面神經(jīng)元里的權(quán)重和偏置取如下數(shù)值:
w=[0,1]
b = 4
w=[0,1]是w1=0、w2=1的向量形式寫法。給神經(jīng)元一個(gè)輸入x=[2,3],可以用向量點(diǎn)積的形式把神經(jīng)元的輸出計(jì)算出來(lái):
w·x+b =(x1 × w1)+(x2 × w2)+ b = 0×2+1×3+4=7
y=f(w?X+b)=f(7)=0.999
以上步驟的Python代碼是:
import numpy as np def sigmoid(x): # Our activation function: 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): # Weight inputs, add bias, then use the activation function 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
我們?cè)诖a中調(diào)用了一個(gè)強(qiáng)大的Python數(shù)學(xué)函數(shù)庫(kù)NumPy。
搭建神經(jīng)網(wǎng)絡(luò)
神經(jīng)網(wǎng)絡(luò)就是把一堆神經(jīng)元連接在一起,下面是一個(gè)神經(jīng)網(wǎng)絡(luò)的簡(jiǎn)單舉例:
這個(gè)網(wǎng)絡(luò)有2個(gè)輸入、一個(gè)包含2個(gè)神經(jīng)元的隱藏層(h1和h2)、包含1個(gè)神經(jīng)元的輸出層o1。
隱藏層是夾在輸入輸入層和輸出層之間的部分,一個(gè)神經(jīng)網(wǎng)絡(luò)可以有多個(gè)隱藏層。
把神經(jīng)元的輸入向前傳遞獲得輸出的過(guò)程稱為前饋(feedforward)。
我們假設(shè)上面的網(wǎng)絡(luò)里所有神經(jīng)元都具有相同的權(quán)重w=[0,1]和偏置b=0,激活函數(shù)都是sigmoid,那么我們會(huì)得到什么輸出呢?
h1=h2=f(w?x+b)=f((0×2)+(1×3)+0)
=f(3)
=0.9526
o1=f(w?[h1,h2]+b)=f((0?h1)+(1?h2)+0)
=f(0.9526)
=0.7216
以下是實(shí)現(xiàn)代碼:
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 # The Neuron class here is from the previous section 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) # The inputs for o1 are the outputs from h1 and 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
訓(xùn)練神經(jīng)網(wǎng)絡(luò)
現(xiàn)在我們已經(jīng)學(xué)會(huì)了如何搭建神經(jīng)網(wǎng)絡(luò),現(xiàn)在我們來(lái)學(xué)習(xí)如何訓(xùn)練它,其實(shí)這就是一個(gè)優(yōu)化的過(guò)程。
假設(shè)有一個(gè)數(shù)據(jù)集,包含4個(gè)人的身高、體重和性別:
現(xiàn)在我們的目標(biāo)是訓(xùn)練一個(gè)網(wǎng)絡(luò),根據(jù)體重和身高來(lái)推測(cè)某人的性別。
為了簡(jiǎn)便起見,我們將每個(gè)人的身高、體重減去一個(gè)固定數(shù)值,把性別男定義為1、性別女定義為0。
在訓(xùn)練神經(jīng)網(wǎng)絡(luò)之前,我們需要有一個(gè)標(biāo)準(zhǔn)定義它到底好不好,以便我們進(jìn)行改進(jìn),這就是損失(loss)。
比如用均方誤差(MSE)來(lái)定義損失:
n是樣本的數(shù)量,在上面的數(shù)據(jù)集中是4;
y代表人的性別,男性是1,女性是0;
ytrue是變量的真實(shí)值,ypred是變量的預(yù)測(cè)值。
顧名思義,均方誤差就是所有數(shù)據(jù)方差的平均值,我們不妨就把它定義為損失函數(shù)。預(yù)測(cè)結(jié)果越好,損失就越低,訓(xùn)練神經(jīng)網(wǎng)絡(luò)就是將損失最小化。
如果上面網(wǎng)絡(luò)的輸出一直是0,也就是預(yù)測(cè)所有人都是男性,那么損失是:
MSE= 1/4 (1+0+0+1)= 0.5
計(jì)算損失函數(shù)的代碼如下:
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
減少神經(jīng)網(wǎng)絡(luò)損失
這個(gè)神經(jīng)網(wǎng)絡(luò)不夠好,還要不斷優(yōu)化,盡量減少損失。我們知道,改變網(wǎng)絡(luò)的權(quán)重和偏置可以影響預(yù)測(cè)值,但我們應(yīng)該怎么做呢?
為了簡(jiǎn)單起見,我們把數(shù)據(jù)集縮減到只包含Alice一個(gè)人的數(shù)據(jù)。于是損失函數(shù)就剩下Alice一個(gè)人的方差:
預(yù)測(cè)值是由一系列網(wǎng)絡(luò)權(quán)重和偏置計(jì)算出來(lái)的:
所以損失函數(shù)實(shí)際上是包含多個(gè)權(quán)重、偏置的多元函數(shù):
(注意!前方高能!需要你有一些基本的多元函數(shù)微分知識(shí),比如偏導(dǎo)數(shù)、鏈?zhǔn)角髮?dǎo)法則。)
如果調(diào)整一下w1,損失函數(shù)是會(huì)變大還是變???我們需要知道偏導(dǎo)數(shù)?L/?w1是正是負(fù)才能回答這個(gè)問(wèn)題。
根據(jù)鏈?zhǔn)角髮?dǎo)法則:
而L=(1-ypred)2,可以求得第一項(xiàng)偏導(dǎo)數(shù):
接下來(lái)我們要想辦法獲得ypred和w1的關(guān)系,我們已經(jīng)知道神經(jīng)元h1、h2和o1的數(shù)學(xué)運(yùn)算規(guī)則:
實(shí)際上只有神經(jīng)元h1中包含權(quán)重w1,所以我們?cè)俅芜\(yùn)用鏈?zhǔn)角髮?dǎo)法則:
然后求?h1/?w1
我們?cè)谏厦娴挠?jì)算中遇到了2次激活函數(shù)sigmoid的導(dǎo)數(shù)f′(x),sigmoid函數(shù)的導(dǎo)數(shù)很容易求得:
總的鏈?zhǔn)角髮?dǎo)公式:
這種向后計(jì)算偏導(dǎo)數(shù)的系統(tǒng)稱為反向傳播(backpropagation)。
上面的數(shù)學(xué)符號(hào)太多,下面我們帶入實(shí)際數(shù)值來(lái)計(jì)算一下。h1、h2和o1
h1=f(x1?w1+x2?w2+b1)=0.0474
h2=f(w3?x3+w4?x4+b2)=0.0474
o1=f(w5?h1+w6?h2+b3)=f(0.0474+0.0474+0)=f(0.0948)=0.524
神經(jīng)網(wǎng)絡(luò)的輸出y=0.524,沒有顯示出強(qiáng)烈的是男(1)是女(0)的證據(jù)?,F(xiàn)在的預(yù)測(cè)效果還很不好。
我們?cè)儆?jì)算一下當(dāng)前網(wǎng)絡(luò)的偏導(dǎo)數(shù)?L/?w1:
這個(gè)結(jié)果告訴我們:如果增大w1,損失函數(shù)L會(huì)有一個(gè)非常小的增長(zhǎng)。
隨機(jī)梯度下降
下面將使用一種稱為隨機(jī)梯度下降(SGD)的優(yōu)化算法,來(lái)訓(xùn)練網(wǎng)絡(luò)。
經(jīng)過(guò)前面的運(yùn)算,我們已經(jīng)有了訓(xùn)練神經(jīng)網(wǎng)絡(luò)所有數(shù)據(jù)。但是該如何操作?SGD定義了改變權(quán)重和偏置的方法:
η是一個(gè)常數(shù),稱為學(xué)習(xí)率(learning rate),它決定了我們訓(xùn)練網(wǎng)絡(luò)速率的快慢。將w1減去η·?L/?w1,就等到了新的權(quán)重w1。
當(dāng)?L/?w1是正數(shù)時(shí),w1會(huì)變??;當(dāng)?L/?w1是負(fù)數(shù) 時(shí),w1會(huì)變大。
如果我們用這種方法去逐步改變網(wǎng)絡(luò)的權(quán)重w和偏置b,損失函數(shù)會(huì)緩慢地降低,從而改進(jìn)我們的神經(jīng)網(wǎng)絡(luò)。
訓(xùn)練流程如下:
1、從數(shù)據(jù)集中選擇一個(gè)樣本;
2、計(jì)算損失函數(shù)對(duì)所有權(quán)重和偏置的偏導(dǎo)數(shù);
3、使用更新公式更新每個(gè)權(quán)重和偏置;
4、回到第1步。
我們用Python代碼實(shí)現(xiàn)這個(gè)過(guò)程:
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 and y_pred are numpy arrays of the same length. 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) *** DISCLAIMER ***: The code below is intended to be simple and educational, NOT optimal. Real neural net code looks nothing like this. DO NOT use this code. Instead, read/run it to understand how this specific network works. ''' def __init__(self): # 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 is a numpy array with 2 elements. 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 # number of times to loop through the entire dataset for epoch in range(epochs): for x, y_true in zip(data, all_y_trues): # --- Do a feedforward (we'll need these values later) 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 # --- Calculate partial derivatives. # --- 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) # --- Update weights and biases # 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 # --- Calculate total loss at the end of each epoch 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)) # Define dataset 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 ]) # Train our neural network! network = OurNeuralNetwork() network.train(data, all_y_trues)
隨著學(xué)習(xí)過(guò)程的進(jìn)行,損失函數(shù)逐漸減小。
現(xiàn)在我們可以用它來(lái)推測(cè)出每個(gè)人的性別了:
# Make some predictions emily = np.array([-7, -3]) # 128 pounds, 63 inches frank = np.array([20, 2]) # 155 pounds, 68 inches print("Emily: %.3f" % network.feedforward(emily)) # 0.951 - F print("Frank: %.3f" % network.feedforward(frank)) # 0.039 - M
更多
這篇教程只是萬(wàn)里長(zhǎng)征第一步,后面還有很多知識(shí)需要學(xué)習(xí):
1、用更大更好的機(jī)器學(xué)習(xí)庫(kù)搭建神經(jīng)網(wǎng)絡(luò),如Tensorflow、Keras、PyTorch
2、在瀏覽器中的直觀理解神經(jīng)網(wǎng)絡(luò):https://playground.tensorflow.org/
3、學(xué)習(xí)sigmoid以外的其他激活函數(shù):https://keras.io/activations/
4、學(xué)習(xí)SGD以外的其他優(yōu)化器:https://keras.io/optimizers/
5、學(xué)習(xí)卷積神經(jīng)網(wǎng)絡(luò)(CNN)
6、學(xué)習(xí)遞歸神經(jīng)網(wǎng)絡(luò)(RNN)
這些都是Victor給自己挖的“坑”。他表示自己未來(lái)“可能”會(huì)寫這些主題內(nèi)容,希望他能陸續(xù)把這些坑填完。如果你想入門神經(jīng)網(wǎng)絡(luò),不妨去訂閱他的博客。
關(guān)于這位小哥
Victor Zhou是普林斯頓2019級(jí)CS畢業(yè)生,已經(jīng)拿到Facebook軟件工程師的offer,今年8月入職。他曾經(jīng)做過(guò)JS編譯器,還做過(guò)兩款頁(yè)游,一個(gè)仇恨攻擊言論的識(shí)別庫(kù)。
最后附上小哥的博客鏈接:
https://victorzhou.com/