BP神经网络的C++实现

    之前说了组里的任务是手写BP神经网络,上一篇文总结了一下BP神经网络的概念,老实说,总结概念前的一个C++实现版本在总结概念之后重新审视时觉得实在是惨不忍睹,于是今晚回炉重写了。这篇文就来挂我的BP神经网络C++实现。

    老师提出的具体问题是平面对点集的二分类。ACM战过这么多场,写板子早成了习惯,就把通用的BP认真封装了一下:
    头文件里的Data结构体是输入数据的数据结构,可自定义,这里用的是点分类问题的模型,BP.h:

#ifndef _BP_H_
#define _BP_H_

#include <vector>
#include <string>
using namespace std;

/* 数据样本类 */
struct Data
{
    /* 输入参数,包括: */
    /* 三维直角坐标系点的坐标(x, y, z) */
    /* 三维直角坐标系平面a*x + b*y + c*z + d = 0的4个系数 */
    double x[7];
    /* 期望输出即监督值 */
    /* 拟合 */
    // double d[1];
    /* 分类 */
    double d[2];
    /* 数据构造函数 */
    Data();
};

class BP
{
private:
    /* ========== 常数 ========== */
    /* 输入层节点数 */
    int I;
    /* 隐含层神经元数 */
    int H;
    /* 输出层神经元数 */
    int O;
    /* 权重学习速率 */
    double LR;
    /* 偏置学习速率 */
    double LR2;
    /* 学习速率衰减率(每次衰减与当前LR相乘) */
    double LRDecay;
    /* 误差函数收敛阈值 */
    double C;
    /* ========== 容器 ========== */
    /* 训练用数据样本集 */
    vector<Data> trainDS;
    /* 测使用数据集 */
    vector<Data> testDS;
    /* 输入层与隐含层间的全连接权重:w[I(包含一个偏置值)][H] */
    double **w;
    /* w修正值 */
    double *dw;
    /* 隐含层阈值 */
    double *th;
    /* 隐含层输入积累即净激活,也存放之后的激活输出值:u[H] */
    double *u;
    /* 隐含层与输出层间的全连接权重:v[H][O] */
    double **v;
    /* v修正值 */
    double *dv;
    /* 输出层阈值 */
    double *to;
    /* 输出层输入积累即净激活,也存放之后的激活输出值:y[O] */
    double *y;
    /* 拟合标识 */
    bool regression;
    /* ========== 方法 ========== */
    /* 填充训练用数据样本集 */
    void fillTrainDS(int sampleCnt);
    /* 清空训练用数据样本集 */
    void clearTrainDS();
    /* 填充测试使用数据集 */
    void fillTestDS(int sampleCnt);
    /* 清空测试使用数据集 */
    void clearTestDS();
    /* 预测 */
    void forward(int index, bool test = false);
    /* 调整 */
    void backward(int index);
public:
    /* ========== 接口 ========== */
    /*
     * 类构造函数,初始化BP神经网络结构和训练参数
     * int _I:        输入参数数目。
     * int _O:        输出值数目。
     * int A:        隐含层调整因子(1~10)。
     * double _LR:        权重学习速率(0.01~0.8)。
     * double _LR2:    偏置学习速率(0.01~0.8)。
     * double _LRDecay:    学习速率衰减率(每次衰减与当前LR相乘)。
     * double _C:        误差函数收敛阈值。
     * bool regression:    拟合标识。
     */
    BP(int _I, int _O, int A = 1, double _LR = 0.01, double _LR2 = 0.035, double _LRDecay = 1.0, double _C = 0.01, bool regression = false);
    /* 类析构函数,释放容器分配的堆空间 */
    ~BP();
    /* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
    double train(int sampleCnt = 1000, int trainCnt = 100);
    /*
     * 使用指定数目的样本循环训练。
     * 误差函数值进入可接受范围判定收敛并停止训练;
     * 到达最大训练次数时停止训练。
     * 返回是否收敛。
     */
    bool trainTillConvergent(int sampleCnt = 1000, int maxEpoch = 1000);
    /* 生成指定数目组数据测试当前神经网络 */
    void testNetwork(int testCnt = 1000);
    /* 保存当前神经网络,即两个权重数组 */
    void saveNetwork(string wPath = "wNetwork", string vPath = "vNetwork");
    /* 载入两个权重数组,还原神经网络 */
    void loadNetwork(string wPath = "wNetwork", string vPath = "vNetwork");
    /* 使用指定数据预测输出值并返回输出值 */
    vector<double> runNetwork(vector<double> x);
};

