【学习笔记:第二版】用Java实现基于BP算法的多层前馈神经网络

本文最后更新于:2 年前

第二版笔记更新:优化了 Java 实现 并用 神经网络完成西瓜分类任务,增加了 累积BP算法的实现。第二版笔记删除了 神经网络原理的推导,是因为此处的重点是用Java编写神经网络,后续博主有时间会在其它博文中记录之。

更新时间(2021–5–15 13:27)

传播搞机的快乐,分享计算机知识!—TCJ


以下是Java实现

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
import java.util.ArrayList;
import java.util.List;

public class neural_networks {//神经网络类(多层前馈神经网络)
static private int[] nums = {2, 5, 4, 3}; //神经网络基本属性:神经网络层数及各层神经元数(此处已指定)

static class networks {//神经网络数据结构类(内部类)
static private double[][][] Q_Values = new double[3][5][5];//权重值数组
static private double[][] Yu_Values = new double[3][5];//阈值数组
static private double[][] IntputValues = new double[4][5];//输入值数组
static private double[][] OutputValues = new double[4][5];//输出值数组
static private double[][] TDValues = new double[3][5];//梯度项数组

private networks() {//无参构造方法(此处初始化一个4层神经网络,每层神经元数分别为:2、5、4、3)
for (int i = 1; i < nums.length; i++) {//从第一层隐层开始
for (int j = 0; j < nums[i]; j++) {//初始化阈值
Yu_Values[i - 1][j] = Math.random();//添加随机值范围(0~1)
for (int k = 0; k < nums[i - 1]; k++) {//单个神经元下的权重
Q_Values[i - 1][j][k] = Math.random();//添加随机值范围(0~1)
}

}
}

}

//i是层数、j是神经元数、k是权重数(下同)
private static void set(int i, int j, int k, double Qvalue) {//set方法(权重值更新)

Q_Values[i][j][k] = Qvalue;

}

private static void set(int i, int j, double Value, int k) {//set方法(阈值、输入值、输出值、梯度项值更新)
if (k == 0) {//阈值更新
Yu_Values[i][j] = Value;
} else if (k == 1) {//输入值更新
IntputValues[i][j] = Value;
} else if (k == 2) {//输出值更新
OutputValues[i][j] = Value;
} else {//梯度项更新
TDValues[i][j] = Value;

}

}

private static double getIntputValue(int i, int j) {//获取神经元的输入
return IntputValues[i][j];

}

private static double getOutputValue(int i, int j) {//获取神经元的输出
return OutputValues[i][j];

}

private static double getTDValue(int i, int j) {//获取神经元的梯度项值
return TDValues[i][j];

}

private static double getQ_Value(int i, int j, int k) {//获取神经元的权值
return Q_Values[i][j][k];

}

private static double getYu_Value(int i, int j) {//获取神经元的阈值
return Yu_Values[i][j];

}

}

static class neuron {//神经元类(内部类)

private neuron() {//无参构造方法

}

private double getIntputValue(int i, int j) {//获取神经元的输入值
return networks.getIntputValue(i, j);

}

private double getOutputValue(int i, int j) {//获取神经元的输出值
return networks.getOutputValue(i, j);

}

private double getTDValue(int i, int j) {//获取神经元的梯度项值
return networks.getTDValue(i, j);

}

private double getQ_Value(int i, int j, int k) {//获取神经元的权值
return networks.getQ_Value(i, j, k);

}

private double getYu_Value(int i, int j) {//获取神经元的阈值
return networks.getYu_Value(i, j);

}

private void set(int i, int j, int k, double Qvalue) {//set方法(权重值更新)
networks.set(i, j, k, Qvalue);

}

private void set(int i, int j, double Value, int k) {//set方法(阈值、输入值、输出值、梯度项值更新)
networks.set(i, j, Value, k);
}

private boolean countOutputValueandSave(double IntputValue) {//计算神经元的输出值(此处使用sigmiod函数作为激活函数)需要被重写
return true;

}

private boolean countTDValueandSave() {//计算神经元的梯度项值,需要被重写
return true;
}

private boolean countIntputValueandSave(int i, int j) {//计算神经元的输入值,需要被重写
return true;

}

private boolean renewYu(int i, int j, double rate) {//更新阈值,需要被重写
return true;
}

private boolean renewQuan(int i, int j, int k, double rate) {//更新权重,需要被重写
return true;

}

}

static class hiddenneuron extends neuron {//隐结点类(内部类)

private hiddenneuron() {//无参构造方法

}

private boolean countTDValueandSave(int i, int j) {//计算隐层神经元的梯度项值
double d = 0;//ΣΩg
for (int k = 0; k < nums[i]; k++) {//权重和下一层的梯度项
d += super.getQ_Value(i, k, j) * super.getTDValue(i, k);
}
double b = super.getIntputValue(i, j);
double c = b * (b - 1) * d;
super.set(i - 1, j, c, 3);
return true;

}

private boolean countIntputValueandSave(int i, int j) {//计算隐层神经元的输入值
double b = 0;
for (int k = 0; k < nums[i - 1]; k++) {
b += super.getOutputValue(i - 1, k) * super.getQ_Value(i - 1, j, k);

}
super.set(i, j, b, 1);
return true;

}

private boolean countOutputValueandSave(int i, int j) {//计算神经元的输出值(此处使用sigmiod函数作为激活函数)
double b = super.getIntputValue(i, j);
double yu = super.getYu_Value(i - 1, j);
double c = 1 / (1 + Math.pow(Math.E, yu - b));
super.set(i, j, c, 2);
return true;

}

private boolean renewYu(int i, int j, double rate) {//更新阈值
double e = super.getTDValue(i - 1, j);
double c = super.getYu_Value(i - 1, j) + rate * e;
super.set(i - 1, j, c, 0);
return true;
}

private boolean renewQuan(int i, int j, int k, double rate) {//更新权重
double e = super.getTDValue(i - 1, j);
double c = super.getQ_Value(i - 1, j, k) - rate * e * networks.getOutputValue(i - 1, k);
super.set(i - 1, j, k, c);
return true;

}

}

static class outneuron extends neuron {//输出结点类(内部类)

private outneuron() {//无参构造方法

}

private boolean countTDValueandSave(int i, int j, double y) {//计算输出层神经元的梯度项值
double out = super.getOutputValue(i - 1, j);
double c = out * (1 - out) * (out - y);
super.set(i - 1, j, c, 3);
return true;

}

private boolean countIntputValueandSave(int i, int j) {//计算输出层神经元的输入值
double b = 0;
for (int k = 0; k < nums[i - 1]; k++) {
b += super.getOutputValue(i - 1, k) * super.getQ_Value(i - 1, j, k);
}
super.set(i, j, b, 1);
return true;

}

private boolean countOutputValueandSave(int i, int j) {//计算神经元的输出值(此处使用sigmiod函数作为激活函数)
double b = super.getIntputValue(i, j);
double yu = super.getYu_Value(i - 1, j);
double c = 1.0 / (1 + Math.pow(Math.E, yu - b));
super.set(i, j, c, 2);
return true;

}

private boolean renewYu(int i, int j, double rate) {//更新阈值
double g = super.getTDValue(i - 1, j);
double c = super.getYu_Value(i - 1, j) + rate * g;
super.set(i - 1, j, c, 0);
return true;
}

private boolean renewQuan(int i, int j, int k, double rate) {//更新权重
double g = super.getTDValue(i - 1, j);
double c = super.getQ_Value(i - 1, j, k) - rate * g * super.getOutputValue(i - 1, k);
super.set(i - 1, j, k, c);
return true;
}

private double getOutputValue(int i, int j) {//获取神经元的输出值
return super.getOutputValue(i, j);

}

}

public static void main(String[] args) {

/**
* 训练集:
* (正例,都是好瓜①密度 0.697 含糖率 0.460 ②密度 0.774 含糖率0.376 ③密度 0.634 含糖率 0.264
* ④密度 0.608 含糖率 0.318 ⑤密度 0.556 含糖率 0.215)
* (反例①密度 0.666 含糖率 0.091 ②密度 0.243 含糖率 0.267 ③密度 0.245 含糖率 0.057
* ④密度 0.343 含糖率 0.099 ⑤密度 0.639 含糖率 0.161 ⑥密度 0.657 含糖率 0.198
* )
* 测试集:
* (正例①密度 0.481 含糖率 0.149 ②密度 0.437 含糖率 0.211 ③密度 0.403 含糖率 0.237)
* (反例①密度 0.593 含糖率 0.042 ②密度 0.719 含糖率 0.103 ④密度 0.360 含糖率 0.370)
*/
List traindatas = new ArrayList();
List traindatass = new ArrayList();
List zresult = new ArrayList();
traindatas.add(0.697);
traindatass.add(0.460);
traindatas.add(0.774);
traindatass.add(0.376);
traindatas.add(0.634);
traindatass.add(0.264);
traindatas.add(0.608);
traindatass.add(0.318);
traindatas.add(0.556);
traindatass.add(0.215);
//正例↑
traindatas.add(0.666);
traindatass.add(0.091);
traindatas.add(0.243);
traindatass.add(0.267);
traindatas.add(0.245);
traindatass.add(0.057);
traindatas.add(0.343);
traindatass.add(0.099);
traindatas.add(0.639);
traindatass.add(0.161);
traindatas.add(0.657);
traindatass.add(0.198);
zresult.add(1);
zresult.add(1);
zresult.add(1);
zresult.add(1);
zresult.add(1);
zresult.add(0);
zresult.add(0);
zresult.add(0);
zresult.add(0);
zresult.add(0);
zresult.add(0);
List ftraindatas = new ArrayList();
List ftraindatass = new ArrayList();
List fzresult = new ArrayList();
ftraindatas.add(0.481);
ftraindatass.add(0.149);
ftraindatas.add(0.437);
ftraindatass.add(0.211);
ftraindatas.add(0.593);
ftraindatass.add(0.042);
ftraindatas.add(0.719);
ftraindatass.add(0.103);
ftraindatas.add(0.403);
ftraindatass.add(0.237);
ftraindatas.add(0.360);
ftraindatass.add(0.370);
fzresult.add(1);
fzresult.add(1);
fzresult.add(1);
fzresult.add(0);
fzresult.add(0);
fzresult.add(0);
networks n = new networks();//new 一个神经网络
neuron ne = new neuron();//new 一个神经元
hiddenneuron hne = new hiddenneuron();//new一个隐结点对象
outneuron one = new outneuron();//new一个输出结点对象
double rate = 0.1;//学习率
for (int i = 0; i < 1500; i++) {

for (int j = 0; j < traindatas.size(); j++) {
networks.set(0, 0, (double) traindatas.get(j), 1);
networks.set(0, 0, (double) traindatas.get(j), 2);
networks.set(0, 1, (double) traindatass.get(j), 1);
networks.set(0, 1, (double) traindatass.get(j), 2);

for (int k = 1; k < nums.length - 1; k++) {//循环遍历隐结点:计算输入输出值
for (int z = 0; z < nums[k]; z++) {
hne.countIntputValueandSave(k, z);
hne.countOutputValueandSave(k, z);
}

}

for (int k = 0; k < nums[nums.length - 1]; k++) {//遍历输出结点
int c = nums.length - 1;
one.countIntputValueandSave(c, k);
one.countOutputValueandSave(c, k);
one.countTDValueandSave(c, k, Double.valueOf(zresult.get(j).toString()));
one.renewYu(c, k, rate);
for (int z = 0; z < nums[c - 1]; z++) {
one.renewQuan(c, k, z, rate);
}

}

for (int k = 1; k < nums.length - 1; k++) {//计算隐结点梯度项
for (int z = 0; z < nums[nums.length - 1 - k]; z++) {
hne.countTDValueandSave(nums.length - 1 - k, z);
hne.renewYu(nums.length - 1 - k, z, rate);
for (int t = 0; t < nums[nums.length - 2 - k]; t++) {
hne.renewQuan(nums.length - 1 - k, z, t, rate);
}

}

}

}
//以下是测试
int cw = 0;//记录错误示例个数
for (int j = 0; j < ftraindatas.size(); j++) {
networks.set(0, 0, (double) ftraindatas.get(j), 1);
networks.set(0, 0, (double) ftraindatas.get(j), 2);
networks.set(0, 1, (double) ftraindatass.get(j), 1);
networks.set(0, 1, (double) ftraindatass.get(j), 2);
for (int k = 1; k < nums.length - 1; k++) {
for (int z = 0; z < nums[k]; z++) {
hne.countIntputValueandSave(k, z);
hne.countOutputValueandSave(k, z);
}

}
for (int k = 0; k < 1; k++) {
int c = nums.length - 1;
double averageout = 0;//神经网络有多个输出,在这里取所有输出的平均值
for (int z = 0; z < nums[c]; z++) {
one.countIntputValueandSave(c, z);
one.countOutputValueandSave(c, z);
}
for (int z = 0; z < nums[c]; z++) {
averageout += one.getOutputValue(c, z);
}
averageout /= (double) nums[c];
if (averageout > 0.5 & Double.valueOf(fzresult.get(j).toString()) == 0.0) {
System.out.println("第" + (j + 1) + "个示例分类错误");
cw++;
}
if (averageout < 0.5 & Double.valueOf(fzresult.get(j).toString()) == 1.0) {
System.out.println("第" + (j + 1) + "个示例分类错误");
cw++;
}
if (averageout == 0.5) {
System.out.println("第" + (j + 1) + "个示例分类错误");
cw++;
}

}

}
System.out.println("错误率:" + cw / (double) fzresult.size());

}

}
}

对于累积BP算法,我们可以先求出多个误差,再进行平均。此方法有时可以免去误差抵销的现象。

例如:你辛辛苦苦算了一节课的数学题,最后发现是条件看错了。所以,舍得花时间先看清条件是必要的。

如果神经网络的模型非常复杂(即有超多个地方需要计算),每算一次需要花费巨大的时间,那你可不想看到被抵销的现象吧,用累积BP算法就可以节省大量时间,提高学习效率。

标准BP算法就是累积BP算法每次训练样例数取1时的特例。一般情况下,模型越复杂,每次训练样例数取得越多。

训练及测试结果:

控制台打印计算结果

不知道是因为任务太简单了还是神经网络的学习能力太强了,在几十次训练后就达到了100%的精度

我更愿相信它是过拟合了

用到的神经元模型:M-P神经元