《深度学习图解》梯度下降


比较

本章中,我们只介绍一种简单的测量误差的方法:均方误差

“比较”这一步会让你知道自己的模型错了多少,但这还不足以让它真正学会,因为只是“比较”它不会告诉你为什么错了,在什么方向产生了失误,应该做什么来纠正错误。它只能给出表示“严重失误”“轻微失误”或“完美预测”的误差度量。

学习

本章中,我们介绍一种最流行的误差学习的方法:梯度下降

**“学习”告诉权重应该如何改变以降低误差。**在“预测”步骤结束时,“学习”这一步会为每项权重计算一个数字,这个数字告诉我们,如果想要减少误差,权重应该向哪个方向变化。然后我们根据这个数字对权重做出相应调节,直到达到目的。

测量误差

image-20231127174434112

代码示例:

knob_weight = 0.5 # 定义一个旋钮的权重
input = 0.5 # 输入值
goal_pred = 0.8 # 期望的输出值

pred = input * knob_weight # 计算预测输出值
'''
这段代码中误差被定义为预测值和期望输出值之差的平方
原因:
1.平方差可以将误差转化为非负值,可以确保误差始终为正数
2.平方差放大了较大误差的影响
3.平方差对离群值(预测误差较大的值)更敏感,通过平方操作,离群值的误差将被进一步放大
'''
error = (pred - goal_pred) ** 2 # 计算出预测误差
print(error)
  • goal_pred 变量是什么?

    是从真实世界的某个地方得到的数字。通常情况下,这个数字很难通过直接观察获得,比如在给定温度下,“穿运动外套的人的百分比”等。

  • 为什么需要对误差进行平方运算?

    主要原因是它能够迫使输出值为正的值在某些情况下可能是负的,这显然与真实世界中对误差的定义不同。

  • 平方运算难道不会使较大的误差(>1)变大而使较小的误差(<1)变小吗?

    事实证明,放大那些更大的误差和减小那些较小的误差并没有什么问题,我们会希望神经网络更关注大的错误,同时不必太在意那些小的错误。

为什么需要测量误差?

  • 测量误差能够简化问题。

    事实证明,修改 knob_weight(节点权重)使网络正确预测 goal_forecast(预测目标),要比修改 knob_weight 使 error==0(误差为0)稍微复杂一些。将误差取到0似乎更直接。

  • 在测量误差的不同方法中,误差的优先级不同。

    如果使用均方误差来测量损失,则较大的误差将被优先考虑,而较小的会被忽略掉。训练过程能够修正你对误差的认识,放大那些较大的误差,并忽略那些较小的误差。

  • 为什么只需要正的误差?

    我们希望能够将平均误差降到0,如果误差有正有负,那么可能会产生正负相抵平均误差为0的结果,这是我们不希望看到的。

最简单的神经学习形式是什么?

冷热法

冷热学习指的是通过扰动权重来确定向哪个方向调整可以使得误差的降低幅度最大,基于此将权重的值向那个方向移动,不断重复这个过程,直到误差趋于0。

image-20231127180957570

image-20231127181004405

image-20231127181016395

image-20231127181023943

image-20231127181456526

代码示例:

weight = 0.5
input = 0.5
goal_prediction = 0.8

step_amount = 0.001

for iteration in range(1101):

    prediction = input * weight
    error = (prediction - goal_prediction) ** 2

    print("Error:" + str(error) + " Prediction:" + str(prediction))
    
    up_prediction = input * (weight + step_amount)
    up_error = (goal_prediction - up_prediction) ** 2

    down_prediction = input * (weight - step_amount)
    down_error = (goal_prediction - down_prediction) ** 2

    if(down_error < up_error):
        weight = weight - step_amount
        
    if(down_error > up_error):
        weight = weight + step_amount

缺点:正常情况下,必须重复这个过程很多次才能找到正确的权重。有些人必须训练他们的网络数周或数月,才能找到足够好的权重配置。

这揭示了在神经网络中学习的本质:搜素。为了获得最佳的权重配置,我们不断进行搜索,直到网络的误差降到0(完美预测)。

冷热法的特点
  1. 它很简单

    在上一次做出预测之后,模型又进行两次预测,一次的权重稍微高一些,另一次的权重稍微低一些。然后,权重实际进行调节的量取决于哪个方向所得到的误差更小,重复这一过程足够多次,最终误差将降低到0。

  2. 效率低下

    必须通过多次预测才能进行一次权重的更新。这似乎非常低效。

  3. 有时准确预测出目标是不可能的

    给定步长大小的值,除非权重恰好分毫不差地落在距离初始权重 n倍数的步幅大小 的位置,否则最终会超出某个小于步长大小的值。

    这一情况发生时,网络预测的输出值就会开始在 goal_prediction 周围来回波动。

    所以真正问题是,即使你知道调节权重的正确方向,你也没办法知道正确的幅度

基于误差调节权重

比冷热法更高级的学习形式,叫做梯度下降。该方法运行同时进行方向和幅度的计算,对权重进行调整以减少错误。

image-20231127185422948

  • 什么是停止调节?

    停止调节用于消除对纯净误差的第一个(也是最简单的一个)影响,往往来自于其与输入相乘造成的误差。如果输入为0,则 direction_and_amount 也为0。当输入为0时,模型不会进行学习,所有权重值都会产生相同的误差。

  • 什么是负值反转?

    通常当输入为正时,向上提升权重的值能使预测结果也向上提升。但是如果输入是负的,那么权重的调节就会突然改变方向!当输入为负时,向上提升权重的值将使预测值下降。如何解决这个问题?如果输入是负的,那么将纯误差乘以输入将改变direction_and_amount 的符号,即使输入是负的,我们也要确保权重朝着正确方向移动。

  • 什么是缩放?

    从逻辑上讲,如果输入很大,则权重更新也会变得很大。这更像一种副作用,因为它经常可能失去控制,稍后我们将使用 alpha 来处理这种情况。

代码示例:

weight = 0.5
goal_pred = 0.8
input = 0.5

for iteration in range(20):
    pred = input * weight
    error = (pred - goal_pred) ** 2
    direction_and_amount = (pred - goal_pred) * input
    weight = weight - direction_and_amount

    print("Error:" + str(error) + " Prediction:" + str(pred))

梯度下降的一次迭代

image-20231127191224787

image-20231127191233223

image-20231127191305814

与前文的梯度下降的主要区别是新的变量 delta,而非直接计算 direction_and_amount。它是节点过高或过低的原始计量。

image-20231127191543047

image-20231127191625114

在使用 weight_delta 更新权重值前,可将它乘以一个小数值 alpha。这可以让你控制网络的学习速度,避免过度调整。

注意:这里权重更新的增量与冷热学习的结果是相同的(小幅增加)。

学习就是减少误差

可以修改权重以减少误差

image-20231127221203589

  1. 预测值输入权重决定
  2. 原始误差预测值和固定值期望预测决定
  3. 增量值同上
  4. 权重增量增量值输入值决定
  5. 权重权重增量决定

注意:我们需要修改误差计算函数的特定部分,直到误差值变为0。这个误差函数是基于各种变量的组合计算的,有些变量可以改变(权重),有些则不能改变(输入数据、预测真值和误差度量)。

现在的目标就是找出正确的方向和幅度来改变权重,使误差减小。这里的秘诀在于预测值和误差的计算方式,即 error = ((input * weight) - goal_pred) ** 2。注意此处的 inputgoal_pred 是确定的值,需要在网络开始训练之前就完成对它们的设置。

如果想往某个特定方向移动误差怎么办?

根据前一个公式中的关系,下图表示了在整个取值空间中权重所对应的误差值。

黑色圆点表示了当前的 weight(权重)和 error(误差)所在的位置。

下方虚线描绘出的圆圈则是我们最终想要的权重位置(此时误差 error == 0)

image-20231127223220719

回顾学习的步骤

代码示例:

weight, goal_pred, input = (0.0, 0.8, 1.1)
for iteration in range(4):
    
    pred = input * weight
    error = (pred - goal_pred) ** 2
    delta = pred - goal_pred
    weight_delta = delta * input
    weight = weight - weight_delta
    print("Error:" + str(error) + " Prediction:" + str(pred))

输出:

Error:0.6400000000000001 Prediction:0.0 Weight_Delta:-0.8800000000000001
Error:0.02822400000000005 Prediction:0.9680000000000002 Weight_Delta:0.1848000000000002
Error:0.0012446784000000064 Prediction:0.76472 Weight_Delta:-0.0388080000000001
Error:5.4890317439999896e-05 Prediction:0.8074088 Weight_Delta:0.008149679999999992

image-20231127223952870

image-20231127224014805

image-20231127224209168

image-20231127224216239

导数与梯度下降的关系

导数的两种解释方式:

  1. 将它理解成函数中的一个变量在你移动另一个变量时是如何变化的。
  2. 导数是直线或曲线上一点的斜率。

如下图,离我们要找到的目标权重越远,斜率就越大:

image-20231127225248840

对于任何函数,你都可以选择两个变量,计算出当你更改其中一个变量时,另一个变量发生了多大的变化。