#endif

    类实现源码里的Data结构体的构造函数是输入数据的处理和问题模型的建立,可自定义,这里用的是点分类问题的数据处理,BP.cpp:

#include "BP.h"
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <fstream>
using namespace std;

/* 数据构造函数 */
Data::Data()
{
    /* 归一参数 */
    for(int i = 1 ; i < 8 ; i++)
    {
        x[i] = rand()/(double)RAND_MAX;
    }
    /* 实际参数 */
    double _x = x[1]*20 - 10;
    double y = x[2]*20 - 10;
    double z = x[3]*20 - 10;
    double a = x[4]*20 - 10;
    double b = x[5]*20 - 10;
    double c = x[6]*20 - 10;
    double _d = x[7]*20 - 10;
    /* 拟合 */
    // d[0] = z - (a*_x + b*y + _d)/(-1*c);
    /* 分类 */
    d[0] = z > (a*_x + b*y + _d)/(-1*c) ? 1 : 0;
    d[1] = d[0] ? 0 : 1;
}

/* 填充训练用数据样本集 */
void BP::fillTrainDS(int sampleCnt)
{
    while(sampleCnt--)
    {
        trainDS.push_back(Data());
    }
}

/* 清空训练用数据样本集 */
void BP::clearTrainDS()
{
    vector<Data> v;
    trainDS.swap(v);
}

/* 填充测试使用数据集 */
void BP::fillTestDS(int sampleCnt)
{
    while(sampleCnt--)
    {
        testDS.push_back(Data());
    }
}

/* 清空测试使用数据集 */
void BP::clearTestDS()
{
    vector<Data> v;
    testDS.swap(v);
}

/* 预测 */
void BP::forward(int index, bool test)
{
    /* 隐含层 */
    for(int i = 0 ; i < H ; i++)
    {
        u[i] = 0;
        for(int j = 0 ; j < I ; j++)
        {
            double x = test ? testDS[index].x[j] : trainDS[index].x[j];
            /* 积累输入 */
            u[i] += w[j][i]*x;
        }
        u[i] += th[i];
        /* Sigmoid函数作为激活函数 */
        u[i] = 1 / (1 + exp(-1*u[i]));
    }
    /* 输出层 */
    for(int i = 0 ; i < O ; i++)
    {
        y[i] = 0;
        for(int j = 0 ; j < H ; j++)
        {
            /* 积累输入 */
            y[i] += v[j][i]*u[j];
        }
        y[i] += to[i];
        /* 分类:Sigmoid函数作为激活函数 */
        if (!regression)
        {
            y[i] = 1 / (1 + exp(-1*y[i]));
        }
    }
}

/* 调整 */
void BP::backward(int index)
{
    /* 计算隐含层与输出层间权重调整值 */
    for(int i = 0 ; i < O ; i++)
    {
        /* 拟合:计算输出层学习误差 */
        if (regression)
        {
            dv[i] = y[i] - trainDS[index].d[i];
        }
        /* 分类:计算输出层学习误差 */
        else
        {
            dv[i] = (y[i] - trainDS[index].d[i])*y[i]*(1 - y[i]);
        }
    }
    /* 计算输入层与隐含层间权重调整值 */
    double t;
    for(int i = 0 ; i < H ; i++)
    {
        t = 0;
        for(int j = 0 ; j < O ; j++)
        {
            t += dv[j]*v[i][j];
        }
        dw[i] = t*u[i]*(1 - u[i]);
    }
    /* 调整隐含层与输出层间权重 */
    for(int i = 0 ; i < H ; i++)
    {
        for(int j = 0 ; j < O ; j++)
        {
            v[i][j] -= LR*dv[j]*u[i];
        }
    }
    /* 调整输出层偏置 */
    for(int i = 0 ; i < O ; i++)
    {
        to[i] -= LR2*dv[i];
    }
    /* 调整输入层与隐含层间权重 */
    for(int i = 0 ; i < I ; i++)
    {
        for(int j = 0 ; j < H ; j++)
        {
            w[i][j] -= LR*dw[j]*trainDS[index].x[i];
        }
    }
    /* 调整隐含层偏置 */
    for(int i = 0 ; i < H ; i++)
    {
        th[i] -= LR2*dw[i];
    }
}

