Простейшая нейронная сеть, мой опыт и выводы

—

от автора

Недавно у меня возник резкий интерес к шахматным движкам, и желание разработать подобный. Писать конечно же нужно по последним веяниям моды с использованием оценки нейросети, но вот незадача, подобного опыта да и знаний у меня не имеется. Я собираюсь написать серию статей о моем тернистом пути изучения нейронных сетей. Постараюсь сразу расписывать все ответы на возникшие у меня вопросы. Основная цель — понять суть самым маленьким. Данная статья будет первой и надеюсь не последней.

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

Максимально простой задачей будет написать нейронную сеть, которая конвертирует градусы цельсия в градусы фаренгейта. Подобная нейронная сеть будет иметь всего один вес и смещение если посмотреть на формулу Т (° F) = Т (° C) × 1,8 + 32 . В идеале, после обучения наша нейронная сеть должна иметь вес 1.8 и смещение 32. Я буду использовать метод градиентного спуска.

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

class NeuralNetwork { private:   float weight;   float bias; public:   NeuralNetwork(){     weight = 1.0;     bias = 1.0;   } };

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

float valueFahrenheit(float valueCelsius){     return valueCelsius*weight + bias; }

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

В переменной result будем хранить результат работы нашей немного бессмысленной нейросети, для оценки требуемых изменений веса и смещения. В переменную error поместим разницу полученного и ожидаемого значения. Градиент веса учитывается в зависимости от величины отклонения(в нашем случае error) и входного значения celsiusData[i]. Градиент же смещения будет приравниваться только к величине ошибки. Это различие связано с тем, что вес определяет степень влияния каждого нейрона (не будем обращать внимание на то, что он у нас один), вес умножается на входное значение, и нам нужно корректировать веса, чтобы соответствовать данным обучения. С другой стороны, смещения является дополнительным параметром и не связано с входным значением. От веса и смещения отнимаем произведение нужных градиентов на скорость обучения. Отнимаем мы, а не прибавляем, так как градиент по сути показывает нам направление наискорейшего роста функции потерь, а мы стремимся как раз к обратному.

void train(std::vector<float> celsiusData, std::vector<float> fahrenheitData, float learningRate){     for (int i = 0; i < celsiusData.size(); i++)     {       float result = valueFahrenheit(celsiusData[i]);       float error = result - fahrenheitData[i];       float gradientWeight = error * celsiusData[i];       float gradientBias = error;        weight -= learningRate*gradientWeight;       bias -= learningRate*gradientBias;     }   }

Остается сгенерировать данные для примера.

std::srand(std::time(nullptr)); for (int i = 0; i < valueOfData; i++) {   int value = std::rand()%200-100;   celsiusData.push_back(value);   fahrenheitData.push_back(value*1.8 + 32); }

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

int main(){   NeuralNetwork mynn;    std::vector<float> celsiusData;   std::vector<float> fahrenheitData;    float learningRate = 0.025;   int valueOfData = 10000;    std::srand(std::time(nullptr));   for (int i = 0; i < valueOfData; i++)   {     int value = std::rand()%200-100;     celsiusData.push_back(value);     fahrenheitData.push_back(value*1.8 + 32);   }      mynn.train(celsiusData,fahrenheitData,learningRate);    float testCount = 25.0;   std::cout<<"Degrees Celsius: "<<testCount<<"\n"<<"Degrees Fahrenheit: "<<mynn.valueFahrenheit(testCount);      return 0; }

В результате получаем nan, ищем ошибку. Первое, что мне пришло в голову, это проверить значение веса и смещения при каждой итерации. Выясняется что наши вес и смещение улетают в бесконечность. После некоторых поисков я узнал, что данное явление называется взрывом градиента(Gradient Explosion) и чаще всего появляется при неправильном подборе начальных весов или скорости обучения. После добавления пары ноликов после точки в скорости обучений проблема решилась. Не буду утруждать себя слишком доскональным подбором скорости обучения и количества итераций обучения, оптимальные значения подобранные на скорую руку: learningRate = 0.00025, valueOfData = 100000. После обучения вес и смещение получили такие значения: Weight: 1.80001, Bias: 31.9994.

Попробуем повысить точность, заменив везде float на double. Это оказалось правильным решением, теперь при правильном количестве итераций вес всегда принимает значение 1.8 и смещение 32.

Весь код кому интересно:

