Keras自然语言处理(十五)

第十二章 开发CNN模型:用于情感分析

词嵌入是用来表示文本的技术,其中具有相同含义的不同词之间具有类似的向量表示。这是一项巨大的突破,他使得神经网络模型在一系列具有挑战性的自然语言问题上表现出色。本章要介绍的是如何使用卷积神经网络和词嵌入来对电影评论分类,接下来你将了解:

  • 如何使用深度学习方法为电影评论分类做准备
  • 如何使用词嵌入和卷积神经网络开发分类模型
  • 如何评估所开发的模型

12.1 概述

本章分为以下几个部分:

  1. 电影评论数据集
  2. 准备数据
  3. 构建CNN网络训练模型
  4. 评估所训练的模型

12.2 电影数据集

在这里插入图片描述

12.3 数据准备

下面我我们需要完成:

  1. 将数据集分成训练集和测试集
  2. 加载并清洗数据
  3. 定义词汇表

12.3.1 将数据集分为训练集和测试集

我们假设正在开发一个系统,用于预测用户对一个电影的评论是正面的还是负面的。首先我们得需要一个训练集,让模型在训练集上训练数据,然后使用测试集对训练的模型做评估(这里省略了验证集和调整模型的过程)。我们要在数据准备之前就要将数据集拆分为训练数据和测试数据,这个约束一直贯穿与整个模型的评估中。同时测试集的内容不能出现在训练集上,训练的集的内容也不能出现在测试集中。这样要保证测试的内容对训练数据是完全未知的。为此我们将100个正面和100个负面评论留作测试集,其他的数据作为训练集(这里假设数据集中的内容是完全分离的)

12.3.2 加载并清洗数据

这里的文本数据已经非常干净了,不需要太多的准备工作,在我之前的文章中已经给出的代码进行文本清洗。下面我们再复习一遍数据清洗的过程:

  1. 用空格来分词
  2. 单词中删除标点
  3. 删除不完全由字母字符组成的单词
  4. 删除停用词
  5. 删除长度小于等于1的单词
import re,string,os
from nltk.corpus import stopwords
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text =f.read()
    return text
def clean_doc(doc):
    tokens = doc.split()
    re_punc =re.compile('[%s]'%re.escape(string.punctuation))
    tokens = [re_punc.sub(' ',w)for w in tokens]
    tokens = [w for w in tokens if w.isalpha()]
    stop_words = set(stopwords.words('english'))
    tokens = [w for w in tokens if not w in stop_words]
    tokens = [w for w in tokens if len(w)>1]
    return tokens
file_path = r"F:\5-model and data\movie review\txt_sentoken\pos"
target_file = 'cv012_29576.txt'
file_name = os.path.join(file_path,target_file)
text = load_doc(file_name)
tokens = clean_doc(text)
print(tokens)

其结果为:

['synopsis', 'bobby', 'garfield', 'yelchin', 'lives', 'small', 'town', 'mirthless', 'widowed', 'mother', 'hope', 'davis', 'world', 'revolves', 'around', 'friends', 'especially', 'spritely', 'carol', 'boorem', 'one', 'day', 'new', 'boarder', 'arrives', 'house', 'ted', 'brautigan', 'hopkins', 'enigmatic', 'man', 'bobby', 'takes', 'immediate', 'liking', 'bond', 'bobby', 'ted', 'deepens', 'bobby', 'becomes', 'privy', 'great', 'secret', 'event', 'change', 'lives', 'forever', 'review', 'small', 'enchanting', 'movie', 'hearts', 'atlantis', 'easily', 'recalls', 'another', 'film', 'stand', 'terms', 'setting', 'sentiment', 'conveys', 'hearts', 'tribute', 'magic', 'childhood', 'summers', 'days', 'seem', 'neverending', 'nothing', 'means', 'closest', 'friends', 'unlike', 'stand', 'supernatural', 'element', 'hearts', 'although', 'key', 'plot', 'prominent', 'like', 'stand', 'mostly', 'film', 'benefits', 'greatly', 'superb', 'casting', 'yelchin', 'good', 'bobby', 'finding', 'good', 'mix', 'innocence', 'resignation', 'splendid', 'still', 'boorem', 'praised', 'highly', 'work', 'along', 'came', 'spider', 'simply', 'radiant', 'carol', 'hopkins', 'despite', 'playing', 'quiet', 'introspective', 'character', 'ted', 'nonetheless', 'commands', 'attention', 'every', 'time', 'onscreen', 'less', 'successful', 'davis', 'whose', 'strident', 'elizabeth', 'comes', 'across', 'overly', 'cartoonish', 'also', 'found', 'odd', 'bobby', 'friend', 'sully', 'whose', 'death', 'adult', 'sets', 'flashback', 'framing', 'device', 'paid', 'virtually', 'attention', 'direction', 'lovely', 'without', 'cloying', 'despite', 'general', 'lack', 'incident', 'never', 'ceases', 'weave', 'spell', 'audience']

