Нейронная сеть с обратным распространением ошибки java

Реализация нейронной сети на Java

Пример реализации простой нейронной сети, обучаемой по алгоритму обратного распространенния ошибки.

NNet package

Layer.java

Базовый интерфейс любого нейронного слоя. Размер слоя — количество нейронов содержащихся в слое и соотвественно размер выходного вектора слоя.

package nnet;
import java.io.Serializable;

/**
 * Интерфейс нейронного слоя
 */
public interface Layer extends Serializable {
    /**
     * Получает размер входного вектора
     * @return Размер входного вектора
     */
    int getInputSize();

    /**
     * Получает размер слоя
     * @return Размер слоя
     */
    int getSize();

    /**
     * Вычисляет отклик слоя
     * @param input Входной вектор
     * @return Выходной вектор
     */
    float[] computeOutput(float[] input);
}

Network.java

Реализация базовой функциональности нейронной сети, состоящей из нескольких слоев.

package nnet;

import java.io.*;

/**
 * Базовая реализация нейронной сети
 */
public class Network implements Serializable {
    /**
     * Конструирует нейронную сеть с заданными слоями
     * @param layers Нейронные слои
     */
    public Network(Layer[] layers) {
        // проверки
        if (layers == null || layers.length == 0) throw new IllegalArgumentException();
        // проверим детально
        final int size = layers.length;
        for (int i = 0; i < size; i++)
            if (layers[i] == null || (i > 1 && layers[i].getInputSize() != layers[i - 1].getSize()))
                throw new IllegalArgumentException();

        // запомним слои
        this.layers = layers;
    }

    /**
     * Получает размер входного вектора
     * @return Размер входного вектора
     */
    public final int getInputSize() {
        return layers[0].getInputSize();
    }

    /**
     * Получает размер выходного вектора
     * @return Размер выходного вектора
     */
    public final int getOutputSize() {
        return layers[layers.length - 1].getSize();
    }

    /**
     * Получает размер сети
     * @return Размер сети
     */
    public final int getSize() {
        return layers.length;
    }

    /**
     * Получает нейронный слой по индексу
     * @param index Индекс слоя
     * @return Нейронный слой
     */
    public final Layer getLayer(int index) {
        return layers[index];
    }

    /**
     * Вычисляет отклик сети
     * @param input Входной вектор
     * @return Выходной вектор
     */
    public float[] computeOutput(float[] input) {
        // проверки
        if (input == null || input.length != getInputSize())
                throw new IllegalArgumentException();

        // вычислим выходной отклик сети
        float[] output = input;
        final int size = layers.length;
        for (int i = 0; i < size; i++)
            output = layers[i].computeOutput(output);

        // вернем выход
        return output;
    }

    /**
     * Сохраняет нейронну сеть в файл
     * @param fileName Имя файла
     */
    public void saveToFile(String fileName) {
        // проверки
        if (fileName == null) throw new IllegalArgumentException();

        // сохраняем
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(fileName));
            outputStream.writeObject(this);
            outputStream.close();
        }
        catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Загружает нейронную сеть из файла
     * @param fileName Имя файла
     * @return Нейронную сеть
     */
    public static Network loadFromFile(String fileName) {
        // проверки
        if (fileName == null) throw new IllegalArgumentException();

        // загружаем
        Object network = null;
        try {
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(fileName));
            network = inputStream.readObject();
            inputStream.close();
        }
        catch (Exception e) {
            throw new IllegalArgumentException(e);
        }

        // отдадим сеть
        return (Network)network;
    }

    /**
     * Слои
     */
    private Layer[] layers;
}

BackpropLayer.java

Интерфейс нейронного слоя обучаемого по алгоритму обратного распространения ошибки. Метод randomize задает начальные случайные значения весов в слое. Метод computeBackwardError вычисляет ошибку в обратном направлении: то есть ту, которая пришла на вход слоя от предыдущего слоя. В качестве параметров computeBackwardError принимает входной вектор, который подавался на вход и вектор ошибки для этого слоя. Метод же adjust подгоняет веса нейроннов в сторону уменьшения ошибки.

package nnet;

/**
 * Интерфейс слоя обучаемого по алгоритму обратного распространения ошибки
 */
public interface BackpropLayer extends Layer {
    /**
     * Придает случайные значения весам нейронов
     * @param min Минимальное значение
     * @param max Максимальное значение
     */
    void randomize(float min,float max);

    /**
     * Выичисляет следующий вектор ошибки в обратном направлении
     * @param input Входной вектор
     * @param error Вектор ошибки
     * @return Следующий вектор ошибки в обратном направлении
     */
    float[] computeBackwardError(float[] input,float[] error);

    /**
     * Подгоняет веса нейронов в сторону уменьшения ошибки
     * @param input Входной вектор
     * @param error Вектор ошибки
     * @param rate Скорость обучения
     * @param momentum Моментум
     */
    void adjust(float[] input,float[] error,float rate,float momentum);
}

BackpropNetwork.java

Реализация нейронной сети, обучаемой по алгоритму обратного распространенния ошибки.

package nnet;

/**
 * Нейронная сеть обучаемая по алгоритму обратного распространения ошибки
 */
public final class BackpropNetwork extends Network {
    /**
     * Констрирует нейронную сеть с заданными слоями
     * @param layers
     */
    public BackpropNetwork(Layer[] layers) {
        // передадим родакам
        super(layers);
        // рандомизируем веса
        randomize(0,0.3f);
    }

    /**
     * Придает случайные значения весам нейроннов в сети
     * @param min
     * @param max
     */
    public void randomize(float min,float max) {
        // придаем случайные значения весам в сети
        final int size = getSize();
        for (int i = 0; i < size; i++) {
            Layer layer = getLayer(i);
            if (layer instanceof BackpropLayer) ((BackpropLayer)layer).randomize(min,max);
        }
    }

    /**
     * Обучает сеть паттерну
     * @param input Входной вектор
     * @param goal Заданный выходной вектор
     * @param rate Скорость обучения
     * @param momentum Моментум
     * @return Текущую ошибку обучения
     */
    public float learnPattern(float[] input,float[] goal,float rate,float momentum) {
        // проверки
        if (input == null || input.length != getInputSize() || 
                goal == null || goal.length != getOutputSize()) throw new IllegalArgumentException();

        // делаем проход вперед
        final int size = getSize();
        float[][] outputs = new float[size][];

        outputs[0] = getLayer(0).computeOutput(input);
        for (int i = 1; i < size; i++)
            outputs[i] = getLayer(i).computeOutput(outputs[i - 1]);

        // вычислим ошибку выходного слоя
        Layer layer = getLayer(size - 1);
        final int layerSize = layer.getSize();
        float[] error = new float[layerSize];
        float totalError = 0;

        for (int i = 0; i < layerSize; i++) {
            error[i] = goal[i] - outputs[size - 1][i];
            totalError += Math.abs(error[i]);
        }

        // обновим выходной слой
        if (layer instanceof BackpropLayer)
            ((BackpropLayer)layer).adjust(size == 1 ? input : outputs[size - 2],error,rate,momentum);

        // идем по скрытым слоям
        float[] prevError = error;
        Layer prevLayer = layer;

        for (int i = size - 2; i >= 0; i--,prevError = error,prevLayer = layer) {
            // получим очередной слой
            layer = getLayer(i);
            // вычислим для него ошибку
            if (prevLayer instanceof BackpropLayer)
                error = ((BackpropLayer)prevLayer).computeBackwardError(outputs[i],prevError);
            else
                error = prevError;
            // обновим слой
            if (layer instanceof BackpropLayer)
                ((BackpropLayer)layer).adjust(i == 0 ? input : outputs[i - 1],error,rate,momentum);
        }

        // вернем суммарную ошибку
        return totalError;
    }
}

SigmoidLayer.java

Реализация сигмоидального слоя. В качестве функции активации может использоваться гиперболический тангенс, либо сигмоидальная функция.

package nnet;

/**
 * Сигмоидальный слой
 */
public final class SigmoidLayer implements BackpropLayer {
    /**
     * Вес
     */
    private final int WEIGHT = 0;

    /**
     * Дельта
     */
    private final int DELTA = 1;

