BP网络c++实现

单隐藏层流程

bp网络
下面实现一个单隐藏层的简单BP算法,其中输入层、隐藏层、输出层节点个数可以自定义,而前向传播和后向传播是主要过程,下面是按上图进行的公式推导

1. 前向传播

1.1 输入层向隐藏层传播

q=∑ixiwij−βjhj(q)=σ(∑ixiwij−βj)q=\sum_i x_i w_{ij} - \beta_j\\ h_j(q)=\sigma( \sum_i x_i w_{ij} - \beta_j ) q=i∑​xi​wij​−βj​hj​(q)=σ(i∑​xi​wij​−βj​)

其中hjh_jhj​为第jjj个隐藏层节点的值,xix_ixi​为第iii个输入层节点的值,wijw_{ij}wij​为第iii个输入层节点到第jjj个隐藏层节点的权重,βj\beta_jβj​为第jjj个隐藏层节点偏置值,σ(x)\sigma(x)σ(x)为Sigmoid激活函数,其公式如下

σ(x)=11+e−x\sigma(x) = \frac{1}{1+e^{-x}} σ(x)=1+e−x1​

代码

//计算h_j(q)
int computeH_j(){
    for (size_t j = 0; j < HIDENODE; j++) {
        double sum = 0;
    	//计算q
        for (size_t i = 0; i < INNODE; i++) {
            sum += inputLayer[i]->value * inputLayer[i]->weight[j];
        }
        sum -= hideLayer[j]->bias;
		//调用sigma函数
        hideLayer[j]->value = utils::sigmoid(sum);
    }
}

1.2 隐藏层向输出层传播

yk^=σ(∑jhjvjk−λk)\hat{y_k} = \sigma( \sum_j h_j v_{jk} - \lambda_k ) yk​^​=σ(j∑​hj​vjk​−λk​)

其中yk^\hat{y_k}yk​^​为第kkk个输出层节点的值(预测值),hjh_jhj​为第jjj个隐藏层节点的值,vjkv_{jk}vjk​为第jjj个隐藏层节点到第kkk个输出层节点的权重,λk\lambda_kλk​为第kkk个输出层节点的偏置值,σ(x)\sigma(x)σ(x)为激活函数。

代码

//计算\hat{y},默认OUTNODE为1,不使用循环也可以
int computeHat_y(){
    for (size_t j = 0; j < OUTNODE; j++) {
        double sum = 0;
        for (size_t i = 0; i < HIDENODE; i++) {
            sum += hideLayer[i]->value * hideLayer[i]->weight[j];
        }
        sum -= outLayer[j]->bias;

        outLayer[j]->value = utils::sigmoid(sum);
    }
}

2. 计算损失函数

损失函数定义如下:

Loss=12∑k(yk−yk^)2Loss = \frac{1}{2}\sum_k ( y_k - \hat{y_k} )^2 Loss=21​k∑​(yk​−yk​^​)2

其中yky_kyk​为第kkk个输出层节点的目标值(真实值),yk^\hat{y_k}yk​^​为第kkk个输出层节点的值(预测值)。

代码

// 计算误差
double error = 0.f;
for (size_t i = 0; i < OUTNODE; i++) {
    double tmp = fabs(outLayer[i]->value - idx.out[i]);
    error += tmp * tmp / 2;
}
//用于判断是否停止训练
error_max = max(error_max, error);

3. 反向传播

利用梯度下降法进行优化,激活函数使用Sigmoid,公式σ(x)=11+e−x\sigma(x) = \frac{1}{1+e^{-x}}σ(x)=1+e−x1​

3.1 计算Δλk\Delta \lambda_kΔλk​(输出层节点偏置值的修正值)

其计算公式如下:

Δλk=∂loss∂λk=η(yi−yi^)yi^(1−yi^)(−1)\Delta\lambda_k=\frac {\partial loss}{\partial \lambda_k}=\eta (y_i-\hat{y_i})\hat{y_i}(1-\hat{y_i})(-1) Δλk​=∂λk​∂loss​=η(yi​−yi​^​)yi​^​(1−yi​^​)(−1)

其中η\etaη为学习率,默认η=0.8\eta=0.8η=0.8。

代码

for (size_t i = 0; i < OUTNODE; i++) {
    double bias_delta = -(idx.out[i] - outLayer[i]->value) *
            outLayer[i]->value * (1.0 - outLayer[i]->value);
    outLayer[i]->bias_delta += bias_delta;
}

3.2 计算Δvjk\Delta v_{jk}Δvjk​(隐藏层节点到输出层节点权重的修正值)

其计算公式如下:

Δvjk=∂loss∂v=η(yk−yk^)yk^(1−yk^)hj\Delta v_{jk} =\frac {\partial loss}{\partial v}= \eta ( y_k - \hat{y_k} ) \hat{y_k} ( 1 - \hat{y_k} ) h_j Δvjk​=∂v∂loss​=η(yk​−yk​^​)yk​^​(1−yk​^​)hj​

其中hjh_jhj​为第jjj个隐藏层节点的值。

代码

for (size_t i = 0; i < HIDENODE; i++) {
    for (size_t j = 0; j < OUTNODE; j++) {
        double weight_delta = (idx.out[j] - outLayer[j]->value) *
                outLayer[j]->value * (1.0 - outLayer[j]->value) *
                hideLayer[i]->value;
        hideLayer[i]->weight_delta[j] += weight_delta;
    }
}

3.3 计算Δβj\Delta \beta_jΔβj​(隐藏层节点偏置值的修正值)

其计算公式如下:

