NN с нуля работает с простыми проблемами, но не с номерами MNIST - PullRequest
0 голосов
/ 07 августа 2020

Я работал над сетевым узлом с одним скрытым слоем с гибким количеством узлов в каждом из трех уровней. Вот код:

import time
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.datasets import mnist

class_names = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

class NeuralNetwork():

    correct = 0
    num_predictions = 10
    epochs = 100
    sizeOfEpoch = 5000
    Lambda = 10
    learningRate = 0.00001

    def __init__(self, sizes):
        self.dimensions = sizes

        self.x = np.arange(1,self.epochs+1)
        self.y = np.empty(self.epochs)

        self.secondLayerNeurons = np.empty(sizes[1])
        self.outputNeurons = np.empty(sizes[2])

        self.firstLayerWeights = np.random.rand(sizes[1], sizes[0])
        self.secondLayerWeights = np.random.rand(sizes[2], sizes[1])
        self.firstLayerBiases = np.random.rand(sizes[1])
        self.secondLayerBiases = np.random.rand(sizes[2])

        self.firstLayerWeightsSummations = np.zeros([sizes[1], sizes[0]])
        self.secondLayerWeightsSummations = np.zeros([sizes[2], sizes[1]])
        self.firstLayerBiasesSummations = np.zeros([sizes[1]])
        self.secondLayerBiasesSummations = np.zeros([sizes[2]])

        self.hiddenLayerErrors = np.empty(sizes[1])
        self.outputLayerErrors = np.empty(sizes[2])

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

    def sigmoidDerivative(self, x):
        return np.multiply(x,(1-x))

    def forwardProp(self, inputs):
        for i in range (self.dimensions[1]):
            self.secondLayerNeurons[i] = self.sigmoid(np.dot(self.firstLayerWeights[i], inputs)+self.firstLayerBiases[i])
        for i in range (self.dimensions[2]):
            self.outputNeurons[i] = self.sigmoid(np.dot(self.secondLayerWeights[i], self.secondLayerNeurons)+self.secondLayerBiases[i])

    def backProp(self, inputs, correct_output):
        self.outputLayerErrors = np.subtract(self.outputNeurons, correct_output)
        self.hiddenLayerErrors = np.multiply(np.dot(self.secondLayerWeights.T, self.outputLayerErrors), self.sigmoidDerivative(self.secondLayerNeurons))

        for i in range (self.dimensions[2]):
            for j in range (self.dimensions[1]):
                if j==0:
                    self.secondLayerBiasesSummations[i] += self.outputLayerErrors[i]
                self.secondLayerWeightsSummations[i][j] += self.outputLayerErrors[i]*self.secondLayerNeurons[j]
        for i in range (self.dimensions[1]):
            for j in range (self.dimensions[0]):
                if j==0:
                    self.firstLayerBiasesSummations[i] += self.hiddenLayerErrors[i]
                self.firstLayerWeightsSummations[i][j] += self.hiddenLayerErrors[i]*inputs[j]

    def train(self, trainImages, trainLabels):
        size = str(self.sizeOfEpoch)
        greatestError = 0.0
        start_time2 = time.time()

        for m in range (self.sizeOfEpoch):
            correct_output = np.zeros([self.dimensions[2]])
            correct_output[int(class_names[trainLabels[m]])] = 1.0

            self.forwardProp(trainImages[m].flatten())
            self.backProp(trainImages[m].flatten(), correct_output)

            if np.argmax(self.outputNeurons) == int(trainLabels[m]):
                self.correct+=1

            if m%200 == 0:
                error = np.amax(np.absolute(self.outputLayerErrors))
                if error > greatestError:
                    greatestError = error
                accuracy = str(int((self.correct/(m+1))*100)) + '%'
                percent = str(int((m/self.sizeOfEpoch)*100)) + '%'
                print ("Progress: " + percent + " -- Accuracy: " + accuracy + " -- Error: " + str(greatestError), end="\r")
        self.change()

        time2 = str(round((time.time() - start_time2), 2))
        print (size + '/' + size + " -- " + time2 + "s" + " -- Accuracy: " + accuracy + " -- Error: " + str(greatestError), end="\r")
        return greatestError

    def change(self):
        for i in range (self.dimensions[2]):
            for j in range (self.dimensions[1]):
                if j == 0:
                    self.secondLayerBiases[i] -= self.learningRate*self.secondLayerBiasesSummations[i]
                self.secondLayerWeights[i][j] -= self.learningRate*(self.secondLayerWeightsSummations[i][j]+self.Lambda*self.secondLayerWeights[i][j])
        for i in range (self.dimensions[1]):
            for j in range (self.dimensions[0]):
                if j == 0:
                    self.firstLayerBiases[i] -= self.learningRate*self.firstLayerBiasesSummations[i]
                self.firstLayerWeights[i][j] -= self.learningRate*(self.firstLayerWeightsSummations[i][j]+self.Lambda*self.firstLayerWeights[i][j])

        self.firstLayerSummations = np.zeros([self.dimensions[1], self.dimensions[0]])
        self.secondLayerSummations = np.zeros([self.dimensions[2], self.dimensions[1]])
        self.firstLayerBiasesSummations = np.zeros(self.dimensions[1])
        self.secondLayerBiasesSummations = np.zeros(self.dimensions[2])
        self.correct = 0
            
    def predict(self, testImage):
        secondLayerAnsNodes = np.empty([self.dimensions[1]])
        outputAns = np.empty([self.dimensions[2]])
        for i in range (self.dimensions[1]):
            secondLayerAnsNodes[i] = self.sigmoid(np.dot(self.firstLayerWeights[i], testImage)+self.firstLayerBiases[i])
        for i in range (self.dimensions[2]):
            outputAns[i] = self.sigmoid(np.dot(self.secondLayerWeights[i], secondLayerAnsNodes)+self.secondLayerBiases[i])
        return np.argmax(outputAns)