/*
 * 类构造函数,初始化BP神经网络结构和训练参数
 * int _I:        输入参数数目,包括偏置值对应的参数-1。
 * int _O:        输出值数目。
 * int A:        隐含层调整因子(1~10)。
 * double _LR:        权重学习速率(0.01~0.8)。
 * double _LR2:    偏置学习速率(0.01~0.8)。
 * double _LRDecay:    学习速率衰减率(每次衰减与当前LR相乘)。
 * double _C:        误差函数收敛阈值。
 * bool regression:    拟合标识。
 */
BP::BP(int _I, int _O, int A, double _LR, double _LR2, double _LRDecay, double _C, bool _regression)
{
    /* ========== 初始化常数 ========== */
    I = _I;
    H = ceil(sqrt(_I + _O)) + A;
    O = _O;
    LR = _LR;
    LR2 = _LR2;
    LRDecay = _LRDecay;
    C = _C;
    regression = _regression;
    /* ========== 初始化容器 ========== */
    srand((unsigned)time(NULL));
    /* 初始化w */
    w = new double*[I];
    for(int i = 0 ; i < I ; i++)
    {
        w[i] = new double[H];
        for(int j = 0 ; j < H ; j++)
        {
            w[i][j] = rand()/(double)RAND_MAX;
        }
    }
    /* 初始化dw */
    dw = new double[H];
    /* 初始化th */
    th = new double[H];
    for(int i = 0 ; i < H ; i++)
    {
        th[i] = rand()/(double)RAND_MAX;
    }
    /* 初始化u */
    u = new double[H];
    /* 初始化v */
    v = new double*[H];
    for(int i = 0 ; i < H ; i++)
    {
        v[i] = new double[O];
        for(int j = 0 ; j < O ; j++)
        {
            v[i][j] = rand()/(double)RAND_MAX;
        }
    }
    /* 初始化dv */
    dv = new double[O];
    /* 初始化to */
    to = new double[O];
    for(int i = 0 ; i < O ; i++)
    {
        to[i] = rand()/(double)RAND_MAX;
    }
    /* 初始化y */
    y = new double[O];
}

/* 类析构函数,释放容器分配的堆空间 */
BP::~BP()
{
    /* 释放w */
    for(int i = 0 ; i < I ; i++)
    {
        delete []w[i];
    }
    delete []w;
    /* 释放dw */
    delete []dw;
    /* 释放th */
    delete []th;
    /* 释放u */
    delete []u;
    /* 释放v */
    for(int i = 0 ; i < H ; i++)
    {
        delete []v[i];
    }
    delete []v;
    /* 释放dv */
    delete []dv;
    /* 释放to */
    delete []to;
    /* 释放y */
    delete []y;
}

/* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
double BP::train(int sampleCnt, int trainCnt)
{
    fillTrainDS(sampleCnt);
    double e;
    while(trainCnt--)
    {
        e = 0;
        for(int i = 0 ; i < trainDS.size() ; i++)
        {
            /* 预测 */
            forward(i);
            /* 误差积累 */
            for(int j = 0 ; j < O ; j++)
            {
                e += pow(y[j] - trainDS[i].d[j], 2.0);
            }
            /* 调整 */
            backward(i);
        }
        e /= 2*sampleCnt;
        /* 学习速率衰减 */
        if (LR > 0.01)
        {
            LR *= LRDecay;
        }
    }
    clearTrainDS();
    return e;
}

/*
 * 使用指定数目的样本循环训练。
 * 误差函数值进入可接受范围判定收敛并停止训练;
 * 到达最大训练次数时停止训练。
 * 返回是否收敛。
 */
