Как стать автором
Обновить

Пример простой нейросети на С/C++

Время на прочтение 8 мин
Количество просмотров 110K
Всем привет.

Решил поделиться простым и ёмким на мой взгляд решением нейронной сети на С++.

Почему эта информация должна быть интересна?

Ответ: я старался в минимальном наборе запрограммировать работу многослойного перцептрона, да так, чтобы его можно было настраивать как душе угодно всего в нескольких строчках кода, а реализация основных алгоритмов работы на «С» позволит с лёгкостью переносить на «С» ориентированные языки(в прочем и на любые другие) без использования сторонних библиотек!

Прошу взглянуть на то, что из этого вышло


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

Исходный код вы найдёте в конце статьи, а пока по порядку.

Начнём разбор


1) Архитектура и технические подробности


многослойный перцептрон с возможностью конфигурации любого количества слоев с заданной шириной. Ниже представлен

пример конфигурации
myNeuero.cpp

    inputNeurons = 100; 	//ширина входного слоя
    outputNeurons =2;    	//ширина выходного слоя
    nlCount = 4;			//количество слоёв ( по факту их 3, указываемое число намеренно увеличено на 1
    list = (nnLay*) malloc((nlCount)*sizeof(nnLay));

    inputs = (float*) malloc((inputNeurons)*sizeof(float));
    targets = (float*) malloc((outputNeurons)*sizeof(float));

    list[0].setIO(100,20);  //установка ширины INPUTS/OUTPUTS для каждого слоя
    list[1].setIO(20,6);	//  -//-
    list[2].setIO(6,3);     //  -//-
    list[3].setIO(3,2);     //  -//- выходной слой


Обратите внимание, что установка ширины входа и выхода для каждого слоя выполняется по определённому правилу — вход текущего слоя = выходу предыдущего. Исключением является входной слой.

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

— реализация механизма обратного распространения ошибки с возможностью задания скорости обучения

myNeuero.h

 #define learnRate 0.1 

— установка начальных весов

myNeuero.h

 #define randWeight (( ((float)qrand() / (float)RAND_MAX) - 0.5)* pow(out,-0.5)) 

Примечание: если слоёв больше трёх (nlCount > 4), то pow(out,-0.5) необходимо увеличивать, чтобы при прямом прохождении сигнала его энергия не сводилась к 0. Пример pow(out,-0.2)

основа кода на С. Основные алгоритмы и хранение весовых коэффициентов реализовано в виде структуры на языке С, всё остальное является оболочкой вызывающей функции данной структуры, она же является отображением любого из слоёв взятом в отдельности

Структура слоя
myNeuero.h

    struct nnLay{
           int in;
           int out;
           float** matrix;
           float* hidden;
           float* errors;
           int getInCount(){return in;}
           int getOutCount(){return out;}
           float **getMatrix(){return matrix;}
           void updMatrix(float *enteredVal)
           {
               for(int ou =0; ou < out; ou++)
               {

                   for(int hid =0; hid < in; hid++)
                   {
                       matrix[hid][ou] += (learnRate * errors[ou] * enteredVal[hid]);
                   }
                   matrix[in][ou] += (learnRate * errors[ou]);
               }
           };
           void setIO(int inputs, int outputs)
           {
               in=inputs;
               out=outputs;
               hidden = (float*) malloc((out)*sizeof(float));

               matrix = (float**) malloc((in+1)*sizeof(float));
               for(int inp =0; inp < in+1; inp++)
               {
                   matrix[inp] = (float*) malloc(out*sizeof(float));
               }
               for(int inp =0; inp < in+1; inp++)
               {
                   for(int outp =0; outp < out; outp++)
                   {
                       matrix[inp][outp] =  randWeight;
                   }
               }
           }
           void makeHidden(float *inputs)
           {
               for(int hid =0; hid < out; hid++)
               {
                   float tmpS = 0.0;
                   for(int inp =0; inp < in; inp++)
                   {
                       tmpS += inputs[inp] * matrix[inp][hid];
                   }
                   tmpS += matrix[in][hid];
                   hidden[hid] = sigmoida(tmpS);
               }
           };
           float* getHidden()
           {
               return hidden;
           };
           void calcOutError(float *targets)
           {
               errors = (float*) malloc((out)*sizeof(float));
               for(int ou =0; ou < out; ou++)
               {
                   errors[ou] = (targets[ou] - hidden[ou]) * sigmoidasDerivate(hidden[ou]);
               }
           };
           void calcHidError(float *targets,float **outWeights,int inS, int outS)
           {
               errors = (float*) malloc((inS)*sizeof(float));
               for(int hid =0; hid < inS; hid++)
               {
                   errors[hid] = 0.0;
                   for(int ou =0; ou < outS; ou++)
                   {
                       errors[hid] += targets[ou] * outWeights[hid][ou];
                   }
                   errors[hid] *= sigmoidasDerivate(hidden[hid]);
               }
           };
           float* getErrors()
           {
               return errors;
           };
           float sigmoida(float val)
           {
              return (1.0 / (1.0 + exp(-val)));
           }
           float sigmoidasDerivate(float val)
           {
                return (val * (1.0 - val));
           };
    };


