用java实现一个简单的深度神经网络

初识神经网络

简介

  • 硬编码的智能:人工智能早期,用以实现专家系统。特点,实现难度高,并且难以适应变化。
  • 机器学习:程序本身不编码实现任何解决具体问题的方法,而是模仿生物的进化、认知,能够不断地从周围环境中获取数据进行学习。特点:实现难度相较于上一个简单,但是需要大量数据,训练的方法也很重要,能够适应变化解决各种新的问题。

算法原理

  • 外界信号通过你的眼睛、鼻子、舌头等等一系列感知器官(输入)捕获后传入大脑皮层(这个我也不清除,先假装是的),大脑皮层是第一层神经元,经过第一层处理后,然后传入第二层、第三层,最终经过n层处理后得到结果,所以我们能够对纷繁复杂的世界产生正确的认知。

  • 深度神经网络算法,就是基于人脑认知事务进行学习的这一特性的算法。初始数据经由多层神经元的计算后得到输出,刚开始时可能会有较大的误差,这时就需要我们的训练。

  • 训练分为监督式训练、无监督训练。这里我们讲的是监督式训练。

监督式训练:

给出一组样本数据,以及对应的正确期望结果。让神经网络不断学习,神经网络会在学习的过程中不断地矫正神经元连接的突触的权值、动量系数等等。理论上经过若干次的训练后,这个神经网络就会调整为一个最佳的情况,这时,对给定的输入,能够在比较大的概率下给出正确的结果。

算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import java.util.Random;
public class BpDeep{
public double[][] layer;//神经网络各层节点
public double[][] layerErr;//神经网络各节点误差
public double[][][] layer_weight;//各层节点权重
public double[][][] layer_weight_delta;//各层节点权重动量
public double mobp;//动量系数
public double rate;//学习系数

public BpDeep(int[] layernum, double rate, double mobp){
this.mobp = mobp;
this.rate = rate;
layer = new double[layernum.length][];
layerErr = new double[layernum.length][];
layer_weight = new double[layernum.length][][];
layer_weight_delta = new double[layernum.length][][];
Random random = new Random();
for(int l=0;l<layernum.length;l++){
layer[l]=new double[layernum[l]];
layerErr[l]=new double[layernum[l]];
if(l+1<layernum.length){
layer_weight[l]=new double[layernum[l]+1][layernum[l+1]];
layer_weight_delta[l]=new double[layernum[l]+1][layernum[l+1]];
for(int j=0;j<layernum[l]+1;j++)
for(int i=0;i<layernum[l+1];i++)
layer_weight[l][j][i]=random.nextDouble();//随机初始化权重
}
}
}
//逐层向前计算输出
public double[] computeOut(double[] in){
for(int l=1;l<layer.length;l++){
for(int j=0;j<layer[l].length;j++){
double z=layer_weight[l-1][layer[l-1].length][j];
for(int i=0;i<layer[l-1].length;i++){
layer[l-1][i]=l==1?in[i]:layer[l-1][i];
z+=layer_weight[l-1][i][j]*layer[l-1][i];
}
layer[l][j]=1/(1+Math.exp(-z));
}
}
return layer[layer.length-1];
}
//逐层反向计算误差并修改权重
public void updateWeight(double[] tar){
int l=layer.length-1;
for(int j=0;j<layerErr[l].length;j++)
layerErr[l][j]=layer[l][j]*(1-layer[l][j])*(tar[j]-layer[l][j]);

while(l-->0){
for(int j=0;j<layerErr[l].length;j++){
double z = 0.0;
for(int i=0;i<layerErr[l+1].length;i++){
z=z+l>0?layerErr[l+1][i]*layer_weight[l][j][i]:0;
layer_weight_delta[l][j][i]= mobp*layer_weight_delta[l][j][i]+rate*layerErr[l+1][i]*layer[l][j];//隐含层动量调整
layer_weight[l][j][i]+=layer_weight_delta[l][j][i];//隐含层权重调整
if(j==layerErr[l].length-1){
layer_weight_delta[l][j+1][i]= mobp*layer_weight_delta[l][j+1][i]+rate*layerErr[l+1][i];//截距动量调整
layer_weight[l][j+1][i]+=layer_weight_delta[l][j+1][i];//截距权重调整
}
}
layerErr[l][j]=z*layer[l][j]*(1-layer[l][j]);//记录误差
}
}
}

public void train(double[] in, double[] tar){
double[] out = computeOut(in);
updateWeight(tar);
}
}

测试1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Arrays;
public class BpDeepTest{
public static void main(String[] args){
//初始化神经网络的基本配置
BpDeep bp = new BpDeep(new int[]{2,10,2}, 0.15, 0.8);
//BpDeep bp = new BpDeep(new int[]{2,10,1}, 0.15, 0.8);

//设置样本数据
double[][] data = new double[][]{{1,2},{2,2},{1,1},{2,1}};
//设置目标数据
double[][] target = new double[][]{{1,0},{0,1},{0,1},{1,0}};
//double[][] target = new double[][]{{1},{0},{0},{1}};

//迭代训练
for(int n=0;n<5000;n++)
for(int i=0;i<data.length;i++)
bp.train(data[i], target[i]);

for(int j=0;j<data.length;j++){
double[] result = bp.computeOut(data[j]);
System.out.println(Arrays.toString(data[j])+":"+Arrays.toString(result));
}

double[] x = new double[]{3,1};
double[] result = bp.computeOut(x);
System.out.println(Arrays.toString(x)+":"+Arrays.toString(result));
}
}