if __name__ == "__main__":

    (train_images, train_labels), (test_images, test_labels) = mnist.load_data()
    train_images = train_images/255.0
    test_images = test_images/255.0

    neural_network = NeuralNetwork([784, 16, 10])

    start_time = time.time()
    for i in range (neural_network.epochs):
        print ("\nEpoch", str(i+1) + "/" + str(neural_network.epochs))
        neural_network.y[i]=neural_network.train(train_images, train_labels)
    time = time.time() - start_time

    plt.plot(neural_network.x, neural_network.y, 'b')
    plt.ylabel('Error Change')
    plt.xlabel('Epochs')
    plt.show()

    print("\n\n\nTotal Time Used")
    if time/60 < 60:
        print("Minutes: %s" % round((time/60),2))
    else:
        print("Seconds: %s" % round(time,2))

    for i in range (neural_network.num_predictions):
        prediction = neural_network.predict(test_images[i].flatten())
        plt.grid(False)
        plt.imshow(test_images[i], cmap=plt.cm.binary)
        plt.title("Prediction: " + str(prediction) + " -- Actual: " + class_names[test_labels[i]] + "\n" + str(i+1) + "/" + str(neural_network.num_predictions))
        plt.show()

По какой-то причине этот код не работает с более сложными задачами. Ошибка не сводится к минимуму, а точность остается прежней. Этот точный код работает для проблемы xor и другой похожей на нее. Когда я пытаюсь дать ему набор данных MNIST, он не работает. Единственная разница в том, что на каждом слое больше узлов, алгоритм тот же.

В чем может быть проблема?

Это график после прохождения 20 эпох со скоростью обучения 0,000001 и лямбда 10. Он показывает ошибку на эпоху. Метка y должна указывать на ошибку, а не на изменение ошибки. https://i.stack.imgur.com/fLXzz.png

1 Ответ

