Глубокое Обучение Для Java Инженеров
  • Предисловие
  • Содержание
  • 1: Вступление
  • 2: Первая нейронная сеть
  • 3: Как обучить нейронную сеть?
Powered by GitBook
On this page
  • Глава 2: Наша первая Нейронная Сеть
  • Что такое Нейрон
  • Наш Первый Нейрон
  • Links

2: Первая нейронная сеть

Previous1: ВступлениеNext3: Как обучить нейронную сеть?

Last updated 7 years ago

Глава 2: Наша первая Нейронная Сеть

Что такое Нейрон

Сперва, давайте начнем с ответа на простой вопрос: Что такое Нейрон? Давайте рассмотрим формальное определение:

Нейрон, или нейрон, или нервная клетка - электрически возбудимая клетка, которая принимает, обрабатывает и передает информацию при помощи электрических и химических сигналов [1]

Как вы могли заметить, у Нейрона есть три основные функции:

  • Прием сигналов

  • Обработка сигналов

  • Передача результата после обработки

Эти три функции отлично сочетаются с тремя ключевыми компонентами настоящего Нейрона, посмотрите на следующий рисунок Нейрона:

Эти три компонента один к одному соответствуют основным функциям, исполняющимся Нейроном::

  • Дендриты - получает сигнал и передает его в тело клетки (нейрона)

  • Сома (тело) - обрабатывает сигнал (body)

  • Аксон - посылает сигнал другим нейронам

Теперь мы можем воссоздать математическую модель Нейрона:

Веса – каждый вес отражает дендрит и отвечает за прием сигнала из окружения. Поскольку у каждого дендрита есть свой размер (форма), существует некое отклонение между сигналом, передающимся дендриту, и сигналом, передающимся от дендрита в сому. Потому нам и нужен вес для каждой связи. Вес, по сути, говорит нам, как дендрит влияет на сигнал. Как только все сигналы собираются и перемножаются с весом, они суммируются и передаются в сому.

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

Наш Первый Нейрон

Со всеми полученными знаниями мы наконец готовы приступить к написанию кода.

Весь обсуждаемый код можно найти в следующем бранче: https://github.com/DeepJavaUniverse/DJ-core/tree/chapter_2

Теперь давайте на самом деле разберем пример: случай с вечеринкой. Смоделируем его с помощью ручки и бумаги. 3 входных нейрона, 1 выходной с очень простой сигмоидальной функцией для активации.

public interface Neuron {

     /**
     * Должен быть вызван, когда Нейрон получает входной сигнал от связанного
     * нейрона.
     * К примеру, давайте рассмотрим следующую сеть:
     * NeuronA
     *         \
     *          \
     *           \
     * NeuronB --- NeuronD
     *           /
     *          /
     *         /
     * NeuronC
     *
     * Если NeuronA или NeuronB или NeuronC посылают сигнал в NeuronD, метод
     * должен быть вызван.
     *
     * @param from - Нейрон, который посылает сигнал.
     */
    void forwardSignalReceived(Neuron from, Double value);

    default void connect(Neuron neuron, Double weight) {
        this.addForwardConnection(neuron);
        neuron.addBackwardConnection(this, weight);
    }

    void addForwardConnection(Neuron neuron);

    void addBackwardConnection(Neuron neuron, Double weight);
}

Чтобы сделать сеть нам необходимо обозначить 2 типа нейронов:

  • Входные нейроны;

  • Связанные нейроны;

Входной нейрон:

public class InputNeuron implements Neuron {

    private Set<Neuron> connections = new HashSet<>();

    @Override
    public void forwardSignalReceived(final Neuron from, final Double value) {
        connections.forEach(n -> n.forwardSignalReceived(this, value));
    }

    @Override
    public void addForwardConnection(final Neuron neuron) {
        connections.add(neuron);
    }

    @Override
    public void addBackwardConnection(final Neuron neuron, final Double weight) { } // No op
}

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

  • Интерфейс для Функции Активации;

  • Нашу первую простейшую Функцию Активации;

Интерфейс будет простым:

public interface ActivationFunction {

    Double forward(final Double x);
}

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

public class StepFunction implements ActivationFunction {

    public Double forward(Double x) {
        return x >= 1. ? 1. : 0.;
    }
}