12.3.3 定义词汇表

使用文本模型时定义文本已知单词的词汇表很重要。单词越多,文档就会越大,因此将单词限定显得很重要,另一方面哪些单词被限定,使用什么方法去限定,是否有包含情感类的单词被限定,这些我们都很难事先知晓,更重要的是我们需要测试不同的文本来确定如何构建有用的词汇表。从上一小节中我们已经知道如何从词汇表中删除标点和数字,我们可以对所有文档重复这个操作,并构建一组已知的单词。我们可以使用Counter类来构建一个词汇表,它是一个单词和计数的字典,可以让我们轻松更新和查询。每个文档都可以添加到Counter(通过add_doc_to_vocab函数)我们可以先处理负面评论目录中的所有评论,然后再处理正面评价目录中的所有评论。
完整过程:

import re,string,os
from nltk.corpus import stopwords
from collections import Counter
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text =f.read()
    return text
def clean_doc(doc):
    tokens = doc.split()
    re_punc =re.compile('[%s]'%re.escape(string.punctuation))
    tokens = [re_punc.sub(' ',w)for w in tokens]
    tokens = [w for w in tokens if w.isalpha()]
    stop_words = set(stopwords.words('english'))
    tokens = [w for w in tokens if not w in stop_words]
    tokens = [w for w in tokens if len(w)>1]
    return tokens
def add_doc_to_vocab(filename,vocab):
    doc = load_doc(filename)
    tokens = clean_doc(doc)
    vocab.update(tokens)
def process_docs(directory,vocab):
    for filaname in os.listdir(directory):
        if filaname.startswith('cv9'):
            continue
        path = os.path.join(directory,filaname)
        add_doc_to_vocab(path,vocab)
vocab =Counter()
pos_path = r"F:\5-model and data\movie review\txt_sentoken\pos"
neg_path = r"F:\5-model and data\movie review\txt_sentoken\neg"
process_docs(pos_path,vocab)
process_docs(neg_path,vocab)
print(len(vocab))
print(vocab.most_common(50))

其结果:

36053
[('film', 7974), ('one', 4939), ('movie', 4815), ('like', 3193), ('even', 2261), ('good', 2073), ('time', 2039), ('story', 1899), ('would', 1843), ('much', 1823), ('also', 1757), ('get', 1723), ('character', 1699), ('two', 1642), ('characters', 1618), ('first', 1586), ('see', 1553), ('way', 1515), ('well', 1477), ('make', 1418), ('really', 1400), ('little', 1347), ('films', 1338), ('life', 1329), ('plot', 1286), ('people', 1267), ('could', 1248), ('bad', 1246), ('scene', 1240), ('never', 1197), ('best', 1176), ('new', 1139), ('scenes', 1132), ('many', 1129), ('man', 1122), ('know', 1092), ('movies', 1027), ('great', 1011), ('another', 992), ('action', 980), ('love', 975), ('us', 967), ('go', 950), ('director', 947), ('something', 944), ('end', 943), ('still', 935), ('seems', 930), ('back', 921), ('made', 911)]

现在删除频率比较低的(只出现1次的)单词

