來源 | Jack Cui
責編 | Carol
封圖 | CSDN下載自視覺中國
前言
圖解 AI 算法系列教程,不僅僅是涉及深度學習基礎知識,還會有強化學習、遷移學習等,再往小了講就比如拆解目標檢測算法,對抗神經網絡(GAN)等等。
難度會逐漸增加,今天咱先熱熱身,來點輕鬆的,當作這個系列的開篇。
深度學習
深度學習(Deep Learning)是近年來發展十分迅速的研究領域,並且在人 工智能的很多子領域都取得了巨大的成功。從根源來講,深度學習是機器學習的一個分支。
深度學習就是從有限樣例中通過算法總結出一般性的規律,並可以應用到新的未知數據上。
比如,我們可以從一些歷史病例的集合中總結出症狀和疾病之間的規律。這樣,當有新的病人時,我們可以利用總結出來的規律來判斷這個病人得了什麼疾病。
深度學習主要由上圖所示的幾個部分組成,想學一個深度學習算法的原理,就看它是什麼樣的網絡結構,Loss 是怎麼計算的,預處理和後處理都是怎麼做的。
權重初始化和學習率調整策略、優化算法、深度學習框架就那麼多,並且也不是所有都要掌握,比如深度學習框架,Pytorch 玩的溜,就能應付大多數場景。
先有個整體的認知,然後再按照這個思維導圖,逐個知識點學習,最後整合到一起,你會發現,你也可以自己實現各種功能的算法了。
深度學習的主要目的是從數據中自動學習到有效的特徵表示,它是怎麼工作的?那得從神經元說起。
隨著神經科學、認知科學的發展,我們逐漸知道人類的智能行為都和大腦活動有關。
人腦神經系統[1]是一個非常複雜的組織,包含近 860 億個神經元,這 860 億的神經元構成了超級龐大的神經網絡。
我們知道,一個人的智力不完全由遺傳決定,大部分來自於生活經驗。也就是說人腦神經網絡是一個具有學習能力的系統。
不同神經元之間的突觸有強有弱,其強度是可以通過學習(訓練)來不斷改變的,具有一定的可塑性,不同的連接又形成了不同的記憶印痕。
而深度學習的神經網絡,就是受人腦神經網絡啟發,設計的一種計算模型,它從結構、實現機理和功能上模擬人腦神經網絡。
比如下圖就是一個最簡單的前饋神經網絡,第 0 層稱為輸入層,最後一層稱為輸出層,其他中間層稱為隱藏層。
那神經網絡如何工作的?網絡層次結構、損失函數、優化算法、權重初始化、學習率調整都是如何運作的?
反向傳播給你答案。前方,高能預警!
反向傳播
要想弄懂深度學習原理,必須搞定反向傳播[2]和鏈式求導法則。
先說思維導圖裡的網絡層級結構,一個神經網絡,可複雜可簡單,為了方便推導,假設,你有這樣一個網絡層:
第一層是輸入層,包含兩個神經元 i1, i2 和截距項 b1(偏置);
第二層是隱含層,包含兩個神經元 h1, h2 和截距項 b2 ;
第三層是輸出層o1 和 o2 ,每條線上標的 wi 是層與層之間連接的權重,激活函數我們默認為 sigmoid 函數。
在訓練這個網絡之前,需要初始化這些 wi 權重,這就是權重初始化,這裡就有不少的初始化方法,我們選擇最簡單的,隨機初始化。
隨機初始化的結果,如下圖所示:
其中,輸入數據: i1=0.05, i2=0.10;
輸出數據(期望的輸出) : o1=0.01, o2=0.99;
初始權重: w1=0.15, w2=0.20, w3=0.25, w4=0.30, w5=0.40, w6=0.45, w7=0.50, w8=0.55。
目標:給出輸入數據 i1, i2(0.05 和 0.10),使輸出儘可能與原始輸出o1, o2(0.01 和 0.99)接近。
神經網絡的工作流程分為兩步:前向傳播和反向傳播。
1、前向傳播
前向傳播是將輸入數據根據權重,計算到輸出層。
1)輸入層->隱藏層
計算神經元 h1 的輸入加權和:
神經元後面,要跟個激活層,從而引入非線性因素,這就像人的神經元一樣,讓細胞處於興奮或抑制的狀態。
數學模擬的形式就是通過激活函數,大於閾值就激活,反之抑制。
常用的激活函如思維導圖所示,這裡以非常簡單的 sigmoid 激活函數為例,它的函數形式如下:
數學公式:
使用 sigmoid 激活函數,繼續計算,神經元 h1 的輸出 o_h1:
同理,可計算出神經元 h2 的輸出 o_h2:
2)隱藏層->輸出層
計算輸出層神經元 o1 和 o2 的值:
這樣前向傳播的過程就結束了,根據輸入值和權重,我們得到輸出值為[0.75136079, 0.772928465],與實際值(目標)[0.01, 0.99]相差還很遠,現在我們對誤差進行反向傳播,更新權值,重新計算輸出。
2、反向傳播
前向傳播之後,發現輸出結果與期望相差甚遠,這時候就要更新權重了。
所謂深度學習的訓練(煉丹),學的就是這些權重,我們期望的是調整這些權重,讓輸出結果符合我們的期望。
而更新權重的方式,依靠的就是反向傳播。
1)計算總誤差
一次前向傳播過後,輸出值(預測值)與目標值(標籤值)有差距,那得衡量一下有多大差距。
衡量的方法,就是用思維導圖中的損失函數。
損失函數也有很多,咱們還是選擇一個最簡單的,均方誤差(MSE loss)。
均方誤差的函數公式:
根據公式,直接計算預測值與標籤值的總誤差:
有兩個輸出,所以分別計算 o1 和 o2 的誤差,總誤差為兩者之和:
2)隱含層->輸出層的權值更新
以權重參數 w5 為例,如果我們想知道 w5 對整體誤差產生了多少影響,可以用整體誤差對 w5 求偏導求出。
這是鏈式法則,它是微積分中複合函數的求導法則,就是這個:
根據鏈式法則易得:
下面的圖可以更直觀的看清楚誤差是怎樣反向傳播的:
現在我們來分別計算每個式子的值:
計算
計算:
這一步實際上就是對sigmoid函數求導,比較簡單,可以自己推導一下。
計算:
最後三者相乘:
這樣我們就計算出整體誤差E(total)對 w5 的偏導值。
回過頭來再看看上面的公式,我們發現:
為了表達方便,用來表示輸出層的誤差:
因此,整體誤差E(total)對w5的偏導公式可以寫成:
如果輸出層誤差計為負的話,也可以寫成:
最後我們來更新 w5 的值:
這個更新權重的策略,就是思維導圖中的優化算法, 是學習率,我們這裡取0.5。
如果學習率要根據迭代的次數調整,那就用到了思維導圖中的學習率調整。
同理,可更新w6,w7,w8:
3)隱含層->隱含層的權值更新
方法其實與上面說的差不多,但是有個地方需要變一下,在上文計算總誤差對 w5 的偏導時,是從out(o1)->net(o1)->w5,但是在隱含層之間的權值更新時,是out(h1)->net(h1)->w1,而 out(h1) 會接受 E(o1) 和 E(o2) 兩個地方傳來的誤差,所以這個地方兩個都要計算。
計算:
先計算:
同理,計算出:
兩者相加得到總值:
再計算:
再計算:
最後,三者相乘:
為了簡化公式,用 sigma(h1) 表示隱含層單元 h1 的誤差:
最後,更新 w1 的權值:
同理,額可更新w2,w3,w4的權值:
這樣誤差反向傳播法就完成了,最後我們再把更新的權值重新計算,不停地迭代。
在這個例子中第一次迭代之後,總誤差E(total)由0.298371109下降至0.291027924。
迭代10000次後,總誤差為0.000035085,輸出為[0.015912196,0.984065734](原輸入為[0.01,0.99]),證明效果還是不錯的。
這就是整個神經網絡的工作原理,如果你跟著思路,順利看到這裡。那麼恭喜你,深度學習的學習算是通過了一關。
Python 實現
整個過程,可以用 Python 代碼實現。
#coding:utf-8
import random
import math
#
# 參數解釋:
# "pd_" :偏導的前綴
# "d_" :導數的前綴
# "w_ho" :隱含層到輸出層的權重係數索引
# "w_ih" :輸入層到隱含層的權重係數的索引
class NeuralNetwork:
LEARNING_RATE = 0.5
def __init__(self, num_inputs, num_hidden, num_outputs, hidden_layer_weights = None, hidden_layer_bias = None, output_layer_weights = None, output_layer_bias = None):
self.num_inputs = num_inputs
self.hidden_layer = NeuronLayer(num_hidden, hidden_layer_bias)
self.output_layer = NeuronLayer(num_outputs, output_layer_bias)
self.init_weights_from_inputs_to_hidden_layer_neurons(hidden_layer_weights)
self.init_weights_from_hidden_layer_neurons_to_output_layer_neurons(output_layer_weights)
def init_weights_from_inputs_to_hidden_layer_neurons(self, hidden_layer_weights):
weight_num = 0
for h in range(len(self.hidden_layer.neurons)):
for i in range(self.num_inputs):
if not hidden_layer_weights:
self.hidden_layer.neurons[h].weights.append(random.random)
else:
self.hidden_layer.neurons[h].weights.append(hidden_layer_weights[weight_num])
weight_num += 1
def init_weights_from_hidden_layer_neurons_to_output_layer_neurons(self, output_layer_weights):
weight_num = 0
for o in range(len(self.output_layer.neurons)):
for h in range(len(self.hidden_layer.neurons)):
if not output_layer_weights:
self.output_layer.neurons[o].weights.append(random.random)
else:
self.output_layer.neurons[o].weights.append(output_layer_weights[weight_num])
weight_num += 1
def inspect(self):
print('------')
print('* Inputs: {}'.format(self.num_inputs))
print('------')
print('Hidden Layer')
self.hidden_layer.inspect
print('------')
print('* Output Layer')
self.output_layer.inspect
print('------')
def feed_forward(self, inputs):
hidden_layer_outputs = self.hidden_layer.feed_forward(inputs)
return self.output_layer.feed_forward(hidden_layer_outputs)
def train(self, training_inputs, training_outputs):
self.feed_forward(training_inputs)
# 1. 輸出神經元的值
pd_errors_wrt_output_neuron_total_net_input = [0] * len(self.output_layer.neurons)
for o in range(len(self.output_layer.neurons)):
# E/z
pd_errors_wrt_output_neuron_total_net_input[o] = self.output_layer.neurons[o].calculate_pd_error_wrt_total_net_input(training_outputs[o])
# 2. 隱含層神經元的值
pd_errors_wrt_hidden_neuron_total_net_input = [0] * len(self.hidden_layer.neurons)
for h in range(len(self.hidden_layer.neurons)):
# dE/dy = Σ E/z * z/y = Σ E/z * w
d_error_wrt_hidden_neuron_output = 0
for o in range(len(self.output_layer.neurons)):
d_error_wrt_hidden_neuron_output += pd_errors_wrt_output_neuron_total_net_input[o] * self.output_layer.neurons[o].weights[h]
# E/z = dE/dy * z/
pd_errors_wrt_hidden_neuron_total_net_input[h] = d_error_wrt_hidden_neuron_output * self.hidden_layer.neurons[h].calculate_pd_total_net_input_wrt_input
# 3. 更新輸出層權重係數
for o in range(len(self.output_layer.neurons)):
for w_ho in range(len(self.output_layer.neurons[o].weights)):
# E/w = E/z * z/w
pd_error_wrt_weight = pd_errors_wrt_output_neuron_total_net_input[o] * self.output_layer.neurons[o].calculate_pd_total_net_input_wrt_weight(w_ho)
# Δw = α * E/w
self.output_layer.neurons[o].weights[w_ho] -= self.LEARNING_RATE * pd_error_wrt_weight
# 4. 更新隱含層的權重係數
for h in range(len(self.hidden_layer.neurons)):
for w_ih in range(len(self.hidden_layer.neurons[h].weights)):
# E/w = E/z * z/w
pd_error_wrt_weight = pd_errors_wrt_hidden_neuron_total_net_input[h] * self.hidden_layer.neurons[h].calculate_pd_total_net_input_wrt_weight(w_ih)
# Δw = α * E/w
self.hidden_layer.neurons[h].weights[w_ih] -= self.LEARNING_RATE * pd_error_wrt_weight
def calculate_total_error(self, training_sets):
total_error = 0
for t in range(len(training_sets)):
training_inputs, training_outputs = training_sets[t]
self.feed_forward(training_inputs)
for o in range(len(training_outputs)):
total_error += self.output_layer.neurons[o].calculate_error(training_outputs[o])
return total_error
class NeuronLayer:
def __init__(self, num_neurons, bias):
# 同一層的神經元共享一個截距項b
self.bias = bias if bias else random.random
self.neurons =
for i in range(num_neurons):
self.neurons.append(Neuron(self.bias))
def inspect(self):
print('Neurons:', len(self.neurons))
for n in range(len(self.neurons)):
print(' Neuron', n)
for w in range(len(self.neurons[n].weights)):
print(' Weight:', self.neurons[n].weights[w])
print(' Bias:', self.bias)
def feed_forward(self, inputs):
outputs =
for neuron in self.neurons:
outputs.append(neuron.calculate_output(inputs))
return outputs
def get_outputs(self):
outputs =
for neuron in self.neurons:
outputs.append(neuron.output)
return outputs
class Neuron:
def __init__(self, bias):
self.bias = bias
self.weights =
def calculate_output(self, inputs):
self.inputs = inputs
self.output = self.squash(self.calculate_total_net_input)
return self.output
def calculate_total_net_input(self):
total = 0
for i in range(len(self.inputs)):
total += self.inputs[i] * self.weights[i]
return total + self.bias
# 激活函數sigmoid
def squash(self, total_net_input):
return 1 / (1 + math.exp(-total_net_input))
def calculate_pd_error_wrt_total_net_input(self, target_output):
return self.calculate_pd_error_wrt_output(target_output) * self.calculate_pd_total_net_input_wrt_input;
# 每一個神經元的誤差是由平方差公式計算的
def calculate_error(self, target_output):
return 0.5 * (target_output - self.output) ** 2
def calculate_pd_error_wrt_output(self, target_output):
return -(target_output - self.output)
def calculate_pd_total_net_input_wrt_input(self):
return self.output * (1 - self.output)
def calculate_pd_total_net_input_wrt_weight(self, index):
return self.inputs[index]
# 文中的例子:
nn = NeuralNetwork(2, 2, 2, hidden_layer_weights=[0.15, 0.2, 0.25, 0.3], hidden_layer_bias=0.35, output_layer_weights=[0.4, 0.45, 0.5, 0.55], output_layer_bias=0.6)
for i in range(10000):
nn.train([0.05, 0.1], [0.01, 0.09])
print(i, round(nn.calculate_total_error([[[0.05, 0.1], [0.01, 0.09]]]), 9))
#另外一個例子,可以把上面的例子注釋掉再運行一下:
# training_sets = [
# [[0, 0], [0]],
# [[0, 1], [1]],
# [[1, 0], [1]],
# [[1, 1], [0]]
# ]
# nn = NeuralNetwork(len(training_sets[0][0]), 5, len(training_sets[0][1]))
# for i in range(10000):
# training_inputs, training_outputs = random.choice(training_sets)
# nn.train(training_inputs, training_outputs)
# print(i, nn.calculate_total_error(training_sets))
其他
預處理和後處理就相對簡單很多,預處理就是一些常規的圖像變換操作,數據增強方法等。
後處理每個任務都略有不同,比如目標檢測的非極大值抑制等,這些內容可以放在以後再講。
至於深度學習框架的學習,那就是另外一大塊內容了,深度學習框架是一種為了深度學習開發而生的工具,庫和預訓練模型等資源的總和。
我們可以用 Python 實現簡單的神經網絡,但是複雜的神經網絡,還得靠框架,框架的使用可以大幅度降低我們的開發成本。
至於學哪種框架,看個人喜好,Pytorch 和 Tensorflow 都行。
學習資料推薦
學完本文,只能算是深度學習入門,還有非常多的內容需要深入學習。
推薦一些資料,方便感興趣的讀者繼續研究。
視頻:
吳恩達的深度學習公開課[3]:https://mooc.study.163.com/university/deeplearning_ai書籍:
《神經網絡與深度學習》《PyTorch深度學習實戰》開源項目:
Pytorch教程 1:https://github.com/yunjey/pytorch-tutorialPytorch教程 2:https://github.com/pytorch/tutorials學習的積累是個漫長而又孤獨的過程,厚積才能薄發,有不懂的知識就多看多想,要相信最後勝利的,是堅持下去的那個人。
參考資料
[1]
推薦深度學習書籍: 《神經網絡與深度學習》
[2]
反向傳播: https://www.cnblogs.com/charlotte77/p/5629865.html
[3]
吳恩達的深度學習公開課: https://mooc.study.163.com/university/deeplearning_ai