Δβj=∂loss∂β=−η∑k(yk−yk^)yk^(1−yk^)vjkhj(1−hj)\Delta \beta_j = \frac {\partial loss}{\partial \beta} =- \eta \sum_k ( y_k - \hat{y_k} ) \hat{y_k} ( 1 - \hat{y_k} ) v_{jk} h_j ( 1 - h_j ) Δβj​=∂β∂loss​=−ηk∑​(yk​−yk​^​)yk​^​(1−yk​^​)vjk​hj​(1−hj​)

其中vjkv_{jk}vjk​为第jjj个隐藏层节点到第kkk个输出层节点的权重。

代码

for (size_t i = 0; i < HIDENODE; i++) {
        double sum = 0;
        for (size_t j = 0; j < OUTNODE; j++) {
            sum += -(idx.out[j] - outLayer[j]->value) *
                    outLayer[j]->value * (1.0 - outLayer[j]->value) *
                    hideLayer[i]->weight[j];
        }
        hideLayer[i]->bias_delta +=
                sum * hideLayer[i]->value * (1.0 - hideLayer[i]->value);
    }

3.4 计算Δwij\Delta w_{ij}Δwij​(输入层节点到隐藏层节点权重的修正值)

其计算公式如下:

Δwij=∂loss∂wij=η∑k(yk−yk^)yk^(1−yk^)vjkhj(1−hj)xi\Delta w_{ij} =\frac {\partial loss}{\partial w_{ij}}= \eta \sum_k ( y_k - \hat{y_k} ) \hat{y_k} ( 1 - \hat{y_k} ) v_{jk} h_j ( 1 - h_j ) x_i Δwij​=∂wij​∂loss​=ηk∑​(yk​−yk​^​)yk​^​(1−yk​^​)vjk​hj​(1−hj​)xi​

其中xix_ixi​为第iii个输入层节点的值。

代码

for (size_t i = 0; i < INNODE; i++) {
    for (size_t j = 0; j < HIDENODE; j++) {
        double sum = 0.f;
        for (size_t k = 0; k < OUTNODE; k++) {
            sum += (idx.out[k] - outLayer[k]->value) *
                    outLayer[k]->value * (1.0 - outLayer[k]->value) *
                    hideLayer[j]->weight[k];
        }
        inputLayer[i]->weight_delta[j] +=
                sum *
                hideLayer[j]->value * (1.0 - hideLayer[j]->value) *
                inputLayer[i]->value;
    }
}

4. 训练部分

4.1 初始化函数

  • 初始化三层神经网络:输入层(INNODE个神经元),隐藏层(HIDENODE个神经元),输出层(OUTNODE个神经元)。
  • 每个神经元的权重和偏置(bias)被初始化为随机值

4.2 读取训练数据

  • 使用getData函数从文件中读取训练数据(包含输入和输出)。

4.3 训练过程

  • 对于训练数据的每一个样本:
    • 正向传播 (forwardPropagation):输入数据被传递到输入层,然后经过隐藏层,并最终到达输出层。每一层的节点值根据权重和偏置计算,并通过激活函数处理。
    • 计算误差:使用均方误差,计算输出层的值与训练数据中的实际输出之间的差异。
    • 反向传播 (backwardPropagation):通过反向传播算法调整网络中每个神经元的权重和偏置的梯度(即delta)。
    • 更新权重和偏置 (revise):根据计算得到的梯度调整每个神经元的权重和偏置。

4.4 停止条件

  • 如果达到最大训练次数(mosttimes)或误差小于设定的阈值(threshold),则停止训练。

代码

// 训练
for (size_t times = 0; times < mosttimes; times++) {
    // 重置权重和偏置的delta
    reset_delta();
    // 计算误差,用于判断是否停止训练
    double error_max = 0.f;
    // 遍历训练数据
    for (auto &idx : train_data) {
        // 正向传播
        forwardPropagation(idx);
        // 计算误差
        double error = 0.f;
        for (size_t i = 0; i < OUTNODE; i++) {
            double tmp = fabs(outLayer[i]->value - idx.out[i]);
            error += tmp * tmp / 2;
        }
        error_max = max(error_max, error);
        // 反向传播
        backwardPropagation(idx);
    }
    //  如果误差小于阈值,停止训练
    if (error_max < threshold) {
        cout << "Success with " << times + 1 << " times training." << endl;
        cout << "Maximum error: " << error_max << endl;
        break;
    }
    // 更新权重
    revise(train_data);
}

5. 预测部分

5.1 读取测试数据

  • 使用getData函数从文件中读取测试数据(仅输入,无输出)。

5.2 预测过程

  • 对于测试数据的每一个样本:
    • 通过与训练类似的正向传播过程计算输出层的值。
    • 输出每个样本的输入和对应的预测输出。

代码

void predict(vector<Sample> test_data){
    for (auto &idx : test_data) {
        // 设置输入层的值
        for (size_t i = 0; i < INNODE; i++) {
            inputLayer[i]->value = idx.in[i];
        }
        // 计算隐藏层的值
        for (size_t j = 0; j < HIDENODE; j++) {
            double sum = 0;
            for (size_t i = 0; i < INNODE; i++) {
                sum += inputLayer[i]->value * inputLayer[i]->weight[j];
            }
            sum -= hideLayer[j]->bias;

            hideLayer[j]->value = utils::sigmoid(sum);
        }
        // 计算输出层的值
        for (size_t j = 0; j < OUTNODE; j++) {
            double sum = 0;
            for (size_t i = 0; i < HIDENODE; i++) {
                sum += hideLayer[i]->value * hideLayer[i]->weight[j];
            }
            sum -= outLayer[j]->bias;

            outLayer[j]->value = utils::sigmoid(sum);

            idx.out.push_back(outLayer[j]->value);
            // 输出结果
            for (auto &tmp : idx.in) {
                cout << tmp << " ";
            }
            for (auto &tmp : idx.out) {
                cout << tmp << " ";
            }
            cout << endl;
        }

    }
}

多隐藏层流程