Код
#include <iostream> #include <vector> #include <ctime> #include <cmath>  class NeuralNetwork { private:   double weight;   double bias; public:   NeuralNetwork(){     weight = 1.0;     bias = 1.0;   }    double valueFahrenheit(double valueCelsius){     return valueCelsius*weight + bias;   }    void printValue(){     std::cout<<"Weight: "<<weight<<"\n"<<"Bias: "<<bias<<"\n";   }    void train(std::vector<double> celsiusData, std::vector<double> fahrenheitData, double learningRate){     for (int i = 0; i < celsiusData.size(); i++)     {       double result = valueFahrenheit(celsiusData[i]);       double error = result - fahrenheitData[i];       double gradientWeight = error * celsiusData[i];       double gradientBias = error;        weight -= learningRate*gradientWeight;       bias -= learningRate*gradientBias;       //printValue();     }   } };  int main(){   NeuralNetwork mynn;    std::vector<double> celsiusData;   std::vector<double> fahrenheitData;    double learningRate = 0.00025;   int valueOfData = 60000;    std::srand(std::time(nullptr));   for (int i = 0; i < valueOfData; i++)   {     int value = std::rand()%200-100;     celsiusData.push_back(value);     fahrenheitData.push_back(value*1.8 + 32);   }      mynn.train(celsiusData,fahrenheitData,learningRate);    double testCount = 1000.0;   std::cout<<"Degrees Celsius: "<<testCount<<"\n"<<"Degrees Fahrenheit: "<<mynn.valueFahrenheit(testCount)<<"\n";   mynn.printValue();      return 0; }

Теперь можно и попробовать сделать нахождение коэффициентов функции y = a*7 + b*3 + c*5 + 32. Переменную одного веса поменяем на вектор, и в тренировки добавим обновление каждого нейрона. Также теперь функция тренировки будет принимать вектор из векторов, так как у нас несколько коэффициентов. Упростим код для большей читабельности. В итоге наша функция примет такой вид:

void train(std::vector<std::vector<double>> inputValue, std::vector<double> outputValue, double learningRate){     for (int i = 0; i < outputValue.size(); i++)     {       double result = expectedValue(inputValue[i][0], inputValue[i][1], inputValue[i][2]);       double error = result - outputValue[i];       weight[0] -= learningRate * error * inputValue[i][0];       weight[1] -= learningRate * error * inputValue[i][1];       weight[2] -= learningRate * error * inputValue[i][2];       bias -= learningRate*error;     }   }

Обновляем генерацию данных для обучения, настраиваем скорость обучения и наслаждаемся.

Код
#include <iostream> #include <vector> #include <ctime> #include <cmath>  class NeuralNetwork { private:   std::vector<double> weight;   double bias; public:   NeuralNetwork(){     weight = {1.0,1.0,1.0};     bias = 1.0;   }    double getWeight(int value){     return weight[value];   }   double getBias(){     return bias;   }    double expectedValue(double a, double b, double c){     return a*weight[0] + b*weight[1] + c*weight[2] + bias;   }    void train(std::vector<std::vector<double>> inputValue, std::vector<double> outputValue, double learningRate){     for (int i = 0; i < outputValue.size(); i++)     {       double result = expectedValue(inputValue[i][0], inputValue[i][1], inputValue[i][2]);       double error = result - outputValue[i];       weight[0] -= learningRate * error * inputValue[i][0];       weight[1] -= learningRate * error * inputValue[i][1];       weight[2] -= learningRate * error * inputValue[i][2];       bias -= learningRate*error;     }   } };  double targetFunction(double a, double b, double c){   return a*7 + b*3 + c*5 + 32; }  int main(){   NeuralNetwork mynn;    std::vector<std::vector<double>> inputValue;   std::vector<double> outputValue;    double learningRate = 0.0002;   int valueOfData = 70000;    std::srand(std::time(nullptr));   for (int i = 0; i < valueOfData; i++)   {     std::vector<double> input;     input.push_back((double)(std::rand()%200-100)/10);     input.push_back((double)(std::rand()%200-100)/10);     input.push_back((double)(std::rand()%200-100)/10);     inputValue.push_back(input);     outputValue.push_back(targetFunction(inputValue[i][0],                                          inputValue[i][1],                                         inputValue[i][2]));   }      mynn.train(inputValue, outputValue,learningRate);    std::cout<<"Weight 0: "<<mynn.getWeight(0)<<"\n"<<             "Weight 1: "<<mynn.getWeight(1)<<"\n"<<             "Weight 2: "<<mynn.getWeight(2)<<"\n"<<             "Bias: "<<mynn.getBias()<<"\n";      return 0; }

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


ссылка на оригинал статьи https://habr.com/ru/articles/807411/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *