Добрый день. В прошлой статье «Нейронные сети за 1 день» мы рассмотрели НС которая решала задачу AND, причём сеть была однослойная. В этот раз мы создадим нейронную сеть, которая будет способна решить задачу XOR, она будет многослойная. Эта статья научит вас использовать метод обратного распространения ошибки, введёт в классификацию.

image

О задаче


Часто, для того чтобы продемонстрировать ограниченные возможности однослойных персептронов при решении задач прибегают к рассмотрению так называемой проблемы XOR – исключающего ИЛИ. Данная логическая функция принимает два аргумента, которые могут быть 1 или 0. Функция принимает значение 1 когда один из двух аргументов 1, а другой 0. Если же оба равняются 0 или 1, то в результате мы получаем 0. На такое однослойная НС не способна.

image

Модель


Посмотрите внимательно на данную картинку.

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

Программируем


Программировать будем на JavaScript. Для начала нам необходимо написать все необходимые массивы и переменные: синапсы, входы, выход и нейроны.

{
var enters = new Array(2); // 2 входа
var hidden_layer = new Array(2); // два скрытых нейрона
var synapses_hidden = [[0.3, 1.3], [0.5,0.1]]; // от входов к скрытым нейронам
var synapses_output = [0.5, 0.1]; // от скрытых нейронов к выходу
var output = 0; // выход

Теперь создадим матрицу обучения сети.

var learn = [[0,0],[0,1],[1,0],[1,1]];
var learn_answers = [0,1,1,0];    
}

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

function sum(){
    for ( var i = 0; i < hidden_layer.length; i++ ){
        hidden_layer[i] = 0;
        for ( var j = 0; j < enters.length; j++ )
            hidden_layer[i] += synapses_hidden[j][i] * enters[j];
        if ( hidden_layer[i] > 0.5 ) hidden_layer[i] = 1; else hidden_layer[i] = 0;
    }
    output = 0;
    for ( var i = 0; i < hidden_layer.length; i++ )
        output += synapses_output[i] * hidden_layer[i];
    if ( output > 0.5 ) output = 1; else output = 0;
}

Обратное распространение ошибки

Уйдём ненадолго от программирования. Метод обратного распространения ошибки похож на delta правило. То есть сначала мы вычисляем ошибку(правильный ответ минус ответ сети), и затем меняем веса по формуле: w = w_last + err * n * x
Где w_last — прошлый вес, err — ошибка, n — скорость обучения и x — входной сигнал
В данном случае мы передаём ошибку по таким же связям, откуда пришёл сигнал. То есть от выхода к входу. Ещё необходимо завести счётчик ошибок обрабатывающего слоя и глобальную ошибку(gError = Math.abs(lErr))

var gError = 0; // глобальная ошибка
var errors = new Array(hidden_layer.length); // слой ошибок
do {
    gError = 0; // обнуляем
    for ( var p = 0; p < learn.length; p++ ){
        for ( var i = 0; i < enters.length; i++ )
            enters[i] = learn[p][i]; // подаём об.входы на входы сети
        sum(); // запускаем распространение сигнала
        var error = learn_answers[p] - output; // получаем ошибку
        gError += Math.abs(error); // записываем в глобальную
        for ( var i = 0; i < errors.length; i++ ) 
            errors[i] = error * synapses_output[i]; // передаём ошибку на слой ошибок
          // по связям к выходу
        for ( var i = 0; i < enters.length; i++ ){
            for ( var j = 0; j < hidden_layer.length; j++ )
                synapses_hidden[i][j] += 0.1 * errors[i] * enters[j]; // меняем веса
        }
        for ( var i = 0; i < synapses_output.length; i++ )
            synapses_output[i] += 0.1 * error * hidden_layer[i]; // меняем веса
    } 
} while(gError != 0);

Теперь пишем скрипт на запуск сети:

for ( var p = 0; p < learn.length; p++ ){
    for ( var i = 0; i < enters.length; i++ )
        enters[i] = learn[p][i]; // записываем входы
    sum(); // распространяем сигнал
    document.write(output + "<br/>"); // выводим ответы
}

Мы получим такой результат:

0 1 1 0

Если вам интересно — демонстрация обучения и написание сети на видео:


Финал


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

Комментарии (28)


  1. Akon32
    11.02.2017 21:16
    +6

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


    1. EmeraldSoft
      11.02.2017 21:18
      -4

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


      1. Akon32
        11.02.2017 21:36
        +8

        Проблема в том, что уже существуют куча объяснений "для новичков" сетей Хопфилда, Хэмминга, Кохонена, перцептронов (особенно перцептронов!), градиентного спуска, обратного распространения ошибки, но нет (или очень мало) туториалов по практическому применению нейросетей и по типам сетей, которые имеют практическое применение. В результате обучение этой теме обычно останавливается на примитивном устаревшем и теоретическом уровне.


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


        Это будет намного интереснее, чем сухие теоретические примеры (тем более, что и так тысячи их).


        1. EmeraldSoft
          11.02.2017 21:44
          -6

          Ну суть моих статей и заключается как раз в том, что я даю людям теорию и практиктическое применение.


          1. Akon32
            11.02.2017 21:49

            Вы действительно применяли нейросеть для функции xor? Неужели это эффективно?


            1. EmeraldSoft
              11.02.2017 22:03
              -2

              С однослойной НС — нет. Для того, чтобы показать самый простой пример классификации на логической функции XOR — сойдёт многослойная НС.


  1. AlexSerbul
    11.02.2017 21:20
    -13

    Важное направление, нужно двигать и разъяснять, спасибо. Только бы не на JavaScript, а на более современном и правильном языке типа Scala — чтобы прививать любовь к правильному


    1. EmeraldSoft
      11.02.2017 21:22
      +1

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


      1. AlexSerbul
        11.02.2017 21:28
        -5

        Фреймворки бы разные еще людям объяснить полезно: Keras, Torch7, TF, Deeplearning4j с примерчиками простенькими, те же AND и XOR, как фундаментальные. Но что делать с уровнем математики у населения, сам не знаю :-)


        1. PapaBubaDiop
          11.02.2017 22:45
          -3

          А что с уровнем? Зашкаливает?


          1. AlexSerbul
            12.02.2017 00:27
            -5

            Кровь течет из ушей при демонстрации приемов линейной алгебры :-)


    1. worldmind
      11.02.2017 23:40
      +2

      Python думаю был бы идеален в данном случае


      1. Suvitruf
        12.02.2017 00:22
        +1

        На Фортране давайте.


      1. AlexSerbul
        12.02.2017 00:28
        -1

        да, с питона проще начать будет. Хотя torch7 на lua и есть подозрение, что Javascript-ом нас поддалкивают к torch ;-)


        1. Roman_Kh
          12.02.2017 11:03

          pytorch уже есть


  1. Jabher
    12.02.2017 11:18
    +3

    гхм. Я 2 месяца назад писал гораздо более объемную и полную статью про нейронные сети на JS. С объяснением того, зачем нам нужен сумматор и функция активации, как работает градиентный спуск, и с хорошим таким ноутбуком кода на RunKit-е.


    https://habrahabr.ru/company/epam_systems/blog/317050/


    И даже тогда я волновался, не пишу ли я вторичный контент.


    Перед публикацией стоит проверять, не делал ли кто-то то же самое до вас.


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


    1. EmeraldSoft
      12.02.2017 11:34
      -1

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


      1. Jabher
        12.02.2017 12:49
        +2

        в качестве общего курса для новичков стоит пользоваться известным, проверенным курсом. Например, курс Andrew Ng на coursera. На русском тоже есть большое количество известных и популярных курсов.


        В случае с моей публикацией — она затачивалась конкретно под JS-программистов, которые пытаются встать на дорожку DS, в вашем же случае идет какое-то очень странное распыление.


      1. MaximChistov
        12.02.2017 13:05

        ой да вы все «огроменные циклы статей» обещаете, оправдывая этим унылость начальных, а потом на них все и заканчивается… :(


        1. EmeraldSoft
          12.02.2017 13:07

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


  1. bitver
    12.02.2017 12:21
    +2

    Статью изначально неинтересно читать, так как непонятно зачем это вообще нужно.

    //Мой мозг мне кричит: "Здесь всё ясно, а в том коде много лишнего даже и не вздумай запоминать".
    function XOR(a,b) {
      return ( a || b ) && !( a && b );
    }
    


    Да и что за магические константы у синапсов?


    1. MaximChistov
      12.02.2017 13:06
      +1

      function XOR(a,b) {
        return ( a || b ) && !( a && b );
      }
      

      Тот самый момент, когда еще не дошел до оператора ^


      1. Sirion
        12.02.2017 13:46

        ^ — это побитовый. Логического XOR в JS нема(


      1. bitver
        12.02.2017 14:07
        +1

        Тот самый момент, когда читаешь очередной коммент с «Тот самый момент...» на протяжении 5 лет разработки на JS.
        Это логическое исключающее ИЛИ(которое описано в статье!), а не побитовое — оно даёт ясно понять с каким типом данных идёт работа (true | false).


        1. MaximChistov
          12.02.2017 14:18

          Что мешает преобразовать к

          a !=0 ^ b != 0
          

          ?


          1. MaximChistov
            12.02.2017 14:29

            Или более JS-style вариант:

            !!a ^ !!b
            


            1. bitver
              12.02.2017 14:40

              var a = true, b = false;
              (a !=0 ^ b != 0) !== true; //Как ни крути
              (!!a ^ !!b) !== true; // ^ - операция не логическая
              


          1. bitver
            12.02.2017 14:34

            Вы придираетесь не по теме, как меня научили в универе на курсах дискретки делать, я так и делаю. Это выражение понятно всем и легко читается, а ваши варианты — пляски со свойствами конкретного языка, которые ещё вспомнить надо.