Русский
Русский
English
Статистика
Реклама

Перцептрон

Из песочницы Изобретаем велосипед или пишем персептрон на С. Часть 1 и 2

08.08.2020 20:04:04 | Автор: admin

Изобретаем велосипед или пишем персептрон на C++. Часть 1


Напишем простую библиотеку для реализации персептрона на C++




Вступление


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

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

Итак, поехали.


Немного теории


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


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


Как мы видим каждый нейрон слоя связан с каждым нейроном предыдущего слоя. А каждый узел этой сети будет называться нейроном.

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



Допустим, что в наш нейрон приходят три сигнала(х1, х2, х3), тогда для вычисления значения u нейрон складывает произведения входных сигналов на веса входов (w1, w2, w3), или проще говоря:
u = x1*w1 + x2*w2 + x3*w3
В более общем виде выражение записывается так:



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

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



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


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

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

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

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





Пусть у нас есть 8 нейронов в слое (с n1 по n8), а мы хотим хранить значения той самой суммы произведений u, вычисленного из сигмоиды значения y(u) и ошибки err, тогда воспользуемся двумерным массивом (матрицей). Смысл поля err поясню в следующей части.

Но такой вариант подходит лишь для одного слоя, а слоёв в сети немногим больше, чем один.




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


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


Ну вроде принципы хранения нужных значений в нейронах получилось объяснить. Теперь разберёмся с хранением весов связей между нейронами.


Возьмём для примера вот такую сеть:

.


Уже зная, как структурировать в памяти нейроны, сделаем подобную таблицу для весов:





Её структура совсем не сложная: например, значение веса между нейроном N1 и нейроном n1 содержится в ячейке w1-1, аналогично и с другими весами. Но опять же, такая матрица пригодна для хранения весов только между двумя первыми слоями, но ведь в сети есть ещё веса между вторым и третьим слоями. Воспользуемся уже знакомым приёмом добавим новое измерение в массив, но с оговоркой: пускай названия строчек отображают слой нейронов слева относительно пучка весов, а слой нейронов справа вписывается в названия столбцов.


Тогда получим для второго пучка весов такую таблицу:



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



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


И в заключение первой части


А на этом первая часть заканчивается, во второй части разберём программную реализацию.

Изобретаем велосипед или пишем персептрон на C++. Часть 2


В этой части программно реализуем идеи, описанные в прошлый раз.


Вступление


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


Итак, поехали!


Оформление header файла


Дабы наш код можно было использовать в различных проектах, оформим его как библиотеку. Для этого создадим header файл (пусть называется neuro.h). Внутри него опишем класс с основными методами:



class NeuralNet {public:    NeuralNet(uint8_t L, uint16_t *n);    void Do_it(uint16_t size, double *data);    void getResult(uint16_t size, double* data);    void learnBackpropagation(double* data, double* ans, double acs, double k);private:    vector<vector<vector<double>>> neurons;    vector<vector<vector<double>>> weights;    uint8_t numLayers;    vector<double> neuronsInLayers;    double Func(double in);    double Func_p(double in);    uint32_t MaxEl(uint16_t size, uint16_t *arr);    void CreateNeurons(uint8_t L, uint16_t *n);    void CreateWeights(uint8_t L, uint16_t *n);};

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



//строки ниже нужны, чтобы сказать предпроцессору о компиляции этого файла, если ранее он не был упомянут в коде#ifndef NEURO_H#define NEURO_H#include <vector> //файл для работы с векторами#include <math.h> //библиотека для работы с математикой, нужна для объявления активационной функции#include <stdint.h> //эта библиотека позволит использовать более оптимизированные типы данных, что немного сократит объём выделяемой памяти для нашего не самого оптимизированного кода.

Разберёмся с публичными функциями класса:


NeuralNet(uint8_t L, uint16_t *n);

Функция представляет собой просто конструктор класса, в который мы будем передавать данные о НС, а именно кол-во слоёв и количество нейронов в каждом из этих слоёв в виде массива.