0 голосов
/ 08 августа 2020

В вашей реализации нет ничего технически неправильного. Однако следует отметить несколько моментов, все из которых существенно влияют на производительность, которую вы видите. Это длинный ответ, но каждая часть отражает важное изменение, которое я внес в ваш код, чтобы он работал должным образом, поэтому внимательно прочтите его.

Во-первых, вам не следует инициализировать свои веса в (0 , 1), что и делает np.random.randn по умолчанию. В частности, если вы собираетесь выбрать одинаковые случайные веса, равномерное распределение должно быть центрировано на нуле. Например, выберите случайные числа в диапазоне (-1, 1) или (-.1, .1). В противном случае ваш MLP сразу окажется необъективным; многие нейроны скрытого слоя сразу же будут отображаться почти на 1 через активацию сигмовидной кишки. В конце концов, активация сигмоида центрируется (по оси x) на нуле, поэтому ваши входы по умолчанию также должны быть. Эта проблема может очень легко помешать вашему MLP вообще сойтись (и, по сути, это так в вашем случае). Есть лучшие методы инициализации веса, чем выборка из равномерного случайного распределения, но это не означает, что этот метод не будет работать, если все сделано правильно.

Во-вторых, вам, вероятно, следует нормализовать данные изображения. Нейронные сети плохо справляются с вводом от 0 до 255, поэтому данные изображения по умолчанию экспортируются из keras. Вы можете исправить это, просто разделив каждую входную функцию на 255. Причина этого в том, что сигмоидальная кривая имеет очень маленькие производные в субдоменах с высокой величиной. Другими словами, когда x очень велик или очень мало (очень отрицательно), производная сигмоида (x) по x очень близка к нулю. Когда вы умножаете некоторые веса на очень большие значения (например, 255), вы, скорее всего, сразу же попадете в область высоких значений сигмовидной кривой. Это не обязательно предотвратит схождение вашего net, но определенно замедлит его вначале, так как небольшие производные приводят к небольшим градиентам, что, в свою очередь, приводит к небольшим обновлениям веса. Вы можете увеличить скорость обучения, но это может привести к выходу нейронной сети за пределы (и, возможно, расхождению), как только она выйдет из областей с низкой производной сигмовидной кривой. Опять же, я протестировал (и исправил) эту проблему в вашей программе c, и она действительно имеет существенное значение (окончательная точность около 0,8 по сравнению с 0,6).

Далее, ваш способ вычисление «ошибки» немного странно. Он вычисляет самую большую ошибку за всю эпоху и печатает ее. Самая большая ошибка эпохи вряд ли может быть полезной метрикой ошибки; даже очень хорошо спроектированная, хорошо обученная глубокая сверточная нейронная сеть иногда будет плохо работать хотя бы на одной точке данных в течение всей эпохи. Ваша мера точности, вероятно, достаточно прилична, чтобы оценить, насколько хорошо ваша модель сходится. Однако я также добавил «среднюю ошибку», просто адаптировав ваш текущий расчет ошибки. Поскольку вы используете потерю перекрестной энтропии (по крайней мере, это верно, учитывая ваш метод расчета градиентов), я бы рекомендовал написать функцию, которая на самом деле вычисляет потерю перекрестной энтропии (сумма отрицательных логарифмических вероятностей в ваш случай). При интерпретации такой потери имейте в виду, что отрицательная логарифмическая вероятность по сигмоиду ограничена (0, бесконечность), и, следовательно, потеря перекрестной энтропии также ограничена.

Конечно, другой проблемой является скорость обучения . Фактически, большинство будет утверждать, что скорость обучения - самый важный гиперпараметр, который нужно настраивать. В итоге я использовал 0.00001, хотя я не делал особого поиска по сетке.

