Kaggle NLP Disaster Twitter竞赛的解决方案(基于TensorFlow 2.x实现)

最近打算深入研究一下NLP,先拿Kaggle上面的NLP的竞赛来练一下手。

之前我已经参加过一个Kaggle NLP的竞赛,题目是根据推特的内容以及情感分类标签,判断推特里面的那一部分内容支持这个情感分类标签的,具体可见我之前的博客,https://blog.csdn.net/gzroy/article/details/107739963

这次的NLP竞赛,是Kaggle专门为NLP准备的一个练手的竞赛,需要根据推特的内容来判断是否和灾难主题相关,竞赛的具体内容和介绍可见链接,https://blog.csdn.net/gzroy/article/details/107739963

TensorFlow的最新版本里面推出了Tensorflow Text的库,里面集成了很多预训练好的模型,例如BERT等等。因此我也基于这个最新的Text库,来构建代码。这里列出了两种思路,一个是基于BERT模型对推特内容进行编码,得到编码向量之后再添加一个Dense层,最后做Sigmoid激活得到分类的概率值。另一个思路是用传统的word embedding+双向LSTM来构建模型。

基于BERT模型进行文本分类

1. 数据预处理

数据集包括了train.csv和test.csv,train.csv的内容如下:

idkeywordlocationtexttarget
1Our Deeds are the Reason of this #earthquake May ALLAH Forgive us all1
4Forest fire near La Ronge Sask. Canada1
5All residents asked to 'shelter in place' are being notified by officers. No other evacuation or shelter in place orders are expected1
49ablazeEst. September 2012 - BristolWe always try to bring the heavy. #metal #RT http://t.co/YAo1e0xngw0

其中target为1表示这条推特的内容和灾难相关。

从train.csv看到,大部分的推特都缺失了keyword和location的信息。在推特的内容里面,有很多@XXX或者http://t.co/xxxx的信息,这些和判断是否灾难是没有太大关系的。因此在数据的预处理的时候,我将只保留text和target,并且对text内容删去@xxx以及http链接的内容。

以下代码是对数据集进行预处理并构建一个tensorflow的dataset

import tensorflow as tf

LABEL_COLUMN = 'target'
def get_dataset(file_path):
    dataset = tf.data.experimental.make_csv_dataset(
      file_path,
      batch_size=32, 
      label_name=LABEL_COLUMN,
      select_columns=['text', 'target'],
      na_value="?",
      shuffle=True,
      num_epochs=1,
      ignore_errors=True)
    return dataset
raw_train_data = get_dataset('train.csv')
processed_train_data = raw_train_data\
    .map(lambda x,y:(tf.strings.regex_replace(x['text'], '@\w+', ''), y))\
    .map(lambda x,y:(tf.strings.regex_replace(x, 'http[s]*://[\w\.\-\/]+', ''), y))

2. 对文本内容进行token编码

以上步骤生成的数据集输出的是推特内容的byte编码,我们需要把这些byte编码转换成token id。因为我们要用BERT模型进行训练,因此也需要按照BERT模型的要求来进行token转换。TensorFlow Text提供了很方便的方式可以加载预训练好的BERT模型和Token转换工具。

以下代码列出了所有可选的BERT模型,我在这里选择了其中一个比较小的模型。

bert_model_name = 'small_bert/bert_en_uncased_L-4_H-512_A-8' 

map_name_to_handle = {
    'bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_L-12_H-768_A-12/3',
    'bert_en_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_cased_L-12_H-768_A-12/3',
    'bert_multi_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_multi_cased_L-12_H-768_A-12/3',
    'small_bert/bert_en_uncased_L-2_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-2_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-2_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-2_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-4_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-4_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-4_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-4_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-6_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-6_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-6_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-6_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-8_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-8_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-8_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-8_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-10_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-10_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-10_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-10_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-12_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-12_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-12_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-768_A-12/1',
    'albert_en_base':
        'https://hub.tensorflow.google.cn/tensorflow/albert_en_base/2',
    'electra_small':
        'https://hub.tensorflow.google.cn/google/electra_small/2',
    'electra_base':
        'https://hub.tensorflow.google.cn/google/electra_base/2',
    'experts_pubmed':
        'https://hub.tensorflow.google.cn/google/experts/bert/pubmed/2',
    'experts_wiki_books':
        'https://hub.tensorflow.google.cn/google/experts/bert/wiki_books/2',
    'talking-heads_base':
        'https://hub.tensorflow.google.cn/tensorflow/talkheads_ggelu_bert_en_base/1',
}

map_model_to_preprocess = {
    'bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'bert_en_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_cased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'bert_multi_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_multi_cased_preprocess/3',
    'albert_en_base':
        'https://hub.tensorflow.google.cn/tensorflow/albert_en_preprocess/3',
    'electra_small':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'electra_base':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'experts_pubmed':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'experts_wiki_books':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'talking-heads_base':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
}

