├── .gitattributes ├── iris.csv ├── bp_iris.py └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-language=python 2 | -------------------------------------------------------------------------------- /iris.csv: -------------------------------------------------------------------------------- 1 | 5.1,3.5,1.4,0.2,Iris-setosa 2 | 4.9,3.0,1.4,0.2,Iris-setosa 3 | 4.7,3.2,1.3,0.2,Iris-setosa 4 | 4.6,3.1,1.5,0.2,Iris-setosa 5 | 5.0,3.6,1.4,0.2,Iris-setosa 6 | 5.4,3.9,1.7,0.4,Iris-setosa 7 | 4.6,3.4,1.4,0.3,Iris-setosa 8 | 5.0,3.4,1.5,0.2,Iris-setosa 9 | 4.4,2.9,1.4,0.2,Iris-setosa 10 | 4.9,3.1,1.5,0.1,Iris-setosa 11 | 5.4,3.7,1.5,0.2,Iris-setosa 12 | 4.8,3.4,1.6,0.2,Iris-setosa 13 | 4.8,3.0,1.4,0.1,Iris-setosa 14 | 4.3,3.0,1.1,0.1,Iris-setosa 15 | 5.8,4.0,1.2,0.2,Iris-setosa 16 | 5.7,4.4,1.5,0.4,Iris-setosa 17 | 5.4,3.9,1.3,0.4,Iris-setosa 18 | 5.1,3.5,1.4,0.3,Iris-setosa 19 | 5.7,3.8,1.7,0.3,Iris-setosa 20 | 5.1,3.8,1.5,0.3,Iris-setosa 21 | 5.4,3.4,1.7,0.2,Iris-setosa 22 | 5.1,3.7,1.5,0.4,Iris-setosa 23 | 4.6,3.6,1.0,0.2,Iris-setosa 24 | 5.1,3.3,1.7,0.5,Iris-setosa 25 | 4.8,3.4,1.9,0.2,Iris-setosa 26 | 5.0,3.0,1.6,0.2,Iris-setosa 27 | 5.0,3.4,1.6,0.4,Iris-setosa 28 | 5.2,3.5,1.5,0.2,Iris-setosa 29 | 5.2,3.4,1.4,0.2,Iris-setosa 30 | 4.7,3.2,1.6,0.2,Iris-setosa 31 | 4.8,3.1,1.6,0.2,Iris-setosa 32 | 5.4,3.4,1.5,0.4,Iris-setosa 33 | 5.2,4.1,1.5,0.1,Iris-setosa 34 | 5.5,4.2,1.4,0.2,Iris-setosa 35 | 4.9,3.1,1.5,0.1,Iris-setosa 36 | 5.0,3.2,1.2,0.2,Iris-setosa 37 | 5.5,3.5,1.3,0.2,Iris-setosa 38 | 4.9,3.1,1.5,0.1,Iris-setosa 39 | 4.4,3.0,1.3,0.2,Iris-setosa 40 | 5.1,3.4,1.5,0.2,Iris-setosa 41 | 5.0,3.5,1.3,0.3,Iris-setosa 42 | 4.5,2.3,1.3,0.3,Iris-setosa 43 | 4.4,3.2,1.3,0.2,Iris-setosa 44 | 5.0,3.5,1.6,0.6,Iris-setosa 45 | 5.1,3.8,1.9,0.4,Iris-setosa 46 | 4.8,3.0,1.4,0.3,Iris-setosa 47 | 5.1,3.8,1.6,0.2,Iris-setosa 48 | 4.6,3.2,1.4,0.2,Iris-setosa 49 | 5.3,3.7,1.5,0.2,Iris-setosa 50 | 5.0,3.3,1.4,0.2,Iris-setosa 51 | 7.0,3.2,4.7,1.4,Iris-versicolor 52 | 6.4,3.2,4.5,1.5,Iris-versicolor 53 | 6.9,3.1,4.9,1.5,Iris-versicolor 54 | 5.5,2.3,4.0,1.3,Iris-versicolor 55 | 6.5,2.8,4.6,1.5,Iris-versicolor 56 | 5.7,2.8,4.5,1.3,Iris-versicolor 57 | 6.3,3.3,4.7,1.6,Iris-versicolor 58 | 4.9,2.4,3.3,1.0,Iris-versicolor 59 | 6.6,2.9,4.6,1.3,Iris-versicolor 60 | 5.2,2.7,3.9,1.4,Iris-versicolor 61 | 5.0,2.0,3.5,1.0,Iris-versicolor 62 | 5.9,3.0,4.2,1.5,Iris-versicolor 63 | 6.0,2.2,4.0,1.0,Iris-versicolor 64 | 6.1,2.9,4.7,1.4,Iris-versicolor 65 | 5.6,2.9,3.6,1.3,Iris-versicolor 66 | 6.7,3.1,4.4,1.4,Iris-versicolor 67 | 5.6,3.0,4.5,1.5,Iris-versicolor 68 | 5.8,2.7,4.1,1.0,Iris-versicolor 69 | 6.2,2.2,4.5,1.5,Iris-versicolor 70 | 5.6,2.5,3.9,1.1,Iris-versicolor 71 | 5.9,3.2,4.8,1.8,Iris-versicolor 72 | 6.1,2.8,4.0,1.3,Iris-versicolor 73 | 6.3,2.5,4.9,1.5,Iris-versicolor 74 | 6.1,2.8,4.7,1.2,Iris-versicolor 75 | 6.4,2.9,4.3,1.3,Iris-versicolor 76 | 6.6,3.0,4.4,1.4,Iris-versicolor 77 | 6.8,2.8,4.8,1.4,Iris-versicolor 78 | 6.7,3.0,5.0,1.7,Iris-versicolor 79 | 6.0,2.9,4.5,1.5,Iris-versicolor 80 | 5.7,2.6,3.5,1.0,Iris-versicolor 81 | 5.5,2.4,3.8,1.1,Iris-versicolor 82 | 5.5,2.4,3.7,1.0,Iris-versicolor 83 | 5.8,2.7,3.9,1.2,Iris-versicolor 84 | 6.0,2.7,5.1,1.6,Iris-versicolor 85 | 5.4,3.0,4.5,1.5,Iris-versicolor 86 | 6.0,3.4,4.5,1.6,Iris-versicolor 87 | 6.7,3.1,4.7,1.5,Iris-versicolor 88 | 6.3,2.3,4.4,1.3,Iris-versicolor 89 | 5.6,3.0,4.1,1.3,Iris-versicolor 90 | 5.5,2.5,4.0,1.3,Iris-versicolor 91 | 5.5,2.6,4.4,1.2,Iris-versicolor 92 | 6.1,3.0,4.6,1.4,Iris-versicolor 93 | 5.8,2.6,4.0,1.2,Iris-versicolor 94 | 5.0,2.3,3.3,1.0,Iris-versicolor 95 | 5.6,2.7,4.2,1.3,Iris-versicolor 96 | 5.7,3.0,4.2,1.2,Iris-versicolor 97 | 5.7,2.9,4.2,1.3,Iris-versicolor 98 | 6.2,2.9,4.3,1.3,Iris-versicolor 99 | 5.1,2.5,3.0,1.1,Iris-versicolor 100 | 5.7,2.8,4.1,1.3,Iris-versicolor 101 | 6.3,3.3,6.0,2.5,Iris-virginica 102 | 5.8,2.7,5.1,1.9,Iris-virginica 103 | 7.1,3.0,5.9,2.1,Iris-virginica 104 | 6.3,2.9,5.6,1.8,Iris-virginica 105 | 6.5,3.0,5.8,2.2,Iris-virginica 106 | 7.6,3.0,6.6,2.1,Iris-virginica 107 | 4.9,2.5,4.5,1.7,Iris-virginica 108 | 7.3,2.9,6.3,1.8,Iris-virginica 109 | 6.7,2.5,5.8,1.8,Iris-virginica 110 | 7.2,3.6,6.1,2.5,Iris-virginica 111 | 6.5,3.2,5.1,2.0,Iris-virginica 112 | 6.4,2.7,5.3,1.9,Iris-virginica 113 | 6.8,3.0,5.5,2.1,Iris-virginica 114 | 5.7,2.5,5.0,2.0,Iris-virginica 115 | 5.8,2.8,5.1,2.4,Iris-virginica 116 | 6.4,3.2,5.3,2.3,Iris-virginica 117 | 6.5,3.0,5.5,1.8,Iris-virginica 118 | 7.7,3.8,6.7,2.2,Iris-virginica 119 | 7.7,2.6,6.9,2.3,Iris-virginica 120 | 6.0,2.2,5.0,1.5,Iris-virginica 121 | 6.9,3.2,5.7,2.3,Iris-virginica 122 | 5.6,2.8,4.9,2.0,Iris-virginica 123 | 7.7,2.8,6.7,2.0,Iris-virginica 124 | 6.3,2.7,4.9,1.8,Iris-virginica 125 | 6.7,3.3,5.7,2.1,Iris-virginica 126 | 7.2,3.2,6.0,1.8,Iris-virginica 127 | 6.2,2.8,4.8,1.8,Iris-virginica 128 | 6.1,3.0,4.9,1.8,Iris-virginica 129 | 6.4,2.8,5.6,2.1,Iris-virginica 130 | 7.2,3.0,5.8,1.6,Iris-virginica 131 | 7.4,2.8,6.1,1.9,Iris-virginica 132 | 7.9,3.8,6.4,2.0,Iris-virginica 133 | 6.4,2.8,5.6,2.2,Iris-virginica 134 | 6.3,2.8,5.1,1.5,Iris-virginica 135 | 6.1,2.6,5.6,1.4,Iris-virginica 136 | 7.7,3.0,6.1,2.3,Iris-virginica 137 | 6.3,3.4,5.6,2.4,Iris-virginica 138 | 6.4,3.1,5.5,1.8,Iris-virginica 139 | 6.0,3.0,4.8,1.8,Iris-virginica 140 | 6.9,3.1,5.4,2.1,Iris-virginica 141 | 6.7,3.1,5.6,2.4,Iris-virginica 142 | 6.9,3.1,5.1,2.3,Iris-virginica 143 | 5.8,2.7,5.1,1.9,Iris-virginica 144 | 6.8,3.2,5.9,2.3,Iris-virginica 145 | 6.7,3.3,5.7,2.5,Iris-virginica 146 | 6.7,3.0,5.2,2.3,Iris-virginica 147 | 6.3,2.5,5.0,1.9,Iris-virginica 148 | 6.5,3.0,5.2,2.0,Iris-virginica 149 | 6.2,3.4,5.4,2.3,Iris-virginica 150 | 5.9,3.0,5.1,1.8,Iris-virginica -------------------------------------------------------------------------------- /bp_iris.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | @author: lqhou 5 | @file: bp_iris.py 6 | @time: 2019/06/21 7 | """ 8 | 9 | from csv import reader 10 | import numpy as np 11 | from sklearn.preprocessing import MinMaxScaler 12 | import random 13 | import matplotlib.pyplot as plt 14 | import math 15 | 16 | 17 | def load_dataset(dataset_path, n_train_data): 18 | """加载数据集,对数据进行预处理,并划分训练集和验证集 19 | :param dataset_path: 数据集文件路径 20 | :param n_train_data: 训练集的数据量 21 | :return: 划分好的训练集和验证集 22 | """ 23 | dataset = [] 24 | label_dict = {'Iris-setosa': 0, 'Iris-versicolor': 1, 'Iris-virginica': 2} 25 | with open(dataset_path, 'r') as file: 26 | # 读取CSV文件,以逗号为分隔符 27 | csv_reader = reader(file, delimiter=',') 28 | for row in csv_reader: 29 | # 将字符串类型的特征值转换为浮点型 30 | row[0:4] = list(map(float, row[0:4])) 31 | # 将标签替换为整型 32 | row[4] = label_dict[row[4]] 33 | # 将处理好的数据加入数据集中 34 | dataset.append(row) 35 | 36 | # 对数据进行归一化处理 37 | dataset = np.array(dataset) 38 | mms = MinMaxScaler() 39 | for i in range(dataset.shape[1] - 1): 40 | dataset[:, i] = mms.fit_transform(dataset[:, i].reshape(-1, 1)).flatten() 41 | 42 | # 将类标转为整型 43 | dataset = dataset.tolist() 44 | for row in dataset: 45 | row[4] = int(row[4]) 46 | # 打乱数据集 47 | random.shuffle(dataset) 48 | 49 | # 划分训练集和验证集 50 | train_data = dataset[0:n_train_data] 51 | val_data = dataset[n_train_data:] 52 | 53 | return train_data, val_data 54 | 55 | 56 | def fun_z(weights, inputs): 57 | """计算神经元的输入:z = weight * inputs + b 58 | :param weights: 网络参数(权重矩阵和偏置项) 59 | :param inputs: 上一层神经元的输出 60 | :return: 当前层神经元的输入 61 | """ 62 | bias_term = weights[-1] 63 | z = 0 64 | for i in range(len(weights)-1): 65 | z += weights[i] * inputs[i] 66 | z += bias_term 67 | return z 68 | 69 | 70 | def sigmoid(z): 71 | """激活函数(Sigmoid):f(z) = Sigmoid(z) 72 | :param z: 神经元的输入 73 | :return: 神经元的输出 74 | """ 75 | return 1.0 / (1.0 + math.exp(-z)) 76 | 77 | 78 | def sigmoid_derivative(output): 79 | """Sigmoid激活函数求导 80 | :param output: 激活函数的输出值 81 | :return: 求导计算结果 82 | """ 83 | return output * (1.0 - output) 84 | 85 | 86 | def forward_propagate(network, inputs): 87 | """前向传播计算 88 | :param network: 神经网络 89 | :param inputs: 一个样本数据 90 | :return: 前向传播计算的结果 91 | """ 92 | for layer in network: # 循环计算每一层 93 | new_inputs = [] 94 | for neuron in layer: # 循环计算每一层的每一个神经元 95 | z = fun_z(neuron['weights'], inputs) 96 | neuron['output'] = sigmoid(z) 97 | new_inputs.append(neuron['output']) 98 | inputs = new_inputs 99 | return inputs 100 | 101 | 102 | def backward_propagate_error(network, actual_label): 103 | """误差进行反向传播 104 | :param network: 神经网络 105 | :param actual_label: 真实的标签值 106 | :return: 107 | """ 108 | for i in reversed(range(len(network))): # 从最后一层开始计算误差 109 | layer = network[i] 110 | errors = list() 111 | if i != len(network)-1: # 不是输出层 112 | for j in range(len(layer)): # 计算每一个神经元的误差 113 | error = 0.0 114 | for neuron in network[i + 1]: 115 | error += (neuron['weights'][j] * neuron['delta']) 116 | errors.append(error) 117 | else: # 输出层 118 | for j in range(len(layer)): # 计算每一个神经元的误差 119 | neuron = layer[j] 120 | errors.append(actual_label[j] - neuron['output']) 121 | # 计算误差项 delta 122 | for j in range(len(layer)): 123 | neuron = layer[j] 124 | neuron['delta'] = errors[j] * sigmoid_derivative(neuron['output']) 125 | 126 | 127 | def update_parameters(network, row, l_rate): 128 | """利用误差更新神经网络的参数(权重矩阵和偏置项) 129 | :param network: 神经网络 130 | :param row: 一个样本数据 131 | :param l_rate: 学习率 132 | :return: 133 | """ 134 | for i in range(len(network)): 135 | inputs = row[:-1] 136 | if i != 0: # 获取上一层网络的输出 137 | inputs = [neuron['output'] for neuron in network[i - 1]] 138 | for neuron in network[i]: 139 | # 更新权重矩阵 140 | for j in range(len(inputs)): 141 | neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j] 142 | # 更新偏置项 143 | neuron['weights'][-1] += l_rate * neuron['delta'] 144 | 145 | 146 | def initialize_network(n_inputs, n_hidden, n_outputs): 147 | """初始化BP网络(初始化隐藏层和输出层的参数:权重矩阵和偏置项) 148 | :param n_inputs: 特征列数 149 | :param n_hidden: 隐藏层神经元个数 150 | :param n_outputs: 输出层神经元个数,即分类的总类别数 151 | :return: 初始化后的神经网络 152 | """ 153 | network = list() 154 | # 隐藏层 155 | hidden_layer = [{'weights': [random.random() for i in range(n_inputs + 1)]} for i in range(n_hidden)] 156 | network.append(hidden_layer) 157 | # 输出层 158 | output_layer = [{'weights': [random.random() for i in range(n_hidden + 1)]} for i in range(n_outputs)] 159 | network.append(output_layer) 160 | return network 161 | 162 | 163 | def train(train_data, l_rate, epochs, n_hidden, val_data): 164 | """训练神经网络(迭代n_epoch个回合) 165 | :param train_data: 训练集 166 | :param l_rate: 学习率 167 | :param epochs: 迭代的回合数 168 | :param n_hidden: 隐藏层神经元个数 169 | :param val_data: 验证集 170 | :return: 训练好的网络 171 | """ 172 | # 获取特征列数 173 | n_inputs = len(train_data[0]) - 1 174 | # 获取分类的总类别数 175 | n_outputs = len(set([row[-1] for row in train_data])) 176 | # 初始化网络 177 | network = initialize_network(n_inputs, n_hidden, n_outputs) 178 | 179 | acc = [] 180 | for epoch in range(epochs): # 训练epochs个回合 181 | for row in train_data: 182 | # 前馈计算 183 | _ = forward_propagate(network, row) 184 | # 处理一下类标,用于计算误差 185 | actual_label = [0 for i in range(n_outputs)] 186 | actual_label[row[-1]] = 1 187 | # 误差反向传播计算 188 | backward_propagate_error(network, actual_label) 189 | # 更新参数 190 | update_parameters(network, row, l_rate) 191 | # 保存当前epoch模型在验证集上的准确率 192 | acc.append(validation(network, val_data)) 193 | # 绘制出训练过程中模型在验证集上的准确率变化 194 | plt.xlabel('epochs') 195 | plt.ylabel('accuracy') 196 | plt.plot(acc) 197 | plt.show() 198 | 199 | return network 200 | 201 | 202 | def validation(network, val_data): 203 | """测试模型在验证集上的效果 204 | :param network: 神经网络 205 | :param val_data: 验证集 206 | :return: 模型在验证集上的准确率 207 | """ 208 | # 获取预测类标 209 | predicted_label = [] 210 | for row in val_data: 211 | prediction = predict(network, row) 212 | predicted_label.append(prediction) 213 | # 获取真实类标 214 | actual_label = [row[-1] for row in val_data] 215 | # 计算准确率 216 | accuracy = accuracy_calculation(actual_label, predicted_label) 217 | # print("测试集实际类标:", actual_label) 218 | # print("测试集上的预测类标:", predicted_label) 219 | return accuracy 220 | 221 | 222 | def accuracy_calculation(actual_label, predicted_label): 223 | """计算准确率 224 | :param actual_label: 真实类标 225 | :param predicted_label: 模型预测的类标 226 | :return: 准确率(百分制) 227 | """ 228 | correct_count = 0 229 | for i in range(len(actual_label)): 230 | if actual_label[i] == predicted_label[i]: 231 | correct_count += 1 232 | return correct_count / float(len(actual_label)) * 100.0 233 | 234 | 235 | def predict(network, row): 236 | """使用模型对当前输入的数据进行预测 237 | :param network: 神经网络 238 | :param row: 一个数据样本 239 | :return: 预测结果 240 | """ 241 | outputs = forward_propagate(network, row) 242 | return outputs.index(max(outputs)) 243 | 244 | 245 | if __name__ == "__main__": 246 | file_path = './iris.csv' 247 | 248 | # 参数设置 249 | l_rate = 0.2 # 学习率 250 | epochs = 300 # 迭代训练的次数 251 | n_hidden = 5 # 隐藏层神经元个数 252 | n_train_data = 130 # 训练集的大小(总共150条数据,训练集130条,验证集20条) 253 | 254 | # 加载数据并划分训练集和验证集 255 | train_data, val_data = load_dataset(file_path, n_train_data) 256 | # 训练模型 257 | network = train(train_data, l_rate, epochs, n_hidden, val_data) 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 上篇 2 | 3 |

反向传播算法(Backpropagation Algorithm,简称BP算法)是深度学习的重要思想基础,对于初学者来说也是必须要掌握的基础知识!本文希望以一个清晰的脉络和详细的说明,来让读者彻底明白BP算法的原理和计算过程。

4 |

全文分为上下两篇,上篇主要介绍BP算法的原理(即公式的推导),介绍完原理之后,我们会将一些具体的数据带入一个简单的三层神经网络中,去完整的体验一遍BP算法的计算过程;下篇是一个项目实战,我们将带着读者一起亲手实现一个BP神经网络(不使用任何第三方的深度学习框架)来解决一个具体的问题。

5 |

1. BP算法的推导

6 |
图1 一个简单的三层神经网络
图1 一个简单的三层神经网络
7 |

图 1 所示是一个简单的三层(两个隐藏层,一个输出层)神经网络结构,假设我们使用这个神经网络来解决二分类问题,我们给这个网络一个输入样本,通过前向运算得到输出。输出值的值域为,例如的值越接近0,代表该样本是"0"类的可能性越大,反之是"1"类的可能性大。

8 |

1.1 前向传播的计算

9 |

为了便于理解后续的内容,我们需要先搞清楚前向传播的计算过程,以图1所示的内容为例:

10 |

输入的样本为:

11 |

12 |

第一层网络的参数为:

13 |

14 |

第二层网络的参数为:

15 |

16 |

第三层网络的参数为:

17 |

18 |

1.1.1 第一层隐藏层的计算

19 |
图2 计算第一层隐藏层
图2 计算第一层隐藏层
20 |

第一层隐藏层有三个神经元:。该层的输入为:

21 |

22 |

神经元为例,则其输入为:

23 |

24 |

同理有:

25 |

26 |

假设我们选择函数作为该层的激活函数(图1中的激活函数都标了一个下标,一般情况下,同一层的激活函数都是一样的,不同层可以选择不同的激活函数),那么该层的输出为:

27 |

1.1.2 第二层隐藏层的计算

28 |
图3 计算第二层隐藏层
图3 计算第二层隐藏层
29 |

第二层隐藏层有两个神经元:。该层的输入为:

30 |

31 |

即第二层的输入是第一层的输出乘以第二层的权重,再加上第二层的偏置。因此得到和的输入分别为:

32 |

33 |

该层的输出分别为:

34 |

1.1.3 输出层的计算

35 |
图4 计算输出层
图4 计算输出层
36 |

输出层只有一个神经元:。该层的输入为:

37 |

38 |

即:

39 |

40 |

因为该网络要解决的是一个二分类问题,所以输出层的激活函数也可以使用一个Sigmoid型函数,神经网络最后的输出为:

41 |

1.2 反向传播的计算

42 |

在1.1节里,我们已经了解了数据沿着神经网络前向传播的过程,这一节我们来介绍更重要的反向传播的计算过程。假设我们使用随机梯度下降的方式来学习神经网络的参数,损失函数定义为,其中 是该样本的真实类标。使用梯度下降进行参数的学习,我们必须计算出损失函数关于神经网络中各层参数(权重和偏置)的偏导数。

43 |
44 |

假设我们要对第层隐藏层的参数求偏导数,即求 。假设代表第层神经元的输入,即,其中 为前一层神经元的输出,则根据链式法则有:

45 |

46 |

47 |

因此,我们只需要计算偏导数

48 |

1.2.1 计算偏导数$\frac{\partial z^{(k)}}{\partial W^{(k)}}$

49 |

前面说过,第k层神经元的输入为:,因此可以得到:

50 |
51 |

上式中,代表第层神经元的权重矩阵的第行,代表第层神经元的权重矩阵的第行中的第列。

52 |

我们以1.1节中的简单神经网络为例,假设我们要计算第一层隐藏层的神经元关于权重矩阵的导数,则有:

53 |

54 |

1.2.2 计算偏导数$\frac{\partial z^{(k)}}{\partial b^{(k)}}$

55 |

因为偏置b是一个常数项,因此偏导数的计算也很简单:

56 |

57 |

依然以第一层隐藏层的神经元为例,则有:

58 |

59 |

1.2.3 计算偏导数$\frac{\partial \mathrm{L}(\mathrm{y}, \widehat{\mathrm{y}})}{\partial z^{(k)}}$

60 |

偏导数又称为误差项(error term,也称为“灵敏度”),一般用表示,例如是第一层神经元的误差项,其值的大小代表了第一层神经元对于最终总误差的影响大小。

61 |

根据第一节的前向计算,我们知道第层的输入与第层的输出之间的关系为:

62 |

63 |

又因为,根据链式法则,我们可以得到为:

64 |

65 |

由上式我们可以看到,第层神经元的误差项是由第层的误差项乘以第层的权重,再乘以第层激活函数的导数(梯度)得到的。这就是误差的反向传播。
现在我们已经计算出了偏导数 ,则可分别表示为:

66 |

67 |

68 |

下面是基于随机梯度下降更新参数的反向传播算法:

69 |
70 |

单纯的公式推导看起来有些枯燥,下面我们将实际的数据带入图1所示的神经网络中,完整的计算一遍。

71 |

2. 图解BP算法

72 |
图5 图解BP算法
图5 图解BP算法
73 |

我们依然使用如图5所示的简单的神经网络,其中所有参数的初始值如下:

74 |

输入的样本为(假设其真实类标为"1"):

75 |

76 |

第一层网络的参数为:

77 |

78 |

第二层网络的参数为:

79 |

80 |

第三层网络的参数为:

81 |

82 |

假设所有的激活函数均为Logistic函数:。使用均方误差函数作为损失函数:

83 |

84 |

为了方便求导,我们将损失函数简化为:

85 |

86 |

2.1 前向传播

87 |

我们首先初始化神经网络的参数,计算第一层神经元:

88 |
89 |

90 |

91 |

上图中我们计算出了第一层隐藏层的第一个神经元的输入和输出,同理可以计算第二个和第三个神经元的输入和输出:

92 |
93 |

94 |

95 |

96 |

97 |

接下来是第二层隐藏层的计算,首先我们计算第二层的第一个神经元的输入z₄和输出f₄(z₄):

98 |
99 |

100 |

101 |

同样方法可以计算该层的第二个神经元的输入和输出

102 |
103 |

104 |

105 |

最后计算输出层的输入和输出

106 |
107 |

108 |

109 |

2.2 误差反向传播

110 |
111 |

首先计算输出层的误差项,我们的误差函数为,由于该样本的类标为“1”,而预测值为,因此误差为,输出层的误差项为:

112 |

113 |

接着计算第二层隐藏层的误差项,根据误差项的计算公式有:

114 |




115 |

最后是计算第一层隐藏层的误差项:

116 |




117 |

2.3 更新参数

118 |

上一小节中我们已经计算出了每一层的误差项,现在我们要利用每一层的误差项和梯度来更新每一层的参数,权重W和偏置b的更新公式如下:

119 |

120 |

通常权重的更新会加上一个正则化项来避免过拟合,这里为了简化计算,我们省去了正则化项。上式中的是学习率,我们设其值为0.1。参数更新的计算相对简单,每一层的计算方式都相同,因此本文仅演示第一层隐藏层的参数更新:

121 |
122 |

3. 小结

123 |

至此,我们已经完整介绍了BP算法的原理,并使用具体的数值做了计算。在下篇中,我们将带着读者一起亲手实现一个BP神经网络(不使用任何第三方的深度学习框架)。

124 | 125 | # 下篇 126 | 127 | 在上篇中我们详细介绍了BP算法的原理和推导过程,并且用实际的数据进行了计算演练。在下篇中,我们将自己实现BP算法(不使用第三方的算法框架),并用来解决鸢尾花分类问题。 128 | 129 | ![图1 鸢尾花](https://upload-images.jianshu.io/upload_images/13056713-f8db8fb1a15c3442?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 130 | 131 | 132 | 133 | 鸢尾花数据集如图2所示,总共有三个品种的鸢尾花(setosa、versicolor和virginica),每个类别50条样本数据,每个样本有四个特征(花萼长度、花萼宽度、花瓣长度以及花瓣宽度)。 134 | 135 | ![图2 鸢尾花数据集](https://upload-images.jianshu.io/upload_images/13056713-05f644b8ac047aca?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 136 | 137 | 138 | 139 | 首先我们导入需要的包: 140 | 141 | 142 | ``` python 143 | from csv import reader 144 | import numpy as np 145 | from sklearn.preprocessing import MinMaxScaler 146 | import random 147 | import matplotlib.pyplot as plt 148 | import math 149 | ``` 150 | 151 | 152 | 接下来我们实现一个数据集的加载和预处理的函数`load_dataset`: 153 | 154 | ``` python 155 | def load_dataset(dataset_path, n_train_data): 156 | """加载数据集,对数据进行预处理,并划分训练集和验证集 157 | :param dataset_path: 数据集文件路径 158 | :param n_train_data: 训练集的数据量 159 | :return: 划分好的训练集和验证集 160 | """ 161 | dataset = [] 162 | label_dict = {'Iris-setosa': 0, 'Iris-versicolor': 1, 'Iris-virginica': 2} 163 | with open(dataset_path, 'r') as file: 164 | # 读取CSV文件,以逗号为分隔符 165 | csv_reader = reader(file, delimiter=',') 166 | for row in csv_reader: 167 | # 将字符串类型的特征值转换为浮点型 168 | row[0:4] = list(map(float, row[0:4])) 169 | # 将标签替换为整型 170 | row[4] = label_dict[row[4]] 171 | # 将处理好的数据加入数据集中 172 | dataset.append(row) 173 | 174 | # 对数据进行归一化处理 175 | dataset = np.array(dataset) 176 | mms = MinMaxScaler() 177 | for i in range(dataset.shape[1] - 1): 178 | dataset[:, i] = mms.fit_transform(dataset[:, i].reshape(-1, 1)).flatten() 179 | 180 | # 将类标转为整型 181 | dataset = dataset.tolist() 182 | for row in dataset: 183 | row[4] = int(row[4]) 184 | # 打乱数据集 185 | random.shuffle(dataset) 186 | 187 | # 划分训练集和验证集 188 | train_data = dataset[0:n_train_data] 189 | val_data = dataset[n_train_data:] 190 | 191 | return train_data, val_data 192 | ``` 193 | 194 | 在`load_dataset`函数中,我们实现了数据集的读取、数据的归一化处理以及对数据集进行了`shuffle`操作等,最后函数返回了划分好的训练集和验证集。 195 | 196 | 实现数据预处理之后,接下来我们开始实现BP算法的关键部分(**如果读者对算法原理有不清楚的地方,可以查看"一文彻底搞懂BP算法:原理推导+数据演示+项目实战(上篇)"**)。首先我们实现神经元的计算部分、激活函数以及激活函数的求导部分。 197 | 198 | 199 | ``` python 200 | def fun_z(weights, inputs): 201 | """计算神经元的输入:z = weight * inputs + b 202 | :param weights: 网络参数(权重矩阵和偏置项) 203 | :param inputs: 上一层神经元的输出 204 | :return: 当前层神经元的输入 205 | """ 206 | bias_term = weights[-1] 207 | z = 0 208 | for i in range(len(weights)-1): 209 | z += weights[i] * inputs[i] 210 | z += bias_term 211 | return z 212 | 213 | 214 | def sigmoid(z): 215 | """激活函数(Sigmoid):f(z) = Sigmoid(z) 216 | :param z: 神经元的输入 217 | :return: 神经元的输出 218 | """ 219 | return 1.0 / (1.0 + math.exp(-z)) 220 | 221 | 222 | def sigmoid_derivative(output): 223 | """Sigmoid激活函数求导 224 | :param output: 激活函数的输出值 225 | :return: 求导计算结果 226 | """ 227 | return output * (1.0 - output) 228 | ``` 229 | 230 | 231 | 函数`fun_z`实现了公式"z = weight * inputs + b",其中inputs是上一层网络的输出,weight是当前层的权重矩阵,b是当前层的偏置项,计算得到的z是当前层的输入。 232 | 233 | 函数`sigmoid`是Sigmoid激活函数的实现,将z作为激活函数的输入,计算得到当前层的输出,并传递到下一层。 234 | 235 | 函数`sigmoid_derivative`是Sigmoid函数求导的实现,在误差反向传播的时候需要用到。 236 | 237 | 接下来我们实现BP网络的前向传播: 238 | 239 | 240 | ``` python 241 | def forward_propagate(network, inputs): 242 | """前向传播计算 243 | :param network: 神经网络 244 | :param inputs: 一个样本数据 245 | :return: 前向传播计算的结果 246 | """ 247 | for layer in network: # 循环计算每一层 248 | new_inputs = [] 249 | for neuron in layer: # 循环计算每一层的每一个神经元 250 | z = fun_z(neuron['weights'], inputs) 251 | neuron['output'] = sigmoid(z) 252 | new_inputs.append(neuron['output']) 253 | inputs = new_inputs 254 | return inputs 255 | ``` 256 | 257 | 258 | 前向计算的过程比较简单,和我们在上篇中介绍的计算过程一致。稍微麻烦一点的是误差反向传播的计算: 259 | 260 | 261 | ``` python 262 | def backward_propagate_error(network, actual_label): 263 | """误差进行反向传播 264 | :param network: 神经网络 265 | :param actual_label: 真实的标签值 266 | :return: 267 | """ 268 | for i in reversed(range(len(network))): # 从最后一层开始计算误差 269 | layer = network[i] 270 | errors = list() 271 | if i != len(network)-1: # 不是输出层 272 | for j in range(len(layer)): # 计算每一个神经元的误差 273 | error = 0.0 274 | for neuron in network[i + 1]: 275 | error += (neuron['weights'][j] * neuron['delta']) 276 | errors.append(error) 277 | else: # 输出层 278 | for j in range(len(layer)): # 计算每一个神经元的误差 279 | neuron = layer[j] 280 | errors.append(actual_label[j] - neuron['output']) 281 | # 计算误差项 delta 282 | for j in range(len(layer)): 283 | neuron = layer[j] 284 | neuron['delta'] = errors[j] * sigmoid_derivative(neuron['output']) 285 | ``` 286 | 287 | 288 | 误差反向传播过程中,我们首先需要根据模型的输出来计算得到误差,然后计算输出层的误差项。得到输出层的误差项之后,我们就可以根据上篇中介绍的"第k层神经元的误差项是由第k+1层的误差项乘以第k+1层的权重,再乘以第k层激活函数的导数得到"来计算其它层的误差项。 289 | 290 | 在计算得到每一层的误差项之后,我们根据上篇中介绍的权重矩阵和偏置项的更新公式来更新参数: 291 | 292 | 293 | ``` python 294 | def update_parameters(network, row, l_rate): 295 | """利用误差更新神经网络的参数(权重矩阵和偏置项) 296 | :param network: 神经网络 297 | :param row: 一个样本数据 298 | :param l_rate: 学习率 299 | :return: 300 | """ 301 | for i in range(len(network)): 302 | inputs = row[:-1] 303 | if i != 0: # 获取上一层网络的输出 304 | inputs = [neuron['output'] for neuron in network[i - 1]] 305 | for neuron in network[i]: 306 | # 更新权重矩阵 307 | for j in range(len(inputs)): 308 | neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j] 309 | # 更新偏置项 310 | neuron['weights'][-1] += l_rate * neuron['delta'] 311 | ``` 312 | 313 | 314 | 到这里所有的关键部分我们都已经实现了,接下来我们实现网络的初始化以及网络的训练部分,首先实现网络的初始化: 315 | 316 | 317 | ``` python 318 | def initialize_network(n_inputs, n_hidden, n_outputs): 319 | """初始化BP网络(初始化隐藏层和输出层的参数:权重矩阵和偏置项) 320 | :param n_inputs: 特征列数 321 | :param n_hidden: 隐藏层神经元个数 322 | :param n_outputs: 输出层神经元个数,即分类的总类别数 323 | :return: 初始化后的神经网络 324 | """ 325 | network = list() 326 | # 隐藏层 327 | hidden_layer = [{'weights': [random.random() for i in range(n_inputs + 1)]} for i in range(n_hidden)] 328 | network.append(hidden_layer) 329 | # 输出层 330 | output_layer = [{'weights': [random.random() for i in range(n_hidden + 1)]} for i in range(n_outputs)] 331 | network.append(output_layer) 332 | return network 333 | ``` 334 | 335 | 336 | 这里我们初始化了一个两层神经网络(一个隐藏层和一个输出层)。在初始化参数的时候,我们将权重矩阵和偏置项放在了一个数组中("weights"),数组的最后一个元素是偏置项,前面的元素是权重矩阵。 337 | 338 | 接下来我们实现模型的训练部分: 339 | 340 | 341 | ``` python 342 | def train(train_data, l_rate, epochs, n_hidden, val_data): 343 | """训练神经网络(迭代n_epoch个回合) 344 | :param train_data: 训练集 345 | :param l_rate: 学习率 346 | :param epochs: 迭代的回合数 347 | :param n_hidden: 隐藏层神经元个数 348 | :param val_data: 验证集 349 | :return: 训练好的网络 350 | """ 351 | # 获取特征列数 352 | n_inputs = len(train_data[0]) - 1 353 | # 获取分类的总类别数 354 | n_outputs = len(set([row[-1] for row in train_data])) 355 | # 初始化网络 356 | network = initialize_network(n_inputs, n_hidden, n_outputs) 357 | 358 | acc = [] 359 | for epoch in range(epochs): # 训练epochs个回合 360 | for row in train_data: 361 | # 前馈计算 362 | _ = forward_propagate(network, row) 363 | # 处理一下类标,用于计算误差 364 | actual_label = [0 for i in range(n_outputs)] 365 | actual_label[row[-1]] = 1 366 | # 误差反向传播计算 367 | backward_propagate_error(network, actual_label) 368 | # 更新参数 369 | update_parameters(network, row, l_rate) 370 | # 保存当前epoch模型在验证集上的准确率 371 | acc.append(validation(network, val_data)) 372 | # 绘制出训练过程中模型在验证集上的准确率变化 373 | plt.xlabel('epochs') 374 | plt.ylabel('accuracy') 375 | plt.plot(acc) 376 | plt.show() 377 | 378 | return network 379 | ``` 380 | 381 | 382 | 我们总共训练了`epochs`个回合,这里我们使用随机梯度下降来优化模型,因此每次都用一个样本来更新参数。接下来我们实现一个函数用来验证模型的效果: 383 | 384 | 385 | ``` python 386 | def validation(network, val_data): 387 | """测试模型在验证集上的效果 388 | :param network: 神经网络 389 | :param val_data: 验证集 390 | :return: 模型在验证集上的准确率 391 | """ 392 | # 获取预测类标 393 | predicted_label = [] 394 | for row in val_data: 395 | prediction = predict(network, row) 396 | predicted_label.append(prediction) 397 | # 获取真实类标 398 | actual_label = [row[-1] for row in val_data] 399 | # 计算准确率 400 | accuracy = accuracy_calculation(actual_label, predicted_label) 401 | # print("测试集实际类标:", actual_label) 402 | # print("测试集上的预测类标:", predicted_label) 403 | return accuracy 404 | ``` 405 | 406 | 407 | 训练过程中的每一个回合,我们都用模型对验证集进行一次预测,并将预测的结果保存,用来绘制训练过程中模型在验证集上的准确率的变化过程。准确率的计算以及使用模型进行预测的实现如下: 408 | 409 | 410 | ``` python 411 | def accuracy_calculation(actual_label, predicted_label): 412 | """计算准确率 413 | :param actual_label: 真实类标 414 | :param predicted_label: 模型预测的类标 415 | :return: 准确率(百分制) 416 | """ 417 | correct_count = 0 418 | for i in range(len(actual_label)): 419 | if actual_label[i] == predicted_label[i]: 420 | correct_count += 1 421 | return correct_count / float(len(actual_label)) * 100.0 422 | 423 | 424 | def predict(network, row): 425 | """使用模型对当前输入的数据进行预测 426 | :param network: 神经网络 427 | :param row: 一个数据样本 428 | :return: 预测结果 429 | """ 430 | outputs = forward_propagate(network, row) 431 | return outputs.index(max(outputs)) 432 | ``` 433 | 434 | 435 | 最后我们运行代码: 436 | 437 | 438 | ``` python 439 | if __name__ == "__main__": 440 | file_path = './iris.csv' 441 | 442 | # 参数设置 443 | l_rate = 0.2 # 学习率 444 | epochs = 300 # 迭代训练的次数 445 | n_hidden = 5 # 隐藏层神经元个数 446 | n_train_data = 130 # 训练集的大小(总共150条数据,训练集130条,验证集20条) 447 | 448 | # 加载数据并划分训练集和验证集 449 | train_data, val_data = load_dataset(file_path, n_train_data) 450 | # 训练模型 451 | network = train(train_data, l_rate, epochs, n_hidden, val_data) 452 | ``` 453 | 454 | 455 | 训练过程如图3所示: 456 | 457 | ![](https://upload-images.jianshu.io/upload_images/13056713-766f8b296b97ab93?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 458 | --------------------------------------------------------------------------------