В последнее время всё чаще стали появляться статьи о машинном обучении и о нейронных сетях. «Нейронная сеть написала классическую музыку», «Нейронная сеть распознала стиль по интерьеру», нейронные сети научились очень многому, и на волне возрастющего интереса к этой теме я решил сам написать хотя бы небольшую нейронную сеть, не имея специальных знаний и навыков.

К своему большому удивлению, я не нашел простейших и прозрачных примеров а-ля «Hello world». Да, есть coursera и потрясающий Andrew Ng, есть статьи про нейронные сети на хабре (советую остановиться тут и прочитать, если не знаете самых основ), но нет простейшего примера с кодом. Я решил создать перцептрон для распознования «AND» или «OR» на своем любимом языке C++. Если вам интересно, добро пожаловать под кат.

Итак, что же нам потребуется для создания такой сети:
1) Основные знания C++.
2) Библиотека линейной алгебры Armadillo.
В ArchLinux она ставится просто:
yaourt -S armadillo

Создадим два файла: CMakeLists.txt и Main.cpp.

CMakeLists.txt отвечает за конфигурацию проекта и содержит следующий код:
project(Perc)
cmake_minimum_required(VERSION 3.2)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set(CMAKE_BUILD_TYPE Debug)
set(EXECUTABLE_NAME "Perc")
file(GLOB SRC
    "*.h"
    "*.cpp"
)

#Subdirectories
option(USE_CLANG "build application with clang" ON)

find_package(Armadillo REQUIRED)
include_directories(${ARMADILLO_INCLUDE_DIRS})

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin")
add_executable(${EXECUTABLE_NAME} ${SRC} )
TARGET_LINK_LIBRARIES(  ${EXECUTABLE_NAME}  ${ARMADILLO_LIBRARIES} )

Main.cpp:
#include <iostream>
#include <armadillo>

using namespace std;
using namespace arma;

int main(int argc, char** argv)
  {
  mat A = randu<mat>(4,5);
  mat B = randu<mat>(4,5);
  
  cout << A*B.t() << endl;
  
  return 0;
  }

Это тестовый пример для того, чтобы проверить, все ли правильно настроено.
cmake
make
./bin/NeuroBot

Если все работает, то продолжаем!

Как же нейронная сеть работает и понимает, что есть AND а что есть OR? Так она выглядит:



Строго говоря, это лишь нейрон, но в то же время это и основной концепт сети. Обо всем по порядку:
x1 и x2 и x...- наши входные данные. Возьмем логическое «AND»



Наши входные данные — A и B, то есть матрица 4 х 2, так как с матрицами удобнее работать.

w1 и w2 — «веса», это то, что нейронная сеть и будет обучать. Обычно весов на один больше чем входов, в нашем случае их 3 ( + биас).

Опять матрица: 3x1.

Y — выход, это наш результат, он будет полностью совпадать с Q. Матрица 4х1. Матрицы очень удобно использовать с векторизацией.

Ячейка нейрона — это нейрон, который будет учить w1 и w2. В нашем случае это будет логистическая регрессия. Для обучения w1 и w2 мы будем использовать алгоритм градиентного спуска.

Почему логистическая регрессия и градиентный спуск? Логистическая регрессия используется потому, что это логическая задача 0 / 1. Логистическа регрессия (сигмоида) строит гладкую монотонную нелинейную функцую, имеющую форму буквы «S»:

image

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

image

На этом теоретическая часть заканчивается, перейдем к практике!

Итак, алгоритм следующий:
1) Задаем на вход данные
const int n = 2; //Количество нейронов
    const int epoches = 100; //Количество эпох, сколько раз мы "подгоняем" w1 и w2
    double lr = 1.0; //Коэффициент обучения
    mat samples({
            0.0, 0.0, 1.0,
            1.0, 0.0, 1.0,
            0.0, 1.0, 1.0,
            1.0, 1.0, 1.0
        });
    samples.set_size(4, 3);
    //Ответы
    mat targets{0.0, 0.0, 0.0, 1.0};
    targets.set_size(4, 1);
    mat w; w.set_size(3,1);
    //Случайные весы от -1 до 1
    w.transform([](double val)
    {
        double f = (double)rand() / RAND_MAX;
        val= 1.0 + f * (-1.0 - 1.0);
        return val;
    });


