小白都能看懂的神經(jīng)網(wǎng)絡教程:從原理到優(yōu)化如此簡單
程序員們紛紛夸贊這篇文章的代碼寫得很好,變量名很規(guī)范,讓人一目了然。
曉查 發(fā)自 凹非寺
量子位 報道 | 公眾號 QbitAI
“我在網(wǎng)上看到過很多神經(jīng)網(wǎng)絡的實現(xiàn)方法,但這一篇是最簡單、最清晰的?!?/p>
一位來自普林斯頓的華人小哥Victor Zhou,寫了篇神經(jīng)網(wǎng)絡入門教程,在線代碼網(wǎng)站Repl.it聯(lián)合創(chuàng)始人Amjad Masad看完以后,給予如是評價。
這篇教程發(fā)布僅天時間,就在Hacker News論壇上收獲了574贊。程序員們紛紛夸贊這篇文章的代碼寫得很好,變量名很規(guī)范,讓人一目了然。
下面就讓我們一起從零開始學習神經(jīng)網(wǎng)絡吧。
實現(xiàn)方法
搭建基本模塊——神經(jīng)元
在說神經(jīng)網(wǎng)絡之前,我們討論一下神經(jīng)元(Neurons),它是神經(jīng)網(wǎng)絡的基本單元。神經(jīng)元先獲得輸入,然后執(zhí)行某些數(shù)學運算后,再產(chǎn)生一個輸出。比如一個2輸入神經(jīng)元的例子:
在這個神經(jīng)元中,輸入總共經(jīng)歷了3步數(shù)學運算,
先將兩個輸入乘以權(quán)重(weight):
x1→x1 × w1
x2→x2 × w2
把兩個結(jié)果想加,再加上一個偏置(bias):
(x1 × w1)+(x2 × w2)+ b
最后將它們經(jīng)過激活函數(shù)(activation function)處理得到輸出:
y = f(x1 × w1 + x2 × w2 + b)
激活函數(shù)的作用是將無限制的輸入轉(zhuǎn)換為可預測形式的輸出。一種常用的激活函數(shù)是sigmoid函數(shù):
sigmoid函數(shù)的輸出介于0和1,我們可以理解為它把 (?∞,+∞) 范圍內(nèi)的數(shù)壓縮到 (0, 1)以內(nèi)。正值越大輸出越接近1,負向數(shù)值越大輸出越接近0。
舉個例子,上面神經(jīng)元里的權(quán)重和偏置取如下數(shù)值:
w=[0,1]
b = 4
w=[0,1]是w1=0、w2=1的向量形式寫法。給神經(jīng)元一個輸入x=[2,3],可以用向量點積的形式把神經(jīng)元的輸出計算出來:
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
我們在代碼中調(diào)用了一個強大的Python數(shù)學函數(shù)庫NumPy。
搭建神經(jīng)網(wǎng)絡
神經(jīng)網(wǎng)絡就是把一堆神經(jīng)元連接在一起,下面是一個神經(jīng)網(wǎng)絡的簡單舉例:
這個網(wǎng)絡有2個輸入、一個包含2個神經(jīng)元的隱藏層(h1和h2)、包含1個神經(jīng)元的輸出層o1。
隱藏層是夾在輸入輸入層和輸出層之間的部分,一個神經(jīng)網(wǎng)絡可以有多個隱藏層。
把神經(jīng)元的輸入向前傳遞獲得輸出的過程稱為前饋(feedforward)。
我們假設上面的網(wǎng)絡里所有神經(jīng)元都具有相同的權(quán)重w=[0,1]和偏置b=0,激活函數(shù)都是sigmoid,那么我們會得到什么輸出呢?
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
以下是實現(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
訓練神經(jīng)網(wǎng)絡
現(xiàn)在我們已經(jīng)學會了如何搭建神經(jīng)網(wǎng)絡,現(xiàn)在我們來學習如何訓練它,其實這就是一個優(yōu)化的過程。
假設有一個數(shù)據(jù)集,包含4個人的身高、體重和性別:
現(xiàn)在我們的目標是訓練一個網(wǎng)絡,根據(jù)體重和身高來推測某人的性別。
為了簡便起見,我們將每個人的身高、體重減去一個固定數(shù)值,把性別男定義為1、性別女定義為0。
在訓練神經(jīng)網(wǎng)絡之前,我們需要有一個標準定義它到底好不好,以便我們進行改進,這就是損失(loss)。
比如用均方誤差(MSE)來定義損失:
n是樣本的數(shù)量,在上面的數(shù)據(jù)集中是4;
y代表人的性別,男性是1,女性是0;
ytrue是變量的真實值,ypred是變量的預測值。
顧名思義,均方誤差就是所有數(shù)據(jù)方差的平均值,我們不妨就把它定義為損失函數(shù)。預測結(jié)果越好,損失就越低,訓練神經(jīng)網(wǎng)絡就是將損失最小化。
如果上面網(wǎng)絡的輸出一直是0,也就是預測所有人都是男性,那么損失是:
MSE= 1/4 (1+0+0+1)= 0.5
計算損失函數(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)絡損失
這個神經(jīng)網(wǎng)絡不夠好,還要不斷優(yōu)化,盡量減少損失。我們知道,改變網(wǎng)絡的權(quán)重和偏置可以影響預測值,但我們應該怎么做呢?
為了簡單起見,我們把數(shù)據(jù)集縮減到只包含Alice一個人的數(shù)據(jù)。于是損失函數(shù)就剩下Alice一個人的方差:
預測值是由一系列網(wǎng)絡權(quán)重和偏置計算出來的:
所以損失函數(shù)實際上是包含多個權(quán)重、偏置的多元函數(shù):
(注意!前方高能!需要你有一些基本的多元函數(shù)微分知識,比如偏導數(shù)、鏈式求導法則。)
如果調(diào)整一下w1,損失函數(shù)是會變大還是變???我們需要知道偏導數(shù)?L/?w1是正是負才能回答這個問題。
根據(jù)鏈式求導法則:
而L=(1-ypred)2,可以求得第一項偏導數(shù):
接下來我們要想辦法獲得ypred和w1的關(guān)系,我們已經(jīng)知道神經(jīng)元h1、h2和o1的數(shù)學運算規(guī)則:
實際上只有神經(jīng)元h1中包含權(quán)重w1,所以我們再次運用鏈式求導法則:
然后求?h1/?w1
我們在上面的計算中遇到了2次激活函數(shù)sigmoid的導數(shù)f′(x),sigmoid函數(shù)的導數(shù)很容易求得:
總的鏈式求導公式:
這種向后計算偏導數(shù)的系統(tǒng)稱為反向傳播(backpropagation)。
上面的數(shù)學符號太多,下面我們帶入實際數(shù)值來計算一下。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)絡的輸出y=0.524,沒有顯示出強烈的是男(1)是女(0)的證據(jù)?,F(xiàn)在的預測效果還很不好。
我們再計算一下當前網(wǎng)絡的偏導數(shù)?L/?w1:
這個結(jié)果告訴我們:如果增大w1,損失函數(shù)L會有一個非常小的增長。
隨機梯度下降
下面將使用一種稱為隨機梯度下降(SGD)的優(yōu)化算法,來訓練網(wǎng)絡。
經(jīng)過前面的運算,我們已經(jīng)有了訓練神經(jīng)網(wǎng)絡所有數(shù)據(jù)。但是該如何操作?SGD定義了改變權(quán)重和偏置的方法:
η是一個常數(shù),稱為學習率(learning rate),它決定了我們訓練網(wǎng)絡速率的快慢。將w1減去η·?L/?w1,就等到了新的權(quán)重w1。
當?L/?w1是正數(shù)時,w1會變??;當?L/?w1是負數(shù) 時,w1會變大。
如果我們用這種方法去逐步改變網(wǎng)絡的權(quán)重w和偏置b,損失函數(shù)會緩慢地降低,從而改進我們的神經(jīng)網(wǎng)絡。
訓練流程如下:
1、從數(shù)據(jù)集中選擇一個樣本;
2、計算損失函數(shù)對所有權(quán)重和偏置的偏導數(shù);
3、使用更新公式更新每個權(quán)重和偏置;
4、回到第1步。
我們用Python代碼實現(xiàn)這個過程:
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)
隨著學習過程的進行,損失函數(shù)逐漸減小。
現(xiàn)在我們可以用它來推測出每個人的性別了:
# 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
更多
這篇教程只是萬里長征第一步,后面還有很多知識需要學習:
1、用更大更好的機器學習庫搭建神經(jīng)網(wǎng)絡,如Tensorflow、Keras、PyTorch
2、在瀏覽器中的直觀理解神經(jīng)網(wǎng)絡:https://playground.tensorflow.org/
3、學習sigmoid以外的其他激活函數(shù):https://keras.io/activations/
4、學習SGD以外的其他優(yōu)化器:https://keras.io/optimizers/
5、學習卷積神經(jīng)網(wǎng)絡(CNN)
6、學習遞歸神經(jīng)網(wǎng)絡(RNN)
這些都是Victor給自己挖的“坑”。他表示自己未來“可能”會寫這些主題內(nèi)容,希望他能陸續(xù)把這些坑填完。如果你想入門神經(jīng)網(wǎng)絡,不妨去訂閱他的博客。
關(guān)于這位小哥
Victor Zhou是普林斯頓2019級CS畢業(yè)生,已經(jīng)拿到Facebook軟件工程師的offer,今年8月入職。他曾經(jīng)做過JS編譯器,還做過兩款頁游,一個仇恨攻擊言論的識別庫。
最后附上小哥的博客鏈接:
https://victorzhou.com/
- 腦機接口走向現(xiàn)實,11張PPT看懂中國腦機接口產(chǎn)業(yè)現(xiàn)狀|量子位智庫2021-08-10
- 張朝陽開課手推E=mc2,李永樂現(xiàn)場狂做筆記2022-03-11
- 阿里數(shù)學競賽可以報名了!獎金增加到400萬元,題目面向大眾公開征集2022-03-14
- 英偉達遭黑客最后通牒:今天必須開源GPU驅(qū)動,否則公布1TB機密數(shù)據(jù)2022-03-05