深度学习实战案例:基于 Pytorch +DNN的顾客流失预测

PyTorch和TensorFlow库是深度学习中最常用的两个Python库。PyTorch由Facebook开发,而TensorFlow是Google项目。在本文中,您将看到如何使用PyTorch库解决分类问题。

分类问题属于机器学习问题的一类,在给定一组特征的情况下,任务是预测离散值。预测肿瘤是否为癌症或者学生是否可能通过考试都是分类问题的常见例子。

在本文中,我们将根据银行客户的某些特征来预测客户在6个月后是否有可能离开银行。顾客离开组织的现象也称为顾客流失。因此,我们的任务是基于各种客户特征来预测顾客流失。

在继续之前,请假定您具有Python编程语言的中级水平,并已安装了PyTorch库。同时了解基本机器学习概念可能会有所帮助。

如果您尚未安装PyTorch,则可以使用以下pip命令进行安装:

$ pip install pytorch

技术提升

技术要学会分享、交流,不建议闭门造车。一个人走的很快、一堆人可以走的更远。

完整代码、数据、技术交流提升, 均可加入知识星球交流群获取,群友已超过2000人,添加时切记的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。

方式①、添加微信号:pythoner666,备注:来自 CSDN + 顾客流失
方式②、微信搜索公众号:Python学习与数据挖掘,后台回复:资料

数据集

本文中我们将使用的数据集可以在 Kaggle 上免费获取。

让我们导入所需的库和数据集到 Python 应用程序中:

import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

我们可以使用pandas库的read_csv()方法导入包含数据集的CSV文件。

dataset = pd.read_csv(r'E:Datasets\customer_data.csv')

让我们打印出数据集的形状:

dataset.shape

## output
(10000, 14)

输出显示数据集有1万条记录和14列。

我们可以使用 pandas dataframe 的 head()方法打印出数据集的前五行。

dataset.head()


您可以在我们的数据集中看到14列。基于前13列,我们的任务是预测第14列即“Exited”的值。需要注意的是,由于任务是预测客户流失,在记录客户信息后6个月获得退出列的值之前,前13列的值已经记录了6个月。

探索性数据分析

让我们对数据集进行一些探索性数据分析。首先,我们将预测实际离开银行的客户比例,并使用饼图进行可视化。
让我们先增加默认绘图大小:

fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 10
fig_size[1] = 8
plt.rcParams["figure.figsize"] = fig_size

以下脚本绘制了“Exited”列的饼图。

dataset.Exited.value_counts().plot(kind='pie', autopct='%1.0f%%', colors=['skyblue', 'orange'], explode=(0.05, 0.05))

输出结果显示,在我们的数据集中,有20%的客户离开了银行。这里1表示客户离开银行的情况,0表示客户没有离开银行的情况。
让我们绘制数据集中所有地理位置的客户数量:

sns.countplot(x='Geography', data=dataset)

输出结果显示,近一半的客户属于法国,而属于西班牙和德国的客户比例各为25%。
现在让我们绘制每个唯一地理位置的客户数量以及客户流失信息。我们可以使用seaborn库中的countplot()函数来实现。

sns.countplot(x='Exited', hue='Geography', data=dataset)


输出结果显示,尽管法国客户的总数是西班牙和德国客户数量的两倍,但离开银行的客户比例对于法国和德国客户来说是相同的。同样地,德国和西班牙客户的总数相同,但是离开银行的德国客户人数是西班牙客户人数的两倍,这表明在6个月后更有可能离开银行。

数据预处理

在训练 PyTorch 模型之前,我们需要对数据进行预处理。如果您查看数据集,您会发现它有两种类型的列:数值和分类。 数值列包含数字信息。CreditScore、Balance、Age 等等。

同样,地理位置和性别是分类列,因为它们包含客户的地理位置和性别等分类信息。

还有一些列既可以被视为数字也可以被视为分类变量。例如 HasCrCard 列可以具有 1 或 0 的值。

