吴恩达深度学习_2_Week2优化算法

优化算法


1、梯度下降
2、小批量梯度下降
3、动量
4、Adam


第二门课:改善深层神经网络:超参数调参、正则化以及优化
第二周:优化算法

优化算法简介

之前的算法一直用梯度下降来更新参数并最小化代价函数,本优化算法可以加速学习过程,甚至获得更好的代价函数最终值。
在这里插入图片描述

函数包

import numpy as np
import matplotlib.pyplot as plt
import scipy.io
import math
import sklearn
import sklearn.datasets

from opt_utils import load_params_and_grads, initialize_parameters, forward_propagation, backward_propagation
from opt_utils import compute_cost, predict, predict_dec, plot_decision_boundary, load_dataset
from testCases import *

plt.rcParams['figure.figsize'] = (7.0, 4.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'         

1、梯度下降

在机器学习中,一种简单的优化方法是梯度下降(GD),当在每一步中针对所有 𝑚 个示例进行梯度步骤时,它也被称为批量梯度下降(Batch Gradient Descent)
在这里插入图片描述

使用梯度下降的一步更新参数
# 参数:
#   parameters -- 包含要更新的参数的 Python 字典:
#   parameters['W' + str(l)] = Wl
#   parameters['b' + str(l)] = bl
#   grads -- 包含要更新每个参数的梯度的 Python 字典:
#   grads['dW' + str(l)] = dWl
#   grads['db' + str(l)] = dbl
#   learning_rate -- 学习率,标量。
# 返回值:
#   parameters -- 包含更新后参数的 Python 字典
def update_parameters_with_gd(parameters, grads, learning_rate):
    L = len(parameters) // 2
    # 每个参数的更新规则如下:
    for l in range(L):
        parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l + 1)]
        parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]
    return parameters

parameters, grads, learning_rate = update_parameters_with_gd_test_case()
parameters = update_parameters_with_gd(parameters, grads, learning_rate)
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

在这里插入图片描述
这个变种是随机梯度下降(Stochastic Gradient Descent,SGD),它相当于每个小批量只有一个样本的小批量梯度下降。你刚刚实现的更新规则不会改变。不同之处在于你将一次只计算一个训练样本上的梯度,而不是在整个训练集上计算梯度。下面的代码示例展示了随机梯度下降和(批量)梯度下降之间的区别。

(Batch) Gradient Descent:

X = data_input
Y = labels
parameters = initialize_parameters(layers_dims)
for i in range(0, num_iterations):
    # Forward propagation
    a, caches = forward_propagation(X, parameters)
    # Compute cost.
    cost = compute_cost(a, Y)
    # Backward propagation.
    grads = backward_propagation(a, caches, parameters)
    # Update parameters.
    parameters = update_parameters(parameters, grads)

Stochastic Gradient Descent:

X = data_input
Y = labels
parameters = initialize_parameters(layers_dims)
for i in range(0, num_iterations):
    for j in range(0, m):
        # Forward propagation
        a, caches = forward_propagation(X[:,j], parameters)
        # Compute cost
        cost = compute_cost(a, Y[:,j])
        # Backward propagation
        grads = backward_propagation(a, caches, parameters)
        # Update parameters.
        parameters = update_parameters(parameters, grads)

在随机梯度下降中,您在更新梯度之前只使用一个训练样本。当训练集很大时,SGD可能更快。但是参数会出现“振荡”,而不是平滑收敛到最小值。下面是这一点的说明:
在这里插入图片描述
请注意,实现随机梯度下降总共需要3个for循环:
1、迭代次数循环
2、训练样本数量循环(m个样本)
3、层循环(更新所有参数,从W[1]、b[1]到W[L]、b[L])
在实践中,如果您不使用整个训练集或仅使用一个训练样本来执行每次更新,通常可以获得更快的结果。小批量梯度下降使用每一步的中间数量的样本。使用小批量梯度下降,您循环遍历小批量而不是逐个训练样本。
在这里插入图片描述
记住:梯度下降、小批量梯度下降和随机梯度下降之间的区别在于您用于执行一次更新步骤的样本数量。
您需要调整学习率超参数𝛼。使用适当调整的小批量大小通常优于梯度下降或随机梯度下降(特别是在训练集较大时)。

