深度学习和神经网络最近几年如火如荼,各种NN层出不穷, 这年头程序员要没听说过CNN, RNN, 还真不好意思跟人打招呼。各种库也非常多,什么pyTorch, mxnet, theano, tensorflow, keras,cntk,各种听过的没听过的满天飞。都快是全民AI学习机器学习的节奏了,怪不得Nvidia的股票一直在涨。
虽然用C++ 实现神经网络会比较麻烦,不过如果注意到以下下的trick, 在用C++ 实现神经网络时会舒服很多,代码也会非常简明,核心代码在百行左右也不是问题。
我最近实现了一把基本的神经网络,包括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);
}
有个上面的基本训练算法框架,实现具体的算法就很简单了,比如LR的模型是这样的, y 是OneHot的target, x是输入向量
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;
}
NN也类似,和LR不同的是,NN在输出层之前加了若干隐藏层。 公式会变得复杂一些,但是实现还是很直接
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;
}
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();
<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
<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