(2-5)基于内容的推荐:文本分类和标签提取

2.5  文本分类和标签提取

文本分类是指将文本数据按照预定义的类别或标签进行分类的任务。它是自然语言处理(NLP)领域中的一个重要问题,具有广泛的应用,例如情感分析、垃圾邮件过滤、新闻分类等。在Python中,有多种方法可以进行文本分类和标签提取,其中常用的方法有三种:传统机器学习、卷积神经网络、循环神经网络。

2.5.1  传统机器学习方法

在Python中,可以使用机器学习技术实现文本分类和标签提取。文本分类是将文本数据分为不同的预定义类别或标签的任务,而标签提取是从文本中提取关键标签或关键词的任务。在接下来的内容中,将简要介绍两种实现文本分类和标签提取的机器学习方法。

1. 朴素贝叶斯分类器(Naive Bayes Classifier)

朴素贝叶斯分类器是一种简单但有效的文本分类方法。它基于朴素贝叶斯定理和特征独立性假设,将文本特征与类别之间的条件概率进行建模。常见的朴素贝叶斯分类器包括多项式朴素贝叶斯(Multinomial Naive Bayes)和伯努利朴素贝叶斯(Bernoulli Naive Bayes)。例如下面是一个使用朴素贝叶斯分类器进行文本分类和标签提取的例子,功能是对电影评论信息进行文本分类。

源码路径:daima/2/pusu.py

from sklearn.feature_extraction.text import CountVectorizer

from sklearn.naive_bayes import MultinomialNB



# 文本数据

texts = [

    "This movie is great!",

    "I loved the acting in this film.",

    "The plot of this book is intriguing.",

    "I didn't enjoy the music in this concert.",

]



# 对文本进行特征提取

vectorizer = CountVectorizer()

X = vectorizer.fit_transform(texts)



# 标签数据

labels = ['Positive', 'Positive', 'Positive', 'Negative']



# 创建朴素贝叶斯分类器模型并训练

clf = MultinomialNB()

clf.fit(X, labels)



# 进行文本分类和标签提取

test_text = "The acting in this play was exceptional."

test_X = vectorizer.transform([test_text])

predicted_label = clf.predict(test_X)



print(f"文本: {test_text}")

print(f"预测标签: {predicted_label}")

在上述代码中,使用了库scikit-learn中的CountVectorizer进行文本特征提取,并使用MultinomialNB实现了朴素贝叶斯分类器。通过将训练好的模型应用于新的文本,可以进行分类和标签提取。 执行后会输出:

文本: The acting in this play was exceptional.

预测标签: ['Positive']

2. 支持向量机(Support Vector Machines,SVM)

支持向量机是一种强大的文本分类算法,可以通过构建高维特征空间并找到最佳的分割超平面来实现分类。SVM在文本分类中的应用主要包括线性支持向量机(Linear SVM)和核支持向量机(Kernel SVM)。核函数可以帮助SVM处理非线性问题,如径向基函数核(Radial Basis Function Kernel)。下面是一个简单的实例,演示了使用支持向量机实现音乐推荐的文本分类的用法。使用音乐的特征描述作为模型的输入,并将音乐的推荐标签作为目标变量进行训练。

源码路径:daima/2/xiang.py

from sklearn.feature_extraction.text import TfidfVectorizer

from sklearn.svm import SVC

from sklearn.metrics import accuracy_score



# 音乐数据

music_features = [

    "This song has a catchy melody and upbeat rhythm.",

    "The lyrics of this track are deep and thought-provoking.",

    "The vocals in this album are powerful and emotional.",

    "I don't like the repetitive beats in this song.",

]



# 推荐标签数据

recommendations = ['Pop', 'Indie', 'Rock', 'Electronic']



# 对音乐特征进行文本特征提取

vectorizer = TfidfVectorizer()

X = vectorizer.fit_transform(music_features)



# 创建支持向量机分类器模型并训练

clf = SVC()

clf.fit(X, recommendations)



# 进行音乐推荐