每个隐藏层的输出都依赖于前一层的输出,误差梯度需要逐层反向传播。

1. 前向传播

前向传播是指从输入层到输出层的计算流程,具体过程如下:

  1. 输入层到第一个隐藏层: 输入层的节点直接与第一个隐藏层的节点相连。对于每个隐藏层节点,计算所有输入层节点的加权和,减去偏置,并通过激活函数(使用sigmoid函数)。
  2. 隐藏层之间的传播: 每个隐藏层的输出成为下一个隐藏层的输入。对于每个隐藏层节点,计算前一隐藏层所有节点的加权和,减去偏置,并通过激活函数。
  3. 最后一个隐藏层到输出层: 最后一个隐藏层的输出经过加权和和偏置调整后,通过激活函数形成输出层的值。

代码

void forwardPropagation(const Sample& sample) {
    // 设置输入层的值
    for (size_t i = 0; i < INNODE; ++i) {
        inputLayer[i]->value = sample.in[i];
    }

    // 逐层计算隐藏层的值
    for (size_t l = 0; l < hiddenLayers.size(); ++l) {
        for (size_t j = 0; j < hiddenLayers[l].size(); ++j) {
            double sum = 0;
            for (size_t i = 0; i < (l == 0 ? INNODE : hiddenLayers[l - 1].size()); ++i) {
                double inputVal = (l == 0) ? sample.in[i] : hiddenLayers[l - 1][i]->value;
                sum += inputVal * hiddenLayers[l][j]->weight[i];
            }
            sum -= hiddenLayers[l][j]->bias;
            hiddenLayers[l][j]->value = utils::sigmoid(sum);
        }
    }

    // 计算输出层的值
    for (size_t i = 0; i < OUTNODE; ++i) {
        double sum = 0;
        for (size_t j = 0; j < hiddenLayers.back().size(); ++j) {
            sum += hiddenLayers.back()[j]->value * outLayer[i]->weight[j];
        }
        sum += outLayer[i]->bias;
        outLayer[i]->value = utils::sigmoid(sum);
    }
}

2. 反向传播

反向传播用于计算误差并更新网络中的权重和偏置,具体过程如下

  1. 输出层误差和梯度计算: 首先计算输出层节点的误差(实际输出yyy与期望输出y^\hat{y}y^​之差)。然后,计算这些误差关于激活函数的导数。
  2. 隐藏层误差和梯度传播: 误差从输出层反向传播到每个隐藏层。对于每个隐藏层节点,计算其对应的误差,这通常是由后一层的误差和权重导出的。然后,计算误差关于激活函数的导数。
  3. 权重和偏置更新: 以学习率为标准,利用计算出的梯度更新每层的权重和偏置。

代码

void backwardPropagation(const Sample& sample) {
    // 计算输出层的误差...
    double error_sum = 0.0; // 初始化平方误差和
    // 计算输出层的误差
    for (size_t i = 0; i < OUTNODE; ++i) {
        double tmp = sample.out[i] - outLayer[i]->value;
        error_sum += tmp * tmp /2; // 累加平方误差
    }
    error_max = max(error_max, error_sum);
    

    // 首先,计算输出层的误差梯度和偏置变化
    vector<double> outputGradients(OUTNODE);
    for (size_t i = 0; i < OUTNODE; ++i) {
        double delta = sample.out[i] - outLayer[i]->value; // 误差
        outputGradients[i] = delta * outLayer[i]->value * (1 - outLayer[i]->value); // 梯度
    }

    // 反向传播到隐藏层
    for (int l = hiddenLayers.size() - 1; l >= 0; --l) {
        vector<double> hiddenGradients(hiddenLayers[l].size());
        for (size_t j = 0; j < hiddenLayers[l].size(); ++j) {
            double error = 0.0;
            if (l == hiddenLayers.size() - 1) {
                // 对于最后一个隐藏层,直接与输出层相关联
                for (size_t k = 0; k < OUTNODE; ++k) {
                    error += outputGradients[k] * hiddenLayers[l][j]->weight[k];
                }
            } else {
                // 对于其他隐藏层,与下一层相关联
                for (size_t k = 0; k < hiddenLayers[l + 1].size(); ++k) {
                    error += hiddenGradients[k] * hiddenLayers[l + 1][k]->weight[j];
                }
            }
            hiddenGradients[j] = error * hiddenLayers[l][j]->value * (1 - hiddenLayers[l][j]->value);
        }

        // 更新权重和偏置的变化量
        for (size_t i = 0; i < hiddenLayers[l].size(); ++i) {
            hiddenLayers[l][i]->bias_delta += hiddenGradients[i];
            for (size_t k = 0; k < (l == 0 ? INNODE : hiddenLayers[l - 1].size()); ++k) {
                double inputVal = (l == 0) ? sample.in[k] : hiddenLayers[l - 1][k]->value;
                hiddenLayers[l][i]->weight_delta[k] += hiddenGradients[i] * inputVal;
            }
        }
    }

    // 更新输出层权重和偏置的变化量
    for (size_t i = 0; i < OUTNODE; ++i) {
        outLayer[i]->bias_delta += outputGradients[i];
        for (size_t j = 0; j < hiddenLayers.back().size(); ++j) {
            outLayer[i]->weight_delta[j] += outputGradients[i] * hiddenLayers.back()[j]->value;
        }
    }
}

数字距离测试

实现功能:输入两个值,若两个值差的绝对值小于0.05,则输出1,否则输出0

初始化:输入层节点为2,隐藏层节点为4,输出层节点为1,学习率0.8,误差阈值10−410^{-4}10−4

train数据集样例,完整用例见traindata.txt