2) Применение


Тестирование проекта с набором mnist произошло удачно, удалось добиться условной вероятности распознавания рукописного текста 0,9795 (nlCount = 4, learnRate = 0.03 и несколько эпох). Основная цель теста была в проверке работоспособности нейронной сети, с чем она справилась.

Ниже мы рассмотрим работу на «условной задаче».

Исходные данные:

-2 случайных входных вектора размером в 100 значений
-нейросеть со случайной генерацией весов
-2 заданные цели

Код в функции main()

{
   //!!!________ ДЛЯ ВЫВОДА ВМЕСТО qDebug() можете использовать std::cout или std::cerr
       myNeuro *bb = new myNeuro();

     //----------------------------------INPUTS----GENERATOR-------------
         /! создаём 2 случайнозаполненных входных вектора 
        qsrand((QTime::currentTime().second()));
        float *abc = new float[100];
            for(int i=0; i<100;i++)
            {
            abc[i] =(qrand()%98)*0.01+0.01;
            }

        float *cba = new float[100];
            for(int i=0; i<100;i++)
            {
            cba[i] =(qrand()%98)*0.01+0.01;
            }

    //---------------------------------TARGETS----GENERATOR-------------
       // создаем 2 цели обучения
        float *tar1 = new float[2];
        tar1[0] =0.01;
        tar1[1] =0.99;
        float *tar2 = new float[2];
        tar2[0] =0.99;
        tar2[1] =0.01;

    //--------------------------------NN---------WORKING---------------
       // первичный опрос сети 
        bb->query(abc);
        qDebug()<<"_________________________________";
        bb->query(cba);
        
        // обучение
        int i=0;
        while(i<100000)
        {
            bb->train(abc,tar1);
            bb->train(cba,tar2);
            i++;
        }
        //просмотр результатов обучения (опрос сети второй раз)
        qDebug()<<"___________________RESULT_____________";
        bb->query(abc);
        qDebug()<<"______";
        bb->query(cba);
}

Результат работы нейронной сети

image

Итоги


Как вы видите, вызов функции query(inputs) до обучения для каждого из векторов не даёт нам судить об их отличиях. Далее, вызывая функцию train(input, target), для обучения с целью расстановки весовых коэффициентов так, чтобы нейросеть в последующем могла различать входные вектора.

После завершения обучения наблюдаем, что попытка сопоставить вектору «abc» — «tar1», а «cba» — «tar2» удалась.

Вам предоставляется возможность используя исходные коды самостоятельно протестировать работоспособность и поэкспериментировать с конфигурацией!

P.S.: данный код писался из QtCreator, надеюсь «заменить вывод» вам не составит труда, оставляйте свои замечания и комментарии.

P.P.S.: если кому интересен детальный разбор работы struct nnLay{} пишите, будет новый пост.

P.P.P.S.: надеюсь кому нибудь пригодится «С» ориентированный код для переноса на другие инструменты.