bool BP::trainTillConvergent(int sampleCnt, int maxEpoch)
{
    fillTrainDS(sampleCnt);
    double e;
    for(;;)
    {
        e = 0;
        for(int i = 0 ; i < trainDS.size() ; i++, maxEpoch--)
        {
            if (!maxEpoch)
            {
                clearTrainDS();
                return false;
            }
            /* 预测 */
            forward(i);
            /* 误差积累 */
            for(int j = 0 ; j < O ; j++)
            {
                e += pow(y[j] - trainDS[i].d[j], 2.0);
            }
            /* 调整 */
            backward(i);
        }
        /* 判定收敛,中止训练 */
        if (e/(2*sampleCnt) < C)
        {
            clearTrainDS();
            return true;
        }
        /* 学习速率衰减 */
        if (LR > 0.01)
        {
            LR *= LRDecay;
        }
    }
    clearTrainDS();
    return false;
}

/* 生成指定数目组数据测试当前神经网络 */
void BP::testNetwork(int testCnt)
{
    fillTestDS(testCnt);
    double e = 0;
    for(int i = 0 ; i < testCnt ; i++)
    {
        forward(i, true);
        for(int j = 0 ; j < O ; j++)
        {
            e += pow(y[j] - testDS[i].d[j], 2.0);
        }
    }
    cout << testCnt << "组数据测试预测值相对期望值方差为:" << e/(2*testCnt) << endl;
    clearTestDS();
}

/* 保存当前神经网络,即两个权重数组 */
void BP::saveNetwork(string wPath, string vPath)
{
    ofstream fout_w(wPath);
    if(fout_w == NULL)
    {
        cout << "打开文件失败" << endl;
        return;
    }
    for(int i = 0 ; i < I ; i++)
    {
        for(int j = 0 ; j < H ; j++)
        {
            fout_w << w[i][j] << '\t';
        }
        fout_w << '\n';
    }
    fout_w.close();
    ofstream fout_v(vPath);
    if(fout_v == NULL)
    {
        cout << "打开文件失败" << endl;
        return;
    }
    for(int i = 0 ; i < H ; i++)
    {
        for(int j = 0 ; j < O ; j++)
        {
            fout_v << v[i][j] << '\t';
        }
        fout_v << '\n';
    }
    fout_v.close();
}

/* 载入两个权重数组,还原神经网络 */
void BP::loadNetwork(string wPath, string vPath)
{
    ifstream fin_w(wPath);
    if(fin_w == NULL)
    {
        cout << "打开文件失败" << endl;
        return;
    }
    for(int i = 0 ; i < I ; i++)
    {
        for(int j = 0 ; j < H ; j++)
        {
            fin_w >> w[i][j];
        }
    }
    fin_w.close();
    ifstream fin_v(vPath);
    if(fin_v == NULL)
    {
        cout << "打开文件失败" << endl;
        return;
    }
    for(int i = 0 ; i < H ; i++)
    {
        for(int j = 0 ; j < O ; j++)
        {
            fin_v >> v[i][j];
        }
    }
    fin_v.close();
}

/* 使用指定数据预测输出值并返回输出值 */
vector<double> BP::runNetwork(vector<double> x)
{
    /* 隐含层 */
    for(int i = 0 ; i < H ; i++)
    {
        u[i] = 0;
        for(int j = 0 ; j < x.size() ; j++)
        {
            /* 积累输入 */
            u[i] += w[j][i]*x[j];
        }
        u[i] += th[i];
        /* Sigmoid函数作为激活函数 */
        u[i] = 1 / (1 + exp(-1*u[i]));
    }
    vector<double> o;
    double y;
    /* 输出层 */
    for(int i = 0 ; i < O ; i++)
    {
        y = 0;
        for(int j = 0 ; j < H ; j++)
        {
            /* 积累输入 */
            y += v[j][i]*u[j];
        }
        y += to[i];
        /* 分类:Sigmoid函数作为激活函数 */
        if (!regression)
        {
            y = 1 / (1 + exp(-1*y));
        }
        o.push_back(y);
    }
    return o;
}

    类的测试方法里无非是根据实际问题初始化类,做一系列的训练,调整参数,慢慢提高预测精度,test.cpp:

#include "BP.h"
#include <iostream>

using namespace std;

