C++简单的BP神经网络

基于C++搭建BP神经网络

BP神经网络

​ 包括信号的向前传播和误差的反向传播两个过程。即计算误差输出时按从输入到输出的方向进行,而调整权值和阈值则从输出到输入的方向进行。当实际输出与理想输出之间的误差超过期望时,就需要进入误差的反向传播过程。它首先从输出层开始,误差按照梯度下降的方法对各层权值进行修正,并依次向隐含层、输入层传播。通过不断的信息正向传播和误差反向传播,各层权值会不断进行调整,这就是神经网络的学习训练。当输出的误差减小到期望程度或者预先设定的学习迭代次数时,训练结束,BP神经网络完成学习。

BP神经网络

数据集

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

​ 其中,训练数据集包括60000个样本及标签,测试数据集包括10000个样本及标签。

MNIST数据集训练数据集测试数据集
数字样本train-images-idx3-ubyte.gzt10k-images-idx3-ubyte.gz
数字标签train-labels-idx1-ubyte.gzt10k-labels-idx1-ubyte.gz

​ 下载地址(GitHub镜像)(https://521github.com/golbin/TensorFlow-MNIST/tree/master/mnist/data)

MNIST

代码实现

头文件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 int TRAIN_TIMES = 10; //迭代训练次数
const double LEARN_RATE = 0.3; //学习率
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#pragma once

源文件BP.cpp

#define _CRT_SECURE_NO_WARNINGS
#include "BP.h"
#include <iostream>
#include <fstream>   
#include <string> 
#include<stdio.h>
#include<ctime>
using namespace std;
//初始化各参数
int total_times = 0; //当前测试了几次
int success_times = 0; //当前正确了几次
double rate = 0.0, best_rate = 0.0;
time_t time1, time2, time3;
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;
  //  FILE* test_labels;
  //  FILE* test_images;
    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");
    

    if (test_images == NULL || test_labels == NULL)
    {
        cout << "open training file error!" << endl;
        exit(0);
    }
    unsigned char image[784];
    unsigned char label[10];
    unsigned char temp[100];
    //读取文件开头
    fread(temp, 1, 16, test_images);
    fread(temp, 1, 8, test_labels);

    total_times = 0; //当前测试了几次
    success_times = 0; //当前正确了几次
    rate = 0.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;
    }
    rate = 100.0 * success_times / total_times;
    if (rate > best_rate)
        best_rate = rate;
    cout << "正确率: " << rate << "%" << endl << endl;
    cout << "*************************" << endl << endl;

}

int main(int argc, char* argv[])
{
    BP bp;
    bp.Init();
    time_t start_time = time(nullptr);
    //训练数据反复利用TRAIN_TIMES次
    best_rate = 0.0
    for (int i = 0; i < TRAIN_TIMES; i++)
    {   
        time1 = time(nullptr);
        cout << "开始第" << i + 1 << "轮迭代" << endl << endl;
        bp.Train();
        time2 = time(nullptr);
        time3 = time2 - time1;
        cout << "第"<<i+1<<"次迭代时间为 " << time3 << " 秒" << std::endl;
        cout << "此次迭代最佳精度为" << best_rate << "%" << endl;
    }
    time_t end_time = time(nullptr);
    time_t elapsed_time = end_time - start_time;
    cout << "程序运行时间为 " << elapsed_time << " 秒" << std::endl;

    return 0;
}

实验遇到的问题

  • 由于编译器软件的问题,Visual Studio 2022不支持bits/stdc++.h头文件,因此根据代码要求进行头文件的添加,依次添加了iostream、fstream、string、stdio.h、ctime
  • 编译器Visual Studio认为fopen函数存在安全风险,实验报错,因此在源文件开头添加上宏定义 _CRT_SECURE_NO_WARNINGS
  • 使用fopen函数时,注意文件名称应当正确,否则会出现ERROR_FILE_NOT_FOUND,推荐复制文件名,用相对路径的方法打开文件即可减少此错误发生的概率
  • 从GitHub上下载的数据集是四个压缩文件,因此需要先进行文件解压操作,如果没有解压直接使用fopen函数,则会导致FILE指针读取不了文件,解决方法:
    1. 通过vckpg下载zlib库,然后在源文件中添加上zlib头文件,再将fopen、fread函数替换为与之对应的gzopen和gzlib函数。不过此方法较为繁琐,不推荐使用。
    2. 将训练数据集和测试数据集依次解压,解压后可直接使用fopen、fread函数进行操作 ,更加简洁。

实验结果

每次迭代使用全部的60000组数据进行训练,每训练10000组时进行一次测试操作,一共进行十次迭代,记录每次迭代的最佳精度、每次迭代的时长以及10次迭代的总时长。分别在学习率为0.1 0.3和3下进行了实验,以下是实验数据:

学习率=0.1
迭代轮数时长最佳精度
23 秒89.7207%
21 秒91.5008%
22 秒92.4708%
22 秒93.1007%
21 秒93.3607%
21 秒93.6506%
21 秒93.8306%
21 秒93.9906%
22 秒94.0606%
21 秒94.1806%
总时长215 秒

0.1

学习率=0.3
迭代轮数时长最佳精度
21 秒91.1809%
21 秒92.9207%
21 秒93.6406%
21 秒94.1206%
21 秒94.1306%
21 秒94.4706%
21 秒94.6805%
21 秒94.5705%
21 秒94.9505%
21 秒94.5505%
总时长213 秒

0.3

学习率=3
迭代轮数时长最佳精度
22 秒64.2336%
22 秒67.3833%
21 秒70.6029%
21 秒69.3631%
21 秒70.6529%
22 秒73.9626%
21 秒72.0128%
22 秒73.5026%
21 秒73.7626%
21 秒75.3125%
总时长214 秒

3

总结

BP神经网络在不同学习率的情况下最终的迭代精度会有所不同,学习率较小时,达到同样精度需要更多的迭代次数,学习率过大时,则会导致迭代精度偏差较大,甚至出现震荡现象,容易导致过拟合,因此在实际情况中应该选择一个合适的学习率,或者根据迭代次数进行学习率的动态变化从而达到更好的效果。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值