test_music = "I love the electronic beats in this track."

test_X = vectorizer.transform([test_music])

predicted_recommendation = clf.predict(test_X)



print(f"音乐特征: {test_music}")

print(f"推荐标签: {predicted_recommendation}")

在上述代码中,使用了库scikit-learn中的TfidfVectorizer来提取音乐特征的文本表示,然后使用SVC来构建支持向量机分类器模型,并进行音乐推荐的标签预测。你可以根据实际情况调整训练数据和测试数据,并使用更复杂的特征提取方法和模型调参来提高预测的准确性。执行后会输出:

音乐特征: I love the electronic beats in this track.

推荐标签: ['Electronic']

2.5.2  卷积神经网络

卷积神经网络(Convolutional Neural Network,CNN)是一种在推荐系统中广泛应用的深度学习模型,它在图像处理任务上取得了巨大的成功,并且在自然语言处理领域也得到了广泛应用。CNN在推荐系统中常用于文本分类、图像推荐和音乐推荐等任务,能够从输入数据中提取特征并进行高效的模式识别。

下面简要介绍CNN在推荐系统中的应用和一些关键概念:

  1. 卷积层(Convolutional Layer):卷积层是CNN的核心组成部分,它通过应用卷积操作来提取输入数据的局部特征。在文本分类任务中,卷积层可以识别关键词组合或短语,捕捉文本中的局部模式。
  2. 池化层(Pooling Layer):池化层用于降低卷积层输出的维度,并保留最重要的特征。常用的池化操作包括最大池化(Max Pooling)和平均池化(Average Pooling),它们可以减少数据的大小,并提取最显著的特征。
  3. 全连接层(Fully Connected Layer):全连接层用于将卷积和池化层提取的特征映射到输出标签空间。在推荐系统中,全连接层可以将提取的特征与用户行为数据进行关联,实现个性化推荐。
  4. 嵌入层(Embedding Layer):在文本推荐中,嵌入层将离散的文本输入转换为连续的向量表示。它可以学习单词之间的语义关系,并捕捉文本中的语义信息。
  5. 激活函数(Activation Function):激活函数引入非线性特性,使得CNN能够学习更复杂的模式和特征。常用的激活函数包括ReLU、Sigmoid和Tanh。

在下面的内容中将通过一个具体实例的实现过程详细讲解使用卷积神经网络对花朵图像进行分类的过程让那个。本实例将使用keras.Sequential模型创建图像分类器,并使用preprocessing.image_dataset_from_directory加载数据。

源码路径:daima/2/cnn02.py

1. 准备数据集

本实例使用大约 3,700 张鲜花照片的数据集,数据集包含5 个子目录,每个类别一个目录:

flower_photo/

  daisy/

  dandelion/

  roses/

  sunflowers/

  tulips/

1)下载数据集,代码如下:

import pathlib

dataset_url = "https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz"

data_dir = tf.keras.utils.get_file('flower_photos', origin=dataset_url, untar=True)

data_dir = pathlib.Path(data_dir)

image_count = len(list(data_dir.glob('*/*.jpg')))

print(image_count)

执行后会输出

3670

这说明在数据集中共有3670 张图像,

(2)浏览数据集中“roses”目录中的第一个图像,代码如下:

roses = list(data_dir.glob('roses/*'))

PIL.Image.open(str(roses[0]))

执行后显示数据集中“roses”目录中的第一个图像,如图2-1所示

图2-1  roses”目录中的第一个图像

(3)也可以浏览数据集中“tulips”目录中的第一个图像,代码如下:

tulips = list(data_dir.glob('tulips/*'))

PIL.Image.open(str(tulips[0]))

执行效果如图2-2所示

2-2  tulips”目录中的第一个图像

2. 创建数据集

使用image_dataset_from_directory从磁盘中加载数据集中的图像,然后从头开始编写自己的加载数据集代码。

(1)首先为加载器定义加载参数,代码如下:

batch_size = 32

img_height = 180

img_width = 180

