欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++从零实现深度神经网络(二)前向传播和反向传播

程序员文章站 2022-03-24 13:44:19
c++从零实现深度神经网络(二)前向传播和反向传播:前一篇博客net类的设计和神经网络的初始化中,大部分还是比较简单的。因为最重要事情就是生成各种矩阵并初始化。神经网络中的重点和核心就是本文的内容&...

c++从零实现深度神经网络(二)前向传播和反向传播:前一篇博客net类的设计和神经网络的初始化中,大部分还是比较简单的。因为最重要事情就是生成各种矩阵并初始化。神经网络中的重点和核心就是本文的内容——前向和反向传播两大计算过程。每层的前向传播分别包含加权求和(卷积?)的线性运算和激活函数的非线性运算。反向传播主要是用bp算法更新权值。本文也分为两部分介绍。

一、前向过程

如前所述,前向过程分为线性运算和非线性运算两部分。相对来说比较简单。

线型运算可以用y = wx+b来表示,其中x是输入样本,这里即是第n层的单列矩阵,w是权值矩阵,y是加权求和之后的结果矩阵,大小与n+1层的单列矩阵相同。b是偏置,默认初始化全部为0。不难推知(鬼知道我推了多久!),w的大小是(n+1).rows * n.rows。正如上一篇中生成weights矩阵的代码实现一样:

weights[i].create(layer[i + 1].rows, layer[i].rows, cv_32fc1); 

非线性运算可以用o=f(y)来表示。y就是上面得到的y。o就是第n+1层的输出。f就是我们一直说的激活函数。激活函数一般都是非线性函数。它存在的价值就是给神经网络提供非线性建模能力。激活函数的种类有很多,比如sigmoid函数,tanh函数,relu函数等。各种函数的优缺点可以参考更为专业的论文和其他更为专业的资料。

我们可以先来看一下前向函数forward()的代码:

	//forward
	void net::farward()
	{
		for (int i = 0; i < layer_neuron_num.size() - 1; ++i)
		{
			cv::mat product = weights[i] * layer[i] + bias[i];
			layer[i + 1] = activationfunction(product, activation_function);
		}
	}

for循环里面的两句就分别是上面说的线型运算和激活函数的非线性运算。

激活函数activationfunction()里面实现了不同种类的激活函数,可以通过第二个参数来选取用哪一种。代码如下:

	//activation function
	cv::mat net::activationfunction(cv::mat &x, std::string func_type)
	{
		activation_function = func_type;
		cv::mat fx;
		if (func_type == "sigmoid")
		{
			fx = sigmoid(x);
		}
		if (func_type == "tanh")
		{
			fx = tanh(x);
		}
		if (func_type == "relu")
		{
			fx = relu(x);
		}
		return fx;
	}

各个函数更为细节的部分在function.h和function.cpp文件中。在此略去不表,感兴趣的请君移步github。

需要再次提醒的是,上一篇博客中给出的net类是精简过的,下面可能会出现一些上一篇net类里没有出现过的成员变量。完整的net类的定义还是在github里。

二、反向传播过程

反向传播原理是链式求导法则,其实就是我们高数中学的复合函数求导法则。这只是在推导公式的时候用的到。具体的推导过程我推荐看看下面这一篇教程,用图示的方法,把前向传播和反向传播表现的清晰明了,强烈推荐!

principles of training multi-layer neural network using backpropagation。

一会将从这一篇文章中截取一张图来说明权值更新的代码。在此之前,还是先看一下反向传播函数backward()的代码是什么样的:

	//forward
	void net::backward()
	{
		calcloss(layer[layer.size() - 1], target, output_error, loss);
		deltaerror();
		updateweights();
	}
可以看到主要是是三行代码,也就是调用了三个函数:

第一个函数calcloss()计算输出误差和目标函数,所有输出误差平方和的均值作为需要最小化的目标函数。

第二个函数deltaerror()计算delta误差,也就是下图中delta1*df()那部分。

第三个函数updateweights()更新权值,也就是用下图中的公式更新权值。

下面是从前面强烈推荐的文章中截的一张图:

C++从零实现深度神经网络(二)前向传播和反向传播

(ps:写到这里的时候,总感觉自己的程序哪里有问题,看了几遍没发现哪里有问题。而且程序确实可以训练。。感觉怪怪的,如果有人看出来了,一定要告诉我啊。)

就看下updateweights()函数的代码:

	//update weights
	void net::updateweights()
	{
		for (int i = 0; i < weights.size(); ++i)
		{
			cv::mat delta_weights = learning_rate * (delta_err[i] * layer[i].t());
			weights[i] = weights[i] + delta_weights;
		}
	}

核心的两行代码应该还是能比较清晰反映上图中的那个权值更新的公式的。图中公式里的eta常被称作学习率。训练神经网络调参的时候经常要调节这货。

计算输出误差和delta误差的部分纯粹是数学运算,乏善可陈。但是把代码贴在下面吧,因为觉得怪怪的就是这一部分,希望高手指点。

calcloss()函数在function.cpp文件中:

	//objective function
	void calcloss(cv::mat &output, cv::mat &target, cv::mat &output_error, float &loss)
	{
		if (target.empty())
		{
			std::cout << "can't find the target cv::matrix" << std::endl;
			return;
		}
		output_error = target - output;
		cv::mat err_sqrare;
		pow(output_error, 2., err_sqrare);
		cv::scalar err_sqr_sum = sum(err_sqrare);
		loss = err_sqr_sum[0] / (float)(output.rows);
	}

deltaerror()在net.cpp中(貌似感觉哪里不对的样子,好像没问题啊):
	//compute delta error
	void net::deltaerror()
	{
		delta_err.resize(layer.size() - 1);
		for (int i = delta_err.size() - 1; i >= 0; i--)
		{
			delta_err[i].create(layer[i + 1].size(), layer[i + 1].type());
			//cv::mat dx = layer[i+1].mul(1 - layer[i+1]);
			cv::mat dx = derivativefunction(layer[i + 1], activation_function);
			//output layer delta error
			if (i == delta_err.size() - 1)
			{
				delta_err[i] = dx.mul(output_error);
			}
			else  //hidden layer delta error
			{
				cv::mat weight = weights[i];
				cv::mat weight_t = weights[i].t();
				cv::mat delta_err_1 = delta_err[i];
				delta_err[i] = dx.mul((weights[i + 1]).t() * delta_err[i + 1]);
			}
		}
	}

需要注意的就是计算的时候输出层和隐藏层的计算公式是不一样的。

至此,神经网络最核心的部分已经实现完毕。剩下的就是想想该如何训练了。这个时候你如果愿意的话仍然可以写一个小程序进行几次前向传播和反向传播。还是那句话,鬼知道我在能进行传播之前到底花了多长时间调试!