机器学习(八):梯度下降优化_数据归一化

文章详细探讨了梯度下降算法的优化问题,强调了数据归一化的重要性。0-1标准化和Z-score标准化被介绍并分析,包括它们的性质和对模型训练的影响。通过实例展示了数据归一化如何加速梯度下降的收敛,提高模型训练效率和性能。最后,文章指出学习率选择对梯度下降的重要性,并鼓励读者进一步探索相关主题。
摘要由CSDN通过智能技术生成

全文共12000余字,预计阅读时间约24~40分钟 | 满满干货,建议收藏!

在这里插入图片描述

一、引言

梯度下降是一种用于优化目标函数的通用技术,广泛应用于机器学习和深度学习。然而,梯度下降可能受到一些问题的困扰,例如收敛慢、陷入局部最小值、对于学习率的高度敏感性等。虽然通过引入随机梯度下降和小批量梯度下降后,通过增加样本的随机性,使得算法在一定程度上能够跨越局部最小值点,但是它们却衍生了收敛不稳定、收敛结果持续震荡等新问题。

因此,对梯度下降的优化成为了提高模型训练效率与性能的关键。

所谓的的梯度下降算法的优化,目的只有一个:就是希望又好又快的收敛到全域最小值点。好指的是收敛过程比较平稳,没有比较明显的震荡情况,快指的是使用更少的迭代轮次就能收敛

二、数据归一化

要真正帮助随机梯度下降和小批量梯度下降解决随机性所造成的“麻烦”,就必须采用一些围绕迭代过程的优化方法,而所有的围绕迭代过程的优化方法中,最基础也是最通用的两种方法,分别是数据归一化方法和学习率调度。我们先来看数据归一化。

数据归一化(Normalization)是一种在预处理数据时常用的技术。它的目的是调整数据的规模,使其落在一个较小的、特定的范围内,比如 [0,1] 或者 [-1,1]。通过数据归一化,可以使不同规模或单位的特征在模型训练中具有相等的权重,从而避免因某些特征的规模过大而对模型结果产生过大的影响。

数据归一化方法的本质是一种对数据进行线性转换的方法,通过构建一种样本空间之间的线性映射关系来进行数据数值的转化,这种转化并不会影响数据分布,即不会影响数据的内在规律,只是对数据的数值进行调整。数据归一化有很多方法,并且在机器学习领域有诸多用途,不仅是能够作为梯度下降的优化算法,同时还能帮助一些数据集避免量纲不一致等问题。常见的数据归一化方法包括最小-最大归一化(Min-Max Normalization)和标准化(Standardization)。

2.1 0-1标准化

0-1标准化是一种将特征的尺度缩放到0-1之间的方法,该方法通过在输入特征中逐列遍历其中里的每一个数据,将Max和Min的记录下来,并通过Max-Min作为基数(即Min=0,Max=1)进行数据的归一化处理,基本公式为:

x ′ = x − min ⁡ ( x ) max ⁡ ( x ) − min ⁡ ( x ) (1) x' = \frac{x - \min(x)}{\max(x) - \min(x)} \tag{1} x=max(x)min(x)xmin(x)(1)

在这个公式中, x ′ x' x 是新的、已经被归一化的值, x x x 是原始数据值, min ⁡ ( x ) \min(x) min(x) max ⁡ ( x ) \max(x) max(x) 分别是数据集中的最小值和最大值。

这个公式的作用是:对于数据集中的每一个数值,减去该数据集的最小值,然后再除以该数据集的极差(最大值与最小值之差)。这样处理后,原始数据中的最小值会被映射到0,最大值会被映射到1,而其他的数值则会落在0和1之间。

2.2 0-1标准化的性质

从实验上来看0-1标准化的性质,直接上代码:

import numpy as np
import matplotlib.pyplot as plt

# 生成随机数据
np.random.seed(0)
data = np.random.randint(0, 100, 50)

# 进行0-1标准化
data_normalized = (data - np.min(data)) / (np.max(data) - np.min(data))

# 打印最小值和最大值,验证性质1
print(f"Min: {data_normalized.min()}, Max: {data_normalized.max()}")

