首页 > 代码库 > Duanxx的神经网络学习: 自己动手写神经网络(二) 前馈网络的实现

Duanxx的神经网络学习: 自己动手写神经网络(二) 前馈网络的实现

            本文是继续Duanxx的神经网络学习: 自己动手写神经网络(一) 搭建一个简单的网络框架 写的第二篇文章,将神经网络的前馈网络实现并测试。


            本文的代码下载地址


1 为神经元Neuron添加权Weight 

         在上一篇文章中,我已经搭起了一个神经网络的框架。

         但是那只是一个框架而已,什么都没有是实现,而这个框架的最基本的东西就是神经元Nenron,这里就考虑将Neuron实现一下。

        对于一个神经元而言,它的输入是上一层神经元的输出,可以不用太关心,它也有自己的输出outputvalue,同时,它还要控制到下一层神经元的权值outputWeight,所以这里Neuron的成员变量至少就有两个:输出权值


        由于权值是和当前层神经元和下一层所有的神经元的链接,所以每个神经元的权值是以向量的形式存在向量的长度和下一层神经元的个数是一样的

       这里为了更好的操作,我将权值也作为我的对象Weight,权值除了权自身的数值weight以外,因为在神经网络中我们还需要求梯度等量,所以还有权值的差值deltweight也需要考虑进去。


        此时的类图就是下面这个样子:



            通过以上的分析,我可以得到下面的代码:

<span style="font-family:KaiTi_GB2312;font-size:18px;">/**
*@brief 类Weight的实现
*
*这里实现了神经网络中神经元的权值
*
*/
class Weight
{
public:
	double weight;
	double deltweight;
private:

};

/**
*@brief 类Neuron的实现
*
*这里实现了神经网络中神经元的类
*
*/
class Neuron
{
public:
	Neuron(){}; ///<  Neuron的构造函数
private:
	double m_outputValue;
	vector<Weight> m_outputWeights;
};
</span>





2 扩展神经元的构造函数Neuron

毫无疑问,现在我如果想要实例化一个神经元的话,我就必须要知道下一层神经元的个数,那么,这里就将神经元的构造函数做一下修改,让其接收参数:下一层神经元的个数,也就是当前神经元的输出的个数:numOutputs,那么,神经元的构造函数就改为:

<span style="font-family:KaiTi_GB2312;font-size:18px;">Neuron(unsigned int numOutputs); ///<  Neuron的构造函数</span>

            同时,在NeuralNet的构造函数中,通过下面这行代码获得下一层神经元的个数,需要注意的是,下一层的偏置(bias)是不考虑在内的。

             如果是最后输出层的神经元的话,是没有下一层神经元的,需要剔除这种情况:

<span style="font-family:KaiTi_GB2312;font-size:18px;">unsigned int numOutputs = layerCounter == topology.size() -1 ? 0 : topology[layerCounter + 1];</span>

现在需要做的就是按照下一层神经元的个数,为当前神经元添加权值对象。

在生成权值对象的同时,还需要为权值对象随机生成权重,权值的范围限制在[0,1]中,我可以通过下面这行代码生成[0,1]之间的随机数:

<span style="font-family:KaiTi_GB2312;font-size:18px;">rand()/double(RAND_MAX)</span></span>

那么,最终的Neuron的构造函数就如下代码:

<span style="font-family:KaiTi_GB2312;font-size:18px;">/**
*@brief Neuron的构造函数
*
*根据传入的是神经元输出的个数:numOutputs
*也就是下一层神经元的个数,不包括偏置
*
*@param numOutputs	下一层神经元的个数,也是当前神经元的输出的个数
*
*/
Neuron::Neuron(unsigned int numOutputs)
{
	///<按照下一层神经元的个数,为当前神经元添加权值对象
	for (unsigned int outputCounter = 0;outputCounter < numOutputs;outputCounter++)
	{
		m_outputWeights.push_back(Weight());
		m_outputWeights.back().weight = rand()/double(RAND_MAX); ///<随机生成一个在[0,1]之间的权值
	}
}</span>

         这里为了测试代码是否成功,我写了一个神经网络权值的测试函数,NeuralNetWeight,其参数为神经网络,其功能是输出神经网络中,每个神元的的权值。实现很简单:


Step1:横向的按列扫描每一层

