♻️ 资源大小469KB➡️资源下载https://download.csdn.net/download/s1t16/87430297CBp 神经网络执行流程正向反向算法原理误差“d”输出值的正确结果“o”实际输出值“k”输出节点的个数因为如果输出层节点不止一个时 就把多个节点的误差相加该式子是输出层误差的进一步分解“f(netk)”是把输出层误差的o替换掉“f(x)”指激活函数 本文用的sigmoid函数激活函数通常不是自定 有固定的函数去选择“netk”输入层从隐藏层取到 并且还没有经过激活函数的值第二个式子使把netk又进一步分解 用隐藏层的值来表示“j”隐藏层节点数本文用的单层隐藏层“w”隐藏层第 j 个节点对输出层第 k 个节点的加权“y”第 j 个隐藏层节点的值该值是已经经过了激活函数的值综上输出层一个节点未经过激活函数的值netk 就等于隐藏层每个节点的值 都乘其对输出层那个对应节点的加权的和该式子又是对隐藏层误差第二个式子的分解“f(netj)”是把“yj替换掉了yj指的隐藏层节点的值 换成输入层的值来表示“i”输入层的节点数“v”输入层第 i 个节点对隐藏层第 j 个节点的加权“x”输入层第 i 个节点输入的值综上隐藏层一个节点未经过激活函数的值netk 就等于输入层每个节点输入的值 都乘其对隐藏层那个对应节点的加权的和总结实际上每次往前一层都是分解该层未经过激活函数的值 把该值用上一层每个节点的值*每个节点对该节点的加权的和来替换 不断向前扩大 用前一层来替换同时可以看到我们可以改变加权“w”、“v来减小误差bp 神经网络是如何减小误差的一.修改加权1.如何修改隐藏层加权修改的表达式链式原则“w7”一个隐藏层的一个加权“net”输出层从隐藏层取到未经过加权的值“out”经过加权函数后的值“Eo1”o1 节点的误差“Etotal”所有节点最终的误差和根据上面的链式原则可以把式子变为这就相当于分别计算每个偏导第一个偏导因为相当于除了 E(t1) 有o1其他都没有 所以都被看作常数了即用 E(t1)对 outo1求导得该结果target 指的是正确结果的值*第二个偏导该偏导就相当于对激活函数 sigmoid 得偏导 所有使用 sigmoid 激活函数的 out 对 net 的求导都是该值第三个偏导综上所得各个偏导的乘积2.如何修改输入层加权修改的表达式链式原则“w1”输入层加权“neth1”隐藏层获取输入层 且没经过激活函数的值“outh1”经过激活函数的值“neto1”输出层没加权的值“outo1”输出层经过加权的值 也是最终输出结果“Eo1”o1 节点得误差o2 同理…可以看出输出层的加权影响的输出节点不止一个根据上面的链 写出下面表达式outh1为分叉处 所以用Etotal对outh1的偏导表示其之后所有的影响下面又把对outh1的偏导进行拆分 分成两路所以每次计算输入层偏导时要分成两部分来算二.修改偏移上面的 bp 图没有画偏移 这又找了一个b1 和 b2 指的是偏移即在加到下一层的时候 加权永远都是 1 但自己的值是改变的用 o1 的值举例偏移怎莫用注意b1 的值对 o1、o2 是不同的值 一个节点有一个自己偏移值输入层的节点都没有偏移值 最后再加上该值就是未经过激活函数的 net 值要修改偏移的话同理 用链式先画出影响的链 再一步一步偏导下面的代码中有实现和讲解其他减小误差的方法就不赘述了 欢迎大佬补充代码实现一.神经网络图这个图就是我们一会儿要解决 xor 问题的神经网络图两个输入节点、一层隐藏层且 4 个节点、一个输出节点注意图中 b1 和 b2 指的是偏移 每个节点只有一个该值 用于从上一层获取到值之后加上该值所以隐藏层每个节点都有其不同偏移 b1、输出层每个节点也有其不同的偏移 b2输入层节点都没有偏移net 和 out 的区别是 net 没经过激活函数但是已经加了偏移 out 是 net 经过激活函数后的值图中写成 out 形式 代表该节点的最终输出值二.怎莫计算误差因为我们要用代码实现的神经网络图只有一个输出节点 所以这里的 Etotal 就是那一个输出节点的误差三.怎莫计算改变量怎莫计算反向传递时输出节点偏移的改变量1依然用上面说的链式的原则 一直进行偏导 就得到上面的结果2最后*1是因为neto1对b2的偏导时 因为 b2 是偏移量 直接相加 所以偏移量对下一层的加权是 1怎莫计算反向传递时隐藏层节点加权的改变量这里以 w9 进行举例 其他加权做法相同怎莫计算反向传递时隐藏层节点偏移的改变量这里之所以用两个括号括起来 因为虽然咱们实现的 bp 网络只有一个输出节点 但是咱们在代码要遍历输出层的节点 遍历的时候咱们并不知道有多少个输出节点 所以用括号括起来如果输出有多个节点 参照上面的如何订正输入层加权 把第一个括号里的式子进行拓展最后对 b1 偏导的结果是 1 的道理跟上面相同怎莫计算反向传递时输入层节点加权的改变量这里用 w1 进行举例 其他加权求变化量原理相同用括号括起来也是因为代码中遍历输出节点时不知道节点个数 第一个式子会变成多个节点情况的和 但实际只有一个通过上面可以看到输出层节点偏移量的改变值和隐藏层加权的改变值只是最后一个偏导不同隐藏层节点的偏移量和输入层节点加权的该变量也只是最后一个偏导的值不同四.有了变化量到底怎样更新该值偏移更新的表达式同理五.写代码前注意事项在正向传播获取值时在遍历当前层节点时 遍历上一层节点从隐藏层开始在反向传播获修改加权时在遍历当前层节点时 遍历下一层节点 找到对应的加权bpnet头文件# ifndef BPNET_H # define BPNET_H # include iostream # include cmath # include vector # include cstdlib # include ctime # define INNODE 2 // 输入结点数 # define HIDENODE 4 // 隐含结点数 # define OUTNODE 1 // 输出结点数 # define LEARNINGRATE 0.9 // 学习速率注意越高虽然越快 也容易误差较大 /** * 输入层节点 */ typedef struct inputNode{ double value; // 输入值 std::vectordouble weight // 输入层单个节点对下一层每个节点的加权值 , wDeltaSum; // 单个加权的不同样本和 }InputNode; /** * 输出层节点 */ typedef struct outputNode{ double o_value // 节点最终值 经过偏移与激活函数后的值 , rightout // 正确输出值 , bias // 偏移量 每个节点只有一个 , bDeltaSum; // 反向传播时 经过计算后的偏移量需要改变的值 因为有多个样本所以是sum }OutputNode; /** * 隐含层节点 */ typedef struct hiddenNode{ double o_value // 节点最终值 经过偏移与激活函数后的值 , bias // 偏移量 每个节点只有一个 , bDeltaSum; // 反向传播时 经过计算后的偏移量需要改变的值 因为有多个样本所以是sum std::vectordouble weight // 隐藏层单个节点对下一层每个节点的加权值 , wDeltaSum; // 单个加权的不同样本和 }HiddenNode; /** * 单个样本 */ typedef struct sample{ std::vectordouble in // 输入层value的迭代器 里面的数据有输入层节点数个输入层每个节点的value值 代表一份样本数据中 一个输入属性的值 , out; // 输出层rightout的迭代器 里面的数据也有输出层层节点数个输出层每个节点的rightout值 代表一份样本数据 应该输出属性的正确值 }Sample; /** * BP神经网络 */ class BpNet{ public: BpNet(); // 构造函数 用来初始化加权和偏移 void fp(); // 单个样本前向传播 void bp(); // 单个样本后向传播 void doTraining(std::vectorSample sampleGroup, double threshold,int mostTimes); // 训练更新 weight, bias void afterTrainTest(std::vectorSample testGroup); // 神经网络学习后进行预测 void setInValue (std::vectordouble inValue); // 设置学习样本输入 void setOutRightValue(std::vectordouble outRightValue); // 设置学习样本输出 public://设置成public就不用get、set麻烦 double error; //误差率 InputNode* inputLayer[INNODE]; // 输入层任何模型都只有一层 OutputNode* outputLayer[OUTNODE]; // 输出层任何模型都只有一层 HiddenNode* hiddenLayer[HIDENODE]; // 隐含层我们这个只有一个隐藏层所以一维数组 但如果有多层是二维数组 }; # endif // BPNET_Hbpnet实现类# include BpNet.h using namespace std; /** * 产生-1~1的随机数 */ inline double getRandom() { return ((2.0*(double)rand()/RAND_MAX) - 1); } /** * sigmoid 函数激活函数 要保证单调 且只有一个变量 */ inline double sigmoid(double x){ // 一般bp用作分类的话都用该函数 double ans 1 / (1exp(-x)); return ans; } /** * 初始化给加权或者偏移赋初值 */ BpNet::BpNet(){ srand((unsigned)time(NULL)); // error初始值只要能保证大于阀值进入训练就可以 error 100.f; /* * 初始化输入层每个节点对下一层每个节点的加权 */ for (int i 0; i INNODE; i){ inputLayer[i] new InputNode(); for (int j 0; j HIDENODE; j){ inputLayer[i]-weight.push_back(getRandom()); inputLayer[i]-wDeltaSum.push_back(0.f); } } /* * 初始化隐藏层每个节点对下一层每个节点的加权 * 初始化隐藏层每个节点的偏移 */ for (int i 0; i HIDENODE; i){ hiddenLayer[i] new HiddenNode(); hiddenLayer[i]-bias getRandom(); // 初始化加权 for (int j 0; j OUTNODE; j){ hiddenLayer[i]-weight.push_back(getRandom()); hiddenLayer[i]-wDeltaSum.push_back(0.f); } } /* * 初始化输出层每个节点的偏移 */ for (int i 0; i OUTNODE; i){ outputLayer[i] new OutputNode(); outputLayer[i]-bias getRandom(); } } /** * 正向传播 获取一个样本从输入到输出的结果 */ void BpNet::fp(){ /* * 隐藏层向输入层获取数据 */ // 遍历隐藏层节点 for (int i 0; i HIDENODE; i){ double sum 0.f; // 遍历输入层每个节点 for (int j 0; j INNODE; j){ sum inputLayer[j]-value * inputLayer[j]-weight[i]; } // 增加偏移 sum hiddenLayer[i]-bias; // 调用激活函数 设置o_value hiddenLayer[i]-o_value sigmoid(sum); } /* * 输出层向隐藏层获取数据 */ // 遍历输出层节点 for (int i 0; i OUTNODE; i){ double sum 0.f; // 遍历隐藏层节点 for (int j 0; j HIDENODE; j){ sum hiddenLayer[j]-o_value * hiddenLayer[j]-weight[i]; } sum outputLayer[i]-bias; outputLayer[i]-o_value sigmoid(sum); } } /** * 反向传播 从输出层再反向 * * 该方法目的是返回多个样本加权应该变化值的和【wDeltaSum】、多个样本偏移应该变化值的和【bDeltaSum】 * 在训练时根据样本数变化值的求平均值 用该平均值修改加权、偏移 * */ void BpNet::bp(){ /* * 求误差值error */ for (int i 0; i OUTNODE; i){ double tmpe fabs(outputLayer[i]-o_value - outputLayer[i]-rightout); // 计算误差 参照上面第一个公式 error tmpe * tmpe / 2; } /* * 求输出层偏移的变化值 */ for(int i 0; i OUTNODE ; i){ // 偏移应该变化的值 参照b2公式 double bDelta(-1) * (outputLayer[i]-rightout - outputLayer[i]-o_value) * outputLayer[i]-o_value * (1 - outputLayer[i]-o_value); outputLayer[i]-bDeltaSum bDelta; } /* * 求对输出层加权的变化值 */ for (int i 0; i HIDENODE; i){ for(int j 0;j OUTNODE;j){ // 加权应该变化的值 参照w9公式 double wDelta(-1) * (outputLayer[j]-rightout - outputLayer[j]-o_value) * outputLayer[j]-o_value * (1 - outputLayer[j]-o_value) * hiddenLayer[i]-o_value; hiddenLayer[i]-wDeltaSum[j] wDelta; } } /* * 求隐藏层偏移 */ for(int i 0; i HIDENODE; i){ double sum0; // 因为是遍历输出层节点 不可以确定有多少个输出节点 参照b1公式的第一个公因式 for(int j 0;j OUTNODE; j){ sum (-1) * (outputLayer[j]-rightout - outputLayer[j]-o_value) * outputLayer[j]-o_value * (1 - outputLayer[j]-o_value) * hiddenLayer[i]-weight[j]; } // 参照公式b1 hiddenLayer[i]-bDeltaSum (sum * hiddenLayer[i]-o_value * (1 - hiddenLayer[i]-o_value)); } /* * 求输入层对隐藏层的加权变化 */ for(int i 0; i INNODE; i){ // 从公式b1和w1可以看出 两个公式是有公因式 所以这部分代码相同 double sum 0; for(int j 0;j HIDENODE; j){ for(int k 0; k OUTNODE; k){ sum (-1) * (outputLayer[k]-rightout - outputLayer[k]-o_value) * outputLayer[k]-o_value * (1 - outputLayer[k]-o_value) * hiddenLayer[j]-weight[k]; } // 参照公式w1 inputLayer[i]-wDeltaSum[j] (sum * hiddenLayer[j]-o_value * (1 - hiddenLayer[j]-o_value) * inputLayer[i]-value); } } } /** * 进行训练 参照上面最后修改的公式 */ void BpNet::doTraining(vectorSample sampleGroup, double threshold, int mostTimes){ int sampleNum sampleGroup.size(); int trainTimes 0; bool isSuccess true; while(error threshold){ // 判断是否超过最大训练次数 if(trainTimes mostTimes){ isSuccess false; break; } cout训练次数:trainTimes\t\t当前误差: error endl; error 0.f; // 初始化输入层加权的delta和 for (int i 0; i INNODE; i){ inputLayer[i]-wDeltaSum.assign(inputLayer[i]-wDeltaSum.size(), 0.f); } // 初始化隐藏层加权和偏移的delta和 for (int i 0; i HIDENODE; i){ hiddenLayer[i]-wDeltaSum.assign(hiddenLayer[i]-wDeltaSum.size(), 0.f); hiddenLayer[i]-bDeltaSum 0.f; } // 初始化输出层的偏移和 for (int i 0; i OUTNODE; i){ outputLayer[i]-bDeltaSum 0.f; } // 完成所有样本的调用与反馈 for (int iter 0; iter sampleNum; iter){ setInValue(sampleGroup[iter].in); setOutRightValue(sampleGroup[iter].out); fp(); bp(); } // 修改输入层的加权 for (int i 0; i INNODE; i){ for (int j 0; j HIDENODE; j){ //每一个加权的和都是所有样本累积的 所以要除以样本数 inputLayer[i]-weight[j] - LEARNINGRATE * inputLayer[i]-wDeltaSum[j] / sampleNum; } } // 修改隐藏层的加权和偏移 for (int i 0; i HIDENODE; i){ // 修改每个节点的偏移 因为一个节点就一个偏移 所以不用在节点里再遍历 hiddenLayer[i]-bias - LEARNINGRATE * hiddenLayer[i]-bDeltaSum / sampleNum; // 修改每个节点的各个加权的值 for (int j 0; j OUTNODE; j){ hiddenLayer[i]-weight[j] - LEARNINGRATE * hiddenLayer[i]-wDeltaSum[j] / sampleNum; } } //修改输出层的偏移 for (int i 0; i OUTNODE; i){ outputLayer[i]-bias - LEARNINGRATE * outputLayer[i]-bDeltaSum / sampleNum; } } if(isSuccess){ cout endl 训练成功!!! \t\t最终误差: error endl endl; }else{ cout endl 训练失败! 超过最大次数! \t\t最终误差: error endl endl; } } /** * 训练后进行测试使用 */ void BpNet::afterTrainTest(vectorSample testGroup){ int testNum testGroup.size(); for (int iter 0; iter testNum; iter){ // 把样本输出清空 testGroup[iter].out.clear(); setInValue(testGroup[iter].in); // 从隐藏层从输入层获取数据 for (int i 0; i HIDENODE; i){ double sum 0.f; for (int j 0; j INNODE; j){ sum inputLayer[j]-value * inputLayer[j]-weight[i]; } sum hiddenLayer[i]-bias; hiddenLayer[i]-o_value sigmoid(sum); } // 输出层从隐藏层获取数据 for (int i 0; i OUTNODE; i){ double sum 0.f; for (int j 0; j HIDENODE; j){ sum hiddenLayer[j]-o_value * hiddenLayer[j]-weight[i]; } sum outputLayer[i]-bias; outputLayer[i]-o_value sigmoid(sum); // 设置输出的值 testGroup[iter].out.push_back(outputLayer[i]-o_value); } } } /** * 给输入层每个节点设置输入值 每个样本进行训练时都要调用 */ void BpNet::setInValue(vectordouble sampleIn){ // 对应一次样本 输入层每个节点的输入值 for (int i 0; i INNODE; i){ inputLayer[i]-value sampleIn[i]; } } /** * 给输出层每个节点设置正确值 每个样本进行训练时都要调用 */ void BpNet::setOutRightValue(vectordouble sampleOut){ // 对应一次样本 输出层层每个节点的正确值 for (int i 0; i OUTNODE; i){ outputLayer[i]-rightout sampleOut[i]; } }util.h工具类# ifndef UTIL_H # define UTIL_H # include vector /** * 工具类 */ class Util { public: // 获得txt文件中准备的数据 std::vectordouble getFileData(char* fileName); }; # endif // UTIL_Hutil.cpp实现类# include Util.h # include string # include cstring # include cstdlib # include fstream # include vector using namespace std; vectordouble Util::getFileData(char* fileName){ vectordouble res; ifstream input(fileName); if(!input){ return res; } string buff; while(getline(input,buff)){ char* datas (char*)buff.c_str(); const char* spilt ; // strtok字符串拆分函数 char* data strtok(datas,spilt); while(data ! NULL){ // atof是stdlib头文件下转化字符串为数字的函数 res.push_back(atof(data)); // NULL代表从上次没拆分完地方继续拆 data strtok(NULL,spilt); } } input.close(); return res; }main.cpp训练及测试# include BpNet.h # include Util.h using namespace std; void getInput(double threshold,int mostTimes); // 获得输入的阀值和误差大小 vectorSample getTrianData(); // 从文件获取训练数据 没获取到直接退出 vectorSample getTestData(); // 从文件获取测试数据 没获取到直接退出 void showTest(vectorSampletestGroup); // 输出测试数据的结果 int main(){ // 准备所有数据 BpNet bpNet; vectorSample sampleGroup getTrianData(); vectorSample testGroup getTestData(); double threshold; // 设定的阀值 int mostTimes; // 最大训练次数 // 获取输入 并提示数据已经录入 getInput(threshold,mostTimes); // 进行训练 bpNet.doTraining(sampleGroup,threshold,mostTimes); // 训练后测试录入的数据 这里的参数是引用 bpNet.afterTrainTest(testGroup); // 打印提前录入数据的测试结果 showTest(testGroup); return 0; } void getInput(double threshold , int mostTimes){ cout 训练及测试数据已从文件读入 endlendl; cout 请输入XOR训练最大误差; //0.0001最好 cin threshold; cout 请输入XOR训练最大次数; cin mostTimes; } void showTest(vectorSample testGroup){ // 输出测试结果 cout 系统测试数据: endl; for (int i 0 ; i testGroup.size() ; i){ for (int j 0 ; jtestGroup[i].in.size() ; j){ cout testGroup[i].in[j] \t; } cout -- XOR训练结果 :; for (int j0;jtestGroup[i].out.size();j){ cout testGroup[i].out[j] \t; } cout endl; } cout endl endl; system(pause); } vectorSample getTestData(){ Util util; vectordouble testData util.getFileData(test.txt); if(testData.size() 0){ cout 载入测试数据失败 endl; exit(0); } int groups testData.size()/2; // 创建测试数据 Sample testInOut[groups]; for (int i 0,index0; i groups; i){ for(int j0;j2;j){ testInOut[i].in.push_back(testData[index]); } } // 初始化数据 return vectorSample(testInOut,testInOutgroups); } vectorSample getTrianData(){ Util util; vectordouble trainData util.getFileData(data.txt); if(trainData.size() 0){ cout 载入训练数据失败 endl; exit(0); } int groups trainData.size()/3; // 创建样本数据 Sample trainInOut[groups]; // 把vector设置给样本Sample for (int i 0,index0; i groups; i){ for(int j0;j3;j){ if(j%3!2){ trainInOut[i].in.push_back(trainData[index]); }else{ trainInOut[i].out.push_back(trainData[index]); } } } // 初始化录入的个数据 return vectorSample(trainInOut,trainInOutgroups); }训练数据测试数据运行结果误差0.001输入三次测试结果误差0.0001输入两次测试结果总结可以看出结果还是比较符合预期 在两个数相差很小时的判断结果就很接近 0 其余情况就很接近 1在进行调整时 通过减少样本数提高误差的减小速度 从而可以输入更低的误差 但效果不是并很好 所以就选择增加样本 增大学习效率 同时输入一个折中的误差 似乎效果更好点在设置训练数据的时候要尽量包含的范围段全一些 可以大幅提高准确率但是训练数据如果设置的不太合理的话 可能会导致训练时误差减少的特别慢 最后训练次数可能达到最大值但也没到设置的误差阀值在把误差从 0.001 降到 0.0001 之后 训练次数也是大幅翻倍甚至达到 250w 次 但是准确率也明显提高 每组结果都更加接近 0 或 1