    /**
     * Констрирует сигмоидальный слой
     * @param inputSize Размер входного вектора
     * @param size Размер слоя
     * @param bipolar Флаг биполярного слоя
     */
    public SigmoidLayer(int inputSize,int size,boolean bipolar) {
        // проверки
        if (inputSize < 1 || size < 1) throw new IllegalArgumentException();

        // создаем слой
        matrix = new float[size][inputSize + 1][2];

        // запомним параметры
        this.inputSize = inputSize;
        this.bipolar = bipolar;
    }

    /**
     * Конструирует биполярный слой
     * @param inputSize Размер входного вектора
     * @param size Размер слоя
     */
    public SigmoidLayer(int inputSize,int size) {
        this(inputSize,size,true);
    }

    public int getInputSize() {
        return inputSize;
    }

    public int getSize() {
        return matrix.length;
    }

    public float[] computeOutput(float[] input) {
        // проверки
        if (input == null || input.length != inputSize)
                throw new IllegalArgumentException();

        // вычислим выход
        final int size = matrix.length;
        float[] output = new float[size];
        for (int i = 0; i < size; i++) {
            output[i] = matrix[i][0][WEIGHT];
            for (int j = 0; j < inputSize; j++)
                output[i] += input[j] * matrix[i][j + 1][WEIGHT];
            if (bipolar)
                output[i] = (float)Math.tanh(output[i]);
            else
                output[i] = 1 / (1 + (float)Math.exp(-output[i]));
        }

        // вернем оклик
        return output;
    }

    public void randomize(float min,float max) {
        final int size = matrix.length;
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < inputSize + 1; j++) {
                matrix[i][j][WEIGHT] = min + (max - min) * (float)Math.random();
                matrix[i][j][DELTA] = 0;
            }
        }
    }

    public float[] computeBackwardError(float[] input,float[] error) {
        // проверки
        if (input == null || input.length != inputSize ||
                error == null || error.length != matrix.length) throw new IllegalArgumentException();

        // вычислим входящую ошибку
        float[] output = computeOutput(input);
        final int size = matrix.length;
        float[] backwardError = new float[inputSize];

        for (int i = 0; i < inputSize; i++) {
            backwardError[i] = 0;
            for (int j = 0; j < size; j++)
                backwardError[i] += error[j] * matrix[j][i + 1][WEIGHT] *
                        (bipolar ? 1 - output[j] * output[j] : output[j] * (1 - output[j]));
        }

        // вернем ошибку
        return backwardError;
    }

    public void adjust(float[] input,float[] error,float rate,float momentum) {
        // проверки
        if (input == null || input.length != inputSize || 
                error == null || error.length != matrix.length) throw new IllegalArgumentException();

        // обновляем веса
        float[] output = computeOutput(input);
        final int size = matrix.length;

        for (int i = 0; i < size; i++) {
            final float grad = error[i] * (bipolar ? 1 - output[i] * output[i] : output[i] * (1 - output[i]));
            // обновляем нулевой вес
            matrix[i][0][DELTA] = rate * grad + momentum * matrix[i][0][DELTA];
            matrix[i][0][WEIGHT] += matrix[i][0][DELTA];
            // обновим остальные веса
            for (int j = 0; j < inputSize; j++) {
                matrix[i][j + 1][DELTA] = rate * input[j] * grad + momentum * matrix[i][j + 1][DELTA];
                matrix[i][j + 1][WEIGHT] += matrix[i][j + 1][DELTA];
            }
        }
    }

    /**
     * Размер входного вектора
     */
    private final int inputSize;

    /**
     * Флаг биполярного слоя
     */
    private final boolean bipolar;

    /**
     * Матрица слоя
     */
    private float[][][] matrix;
}

WTALayer.java

Реализация слоя победитель получает все. Победителем признаеться тот нейрон, у которого выход больше всех нейроннов на величину не менее minLevel. В противном случае все нейронны признаются проигравшими.

package nnet;

/**
 * WTA слой
 */
public final class WTALayer implements BackpropLayer {
    /**
     * Конструирует WTA слой заданного размера и уровнем доверия
     * @param size Размер слоя
     * @param minLevel Уровень доверия
     */
    public WTALayer(int size,float minLevel) {
        // проверки
        if (size < 1) throw new IllegalArgumentException();
        // запомним параметры слоя
        this.size = size;
        this.minLevel = minLevel;
    }

    public int getInputSize() {
        return size;
    }

    public int getSize() {
        return size;
    }

    public float[] computeOutput(float[] input) {
        // проверки
        if (input == null || input.length != size) throw new IllegalArgumentException();

        // найдем победителя
        int winner = 0;
        for (int i = 1; i < size; i++)
            if (input[i] > input[winner]) winner = i;

        // готовим ответ
        float[] output = new float[size];

        // проверим на минимальный уровень расхождения
        if (minLevel > 0) {
            float level = Float.MAX_VALUE;
            for (int i = 0; i < size; i++)
                if (i != winner && Math.abs(input[i] - input[winner]) < level)
                    level = Math.abs(input[i] - input[winner]);
            if (level < minLevel) return output;
        }

        // говорим кто победитель
        output[winner] = 1;

        // вернем отклик
        return output;
    }

    public void randomize(float min,float max) {
    }

    public float[] computeBackwardError(float[] input,float []error) {
        // проверки
        if (input == null || input.length != size || error == null ||
                error.length != size) throw new IllegalArgumentException();

        // расчитываем ошибку
        float[] backwardError = new float[size];
        float[] output = computeOutput(input);

        for (int i = 0; i < size; i++)
            backwardError[i] = error[i] + output[i] - input[i];

        // вернем входящую ошибку
        return backwardError;
    }

    public void adjust(float[] input,float[] error,float rate,float momentum) {
    }

    /**
     * Получает минимальный уровень между победителем и всеми остальными нейронами
     * @return Минимальный уровень между победителем и всеми остальными нейронами
     */
    public float getMinLevel() {
        return minLevel;
    }

    /**
     * Устанавливает минимальный уровень между победитлем и всеми остальными нейронами
     * @param minLevel Новый минимальный уровень
     */
    public void setMinLevel(float minLevel) {
        this.minLevel = minLevel;
    }

    /**
     * Размер слоя
     */
    private final int size;

    /**
     * Минимальный уровень между победителем и всеми остальными нейронами
     */
    private float minLevel;
}

Обратное распространение ошибки — стандартный способ
обучения нейронной сети, хотя существуют и другие методы (о них в одной
из следующих глав). Принцип работы примерно такой:

1. Входной набор данных, на котором сеть должна быть обучена, подается на входной слой сети, и сеть
функционирует в нормальном режиме (т.е. вычисляет выходные данные).

(1988 bytes)

2. Полученные данные сравниваются с известными выходными данными для рассматриваемого входного набора. Разница
между полученными и известными (опытными) данными — вектор ошибки.

(2573 bytes)

2. Полученные данные сравниваются с известными выходными данными для рассматриваемого входного набора. Разница
между полученными и известными (опытными) данными — вектор ошибки.

(2573 bytes)

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

(1513 bytes)

4. Затем таким же образом модифицируются весовые коэффициенты скрытого слоя, на этот раз
сравниваются выходные сигналы нейронов скрытого слоя и входные сигналы нейронов выходного слоя,
целью данного сравнения является формирование вектора ошибки для скрытого слоя.

(1500 bytes)

4. Затем таким же образом модифицируются весовые коэффициенты скрытого слоя, на этот раз
сравниваются выходные сигналы нейронов скрытого слоя и входные сигналы нейронов выходного слоя,
целью данного сравнения является формирование вектора ошибки для скрытого слоя.

(1500 bytes)

5. Наконец, если в сети существует входной
слой (именно слой, а не ряд входных значений), то
проводятся аналогичные действия и с ним.

Следует заметить, что ошибка может
быть распространена на любой желаемый уровень
(т.е. в нейронной сети может быть неограниченное
количество скрытых слоев, для которых мы
рассчитываем вектор ошибки по одной и той же
схеме). Метод обратного распространения ошибки
напоминает мне волны прибоя, — входные сигналы
движутся в сторону выходного слоя (т.е. берега), а
ошибки — в обратном направлении (как и морская
волна вынуждена в конечном счете отступить от
суши).

