当前位置:   article > 正文

基于Eigen库的C++深度神经网络类_c++神经网络库

c++神经网络库

都在用Python做深度学习,懒人的福利。

手写一个基于Eigen的C++深度神经网络类,对于弄懂其中的过程还是很有好处的。代码分享给大家。

一、构造函数

类构造函数两个

(1)DeepLearn::DeepLearn(Eigen::MatrixXi layer_structure)

参数是一个1行多列的向量,各列数据即为每层神经元个数;

DeepLearn::DeepLearn(Eigen::MatrixXi layer_structure):mLayerStructure(layer_structure)
{
    int nLayers = layer_structure.size() - 1;

    for (int i = 0; i < nLayers; i++)
    {
        Eigen::MatrixXd w = Eigen::MatrixXd::Random(layer_structure(i + 1), layer_structure(i));
        Eigen::MatrixXd b = Eigen::MatrixXd::Random(layer_structure(i + 1), 1);
        Eigen::MatrixXd z = Eigen::MatrixXd::Zero(layer_structure(i + 1), 1);
        Eigen::MatrixXd a = Eigen::MatrixXd::Zero(layer_structure(i + 1), 1);
        Eigen::MatrixXd d = Eigen::MatrixXd::Zero(layer_structure(i + 1), 1);
        Eigen::MatrixXd dw = Eigen::MatrixXd::Zero(layer_structure(i + 1), layer_structure(i));
        Eigen::MatrixXd db = Eigen::MatrixXd::Zero(layer_structure(i + 1), 1);

        vLayerWeights.push_back(w);
        vLayerBiases.push_back(b);
        vLayerZetas.push_back(z);
        vLayerActivators.push_back(a);
        vLayerDeltas.push_back(d);
        vAccumulateLayerWeights.push_back(dw);
        vAccumulateLayerBiases.push_back(db);
    }
    vSampleInputs.clear();
    vSampleExpectations.clear();
    fAccumulateError = 0.0;
}

(2)DeepLearn::DeepLearn(std::string param_file)

参数是文件名,该文件存储已经训练了若干次的神经网络数据。

即,首次训练使用(1)构造函数,训练完毕后,网络参数会自动保存为文件,后续如果想接着训练,则可以传入文件名,使用(2)构造函数构造网络。

DeepLearn::DeepLearn(std::string param_file)
{
    std::ifstream f;
    f.open(param_file, std::ios::binary | std::ios::in);
    int row, col;
    f.read((char*)&row, sizeof(int));
    f.read((char*)&col, sizeof(int));
    mLayerStructure = Eigen::MatrixXi::Zero(row, col);
    f.read((char*)mLayerStructure.data(), sizeof(int)*row*col);
    int nLayers = mLayerStructure.size() - 1;
    for (int i = 0; i < nLayers; i++)
    {
        int r = mLayerStructure(i + 1);
        int c = mLayerStructure(i);
        Eigen::MatrixXd w = Eigen::MatrixXd::Zero(r, c);
        Eigen::MatrixXd b = Eigen::MatrixXd::Zero(r, 1);
        f.read((char*)w.data(), sizeof(double)*r*c);
        f.read((char*)b.data(), sizeof(double)*r);
        Eigen::MatrixXd z = Eigen::MatrixXd::Zero(r, 1);
        Eigen::MatrixXd a = Eigen::MatrixXd::Zero(r, 1);
        Eigen::MatrixXd d = Eigen::MatrixXd::Zero(r, 1);
        Eigen::MatrixXd dw = Eigen::MatrixXd::Zero(r, c);
        Eigen::MatrixXd db = Eigen::MatrixXd::Zero(r, 1);

        vLayerWeights.push_back(w);
        vLayerBiases.push_back(b);
        vLayerZetas.push_back(z);
        vLayerActivators.push_back(a);
        vLayerDeltas.push_back(d);
        vAccumulateLayerWeights.push_back(dw);
        vAccumulateLayerBiases.push_back(db);

    }
    f.close();
    vSampleInputs.clear();
    vSampleExpectations.clear();
    fAccumulateError = 0.0;

}

二、析构函数

所有数据均使用容器存储,故析构函数为空。

三、前向传播

前向传播函数:

double DeepLearn::forward(Eigen::MatrixXd x, Eigen::MatrixXd y)
{
    vSampleInputs.push_back(x);
    vSampleExpectations.push_back(y);

    int nLayers = mLayerStructure.size() - 1;
    Eigen::MatrixXd zeta;
    Eigen::MatrixXd activator = x;
    for (int i = 0; i < nLayers; i++)
    {
        zeta = vLayerWeights[i] * activator + vLayerBiases[i];
        activator = sigmoid(zeta);
        vLayerZetas[i] = zeta;
        vLayerActivators[i] = activator;
    }
    Eigen::MatrixXd err = (activator - y).transpose()*(activator - y);
    return err(0, 0);
}

传入参数x为一个样本,y为该样本的期望输出。

四、反向传播

反向传播函数:

void DeepLearn::backward()
{
    int nLayers = mLayerStructure.size() - 1;
    Eigen::MatrixXd ao = vLayerActivators[nLayers - 1];
    vLayerDeltas[nLayers - 1] = ao.cwiseProduct(Eigen::MatrixXd::Ones(ao.rows(), ao.cols()) - ao).cwiseProduct(ao - vSampleExpectations.back());        // 二次代价函数
//     vLayerDeltas[nLayers - 1] = ao - vSampleExpectations.back();                                                                                        // 交叉熵代价函数
    Eigen::MatrixXd prev_activator = nLayers > 1 ? vLayerActivators[nLayers - 2] : vSampleInputs.back();
    vAccumulateLayerWeights[nLayers - 1] += vLayerDeltas[nLayers - 1] * prev_activator.transpose();
    vAccumulateLayerBiases[nLayers - 1] += vLayerDeltas[nLayers - 1];

    for (int i = nLayers - 1; i >= 1; i--)
    {
        vLayerDeltas[i - 1] = vLayerActivators[i - 1].cwiseProduct(Eigen::MatrixXd::Ones(vLayerActivators[i - 1].rows(), vLayerActivators[i - 1].cols()) - vLayerActivators[i - 1]).cwiseProduct(vLayerWeights[i].transpose()*vLayerDeltas[i]);
        vAccumulateLayerWeights[i - 1] = i > 1 ? vLayerDeltas[i - 1] * vLayerActivators[i - 2].transpose() : vLayerDeltas[i - 1] * vSampleInputs.back().transpose();
        vAccumulateLayerBiases[i - 1] += vLayerDeltas[i - 1];
    }
}

无输入参数。其中提供了两种代价函数,其一为二次代价函数,其二为交叉熵代价函数,不用的注释掉即可。

五、参数更新

更新函数:

void DeepLearn::evolve(int batch_size,double eta)
{
    eta = eta / (double)batch_size;
    int nLayers = mLayerStructure.size() - 1;
    for (int i = 0; i < nLayers; i++)
    {
        vLayerWeights[i] -= eta*vAccumulateLayerWeights[i];
        vLayerBiases[i] -= eta*vAccumulateLayerBiases[i];
    }
}

负责更新网络参数。输入参数batch_size为小批量样本数,eta为学习速率。

六、σ函数及其导函数

使用S型神经网络,σ函数及其导数原型:

Eigen::MatrixXd DeepLearn::sigmoid(Eigen::MatrixXd m)
{
    Eigen::MatrixXd mo(m);
    for (int i = 0; i < m.rows(); i++)
    {
        for (int j = 0; j < m.cols(); j++)
        {
            mo(i,j) = sigmoid(m(i,j));
        }
    }
    return mo;
}
Eigen::MatrixXd DeepLearn::sigmoid_gradient(Eigen::MatrixXd m)
{
    Eigen::MatrixXd mo(m);
    for (int i = 0; i < m.rows(); i++)
    {
        for (int j = 0; j < m.cols(); j++)
        {
            mo(i, j) = sigmoid_gradient(m(i, j));
        }
    }
    return mo;
}

七、训练接口函数

上述前向、反向、更新及σ函数和导函数均为私有,故封装了两个接口,其一为训练,其二为更新。

训练:

void DeepLearn::Train(Eigen::MatrixXd x,Eigen::MatrixXd y)
{
    fAccumulateError += forward(x, y);
    backward();
}

更新:

double DeepLearn::ThisBatchDone(int batch_size, double eta)
{
    double err = fAccumulateError / (double)batch_size;
    evolve(batch_size,eta);
    reset();
    return err;
}

对于一个数量为N的小批量样本,循环执行N次Train(x,y)后,调用一次ThisBatchDone(N,eta)。

reset()函数用于每次更新参数后,清除训练过程产生的临时变量。

void DeepLearn::reset()
{
    int nLayers = mLayerStructure.size() - 1;
    for (int i = 0; i < nLayers; i++)
    {
        vLayerZetas[i].setZero();
        vLayerActivators[i].setZero();
        vLayerDeltas[i].setZero();
        vAccumulateLayerWeights[i].setZero();
        vAccumulateLayerBiases[i].setZero();
    }
    vSampleInputs.clear();
    vSampleExpectations.clear();
    fAccumulateError = 0.0;
}

