用C++实现多层神经网络BP算法,拟合一元非线性函数(以y=sin(x)为例)

前言:
笔者在学习过程中遇到了需要用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神经元模型

如图所示为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=1nwixiθ)
需要注意:
上面所示的是完整的神经元的结构,在构建神经网络时,神经元往往会按照其“职能”分为两类:①输入神经元(不含阈值、不经激活处理、输入等于输出);②功能神经元(含有阈值、激活处理)。输入神经元包含在输入层中,功能神经元包含在隐藏层、输出层中。

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) limx0sgn(x)=limx0+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 (yy^)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)) zy^(假设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;
}

运行效果

黄色是训练数据,红色是拟合结果,拟合效果最好时的情况如图
在这里插入图片描述
在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值