В вашей реализации нет ничего технически неправильного. Однако следует отметить несколько моментов, все из которых существенно влияют на производительность, которую вы видите. Это длинный ответ, но каждая часть отражает важное изменение, которое я внес в ваш код, чтобы он работал должным образом, поэтому внимательно прочтите его.
Во-первых, вам не следует инициализировать свои веса в (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), далее настраивая скорость обучения и параметр регуляризации, пробуя разные размеры пакетов и настраивая количество эпох.
Дайте мне знать, если у вас есть какие-либо вопросы.