min_occurrence = 2
tokens = [t for t,c in vocab.items() if c>=min_occurrence]
print(len(tokens))

结果:

36053
23275

最后将词汇表保存在一个名为12_vocab.txt的文件中。以后我们能加载和过滤电影评论,然后在对评论进行编码建模。这里我们定义一个ssave_list的函数

def save_list(lines,filename):
    data = '\n'.join(lines)
    with open(filename,'w',encoding='utf-8')as f:
        f.write(data)
save_list(tokens,'12_vocab.txt')

结合在一起:

import re,string,os
from nltk.corpus import stopwords
from collections import Counter
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text =f.read()
    return text
def clean_doc(doc):
    tokens = doc.split()
    re_punc =re.compile('[%s]'%re.escape(string.punctuation))
    tokens = [re_punc.sub(' ',w)for w in tokens]
    tokens = [w for w in tokens if w.isalpha()]
    stop_words = set(stopwords.words('english'))
    tokens = [w for w in tokens if not w in stop_words]
    tokens = [w for w in tokens if len(w)>1]
    return tokens
def add_doc_to_vocab(filename,vocab):
    doc = load_doc(filename)
    tokens = clean_doc(doc)
    vocab.update(tokens)
def process_docs(directory,vocab):
    for filaname in os.listdir(directory):
        if filaname.startswith('cv9'):
            continue
        path = os.path.join(directory,filaname)
        add_doc_to_vocab(path,vocab)
def save_list(lines,filename):
    data = '\n'.join(lines)
    with open(filename,'w',encoding='utf-8')as f:
        f.write(data)
vocab =Counter()
pos_path = r"F:\5-model and data\movie review\txt_sentoken\pos"
neg_path = r"F:\5-model and data\movie review\txt_sentoken\neg"
process_docs(pos_path,vocab)
process_docs(neg_path,vocab)
print(len(vocab))
print(vocab.most_common(50))
min_occurrence = 2
tokens = [t for t,c in vocab.items() if c>=min_occurrence]
print(len(tokens))
save_list(tokens,'12_vocab.txt')

12.4 训练CNN

在本节中,我们将使用CNN来对电影评论作分类。
首先,加载12_vocab.txt文件(词汇表文件)

def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
vocab_file = '12_vocab.txt'
vocab = load_doc(vocab_file)
vocab =set(vocab.split())

接下来定义process_docs函数

def process_docs(directory,vocab,is_train):
    documents = list()
    for filename in os.listdir(directory):
        if is_train and filename.startswith('cv9'):
            continue
        if not is_train and not filename.startswith('cv9'):
            continue
        path = os.path.join(directory,filename)
        doc = load_doc(path)
        tokens = clean_doc(doc,vocab)
    documents.append(tokens)
    return documents

我们调用process_docs处理neg和pos目录,并将评论组合到单个训练或测试数据集中,同时为数据集添加分类标签,下面定义load_clean_dataset函数将加载所有评论并为训练和测试数据添加标签。

def load_clean_dataset(vocab,is_train):
    neg_path = r"F:\5-model and data\movie review\txt_sentoken\neg"
    pos_path = r"F:\5-model and data\movie review\txt_sentoken\pos"
    pos = process_docs(pos_path, vocab,is_train)
    neg = process_docs(neg_path, vocab,is_train)
    docs = pos +neg
    labels = ny.array([0 for _ in range(len(neg))]+[1 for _ in range(len(pos))])
    return docs,labels

下面将整个文档编码成整数序列。Keras的Embedding层需要整数输入,其中每个整数映射到单个标记,该标记在嵌入中具有特定的实数值的矢量表示。这些向量在训练开始时是随机的,随着训练对网络来说含义会逐渐明确。我们可以使用Keras API中的tokenizer类将训练文档编码为整数序列,首先我们创建一个Tokenizer类的示例,然后使用训练数据中的所有文档来训练它,这类示例中会保存训练数据集中所有标记的词汇表以及词汇表中的单词到唯一整数的一一映射。我们可以使用我们的词汇表文件轻松地开发映射,下面是create_tokenizer函数将根据训练数据准备一个Tokenizer