为使得误差减小,我们尽力去寻找权重应该变化的方向和幅度,我们可以用导数来确定权重和误差之间的关系。这种学习(寻找误差最小值)称为梯度下降法。

梯度爆炸情况

如果修改输入值,但仍然试着让算法预测出0.8,结果如下:

Error:0.03999999999999998 Prediction:1.0 Weight_Delta:0.3999999999999999
Error:0.3599999999999998 Prediction:0.20000000000000018 Weight_Delta:-1.1999999999999997
Error:3.2399999999999984 Prediction:2.5999999999999996 Weight_Delta:3.599999999999999
Error:29.159999999999986 Prediction:-4.599999999999999 Weight_Delta:-10.799999999999997

预测的结果爆炸了!它们从负数变成正数,又从正数变成负数,来回往复,但离真正的答案越来越远。对权重的每次更新都会造成过度修正

过渡修正的可视化

image-20231127231134343

image-20231127231144308

image-20231127231157105

那么为什么神经网络的输出会爆炸呢?

image-20231127231247371

误差的激增是由于输入变大了。回顾一下我们更新权重的方法:weight = weight - (input * (pred - goal_pred))

如果输入足够大,即使误差很小,也会使权重的增量很大。**当你的权重增量很大而误差的量很小的时候,网络就会矫枉过正。**如果新的误差更大,网络就会尝试着更多地纠正错误,这就导致了发散。

引入α

这是防止过度修正权重的最简单的方法。

当你矫枉过正的时候,新的导数的大小甚至比开始时还要大(导数的绝对值会变得更大),这使得我们下一步更加远离目标,神经网络会持续这样发散下去。

我们的解决方案就是将权值的增量乘以一个系数,让它变得更小,大多数情况下,这个方法表现为将权值增量乘以一个介于0和1之间的实数,称为α。

注意:α不会对核心问题产生任何影响,也就是说输入还是那个较大的值。同时也会减少那些不太大的输入所对应的权重增量。

  • 如果随着时间的推移,误差逐渐开始上升,那么α的值太高了,需要调低。
  • 如果随着时间的推移,学习进展太慢,那么α的值太低,需要调高。

代码示例:

weight = 0.5
goal_pred = 0.8
input = 2
alpha = 0.1

for iteration in range(20):
    pred  = input * weight
    error = (pred - goal_pred) ** 2
    derivative = input * (pred - goal_pred)
    weight = weight - (alpha * derivative)
    
    print("Error:" + str(error) + " Prediction:" + str(pred))

输出为:

Error:0.03999999999999998 Prediction:1.0
Error:0.0144 Prediction:0.92
Error:0.005183999999999993 Prediction:0.872
Error:0.0018662400000000014 Prediction:0.8432000000000001
Error:0.0006718464000000028 Prediction:0.8259200000000001
Error:0.00024186470400000033 Prediction:0.815552
Error:8.70712934399997e-05 Prediction:0.8093312
Error:3.134566563839939e-05 Prediction:0.80559872
Error:1.1284439629823931e-05 Prediction:0.803359232
Error:4.062398266736526e-06 Prediction:0.8020155392
Error:1.4624633760252567e-06 Prediction:0.8012093235200001
Error:5.264868153690924e-07 Prediction:0.8007255941120001
Error:1.8953525353291194e-07 Prediction:0.8004353564672001
Error:6.82326912718715e-08 Prediction:0.8002612138803201
Error:2.456376885786678e-08 Prediction:0.8001567283281921
Error:8.842956788836216e-09 Prediction:0.8000940369969153
Error:3.1834644439835434e-09 Prediction:0.8000564221981492
Error:1.1460471998340758e-09 Prediction:0.8000338533188895
Error:4.125769919393652e-10 Prediction:0.8000203119913337
Error:1.485277170987127e-10 Prediction:0.8000121871948003

多输入梯度下降

image-20231128171416576

image-20231128171448304

直到生成输出节点上的 delta 之前,单输入和多输入梯度下降的算法都是相同的。但存在以下的问题:当只有一个权重时,我们只需要一个输入,以生成一个 weight_delta(权重增量)。现在你有三个权重,如何生成三个 weight_delta 呢?

delta 的定义:在当前的训练实例中,用于衡量你所希望的当前节点的值的变化,以便完美地预测结果。

weight_delta 的定义:一项基于导数的对权重移动的方向和数量的估计,目标是降低 node_delta,需要考虑关于缩放、负值反转、条件停止的处理。

因为每项权重都有一个唯一的输入,并共享一个 delta,我们可以使用每个权值的输入乘以 delta,来创建对应的 weight_delta

image-20231128172927675

image-20231128173015006

image-20231128173032716