但是 HasCrCard 列包含关于客户是否拥有信用卡的信息。建议将既可作为分类又可作为数值的列视为分类变量进行处理,但这完全取决于数据集领域知识。

让我们再次打印出数据集中所有的列,并找出哪些列应该被视为数值以及哪些应该被视为分类变量 。DataFrame 的 columns 属性打印所有的列名:

dataset.columns
## output
Index(['RowNumber', 'CustomerId', 'Surname', 'CreditScore', 'Geography',
       'Gender', 'Age', 'Tenure', 'Balance', 'NumOfProducts', 'HasCrCard',
       'IsActiveMember', 'EstimatedSalary', 'Exited'],
      dtype='object')

从我们的数据集中,我们将不使用RowNumber、CustomerId和Surname列,因为这些列的值是完全随机的,并且与输出没有关系。
例如,客户的姓氏对于客户是否离开银行没有影响。在其余的列中,地理位置、性别、HasCrCard和IsActiveMember可以被视为分类变量。让我们创建一个这些列的列表:

categorical_columns = ['Geography', 'Gender', 'HasCrCard', 'IsActiveMember']

除了“Exited”列之外,所有剩余的列都可以视为数值列。

numerical_columns = ['CreditScore', 'Age', 'Tenure', 'Balance', 'NumOfProducts', 'EstimatedSalary']

最后,输出(Exited列中的值)被存储在outputs变量中。

outputs = ['Exited']

我们已经创建了分类、数值和输出列的列表。然而,目前分类列的类型不是分类型的。您可以使用以下脚本检查数据集中所有列的类型:

dataset.dtypes
## output
RowNumber            int64
CustomerId           int64
Surname             object
CreditScore          int64
Geography           object
Gender              object
Age                  int64
Tenure               int64
Balance            float64
NumOfProducts        int64
HasCrCard            int64
IsActiveMember       int64
EstimatedSalary    float64
Exited               int64
dtype: object

你可以看到,地理和性别列的类型是对象(object),而HasCrCard和IsActive列的类型是int64。我们需要将分类列的类型转换为category。我们可以使用astype()函数来实现,如下所示:

for category in categorical_columns:
    dataset[category] = dataset[category].astype('category')

现在,如果您再次绘制数据集中列的类型,则应该看到以下结果:

RowNumber             int64
CustomerId            int64
Surname              object
CreditScore           int64
Geography          category
Gender             category
Age                   int64
Tenure                int64
Balance             float64
NumOfProducts         int64
HasCrCard          category
IsActiveMember     category
EstimatedSalary     float64
Exited                int64
dtype: object

现在让我们看一下地理栏中的所有类别:

dataset['Geography'].cat.categories
## output
Index(['France', 'Germany', 'Spain'], dtype='object')

当您将列的数据类型更改为类别时,该列中的每个类别都会被分配一个唯一代码。例如,让我们绘制地理信息列的前五行并打印前五行的代码值:

dataset['Geography'].head()
## output
0    France
1     Spain
2    France
3    France
4     Spain
Name: Geography, dtype: category
Categories (3, object): [France, Germany, Spain]

以下脚本绘制了地理列前五行数值的代码:

dataset['Geography'].head().cat.codes
## outputt
0    0
1    2
2    0
3    0
4    2
dtype: int8

输出结果显示,法国已编码为0,西班牙已编码为2。

将分类列与数值列分开的基本目的是可以直接将数值列中的值馈送到神经网络中。然而,分类列中的值必须首先转换为数字类型。对分类列中的值进行编码部分解决了分类列数字转换任务。

由于我们将使用PyTorch进行模型训练,因此需要将分类和数值列转换为张量。

让我们首先将分类列转换为张量。在PyTorch中,可以通过numpy数组创建张量。我们首先将四个分类列中的数据转换成numpy数组,然后按水平方向堆叠所有列,如下面所示:

geo = dataset['Geography'].cat.codes.values
gen = dataset['Gender'].cat.codes.values
hcc = dataset['HasCrCard'].cat.codes.values
iam = dataset['IsActiveMember'].cat.codes.values

