利用 Java 实现 反向传播神经网络(BackPropagation)
悠扬的幻想天空 - 博客
December 4, 2018 日常 • 作者:悠扬
Github:https://github.com/izoyo/BackePropagation
训练集是150条鸢尾花数据,前四个是它的属性,是连续数据,不用在意它代表什么。最后一项是它的类别,共三类,Iris-setosa、Iris-versicolor和Iris-virginica。
1、 构造训练集的对象
class Flower{
double[] attr;//属性
int type;//种类
Flower(){
attr = new double[cv.lenAttr];
}
}
class tGroup{
int num;//成员数
Flower[] member;//成员
tGroup(){
member = new Flower[200];
num = 0;
}
//预测鸢尾花
public void preF() throws IOException{
/**
…
*/
}
}
2、 导入训练数据
try {
DataInputStream in = new DataInputStream(new FileInputStream("G:\\data\\data.txt"));
BufferedReader d = new BufferedReader(new InputStreamReader(in));
String rLine;
this.num = 0;
while((rLine = d.readLine()) != null){
String[] aa = rLine.split(",");//分割出属性
if(aa.length == cv.lenAttr+1){//因为数据多了个类别
this.member[this.num] = new Flower();
for (int i = 0; i < cv.lenAttr; i++) {
this.member[this.num].attr[i] = Double.valueOf(aa[i]);
}
switch(aa[cv.lenAttr]){
case "Iris-versicolor":
this.member[this.num].type = 1;
break;
case "Iris-virginica":
this.member[this.num].type = 2;
break;
case "Iris-setosa":
this.member[this.num].type = 0;
break;
}
this.num += 1;
}
}
d.close();
}catch (Exception e){
e.printStackTrace();
}
3、 初始化权重和阈值
用很小范围内均匀分布的随机数设置网络的权重和阈值。权重的初值要逐个神经元来设置。
Random r = new Random();
double f = 2.4 / input.length;
for (int i = 0; i < w.length; i++)
for (int j = 0; j < w[i].length; j++) {
w[i][j] = (r.nextDouble() * f * 2 - f);
}
for (int i = 0; i < hideO.length; i++) {
hideO[i] = (r.nextDouble() * f * 2 - f);
}
4、正向传播再反向传播
do {
acOut = new double[hideW.length];//临时输出
forword(input, acOut);//正向传播
allErr = 0;//计算当前误差
for (int i = 0; i < exceptOut.length; i++) {
allErr += (exceptOut[i] - acOut[i]) * (exceptOut[i] - acOut[i]);
}
backword(acOut);//反向传播
trainNum++;
if (trainNum > 10000) break;//避免死循环
}while (allErr > 0.001);
并计算误差,当误差小于0.001时候训练结束。
5、正向传播
先求和当前节点所有输入和权重的乘积再减去阈值,然后使用激活函数求出实际输出。
double z;
for (int i = 0; i < w.length; i++) {
z = 0;
for (int j = 0; j < x.length; j++) {
z += x[j] * w[i][j];
}
y[i] = sigmoid(z - o[i]);
}
6、激活函数
public double sigmoid(double x){
return 1/(1+Math.exp(-x));
}
书上还有一个激活函数2*1.716/(1+Math.exp(-x*0.667))-1.716
来加速,但实际使用会导致计算结果出错……
7、反向传播并修正权重和阈值
计算输出层误差梯度:
for (int i = 0; i < exceptOut.length; i++) {
e = exceptOut[i] - output[i];//误差
outE[i] = output[i] * (1 - output[i]) * e;
}
计算隐含层误差梯度:
for (int j = 0; j < error.length; j++) {
double sum = 0;
for (int k = 0; k < outW.length; k++) {
sum += NeLaErr[k] * outW[k][j];//求和所有输出层(误差梯度*权重)
}
error[j] = outX[j] * (1 - outX[j]) * sum;//当前隐含层误差梯度
}
求出误差梯度就可以修改权重和阈值了
//更新权重
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight[i].length; j++) {
weight[i][j] = weight[i][j] + rate * error[i] * x[j];
}
}
//更新阈值
for (int i = 0; i < o.length; i++) {
o[i] = o[i] - rate * error[i];
}
8、预测分类
当完成训练之后,只需要对传入数值进行一次正向传播就可以得到分类结果了。
public void preResult(double[] data, double[] output) {
double[] out_y = new double[outW.length];
System.arraycopy(data, 0, input, 0, data.length);
forword(input, out_y);
System.arraycopy(out_y, 0, output, 0, out_y.length);
}
这一次的难度感觉是上一次贝叶斯模型的好几倍,特别是那好几条公式,如果变量搞错了那么就很容易会影响整个训练模型的结果,还不容易发现哪里出错。一开始把整套流程写下来之后一直调试不了结果,虽然误差梯度看上去没问题,但是所有实际输出结果却一直逼近1,重复几次之后直接函数爆掉了。以为是输入数值的问题,但无论是函数x值直接除以1000还是对输入值归一化,输出值还会一直诡异的增加。调试很多遍之后,选择把书上121页的异或训练数据权重、阈值什么的一个个敲上去,然后对比自己的模型和书上的区别,果然很快找到问题了,没想到是因为我在循环里面忘记每一次都把权重置零,导致权重一直累加,最后导致输出结果异常增长。但是训练结果还是出现异常,发现是归一化的问题,去掉就好了。
你好,请问可以请教您一些问题吗