Fork me on GitHub

手动搭建BP神经网络


人工智能的最后一次作业,搭建BP神经网络实现手写体数字识别。

数据集介绍

数据集采用著名的MNIST数据集,来自美国国家标准与技术研究所,由来自250个不同人手写的数字构成,其中50%是高中学生,50%来自人口普查局的工作人员。测试集也是同样比例的手写数字数据。

它包含了四个部分:前两个文件为训练集,后两个文件为测试集。

-数字样本 -数字标签
Training set images:train-images-idx3-ubyte.gz (包含60,000个样本) Training set labels:train-labels-idx1-ubyte.gz (包含60,000个标签)
Test set images: t10k-images-idx3-ubyte.gz (包含10,000个样本) Test set labels: t10k-labels-idx1-ubyte.gz (包含10,000个标签)

算法描述

BP神经网络是一种按误差逆传播算法训练的多层前馈网络,能学习和存贮大量的输入-输出模式映射关系,而无需事前揭示描述这种映射关系的数学方程。它的学习规则是使用梯度下降法,通过反向传播来不断调整网络的权值和阈值,使网络的误差最小,其结构包括输入层、隐藏层和输出层。

本实验中,对于每一张手写图片,我先把它处理成一个28 * 28 的01矩阵,其中1代表数字的笔画着色部分,0则代表空白。然后我们把该矩阵,扁平成一个784维的输入向量,输入到输入层。经过隐藏层到达输出层时,是一个10维的输出向量,每一位分别对应是数字0~9的可能性。通过比较输出层的实际输出与期望输出,进行反向反馈调节,并循环重复上述步骤直到达到指定迭代次数。

代码

代码并不是很长,我用c++进行实现。

BP.h文件

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
#ifndef BP_H_INCLUDED
#define BP_H_INCLUDED
const int INPUT_LAYER = 784; //输入层维度
const int HIDDEN_LAYER = 40; //隐含层维度
const int OUTPUT_LAYER = 10; //输出层维度
const double LEARN_RATE = 0.3; //学习率
const int TRAIN_TIMES = 10; //迭代训练次数
class BP
{
private:
int input_array[INPUT_LAYER]; //输入向量
int aim_array[OUTPUT_LAYER]; //目标结果
double weight1_array[INPUT_LAYER][HIDDEN_LAYER]; //输入层与隐含层之间的权重
double weight2_array[HIDDEN_LAYER][OUTPUT_LAYER]; //隐含层与输出层之间的权重
double output1_array[HIDDEN_LAYER]; //隐含层输出
double output2_array[OUTPUT_LAYER]; //输出层输出
double deviation1_array[HIDDEN_LAYER]; //隐含层误差
double deviation2_array[OUTPUT_LAYER]; //输出层误差
double threshold1_array[HIDDEN_LAYER]; //隐含层阈值
double threshold2_array[OUTPUT_LAYER]; //输出层阈值
public:
void Init(); //初始化各参数
double Sigmoid(double x); //sigmoid激活函数
void GetOutput1(); //得到隐含层输出
void GetOutput2(); //得到输出层输出
void GetDeviation1(); //得到隐含层误差
void GetDeviation2(); //得到输出层误差
void Feedback1(); //反馈输入层与隐含层之间的权重
void Feedback2(); //反馈隐含层与输出层之间的权重
void Train(); //训练
void Test(); //测试
};
#endif // BP_H_INCLUDED

