之前说了组里的任务是手写BP神经网络,上一篇文总结了一下BP神经网络的概念,老实说,总结概念前的一个C++实现版本在总结概念之后重新审视时觉得实在是惨不忍睹,于是今晚回炉重写了。这篇文就来挂我的BP神经网络C++实现。
老师提出的具体问题是平面对点集的二分类。ACM战过这么多场,写板子早成了习惯,就把通用的BP认真封装了一下:
头文件里的Data结构体是输入数据的数据结构,可自定义,这里用的是点分类问题的模型,BP.h:
#ifndef _BP_H_
#define _BP_H_
#include <vector>
#include <string>
using namespace std;
/* 数据样本类 */
struct Data
{
/* 输入参数,包括: */
/* 三维直角坐标系点的坐标(x, y, z) */
/* 三维直角坐标系平面a*x + b*y + c*z + d = 0的4个系数 */
double x[7];
/* 期望输出即监督值 */
/* 拟合 */
// double d[1];
/* 分类 */
double d[2];
/* 数据构造函数 */
Data();
};
class BP
{
private:
/* ========== 常数 ========== */
/* 输入层节点数 */
int I;
/* 隐含层神经元数 */
int H;
/* 输出层神经元数 */
int O;
/* 权重学习速率 */
double LR;
/* 偏置学习速率 */
double LR2;
/* 学习速率衰减率(每次衰减与当前LR相乘) */
double LRDecay;
/* 误差函数收敛阈值 */
double C;
/* ========== 容器 ========== */
/* 训练用数据样本集 */
vector<Data> trainDS;
/* 测使用数据集 */
vector<Data> testDS;
/* 输入层与隐含层间的全连接权重:w[I(包含一个偏置值)][H] */
double **w;
/* w修正值 */
double *dw;
/* 隐含层阈值 */
double *th;
/* 隐含层输入积累即净激活,也存放之后的激活输出值:u[H] */
double *u;
/* 隐含层与输出层间的全连接权重:v[H][O] */
double **v;
/* v修正值 */
double *dv;
/* 输出层阈值 */
double *to;
/* 输出层输入积累即净激活,也存放之后的激活输出值:y[O] */
double *y;
/* 拟合标识 */
bool regression;
/* ========== 方法 ========== */
/* 填充训练用数据样本集 */
void fillTrainDS(int sampleCnt);
/* 清空训练用数据样本集 */
void clearTrainDS();
/* 填充测试使用数据集 */
void fillTestDS(int sampleCnt);
/* 清空测试使用数据集 */
void clearTestDS();
/* 预测 */
void forward(int index, bool test = false);
/* 调整 */
void backward(int index);
public:
/* ========== 接口 ========== */
/*
* 类构造函数,初始化BP神经网络结构和训练参数
* int _I: 输入参数数目。
* int _O: 输出值数目。
* int A: 隐含层调整因子(1~10)。
* double _LR: 权重学习速率(0.01~0.8)。
* double _LR2: 偏置学习速率(0.01~0.8)。
* double _LRDecay: 学习速率衰减率(每次衰减与当前LR相乘)。
* double _C: 误差函数收敛阈值。
* bool regression: 拟合标识。
*/
BP(int _I, int _O, int A = 1, double _LR = 0.01, double _LR2 = 0.035, double _LRDecay = 1.0, double _C = 0.01, bool regression = false);
/* 类析构函数,释放容器分配的堆空间 */
~BP();
/* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
double train(int sampleCnt = 1000, int trainCnt = 100);
/*
* 使用指定数目的样本循环训练。
* 误差函数值进入可接受范围判定收敛并停止训练;
* 到达最大训练次数时停止训练。
* 返回是否收敛。
*/
bool trainTillConvergent(int sampleCnt = 1000, int maxEpoch = 1000);
/* 生成指定数目组数据测试当前神经网络 */
void testNetwork(int testCnt = 1000);
/* 保存当前神经网络,即两个权重数组 */
void saveNetwork(string wPath = "wNetwork", string vPath = "vNetwork");
/* 载入两个权重数组,还原神经网络 */
void loadNetwork(string wPath = "wNetwork", string vPath = "vNetwork");
/* 使用指定数据预测输出值并返回输出值 */
vector<double> runNetwork(vector<double> x);
};
#endif
类实现源码里的Data结构体的构造函数是输入数据的处理和问题模型的建立,可自定义,这里用的是点分类问题的数据处理,BP.cpp:
#include "BP.h"
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <fstream>
using namespace std;
/* 数据构造函数 */
Data::Data()
{
/* 归一参数 */
for(int i = 1 ; i < 8 ; i++)
{
x[i] = rand()/(double)RAND_MAX;
}
/* 实际参数 */
double _x = x[1]*20 - 10;
double y = x[2]*20 - 10;
double z = x[3]*20 - 10;
double a = x[4]*20 - 10;
double b = x[5]*20 - 10;
double c = x[6]*20 - 10;
double _d = x[7]*20 - 10;
/* 拟合 */
// d[0] = z - (a*_x + b*y + _d)/(-1*c);
/* 分类 */
d[0] = z > (a*_x + b*y + _d)/(-1*c) ? 1 : 0;
d[1] = d[0] ? 0 : 1;
}
/* 填充训练用数据样本集 */
void BP::fillTrainDS(int sampleCnt)
{
while(sampleCnt--)
{
trainDS.push_back(Data());
}
}
/* 清空训练用数据样本集 */
void BP::clearTrainDS()
{
vector<Data> v;
trainDS.swap(v);
}
/* 填充测试使用数据集 */
void BP::fillTestDS(int sampleCnt)
{
while(sampleCnt--)
{
testDS.push_back(Data());
}
}
/* 清空测试使用数据集 */
void BP::clearTestDS()
{
vector<Data> v;
testDS.swap(v);
}
/* 预测 */
void BP::forward(int index, bool test)
{
/* 隐含层 */
for(int i = 0 ; i < H ; i++)
{
u[i] = 0;
for(int j = 0 ; j < I ; j++)
{
double x = test ? testDS[index].x[j] : trainDS[index].x[j];
/* 积累输入 */
u[i] += w[j][i]*x;
}
u[i] += th[i];
/* Sigmoid函数作为激活函数 */
u[i] = 1 / (1 + exp(-1*u[i]));
}
/* 输出层 */
for(int i = 0 ; i < O ; i++)
{
y[i] = 0;
for(int j = 0 ; j < H ; j++)
{
/* 积累输入 */
y[i] += v[j][i]*u[j];
}
y[i] += to[i];
/* 分类:Sigmoid函数作为激活函数 */
if (!regression)
{
y[i] = 1 / (1 + exp(-1*y[i]));
}
}
}
/* 调整 */
void BP::backward(int index)
{
/* 计算隐含层与输出层间权重调整值 */
for(int i = 0 ; i < O ; i++)
{
/* 拟合:计算输出层学习误差 */
if (regression)
{
dv[i] = y[i] - trainDS[index].d[i];
}
/* 分类:计算输出层学习误差 */
else
{
dv[i] = (y[i] - trainDS[index].d[i])*y[i]*(1 - y[i]);
}
}
/* 计算输入层与隐含层间权重调整值 */
double t;
for(int i = 0 ; i < H ; i++)
{
t = 0;
for(int j = 0 ; j < O ; j++)
{
t += dv[j]*v[i][j];
}
dw[i] = t*u[i]*(1 - u[i]);
}
/* 调整隐含层与输出层间权重 */
for(int i = 0 ; i < H ; i++)
{
for(int j = 0 ; j < O ; j++)
{
v[i][j] -= LR*dv[j]*u[i];
}
}
/* 调整输出层偏置 */
for(int i = 0 ; i < O ; i++)
{
to[i] -= LR2*dv[i];
}
/* 调整输入层与隐含层间权重 */
for(int i = 0 ; i < I ; i++)
{
for(int j = 0 ; j < H ; j++)
{
w[i][j] -= LR*dw[j]*trainDS[index].x[i];
}
}
/* 调整隐含层偏置 */
for(int i = 0 ; i < H ; i++)
{
th[i] -= LR2*dw[i];
}
}
/*
* 类构造函数,初始化BP神经网络结构和训练参数
* int _I: 输入参数数目,包括偏置值对应的参数-1。
* int _O: 输出值数目。
* int A: 隐含层调整因子(1~10)。
* double _LR: 权重学习速率(0.01~0.8)。
* double _LR2: 偏置学习速率(0.01~0.8)。
* double _LRDecay: 学习速率衰减率(每次衰减与当前LR相乘)。
* double _C: 误差函数收敛阈值。
* bool regression: 拟合标识。
*/
BP::BP(int _I, int _O, int A, double _LR, double _LR2, double _LRDecay, double _C, bool _regression)
{
/* ========== 初始化常数 ========== */
I = _I;
H = ceil(sqrt(_I + _O)) + A;
O = _O;
LR = _LR;
LR2 = _LR2;
LRDecay = _LRDecay;
C = _C;
regression = _regression;
/* ========== 初始化容器 ========== */
srand((unsigned)time(NULL));
/* 初始化w */
w = new double*[I];
for(int i = 0 ; i < I ; i++)
{
w[i] = new double[H];
for(int j = 0 ; j < H ; j++)
{
w[i][j] = rand()/(double)RAND_MAX;
}
}
/* 初始化dw */
dw = new double[H];
/* 初始化th */
th = new double[H];
for(int i = 0 ; i < H ; i++)
{
th[i] = rand()/(double)RAND_MAX;
}
/* 初始化u */
u = new double[H];
/* 初始化v */
v = new double*[H];
for(int i = 0 ; i < H ; i++)
{
v[i] = new double[O];
for(int j = 0 ; j < O ; j++)
{
v[i][j] = rand()/(double)RAND_MAX;
}
}
/* 初始化dv */
dv = new double[O];
/* 初始化to */
to = new double[O];
for(int i = 0 ; i < O ; i++)
{
to[i] = rand()/(double)RAND_MAX;
}
/* 初始化y */
y = new double[O];
}
/* 类析构函数,释放容器分配的堆空间 */
BP::~BP()
{
/* 释放w */
for(int i = 0 ; i < I ; i++)
{
delete []w[i];
}
delete []w;
/* 释放dw */
delete []dw;
/* 释放th */
delete []th;
/* 释放u */
delete []u;
/* 释放v */
for(int i = 0 ; i < H ; i++)
{
delete []v[i];
}
delete []v;
/* 释放dv */
delete []dv;
/* 释放to */
delete []to;
/* 释放y */
delete []y;
}
/* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
double BP::train(int sampleCnt, int trainCnt)
{
fillTrainDS(sampleCnt);
double e;
while(trainCnt--)
{
e = 0;
for(int i = 0 ; i < trainDS.size() ; i++)
{
/* 预测 */
forward(i);
/* 误差积累 */
for(int j = 0 ; j < O ; j++)
{
e += pow(y[j] - trainDS[i].d[j], 2.0);
}
/* 调整 */
backward(i);
}
e /= 2*sampleCnt;
/* 学习速率衰减 */
if (LR > 0.01)
{
LR *= LRDecay;
}
}
clearTrainDS();
return e;
}
/*
* 使用指定数目的样本循环训练。
* 误差函数值进入可接受范围判定收敛并停止训练;
* 到达最大训练次数时停止训练。
* 返回是否收敛。
*/
bool BP::trainTillConvergent(int sampleCnt, int maxEpoch)
{
fillTrainDS(sampleCnt);
double e;
for(;;)
{
e = 0;
for(int i = 0 ; i < trainDS.size() ; i++, maxEpoch--)
{
if (!maxEpoch)
{
clearTrainDS();
return false;
}
/* 预测 */
forward(i);
/* 误差积累 */
for(int j = 0 ; j < O ; j++)
{
e += pow(y[j] - trainDS[i].d[j], 2.0);
}
/* 调整 */
backward(i);
}
/* 判定收敛,中止训练 */
if (e/(2*sampleCnt) < C)
{
clearTrainDS();
return true;
}
/* 学习速率衰减 */
if (LR > 0.01)
{
LR *= LRDecay;
}
}
clearTrainDS();
return false;
}
/* 生成指定数目组数据测试当前神经网络 */
void BP::testNetwork(int testCnt)
{
fillTestDS(testCnt);
double e = 0;
for(int i = 0 ; i < testCnt ; i++)
{
forward(i, true);
for(int j = 0 ; j < O ; j++)
{
e += pow(y[j] - testDS[i].d[j], 2.0);
}
}
cout << testCnt << "组数据测试预测值相对期望值方差为:" << e/(2*testCnt) << endl;
clearTestDS();
}
/* 保存当前神经网络,即两个权重数组 */
void BP::saveNetwork(string wPath, string vPath)
{
ofstream fout_w(wPath);
if(fout_w == NULL)
{
cout << "打开文件失败" << endl;
return;
}
for(int i = 0 ; i < I ; i++)
{
for(int j = 0 ; j < H ; j++)
{
fout_w << w[i][j] << '\t';
}
fout_w << '\n';
}
fout_w.close();
ofstream fout_v(vPath);
if(fout_v == NULL)
{
cout << "打开文件失败" << endl;
return;
}
for(int i = 0 ; i < H ; i++)
{
for(int j = 0 ; j < O ; j++)
{
fout_v << v[i][j] << '\t';
}
fout_v << '\n';
}
fout_v.close();
}
/* 载入两个权重数组,还原神经网络 */
void BP::loadNetwork(string wPath, string vPath)
{
ifstream fin_w(wPath);
if(fin_w == NULL)
{
cout << "打开文件失败" << endl;
return;
}
for(int i = 0 ; i < I ; i++)
{
for(int j = 0 ; j < H ; j++)
{
fin_w >> w[i][j];
}
}
fin_w.close();
ifstream fin_v(vPath);
if(fin_v == NULL)
{
cout << "打开文件失败" << endl;
return;
}
for(int i = 0 ; i < H ; i++)
{
for(int j = 0 ; j < O ; j++)
{
fin_v >> v[i][j];
}
}
fin_v.close();
}
/* 使用指定数据预测输出值并返回输出值 */
vector<double> BP::runNetwork(vector<double> x)
{
/* 隐含层 */
for(int i = 0 ; i < H ; i++)
{
u[i] = 0;
for(int j = 0 ; j < x.size() ; j++)
{
/* 积累输入 */
u[i] += w[j][i]*x[j];
}
u[i] += th[i];
/* Sigmoid函数作为激活函数 */
u[i] = 1 / (1 + exp(-1*u[i]));
}
vector<double> o;
double y;
/* 输出层 */
for(int i = 0 ; i < O ; i++)
{
y = 0;
for(int j = 0 ; j < H ; j++)
{
/* 积累输入 */
y += v[j][i]*u[j];
}
y += to[i];
/* 分类:Sigmoid函数作为激活函数 */
if (!regression)
{
y = 1 / (1 + exp(-1*y));
}
o.push_back(y);
}
return o;
}
类的测试方法里无非是根据实际问题初始化类,做一系列的训练,调整参数,慢慢提高预测精度,test.cpp:
#include "BP.h"
#include <iostream>
using namespace std;
int main()
{
/*
* 类构造函数,初始化BP神经网络结构和训练参数
* int _I: 输入参数数目,包括偏置值对应的参数-1。
* int _O: 输出值数目。
* int A: 隐含层调整因子(1~10)。
* double _LR: 学习速率(0.01~0.8)。
* double _LR2: 偏置学习速率(0.01~0.8)。
* double _LRDecay: 学习速率衰减率(每次衰减与当前LR相乘)。
* double _C: 误差函数收敛阈值。
* bool regression: 拟合标识。
*/
BP o(7, 2, 8, 0.2, 0.02, 0.99, 3.2e-2, false);
// o.loadNetwork();
/* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
for(int i = 0 ; i < 20 ; i++)
{
cout << "误差函数值:" << o.train(1000, 2e3) << endl;
}
/*
* 使用指定数目的样本循环训练。
* 误差函数值进入可接受范围判定收敛并停止训练;
* 到达最大训练次数时停止训练。
* 返回是否收敛。
*/
// bool success = o.trainTillConvergent(1000, 1e5*1000);
// cout << "收敛:" << (success ? "success" : "fail") << endl;
o.testNetwork(1000);
o.saveNetwork();
vector<double> X;
double x = 0.1;
double y = 0.1;
double z = 0.1;
double a = 1;
double b = 1;
double c = 1;
double d = -1;
X.push_back((x + 10)/20.0);
X.push_back((y + 10)/20.0);
X.push_back((z + 10)/20.0);
X.push_back((a + 10)/20.0);
X.push_back((b + 10)/20.0);
X.push_back((c + 10)/20.0);
X.push_back((d + 10)/20.0);
vector<double> Y = o.runNetwork(X);
/* 拟合 */
// cout << "期望值:" << z - (a*x + b*y + d)/(-c) << endl;
// cout << "输出值:" << Y[0] << endl;
/* 分类 */
cout << "期望值:" << (z > (a*x + b*y + d)/(-c) ? "1\t0" : "0\t1") << endl;
cout << "输出值:" << Y[0] << '\t' << Y[1] << endl;
return 0;
}
点分类问题我先尝试了函数拟合方式,训练情况很糟糕,动辄上十万的方差。后来改用分类方式,降低训练要求,方差立马降下来了。下午例会就要做报告了,我没有训练到充分收敛,但是离平面比较远的点判断正确率已经很高了。下面提供平面分类点集问题当前训练进度下,输入层与隐含层间权重数组、隐含层与输出层间权重数组的保存文件。
输入层与隐含层间权重数组wNetwork:
0.15513 0.56829 0.461073 0.27552 0.44408 0.144657 0.95798 0.335522 0.325092 0.254423 0.79217
-1.06417 -0.0241895 3.99915 0.228275 -0.483658 6.68402 -1.61574 0.753689 4.14272 -4.58112 -3.09481
3.70442 -3.7423 0.0852013 -0.564758 -4.99619 1.29436 -4.19077 -1.75579 -0.568722 -2.57748 1.20149
-0.311858 2.12485 0.494471 -3.32854 -0.393046 1.12112 -0.585242 -3.45739 -0.687711 -1.37346 -0.860532
1.29214 0.74987 4.41025 -1.37666 0.268284 6.72309 2.0978 -1.8259 4.20105 -6.15165 5.04569
-3.76331 -3.22432 -0.450262 1.77063 -5.89358 3.40051 3.64122 1.30879 -0.98291 -0.931413 -1.72719
9.53636 9.64562 9.82743 13.4779 -12.3668 12.5147 8.37658 6.32437 -10.263 -13.8476 -5.31933
隐含层与输出层间权重数组vNetwork:
31.4711 -31.5094
27.536 -27.5713
-25.7692 25.7999
-27.4329 27.467
-17.6709 17.6933
-18.464 17.5204
25.8167 -25.8471
-23.2151 23.2434
27.3533 -27.387
26.9126 -26.9519
-24.7905 24.8209
这个类我还做过a+b问题的函数拟合,收敛非常顺利,精度很高。
后来,例会上提任务的博士说他明明是要我们寻找分割点集的平面,我把问题泛化成寻找点与平面位置关系规则的问题了,能用泛化能力不太强的BP整的差不多收敛也是不容易。我表示生无可恋……
读者注意一下,上一篇BP网络的理论介绍中,隐层与输入层之间的权重调整值的推导有一些问题,少乘了一个wij。推导已经改过来了,但是比较忙没空改代码,有需要的同学请自己参照上一篇博文修改一下代码。OTZ