Шаг 92.
Задачи ComputerScience на Python.
Простейшие нейронные сети. Построение сети. Реализация сети

    На этом шаге мы рассмотрим реализацию сети.

    Сама сеть хранит только один элемент состояния - слои, которыми она управляет. Класс Network отвечает за инициализацию составляющих его слоев.

    Метод __init__() принимает список элементов типа int, описывающий структуру сети. Например, список [2, 4, 3] описывает сеть, имеющую два нейрона во входном слое, четыре нейрона - в скрытом и три нейрона - в выходном. Работая над этой простой сетью, предполагаем, что все слои сети используют одну и ту же функцию активации для своих нейронов и имеют одинаковую скорость обучения (файл network.ру).

from __future__ import annotations
from typing import List, Callable, TypeVar, Tuple
from functools import reduce
from layer import Layer
from util import sigmoid, derivative_sigmoid

T = TypeVar('T')  # тип выходных данных в интерпретации нейронной сети


class Network:
    def __init__(self, layer_structure: List[int], learning_rate: float,
                 activation_function: Callable[[float], float] = sigmoid,
                 derivative_activation_function: Callable[[float], float] = derivative_sigmoid) \
            -> None:
        if len(layer_structure) < 3:
            raise ValueError("Ошибка: должно быть, как минимум, 3 слоя "
                             "(1 входной, 1 скрытый, 1 выходной).")
        self.layers: List[Layer] = []
        # входной слой
        input_layer: Layer = Layer(None, layer_structure[0], learning_rate, activation_function,
                                   derivative_activation_function)
        self.layers.append(input_layer)
        # скрытые слои и выходной слой
        for previous, num_neurons in enumerate(layer_structure[1::]):
            next_layer = Layer(self.layers[previous], num_neurons, learning_rate, 
                               activation_function, derivative_activation_function)
            self.layers.append(next_layer)

    Выходные данные нейронной сети - это результат обработки сигналов, проходящих через все ее слои. Обратите внимание на то, как компактно метод reduce() используется в output() для многократной передачи сигналов между слоями по всей сети (файл network.ру).

    # Помещает входные данные на первый слой, затем выводит их
    # с первого слоя и подает на второй слой в качестве входных данных,
    # со второго - на третий и т. д.
    def outputs(self, input: List[float]) -> List[float]:
        return reduce(lambda inputs, layer: layer.outputs(inputs), self.layers, input)

    Метод backpropagate() отвечает за вычисление дельт для каждого нейрона в сети. В этом методе последовательно задействуются методы Layer calculate_ deltas_for_output_layer() и calculate_deltas_for_hidden_layer(). (Напомним, что при обратном распространении дельты вычисляются в обратном порядке.) Функция backpropagate() передает ожидаемые значения выходных данных для заданного набора входных данных в функцию calc_deltas_for_output_layer(). Эта функция использует ожидаемые значения, чтобы найти ошибку, с помощью которой вычисляется дельта (файл network.ру).

    # Определяет изменения каждого нейрона на основании ошибок
    # выходных данных по сравнению с ожидаемым выходом
    def backpropagate(self, expected: List[float]) -> None:
        # вычисление дельты для нейронов выходного слоя
        last_layer: int = len(self.layers) - 1
        self.layers[last_layer].calculate_deltas_for_output_layer(expected)
        # вычисление дельты для скрытых слоев в обратном порядке
        for l in range(last_layer - 1, 0, -1):
            self.layers[l].calculate_deltas_for_hidden_layer(self.layers[l + 1])

    Функция backpropagate() отвечает за вычисление всех дельт, но не изменяет веса элементов сети. Для этого после backpropagate() должна вызываться функция update_weights(), поскольку изменение веса зависит от дельт (файл network.ру). Этот метод вытекает непосредственно из формулы, представленной на рисунке 3 84 шага.

    # Сама функция backpropagate() не изменяет веса
    # Функция update_weights() использует дельты, вычисленные в backpropagate(),
    # чтобы действительно изменить веса
    def update_weights(self) -> None:
        for layer in self.layers[1:]:  # пропустить входной слой
            for neuron in layer.neurons:
                for w in range(len(neuron.weights)):
                    neuron.weights[w] = neuron.weights[w] + (
                                neuron.learning_rate * (
                                layer.previous_layer.output_cache[w]) * neuron.delta)

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

    Каждый набор входных данных пропускается через сеть, после чего ее веса обновляются посредством вызова backpropagate() для ожидаемого результата и последующего вызова update_weights(). Попробуйте добавить сюда код, который позволит вывести на печать частоту ошибок, когда через сеть проходит обучающий набор данных. Так вы сможете увидеть как постепенно уменьшается частота ошибок сети по мере ее продвижения вниз по склону в процессе градиентного спуска (файл network.ру).

    # Функция train() использует результаты выполнения функции outputs()
    # для нескольких входных данных, сравнивает их
    # с ожидаемыми результатами и передает полученное
    # в backpropagate() и update_weights()
    def train(self, inputs: List[List[float]], expecteds: List[List[float]]) -> None:
        for location, xs in enumerate(inputs):
            ys: List[float] = expecteds[location]
            outs: List[float] = self.outputs(xs)
            self.backpropagate(ys)
            self.update_weights()

    Наконец, после обучения сеть необходимо протестировать. Функция validate() принимает входные данные и ожидаемые выходные данные так же, как train(), но, в отличие от train(), использует их не для обучения, а для вычисления процента точности, так как предполагается, что сеть уже обучена. Функция validate() принимает также функцию interpret_output(), с помощью которой интерпретируются выходные данные нейронной сети для сравнения с ожидаемыми выходными данными. (Возможно, ожидаемый вывод - это не набор чисел с плавающей запятой, а строка типа "Amphibian".) Функция interpret_output() должна принимать числа с плавающей точкой, полученные в сети, и преобразовывать их в нечто сопоставимое с ожидаемыми выходными данными. Это специальная функция, созданная для конкретного набора данных. Функция validate() возвращает количество правильных классификаций, общее количество протестированных образцов и процент правильных классификаций (файл network.ру).

    # Для параметризованных результатов, которые требуют классификации,
    # зта функция возвращает правильное количество попыток
    # и процентное отношение по сравнению с общим количеством
    def validate(self, inputs: List[List[float]], expecteds: List[T], 
                 interpret_output: Callable[[List[float]], T]) -> \
            Tuple[int, int, float]:
        correct: int = 0
        for input, expected in zip(inputs, expecteds):
            result: T = interpret_output(self.outputs(input))
            if result == expected:
                correct += 1
        percentage: float = correct / len(inputs)
        return correct, len(inputs), percentage
Архив с файлом можно взять здесь.

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

    На следующем шаге мы поговорим о задачах классификации.




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