0 0 0
0 1 1
1 0 1
1 1 0
0.8 0.8 0
0.6 0.6 0
0.4 0.4 0
0.2 0.2 0
1.0 0.8 1
1.0 0.2 1
0.89 0.99 1
0.523 0.255 1
0.517 0.878 1
0.737 0.741 0
0.555 0.571 0
0.543 0.176 1
0.286 0.425 1
0.865 0.558 1
0.739 0.294 1
0.297 0.992 1
0.258 0.851 1
0.779 0.769 0
0.825 0.102 1
0.091 0.727 1
0.250 0.147 1
0.376 0.352 0
0.524 0.548 0
0.474 0.452 0
0.626 0.648 0

test数据集样例,完整用例见testdata.txt

0.111 0.112
0.001 0.999
0.123 0.345
0.123 0.456
0.123 0.789
0.234 0.567
0.234 0.678
0.387 0.401
0.616 0.717
0.701 0.919
0.701 0.720
0.212 0.313
0.212 0.222

实际输出

根据输出结果能看出,正确率为100%(前两个数字差的绝对值小于0.05,输出应该为0)在此简单分类的测试时,BP网络发挥很好
两次运行结果相似
0.111 0.112 0.00887687   
0.001 0.999 1 
0.123 0.345 1 
0.123 0.456 1 
0.123 0.789 1 
0.234 0.567 1 
0.234 0.678 1 
0.387 0.401 0.00927063   
0.616 0.717 0.999986     
0.701 0.919 1 
0.701 0.72 0.00891922    
0.212 0.313 0.999989     
0.212 0.222 0.0109173

西瓜数据集测试

从网上取得小量西瓜数据集,来源https://gist.github.com/zhenghaoz/abb86b21797b6274b0fcb1dfc96274b2,根据西瓜的各项指标来判断是否是好瓜,好瓜输出1,否则输出0

需要将输入层节点修改为8,隐藏层节点修改为10

编号,色泽,根蒂,敲声,纹理,脐部,触感,密度,含糖率,好瓜
1,青绿,蜷缩,浊响,清晰,凹陷,硬滑,0.697,0.460,是
2,乌黑,蜷缩,沉闷,清晰,凹陷,硬滑,0.774,0.376,是
3,乌黑,蜷缩,浊响,清晰,凹陷,硬滑,0.634,0.264,是
4,青绿,蜷缩,沉闷,清晰,凹陷,硬滑,0.608,0.318,是
5,浅白,蜷缩,浊响,清晰,凹陷,硬滑,0.556,0.215,是
6,青绿,稍蜷,浊响,清晰,稍凹,软粘,0.403,0.237,是
7,乌黑,稍蜷,浊响,稍糊,稍凹,软粘,0.481,0.149,是
8,乌黑,稍蜷,浊响,清晰,稍凹,硬滑,0.437,0.211,是
9,乌黑,稍蜷,沉闷,稍糊,稍凹,硬滑,0.666,0.091,否
10,青绿,硬挺,清脆,清晰,平坦,软粘,0.243,0.267,否
11,浅白,硬挺,清脆,模糊,平坦,硬滑,0.245,0.057,否
12,浅白,蜷缩,浊响,模糊,平坦,软粘,0.343,0.099,否
13,青绿,稍蜷,浊响,稍糊,凹陷,硬滑,0.639,0.161,否
14,浅白,稍蜷,沉闷,稍糊,凹陷,硬滑,0.657,0.198,否
15,乌黑,稍蜷,浊响,清晰,稍凹,软粘,0.360,0.370,否
16,浅白,蜷缩,浊响,模糊,平坦,硬滑,0.593,0.042,否
17,青绿,蜷缩,沉闷,稍糊,稍凹,硬滑,0.719,0.103,否

将上述数据简单转换,得到面的训练数据(traindata_g.txt)

1 1 1 1 1 1 0.697 0.460 1
2 1 2 1 1 1 0.774 0.376 1
2 1 1 1 1 1 0.634 0.264 1
1 1 2 1 1 1 0.608 0.318 1
3 1 1 1 1 1 0.556 0.215 1
1 2 1 1 2 2 0.403 0.237 1
2 2 1 2 2 2 0.481 0.149 1
2 2 1 1 2 1 0.437 0.211 1
2 2 2 2 2 1 0.666 0.091 0
1 3 3 1 3 2 0.243 0.267 0
3 3 3 3 3 1 0.245 0.057 0
3 1 1 3 3 2 0.343 0.099 0
1 2 1 2 1 1 0.639 0.161 0
3 2 2 2 1 1 0.657 0.198 0
2 2 1 1 2 2 0.360 0.370 0
3 1 1 3 3 1 0.593 0.042 0
1 1 2 2 2 1 0.719 0.103 0

下面是测试数据(testdata_g.txt)

3 1 2 2 2 1 0.227 0.160
3 1 2 2 3 1 0.474 0.176
3 2 3 2 3 2 0.634 0.264
1 3 2 3 3 1 0.408 0.318
1 1 3 2 3 2 0.356 0.215
1 2 3 3 2 2 0.203 0.137
2 3 3 2 2 3 0.281 0.149
2 3 3 3 2 3 0.237 0.111
2 3 3 3 2 1 0.666 0.091
1 1 1 1 1 2 0.443 0.367
1 1 2 1 1 1 0.445 0.357
3 1 1 1 1 1 0.543 0.499
1 2 1 1 1 1 0.639 0.361
3 1 1 2 1 1 0.657 0.398
2 1 1 1 3 1 0.461 0.371
1 2 1 1 1 1 0.593 0.442
1 1 2 1 1 1 0.719 0.303

实际输出