2、小批量梯度下降

关于训练集(X,Y)如何建立小批量梯度下降
有两步骤:
洗牌(Shuffle):按照下面所示,创建训练集(X,Y)的洗牌版本。X和Y的每一列代表一个训练样本。请注意,X和Y之间的随机洗牌是同步进行的。这样,在洗牌之后,X的第i列是对应于Y中第i个标签的示例。洗牌步骤确保样本将被随机地分割成不同的小批次。
在这里插入图片描述
分割(Partition):将洗牌后的(X,Y)分割成大小为mini_batch_size(这里为64)的小批量。请注意,训练样本的数量并不总是能够被mini_batch_size整除。最后一个小批量可能会更小,但您不需要担心这个问题。当最后一个小批量小于完整的mini_batch_size时,它将如下所示:
在这里插入图片描述
在这里插入图片描述

# 参数:
#   X -- 输入数据,形状为(输入大小,样本数)
#   Y -- 真实的 "标签" 向量(1 表示蓝点 / 0 表示红点),形状为(1,样本数)
#   mini_batch_size -- 小批量的大小,整数
# 返回值:
#   mini_batches -- 同步的(mini_batch_X,mini_batch_Y)列表
# GRADED FUNCTION: random_mini_batches
def random_mini_batches(X, Y, mini_batch_size=64, seed=0):
    np.random.seed(seed)    # 为了使您的 "随机" 小批量与我们的一致
    m = X.shape[1]
    mini_batches = []

    # Step 1: 对(X, Y)进行洗牌操作
    permutation = list(np.random.permutation(m))
    shuffled_X = X[:, permutation]
    shuffled_Y = Y[:, permutation].reshape((1, m))

    # Step 2: 对 (shuffled_X, shuffled_Y) 进行划分
    num_complete_minibatches = math.floor(
        m / mini_batch_size)      # 在你的划分中,有多少个大小为 mini_batch_size 的小批量
    for k in range(0, num_complete_minibatches):
        mini_batch_X = shuffled_X[:, k * mini_batch_size: (k + 1) * mini_batch_size]
        mini_batch_Y = shuffled_Y[:, k * mini_batch_size: (k + 1) * mini_batch_size]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)

    # 处理最后一个情况(最后一个小批量小于 mini_batch_size)
    if m % mini_batch_size != 0:
        mini_batch_X = shuffled_X[:, num_complete_minibatches * mini_batch_size: m]
        mini_batch_Y = shuffled_Y[:, num_complete_minibatches * mini_batch_size: m]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)

    return mini_batches

X_assess, Y_assess, mini_batch_size = random_mini_batches_test_case()
mini_batches = random_mini_batches(X_assess, Y_assess, mini_batch_size)

print ("shape of the 1st mini_batch_X: " + str(mini_batches[0][0].shape))
print ("shape of the 2nd mini_batch_X: " + str(mini_batches[1][0].shape))
print ("shape of the 3rd mini_batch_X: " + str(mini_batches[2][0].shape))
print ("shape of the 1st mini_batch_Y: " + str(mini_batches[0][1].shape))
print ("shape of the 2nd mini_batch_Y: " + str(mini_batches[1][1].shape))
print ("shape of the 3rd mini_batch_Y: " + str(mini_batches[2][1].shape))
print ("mini batch sanity check: " + str(mini_batches[0][0][0][0:3]))

在这里插入图片描述
洗牌和分割是构建小批量的两个必要步骤。
常常选择2的幂作为小批量大小,例如16、32、64、128等

3、动量

由于小批量梯度下降在仅看到示例子集后会进行参数更新,因此更新的方向存在一些差异,因此小批量梯度下降所采用的路径将“振荡”到收敛。使用动量可以减少这些振荡。
动量会考虑过去的梯度,以平滑更新。我们将先前梯度的“方向”存储在变量 v 中 .从形式上讲,这将是前面步骤中梯度的指数加权平均值。你也可以想到 v作为滚下坡坡的球的“速度”,根据坡度/坡度的方向增加速度(和动量)。
在这里插入图片描述
在这里插入图片描述

# 将速度初始化为一个Python字典,其中:
# 键: "dW1","db1",...,"dWL","dbL"
# 值:与相应的梯度/参数形状相同的零值NumPy数组。
# 参数:
#   parameters -- 包含参数的Python字典。
#   parameters['W' + str(l)] = Wl
#   parameters['b' + str(l)] = bl
# 返回值:
#   v -- 包含当前速度的Python字典。
#   v['dW' + str(l)] = dWl的速度
#   v['db' + str(l)] = dbl的速度
# GRADED FUNCTION: initialize_velocity
def initialize_velocity(parameters):
    L = len(parameters) // 2
    v = {}
    # Initialize velocity
    for l in range(L):
        v["dW" + str(l + 1)] = np.zeros(parameters["W" + str(l + 1)].shape)
        v["db" + str(l + 1)] = np.zeros(parameters["b" + str(l + 1)].shape)
    return v

parameters = initialize_velocity_test_case()
v = initialize_velocity(parameters)
print("v[\"dW1\"] = " + str(v["dW1"]))
print("v[\"db1\"] = " + str(v["db1"]))
print("v[\"dW2\"] = " + str(v["dW2"]))
print("v[\"db2\"] = " + str(v["db2"]))

在这里插入图片描述在这里插入图片描述

# 使用动量更新参数。
# 参数:
#   parameters -- 包含参数的Python字典:
#   parameters['W' + str(l)] = Wl
#   parameters['b' + str(l)] = bl
#   grads -- 包含每个参数的梯度的Python字典:
#   grads['dW' + str(l)] = dWl
#   grads['db' + str(l)] = dbl
#   v -- 包含当前速度的Python字典:
#   v['dW' + str(l)] = ...
#   v['db' + str(l)] = ...
#   beta -- 动量超参数,标量
#   learning_rate -- 学习率,标量
# 返回值:
#   parameters -- 包含更新后参数的Python字典
#   v -- 包含更新后速度的Python字典
def update_parameters_with_momentum(parameters, grads, v, beta, learning_rate):
    L = len(parameters) // 2

    # Momentum update for each parameter
    for l in range(L):
        # compute velocities
        v["dW" + str(l + 1)] = beta * v["dW" + str(l + 1)] + (1 - beta) * grads["dW" + str(l + 1)]
        v["db" + str(l + 1)] = beta * v["db" + str(l + 1)] + (1 - beta) * grads["db" + str(l + 1)]
        # update parameters
        parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * v["dW" + str(l + 1)]
        parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * v["db" + str(l + 1)]
    return parameters, v

parameters, grads, v = update_parameters_with_momentum_test_case()

parameters, v = update_parameters_with_momentum(parameters, grads, v, beta = 0.9, learning_rate = 0.01)
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))
print("v[\"dW1\"] = " + str(v["dW1"]))
print("v[\"db1\"] = " + str(v["db1"]))
print("v[\"dW2\"] = " + str(v["dW2"]))
print("v[\"db2\"] = " + str(v["db2"]))

在这里插入图片描述
请注意:速度初始化为零。因此,该算法将需要几次迭代来“建立”速度并开始采取更大的步骤。
如果 β=0,那么这只是没有动量的标准梯度下降。
你如何选择β ?
1、动量越大β是,更新越顺畅,因为我们考虑过去的梯度越多。但如果β太大了,它也可能使更新变得过于平滑。
2、β的通用值 范围从 0.8 到 0.999。如果您不愿意调整它,β=0.9通常是一个合理的默认值。
3、调整最佳β因为您的模型可能需要尝试多个值,以查看在降低成本函数 J 的值方面最有效的方法
你应该记住的: - 动量考虑了过去的梯度,以平滑梯度下降的步骤。它可以应用于批量梯度下降、小批量梯度下降或随机梯度下降。- 你必须调整动量超参数β和学习率α

4、Adam

Adam 是训练神经网络最有效的优化算法之一。它结合了 RMSProp(在讲座中描述)和 Momentum 的想法。
在这里插入图片描述
在这里插入图片描述

#将 v 和 s 初始化为两个 Python 字典,其中:
# 键: "dW1","db1",...,"dWL","dbL"
# 值:与相应的梯度/参数形状相同的零值 NumPy 数组。
# 参数:
#   parameters -- 包含参数的 Python 字典。
#   parameters["W" + str(l)] = Wl
#   parameters["b" + str(l)] = bl
# 返回值:
#   v -- 包含梯度的指数加权平均值的 Python 字典。
#   v["dW" + str(l)] = ...
#   v["db" + str(l)] = ...
#   s -- 包含梯度平方的指数加权平均值的 Python 字典。
#   s["dW" + str(l)] = ...
#   s["db" + str(l)] = ...
def initialize_adam(parameters):
    L = len(parameters) // 2
    v = {}
    s = {}
    # Initialize v, s. Input: "parameters". Outputs: "v, s".
    for l in range(L):
        v["dW" + str(l + 1)] = np.zeros(parameters["W" + str(l + 1)].shape)
        v["db" + str(l + 1)] = np.zeros(parameters["b" + str(l + 1)].shape)
        s["dW" + str(l + 1)] = np.zeros(parameters["W" + str(l + 1)].shape)
        s["db" + str(l + 1)] = np.zeros(parameters["b" + str(l + 1)].shape)
    return v, s

parameters = initialize_adam_test_case()

v, s = initialize_adam(parameters)
print("v[\"dW1\"] = " + str(v["dW1"]))
print("v[\"db1\"] = " + str(v["db1"]))
print("v[\"dW2\"] = " + str(v["dW2"]))
print("v[\"db2\"] = " + str(v["db2"]))
print("s[\"dW1\"] = " + str(s["dW1"]))
print("s[\"db1\"] = " + str(s["db1"]))
print("s[\"dW2\"] = " + str(s["dW2"]))
print("s[\"db2\"] = " + str(s["db2"]))

在这里插入图片描述
在这里插入图片描述

# 使用Adam更新参数。
# 参数:
#   parameters -- 包含参数的Python字典:
#   parameters['W' + str(l)] = Wl
#   parameters['b' + str(l)] = bl
#   grads -- 包含每个参数的梯度的Python字典:
#   grads['dW' + str(l)] = dWl
#   grads['db' + str(l)] = dbl
#   v -- Adam变量,第一个梯度的移动平均值,Python字典
#   s -- Adam变量,梯度平方的移动平均值,Python字典
#   learning_rate -- 学习率,标量。
#   beta1 -- 第一矩估计的指数衰减超参数
#   beta2 -- 第二矩估计的指数衰减超参数
#   epsilon -- 防止Adam更新中除以零的超参数
# 返回值:
#   parameters -- 包含更新后参数的Python字典
#   v -- Adam变量,第一个梯度的移动平均值,Python字典
#   s -- Adam变量,梯度平方的移动平均值,Python字典
def update_parameters_with_adam(parameters, grads, v, s, t, learning_rate=0.01,
                                beta1=0.9, beta2=0.999, epsilon=1e-8):
    L = len(parameters) // 2
    v_corrected = {}  # Initializing first moment estimate, python dictionary
    s_corrected = {}  # Initializing second moment estimate, python dictionary

    # Perform Adam update on all parameters
    for l in range(L):
        # Moving average of the gradients. Inputs: "v, grads, beta1". Output: "v".
        v["dW" + str(l + 1)] = beta1 * v["dW" + str(l + 1)] + (1 - beta1) * grads['dW' + str(l + 1)]
        v["db" + str(l + 1)] = beta1 * v["db" + str(l + 1)] + (1 - beta1) * grads['db' + str(l + 1)]

        # Compute bias-corrected first moment estimate. Inputs: "v, beta1, t". Output: "v_corrected".
        v_corrected["dW" + str(l + 1)] = v["dW" + str(l + 1)] / (1 - beta1 ** t)
        v_corrected["db" + str(l + 1)] = v["db" + str(l + 1)] / (1 - beta1 ** t)

        # Moving average of the squared gradients. Inputs: "s, grads, beta2". Output: "s".
        s["dW" + str(l + 1)] = s["dW" + str(l + 1)] + (1 - beta2) * (grads['dW' + str(l + 1)] ** 2)
        s["db" + str(l + 1)] = s["db" + str(l + 1)] + (1 - beta2) * (grads['db' + str(l + 1)] ** 2)

    # Compute bias-corrected second raw moment estimate. Inputs: "s, beta2, t". Output: "s_corrected".
    s_corrected["dW" + str(l + 1)] = s["dW" + str(l + 1)] / (1 - beta2 ** t)
    s_corrected["db" + str(l + 1)] = s["db" + str(l + 1)] / (1 - beta2 ** t)
    # Update parameters. Inputs: "parameters, learning_rate, v_corrected, s_corrected, epsilon". Output: "parameters".
    parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * (
                v_corrected["dW" + str(l + 1)] / (np.sqrt(s_corrected["dW" + str(l + 1)]) + epsilon))
    parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * (
                v_corrected["db" + str(l + 1)] / (np.sqrt(s_corrected["db" + str(l + 1)]) + epsilon))

    return parameters, v, s

