Как правилно да изчислявате градиенти в невронна мрежа с numpy

Опитвам се да изградя прост клас невронна мрежа от нулата, използвайки numpy, и го тествам, използвайки проблема XOR. Но функцията за обратно разпространение (backprop) изглежда не работи правилно.

В класа конструирам екземпляри, като предавам размера на всеки слой и функциите за активиране, които да се използват на всеки слой. Предполагам, че крайната функция за активиране е softmax, така че мога да изчисля производната на загубата на кръстосана ентропия спрямо Z на последния слой. Аз също нямам отделен набор от матрици на отклонение в моя клас. Просто ги включвам в тегловните матрици като допълнителна колона в края.

Знам, че моята backprop функция не работи правилно, тъй като невронната мрежа никога не се събира в донякъде правилен изход. Също така създадох цифрова градиентна функция и при сравняване на резултатите от двете. Получавам драстично различни числа.

Моето разбиране от това, което прочетох, е, че делта стойностите на всеки слой (като L е последният слой и аз представлявам всеки друг слой) трябва да бъдат:

delta_L = (dE/dZ)

delta_i = WT(i+1)dotdelta(i+1)*sigmoid_prime(zi)

И съответните градиенти/актуализация на теглото на тези слоеве трябва да бъдат:

градиент_l = (делта_l)(активации(l-1).T)

W = W - алфа(W)(dE/dW)

Където * е хардамард продукт, 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 успя да научи правилните тегла за класифициране на всеки от 4-те случая в XOR поотделно, но когато се опитам да тренирам, използвайки всички примери за обучение наведнъж (както е показано), получените тегла почти винаги извеждат нещо около .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)

person jhanreg11    schedule 13.10.2019    source източник


Отговори (1)


Softmax не изглежда както трябва

Използвайки кръстосана загуба на ентропия, производната за softmax е наистина хубава (ако приемем, че използвате 1 горещ вектор, където "1 горещ" по същество означава масив от всички 0 с изключение на едно 1, т.е.: [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 ^ означава xor.

РЕДАКТИРАНЕ:

Използвах този код и открих странните размери на матрицата (променени на ред 69 във функцията backprop)

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()
person Recessive    schedule 14.10.2019
comment
Съжалявам, те трябваше да представят уравненията, които използвах за изчисляване на моите градиенти. Не е действителен код, пренаписах ги, за да се надявам да го направя по-лесен за разбиране. Когато казвате Градиентът във всеки от възлите на softmax, имате ли предвид частичната производна на функцията за грешка спрямо неактивирания изход на този слой (z)? Ако е така, вярвам, че част от моя код е правилна. - person jhanreg11; 14.10.2019
comment
@jhanreg11 Прав си, твоят softmax е правилен. Все пак забелязвам нещо странно, ти си предпоследният слой, имаш ли 2 неврона? И последният ви слой също има 2 неврона? Това, което е объркващо е, че матрицата на теглото за последния слой е с размер 2x3, не трябва ли да е 2x2? Има ли причина за това? - person Recessive; 14.10.2019
comment
А, да, правя го, за да не се налага да включвам отделна матрица на отклонение за всеки слой. Вместо това просто добавям друг ред със стойност 1 към входа на всеки слой. По този начин допълнителната колона винаги се умножава по едно, което прави каквато и да е нейната стойност в матрицата, да бъде отклонението. - person jhanreg11; 14.10.2019
comment
@jhanreg11 А, разбирам, това е готин начин да го направите (въпреки че внимавайте, ще бъде много лесно да направите грешка, може да е по-ефективно за паметта, но като цяло паметта не е проблемът с машинното обучение - това е скоростта на изчисление) . Аз самият нямам време да прегледам всичко поотделно, но ако искате да го прегледате и да сте сигурни, че правите всичко правилно, ще трябва да го сравните с невронна мрежа на хартия. Задайте вашето изпълнение, така че да е последователно, след това пресъздайте точната архитектура на ръка и направете 1 преминаване напред и назад и го сравнете със стойностите на всяка стъпка, която сте получили изчислително. - person Recessive; 15.10.2019