最后一项是输出,根据结果看出,输出正确率较高,接近100%
Success with 122614 times training.
Maximum error: 9.99991e-05
3 1 2 2 2 1 0.227 0.16 0.000902219 
3 1 2 2 3 1 0.474 0.176 0.00800002 
3 2 3 2 3 2 0.634 0.264 0.00187203 
1 3 2 3 3 1 0.408 0.318 0.000876587 
1 1 3 2 3 2 0.356 0.215 0.00235054 
1 2 3 3 2 2 0.203 0.137 0.000933375 
2 3 3 2 2 3 0.281 0.149 0.0143263 
2 3 3 3 2 3 0.237 0.111 0.000985916 
2 3 3 3 2 1 0.666 0.091 0.000914765 
//下面理论值为1,输出结果是最后一项
1 1 1 1 1 2 0.443 0.367 0.977018 
1 1 2 1 1 1 0.445 0.357 0.960767 
3 1 1 1 1 1 0.543 0.499 0.965584 
1 2 1 1 1 1 0.639 0.361 0.998939 
3 1 1 2 1 1 0.657 0.398 0.917984 
2 1 1 1 3 1 0.461 0.371 0.997688 
1 2 1 1 1 1 0.593 0.442 0.998693 
1 1 2 1 1 1 0.719 0.303 0.998758
PixPin_2023-12-28_14-02-15

完整代码

单隐藏层(main.cpp)

#include <iostream>
#include <cmath>
#include <vector>
#include <fstream>
#include <random>

// 输入层节点数
#define INNODE 2
// 隐含层节点数
#define HIDENODE 4
// 输出层节点数
#define OUTNODE 1

// 学习率
double rate = 0.8;
// 误差阈值
double threshold = 1e-4;
// 最大训练次数
size_t mosttimes = 1e6;

using namespace std;
// 样本包括输入和输出
struct Sample {
    vector<double> in, out;
};

// 节点包括值、偏置、权重和权重的delta
struct Node {
    double value{}, bias{}, bias_delta{};
    vector<double> weight, weight_delta;
};

namespace utils {
    // sigmoid函数
    inline double sigmoid(double x) {
        double res = 1.0 / (1.0 + exp(-x));
        return res;
    }
    // 读取文件数据
    vector<double> getFileData(string filename) {
        vector<double> res;

        ifstream in(filename);
        if (in.is_open()) {
            while (!in.eof()) {
                double buffer;
                in >> buffer;
                res.push_back(buffer);
            }
            in.close();
        } else {
            cout << "Error in reading " << filename << endl;
        }

        return res;
    }
    // 读取数据,includeOutData为true时,读取的数据包括输入和输出,否则只读取输入
    vector<Sample> getData(string filename, bool includeOutData) {
        vector<Sample> res;
        vector<double> buffer = getFileData(filename);

        size_t stepSize = includeOutData ? (INNODE + OUTNODE) : INNODE;

        for (size_t i = 0; i < buffer.size(); i += stepSize) {
            Sample tmp;
            for (size_t t = 0; t < INNODE; t++) {
                tmp.in.push_back(buffer[i + t]);
            }
            if (includeOutData) {
                for (size_t t = 0; t < OUTNODE; t++) {
                    tmp.out.push_back(buffer[i + INNODE + t]);
                }
            }
            res.push_back(tmp);
        }

        return res;
    }

}

Node *inputLayer[INNODE], *hideLayer[HIDENODE], *outLayer[OUTNODE];

inline void init() {
    mt19937 rd;
    rd.seed(random_device()());
    // 生成-1到1的随机数
    uniform_real_distribution<double> distribution(-1, 1);
    // 初始化权重和偏置
    for (size_t i = 0; i < INNODE; i++) {
        inputLayer[i] = new Node();
        for (size_t j = 0; j < HIDENODE; j++) {
            inputLayer[i]->weight.push_back(distribution(rd));
            inputLayer[i]->weight_delta.push_back(0.f);
        }
    }
    // 初始化隐藏层的权重和偏置
    for (size_t i = 0; i < HIDENODE; i++) {
        hideLayer[i] = new Node();
        hideLayer[i]->bias = distribution(rd);
        for (size_t j = 0; j < OUTNODE; j++) {
            hideLayer[i]->weight.push_back(distribution(rd));
            hideLayer[i]->weight_delta.push_back(0.f);
        }
    }
    // 初始化输出层的权重和偏置
    for (size_t i = 0; i < OUTNODE; i++) {
        outLayer[i] = new Node();
        outLayer[i]->bias = distribution(rd);
    }

}

// 重置权重和偏置的delta
inline void reset_delta() {
    
    for (size_t i = 0; i < INNODE; i++) {
        inputLayer[i]->weight_delta.assign(inputLayer[i]->weight_delta.size(), 0.f);
    }

    for (size_t i = 0; i < HIDENODE; i++) {
        hideLayer[i]->bias_delta = 0.f;
        hideLayer[i]->weight_delta.assign(hideLayer[i]->weight_delta.size(), 0.f);
    }

    for (size_t i = 0; i < OUTNODE; i++) {
        outLayer[i]->bias_delta = 0.f;
    }

}

void forwardPropagation(const Sample idx) {
    // 设置输入层的值
    for (size_t i = 0; i < INNODE; i++) {
        inputLayer[i]->value = idx.in[i];
    }

    // 计算隐藏层的值
    for (size_t j = 0; j < HIDENODE; j++) {
        double sum = 0;
        for (size_t i = 0; i < INNODE; i++) {
            sum += inputLayer[i]->value * inputLayer[i]->weight[j];
        }
        sum -= hideLayer[j]->bias;

        hideLayer[j]->value = utils::sigmoid(sum);
    }
    // 计算输出层的值
    for (size_t j = 0; j < OUTNODE; j++) {
        double sum = 0;
        for (size_t i = 0; i < HIDENODE; i++) {
            sum += hideLayer[i]->value * hideLayer[i]->weight[j];
        }
        sum -= outLayer[j]->bias;

        outLayer[j]->value = utils::sigmoid(sum);
    }
}

void backwardPropagation(const Sample idx){
    // 反向传播
    // 计算输出层的delta_bias,对应\labmda
    for (size_t i = 0; i < OUTNODE; i++) {
        double bias_delta = -(idx.out[i] - outLayer[i]->value) *
                outLayer[i]->value * (1.0 - outLayer[i]->value);
        outLayer[i]->bias_delta += bias_delta;
    }
    // 计算隐藏层的delta_weight,对应v
    for (size_t i = 0; i < HIDENODE; i++) {
        for (size_t j = 0; j < OUTNODE; j++) {
            double weight_delta = (idx.out[j] - outLayer[j]->value) *
                    outLayer[j]->value * (1.0 - outLayer[j]->value) *
                    hideLayer[i]->value;
            hideLayer[i]->weight_delta[j] += weight_delta;
        }
    }
    // 计算隐藏层的delta_bias,对应\beta
    for (size_t i = 0; i < HIDENODE; i++) {
        double sum = 0;
        for (size_t j = 0; j < OUTNODE; j++) {
            sum += -(idx.out[j] - outLayer[j]->value) *
                    outLayer[j]->value * (1.0 - outLayer[j]->value) *
                    hideLayer[i]->weight[j];
        }
        hideLayer[i]->bias_delta +=
                sum * hideLayer[i]->value * (1.0 - hideLayer[i]->value);
    }
    // 计算输入层的delta_weight,对应w
    for (size_t i = 0; i < INNODE; i++) {
        for (size_t j = 0; j < HIDENODE; j++) {
            double sum = 0.f;
            for (size_t k = 0; k < OUTNODE; k++) {
                sum += (idx.out[k] - outLayer[k]->value) *
                        outLayer[k]->value * (1.0 - outLayer[k]->value) *
                        hideLayer[j]->weight[k];
            }
            inputLayer[i]->weight_delta[j] +=
                    sum *
                    hideLayer[j]->value * (1.0 - hideLayer[j]->value) *
                    inputLayer[i]->value;
        }
    }
}

void revise(vector<Sample> train_data){
    // 更新权重
    auto train_data_size = double(train_data.size());
    // 更新输入层的权重
    for (size_t i = 0; i < INNODE; i++) {
        for (size_t j = 0; j < HIDENODE; j++) {
            inputLayer[i]->weight[j] +=
                    rate * inputLayer[i]->weight_delta[j] / train_data_size;
        }
    }
    // 更新隐藏层的权重
    for (size_t i = 0; i < HIDENODE; i++) {
        hideLayer[i]->bias +=
                rate * hideLayer[i]->bias_delta / train_data_size;
        for (size_t j = 0; j < OUTNODE; j++) {
            hideLayer[i]->weight[j] +=
                    rate * hideLayer[i]->weight_delta[j] / train_data_size;
        }
    }
    // 更新输出层的权重
    for (size_t i = 0; i < OUTNODE; i++) {
        outLayer[i]->bias +=
                rate * outLayer[i]->bias_delta / train_data_size;
    }
}

void predict(vector<Sample> test_data){
    for (auto &idx : test_data) {
        // 设置输入层的值
        for (size_t i = 0; i < INNODE; i++) {
            inputLayer[i]->value = idx.in[i];
        }
        // 计算隐藏层的值
        for (size_t j = 0; j < HIDENODE; j++) {
            double sum = 0;
            for (size_t i = 0; i < INNODE; i++) {
                sum += inputLayer[i]->value * inputLayer[i]->weight[j];
            }
            sum -= hideLayer[j]->bias;

            hideLayer[j]->value = utils::sigmoid(sum);
        }
        // 计算输出层的值
        for (size_t j = 0; j < OUTNODE; j++) {
            double sum = 0;
            for (size_t i = 0; i < HIDENODE; i++) {
                sum += hideLayer[i]->value * hideLayer[i]->weight[j];
            }
            sum -= outLayer[j]->bias;

            outLayer[j]->value = utils::sigmoid(sum);

            idx.out.push_back(outLayer[j]->value);
            // 输出结果
            for (auto &tmp : idx.in) {
                cout << tmp << " ";
            }
            for (auto &tmp : idx.out) {
                cout << tmp << " ";
            }
            cout << endl;
        }

    }
}

int main(int argc, char *argv[]) {
    // 初始化
    init();
    // 读取训练数据
    vector<Sample> train_data = utils::getData("traindata.txt",1);
    // 训练
    for (size_t times = 0; times < mosttimes; times++) {
        // 重置权重和偏置的delta
        reset_delta();
        // 计算误差,用于判断是否停止训练
        double error_max = 0.f;
        // 遍历训练数据
        for (auto &idx : train_data) {
            // 正向传播
            forwardPropagation(idx);
            // 计算误差
            double error = 0.f;
            for (size_t i = 0; i < OUTNODE; i++) {
                double tmp = fabs(outLayer[i]->value - idx.out[i]);
                error += tmp * tmp / 2;
            }
            error_max = max(error_max, error);
            // 反向传播
            backwardPropagation(idx);
        }
        //  如果误差小于阈值,停止训练
        if (error_max < threshold) {
            cout << "Success with " << times + 1 << " times training." << endl;
            cout << "Maximum error: " << error_max << endl;
            break;
        }
        // 更新权重
        revise(train_data);
    }
    // 测试
    vector<Sample> test_data = utils::getData("testdata.txt",0);
    predict(test_data);
    return 0;
}

多隐藏层(multi_hidden_layers_version.cpp)

#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>
#include <random>

using namespace std;

#define INNODE 2
#define OUTNODE 1

double rate = 0.8;
double threshold = 1e-4;
size_t mosttimes = 5e5;
int numberOfHiddenLayers = 1; // 示例:2个隐藏层
vector<int> hiddenLayerSizes = {4}; // 示例:第一个隐藏层有4个节点,第二个有3个节点
double error_max = 0.f; // 初始化最大误差
struct Sample {
    vector<double> in, out;
};

struct Node {
    double value{}, bias{}, bias_delta{};
    vector<double> weight, weight_delta;
};


Node *inputLayer[INNODE], *outLayer[OUTNODE];
vector<vector<Node*>> hiddenLayers;

namespace utils {

    inline double sigmoid(double x) {
        double res = 1.0 / (1.0 + exp(-x));
        return res;
    }

    vector<double> getFileData(string filename) {
        vector<double> res;

        ifstream in(filename);
        if (in.is_open()) {
            while (!in.eof()) {
                double buffer;
                in >> buffer;
                res.push_back(buffer);
            }
            in.close();
        } else {
            cout << "Error in reading " << filename << endl;
        }

        return res;
    }

    // 读取数据,includeOutData为true时,读取的数据包括输入和输出,否则只读取输入
    vector<Sample> getData(string filename, bool includeOutData) {
        vector<Sample> res;
        vector<double> buffer = getFileData(filename);

        size_t stepSize = includeOutData ? (INNODE + OUTNODE) : INNODE;

        for (size_t i = 0; i < buffer.size(); i += stepSize) {
            Sample tmp;
            for (size_t t = 0; t < INNODE; t++) {
                tmp.in.push_back(buffer[i + t]);
            }
            if (includeOutData) {
                for (size_t t = 0; t < OUTNODE; t++) {
                    tmp.out.push_back(buffer[i + INNODE + t]);
                }
            }
            res.push_back(tmp);
        }

        return res;
    }

}



inline void init() {
    mt19937 rd;
    rd.seed(random_device()());
    uniform_real_distribution<double> distribution(-1, 1);

    // 初始化输入层
    for (int i = 0; i < INNODE; ++i) {
        Node* node = new Node();
        for (int j = 0; j < hiddenLayerSizes[0]; ++j) {
            node->weight.push_back(distribution(rd)); // 随机初始权重
            node->weight_delta.push_back(0.0); // 初始权重变化量设置为0
        }
        inputLayer[i] = node;
    }
    // 初始化隐藏层
    for (int i = 0; i < numberOfHiddenLayers; ++i) {
        vector<Node*> layer;
        int nodeCount = hiddenLayerSizes[i];
        int prevLayerSize = (i == 0) ? INNODE : hiddenLayerSizes[i - 1]; // 前一层的节点数
        for (int j = 0; j < nodeCount; ++j) {
            Node* node = new Node();
            node->bias = distribution(rd);// 随机初始偏置
            for (int k = 0; k < prevLayerSize; ++k) {
                node->weight.push_back(distribution(rd)); // 对应前一层的每个节点
                node->weight_delta.push_back(0.0);
            }
            layer.push_back(node);
        }
        hiddenLayers.push_back(layer);
    }

    // 初始化输出层
    for (int i = 0; i < OUTNODE; ++i) {
        Node* node = new Node();
        node->bias = distribution(rd); // 随机初始偏置
        for (int j = 0; j < hiddenLayerSizes.back(); ++j) {
            node->weight.push_back(distribution(rd)); // 随机初始权重
            node->weight_delta.push_back(0.0); // 初始权重变化量设置为0
        }
        outLayer[i] = node;
    }
}

void forwardPropagation(const Sample& sample) {
    // 设置输入层的值
    for (size_t i = 0; i < INNODE; ++i) {
        inputLayer[i]->value = sample.in[i];
    }

    // 逐层计算隐藏层的值
    for (size_t l = 0; l < hiddenLayers.size(); ++l) {
        for (size_t j = 0; j < hiddenLayers[l].size(); ++j) {
            double sum = 0;
            for (size_t i = 0; i < (l == 0 ? INNODE : hiddenLayers[l - 1].size()); ++i) {
                double inputVal = (l == 0) ? sample.in[i] : hiddenLayers[l - 1][i]->value;
                sum += inputVal * hiddenLayers[l][j]->weight[i];
            }
            sum -= hiddenLayers[l][j]->bias;
            hiddenLayers[l][j]->value = utils::sigmoid(sum);
        }
    }

    // 计算输出层的值
    for (size_t i = 0; i < OUTNODE; ++i) {
        double sum = 0;
        for (size_t j = 0; j < hiddenLayers.back().size(); ++j) {
            sum += hiddenLayers.back()[j]->value * outLayer[i]->weight[j];
        }
        sum += outLayer[i]->bias;
        outLayer[i]->value = utils::sigmoid(sum);
    }
}


