0%

C语言实现BP神经网络的推理以及反向传播

C语言实现BP神经网络的推理以及反向传播

代码

  • 头文件
    #ifndef BPNETWORK_H
    #define BPNETWORK_H
    //所需头文件
    #include<math.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>


    #define f(x) Sigmoid(x)//激活函数设定
    #define f_(x) Sigmoidf(x)//导函数

    typedef struct {
    double* ws;//权重矩阵
    double* bs;//偏置数组
    double* os;//输出数组
    double* ss;//误差(总误差关于加权和的偏导)
    } Layer;
    typedef struct {
    int lns;//层数
    int* ns;//每层神经元的数量
    double* is;//神经网络输入
    double* ts;//理想输出
    Layer* las;//神经网络各个层(不包括输入层)
    double ln;//学习率
    }BPNetWork;



    //创建神经网络
    BPNetWork* BPCreate(int* nums, int len,double ln);
    //运行一次神经网络
    void RunOnce(BPNetWork* network);
    //载入训练集
    void LoadIn(BPNetWork* network, double* input, double* putout);
    //反向传播一次(训练一次)
    void TrainOnce(BPNetWork* network);
    //输出总误差
    double ETotal(BPNetWork* network);

    //sigmoid激活函数
    #define Sigmoid(x) (1 / (1 + exp(-(x))))
    //sigmoid激活函数的导函数(用反函数的形式表示),输入为sigmoid输出
    #define Sigmoidf(f) ((f) * (1 - (f)))
    #define Tanh(x) ((2 / (1 + exp(-2 * (x))))-1)
    #define Tanhf(f) ((1+(f))*(1-(f)))
    #endif
  • .c文件
    #include"BPNetWork.h"


    //神经网络的层数
    #define LS network->lns

    //输入层神经元的数量
    #define INNS network->ns[0]

    //输入层的第a个输入
    #define INS(a) network->is[a-1]

    //第a个理想输出
    #define TAS(a) network->ts[a-1]

    //输出层神经元的数量
    #define OUTNS network->ns[LS-1]

    //第n层神经元的数量
    #define NS(n) network->ns[n-1]

    //第n层第a个神经元的第p个权重
    #define WF(n,a,p) network->las[n-2].ws[(p-1)+(a-1)*NS(n-1)]

    //第n层的第a个神经元的偏置
    #define BF(n,a) network->las[n-2].bs[a-1]

    //第n层第a个神经元的输出
    #define OF(n,a) network->las[n-2].os[a-1]

    //第n层第a个神经元的误差
    #define SF(n,a) network->las[n-2].ss[a-1]

    //学习率
    #define LN network->ln

    BPNetWork* BPCreate(int* nums, int len,double ln)
    {
    BPNetWork* network = malloc(sizeof(BPNetWork));
    network->lns = len;
    network->ns = malloc(len * sizeof(int));
    network->ln = ln;
    memcpy(network->ns, nums, len * sizeof(int)); // nums传入的是每层的神经元数目,将其拷贝到储存每层神经元数量的ns
    //
    network->is = malloc(nums[0] * sizeof(double)); // 神经网络输入
    network->las = malloc(sizeof(Layer) * (len - 1)); // 神经网络各个层
    network->ts = malloc(sizeof(double) * nums[len - 1]); // 神经网络理想输出
    srand(&network);//用networkd的内存地址做为随机数种子
    for (int p = 0; p < len - 1; p++) {
    int lastnum = nums[p];//上一层的神经元数量
    int num = nums[p + 1];//当前层的神经元数量(从第二层开始)
    network->las[p].bs = malloc(sizeof(double) * num); //偏置数组
    //
    network->las[p].ws = malloc(sizeof(double) * num * lastnum); //权重矩阵(大小是本层与上一层的节点数量的乘积)
    //
    network->las[p].os = malloc(sizeof(double) * num); //输出数组
    //
    network->las[p].ss = malloc(sizeof(double) * num); //误差
    for (int pp = 0; pp < num; pp++) {
    //这里rand()/2.0的意思是把整数除整数转换为浮点数除整数
    //如果是整数除整数,输出则为带余的商
    network->las[p].bs[pp] = rand() / 2.0 / RAND_MAX; //偏置矩阵初始化随机数
    for (int ppp = 0; ppp < lastnum; ppp++) {
    network->las[p].ws[ppp + pp * lastnum] = rand() / 2.0 / RAND_MAX; //权重矩阵初始化随机数
    }
    }
    }
    return network;
    }

    void RunOnce(BPNetWork* network) {
    //计算输入层到第二层
    for (int a = 1; a <= NS(2); a++) {
    double net = 0;
    double* o = &OF(2,a);// 第2层的输出值指针
    for (int aa = 1; aa <= INNS; aa++) {
    // 和是向量积
    net += INS(aa) * WF(2, a, aa);//输入层的某个输入*第一个全连接层中相应的权重*第一个全连接层的神经元数值
    }
    *o = f(net + BF(2,a)); //加偏置计算最终结果,然后乘激活函数,写入第二层的输出数组中
    }
    for (int n = 2; n <= LS-1; n++) { //LS是层数
    for (int a = 1; a <= NS(n + 1); a++) {//NS是对应层神经元的数量,循环内容是针对下一层的每个神经元
    double net = 0;
    double* o = &OF(n+1,a);
    for (int aa = 1; aa <= NS(n); aa++) { // 计算向量积
    double oo = OF(n, aa); // 上一层某个神经元的输出
    double* ww = &WF(n + 1, a, aa); // 第a个和第aa个的权重
    net += oo * (*ww); // 和是向量积
    }
    *o = f(net + BF(n + 1, a));
    }
    }
    }

    void TrainOnce(BPNetWork* network) {
    //计算输出层的误差函数
    for (int a = 1; a <= OUTNS; a++) {
    double* s = &SF(LS,a);//获取第a个神经元的误差
    double* b = &BF(LS, a);//获取第a个神经元的偏置
    double o = OF(LS, a);//获取第a个神经元的输出
    *s = (2.0 / OUTNS) * (o - TAS(a))* f_(o); // 2/输出层元素数量*(某个神经元的输出-该位置的理想输出)* 斜率
    *b = *b - LN * (*s); //更新偏置
    //更新权重
    for (int aa = 1; aa <=NS(LS-1) ; aa++) {
    double* w = &WF(LS, a, aa); // 获得最后一层权重矩阵的权重
    *w = *w - LN * (*s) * OF(LS-1, aa); // 权重减去 学习率*上面计算出的s*上一层该神经元的输出
    }
    }

    //计算隐藏层的误差
    for (int a = LS-1; a > 2; a--) {
    //开始计算第a层每个神经元的误差
    for (int n = 1; n <= NS(a); n++) {//当前层
    double* s = &SF(a, n);//获取第a个神经元的误差
    *s = 0;
    double* b = &BF(a, n);//获取第a个神经元的偏置
    double o = OF(a, n);//获取第a个神经元的输出
    for (int nn = 1; nn <= NS(a+1); nn++) {//下一层
    double lw = WF(a + 1, nn, n);//获取下一层到当前神经元的权重
    double ls = SF(a + 1, nn);//获取下一层第nn个神经元的误差
    *s += ls * lw * f_(o); // 权重*误差*激活函数斜率,和是向量积
    }
    *b = *b - LN * (*s);//更新偏置
    //更新权重
    for (int nn = 1; nn <= NS(a - 1); nn++) {//上一层
    double* w = &WF(a, n, nn); // 更新上一层到这一层的权重矩阵
    *w = *w - LN * (*s) *OF(a - 1, nn);
    }
    }
    }

    //计算第2层的误差函数(输入层到第一隐藏层)
    for (int n = 1; n <= NS(2); n++) {//当前层
    double* s = &SF(2, n);//获取第a个神经元的误差
    *s = 0;
    double* b = &BF(2, n);//获取第a个神经元的偏置
    double o = OF(2, n);//获取第a个神经元的输出
    for (int nn = 1; nn <= NS(3); nn++) {//下一层
    double lw = WF(3, nn, n);//获取下一层到当前神经元的权重
    double ls = SF(3, nn);//获取下一层第nn个神经元的误差
    *s += ls * lw * f_(o);
    }
    *b = *b - LN * (*s);//更新偏置
    //更新权重
    for (int nn = 1; nn <= INNS; nn++) {//上一层
    double* w = &WF(2, n, nn);
    *w = *w - LN * (*s) * INS(nn);
    }
    }

    }

    void LoadIn(BPNetWork* network,double* input,double* putout) {
    memcpy(network->is, input, INNS*sizeof(double));
    memcpy(network->ts, putout, OUTNS*sizeof(double));
    }

    int main() {
    int a[] = { 1,20,20,1 };//4层神经元,数量分别为1,20,20,1
    double in[1] = { 0.9 };//训练样本输入1
    double in1[1] = { 0.1 };//训练样本输入2
    double in2[1] = { 0.5 };//训练样本输入3
    double out[1] = { 0.1 };//理想输出
    //神经网络训练目标:
    //输入任意值,输出0.1
    BPNetWork* network = BPCreate(a, 4, 0.5);
    int c = 1000;//训练1000次
    while (c--) {
    LoadIn(network, in, out);
    RunOnce(network);
    TrainOnce(network);
    LoadIn(network, in1, out);
    RunOnce(network);
    TrainOnce(network);
    LoadIn(network, in2, out);
    RunOnce(network);
    TrainOnce(network);
    }
    //训练完后来一波测试
    double t[1] = { 0.7 };//输入
    double o[1] = { 0.2 };//凑数
    LoadIn(network, t, o);
    RunOnce(network);
    printf("OK\n");
    printf("%g\n", ETotal(network));
    printf("%g", OF(4, 1));
    return 0;
    }

计算图

正向

  • 正向推理的计算过程是某层的某个节点的输出数值等于该层上一层的某个节点的输出×这个节点到该层待求的节点的权重, 如上对上一层每个节点计算一遍并求和,然后加上该层该节点的偏置,并且带入激活函数计算得到这层这个节点的输出
  • 图 1

    反向传播

  • 反向传播的计算对于输出层到上一层的权重更新而言就是上一层到这一层某节点的权重减去输出层某节点的实际输出和理想输出的差×激活函数的导数×系数×学习率×上一层对应节点的输出
  • 对于中间层的计算,某个节点的误差等于下一层某节点到该节点的权重×下一层对应节点的误差×该节点激活函数的斜率,如上计算求和得到该节点的误差,然后对于上一层每个节点到这一个节点的权重,更新方法为减去学习率×该节点的误差(前文计算的)×上一层对应节点的输出
  • 图 2