深度學習發展到如今的地位,離不開下面這幾段代碼。本文介紹了這些代碼的創作者及其完成這些突破性成就的故事背景。每個故事都有簡單的代碼示例
1. 最小二乘法
1805 年由 Adrien-Marie Legendre 首次提出(1805, Legendre),這位巴黎數學家也以測量儀器聞名。他極其癡迷于預測彗星的方位,堅持不懈地尋找一種可以基于彗星方位歷史數據計算其軌跡的算法。
他嘗試了許多種算法,一遍遍試錯,終于找到了一個算法與結果相符。Legendre 的算法是首先預測彗星未來的方位,然后計算誤差的平方,最終目的是通過修改預測值以減少誤差平方和。而這也正是線性回歸的基本思想。
運行上述代碼來加深對這個算法的理解。m 是系數,b 是預測的常數項,coordinates 是彗星的位置。目標是找到合適的 m 和 b 使其誤差盡可能小。
這是深度學習的核心思想:給定輸入值和期望的輸出值,然后尋找兩者之間的相關性。
# 最小二乘法
# y = mx + b
# m is slope, b is y-intercept
def compute_error_for_line_given_points(b, m, coordinates):
totalError = 0
for i in range(0, len(coordinates)):
x = coordinates[i][0]
y = coordinates[i][1]
totalError += (y - (m * x + b)) ** 2
return totalError / float(len(coordinates))
# example
compute_error_for_line_given_points(1, 2, [[3,6],[6,9],[12,18]])
22.0
2.梯度下降
Legendre 這種通過手動嘗試來降低錯誤率的方法非常耗時。荷蘭的諾貝爾獎得主 Peter Debye 在一個世紀后(1909 年)正式提出了一種簡化這個過程的方法。
假設 Legendre 的算法需要考慮一個參數 —— 我們稱之為 X 。Y 軸表示每個 X 的誤差值。Legendre 的算法是找到使得誤差最小的 X。在下圖中,我們可以看到當 X = 1.1 時,誤差 Y 取到最小值。
Peter Debye 注意到最低點左邊的斜率是負的,而另一邊則是正的。因此,如果知道了任意給定 X 的斜率值,就可以找到 Y 的最小值點。
這便是梯度下降算法的基本思想。幾乎所有的深度學習模型都會用到梯度下降算法。
要實現這個算法,我們假設誤差函數是 Error = x ^ 5 -2x ^ 3-2。要得到任意給定 X 的斜率,我們需要對其求導,即 5x^4 – 6x^2:
這里的竅門在于 learning_rate。我們通過沿斜率的相反方向行進來逼近最低點。此外,越接近最低點,斜率越小。因此當斜率接近零時,每一步下降的幅度會越來越小。
num_iterations 是你預計到達最小值之前所需的迭代次數。可以通過調試該參數訓練自己關于梯度下降算法的直覺。
current_x = 0.5 # the algorithm starts at x=0.5
learning_rate = 0.01 # step size multiplier
num_iterations = 60 # the number of times to train the function
#the derivative of the error function (x**4 = the power of 4 or x^4)
def slope_at_given_x_value(x):
return 5 * x**4 - 6 * x**2
# Move X to the right or left depending on the slope of the error function
for i in range(num_iterations):
previous_x = current_x
current_x += -learning_rate * slope_at_given_x_value(previous_x)
print(previous_x)
print("The local minimum occurs at %f" % current_x)
0.5
0.511875
0.5241633413153
0.5368738723924315
0.5500139565765964
0.5635891007995755
0.5776025354752059
0.5920547451593525
0.6069429517083986
0.6222605546198048
0.6379965370663893
0.6541348509073133
0.6706537996630098
0.6875254449508584
0.7047150688998199
0.7221807320875879
0.7398729728132125
0.7577346980096246
0.7757013175681643
0.7937011709295161
0.8116562862093865
0.8294834969490578
0.8470959195861032
0.8644047667410041
0.881321439496782
0.8977598093924315
0.9136385722426767
0.9288835358868889
0.943429696757781
0.9572229683912065
0.9702214489531998
0.9823961521131122
0.9937311713023107
1.004223295220484
1.0138811358280073
1.0227238635418048
1.0307796645701868
1.0380840414117025
1.044678070931505
1.0506067181771692
1.05591728202966
1.060658024670622
1.06487701379143
1.0686211866128092
1.0719356292600397
1.0748630541233568
1.0774434511849047
1.0797138862095075
1.0817084183291976
1.0834581110655934
1.0849911135011239
1.0863327915502436
1.0875058926711327
1.0885307306128311
1.0894253797422326
1.0902058710556728
1.0908863841268206
1.0914794309907059
1.0919960293497617
1.0924458635591308
The local minimum occurs at 1.092837
線性回歸
最小二乘法配合梯度下降算法,就是一個完整的線性回歸過程。在 20 世紀 50 年代和 60 年代,一批實驗經濟學家在早期的計算機上實現了這些想法。這個過程是通過實體打卡 —— 真正的手工軟件程序實現的。準備這些打孔卡就需要幾天的時間,而通過計算機進行一次回歸分析最多需要 24 小時。
#Price of wheat/kg and the average price of bread
wheat_and_bread = [[0.5,5],[0.6,5.5],[0.8,6],[1.1,6.8],[1.4,7]]
def step_gradient(b_current, m_current, points, learningRate):
b_gradient = 0
m_gradient = 0
N = float(len(points))
for i in range(0, len(points)):
x = points[i][0]
y = points[i][1]
b_gradient += -(2/N) * (y - ((m_current * x) + b_current))
m_gradient += -(2/N) * x * (y - ((m_current * x) + b_current))
new_b = b_current - (learningRate * b_gradient)
new_m = m_current - (learningRate * m_gradient)
return [new_b, new_m]
def gradient_descent_runner(points, starting_b, starting_m, learning_rate, num_iterations):
b = starting_b
m = starting_m
for i in range(num_iterations):
b, m = step_gradient(b, m, points, learning_rate)
return [b, m]
gradient_descent_runner(wheat_and_bread, 1, 1, 0.01, 100)
[3.30446237250121, 2.966062687410304]
感知機
接下來讓我們來認識一下 Frank Rosenblatt。這是一個白天解剖老鼠大腦,晚上尋找外星生命跡象的家伙。1958年,他發明了一個模仿神經元的機器(1958, Rosenblatt),并因此登上《紐約時報》的頭條:“New Navy Device Learns By Doing”。
如果向 Rosenblatt 的機器展示 50 組分別在左右兩側有標記的圖像,它可以在沒有預先編程的情況下分辨出兩張圖像(標記的位置)。大眾被這個可能真正擁有學習能力的機器震驚了。
如上圖所示,每個訓練周期都是從左側輸入數據開始。給所有輸入數據添加一個初始的隨機權重。然后將它們相加。如果總和為負,將其輸出為 0,否則輸出為 1。
如果預測結果是正確的,就不改變循環中的權重。如果預測結果是錯誤的,可以用誤差乘以學習率來相應地調整權重。
我們用經典的“或”邏輯來運行感知機。
輸入 輸出
0 0 = 0
0 1 = 1
1 0 = 1
1 1 = 1
經過最初的炒作一年之后,Marvin Minsky 和 Seymour Papert 擊碎了這個想法(1969, Minsky & Papert)。當時,Minsky 和 Papert 都在麻省理工學院的 AI 實驗室工作。他們寫了一本書,證明感知機只能解決線性問題。他們還批判了關于多層感知機的想法。可悲的是,Frank Rosenblatt 兩年后因船難去世。
在 Minsky 和 Papert 的書籍出版一年之后,一位芬蘭碩士研究生提出了用多層感知機解決非線性問題的理論(Linnainmaa, 1970)。由于業內主流對感知機普遍不看好,十多年來 AI 的研究資金也非常短缺。這是 AI 首次遇冷。
from random import choice
from numpy import array, dot, random
v1_or_0 = lambda x: 0 if x < 0 else 1
training_data = [ (array([0,0,1]), 0),
(array([0,1,1]), 1),
(array([1,0,1]), 1),
(array([1,1,1]), 1), ]
weights = random.rand(3)
errors = []
learning_rate = 0.2
num_iterations = 100
for i in range(num_iterations):
input, truth = choice(training_data)
result = dot(weights, input)
error = truth - v1_or_0(result)
errors.append(error)
weights += learning_rate * error * input
w = weights
for x, _ in training_data:
result = dot(x, w)
print("{}: {} -> {}".format(input[:2], result, v1_or_0(result)))
[1 1]: -0.05961306140476624 -> 0
[1 1]: 0.4990767670409854 -> 1
[1 1]: 0.3576917409623607 -> 1
[1 1]: 0.9163815694081123 -> 1
人工神經網絡
到 1986 年,幾項實驗證明,神經網絡可以解決復雜的非線性問題(Rumelhart et al., 1986)。 當時計算機的運算速度比該理論提出的時候快了一萬倍。Rumelhart 等人是這樣介紹他們赫赫有名的論文的:
我們描述了一種新的類神經元網絡學習過程——反向傳播。該過程通過反復調整網絡中的連接權重,最小化網絡的實際輸出向量與期望輸出向量之間的差異。調整權重的結果就是,不屬于輸入或輸出的內部“隱藏”單元成為了描述任務域的重要特征,并且這些單元的交互項還捕獲了任務中的正則條件。 相較于早期更簡單的方法,如“感知機收斂過程” Nature 323, 533 – 536 (09 October 1986),反向傳播可以創造出有用的新特征。
為了理解這篇文章的核心內容,我會在下面重現 DeepMind 團隊 Andrew Trask 的代碼。這不是一段普通的代碼。它曾被用于斯坦福大學 Andrew Karpathy 的深度學習課程,以及 Siraj Raval 的 Udacity 課程。最重要的是,它解決了“異或”問題,也結束了 AI 遇冷的時代。
學習這段代碼之前,我們首先通過這個模擬器 交互學習一到兩個小時來掌握神經網絡的核心邏輯。需要注意到,X_XOR 數據中添加的參數 [1] 是偏置神經元,它們等價于線性函數中的常數項。
反向傳播,矩陣乘法和梯度下降放在一起會讓人很難理解。這個過程的可視化通常是對其背后原理的簡化。專注于理解其背后的邏輯,但不要過多地考慮直覺上的理解。
import numpy as np
X_XOR = np.array([[0,0,1], [0,1,1], [1,0,1],[1,1,1]])
y_truth = np.array([[0],[1],[1],[0]])
np.random.seed(1)
syn_0 = 2*np.random.random((3,4)) - 1
syn_1 = 2*np.random.random((4,1)) - 1
def sigmoid(x):
output = 1/(1+np.exp(-x))
return output
def sigmoid_output_to_derivative(output):
return output*(1-output)
for j in range(60000):
layer_1 = sigmoid(np.dot(X_XOR, syn_0))
layer_2 = sigmoid(np.dot(layer_1, syn_1))
error = layer_2 - y_truth
layer_2_delta = error * sigmoid_output_to_derivative(layer_2)
layer_1_error = layer_2_delta.dot(syn_1.T)
layer_1_delta = layer_1_error * sigmoid_output_to_derivative(layer_1)
syn_1 -= layer_1.T.dot(layer_2_delta)
syn_0 -= X_XOR.T.dot(layer_1_delta)
print("Output After Training: n", layer_2)
Output After Training: n [[ 0.00260572]
[ 0.99672209]
[ 0.99701711]
[ 0.00386759]]