代码示例:

def neural_network(input, weights):
  out = 0
  for i in range(len(input)):
    out += (input[i] * weights[i])
  return out

def ele_mul(scalar, vector):
  out = [0,0,0]
  for i in range(len(out)):
    out[i] = vector[i] * scalar
  return out

toes =  [8.5, 9.5, 9.9, 9.0]
wlrec = [0.65, 0.8, 0.8, 0.9]
nfans = [1.2, 1.3, 0.5, 1.0]

win_or_lose_binary = [1, 1, 0, 1]
true = win_or_lose_binary[0]

alpha = 0.3
weights = [0.1, 0.2, -.1]
input = [toes[0],wlrec[0],nfans[0]]

for iter in range(3):

  pred = neural_network(input,weights)

  error = (pred - true) ** 2
  delta = pred - true

  weight_deltas=ele_mul(delta,input)
  weight_deltas[0] = 0

  print("Iteration:" + str(iter+1))
  print("Pred:" + str(pred))
  print("Error:" + str(error))
  print("Delta:" + str(delta))
  print("Weights:" + str(weights))
  print("Weight_Deltas:")
  print(str(weight_deltas))
  print(
  )

  for i in range(len(weights)):
    weights[i]-=alpha*weight_deltas[i]

输出为:

Iteration:1
Pred:0.8600000000000001
Error:0.01959999999999997
Delta:-0.1399999999999999
Weights:[0.1, 0.2, -0.1]
Weight_Deltas:
[0, -0.09099999999999994, -0.16799999999999987]

Iteration:2
Pred:0.9382250000000001
Error:0.003816150624999989
Delta:-0.06177499999999991
Weights:[0.1, 0.2273, -0.04960000000000005]
Weight_Deltas:
[0, -0.040153749999999946, -0.07412999999999989]

Iteration:3
Pred:0.97274178125
Error:0.000743010489422852
Delta:-0.027258218750000007
Weights:[0.1, 0.239346125, -0.02736100000000008]
Weight_Deltas:
[0, -0.017717842187500006, -0.032709862500000006]

注意:大部分的学习(权重改变)都是在输入值最大的权重上进行的,因为输入能够显著地改变斜率。

因此,当数据集具有这样的特征时,为了鼓励学习的过程用到所有的权重,我们需要引入一个名为标准化的子领域。基于斜率的显著差异,我们必须将 α 设置得比预想中的还要低(0.01而不是0.1)。

单项权重冻结

以上的代码还包括了单项权重冻结,其中权重项a永远不会调整 weight_deltas[0] = 0

image-20231128180000616

但你会惊讶地发现,权重a仍然慢慢滑向U形曲线底部。这是因为这条曲线是每个单独权重对于全局误差所产生的影响的度量。因此误差是共享的,当一项权重到达U形曲线底部时,所有权重都会到达U形曲线底部。

首先,如果你使权重项b和c达到了收敛(误差为0),然后尝试训练权重a,则a不会移动:因为误差已经为0,error=0,也就意味着 weight_delta 为0。

这揭示了神经网络潜在的一项负面特性:权重项a可能对应着重要的输入数据,会对预测结果产生举足轻重的影响,但如果网络在训练数据集中意外找到了一种不需要它也可以准确预测的情况,那么权重a将不再对预测结果产生任何影响。

注意:图表中曲率是由训练数据决定的。因为误差由训练数据决定,任何网络的权重都可以任意取值,但是给定任意特定权重的设置后,则误差值百分之百由数据决定。

多输出梯度下降

代码示例:

# Instead of predicting just 
# whether the team won or lost, 
# now we're also predicting whether
# they are happy/sad AND the
# percentage of the team that is
# hurt. We are making this
# prediction using only
# the current win/loss record.

weights = [0.3, 0.2, 0.9] 

def neural_network(input, weights):
    pred = ele_mul(input,weights)
    return pred

wlrec = [0.65, 1.0, 1.0, 0.9]

hurt  = [0.1, 0.0, 0.0, 0.1]
win   = [  1,   1,   0,   1]
sad   = [0.1, 0.0, 0.1, 0.2]

input = wlrec[0]
true = [hurt[0], win[0], sad[0]]

pred = neural_network(input,weights)

error = [0, 0, 0] 
delta = [0, 0, 0]

for i in range(len(true)):
    error[i] = (pred[i] - true[i]) ** 2
    delta[i] = pred[i] - true[i]
    
def scalar_ele_mul(number,vector):
    output = [0,0,0]

    assert(len(output) == len(vector))

    for i in range(len(vector)):
        output[i] = number * vector[i]

    return output

weight_deltas = scalar_ele_mul(input,delta)