Сеть обучается путем предъявления
каждого входного набора данных и последующего
распространения ошибки. Этот цикл повторяется
много раз. Например, если вы распознаете цифры от
0 до 9, то сначала обрабатывается символ «0»,
символ «1» и так далее до «9», затем весь
цикл повторяется много раз. Не следует поступать
иначе, а именно, обучать сеть по отдельности
сначала символу «0» (n-ое количество раз),
потом «1», потом «2» и т.д., т.к. сеть
вырабатывает очень «четкие» весовые
коэффициенты для последнего входного набора (то
есть для «9»), «забывая» предыдущие.
Например, к тысячному повтору обучения символу
«1» теряются весовые коэффициенты для
распознавания символа «0». Повторяя весь
цикл для всего словарного набора входных данных,
мы предполагаем, что каждый символ оказывает
равноправное влияние на значения весовых
коэффициентов.

Запомните
Обозначим через переменную NUM_HID
количество нейронов в скрытом слое (нумерация
начинается с индекса 1). NUM_OUT — количество нейронов
в выходном слое.

Обратное распространение и формулы.
А теперь, настройтесь! Я собираюсь привести ниже
множество математических выкладок.

  • Во-первых, инициализируем пороговые значения и
    весовые коэффициенты небольшими случайными
    величинами (не более 0.4)

  • Теперь прогоним сеть в режиме прямого
    функционирования — процедура run_network (см. прошлую главу)

  • Вычислим ошибки для выходного слоя. При этом мы
    используем следующую формулу для каждого i-ого
    значения выходного слоя (т.е. проходим по всем
    узлам выходного слоя):

    Ei = (ti - ai).ai.(1 - ai)

    Здесь Ei — ошибка для i-ого узла выходного слоя, ai — активность данного узла,
    ti — требуемая активность для него же (т.е. требуемое выходное значение).

    Вот код на паскале:

    procedure calculate_output_layer_errors;
    var i : byte; 
    begin
        for i:=1 to NUM_OUT do
            with ol[i] do
                E:=(desired_output[i] - a) * a * (1 - a)
    end;
    

    Здесь видно, почему я ввел переменную для ошибки непосредственно в описание нейрона. Благодаря
    этому становится ненужным создание отдельного массива для значений ошибки.

  • Сейчас мы можем использовать значения ошибок
    выходного слоя для определения ошибок в скрытом слое. Формула практически та же, но теперь не
    определены желаемые выходные значения. — Мы вычисляем взвешенную сумму значений ошибок
    выходного слоя:

    Ei = ai . (1 - ai) . Sj Ej.wij

    Смысл переменных по сравнению с прошлой формулой изменился незначительно. индекс i используется
    для нейронов скрытого слоя ( а не выходного), Ei, следовательно, значение ошибки для нейрона
    скрытого слоя, и ai — сигнал на выходе нейрона. Индекс j относится к нейронам выходного
    слоя: wij — вес (весовой коэффициент) связи между i-ым скрытым нейроном и j-ым выходным нейроном,
    а Ej — значение ошибки для выходного нейрона j. Суммирование проводится для всех весов связей
    между отдельно взятым i-ым нейроном и всеми нейронами выходного слоя.

    И вновь турбо паскаль. Обратите внимание, что сумма включает в себя взвешенные
    связи только между рассматриваемым в данный момент нейроном скрытого слоя и всеми нейронами
    выходного слоя.

    procedure calculate_hidden_layer_errors;
    var
        i,j : byte;
        sum : real;
    begin
        for i:=1 to NUM_HID do 
            with hl[i] do
            begin
                sum:=0; 
                for j:=1 to NUM_OUT do
                    sum:=sum + ol[j].E * ol[j].w[i] 
                E:=a * (1 - a) * sum 
            end;
    end;
    
  • Полученные значения ошибок для выходного слоя мы используем для изменения весовых
    коэффициентов между скрытым и выходным слоями.Мы должны вычислить все значения ошибок
    до модификации весовых коэффициентов, так как в формуле присутствуют и старые значения весов.
    Если же мы вычислим сначала весовые коэффициенты, а уже затем — значения ошибок, то
    процесс обучения застопорится.

    Применяем уравнение:

    new wij = old wij + h.dj.xi

    где wij — вес связи между нейроном i скрытого слоя и нейроном j выходного,
    dj — приращение ошибки для выходного нейрона j и
    xi — сигнал на выходе скрытого нейрона i, h — константа.
    Эта константа используется для того, чтобы обучение не проводилось слишком быстро, то есть весовые
    коэффициенты не изменялись слишком сильно за один шаг обучения (что является причиной прыжков
    сходимости при обучении сети).

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

    new threshold = old threshold + h.dj.(-1)

    или ( в более удобном виде):

    new threshold = old threshold - h.dj

    Данная процедура обучает весовые коэффициенты и пороговые уровни:

    procedure update_output_weights;
    const
        LEARNING_RATE = 0.025;
    var
        i,j : byte;
    begin
        for j := 1 to NUM_OUT do 
        with ol[j] do
        begin 
            for i := 1 to NUM_HID do
                w[i] := w[i] + LEARNING_RATE * E * hl[i].out;
            
            threshold := threshold - LEARNING_RATE * E
        end
    end;
    

    В этом коде я использовал j для индексирования узлов выходного слоя, чтобы привести в
    соответствие с уравнением (т.е. E соответствует dj).
    Таким же образом hl[i].out соответствует xi, а w[i] — wij.

  • Наконец, мы должны модифицировать веса скрытого слоя. В реальности, если имеются дополнительные
    слои, приведенный код также работает.

    procedure update_hidden_weights;
    const
        LEARNING_RATE = 0.025; 
    var
        i,j : byte;
    begin
        for j := 1 to NUM_HID do 
        with hl[j] do
        begin 
            for i := 1 to NUM_INP do
            w[i] := w[i] + LEARNING_RATE * E * test_pat[i];
            
            threshold := threshold - LEARNING_RATE * E
        end
    end;
    

Все то же самое на JAVA
Ниже приведена реализация нейронной сети, обучаемой методом обратного распространения ошибки на JAVA:

Если вам нужен исходный текст, то кликните здесь. Конечно же, изменяйте его
как хотите.

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

Сеть, как она здесь представлена, имеет фиксированное количество входов (6) и
фиксированное количество выходов (5). Скрытые узлы в середине изображены синими кружками.

(337 bytes)

Обучающие наборы представлены по левую сторону. Для того, чтобы изменить входные
наборы, желаемые выходные наборы, кликните на столбец из шести входных квадратов или на
столбец из пяти выходных квадратов. Каждый клик мышью затемняет квадрат (белый — 0, светлосерый —
0.3, темносерый — 0.7, черный — 1).

(371 bytes)

Эта комбинация, например, обозначает, что для входного набора установлены данные
(1, 0, 0, 0.3, 0, 0.7), а для выходного набора — (0, 0, 0.7, 0.7, 0). Кликните на символ + или — (около
словосочетания «Training patterns») для изменения количества наборов.

Для того, чтобы обучить сеть, кликните на кнопку «Train». Для тестирования
сети введите значение компонента входного набора в слот наверху, а затем кликните на
компонент входного набора (на одно из тех текстовых значений в рамке слева на структурной
схеме сети). Запустите сеть — кнопка «Run»


Предыдущая Оглавление Следующая

Во-первых, что такое искусственная нейронная сеть? Проще говоря, один перцептрон используется в качестве узла нейронной сети, а затем такие узлы используются для формирования иерархической сетевой структуры.Мы называем эту сеть искусственной нейронной сетью (мое собственное понимание). Когда уровень сети больше или равен 3 слоям (входной слой + скрытый слой (больше или равен 1) + выходной слой), мы называем это многослойной искусственной нейронной сетью.

1. Выбор нейронного блока.

Итак, какой перцептрон мы должны использовать в качестве узла нейронной сети? В предыдущей статье мы представили алгоритм перцептрона, но если его использовать напрямую, возникнут следующие проблемы:

1) Вывод в правилах обучения перцептрона