def create_tokenizer(lines):
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(lines)
    return tokenizer

现在我们已经准备好了单词到整数的映射,我们可以使用它来对训练数据中的评论进行编码,我们可以通过调用Tokenizer上的text_to_sequence函数来实现。我们还需要确保所有文档具有相同的长度,我们就将所有评论中最长的作为向量长度,比较短的评论在转化为向量的时候就用0来填充,我们使用Keras的pad_sequence函数来实现

max_length = max([len(s.split())for s in train_docs])
print('Maximum length: %d'%max_length)

然后使用最大长度作为函数的参数来进行整数编码和填充序列

def encode_docs(tokenizer,max_langth,docs):
    encoded = tokenizer.text_to_sequences(docs)
    padded = pad_sequences(encoded,maxlen=max_length,padding='post')
    return padded

接下来定义神经网路模型,该模型将使用Embeddin层作为第一层,Embedding层需要指定词汇表大小,向量空间的大小为输入文档的最大长度。词汇表大小是我们词汇表中单词总数,加上一个未知单词,这可以对文档进行整数编码的词汇集长度和词汇大小:

vocab_size = len(tokenizer.word_index)+1
print('Vocabulary size :%d'%vocab_size)

我们这里使用100维向量空间。最后在填充期间使用评论文档最大长度的max_length。下面我们列出完整的模型定义,包括Embedding层,卷积层,卷积层设置为32个8*8的卷积核,激活函数为:relu接下来是池化层,然后用展开层,将来自CNN模型的输出数据展平,最后将展平的数据输入到一个全连接层中,使用sigmoid激活函数来判定评论是正面还是负面的,输出值介于0-1之间。

def define_model(vocab_size,max_length):
    model = Sequential()
    model.add(Embedding(vocab_size,100,input_length=max_length))
    model.add(Conv1D(filters=32,kernel_size=8,activation='relu'))
    model.add(BatchNormalization())
    model.add(MaxPooling1D(pool_size=2))
    model.add(Flatten())
    model.add(Dense(16,activation='relu'))
    model.add(Dense(1,activation='sigmoid'))
    model.compile(
        loss='binary_crossentropy',
        optimizer='adam',
        metrics=['accuracy']
    )
    model.summary()
    return model

将所有结合起来:

import os,re,string
import numpy as ny
from tensorflow.python.keras.preprocessing.text import Tokenizer
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Dense,Conv1D,Flatten,MaxPooling1D,Embedding,BatchNormalization
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
def clean_doc(doc,vocab):
    tokens = doc.split()
    re_punc =re.compile('[%s]'%re.escape(string.punctuation))
    tokens = [re_punc.sub(' ',w)for w in tokens]
    tokens = [w for w in tokens if w.isalpha()]
    tokens = [w for w in tokens if w in vocab]
    tokens = ' '.join(tokens)
    return tokens
def process_docs(directory,vocab,is_train):
    documents = list()
    for filename in os.listdir(directory):
        if is_train and filename.startswith('cv9'):
            continue
        if not is_train and not filename.startswith('cv9'):
            continue
        path = os.path.join(directory,filename)
        doc = load_doc(path)
        tokens = clean_doc(doc,vocab)
    documents.append(tokens)
    return documents
def load_clean_dataset(vocab,is_train):
    pos_path = r"F:\5-model and data\movie review\txt_sentoken\pos"
    neg_path = r"F:\5-model and data\movie review\txt_sentoken\neg"
    pos = process_docs(pos_path, vocab,is_train)
    neg = process_docs(neg_path, vocab,is_train)
    docs = pos +neg
    labels = ny.array([0 for _ in range(len(neg))]+[1 for _ in range(len(pos))])
    return docs,labels
def create_tokenizer(lines):
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(lines)
    return tokenizer
def encode_docs(tokenizer,max_length,docs):
    encoded = tokenizer.texts_to_sequences(docs)
    padded = pad_sequences(encoded,maxlen=max_length,padding='post')
    return padded
