基于C++复现BP神经网络手写字识别
文章目录
前言
BP(Back-propagation,反向传播)神经网络是最传统的神经网络。也就是使用了Back-propagation算法的神经网络。BP神经网络是一种按误差反向传播(简称误差反传)训练的多层前馈网络,其算法称为BP算法,它的基本思想是梯度下降法,利用梯度搜索技术,以期使网络的实际输出值和期望输出值的误差均方差为最小。基本BP算法包括信号的前向传播和误差的反向传播两个过程。即计算误差输出时按从输入到输出的方向进行,而调整权值和阈值则从输出到输入的方向进行。正向传播时,输入信号通过隐含层作用于输出节点,经过非线性变换,产生输出信号,若实际输出与期望输出不相符,则转入误差的反向传播过程。误差反传是将输出误差通过隐含层向输入层逐层反传,并将误差分摊给各层所有单元,以从各层获得的误差信号作为调整各单元权值的依据。通过调整输入节点与隐层节点的联接强度和隐层节点与输出节点的联接强度以及阈值,使误差沿梯度方向下降,经过反复学习训练,确定与最小误差相对应的网络参数(权值和阈值),训练即告停止。此时经过训练的神经网络即能对类似样本的输入信息,自行处理输出误差最小的经过非线形转换的信息。
组成
BP神经网络(Backpropagation Neural Network)由多个神经元(Neurons)和多层组成,通常包括以下几个主要组成部分:
1.输入层(Input Layer): 输入层接受外部输入数据,每个输入特征对应一个输入神经元。例如,对于图像分类任务,每个像素可以被看作一个输入特征,输入层的神经元数量等于输入特征的数量。
2.隐藏层(Hidden Layers): 隐藏层位于输入层和输出层之间,可以包含一个或多个层次。这些层中的每一层都包括多个神经元。隐藏层的主要作用是学习从输入到输出之间的复杂映射关系,以便更好地拟合数据。每个隐藏层都有其独特的权重和激活函数。
3.输出层(Output Layer): 输出层生成网络的最终输出,其神经元数量通常取决于问题类型。例如,对于二分类问题,可以有一个输出神经元表示两个类别的概率,对于多分类问题,可以有多个输出神经元,每个神经元对应一个类别的概率。
4.权重(Weights): 每个连接输入、隐藏和输出层的神经元都有一个权重,权重决定了信号传递的强度和方向。在训练中,这些权重会不断调整以最小化损失函数。
5.偏差(Biases): 每个神经元都有一个偏差项,它可以调整神经元对输入信号的敏感程度。偏差项允许神经元更灵活地学习。
6.激活函数(Activation Function): 每个神经元都通过激活函数对输入进行非线性变换。常见的激活函数包括Sigmoid、ReLU、Tanh等,它们引入非线性性质,使神经网络能够适应更复杂的数据分布。
7.前向传播(Forward Propagation): 在前向传播过程中,输入数据从输入层传递到输出层。每个神经元接收来自前一层神经元的输入,将这些输入与其权重相乘,并将结果传递给激活函数。前向传播计算网络的输出。
8.反向传播(Backpropagation): 反向传播是训练BP神经网络的关键步骤。它通过计算损失函数关于权重和偏差的梯度,然后使用梯度下降或其他优化算法来更新权重和偏差,以减小损失函数的值。
9.损失函数(Loss Function): 损失函数用于衡量网络的输出与实际目标的差异。在训练中,目标是最小化损失函数,以提高网络的性能。
应用
1.图像识别和分类: BP神经网络可以用于图像识别和分类任务,如手写数字识别、物体识别、人脸识别等。
2.自然语言处理(NLP): 在自然语言处理领域,BP神经网络可以用于文本分类、情感分析、机器翻译等任务。
3.声音识别: 用于语音识别和语音合成,如语音助手和语音命令识别。
BP神经网络是一种多功能的机器学习模型,适用于各种领域的模式识别和预测任务。
一、BP神经网络手工推导
上图为神经网络的一部分,x1,x2,x3为外部输入,w1,w2,w3为这些输入的权重(表示这些输入的重要程度)。后面的大⚪相当与一个神经元,其中∑ = x1w1 + x2w2 + x3w2 - β ( β为偏置) ,y = f(∑) ,f为激活函数,最常见激活函数是Sigmoid函数,其图像及表达式如下图所示(本文后面所有激活函数都为Sigmoid函数)
下面开始代码实战
二、固定学习率
1.下载MNIST数据集
包含70000张图片,每张大小28*28=784
解压后如下所示
2.复现
提示:把以下程序和数据集放同一目录下
BP.h
#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
#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;
}
3.实验结果
不同的学习率得到准确率结果对比如下表(其中每一轮记录较好的准确率)
轮数\学习率 | 0.3 | 0.2 | 0.1 |
---|---|---|---|
第一轮 | 92.42 | 92.66 | 90.54 |
第三轮 | 93.46 | 93.53 | 93.19 |
第五轮 | 94.01 | 93.53 | 94.11 |
第七轮 | 94.60 | 94.57 | 94.20 |
第九轮 | 94.84 | 95.13 | 94.49 |
第十轮 | 95.06 | 95.16 | 94.59 |
总时间 | 281s | 237.2s | 222.3s |
三、动态学习率
1.调整程序
BP.h代码如下(示例):
#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 double LEARNING_RATE_DECAY = 0.95; // 学习率衰减率
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代码如下(示例):
#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()
{
double current_learn_rate = LEARN_RATE; // 当前学习率
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();
current_learn_rate *= LEARNING_RATE_DECAY;// 在每个训练步骤后逐渐减小学习率
}
}
//测试
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;
}
2.实验结果
以下表中是每一轮较好的准确率,总时间247s
轮数 | 准确率 |
---|---|
第一轮 | 91.91 |
第三轮 | 93.97 |
第五轮 | 94.53 |
第七轮 | 95.10 |
第九轮 | 95.18 |
第十轮 | 95.17 |
附录
本文参考:手动搭建BP神经网络