tfhub_handle_encoder = map_name_to_handle[bert_model_name]
tfhub_handle_preprocess = map_model_to_preprocess[bert_model_name]

bert_preprocess_model = hub.KerasLayer(tfhub_handle_preprocess)
bert_model = hub.KerasLayer(tfhub_handle_encoder)

让我们测试一下这个token编码的结果

bert_model = hub.KerasLayer(tfhub_handle_encoder)
text_preprocessed = bert_preprocess_model(['This is a test!'])

print(f'Keys       : {list(text_preprocessed.keys())}')
print(f'Shape      : {text_preprocessed["input_word_ids"].shape}')
print(f'Word Ids   : {text_preprocessed["input_word_ids"][0, :12]}')
print(f'Input Mask : {text_preprocessed["input_mask"][0, :12]}')
print(f'Type Ids   : {text_preprocessed["input_type_ids"][0, :12]}')

输出结果如下:

Keys       : ['input_word_ids', 'input_mask', 'input_type_ids']
Shape      : (1, 128)
Word Ids   : [ 101 2023 2003 1037 3231  999  102    0    0    0    0    0]
Input Mask : [1 1 1 1 1 1 1 0 0 0 0 0]
Type Ids   : [0 0 0 0 0 0 0 0 0 0 0 0]

其中的Word Ids是对文本进行小写转换以及分词后,根据词汇表映射得到的ID。Input Mask用于表示哪些ID是有效的,哪些是Padding。Type Ids是用于当每个文本是由两个句子组成的场景下,0表是第一个句子,1表示第2个句子,在我们这个场景中用不到。

下载这个预处理模型的词汇表,可以看到Word Ids对应的token如下:

['[CLS]', 'this', 'is', 'a', 'test', '!', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']

3. 搭建文本分类模型

预处理完成后就可以搭建一个文本分类模型了。这个模型的输入是Dataset的文本内容,然后通过一个预处理层转换为word id,之后通过预训练好的BERT模型处理后输出一个维度为512的嵌入向量(pooled_output),这个向量反映了文本的隐含的意思。然后通过一个dropout层(随机屏蔽10%的神经元连接以避免过拟合),最后通过一个512*1的Dense Layer输出一个二分类的数值,通过Sigmoid激活之后即可得到一个概率值,表示这个文本是否和灾难相关。

代码如下:

def build_classifier_model():
    text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
    preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
    encoder_inputs = preprocessing_layer(text_input)
    encoder = hub.KerasLayer(tfhub_handle_encoder, trainable=True, name='BERT_encoder')
    outputs = encoder(encoder_inputs)
    net = outputs['pooled_output']
    net = tf.keras.layers.Dropout(0.1)(net)
    net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
    return tf.keras.Model(text_input, net)

epochs = 10
steps_per_epoch = 238
num_train_steps = steps_per_epoch * epochs
num_warmup_steps = int(0.1*num_train_steps)
init_lr = 3e-5

loss = tf.keras.losses.BinaryCrossentropy(from_logits=True)
metrics = tf.metrics.BinaryAccuracy()

4. 模型训练

考虑到训练数据集比较小,这里采用kfold(k=5)来构建训练集和验证集。每个Fold训练的时候都训练一个新的模型。最后再对测试集做预测的时候,集成这个5个模型的预测结果,通过投票的方式来进行预测。

Tensorflow里面没有直接的方法来构建KFOLD,不像SKLEARN。这里我采用先把数据集通过SHARD的方式分成5份,每次训练的时候把其中4份组合成一个训练集,剩下1份作为验证集。

每个Fold训练10个EPOCH,如果验证集的LOSS有降低则保存模型参数。

代码如下:

kfold = 5
dataset_list = []
for i in range(kfold):
    dataset_list.append(processed_train_data.shard(num_shards=kfold, index=i))

#Train for 5 fold, after each fold training there is a model saved
for i in range(kfold):
    print("#######################################################")
    print("###Start Fold " + str(i) + " Training")
    print("#######################################################")
    dataset_list_copy = dataset_list.copy()
    val_ds = dataset_list_copy.pop(i)
    train_ds = dataset_list_copy[0]
    for j in range(1, kfold-1):
        train_ds = train_ds.concatenate(dataset_list_copy[j])
    #For each fold training, there will train 10 epochs, only save the model weights with best val_loss
    sv = tf.keras.callbacks.ModelCheckpoint(
        'twitter-%i.h5'%(i), monitor='val_loss', verbose=1, save_best_only=True,
        save_weights_only=True, mode='auto', save_freq='epoch')
    optimizer = optimization.create_optimizer(
        init_lr=init_lr,
        num_train_steps=num_train_steps,
        num_warmup_steps=num_warmup_steps,
        optimizer_type='adamw')
    classifier_model = build_classifier_model()
    classifier_model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
    history = classifier_model.fit(
        x=train_ds,
        validation_data=val_ds,
        callbacks=[sv],
        epochs=epochs)

5. 对测试数据集进行预测

模型训练完成之后,我们有了5个模型,通过集成学习的方式来做一个组合,通过投票的方式来进行预测。

首先是构建测试数据集,这里需要把每条数据的ID加进去,因为最后生成submission.csv文件的时候需要有ID,代码如下:

def get_test_dataset(file_path):
    dataset = tf.data.experimental.make_csv_dataset(
      file_path,
      batch_size=1,
      select_columns=['id', 'text'],
      na_value="?",
      shuffle=False,
      num_epochs=1,
      ignore_errors=True)
    return dataset
raw_test_data = get_test_dataset('test.csv')
processed_test_data = raw_test_data\
    .map(lambda x:(x['id'], tf.strings.regex_replace(x['text'], '@\w+', '')))\
    .map(lambda x,y:(x, tf.strings.regex_replace(y, 'http[s]*://[\w\.\-\/]+', '')))

result_dict = {}
result = ['id,target']
classifier_model = build_classifier_model()
for i in range(kfold):
    test_data = iter(processed_test_data)
    classifier_model.load_weights('twitter-%i.h5'%kfold)
    while(True):
        try:
            test_id, test_text = test_data.next()
            prob = tf.nn.sigmoid(classifier_model(test_text, training=False)).numpy()[0,0]
            if prob>=0.5:
                prob = 1
            else:
                prob = 0
            test_id = test_id.numpy()[0]
            if test_id in result_dict:
                result_dict[test_id] += prob
            else:
                result_dict[test_id] = prob
        except StopIteration:
            break

for key in result_dict:
    prob = result_dict[key]/kfold
    if prob>=0.5:
        prob = 1
    else:
        prob = 0
    result.append(str(key)+","+str(prob))
        
result_text = '\n'.join(result)
with open('submission.csv', 'w') as f:
    f.write(result_text)

把submission.csv文件上传之后,得到的分数是0.81581,这个成绩在3140个参赛选手之中排名665(剔除掉140个分数为1.0的选手,因为测试集的数据泄漏了),这个成绩为前20%以内。考虑到模型比较简单,应该还有进一步提高的空间,留待以后继续改进。

基于RNN模型进行分类

1. 文本预处理

这里对文本的预处理以及Token与ID的转换,和以上BERT模型的处理是一样的。

2. 模型的搭建

模型的搭建首先是一个Embedding层,把输入的文本ID映射为嵌入向量,这里我设置嵌入向量的维度为64,然后是搭建1个双向LSTM层,输出的维度也是64。之后可以再搭建一个双向LSTM层,输出的维度是32,最后再接一个全连接层,输出维度为1。

代码如下:

def build_rnn_classifier_model():
    text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
    preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
    encoder_inputs = preprocessing_layer(text_input)["input_word_ids"]
    embedding = tf.keras.layers.Embedding(vocab_size, 64, mask_zero=True)(encoder_inputs)
    net = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64, return_sequences=True))(embedding)
    net = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32))(net)
    net = tf.keras.layers.Dense(32, activation='relu')(net)
    net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
    return tf.keras.Model(text_input, net)

3. 模型的训练

同样采用kfold的方式训练,如以下代码

kfold = 5
epochs = 20
dataset_list = []
for i in range(kfold):
    dataset_list.append(processed_train_data.shard(num_shards=kfold, index=i))

#Train for 5 fold, after each fold training there is a model saved
for i in range(kfold):
    print("#######################################################")
    print("###Start Fold " + str(i) + " Training")
    print("#######################################################")
    dataset_list_copy = dataset_list.copy()
    val_ds = dataset_list_copy.pop(i)
    train_ds = dataset_list_copy[0]
    for j in range(1, kfold-1):
        train_ds = train_ds.concatenate(dataset_list_copy[j])
    #For each fold training, there will train 10 epochs, only save the model weights with best val_loss
    sv = tf.keras.callbacks.ModelCheckpoint(
        'twitter-rnn-%i.h5'%(i), monitor='val_loss', verbose=1, save_best_only=True,
        save_weights_only=True, mode='auto', save_freq='epoch')
    rnn_model = build_rnn_classifier_model()
    rnn_model.compile(
        loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
        optimizer=tf.keras.optimizers.Adam(1e-4),
        metrics=['accuracy']
    )
    history = rnn_model.fit(
        x=train_ds,
        validation_data=val_ds,
        callbacks=[sv],
        epochs=epochs)

最后在测试集上的准确率比BERT模型稍微低一些。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

gzroy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值