(2)在现实中通常使用验证拆分法创建神经网络模型,在本实例中将使用 80% 的图像进行训练,使用 20% 的图像进行验证。使用80%的图像进行训练的代码如下:

train_ds = tf.keras.preprocessing.image_dataset_from_directory(

  data_dir,

  validation_split=0.2,

  subset="training",

  seed=123,

  image_size=(img_height, img_width),

  batch_size=batch_size)

执行后会输出

Found 3670 files belonging to 5 classes.

Using 2936 files for training.

使用 20% 的图像进行验证的代码如下:

val_ds = tf.keras.preprocessing.image_dataset_from_directory(

  data_dir,

  validation_split=0.2,

  subset="validation",

  seed=123,

  image_size=(img_height, img_width),

  batch_size=batch_size)

执行后会输出

Found 3670 files belonging to 5 classes.

Using 734 files for validation.

可以在数据集的属性class_names中找到类名,每个类名和目录名称的字母顺序对应。例如下面的代码:

class_names = train_ds.class_names

print(class_names)

执行后会显示类名

['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']

(3)可视化数据集中的数据,通过如下代码显示训练数据集中的前 9 张图像。

import matplotlib.pyplot as plt



plt.figure(figsize=(10, 10))

for images, labels in train_ds.take(1):

  for i in range(9):

    ax = plt.subplot(3, 3, i + 1)

    plt.imshow(images[i].numpy().astype("uint8"))

    plt.title(class_names[labels[i]])

    plt.axis("off")

执行效果如图2-3所示

2-3  训练数据集中的前 9 张图像

(4)接下来将通过将这些数据集传递给训练模型model.fit,也可以手动迭代数据集并检索批量图像。代码如下:

for image_batch, labels_batch in train_ds:

  print(image_batch.shape)

  print(labels_batch.shape)

  break

执行后会输出

(32, 180, 180, 3)

(32,)

通过上述输出可知,image_batch是形状的张量(32, 180, 180, 3)。这是一批 32 张形状图像:180x180x3(最后一个维度是指颜色通道 RGB),label_batch是形状的张量(32,),这些都是对应标签32倍的图像。我们可以通过numpy()在image_batch和labels_batch张量将上述图像转换为一个numpy.ndarray。

3. 配置数据集

(1)接下来将配置数据集以提高性能,确保本实例使用缓冲技术以确保可以从磁盘生成数据,而不会导致 I/O 阻塞,下面在加载数据时建议使用的两种重要方法。

  1. Dataset.cache():当从磁盘加载图像后,将图像保存在内存中。这将确保数据集在训练模型时不会成为瓶颈。如果您的数据集太大而无法放入内存,您也可以使用此方法来创建高性能的磁盘缓存。
  2. Dataset.prefetch():在训练时重叠数据预处理和模型执行。

(2)然后进行数据标准化处理,因为RGB 通道值在[0, 255]范围内,这对于神经网络来说并不理想。一般来说,应该设法使输入值变小。在本实例中将使用[0, 1]重新缩放图层将值标准化为范围内。

normalization_layer = layers.experimental.preprocessing.Rescaling(1./255)

(3)可以通过调用 map 将该层应用于数据集:

normalized_ds = train_ds.map(lambda x, y: (normalization_layer(x), y))

image_batch, labels_batch = next(iter(normalized_ds))

first_image = image_batch[0]

print(np.min(first_image), np.max(first_image))

执行后会输出:

0.0 0.9997713

或者,可以在模型定义中包含该层,这样可以简化部署,本实例将使用第二种方法。

4. 创建模型

本实例的模型由三个卷积块组成,每个块都有一个最大池层。有一个全连接层,上面有128个单元,由激活函数激活。该模型尚未针对高精度进行调整,本实例的目标是展示一种标准方法。代码如下:

num_classes = 5



