BP(back propagation)神经网络是1986年由Rumelhart和McClelland为首的科学家提出的概念,是一种按照误差逆向传播算法训练的多层前馈神经网络,是应用最广泛的神经网络。
BP算法的基本思想是,学习过程由信号的正向传播与误差的反向传播两个过程组成。即计算误差输出时按从输入到输出的方向进行,而调整权值和阈值则从输出到输入的方向进行。
正向传播时,输入信号通过隐含层作用于输出节点,经过非线性变换,产生输出信号,若实际输出与期望输出不相符,则转入误差的反向传播过程。误差反传是将输出误差通过隐含层向输入层逐层反传,并将误差分摊给各层所有单元,以从各层获得的误差信号作为调整各单元权值的依据。通过调整输入节点与隐层节点的联接强度和隐层节点与输出节点的联接强度以及阈值,使误差沿梯度方向下降,经过反复学习训练,确定与最小误差相对应的网络参数(权值和阈值),训练即告停止。此时经过训练的神经网络即能对类似样本的输入信息,自行处理输出误差最小的经过非线形转换的信息。
详细的误差反向传播的推导建议参考这篇博客:误差反向传播算法推导。
误差虽然是反向传播的,但是更新权重的时候却是正向更新的,细心的小伙伴可以去想想这是为什么。
下面实现一个简单的3层BP网络的例子。
输入、隐藏、输出神经元与偏差单元都是2个,假设输入神经元的输入值分别为0.05与0.1,各神经元之间的初始权重如上图,我们希望最后输出的结果为0.01与0.99,或者输出结果与0.01与0.99相差不大。神经元的激活函数为sigmoid函数。
//BP神经网络
#include <iostream>
#include <string.h>
#include <time.h>
#include<math.h>
using namespace std;
class sigmoidNeuron { //sigmoid神经元
public:
double input;
double output;
double weight[2];
};
class inputNeuron { //输入神经元与偏差单元
public:
double value;
double weight[2];
/*inputNeuron(double val);*/
};
//inputNeuron::inputNeuron(double val) {
// value = val;
//}
double sigmoid(double input) {
double output = 0;
output = 1 / (1 + exp(-input));
return output;
}
template <class T>
int getArrayLen(T& array)
{
return sizeof(array) / sizeof(array[0]);
}
void forward(inputNeuron(&input)[2], inputNeuron(&bias)[2], sigmoidNeuron(&hidden)[2], sigmoidNeuron(&output)[2]) { //前向计算时应先对神经元的输入进行初始化,不能直接累加
for (int i = 0; i < getArrayLen(hidden); i++) {
hidden[i].input = 0;
}
for (int i = 0; i < getArrayLen(output); i++) {
output[i].input = 0;
}
for (int i = 0; i < getArrayLen(hidden); i++) {
for (int j = 0; j < getArrayLen(input); j++) {
hidden[i].input += input[j].value * input[j].weight[i];
}
hidden[i].input += bias[0].value * bias[0].weight[i];
}
for (int i = 0; i < getArrayLen(hidden); i++) {
hidden[i].output = sigmoid(hidden[i].input);
}
for (int i = 0; i < getArrayLen(output); i++) {
for (int j = 0; j < getArrayLen(hidden); j++) {
output[i].input += hidden[j].output * hidden[j].weight[i];
}
output[i].input += bias[1].value * bias[1].weight[i];
}
for (int i = 0; i < getArrayLen(output); i++) {
output[i].output = sigmoid(output[i].input);
}
}
void backPropagation(double (&object)[2], inputNeuron(&input)[2], sigmoidNeuron(&hidden)[2], sigmoidNeuron(&output)[2], double (&learningRate)) { //先更新输入层的权重,再更新隐藏层的权重
double partialDer = 0.0;
for (int i = 0; i < getArrayLen(input); i++) {
for (int j = 0; j < getArrayLen(input[i].weight); j++) {
for (int k = 0; k < getArrayLen(output); k++) {
partialDer += (object[k] - output[k].output) * output[k].output * (1 - output[k].output) * hidden[j].weight[k] * hidden[j].output * (1 - hidden[j].output) * input[i].value;
}
input[i].weight[j] += learningRate * partialDer;
partialDer = 0.0;
}
}
for (int i = 0; i < getArrayLen(hidden); i++) {
for (int j = 0; j < getArrayLen(hidden[i].weight); j++) {
hidden[i].weight[j] += learningRate * (object[j] - output[j].output) * output[j].output * (1 - output[j].output) * hidden[i].output;
}
}
}
bool calError(double(&objVale)[2], sigmoidNeuron(&output)[2]) {
bool judge = true;
for (int i = 0; i < getArrayLen(objVale); i++) {
if (abs(objVale[i] - output[i].output) > 0.2) {
judge = false;
}
}
return judge;
}
int main()
{
inputNeuron input[2];
input[0].value = 0.05;
input[1].value = 0.1;
input[0].weight[0] = 0.15;
input[0].weight[1] = 0.25;
input[1].weight[0] = 0.20;
input[1].weight[1] = 0.30;
inputNeuron bias[2];
bias[0].value = bias[1].value = 1;
bias[0].weight[0] = 0.35;
bias[0].weight[1] = 0.35;
bias[1].weight[0] = 0.60;
bias[1].weight[1] = 0.60;
sigmoidNeuron hidden[2];
hidden[0].weight[0] = 0.40;
hidden[0].weight[1] = 0.50;
hidden[1].weight[0] = 0.45;
hidden[1].weight[1] = 0.55;
sigmoidNeuron output[2];
double object[2] = {0.01,0.99};
forward(input, bias, hidden, output);
int iters = 0;
double learningRate = 0.5;
while (!calError(object,output)) {
backPropagation(object, input, hidden, output, learningRate);
forward(input, bias, hidden, output);
cout << "此次迭代后,输出为:" << output[0].output << " " << output[1].output;
cout << endl;
iters += 1;
}
cout << "算法迭代了" << iters << "次才学会了符合要求的权重。";
cout << "最终输出为:" << output[0].output << " " << output[1].output;
return 0;
}
这里我设置的误差阈值为0.2,即输出神经元与目标函数值之间的误差在0.2以内就停止训练,最终算法仅迭代了79次就学到了满足要求的权重。