前言:
笔者在学习过程中遇到了需要用C/C++实现BP神经网络算法拟合y=sin(x),x∈(-2
π
\pi
π,2
π
\pi
π)的问题,于是搜索了相关文章和实现代码发现,网上用C++实现的BP算法大都是基于固定的神经网络层数、固定的神经元个数、固定的损失函数、固定的激活函数等等,灵活性低且扩展性差,不具有特别好的普适性,因此笔者根据BP算法的原理,加上程序设计的思想,实现了可变层数、可变宽度,可自定激活函数、自定义损失函数的BPNN神经网络,经过测试,本程序可以训练(监督学习)出很好的模型来拟合y=sin(x),最好的运行效果如图(黄色曲线的是训练集的图像,红色是测试/验证集的图像)
正文:
BP(error Back-propagation)错误逆传播算法又名“反向传播算法”,是神经网络的重要算法,本质上是利用梯度下降的方法最小化误差,然后估计出模型(Model)的参数的过程。
理论
1.神经元
如图所示为M-P神经元的模型,它有输入(input)、输出(output)、阈值(threshold)或者叫偏置(bias)、连接权(weight)等信息。它的作用是把当前神经元的输入
∑
i
=
1
n
w
i
x
i
\sum_{i=1}^{n}w_ix_i
∑i=1nwixi 减去阈值
θ
\theta
θ 然后交给激活函数
f
f
f 处理后得到输出
y
y
y ,即
y
=
f
(
∑
i
=
1
n
w
i
x
i
−
θ
)
y=f\bigg(\sum_{i=1}^{n}w_ix_i-\theta\bigg)
y=f(i=1∑nwixi−θ)
需要注意:
上面所示的是完整的神经元的结构,在构建神经网络时,神经元往往会按照其“职能”分为两类:①输入神经元(不含阈值、不经激活处理、输入等于输出);②功能神经元(含有阈值、激活处理)。输入神经元包含在输入层中,功能神经元包含在隐藏层、输出层中。
2.阈值
上边提到, ∑ i = 1 n w i x i \sum_{i=1}^{n}w_ix_i ∑i=1nwixi 要减去阈值 θ \theta θ 然后才交给激活函数 f f f 处理,这个阈值(threshold)是什么东西呢?其实它就是偏置(bias),偏置可以简单理解为我们在做决策时要有一个偏好,偏好限制越严格,判断错误的概率就越小,但是泛化能力可能就越弱,偏好限制越松,判断错误的概率就越大,模型精度可能就越差,所以需要找到一个尽可能好的阈值/偏置,力求达到精度和泛化性能都比较好的一个程度。一般在神经网络初始化时随机生成一个(0,1)之间的数作为阈值的初始值,在训练中按梯度下降策略反复更新这个阈值。
3.激活函数
上面提到,神经元的输入(input)需要交给激活函数
f
f
f 处理后的到输出
y
y
y ,这里的
f
f
f 具体是什么函数呢?答案是根据具体问题而定,对不同的任务可能会使用不同的激活函数。
例如,判断一个瓜是否为好瓜,可以用1代表是,用0代表否,只需要构建一个输出层含一个神经元(其他层神经元节点数合理即可),激活函数输出Y∈{0,1}的神经网络就可以解决这一问题,那么,它最理想的激活函数就是
s
g
n
sgn
sgn(阶跃函数),如下图
但是,阶跃函数连续但不可导(
l
i
m
x
→
0
−
s
g
n
(
x
)
≠
l
i
m
x
→
0
+
s
g
n
(
x
)
lim_{x\rightarrow0^{-}}sgn(x) \neq lim_{x\rightarrow0^+}sgn(x)
limx→0−sgn(x)=limx→0+sgn(x),x=0处的sgn(x)左右极限不相等),因而不能利用梯度下降法更新参数。所以,常用的激活函数是下面的
s
i
g
m
o
i
d
sigmoid
sigmoid 函数
不过
s
i
g
m
o
i
d
sigmoid
sigmoid函数就不是输出0或1其中一个整数,而是输出0~1之间的一个小数,此时我们将这个数赋予一个特定的含义——是好瓜的概率,假设当是好瓜的概率>0.5时我们认为是好瓜,反之是坏瓜,这样就利用
s
i
g
m
o
i
d
sigmoid
sigmoid激活解决了瓜的判断问题。
当然,除了
s
i
g
m
o
i
d
sigmoid
sigmoid函数外,还有
t
a
n
h
tanh
tanh、
R
e
L
u
ReLu
ReLu、
s
o
f
t
m
a
x
softmax
softmax等函数常被用作激活函数,这里不再过多展开。
4.神经网络
多个神经元按照层次结构顺次连接,同层之间不互连、相邻层之间全连接、不跨层相连,构成神经网络(即“前馈神经网络”),如下图
(1)神经网络的输入输出
按照神经元的层次和功能对神经网络可以划分为输入层、隐层(隐藏层)、输出层;输入和输出各占一层,其余层都是隐藏层。
需要注意:
①输入层神经元的个数(输入层的宽度)代表示例(instance)的特征数量(即输入向量x的维度),输出层神经元的个数(输出层的宽度)表示类别数量。
②样本(sample)标记(label同“标签”)需要作归一化处理,才能直接比较标记
y
y
y和预测值
y
^
\hat{y}
y^的差距,如果没有对标记做归一化处理,那么神经网络最后一层输出神经元就不做激活输出(本文就是随后一层输出不做激活处理)。
③输入神经元的输出等于输入,不用偏置也不用激活处理。
(2)连接权
各相连节点之间会有权重( w e i g h t weight weight),这个权重就是我们要训练的模型(Model)的参数之一。连接权的表示方式为 w i j w_{ij} wij,表示当前层第 i i i个(神经元)节点到前向的下一层第 j j j个(神经元)节点的连接权。一般在神经网络初始时会从0~1区间随机选一个小数作为初始权值,然后在随后的训练中反复修改这个权值。
5.正向传播
正向传播的过程实际上就是计算神经网络最后一层输出(output)的过程;计算每层每个神经元的input都需要用到上一层的output,如下图
根据图中红线所示,求
b
h
b_h
bh节点的input需要输入层所有节点的output、与
b
h
b_h
bh相应的连接weight;求
y
i
y_i
yi节点的input,需要隐藏层所有节点的output、与
y
i
y_i
yi相应的连接weight。由图中所示信息,已知隐层第h个神经元(
b
h
b_h
bh)的input为
α
h
=
∑
i
=
1
d
v
i
h
x
i
\alpha_h=\sum_{i=1}^{d}v_{ih}x_i
αh=∑i=1dvihxi,若其阈值(偏置)为
θ
h
\theta_h
θh,激活函数为
f
f
f,可计算
b
h
b_h
bh的output即为
b
h
=
f
(
α
h
−
θ
h
)
b_h=f\big(\alpha_h-\theta_h\big)
bh=f(αh−θh),其余各功能型神经元的输入输出计算过程也是如此,这里不再赘述。
6.反向传播
一次反向传播的过程实际上就是,指定一个公式
E
E
E 衡量样例标记(真实值)
y
y
y 与神经网络输出(预测值)
y
ˆ
y^{\^{}}
yˆ 的差距,这个公式可以是均方差
(
y
−
y
^
)
2
\Big(y-\hat{y}\Big)^2
(y−y^)2,然后沿着
E
E
E对各个
w
(
w
e
i
g
h
t
)
w(weight)
w(weight)、
θ
(
或叫
b
i
a
s
)
\theta(或叫bias)
θ(或叫bias)的梯度的负方向,在一个
η
\eta
η(学习率) 步率的改变量下进行修改,直至传递到输入层停止,完成所有连接权
w
w
w 和阈值(偏置)
θ
(
b
i
a
s
)
\theta(bias)
θ(bias) 的更新这一过程。具体计算过程,结合图来看
代码实现
1.相关类的声明
(1)Neuron神经元
每个神经元都有输入和输出,因此Neuron类有两个属性:input和output,Neuron类还有许多成员函数,将这些成员函数均声明为虚函数或纯虚函数,这样方便实现多态。
//Neuron.h
#ifndef Neuron_h
#define Neuron_h
//神经元
class Neuron
{
protected:
double input;//输入
double output;//输出
public:
virtual double getOutput() = 0;
virtual void setBias(double theta) {}
virtual double getBias()=0;
virtual void setInput(double x) = 0;
virtual double getInput() {return input;}
virtual double getNoActOutput() { return 0; }
Neuron() { input = output = 0; }
~Neuron() {}
};
#endif //Neuron_h
功能神经元的激活输出getOutput()函数的实现如下
//Neuron.cpp
#include "Neuron.h"
double FuncNeuron::getOutput()
{
output = actFun(input - bias);
return output;
}
1)InputNeuron输入神经元
输入神经元的input和output是一样的,没有偏置(偏置为0)和激活功能,需要重写getBias()和getOutput()函数
//Neuron.h
//输入神经元
class InputNeuron : public Neuron {
public:
InputNeuron(double x) { output = input = x; }
void setInput(double x) { output = input = x; }
~InputNeuron() {}
//输出
virtual double getOutput() { return output; }
virtual double getBias() { return 0; }
};
2)FuncNeuron功能神经元
功能神经元有偏置和激活函数,可分别通过setBias()和setActFunc()函数对神经元的偏置和激活函数进行设置;功能神经元重写从Neuron类继承来的getOutput()函数,返回激活处理后的output。
//Neuron.h
//功能神经元
class FuncNeuron : public Neuron {
private:
double bias;//阈值/偏置
double(*actFun) (double x);//激活函数
public:
FuncNeuron(double theta):actFun(nullptr)/*, delta_bias(0)*/{ bias = theta; }
~FuncNeuron(){}
//输入
void setInput(double alpha) { input = alpha; }
double getInput() { return input; }
//设置激活函数
void setActFunc(double(*fun)(double)) { actFun = fun; }
//激活输出
virtual double getOutput() override;
//非激活输出
double getNoActOutput() { return input - bias; }
//修改阈值/偏置
void setBias(double theta) { bias = theta; }
//获取阈值/偏置
double getBias() { return bias; }
};
(2)NeuralNet神经网络
神经网络主要维护了所有层的神经元neuron、保存了各层神经元之间的连接权weight、连接权的修改量delta_weight、从输出层到各神经元节点的累积梯度gradient等数据,声明了一次训练(按层向前传播forward+按层反向传播backward)、多次训练train等等成员函数,最关键的BP算法就是在forward()和backward()函数中实现的。
//NeuralNet.h
#include"Neuron.h"
#include"Util.h"
#ifndef NeuralNet_h
#define NeuralNet_h
/*
* 神经网络
* 有监督训练 回归问题
*/
class NeuralNet
{
private:
size_t layers_num;//层数
std::vector<size_t> neurons_num;//各层神经元个数
std::vector<std::vector<Neuron*>>net;//神经网络
std::vector<std::vector<std::vector<double>>> weights; //各层连接权 weight[i][j][k]
std::vector<std::vector<double>> grads;//从输出层累积到各节点的梯度
std::vector<std::vector<std::vector<double>>> delta_weights; //连接权修改量 Δw
double learnRate;//学习率
public:
NeuralNet(std::vector<size_t>& neurons_num, double(*activeFun)(double), double learnRate = 0.1);
void printWeights(const char* weightsOutFile ="weights.txt");
void printBias(const char* biasOutFile = "bias.txt");
void forward(size_t layer);//前向传播
void backward(size_t layer, double(*gradFun)(double));//反向传播
void trainOnce(Sample& sample, double(*gradFun)(double), double(*lossFun)(double, double),double *);//一次训练
void train(std::vector<Sample>& dataSet, double(*gradFun)(double), double(*lossFun)(double, double), size_t k);
void test(std::vector<Sample>& testSet);
void updateWeights();
void printDWeights();
~NeuralNet() ;
};
#endif //NeuralNet
2.类成员函数的实现
(1)初始化神经网络
初始化神经网络的工作放在构造函数里做,建立一个layers_num层的网络,各层功能神经元的偏置(阈值)和各相邻层神经元之间的连接权初始化为(0,1)区间的一个随机数,权重的增量和梯度的增量均初始化为0,功能神经元的激活函数初始化为形参activeFun指向的激活函数。
//NeuralNet.cpp
#include <algorithm>
#include "NeuralNet.h"
#define SHOW_INFO if(false)
#define RANDOM_W_T rand() % 100 * 0.01 //(0,1)范围内随机初始化网络中所有连接权和阈值
#define SHOW_BP_INFO if(false)
NeuralNet::NeuralNet(std::vector<size_t>& neurons_num, double(*activeFun)(double), double learnRate)
{
this->neurons_num = neurons_num;
layers_num = neurons_num.size(); //网络总层数
this->learnRate = learnRate;
SHOW_INFO std::cout << "========神经元阈值/偏置========\n";
for (size_t i = 0; i < layers_num; i++) {//初始化各层神经元
size_t max = neurons_num.at(i);
std::vector<Neuron*>neurons;//一层神经元
std::vector<double> grad;//一层梯度
if (i == 0) {//输入层
SHOW_INFO std::cout << "net[0]输入层:\n";
for (size_t j = 0; j < max; j++) {
InputNeuron* i_neuron =new InputNeuron(0);//动态分配
SHOW_INFO std::cout << i_neuron->getOutput() << "\t";
neurons.push_back(i_neuron);
grad.push_back(0);
}
net.push_back(neurons);
grads.push_back(grad);
}
else if (i > 0) {//隐藏层、输出层
SHOW_INFO std::cout << std::endl << "net[" << i;
SHOW_INFO if (i < layers_num - 1)std::cout << "]隐含层:\n";
else std::cout << "]输出层:\n";
for (size_t j = 0; j < max; j++) {
double t = RANDOM_W_T;
FuncNeuron *f_neuron = new FuncNeuron(t ? t : 0.5);
f_neuron->setActFunc(activeFun);
SHOW_INFO std::cout << f_neuron->getBias() << "\t";
neurons.push_back(f_neuron);
grad.push_back(0);
}
net.push_back(neurons);
grads.push_back(grad);
}
}
//初始化连接权
for (size_t lay = 0; lay < layers_num - 1;lay++) {
std::vector<std::vector<double>> ws;
std::vector<std::vector<double>> dWs;
for (size_t i = 0; i < net[lay].size();i++) {
std::vector<double> w;
std::vector<double> dW;
for (size_t j = 0; j < net[lay + 1].size(); j++) {
w.push_back(RANDOM_W_T);
dW.push_back(0); //delta_w全部初始化为0
}
ws.push_back(w);
dWs.push_back(dW);
}
weights.push_back(ws);
delta_weights.push_back(dWs);
}
}
需要注意:为了实现多态,net二维数组的每个元素都是Neuron*指针类型,初始化时通过new申请了堆内存,因此,在NeuralNet类的析构函数中一定要逐个delete释放这些内存,避免造成内存泄漏!
//NeuralNet.cpp![请添加图片描述](https://img-blog.csdnimg.cn/b791a35d499f45d5a9d1e29872f604ff.png)
NeuralNet::~NeuralNet()
{
//std::cout << "net[" << i << "][" << j << "]" << std::endl;
for (size_t i = 0; i < net.size(); i++) {
for (Neuron* j : net[i]) {
delete j; //释放内存
j = nullptr;
}
}
}
(2)单次训练
每次训练就是将样本示例对应赋给输入层神经元,然后按层向隐层、输出层计算,直到输出层所有神经元的output计算完成,然后比较标签真实值y与预测值y^的差异,按梯度下降的策略计算输出层、隐层神经元的偏置改变量,计算输出层、隐层,相邻层之间神经元的连接权的改变量,直到到达输入层,然后更新所有偏置和连接权,这样就完成了一次训练。
//NeuralNet.cpp
void NeuralNet::trainOnce(Sample& sample, double(*gradFun)(double),double(*lossFun)(double,double), double* loss)
{
for (size_t i = 0; i < neurons_num[0]; i++) net[0][i]->setInput(sample.getX()); //net[0][0]->setInput(sample.getX());
for (size_t layer = 1; layer < layers_num; layer++) {//前向遍历所有层
forward(layer);
}
double y_hat=0,y,current_loss;
SHOW_INFO std::cout << "\nnet[" << layers_num - 1 << "]输出层noActOutput:";
for (size_t l = 0; l < neurons_num[layers_num - 1]; l++) { //y_hat = net[layers_num - 1][0]->getNoActOutput();
y_hat = net[layers_num - 1][l]->getNoActOutput();
//std::cout << y_hat << "\t";
}
y = sample.getY();
SHOW_INFO std::cout << y_hat<<std::endl<<"真实值:" << y;
current_loss = lossFun(y_hat, y);
*loss = (*loss) + current_loss; //std::cout << "loss:" << 0.5*(y_hat - y) * (y_hat - y) << std::endl;
double pianEpianYHat = (y_hat - y) * 1;
grads[layers_num - 1][0] = pianEpianYHat;//更新输出层节点的累积梯度
for (size_t layer = layers_num - 1; layer > 0; layer--) {//反向遍历所有层
backward(layer, gradFun);
}
//更新权
updateWeights();
}
trainOne()函数中调用了forward()、backward()和updateWeights()函数,这几个函数的具体实现如下
1)forward正向传播
正向传播的过程就是从输入层数据计算隐层数据、一直计算到得到输出 y ^ \hat{y} y^为止,具体实现就是用两重循环,外层循环遍历层,内层循环遍历神经元,逐个节点计算input、output,先来看代码:
//NeuralNet.cpp
//计算第layer层的输入输出
void NeuralNet::forward(size_t layer)
{
SHOW_INFO std::cout <<"\nnet["<<layer<<"]层input/output:\n";
for (size_t i = 0; i < net[layer].size(); i++) {
double alpha = 0, w=0, x=0;
for (size_t li = 0; li < net[layer-1].size();li++) {
x = net[layer - 1][li]->getOutput();
w = weights[layer - 1][li][i];
alpha += (w*x);
SHOW_INFO if (li != 0)std::cout << "+";
SHOW_INFO std::cout << w << "*" << x ;
}
SHOW_INFO std::cout << "=" << alpha;
net[layer][i]->setInput(alpha);
SHOW_INFO std::cout << "\toutput:" << net[layer][i]->getOutput() << std::endl;
}
}
然后根据图来理解这个过程就很容易了,如下
2)backward反向传播
由于本次实现的输出层神经元的
o
u
t
p
u
t
j
的值是
i
n
p
u
t
j
−
θ
j
output_j的值是input_j-\theta_j
outputj的值是inputj−θj,并没有做激活
f
(
i
n
p
u
t
j
−
θ
j
)
f\Big(input_j-\theta_j\Big)
f(inputj−θj)处理。
因此需要判断当前要计算的是不是输出层,如果是输出层的话,链式求导时会少一项 ∂ y ^ ∂ z ( 假设 y ^ = f ( z ) ) \frac{\partial{\hat{y}}}{\partial{z}}(假设\hat{y}=f(z)) ∂z∂y^(假设y^=f(z)),除此以外,其它层的计算是一致的,代码如下
//NeuralNet.cpp
//计算layer层神经元的delta_bias,计算layer-1到layer层神经元的delta_weight
void NeuralNet::backward(size_t layer, double(*gradFun)(double))
{
SHOW_BP_INFO std::cout <<"反向传播net["<<layer<<"]:" << std::endl;
if (layer == layers_num - 1) {
size_t outlayer_i = 0;
double lastLayerG = grads[layer][outlayer_i];
SHOW_BP_INFO std::cout << "偏E/偏y^:" << lastLayerG << std::endl;
for (size_t i = 0; i < neurons_num[layer-1]; i++) {//遍历layer-1层的节点
double g_weight = lastLayerG * (net[layer - 1][i]->getOutput());//layer-1层第i个神经元的输出
grads[layer - 1][i] = lastLayerG*weights[layer-1][i][outlayer_i];//lastLayerG乘以u对x的导
double delta_weight = g_weight * -1 *learnRate;//Δy=y'(x)Δx,Δx=-0.1
delta_weights[layer - 1][i][outlayer_i] = delta_weight;//第layer-1层,第i个神经元到输出层第outlayer_i个神经元的连接权
SHOW_BP_INFO std::cout << "delta_weights[" << layer - 1 << "][" << i << "]["<<outlayer_i<<"]:" << delta_weight << std::endl;
}
SHOW_BP_INFO for (size_t j = 0; j < neurons_num[layer - 1]; j++) {
std::cout << "grads["<<layer-1<<"]["<<j<<"]:" << grads[layer - 1][j] << std::endl;
}
double g_bias = lastLayerG * (-1);//E对输出节点的偏置的导
double delta_bias = g_bias * -1 * learnRate;//偏置的改变量
SHOW_BP_INFO std::cout << "delta_bias["<<layer<<"][" <<0<< "][" << outlayer_i << "]:" << delta_bias << std::endl;
net[layer][outlayer_i]->setBias(net[layer][outlayer_i]->getBias() + delta_bias);//更新输出层outlayer_i个节点的偏置
}
else {
for (size_t i = 0; i < neurons_num[layer]; i++) {//遍历layer层神经元
double lastLayerG = grads[layer][i] * gradFun(net[layer][i]->getNoActOutput());//上一个g乘以本层i节点actFun对非激活输入α的导
for (size_t j = 0; j < neurons_num[layer - 1]; j++) {//遍历layer-1层神经元
double g_weight = lastLayerG * (net[layer - 1][j]->getOutput());
grads[layer - 1][j] = lastLayerG * weights[layer - 1][j][i];//更新累积到laye-1层第j个神经节点的梯度
double d_weight = g_weight * -1 *learnRate;
delta_weights[layer - 1][j][i] = d_weight;//更新第layer-1层,j神经元到layer层i神经元的连接权
SHOW_BP_INFO std::cout << "delta_weights[" << layer - 1 << "][" << j << "][" << i << "]:" << d_weight << std::endl;
}
double g_bias = lastLayerG * (-1);//lastLayerG乘以α对b的导
double d_bias = -1 * g_bias * learnRate;//本层i神经元节点的bias的改变量
SHOW_BP_INFO std::cout << "delta_bias[" << layer << "][" << i << "]:" << d_bias << std::endl;
net[layer][i]->setBias(net[layer][i]->getBias() + d_bias);//更新layer层i节点偏置
}
}
}
3)updateWeights参数更新
//NeuralNet.cpp
void NeuralNet::updateWeights()
{
SHOW_BP_INFO std::cout << "\n========连接权更新!========\n";
for (size_t lay = 0; lay < layers_num - 1; lay++) {
for (size_t i = 0; i < neurons_num[lay]; i++) {
for (size_t j = 0; j < neurons_num[lay + 1]; j++) {
//std::cout << "delta_w[" << lay << "][" << i << "][" << j << "]=" << delta_weights[lay][i][j] << '\t';
(weights[lay][i][j]) += (delta_weights[lay][i][j]);
}
}
}
}
(3)模型训练和测试
train()函数实现模型的训练,传入数据集和自定的梯度计算函数、自定的损失函数后就可以按照自己指定的梯度、损失计算方法去训练模型。
//NeuralNet.cpp
void NeuralNet::train(std::vector<Sample>& dataSet, double(*gradFun)(double), double(*lossFun)(double, double), size_t k)
{
double loss,batchSize = dataSet.size();
size_t loop = 0;
do{
loss = 0;
std::random_shuffle(dataSet.begin(),dataSet.end()); //打乱数据集
for (size_t i = 0; i < batchSize; i++) {
trainOnce(dataSet[i], gradFun, lossFun, &loss);
}
std::cout << "平均损失:" << loss / batchSize <<std::endl;
//if(loop==k/2) learnRate=0.5*learnRate;
loop++;
} while (loop<k);//loss < 1E-5||
}
测试模型比较简单,传入验证集数据,将示例逐个输入到神经网络,调用前向传播函数,算得对应的 y ^ \hat{y} y^即可
//NeuralNet.cpp
void NeuralNet::test(std::vector<Sample>& testSet)
{
std::vector<Sample>::iterator i = testSet.begin();
for (;i!=testSet.end();i++) {
for(size_t n=0;n<neurons_num[0];n++)
net[0][n]->setInput(i->getX());
for (size_t lay = 1; lay < layers_num;lay++) {
forward(lay);
}
for (size_t j = 0; j < neurons_num[layers_num - 1]; j++){
i->setY(net[layers_num - 1][j]->getNoActOutput());
//std::cout <<"x:"<< i->getX() <<"\ty^:" << i->getY()<<std::endl;
}
}
}
(4)输出并保存模型
//NeuralNet.cpp
void NeuralNet::printWeights(const char* weightsOutFile) {
std::cout << "\n========最终连接权========\n";
std::ofstream ofs;
if (weightsOutFile) {
ofs.open(weightsOutFile);
if (ofs.is_open()) {
for (size_t lay = 0; lay < layers_num - 1; lay++) {
std::cout << "net[" << lay << "]" << "-net[" << lay + 1 << "]层连接权:\n";
for (size_t i = 0; i < net[lay].size(); i++) {
for (size_t j = 0; j < net[lay + 1].size(); j++) {
std::cout << "weights[" << lay << "][" << i << "][" << j << "]=" << weights[lay][i][j] << '\t';
ofs << weights[lay][i][j] << "\t";
}
std::cout << std::endl;
ofs << '\n';
}
ofs << '\n';
}
ofs.close();
}
}
}
void NeuralNet::printDWeights() {
std::cout << "\n========连接权修改值========\n";
for (size_t lay = 0; lay < layers_num - 1; lay++) {
std::cout << "net[" << lay << "]" << "-net[" << lay + 1 << "]层连接权:\n";
for (size_t i = 0; i < neurons_num[lay]; i++) {
for (size_t j = 0; j < neurons_num[lay + 1]; j++) {
std::cout << "delta_w[" << lay << "][" << i << "][" << j << "]=" << delta_weights[lay][i][j] << '\t';
//std::cout << weights[lay][i][j] << "\t";
}
std::cout << std::endl;
}
std::cout << '\n';
}
}
void NeuralNet::printBias(const char* biasOutFile)
{
std::cout << "\n========最终神经元阈值/偏置:========\n";
std::ofstream ofs;
ofs.open(biasOutFile);
if (ofs.is_open()) {
for (auto i : net) {
for (auto j : i) {
ofs << j->getBias()<<'\t';
std::cout <<j->getBias()<<'\t';
}
ofs << '\n';
std::cout << std::endl;
}
ofs.close();
}
}
3.工具方法
(1)工具类的声明
//Util.h
#include <cmath>
#include <vector>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <ctime>
#include <graphics.h>
#include<conio.h>
#ifndef Util_h
#define Util_h
/*
* 样例 Sample(回归问题)
* x:实例 instance
* y:标记 label
*/
class Sample {
private:
double x;//实例(一元/一维~多元/多维)
double label;//标记
public:
Sample(double x, double y = 0) { this->x = x; this->label = y; }
void setY(double newY) { label = newY; }
double getX()const { return x; }
double getY()const { return label; }
~Sample() {}
};
//生成数据集
void makeData(const char* outputFile, double (*f)(double), double begin, double end, size_t quantity);
//生成验证数据(测试泛化性)
void makePredicData(std::vector<Sample>& preData, double start, double end, size_t quantity);
//加载数据集 data
void loadData(const char* fileName, std::vector<Sample>& dataSet);
//初始化窗体
void initWind();
//绘制图像
void draw(std::vector<Sample>& data, COLORREF color, int thick);
//sigmoid()
double sigmoid(double x);
//sigmoid导函数
double sigmoid_grad(double x);
//平方差函数
double square_diff(double y_hat,double y);
#endif //Util_h
(2)工具方法的实现
1)生成数据集并保存到文件
//Util.cpp
/*
* 生成数据集
* outputFile:目标文件
* begin:起始取值
* end:结束取值
* quantity:数据数量
*/
void makeData(const char* outputFile, double (*f)(double), double begin, double end, size_t quantity) {
std::ofstream ofs;
double x = begin, y = 0;
double step = (end - begin) / quantity;
ofs.open(outputFile);
if (ofs.is_open()) {
ofs << "x\t\t\ty\n";
for (size_t i = 0; i < quantity; i++, x += step) {
y = f(x);
ofs << x << " " << y << "\n";
}
ofs.close();
std::cout << quantity << "条数据生成完毕!请打开" << outputFile << "查看\n";
}
}
2)从文件读取数据集
//Util.cpp
//加载数据集 data
void loadData(const char* fileName, std::vector<Sample>& dataSet) {
std::ifstream ifs;
ifs.open(fileName);
if (ifs.is_open()) {
char line[64]{ 0 };
ifs.getline(line, sizeof(line));
int count = 0;
double x, y;
for (; !ifs.eof(); count++) {
ifs >> x >> y;
dataSet.push_back(Sample(x, y));
//std::cout << "x:"<< std::setw(12)<<std::left << x << "y:" << y << "\n";
}
ifs.close();
}
else {
std::cerr << fileName << "打开失败!" << std::endl;
}
}
3)生成测试数据集
//Util.cpp
//生成验证数据(测试泛化性)
void makePredicData(std::vector<Sample>& preData, double start, double end, size_t quantity) {
double x_ = start;
double step_ = (end - start) / quantity;
for (size_t i = 0; i < quantity; i++, x_ += step_) {
preData.push_back(Sample(x_));
//std::cout << x_ << std::endl;
}
}
4)绘制图像
//Util.cpp
#include "Util.h"
static const unsigned int height = 800;
static const unsigned int width = 1000;
//初始化窗体
void initWind() {
initgraph(width, height, SHOWCONSOLE);
line(width / 2, 0, width / 2, height);
line(0, height / 2, width, height / 2);
}
//绘制图像
void draw(std::vector<Sample>& data, COLORREF color,int thick) {
setfillcolor(color);
setlinestyle(PS_NULL);
for (size_t i = 0; i < data.size(); i++) {
double x = data[i].getX() * 75 + (width / 2);
double y = (data[i].getY() * -200 + (height / 2));
//putpixel((int)x,(int)y, color);
fillcircle((int)x, (int)y, thick);
std::cout << "x:" << std::setw(11) << std::left << data[i].getX() << "y:" << data[i].getY() << std::endl;
}
}
5)激活函数、梯度、均方差
//Util.cpp
//sigmoid(x) ∈ (0,1)
double sigmoid(double x) {
return 1 / (1 + exp(-1 * x));
}
//sigmoid_grad
double sigmoid_grad(double x) {
return sigmoid(x) * sigmoid(1 - x);//等同于 exp(-1*x) / pow(1 + exp(-1 * x), 2);
}
//平方差
double square_diff(double y_hat, double y) {
//std::cout << "loss:" << 0.5*pow((y_hat - y), 2) << "\n";
return 0.5 *pow( (y_hat - y), 2);
}
4.main程序
测试运行程序
#define _USE_MATH_DEFINES
//#include "Util.h"
#include"NeuralNet.h"
#define dataSetSize 900
using namespace std;
int main(int agrc,char* argv[] ) {
const char* fileName = "dataset.txt";
vector<Sample> dataSet;
vector<Sample> confirmSet;
makeData(fileName, sin, -2 * M_PI, 2 * M_PI, dataSetSize);//创建数据集,保存到txt文件
makePredicData(confirmSet, -2 * M_PI, 2 * M_PI, 300);
loadData(fileName, dataSet);
srand((size_t)time(NULL));//随机种子
std::vector<size_t> neurons_num{1,10,1};
NeuralNet nn(neurons_num, sigmoid);
nn.train(dataSet, sigmoid_grad, square_diff, 520);
nn.test(confirmSet);
//nn.printDWeights();
//绘制图像
initWind();
draw(dataSet,YELLOW,2);
draw(confirmSet, RED,3);
_getch();
nn.printBias();//输出并保存bias
nn.printWeights();//输出并保存weights
return 0;
}
运行效果
黄色是训练数据,红色是拟合结果,拟合效果最好时的情况如图