Pull to refresh

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

Level of difficultyEasy
Reading time6 min
Views7.2K

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

Первым шагом нужно в принципе понять практическую часть работы нейронных сетей. Для достижения поставленной задачи я решил узнать основы, и написать нейронную сеть на столько простую, на сколько это возможно. Использовать я буду язык 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;
}

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

Tags:
Hubs:
Total votes 12: ↑11 and ↓1+12
Comments14

Articles