本文主要是介绍复现反向传播BP算法:手动实现与Sklearn MLP对比分析【复现】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
完整代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
from sklearn.metrics import accuracy_score# 生成月亮型二分类数据集
X, Y = make_moons(n_samples=500, noise=0.2, random_state=42)
Y = Y.reshape(-1, 1) # 将 Y 转换为列向量# 数据集拆分为训练集和测试集
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)# 初始化参数
input_size = 2
hidden_size = 4
output_size = 1# 可视化数据集
plt.figure(figsize=(8, 6))
plt.scatter(X[:, 0], X[:, 1], c=Y.ravel(), cmap=plt.cm.Spectral)
plt.title("Moon Dataset")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()np.random.seed(42)
W1 = np.random.randn(input_size , hidden_size)
b1 = np.zeros((1,hidden_size))
W2 = np.random.randn(hidden_size , hidden_size)
b2 = np.zeros((1 , hidden_size))
W3 = np.random.randn(hidden_size ,output_size )
b3 = np.zeros((1, output_size))def sigmod(X):return 1 / (1 + np.exp(-X))def sigmoid_derivative(x):return sigmod(x) * ( 1 - sigmod(x))def forward(X, W1, b1, W2, b2 , W3 , b3):# 隐藏层Z1 = np.dot(X,W1) + b1A1 = sigmod(Z1)print(f"输入-隐藏层;A1.shape:{A1.shape}")# 隐藏层Z2 = np.dot(A1 , W2) + b2A2 = sigmod(Z2)print(f"隐藏-隐藏层;A2.shape:{A2.shape}")Z3 = np.dot(A2 , W3) + b3A3 = sigmod(Z3)print(f"隐藏-输出层;A3.shape:{A3.shape}")return A1 , A2 , A3# X = np.array([[0.5 , 0.1]])
print(f"X.shape:{X.shape}")A1 , A2 , A3 = forward(X,W1, b1, W2, b2 , W3 , b3)# print("A1 (Hidden Layer Output):", A1)
# print("A2 (Output Layer Output):", A2)def mean_loss(Y , A3):m = Y.shape[0]print(f"Y.shape[0]:{Y.shape[0]}")loss = np.sum( (Y - A3) ** 2 ) / mreturn lossdef compute_loss(Y , A3):m = Y.shape[0]print(f"Y.shape[0]:{Y.shape[0]}")loss = -np.sum(Y * np.log(A3) + (1 - Y) * np.log(1 - A3 )) / mreturn lossdef backward(X, Y, A1, A2, A3, W1, W2, W3):m = Y.shape[0]dA3 = -(np.divide(Y, A3) - np.divide(1-Y, 1-A3))dZ3 = dA3 * sigmoid_derivative(A3)dW3 = np.dot(A2.T, dZ3) / mdB3 = np.sum(dZ3, axis=0, keepdims=True) / mdA2 = np.dot(dZ3, W3.T)dZ2 = dA2 * sigmoid_derivative(A2)dW2 = np.dot(A1.T, dZ2) / mdB2 = np.sum(dZ2, axis=0, keepdims=True) / mdA1 = np.dot(dZ2, W2.T)dZ1 = dA1 * sigmoid_derivative(A1)dW1 = np.dot(X.T, dZ1) / mdB1 = np.sum(dZ1, axis=0, keepdims=True) / mreturn dW1, dB1, dW2, dB2, dW3, dB3def update_parameters(W1, b1, W2, b2, W3, b3, dW1, db1, dW2, db2, dW3, db3, learning_rate=0.01):W1 = W1 - learning_rate * dW1b1 = b1 - learning_rate * db1W2 = W2 - learning_rate * dW2b2 = b2 - learning_rate * db2W3 = W3 - learning_rate * dW3b3 = b3 - learning_rate * db3return W1, b1, W2, b2, W3, b3def train(X, Y, W1, b1, W2, b2, W3, b3, learning_rate=0.01, num_iterations=100):loss_history = []m = X.shape[0]for i in range(num_iterations):# 随机打乱数据permutation = np.random.permutation(m)X_shuffled = X[permutation, :]Y_shuffled = Y[permutation, :]# 遍历每一个样本for j in range(m):# 获取单个样本X_sample = X_shuffled[j:j + 1]Y_sample = Y_shuffled[j:j + 1]# 前向传播A1, A2, A3 = forward(X_sample, W1, b1, W2, b2, W3, b3)# 计算损失(可选,仅用于监控)loss = compute_loss(Y_sample, A3)# 反向传播dW1, db1, dW2, db2, dW3, db3 = backward(X_sample, Y_sample, A1, A2, A3, W1, W2, W3)# 更新参数W1, b1, W2, b2, W3, b3 = update_parameters(W1, b1, W2, b2, W3, b3, dW1, db1, dW2, db2, dW3, db3,learning_rate)# 计算并存储整个训练集的损失值(用于每个epoch的监控)A1, A2, A3 = forward(X, W1, b1, W2, b2, W3, b3)loss = compute_loss(Y, A3)loss_history.append(loss)if i % 100 == 0:print(f"Iteration {i}, Loss: {loss:.4f}")return W1, b1, W2, b2, W3, b3, loss_historyW1, b1, W2, b2, W3, b3, loss_history = train(X_train, Y_train, W1, b1, W2, b2, W3, b3, learning_rate=0.01, num_iterations=100)# 绘制损失函数随迭代次数的变化曲线
plt.figure(figsize=(8, 6))
plt.plot(loss_history)
plt.title("Loss over Iterations")
plt.xlabel("Iterations")
plt.ylabel("Loss")
plt.show()def predict(X, W1, b1, W2, b2, W3, b3):_, _, A3 = forward(X, W1, b1, W2, b2, W3, b3)predictions = (A3 > 0.5).astype(int)return predictions# 使用训练后的模型进行预测
train_predictions = predict(X_train, W1, b1, W2, b2, W3, b3)
test_predictions = predict(X_test, W1, b1, W2, b2, W3, b3)# 计算准确度
train_accuracy = accuracy_score(Y_train, train_predictions)
test_accuracy = accuracy_score(Y_test, test_predictions)from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score# 创建与手动实现相同结构的 MLPClassifier
# 设置两个隐藏层,每层有4个神经元,激活函数为'sigmoid',对应 'logistic'
model = MLPClassifier(hidden_layer_sizes=(4,),activation='logistic',learning_rate='constant',solver='sgd',learning_rate_init=0.01,max_iter=1000,random_state=42)# 训练模型
model.fit(X_train, Y_train.ravel()) # 注意:sklearn 的 MLPClassifier 期望 Y 是一维数组,因此需要使用 .ravel()# 预测
train_predictions_sklearn = model.predict(X_train)
test_predictions_sklearn = model.predict(X_test)# 计算准确度
train_accuracy_sklearn = accuracy_score(Y_train, train_predictions_sklearn)
test_accuracy_sklearn = accuracy_score(Y_test, test_predictions_sklearn)# 输出对比结果
print(f"Training Accuracy (Manual): {train_accuracy:.4f}")
print(f"Testing Accuracy (Manual): {test_accuracy:.4f}")
print(f"Training Accuracy (Sklearn MLP): {train_accuracy_sklearn:.4f}")
print(f"Testing Accuracy (Sklearn MLP): {test_accuracy_sklearn:.4f}")
我们一步一步来实现反向传播算法(Backpropagation,简称BP)。这个过程将分成多个步骤,每一步都会解释相应的原理和代码实现。
第一步:理解BP算法的目标
反向传播算法用于训练神经网络,通过最小化损失函数来调整网络的权重。主要过程包括:
- 前向传播:计算输入数据通过网络的输出。
- 计算损失:衡量预测输出与真实标签之间的差异。
- 反向传播:计算损失函数对每个权重的梯度,并利用这些梯度更新权重。
计划的步骤
- 构建网络结构:我们首先需要定义一个简单的前馈神经网络(Feedforward Neural Network)。
- 前向传播:实现前向传播,计算每一层的输出。
- 计算损失函数:选择一个损失函数,比如均方误差(Mean Squared Error, MSE),并计算损失值。
- 反向传播计算梯度:计算损失对每个权重的梯度(即偏导数)。
- 更新权重:使用梯度下降法更新网络中的权重。
网络结构定义
我们从最简单的网络结构开始:一个输入层、两个隐藏层和一个输出层。假设我们要解决的是一个二分类问题,输入层有2个节点,隐藏层有4个节点,输出层有1个节点。激活函数我们先使用Sigmoid;网络结构如下所示
这里需要最好还是使用两个隐藏层,因为较好的提取对应的特征
代码实现:
我们先定义网络的权重和偏置:
import numpy as np# 网络结构
input_size = 2 # 输入层节点数
hidden_size = 4 # 隐藏层节点数
output_size = 1 # 输出层节点数# 初始化权重和偏置
np.random.seed(0)
W1 = np.random.randn(input_size, hidden_size) # 输入层到隐藏层的权重
b1 = np.zeros((1, hidden_size)) # 隐藏层的偏置
W2 = np.random.randn(hidden_size, output_size) # 隐藏层到隐藏层的权重
b2 = np.zeros((1, output_size)) # 隐藏层的偏置
W3 = np.random.randn(hidden_size ,output_size ) # 隐藏层到输出层的权重
b3 = np.zeros((1, output_size)) # 输出层的偏置
解释
- W1:表示输入层到隐藏层的权重矩阵,大小为
(input_size, hidden_size)
。 - b1:隐藏层的偏置向量,大小为
(1, hidden_size)
。 - W2:隐藏层到隐藏层的权重矩阵,大小为
(hidden_size, hidden_size)
- b1:隐藏层的偏置向量,大小为
(1, hidden_size)
。 - W3:隐藏层到输出层的权重矩阵,大小为
(hidden_size, output_size)
。 - b3:输出层的偏置向量,大小为
(1, output_size)
。
这些参数是通过随机初始化的,并且会在反向传播过程中被更新。
第二步:前向传播(继续)
我们已经定义了 Z1
和 A1
计算隐藏层的激活值,现在继续定义第二层隐藏层的激活值 Z2
和 A2
,以及输出层的激活值 Z3
和 A3
。具体代码如下:
def sigmoid(x):return 1 / (1 + np.exp(-x))def forward_propagation(X):# 第一隐藏层Z1 = np.dot(X, W1) + b1A1 = sigmoid(Z1)# 第二隐藏层Z2 = np.dot(A1, W2) + b2A2 = sigmoid(Z2)# 输出层Z3 = np.dot(A2, W3) + b3A3 = sigmoid(Z3)return A1, A2, A3
解释
- Z1 和 A1:表示输入层到第一隐藏层的线性组合结果和激活值。
- Z2 和 A2:表示第一隐藏层到第二隐藏层的线性组合结果和激活值。
- Z3 和 A3:表示第二隐藏层到输出层的线性组合结果和激活值。
这样,通过调用 forward_propagation
函数,我们可以从输入数据 X
中得到每一层的激活值,最终的 A3
是网络的预测输出。
第三步:计算损失函数
在这个步骤中,我们选择一个适合二分类问题的损失函数。常用的选择是二分类交叉熵损失(Binary Cross-Entropy Loss)。
公式
二分类交叉熵损失的公式为:
L ( Y , A 3 ) = − 1 m ∑ i = 1 m [ Y ( i ) log ( A 3 ( i ) ) + ( 1 − Y ( i ) ) log ( 1 − A 3 ( i ) ) ] L(Y, A3) = -\frac{1}{m} \sum_{i=1}^{m} \left[Y^{(i)} \log(A3^{(i)}) + (1 - Y^{(i)}) \log(1 - A3^{(i)})\right] L(Y,A3)=−m1i=1∑m[Y(i)log(A3(i))+(1−Y(i))log(1−A3(i))]
其中:
- (Y) 是真实标签,(A3) 是预测输出。
- (m) 是样本数量。
代码实现
def compute_loss(Y, A3):m = Y.shape[0]loss = -np.sum(Y * np.log(A3) + (1 - Y) * np.log(1 - A3)) / mreturn loss
解释
- Y 和 A3:分别是真实标签和预测输出。
- loss:表示整体的损失值,反映了模型的预测能力。
第四步:反向传播计算梯度
在反向传播过程中,我们计算损失函数相对于每个参数的梯度,然后利用这些梯度更新权重。反向传播使用链式法则(Chain Rule)来计算梯度。
1. 输出层到第二隐藏层的梯度计算
从输出层开始,我们已经知道:
d Z 3 = A 3 − Y dZ3 = A3 - Y dZ3=A3−Y
接下来,我们需要计算损失函数相对于输出层权重 (W3) 和偏置 (b3) 的梯度。具体的计算步骤如下:
- 计算 (W3) 的梯度:
d W 3 = ∂ L ∂ W 3 = 1 m ⋅ A 2 T ⋅ d Z 3 dW3 = \frac{\partial L}{\partial W3} = \frac{1}{m} \cdot A2^T \cdot dZ3 dW3=∂W3∂L=m1⋅A2T⋅dZ3
这里, A 2 T A2^T A2T 是第二隐藏层的激活值的转置。 - 计算 (b3) 的梯度:
d b 3 = ∂ L ∂ b 3 = 1 m ⋅ ∑ d Z 3 db3 = \frac{\partial L}{\partial b3} = \frac{1}{m} \cdot \sum dZ3 db3=∂b3∂L=m1⋅∑dZ3
这里,我们对所有样本的梯度进行求和,并取平均值。
2. 第二隐藏层到第一隐藏层的梯度计算
接着,我们计算第二隐藏层的梯度 d Z 2 dZ2 dZ2:
- 计算 (dA2)(对第二隐藏层激活值的导数):
d A 2 = d Z 3 ⋅ W 3 T dA2 = dZ3 \cdot W3^T dA2=dZ3⋅W3T
这里,(W3^T) 是输出层权重的转置。 - 计算 (dZ2)(对第二隐藏层线性组合的导数):
d Z 2 = d A 2 ⋅ sigmoid_derivative ( Z 2 ) dZ2 = dA2 \cdot \text{sigmoid\_derivative}(Z2) dZ2=dA2⋅sigmoid_derivative(Z2)
其中, sigmoid_derivative ( Z 2 ) \text{sigmoid\_derivative}(Z2) sigmoid_derivative(Z2) 是对 Z 2 Z2 Z2 进行激活函数求导的结果。 - 计算 (W2) 的梯度:
d W 2 = ∂ L ∂ W 2 = 1 m ⋅ A 1 T ⋅ d Z 2 dW2 = \frac{\partial L}{\partial W2} = \frac{1}{m} \cdot A1^T \cdot dZ2 dW2=∂W2∂L=m1⋅A1T⋅dZ2
这里, A 1 T A1^T A1T 是第一隐藏层的激活值的转置。 - 计算 (b2) 的梯度:
d b 2 = ∂ L ∂ b 2 = 1 m ⋅ ∑ d Z 2 db2 = \frac{\partial L}{\partial b2} = \frac{1}{m} \cdot \sum dZ2 db2=∂b2∂L=m1⋅∑dZ2
3. 第一隐藏层到输入层的梯度计算
最后,我们计算第一隐藏层的梯度 d Z 1 dZ1 dZ1:
- 计算 (dA1)(对第一隐藏层激活值的导数):
d A 1 = d Z 2 ⋅ W 2 T dA1 = dZ2 \cdot W2^T dA1=dZ2⋅W2T
这里, W 2 T W2^T W2T 是第二隐藏层权重的转置。 - 计算 (dZ1)(对第一隐藏层线性组合的导数):
d Z 1 = d A 1 ⋅ sigmoid_derivative ( Z 1 ) dZ1 = dA1 \cdot \text{sigmoid\_derivative}(Z1) dZ1=dA1⋅sigmoid_derivative(Z1) - 计算 (W1) 的梯度:
d W 1 = ∂ L ∂ W 1 = 1 m ⋅ X T ⋅ d Z 1 dW1 = \frac{\partial L}{\partial W1} = \frac{1}{m} \cdot X^T \cdot dZ1 dW1=∂W1∂L=m1⋅XT⋅dZ1
这里, X T X^T XT 是输入数据的转置。 - 计算 (b1) 的梯度:
d b 1 = ∂ L ∂ b 1 = 1 m ⋅ ∑ d Z 1 db1 = \frac{\partial L}{\partial b1} = \frac{1}{m} \cdot \sum dZ1 db1=∂b1∂L=m1⋅∑dZ1
代码实现
def backward_propagation(X, Y, A1, A2, A3):m = X.shape[0]# 输出层梯度dZ3 = A3 - YdW3 = np.dot(A2.T, dZ3) / mdb3 = np.sum(dZ3, axis=0, keepdims=True) / m# 第二隐藏层梯度dA2 = np.dot(dZ3, W3.T)dZ2 = dA2 * A2 * (1 - A2)dW2 = np.dot(A1.T, dZ2) / mdb2 = np.sum(dZ2, axis=0, keepdims=True) / m# 第一隐藏层梯度dA1 = np.dot(dZ2, W2.T)dZ1 = dA1 * A1 * (1 - A1)dW1 = np.dot(X.T, dZ1) / mdb1 = np.sum(dZ1, axis=0, keepdims=True) / mreturn dW1, db1, dW2, db2, dW3, db3
解释
- dZ3、dZ2、dZ1:分别是输出层和隐藏层的梯度。
- dW3、dW2、dW1:分别是输出层和隐藏层的权重梯度。
- db3、db2、db1:分别是输出层和隐藏层的偏置梯度。
这些梯度将用于更新网络的参数,以减少损失函数的值。
第五步:更新权重
利用梯度下降法,我们使用上一步计算出的梯度更新每个权重和偏置。
公式
权重更新公式为:
W = W − α ⋅ d W W = W - \alpha \cdot dW W=W−α⋅dW
b = b − α ⋅ d b b = b - \alpha \cdot db b=b−α⋅db
其中, α \alpha α 是学习率,即每次调整的步长。
代码实现
def update_parameters(W1, b1, W2, b2, W3, b3, dW1, db1, dW2, db2, dW3, db3, learning_rate=0.01):W1 -= learning_rate * dW1b1 -= learning_rate * db1W2 -= learning_rate * dW2b2 -= learning_rate * db2W3 -= learning_rate * dW3b3 -= learning_rate * db3return W1, b1, W2, b2, W3, b3
解释
- 这里的
learning_rate
是一个超参数,用于控制每次更新的步长。
第六步:训练模型
将前向传播、损失计算、反向传播和权重更新组合起来,我们就可以训练整个模型了。
代码实现
def train(X, Y, W1, b1, W2, b2, W3, b3, learning_rate=0.01, num_iterations=100):loss_history = []for i in range(num_iterations):# 前向传播A1, A2, A3 = forward_propagation(X)# 计算损失loss = compute_loss(Y, A3)loss_history.append(loss)# 反向传播dW1, db1, dW2, db2, dW3, db3 = backward_propagation(X, Y, A1, A2, A3)# 更新参数W1, b1, W2, b2, W3, b3 = update_parameters(W1, b1, W2, b2, W3, b3, dW1, db1, dW2, db2, dW3, db3, learning_rate)# 每隔100次迭代输出一次损失值if i % 100 == 0:print(f"Iteration {i}, Loss: {loss:.4f}")return W1, b1, W2, b2, W3, b3, loss_history
解释
- num_iterations:表示训练的迭代次数。
- loss_history:用于记录每次迭代后的损失值,方便后续分析。
- print 语句会在每100次迭代时输出当前的损失值,方便跟踪模型的训练过程。
Loss 函数下降趋势分析
从图像中可以观察到以下几个关键点和趋势:
-
初始快速下降 (0 - 20次迭代):
- 在训练的初始阶段,损失函数呈现出快速下降的趋势。这是因为在训练初期,权重更新幅度较大,模型对输入数据进行大幅调整,迅速减少预测误差。
- 这是正常现象,说明学习率设置适当,模型正在有效地学习。
-
损失函数平稳期 (20 - 40次迭代):
- 在迭代20次左右,损失函数的下降速度明显放缓,并进入一个较为平稳的阶段。此时模型已经逐渐接近局部最优,调整的步伐变小。
- 这是一个典型的现象,表明模型接近收敛。
-
微小波动期 (40次迭代后):
- 从40次迭代后,损失函数继续缓慢下降,但图像上出现了一些微小的波动。这些波动可能是由于学习率较大或SGD中随机性的影响所导致的。模型在局部最优附近小范围波动。
特别点及原因分析
- 波动原因: 这些微小的波动可能是由于学习率较大,导致模型在接近局部最优时,未能完全稳定在最优点,而是在最优点附近来回波动。同时,SGD本身的随机性也可能引入一定的波动性。
- 损失不再下降: 在迭代大约40次后,损失函数基本保持在0.45左右。这说明当前的优化可能已经达到了某种局部最优,难以进一步下降。
第七步:模型预测
最后,我们使用训练好的模型进行预测。预测函数通过前向传播得到输出值,然后根据阈值 (0.5) 将输出转化为二分类的预测结果。
代码实现
def predict(X, W1, b1, W2, b2, W3, b3):_, _, A3 = forward_propagation(X)predictions = (A3 > 0.5).astype(int)return predictions
解释
- predictions:预测结果,表示模型对于输入 X X X 的预测类别;阈值设置为0.5(Sigmoid激活函数的输出在区间 [ 0 , 1 ] [0, 1] [0,1] 内)。
结果对比
使用我们手动实现的模型和 sklearn
的 MLPClassifier
进行对比,观察训练和测试集上的准确率。
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score# 使用手动实现的模型进行预测
train_predictions = predict(X_train, W1, b1, W2, b2, W3, b3)
test_predictions = predict(X_test, W1, b1, W2, b2, W3, b3)# 计算手动实现的模型的准确度
train_accuracy = accuracy_score(Y_train, train_predictions)
test_accuracy = accuracy_score(Y_test, test_predictions)# 使用 sklearn 的 MLPClassifier 进行对比
model = MLPClassifier(hidden_layer_sizes=(4,),activation='logistic',learning_rate_init=0.01,max_iter=1000,solver='sgd',random_state=42)model.fit(X_train, Y_train.ravel())
train_predictions_sklearn = model.predict(X_train)
test_predictions_sklearn = model.predict(X_test)# 计算 sklearn 模型的准确度
train_accuracy_sklearn = accuracy_score(Y_train, train_predictions_sklearn)
test_accuracy_sklearn = accuracy_score(Y_test, test_predictions_sklearn)print(f"Training Accuracy (Manual): {train_accuracy:.4f}")
print(f"Testing Accuracy (Manual): {test_accuracy:.4f}")
print(f"Training Accuracy (Sklearn MLP): {train_accuracy_sklearn:.4f}")
print(f"Testing Accuracy (Sklearn MLP): {test_accuracy_sklearn:.4f}")
对应的结果如下:
总结
我们已经详细实现了一个三层神经网络的反向传播算法,并逐步拆解了各个过程。整个过程包括:
- 前向传播计算输出。
- 计算损失函数。
- 反向传播计算梯度。
- 利用梯度更新参数。
- 重复上述步骤训练网络。
这篇关于复现反向传播BP算法:手动实现与Sklearn MLP对比分析【复现】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!