用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;
}

运行效果

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

当前非线性拟合和多元拟合的工具较少,这是针对常用的拟合算法,开发的一款数据拟合为主的软件。包括线性拟合的各种算法非线性拟合的各种算法,以及多元拟合的各种算法。其中提供了很多非线性方程的模型,以满足不同的需求,也可以制定自己所需要的指定非线性方程模型的,采用最先进的初始值估算算法,无需初始值就可以拟合自己想要的非线性方程模型各个模块的介绍如下。 1.线性拟合算法模块 根据最小二乘拟合算法,对输入的数据进行变量指定次方的拟合。同时可对自变量或因变量进行自然对数和常用对数的转换后再拟合。根据实际情况,开发了单调性拟合以针对各种定量分析的用途。同时开发了,针对一组数据,得到最高相关系数的自动拟合功能,由程序自动选择拟合次数以及自变量和因变量的数据格式。 2.非线性拟合算法模块 根据非线性方程的特点,开发了最先进的智能初始值估算算法,配合LM迭代算法,进行非线性方程的拟合。只需要输入自变量和因变量,就可以拟合出所需要的非线性方程。拟合相关系数高,方便快捷。并借助微粒群算法,开发了基于微粒群的智能非线性拟合算法拟合出方程的相关系数相当高,甚至会出现过拟合现象。 3.多元拟合算法模块 根据最小二乘算法的原理开发了多元线性拟合算法,同时开发了能够指定变元次数的高次多元线性拟合。由于多元变量的情况下函数关系复杂,采用高次多元线性拟合能有效提高拟合效果而不会出现过拟合现象。同时针对每个变元可能最合适的拟合次数不一定都一样,开发了自适应高次多元拟合算法
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值