0
点赞
收藏
分享

微信扫一扫

机器学习笔记 - 使用python代码实现易于理解的反向传播

一、反向传播概述        

        反向传播可以说是神经网络历史上最重要的算法——如果没有有效的反向传播,就不可能将深度学习网络训练到我们今天看到的深度。反向传播可以被认为是现代神经网络和深度学习的基石。

        反向传播的最初版本是在1970年代引入的,但直到1986年具有开创性的论文,Rumelhart、Hinton和Williams通过反向传播错误学习表示,我们才能够设计出更快的算法,更擅长训练更深的网络。

        网络有若干个关于反向传播的教程。我最喜欢的一些包括:

        1.    AndrewNg在Coursera的机器学习课程中对反向传播的讨论。

        2.    数学动机很强的第2章–MichaelNielsen的神经网络和深度学习中的反向传播算法如何工作。

        3.    斯坦福大学的cs231n对反向传播的探索和分析。

        4.    马特·马祖尔的优秀具体例子证明反向传播如何工作。

        如您所见,不乏反向传播的原理说明——我们这里使用Python语言构建一个直观、易于理解的反向传播算法实现。

        我们将构建一个实际的神经网络并使用反向传播算法对其进行训练,您将了解反向传播的工作原理——也许更重要的是,您将对如何使用该算法从头开始训练神经网络有更深入的了解。

二、反向传播算法

        反向传播算法包括两个阶段:

        1、我们的输入通过网络并获得输出预测的前向传递(也称为传播阶段)。

        2、反向传播,我们计算网络最后一层(即预测层)损失函数的梯度,并使用该梯度递归地应用链式法则来更新我们网络中的权重(也称为权重更新)阶段)。

        下面我们将使用Python实现反向传播算法。

        之后,使用下面两个数据集演示如何在以下两个方面使用反向传播和Python训练自定义神经网络:

        1、XOR数据集

        2、MNIST数据集

1、向前传递

表2:左:按位异或数据集(包括类标签)。右图:插入了偏差列的XOR数据集设计矩阵(为简洁起见,不包括类标签)。

         前向传递的目的是通过应用一系列点积和激活来在网络中传播我们的输入,直到我们到达网络的输出层(即我们的预测)。为了可视化这个过程,让我们首先考虑XOR数据集(表2,左)。

        我们可以看到设计矩阵(左)中的每个条目X都是二维的——每个数据点由两个数字表示。例如,第一个数据点由特征向量(0,0)表示,第二个数据点由(0,1)等表示。然后我们将输出值y作为右列。我们的目标输出值是类标签。给定来自设计矩阵的输入,我们的目标是正确预测目标输出值。

        为了在这个问题上获得完美的分类精度,我们需要一个至少具有单个隐藏层的前馈神经网络,所以让我们继续并从2-2-1开始架构(图9,顶部)。这是一个好的开始;然而,我们忘记包括偏差项。从第9章我们知道,有两种方法可以在我们的网络中包含偏差项b。我们可以:

        1、使用单独的变量。

        2、通过在特征向量中插入一列1,将偏差视为权重矩阵中的可训练参数。

        插入一列1的特征向量是通过编程完成的,但为了确保我们理解这一点,我们可视化XOR设计矩阵以明确看到这一点(表2,右)。可见,一列1已添加到我们的特征向量中。实际上,您可以将此列插入到您喜欢的任何位置,但我们通常将其放在特征向量中的最前面或最后面。

        由于我们改变了输入特征向量的大小(通常在神经网络实现本身内部执行,因此我们不需要明确修改我们的设计矩阵),这将我们的(感知)网络架构从2-2-1更改为(内部)3-3-1(图9,底部)。

