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

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

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

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

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

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

Код

#include 
#include 
#include 
#include 

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: "< celsiusData, std::vector 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 celsiusData;
  std::vector 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: "<

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

void train(std::vector> inputValue, std::vector 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 
#include 
#include 
#include 

class NeuralNetwork
{
private:
  std::vector 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> inputValue, std::vector 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> inputValue;
  std::vector outputValue;

  double learningRate = 0.0002;
  int valueOfData = 70000;

  std::srand(std::time(nullptr));
  for (int i = 0; i < valueOfData; i++)
  {
    std::vector 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: "<

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

© Habrahabr.ru