如何使用 numpy 正确计算神经网络中的梯度

Posted

技术标签:

【中文标题】如何使用 numpy 正确计算神经网络中的梯度【英文标题】:How to correctly calculate gradients in neural network with numpy 【发布时间】:2020-02-10 11:28:49 【问题描述】:

我正在尝试使用 numpy 从头开始​​构建一个简单的神经网络类,并使用 XOR 问题对其进行测试。但是反向传播函数(backprop)似乎不能正常工作。

在类中,我通过传入每一层的大小以及要在每一层使用的激活函数来构造实例。我假设最终的激活函数是softmax,这样我就可以计算交叉熵损失对最后一层Z的导数。我的班级也没有单独的偏置矩阵集。我只是将它们作为最后的额外列包含在权重矩阵中。

我知道我的反向传播函数无法正常工作,因为神经网络永远不会收敛到稍微正确的输出。我还创建了一个数值梯度函数,并在比较两者的结果时。我得到的数字截然不同。

我的理解是,每一层的增量值(L 是最后一层,i 代表任何其他层)应该是:

这些层各自的梯度/权重更新应该是:

其中*是hardamard product,a代表某层的激活,z代表某层的未激活输出。

我用来测试的样本数据在文件的底部。

这是我第一次尝试从头开始实现反向传播算法。所以我有点迷失从这里去哪里。

import numpy as np

def sigmoid(n, deriv=False):
    if deriv:
        return np.multiply(n, np.subtract(1, n))
    return 1 / (1 + np.exp(-n))

def softmax(X, deriv=False):
    if not deriv:
        exps = np.exp(X - np.max(X))
        return exps / np.sum(exps)
    else:
        raise Error('Unimplemented')

def cross_entropy(y, p, deriv=False):
    """
    when deriv = True, returns deriv of cost wrt z
    """
    if deriv:
        ret = p - y
        return ret
    else:
        p = np.clip(p, 1e-12, 1. - 1e-12)
        N = p.shape[0]
        return -np.sum(y*np.log(p))/(N)

class NN:
    def __init__(self, layers, activations):
        """random initialization of weights/biases
        NOTE - biases are built into the standard weight matrices by adding an extra column
        and multiplying it by one in every layer"""
        self.activate_fns = activations
        self.weights = [np.random.rand(layers[1], layers[0]+1)]
        for i in range(1, len(layers)):
            if i != len(layers)-1:
                self.weights.append(np.random.rand(layers[i+1], layers[i]+1))

                for j in range(layers[i+1]):
                    for k in range(layers[i]+1):
                        if np.random.rand(1,1)[0,0] > .5:
                            self.weights[-1][j,k] = -self.weights[-1][j,k]

    def ff(self, X, get_activations=False):
         """Feedforward"""
         activations, zs = [], []
         for activate, w in zip(self.activate_fns, self.weights):
             X = np.vstack([X, np.ones((1, 1))]) # adding bias
             z = w.dot(X)
             X = activate(z)
             if get_activations:
                 zs.append(z)
                 activations.append(X)
         return (activations, zs) if get_activations else X

    def grad_descent(self, data, epochs, learning_rate):
        """gradient descent
        data - list of 2 item tuples, the first item being an input, and the second being its label"""
        grad_w = [np.zeros_like(w) for w in self.weights]
        for _ in range(epochs):
            for x, y in data:
                grad_w = [n+o for n, o in zip(self.backprop(x, y), grad_w)]
            self.weights = [w-(learning_rate/len(data))*gw for w, gw in zip(self.weights, grad_w)]

    def backprop(self, X, y):
        """perfoms backprop for one layer of a NN with softmax/cross_entropy output layer"""
        (activations, zs) = self.ff(X, True)
        activations.insert(0, X)

        deltas = [0 for _ in range(len(self.weights))]
        grad_w = [0 for _ in range(len(self.weights))]
        deltas[-1] = cross_entropy(y, activations[-1], True) # assumes output activation is softmax
        grad_w[-1] = np.dot(deltas[-1], np.vstack([activations[-2], np.ones((1, 1))]).transpose())
        for i in range(len(self.weights)-2, -1, -1):
            deltas[i] = np.dot(self.weights[i+1][:, :-1].transpose(), deltas[i+1]) * self.activate_fns[i](zs[i], True)
            grad_w[i] = np.hstack((np.dot(deltas[i], activations[max(0, i-1)].transpose()), deltas[i]))

        # check gradient
        num_gw = self.gradient_check(X, y, i)
        print('numerical:', num_gw, '\nanalytic:', grad_w)

        return grad_w

    def gradient_check(self, x, y, i, epsilon=1e-4):
        """Numerically calculate the gradient in order to check analytical correctness"""
        grad_w = [np.zeros_like(w) for w in self.weights]
        for w, gw in zip(self.weights, grad_w):
            for j in range(w.shape[0]):
                for k in range(w.shape[1]):
                    w[j,k] += epsilon
                    out1 = cross_entropy(self.ff(x), y)
                    w[j,k] -= 2*epsilon
                    out2 = cross_entropy(self.ff(x), y)
                    gw[j,k] = np.float64(out1 - out2) / (2*epsilon)
                    w[j,k] += epsilon # return weight to original value
        return grad_w

##### TESTING #####
X = [np.array([[0],[0]]), np.array([[0],[1]]), np.array([[1],[0]]), np.array([[1],[1]])]
y = [np.array([[1], [0]]), np.array([[0], [1]]), np.array([[0], [1]]), np.array([[1], [0]])]
data = []
for x, t in zip(X, y):
    data.append((x, t))

def nn_test():
    c = NN([2, 2, 2], [sigmoid, sigmoid, softmax])
    c.grad_descent(data, 100, .01)
    for x in X:
        print(c.ff(x))
nn_test()

更新:我在代码中发现了一个小错误,但仍然不能正确收敛。我手动计算/导出了两个矩阵的梯度,在我的实现中没有发现错误,所以我仍然不知道它有什么问题。

更新 #2:我使用以下代码创建了上面使用的程序版本。经过测试,我发现 NN 能够学习正确的权重,以分别在 XOR 中对 4 个案例中的每一个进行分类,但是当我尝试一次使用所有训练示例进行训练时(如图所示),得到的权重几乎总是输出一些东西两个输出节点都在 0.5 左右。有人可以告诉我为什么会这样吗?

X = [np.array([[0],[0]]), np.array([[0],[1]]), np.array([[1],[0]]), np.array([[1],[1]])]
y = [np.array([[1], [0]]), np.array([[0], [1]]), np.array([[0], [1]]), np.array([[1], [0]])]
weights = [np.random.rand(2, 3) for _ in range(2)]
for _ in range(1000):
    for i in range(4):
        #Feedforward
        a0 = X[i]
        z0 = weights[0].dot(np.vstack([a0, np.ones((1, 1))]))
        a1 = sigmoid(z0)
        z1 = weights[1].dot(np.vstack([a1, np.ones((1, 1))]))
        a2 = softmax(z1)
        # print('output:', a2, '\ncost:', cross_entropy(y[i], a2))

        #backprop
        del1 = cross_entropy(y[i], a2, True)
        dcdw1 = del1.dot(np.vstack([a1, np.ones((1, 1))]).T)
        del0 = weights[1][:, :-1].T.dot(del1)*sigmoid(z0, True)
        dcdw0 = del0.dot(np.vstack([a0, np.ones((1, 1))]).T)

        weights[0] -= .03*weights[0]*dcdw0
        weights[1] -= .03*weights[1]*dcdw1
i = 0
a0 = X[i]
z0 = weights[0].dot(np.vstack([a0, np.ones((1, 1))]))
a1 = sigmoid(z0)
z1 = weights[1].dot(np.vstack([a1, np.ones((1, 1))]))
a2 = softmax(z1)
print(a2)

【问题讨论】:

【参考方案1】:

Softmax 看起来不对

使用交叉熵损失,softmax 的导数非常好(假设您使用的是 1 热向量,其中“1 热”本质上表示除单个 1 之外的所有 0 的数组,即:[0,0, 0,0,0,0,1,0,0])

对于节点y_n,它最终是y_n-t_n。所以对于一个有输出的softmax:

[0.2,0.2,0.3,0.3]

以及想要的输出:

[0,1,0,0]

每个softmax节点的梯度是:

[0.2,-0.8,0.3,0.3]

看起来好像是从整个数组中减去 1。变量名称不是很清楚,所以如果您可以将它们从L 重命名为L 所代表的名称,例如output_layer,我将能够提供更多帮助。

另外,其他层只是为了清理。当您以a^(L-1) 为例时,您的意思是“a 的 (l-1) 次方”还是“a xor (l-1)”?因为在 python 中^ 表示异或。

编辑:

我用这段代码发现了奇怪的矩阵维度(在函数backprop的第69行修改)

deltas = [0 for _ in range(len(self.weights))]
grad_w = [0 for _ in range(len(self.weights))]
deltas[-1] = cross_entropy(y, activations[-1], True) # assumes output activation is softmax
print(deltas[-1].shape)
grad_w[-1] = np.dot(deltas[-1], np.vstack([activations[-2], np.ones((1, 1))]).transpose())
print(self.weights[-1].shape)
print(activations[-2].shape)
exit()

【讨论】:

抱歉,这些是用来表示我用来计算梯度的方程。不是实际的代码,我重写了这些代码,希望让它更容易理解。当您说“每个softmax节点的梯度”时,您的意思是错误函数的偏导数与该层(z)的未激活输出有关吗?如果是这样,我相信我的部分代码是正确的。 @jhanreg11 你是对的,你的 softmax 是正确的。我注意到一些奇怪的东西,你是倒数第二层有 2 个神经元对吗?你的最后一层也有2个神经元?令人困惑的是最后一层的权重矩阵大小为 2x3,不应该是 2x2 吗?这是有原因的吗? 啊,是的,我这样做是为了不必为每一层都包含一个单独的偏置矩阵。相反,我只是在每一层的输入中添加另一个值为 1 的行。这样,额外的列总是被乘以一,使得矩阵中的任何值都是偏差。 @jhanreg11 啊,我明白了,这是一种很酷的方法(虽然要小心,这很容易出错,它可能更节省内存,但通常内存不是问题使用机器学习 - 它的计算速度)。我自己没有时间单独检查整个事情,但如果你想检查它并确保你做的一切都是正确的,你需要将它与纸上的神经网络进行比较。播种您的运行以使其保持一致,然后手动重新创建确切的架构并执行 1 次前向和后向传递,并将其与您通过计算获得的每一步的值进行比较。

以上是关于如何使用 numpy 正确计算神经网络中的梯度的主要内容,如果未能解决你的问题,请参考以下文章

神经网络反向传播梯度计算数学原理

《吴恩达深度学习笔记》01.神经网络和深度学习 W2.神经网络基础

CS231n 卷积神经网络与计算机视觉 7 神经网络训练技巧汇总 梯度检验 参数更新 超参数优化 模型融合 等

神经网络算法-梯度下降GradientDescent

梯度在神经网络中的作用

神经网络 MNIST:反向传播是正确的,但训练/测试精度非常低