BP.cpp文件

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
#include <bits/stdc++.h>
#include "BP.h"
using namespace std;
//初始化各参数
void BP::Init()
{
srand(time(NULL));
for (int i = 0; i < INPUT_LAYER; i++)
for (int j = 0; j < HIDDEN_LAYER; j++)
weight1_array[i][j] = rand()/(double)(RAND_MAX) * 2 - 1;
for (int i = 0; i < HIDDEN_LAYER; i++)
for (int j = 0; j < OUTPUT_LAYER; j++)
weight2_array[i][j] = rand()/(double)(RAND_MAX) * 2 - 1;
for (int i = 0; i < HIDDEN_LAYER; i++)
threshold1_array[i] = rand()/(double)(RAND_MAX) * 2 - 1;
for (int i = 0; i < OUTPUT_LAYER; i++)
threshold2_array[i] = rand()/(double)(RAND_MAX) * 2 - 1;
}
//sigmoid激活函数
double BP::Sigmoid(double x)
{
return 1.0 / (1.0 + exp(-x));
}
//得到隐含层输出
void BP::GetOutput1()
{
for (int j = 0; j < HIDDEN_LAYER; j++)
{
double total = threshold1_array[j];
for (int i = 0; i < INPUT_LAYER; i++)
total += input_array[i] * weight1_array[i][j];
output1_array[j] = Sigmoid(total);
}
}
//得到输出层输出
void BP::GetOutput2()
{
for (int j = 0; j < OUTPUT_LAYER; j++)
{
double total = threshold2_array[j];
for (int i = 0; i < HIDDEN_LAYER; i++)
total += output1_array[i] * weight2_array[i][j];
output2_array[j] = Sigmoid(total);
}
}
//得到隐含层误差
void BP::GetDeviation1()
{
for (int i = 0; i < HIDDEN_LAYER; i++)
{
double total = 0;
for (int j = 0; j < OUTPUT_LAYER; j++)
total += weight2_array[i][j] * deviation2_array[j];
deviation1_array[i] = (output1_array[i]) * (1.0 - output1_array[i]) * total;
}
}
//得到输出层误差
void BP::GetDeviation2()
{
for (int i = 0; i < OUTPUT_LAYER; i++)
deviation2_array[i] = (output2_array[i]) * (1.0 - output2_array[i]) * (output2_array[i] - aim_array[i]);
}
//反馈输入层与隐含层之间的权重
void BP::Feedback1()
{
for (int j = 0; j < HIDDEN_LAYER; j++)
{
threshold1_array[j] -= LEARN_RATE * deviation1_array[j];
for (int i = 0; i < INPUT_LAYER; i++)
weight1_array[i][j] = weight1_array[i][j] - LEARN_RATE * input_array[i] * deviation1_array[j];
}
}
//反馈隐含层与输出层之间的权重
void BP::Feedback2()
{
for (int j = 0; j < OUTPUT_LAYER; j++)
{
threshold2_array[j] = threshold2_array[j] - LEARN_RATE * deviation2_array[j];
for (int i = 0; i < HIDDEN_LAYER; i++)
weight2_array[i][j] = weight2_array[i][j] - LEARN_RATE * output1_array[i] * deviation2_array[j];
}
}
//训练
void BP::Train()
{
FILE *train_images;
FILE *train_labels;
train_images = fopen("train-images.idx3-ubyte", "rb");
train_labels = fopen("train-labels.idx1-ubyte", "rb");
unsigned char image[INPUT_LAYER];
unsigned char label[OUTPUT_LAYER];
unsigned char temp[100];
//读取文件开头
fread(temp, 1, 16, train_images);
fread(temp, 1, 8, train_labels);
int times = 0; //当前训练了几次
cout << "开始训练..." << endl << endl;
while (!feof(train_images) && !feof(train_labels))
{
fread(image, 1, INPUT_LAYER, train_images);
fread(label, 1, 1, train_labels);
//设置输入向量
for (int i = 0; i < INPUT_LAYER; i++)
{
if((unsigned int)image[i] < 64)
input_array[i] = 0;
else
input_array[i] = 1;
}
//设置目标值
int index = (unsigned int)label[0];
memset(aim_array, 0, sizeof(aim_array));
aim_array[index] = 1;
GetOutput1(); //得到隐含层输出
GetOutput2(); //得到输出层输出
GetDeviation2(); //得到输出层误差
GetDeviation1(); //得到隐含层误差
Feedback1(); //反馈输入层与隐含层之间的权重
Feedback2(); //反馈隐含层与输出层之间的权重
++times;
if(times % 2000 == 0)
cout << "已训练 " << times << "组" << endl;
if(times % 10000 == 0) //每10000组就测试一下
Test();
}
}
//测试
void BP::Test()
{
FILE *test_images;
FILE *test_labels;
test_images = fopen("t10k-images.idx3-ubyte", "rb");
test_labels = fopen("t10k-labels.idx1-ubyte", "rb");
unsigned char image[784];
unsigned char label[10];
unsigned char temp[100];
//读取文件开头
fread(temp, 1, 16, test_images);
fread(temp, 1, 8, test_labels);
int total_times = 0; //当前测试了几次
int success_times = 0; //当前正确了几次
cout << "开始测试..." << endl;
while (!feof(test_images) && !feof(test_labels))
{
fread(image, 1, INPUT_LAYER, test_images);
fread(label, 1, 1, test_labels);
//设置输入向量
for (int i = 0; i < INPUT_LAYER; i++)
{
if ((unsigned int)image[i] < 64)
input_array[i] = 0;
else
input_array[i] = 1;
}
//设置目标值
memset(aim_array, 0, sizeof(aim_array));
int index = (unsigned int)label[0];
aim_array[index] = 1;
GetOutput1(); //得到隐含层输出
GetOutput2(); //得到输出层输出
//以输出结果中最大的那个值所对应的数字作为预测的数字
double maxn = -99999999;
int max_index = 0;
for (int i = 0; i < OUTPUT_LAYER; i++)
{
if (output2_array[i] > maxn)
{
maxn = output2_array[i];
max_index = i;
}
}
//如果预测正确
if (aim_array[max_index] == 1)
++success_times;
++total_times;
if(total_times % 2000 == 0)
cout << "已测试:" << total_times << "组" << endl;
}
cout << "正确率: " << 100.0 * success_times / total_times << "%" << endl << endl;
cout << "*************************" << endl << endl;
}
int main(int argc, char * argv[])
{
BP bp;
bp.Init();
//训练数据反复利用TRAIN_TIMES次
for(int i = 0; i < TRAIN_TIMES; i++)
{
cout << "开始第" << i + 1 << "轮迭代" << endl << endl;
bp.Train();
}
return 0;
}

算法分析

本实验中,BP神经网络中人为可调的参数就两个,一个是隐含层维度,还有一个是学习率。隐含层维度影响了程序判断的正确性,同时也影响着程序运行的时间。学习率的设置也很有讲究,过小会导致收敛过慢以及陷入局部最优,过大会使得结果发生震荡。

此外,由于BP神经网络在训练时有遗忘旧样本的趋势,所以对于60000组测试数据,我进行了反复利用,设置了迭代次数,使得正确率可以进一步提高,但也不可避免地增加了程序的运行时间。

实验结果

每一轮迭代都完整的使用了60000组训练数据,每训练10000组训练数据,就进行一次测试,取最佳准确率。

- 第几轮迭代 - 最佳准确率
1 92.22%
2 93.64%
3 94.13%
4 94.47%
5 94.69%

可以发现,正确率的增长逐渐变得缓慢,程序的运行时间也要相应的加长。

可以改进的地方

由于60000组训练数据被反复训练,所以时间久了会出现过拟合现象,这可以通过画出”学习曲线”来观测,具体可以借鉴吴恩达老师的课程。解决的办法是每次随机地从60000组数据中抽取一部分进行训练,而不是60000组按顺序循环。

此外,学习率我设为了固定值,其实可以根据训练的推进逐步变小,可以达到更好的效果。

donate the author