图9:顶部:为了构建一个神经网络来正确分类XOR数据集,我们需要一个具有两个输入节点、两个隐藏节点和一个输出节点的网络。这产生了2-2-1架构。底部:由于偏差技巧,我们实际的内部网络架构表示为3-3-1。在绝大多数神经网络实现中,对权重矩阵的这种调整发生在内部,您无需担心;然而,了解幕后发生的事情仍然很重要。

         我们仍将这种网络架构称为2-2-1,但在实施时,由于添加了嵌入在权重矩阵中的偏置项,因此它实际上是3-3-1。

        最后,回想一下我们的输入层和所有隐藏层都需要一个偏置项;然而,最终的输出层不需要偏置。应用偏差技巧的好处是我们不再需要明确跟踪偏差参数——它现在是权重矩阵中的一个可训练参数,从而使训练更有效并且更容易实现。请参阅第9章更深入地讨论为什么这种偏差技巧有效。

        为了查看前向传递的作用,我们首先初始化网络中的权重,如图10所示。请注意权重矩阵中的每个箭头如何具有与其关联的值——这是给定节点的当前权重值,表示给定输入被放大或减少的量。这个权重值将在反向传播阶段更新。

        在图10的最左边,我们展示了特征向量(0,1,1)(以及网络的目标输出值1)。在这里我们可以看到0、1和1已经分配给网络中的三个输入节点。为了通过网络传播值并获得最终分类,我们需要在输入和权重值之间取点积,然后应用激活函数(在这种情况下,sigmoid函数,σ)。

图10:前向传播的一个例子。输入向量[0,1,1]呈现给网络。取输入和权重之间的点积,然后应用sigmoid激活函数来获得隐藏层中的值(分别为0.899、0.593和0.378)。最后,计算最后一层的点积和sigmoid激活函数,产生0.506的输出。将step函数应用于0.506产生1,这确实是正确的目标类标签。

         让我们计算隐藏层中三个节点的输入:

        1、σ((0×0.351)+(1×1.076)+(1×1.116))=0.899

        2、σ((0×0.097)+(1×0.165)+(1×0.542))=0.593

        3、σ((0×0.457)+(1×0.165)+(1×0.331))=0.378

        查看隐藏层的节点值(图10,中间),我们可以看到节点已更新以反映我们的计算。

        我们现在有了对隐藏层节点的输入。为了计算输出预测,我们再次计算点积,然后是sigmoid激活:

        因此,网络的输出为0.506。我们可以应用一个阶跃函数来确定这个输出是否是正确的分类:

        应用net=0.506的step函数,我们看到我们的网络预测1,这实际上是正确的类标签。然而,我们的网络对这个类标签不是很有信心——预测值0.506非常接近步骤的阈值。理想情况下,这个预测应该更接近于0.98-0.99.,这意味着我们的网络已经真正了解了数据集中的潜在模式。为了让我们的网络真正“学习”,我们需要应用反向传播。 

2、向后传播

        为了应用反向传播算法,我们的激活函数必须是可微的,以便我们可以计算误差相对于给定权重wi,j的偏导数;Loss(E)、节点输出oj和网络输出netj。

        由于反向传播背后的微积分在以前的作品中已经被详尽地解释了很多次(参见AndrewNg、MichaelNielsen、MattMazur),这里跳过反向传播链规则更新的推导,而是通过下面的代码来解释它。

        对于精通数学的人,请参阅上面的参考资料,了解有关链式法则及其在反向传播算法中的作用的更多信息。通过在代码中解释这个过程,是为了更直观的理解反向传播。

3、使用Python实现反向传播

        让我们创建一个命名为neuralnetwork.py。

# 导入包
import numpy as np