Step2:对于每一层而言,按行,纵向的扫描每个神经元

Step3:对于每个神经元而言,扫描所有的权值


其实现代码如下:

<span style="font-family:KaiTi_GB2312;font-size:18px;">/**
*@brief NeuralNetWeight用于显示神经网络所有的权值
*
*@param neuralNet 输入的神经网络
*
*/
void NeuralNetWeight(const NeuralNet &neuralNet)
{
	vector<Layer>::const_iterator it_layer;
	vector<Neuron>::const_iterator  it_neuron;
	vector<Weight>::const_iterator it_weight;
	unsigned int layerCounter = 0;
	///<对神经网络,按层扫描
	for (it_layer = neuralNet.m_layers.begin();it_layer!=neuralNet.m_layers.end()-1;it_layer++)
	{
		layerCounter++;
		unsigned int neuronCounnter = 0;
		///<对于每一层,按列扫描神经元
		for(it_neuron = it_layer->m_neurons.begin();it_neuron != it_layer->m_neurons.end();it_neuron++)
		{
			neuronCounnter++;
			cout<<"The weights of Neron (";
			CONSOLE_RED
				cout<<layerCounter<<","<<neuronCounnter;
			CONSOLE_WHITE
			cout<<") is :"<<endl;
			///<对于每个神经元,扫描其权值
			for (it_weight = it_neuron->m_outputWeights.begin();it_weight != it_neuron->m_outputWeights.end();it_weight++)
			{
				CONSOLE_GREEN
				cout<<it_weight->weight<<"\t";
				CONSOLE_WHITE
			}
			cout<<endl;
		}
	}
}
</span>

测试结果见图:

         比如,第三行的结果(1,3)表示的是:在第一层,第三个神经元的到第二层的神经元的权值向量。





3 前馈(feedForward)网络的实现

在1.2中,我已经为整个网络初始化了权值,有了权值,就可以考虑实现网络的前馈(feedForward)了

              所谓前馈,就是基于已有的权值神经元从输入层开始,接收前一级输入,并输入到下一级,直至输出层,其数据流可以见下图:


由于前馈是接收前一层的输出,并将自己的输出放入下一层,这样的话,函数feedForward的参数就很容易确定了,就是一个输入向量,其函数声明为:

<span style="font-family:KaiTi_GB2312;font-size:18px;">void feedForward(constvector<double> &inputValues); ///< 前馈</span>


             前馈的理解,是没有任何难度的。我这里将前馈分为以下几个步骤来实现:


Step1:

              对于输入层而言,首先我需要做的事情是保证输入的数据的长度和输入层的神经元的个数是一样的,否则的话神经网络就无法工作了,这里使用了断言函数,用于输入断言:

<span style="font-family:KaiTi_GB2312;font-size:18px;">///<将输入的数据放到神经网络的输入层
	Layer &inputLayer = m_layers[0];
	for (unsigned int inputCounter = 0;inputCounter < inputValues.size();inputCounter++)
	{
		 inputLayer.m_neurons[inputCounter].m_outputValue = http://www.mamicode.com/inputValues[inputCounter];>

Step2:

将输入神经网络的数据传入到输入层,这里的操作是很简单的,输入层就是起到读取数据的作用,所以这里的操作就是将神经网络的输入直接映射到输入层的输出上,其代码实现如下:

<span style="font-family:KaiTi_GB2312;font-size:18px;">///<将输入的数据放到神经网络的输入层
	Layer &inputLayer = m_layers[0];
	for (unsigned int inputCounter = 0;inputCounter < inputValues.size();inputCounter++)
	{
		 inputLayer.m_neurons[inputCounter].m_outputValue = http://www.mamicode.com/inputValues[inputCounter];>


Step3:

           前馈传播,这个原理上就是:从隐藏层开始,依次的扫描神经网络中的每一个神经元,然后对每个神经元做一个前馈操作

           对于单个神经元而言,前馈操作指的就是将前一层神经元输出乘以相对应的权值,并在激励函数的作用下,生成输出。

            此时就需要为Neuron添加一个成员函数:feedForward,其参数为前一层神经元preLayer,即神经元Neuron中添加:

void feedForward(const Layer &prevLayer); ///<Neuron的前馈操作


这里需要注意一个问题,由于权值是前一层的神经元到当前层的神经元的,这里preLayer已经知道了前一层的神经元,但是,当前层的神经元到底是哪一个,这里应该是在神经元Neuron中使用一个成员变量m_Index做标记。

            所以需要在Neuron中添加成员变量m_Index,并更改Neuron的构造函数声明,那么相应的代码更改如下,添加myIndex

<span style="font-family:KaiTi_GB2312;font-size:18px;">Neuron(unsigned int numOutputs,unsigned int myIndex); ///<  Neuron的构造函数</span>

             在Neuron的构造函数中添加下面一行代码

<span style="font-family:KaiTi_GB2312;font-size:18px;">m_myIndex  = myIndex;</span>


在NeuralNet的构造函数中,Neuron的实例化时需要neuronCounter作为Neuron自身的下标。

currLayer.m_neurons.push_back(Neuron(numOutputs,neuronCounter));


这时候,就可以给出Neuron的feedFowrd的求和部分的代码了:

<span style="font-family:KaiTi_GB2312;font-size:18px;">///<对前一层的输出加权求和,得到当前层的输出
	///<即前一层的输出*对应的权值
	///<这里包括了前一层的偏置
	for (unsigned int neuronCounter = 0 ;neuronCounter < prevLayer.m_neurons.size();neuronCounter++)
	{
		sum += prevLayer.m_neurons[neuronCounter].m_outputValue * 
				prevLayer.m_neurons[neuronCounter].m_outputWeights[m_myIndex].weight;
	}</span>



Step4:

           在Step3中实现了上一层输出的加权求和,得到输出sum,接下来需要做的就是使用激励函数transferFunction对sum做处理。

          为了能做非线性分类问题,可以考虑使用常用的一些连续的激励函数,比如:Sigmoid函数,这里考虑使用的是tanh,主要是tanh函数在头文件<cmath>中就有,而且其导数为1 – tanh*tanh,比较的好算。

           这里为Neuron添加成员函数transferFunction(),因为这个函数也就Neuron会使用,可以直接考虑声明为静态函数:

<span style="font-family:KaiTi_GB2312;font-size:18px;">static double transferFunction(double sum);///<激励函数</span>


其函数实现为:

<span style="font-family:KaiTi_GB2312;font-size:18px;">/**
*@brief Neuron的transferFunction
*
*这里是神经元的激励函数
*
*@param sum 是当前神经元接收的前一层神经元的和。
*
*/
double Neuron::transferFunction(double sum)
{
	///< tanh -- 输出的范围是:[-1 1]
	return tanh(sum);
}
</span>
                 然后在Neuron的feedForward后面,添加上下面这行代码即可:

<span style="font-family:KaiTi_GB2312;font-size:18px;">m_outputValue = http://www.mamicode.com/Neuron::transferFunction(sum);

                这里我再写一个测试函数:NeuralNetTest()用于将神经网络中每个神经元你的输出显示出来:

<span style="font-family:KaiTi_GB2312;font-size:18px;">/**
*@brief NeuralNetOutput用于显示神经网络所有的输出
*
*@param neuralNet 输入的神经网络
*
*/
void NeuralNetOutput(const NeuralNet &neuralNet)
{
	cout<<endl;
	vector<Layer>::const_iterator it_layer;
	vector<Neuron>::const_iterator  it_neuron;


	unsigned int layerCounter = 0;
	///<对神经网络,按层扫描
	for (it_layer = neuralNet.m_layers.begin();it_layer!=neuralNet.m_layers.end()-1;it_layer++)
	{
		layerCounter++;
		cout<<"The output of Layer "<<layerCounter<<" is :"<<endl;
		///<对于每一层,按列扫描神经元
		CONSOLE_GREEN
		for(it_neuron = it_layer->m_neurons.begin();it_neuron != it_layer->m_neurons.end() - 1;it_neuron++)
		{
			cout<<it_neuron->m_outputValue<<"\t";
		}
		CONSOLE_WHITE
		cout<<endl;
	}
}</span>



            其运行结果如下:

            这里输入为 1 0 1 0,神经网络的拓扑结构为:<4,3,2>,前馈网络第一次运算的结果如下:





























Duanxx的神经网络学习: 自己动手写神经网络(二) 前馈网络的实现