Привет еще раз ребята!!! Сегодня мы собираемся попробовать реализацию нейронной сети с использованием C++ без внешней библиотеки. Я знаю, что это слишком много, но я хотел посмотреть, как все работает за кулисами, и это было бы хорошей кривой для нас, но на самом деле используйте хорошие библиотеки, такие как Tensorflow, OpenNN или MLPack и т. д.

Для этого проекта мы будем использовать компилятор C++11 и g++, а набор тестовых данных будет XOR, который настолько прост, насколько мы можем, чтобы мы понимали работу с нейронной сетью.

int main()
{
 NeuralNetwork neural_network;
 size_t hidden_layer = 5;
 neural_network.addLayer({{"type", LayerType::INPUT}, {"size", 2}});

 for (size_t i = 0; i < hidden_layer; i++)
  neural_network.addLayer({{"type", LayerType::HIDDEN}, {"size", 10}, {"activation", ActivationFunction::SIGMOID}});

 neural_network.addLayer({{"type", LayerType::OUTPUT}, {"size", 1}, {"activation", ActivationFunction::SIGMOID}});
 neural_network.autogenerate();

 //Creating DataSet
 DataSet data("../xor.txt");
 data.split(0.8);

 //Shaking Tree
 // Shakingtree algo;
 // algo.setNeuralNetwork(&neural_network);
 // algo.setDataSet(&data);
 // algo.mapParameters();

 BackPropagation algo;
 algo.setBatchSize(10);
 LEARNING_RATE = 5.5;
 algo.setNeuralNetwork(&neural_network);
 algo.setDataSet(&data);

 //Init the main training loop (nb: the goal is to lower the score, score = loss)
 double reduce_amplitude = 1.1;
 int reduce_schedule = 50;
 int iteration = 50000;
 int validation_at = 10;
 double mintest = 1;
 int i = 0;
 clock_t t = clock();
 algo.backpropagate(data.getIns(TRAIN), data.getOuts(TRAIN));

 std::thread t1([&]
       {
        while (i < iteration)
        {
         //For Backpropagation: A batch is read by the validator, which then sends it to the neural network for computation and apply gradient.
         //For ShakingTree: The general concept is to test random parameters and keep the good ones, depending on activity.
         algo.backpropagate(data.getIns(TEST), data.getOuts(TEST));
         // Uncomment minimize() when testing prediction socre.
         // algo.minimize();
         // Validate when reach at validation point.
         if (i % validation_at == 0)
         {

          std::cout << "output:" << neural_network.outputString() << std::endl;
          //    std::cout << "output:" << neural_network.toString() << std::endl;

          algo.backpropagate(data.getIns(TRAIN), data.getOuts(TRAIN));
          // Dectecting prediction score.
          //    double strain = neural_network.predictAllForScore(data, TRAIN);
          //    double stest = neural_network.predictAllForScore(data, TEST);
          //    mintest = stest < mintest ? stest : mintest;
          //    std::cout << "itteration:" << i << "\n"
          //     << "test_score:" << stest << "\ntrain_score:" << strain << "\nbest_test_score:" << mintest << "\n"
          //     << std::endl;
         }
         if (i % reduce_schedule == 0)
         {
          LEARNING_RATE = LEARNING_RATE * reduce_amplitude;
          //    cout << LEARNING_RATE << endl;
         }
         i++;
        }
       });

 Thread_guard tg(t1);
 return 0;
}

Чтобы разбить приведенную выше реализацию в основной функции, мы создаем экземпляр класса нейронной сети и определяем входной, скрытый и выходной слои, вы можете решить, что числа могут увеличить их размер, а используемая нами функция — сигмовидная, мы также можем использовать reul и линейный, хотя после этого мы читаем данные из файла xor.txt и разбиваем данные на обучение и тестируем значение для разделения обучения должно быть между 1 и 0, и чем больше данных обучения вы передаете, тем больше данных обучения у вас будет, после что мы можем использовать либо алгоритм обратного распространения, либо алгоритм встряхивания дерева, например, мы используем обратное распространение.

По определению:

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

Работающий:

У нас есть результаты нашей работы и точный прогноз (цель). Между этими двумя мы можем предсказать ошибку (ошибка = цель — результат). Кроме того, поскольку все можно дифференцировать и это всего лишь ряд математических операций, таких как y = f(x), мы можем вычислить частную производную ошибки относительно весов. (в основном Δerr/Δw).

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

(новый_вес) будет (старый_вес — Δerr/Δw).

Блок «если» требует цели (значения, которые мы пытаемся узнать), а блок «иначе» требует сохранения в памяти части вычислений, чтобы применить цепное правило на всех других уровнях.

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

строку в разделе if с dw[i] =…), цепное правило помещает err/w в три операции, давая трем значениям d1, d2 и (d3) предыдущий результат, который вы получили в коде. В сущности, dw[i] = err/w.

Имена функций призваны упростить их чтение и понимание.

Наша цель:

Желаемый результат — превзойти алгоритм обратного распространения ошибки. Как работает бэкпроп? Сделайте прямой проход, рассчитайте потери, а затем используйте сложную математику для оценки градиентов. Затем повторите.

Можно ли изменить веса нейронной сети без вычисления потерь с помощью прямого прохода? Что ж, бывают моменты, когда вы должны оценить, что вы делаете и как это влияет на потери (1)+(2), поэтому главное, от чего я хотел избавиться, — это вычисление градиентов (3).

Однако мне все еще нужен механизм для регулировки веса. Хорошо, некоторые люди пытались использовать генетические алгоритмы; это работает, но это действительно случайно, и, естественно, чем больше веса, тем сложнее найти правильные значения… но то же самое верно и для исчезающих градиентов. Мы знаем, что градиенты дают хорошую оценку того, как настроить веса нейронной сети. Основная цель ShakingTree — получить аппроксимацию градиента из случайных значений без явного вычисления градиентов (ИЛИ чего-либо, что было бы по крайней мере столь же превосходным, как градиенты).

Имеет ли смысл быстро пробовать случайные значения? Или предпочтительнее вычислять градиенты постепенно? Можно ли быстро вычислить «случайные» значения, превосходящие градиенты? Я сделал попытку ответить на это.

Я пробовал и «простой», и «сложный» подход.

  1. Основной метод
  • Оценить оценку с текущими весами в части набора данных
  • Сохранить текущие веса
  • Применяйте новые веса случайным образом (амплитуда, распределение, количество весов и т. д., многие гиперпараметры)
  • Переоценить оценку по (другой?) части набора данных
  • Если новая оценка наихудшая, повторно примените старые веса.

2. Сложный метод

  • Оцените счет с текущими весами
  • Создайте дельту, которая будет применяться ко всем весам (эта дельта должна действовать как градиенты)
  • Примените дельта-вес, оцените его, сохраните дельта-счет и дельта-вес
  • Теперь технически вы можете оценить вид f(delta_weight) = delta_score (именно на это нацелены градиенты!)
  • Каждые N итераций усредняйте delta_weight весов, которые уменьшили оценку (delta_score ‹ 0), и применяйте эту дельту.

Это все для теории, теперь вы знаете идею, каковы результаты?

void NeuralNetwork::shiftBackWeights(const vector<vector<vector<double> > > &weights)
{
 for (int i_layer = _layers.size() - 1; i_layer >= 0; --i_layer)
  if (weights[i_layer].size() != 0)
   _layers[i_layer]->shiftBackWeights(weights[i_layer]);
}

vector<vector<vector<double *> > > NeuralNetwork::getWeights()
{
 vector<vector<vector<double *> > > w;
 w.reserve(_layers.size() - 1);
 for (size_t i_layer = 0; i_layer < _layers.size() - 1; ++i_layer)
  w.push_back(std::move(_layers[i_layer]->getWeights()));
 return std::move(w);
}

vector<vector<vector<Edge *> > > NeuralNetwork::getEdges()
{
 vector<vector<vector<Edge *> > > w;
 w.reserve(_layers.size() - 1);
 for (size_t i_layer = 0; i_layer < _layers.size() - 1; ++i_layer)
  w.push_back(std::move(_layers[i_layer]->getEdges()));
 return std::move(w);
}

void NeuralNetwork::randomizeAllWeights()
{
 for (size_t i_layer = 0; i_layer < _layers.size() - 1; ++i_layer)
  _layers[i_layer]->randomizeAllWeights(RAND_MAX_WEIGHT); //random weights from -RAND_MAX_WEIGHT to RAND_MAX_WEIGHT
}

double NeuralNetwork::loss(const vector<double> &in, const vector<double> &out)
{
 double sum = 0;
 auto out_exp = predict(in);
 if (_layers.back()->getParameters().at("activation") == ActivationFunction::SIGMOID)
  for (size_t i = 0; i < out.size(); ++i)
   sum += 0.5 * (out[i] - out_exp[i]) * (out[i] - out_exp[i]);
 if (_layers.back()->getParameters().at("activation") == ActivationFunction::LINEAR)
  for (size_t i = 0; i < out.size(); ++i)
   sum += (out[i] - out_exp[i]) * (out[i] - out_exp[i]);
 return sum;
}

double NeuralNetwork::loss(const vector<vector<double> *> &ins, const vector<vector<double> *> &outs)
{
 double sum = 0;
 for (size_t i = 0; i < ins.size(); i++)
 {
  sum += loss(*ins[i], *outs[i]);
 }
 return sum / ins.size();
}

string NeuralNetwork::toString()
{
 string s = "NeuralNetwork";
 s.push_back('\n');
 for (Layer *l : _layers)
  s += l->toString();
 return s;
}

void NeuralNetwork::shiftWeights(float percentage_of_range)
{
 float range = percentage_of_range * (RAND_MAX_WEIGHT + RAND_MAX_WEIGHT); //distance entre min et max des poids
 for (Layer *l : _layers)
  l->shiftWeights(range);
}

vector<double> NeuralNetwork::predict(const vector<double> &in)
{
 setInput(in);
 trigger();
 return output();
}

double NeuralNetwork::predictAllForScore(const DataSet &dataset, Datatype d, int limit)
{
 if (limit == 0)
  return 1;
 double s = 0;

 if (limit == -1)
  for (size_t i = 0; i < dataset.getIns(d).size(); i++)
   s += distanceVector(predict(*dataset.getIns(d)[i]), *dataset.getOuts(d)[i]);
 else
  for (int i = 0; i < limit; i++)
  {
   int r = rand() % dataset.getIns(d).size();
   s += distanceVector(predict(*dataset.getIns(d)[r]), *dataset.getOuts(d)[r]);
  }

 //On moyenne le score
 if (limit == -1)
  s /= dataset.getIns(d).size();
 else
  s /= limit;
  
 return s;
}

Исполнение:

Для запуска проекта вы можете либо создать свой собственный набор данных XOR, либо использовать существующий в репозитории «xor.txt».

g++ -std=c++11 ./helpers/utils.cpp ./algos/Validator.cpp ./dataset/DataSet.cpp ./algos/BackPropagation.cpp ./neural_network/Layer.cpp ./neural_network/Neuron.cpp ./neural_network/NeuralNetwork.cpp ./neural_network/Edge.cpp ./algos/ThreadGuard.cpp ./main.cpp  -o ./main

Заключение:

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

Скачать проект можно с github: https://github.com/syedMohib44/Multithread_NeuralNetwork