2) Пока количество эпох не подошло к концу (альтернативный способ: сравнивать заготовленные ответы с полученными и остановиться при первом совпадении), умножаем веса на входные данные image, применяем логистическую регрессию (сигмоида — sig), image подправляем веса с помощью градиентного спуска.
for(int i = 0; i < epoches; i++)
    {
        mat z = samples * w; //Summator
        auto outputs = sig(z);
        //Gradient Descend
        w -= (lr*((outputs - targets) % sig_der(outputs)).t() * (samples) / samples.size ()).t();
        std::cout << outputs << std::endl << std::endl;
    }

3) В конце запускаем активационную функцию (Аксон), округляем матрицу и выводим результат.
 //Activate function
    mat a = samples * w;
    mat result = round(sig(a));
    std::cout << result;

Перцептрон готов. Измените Y на «OR» и убедитесь, что все правильно работает.

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

Ссылка на Main.cpp gist.github.com/Warezovvv/0c1e25723be1e600d8f2
Ссылка на источник иллюстраций: robocraft.ru/blog/algorithm/558.html

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


  1. AndrewNikolaevich
    24.08.2015 11:14
    +1

    Насколько актуально программирование подобных сетей на таких языках как Java, C# и на им подобных? Или производительность настолько критична, что требуется С/С++?


    1. GavriKos
      24.08.2015 12:02
      -2

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


    1. grossws
      24.08.2015 14:13

      Иногда — да. И тут же хочется icc, AVX2/AVX-512 и подобных вещей, позволяющих выжать ещё немного производительности.

      Для начальных же экспериментов часть хватает python'а c нормально собранным numpy (как минимум, с каким-нибудь blas, ускоряет работу в 2-3 раза, что вполне ощутимо).


  1. kirichenko
    24.08.2015 11:19
    +5

    Я решил создать перцептрон для распознования «AND» или «OR» на своем любимом языке C++

    Ну и где class Perceptron или подобная конструкция?


  1. ls1
    24.08.2015 11:23
    +4

    Логистическа регрессия (сигмоида) строит гладкую монотонную нелинейную функцую, имеющую форму буквы «S»:
    image
    Иллюстрация шикарна


  1. zo_oz
    24.08.2015 11:30
    +6

    Статья откуда взяты картинки (http://robocraft.ru/blog/algorithm/558.html, не сочтите за рекламу, сам нашел 2 минуты назад в гугле) написана гораздо лучше, а главное там написан список ссылок, как-то невежливо…


    1. ls1
      24.08.2015 11:36

      Ах вон оно что, то-то я понять не могу что за обрывки мыслей иллюстрированные случайными картинками


  1. shtorman
    24.08.2015 13:48
    +1

    Согласен, с zo_oz. Практическая реализация расписана — это хорошо, но вставьте ссылочки на статью с теорией, тем более что картинки не авторские!
    И — да, мне понравилась статья, и с удовольствием прочел бы о реализации многослойного перцептрона на примере XOR!


  1. Warezovvv
    24.08.2015 14:00

    Теория была написана мной с нуля, поэтому она выглядит как обрывки мыслей.
    Источник на картинки я вставил.
    Класс Perceptron я не стал расписывать, ведь я хотел всего лишь показать как с чистого листа сделать свою маленькую нейронную сеть без каких либо конструкций.
    Мне правда очень приятно, что кому то понравилась статья, ведь это мой первый опыт! Большое спасибо!


  1. BalinTomsk
    24.08.2015 23:06

    ---(outputs — targets) % sig_der(outputs)

    это что деление векторов? Это как?

    В теории матриц нет понятия «деления матрицы», матрицы можно только умножать.


    1. Warezovvv
      24.08.2015 23:20

      Я вижу, вы не попробовали данный пример и не вникли в градиентный спуск. Это поэлементное умножение.
      arma.sourceforge.net/docs.html#operators


    1. BalinTomsk
      25.08.2015 00:15

      Не смотря что тоже люблю C++ переписал на TSQL.

      Считает 3 секунды.

       create function sigma(@x float) returns float as begin return (1.0 / (1 + exp(-1.0 * @x))) end;
       create function sig_der(@x float) returns float as begin return @x * (1.0 - @x) end;
      GO
      
      declare @neurons int = 2;
      declare @epoches int = 100;
      declare @koef_edication float = 1.0;
      declare @mat_samples table (val1 float, val2 float, val3 float, id int not null identity(1,1) primary key);
      
      INSERT INTO @mat_samples( val1, val2, val3 ) VALUES
      (0.0, 0.0, 1.0),
      (1.0, 0.0, 1.0),
      (0.0, 1.0, 1.0),
      (1.0, 1.0, 1.0)
      
      declare @mat_targets table (val float, id int not null identity(1,1) primary key);
      INSERT INTO @mat_targets( val ) VALUES (0.0), (0.0), (0.0), (1.0)
       
       declare @w table (val1 float, val2 float, val3 float);		--  w.set_size(3,1);
       INSERT INTO @w (val1, val2, val3) SELECT 1.0 + (rand()/0.9999999) * (-1.0 - 1.0), 1.0 + (rand()/0.9999999) * (-1.0 - 1.0), 1.0 + (rand()/0.9999999) * (-1.0 - 1.0);
       
      declare @size_sample int = 3 * (select count(*) from @mat_samples)
      while @epoches > 0
       begin
      	declare @z table (val float);
      	insert into @z (val)				-- Summator
      		select s.val1 * w.val1 + s.val2 * w.val2 + s.val3 * w.val3 from @mat_samples s, @w w
      
      	declare @output table (val float, id int not null identity(1,1) primary key);
      	insert into @output (val)			--  auto outputs = sig(z);
      		select dbo.sigma(val) from @z
      
      	-- Gradient Descend
      	update o set o.val = cast(n.val / nullif(dbo.sig_der(o.val), 0.0) as int) from @output o 
      		join (select o.val - t.val as val, o.id from @output o join @mat_targets t on o.id = t.id) n on o.id = n.id
          
      	declare @rs table (val1 float, val2 float, val3 float);
      	insert into @rs 
      	  select sum(s.val1*t.val1 / @size_sample), sum(s.val2*t.val2 / @size_sample), sum(s.val3*t.val3 / @size_sample) from @mat_samples s ,  (	
      	    select sum(val1) as val1, sum(val2) as val2, sum(val3) as val3 from (
      				select val as val1, 0 as val2, 0 as val3 from @output where id = 1
      					union all
      				select 0, val, 0 from @output where id = 2
      					union all 
      				select 0, 0, val from @output where id = 3
      	    ) k ) t
      	update w set w.val1=@koef_edication * (w.val1-r.val1), w.val2=@koef_edication *(w.val2-r.val2), w.val3=@koef_edication *(w.val3-r.val3) from @w w, @rs r
      	set @epoches = @epoches - 1
       end
      
       select round(sum(w.val1*s.val1), 0), round(sum(w.val2*s.val2), 0), round(sum(w.val3*s.val3), 0) from @w w, @mat_samples s
      
      


  1. intermed
    25.08.2015 05:41

    А вот моя реализация перцептрона розенблатта для браузера на JavaScipt http://pierceptio.appspot.com/. Делал как курсовой лет 5 назад, переписывал код из этой статьи http://habrahabr.ru/post/140495/


  1. fareloz
    25.08.2015 13:35

    «я не нашел простейших и прозрачных примеров а-ля «Hello world»»
    И чтобы исправить это Вы написали статью с кучей графиков и сторонней библиотекой (с которой нужно разбираться). Действительно думаете, что это «Hello World»? IMHO пару кастомных классов — это должно быть пределом для этой статьи.