Keras自然语言处理(二十)

第十七章 开发文本生成的语言模型

语言模型可以根据序列中已经观察到的单词来预测下一个单词的概率,神经网路模型是用于开发统计语言模型的优选方法,他们使用了分布式表示,具有相识含义的不同单词具有相识的表示,他们在进行预测的时候可以使用最近观察到的单词的上下文,在本章中你将了解到如何使用python中的深度学习开发统计语言模型,接下来你将了解:

  • 如何准备文本(为模型准备数据)
  • 如何设计和拟合具有可训练词嵌入层额LSTM的神经网络
  • 如何使用学习的模型生成具有与原文本类似的具有统计属性的新文本

17.1 概述

本章分为以下几个部分:

  1. 文本数据:柏拉图的理想国
  2. 数据准备
  3. 训练模型
  4. 使用模型

17.2 文本数据:柏拉图的理想国

理想国是古希腊哲学家柏拉图最著名的作品,它以对话的形式讲述了城市国家内秩序和正义。整本书可以在网上获得,也可以在我的上传资源中寻找17-republic.txt文件。(下载链接https://download.csdn.net/download/weixin_44755244/11932631)如果你们下载其他的文本文件,应该删除前面分析信息,开始应该是:
在这里插入图片描述这里我们使用的文件就是删除了前面大段分析,并将文件另保存为17-republic.txt的文件。

17.3 数据准备

我们将准备建模开始,第一步是查看数据。

17.3.1 查看文本

在这里插入图片描述查看文本内容是为了了解我们在准备数据时需要处理什么,我们快速看到的内容:

  • 书章节(BOOK Ⅰ )
  • 标点
  • 奇怪的名字(Polemarchus)
  • 一个漫长的独白
  • 一些引用对话

这些观察能够帮助我们在后面的数据清洗中给出具体的策略。

17.3.2 语言模型设计

在本节中,我们将开发一个文本模型,然后我们可以使用它来生成新的文本序列,语言模型是一种统计模型,并且给其输入一定的文本序列可以预测其后单词的概率,然后预测的单词作为输入,进而生成下一个单词。关键的设决策是输入序列应该有多长,他们需要足够长以允许模型能够学习到要预测的单词上下文,此输入长度也是在模型用于生成新序列时的种子文本的长度。
没有一成不变的设计思路和策略。如果有足够的时间和资源,我们可以尝试在不同大小的输入序列条件下测试模型的学习能力。相反如果条件有限,我们可以根据输入序列的总长度随机选择一个值作为输入字符的长度,比如50个字的长度。这有些随意,但是我们可以自己选择长度来处理数据,以便模型能够处理自包含的句子。并填充或截断文本以满足每个输入序列的长度要求。
为了使得示例简洁,我们让所有文本以数据流的方式输入模型并训练,以预测文本中的句子,段落甚至书籍或章节中的下一个单词。现在我们设计一个模型,我们可以看看将原始文本转换为100个输入字到1个输出字的序列。

17.3.3 加载文本

第一步是将文本加载到内存中,我们定义一个load_doc的函数来加载指定的文本。

def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
filename = '17-republic.txt'
text = load_doc(filename)
print(text[:100])

结果为:

BOOK I.

I went down yesterday to the Piraeus with Glaucon the son of Ariston,
that I might offer up

接下来我们清洗文本。

17.3.4 文本清洗

我们需要将原始文本转换为一系列标记或单词,这样我们就可以将清洗后的文本作为模型训练的数据源。基于查看原始文本,下面我们将制定一些针对该文本特定的清洗过程。当然你也可以自己增加清洗步骤。

  • 将“-”替换为空格,方便后续的分词
  • 基于空格分词
  • 删除单词中的标点
  • 删除所有非字母的单词
  • 将单词小写
    这里我们定义一个clean_doc的函数来清洗文本
def clean_doc(doc):
    doc = doc.replace('--',' ')
    tokens = doc.split()
    re_punc = re.compile('[%s]'%re.escape(string.punctuation))
    tokens = [re_punc.sub(' ',w) for w in tokens]
    tokens = [word for word in tokens if word.isalpha()]
    tokens = [w.lower() for w in tokens ]
    return tokens

我们清洗刚刚加载的文档:

tokens = clean_doc(text)
print(tokens[:200])
print('Total Tokens:%d'%len(tokens))
print('Unique Tokens:%d'%len(set(tokens)))  #set() 函数创建一个无序不重复元素集

其结果:

['book', 'i', 'went', 'down', 'yesterday', 'to', 'the', 'piraeus', 'with', 'glaucon', 'the', 'son', 'of', 'that', 'i', 'might', 'offer', 'up', 'my', 'prayers', 'to', 'the', 'goddess', 'the', 'thracian', 'and', 'also', 'because', 'i', 'wanted', 'to', 'see', 'in', 'what', 'manner', 'they', 'would', 'celebrate', 'the', 'which', 'was', 'a', 'new', 'i', 'was', 'delighted', 'with', 'the', 'procession', 'of', 'the', 'but', 'that', 'of', 'the', 'thracians', 'was', 'if', 'not', 'when', 'we', 'had', 'finished', 'our', 'prayers', 'and', 'viewed', 'the', 'we', 'turned', 'in', 'the', 'direction', 'of', 'the', 'and', 'at', 'that', 'instant', 'polemarchus', 'the', 'son', 'of', 'cephalus', 'chanced', 'to', 'catch', 'sight', 'of', 'us', 'from', 'a', 'distance', 'as', 'we', 'were', 'starting', 'on', 'our', 'way', 'and', 'told', 'his', 'servant', 'to', 'run', 'and', 'bid', 'us', 'wait', 'for', 'the', 'servant', 'took', 'hold', 'of', 'me', 'by', 'the', 'cloak', 'and', 'polemarchus', 'desires', 'you', 'to', 'i', 'turned', 'and', 'asked', 'him', 'where', 'his', 'master', 'there', 'he', 'said', 'the', 'coming', 'after', 'if', 'you', 'will', 'only', 'certainly', 'we', 'said', 'and', 'in', 'a', 'few', 'minutes', 'polemarchus', 'and', 'with', 'him', 'niceratus', 'the', 'son', 'of', 'and', 'several', 'others', 'who', 'had', 'been', 'at', 'the', 'polemarchus', 'said', 'to', 'i', 'that', 'you', 'and', 'your', 'companion', 'are', 'already', 'on', 'your', 'way', 'to', 'the', 'you', 'are', 'not', 'far', 'i', 'but', 'do', 'you', 'he', 'how', 'many', 'we', 'of', 'and', 'are', 'you', 'stronger']
Total Tokens:101762
Unique Tokens:6245

我们获得了一个干净文档的一些统计信息。我们可以看到文档的所有单词不超过110 000个单词,而词汇量不到6300。
接下来我们看看如何将标记转化成序列并将他们保存在文件中。

17.3.5 保存干净的文字

我们将长标记列表转换为52个输入字和1个输出字的序列,也就是51个单词序列。我们从标记51为开始迭代标记列表,其位置之前50个标记为输入序列,当前位置标记作为输出,然后重复这个过程到标记列表的末尾。我们将标记转换为以空格分隔的字符串,以便以后储存在文件中。下面列出了将干净标记列表拆分为长度为51个标记序列的代码

length = 50+1
sequences = list()
for i in range(length,len(tokens)):
    seq = tokens[i-length:i]
    line = ' '.join(seq)
    sequences.append(line)
print('Total Sequences:%d'%len(sequences))

运行此段代码会得到一长串的行,在列表上打印统计数据,我们得到:

Total Sequences:101711

接下来我们将序列保存到新文件中以便加载,这里定义save_doc函数来保存文本行到文件,它以行列表和文件名作为参数:

def save_doc(lines,filename):
    data = '\n'.join(lines)
    with open(filename,'w',encoding='utf-8')as f:
        f.write(data)
out_filename = '17-republic_sequences.txt'
save_doc(sequences,out_filename)

保存了一个名字为17-republic_sequences.txt的文件。查看文本你会发现每一行都沿着一个单词移动。

17.3.6 完整的示例

import re,string
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
def clean_doc(doc):
    doc = doc.replace('--',' ')
    tokens = doc.split()
    re_punc = re.compile('[%s]'%re.escape(string.punctuation))
    tokens = [re_punc.sub(' ',w) for w in tokens]
    tokens = [word for word in tokens if word.isalpha()]
    tokens = [w.lower() for w in tokens ]
    return tokens
def save_doc(lines,filename):
    data = '\n'.join(lines)
    with open(filename,'w',encoding='utf-8')as f:
        f.write(data)
filename = '17-republic.txt'
doc = load_doc(filename)
print(doc[:200])
tokens = clean_doc(doc)
print(tokens[:200])
print('Total Tokens:%d'%len(tokens))
print('Unique Tokens:%d'%len(set(tokens)))  #set() 函数创建一个无序不重复元素集
length = 50+1
sequences = list()
for i in range(length,len(tokens)):
    seq = tokens[i-length:i]
    line = ' '.join(seq)
    sequences.append(line)
print('Total Sequences:%d'%len(sequences))
out_filename = '17-republic_sequences.txt'
save_doc(sequences,out_filename)

现在当前工作目录中存在一份17-republic_sequences.txt的文件,接下来让我们如何使用语言模型拟合这些数据。

17.4 训练模型

我们现在可以用准备好的数据来训练语言模型了,该模型是神经语言模型,它有一些独特的特点:

  • 它使用单词的分布式表示,具有相似含义的不同单词具有相似的表示
  • 他在训练模型时学习
  • 它学会使用最后100个单词的上下文预测下一个单词的概率

具体来说,我们使用Embedding层来学习单词的表示,使用长短期记忆循环网路来学习,并根据上下文预测单词。接下来我们就从加载训练数据开始

17.4.1 加载数据

我们可以使用上面的load_doc函数来加载训练数据,加载后,我们可以通过换行符将数据拆分为单独的训练序列。下面的代码从当前工作目录加载17-republic_sequences.txt文件

def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
in_filename = '17-republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split('\n')

接下来,我们编码数据

17.4.2 单词编码

单词嵌入层的输入是整数序列。我们可以词汇表中的每个单词映射到唯一的整数,并对输入序列进行编码。当我们进行预测时,我们可以将预测转化为数字并在同一预测中查找关联的单词。要执行词编码,我们可以使用Keras中的Tokenizer类来完成。
首先,必须在整个训练数据集上拟合Tokenizer的一个实例,这意味着它会找到数据中的所有唯一单词,并为每个单词分配唯一的整数。然后我们可以使用拟合好的Tokenizer对所有训练序列进行编码,将每个序列从单词列表转换为整数列表。

from tensorflow.python.keras.preprocessing.text import Tokenizer
tokenizer= Tokenizer()
tokenizer.fit_on_texts(lines)
sequences = tokenizer.texts_to_sequences(lines)

我们可以通过Tokenizer对象上的word_index属性访问单词到整数的映射。我们使用嵌入层时需要知道词汇表的大小,这个我们可以通过计算映射字典的大小来确定词汇表的大小。
单词映射从1开始到单词的总数值(例如6245),Embedding层需要为此词汇表中的每个单词分配一个向量表示,从索引1到最大索引,并且因为数组的索引是从零开始的,所以词汇结尾的单词索引将是6245,这意味着数组的长度必须为6245+1。因此,在为Embedding层指定词汇表大小时,需要比实际词汇大1的大小。

vocab_size= len(tokenizer.word_index)+1

17.4.3 序列化输入和输出

现在我们来编码输入序列,我们需要将他们拆分为输入(x)和输出(y)元素。我们可以通过数组切片来完成,拆分后,我们对输出单词进行one-hot编码,这意味着将它从一个整数转换为一个稀疏向量(one-hot编码特点),0的个数就是词汇表的大小,只在该单词索引位置为1。
这样,模型就可以学习预测下一个单词的概率分布,并且有这样一个事实存在:除了下一个单词的概率是1外,其他的单词的概率都是0。Keras提供了to_categorical函数,可用于对每个输入-输出序列对的输出字进行one-hot编码。
最后,我们需要为嵌入层指定输入序列的长度,之前准备的输入序列是50个单词,我们在设计模型的时候最好不要指定一个具体的数值,最通用的方法是使用输入数据的第二维度(列数),这样,如果在准备数据阶段修改序列长度,则无需更改此数据的代码。

import numpy as ny
from tensorflow.python.keras.utils import to_categorical
sequences = ny.array(sequences)
x,y = sequences[:,:-1],sequences[:,-1]
y = to_categorical(y,num_classes = vocab_size)
seq_length = x.shape[1]

17.4.4 定义模型

现在我们可以在训练数据上定义和拟合我们的语言模型了,如前所述,学习嵌入需要知道词汇表的大小和输入序列的长度,还需要一个参数来指定将用于表示每个单词的维度,也就是嵌入向量的空间大小。
常用的值有50,100,200,300。这里我们使用200的大小,你也可以考虑更大或者更小的值,我们这里将使用两层LSTM网络,每层有512个单元。更多的单元和更深的网络结构使得网络具备更好的性能。
具有512个单元的全连接到LSTM层以解释从序列提取的特征,输出层将下一个单词预测为单个向量,该单个向量是词汇表的大小,其中词汇表的每个单词具有的概率。softmax激活函数用于确保输出具有归一化概率的特征。

from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import LSTM,Dense,Embedding,BatchNormalization
def define_model(vocab_size,seq_length):
    model = Sequential()
    model.add(Embedding(vocab_size,200,input_length=seq_length))
    model.add(LSTM(512,return_sequences=True))
    model.add(BatchNormalization())
    model.add(LSTM(512))
    model.add(Dense(512,activation='relu'))
    model.add(Dense(vocab_size,activation='softmax'))
    model.compile(
        loss='categorical_crossentropy',
        optimizer='adam',
        metrics=['accuracy']
    )
    model.summary()
    return model

编译该模型,指定拟合模型所需的分类交叉熵损失。从技术上来说,该模型学习多分类任务,这是此类问题最合适的损失函数,使用adam算法从参数的梯度下降角度来优化下降算法的,使得评估模型准确性更高。最后该模型在训练数据上训练100个迭代,为了加快速度,批量大小设置为128。在训练期间,你可以看到性能摘要,在每次批次更新结束时训练数据评估的loss值和accuracy值,你可能得到不同的结果。但是在预测序列中下一个单词的准确度可能只有50%左右,甚至不到40%,这无关紧要,我们的目的不是100%的准确率,而仅仅只是一个捕捉文本本质的模型练习。

17.4.5 保存模型

在训练结束后,将训练的模型保存到文件中。在这里,我们使用Keras模型API将模型保存到当前工作目录下。之后我们加载这个模型进行预测时,还需要将单词映射为整数,这些映射在Tokenizer的示例对象中,可以使用Pickle来保存它。

model.save('17-4-model.h5')
dump(tokenizer,open('tokenizer.pkl','wb'))

17.4.6 完整的示例

下面我们将本小结的示例完整的结合在一起:

import numpy as ny
from pickle import dump
from tensorflow.python.keras.preprocessing.text import Tokenizer
from tensorflow.python.keras.utils import to_categorical
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import LSTM,Dense,Embedding,BatchNormalization
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
def define_model(vocab_size,seq_length):
    model = Sequential()
    model.add(Embedding(vocab_size,200,input_length=seq_length))
    model.add(LSTM(512,return_sequences=True))
    model.add(BatchNormalization())
    model.add(LSTM(512))
    model.add(Dense(512,activation='relu'))
    model.add(Dense(vocab_size,activation='softmax'))
    model.compile(
        loss='categorical_crossentropy',
        optimizer='adam',
        metrics=['accuracy']
    )
    model.summary()
    return model
in_filename = '17-republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split('\n')
tokenizer= Tokenizer()
tokenizer.fit_on_texts(lines)
sequences = tokenizer.texts_to_sequences(lines)
vocab_size= len(tokenizer.word_index)+1
sequences = ny.array(sequences)
x,y = sequences[:,:-1],sequences[:,-1]
y = to_categorical(y,num_classes = vocab_size)
seq_length = x.shape[1]
model = define_model(vocab_size,seq_length)
history = model.fit(x,y,batch_size=512,epochs=100)
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()
model.save('17-4-model.h5')
dump(tokenizer,open('tokenizer.pkl','wb'))

网络模型:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 50, 200)           1249200   
_________________________________________________________________
lstm (LSTM)                  (None, 50, 512)           1460224   
_________________________________________________________________
batch_normalization (BatchNo (None, 50, 512)           2048      
_________________________________________________________________
lstm_1 (LSTM)                (None, 512)               2099200   
_________________________________________________________________
dense (Dense)                (None, 512)               262656    
_________________________________________________________________
dense_1 (Dense)              (None, 6246)              3204198   
=================================================================
Total params: 8,277,526
Trainable params: 8,276,502
Non-trainable params: 1,024
_________________________________________________________________

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

17.5 使用模型

现在我们有一个训练好的模型,我们就可以使用它,在这种情况下,我们可以使用它来生成与源文本具有相同统计属性的新文本序列,但在这里这个想法不太容易实现,主要是在与训练的程度不够,但是他给出了语言模型学到的具体例子。我们将首先再次加载训练序列。

17.5.1 加载数据

这里我们使用和上一节一样的代码来加载文本的训练数据序列。具体而言是load_doc函数

def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
in_filename = '17-republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split('\n')

我们在前面知道17-republic_sequences.txt是清洗,分词和序列化之后的文本,我们选择该文件作为源序列作为模型的输入,以生成新的文本。该模型还需要50个单词作为输入。稍后我们需要指定预期的输入长度,我们可以通过计算加载数据的一行长度并从同一行减去1来确定输入的文本长度。

seq_length= len(lines[0].split())-1

17.5.2 加载模型

我们现在可以从文件中加载模型。Keras提供了load_model函数来加载模型,

model = load_model('17-4-model.h5')

我们还可以使用Pickle从文件中加载tokenizer

tokenizer= load(open('tokenizer.pkl','rb'))

接下来使用加载的模型

17.5.3 生成文本

生成文本的第一步是准备自定义输入文本,为此我们将选择17-republic_sequences.txt中随机选择一行作为输入,选择后我们打印它,这样能让我们了解所使用的的内容。

seed_text = lines[randint(0,len(lines))]
print(seed_text+'\n')

接下来,我们可以一次生成一个新单词。首先,必须使用我们在训练模型时使用的相同标记器将自定义文本编码成整数。

encoded = tokenizer.texts_to_sequences([seed_text])[0]

该模型调用model.predict_classes直接预测下一个单词,该类将将返回具有最高概率的单词索引。

yhat = model.predict_classes(encoded)

然后我们在Tokenizer的映射中查找索引以获取关联的单词。

out_word = ''
for word,index in tokenizer.word_index.items():
	if index == yhat:
		out_word = word
		break

然后,我们将次单词附加到种子文本后面并重复该过程,值得注意的是,输入序列的长度。这里在输入序列编码为整数后,我们将其截断为所需的长度,Keras中提供了pad_sequence函数,我们可以使用它来执行截断操作。

encoded = pad_sequence([encoded],maxlen = seq_length,truncating= 'pre')

我们将上面的操作放在一个名为generate_seq的函数中,该函数的输入为:模型,标记生成器,输入序列长度,输入文本和要生成的单词数作为输入,然后返回由模型生成的一系列单词。

def generate_seq(model,tokenizer,seq_length,seed_text,n_words):
	results = list()
	in_text = seed_text
	for _ in range(n_words):
		encoded = tokenizer.texts_to_sequences([in_text])[0]
		encoded = pad_sequences([encoded],maxlen = seq_length,truncating='pre')
		yhat = model.predict_classes(encoded)
		out_word = ''
		for word, index in tokenizer.word_index.items():
			if index == yhat:
				out_word = word
				break
		in_text +=' '+out_word
		results.append(out_word)
	return ' '.join(results)

我们现在准备给定一些输入文本的情况下生成一系列单词。

generated= generate_seq(model,tokenizer,seq_length,seed_word,50)
print(generated)

下面是完整代码:

from random import randint
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
from tensorflow.python.keras.models import load_model
from pickle import load
def load_doc(filename):
    with open(filename,'r',encoding='utf-8')as f:
        text = f.read()
    return text
def generate_seq(model,tokenizer,seq_length,seed_text,n_words):
    result = list()
    in_text = seed_text
    for _ in range(n_words):
        encoded = tokenizer.texts_to_sequences([in_text])[0]
        encoded = pad_sequences([encoded],maxlen= seq_length,truncating = 'pre')
        yhat = model.predict_classes(encoded)
        out_word = ''
        for word,index in tokenizer.word_index.items():
            if index ==yhat:
                out_word = word
                break
        in_text +=' '+out_word
        result.append(out_word)
    return ' '.join(result)
in_filename = '17-republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split('\n')
seq_length = len(lines[0].split())-1
model = load_model('17-4-model.h5')
tokenizer = load(open('17-4-tokenizer.pkl','rb'))
seed_text = lines[randint(0,len(lines))]
print(seed_text+'\n')
generated = generate_seq(
    model=model,
    tokenizer=tokenizer,
    seq_length=seq_length,
    seed_text=seed_text,
    n_words=50
)
print(generated)

运行结果:

life is certainly concerned with then if the good and just man be thus superior in pleasure to the evil and his superiority will be infinitely greater in propriety of life and in beauty and immeasurably i and now having arrived at this stage of the we may revert to the

words which brought us was not some one saying that injustice was a gain to the perfectly unjust who was reputed to be that was now having determined the power and quality of justice and let us have a little conversation with what shall we say to let us make

可以看到文本看似合理,其实没有意义。实际上添加链接将有助于解释种子和生成的文本,然而,生成的文本以正确的顺序获得正确的单词需要其他的处理或更大的训练,尝试运行该实例几次,查看生成的其他文本。

17.6 扩展

下面列出一些扩展内容以便你将来能在上面教程的基础上得到更加完美的模型:

  • 精细设计输入文本。手工制作或选择输入文本并评估输入文本如何影响生成的文本,特别是生成的初始单词和句子。
  • 精简词汇,尝试一个更加简单的词汇表
  • 数据清洗,或多或少的考虑文本清洗策略,也许留下一些标点或者使用一个或少数替换所有连词符号,。评估这些词对词汇表大小的更改是如何影响生成的文本的。
  • 调整模型,例如嵌入的大小和网络层中存储单元的数量,查看是否可以开发更好的模型
  • 更深的网络。扩展具有更多的LSTM层的网络,可能使用Dropout和BatchNormallization可以开发更好的模型。
  • 开发预训练嵌入。使用预训练的Word2Vec向量查看它是否会产生更好的模型。
  • 使用GloVe嵌入,使用Glove词嵌入向量,无论是否经过网络微调,都可以评估它对训练和生成的单词的影响。
  • 序列长度。尝试使用不同长度的输入序列(更长或更短)训练模型,并评估它如何影响生成的文本质量
  • 减少范围。本例中是使用所有的章节,你可以尝试使用几个章节或原始文本的子集上训练模型,并评估对训练,训练速度以及生成文本的影响。
  • 句子模型,基于句子拆分原始数据,并将每个句子填充到固定长度
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值