class NeuralNetwork:
    # layers,代表前馈网络实际架构的整数列表
    # alpha,学习率,在权重更新阶段应用此值。
    def __init__(self, layers, alpha=0.1):
        # 始化每个层的权重列表W
        # 然后存储层和学习率
        self.W = []
        self.layers = layers
        self.alpha = alpha
        # 初始化权重
        for i in np.arange(0, len(layers) - 2):
            # 网络中的每一层都是通过从标准正态分布中采样值来构建MxN权重矩阵来随机初始化的
            # 为了解决偏差,我们将层数[i]和层数[i+1]加一——这样做会改变我们的权重矩阵w,使其形状为3x3
            w = np.random.randn(layers[i] + 1, layers[i + 1] + 1)
            # 除以当前层节点数的平方根来缩放w,从而标准化每个神经元输出的方差
            self.W.append(w / np.sqrt(layers[i]))

        # the last two layers are a special case where the input
        # connections need a bias term but the output does not
        w = np.random.randn(layers[-2] + 1, layers[-1])
        self.W.append(w / np.sqrt(layers[-2]))

    def __repr__(self):
        # construct and return a string that represents the network
        # architecture
        return "NeuralNetwork: {}".format("-".join(str(l) for l in self.layers))

    def sigmoid(self, x):
        # 计算并返回给定输入值的 sigmoid 激活值
        return 1.0 / (1 + np.exp(-x))

    def sigmoid_deriv(self, x):
        # 计算 sigmoid 函数的导数
        return x * (1 - x)

    def fit(self, X, y, epochs=1000, displayUpdate=100):
        # insert a column of 1’s as the last entry in the feature
        # matrix -- this little trick allows us to treat the bias
        # as a trainable parameter within the weight matrix
        X = np.c_[X, np.ones((X.shape[0]))]

        # loop over the desired number of epochs
        for epoch in np.arange(0, epochs):
            # loop over each individual data point and train
            # our network on it
            for (x, target) in zip(X, y):
                self.fit_partial(x, target)

            # check to see if we should display a training update
            if epoch == 0 or (epoch + 1) % displayUpdate == 0:
                loss = self.calculate_loss(X, y)
                print("[INFO] epoch={}, loss={:.7f}".format(epoch + 1, loss))

    def fit_partial(self, x, y):
        # construct our list of output activations for each layer
        # as our data point flows through the network; the first
        # activation is a special case -- it’s just the input
        # feature vector itself
        A = [np.atleast_2d(x)]

        # FEEDFORWARD:
        # loop over the layers in the network

        for layer in np.arange(0, len(self.W)):
            # feedforward the activation at the current layer by
            # taking the dot product between the activation and
            # the weight matrix -- this is called the "net input"
            # to the current layer
            net = A[layer].dot(self.W[layer])

            # computing the "net output" is simply applying our
            # nonlinear activation function to the net input
            out = self.sigmoid(net)

            # once we have the net output, add it to our list of
            # activations
            A.append(out)

        # 反向传播
        # 反向传播的第一阶段是计算我们的预测与真实目标值之间的差异
        error = A[-1] - y
        # 从这里开始,我们需要应用链式规则并构建我们的增量列表“D”; 
        # 增量中的第一项只是输出层的误差乘以我们的激活函数对输出值的导数
        D = [error * self.sigmoid_deriv(A[-1])]

        # 一旦你理解了链式规则,使用“for”循环就变得非常容易实现
        for layer in np.arange(len(A) - 2, 0, -1):
            # the delta for the current layer is equal to the delta
            # of the *previous layer* dotted with the weight matrix
            # of the current layer, followed by multiplying the delta
            # by the derivative of the nonlinear activation function
            # for the activations of the current layer
            delta = D[-1].dot(self.W[layer].T)
            delta = delta * self.sigmoid_deriv(A[layer])
            D.append(delta)

        # since we looped over our layers in reverse order we need to
        # reverse the deltas
        D = D[::-1]

        # WEIGHT UPDATE PHASE
        # loop over the layers

        for layer in np.arange(0, len(self.W)):
            # update our weights by taking the dot product of the layer
            # activations with their respective deltas, then multiplying
            # this value by some small learning rate and adding to our
            # weight matrix -- this is where the actual "learning" takes
            # place
            self.W[layer] += -self.alpha * A[layer].T.dot(D[layer])

    def predict(self, X, addBias=True):
        # initialize the output prediction as the input features -- this
        # value will be (forward) propagated through the network to
        # obtain the final prediction
        p = np.atleast_2d(X)
        # check to see if the bias column should be added

        if addBias:
            # insert a column of 1’s as the last entry in the feature
            # matrix (bias)
            p = np.c_[p, np.ones((p.shape[0]))]

        # loop over our layers in the network
        for layer in np.arange(0, len(self.W)):
            # computing the output prediction is as simple as taking
            # the dot product between the current activation value ‘p‘
            # and the weight matrix associated with the current layer,
            # then passing this value through a nonlinear activation
            # function
            p = self.sigmoid(np.dot(p, self.W[layer]))

        # return the predicted value
        return p

    def calculate_loss(self, X, targets):
        # make predictions for the input data points then compute
        # the loss
        targets = np.atleast_2d(targets)
        predictions = self.predict(X, addBias=False)
        loss = 0.5 * np.sum((predictions - targets) ** 2)
        # return the loss
        return loss

4、示例1 - 异或数据

        创建文件nn_xor.py

