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 )

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

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

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
//计算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^\hat{y_k}为第kk个输出层节点的值(预测值),hjh_j为第jj个隐藏层节点的值,vjkv_{jk}为第jj个隐藏层节点到第kk个输出层节点的权重,λk\lambda_k为第kk个输出层节点的偏置值,σ(x)\sigma(x)为激活函数。

代码

1
2
3
4
5
6
7
8
9
10
11
12
//计算\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=12k(ykyk^)2Loss = \frac{1}{2}\sum_k ( y_k - \hat{y_k} )^2

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

代码

1
2
3
4
5
6
7
8
// 计算误差
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+ex\sigma(x) = \frac{1}{1+e^{-x}}

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

其计算公式如下:

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

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

代码

1
2
3
4
5
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=lossv=η(ykyk^)yk^(1yk^)hj\Delta v_{jk} =\frac {\partial loss}{\partial v}= \eta ( y_k - \hat{y_k} ) \hat{y_k} ( 1 - \hat{y_k} ) h_j

其中hjh_j为第jj个隐藏层节点的值。

代码

1
2
3
4
5
6
7
8
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=lossβ=ηk(ykyk^)yk^(1yk^)vjkhj(1hj)\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 )

其中vjkv_{jk}为第jj个隐藏层节点到第kk个输出层节点的权重。

代码

1
2
3
4
5
6
7
8
9
10
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=losswij=ηk(ykyk^)yk^(1yk^)vjkhj(1hj)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

其中xix_i为第ii个输入层节点的值。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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),则停止训练。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 训练
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 预测过程

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

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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. 最后一个隐藏层到输出层: 最后一个隐藏层的输出经过加权和和偏置调整后,通过激活函数形成输出层的值。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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. 输出层误差和梯度计算: 首先计算输出层节点的误差(实际输出yy与期望输出y^\hat{y}之差)。然后,计算这些误差关于激活函数的导数。
  2. 隐藏层误差和梯度传播: 误差从输出层反向传播到每个隐藏层。对于每个隐藏层节点,计算其对应的误差,这通常是由后一层的误差和权重导出的。然后,计算误差关于激活函数的导数。
  3. 权重和偏置更新: 以学习率为标准,利用计算出的梯度更新每层的权重和偏置。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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,误差阈值10410^{-4}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

1
2
3
4
5
6
7
8
9
10
11
12
13
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网络发挥很好

两次运行结果相似

1
2
3
4
5
6
7
8
9
10
11
12
13
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
编号,色泽,根蒂,敲声,纹理,脐部,触感,密度,含糖率,好瓜
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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%

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#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;
}