Если вы построите график функции, то он будет выглядеть так:

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

    private final ActivationFunction activationFunction;

    /**
     * Представляет соединения от нейрона к нейронам, от которых он получает 
     * сигналы. К примеру, в следующей сети:
     * NeuronA ___
     *            \ weight1 = -0.1
     *             \
     * weight2 = 0.1\
     * NeuronB ------ NeuronD
     *              /
     *             /
     *            /  weight3 = 0.8
     * NeuronC ---
     * backwardConnections map будет выглядеть так:
     * NeuronA => -0.1
     * NeuronB => 0.1
     * NeuronC => 0.8
     */
    private final Map<Neuron, Double> backwardConnections = new HashMap<>();

    /**
     * Представляет набор Нейронов, которым текущий нейрон посылает сигнал, здесь 
     * нет необходимости в весах.
     */
    private final Set<Neuron> forwardConnections = new HashSet<>();

    /**
     * inputSignals используется для хранения сигналов от других Нейронов. Ключи в
     * этом словаре должны быть абсолютно идентичны ключам в 
     * {@link #backwardConnections}. Как только все сигналы получены Нейроном,
     * он может начинать обрабатывать их.   
     */
    private final Map<Neuron, Double> inputSignals = new HashMap<>();

    /**
     * Количество сигналов, уже полученных.
     * Как только это количество достигнет размера {@link #inputSignals} словаря,
     * нейрон готов к обработке входных сигналов и отправке сигнала дальше.   
     */
    private volatile int signalReceived;
    private final double bias;

    /**
     * Хранит результаты последнего отправленного сигнала от Нейрона к другим 
     * Нейронам. Это в основном необходимо для выходных Нейронов,
     * поскольку у них не хватает других Нейронов для отсылки сигнала, и в то же 
     * время должна быть возможность получить это значение.
     */
    private volatile double forwardResult;

К этому моменту должно стать очевидно, как работает добавление связей:

    @Override
    public void addForwardConnection(final Neuron neuron) {
        forwardConnections.add(neuron);
    }

    @Override
    public void addBackwardConnection(final Neuron neuron, final Double weight) {
        backwardConnections.put(neuron, weight);
        inputSignals.put(neuron, Double.NaN);
    }

И главная функция, делающая всю магию:

    @Override
    public void forwardSignalReceived(final Neuron from, final Double value) {
        signalReceived++;
        inputSignals.put(from, value);
        // Следующий if – проверка на то, являлся ли текущий сигнал последним 
        // оставшимся для получения. Если это так и все входные сигналы были
        // получены, Нейрон может начинать их обработку и испускать сигнал сам
        if (backwardConnections.size() == signalReceived) {
            // Нейрону для обработки входных сигналов необходимо 4 шага:
            // 1. Посчитать input = W * X + b
            // 2. Посчитать output = f(input), где f – функция активации
            // 3. Отправить output другим нейронам
            // 4. Аннулировать состояние

            // Шаг #1
            // Посчитать W * X + b – сумма всех входных сигналов, умноженных на
            // соответствующий ему вес.
            // Отклонение (bias) добавляется в конце.
            double forwardInputToActivationFunction
                    = backwardConnections
                        .entrySet()
                        .stream()
                        .mapToDouble(connection ->                                
                                // inputSignals хранит сигнал, где 
                                // connection.getValue() дает вам
                                // вес, на который должен умножаться сам сигнал.
                                // Таким образом это и есть X * W.
                                inputSignals.get(connection.getKey())
                                        * connection.getValue())
                        .sum() + bias;

            // Шаг #2
            double signalToSend
                    = activationFunction.forward(
                            forwardInputToActivationFunction);
            forwardResult = signalToSend;

            // Шаг #3 Поскольку сигнал посчитан, теперь мы можем отослать 
            // другим нейронам
            forwardConnections
                    .stream()
                    .forEach(connection ->
                        connection
                                .forwardSignalReceived(
                                        ConnectedNeuron.this,
                                        signalToSend)
                    );
            // Шаг #4
            signalReceived = 0;
        }
    }

Одна вещь, которую мы опускаем – Builder. Поскольку паттерн builder не имеет ничего общего с DeepLearning мы просто предположим, что Builder существует для нашего класса, но мы не показываем фрагмент кода здесь.