model = Sequential([

  layers.experimental.preprocessing.Rescaling(1./255, input_shape=(img_height, img_width, 3)),

  layers.Conv2D(16, 3, padding='same', activation='relu'),

  layers.MaxPooling2D(),

  layers.Conv2D(32, 3, padding='same', activation='relu'),

  layers.MaxPooling2D(),

  layers.Conv2D(64, 3, padding='same', activation='relu'),

  layers.MaxPooling2D(),

  layers.Flatten(),

  layers.Dense(128, activation='relu'),

  layers.Dense(num_classes)

])

5. 编译模型

(1)在本实例中使用optimizers.Adam优化器和losses.SparseCategoricalCrossentropy损失函数。要想查看每个训练时期的训练和验证准确性,需要传递metrics参数。代码如下:

model.compile(optimizer='adam',

              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),

              metrics=['accuracy'])

(2)使用模型的函数summary查看网络中的所有层,代码如下:

model.summary()

6. 训练模型

开始训练模型,代码如下:

epochs=10

history = model.fit(

  train_ds,

  validation_data=val_ds,

  epochs=epochs

)

执行后会输出

Epoch 1/10

92/92 [========================

///省略部分结果

Epoch 10/10

92/92 [==============================] - 1s 10ms/step - loss: 0.0566 - accuracy: 0.9847 -

7. 可视化训练结果

在训练集和验证集上创建损失图和准确度图,然后绘制可视化结果,代码如下:

acc = history.history['accuracy']

val_acc = history.history['val_accuracy']



loss = history.history['loss']

val_loss = history.history['val_loss']



epochs_range = range(epochs)



plt.figure(figsize=(8, 8))

plt.subplot(1, 2, 1)

plt.plot(epochs_range, acc, label='Training Accuracy')

plt.plot(epochs_range, val_acc, label='Validation Accuracy')

plt.legend(loc='lower right')

plt.title('Training and Validation Accuracy')



plt.subplot(1, 2, 2)

plt.plot(epochs_range, loss, label='Training Loss')

plt.plot(epochs_range, val_loss, label='Validation Loss')

plt.legend(loc='upper right')

plt.title('Training and Validation Loss')

plt.show()

执行后的效果如图2-4所示

2-4  可视化损失图和准确度图

8. 过拟合处理:数据增强

从可视化损失图和准确度的图中执行效果可以看出,训练准确率和验证准确率相差很大,模型在验证集上的准确率只有 60% 左右。训练准确度随着时间线性增加,而验证准确度在训练过程中停滞在 60% 左右。此外,训练和验证准确性之间的准确性差异是显而易见的,这是过度拟合的迹象。

当训练样例数量较少时,模型有时会从训练样例中的噪声或不需要的细节中学习,这在一定程度上会对模型在新样例上的性能产生负面影响。这种现象被称为过拟合。这意味着该模型将很难在新数据集上泛化。在训练过程中有多种方法可以对抗过度拟合。

过拟合通常发生在训练样本较少时,数据增强采用的方法是从现有示例中生成额外的训练数据,方法是使用随机变换来增强它们,从而产生看起来可信的图像。这有助于将模型暴露于数据的更多方面并更好地概括。

(1)通过使用tf.keras.layers.experimental.preprocessing实效数据增强,可以像其他层一样包含在模型中,并在 GPU上运行。代码如下:

data_augmentation = keras.Sequential(

  [

    layers.experimental.preprocessing.RandomFlip("horizontal",

                                                 input_shape=(img_height,

                                                              img_width,

                                                              3)),

    layers.experimental.preprocessing.RandomRotation(0.1),

    layers.experimental.preprocessing.RandomZoom(0.1),

  ]

)

此时通过对同一图像多次应用数据增强技术,下面是可视化数据增的代码:

plt.figure(figsize=(10, 10))

for images, _ in train_ds.take(1):

  for i in range(9):

    augmented_images = data_augmentation(images)

    ax = plt.subplot(3, 3, i + 1)

    plt.imshow(augmented_images[0].numpy().astype("uint8"))

    plt.axis("off")

执行后的效果如图2-5所示

2-5  数据增强

9. 过拟合处理:将Dropout引入网络

接下来介绍另一种减少过拟合的技术:将Dropout引入网络,这是一种正则化处理形式。当将 Dropout 应用于一个层时,它会在训练过程中从该层中随机删除(通过将激活设置为零)许多输出单元。Dropout将一个小数作为其输入值,例如0.1、0.2、0.4 等,这意味着从应用层中随机丢弃 10%、20% 或 40% 的输出单元。请看下面的代码,创建一个新的神经网络layers.Dropout,然后使用增强图像对其进行训练。

model = Sequential([

  data_augmentation,

  layers.experimental.preprocessing.Rescaling(1./255),

  layers.Conv2D(16, 3, padding='same', activation='relu'),

  layers.MaxPooling2D(),

  layers.Conv2D(32, 3, padding='same', activation='relu'),

  layers.MaxPooling2D(),

  layers.Conv2D(64, 3, padding='same', activation='relu'),

  layers.MaxPooling2D(),

  layers.Dropout(0.2),

  layers.Flatten(),

  layers.Dense(128, activation='relu'),

  layers.Dense(num_classes)

])

10. 重新编译和训练模型

经过前面的过拟合处理接下来重新编译和训练模型,重新编译模型的代码如下:

model.compile(optimizer='adam',

              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),

              metrics=['accuracy'])