Поскольку знаковая функция является прерывистой функцией, что делает ее недифференцируемой, описанный выше алгоритм градиентного спуска не может использоваться для минимизации функции потерь.

2) Выход по инкрементному закону:

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

Чтобы решить указанные выше проблемы, с одной стороны, мы не можем напрямую использовать линейную комбинацию для прямого вывода, нам нужно добавить функцию обработки при выводе; с другой стороны, добавленная функция обработки должна быть дифференцируемой, чтобы мы могли использовать градиент Алгоритм отклонения.

Вышеупомянутым условиям удовлетворяет множество функций, но наиболее классической является сигмовидная функция, также известная как логистическая функция. Эта функция можетЛюбое число в сжимается до (0,1), поэтому эта функция также называется функцией сжатия. Чтобы нормализовать ввод этой функции, мы добавляем порог к линейной комбинации ввода, чтобы линейная комбинация ввода использовала 0 в качестве точки разграничения.

Сигмовидная функция:

Его функциональная кривая показана на рисунке 1.1.

Рисунок 1.1 сигмовидная функция.[2]

Важной особенностью этой функции является ее производная:

С помощью этой функции гораздо проще рассчитать градиентный спуск.

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

2. Алгоритм обратного распространения также называется алгоритмом BP (обратное распространение).

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

Рисунок 2.1 Топологическая структура сети БП[3]

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

Итак, как определить весовой вектор каждого персептрона? В настоящее время нам нужно использовать алгоритм обратного распространения ошибки для постепенной оптимизации. Прежде чем официально представить алгоритм обратного распространения ошибки, продолжим анализ.

В предыдущей статье о персептроне, чтобы получить вектор веса, мы постоянно корректируем вектор веса, минимизируя функцию потерь. Этот метод также подходит для решения вектора весов здесь. Во-первых, нам нужно определить функцию потерь. Поскольку выходной слой сети имеет несколько выходных узлов, нам нужно просуммировать квадрат разницы каждого выходного узла выходного слоя. Таким образом, функция потерь каждого обучающего примера: (добавьте 0,5 впереди, чтобы облегчить использование производной позже)

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

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

Алгоритм обратного распространения ошибки сети прямого распространения с двухслойным сигмовидным блоком:

1) Произвольно инициализируйте значение владения в сети.

2) Для каждого обучающего примера выполните следующие операции:

A) Согласно входным данным примера, рассчитайте от начала до конца, чтобы получить выходные данные каждой единицы выходного слоя. Затем член ошибки каждой единицы каждого слоя вычисляется в обратном направлении от выходного слоя.

B) Для каждой единицы k выходного слоя вычислите его член ошибки:

C) Для каждого скрытого блока h в сети вычислите его член ошибки:

D) Обновите каждый вес:

Описание символа:

xji: Вход от узла i к узлу j, wjiСоответствующий вес.

output: представляет набор узлов выходного слоя.

Весь алгоритм похож на алгоритм стохастического градиентного спуска правила дельты, а анализ алгоритма выглядит следующим образом:

1) Обновление веса аналогично правилу дельты, которое в основном зависит от скорости обучения, входных данных, соответствующих весу, и члена ошибки модуля.

2) Для блока выходного слоя его член ошибки (t-o) умножается на производную o сигмоидной функции.k(1-ok), который отличается от члена ошибки дельта-правила, который равен (t-o).

3) Для единицы скрытого слоя из-за отсутствия прямого целевого значения для вычисления ошибки скрытого модуля необходимо вычислить член ошибки скрытого слоя косвенным способом.Ошибка для каждого объекта, на который воздействует скрытый блок hВыполните взвешенную сумму, каждая ошибкаВес wkh, wkhЭто вес от скрытой единицы h до единицы вывода k.

3. Вывод алгоритма обратного распространения.

Процесс вывода алгоритма — это в основном процесс использования алгоритма градиентного спуска для минимизации функции потерь. Теперь функция потерь:

Для каждого веса wji в сети вычислите его производную:

1) Если j — единица выходного уровня сети

ЧиститьjВывод:

среди них:

  

  

F:

Чтобы выражение было кратким, мы используем:

Изменение веса происходит в направлении отрицательного градиента функции потерь, поэтому величина изменения веса:

2) Если j — скрытый объект в сети

Поскольку значение w в скрытом модуле косвенно влияет на ввод через следующий уровень, для вывода используется послойный метод удаления:

потому как:

и другие:

Аналогично мы используем:

Итак, величина изменения веса:

4. Улучшение алгоритма

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

1) Увеличьте импульсный элемент

Этот метод в основном предназначен для изменения правила обновления веса. Его основная идея состоит в том, чтобы сделать часть обновления веса n-й итерации зависимой от n-1-го веса.

Среди них 0 <= a <1: он называется коэффициентом импульса. Добавление импульсного члена приводит к некоторому увеличению длины шага поиска, так что он может сходиться быстрее. С другой стороны, поскольку в многоуровневой сети функция потерь легко сводится к локальному минимуму, импульсный член может в определенной степени превосходить некоторый узкий локальный минимум и достигать меньшего места.

2) Изучение произвольных глубоких ациклических сетей

Алгоритм обратного распространения, представленный выше, на самом деле имеет только три слоя, то есть, когда есть только один скрытый слой, как с ним обращаться, если скрытых слоев много?

Теперь предположим, что нейронная сеть имеет m + 2 слоя, то есть есть m скрытых слоев. В настоящее время необходимо изменить только одно место, чтобы получить алгоритм обратного распространения ошибки с m скрытыми слоями. Значение ошибки единицы r k-го слоя вычисляется из члена ошибки более глубокого k + 1-го слоя:

Используйте Java, чтобы построить пример нейронной сети ниже.

Класс BPNN, реализующий основной алгоритм нейронной сети.

package bpnn;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;

/**
 * BP neural network core code and prediction processing code
 * @author Pumpkin
 * @since 2018/04/02
 * @version 1.0
 */
public class BPNN {

    
    // private static int LAYER = 3; // Three-layer neural network
    private static final int NodeNum = 10; // The maximum number of nodes per layer
    private static final int ADJUST = 5; // Hidden layer node adjustment constant
    private static final int MaxTrain = 2000; // Maximum training times
    private static final double ACCU = 0.015; // Allowable error for each iteration
    private double ETA_W = 0.5; // Weight learning efficiency
    private double ETA_T = 0.5; // Threshold learning efficiency
    private double accu;

         // дополнительный импульсный член
         // private static final double ETA_A = 0.3; // постоянная импульса 0.1
         // private double [] [] in_hd_last; // Последняя корректировка веса
    //private double[][] hd_out_last;

    private int in_num; // The number of input layer nodes
    private int hd_num; // The number of hidden layer nodes
    private int out_num; // The number of output layer nodes

    private ArrayList<ArrayList<Double>> list = new ArrayList<>(); // Input and output data

    private double[][] in_hd_weight; // BP network in-hidden synaptic weight
    private double[][] hd_out_weight; // BP network hidden_out synaptic weight
    private double[] in_hd_th; // BP network in-hidden threshold
    private double[] hd_out_th; // BP network hidden-out threshold

    private double[][] out; // The output value of each neuron converted by the sigmoid function, the input layer is the original value
    private double[][] delta; // Delta learning rules

    /** Obtaining the largest number of neurons in the third layer of the network
     * @return  **/
    public int GetMaxNum() {
        return Math.max(Math.max(in_num, hd_num), out_num);
    }

    // Setting the weight learning rate
    public void SetEtaW() {
        ETA_W = 0.5;
    }

    // Set threshold learning rate
    public void SetEtaT() {
        ETA_T = 0.5;
    }

    // BP neural network training
    public void Train(int in_number, int out_number,
            ArrayList<ArrayList<Double>> arraylist) throws IOException {
        list = arraylist;
        in_num = in_number;
        out_num = out_number;

        GetNums(in_num, out_num); // Get the number of nodes in the input layer, hidden layer, and output layer
        InitNetWork(); // Initialize network weights and thresholds

        int datanum = list.size(); // Number of training data 
        int createsize = GetMaxNum(); // Compare to create an array that stores the output data for each layer
        out = new double[3][createsize];

        for (int iter = 0; iter < MaxTrain; iter++) {
            for (int cnd = 0; cnd < datanum; cnd++) {
                                 // Назначение входного узла первого слоя

                for (int i = 0; i < in_num; i++) {
                                         out [0] [i] = list.get (cnd) .get (i); // Присваиваем значение узлу входного слоя, вход такой же, как и выход
                }
                                 Forward (); // прямое распространение
                                 Backward (cnd); // Обратное распространение ошибки

            }
            System.out.println("This is the " + (iter + 1)
                    + " th trainning NetWork !");
            accu = GetAccu();
            System.out.println("All Samples Accuracy is " + accu);
            if (accu < ACCU)
                break;

        }

    }

         // Получение количества узлов во входном слое, скрытом слое и выходном слое. In_number и out_number - это количество узлов входного слоя и узлов выходного слоя соответственно
    public void GetNums(int in_number, int out_number) {
        in_num = in_number;
        out_num = out_number;
        hd_num = (int) Math.sqrt(in_num + out_num) + ADJUST;
        if (hd_num > NodeNum)
                         hd_num = NodeNum; // Количество узлов скрытого слоя не может быть больше максимального количества узлов
    }

         // Инициализируем вес и порог сети
    public void InitNetWork() {
                 // Инициализируем последнее значение веса, диапазон от -0,5 до 0,5
        //in_hd_last = new double[in_num][hd_num];
        //hd_out_last = new double[hd_num][out_num];

        in_hd_weight = new double[in_num][hd_num];
        for (int i = 0; i < in_num; i++)
            for (int j = 0; j < hd_num; j++) {
                                 int flag = 1; // бит флага символа (-1 или 1)
                if ((new Random().nextInt(2)) == 1)
                    flag = 1;
                else
                    flag = -1;
                                 in_hd_weight [i] [j] = (new Random (). nextDouble () / 2) * flag; // инициализировать скрытый вес
                //in_hd_last[i][j] = 0;
            }

        hd_out_weight = new double[hd_num][out_num];
        for (int i = 0; i < hd_num; i++)
            for (int j = 0; j < out_num; j++) {
                                 int flag = 1; // бит флага символа (-1 или 1)
                if ((new Random().nextInt(2)) == 1)
                    flag = 1;
                else
                    flag = -1;
                                 hd_out_weight [i] [j] = (new Random (). nextDouble () / 2) * flag; // инициализируем скрытый вес
                //hd_out_last[i][j] = 0;
            }

                 // Порог инициализируется 0
        in_hd_th = new double[hd_num];
        for (int k = 0; k < hd_num; k++)
            in_hd_th[k] = 0;

        hd_out_th = new double[out_num];
        for (int k = 0; k < out_num; k++)
            hd_out_th[k] = 0;

    }

         // Рассчитываем ошибку единичного образца
    public double GetError(int cnd) {
        double ans = 0;
        for (int i = 0; i < out_num; i++)
            ans += 0.5 * (out[2][i] - list.get(cnd).get(in_num + i))
                    * (out[2][i] - list.get(cnd).get(in_num + i));
        return ans;
    }

         // Рассчитываем среднюю точность всех выборок
    public double GetAccu() {
        double ans = 0;
        int num = list.size();
        for (int i = 0; i < num; i++) {
            int m = in_num;
            for (int j = 0; j < m; j++)
                out[0][j] = list.get(i).get(j);
            Forward();
            int n = out_num;
            for (int k = 0; k < n; k++)
                ans += 0.5 * (list.get(i).get(in_num + k) - out[2][k])
                        * (list.get(i).get(in_num + k) - out[2][k]);
        }
        return ans / num;
    }

         // прямое распространение
    public void Forward() {
                 // Рассчитываем выходное значение узла скрытого слоя
        for (int j = 0; j < hd_num; j++) {
            double v = 0;
            for (int i = 0; i < in_num; i++)
                v += in_hd_weight[i][j] * out[0][i];
            v += in_hd_th[j];
            out[1][j] = Sigmoid(v);
        }
                 // Рассчитываем выходное значение узла выходного слоя
        for (int j = 0; j < out_num; j++) {
            double v = 0;
            for (int i = 0; i < hd_num; i++)
                v += hd_out_weight[i][j] * out[1][i];
            v += hd_out_th[j];
            out[2][j] = Sigmoid(v);
        }
    }

         // Обратное распространение ошибки
    public void Backward(int cnd) {
                 CalcDelta (cnd); // Рассчитываем поправку веса
        UpdateNetWork (); // Обновляем веса и пороги нейронной сети БП
    }

         // Рассчитываем корректировку дельты
    public void CalcDelta(int cnd) {

                 int createdize = GetMaxNum (); // сравниваем и создаем массив
        delta = new double[3][createsize];
                 // Рассчитываем значение дельты выходного слоя
        for (int i = 0; i < out_num; i++) {
            delta[2][i] = (list.get(cnd).get(in_num + i) - out[2][i])
                    * SigmoidDerivative(out[2][i]);
        }

                 // Рассчитываем значение дельты скрытого слоя
        for (int i = 0; i < hd_num; i++) {
            double t = 0;
            for (int j = 0; j < out_num; j++)
                t += hd_out_weight[i][j] * delta[2][j];
            delta[1][i] = t * SigmoidDerivative(out[1][i]);
        }
    }

         // Обновляем веса и пороги нейронной сети БП
    public void UpdateNetWork() {

                 // Настройка веса и порога между скрытым слоем и выходным слоем
        for (int i = 0; i < hd_num; i++) {
            for (int j = 0; j < out_num; j++) {
                                 hd_out_weight [i] [j] + = ETA_W * delta [2] [j] * out [1] [i]; // член импульса невзвешенного значения
                                 / * Импульс
                 * hd_out_weight[i][j] += (ETA_A * hd_out_last[i][j] + ETA_W
                 * delta[2][j] * out[1][i]); hd_out_last[i][j] = ETA_A *
                 * hd_out_last[i][j] + ETA_W delta[2][j] * out[1][i];
                 */
            }

        }
        for (int i = 0; i < out_num; i++)
            hd_out_th[i] += ETA_T * delta[2][i];

                 // Настройка веса и порога между входным слоем и скрытым слоем
        for (int i = 0; i < in_num; i++) {
            for (int j = 0; j < hd_num; j++) {
                                 in_hd_weight [i] [j] + = ETA_W * delta [1] [j] * out [0] [i]; // член импульса невзвешенного значения
                                 / * Импульс
                 * in_hd_weight[i][j] += (ETA_A * in_hd_last[i][j] + ETA_W
                 * delta[1][j] * out[0][i]); in_hd_last[i][j] = ETA_A *
                 * in_hd_last[i][j] + ETA_W delta[1][j] * out[0][i];
                 */
            }
        }
        for (int i = 0; i < hd_num; i++)
            in_hd_th[i] += ETA_T * delta[1][i];
    }

         // знак функции sign
    public int Sign(double x) {
        if (x > 0)
            return 1;
        else if (x < 0)
            return -1;
        else
            return 0;
    }

         // возвращаем максимальное значение
    public double Maximum(double x, double y) {
        if (x >= y)
            return x;
        else
            return y;
    }

         // возвращаем минимум
    public double Minimum(double x, double y) {
        if (x <= y)
            return x;
        else
            return y;
    }

         // лог-сигмоидальная функция
    public double Sigmoid(double x) {
        return (double) (1 / (1 + Math.exp(-x)));
    }

         // Обратная логарифмическая сигмоидальная функция
    public double SigmoidDerivative(double y) {
        return (double) (y * (1 - y));
    }

         // загар-сигмовидная функция
    public double TSigmoid(double x) {
        return (double) ((1 - Math.exp(-x)) / (1 + Math.exp(-x)));
    }

         // Обратное значение tan-сигмовидной функции
    public double TSigmoidDerivative(double y) {
        return (double) (1 - (y * y));
    }

         // Функция предсказания классификации
    public ArrayList<ArrayList<Double>> ForeCast(
            ArrayList<ArrayList<Double>> arraylist) {

        ArrayList<ArrayList<Double>> alloutlist = new ArrayList<>();
        ArrayList<Double> outlist = new ArrayList<>();
        int datanum = arraylist.size();
        for (int cnd = 0; cnd < datanum; cnd++) {
            for (int i = 0; i < in_num; i++)
                                 out [0] [i] = arrayylist.get (cnd) .get (i); // Присваиваем значение входному узлу
            Forward();
            for (int i = 0; i < out_num; i++) {
                if (out[2][i] > 0 && out[2][i] < 0.5)
                    out[2][i] = 0;
                else if (out[2][i] > 0.5 && out[2][i] < 1) {
                    out[2][i] = 1;
                }
                outlist.add(out[2][i]);
            }
            alloutlist.add(outlist);
            outlist = new ArrayList<>();
            outlist.clear();
        }
        return alloutlist;
    }
    
}

Класс DataUtil в основном используется для предварительной обработки данных, а также данных для обучения и тестирования.

package bpnn;

/**
 * Data processing class to process training data and test data
 * @author Pumpkin
 * @
 */
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;

class DataUtil {

         private final ArrayList <ArrayList <Double>> alllist = new ArrayList <> (); // Сохраняем все данные
         private final ArrayList <String> outlist = new ArrayList <> (); // Сохраняем выходные данные, индекс соответствует выходным данным каждого списка
         private final ArrayList <String> checklist = new ArrayList <> (); // Сохраняем реальную строку вывода тестового набора
    private int in_num = 0;
         private int out_num = 0; // количество входных и выходных данных
         private int type_num = 0; // Количество типов вывода
         private double [] [] nom_data; // нормализуем максимальное и минимальное значения во входных данных / *
         private int in_data_num = 0; // Получить количество входных данных заранее * /

         // Получаем количество типов вывода
    public int GetTypeNum() {
        return type_num;
    }

    // Устанавливаем количество типов вывода
    public void SetTypeNum(int type_num) {
        this.type_num = type_num;
    }

         // Получаем количество входных данных
    public int GetInNum() {
        return in_num;
    }

         // Получаем количество выходных данных
    public int GetOutNum() {
        return out_num;
    }

         // Получаем массив всех данных
    public ArrayList<ArrayList<Double>> GetList() {
        return alllist;
    }

         // Получаем вывод данных в виде строки
    public ArrayList<String> GetOutList() {
        return outlist;
    }

         // Получаем вывод данных в виде строки
    public ArrayList<String> GetCheckList() {
        return checklist;
    }

         // Возвращаем максимальное и минимальное значения, необходимые для нормализованных данных
    public double[][] GetMaxMin(){

        return nom_data;
    }

         // считываем данные инициализации файла
    public void ReadFile(String filepath, String sep, int flag)
            throws Exception {

                 ArrayList <Double> everylist = new ArrayList <> (); // Сохраняем каждую группу входных и выходных данных
        int readflag = flag; // flag=0,train;flag=1,test
        String encoding = "utf-8";
        File file = new File(filepath);
                 if (file.isFile () && file.exists ()) {// Определяем, существует ли файл
            InputStreamReader read = new InputStreamReader(new FileInputStream(
                                         file), encoding); // Учитываем формат кодировки
            try (BufferedReader bufferedReader = new BufferedReader(read)) {
                String lineTxt = null;
                while ((lineTxt = bufferedReader.readLine()) != null) {
                    int in_number = 0;
                                         String splits [] = lineTxt.split (sep); // Нажмите ',', чтобы перехватить строку
                    if (readflag == 0) {
                        for (int i = 0; i < splits.length; i++)
                            try {
                                everylist.add(Normalize(Double.valueOf(splits[i]),nom_data[i][0],nom_data[i][1]));
                                in_number++;
                            } catch (NumberFormatException e) {
                                if (!outlist.contains(splits[i]))
                                                                         outlist.add (splits [i]); // Сохраняем выходные данные в строковой форме
                                for (int k = 0; k < type_num; k++) {
                                    everylist.add(0.0);
                                }
                            //everylist.set(in_number + outlist.indexOf(splits[i]),1.0);
                            }
                    } else if (readflag == 1) {
                        for (int i = 0; i < splits.length; i++)
                            try {
                                everylist.add(Normalize(Double.valueOf(splits[i]),nom_data[i][0],nom_data[i][1]));
                                in_number++;
                            } catch (NumberFormatException e) {
                                                                 checklist.add (splits [i]); // Сохраняем выходные данные в строковой форме
                            }
                    }
                                         alllist.add (everylist); // сохраняем все данные
                    in_num = in_number;
                    out_num = type_num;
                    everylist = new ArrayList<>();
                    everylist.clear();
                    
                }
            }
        }
    }

         // Записываем результаты классификации в файл
    public void WriteFile(String filepath, ArrayList<ArrayList<Double>> list, int in_number,  ArrayList<String> resultlist) throws IOException{
        File file = new File(filepath);
        FileWriter fw = null;
        BufferedWriter writer = null;
        try {
            fw = new FileWriter(file);
            writer = new BufferedWriter(fw);
            for(int i=0;i<list.size();i++){
                for(int j=0;j<in_number;j++)
                    writer.write(list.get(i).get(j)+",");
                writer.write(resultlist.get(i));
                writer.newLine();
            }
            writer.flush();
        } catch (IOException e) {
        }finally{
            writer.close();
            fw.close();
        }
    }


         // Изучить нормализацию выборки и найти максимальное и минимальное значения входных данных выборки
    public void NormalizeData(String filepath) throws IOException{
                 // Получаем заранее количество входных данных   
        GetBeforIn(filepath);
        int flag=1;
        nom_data = new double[in_data_num][2];
        String encoding = "utf-8";
        File file = new File(filepath);
                 if (file.isFile () && file.exists ()) {// Определяем, существует ли файл
            InputStreamReader read = new InputStreamReader(new FileInputStream(
                                         file), encoding); // Учитываем формат кодировки
            try (BufferedReader bufferedReader = new BufferedReader(read)) {
                String lineTxt = null;
                while ((lineTxt = bufferedReader.readLine()) != null) {
                                         String splits [] = lineTxt.split (","); // Нажмите ',', чтобы перехватить строку
                    for (int i = 0; i < splits.length-1; i++){
                        if(flag==1){
                            nom_data[i][0]=Double.valueOf(splits[i]);
                            nom_data[i][1]=Double.valueOf(splits[i]);
                        }
                        else{
                            if(Double.valueOf(splits[i])>nom_data[i][0])
                                nom_data[i][0]=Double.valueOf(splits[i]);
                            if(Double.valueOf(splits[i])<nom_data[i][1])
                                nom_data[i][1]=Double.valueOf(splits[i]);
                        }
                    }
                    flag=0;
                }
            }
        }
    }

         // Получаем количество входных данных до нормализации
    public void GetBeforIn(String filepath) throws IOException{
        String encoding = "utf-8";
        File file = new File(filepath);
                 if (file.isFile () && file.exists ()) {// Определяем, существует ли файл
            InputStreamReader read = new InputStreamReader(new FileInputStream(
                                         file), encoding); // Учитываем формат кодировки
            try (BufferedReader beforeReader = new BufferedReader(read)) {
                String beforetext = beforeReader.readLine();
                String splits[] = beforetext.split(",");
                in_data_num = splits.length-1;
            }
        }
    }

         // Формула нормализации
    public double Normalize(double x, double max, double min){
        double y = 0.1+0.8*(x-min)/(max-min);
        return y;
    }
}

Тестовый класс, считывание данных и отображение результатов обучения.

package bpnn;

import java.util.ArrayList;

/**
 *
 * @author Pumpkin
 */