parameters, grads, v, s = update_parameters_with_adam_test_case()
parameters, v, s  = update_parameters_with_adam(parameters, grads, v, s, t = 2)

print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))
print("v[\"dW1\"] = " + str(v["dW1"]))
print("v[\"db1\"] = " + str(v["db1"]))
print("v[\"dW2\"] = " + str(v["dW2"]))
print("v[\"db2\"] = " + str(v["db2"]))
print("s[\"dW1\"] = " + str(s["dW1"]))
print("s[\"db1\"] = " + str(s["db1"]))
print("s[\"dW2\"] = " + str(s["dW2"]))
print("s[\"db2\"] = " + str(s["db2"]))

在这里插入图片描述

5、具有不同优化算法的模型

让我们使用以下“moons”数据集来测试不同的优化方法。(数据集被命名为“moons”,因为这两个类的数据看起来有点像新月形的月亮。
在这里插入图片描述

# 一个可以在不同优化器模式下运行的3层神经网络模型。
# 参数:
#   X -- 输入数据,形状为(2,样本数)
#   Y -- 真实的标签向量(1表示蓝点/0表示红点),形状为(1,样本数)
#   layers_dims -- 包含每个层大小的Python列表
#   learning_rate -- 学习率,标量
#   mini_batch_size -- 小批量的大小
#   beta -- 动量超参数
#   beta1 -- 过去梯度估计的指数衰减超参数
#   beta2 -- 过去梯度平方估计的指数衰减超参数
#   epsilon -- 防止Adam更新中除以零的超参数
#   num_epochs -- 迭代次数
#   print_cost -- 是否每1000个迭代打印成本
# 返回值:
#   parameters -- 包含更新后参数的Python字典
train_X, train_Y = load_dataset()
def model(X, Y, layers_dims, optimizer, learning_rate=0.0007, mini_batch_size=64, beta=0.9,
          beta1=0.9, beta2=0.999, epsilon=1e-8, num_epochs=10000, print_cost=True):

    L = len(layers_dims)
    costs = []  # to keep track of the cost
    t = 0  # initializing the counter required for Adam update
    seed = 10  # 为了评分目的,使得你的“随机”小批量与我们的相同。

    # Initialize parameters
    parameters = initialize_parameters(layers_dims)
    # Initialize the optimizer
    if optimizer == "gd":
        pass  # no initialization required for gradient descent
    elif optimizer == "momentum":
        v = initialize_velocity(parameters)
    elif optimizer == "adam":
        v, s = initialize_adam(parameters)

    # Optimization loop
    for i in range(num_epochs):

        # 定义随机小批量。我们增加种子以在每个时期之后重新洗牌数据集,以便得到不同的洗牌结果。
        seed = seed + 1
        minibatches = random_mini_batches(X, Y, mini_batch_size, seed)

        for minibatch in minibatches:
            # Select a minibatch
            (minibatch_X, minibatch_Y) = minibatch
            # Forward propagation
            a3, caches = forward_propagation(minibatch_X, parameters)
            # Compute cost
            cost = compute_cost(a3, minibatch_Y)
            # Backward propagation
            grads = backward_propagation(minibatch_X, minibatch_Y, caches)
            # Update parameters
            if optimizer == "gd":
                parameters = update_parameters_with_gd(parameters, grads, learning_rate)
            elif optimizer == "momentum":
                parameters, v = update_parameters_with_momentum(parameters, grads, v, beta, learning_rate)
            elif optimizer == "adam":
                t = t + 1  # Adam counter
                parameters, v, s = update_parameters_with_adam(parameters, grads, v, s,
                                                               t, learning_rate, beta1, beta2, epsilon)
        # Print the cost every 1000 epoch
        if print_cost and i % 1000 == 0:
            print("Cost after epoch %i: %f" % (i, cost))
        if print_cost and i % 100 == 0:
            costs.append(cost)
    # plot the cost
    plt.plot(costs)
    plt.ylabel('cost')
    plt.xlabel('epochs (per 100)')
    plt.title("Learning rate = " + str(learning_rate))
    plt.show()

    return parameters

1)小批量梯度下降