def define_model(vocab_size,max_length):
    model = Sequential()
    model.add(Embedding(vocab_size,100,input_length=max_length))
    model.add(Conv1D(filters=32,kernel_size=8,activation='relu'))
    model.add(BatchNormalization())
    model.add(MaxPooling1D(pool_size=2))
    model.add(Flatten())
    model.add(Dense(16,activation='relu'))
    model.add(Dense(1,activation='sigmoid'))
    model.compile(
        loss='binary_crossentropy',
        optimizer='adam',
        metrics=['accuracy']
    )
    model.summary()
    return model

vocab_file = '12_vocab.txt'
vocab = load_doc(vocab_file)
vocab =set(vocab.split())
train_docs ,y_train = load_clean_dataset(vocab,True)
tokenizer = create_tokenizer(train_docs)
vocab_size = len(tokenizer.word_index)+1
print('Vocabulary size :%d'%vocab_size)
max_length = max([len(s.split())for s in train_docs])
print('Maximum length: %d'%max_length)
x_trian =encode_docs(tokenizer,max_length,train_docs)
model = define_model(vocab_size=vocab_size,max_length=max_length)
history =model.fit(
    x_trian,
    y_train,
    epochs=10
)
import time
now = time.strftime('%Y-%m-%d %H %M %S')
model_path = r"E:\1- data\models"
name = now +'-emotion-analysis.h5'
model_name = os.path.join(model_path,name)
model.save(model_name)
import matplotlib.pyplot as plt
acc = history.history['acc']
loss = history.history['loss']
epochs = range(1,len(acc)+1)
plt.plot(epochs,acc,label = 'Training acc')
plt.title('Training accuracy')
plt.legend()
plt.figure()
plt.plot(epochs,loss,label = 'Training loss')
plt.title('Training loss')
plt.legend()
plt.show()

下面给出运行结果:在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
相比前面的过程,可以看出卷积网络给出的结果更稳定,训练次数更少。

12.5 评估模型

在本节中我们将使用上面保存的模型来预测新的数据,首先我们可以使用内置的evaluate函数来评估模型在训练和测试数据中的表现,这要求我们加载并处理训练和测试文本

train_docs,y_tain = load_clean_dataset(vocab,True)
test_docs,y_test = load_clean_dataset(vocab,False)
tokenizer =create_tokenizer(train_docs)
vocab_size = len(tokenizer.word_index)+1
print('Vocabulary size :%d'%vocab_size)
max_length = max([len(s.split())for s in train_docs])
print('Maximum length: %d'%max_length)
x_trian = encode_docs(tokenizer,max_length,train_docs)
x_test = encode_docs(tokenizer,max_length,test_docs)

然后必须与训练集上相同的文本编码方案来准备新数据,准备好后,可以通过调用模型上的perdict函数来进行预测。以下是predict_sentiment()的函数将对给定的电影评论文本进行编码和填充,并根据百分比和标签返回。

def predict_sentiment(review,vocab,tokenizer,max_length,model):
    line = clean_doc(review,vocab)
    padded = encode_docs(tokenizer,max_length,[line])
    #predict sentiment
    yhat =model.predict(padded)
    percent_pos= yhat[0,0]
    if round(percent_pos) ==0:
        return (1-percent_pos),'Negative'
    return percent_pos ,'Positive'

整个过程:

import os,re,string
import numpy as ny
from tensorflow.python.keras.preprocessing.text import Tokenizer
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
from tensorflow.python.keras.models import load_model
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
def clean_doc(doc,vocab):
    tokens = doc.split()
    re_punc =re.compile('[%s]'%re.escape(string.punctuation))
    tokens = [re_punc.sub(' ',w)for w in tokens]
    tokens = [w for w in tokens if w in vocab]
    tokens = ' '.join(tokens)
    return tokens
def process_docs(directory,vocab,is_train):
    documents = list()
    for filename in os.listdir(directory):
        if is_train and filename.startswith('cv9'):
            continue
        if not is_train and not filename.startswith('cv9'):
            continue
        path = os.path.join(directory,filename)
        doc = load_doc(path)
        tokens = clean_doc(doc,vocab)
    documents.append(tokens)
    return documents