int main()
{
    /*
     * 类构造函数,初始化BP神经网络结构和训练参数
     * int _I:        输入参数数目,包括偏置值对应的参数-1。
     * int _O:        输出值数目。
     * int A:        隐含层调整因子(1~10)。
     * double _LR:        学习速率(0.01~0.8)。
     * double _LR2:    偏置学习速率(0.01~0.8)。
     * double _LRDecay:    学习速率衰减率(每次衰减与当前LR相乘)。
     * double _C:        误差函数收敛阈值。
     * bool regression:    拟合标识。
     */
    BP o(7, 2, 8, 0.2, 0.02, 0.99, 3.2e-2, false);

    // o.loadNetwork();

    /* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
    for(int i = 0 ; i < 20 ; i++)
    {
        cout << "误差函数值:" << o.train(1000, 2e3) << endl;
    }

    /*
     * 使用指定数目的样本循环训练。
     * 误差函数值进入可接受范围判定收敛并停止训练;
     * 到达最大训练次数时停止训练。
     * 返回是否收敛。
     */
    // bool success = o.trainTillConvergent(1000, 1e5*1000);
    // cout << "收敛:" << (success ? "success" : "fail") << endl;

    o.testNetwork(1000);

    o.saveNetwork();

    vector<double> X;
    double x = 0.1;
    double y = 0.1;
    double z = 0.1;
    double a = 1;
    double b = 1;
    double c = 1;
    double d = -1;
    X.push_back((x + 10)/20.0);
    X.push_back((y + 10)/20.0);
    X.push_back((z + 10)/20.0);
    X.push_back((a + 10)/20.0);
    X.push_back((b + 10)/20.0);
    X.push_back((c + 10)/20.0);
    X.push_back((d + 10)/20.0);
    vector<double> Y = o.runNetwork(X);
    /* 拟合 */
    // cout << "期望值:" << z - (a*x + b*y + d)/(-c) << endl;
    // cout << "输出值:" << Y[0] << endl;
    /* 分类 */
    cout << "期望值:" << (z > (a*x + b*y + d)/(-c) ? "1\t0" : "0\t1") << endl;
    cout << "输出值:" << Y[0] << '\t' << Y[1] << endl;

    return 0;
}

    点分类问题我先尝试了函数拟合方式,训练情况很糟糕,动辄上十万的方差。后来改用分类方式,降低训练要求,方差立马降下来了。下午例会就要做报告了,我没有训练到充分收敛,但是离平面比较远的点判断正确率已经很高了。下面提供平面分类点集问题当前训练进度下,输入层与隐含层间权重数组、隐含层与输出层间权重数组的保存文件。

    输入层与隐含层间权重数组wNetwork:

0.15513    0.56829    0.461073    0.27552    0.44408    0.144657    0.95798    0.335522    0.325092    0.254423    0.79217
-1.06417    -0.0241895    3.99915    0.228275    -0.483658    6.68402    -1.61574    0.753689    4.14272    -4.58112    -3.09481
3.70442    -3.7423    0.0852013    -0.564758    -4.99619    1.29436    -4.19077    -1.75579    -0.568722    -2.57748    1.20149
-0.311858    2.12485    0.494471    -3.32854    -0.393046    1.12112    -0.585242    -3.45739    -0.687711    -1.37346    -0.860532
1.29214    0.74987    4.41025    -1.37666    0.268284    6.72309    2.0978    -1.8259    4.20105    -6.15165    5.04569
-3.76331    -3.22432    -0.450262    1.77063    -5.89358    3.40051    3.64122    1.30879    -0.98291    -0.931413    -1.72719
9.53636    9.64562    9.82743    13.4779    -12.3668    12.5147    8.37658    6.32437    -10.263    -13.8476    -5.31933

    隐含层与输出层间权重数组vNetwork:

31.4711    -31.5094
27.536    -27.5713
-25.7692    25.7999
-27.4329    27.467
-17.6709    17.6933
-18.464    17.5204
25.8167    -25.8471
-23.2151    23.2434
27.3533    -27.387
26.9126    -26.9519
-24.7905    24.8209

    这个类我还做过a+b问题的函数拟合,收敛非常顺利,精度很高。
    后来,例会上提任务的博士说他明明是要我们寻找分割点集的平面,我把问题泛化成寻找点与平面位置关系规则的问题了,能用泛化能力不太强的BP整的差不多收敛也是不容易。我表示生无可恋……

    读者注意一下,上一篇BP网络的理论介绍中,隐层与输入层之间的权重调整值的推导有一些问题,少乘了一个wij。推导已经改过来了,但是比较忙没空改代码,有需要的同学请自己参照上一篇博文修改一下代码。OTZ

Fork me on GitHub