# train 3-layer model
layers_dims = [train_X.shape[0], 5, 2, 1]
parameters = model(train_X, train_Y, layers_dims, optimizer = "gd")

# Predict
predictions = predict(train_X, train_Y, parameters)

# Plot decision boundary
plt.title("Model with Gradient Descent optimization")
axes = plt.gca()
axes.set_xlim([-1.5,2.5])
axes.set_ylim([-1,1.5])
plot_decision_boundary(lambda x: predict_dec(parameters, x.T), train_X, train_Y)

2)带有动量的小批量梯度下降

# train 3-layer model
layers_dims = [train_X.shape[0], 5, 2, 1]
parameters = model(train_X, train_Y, layers_dims, beta = 0.9, optimizer = "momentum")

# Predict
predictions = predict(train_X, train_Y, parameters)

# Plot decision boundary
plt.title("Model with Momentum optimization")
axes = plt.gca()
axes.set_xlim([-1.5,2.5])
axes.set_ylim([-1,1.5])
plot_decision_boundary(lambda x: predict_dec(parameters, x.T), train_X, train_Y)

3)使用Adam优化器的小批量模式

# train 3-layer model
layers_dims = [train_X.shape[0], 5, 2, 1]
parameters = model(train_X, train_Y, layers_dims, optimizer = "adam")

# Predict
predictions = predict(train_X, train_Y, parameters)

# Plot decision boundary
plt.title("Model with Adam optimization")
axes = plt.gca()
axes.set_xlim([-1.5,2.5])
axes.set_ylim([-1,1.5])
plot_decision_boundary(lambda x: predict_dec(parameters, x.T), train_X, train_Y)

4、总结

在这里插入图片描述
动量通常会有所帮助,但考虑到学习率小和数据集简单,其影响几乎可以忽略不计。此外,您在成本中看到的巨大振荡来自这样一个事实,即对于优化算法来说,某些小批量比其他小批量更难。
另一方面,Adam 的表现明显优于小批量梯度下降和动量。如果在这个简单的数据集上运行模型更多时期,则所有三种方法都将获得非常好的结果。但是,你已经看到亚当收敛得更快了。

Adam的一些优点包括:
1、相对较低的内存要求(尽管高于梯度下降和带动量的梯度下降)
2、即使对超参数进行少量调整,通常也能很好地工作(除了α )

References:
Adam paper: https://arxiv.org/pdf/1412.6980.pdf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值