alpha = 0.1

for i in range(len(weights)):
    weights[i] -= (weight_deltas[i] * alpha)
    
print("Weights:" + str(weights))
print("Weight Deltas:" + str(weight_deltas))

多输入和多输出的梯度下降

image-20231128183627282

image-20231128183648880

image-20231128183753662

image-20231128183818329

代码示例:

            #toes %win #fans
weights = [ [0.1, 0.1, -0.3],#hurt?
            [0.1, 0.2, 0.0], #win?
            [0.0, 1.3, 0.1] ]#sad?

def w_sum(a,b):
    assert(len(a) == len(b))
    output = 0

    for i in range(len(a)):
        output += (a[i] * b[i])

    return output

def vect_mat_mul(vect,matrix):
    assert(len(vect) == len(matrix))
    output = [0,0,0]
    for i in range(len(vect)):
        output[i] = w_sum(vect,matrix[i])
    return output

def neural_network(input, weights):
    pred = vect_mat_mul(input,weights)
    return pred

toes  = [8.5, 9.5, 9.9, 9.0]
wlrec = [0.65,0.8, 0.8, 0.9]
nfans = [1.2, 1.3, 0.5, 1.0]

hurt  = [0.1, 0.0, 0.0, 0.1]
win   = [  1,   1,   0,   1]
sad   = [0.1, 0.0, 0.1, 0.2]

alpha = 0.01

input = [toes[0],wlrec[0],nfans[0]]
true  = [hurt[0], win[0], sad[0]]

pred = neural_network(input,weights)

error = [0, 0, 0] 
delta = [0, 0, 0]

for i in range(len(true)):
    error[i] = (pred[i] - true[i]) ** 2
    delta[i] = pred[i] - true[i]
import numpy as np
def outer_prod(a, b):
    
    # just a matrix of zeros
    out = np.zeros((len(a), len(b)))

    for i in range(len(a)):
        for j in range(len(b)):
            out[i][j] = a[i] * b[j]
    return out

weight_deltas = outer_prod(delta,input)

for i in range(len(weights)):
    for j in range(len(weights[0])):
        weights[i][j] -= alpha * weight_deltas[i][j]

这些权重都学习到了什么?

每个权重都试图减少误差,但总的来看它们学到了什么呢?

以MNIST手写数据集为例,有一个脚本MNISTPreprocessor可以用于预处理它:

  1. 它可以将前1000张图片及其对应的标签加载到两个名为images(图像)和labels(标签)的NumPy矩阵中。

    图像是二维的,那么如何才能将其输入到一维的神经网络呢?

    压平。先抽出第一行像素,将它与第二行首尾相连,然后是第三行,以此类推,直到得到一个一维的像素列表。

  2. 下图代表MNIST分类神经网络,唯一与之前的神经网络的区别是输入和输出的数量增加了很多:这个网络有784个输入(每个对应着28×28的图像中的一个像素)和10个输出(每个对应着0-9之间可能出现的一个数字)。

    image-20231128184854703

  3. 如果这个网络能够完美地预测,它将接收图像的像素值(比如数字2,像下图中的这个),然后在正确的输出位置(第三个位置,因为我们从0开始数)预测出1,在其他位置预测出0。

    如果它能对数据集中的每一个图像都正确地做到这一点,那么误差将不存在:

    image-20231128193509867

  4. 权重可视化

    image-20231128194430439

    每个输出节点都有一条权重边,与每个输入节点(像素)相连接。例如,上图中,所呈现的2这个节点上有784个输入权重,每个权重都将数字2和图像中对应的像素相连接。

    如果权重值比较高,那么模型认为对应的像素和数字2的相关度会比较高;如果权重值非常低(负数),那么说明神经网络认为对应的像素和数字2的相关度非常低,甚至可能是负相关。

    如果将权重画成一个和输入数据形状相同的图像,可以看到对应某个特殊的输出节点,哪些像素具有最高的相关性。

    示例中,在我们的示例中,这两张图像分别使用输出2和输出1所对应的权重创建,我们可以依稀发现,在图像中分别出现了一个非常模糊的2和1。**明亮的区域表示高权重,阴暗的区域表示权重为负值。中性的颜色表示对应的权重为0。**这说明,神经网络在一般情况下能够知道2和1的形状。

  5. 点积(加权和)可视化

    为什么神经网络能知道2和1的形状?

    考虑下面的这个例子:

    image-20231128194607364

    a和b中的每个元素相乘,在本例中会生成一个全0向量。这个向量的和也是0。为什么?因为这两个向量没有任何共同点。

    点积是对两个向量之间相似性的松散度量。


文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录