void Do_it(uint16_t size, double *data);

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


void getResult(uint16_t size, double* data);

Эта функция позволяет получить выходные данные с последнего слоя сети.


void learnBackpropagation(double* data, double* ans, double acs, double k);

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


На этом публичные методы класса заканчиваются, сейчас остановимся на приватных полях класса:


    vector<vector<vector<double>>> neurons; //трёхмерный вектор с нейронами, который мы описывали ранее    vector<vector<vector<double>>> weights; //трёхмерный вектор с весами, его мы тоже описали в первой части    uint8_t numLayers; //количество слоёв сети    vector<double> neuronsInLayers; //вектор, хранящий количество нейронов на каждом слое/*Вообще это поле и предыдущее можно было бы и не объявлять, а брать количество слоёв и нейронов, исходя из размеров пространств весов и нейронов, но в этой статье мы не сильно затрагиваем вопросы оптимизации, этим займёмся позже*/    double Func(double in); // та самая активационная функция    double Func_p(double in); // производная той самой активационной функции    uint32_t MaxEl(uint16_t size, uint16_t *arr);// простенькая функция для поиска максимума в массиве    void CreateNeurons(uint8_t L, uint16_t *n);// эту и следующую функции использует конструктор для разметки векторов с весами и нейронами    void CreateWeights(uint8_t L, uint16_t *n);

Закончим header файл строкой:

#endif

На этом завершим header и оставим его в покое. Переходим к самому вкусному source файлу).


Код внутри source файла


Ссылка на весь код будет в конце, мы же разберём самые интересные его места.


Конструктор класса изнутри выглядит так:


NeuralNet::NeuralNet(uint8_t L, uint16_t *n) {CreateNeurons(L, n); //переразмечаем пространство нейроновCreateWeights(L, n); //переразмечаем пространство весовthis->numLayers = L;this->neuronsInLayers.resize(L);for (uint8_t l = 0; l < L; l++)this->neuronsInLayers[l] = n[l]; //в последних трёх строках заполняем все переменные класса}

Про функцию прямого распространения рассказать особо нечего, просто берём и считаем всё от слоя к слою:


void NeuralNet::Do_it(uint16_t size, double *data) {for (int n = 0; n < size; n++) { // тут вносим данные в нейроны первого слояneurons[n][0][0] = data[n]; // нулевое место отвечает за хранение входного значенияneurons[n][1][0] = Func(neurons[n][0][0]); // первое место отвечает за значение функции от входного в нейрон значения}for (int L = 1; L < numLayers; L++) { // а здесь от слоя к слою считаем входные значения каждого нейрона и значения их активационных функцийfor (int N = 0; N < neuronsInLayers[L]; N++) { double input = 0;for (int lastN = 0; lastN < neuronsInLayers[L - 1]; lastN++) {// для каждого отдельного нейрона подсчитаем сумму его входов для отправки в активационную функциюinput += neurons[lastN][1][L - 1] * weights[lastN][N][L - 1];}neurons[N][0][L] = input;neurons[N][1][L] = Func(input);}}}

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


void NeuralNet::getResult(uint16_t size, double* data) {for (uint16_t r = 0; r < size; r++) {data[r] = neurons[r][1][numLayers - 1];}}

Уход в закат


На этом мы приостановимся, следующая часть будет посвящена одной единственной функции, позволяющей обучить сеть. Из-за сложности и обилия математики я решил вынести её в отдельную часть, там же мы и протестируем работу всей библиотеки в целом.


Опять же, жду ваши советы и замечания в комментах.


Спасибо за уделённое внимание к статье, до скорого!


P.S.: Как и обещал ссылка на исходники: GitHub



Подробнее..

Изобретаем велосипед или пишем персептрон на С. Часть 3

20.08.2020 14:16:33 | Автор: admin

Изобретаем велосипед или пишем персептрон на C++. Часть 3


Реализуем обучение многослойного персептрона на C++ при помощи метода обратного распространения ошибки.




Предисловие


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


Введение


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


Немного математики


Я наткнулся на классную статью по backpropagation ссылка.


В этой статье всё хорошо расписано, поэтому я, в основном, буду повествовать о использовании тех методов в своём коде.


Вкратце объясню принцип метода на вот такой сети:


Постараюсь правильно передать смысл обучения этим способом, но если что, то поправьте в комментариях. Итак, чтобы обучить сеть, мы должны изменить значения весовых коэффициентов. Причём, изменение веса связи прямо пропорционально ошибке, которую даёт соответствующий нейрон. В той статье ошибка определяется так: $inline$d j =f(net j )(1f(net j )) k d k w kj $inline$ (1), где j номер элемента, для которого вычисляем ошибку, k номер слоя, от которого пришла ошибка, т.е. слоя правее на нашей сети.


Начнём вычислять ошибку каждого нейрона, идём с конца. Пусть на выходе сети мы ожидаем значение "O". Тогда для нейрона n6 ошибка составляет d6 = (O y)*( f(in) )*(1 f(in) ) (2), где in входное значение в
элемент n6, f(in) значение активационной функции от этого входного значения.


Теперь исходя из формулы (1), рассчитываем ошибки для нейронов слоя слева. Для n4 ошибка выглядит так:

d4 = d6 * w46 *( f(in4) ) * (1 f(in4)), где w46 вес связи между n4 и n6, in4 входное значение нейрона n4.


d5 = d6 * w56 *( f(in5) ) * (1 f(in5)), для n5.


d1 = (d4 * w14 + d5 * w15) *( f(in1) ) * (1 f(in1)), а вот так выглядит ошибка нейрона n1, у него ведь не одна связь, а две поэтому мы вносим в формулу ещё ошибку второй связи.


Тогда для n2 и n3 ошибки выглядят соответственно:


d2 = (d4 * w24 + d5 * w25) *( f(in2) ) * (1 f(in2))


d3 = (d4 * w34 + d5 * w35) *( f(in3) ) * (1 f(in3))



А сейчас, наконец, рассчитаем корректировку весов:


w46 = d6 * A * f(in4), w46 вес связи n4 и n6 нейрона, а f(in4) значение активационной функции от входного значения нейрона n4, A коэффициент скорости обучения обучения, чем он ближе к нулю тем сеть медленнее, но точнее обучается.


w56 = d6 * A * f(in5), соответственно корректировка связи элементов n5 и n6.


Составим корректировки для остальных связей:


w14 = d4 * A * f(in1)


w24 = d4 * A * f(in2)


w34 = d4 * A * f(in3)


w15 = d4 * A * f(in1)


w25 = d4 * A * f(in2)


w35 = d4 * A * f(in3)


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


Пишем код


Особые моменты функции будем рассматривать подробнее


void NeuralNet::learnBackpropagation(double* data, double* ans, double acs, double k) {  //data - массив обучающих данных, ans - массив ответов на обучающие данные, k - количество эпох обучения, acs- скорость обучения

Далее идёт цикл, отсчитывающий кол-во итераций обучения:


for (uint32_t e = 0; e < k; e++) {double* errors = new double[neuronsInLayers[numLayers - 1]]; //объявим массив для хранения ошибок выходного слоя                //Шуточное "Do_it" было заменено на "Forward"Forward(neuronsInLayers[0], data);//прогоним обучающую выборку через сетьgetResult(neuronsInLayers[numLayers - 1], errors);//получаем выходные данные после обучающей выборки

В следующем цикле вычисляем ошибки нейронов выходного слоя:


for (uint16_t n = 0; n < neuronsInLayers[numLayers - 1]; n++) {neurons[n][2][numLayers - 1] = (ans[n] - neurons[n][1][numLayers - 1]) * (neurons[n][1][numLayers - 1]) * (1 - neurons[n][1][numLayers - 1]);}

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


for (uint8_t L = numLayers - 2; L > 0; L--) {for (uint16_t neu = 0; neu < neuronsInLayers[L]; neu++) {for (uint16_t lastN = 0; lastN < neuronsInLayers[L + 1]; lastN++) {neurons[neu][2][L] += neurons[lastN][2][L + 1] * weights[neu][lastN][L] * neurons[neu][1][L] * (1 - neurons[neu][1][L]);weights[neu][lastN][L] += neurons[neu][1][L] * neurons[lastN][2][L + 1] * acs;}}}

Полностью функция выглядит так:


void NeuralNet::learnBackpropagation(double* data, double* ans, double acs, double k) {  //k - количество эпох обучения acs- скорость обученияfor (uint32_t e = 0; e < k; e++) {double* errors = new double[neuronsInLayers[numLayers - 1]];Forward(neuronsInLayers[0], data);getResult(neuronsInLayers[numLayers - 1], errors);for (uint16_t n = 0; n < neuronsInLayers[numLayers - 1]; n++) {neurons[n][2][numLayers - 1] = (ans[n] - neurons[n][1][numLayers - 1]) * (neurons[n][1][numLayers - 1]) * (1 - neurons[n][1][numLayers - 1]);}for (uint8_t L = numLayers - 2; L > 0; L--) {for (uint16_t neu = 0; neu < neuronsInLayers[L]; neu++) {for (uint16_t lastN = 0; lastN < neuronsInLayers[L + 1]; lastN++) {neurons[neu][2][L] += neurons[lastN][2][L + 1] * weights[neu][lastN][L] * neurons[neu][1][L] * (1 - neurons[neu][1][L]);weights[neu][lastN][L] += neurons[neu][1][L] * neurons[lastN][2][L + 1] * acs;}}}}}

А теперь тесты


Основные моменты для работы сети мы уже прописали. Сейчас давайте накидаем демонстрационный код:


#include <stdio.h>#include "neuro.h"int main(){        uint16_t neurons[3] = {16, 32, 10}; //в данном массиве содержится количество нейронов в каждом слое, кол-во нейронов на втором слое взято с потолка/* каждый нейрон первого слоя воспринимает значение одного "пикселя" из матрицыкаждый нейрон последнего слоя обозначает одно из десятичных цифр*/    NeuralNet net(3, neurons);    double teach[4 * 4] = {// создаём "рисунок" цифры "4" для обучения, кол-во пикселей: 4*4 = 16        1,0,1,0,        1,1,1,0,        0,0,1,0,        0,0,1,0,    };    double test[4 * 4] = {//рисуем цифру "4", но по-другому        1,0,0,1,        1,1,1,1,        0,0,0,1,        0,0,0,1,    };    double ans[10] = {0, 0, 0, 0, 1, 0, 0, 0, 0, 0,};// тут мы содержим ответы для обучающей выборки, "1" на пятом месте означает, что в тестовой выборке содержится цифра "4"    double output[10] = { 0 };// массив для выгрузки данных из сети    net.Forward(4*4, teach); // проверяем работу необученной сети    net.getResult(10, output);     for (uint8_t i = 0; i < 10; i++) printf("%d: %f \n", i, output[i]*100); //выводим результат работы    net.learnBackpropagation(teach, ans, 0.50, 1000); //обучаем сеть на выборке "test", скорость обучения: 0.5    printf("\n");    net.Forward(4 * 4, test);// проверяем работу сети после обучения    net.getResult(10, output);    for (uint8_t i = 0; i < 10; i++) printf("%d: %f \n", i, output[i]*100);    return 0;}

Получаем в выводе следующую картину:





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

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


В конце хотелось бы сказать...


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


Спасибо за уделённое статье внимание, и за комментарии к предыдущей публикации. Продублирую ссылку на файлы библиотеки.


Оставляйте свои комментарии, предложения. До скорого!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru