[深度学习系列之一] C++ 实现神经网络

C++ 实现神经网络

介绍

深度学习和神经网络最近几年如火如荼,各种NN层出不穷, 这年头程序员要没听说过CNN, RNN, 还真不好意思跟人打招呼。各种库也非常多,什么pyTorch, mxnet, theano, tensorflow, keras,cntk,各种听过的没听过的满天飞。都快是全民AI学习机器学习的节奏了,怪不得Nvidia的股票一直在涨。

虽然用C++ 实现神经网络会比较麻烦,不过如果注意到以下下的trick, 在用C++ 实现神经网络时会舒服很多,代码也会非常简明,核心代码在百行左右也不是问题。

  1. C++ 没有内置的向量,矩阵库,可以自己先轮几个Vector, Matrix类以及各种数学运算操作,最好搞成链式操作。
  2. 不要用index form来推导和实现BP算法,个人比较喜欢matrix form, 公式会非常紧凑,实现起来也不太容易出错
  3. 顶层框架设计,比如先设计几个基类Model类,数据库类,定义几个通用的接口。实现具体的算法时,只要实现特定接口即可。

我最近实现了一把基本的神经网络,包括LR和多层NN模型, 使用C++11, Visual Studio 2017开发,源码放在了https://github.com/speedmancs/ML上。

训练程序的基本框架

在基类Model中,训练的框架逻辑在Fit方法中,主要流程就是SGD或者batch训练, 对于不同的模型,需要实现其中的ClearGradient, Eval, ComputeGradient, Update等方法
1. Eval 也就是神经网络的forward过程,前向计算得到当前的参数下的预测结果 数学上就是 计算 E=f(θ)
2. ComputeGradient 是BP 计算参数导数的过程, Eθ
3. 再得到导数之后, 再Update更新参数。 θ=θηEθ

void Model::Fit(const DataSet& trainingSet, const DataSet& validateSet)
{
    for (int i = 0; i < m_config.train_epoch; i++)
    {
        for (size_t j = 0; j < trainingSet.Size(); j++)
        {
            if (j % m_config.batchSize == 0) ClearGradient();
            size_t pred = Eval(trainingSet.GetData(j));
            ComputeGradient(trainingSet.GetData(j), trainingSet.GetTarget(j));
            if (j % m_config.batchSize == m_config.batchSize - 1 || j == trainingSet.Size() - 1) Update();
        }
    }
}

在batch模式下训练时,ComputeGradient需要把batch个样例的所有导数加起来,最后平均一下后进行update。在一个新的batch开始之前,需要调用ClearGradient对存储这些参数导数的变量清零。以logistic regression为例, 其中W,b分别是权重参数和偏置项, dW, db分别是其导数。

void LRModel::ClearGradient()
{
    db = 0;
    dW = 0;
}

void LRModel::Update()
{
    db.Div((float)m_config.batchSize);
    dW.Div((float)m_config.batchSize);
    b.Sub(m_config.learning_rate, db);
    W.Sub(m_config.learning_rate, dW);
}

Logistic Regression Model

有个上面的基本训练算法框架,实现具体的算法就很简单了,比如LR的模型是这样的, y 是OneHot的target, x是输入向量

Forward

y^=softmax(Wx+b)

Compute gradients

EW=(y^y)xT

Eb=y^y

照着公式实现Eval, ComputeGradient就行了。

void LRModel::ComputeGradient(const Vector<float>& x, const OneHotVector& y)
{
    (y_diff = y_hat).Sub(y);
    db.Add(y_diff);
    dW.AddMul(y_diff, x);
}

size_t LRModel::Eval(const Vector<float>& x)
{
    y_hat.AssignMul(W, x).Add(b).SoftMax();
    return y_hat.Max().second;
}

Vanilla Neural Network

NN也类似,和LR不同的是,NN在输出层之前加了若干隐藏层。 公式会变得复杂一些,但是实现还是很直接

Forward

s(l)=W(l)x(l1)+b(l)l[1,L]

x(l)=σl(s(l))

x(0) 是输入, x(L) 是最终的输出 y^ , σL 是softMax函数, σl,l[1,L1] 是sigmod函数

back propagation

δ(l)=σ(s(l))(1σ(s(l))((W(l+1))Tδ(l+1))l[1,L1]

δ(L)=y^y

EW(l)=δ(l)(x(l1))T

Eb(l)=δ(l)

void VanillaNNModel::ComputeGradient(const Vector<float>& x, const OneHotVector& y)
{
    int L = m_config.LayerNumber;
    (gradient_layers[L - 1] = activate_layers[L - 1]).Sub(y);
    for (int i = L - 2; i >= 0; i--)
    {
        gradient_layers[i].AssignMulTMat(weights[i + 1], gradient_layers[i + 1]);            
        gradient_layers[i].Mul(activate_layers[i]).MulScalarVecSub(1.0f, activate_layers[i]);
    }

    for (int i = 0; i < L; i++)
    {
        d_biases[i].Add(gradient_layers[i]);
        d_weights[i].AddMul(gradient_layers[i], i > 0 ? activate_layers[i - 1] : x);
    }
}

size_t VanillaNNModel::Eval(const Vector<float>& data)
{
    int L = m_config.LayerNumber;        
    for (int i = 0; i < L; i++)
    {
        layers[i].AssignMul(weights[i], i == 0 ? data : activate_layers[i - 1]).Add(biases[i]);        
        activate_layers[i] = layers[i];
        if (i != L - 1) activate_layers[i].Sigmod();
        else activate_layers[i].SoftMax();
    }

    y_hat = activate_layers[L - 1];
    return y_hat.Max().second;
}

基本程序库

这些主要是向量,OneHotVector和矩阵运算,以及一些常用的utility,基本上用到了什么数学操作就顺手加了进去

template<class T>
class Vector
{
public:
    // ......
    Vector& AddMul(const Matrix& m, const Vector& v);
    Vector& MulScalarVecSub(T value, const Vector& other);
    Vector& SoftMax();
};
template<class T>
class Matrix
{
public:
    // ......
    Matrix& AddMul(const Vector& a, const Vector& b);
    Matrix& Add(T scale, const Matrix& other);
};

实战数字识别

mnist数字识别堪称深度学习的hello world, 这个数据库包括60000个训练样本,10000个测试样本,大小是28*28的手写数字。

数据准备

mnist网站上给出的原始文件是二进制的,对照其网站的格式,我们实现了MnistDataSet类,它的基类DataSet如下:

class DataSet
{
public:
    DataSet() = default;
    DataSet(int categoryNumber);
    virtual bool Load(const std::string& dataFile, 
        const std::string& labelFile) = 0;
    // ......
};

主要需要override Load方法,下面是load方法中parse label的代码示例

bool MnistDataSet::LoadLabels(const std::string& labelFile)
{
    std::ifstream fin(labelFile.c_str(), std::ifstream::binary);
    if (!fin)
        return false;

    int magicNumber;
    int number;
    fin.read((char *)&magicNumber, 4);
    magicNumber = Utility::EndiannessSwap(magicNumber);
    fin.read((char *)&number, 4);
    number = Utility::EndiannessSwap(number);

    unsigned char t;
    for (int i = 0; i < number; i++)
    {
        fin.read((char *)&t, 1);
        m_targets.push_back(OneHotVector((size_t)t, m_categoryNumber));
    }
    return true;
}

mnist数字识别训练程序

string configPath = argv[1];
auto pConfig = Configuration::CreateConfiguration(configPath);
// 加载mnist 数据库
MnistDataSet trainSet(pConfig->category_number);
trainSet.Load(pConfig->trainingDataPath, pConfig->trainingLabelPath);
MnistDataSet testSet(pConfig->category_number);
testSet.Load(pConfig->validateDataPath, pConfig->validateLabelPath);

// 创建Model训练
auto pModel = pConfig->CreateModel();
pModel->Fit(trainSet, testSet);
pModel->Save();

logistic regression配置文件


<Configuration>
    <DataSetting>
        <CategoryNumber>10CategoryNumber>
        <FeatureNumber>784FeatureNumber>
        <TrainingData path="C:\train-images.idx3-ubyte" labelFile="C:\train-labels.idx1-ubyte"/>
        <ValidateData path="C:\t10k-images.idx3-ubyte" labelFile="C:\t10k-labels.idx1-ubyte"/>
    DataSetting>

    <ModelSetting type="LR" savePath="C:\lrmodel.txt">
        <TrainEpoch>200TrainEpoch>
        <LearningRate>0.1LearningRate>
        <BatchSize>128BatchSize>
    ModelSetting>
Configuration>

运行命令

.\MinstDigitalRec.exe C:\workspace\github\ml\sample\LR.config.xml

LR训练的结果

[深度学习系列之一] C++ 实现神经网络_第1张图片
最终在测试集上达到92%左右

2个隐藏层的NN配置文件


<Configuration>
    <DataSetting>
        <CategoryNumber>10CategoryNumber>
        <FeatureNumber>784FeatureNumber>
        <TrainingData path="C:\train-images.idx3-ubyte" labelFile="C:\train-labels.idx1-ubyte"/>
        <ValidateData path="C:\t10k-images.idx3-ubyte" labelFile="C:\t10k-labels.idx1-ubyte"/>
    DataSetting>

    <ModelSetting type="VanillaNN" savePath="C:\vnnmodel.txt">
        <TrainEpoch>200TrainEpoch>
        <LearningRate>0.1LearningRate>
        <BatchSize>128BatchSize>
        <hiddenLayer size="50"/>
        <hiddenLayer size="50"/>       
    ModelSetting>
Configuration>

运行命令

.\MinstDigitalRec.exe C:\workspace\github\ml\sample\VanillaNN.config.xml

NN训练60多轮结果

[深度学习系列之一] C++ 实现神经网络_第2张图片
最终在测试集上达到97%

你可能感兴趣的:(深度学习)