利用 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页的异或训练数据权重、阈值什么的一个个敲上去,然后对比自己的模型和书上的区别,果然很快找到问题了,没想到是因为我在循环里面忘记每一次都把权重置零,导致权重一直累加,最后导致输出结果异常增长。但是训练结果还是出现异常,发现是归一化的问题,去掉就好了。



添加新评论