Как выполнить обратное распространение слоями разного размера? - PullRequest
2 голосов
/ 07 апреля 2020

Я занимаюсь разработкой своей первой нейронной сети, используя хорошо известную базу данных MNIST, написанную от руки git. Я хочу, чтобы NN мог классифицировать число от 0 до 9 по данному изображению.

Моя нейронная сеть состоит из трех слоев: входной слой (784 нейрона, каждый на каждый пиксель ди * 1035). *), скрытый слой из 30 нейронов (это также может быть 100 или 50, но я пока не слишком беспокоюсь о настройке гиперпараметра) и выходной слой, 10 нейронов, каждый из которых представляет активацию для каждого ди git , Это дает мне две весовые матрицы: одну из 30х724 и вторую 10х30.

Я знаю и понимаю теорию обратного распространения, оптимизацию и математические формулы, лежащие в основе этого, это не проблема как таковая. Я могу оптимизировать веса для второй матрицы весов, и стоимость действительно со временем снижается. Но я не могу продолжать распространять это обратно из-за структуры матрицы.

Зная, что я нашел производную стоимости по весам:

d(cost) / d(w) = d(cost) / d(f(z)) * d(f(z)) / d(z) * d(z) / d(w)

(Быть f функция активации и z скалярное произведение плюс смещение нейрона)

Итак, я на самом правом уровне, с выходным массивом из 10 элементов. d(cost) / d(f(z)) - вычитание наблюдаемых прогнозируемых значений. Я могу умножить это на d(f(z)) / d(z), который является просто f'(z) от самого правого слоя, также является одномерным вектором из 10 элементов, который теперь вычислен d(cost) / d(z). Тогда d(z)/d(w) - это просто вход для этого слоя, то есть выход предыдущего, который представляет собой вектор из 30 элементов. Я подумал, что могу транспонировать d(cost) / d(z) так, чтобы T( d(cost) / d(z) ) * d(z) / d(w) дал мне матрицу (10, 30), что имеет смысл, поскольку она соответствует размеру самой правой весовой матрицы.

Но тогда я получаю застрял. Размер d(cost) / d(f(z)) равен (1, 10), для d(f(z)) / d(z) равен (1, 30), а для d(z) / d(w) равен (1, 784). Я не знаю, как придумать результат для этого.

Это то, что я до сих пор кодировал. Неполная часть - это метод _propagate_back. Я пока не забочусь об уклонах, потому что я просто застрял с весами, и сначала я хочу это выяснить.

import random
from typing import List, Tuple

import numpy as np
from matplotlib import pyplot as plt

import mnist_loader

np.random.seed(42)

NETWORK_LAYER_SIZES = [784, 30, 10]
LEARNING_RATE = 0.05
BATCH_SIZE = 20
NUMBER_OF_EPOCHS = 5000


def sigmoid(x):
    return 1 / (1 + np.exp(-x))


def sigmoid_der(x):
    return sigmoid(x) * (1 - sigmoid(x))


class Layer:

    def __init__(self, input_size: int, output_size: int):
        self.weights = np.random.uniform(-1, 1, [output_size, input_size])
        self.biases = np.random.uniform(-1, 1, [output_size])
        self.z = np.zeros(output_size)
        self.a = np.zeros(output_size)
        self.dz = np.zeros(output_size)

    def feed_forward(self, input_data: np.ndarray):
        input_data_t = np.atleast_2d(input_data).T
        dot_product = self.weights.dot(input_data_t).T[0]
        self.z = dot_product + self.biases
        self.a = sigmoid(self.z)
        self.dz = sigmoid_der(self.z)


class Network:

    def __init__(self, layer_sizes: List[int], X_train: np.ndarray, y_train: np.ndarray):
        self.layers = [
            Layer(input_size, output_size)
            for input_size, output_size
            in zip(layer_sizes[0:], layer_sizes[1:])
        ]
        self.X_train = X_train
        self.y_train = y_train

    @property
    def predicted(self) -> np.ndarray:
        return self.layers[-1].a

    def _normalize_y(self, y: int) -> np.ndarray:
        output_layer_size = len(self.predicted)
        normalized_y = np.zeros(output_layer_size)
        normalized_y[y] = 1.

        return normalized_y

    def _calculate_cost(self, y_observed: np.ndarray) -> int:
        y_observed = self._normalize_y(y_observed)
        y_predicted = self.layers[-1].a

        squared_difference = (y_predicted - y_observed) ** 2

        return np.sum(squared_difference)

    def _get_training_batches(self, X_train: np.ndarray, y_train: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        train_batch_indexes = random.sample(range(len(X_train)), BATCH_SIZE)

        return X_train[train_batch_indexes], y_train[train_batch_indexes]

    def _feed_forward(self, input_data: np.ndarray):
        for layer in self.layers:
            layer.feed_forward(input_data)
            input_data = layer.a

    def _propagate_back(self, X: np.ndarray, y_observed: int):
        """
        der(cost) / der(weight) = der(cost) / der(predicted) * der(predicted) / der(z) * der(z) / der(weight)
        """
        y_observed = self._normalize_y(y_observed)
        d_cost_d_pred = self.predicted - y_observed

        hidden_layer = self.layers[0]
        output_layer = self.layers[1]

        # Output layer weights
        d_pred_d_z = output_layer.dz
        d_z_d_weight = hidden_layer.a  # Input to the current layer, i.e. the output from the previous one

        d_cost_d_z = d_cost_d_pred * d_pred_d_z
        d_cost_d_weight = np.atleast_2d(d_cost_d_z).T * np.atleast_2d(d_z_d_weight)

        output_layer.weights -= LEARNING_RATE * d_cost_d_weight

        # Hidden layer weights
        d_pred_d_z = hidden_layer.dz
        d_z_d_weight = X

        # ...

    def train(self, X_train: np.ndarray, y_train: np.ndarray):
        X_train_batch, y_train_batch = self._get_training_batches(X_train, y_train)
        cost_over_epoch = []

        for epoch_number in range(NUMBER_OF_EPOCHS):
            X_train_batch, y_train_batch = self._get_training_batches(X_train, y_train)

            cost = 0
            for X_sample, y_observed in zip(X_train_batch, y_train_batch):
                self._feed_forward(X_sample)
                cost += self._calculate_cost(y_observed)
                self._propagate_back(X_sample, y_observed)

            cost_over_epoch.append(cost / BATCH_SIZE)

        plt.plot(cost_over_epoch)
        plt.ylabel('Cost')
        plt.xlabel('Epoch')
        plt.savefig('cost_over_epoch.png')


training_data, validation_data, test_data = mnist_loader.load_data()
X_train, y_train = training_data[0], training_data[1]

network = Network(NETWORK_LAYER_SIZES, training_data[0], training_data[1])
network.train(X_train, y_train)

Это код для mnist_loader, на случай, если кто-то захочет воспроизвести пример:

import pickle
import gzip


def load_data():
    f = gzip.open('data/mnist.pkl.gz', 'rb')
    training_data, validation_data, test_data = pickle.load(f, encoding='latin-1')
    f.close()

    return training_data, validation_data, test_data

1 Ответ

1 голос
/ 08 апреля 2020

Если у вас есть d(cost) / d(z), я думаю, что вы должны умножить его на матрицу весов: только так вы можете переместить ошибку d(cost) / d(z) назад на новый слой (и получить значимую форму матрицы).

Вот как я изменил вам функцию обратного прохода:

def _propagate_back(self, X: np.ndarray, y_observed: int):
    """
    der(cost) / der(weight) = der(cost) / der(predicted) * der(predicted) / der(z) * der(z) / der(weight)
    """
    y_observed = self._normalize_y(y_observed)
    d_cost_d_pred = self.predicted - y_observed

    hidden_layer = self.layers[0]
    output_layer = self.layers[1]

    # Output layer weights
    d_pred_d_z = output_layer.dz
    d_z_d_weight = np.atleast_2d(hidden_layer.a)  # Input to the current layer, i.e. the output from the previous one

    d_cost_d_z = np.atleast_2d(d_cost_d_pred * d_pred_d_z)
    d_cost_d_weight = np.dot(d_cost_d_z.T, d_z_d_weight)

    output_layer.weights -= LEARNING_RATE * d_cost_d_weight

    # Hidden layer weights
    d_pred_d_z = hidden_layer.dz
    d_z_d_weight = np.atleast_2d(X)

    hidden_err = np.dot(d_cost_d_z, output_layer.weights)
    d_cost_d_z = np.atleast_2d(hidden_err * d_pred_d_z)
    d_cost_d_weight = np.dot(d_cost_d_z.T, d_z_d_weight)

    hidden_layer.weights -= LEARNING_RATE * d_cost_d_weight

Две ноты:

  • строка hidden_err = np.dot(d_cost_d_z, output_layer.weights), где я умножаю d(cost) / d(z) на матрицу весов
  • Я заменил некоторые вхождения оператора * (произведение Адамара в Numpy, если я прав) на применение функции np.dot (умножение матриц в Numpy)

Я не эксперт, поэтому я надеюсь, что не совершил какой-то ужасной ошибки ... во всяком случае, мой ответ был в основном основан на этой главе из Neural Networks and Deep Изучение Майклом Нильсеном.

...