На этом шаге мы рассмотрим особенности реализации такой тренировки.
Приступим к решению несколько более сложной задачи тренировки сети. Ее можно разделить на две части.
Сначала запишем готовую первую часть.
# тренировка нейронной сети 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, но фактически это закономерный результат нашего упорного труда, вложенного в упрощение того, что легко могло стать сложным и приобрести устрашающий вид.
На следующем шаге мы приведем полный код нейронной сети.