ML - реализация нейронной сети на C ++ с нуля
Что мы здесь будем делать?
В этой статье объясняется, как создать сверхбыструю искусственную нейронную сеть, которая может обрабатывать миллионы точек данных за секунды! даже миллисекунды. Искусственный интеллект и машинное обучение в настоящее время являются одной из самых популярных тем среди компьютерных фанатов. Технологические гиганты нанимают специалистов по данным за их выдающиеся достижения в этих областях.
Зачем использовать C ++
Теперь, если вы уже реализовали модель нейронной сети на каком-то другом языке программирования, то вы могли заметить (если у вас недорогой ПК), что ваши модели работают довольно медленно даже с небольшими наборами данных. Когда вы начали изучать нейронные сети, вы, возможно, уже искали в Google Какой язык лучше всего подходит для машинного обучения? и очевидный ответ, который вы получите: Python или R лучше всего подходят для машинного обучения, другие языки сложны, поэтому вы не должны тратить на них свое время! . Теперь, если пользователь начинает программировать, он сталкивается с проблемой потребления времени и ресурсов. Итак, в этой статье показано, как создать сверхбыструю нейронную сеть.
Предпосылки:
- Базовые знания о том, что такое классы и как они работают.
- Используйте библиотеку линейной алгебры под названием Eigen
- Некоторые базовые операции чтения / записи в C ++
- Некоторые базовые знания о линейной алгебре, поскольку мы используем для этого библиотеку
Айген 101:
По сути, Eigen - это библиотека для сверхбыстрых операций линейной алгебры, самая быстрая и простая из существующих. Некоторые ресурсы для изучения основ Eigen.
- Начиная!
- Класс Eigen Matrix
Изучая Eigen, вы столкнетесь с одной из самых мощных функций C ++ - метапрограммированием шаблонов. Рекомендуется прямо сейчас не отклоняться от курса (если вы новичок в C ++) и принимать их в качестве основных параметров функции! Однако, если вы действительно одержимы изучением новых и мощных вещей, то вот хорошая статья и видео для этого.
Написание класса нейронной сети
Прежде чем идти дальше, я предполагаю, что вы знаете, что такое нейронная сеть и как она обучается. Если нет, то я рекомендую вам взглянуть на следующие страницы!
- Основы нейронных сетей
- Прямое и обратное распространение в нейронных сетях
Код: класс нейронной сети
// NeuralNetwork.hpp #include <eigen3/Eigen/Eigen> #include <iostream> #include <vector> // use typedefs for future ease for changing data types like : float to double typedef float Scalar; typedef Eigen::MatrixXf Matrix; typedef Eigen::RowVectorXf RowVector; typedef Eigen::VectorXf ColVector; // neural network implementation class! class NeuralNetwork { public : // constructor NeuralNetwork(std::vector<uint> topology, Scalar learningRate = Scalar(0.005)); // function for forward propagation of data void propagateForward(RowVector& input); // function for backward propagation of errors made by neurons void propagateBackward(RowVector& output); // function to calculate errors made by neurons in each layer void calcErrors(RowVector& output); // function to update the weights of connections void updateWeights(); // function to train the neural network give an array of data points void train(std::vector<RowVector*> data); // storage objects for working of neural network /* use pointers when using std::vector<Class> as std::vector<Class> calls destructor of Class as soon as it is pushed back! when we use pointers it can't do that, besides it also makes our neural network class less heavy!! It would be nice if you can use smart pointers instead of usual ones like this */ std::vector<RowVector*> neuronLayers; // stores the different layers of out network std::vector<RowVector*> cacheLayers; // stores the unactivated (activation fn not yet applied) values of layers std::vector<RowVector*> deltas; // stores the error contribution of each neurons std::vector<Matrix*> weights; // the connection weights itself Scalar learningRate; }; |
Затем мы продвигаемся вперед, реализуя каждую функцию одну за другой… Но сначала создайте два файла (NeuralNetwork.cpp и NeuralNetwork.hpp) и сами напишите приведенный выше код класса NeuralNetwork в «NeuralNetwork.hpp». Следующую строку кода необходимо скопировать в файл «NeuralNetwork.cpp».
Код: конструктор класса нейронной сети
// constructor of neural network class NeuralNetwork::NeuralNetwork(std::vector<uint> topology, Scalar learningRate) { this ->topology = topology; this ->learningRate = learningRate; for (uint i = 0; i < topology.size(); i++) { // initialze neuron layers if (i == topology.size() - 1) neuronLayers.push_back( new RowVector(topology[i])); else neuronLayers.push_back( new RowVector(topology[i] + 1)); // initialize cache and delta vectors cacheLayers.push_back( new RowVector(neuronLayers.size())); deltas.push_back( new RowVector(neuronLayers.size())); // vector.back() gives the handle to recently added element // coeffRef gives the reference of value at that place // (using this as we are using pointers here) if (i != topology.size() - 1) { neuronLayers.back()->coeffRef(topology[i]) = 1.0; cacheLayers.back()->coeffRef(topology[i]) = 1.0; } // initialze weights matrix if (i > 0) { if (i != topology.size() - 1) { weights.push_back( new Matrix(topology[i - 1] + 1, topology[i] + 1)); weights.back()->setRandom(); weights.back()->col(topology[i]).setZero(); weights.back()->coeffRef(topology[i - 1], topology[i]) = 1.0; } else { weights.push_back( new Matrix(topology[i - 1] + 1, topology[i])); weights.back()->setRandom(); } } } }; |
Объяснение функции конструктора - Инициализация нейронов, кеша и дельт
Вектор топологии описывает, сколько нейронов у нас в каждом слое, а размер этого вектора равен количеству слоев в нейронной сети. Каждый слой в нейронной сети представляет собой массив нейронов, мы храним каждый из этих слоев как вектор, так что каждый элемент в этом векторе хранит значение активации нейрона в этом слое (обратите внимание, что массив этих слоев - это сама нейронная сеть. . Теперь в строке 8 мы добавляем дополнительный нейрон смещения к каждому слою, кроме выходного слоя (строка 7). Кэш и вектор дельты имеют те же размеры, что и вектор нейронного слоя. Здесь мы используем векторы в качестве слоев и не 2D-матрица, как мы делаем SGD, а не пакетный или мини-пакетный градиентный спуск.Теперь кеш - это просто другое название суммы взвешенных входных данных из предыдущего слоя.
Обозначение, которое мы будем использовать для размеров матрицы: [mn] обозначает матрицу, имеющую m строк и n столбцов.
Инициализация матрицы весов
Инициализация матрицы весов немного сложна! (математически). Обратите очень серьезное внимание на то, что вы прочитаете в следующих нескольких строках, поскольку это объяснит, как мы хотим использовать матрицу весов в этой статье. Я предполагаю, что вы знаете, как слои взаимосвязаны в нейронной сети.
- CURRENT_LAYER представляет слой, который принимает входные данные, а PREV_LAYER и FWD_LAYER представляют слой позади и перед слоем CURRENT_LAYER.
- c-й столбец в матрице весов представляет соединение c-го нейрона в CURRENT_LAYER со всеми нейронами в PREV_LAYER.
- r-й элемент c-го столбца в матрице весов представляет соединение c-го нейрона в CURRENT_LAYER с r-м нейроном в PREV_LAYER.
- r-я строка в матрице весов представляет соединение всех нейронов в PREV_LAYER с r-м нейроном в CURRENT_LAYER.
- c-й элемент r-й строки в матрице весов представляет соединение c-го нейрона в PREV_LAYER с r-м нейроном в CURRENT_LAYER.
- Точки 1 и 2 будут использоваться, когда мы используем матрицу весов в обычном смысле, но точки 3 и 4 будут использоваться, когда мы будем использовать матрицу весов в транспонированном смысле (a (i, j) = a (j, I))
Теперь помните, что у нас есть дополнительный нейрон смещения в предыдущем слое. Если мы сделаем простое матричное произведение вектора нейронов слоя PREV_LAYER и матрицы весов CURRENT_LAYER, мы получим новый вектор слоя нейронов CURRENT_LAYER. Теперь нам нужно изменить нашу матрицу весов таким образом, чтобы нейрон смещения CURRENT_LAYER не подвергался влиянию умножения матрицы! Для этого мы устанавливаем для всех элементов последнего столбца матрицы весов значение 0 (строка 26), за исключением последнего элемента (строка 27).
Код: алгоритм прямой связи
void NeuralNetwork::propagateForward(RowVector& input) { // set the input to input layer // block returns a part of the given vector or matrix // block takes 4 arguments : startRow, startCol, blockRows, blockCols neuronLayers.front()->block(0, 0, 1, neuronLayers.front()->size() - 1) = input; // propagate the data forawrd for (uint i = 1; i < topology.size(); i++) { // already explained above (*neuronLayers[i]) = (*neuronLayers[i - 1]) * (*weights[i - 1]); } // apply the activation function to your network // unaryExpr applies the given function to all elements of CURRENT_LAYER for (uint i = 1; i < topology.size() - 1; i++) { neuronLayers[i]->block(0, 0, 1, topology[i]).unaryExpr(std::ptr_fun(activationFunction)); } } |
Объяснение алгоритма прямой связи:
C-й элемент (нейрон) CURRENT_LAYER принимает свои входные данные, беря скалярное произведение между вектором NeuronLayers PREV_LAYER и C-м столбцом. Таким образом, входные данные умножаются на вес, и это также автоматически добавляет член смещения. Последний столбец матрицы весов инициализируется установкой всех элементов в 0, кроме последнего элемента (установлен в 1), это означает, что нейрон смещения CURRENT_LAYER принимает входные данные только от нейрона смещения PREV_LAYER.
Расчет ошибок:
void NeuralNetwork::calcErrors(RowVector& output) { // calculate the errors made by neurons of last layer (*deltas.back()) = output - (*neuronLayers.back()); // error calculation of hidden layers is different // we will begin by the last hidden layer // and we will continue till the first hidden layer for (uint i = topology.size() - 2; i > 0; i--) { (*deltas[i]) = (*deltas[i + 1]) * (weights[i]->transpose()); } } |
Код: Обновление весов
void NeuralNetwork::updateWeights() { // topology.size()-1 = weights.size() for (uint i = 0; i < topology.size() - 1; i++) { // in this loop we are iterating over the different layers (from first hidden to output layer) // if this layer is the output layer, there is no bias neuron there, number of neurons specified = number of cols // if this layer not the output layer, there is a bias neuron and number of neurons specified = number of cols -1 if (i != topology.size() - 2) { for (uint c = 0; c < weights[i]->cols() - 1; c++) { for (uint r = 0; r < weights[i]->rows(); r++) { weights[i]->coeffRef(r, c) += learningRate * deltas[i + 1]->coeffRef(c) * activationFunctionDerivative(cacheLayers[i + 1]->coeffRef(c)) * neuronLayers[i]->coeffRef(r); } } } else { for (uint c = 0; c < weights[i]->cols(); c++) { for (uint r = 0; r < weights[i]->rows(); r++) { weights[i]->coeffRef(r, c) += learningRate * deltas[i + 1]->coeffRef(c) * activationFunctionDerivative(cacheLayers[i + 1]->coeffRef(c)) * neuronLayers[i]->coeffRef(r); } } } } } |
Алгоритм обратного распространения ошибки:
void NeuralNetwork::propagateBackward(RowVector& output) { calcErrors(output); updateWeights(); } |
Код: функция активации
Scalar activationFunction(Scalar x) { return tanhf(x); } Scalar activationFunctionDerivative(Scalar x) { return 1 - tanhf(x) * tanhf(x); } // you can use your own code here! |
Код: обучающая нейронная сеть
void NeuralNetwork::train(std::vector<RowVector*> input_data, std::vector<RowVector*> output_data) { for (uint i = 0; i < input_data.size(); i++) { std::cout << "Input to neural network is : " << *input_data[i] << std::endl; propagateForward(*input_data[i]); std::cout << "Expected output is : " << *output_data[i] << std::endl; std::cout << "Output produced is : " << *neuronLayers.back() << std::endl; propagateBackward(*output_data[i]); std::cout << "MSE : " << std:: sqrt ((*deltas.back()).dot((*deltas.back())) / deltas.back()->size()) << std::endl; } } |
Код: загрузка данных
void ReadCSV(std::string filename, std::vector<RowVector*>& data) { data.clear(); std::ifstream file(filename); std::string line, word; // determine number of columns in file getline(file, line, '
' ); std::stringstream ss(line); std::vector<Scalar> parsed_vec; while (getline(ss, word, ', ' )) { parsed_vec.push_back(Scalar(std::stof(&word[0]))); } uint cols = parsed_vec.size(); data.push_back( new RowVector(cols)); for (uint i = 0; i < cols; i++) { data.back()->coeffRef(1, i) = parsed_vec[i]; } // read the file if (file.is_open()) { while (getline(file, line, '
' )) { std::stringstream ss(line); data.push_back( new RowVector(1, cols)); uint i = 0; while (getline(ss, word, ', ' )) { data.back()->coeffRef(i) = Scalar(std::stof(&word[0])); i++; } } } } |
Пользователь может читать файлы csv с помощью этого кода и вставлять их в класс нейронной сети, но будьте осторожны, объявления и определения должны храниться в отдельных файлах (NeuralNetwork.cpp и NeuralNetwork.h). Сохраните все файлы и побывайте со мной несколько минут!
Код: генерировать некоторый шум, т.е. данные обучения
void genData(std::string filename) { std::ofstream file1(filename + "-in" ); std::ofstream file2(filename + "-out" ); for (uint r = 0; r < 1000; r++) { Scalar x = rand () / Scalar(RAND_MAX); Scalar y = rand () / Scalar(RAND_MAX); file1 << x << ", " << y << std::endl; file2 << 2 * x + 10 + y << std::endl; } file1.close(); file2.close(); } |
Код: Реализация нейронной сети.
// main.cpp // don't forget to include out neural network #include "NeuralNetwork.hpp" //... data generator code here typedef std::vector<RowVector*> data; int main() { NeuralNetwork n({ 2, 3, 1 }); data in_dat, out_dat; genData( "test" ); ReadCSV( "test-in" , in_dat); ReadCSV( "test-out" , out_dat); n.train(in_dat, out_dat); return 0; } |
Чтобы скомпилировать программу, откройте свой Linux-терминал и введите:
g ++ main.cpp NeuralNetwork.cpp -o main && ./main
Запустите эту команду. Попробуйте поэкспериментировать с количеством точек данных в этой функции genData.