深度学习背景中的基础算法代码和故事

深度学习发展到如今的地位,离不开下面这几段代码。本文介绍了这些代码的创作者及其完成这些突破性成就的故事背景。每个故事都有简单的代码示例

1. 最小二乘法

  • 1805 年由 Adrien-Marie Legendre 首次提出(1805, Legendre),这位巴黎数学家也以测量仪器闻名。他极其痴迷于预测彗星的方位,坚持不懈地寻找一种可以基于彗星方位历史数据计算其轨迹的算法。

  • 他尝试了许多种算法,一遍遍试错,终于找到了一个算法与结果相符。Legendre 的算法是首先预测彗星未来的方位,然后计算误差的平方,最终目的是通过修改预测值以减少误差平方和。而这也正是线性回归的基本思想。

运行上述代码来加深对这个算法的理解。m 是系数,b 是预测的常数项,coordinates 是彗星的位置。目标是找到合适的 m 和 b 使其误差尽可能小。

这是深度学习的核心思想:给定输入值和期望的输出值,然后寻找两者之间的相关性。

深度学习背景中的基础算法代码和故事_第1张图片
# 最小二乘法
# 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 取到最小值。


深度学习背景中的基础算法代码和故事_第2张图片

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 组分别在左右两侧有标记的图像,它可以在没有预先编程的情况下分辨出两张图像(标记的位置)。大众被这个可能真正拥有学习能力的机器震惊了。

深度学习背景中的基础算法代码和故事_第3张图片

如上图所示,每个训练周期都是从左侧输入数据开始。给所有输入数据添加一个初始的随机权重。然后将它们相加。如果总和为负,将其输出为 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 遇冷的时代。


深度学习背景中的基础算法代码和故事_第4张图片

学习这段代码之前,我们首先通过这个模拟器 交互学习一到两个小时来掌握神经网络的核心逻辑。需要注意到,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]]

你可能感兴趣的:(深度学习背景中的基础算法代码和故事)