categorical_data = np.stack([geo, gen, hcc, iam], 1)

categorical_data[:10]

## output
array([[0, 0, 1, 1],
       [2, 0, 0, 1],
       [0, 0, 1, 0],
       [0, 0, 0, 0],
       [2, 0, 1, 1],
       [2, 1, 1, 0],
       [0, 1, 1, 1],
       [1, 0, 1, 0],
       [0, 1, 0, 1],
       [0, 1, 1, 1]], dtype=int8)

现在,要从上述的numpy数组创建张量,您可以简单地将该数组传递给torch模块的tensor类。请记住,在分类列中,数据类型应为torch.int64。

categorical_data = torch.tensor(categorical_data, dtype=torch.int64)
categorical_data[:10]
## output
tensor([[0, 0, 1, 1],
        [2, 0, 0, 1],
        [0, 0, 1, 0],
        [0, 0, 0, 0],
        [2, 0, 1, 1],
        [2, 1, 1, 0],
        [0, 1, 1, 1],
        [1, 0, 1, 0],
        [0, 1, 0, 1],
        [0, 1, 1, 1]])

在输出中,您可以看到分类数据的numpy数组现在已转换为张量对象。
同样地,我们也可以将数值列转换为张量:

numerical_data = np.stack([dataset[col].values for col in numerical_columns], 1)
numerical_data = torch.tensor(numerical_data, dtype=torch.float)
numerical_data[:5]

## output
tensor([[6.1900e+02, 4.2000e+01, 2.0000e+00, 0.0000e+00, 1.0000e+00, 1.0135e+05],
        [6.0800e+02, 4.1000e+01, 1.0000e+00, 8.3808e+04, 1.0000e+00, 1.1254e+05],
        [5.0200e+02, 4.2000e+01, 8.0000e+00, 1.5966e+05, 3.0000e+00, 1.1393e+05],
        [6.9900e+02, 3.9000e+01, 1.0000e+00, 0.0000e+00, 2.0000e+00, 9.3827e+04],
        [8.5000e+02, 4.3000e+01, 2.0000e+00, 1.2551e+05, 1.0000e+00, 7.9084e+04]])

在输出中,您可以看到前五行包含我们数据集中六个数值列的值。
最后一步是将输出的numpy数组转换为张量对象。

outputs = torch.tensor(dataset[outputs].values).flatten()
outputs[:5]

## output

tensor([1, 0, 1, 0, 0])

现在让我们绘制分类数据、数值数据和相应输出的形状:

print(categorical_data.shape)
print(numerical_data.shape)
print(outputs.shape)
# output
torch.Size([10000, 4])
torch.Size([10000, 6])
torch.Size([10000])

在我们训练模型之前,有一个非常重要的步骤。我们将分类列转换为数字列,其中唯一值由单个整数表示。例如,在地理信息列中,法国用0表示,德国用1表示。我们可以使用这些值来训练模型。然而,更好的方法是以N维向量的形式表示分类列中的值,而不是单个整数。向量能够捕获更多信息,并以更合适的方式找到不同分类值之间的关系。因此,我们将以N维向量的形式表示分类列中的值。这个过程被称为嵌入。

我们需要为所有分类列定义嵌入大小(向量维度)。没有硬性规定关于维度数量方面应该如何定义。一个好的经验法则是将每一列中唯一值数量除以2(但不能超过50)来定义其嵌入大小。例如,在地理信息列中,唯一值数量为3. 地理信息对应嵌入大小将会是 3/2 = 1.5 = 2 (四舍五入)。

以下脚本创建了一个包含所有分类变量独特数目和尺寸大小元组:

categorical_column_sizes = [len(dataset[column].cat.categories) for column in categorical_columns]
categorical_embedding_sizes = [(col_size, min(50, (col_size+1)//2)) for col_size in categorical_column_sizes]
print(categorical_embedding_sizes)

## output
[(3, 2), (2, 1), (2, 1), (2, 1)]

监督式深度学习模型,例如我们在本文中正在开发的模型,是使用训练数据进行训练,并在测试数据集上评估模型性能。因此,我们需要将数据集分成训练集和测试集,如下面的脚本所示:

total_records = 10000
test_records = int(total_records * .2)

categorical_train_data = categorical_data[:total_records-test_records]
categorical_test_data = categorical_data[total_records-test_records:total_records]
numerical_train_data = numerical_data[:total_records-test_records]
numerical_test_data = numerical_data[total_records-test_records:total_records]
train_outputs = outputs[:total_records-test_records]
test_outputs = outputs[total_records-test_records:total_records]

我们的数据集中有1万条记录,其中80%的记录(即8000条记录)将用于训练模型,而剩余20%的记录将用于评估我们模型的性能。请注意,在上面的脚本中,分类和数值数据以及输出已被分成了训练集和测试集。

为了验证我们是否正确地将数据分成了训练集和测试集,请打印出训练记录和测试记录的长度:

print(len(categorical_train_data))
print(len(numerical_train_data))
print(len(train_outputs))

print(len(categorical_test_data))
print(len(numerical_test_data))
print(len(test_outputs))

## output
8000
8000
8000
2000
2000
2000

创建一个预测模型

我们已经将数据分成了训练集和测试集,现在是定义我们的训练模型的时候了。为此,我们可以定义一个名为Model的类来训练模型。请看以下脚本:

class Model(nn.Module):

    def __init__(self, embedding_size, num_numerical_cols, output_size, layers, p=0.4):
        super().__init__()
        all_layers = []
        num_categorical_cols = sum((nf for ni, nf in embedding_size))
        input_size = num_categorical_cols + num_numerical_cols

        for i in layers:
            all_layers.append(nn.Linear(input_size, i))
            all_layers.append(nn.ReLU(inplace=True))
            all_layers.append(nn.BatchNorm1d(i))
            all_layers.append(nn.Dropout(p))
            input_size = i

        all_layers.append(nn.Linear(layers[-1], output_size))

        self.layers = nn.Sequential(*all_layers)

    def forward(self, x_categorical, x_numerical):
        embeddings = []
        for i,e in enumerate(self.all_embeddings):
            embeddings.append(e(x_categorical[:,i]))
        x_numerical = self.batch_norm_num(x_numerical)
        x = self.layers(x)
        return x

如果您以前没有使用过PyTorch,上面的代码可能看起来令人生畏,但我会尝试为您分解它。

在第一行中,我们声明了一个Model类,该类继承自PyTorch的nn模块中的Module类。在类的构造函数(init()方法)中传递以下参数:

  • embedding_size:包含分类列嵌入大小
  • num_numerical_cols:存储数字列总数
  • output_size:输出层大小或可能输出数量。
  • layers:列表,其中包含所有图层的神经元数量。
  • p:默认值为0.5的Dropout。

在构造函数中,初始化了一些变量。首先,all_embeddings 变量包含所有分类列的 ModuleList 对象列表。embedding_dropout 存储所有层的 dropout 值。最后,batch_norm_num 存储所有数值列的 BatchNorm1d 对象列表。
接下来,为了找到输入层的大小,将分类和数值列的数量相加,并将结果存储在 input_size 变量中。之后,一个 for 循环迭代并将相应的层添加到 all_layers 列表中。添加的层包括:

  • Linear:用于计算输入和权重矩阵之间的点积
  • ReLu:作为激活函数应用
  • BatchNorm1d:用于对数值列应用批归一化
  • Dropout:避免过拟合

for 循环结束后,在图层列表末尾附加输出图层。由于我们希望神经网络中所有图层按顺序执行,因此将图层列表传递给 nn.Sequential 类。
接下来,在 forward 方法中,同时传入分类和数值列作为输入。分类列的嵌入发生在以下行代码中。

embeddings = []
for i, e in enumerate(self.all_embeddings):
    embeddings.append(e(x_categorical[:,i]))
x = torch.cat(embeddings, 1)
x = self.embedding_dropout(x)

数值列的批量归一化是使用以下脚本应用的:

x_numerical = self.batch_norm_num(x_numerical)

最后,嵌入式分类列x和数值列x_numerical被连接在一起,并传递到序列层。

训练模型

要训练模型,首先我们必须创建一个在上一节中定义的Model类的对象。

model = Model(categorical_embedding_sizes, numerical_data.shape[1], 2, [200,100,50], p=0.4)

你可以看到,我们传递了分类列的嵌入大小、数值列的数量、输出大小(在本例中为2)和隐藏层中神经元的数量。你可以看到我们有三个隐藏层,分别具有200、100和50个神经元。如果需要,您可以选择任何其他大小。
让我们打印出模型并查看它的样子:

print(model)
## output
Model(
  (all_embeddings): ModuleList(
    (0): Embedding(3, 2)
    (1): Embedding(2, 1)
    (2): Embedding(2, 1)
    (3): Embedding(2, 1)
  )
  (embedding_dropout): Dropout(p=0.4)
  (batch_norm_num): BatchNorm1d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (layers): Sequential(
    (0): Linear(in_features=11, out_features=200, bias=True)
    (1): ReLU(inplace)
    (2): BatchNorm1d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (3): Dropout(p=0.4)
    (4): Linear(in_features=200, out_features=100, bias=True)
    (5): ReLU(inplace)
    (6): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (7): Dropout(p=0.4)
    (8): Linear(in_features=100, out_features=50, bias=True)
    (9): ReLU(inplace)
    (10): BatchNorm1d(50, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (11): Dropout(p=0.4)
    (12): Linear(in_features=50, out_features=2, bias=True)
  )
)

您可以看到,在第一个线性层中,in_features变量的值为11,因为我们有6个数字列和分类列的嵌入维度之和为5,因此6+5=11。同样,在最后一层中,out_features的值为2,因为我们只有两种可能的输出。

在实际训练模型之前,我们需要定义损失函数和优化器来训练模型。由于我们正在解决分类问题,所以将使用交叉熵损失。对于优化器函数,我们将使用Adam优化器。

以下脚本定义了损失函数和优化器:

loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

现在我们拥有训练模型所需的一切。以下脚本用于训练模型:

epochs = 300
aggregated_losses = []

for i in range(epochs):
    i += 1
    y_pred = model(categorical_train_data, numerical_train_data)
    single_loss = loss_function(y_pred, train_outputs)
    aggregated_losses.append(single_loss)

    if i%25 == 1:
        print(f'epoch: {i:3} loss: {single_loss.item():10.8f}')

    optimizer.zero_grad()
    single_loss.backward()
    optimizer.step()

print(f'epoch: {i:3} loss: {single_loss.item():10.10f}')

时代数设置为300,这意味着要训练模型,完整的数据集将被使用300次。一个for循环执行了300次,在每次迭代期间,使用损失函数计算损失。每个迭代期间的损失都附加到聚合损失列表中。为更新权重,调用单一损失对象的backward()函数。最后,优化器函数的step()方法更新梯度。在每25个时代之后打印出损失。

上述脚本的输出如下:

epoch:   1 loss: 0.71847951
epoch:  26 loss: 0.57145703
epoch:  51 loss: 0.48110831
epoch:  76 loss: 0.42529839
epoch: 101 loss: 0.39972275
epoch: 126 loss: 0.37837571
epoch: 151 loss: 0.37133673
epoch: 176 loss: 0.36773482
epoch: 201 loss: 0.36305946
epoch: 226 loss: 0.36079505
epoch: 251 loss: 0.35350436
epoch: 276 loss: 0.35540250
epoch: 300 loss: 0.3465710580

以下脚本将损失函数绘制为随着训练轮数的变化:

plt.plot(range(epochs), aggregated_losses)
plt.ylabel('Loss')
plt.xlabel('epoch');


输出结果显示,最初损失迅速下降。大约在第250个时期之后,损失减少得非常少。

进行预测

最后一步是对测试数据进行预测。

为此,我们只需要将分类测试数据和数值测试数据传递给模型类即可。返回的值可以与实际的测试输出值进行比较
以下脚本对测试类进行预测,并打印出测试数据的交叉熵损失。

with torch.no_grad():
    y_val = model(categorical_test_data, numerical_test_data)
    loss = loss_function(y_val, test_outputs)
print(f'Loss: {loss:.8f}')

## output
Loss: 0.36855841

测试集上的损失为0.3685,略高于训练集上达到的0.3465,这表明我们的模型稍微过拟合了。

需要注意的是,由于我们指定输出层将包含2个神经元,因此每个预测将包含2个值。例如,前5个预测值如下:

print(y_val[:5])

## output
tensor([[ 1.2045, -1.3857],
        [ 1.3911, -1.5957],
        [ 1.2781, -1.3598],
        [ 0.6261, -0.5429],
        [ 2.5430, -1.9991]])

这些预测的背后思想是,如果实际输出为0,则索引0处的值应该高于索引1处的值,反之亦然。我们可以使用以下脚本检索列表中最大值的索引:

y_val = np.argmax(y_val, axis=1)
print(y_val[:5])

## output
tensor([0, 0, 0, 0, 0])

由于在最初预测输出的列表中,前五个记录的零索引处的值大于第一索引处的值,因此我们可以看到处理后输出的前五行中有0。

最后,我们可以使用sklearn.metrics模块中的confusion_matrix、accuracy_score和classification_report类来查找测试集准确率、精度和召回率值以及混淆矩阵。

from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

print(confusion_matrix(test_outputs,y_val))
print(classification_report(test_outputs,y_val))
print(accuracy_score(test_outputs, y_val))

## output
[[1527   83]
 [ 224  166]]
              precision    recall  f1-score   support

           0       0.87      0.95      0.91      1610
           1       0.67      0.43      0.52       390

   micro avg       0.85      0.85      0.85      2000
   macro avg       0.77      0.69      0.71      2000
weighted avg       0.83      0.85      0.83      2000

0.8465

输出结果显示,我们的模型达到了84.65%的准确率,考虑到我们随机选择了神经网络模型的所有参数,这是相当令人印象深刻的。我建议您尝试更改模型参数,例如训练/测试拆分、隐藏层数量和大小等,以查看是否可以获得更好的结果。

结论

PyTorch是Facebook开发的常用深度学习库,可用于分类、回归和聚类等各种任务。本文介绍了如何使用PyTorch库对表格数据进行分类。

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
引用\[1\]: 这个引用是一段代码,用于预测生存概率。根据代码中的注释,它是基于某个模型对测试数据进行预测,并将预测结果与实际值进行比较。预测结果以概率的形式给出,如果概率大于某个阈值,则判断为"活着",否则判断为"死亡"。\[1\] 引用\[2\]: 这个引用是一个链接,指向一个关于使用深度学习和OpenCV进行实时目标检测的文章。\[2\] 引用\[3\]: 这个引用是一段代码,用于加载数据并构建TensorFlow的数据集格式。代码中定义了标签列和标签值,并使用make_csv_dataset函数将训练和测试数据加载为TensorFlow的对象格式。然后通过next(iter(raw_train_data))获取一个批次的数据,并打印出示例和标签。\[3\] 根据你的问题,我无法确定你具体想要预测什么。如果你能提供更多的上下文或明确你的问题,我将能够给出更准确的回答。 #### 引用[.reference_title] - *1* *3* [Python使用tensorflow读取csv训练DNN模型(泰坦尼克号生存与死亡概率预测)](https://blog.csdn.net/Truthwave/article/details/129944230)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [深度学习 + OpenCV,Python实现实时视频目标检测](https://blog.csdn.net/weixin_33831196/article/details/87953331)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值