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

 Теперь поговорим об активационной функции нейрона. На рисунке она указана как y(u), где u – уже известная нам величина. Эта функция нужна для вычисления значения, которое будет на выходе нейрона и пойдёт на входы других нейронов.

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



Данное чудо ограничено диапазоном значений (0; 1), поэтому отлично нам подходит. А величина y(u) будет называться выходным значением нейрона. Фух, ну вроде с минимальной мат. частью разобрались, теперь приступим к практике.
На листе бумаги всё выглядит просто замечательно, но когда дело дошло до написания кода, то возник вопрос с хранением значений нейронов.

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

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

Объясню идею на картинках: 

Тогда на помощь к нам приходит третье измерение массива, а номер матрицы отдельного слоя и будет номером этого слоя. Здесь стоит сделать отступление. Можно круто оптимизировать код, объявив одномерный массив вместо трёхмерного массива, для этого нужно написать функцию, которая будет работать с памятью. Но тут мы пишем простой код, поэтому оптимизацию не будем сильно затрагивать в этой части статьи. Ну вроде принципы хранения нужных значений в нейронах получилось объяснить. Теперь разберёмся с хранением весов связей между нейронами. Возьмём для примера вот такую сеть:  Уже зная, как структурировать в памяти нейроны, сделаем подобную таблицу для весов: Её структура совсем не сложная: например, значение веса между нейроном N1 и нейроном n1 содержится в ячейке w1-1, аналогично и с другими весами. Но опять же, такая матрица пригодна для хранения весов только между двумя первыми слоями, но ведь в сети есть ещё веса между вторым и третьим слоями. Воспользуемся уже знакомым приёмом – добавим новое измерение в массив, но с оговоркой: пускай названия строчек отображают слой нейронов слева относительно «пучка» весов, а слой нейронов справа вписывается в названия столбцов. Тогда получим для второго «пучка» весов такую таблицу:

 А всё пространство весов теперь будет выглядеть так:

 При таком хранении весов и вычислений нейронов возникает проблема высокого расхода памяти «впустую», т.к. количество нейронов на слоях может быть абсолютно любым, но массивы, которые мы делаем «имеют форму» параллелепипеда, из-за чего возникает большое количество пустых ячеек, но под которые резервируется память. Именно поэтому я не считаю свой алгоритм наиболее оптимальным и хотелось бы увидеть ваши предложения по оптимизации в комментариях)).  В этой части программно реализуем идеи, описанные в прошлый раз. В предыдущей части я представил на ваш суд несколько идей, которые позволят реализовать персептрон. На этот раз мы будем писать код. Итак, поехали! Дабы наш код можно было использовать в различных проектах, оформим его как библиотеку. Для этого создадим header — файл (пусть называется «neuro.h»). Внутри него опишем класс с основными методами: Работать будем с векторами, поэтому впишем несколько строк для их работы, но и про стандартную обвязку header'а не забудем). Вставим в начало файла следующие строки: Разберёмся с публичными функциями класса: Функция представляет собой просто конструктор класса, в который мы будем передавать данные о НС, а именно кол-во слоёв и количество нейронов в каждом из этих слоёв в виде массива. Не долго думал над названием этой функции)), но именно она отвечает за прямое распространение исходных данных по сети. Эта функция позволяет получить выходные данные с последнего слоя сети. Благодаря этой процедуре можно обучить нейросеть, используя метод обратного распространения ошибки. На этом публичные методы класса заканчиваются, сейчас остановимся на приватных полях класса: Закончим header — файл строкой:  На этом завершим header и оставим его в покое. Переходим к самому вкусному — source — файлу). Ссылка на весь код будет в конце, мы же разберём самые интересные его места. Конструктор класса изнутри выглядит так: Про функцию прямого распространения рассказать особо нечего, просто берём и считаем всё от слоя к слою: И, наконец, последнее, о чём хотелось бы рассказать, это функция вывода результата. Ну тут мы просто копируем значения из нейронов последнего слоя в массив, переданный нам в качестве параметра: На этом мы приостановимся, следующая часть будет посвящена одной единственной функции, позволяющей обучить сеть. Из-за сложности и обилия математики я решил вынести её в отдельную часть, там же мы и протестируем работу всей библиотеки в целом. Опять же, жду ваши советы и замечания в комментах. Спасибо за уделённое внимание к статье, до скорого! P.S.: Как и обещал — ссылка на исходники: GitHub