На этом шаге мы рассмотрим реализацию передачи сигналов между слоями.
Казалось бы, логично начать с разработки кода, предназначенного для тренировки нейронной сети, заполнив инструкциями функцию train(), которая на данном этапе пуста, но мы отложим ее на время и займемся более простой функцией query(). Благодаря этому мы постепенно приобретем уверенность в своих силах и получим опыт в использовании как языка Python, так и матриц весовых коэффициентов в объекте нейронной сети.
Функция query() принимает в качестве аргумента входные данные нейронной сети и возвращает ее выходные данные. Все довольно просто, но вспомните, что для этого нам нужно передать сигналы от узлов входного слоя через скрытый слой к узлам выходного слоя для получения выходных данных. При этом, как вы помните, по мере распространения сигналов мы должны сглаживать их, используя весовые коэффициенты связей между соответствующими узлами, а также применять сигмоиду для уменьшения выходных сигналов узлов.
В случае большого количества узлов написание для каждого из них кода на Python, осуществляющего сглаживание весовых коэффициентов, суммирование сигналов и применение к ним сигмоиды, превратилось бы в сплошной кошмар.
К счастью, нам не нужно писать детальный код для каждого узла, поскольку мы уже знаем, как записать подобные инструкции в простой и компактной матричной форме. Ниже показано, как можно получить входящие сигналы для узлов скрытого слоя путем сочетания матрицы весовых коэффициентов связей между входным и скрытым слоями с матрицей входных сигналов:
Xскрытый = Wвходной_скрытый * I
В этом выражении замечательно не только то, что в силу его краткости нам легче его записать, но и то, что такие компьютерные языки, как Python, распознают матрицы и эффективно выполняют все расчеты, поскольку им известно об однотипности всех стоящих за этим вычислений.
Вы будете удивлены простотой соответствующего кода на языке Python. Ниже представлена инструкция, которая показывает, как применить функцию скалярного произведения библиотеки numpy к матрицам весов и входных сигналов:
hidden_inputs = numpy.dot(self.wih, inputs)
Вот и все!
Эта короткая строка кода Python выполняет всю работу по объединению всех входных сигналов с соответствующими весами для получения матрицы сглаженных комбинированных сигналов в каждом узле скрытого слоя. Более того, нам не придется ее переписывать, если в следующий раз мы решим использовать входной или скрытый слой с другим количеством узлов. Этот код все равно будет работать!
Именно эта мощь и элегантность матричного подхода являются причиной того, что перед этим мы не пожалели потратить время и усилия на его рассмотрение.
Для получения выходных сигналов скрытого слоя мы просто применяем к каждому из них сигмоиду:
Оскрытый = сигмоида(Xскрытый)
Это не должно вызвать никаких затруднений, особенно если сигмоида уже определена в какой-нибудь библиотеке Python. Оказывается, так оно и есть! Библиотека scipy в Python содержит набор специальных функций, в том числе сигмоиду, которая называется expit(). Библиотека scipy импортируется точно так же, как и библиотека numpy.
# библиотека scipy.special содержит сигмоиду expit() import scipy.special
Поскольку в будущем мы можем захотеть поэкспериментировать с функцией активации, настроив ее параметры или полностью заменив другой функцией, лучше определить ее один раз в объекте нейронной сети во время его инициализации. После этого мы сможем неоднократно ссылаться на нее, точно так же, как на функцию query(). Такая организация программы означает, что в случае внесения изменений нам придется сделать это только в одном месте, а не везде в коде, где используется функция активации.
Ниже приведен код, определяющий функцию активации, который мы используем в разделе инициализации нейронной сети.
# использование сигмоиды в качестве функции активации self.activation_function = lambda x: scipy.special.expit(x)
Что делает этот код? Он не похож ни на что, с чем мы прежде сталкивались. Что это за lambda? Не стоит пугаться, здесь нет ничего страшного. Все, что мы сделали, - это создали функцию наподобие любой другой, только с использованием более короткого способа записи, называемого лямбда-выражением. Вместо привычного определения функции в форме def имя() мы использовали волшебное слово lambda, которое позволяет создавать функции быстрым и удобным способом, что называется, "на лету". В данном случае функция принимает аргумент х и возвращает scipy.special.expit(), а это есть не что иное, как сигмоида. Функции, создаваемые с помощью лямбда-выражений, являются безымянными или, как предпочитают говорить опытные программисты, анонимными, но данной функции мы присвоили имя self.activation_function(). Это означает, что всякий раз, когда потребуется использовать функцию активации, ее нужно будет вызвать как self.activation_function().
Итак, возвращаясь к нашей задаче, мы применим функцию активации к сглаженным комбинированным входящим сигналам, поступающим на скрытые узлы. Соответствующий код совсем не сложен.
# рассчитать исходящие сигналы для скрытого слоя hidden_outputs = self.activation_function(hidden_inputs)
Таким образом, сигналы, исходящие из скрытого слоя, описываются матрицей hidden_outputs.
Мы прошли промежуточный скрытый слой, а как быть с последним, выходным слоем? В действительности распространение сигнала от скрытого слоя до выходного ничем принципиально не отличается от предыдущего случая, поэтому способ расчета остается тем же, а значит, и код будет аналогичен предыдущему.
Ниже приведен итоговый фрагмент кода, объединяющий расчеты сигналов скрытого и выходного слоев.
# рассчитать входящие сигналы для скрытого слоя 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)
Если отбросить комментарии, здесь всего четыре строки кода, которые выполняют все необходимые расчеты: две - для скрытого слоя и две - для выходного слоя.
На следующем шаге мы подведем некоторые итоги.