测试2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.*;
import java.io.*;
public class BpDeepTest2{
public static void main(String[] args) throws Exception{
BpDeep bp = new BpDeep(new int[]{1,20,20,20,1}, 0.4, 0.001);

double[][] data = getData();

for(int n=0;n<1000;n++)
for(int i=0;i<data.length;i++)
bp.train(new double[]{data[i][0]}, new double[]{data[i][1]});

System.out.println("训练结果:");
double rate = 0;
int successCount = 0;
for(int j=0;j<data.length;j++){
double[] result = bp.computeOut(data[j]);
System.out.println(data[j][0] + ":" + result[0]);

if(((int)(result[0]*2)) == data[j][1]){
successCount++;
}
}
rate = (successCount * 1.0) / data.length;
System.out.printf("rate:%.2f\n",rate);

System.out.println("验证:");
double[] x = new double[]{201,202,203,204,205};
for(int j=0;j<x.length;j++){
double[] result = bp.computeOut(new double[]{x[j]});
System.out.println(x[j] + ":" + result[0]);
}
}

public static double[][] getData() throws Exception{

List<Double[]> list = new ArrayList<Double[]>();
Scanner scanner = new Scanner(new FileInputStream("data.txt"));
while(scanner.hasNext()){
double a = scanner.nextDouble();
double b = scanner.nextDouble();

list.add(new Double[]{a,b});
}

double[][] result = new double[list.size()][3];
for(int i = 0;i < list.size();i++){
result[i][0] = list.get(i)[0];
result[i][1] = list.get(i)[1];
}

return result;
}
}

data.txt中的数据如下:
0 1
1 0
2 1
3 0
4 1
5 0
6 1
7 0
8 1
9 0
10 1
11 0
12 1
13 0
14 1
15 0
16 1
17 0
18 1
19 0
20 1

运行演示

示例1

问题:我们需要将笛卡尔坐标系中的点按照y=x分界线,分成两类。

样本数据:{1,2},{2,2},{1,1},{2,1}
期望数据:{1,0},{0,1},{0,1},{1,0}

每组输入数据个数为2,所以第一层神经元的个数为2
每组对应输出结果个数为2,所以最后一层神经元个数为2

{1,0}、{0,1} 分别对应两种结果。

代码简单解释

BpDeep bp = new BpDeep(new int[]{2,10,2}, 0.15, 0.8);
这是构造一个深度神经网络。第一个参数为一个一维数组,数组的长度为3,表示这个神经网络
一共3层。第一个元素为2表示第一层2个神经元,最后一个元素为2,表示最后一层2个神经元。
后面的0.15、0.8是动量系数还有啥的,现在说你们也不懂,先不说了,这两个数字可以随便改。
0到1之间的一个数字,对算法的学习效率有些影响,不影响逻辑。

后面的样本数据和目标数据就不说了,我们直接看结果。

示例运行

这四个是训练的结果:
[1.0, 2.0]:[0.9792365424853283, 0.020600382812002088]
上面对应的样本为:{1,2} {1,0}
[2.0, 2.0]:[0.02256564436382444, 0.9773361523239155]
上面对应的样本为:{2,2} {0,1}
[1.0, 1.0]:[0.016761474334048444, 0.9837176729362672]
上面对应的样本为:{1,1} {0,1}
[2.0, 1.0]:[0.9793917240613336, 0.02048522092272838]
上面对应的样本为:{2,1} {1,0}

这一个是训练结束后,测试的数据:
[3.0, 1.0]:[0.9899489337264226, 0.009920676075267004]
所以得到结果:{3,1}属于分类:{1,0}

示例2

问题:将整数划分为正数和负数两类。暂不考虑0,样本数据如下:
-5 0
-4 0
-3 0
-2 0
-1 0
1 1
2 1
3 1
4 1
5 1

负数输出0,整数输出1。训练完成后,验证数据如下:
100:1
-10:0
63:1
50:1
-9:0
75:1
55:1
63:1

进阶方向

  • 参数调优:
    本示例中并没有测试动量系数和学习稀疏的不同会给学习效率带来何种影响,但这个事实上决定着
    学习结果的效率。

  • 激活函数:
    还有就是算法中使用的激活函数为:1/(1+Math.exp(-z))。这是目前业界用的非常广泛的一个激活
    函数,这个函数自己可以随便换,不同的激活函数对学习也有重大的影响。设计不好的激活函数,
    可能导致极难快速收敛。

  • 输入输出的定义:
    任何数据都可以归结为一个一维数组信号,所有的分类结果都可以归结为一组0和1组成的一维数组。

在示例1中,将两个类别定义为{1,0} {0,1}
在示例2中,将两个类别定义为0 1
这个自己可以视情况而定,无有定解。

  • 神经网络的神经元层数、个数:
    从上我们可以看到,决定第一层神经元数量的是输入数据,决定最后一层神经元数量的是输出数据。
    而中间的神经元层数、个数自己可以视情况而定,没有理论证明多少层更合适。这个需要自己
    不断尝试调优。一个基本的方向是,问题越复杂,需要的神经元数量、层数会越多。因此计算的
    效率也会越低

本算法是一个简化版的深度学习算法,非常简短。要解决稍复杂的问题,可能训练特别耗时,这只是一个单机实现,如果用上云计算,它的优势才能充分的体现出来。

你的鼓励,是我前进的动力!