赞
踩
都在用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版本马上起飞。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。