# 生成一个值来验证性质2
value1 = data[10]
value2 = data[20]
normalized_value1 = data_normalized[10]
normalized_value2 = data_normalized[20]
print(f"Original: {value1} > {value2} --> Normalized: {normalized_value1} > {normalized_value2}")

# 添加一个极端值来验证性质3
data_with_outlier = np.append(data, 1000)
data_with_outlier_normalized = (data_with_outlier - np.min(data_with_outlier)) / (np.max(data_with_outlier) - np.min(data_with_outlier))

plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.hist(data_normalized, bins=20)
plt.title("Without Outlier")
plt.subplot(1, 2, 2)
plt.hist(data_with_outlier_normalized, bins=20)
plt.title("With Outlier")
plt.show()

# 画图来验证性质4
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.hist(data, bins=20)
plt.title("Original Data")
plt.subplot(1, 2, 2)
plt.hist(data_normalized, bins=20)
plt.title("Normalized Data")
plt.show()

这段代码生成了一个在0到100之间的随机整数数组,然后使用0-1标准化对数据进行了转换。通过打印最小值和最大值,可以看到数据位于0和1之间。然后,代码选取了数组中的两个值,比较了它们在标准化前后的大小关系。接下来,代码向数据中添加了一个极端值,通过绘制直方图来展示添加极端值后,大部分数据的分布情况如何被压缩在一个小的范围内。可以看到,在添加极端值后,大部分数据的范围被压缩到了0附近。这就是所说的0-1标准化对极端值敏感的性质。最后,通过绘制直方图,可以看到标准化前后数据的分布形状是一致的。

1

103

所以我们总结一下0-1标准化的基本性质如下:

性质描述
范围限定经过0-1标准化的数据会落在[0,1]范围内,其中最小的数值将被映射到0,最大的数值将被映射到1。
保持数据的相对大小关系0-1标准化不会改变数据的相对大小关系。也就是说,如果在原始数据中,一个数值大于另一个数值,那么在进行0-1标准化后,这种关系仍然保持。
对极端值敏感如果数据集中存在极端值或离群点,它们可能会影响0-1标准化的结果,使得大部分的数据被压缩在较小的范围内。
不改变数据的分布形状0-1标准化并不会改变数据的分布形状,也就是说,它不会改变数据的分布模式(例如,如果原始数据是正态分布的,那么经过0-1标准化后,数据仍然是正态分布的)。

2.3 Z-score标准化

Z-score标准化是每个特征的值都会减去该特征的平均值,然后除以该特征的标准偏差。这样处理之后,每个特征都会有一个0的均值和1的标准偏差。这是一种非常常用的标准化方法,尤其是在数据的分布接近正态分布时,其数学形式为:

Z = X − μ σ (2) Z = \frac{X - \mu}{\sigma} \tag{2} Z=σXμ(2)

其中: X X X 表示原始数据值, μ \mu μ 表示数据集的均值, σ \sigma σ 表示数据集的标准偏差

2.4 Z-score标准化的性质

同样的,从实验上来看0-1标准化的性质,直接上代码:

import numpy as np

# 生成一组随机数据
data = np.random.normal(10, 2, 1000) # 均值为10,标准偏差为2的正态分布

# 使用Z-score标准化
data_normalized = (data - np.mean(data)) / np.std(data)

# 检查标准化后的数据性质
print("Before normalization: Mean =", np.mean(data), ", Std Dev =", np.std(data))
print("After normalization: Mean =", np.mean(data_normalized), ", Std Dev =", np.std(data_normalized))

# 添加一个极端值
data_with_outlier = np.append(data, 1000)
data_with_outlier_normalized = (data_with_outlier - np.mean(data_with_outlier)) / np.std(data_with_outlier)

print("With outlier: Mean =", np.mean(data_with_outlier_normalized), ", Std Dev =", np.std(data_with_outlier_normalized))

# 查看数据分布
import matplotlib.pyplot as plt

plt.figure(figsize=(12,6))
plt.subplot(1,2,1)
plt.hist(data, bins=30, alpha=0.5, label='original data')
plt.hist(data_normalized, bins=30, alpha=0.5, label='z-score normalized data')
plt.legend(loc='upper right')

plt.show()

这段代码显示了原始数据和标准化后的数据的均值和标准偏差,然后通过添加一个极端值,展示了Z-score标准化对极端值的处理。最后,代码通过绘制直方图,展示了数据的分布形状在标准化前后没有发生改变。

我们来解读一下结果:

105

106

所以我们总结一下Z-score标准化的基本性质如下:

性质描述
范围未限定Z-score标准化后的数据范围不再限定在特定的区间内,例如[0,1]。数据可以是任何实数。
保持数据的相对大小关系在Z-score标准化后,数据的相对大小关系不会发生改变。如果在原始数据中,一个数值大于另一个数值,那么在Z-score标准化后,这种关系仍然保持。
对极端值的影响Z-score标准化对极端值和离群点的影响较小。因为它是以标准偏差为单位进行标准化的,所以,极端值和离群点对结果的影响会被降低。
均值为0,标准偏差为1Z-score标准化后的数据,其均值为0,标准偏差为1。这样的处理,使得数据更加符合统计学上的“标准正态分布”。
不改变数据的分布形状Z-score标准化并不会改变数据的分布形状。如果原始数据是正态分布的,那么经过Z-score标准化后,数据仍然是正态分布的。

2.5 标准化过程示例

首先理解一下什么是量纲:量纲是物理量的性质,它决定了该物理量的测量单位。例如,长度的量纲是“长度”,用米(m)作为单位,时间的量纲是“时间”,用秒(s)作为单位,速度的量纲是“长度/时间”,用米/秒(m/s)作为单位。

在数据分析和机器学习中,处理的数据往往包含多种不同的特征,这些特征的量纲可能会各不相同。例如,一个人的年收入可能以美元为单位,他的年龄可能以年为单位,而他的身高可能以米为单位。这些特征的量纲不同,所以它们的取值范围和数值大小可能会有很大的差异。如果直接使用这些数据来训练模型,可能会使得模型的性能受到影响。因此,通常会进行一些预处理步骤,如数据标准化或归一化,来消除不同量纲对模型的影响,使得模型可以更好地从数据中学习。

我们设计一个实验,来理解一下标准化在建模过程中起到的作用。我们首先设定一个回归方程 y = 100x1 + 10x2 + 3,该方程的参数为 100, 10 和 3,分别对应两个特征 x1 和 x2 的权重以及截距项,代码如下:

# 设置随机数生成器的种子以确保结果的可复现性
np.random.seed(0)

# 生成 x1 和 x2
x1 = np.random.rand(100, 1)
x2 = np.random.rand(100, 1)

# 生成正态分布的扰动项
noise = np.random.normal(0, 1, (100, 1))

# 根据回归方程生成 y,同时加入扰动项
y = 100 * x1 + 10 * x2 + 3 + noise

# 把 x1,x2 和一列全为1的数组合并为一个二维数组作为特征矩阵
X = np.hstack([x1, x2, np.ones((100, 1))])

print("X: ", X)
print("y: ", y)  

107

如果给第一个特征一个非常大的量纲、第二个特征一个非常小的量纲,那么数据情况和实际建模情况就将如下所示:

# 改变量纲
X[:, 1] *= 1000  # 第一个特征
X[:, 2] *= 0.001  # 第二个特征

108

然后我们进行线性回归建模,计算参数值:

# 创建线性回归模型
model = LinearRegression()

# 使用线性回归模型拟合数据
model.fit(X, y)

# 输出模型参数
print("Weights: ", ['%.5f' % elem for elem in model.coef_[0]])
print("Bias: %.5f" % model.intercept_)

我们看下模型的 输出结果:

109

从上述这个过程来看,我们设定的已知的线性方程是 y = 100 x 1 + 10 x 2 + 3 y=100x_1+10x_2+3 y=100x1+10x2+3,在调用线性回归模型进行训练时,模型输出的权重参数并不接近我们在创建数据集时设定的值(100和10)。这是因为特征的量纲对模型训练的影响:具有更大量纲的特征在训练过程中的影响力会被放大,而具有较小量纲的特征在训练过程中的影响力则会被缩小。

而真实情况是:我们处理的数据集中,特征的量纲(单位或范围)通常会有所不同。例如,在预测房价的任务中,房屋的面积可能是以平方米为单位,而卧室的数量则是一个简单的计数,两者的量纲明显不同。如果直接将这些不同量纲的特征用于模型训练,可能会导致模型对某些特征过于敏感,而忽略了其他特征。

因此,特征缩放(如0-1标准化或Z-score标准化)在预处理步骤中非常重要,它可以确保所有的特征都在同一量纲下,使得模型可以平等地对待所有特征,从而有助于提高模型的性能和稳定性。

所以我们接下来进行归一化,再来看看效果:

先对X进行归一化,使所有的特征都落在同一个范围[0,1]内,消除了特征间的量纲差异。

# 手动实现 0-1 标准化
X_scaled = np.copy(X)

for i in range(X_scaled.shape[1]):
    if np.max(X_scaled[:, i]) != np.min(X_scaled[:, i]):
        X_scaled[:, i] = (X_scaled[:, i] - np.min(X_scaled[:, i])) / (np.max(X_scaled[:, i]) - np.min(X_scaled[:, i]))

print(X_scaled)

112

然后使用归一化后的数据,再次进行建模求解参数

# 使用线性回归模型拟合数据
model.fit(X_scaled, y)

# 输出模型参数
print("Weights after scaling: ", ['%.5f' % elem for elem in model.coef_[0]])
print("Bias after scaling: %.5f" % model.intercept_)

114

通过上述结果可以看到,通过0-1标准化,模型参数估计更接近于真实值,从而更好地理解了数据归一化的重要性和作用。在实际应用中,因为不同特征的取值范围和量纲可能差别很大,如果不进行适当的标准化处理,可能会对模型的学习造成困扰,而进行标准化处理后,模型的表现往往会得到显著的提升。

2.6 为什么归一化能加速梯度下降的收敛

数据归一化对于梯度下降算法的优化非常重要。原因在于,梯度下降法是基于特征空间进行迭代的,不同尺度的特征会导致特征空间中的尺度也不一致,使得搜索过程中可能需要在某些方向上进行大幅度的移动,而在其他方向上只需要进行小幅度的移动。

"特征尺度"是指特征值的取值范围或者说是分布的规模。例如,一个特征可能是人的年龄,范围在0-100之间;另一个特征可能是人的收入,可能在0-1000000之间。这两个特征的"尺度"或者说规模是不同的,年龄的尺度相对较小,而收入的尺度相对较大。

如果数据没有归一化,那么特征的尺度可能会相差很大,这就导致梯度下降的过程中,各个方向的移动步长差异很大,可能需要更多的迭代次数才能找到最优解。

而当数据被归一化后,各个特征的尺度接近,这就意味着我们在特征空间中的移动步长在各个方向上都变得相对均匀,这样能使梯度下降的路径更直接地指向最优解,减少了迭代的次数,也就是说,加速了梯度下降的收敛。

这就是为什么数据归一化可以加速梯度下降的收敛的原因。看下代码:

import numpy as np
import matplotlib.pyplot as plt

# 定义一个简单的线性回归函数和梯度计算函数
def linear_regression(X, W):
    return np.dot(X, W)

def gradient(X, Y, Y_pred):
    return np.dot(X.T, (Y_pred - Y)) / len(Y)

# 创建数据及进行深拷贝
X = np.array([[1, 1], [3, 1]])
X_norm = np.copy(X)
X_norm[:, :1] = (X_norm[:, :1] - np.mean(X_norm[:, :1])) / np.std(X_norm[:, :1])

Y = np.array([[2], [4]])

np.random.seed(24)
W = np.random.randn(2, 1)
W_norm = np.copy(W)

# 梯度下降并记录权重轨迹
W_res = [W]
W_res_norm = [W_norm]

for _ in range(100):
    Y_pred = linear_regression(X, W)
    W -= 0.1 * gradient(X, Y, Y_pred)
    W_res.append(W.copy())
    
    Y_pred_norm = linear_regression(X_norm, W_norm)
    W_norm -= 0.1 * gradient(X_norm, Y, Y_pred_norm)
    W_res_norm.append(W_norm.copy())

# 绘制权重轨迹
plt.figure(figsize=(12, 6))

plt.subplot(121)
plt.title('Unnormalized')
plt.plot([w[0][0] for w in W_res], [w[1][0] for w in W_res], '-o', color='#ff7f0e')

plt.subplot(122)
plt.title('Normalized')
plt.plot([w[0][0] for w in W_res_norm], [w[1][0] for w in W_res_norm], '-o', color='#ff7f0e')

plt.show()

这段代码中,使用了一个简单的线性回归模型和梯度下降算法,为了让每一步的迭代都被记录下来,在每一步都将权重存储到列表中,然后用两个子图分别描绘出未归一化和归一化数据的权重轨迹。

image-20230620172346946

这两幅图都显示了线性回归模型的参数权重在梯度下降过程中的变化轨迹。横轴代表权重1,纵轴代表权重2。点代表了每一次迭代结束时权重的位置,从左到右可以看出权重如何随着梯度下降算法的运行而变化。

"Unnormalized"图显示了未进行数据标准化处理的情况下,权重的变化轨迹。可以看到,权重变化的轨迹有些曲折,说明权重在不同方向上的更新速度不同,这可能会导致收敛较慢。

"Normalized"图显示了进行了数据标准化处理的情况下,权重的变化轨迹。可以看到,轨迹较为直线,表明权重在不同方向上的更新速度更为一致,可以更快地收敛到最优解。

所以,通过这两个图,可以直观地看出对数据进行标准化处理,可以使得梯度下降算法更快地收敛。

三、案例:实现鲍鱼数据集的回归建模分析

鲍鱼数据集是一个经常用于回归分析的数据集,它来自UCI机器学习仓库(UCI Machine Learning Repository)。包含了4177个样本,每个样本有9个特征。这些特征包括性别(性别被编码为:M(男性),F(女性)或I(幼鲍)),长度,直径,高度,全重,剥壳重,内脏重,壳重以及鲍鱼的年龄(用鲍鱼的年轮数表示,加上1.5)。

鲍鱼数据集可以从 UC Irvine 数据仓库中获得,其 URL 是 下载地址点这里

先来看下数据集的情况:

with open('./abalone/abalone.names', 'r') as f:
    print(f.read())

  关注以下重点信息:

111

112

该数据集通常用于预测鲍鱼的年龄,这是一个回归问题。主要用途是通过鲍鱼的物理测量来预测鲍鱼的年龄。这个任务的主要挑战在于:传统的鲍鱼年龄测定方法(即通过显微镜计数鲍鱼壳上的环数)既耗时又无聊。而使用这个数据集中的其他更容易获取的测量结果来预测鲍鱼的年龄,就显得更为实用和高效。

读取数据集:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler

column_names = ['Sex', 'Length', 'Diameter', 'Height', 'Whole weight', 
                'Shucked weight', 'Viscera weight', 'Shell weight', 'Rings']
data = pd.read_csv('./abalone/abalone.data', names=column_names, delimiter=',')

image-20230625092102572

选取鲍鱼数据集中的Length(身体长度)和Diameter(身体宽度/直径)作为特征,Whole weight(体重)作为标签进行线性回归建模分析

# 选择特征
X = data[['Length', 'Diameter']]

# 选择目标变量
y = data['Whole weight']

分别准备一份原始数据与归一化后的数据。首先是原始数据:

# 对于未经标准化处理的数据
X_original = np.array(X)
intercept = np.ones((X_original.shape[0], 1))
X_original = np.concatenate((X_original, intercept), axis=1)

然后准备一份归一化的数据:

# 创建StandardScaler对象
scaler_X = StandardScaler()

# 对特征数据进行标准化处理
X_norm = scaler_X.fit_transform(X)

# 给标准化后的数据添加截距项
X_norm = np.concatenate((X_norm, intercept), axis=1)

113

接下来使用梯度下降法进行参数学习并绘制损失函数变化曲线,对比两种数据标准下的收敛情况

def MSE_loss(y_pred, y):
    """
    均方误差损失
    """
    return ((y_pred - y)**2).mean()

def gradient_descent(X, y, theta, alpha, iters):
    """
    执行梯度下降
    """
    m = len(y)
    cost_history = []

    for i in range(iters):
        prediction = np.dot(X, theta)
        error = prediction - y
        updates = (1/m)*np.dot(X.T, error)
        theta = theta - alpha * updates

        cost = MSE_loss(prediction, y)
        cost_history.append(cost)

    return theta, cost_history

# 初始化参数
theta_original = np.zeros(X_original.shape[1])
theta_norm = np.zeros(X_norm.shape[1])

# 定义学习率和迭代次数
alpha = 0.02
iters = 100

# 执行梯度下降并绘制损失历史
theta_original, cost_history_original = gradient_descent(X_original, y, theta_original, alpha, iters)
theta_norm, cost_history_norm = gradient_descent(X_norm, y, theta_norm, alpha, iters)

# 绘制图像
plt.figure(figsize=(12, 8))
plt.plot(cost_history_original, label='原始数据')
plt.plot(cost_history_norm, label='标准化数据')
plt.xlabel('迭代次数')
plt.ylabel('损失 (MSE)')
plt.title('损失与训练次数的变化')
plt.legend()
plt.show()

这段代码首先定义了MSE损失函数,然后实现了梯度下降算法。在每次迭代中,它都会计算当前参数下的预测值、误差、更新参数,并将当前的MSE损失添加到损失历史列表中。然后,对于原始数据和标准化数据,进行梯度下降,并保存损失历史值。

image-20230625124516213

这幅图像展示了梯度下降算法在训练过程中损失函数的变化情况,比较了使用原始数据和标准化数据两种情况。经过归一化后的数据集,从损失函数变化图像上来看,收敛速度更快(损失函数下降速度更快),且最终收敛到一个更优的结果。

再来看看全域最小值点:

def calculate_w_and_MSE(X, y):
    # 使用最小二乘法计算权重w
    w = np.linalg.lstsq(X, y, rcond=-1)[0]
    
    # 计算对应的MSE
    mse = MSELoss(X, w, y)
    
    return w, mse

# 对于原始数据
w1, mse1 = calculate_w_and_MSE(X_original, y)
print("对于原始数据,权重w为:", w1)
print("对应的MSE为:", mse1)

# 对于归一化后的数据
w2, mse2 = calculate_w_and_MSE(X_norm, y)
print("对于归一化后的数据,权重w为:", w2)
print("对应的MSE为:", mse2)

image-20230625125353313

从上述结果中可以看出,无论是原始数据还是归一化后的数据,最小二乘法计算出的权重在用于计算MSE时都得到了相同的结果。这表明了在全局最优解的情况下,无论数据是否经过归一化,最终的模型性能(以MSE为评价指标)都是一样的。

然而,当我们看权重w时,会发现它们在数量级和分布上存在显著差异。这主要是因为在归一化过程中,已经调整了各个特征的尺度和分布,使其均值为0,方差为1。这样,模型参数就不再依赖原始特征的尺度,而是反映出各个特征对预测目标的相对重要性。

这就引出了学习率的概念。在未进行归一化的数据上,由于特征的尺度和分布可能各不相同,梯度下降算法在寻找最优解时可能需要更小的学习率和更多的迭代次数。而在进行了归一化的数据上,由于所有特征都在同一尺度上,梯度下降算法可以使用较大的学习率,同时也能更快地收敛到最优解。

因此,特征归一化对于梯度下降等优化算法的性能至关重要。通过特征归一化,我们可以更有效地优化模型参数,同时也可以提升模型的训练效率。

四、总结

本篇文章深入探讨了数据归一化在机器学习,特别是在优化梯度下降算法中的重要性。详细介绍了最常用的两种数据归一化技术——0-1标准化和Z-score标准化,并讨论了它们各自的特性。通过具体的案例展示了归一化如何影响梯度下降的迭代过程,以及如何通过改善梯度下降的收敛性能,从而提高模型的训练效率和性能。

至此,我们还没有完全解决梯度下降的所有问题。例如,我们还没有讨论如何选择合适的学习率。事实上,学习率的选择对于梯度下降的收敛性和效率至关重要。

最后,感谢您阅读这篇文章!如果您觉得有所收获,别忘了点赞、收藏并关注我,这是我持续创作的动力。您有任何问题或建议,都可以在评论区留言,我会尽力回答并接受您的反馈。如果您希望了解某个特定主题,也欢迎告诉我,我会乐于创作与之相关的文章。谢谢您的支持,期待与您共同成长!

期待与您在未来的学习中共同成长。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

算法小陈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值