Шаг 51.
Основы создания нейросети на Python. Создаем нейронную сеть на Python. Проект нейронной сети на Python. Тренировка сети

    На этом шаге мы рассмотрим особенности реализации такой тренировки.

    Приступим к решению несколько более сложной задачи тренировки сети. Ее можно разделить на две части.

    Сначала запишем готовую первую часть.

# тренировка нейронной сети
def train(self, inputs_list, targets_list):
    # преобразовать список входных значений в двухмерный массив 
    inputs = numpy.array(inputs_list, ndmin=2).T
    targets = numpy.array(targets_list, ndmin=2).T

    # рассчитать входящие сигналы для скрытого слоя 
    hidden_inputs = numpy.dot(self.wih, inputs)
    # рассчитать исходящие сигналы для скрытого слоя 
    hidden_outputs = self.activation_function(hidden_inputs)

    # рассчитать входящие сигналы для выходного слоя 
    final_inputs = numpy.dot(self.who, hidden_outputs)
    # рассчитать исходящие сигналы для выходного слоя 
    final_outputs = self.activation_function (final_inputs)

    Этот код почти совпадает с кодом функции query(), поскольку процесс передачи сигнала от входного слоя к выходному остается одним и тем же.

    Единственным отличием является введение дополнительного параметра targets_list, передаваемого при вызове функции, поскольку невозможно тренировать сеть без предоставления ей тренировочных примеров, которые включают желаемые или целевые значения:

def train(self, inputs_list, targets_list):

    Список targets_list преобразуется в массив точно так же, как список input_list:

    targets = numpy.array(targets_list, ndmin=2).T

    Теперь мы очень близки к решению основной задачи тренировки сети - уточнению весов на основе расхождения между расчетными и целевыми значениями.

    Будем решать эту задачу поэтапно.

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

# ошибка = целевое значение - фактическое значение 
output_errors = targets - final_outputs

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

    ошибкискрытый = весаTскрытый_выходной * ошибкивыходной

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

# ошибки скрытого слоя - это ошибки output_errors,
# распределенные пропорционально весовым коэффициентам связей 
# и рекомбинированные на скрытых узлах
hidden_errors = numpy.dot(self.who.T, output_errors)

    Итак, мы получили то, что нам необходимо для уточнения весовых коэффициентов в каждом слое. Для весов связей между скрытым и выходным слоями мы используем переменную output_errors.

    Для весов связей между входным и скрытым слоями мы используем только что рассчитанную переменную hidden_errors.

    Ранее нами было получено выражение для обновления веса связи между узлом j и узлом k следующего слоя в матричной форме.

    Величина α - это коэффициент обучения, a сигмоида - это функция активации, с которой вы уже знакомы. Вспомните, что символ "*" означает обычное поэлементное умножение, а символ "·" - скалярное произведение матриц. Последний член выражения - это транспонированная (T) матрица исходящих сигналов предыдущего слоя. В данном случае транспонирование означает преобразование столбца выходных сигналов в строку.

    Это выражение легко транслируется в код на языке Python. Сначала запишем код для обновления весов связей между скрытым и выходным слоями.

# обновить весовые коэффициенты связей между скрытым и выходным слоями 
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), 
       numpy.transpose(hidden_outputs))

    Это довольно длинная строка кода, но цветовое выделение поможет вам разобраться в том, как она связана с приведенным выше математическим выражением. Коэффициент обучения self.lr просто умножается на остальную часть выражения. Есть еще матричное умножение, выполняемое с помощью функции numpy.dot(), и два элемента, выделенных синим и красным цветами, которые отображают части, относящиеся к ошибке и сигмоидам из следующего слоя, а также транспонированная матрица исходящих сигналов предыдущего слоя.

    Операция += означает увеличение переменной, указанной слева от знака равенства, на значение, указанное справа от него. Поэтому х += 3 означает, что х увеличивается на 3. Это просто сокращенная запись инструкции х = х + 3. Аналогичный способ записи допускается и для других арифметических операций. Например, х /= 3 означает деление х на 3.

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

# обновить весовые коэффициенты связей между скрытым и выходным слоями 
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), 
   numpy.transpose(hidden_outputs))
# обновить весовые коэффициенты связей между входным и скрытым слоями 
self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), 
   numpy.transpose(inputs))

    Вот и все!

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

    На следующем шаге мы приведем полный код нейронной сети.




Предыдущий шаг Содержание Следующий шаг