public class Test {
    public static void main(String args[]) throws Exception {

        ArrayList <ArrayList <Double>> alllist = new ArrayList <> (); // Сохраняем все данные
                 ArrayList <String> outlist = new ArrayList <> (); // Сохраняем классифицированную строку
        int in_num = 0;
                 int out_num = 0; // количество входных и выходных данных

                 DataUtil dataUtil = new DataUtil (); // Инициализируем данные

                 dataUtil.NormalizeData ("C: \ Users \   \ Desktop \ BPNN \ data \ train.txt");

                 dataUtil.SetTypeNum (3); // Устанавливаем количество типов вывода
                 dataUtil.ReadFile ("C: \ Users \   \ Desktop \ BPNN \ data \ train.txt", ",", 0);

                 in_num = dataUtil.GetInNum (); // Получаем количество входных данных
                 out_num = dataUtil.GetOutNum (); // Получение количества выходных данных (число представляет количество типов)
                 alllist = dataUtil.GetList (); // Получаем инициализированные данные

        outlist = dataUtil.GetOutList();
        System.out.print("Classification type:");
        for(int i =0 ;i<outlist.size();i++)
            System.out.print(outlist.get(i)+"  ");
        System.out.println();
        System.out.println("The number of training sets:"+alllist.size());

        BPNN bpnn = new BPNN();
                 // подготовка
        System.out.println("/***Train Start!***/");
        System.out.println(".............");
        bpnn.Train(in_num, out_num, alllist);
        System.out.println("/***Train End!***/");

                 // контрольная работа
        DataUtil testUtil = new DataUtil();

                 testUtil.NormalizeData ("C: \ Users \   \ Desktop \ BPNN \ data \ test.txt");

                 testUtil.SetTypeNum (3); // Устанавливаем количество типов вывода
                 testUtil.ReadFile ("C: \ Users \   \ Desktop \ BPNN \ data \ test.txt", ",", 1);

        ArrayList<ArrayList<Double>> testList = new ArrayList<>();
        ArrayList<ArrayList<Double>> resultList = new ArrayList<>();
                 ArrayList <String> normallist = new ArrayList <> (); // Сохраняем стандартную строку вывода тестового набора
                 ArrayList <String> resultlist = new ArrayList <> (); // Сохраняем выходную строку после расчета тестового набора

                 double right = 0; // номер правильной классификации
                 int type_num = 0; // количество типов
                 double all_num = 0; // Количество наборов тестов
        type_num = outlist.size();

                 testList = testUtil.GetList (); // Получаем тестовые данные
        normallist = testUtil.GetCheckList(); 

                 int errorcount = 0; // количество ошибок классификации
                 resultList = bpnn.ForeCast (testList); // тест
        all_num=resultList.size();
        for (int i = 0; i < all_num; i++) {
            String checkString = "unknow";
            for (int j = 0; j < type_num-1; j++) {
                if(resultList.get(i).get(j)==1.0){
                    checkString = outlist.get(j);
                    resultlist.add(checkString);
                }
                /*else{
                    resultlist.add(checkString);
                }*/
            }
            /*
            if(checkString.equals("unknow"))
                errorcount++;
            */
            if(checkString.equals(normallist.get(i)))
                right++;
        }
                 testUtil.WriteFile ("C: \ Users \   \ Desktop \ BPNN \ data \ result.txt", testList, in_num, resultlist);

        System.out.println("The number of test sets:"+ (new Double(all_num)).intValue());
        System.out.println("Classification correct quantity:"+(new Double(right)).intValue());
        System.out.println("The correct classification rate of the algorithm:"+right/all_num);

                 System.out.println («Результаты классификации хранятся в: C: \ Users \   \ Desktop \ BPNN \ data \ result.txt»);      
    }
}

На этом построение простой нейронной сети в основном завершено, приступайте к работе.

I am building a test neural network and it is definitely not working. My main problem is backpropagation. From my research, I know that it is easy to use the sigmoid function. Therefore, I update each weight by (1-Output)(Output)(target-Output) but the problem with this is what if my Output is 1 but my target is not? If it is one at some point then the weight update will always be 0…For now I am just trying to get the darn thing to add the inputs from 2 input neurons, so the optimal weights should just be 1 as the output neuron simply adds its inputs. I’m sure I have messed this up in lots of places but here is my code:

    public class Main {

        public static void main(String[] args) {
            Double[] inputs = {1.0, 2.0};
            ArrayList<Double> answers = new ArrayList<Double>();
            answers.add(3.0);

            net myNeuralNet = new net(2, 1, answers);

            for(int i=0; i<200; i++){

                myNeuralNet.setInputs(inputs);
                myNeuralNet.start();
                myNeuralNet.backpropagation();
                myNeuralNet.printOutput();
                System.out.println("*****");
                for(int j=0; j<myNeuralNet.getOutputs().size(); j++){
                    myNeuralNet.getOutputs().get(j).resetInput();
                    myNeuralNet.getOutputs().get(j).resetOutput();
                    myNeuralNet.getOutputs().get(j).resetNumCalled();
                }
            }
        }

    }


    package myneuralnet;
    import java.util.ArrayList;

    public class net {

    private ArrayList<neuron> inputLayer;
    private ArrayList<neuron> outputLayer;
    private ArrayList<Double> answers;

    public net(Integer numInput, Integer numOut, ArrayList<Double> answers){
        inputLayer = new ArrayList<neuron>();
        outputLayer = new ArrayList<neuron>();
        this.answers = answers;

        for(int i=0; i<numOut; i++){
            outputLayer.add(new neuron(true));
        }

        for(int i=0; i<numInput; i++){
            ArrayList<Double> randomWeights = createRandomWeights(numInput);
            inputLayer.add(new neuron(outputLayer, randomWeights, -100.00, true));
        }

        for(int i=0; i<numOut; i++){
            outputLayer.get(i).setBackConn(inputLayer);
        }
    }

    public ArrayList<neuron> getOutputs(){
        return outputLayer;
    }

    public void backpropagation(){
        for(int i=0; i<answers.size(); i++){
            neuron iOut = outputLayer.get(i);
            ArrayList<neuron> iOutBack = iOut.getBackConn();
            Double iSigDeriv = (1-iOut.getOutput())*iOut.getOutput();
            Double iError = (answers.get(i) - iOut.getOutput());

            System.out.println("Answer: "+answers.get(i) + " iOut: "+iOut.getOutput()+" Error: "+iError+" Sigmoid: "+iSigDeriv);

            for(int j=0; j<iOutBack.size(); j++){
                neuron jNeuron = iOutBack.get(j);
                Double ijWeight = jNeuron.getWeight(i);

                System.out.println("ijWeight: "+ijWeight);
                System.out.println("jNeuronOut: "+jNeuron.getOutput());

                jNeuron.setWeight(i, ijWeight+(iSigDeriv*iError*jNeuron.getOutput()));
            }
        }

        for(int i=0; i<inputLayer.size(); i++){
            inputLayer.get(i).resetInput();
            inputLayer.get(i).resetOutput();
        }
    }

    public ArrayList<Double> createRandomWeights(Integer size){
        ArrayList<Double> iWeight = new ArrayList<Double>();

        for(int i=0; i<size; i++){
            Double randNum = (2*Math.random())-1;
            iWeight.add(randNum);
        }

        return iWeight;
    }

    public void setInputs(Double[] is){
        for(int i=0; i<is.length; i++){
            inputLayer.get(i).setInput(is[i]);
        }
        for(int i=0; i<outputLayer.size(); i++){
            outputLayer.get(i).resetInput();
        }
    }

    public void start(){
        for(int i=0; i<inputLayer.size(); i++){
            inputLayer.get(i).fire();
        }
    }

    public void printOutput(){
        for(int i=0; i<outputLayer.size(); i++){
            System.out.println(outputLayer.get(i).getOutput().toString());
        }
    }

}

package myneuralnet;
import java.util.ArrayList;

public class neuron {

    private ArrayList<neuron> connections;
    private ArrayList<neuron> backconns;
    private ArrayList<Double> weights;
    private Double threshold;
    private Double input;
    private Boolean isOutput = false;
    private Boolean isInput = false;
    private Double totalSignal;
    private Integer numCalled;
    private Double myOutput;

    public neuron(ArrayList<neuron> conns, ArrayList<Double> weights, Double threshold){
        this.connections = conns;
        this.weights = weights;
        this.threshold = threshold;
        this.totalSignal = 0.00;
        this.numCalled = 0;
        this.backconns = new ArrayList<neuron>();
        this.input = 0.00;
    }

    public neuron(ArrayList<neuron> conns, ArrayList<Double> weights, Double threshold, Boolean isin){
        this.connections = conns;
        this.weights = weights;
        this.threshold = threshold;
        this.totalSignal = 0.00;
        this.numCalled = 0;
        this.backconns = new ArrayList<neuron>();
        this.input = 0.00;
        this.isInput = isin;
    }

