复现反向传播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) + b1
    A1 = sigmod(Z1)

    print(f"输入-隐藏层;A1.shape:{A1.shape}")

    # 隐藏层
    Z2 = np.dot(A1 , W2) + b2
    A2 = sigmod(Z2)
    print(f"隐藏-隐藏层;A2.shape:{A2.shape}")

    Z3 = np.dot(A2 , W3) + b3
    A3 = 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 ) / m
    return loss

def 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 )) / m

    return loss


def 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) / m
    dB3 = np.sum(dZ3, axis=0, keepdims=True) / m

    dA2 = np.dot(dZ3, W3.T)
    dZ2 = dA2 * sigmoid_derivative(A2)
    dW2 = np.dot(A1.T, dZ2) / m
    dB2 = np.sum(dZ2, axis=0, keepdims=True) / m

    dA1 = np.dot(dZ2, W2.T)
    dZ1 = dA1 * sigmoid_derivative(A1)
    dW1 = np.dot(X.T, dZ1) / m
    dB1 = np.sum(dZ1, axis=0, keepdims=True) / m

    return dW1, dB1, dW2, dB2, dW3, dB3


def update_parameters(W1, b1, W2, b2, W3, b3, dW1, db1, dW2, db2, dW3, db3, learning_rate=0.01):
    W1 = W1 - learning_rate * dW1
    b1 = b1 - learning_rate * db1
    W2 = W2 - learning_rate * dW2
    b2 = b2 - learning_rate * db2
    W3 = W3 - learning_rate * dW3
    b3 = b3 - learning_rate * db3
    return W1, b1, W2, b2, W3, b3


def 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_history


W1, 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算法的目标

反向传播算法用于训练神经网络,通过最小化损失函数来调整网络的权重。主要过程包括:

  1. 前向传播:计算输入数据通过网络的输出。
  2. 计算损失:衡量预测输出与真实标签之间的差异。
  3. 反向传播:计算损失函数对每个权重的梯度,并利用这些梯度更新权重。

计划的步骤

  1. 构建网络结构:我们首先需要定义一个简单的前馈神经网络(Feedforward Neural Network)。
  2. 前向传播:实现前向传播,计算每一层的输出。
  3. 计算损失函数:选择一个损失函数,比如均方误差(Mean Squared Error, MSE),并计算损失值。
  4. 反向传播计算梯度:计算损失对每个权重的梯度(即偏导数)。
  5. 更新权重:使用梯度下降法更新网络中的权重。

网络结构定义

我们从最简单的网络结构开始:一个输入层、两个隐藏层和一个输出层。假设我们要解决的是一个二分类问题,输入层有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)) # 输出层的偏置

解释

  1. W1:表示输入层到隐藏层的权重矩阵,大小为(input_size, hidden_size)
  2. b1:隐藏层的偏置向量,大小为(1, hidden_size)
  3. W2:隐藏层到隐藏层的权重矩阵,大小为(hidden_size, hidden_size)
  4. b1:隐藏层的偏置向量,大小为(1, hidden_size)
  5. W3:隐藏层到输出层的权重矩阵,大小为(hidden_size, output_size)
  6. b3:输出层的偏置向量,大小为(1, output_size)

这些参数是通过随机初始化的,并且会在反向传播过程中被更新。

第二步:前向传播(继续)

我们已经定义了 Z1A1 计算隐藏层的激活值,现在继续定义第二层隐藏层的激活值 Z2A2,以及输出层的激活值 Z3A3。具体代码如下:

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def forward_propagation(X):
    # 第一隐藏层
    Z1 = np.dot(X, W1) + b1
    A1 = sigmoid(Z1)
    
    # 第二隐藏层
    Z2 = np.dot(A1, W2) + b2
    A2 = sigmoid(Z2)
    
    # 输出层
    Z3 = np.dot(A2, W3) + b3
    A3 = sigmoid(Z3)
    
    return A1, A2, A3

解释

  • Z1A1:表示输入层到第一隐藏层的线性组合结果和激活值。
  • Z2A2:表示第一隐藏层到第二隐藏层的线性组合结果和激活值。
  • Z3A3:表示第二隐藏层到输出层的线性组合结果和激活值。

这样,通过调用 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=1m[Y(i)log(A3(i))+(1Y(i))log(1A3(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)) / m
    return loss

解释

  • YA3:分别是真实标签和预测输出。
  • loss:表示整体的损失值,反映了模型的预测能力。

第四步:反向传播计算梯度

在反向传播过程中,我们计算损失函数相对于每个参数的梯度,然后利用这些梯度更新权重。反向传播使用链式法则(Chain Rule)来计算梯度。

1. 输出层到第二隐藏层的梯度计算

从输出层开始,我们已经知道:

d Z 3 = A 3 − Y dZ3 = A3 - Y dZ3=A3Y

接下来,我们需要计算损失函数相对于输出层权重 (W3) 和偏置 (b3) 的梯度。具体的计算步骤如下:

  1. 计算 (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=W3L=m1A2TdZ3
    这里, A 2 T A2^T A2T 是第二隐藏层的激活值的转置。
  2. 计算 (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=b3L=m1dZ3
    这里,我们对所有样本的梯度进行求和,并取平均值。

2. 第二隐藏层到第一隐藏层的梯度计算

接着,我们计算第二隐藏层的梯度 d Z 2 dZ2 dZ2

  1. 计算 (dA2)(对第二隐藏层激活值的导数):
    d A 2 = d Z 3 ⋅ W 3 T dA2 = dZ3 \cdot W3^T dA2=dZ3W3T
    这里,(W3^T) 是输出层权重的转置。
  2. 计算 (dZ2)(对第二隐藏层线性组合的导数):
    d Z 2 = d A 2 ⋅ sigmoid_derivative ( Z 2 ) dZ2 = dA2 \cdot \text{sigmoid\_derivative}(Z2) dZ2=dA2sigmoid_derivative(Z2)
    其中, sigmoid_derivative ( Z 2 ) \text{sigmoid\_derivative}(Z2) sigmoid_derivative(Z2) 是对 Z 2 Z2 Z2 进行激活函数求导的结果。
  3. 计算 (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=W2L=m1A1TdZ2
    这里, A 1 T A1^T A1T 是第一隐藏层的激活值的转置。
  4. 计算 (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=b2L=m1dZ2

3. 第一隐藏层到输入层的梯度计算

最后,我们计算第一隐藏层的梯度 d Z 1 dZ1 dZ1

  1. 计算 (dA1)(对第一隐藏层激活值的导数):
    d A 1 = d Z 2 ⋅ W 2 T dA1 = dZ2 \cdot W2^T dA1=dZ2W2T
    这里, W 2 T W2^T W2T 是第二隐藏层权重的转置。
  2. 计算 (dZ1)(对第一隐藏层线性组合的导数):
    d Z 1 = d A 1 ⋅ sigmoid_derivative ( Z 1 ) dZ1 = dA1 \cdot \text{sigmoid\_derivative}(Z1) dZ1=dA1sigmoid_derivative(Z1)
  3. 计算 (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=W1L=m1XTdZ1
    这里, X T X^T XT 是输入数据的转置。
  4. 计算 (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=b1L=m1dZ1

代码实现

def backward_propagation(X, Y, A1, A2, A3):
    m = X.shape[0]

    # 输出层梯度
    dZ3 = A3 - Y
    dW3 = np.dot(A2.T, dZ3) / m
    db3 = 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) / m
    db2 = 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) / m
    db1 = np.sum(dZ1, axis=0, keepdims=True) / m

    return dW1, db1, dW2, db2, dW3, db3

解释

  • dZ3dZ2dZ1:分别是输出层和隐藏层的梯度。
  • dW3dW2dW1:分别是输出层和隐藏层的权重梯度。
  • db3db2db1:分别是输出层和隐藏层的偏置梯度。

这些梯度将用于更新网络的参数,以减少损失函数的值。

第五步:更新权重

利用梯度下降法,我们使用上一步计算出的梯度更新每个权重和偏置。

公式

权重更新公式为:
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 * dW1
    b1 -= learning_rate * db1
    W2 -= learning_rate * dW2
    b2 -= learning_rate * db2
    W3 -= learning_rate * dW3
    b3 -= learning_rate * db3
    
    return 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 函数下降趋势分析

在这里插入图片描述

从图像中可以观察到以下几个关键点和趋势:

  1. 初始快速下降 (0 - 20次迭代):

    • 在训练的初始阶段,损失函数呈现出快速下降的趋势。这是因为在训练初期,权重更新幅度较大,模型对输入数据进行大幅调整,迅速减少预测误差。
    • 这是正常现象,说明学习率设置适当,模型正在有效地学习。
  2. 损失函数平稳期 (20 - 40次迭代):

    • 在迭代20次左右,损失函数的下降速度明显放缓,并进入一个较为平稳的阶段。此时模型已经逐渐接近局部最优,调整的步伐变小。
    • 这是一个典型的现象,表明模型接近收敛。
  3. 微小波动期 (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] 内)。

结果对比

使用我们手动实现的模型和 sklearnMLPClassifier 进行对比,观察训练和测试集上的准确率。

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}")

对应的结果如下:
在这里插入图片描述

总结

我们已经详细实现了一个三层神经网络的反向传播算法,并逐步拆解了各个过程。整个过程包括:

  1. 前向传播计算输出。
  2. 计算损失函数。
  3. 反向传播计算梯度。
  4. 利用梯度更新参数。
  5. 重复上述步骤训练网络。
  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值