model.summary()

Model: "sequential_2"

重新训练模型的代码如下:

epochs = 15

history = model.fit(

  train_ds,

  validation_data=val_ds,

  epochs=epochs

)

执行后会输出

Epoch 1/15

92/92 [==============================] - 2s 13ms/step - loss: 1.2685 - accuracy: 0.4465 - val_loss: 1.0464 - val_accuracy: 0.5899

///省略部分代码

Epoch 15/15

92/92 [==============================] - 1s 11ms/step - loss: 0.4930 - accuracy: 0.8096 - val_loss: 0.6705 - val_accuracy: 0.7384

在使用数据增强和Dropout处理后,过拟合比以前少了,训练和验证的准确性更接近。接下来重新可视化训练结果,代码如下:

acc = history.history['accuracy']

val_acc = history.history['val_accuracy']



loss = history.history['loss']

val_loss = history.history['val_loss']



epochs_range = range(epochs)



plt.figure(figsize=(8, 8))

plt.subplot(1, 2, 1)

plt.plot(epochs_range, acc, label='Training Accuracy')

plt.plot(epochs_range, val_acc, label='Validation Accuracy')

plt.legend(loc='lower right')

plt.title('Training and Validation Accuracy')



plt.subplot(1, 2, 2)

plt.plot(epochs_range, loss, label='Training Loss')

plt.plot(epochs_range, val_loss, label='Validation Loss')

plt.legend(loc='upper right')

plt.title('Training and Validation Loss')

plt.show()

执行后效果如图2-6所示

2-6  可视化结果

11. 预测新数据

最后使用我们最新创建的模型对未包含在训练或验证集中的图像进行分类处理,代码如下:

sunflower_url = "https://storage.googleapis.com/download.tensorflow.org/example_images/592px-Red_sunflower.jpg"
sunflower_path = tf.keras.utils.get_file('Red_sunflower', origin=sunflower_url)

img = keras.preprocessing.image.load_img(
    sunflower_path, target_size=(img_height, img_width)
)
img_array = keras.preprocessing.image.img_to_array(img)
img_array = tf.expand_dims(img_array, 0) # Create a batch

predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])

print(
    "This image most likely belongs to {} with a {:.2f} percent confidence."
    .format(class_names[np.argmax(score)], 100 * np.max(score))
)

执行后会输出

Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/592px-Red_sunflower.jpg

122880/117948 [===============================] - 0s 0us/step

This image most likely belongs to sunflowers with a 99.36 percent confidence.

大家需要注意的是,数据增强和Dropout层在推理时处于非活动状态。

2.5.3  循环神经网络