Исходники
main.cpp

#include <QCoreApplication>
#include <QDebug>
#include <QTime>
#include "myneuro.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    myNeuro *bb = new myNeuro();

     //----------------------------------INPUTS----GENERATOR-------------
        qsrand((QTime::currentTime().second()));
        float *abc = new float[100];
            for(int i=0; i<100;i++)
            {
            abc[i] =(qrand()%98)*0.01+0.01;
            }

        float *cba = new float[100];
            for(int i=0; i<100;i++)
            {
            cba[i] =(qrand()%98)*0.01+0.01;
            }

    //---------------------------------TARGETS----GENERATOR-------------
        float *tar1 = new float[2];
        tar1[0] =0.01;
        tar1[1] =0.99;
        float *tar2 = new float[2];
        tar2[0] =0.99;
        tar2[1] =0.01;

    //--------------------------------NN---------WORKING---------------
        bb->query(abc);
        qDebug()<<"_________________________________";
        bb->query(cba);

        int i=0;
        while(i<100000)
        {
            bb->train(abc,tar1);
            bb->train(cba,tar2);
            i++;
        }

        qDebug()<<"___________________RESULT_____________";
        bb->query(abc);
        qDebug()<<"______";
        bb->query(cba);


        qDebug()<<"_______________THE____END_______________";

    return a.exec();
}

myNeuro.cpp

#include "myneuro.h"
#include <QDebug>

myNeuro::myNeuro()
{
    //--------многослойный
    inputNeurons = 100;
    outputNeurons =2;
    nlCount = 4;
    list = (nnLay*) malloc((nlCount)*sizeof(nnLay));

    inputs = (float*) malloc((inputNeurons)*sizeof(float));
    targets = (float*) malloc((outputNeurons)*sizeof(float));

    list[0].setIO(100,20);
    list[1].setIO(20,6);
    list[2].setIO(6,3);
    list[3].setIO(3,2);

    //--------однослойный---------
//    inputNeurons = 100;
//    outputNeurons =2;
//    nlCount = 2;
//    list = (nnLay*) malloc((nlCount)*sizeof(nnLay));

//    inputs = (float*) malloc((inputNeurons)*sizeof(float));
//    targets = (float*) malloc((outputNeurons)*sizeof(float));

//    list[0].setIO(100,10);
//    list[1].setIO(10,2);

}

void myNeuro::feedForwarding(bool ok)
{
    list[0].makeHidden(inputs);
    for (int i =1; i<nlCount; i++)
        list[i].makeHidden(list[i-1].getHidden());

    if (!ok)
    {
        qDebug()<<"Feed Forward: ";
        for(int out =0; out < outputNeurons; out++)
        {
            qDebug()<<list[nlCount-1].hidden[out];
        }
        return;
    }
    else
    {
       // printArray(list[3].getErrors(),list[3].getOutCount());
        backPropagate();
    }
}

void myNeuro::backPropagate()
{   
    //-------------------------------ERRORS-----CALC---------
    list[nlCount-1].calcOutError(targets);
    for (int i =nlCount-2; i>=0; i--)
        list[i].calcHidError(list[i+1].getErrors(),list[i+1].getMatrix(),
                list[i+1].getInCount(),list[i+1].getOutCount());

    //-------------------------------UPD-----WEIGHT---------
    for (int i =nlCount-1; i>0; i--)
        list[i].updMatrix(list[i-1].getHidden());
    list[0].updMatrix(inputs);
}

void myNeuro::train(float *in, float *targ)
{
    inputs = in;
    targets = targ;
    feedForwarding(true);
}

void myNeuro::query(float *in)
{
    inputs=in;
    feedForwarding(false);
}

void myNeuro::printArray(float *arr, int s)
{
    qDebug()<<"__";
    for(int inp =0; inp < s; inp++)
    {
        qDebug()<<arr[inp];
    }
}

myNeuro.h

#ifndef MYNEURO_H
#define MYNEURO_H
#include <iostream>
#include <math.h>
#include <QtGlobal>
#include <QDebug>