八、预测函数

提供一个预测函数,输入一个样本,给出网络输出:

Eigen::MatrixXd DeepLearn::Predict(Eigen::MatrixXd sample)
{
    int nLayers = mLayerStructure.size() - 1;
    Eigen::MatrixXd x = sample;
    for (int i = 0; i < nLayers; i++)
    {
        Eigen::MatrixXd z = vLayerWeights[i] * x + vLayerBiases[i];
        x = sigmoid(z);
    }
    return x;
}

九、网络参数存储

保存网络参数的函数,被设置为了公有,可被外部调用。

void DeepLearn::SaveNetworkParameters(std::string file_name)
{
    std::ofstream f;
    f.open(file_name, std::ios::binary | std::ios::out | std::ios::trunc);
    int row = mLayerStructure.rows();
    int col = mLayerStructure.cols();
    f.write((char*)&row, sizeof(int));
    f.write((char*)&col, sizeof(int));
    f.write((char*)mLayerStructure.data(), sizeof(int)*row*col);
    int nLayers = mLayerStructure.size() - 1;
    for (int i = 0; i < nLayers; i++)
    {
        f.write((char*)vLayerWeights[i].data(), sizeof(double)*vLayerWeights[i].rows()*vLayerWeights[i].cols());
        f.write((char*)vLayerBiases[i].data(), sizeof(double)*vLayerBiases[i].rows()*vLayerBiases[i].cols());
    }
    f.close();
}

十、其他函数

另外,神经网络输出常常需二值化,即将最大的若干个输出量置为1,其余为0,专门写了个函数:

Eigen::MatrixXd DeepLearn::Binaryzation(Eigen::MatrixXd x, std::vector<Eigen::MatrixXd>& coords, int dim)
{
    Eigen::MatrixXd out = Eigen::MatrixXd::Zero(x.rows(), x.cols());
    coords.clear();
    int r, c;
    double m = x.minCoeff();
    for (int i = 0; i < dim; i++)
    {
        x.maxCoeff(&r, &c);
        out(r, c) = 1;
        x(r, c) = m;
        Eigen::MatrixXd  coord(2, 1);
        coord << r,
            c;
        coords.push_back(coord);
    }
    return out;
}

参数x为需二值化的对象,coords用于接收被置为1的元素的坐标,dim为维度,即指定有dim个参量需要置为1。

十一、类的使用

对于MNIST数据集训练,可以这样写代码:

void tell_handwriting()
{    
    std::vector<Eigen::MatrixXd> images;
    std::vector<Eigen::MatrixXd> labels;
    int image_count;
    int image_rows;
    int image_cols;
    int image_label_count;
    MnistLoader::read_Mnist_Images("E:\\code\\DL\\DL\\train-images.idx3-ubyte", images, image_count, image_rows, image_cols);;
    MnistLoader::read_Mnist_Label("E:\\code\\DL\\DL\\train-labels.idx1-ubyte", labels, image_label_count);

    std::vector<Eigen::MatrixXd> test_images;
    std::vector<Eigen::MatrixXd> test_labels;
    int test_image_count;
    int test_image_rows;
    int test_image_cols;
    int test_image_label_count;
    MnistLoader::read_Mnist_Images("E:\\code\\DL\\DL\\t10k-images.idx3-ubyte", test_images, test_image_count, test_image_rows, test_image_cols);
    MnistLoader::read_Mnist_Label("E:\\code\\DL\\DL\\t10k-labels.idx1-ubyte", test_labels, test_image_label_count);

    Eigen::MatrixXi layer_structure(1, 3);
    layer_structure << 784, 64, 10;
    DeepLearn DL(layer_structure);
    std::string file_name = "e:\\Network.parameters[784,64,10].data";
//     DeepLearn DL(file_name);

    int epoch = 30;
    int batch_size = 10;
    int batch_count = image_count / batch_size;
    time_t tStart = time(NULL);
    int nTotal = epoch;
    for (int i = 0; i < epoch; i++)
    {
        jumble(images, labels);
        double err = 0;
        for (int j = 0; j < batch_count; j++)
        {
            for (int k = 0; k < batch_size; k++)
            {
                DL.Train(images[j*batch_size + k], labels[j*batch_size + k]);
            }
            err = DL.ThisBatchDone(batch_size, 0.1);
        }
        DL.SaveNetworkParameters(file_name);
        int correct = 0;
        for (int u = 0; u < 100; u++)
        {
            std::cout << ".";
            Eigen::MatrixXd result = DL.Predict(test_images[u]);
            Eigen::MatrixXd::Index maxRow_result, maxCol_result, maxRow_label, maxCol_label;
            result.maxCoeff(&maxRow_result, &maxCol_result);
            test_labels[u].maxCoeff(&maxRow_label, &maxCol_label);
            if (maxRow_result == maxRow_label)
                correct++;
        }

        time_t tNow = time(NULL);
        time_t tNeed = (nTotal - (i+1))*(tNow - tStart) / (i+1);
        std::cout << std::endl << "epoch " << i << " : " << correct << "%, Remaining time " << tNeed << " seconds." << std::endl;
        
    }

    char x;
    std::cin >> x;

}