void backwardPropagation(const Sample& sample) {
    // 计算输出层的误差...
    double error_sum = 0.0; // 初始化平方误差和
    // 计算输出层的误差
    for (size_t i = 0; i < OUTNODE; ++i) {
        double tmp = sample.out[i] - outLayer[i]->value;
        error_sum += tmp * tmp /2; // 累加平方误差
    }
    error_max = max(error_max, error_sum);
    

    // 首先,计算输出层的误差梯度和偏置变化
    vector<double> outputGradients(OUTNODE);
    for (size_t i = 0; i < OUTNODE; ++i) {
        double delta = sample.out[i] - outLayer[i]->value; // 误差
        outputGradients[i] = delta * outLayer[i]->value * (1 - outLayer[i]->value); // 梯度
    }

    // 反向传播到隐藏层
    for (int l = hiddenLayers.size() - 1; l >= 0; --l) {
        vector<double> hiddenGradients(hiddenLayers[l].size());
        for (size_t j = 0; j < hiddenLayers[l].size(); ++j) {
            double error = 0.0;
            if (l == hiddenLayers.size() - 1) {
                // 对于最后一个隐藏层,直接与输出层相关联
                for (size_t k = 0; k < OUTNODE; ++k) {
                    error += outputGradients[k] * hiddenLayers[l][j]->weight[k];
                }
            } else {
                // 对于其他隐藏层,与下一层相关联
                for (size_t k = 0; k < hiddenLayers[l + 1].size(); ++k) {
                    error += hiddenGradients[k] * hiddenLayers[l + 1][k]->weight[j];
                }
            }
            hiddenGradients[j] = error * hiddenLayers[l][j]->value * (1 - hiddenLayers[l][j]->value);
        }

        // 更新权重和偏置的变化量
        for (size_t i = 0; i < hiddenLayers[l].size(); ++i) {
            hiddenLayers[l][i]->bias_delta += hiddenGradients[i];
            for (size_t k = 0; k < (l == 0 ? INNODE : hiddenLayers[l - 1].size()); ++k) {
                double inputVal = (l == 0) ? sample.in[k] : hiddenLayers[l - 1][k]->value;
                hiddenLayers[l][i]->weight_delta[k] += hiddenGradients[i] * inputVal;
            }
        }
    }

    // 更新输出层权重和偏置的变化量
    for (size_t i = 0; i < OUTNODE; ++i) {
        outLayer[i]->bias_delta += outputGradients[i];
        for (size_t j = 0; j < hiddenLayers.back().size(); ++j) {
            outLayer[i]->weight_delta[j] += outputGradients[i] * hiddenLayers.back()[j]->value;
        }
    }
}


inline void updateWeightsAndBiases(const vector<Sample>& data) {
    // 使用数据集的大小来计算平均
    double train_data_size = static_cast<double>(data.size());

    // 更新输入层到第一个隐藏层的权重
    for (size_t i = 0; i < INNODE; ++i) {
        for (size_t j = 0; j < hiddenLayerSizes[0]; ++j) {
            inputLayer[i]->weight[j] -= rate * inputLayer[i]->weight_delta[j] / train_data_size;
            inputLayer[i]->weight_delta[j] = 0; // 重置权重变化量
        }
    }

    // 更新隐藏层的权重和偏置
    for (size_t l = 0; l < hiddenLayers.size(); ++l) {
        for (size_t i = 0; i < hiddenLayers[l].size(); ++i) {
            hiddenLayers[l][i]->bias -= rate * hiddenLayers[l][i]->bias_delta / train_data_size;
            hiddenLayers[l][i]->bias_delta = 0; // 重置偏置变化量

            for (size_t j = 0; j < hiddenLayers[l][i]->weight.size(); ++j) {
                hiddenLayers[l][i]->weight[j] -= rate * hiddenLayers[l][i]->weight_delta[j] / train_data_size;
                hiddenLayers[l][i]->weight_delta[j] = 0; // 重置权重变化量
            }
        }
    }

    // 更新输出层的权重和偏置
    for (size_t i = 0; i < OUTNODE; ++i) {
        outLayer[i]->bias -= rate * outLayer[i]->bias_delta / train_data_size;
        outLayer[i]->bias_delta = 0; // 重置偏置变化量

        for (size_t j = 0; j < hiddenLayers.back().size(); ++j) {
            outLayer[i]->weight[j] -= rate * outLayer[i]->weight_delta[j] / train_data_size;
            outLayer[i]->weight_delta[j] = 0; // 重置权重变化量
        }
    }
}


inline void reset_delta() {
    for (size_t i = 0; i < INNODE; i++) {
        inputLayer[i]->weight_delta.assign(::inputLayer[i]->weight_delta.size(), 0.f);
    }

    for (size_t l = 0; l < hiddenLayers.size(); ++l) {
        for (size_t i = 0; i < hiddenLayers[l].size(); ++i) {
            hiddenLayers[l][i]->bias_delta = 0; // 重置偏置变化量
            for (size_t j = 0; j < hiddenLayers[l][i]->weight.size(); ++j) {
                hiddenLayers[l][i]->weight_delta[j] = 0; // 重置权重变化量
            }
        }
    }

    for (size_t i = 0; i < OUTNODE; i++) {
        outLayer[i]->bias_delta = 0.f;
    }

}

int main() {
    // 初始化神经网络
    init();
    cout<<"init finished"<<endl;
    // 加载训练数据
    vector<Sample> train_data = utils::getData("traindata.txt",1);
    cout<<"load data finished"<<endl;
    // 训练过程
    size_t epoch;

    for (epoch = 0; epoch < mosttimes; ++epoch) {
        error_max=0.0;
        for (const auto& sample : train_data) {
            // 重置权重和偏置的delta
            reset_delta();
            // 正向传播
            forwardPropagation(sample);
            // 反向传播,并获取最大误差
            backwardPropagation(sample);
            // 更新权重和偏置
            updateWeightsAndBiases(train_data);

        }
        // 检查是否应该提前停止
        if (error_max < threshold && epoch > 100) {
            cout << "Success with " << epoch + 1 << " times training." << endl;
            cout << "Maximum error: " << error_max << endl;
            break;
        }
    }

    if (epoch == mosttimes) {
        cout << "Reached maximum iterations. " << endl;
        cout << "Maximum error: " << error_max << endl;
    }
    // 加载测试数据
    vector<Sample> test_data = utils::getData("testdata.txt",0);

    // 测试过程
    for (const auto& sample : test_data) {
        // 对每个测试样本进行正向传播以获得预测结果
        forwardPropagation({sample}); // 注意:这里我们将单个样本包装成一个向量

        // 输出输入值和预测结果
        for (auto val : sample.in) {
            cout << val << " ";
        }
        for (size_t i = 0; i < OUTNODE; ++i) {
            cout << outLayer[i]->value << " ";
        }
        cout << endl;
    }

    return 0;
}