    public neuron(Boolean tf){
        this.connections = new ArrayList<neuron>();
        this.weights = new ArrayList<Double>();
        this.threshold = 0.00;
        this.totalSignal = 0.00;
        this.numCalled = 0;
        this.isOutput = tf;
        this.backconns = new ArrayList<neuron>();
        this.input = 0.00;
    }

    public void setInput(Double input){
        this.input = input;
    }

    public void setOut(Boolean tf){
        this.isOutput = tf;
    }

    public void resetNumCalled(){
        numCalled = 0;
    }

    public void setBackConn(ArrayList<neuron> backs){
        this.backconns = backs;
    }

    public Double getOutput(){
        return myOutput;
    }

    public Double getInput(){
        return totalSignal;
    }

    public Double getRealInput(){
        return input;
    }

    public ArrayList<Double> getWeights(){
        return weights;
    }

    public ArrayList<neuron> getBackConn(){
        return backconns;
    }

    public Double getWeight(Integer i){
        return weights.get(i);
    }

    public void setWeight(Integer i, Double d){
        weights.set(i, d);
    }

    public void setOutput(Double d){
        myOutput = d;
    }

    public void activation(Double myInput){
        numCalled++;
        totalSignal += myInput;

        if(numCalled==backconns.size() && isOutput){
            System.out.println("Total Sig: "+totalSignal);
            setInput(totalSignal);
            setOutput(totalSignal);
        }
    }

    public void activation(){
        Double activationValue = 1 / (1 + Math.exp(input));
        setInput(activationValue);
        fire();
    }

    public void fire(){
        for(int i=0; i<connections.size(); i++){
            Double iWeight = weights.get(i);
            neuron iConn = connections.get(i);
            myOutput = (1/(1+(Math.exp(-input))))*iWeight;
            iConn.activation(myOutput);
        }
    }

    public void resetInput(){
        input = 0.00;
        totalSignal = 0.00;
    }

    public void resetOutput(){
        myOutput = 0.00;
    }
}

OK so that is a lot of code so allow me to explain. The net is simple for now, just an input layer and an output layer — I want to add a hidden layer later but I’m taking baby steps for now. Each layer is an arraylist of neurons. Input neurons are loaded with inputs, a 1 and a 2 in this example. These neurons fire, which calculates the sigmoid of the inputs and outputs that to the output neurons, which adds them and stores the value. Then the net backpropagates by taking the (answer-output)(output)(1-output)(output of the specific input neuron) and updates the weights accordingly. A lot of times, it cycles through and I get infinity, which seems to correlate with negative weights or sigmoid. When that doesn’t happen it converges to 1 and since (1-output of 1) is 0, my weights stop updating.

The numCalled and totalSignal values are just so the algorithm waits for all neuron inputs before continuing. I know I’m doing this an odd way, but the neuron class has an arraylist of neurons called connections to hold the neurons that it is forward connected to. Another arraylist called backconns holds the backward connections. I should be updating the correct weights as well since I am getting all back connections between neurons i and j but of all neurons j (the layer above i) I am only pulling weight i. I apologize for the messiness — I’ve been trying lots of things for hours upon hours now and still cannot figure it out. Any help is greatly appreciated!


  Перевод


  Ссылка на автора

В этой статье вы увидите, как теории, представленные в предыдущих двух статьях, могут быть реализованы в простом для понимания Java-коде. Полная реализация нейронной сети может быть загружена, детально проверена, создана и экспериментирована.

Это третья часть в серии статей:

  • Часть 1: Фонд,
  • Часть 2: градиентный спуск и обратное распространение,
  • Часть 3: Реализация в Java,
  • Часть 4: лучше, быстрее, сильнее,
  • Часть 5. Обучение сети чтению рукописных цифр,
  • Дополнительный 1: Как я увеличил точность на 1% за счет увеличения данных,
  • Дополнительно 2: Детская площадка MNIST,

Я предполагаю, что вы прочитали обе предыдущие статьи и достаточно хорошо понимаете прямой проход и этап обучения / обучения нейронной сети.

Эта статья будет совсем другой. Он покажет и опишет все это в нескольких фрагментах кода Java.

Весь мой код, полностью работающую реализацию нейронной сети, можно найти, изучить и загрузить Вот,

Амбиция

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

На этот раз все иначе. Я хочу показать, как работает нейронная сеть с минимальными требованиями с вашей стороны в плане знания других библиотек с открытым исходным кодом. Если вам удастся прочитать код Java 8, все будет в порядке. Все это есть в нескольких коротких и аккуратных файлах.

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

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

Я также свернул вычисления в максимально возможной степени, используя объекты типов Vec и Matrix. Результат аккуратный и недалек от математических выражений. Никакие циклы for в циклах for не размывают вид сверху. Тем не менее, при проверке кода я рекомендую вам убедиться, что любой аккуратный вызов объектов типа Vec или Matrix на самом деле приводит именно к серии арифметических операций, которые были определены в часть 1 а также часть 2,

Две операции над классом Vec встречаются не так часто, как типичные операции, о которых я говорил выше. Это:

  • внешний продукт (Vec.outerProduct ()), который является поэлементным умножением вектора столбца и вектора строки, приводящего к матрице.
  • Продукт Адамара (Vec.elementProduct ()), который просто является поэлементным умножением двух векторов (оба являются векторами столбцов или строк), в результате чего получается новый вектор.

обзор

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

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

Чтобы использовать такую ​​сеть, вы обычно либо вводите в нее один входной вектор, либо добавляете ожидаемый результат в вызовоценки ()-метод. При выполнении последнего сеть будет наблюдать разницу между фактическим выходом и ожидаемым и сохранит впечатление об этом. Другими словами, это распространит ошибку назад. По уважительным причинам (о которых я вернусь в следующей статье), сеть не сразу обновляет свои веса и смещения. Для этого — то есть, пусть «впитываются» любые впечатления — вызовupdateFromLearning ()должно быть сделано. Например:

Подача вперед

Давайте погрузимся в проход вперед в пределахоценки ()-метод. Обратите внимание ниже (строки 9 и 10), что входные данные передаются через слой за слоем:

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

(Причина, по которой есть операции get и set для переменной out, я вернусь в следующей статье А пока просто думайте об этом как о переменной типа Vec)

Функции активации

Код содержит несколько предопределенных функций активации. Каждый из них содержит как фактическую функцию активации,σ, а также производная,σ»,

Функции стоимости

Также есть несколько функций стоимости включены. Квадратик выглядит так:

Функция стоимости имеет один метод для расчета общей стоимости (в виде скаляра), но также имеет важное отличие функции стоимости, которая будет использоваться в…

обратное распространение

Как я упоминал выше в разделе для прямого прохода: если ожидаемый результат передается в функцию оценки, сеть узнает об этом через вызовlearnFrom ()-метод. Смотрите строку 12–13.

В этом методе происходит реальное обратное распространение. Здесь вы должны быть в состоянии следовать инструкциям часть 2 подробно в коде. Несколько успокаивающе видно, что довольно длинные математические выражения из часть 2 просто сводится к этому:

Обратите внимание, чтоучусь(частные производные) в обратном распространении сохраняется для каждого слоя путем вызоваaddDeltaWeightsAndBiases ()-метод.

Не до звонкаupdateFromLearning ()-метод был сделан изменение весов и уклонов:

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

Это то, чтоupdateWeightsAndBias ()-метод выглядит так. Обратите внимание, что среднее из всех рассчитанных изменений весов и смещений подается в два методаupdateWeights ()а такжеupdateBias ()на объекте оптимизатора.

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

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

Пока мы просто используем простую стратегию GradientDescent для обновления наших весов и смещений:

И это в значительной степени так. С помощью нескольких строк кода у нас есть возможность настроить нейронную сеть, передать данные через нее и научить ее классифицировать невидимые данные. Это будет показано в Часть 5. Обучение сети чтению рукописных цифр,

Но во-первых, давайте сделаем это на несколько ступеней. Видел тебя в Часть 4: лучше, быстрее, сильнее,

Обратная связь приветствуется!


Первоначально опубликовано на machinelearning.tobiashill.se 15 декабря 2018 г.

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