代码解释:

上面代码构造了一个三层神经网络,输入层784个神经元,隐藏层1层,神经元数量为64,输出层10个神经元。

上述代码将训练集数据的每10个样本做为一个批次,进行batch_size次Train之后执行一次ThisBatchDone,完成所有样本训练后,做一次测试(一次仅测试了100个样本),输出成功率,同时输出完成全部训练预估的剩余时间;

上述过程重复执行了30次。

十二、别的

1、其中,读取MNIST数据集使用了2个函数:

读标签值:

void MnistLoader::read_Mnist_Label(std::string filename, std::vector<Eigen::MatrixXd>& labels, int& label_count)
{
    std::ifstream file(filename,std::ios::binary);
    if (file.is_open())
    {
        int magic_number = 0;
        file.read((char*)&magic_number, sizeof(magic_number));
        file.read((char*)&label_count, sizeof(label_count));
        magic_number = ReverseInt(magic_number);
        label_count = ReverseInt(label_count);

        unsigned char* pArray = new unsigned char[label_count];
        file.read((char*)pArray, label_count);
        file.close();
        for (int i = 0; i < label_count; i++)
        {
            Eigen::MatrixXd label = Eigen::MatrixXd::Zero(10, 1);
            label(pArray[i], 0) = 1.0;
            labels.push_back(label);
        }
        delete[] pArray;
    }
}

读样本:

void MnistLoader::read_Mnist_Images(std::string filename, std::vector<Eigen::MatrixXd>& images,int& image_count,int& image_rows,int& image_cols)
{
    std::ifstream file(filename, std::ios::binary);
    if (file.is_open())
    {
        int magic_number = 0;
        unsigned char label;
        file.read((char*)&magic_number, sizeof(magic_number));
        file.read((char*)&image_count, sizeof(image_count));
        file.read((char*)&image_rows, sizeof(image_rows));
        file.read((char*)&image_cols, sizeof(image_cols));
        magic_number = MnistLoader::ReverseInt(magic_number);
        image_count = MnistLoader::ReverseInt(image_count);
        image_rows = MnistLoader::ReverseInt(image_rows);
        image_cols = MnistLoader::ReverseInt(image_cols);


        unsigned char* pArray = new unsigned char[image_rows*image_cols];
        for (int i = 0; i < image_count; i++)
        {
            file.read((char*)pArray, image_rows*image_cols);
            Eigen::Map<Eigen::Matrix<unsigned char, Eigen::Dynamic, Eigen::Dynamic>> map(pArray, image_rows*image_cols, 1);
            Eigen::Matrix<unsigned char, Eigen::Dynamic, Eigen::Dynamic> m(map);
            Eigen::MatrixXd mx = m.cast<double>();
            mx /= 255.0;
            images.push_back(mx);
        }
        delete[] pArray;
        file.close();
    }
}

2、关于 jumble(images, labels)函数

该函数用于对训练数据集做一次乱序。注意训练数据和对应标签要一起做对应的乱序,对应关系不能变,不然神仙也训练不出来。代码留给读者自己写吧,不困难。

十三、测试结论

使用上面的代码,对MNIST数据集训练,第1个epoch成功率约63%......第30个epoch成功率约93%。

十四、啰嗦几句

1、基于win7+visual studio 2013+eigen3开发。

2、cpp源代码基本都提供了,自己弄个头文件对大家来说应该不困难,就不赘述了。

3、提醒需要借鉴的,手抄更能加深印象,鄙视复制粘贴。

4、原创文章,原创代码,供学习交流,商用联系本人。

5、最后提醒一下,debug版本慢的要死,如果没有调试需要,改release版本马上起飞。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/79225
推荐阅读
相关标签
  

闽ICP备14008679号