Далее вы используете полное пакетное обучение. Это означает, что вы вычисляете сумму градиентов по каждой отдельной точке данных, а затем обновляете свои веса один раз. Другими словами, вы выполняете только одно обновление веса за эпоху. Если это так, вам придется прожить много эпох, чтобы получить достойные результаты. Если у вас есть время и вычислительные ресурсы, это, вероятно, нормально. Однако, если вы этого не сделаете, вы можете рассмотреть возможность мини-партии. Мини-пакет по-прежнему довольно устойчив к порядку выборки (хотя теоретически вы все равно должны перетасовать данные для каждой эпохи), по крайней мере, по сравнению с онлайн / сточасти c обучением. Он включает в себя разделение вашего полного набора данных на «партии» некоторого заранее определенного размера. Для каждого пакета вы вычисляете сумму градиентов модели по каждой точке данных в пакете. Затем вы обновляете вес (позвонив по номеру change()). После того, как вы просмотрели каждую партию, это составляет одну эпоху. Я использовал мини-пакет и размер пакета 1000.

Наконец (и я хочу сказать самое главное, но другие вещи, которые я упомянул, также препятствуют сходимости), вы не тренируетесь на всех обучающих данных ( 8,000 / 60,000); вы не тренируетесь в течение достаточного количества эпох (5 вряд ли будет достаточно, особенно когда вы тренируетесь только на части данных); и ваша модель, вероятно, слишком проста (недостаточно узлов скрытого слоя). Однако главная проблема заключается в том, что реализация не всегда использует векторизованные операции, когда это необходимо, и, таким образом, обучение на всех обучающих данных с достаточным количеством эпох и сложностью модели происходит слишком медленно.

Я обновил вашу реализацию (особенно backprop() и change()), чтобы по возможности использовать векторизованные операции numpy. Это на несколько порядков ускорило внедрение. Однако я не верю, что это вообще изменило семантику вашего кода. Я также реализовал другие изменения, которые предлагал в этом посте. В среднем я получаю около 85% точности обучения (хотя она варьируется +/- 6% в зависимости от партии) всего за 20 эпох и только 32 скрытых узла в скрытом слое. Я не запускал его с тестовым набором, поэтому я не стал вмешиваться и с параметром регуляризации (я просто установил Lambda на ноль). Вот обновленный код (для краткости я отредактировал части, такие как функция predict()):

import numpy as np
from tensorflow.keras.datasets import mnist

class_names = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

