Пользователь
6,4
рейтинг
24 августа 2015 в 10:09

Разработка → Однослойный перцептрон для начинающих из песочницы tutorial

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

К своему большому удивлению, я не нашел простейших и прозрачных примеров а-ля «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
@Warezovvv
карма
13,0
рейтинг 6,4
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

  • +1
    Насколько актуально программирование подобных сетей на таких языках как Java, C# и на им подобных? Или производительность настолько критична, что требуется С/С++?
    • –2
      Зависит от размера нейронной сети. Но сомневаюсь что вам будет нужна такая нейросеть, в которой в данной задаче плюсы дадут реальный прирост. Так что для экспериментов — смело можно писать на чем угодно.
    • 0
      Иногда — да. И тут же хочется icc, AVX2/AVX-512 и подобных вещей, позволяющих выжать ещё немного производительности.

      Для начальных же экспериментов часть хватает python'а c нормально собранным numpy (как минимум, с каким-нибудь blas, ускоряет работу в 2-3 раза, что вполне ощутимо).
  • +5
    Я решил создать перцептрон для распознования «AND» или «OR» на своем любимом языке C++

    Ну и где class Perceptron или подобная конструкция?
  • +4
    Логистическа регрессия (сигмоида) строит гладкую монотонную нелинейную функцую, имеющую форму буквы «S»:
    image
    Иллюстрация шикарна
  • +6
    Статья откуда взяты картинки (http://robocraft.ru/blog/algorithm/558.html, не сочтите за рекламу, сам нашел 2 минуты назад в гугле) написана гораздо лучше, а главное там написан список ссылок, как-то невежливо…
    • 0
      Ах вон оно что, то-то я понять не могу что за обрывки мыслей иллюстрированные случайными картинками
  • +1
    Согласен, с zo_oz. Практическая реализация расписана — это хорошо, но вставьте ссылочки на статью с теорией, тем более что картинки не авторские!
    И — да, мне понравилась статья, и с удовольствием прочел бы о реализации многослойного перцептрона на примере XOR!
  • 0
    Теория была написана мной с нуля, поэтому она выглядит как обрывки мыслей.
    Источник на картинки я вставил.
    Класс Perceptron я не стал расписывать, ведь я хотел всего лишь показать как с чистого листа сделать свою маленькую нейронную сеть без каких либо конструкций.
    Мне правда очень приятно, что кому то понравилась статья, ведь это мой первый опыт! Большое спасибо!
  • 0
    ---(outputs — targets) % sig_der(outputs)

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

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

    • 0
      Я вижу, вы не попробовали данный пример и не вникли в градиентный спуск. Это поэлементное умножение.
      arma.sourceforge.net/docs.html#operators
    • 0
      Не смотря что тоже люблю 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
      
      
  • 0
    А вот моя реализация перцептрона розенблатта для браузера на JavaScipt http://pierceptio.appspot.com/. Делал как курсовой лет 5 назад, переписывал код из этой статьи http://habrahabr.ru/post/140495/
  • 0
    «я не нашел простейших и прозрачных примеров а-ля «Hello world»»
    И чтобы исправить это Вы написали статью с кучей графиков и сторонней библиотекой (с которой нужно разбираться). Действительно думаете, что это «Hello World»? IMHO пару кастомных классов — это должно быть пределом для этой статьи.
  • –2
    Нейронная сеть вроде целительства — если есть пациент, который хочет верить, то появится и целитель, который будет исцелять снова и снова. Результат будет, но не у заказчика, а у исполнителя в кармане.

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