📜  ML –从零开始在C ++中进行神经网络实现

📅  最后修改于: 2021-06-01 01:10:54             🧑  作者: Mango

我们要在这里做什么?
本文介绍了如何创建一个超快速人工神经网络,该网络可以在几秒钟内处理数百万个数据点!甚至毫秒如今,人工智能和机器学习已成为计算机极客中最热门的话题之一。数据科学家正因其在这些领域的卓越表现而被科技巨头聘用。

为什么要使用C++
现在,如果您已经用其他某种编程语言实现了神经网络模型,那么您可能已经注意到(如果您使用的是低端PC),即使在很小的数据集上,模型的运行速度也相当慢。当您开始学习神经网络时,您可能已经搜索过哪种语言最适合机器学习?而且显而易见的答案是Python或R最适合机器学习,其他语言则很难,因此您一定不要在它们上面浪费时间! 。现在,如果用户开始编程,他们将面临时间和资源消耗的问题。因此,本文展示了如何建立一个超快速的神经网络。

先决条件:

  • 关于什么是类以及它们如何工作的基本知识。
  • 使用称为Eigen的线性代数库
  • C++中的一些基本读写操作
  • 关于线性代数的一些基本知识,因为我们正在使用一个库

本征101:
Eigen的核心是一个用于超快速线性代数运算的库,它是目前最快,最简单的库。一些学习本征基础知识的资源。

  • 入门!
  • 本征矩阵类

在学习Eigen时,您会遇到C++最强大的功能之一–模板元编程。建议不要立即偏离轨道(如果您是C++的新手),并假设这些是函数的基本参数!但是,如果您真的沉迷于学习新功能,那么这里有一篇不错的文章和视频。
编写神经网络课程
在继续之前,我假设您知道什么是神经网络以及它是如何学习的。如果没有,那么我建议您在下面几页中看看!

  • 神经网络基础
  • 神经网络中的正向和反向传播

代码:神经网络类

// NeuralNetwork.hpp
#include 
#include 
#include 
  
// 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 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 data);
  
    // storage objects for working of neural network
    /*
          use pointers when using std::vector as std::vector 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 neuronLayers; // stores the different layers of out network
    std::vector cacheLayers; // stores the unactivated (activation fn not yet applied) values of layers
    std::vector deltas; // stores the error contribution of each neurons
    std::vector weights; // the connection weights itself
    Scalar learningRate;
};

接下来,我们一步一步地实现每个函数。但是,首先,创建两个文件(NeuralNetwork.cpp和NeuralNetwork.hpp),并在“ NeuralNetwork.hpp”中编写上面的NeuralNetwork类代码。必须将以下代码行复制到“ NeuralNetwork.cpp”文件中。

代码:神经网络类的构造函数

// constructor of neural network class
NeuralNetwork::NeuralNetwork(std::vector 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行)外,我们还在每层添加了额外的偏向神经元。cache和delta向量的尺寸与NeuronLayer向量的尺寸相同。而不是像我们在进行SGD一样的2D矩阵,也不是批量或小批量梯度下降,现在,缓存只是来自上一层的加权输入总和的另一个名称。
我们将用于矩阵维的一种表示法是: [mn]表示具有m行n列的矩阵。

初始化权重矩阵
初始化权重矩阵有点棘手! (数学上)。在接下来的几行中,请非常注意您所阅读的内容,因为这将解释我们如何在本文中使用权重矩阵。我假设您知道神经网络中各层如何相互连接。

  • CURRENT_LAYER表示接受输入的图层,PREV_LAYER和FWD_LAYER表示CURRENT_LAYER的背面和正面。
  • 权重矩阵中的第c列表示CURRENT_LAYER中的c个神经元与PREV_LAYER中的所有神经元的连接。
  • 权重矩阵中第c列的第r个元素表示CURRENT_LAYER中的第c个神经元与PREV_LAYER中的第r个神经元的连接。
  • 权重矩阵中的第r行表示PREV_LAYER中所有神经元与CURRENT_LAYER中第r神经元的连接。
  • 权重矩阵中第r行的第c个元素表示PREV_LAYER中的第c个神经元与CURRENT_LAYER中的第r个神经元的连接。
  • 当我们在正常意义上使用权重矩阵时,将使用第1点和第2点,但是当我们在转置意义上使用权重矩阵时,将使用第3点和第4点(a(i,j)= a(j,I))

现在,请记住我们在上一层有一个额外的偏向神经元。如果我们对PREV_LAYER的NeuronsLayer向量和CURRENT_LAYER的权重矩阵做一个简单的矩阵乘积,我们将得到新的CURRENT_LAYER的NeuronsLayer向量。现在,我们要做的是以某种方式修改权重矩阵,以使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));
    }
}

前馈算法说明:
CURRENT_LAYER的第C个元素(神经元)通过获取PREV_LAYER的NeuronLayers向量和第C列之间的点积来获取输入。这样,它将输入乘以权重,这也会自动将偏差项相加。权重矩阵的最后一列通过将除最后一个元素(设置为1)以外的所有元素都设置为0来初始化,这意味着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 input_data, std::vector 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& data)
{
    data.clear();
    std::ifstream file(filename);
    std::string line, word;
    // determine number of columns in file
    getline(file, line, '\n');
    std::stringstream ss(line);
    std::vector 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, '\n')) {
            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 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函数的数据点数。