《深度学习图解》反向传播


反向传播

交通信号灯问题

神经网络如何学习整个数据集?

可以通过解读交通信号灯的含义来知道什么时候过马路是安全的。但是我们只能观察每种灯光组合和周围的人通行或止步的相关性来进行判断:

image-20231128201102104

准备数据

如何训练一个监督神经网络?

可以交给它两个数据集,让它学习如何将其中一个转化到另一个。目前确实有两个数据集,一方面有六组信号灯状态记录;一方面有六组人们是否通行的观察记录。

目前:我们已经知道任意给定时刻的交通信号灯状态+我们想知道穿过马路是否安全。

矩阵和矩阵关系

将交通信号灯转换为数学表达。

我们现在需要用数字形式来模拟交通信号灯的模式:

image-20231128201509445

在数据矩阵中,管理是用一行来表示每个样例记录,并将每一项(属性)对应记录为一列。

如果信号灯配有调光器,能够以不同的强度发光的情况是这样的:

image-20231128202326773

或者矩阵A乘以10得到的矩阵B也是合理的。

同时为了学习输出数据模式,我们还需要以矩阵形式表达这种模式,如下图所示:

image-20231128203300715

使用Python创建矩阵

输入矩阵:

import numpy as np
streetlights = np.array( [ [ 1, 0, 1 ],
                           [ 0, 1, 1 ],
                           [ 0, 0, 1 ],
                           [ 1, 1, 1 ],
                           [ 0, 1, 1 ],
                           [ 1, 0, 1 ] ] )

输出矩阵:

walk_vs_stop = np.array( [ 0, 1, 0, 1, 1, 0 ] )

我们想让神经网络学习如何将输入矩阵转换为输出矩阵。

建立神经网络

代码示例:

import numpy as np
weights = np.array([0.5,0.48,-0.7])
alpha = 0.1

streetlights = np.array( [ [ 1, 0, 1 ],
                           [ 0, 1, 1 ],
                           [ 0, 0, 1 ],
                           [ 1, 1, 1 ],
                           [ 0, 1, 1 ],
                           [ 1, 0, 1 ] ] )

walk_vs_stop = np.array( [ 0, 1, 0, 1, 1, 0 ] )

input = streetlights[0] # [1,0,1]
goal_prediction = walk_vs_stop[0] # equals 0... i.e. "stop"

for iteration in range(20):
    prediction = input.dot(weights)
    error = (goal_prediction - prediction) ** 2
    delta = prediction - goal_prediction
    weights = weights - (alpha * (input * delta))	

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

学习整个数据集

神经网络不光只可以学习一条信号灯记录,我们希望它把所有记录都学完。

import numpy as np

weights = np.array([0.5,0.48,-0.7])
alpha = 0.1

streetlights = np.array( [[ 1, 0, 1 ],
                          [ 0, 1, 1 ],
                          [ 0, 0, 1 ],
                          [ 1, 1, 1 ],
                          [ 0, 1, 1 ],
                          [ 1, 0, 1 ] ] )

walk_vs_stop = np.array( [ 0, 1, 0, 1, 1, 0 ] )

input = streetlights[0] # [1,0,1]
goal_prediction = walk_vs_stop[0] # equals 0... i.e. "stop"

for iteration in range(40):
    error_for_all_lights = 0
    for row_index in range(len(walk_vs_stop)):
        input = streetlights[row_index]
        goal_prediction = walk_vs_stop[row_index]
        
        prediction = input.dot(weights)
        
        error = (goal_prediction - prediction) ** 2
        error_for_all_lights += error
        
        delta = prediction - goal_prediction
        weights = weights - (alpha * (input * delta))	
        print("Prediction:" + str(prediction))
    print("Error:" + str(error_for_all_lights) + "\n")

完全、批量和随机梯度下降

随机梯度下降每次对一个样例更新权重

即分别为每个训练样例执行预测和权重更新,它先拿到第一条信号灯的数据,尝试基于它进行预测,计算权重增量 weight_delta 并更新权重,然后继续读取第二条信号灯的数据,以此类推。

完全梯度下降每次对整个数据集更新权重

网络针对整个数据集计算增量权重 weight_delta 的平均值,并在均值计算完成之后更新权重,而非对每个训练用例更新权重。

批量梯度下降对每n个样例更新权重

它并非在计算一个样例之后或遍历整个样本数据集之后更新权重,而是选择确定批次大小(通常在8到256之间)的样例,然后更新权重。

神经网络对相关性的学习

神经网络并不知道它在处理信号灯的数据。它只是在尝试识别(三种可能中的)哪种输入与输出相关。它通过分析网络的最终权重确认了中间的信号灯。

注意:中间的权重非常接近1,而最左边和最右边的权重都非常接近0。任何权重数值高的位置都具有高相关性,相反,最左边和最右边的输入相对于输出是随机的(它们的权重值非常接近于0)。

网络如何确定相关性?

平均而言,中间那一项权重收到的向上压力更大,其他权重所受到的向下的压力更大。

那么压力从何而来?为什么不同权重所受到的压力有所不同?

向上与向下的压力

来自于数据。

对于给定输入,每个节点都各自尝试做出正确的预测。大多数情况下,每个节点在尝试这样做的时候都会忽略其他所有节点。**唯一的交叉通信在于这三个权值必须共享相同的误差度量。**权重更新只不过是将这个共享的误差度量值与每个对应的输入相乘。

为什么要这样做呢?

神经网络能够学习的关键部分是误差归因,它意味着给定共享误差,神经网络需要找出哪些权重对这些误差产生了影响(可以据此进行调整),哪些权重没有(可以不管它们)。

image-20231128213543423

例如第一个训练样例,因为中间的输入是0,所以中间的权重与预测结果完全无关。不管权重是多少,它都会乘以0(输入数据)。因此,该训练样例中的任何误差(无论它是太高或还是太低)都只能归因于最左边和最右边的权重。

现在我们来看第一个训练样例对权重产生的压力,如果网络预测值应该为0,且相应的输入为1,则会导致错误,从而使得对应的权重值趋近于0。

权重为+表示有向1方向的压力,-表示有向0方向的压力,0表示没有压力,因为输入数据点为0,所以权重不会改变。

总体而言:

  • 竖着看最左边的权重对应的压力两负一正,所以平均而言它的值会趋近于0。
  • 中间权重对应的压力是3个正值,所以平均而言它的值会趋近于1。
  • 右边权重三负三正,所以平均而言它的值会趋近于0。

预测结果是输入数据的加权和。学习算法对与输出相关的输入所对应的权重以向上(向1的方向)压力作为奖励,而对与输出不相关的输入所对应的权重以向下(向0的方向)压力作为惩罚。

边界情况:过拟合

所有的权值都有误差。如果某个特定的权重配置意外地在预测数据集和输出数据集之间创建了完美的吻合(比如使得error === 0),而此时并没有给真正最重要的输入赋予最大的权重,那么神经网络将停止学习。

本质上,神经网络只是记住了那个完美吻合的训练样例,而不是找出能够推广到任意可能的组合的相关性。

深度学习面临的最大挑战是训练你的神经网络去泛化而不仅仅是记忆。

边界情况:压力冲突

image-20231129120027886

最右边的一列似乎有数量相等的向上和向下的力矩,但是网络正确地将之对应的(最右边的)权重降低到了0,这意味着它向下的力矩必须大于向上的力矩,这是怎么回事?

左边和中间的权重有足够的信息使它们自行收敛。左边的权重降低到0,中间的权重升高到1。 随着中间的权重越来越高,正样本所带来的误差也在不断减小。但是当它们接近最优位置时,最右边的权重与预测结果的无关性则更加明显。

让我们考虑-一个极端的例子:左边和中间的权重被完美地分别设置为0和1。网络会发生什么?如果右边的权重值大于0,则网络预测的结果会过高;如果右边的权重值低于0,则网络预测的结果会过低。

在其他节点学习的过程中,它们会吸收一部分误差,它们也会吸收一部分相关性。

这种情况下,中间的权重有持续的信息来吸收所有的相关性(由于中间那项输入与输出之间有着1:1的对应关系),当你想预测1时,误差会变得非常小,但是你想要预测0时,误差变大,这把右边的权重不断向下推。

  • 正则化是有利的,因为如果一个权重具有相同的向上和向下的压力,不会产生任何好处,它不会对任何一个方向有帮助。

  • 正则化的目的是使得只有真正具有强相关性的权重才能保持不变,其他的一切都应该被压制,因为它们会产生噪声。

  • 正则化有副作用,它会导致神经网络的训练加快(迭代次数更少),因为最右边的权重存在同时受到正负两个方向的压力的问题。

    这种情况下,由于最右边的节点没有表现出明确的相关性,网络将立即把它向0的方向推。如果没有经过正则化——就好像之前的训练过程那样,直到左边的权重和中间的权重慢慢开始找到它们的模式,我们才会知道最右侧的输入是无用的。

考虑如下极端情况:

image-20231129123249545

这种情况下任何一列输入和输出之间都没有相关性,每项权重都受到等量的向上和向下的压力,所有的输入在正负压力之间都是平衡的,那么神经网络应该如何处理?

学习间接相关性

如果数据没有相关性,那么创建具有相关性的中间数据!

解决方案:使用两个神经网络。第一个将创建一个与输出数据具有有限相关性的中间数据集,第二个将使用这种有限相关性正确预测输出。

创建关联

我们的目标是训练这个网络使得即使在输入数据集和输出数据集(layer_0和layer_2)之间没有相关性的情况下,用layer_0创建的数据集layer_1仍与layer_2相关。

image-20231129164518587

注意:这个网络仍然只是一个函数。它有一组以某种特定的方式放在一起的权重。此外,梯度下降仍然有效,因为你可以计算每个权重对误差的贡献,调整它使误差降到0。

反向传播:远程错误归因

  1. 从layer_1到layer_2的预测应该是什么?

    是layer_1的数值的加权平均值。

  2. 如果layer_2以x的误差偏高,你如何知道layer_1的哪些值贡献了误差?

    权重较高的值贡献更多误差,权重较低的值贡献的误差较少。

  3. 假设从layer_1到layer_2最左边的权重是0。layer_1上的该节点导致了多少误差?

    答案是0。因为权重准确地描述了每个layer_1节点对layer_2预测的贡献份额,也意味着这些权重能准确描述每个layer_1节点对layer_2的误差的贡献。

  4. 如何利用layer_2的delta(增量)来求出layer_1处的delta(增量)呢?

    将它乘以layer_1的各个权重。这种反向传递增量信号的过程叫作反向传播

    image-20231129170253214

反向传播:为什么有效?

反向传播让你做的事情:“如果你想让这个节点升高x,那么前面4个节点中的每一个都需要变高或者变低 x*weights_1_2 这么多,因为这些权重将预测的结果放大了 weights_1_2 倍。”

当权重矩阵 weights_1_2 反过来使用时,会将误差适当放大。它放大了误差,使得你知道每个 layer_1 节点应该向上或向下移动多少。

对于每一个权重,将它的输出增量乘以它的输入值,然后将权重调整那么多(或者,你也可以用alpha对其进行缩放)。

线性与非线性

目前展示的神经网络是不可用的。例如如下的代数运算:

image-20231129172424649

这里的要点是:对于任何两个乘法,我都可以用一个乘法来完成同样的事情。

image-20231129172712011

如上图,对于你创建的任何一个三层的神经网络,都存在着一个有着相同的行为模式的两层神经网络。即两个连续的加权和运算等价于一个加权和运算,只是代价更昂贵。

如果你现在训练这种三层网络,它不会收敛。

尝试着调整中间层之前,先讨论一下它的实现方式:

  1. 每个输入都有一个权重通往相应的每一个节点。

  2. 中间层中的每个节点都和各个输入节点存在一定的关联,中间节点唯一可以摆脱和某个特定输入节点的相关性的方法,是与另一个输入节点建立更多相关性。

    注意:中间节点不能向这个过程添加任何内容,它们自己不会带来任何相关性,只是或多或少与不同的输入节点相关。

  3. 已知输入和输出之间不存在相关性,中间层只是把一些已经没用的相关性混合在一起。

    我们真正需要的是使中间层能够选择性地与输入节点相关联。

    这被称为条件相关,或者选择相关。

选择性相关的秘密

  1. 当值低于0时“关闭”节点

    看起来太简单了,好像不会产生什么作用,但是不妨考虑一下:如果一个节点的值低于0,通常情况下这个节点仍然与输入具有一定的相关性,只是这个相关性的值恰好是负的而已。

    但是如果在节点为负的时候“关闭”它(将其设置为0),那么它与任何输入的相关性都会为0。这样节点就可以在它想要的时候选择性关联相应的对象,否则如果左边输入的权重值为0,右边输入的权重值是一个大负数,同时接受左右两边的输入将导致节点的值一直为0。

    注意:这对于两层神经网络是不可能的。因此,三层网络具有更强的能力。

  2. 这种“如果节点为负,则将其设置为0”的逻辑有一个漂亮的术语:“非线性”。

    没有这项调整的话,神经网络将是线性的。也就是说,如果没有这项技术,输出
    层能够选择的相关性和它在两层网络中可选的相关性没有什么区别。它直接与输
    入层相关,就意味着它不能解决新的路灯数据集的问题。

    非线性变换有很多种,这种最常用而且是许多情况下最好的方法,称为ReLu

调整权重来降低一系列训练样例上的误差,最终目标在于寻找输入层和输出层之间的相关性。如果不存在相关性,则误差永远不会达到0。

代码示例

  1. 初始化权重并进行正向传播

    image-20231129182637210

  2. 反向传播

    将两层神经网络的代码记住是一件重要的事情。

    image-20231129190036491

    relu2deriv 函数在 output > 0时返回1;否则,返回0。这是 relu 函数的斜率。

    那么为什么layer_1_delta函数要这样做呢?

    1. 首先将输出的delta乘以与之连接的每个权重,这么做算出了每项权重对误差的贡献大小。
    2. 如果relu将某个layer_1节点的输出设置为0,则不会对误差产生任何影响。

    注意:layer_1_delta是于1层与二层之间权重矩阵的转置相乘的,因为矩阵的逆运算。

反向传播的一次迭代

image-20231129193458217

image-20231129193802191

image-20231129194012313

image-20231129194250223

代码示例:

import numpy as np

np.random.seed(1)

def relu(x):
    return (x > 0) * x # returns x if x > 0
                       # return 0 otherwise

def relu2deriv(output):
    return output>0 # returns 1 for input > 0
                    # return 0 otherwise

streetlights = np.array( [[ 1, 0, 1 ],
                          [ 0, 1, 1 ],
                          [ 0, 0, 1 ],
                          [ 1, 1, 1 ] ] )

walk_vs_stop = np.array([[ 1, 1, 0, 0]]).T
    
alpha = 0.2
hidden_size = 4

weights_0_1 = 2*np.random.random((3,hidden_size)) - 1
weights_1_2 = 2*np.random.random((hidden_size,1)) - 1

for iteration in range(60):
   layer_2_error = 0
   for i in range(len(streetlights)):
      layer_0 = streetlights[i:i+1]
      layer_1 = relu(np.dot(layer_0,weights_0_1))
      layer_2 = np.dot(layer_1,weights_1_2)

      layer_2_error += np.sum((layer_2 - walk_vs_stop[i:i+1]) ** 2)

      layer_2_delta = (layer_2 - walk_vs_stop[i:i+1])
      layer_1_delta=layer_2_delta.dot(weights_1_2.T)*relu2deriv(layer_1)

      weights_1_2 -= alpha * layer_1.T.dot(layer_2_delta)
      weights_0_1 -= alpha * layer_0.T.dot(layer_1_delta)

   if(iteration % 10 == 9):
      print("Error:" + str(layer_2_error))

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