循环神经网络(Recurrent Neural Network,RNN)是一种常用于处理序列数据的神经网络模型。在推荐系统中,RNN被广泛应用于序列建模和推荐任务,例如用户行为序列分析、时间序列数据预测、文本生成等。

RNN的特点是能够处理具有时间依赖性的数据,通过记忆过去的信息来影响当前的输出。与传统的前馈神经网络不同,RNN引入了循环连接,使得信息可以在网络内部进行传递和更新。这种循环连接的设计使得RNN在处理序列数据时具有优势。

在Python中,可以使用多种库和框架来构建和训练RNN模型,其中最常用的是TensorFlow和PyTorch。这些工具提供了丰富的RNN实现,包括常用的RNN变体(如长短期记忆网络(LSTM)和门控循环单元(GRU)),以及各种辅助函数和工具,方便进行模型构建、训练和评估。请看下面的实例文件xun.py,功能是使用循环神经网络(LSTM)实现文本分类。

源码路径:daima/2/xun.py

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

# 自定义数据集类
class SentimentDataset(Dataset):
    def __init__(self, texts, labels):
        self.texts = texts
        self.labels = labels

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, idx):
        text = self.texts[idx]
        label = self.labels[idx]
        return text, label

# 自定义循环神经网络模型
class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(LSTMModel, self).__init__()
        self.hidden_size = hidden_size
        self.embedding = nn.Embedding(input_size, hidden_size)
        self.lstm = nn.LSTM(hidden_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        embedded = self.embedding(x)
        output, _ = self.lstm(embedded)
        output = self.fc(output[:, -1, :])  # 取最后一个时刻的输出
        return output

# 准备数据
texts = ["I love this movie", "This film is terrible", "The acting was superb"]
labels = [1, 0, 1]  # 1代表正面情感,0代表负面情感

# 构建词汇表
vocab = set(' '.join(texts))
char_to_idx = {ch: i for i, ch in enumerate(vocab)}

# 创建数据集和数据加载器
dataset = SentimentDataset(texts, labels)
data_loader = DataLoader(dataset, batch_size=1, shuffle=True)

# 定义超参数
input_size = len(vocab)
hidden_size = 128
output_size = 2  # 正面和负面两种情感
num_epochs = 10

# 实例化模型
model = LSTMModel(input_size, hidden_size, output_size)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters())

# 训练模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
criterion.to(device)

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0
    for inputs, labels in data_loader:
        inputs = [char_to_idx[ch] for ch in inputs[0]]
        inputs = torch.tensor(inputs).unsqueeze(0).to(device)
        labels = torch.tensor(labels).to(device)

        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()

    print(f"Epoch {epoch+1}/{num_epochs}, Loss: {epoch_loss/len(data_loader):.4f}")

在上述代码中定义了一个自定义的数据集类SentimentDataset来处理情感分类的文本数据,然后定义了一个简单的LSTM模型LSTMModel,包含一个嵌入层、一个LSTM层和一个全连接层。我们使用自定义数据集类加载样本文本和相应的标签,并根据需要将文本转换为整数索引序列。然后,我们使用数据加载器迭代数据,并在每个批次上训练模型。在训练过程中迭代数据加载器,将每个样本的输入文本转换为整数索引序列,并将其作为输入传递给模型进行训练。使用交叉熵损失函数计算损失,并使用反向传播和优化器更新模型的参数。执行后会输出:

Epoch 1/10, Loss: 0.7174

Epoch 2/10, Loss: 0.5884

Epoch 3/10, Loss: 0.5051

Epoch 4/10, Loss: 0.4218

Epoch 5/10, Loss: 0.3467

Epoch 6/10, Loss: 0.2571

Epoch 7/10, Loss: 0.1835

Epoch 8/10, Loss: 0.1147

Epoch 9/10, Loss: 0.0616

Epoch 10/10, Loss: 0.0392

注意:请根据你的实际数据和需求对代码进行适当的修改,包括修改数据集类、调整模型结构、修改超参数等。

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码农三叔

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

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

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

打赏作者

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

抵扣说明:

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

余额充值