class NeuralNetwork():

    correct = 0
    epochs = 20
    Lambda = 0
    learningRate = 0.00001

    def __init__(self, sizes, batchSize):
        self.batchSize = batchSize
        self.dimensions = sizes

        self.secondLayerNeurons = np.empty(sizes[1])
        self.outputNeurons = np.empty(sizes[2])

        # Draw weights and biases from (-1, 1) by multiplying the (0, 1)
        # values by 2 and subtracting 1. There are better ways of doing this,
        # but this works just fine.
        self.firstLayerWeights = np.random.rand(sizes[1], sizes[0]) * 2 - 1
        self.secondLayerWeights = np.random.rand(sizes[2], sizes[1]) * 2 - 1
        self.firstLayerBiases = np.random.rand(sizes[1]) * 2 - 1
        self.secondLayerBiases = np.random.rand(sizes[2]) * 2 - 1

        self.firstLayerWeightsSummations = np.zeros([sizes[1], sizes[0]])
        self.secondLayerWeightsSummations = np.zeros([sizes[2], sizes[1]])
        self.firstLayerBiasesSummations = np.zeros([sizes[1]])
        self.secondLayerBiasesSummations = np.zeros([sizes[2]])

        self.hiddenLayerErrors = np.empty(sizes[1])
        self.outputLayerErrors = np.empty(sizes[2])

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

    def sigmoidDerivative(self, x):
        return np.multiply(x,(1-x))


    def forwardProp(self, inputs):
        for i in range (self.dimensions[1]):
            self.secondLayerNeurons[i] = self.sigmoid(np.dot(self.firstLayerWeights[i], inputs)+self.firstLayerBiases[i])
        for i in range (self.dimensions[2]):
            self.outputNeurons[i] = self.sigmoid(np.dot(self.secondLayerWeights[i], self.secondLayerNeurons)+self.secondLayerBiases[i])

    def backProp(self, inputs, correct_output):
        self.outputLayerErrors = np.subtract(self.outputNeurons, correct_output)
        self.hiddenLayerErrors = np.multiply(np.dot(self.secondLayerWeights.T, self.outputLayerErrors), self.sigmoidDerivative(self.secondLayerNeurons))

        self.secondLayerBiasesSummations += self.outputLayerErrors
        self.secondLayerWeightsSummations += np.outer(self.outputLayerErrors, self.secondLayerNeurons)

        self.firstLayerBiasesSummations += self.hiddenLayerErrors
        self.firstLayerWeightsSummations += np.outer(self.hiddenLayerErrors, inputs)

    def train(self, trainImages, trainLabels):
        size = str(self.batchSize)
        err_sum = 0.0
        err_count = 0
        avg_err = 0.0

        for m in range (self.batchSize):
            correct_output = np.zeros([self.dimensions[2]])
            correct_output[trainLabels[m]] = 1.0

            self.forwardProp(trainImages[m].flatten())
            self.backProp(trainImages[m].flatten(), correct_output)

            if np.argmax(self.outputNeurons) == int(trainLabels[m]):
                self.correct+=1

            if m%150 == 0:
                error = np.amax(np.absolute(self.outputLayerErrors))
                err_sum += error
                err_count += 1
                avg_err = err_sum / err_count
                accuracy = str(int((self.correct/(m+1))*100)) + '%'
                percent = str(int((m/self.batchSize)*100)) + '%'
                print ("Progress: " + percent + " -- Accuracy: " + accuracy + " -- Error: " + str(avg_err), end="\r")

        self.change()
        print (size + '/' + size + " -- " + " -- Accuracy: " + accuracy + " -- Error: " + str(avg_err), end="\r")
        self.correct = 0

    def change(self):

        self.secondLayerBiases -= self.learningRate * self.secondLayerBiasesSummations
        self.secondLayerWeights -= self.learningRate * self.secondLayerWeightsSummations
        self.firstLayerBiases -= self.learningRate * self.firstLayerBiasesSummations
        self.firstLayerWeights -= self.learningRate * self.firstLayerWeightsSummations

        self.firstLayerSummations = np.zeros([self.dimensions[1], self.dimensions[0]])
        self.secondLayerSummations = np.zeros([self.dimensions[2], self.dimensions[1]])
        self.firstLayerBiasesSummations = np.zeros(self.dimensions[1])
        self.secondLayerBiasesSummations = np.zeros(self.dimensions[2])

if __name__ == "__main__":

    (train_images, train_labels), (test_images, test_labels) = mnist.load_data()
    train_images = train_images / 255 # Normalize image data

    num_using = 60000 # Amount of data points to use. It's fast now, so we may as well use the full 60,000
    bs = 1000 # Batch size. 60,000 is full batch. Consider trying mini-batch
    neural_network = NeuralNetwork([784, 32, 10], bs)

    for i in range (neural_network.epochs):
        print ("\nEpoch", str(i+1) + "/" + str(neural_network.epochs))
        for j in range(int(num_using / bs)):
            print("Batch", str(j+1) + "/" + str(int(60000 / bs)))
            neural_network.train(train_images[int(j * bs):int(j * bs) + bs], train_labels[int(j * bs):int(j * bs) + bs])

Для дальнейших улучшений, требующих минимальных усилий, я бы предложил попробовать еще больше скрытых узлов (возможно, даже 128), далее настраивая скорость обучения и параметр регуляризации, пробуя разные размеры пакетов и настраивая количество эпох.

Дайте мне знать, если у вас есть какие-либо вопросы.

...