def load_clean_dataset(vocab,is_train):
    pos_path = r"F:\5-model and data\movie review\txt_sentoken\pos"
    neg_path = r"F:\5-model and data\movie review\txt_sentoken\neg"
    pos = process_docs(pos_path, vocab,is_train)
    neg = process_docs(neg_path, vocab,is_train)
    docs = pos +neg
    labels = ny.array([0 for _ in range(len(neg))]+[1 for _ in range(len(pos))])
    return docs,labels
def create_tokenizer(lines):
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(lines)
    return tokenizer
def encode_docs(tokenizer,max_length,docs):
    encoded = tokenizer.texts_to_sequences(docs)
    padded = pad_sequences(encoded,maxlen=max_length,padding='post')
    return padded
def predict_sentiment(review,vocab,tokenizer,max_length,model):
    line = clean_doc(review,vocab)
    padded = encode_docs(tokenizer,max_length,[line])
    #predict sentiment
    yhat =model.predict(padded)
    percent_pos= yhat[0,0]
    if round(percent_pos) ==0:
        return (1-percent_pos),'Negative'
    return percent_pos ,'Positive'
vocab_file = '12_vocab.txt'
vocab = load_doc(vocab_file)
vocab =set(vocab.split())
train_docs,y_tain = load_clean_dataset(vocab,True)
test_docs,y_test = load_clean_dataset(vocab,False)
tokenizer =create_tokenizer(train_docs)
vocab_size = len(tokenizer.word_index)+1
print('Vocabulary size :%d'%vocab_size)
max_length = max([len(s.split())for s in train_docs])
print('Maximum length: %d'%max_length)
x_trian = encode_docs(tokenizer,max_length,train_docs)
x_test = encode_docs(tokenizer,max_length,test_docs)
model_path = r"E:\1- data\models"
model_name = "2019-10-17 10 38 13-emotion-analysis.h5"
model = load_model(os.path.join(model_path,model_name))
loss,accuracy = model.evaluate(x_trian,y_tain)
test_loss,test_accuracy = model.evaluate(x_test,y_test)
print('Trian Accuracy is %.2f'%(accuracy*100))
print('Test accuracy is %.2f'%(test_accuracy*100))
text = 'it is a good film,we all like it'
percent, sentiment = predict_sentiment(text,vocab,tokenizer,max_length,model)
print('Review: [%s]\nSentiment:%s (%.3f%%)'%(text,sentiment,percent*100))
text =' this film is so bad'
percent, sentiment = predict_sentiment(text,vocab,tokenizer,max_length,model)
print('Review: [%s]\nSentiment:%s (%.3f%%)'%(text,sentiment,percent*100))

结果:

2/2 [==============================] - 1s 696ms/sample - loss: 0.2853 - acc: 1.0000

2/2 [==============================] - 0s 998us/sample - loss: 0.6978 - acc: 0.5000
Trian Accuracy is 100.00
Test accuracy is 50.00
Review: [it is a good film,we all like it]
Sentiment:Positive (53.118%)
Review: [ this film is so bad]
Sentiment:Positive (53.049%)

可以说我的结果是很不理想,因为模型权重的初始化具有随机性,结果不太一样,大家可以多运行几次,或者更改测试集(在process_docs函数中将‘cv9’改成其他的)。

12.6 扩展

本节列出一些扩展,可能是你希望尝试的想法:

  1. 数据清洗,更多的清洗方案,保留一些连词
  2. 截断序列。如果长序列也其他序列不同,则可以考虑评论长度的分布来截断评论。
  3. 词汇表。可以考虑增大或者减小词汇表的数量
  4. 过滤器的内核。可以尝试调整过滤器的内核大小
  5. epoch和批量大小。该模型似乎很快过拟合,尝试训练epoch和批大小的等同配置,并使用测试数据集作为验证集,
  6. 更深的网络结构。
  7. 使用预训练嵌入层。尝试在Embedding层中使用Glove预训练结构
  8. 更长的测试评论。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值