Сейчас мы можем создать нашу первую нейронную сеть (Neural Network, NN). Для начала давайте опишем нейронную сеть, которую мы собираемся построить. Наша первая NN будет предсказывать, нужно ли идти на вечеринку или нет. Для построения сети у нас должны быть данные для обучения сети. Поскольку мы не знаем, как тренировать нашу сеть, мы будем делать это вручную, наблюдая за шаблонами в наших данных и пытаясь подобрать такие веса, которые позволят сети делать верные предсказания. Итак, здесь есть то, что мы знаем о факторах, влияющих на опыт:

  1. Факт того, будет ли наш лучший друг на вечеринке или нет;

  2. Наличие любимого напитка (давайте назовем его "Vodka" :) );

  3. Погода (солнечно или нет);

Сейчас мы можем создать комбинацию всех этих факторов и желаемое поведение для каждого из них. Чтобы сделать изображение более простым, мы используем 1 или 0, чтобы представить каждый фактор. 1 будет обозначать присутствие фактора, а 0 – его отсутствие. Так как мы говорим о трех факторах, входом для нашей NN будет 3 цифры (1 или 0 каждая), например, следующий массив отображает вход с информацией о том, что наш лучший друг будет на вечеринке, но не будет любимого напитка и погода будет не солнечная, в следствии чего мы не пойдем на вечеринку:

[1, 0, 0] => 0

Еще один пример с информацией об отсутствии лучшего на вечеринке, но будет напиток и солнечная погода на улице – нужно идти на вечеринку.

[0, 1, 1] => 1

Давайте построим все возможные комбинации данных и результатов:

[1, 0, 0] => 0 [0, 1, 0] => 0 [0, 0, 1] => 0 [1, 1, 0] => 1 [1, 0, 1] => 1 [0, 1, 1] => 1 [1, 1, 1] => 1

Как наша NN будет выглядеть? На самом деле, все просто. У нас будет 3 входных нейрона и 1 выходной нейрон. Пожалуй, можно сразу перейти к изображению NN, оно проще для понимания:

Эта сеть называется «полносвязная однослойная сеть». Звучит пугающе, я думаю, что многие люди в науке сознательно пытаются создать такое впечатление :) В любом случае, мы собираемся разделить название на части и последовательно обсудить. Полносвязная просто означает, что у каждого нейрона в слое есть связь с каждым нейроном предшествующего слоя. Слой – абстракция, которой не существует в настоящем мозге, просто упрощение, позволяющее инженерам проще изображать нейронные сети. Каждый слой – коллекция нейронов, у которых нет связи друг с другом. Нейроны в слое получают сигналы только от предшествующего слоя (если такой существует) и посылают сигналы в следующий слой (если такой существует). Глядя на нашу картинку, кто-то может подумать, что это двуслойная сеть. Исторически так сложилось, что входной слой не считают (или, может быть, мы считаем от 0, тогда это имеет смысл). Если мы не считаем входной слой, эта сеть, безусловно, однослойная.

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

    // Создаем три входных нейрона
    InputNeuron inputFriend = new InputNeuron();
    InputNeuron inputVodka = new InputNeuron();
    InputNeuron inputSunny = new InputNeuron();
    // Наш выходной нейрон
    ConnectedNeuron outputNeuron
                // Мы не обсудили builder, впрочем, это простейший Java builder, 
                // если необходимо, то вы можете посмотреть реализацию 
                // в нашем репозитории.
                = new ConnectedNeuron.Builder()
                    .bias(bias)
                    .activationFunction(new StepFunction())
                    .build();

    // Создаем связи между нейронами
    inputFriend.connect(outputNeuron, 0.5);
    inputVodka.connect(outputNeuron, 0.5);
    inputSunny.connect(outputNeuron, 0.5);

    // Посылаем сигналы в сеть
    inputFriend.forwardSignalReceived(null, 1.);
    inputVodka.forwardSignalReceived(null, 1.);
    inputSunny.forwardSignalReceived(null, 0.);

    // Получаем результат и печатаем его:
    double result = outputNeuron.getForwardResult();
    System.out.printf("Prediction: %3f\n", result)

Links

[1] https://en.wikipedia.org/wiki/Neuron