# import the necessary packages
from neuralnetwork import NeuralNetwork
import numpy as np

# 构建异或数据集
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])

# 定义神经网络并进行训练
nn = NeuralNetwork([2, 2, 1], alpha=0.5)
nn.fit(X, y, epochs=20000)

# 进行预测
for (x, target) in zip(X, y):
    # 云测并打印结果
    pred = nn.predict(x)[0][0]
    step = 1 if pred > 0.5 else 0
    print("[INFO] data={}, ground-truth={}, pred={:.4f}, step={}".format(x, target[0], pred, step))

        执行文件,输出如下

        对于每一个数据点,神经网络都能够正确学习异或模式,证明我们的多层神经网络能够学习非线性函数。 

        为了证明学习XOR函数至少需要一个隐藏层,我们将上面的代码nn = NeuralNetwork([2, 2, 1], alpha=0.5)修改为NeuralNetwork([2, 1], alpha=0.5),然后重新训练,得到如下输出。

         无论你如何调整学习率或权重初始化,你永远无法逼近 XOR 函数。 这就是为什么具有通过反向传播训练的非线性激活函数的多层网络如此重要的原因。

5、示例2 - MINST数据集

        第二个例子,是用于手写数字识别的MNIST数据集(下图)的一个子集。MNIST数据集的这个子集内置于scikit-learn库中,包括1797个示例数字,每个数字为8×8个灰度图像(原始图像为28×28)。当展平后,每个图像都是一个8×8=64维向量。

         我们创建一个名为nn_mnist.py的文件。

from neuralnetwork import NeuralNetwork
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn import datasets

# 加载 MNIST 数据集并应用最小/最大缩放将像素强度值缩放到 [0, 1] 范围(每个图像由 8 x 8 = 64-dim 特征向量表示)
print("[INFO] loading MNIST (sample) dataset...")
digits = datasets.load_digits()
data = digits.data.astype("float")
data = (data - data.min()) / (data.max() - data.min())
print("[INFO] samples: {}, dim: {}".format(data.shape[0],data.shape[1]))

# 构建训练集和测试集
(trainX, testX, trainY, testY) = train_test_split(data, digits.target, test_size=0.25)

# 将标签从整数转换为向量
trainY = LabelBinarizer().fit_transform(trainY)
testY = LabelBinarizer().fit_transform(testY)

# train the network
print("[INFO] training network...")
nn = NeuralNetwork([trainX.shape[1], 32, 16, 10])
print("[INFO] {}".format(nn))
nn.fit(trainX, trainY, epochs=1000)

# evaluate the network
print("[INFO] evaluating network...")
predictions = nn.predict(testX)
predictions = predictions.argmax(axis=1)
print(classification_report(testY.argmax(axis=1), predictions))

        运行文件,得到如下输出

         我们在测试集上获得了约98%的分类准确率;

三、小结 

        我们了解如何使用Python从头开始实现反向传播算法。反向传播是梯度下降算法系列的推广,专门用于训练多层前馈网络。

        反向传播算法包括两个阶段:

        1.    前向传递,我们通过网络传递输入以获得我们的输出分类。

        2.    反向传播(即权重更新阶段),我们计算损失函数的梯度并迭代地应用链式规则来更新网络中的权重。

        反向传播即用于训练使用简单的前馈神经网络,也用于训练复杂的深度卷积神经网络。这是通过确保网络内部的激活函数可微分来实现的,从而允许应用链式规则。此外,网络中需要更新权重/参数的任何其他层也必须与反向传播兼容。

        我们使用Python编程语言实现了简单的反向传播算法,并设计了一个多层前馈NeuralNetwork类。然后在XOR数据集上对该实现进行了训练,以证明我们的神经网络能够通过应用具有至少一个隐藏层的反向传播算法来学习非线性函数。然后,我们将相同的反向传播 + Python实现应用于MNIST数据集的一个子集,以证明该算法也可用于处理图像数据。

        在实践中,反向传播不仅难以实现(由于计算梯度的错误),而且如果没有特殊的优化库,也很难使其高效,这就是为什么我们经常使用诸如Keras、TensorFlow、mxnet之类的库,因为这些库已经正确使用优化策略实现反向传播。

举报

相关推荐

0 条评论