#define learnRate 0.1
#define randWeight (( ((float)qrand() / (float)RAND_MAX) - 0.5)* pow(out,-0.5))
class myNeuro
{
public:
    myNeuro();
	
    struct nnLay{
           int in;
           int out;
           float** matrix;
           float* hidden;
           float* errors;
           int getInCount(){return in;}
           int getOutCount(){return out;}
           float **getMatrix(){return matrix;}
           void updMatrix(float *enteredVal)
           {
               for(int ou =0; ou < out; ou++)
               {

                   for(int hid =0; hid < in; hid++)
                   {
                       matrix[hid][ou] += (learnRate * errors[ou] * enteredVal[hid]);
                   }
                   matrix[in][ou] += (learnRate * errors[ou]);
               }
           };
           void setIO(int inputs, int outputs)
           {
               in=inputs;
               out=outputs;
               hidden = (float*) malloc((out)*sizeof(float));

               matrix = (float**) malloc((in+1)*sizeof(float));
               for(int inp =0; inp < in+1; inp++)
               {
                   matrix[inp] = (float*) malloc(out*sizeof(float));
               }
               for(int inp =0; inp < in+1; inp++)
               {
                   for(int outp =0; outp < out; outp++)
                   {
                       matrix[inp][outp] =  randWeight;
                   }
               }
           }
           void makeHidden(float *inputs)
           {
               for(int hid =0; hid < out; hid++)
               {
                   float tmpS = 0.0;
                   for(int inp =0; inp < in; inp++)
                   {
                       tmpS += inputs[inp] * matrix[inp][hid];
                   }
                   tmpS += matrix[in][hid];
                   hidden[hid] = sigmoida(tmpS);
               }
           };
           float* getHidden()
           {
               return hidden;
           };
           void calcOutError(float *targets)
           {
               errors = (float*) malloc((out)*sizeof(float));
               for(int ou =0; ou < out; ou++)
               {
                   errors[ou] = (targets[ou] - hidden[ou]) * sigmoidasDerivate(hidden[ou]);
               }
           };
           void calcHidError(float *targets,float **outWeights,int inS, int outS)
           {
               errors = (float*) malloc((inS)*sizeof(float));
               for(int hid =0; hid < inS; hid++)
               {
                   errors[hid] = 0.0;
                   for(int ou =0; ou < outS; ou++)
                   {
                       errors[hid] += targets[ou] * outWeights[hid][ou];
                   }
                   errors[hid] *= sigmoidasDerivate(hidden[hid]);
               }
           };
           float* getErrors()
           {
               return errors;
           };
           float sigmoida(float val)
           {
              return (1.0 / (1.0 + exp(-val)));
           }
           float sigmoidasDerivate(float val)
           {
                return (val * (1.0 - val));
           };
    };

    void feedForwarding(bool ok);
    void backPropagate();
    void train(float *in, float *targ);
    void query(float *in);
    void printArray(float *arr,int s);

private:
    struct nnLay *list;
    int inputNeurons;
    int outputNeurons;
    int nlCount;

    float *inputs;
    float *targets;
};

#endif // MYNEURO_H



UPD:

Исходники для проверке на mnist лежат по
ссылке
1) Проект
«github.com/mamkin-itshnik/simple-neuro-network»
Тут же имеется графическое описание работы. Если кратко то при опросе сети тестовыми данными, вам выводится значение каждого из выходных нейронов (10 нейронов соответствует цифрам от 0 до 9). Для принятия решения о изображенной цифре, необходимо знать индекс максимального нейрона. Цифра = индекс + 1 (не забываем откуда нумеруются значения в массивах))
2)MNIST
«www.kaggle.com/oddrationale/mnist-in-csv» (при необходимости использования меньшего dataset, просто ограничьте счётчик while при чтении CSV файла ПС: пример на гит имеется)
Теги:
Хабы:
+7
Комментарии 27
Комментарии Комментарии 27

Публикации

Истории

Работа

Программист С
41 вакансия
Data Scientist
58 вакансий
QT разработчик
15 вакансий
Программист C++
128 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн