DatawhaleAI夏令营第三期 - 基于论文摘要的文本分类与关键词抽取挑战

目录

一、赛题背景

基于论文摘要的文本分类与关键词抽取挑战赛

二、赛事任务

任务描述

赛题数据集

评价指标

解题思路

方法1:机器学习方法Baseline

1.导入模块

1.1特征提取

1.2 基于 TF-IDF 提取

1.3选择机器学习模型

2.数据探索

2.1使用pandas读取数据

3.数据清洗

4.特征工程

5.模型训练与验证

6.结果输出

完整代码如下:

结果如下:

方法2:bert模型实战:

BERT介绍

一、BERT 简介

二、预训练 + 微调范式

三、Transformer 与 Attention

四、预训练任务

完整代码如下:

结果如下:

方法3:深度学习Topline方案

一、比赛数据集分析:

1.比赛须知

2.数据集分析

        问题一:数据集中的样本数目是多少?

        问题二:训练集中的样本数量是否均衡?

        问题三:比较训练集每个样本的文本内容(标题+作者+摘要),最小长度、最大长度、平均长度分别是多少?

二、Topline方法:预训练微调+特征融合+后处理

1、 运行代码 - 对数据进行处理

2、运行代码 - 训练模型

3、运行代码 - 测试集推理

结果显示:

三、还可继续优化/探索的地方(进阶/高阶玩法)

①调整超参数

②调整最大序列长度

③更改损失函数

④冻结模型部分参数

⑤融合更多特征

⑥模型集成

⑦对比学习

⑧提示学习

⑨......

方法4:大模型:微调ChatGLM2-6B解决文本二分类问题

导入数据

制作数据集

修改data_info

加载训练好的LoRA权重,进行预测

制作submit


一、赛题背景

基于论文摘要的文本分类与关键词抽取挑战赛

        医学领域的文献库中蕴含了丰富的疾病诊断和治疗信息,如何高效地从海量文献中提取关键信息,进行疾病诊断和治疗推荐,对于临床医生和研究人员具有重要意义。

https://challenge.xfyun.cn/topic/info?type=abstract-of-the-paper&ch=ymfk4uU

二、赛事任务

任务描述

        机器通过对论文摘要等信息的理解,判断该论文是否属于医学领域的文献。

        任务示例:

        输入:

        论文信息,格式如下:

        Inflammatory Breast Cancer: What to Know About This Unique, Aggressive Breast Cancer.,

        [Arjun Menta, Tamer M Fouad, Anthony Lucci, Huong Le-Petross, Michael C Stauder, Wendy A Woodward, Naoto T Ueno, Bora Lim],

        Inflammatory breast cancer (IBC) is a rare form of breast cancer that accounts for only 2% to 4% of all breast cancer cases. Despite its low incidence, IBC contributes to 7% to 10% of breast cancer caused mortality. Despite ongoing international efforts to formulate better diagnosis, treatment, and research, the survival of patients with IBC has not been significantly improved, and there are no therapeutic agents that specifically target IBC to date. The authors present a comprehensive overview that aims to assess the present and new management strategies of IBC.,

        Breast changes; Clinical trials; Inflammatory breast cancer; Trimodality care.

        输出:

        是(1)

赛题数据集

        训练集与测试集数据为CSV格式文件,各字段分别是标题、作者、摘要、关键词。

评价指标

        本次竞赛的评价标准采用F1_score,分数越高,效果越好。

解题思路

文献领域分类

        针对文本分类任务,可以提供两种实践思路,一种是使用传统的特征提取方法(如TF-IDF/BOW)结合机器学习模型,另一种是使用预训练的BERT模型进行建模。

使用特征提取 + 机器学习的思路步骤如下:

        1.数据预处理:首先,对文本数据进行预处理,包括文本清洗(如去除特殊字符、标点符号)、分词等操作。可以使用常见的NLP工具包(如NLTK或spaCy)来辅助进行预处理。

        2.特征提取:使用TF-IDF(词频-逆文档频率)或BOW(词袋模型)方法将文本转换为向量表示。TF-IDF可以计算文本中词语的重要性,而BOW则简单地统计每个词语在文本中的出现次数。可以使用scikit-learn库的TfidfVectorizer或CountVectorizer来实现特征提取。

        3.构建训练集和测试集:将预处理后的文本数据分割为训练集和测试集,确保数据集的样本分布均匀。

        4.选择机器学习模型:根据实际情况选择适合的机器学习模型,如朴素贝叶斯、支持向量机(SVM)、随机森林等。这些模型在文本分类任务中表现良好。可以使用scikit-learn库中相应的分类器进行模型训练和评估。

        5.模型训练和评估:使用训练集对选定的机器学习模型进行训练,然后使用测试集进行评估。评估指标可以选择准确率、精确率、召回率、F1值等。

        6.调参优化:如果模型效果不理想,可以尝试调整特征提取的参数(如词频阈值、词袋大小等)或机器学习模型的参数,以获得更好的性能。

方法1:机器学习方法Baseline

        在这个 Baseline 中,我们使用机器学习的LogisticRegression模型

1.导入模块

        导入我们本次Baseline代码所需的模块:

#import 相关库
# 导入pandas用于读取表格数据
import pandas as pd

# 导入BOW(词袋模型),可以选择将CountVectorizer替换为TfidfVectorizer(TF-IDF(词频-逆文档频率)),注意上下文要同时修改,亲测后者效果更佳
from sklearn.feature_extraction.text import CountVectorizer

# 导入LogisticRegression回归模型
from sklearn.linear_model import LogisticRegression

# 过滤警告消息
from warnings import simplefilter
from sklearn.exceptions import ConvergenceWarning
simplefilter("ignore", category=ConvergenceWarning)

1.1特征提取

        特征提取是机器学习任务中的一个重要步骤。我们将训练数据的每一个维度称为一个特征,例如,如果我们想要基于二手车的品牌、价格、行驶里程数三个变量来预测二手车的价格,则品牌、价格、行驶里程数为该任务的三个特征。所谓特征提取,即从训练数据的特征集合中创建新的特征子集的过程。提取出来的特征子集特征数一般少于等于原特征数,但能够更好地表征训练数据的情况,使用提取出的特征子集能够取得更好的预测效果。对于 NLP、CV 任务,我们通常需要将文本、图像特征提取为计算机可以处理的数值向量特征。我们一般可以使用 sklearn 库中的 feature_extraction 包来实现文本与图片的特征提取。

        在 NLP 任务中,特征提取一般需要将自然语言文本转化为数值向量表示,常见的方法包括基于 TF-IDF(词频-逆文档频率)提取或基于 BOW(词袋模型)提取等,两种方法均在 sklearn.feature_extraction 包中有所实现。

1.2 基于 TF-IDF 提取

        TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术,其中,TF 指 term frequence,即词频,指某个词在文章中出现次数与文章总次数的比值;IDF 指 inverse document frequence,即逆文档频率,指包含某个词的文档数占语料库总文档数的比例。例如,假设语料库为 {"今天 天气 很好","今天 心情 很 不好", "明天 天气 不好"},每一个句子为一个文档,则“今天”的 TF 和 IDF 分别为:

$$TF(今天|文档1)= \frac{词在文档一的出现频率}{文档一的总词数} = \frac{1}{3} $$

$$TF(今天|文档2)= \frac{词在文档二的出现频率}{文档二的总词数} = \frac{1}{4}$$

$$TF(今天|文档3)= 0$$

$$IDF(今天)= log\frac{语料库文档总数}{出现该词的文档数} = log\frac{3}{2}$$

        每个词最终的 IF-IDF 即为 TF 值乘以 IDF 值。计算出每个词的 TF-IDF 值后,使用 TF-IDF 计算得到的数值向量替代原文本即可实现基于 TF-IDF 的文本特征提取。

        我们可以使用 sklearn.feature_extraction.text 中的 TfidfVectorizer 类来简单实现文档基于 TF-IDF 的特征提取:

# 首先导入该类
from sklearn.feature_extraction.text import TfidfVectorizer

# 假设我们已从本地读取数据为 DataFrame 类型,并已经过基本预处理,data 为已处理的 DataFrame 数据
# 实例化一个 TfidfVectorizer 对象,并使用 fit 方法来拟合数据
vector = TfidfVectorizer().fit(data["text"])

# 拟合之后,调用 transform 方法即可得到提取后的特征数据
train_vector = vector.transform()

1.3选择机器学习模型

        我们可以选择多种机器学习模型来拟合训练数据,不同的业务场景、不同的训练数据往往最优的模型也不同。常见的模型包括线性模型、逻辑回归、决策树、支持向量机、集成模型、神经网络等。想要深入学习各种机器学习模型的同学,推荐学习《西瓜书》或《统计学习方法》。

        Sklearn 封装了多种机器学习模型,常见的模型都可以在 sklearn 中找到,sklearn 根据模型的类别组织在不同的包中,此处介绍几个常用包:

  • sklearn.linear_model:线性模型,如线性回归、逻辑回归、岭回归等

  • sklearn.tree:树模型,一般为决策树

  • sklearn.neighbors:最近邻模型,常见如 K 近邻算法

  • sklearn.svm:支持向量机

  • sklearn.ensemble:集成模型,如 AdaBoost、GBDT等

       本案例中,我们使用简单但拟合效果较好的逻辑回归模型作为 Baseline 的模型。此处简要介绍其原理。

        逻辑回归模型,即 Logistic Regression,实则为一个线性分类器,通过 Logistic 函数(或 Sigmoid 函数),将数据特征映射到0~1区间的一个概率值(样本属于正例的可能性),通过与 0.5 的比对得出数据所属的分类。逻辑回归的数学表达式为:

$$f(z) = \frac{1}{1 + e^{-z}}$$

$$z = w^Tx + w_0$$

        逻辑回归模型简单、可并行化、可解释性强,同时也往往能够取得不错的效果,是较为通用的模型。

        我们可以使用 sklearn.linear_model.LogisticRegression 来调用已实现的逻辑回归模型:

# 引入模型
model = LogisticRegression()
# 可以在初始化时控制超参的取值,此处使用默认值,具体参数可以查阅官方文档

# 开始训练,这里可以考虑修改默认的batch_size与epoch来取得更好的效果
# 此处的 train_vector 是已经经过特征提取的训练数据
model.fit(train_vector, train['label'])

# 利用模型对测试集label标签进行预测,此处的 test_vector 同样是已经经过特征提取的测试数据
test['label'] = model.predict(test_vector)

        事实上,sklearn 提供的多种机器学习模型都封装成了类似的类,绝大部分使用方法均和上述一致,即先实例化一个模型对象,再使用 fit 函数拟合训练数据,最后使用 predict 函数预测测试数据即可。

2.数据探索

        数据探索性分析,是通过了解数据集,了解变量间的相互关系以及变量与预测值之间的关系,对已有数据在尽量少的先验假设下通过作图、制表、方程拟合、计算特征量等手段探索数据的结构和规律的一种数据分析方法,从而帮助我们后期更好地进行特征工程和建立模型,是机器学习中十分重要的一步。

        本次baseline实践中我们使用pandas来读取数据以及数据探索。

2.1使用pandas读取数据

        在这部分内容里我们利用pd.read_csv()方法对赛题数据进行读取,pd.read_csv()参数为需要读取的数据地址,读取后返回一个DataFrame 数据

import pandas as pd
train = pd.read_csv('./基于论文摘要的文本分类与关键词抽取挑战赛公开数据/train.csv')
train['title'] = train['title'].fillna('')
train['abstract'] = train['abstract'].fillna('')

test = pd.read_csv('./基于论文摘要的文本分类与关键词抽取挑战赛公开数据/testB.csv')
test['title'] = test['title'].fillna('')
test['abstract'] = test['abstract'].fillna('')

3.数据清洗

        数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。俗话说:garbage in, garbage out。分析完数据后,特征工程前,必不可少的步骤是对数据清洗。

        数据清洗的作用是利用有关技术如数理统计、数据挖掘或预定义的清理规则将脏数据转化为满足数据质量要求的数据。主要包括缺失值处理、异常值处理、数据分桶、特征归一化/标准化等流程。

        同时由于表格中存在较多列,我们将这些列的重要内容组合在一起生成一个新的列方便训练。


# 提取文本特征,生成训练集与测试集
train['text'] = train['title'].fillna('') + ' ' +  train['author'].fillna('') + ' ' + train['abstract'].fillna('')+ ' ' + train['Keywords'].fillna('')
test['text'] = test['title'].fillna('') + ' ' +  test['author'].fillna('') + ' ' + test['abstract'].fillna('')

        在实践学习中有些同学反映对于fillna('')方法存在疑惑,pandas中fillna()方法,能够使用指定的方法填充NA/NaN值。如果数据集中某行缺少title author abstract中的内容,我们需要利用fillna()来保证不会出现报错。

4.特征工程

        特征工程指的是把原始数据转变为模型训练数据的过程,目的是获取更好的训练数据特征。特征工程能使得模型的性能得到提升,有时甚至在简单的模型上也能取得不错的效果。

        这里我们选择使用TF-IDF将文本转换为向量表示:

vector = TfidfVectorizer(stop_words=stops).fit(train["text"])  # 生成词袋模型
train_vector = vector.transform(train["text"])  # 将训练集转换为词袋模型
test_vector = vector.transform(test["text"])  # 将测试集转换为词袋模型

5.模型训练与验证

        特征工程也好,数据清洗也罢,都是为最终的模型来服务的,模型的建立和调参决定了最终的结果。模型的选择决定结果的上限, 如何更好的去达到模型上限取决于模型的调参。

        建模的过程需要我们对常见的线性模型、非线性模型有基础的了解。模型构建完成后,需要掌握一定的模型性能验证的方法和技巧。

# 模型训练
model = LogisticRegression()

# 开始训练,这里可以考虑修改默认的batch_size与epoch来取得更好的效果
model.fit(train_vector, train['label'])

6.结果输出

# 利用模型对测试集label标签进行预测
test['label'] = model.predict(test_vector)
test['Keywords'] = test['title'].fillna('')
# 生成任务一推测结果
test[['uuid', 'Keywords', 'label']].to_csv('submit_task1.csv', index=None)

完整代码如下:

# 导入pandas用于读取表格数据
import pandas as pd

# 导入BOW(词袋模型),可以选择将CountVectorizer替换为TfidfVectorizer(TF-IDF(词频-逆文档频率)),注意上下文要同时修改,亲测后者效果更佳
from sklearn.feature_extraction.text import TfidfVectorizer

# 导入LogisticRegression回归模型
from sklearn.linear_model import LogisticRegression

# 过滤警告消息
from warnings import simplefilter
from sklearn.exceptions import ConvergenceWarning

from sklearn.model_selection import train_test_split

simplefilter("ignore", category=ConvergenceWarning)

# 读取数据集
train = pd.read_csv('./基于论文摘要的文本分类与关键词抽取挑战赛公开数据/train.csv')
train['title'] = train['title'].fillna('')
train['abstract'] = train['abstract'].fillna('')

test = pd.read_csv('./基于论文摘要的文本分类与关键词抽取挑战赛公开数据/testB.csv')
test['title'] = test['title'].fillna('')
test['abstract'] = test['abstract'].fillna('')


# 提取文本特征,生成训练集与测试集
train['text'] = train['title'].fillna('') + ' ' +  train['author'].fillna('') + ' ' + train['abstract'].fillna('')+ ' ' + train['Keywords'].fillna('')
test['text'] = test['title'].fillna('') + ' ' +  test['author'].fillna('') + ' ' + test['abstract'].fillna('')

vector = TfidfVectorizer(stop_words=stops).fit(train["text"])
train_vector = vector.transform(train["text"])  # 将训练集转换为词袋模型
test_vector = vector.transform(test["text"])  # 将测试集转换为词袋模型


# 引入模型
model = LogisticRegression()

# 开始训练,这里可以考虑修改默认的batch_size与epoch来取得更好的效果
model.fit(train_vector, train['label'])

# 利用模型对测试集label标签进行预测
test['label'] = model.predict(test_vector)
# 因为任务一并不涉及关键词提取,而提交中需要这一行所以我们用title列填充Keywords列
test['Keywords'] = test['title'].fillna('')
# 生成任务一推测结果
test[['uuid', 'Keywords', 'label']].to_csv('submit_task1.csv', index=None)

结果如下:

排名大概是27/135,查看此排名的时间:2023年8月15日13:38:07

方法2:bert模型实战:

BERT介绍

一、BERT 简介

        BERT,是一个经典的深度学习、预训练模型。2018年,由 Google 团队发布的论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》提出了预训练模型 BERT(Bidirectional Encoder Representations from Transformers),在自然语言处理领域掀起了巨大浪潮。该模型实现了包括 GLUE、MultiNLI 等七个自然语言处理评测任务的 the-state-of-art(最优表现),堪称里程碑式的成果。自 BERT 推出以来,预训练+微调的模式开始成为自然语言处理任务的主流,标志着各种自然语言处理任务的重大进展以及预训练模型的统治地位建立,一直到去年 ChatGPT 的发布将研究范式带向大语言模型+提示工程,但时至今天,BERT 仍然是自然语言处理领域最常用、最重要的预训练模型之一。

二、预训练 + 微调范式

        自然语言处理领域一直在发展变化,能够在各种任务上达到最优效果的模型、算法也层出不穷。最早的范式是文本表示+ 机器学习,如基础 Baseline 所演示的方法,通过将自然语言文本表示为数值向量,再建立统计机器学习模型实习下游任务。但随着深度学习的发展,自2013年,神经网络词向量登上时代舞台,神经网络逐渐成为了 NLP 的核心方法,NLP 的核心研究范式逐渐向深度学习演化。

        深度学习的研究方法,主要是通过多层的神经网络来端到端处理下游任务,将文本表示、特征工程、建模预测都融合在深度神经网络中,减少了人工特征构建的过程,显著提升了自然语言处理能力。神经网络词向量是其中的核心部分,即文本通过神经网络后的向量表示,这些向量表示能够蕴含深层语义且维度合适,后续研究往往可以直接使用以替代传统的文本表示方法,典型的应用如 Word2Vec 。

        但是,Word2Vec 是静态词向量,即对于每一个词有一个固定的向量表示,无法解决一词多义、复杂特征等问题。2018年,ELMo 模型的提出拉开了动态词向量、预训练模型的时代大幕。ELMo 模型基于双向 LSTM 架构,在训练数据上基于语言模型进行预训练,再针对下游任务进行微调,表现出了更加优越的性能,标志着预训练+微调范式的诞生。

        所谓预训练+微调范式,指先在海量文本数据上进行预训练,再针对特定的下游任务进行微调。预训练一般基于语言模型,即给定上一个词,预测下一个词。语言模型可以在所有文本数据上建模,无需人工标注,因此很容易在海量数据上进行训练。通过在海量数据上进行预训练,模型可以学习到深层的自然语言逻辑。再通过在指定的下游任务上进行微调,即针对部分人工标注的任务数据进行特定训练,如文本分类、文本生成等,来训练模型执行下游任务的能力。

 

        预训练+微调范式一定程度上缓解了标注数据昂贵的问题,显著提升了模型性能,但是,ELMo 使用的双向 LSTM 架构存在难以解决长期依赖、并行效果差的天生缺陷,ELMo 本身也保留了词向量作为特征输入的应用,并没能一锤定音地敲定预训练+微调范式的主流地位。2017年,Transformer 模型的提出,为自然语言处理领域带来了一个新的重要成员——Attention 架构。基于 Attention 架构,同样在2018年,OpenAI 提出的 GPT 模型基于 Transformer 模型,结合 ELMo 模型提出的预训练+微调范式,进一步刷新了众多自然语言处理任务的上限。2023年爆火出圈的 ChatGPT 就是以 GPT 模型作为基础架构的。

        从静态编码到神经网络计算的静态词向量,再到基于双向 LSTM 架构的预训练+微调范式,又诞生了基于 Transformer的预训练+微调模式,预训练模型逐步成为自然语言处理的主流。但,真正奠定预训练+微调范式的重要地位的,还是之后提出的 BERT。BERT 可以说是综合了 ELMo 和 GPT,使用预训练+微调范式,基于 Transformer 架构而抛弃了存在天生缺陷的 LSTM,又针对 GPT 仅能够捕捉单向语句关系的缺陷提出了能够捕捉深层双向语义关系的 MLM 预训练任务,从而将预训练模型推向了一个高潮。

三、Transformer 与 Attention

        BERT 乃至目前正火的 LLM 的成功,都离不开 Attention 机制与基于 Attention 机制搭建的 Transformer 架构。此处简单介绍 Transformer 与 Attention 机制。

        在 Attention 机制提出之前,深度学习主要有两种基础架构:卷积神经网络(CNN)与循环神经网络(RNN)。其中,CNN 在 CV 领域表现突出,而 RNN 及其变体 LSTM 在 NLP 方向上一枝独秀。然而,RNN 架构存在两个天然缺陷:① 序列依序计算的模式限制了计算机并行计算的能力,导致 RNN 为基础架构的模型虽然参数量不算特别大,但计算时间成本却很高。 ② RNN 难以捕捉长序列的相关关系。在 RNN 架构中,距离越远的输入之间的关系就越难被捕捉,同时 RNN 需要将整个序列读入内存依次计算,也限制了序列的长度。

        针对上述两个问题, 2017年 Vaswani 等人发表了论文《Attention Is All You Need》,创造性提出了 Attention 机制并完全抛弃了 RNN 架构。Attention 机制最先源于计算机视觉领域,其核心思想为当我们关注一张图片,我们往往无需看清楚全部内容而仅将注意力集中在重点部分即可。而在自然语言处理领域,我们往往也可以通过将重点注意力集中在一个或几个 token,从而取得更高效高质的计算效果。

        Attention 机制的特点是通过计算 Query (查询值)与Key(键值)的相关性为真值加权求和,从而拟合序列中每个词同其他词的相关关系。其大致计算过程如图:

 

        具体而言,可以简单理解为一个输入序列通过不同的参数矩阵映射为 Q、K、V 三个矩阵,其中,Q 是计算注意力的另一个句子(或词组),V 为待计算句子,K 为待计算句子中每个词的对应键。通过对 Q 和 K 做点积,可以得到待计算句子(V)的注意力分布(即哪些部分更重要,哪些部分没有这么重要),基于注意力分布对 V 做加权求和即可得到输入序列经过注意力计算后的输出,其中与 Q (即计算注意力的另一方)越重要的部分得到的权重就越高。

        而 Transformer 正是基于 Attention 机制搭建了 Encoder-Decoder(编码器-解码器)结构,主要适用于 Seq2Seq(序列到序列)任务,即输入是一个自然语言序列,输出也是一个自然语言序列。其整体架构如下:

        Transformer 由一个 Encoder,一个 Decoder 外加一个 Softmax 分类器与两层编码层构成。上图中左侧方框为 Encoder,右侧方框为 Decoder。

        由于是一个 Seq2Seq 任务,在训练时,Transformer 的训练语料为若干个句对,具体子任务可以是机器翻译、阅读理解、机器对话等。在原论文中是训练了一个英语与德语的机器翻译任务。在训练时,句对会被划分为输入语料和输出语料,输入语料将从左侧通过编码层进入 Encoder,输出语料将从右侧通过编码层进入 Decoder。Encoder 的主要任务是对输入语料进行编码再输出给 Decoder,Decoder 再根据输出语料的历史信息与 Encoder 的输出进行计算,输出结果再经过一个线性层和 Softmax 分类器即可输出预测的结果概率,整体逻辑如下图:

 

四、预训练任务

        BERT 的模型架构直接使用了 Transformer 的 Encoder 作为整体架构,其最核心的思想在于提出了两个新的预训练任务——MLM(Masked Language Model,掩码模型)和 NSP(Next Sentence Prediction,下个句子预测),而不是沿用传统的 LM(语言模型)。

 

        MLM 任务,是 BERT 能够深层拟合双向语义特征的基础。简单来讲,MLM 任务即以一定比例对输入语料的部分 token 进行遮蔽,替换为 (MASK)标签,再让模型基于其上下文预测还原被遮蔽的单词,即做一个完形填空任务。由于在该任务中,模型需要针对 (MASK) 标签左右的上下文信息来预测标签本身,从而会充分拟合双向语义信息。

        例如,原始输入为 I like you。以30%的比例进行遮蔽,那么遮蔽之后的输入可能为:I (MASK) you。而模型的任务即为基于该输入,预测出 (MASK) 标签对应的单词为 like。

        NSP 任务,是 BERT 用于解决句级自然语言处理任务的预训练任务。BERT 完全采用了预训练+微调的范式,因此着重通过预训练生成的模型可以解决各种多样化的下游任务。MLM 对 token 级自然语言处理任务(如命名实体识别、关系抽取等)效果极佳,但对于句级自然语言处理任务(如句对分类、阅读理解等),由于预训练与下游任务的模式差距较大,因此无法取得非常好的效果。NSP 任务,是将输入语料都整合成句对类型,句对中有一半是连贯的上下句,标记为 IsNext,一半则是随机抽取的句对,标记为 NotNext。模型则需要根据输入的句对预测是否是连贯上下句,即预测句对的标签。

        例如,原始输入句对可能是 (I like you ; Because you are so good) 以及 (I like you; Today is a nice day)。而模型的任务即为对前一个句对预测 IsNext 标签,对后一个句对预测 NotNext 标签。

        基于上述两个预训练任务,BERT 可以在预训练阶段利用大量无标注文本数据实现深层语义拟合,从而取得良好的预测效果。同时,BERT 追求预训练与微调的深层同步,由于 Transformer 的架构可以很好地支持各类型的自然语言处理任务,从而在 BERT 中,微调仅需要在预训练模型的最顶层增加一个 SoftMax 分类层即可。同样值得一提的是,由于在实际下游任务中并不存在 MLM 任务的遮蔽,因此在策略上进行了一点调整,即对于选定的遮蔽词,仅 80% 的遮蔽被直接遮蔽,其余将有 10% 被随机替换,10% 被还原为原单词。

完整代码如下:

# import 相关库
# 导入前置依赖
import os
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader

# 用于加载bert模型的分词器
from transformers import AutoTokenizer

# 用于加载bert模型
from transformers import BertModel
from pathlib import Path

batch_size = 16
# 文本的最大长度
text_max_length = 128
# 总训练的epochs数,我只是随便定义了个数
epochs = 100
# 学习率
lr = 3e-5
# 取多少训练集的数据作为验证集
validation_ratio = 0.1
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 每多少步,打印一次loss
log_per_step = 50

# 数据集所在位置
dataset_dir = Path("./基于论文摘要的文本分类与关键词抽取挑战赛公开数据")
os.makedirs(dataset_dir) if not os.path.exists(dataset_dir) else ""

# 模型存储路径
model_dir = Path("./model/bert_checkpoints")
# 如果模型目录不存在,则创建一个
os.makedirs(model_dir) if not os.path.exists(model_dir) else ""

print("Device:", device)

# 读取数据集,进行数据处理

pd_train_data = pd.read_csv("train.csv")
pd_train_data["title"] = pd_train_data["title"].fillna("")
pd_train_data["abstract"] = pd_train_data["abstract"].fillna("")

test_data = pd.read_csv("testB.csv")
test_data["title"] = test_data["title"].fillna("")
test_data["abstract"] = test_data["abstract"].fillna("")
pd_train_data["text"] = (
    pd_train_data["title"].fillna("")
    + " "
    + pd_train_data["author"].fillna("")
    + " "
    + pd_train_data["abstract"].fillna("")
    + " "
    + pd_train_data["Keywords"].fillna("")
)
test_data["text"] = (
    test_data["title"].fillna("")
    + " "
    + test_data["author"].fillna("")
    + " "
    + test_data["abstract"].fillna("")
    + " "
    + pd_train_data["Keywords"].fillna("")
)

# 从训练集中随机采样测试集
validation_data = pd_train_data.sample(frac=validation_ratio)
train_data = pd_train_data[~pd_train_data.index.isin(validation_data.index)]


# 构建Dataset
class MyDataset(Dataset):
    def __init__(self, mode="train"):
        super(MyDataset, self).__init__()
        self.mode = mode
        # 拿到对应的数据
        if mode == "train":
            self.dataset = train_data
        elif mode == "validation":
            self.dataset = validation_data
        elif mode == "test":
            # 如果是测试模式,则返回内容和uuid。拿uuid做target主要是方便后面写入结果。
            self.dataset = test_data
        else:
            raise Exception("Unknown mode {}".format(mode))

    def __getitem__(self, index):
        # 取第index条
        data = self.dataset.iloc[index]
        # 取其内容
        text = data["text"]
        # 根据状态返回内容
        if self.mode == "test":
            # 如果是test,将uuid做为target
            label = data["uuid"]
        else:
            label = data["label"]
        # 返回内容和label
        return text, label

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


train_dataset = MyDataset("train")
validation_dataset = MyDataset("validation")
train_dataset.__getitem__(0)

# 获取Bert预训练模型
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")


# 接着构造我们的Dataloader。
# 我们需要定义一下collate_fn,在其中完成对句子进行编码、填充、组装batch等动作:
def collate_fn(batch):
    """
    将一个batch的文本句子转成tensor,并组成batch。
    :param batch: 一个batch的句子,例如: [('推文', target), ('推文', target), ...]
    :return: 处理后的结果,例如:
             src: {'input_ids': tensor([[ 101, ..., 102, 0, 0, ...], ...]), 'attention_mask': tensor([[1, ..., 1, 0, ...], ...])}
             target:[1, 1, 0, ...]
    """
    text, label = zip(*batch)
    text, label = list(text), list(label)

    # src是要送给bert的,所以不需要特殊处理,直接用tokenizer的结果即可
    # padding='max_length' 不够长度的进行填充
    # truncation=True 长度过长的进行裁剪
    src = tokenizer(
        text,
        padding="max_length",
        max_length=text_max_length,
        return_tensors="pt",
        truncation=True,
    )

    return src, torch.LongTensor(label)


train_loader = DataLoader(
    train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn
)
validation_loader = DataLoader(
    validation_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn
)

inputs, targets = next(iter(train_loader))
print("inputs:", inputs)
print("targets:", targets)


# 定义预测模型,该模型由bert模型加上最后的预测层组成
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()

        # 加载bert模型
        self.bert = BertModel.from_pretrained("bert-base-uncased", mirror="tuna")

        # 最后的预测层
        self.predictor = nn.Sequential(
            nn.Linear(768, 256), nn.ReLU(), nn.Linear(256, 1), nn.Sigmoid()
        )

    def forward(self, src):
        """
        :param src: 分词后的推文数据
        """

        # 将src直接序列解包传入bert,因为bert和tokenizer是一套的,所以可以这么做。
        # 得到encoder的输出,用最前面[CLS]的输出作为最终线性层的输入
        outputs = self.bert(**src).last_hidden_state[:, 0, :]

        # 使用线性层来做最终的预测
        return self.predictor(outputs)


model = MyModel()
model = model.to(device)

# 定义出损失函数和优化器。这里使用Binary Cross Entropy:
criteria = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)


# 由于inputs是字典类型的,定义一个辅助函数帮助to(device)
def to_device(dict_tensors):
    result_tensors = {}
    for key, value in dict_tensors.items():
        result_tensors[key] = value.to(device)
    return result_tensors


# 定义一个验证方法,获取到验证集的精准率和loss
def validate():
    model.eval()
    total_loss = 0.0
    total_correct = 0
    for inputs, targets in validation_loader:
        inputs, targets = to_device(inputs), targets.to(device)
        outputs = model(inputs)
        loss = criteria(outputs.view(-1), targets.float())
        total_loss += float(loss)

        correct_num = (((outputs >= 0.5).float() * 1).flatten() == targets).sum()
        total_correct += correct_num

    return total_correct / len(validation_dataset), total_loss / len(validation_dataset)


# 首先将模型调成训练模式
model.train()

# 清空一下cuda缓存
if torch.cuda.is_available():
    torch.cuda.empty_cache()

# 定义几个变量,帮助打印loss
total_loss = 0.0
# 记录步数
step = 0

# 记录在验证集上最好的准确率
best_accuracy = 0

# 开始训练
for epoch in range(epochs):
    model.train()
    for i, (inputs, targets) in enumerate(train_loader):
        # 从batch中拿到训练数据
        inputs, targets = to_device(inputs), targets.to(device)
        # 传入模型进行前向传递
        outputs = model(inputs)
        # 计算损失
        loss = criteria(outputs.view(-1), targets.float())
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

        total_loss += float(loss)
        step += 1

        if step % log_per_step == 0:
            print(
                "Epoch {}/{}, Step: {}/{}, total loss:{:.4f}".format(
                    epoch + 1, epochs, i, len(train_loader), total_loss
                )
            )
            total_loss = 0

        del inputs, targets

    # 一个epoch后,使用过验证集进行验证
    accuracy, validation_loss = validate()
    print(
        "Epoch {}, accuracy: {:.4f}, validation loss: {:.4f}".format(
            epoch + 1, accuracy, validation_loss
        )
    )
    torch.save(model, model_dir / f"model_{epoch}.pt")

    # 保存最好的模型
    if accuracy > best_accuracy:
        torch.save(model, model_dir / f"model_best.pt")
        best_accuracy = accuracy

# 加载最好的模型,然后进行测试集的预测
model = torch.load(model_dir / f"model_best.pt")
model = model.eval()

test_dataset = MyDataset("test")
test_loader = DataLoader(
    test_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn
)

results = []
for inputs, ids in test_loader:
    outputs = model(inputs.to(device))
    outputs = (outputs >= 0.5).int().flatten().tolist()
    ids = ids.tolist()
    results = results + [(id, result) for result, id in zip(outputs, ids)]

test_label = [pair[1] for pair in results]
test_data["label"] = test_label
test_data["Keywords"] = test_data["title"].fillna("")
test_data[["uuid", "Keywords", "label"]].to_csv("bert_submit_task1.csv", index=None)

结果如下:

查看时间:2023年8月18日10:17:23

方法3:深度学习Topline方案

一、比赛数据集分析:

1.比赛须知

        通过论文标题、摘要、作者这三个信息来判断该文献是否属于医学领域的文献,是则标签为1,不是则标签为0

2.数据集分析

        问题一:数据集中的样本数目是多少?

        答:训练集有6000条样本,测试集有2000条样本。

        经验:这个训练集大小对于分类任务来说已经足够了,再加上判断医学文献的这个二分类任务较为简单,所以用预训练微调的方法能取得很高的分数

        问题二:训练集中的样本数量是否均衡?

        答:正样本共2921条,负样本共3079条。

        经验:这个比例还算是均衡的,不需要做什么处理。

        问题三:比较训练集每个样本的文本内容(标题+作者+摘要),最小长度、最大长度、平均长度分别是多少?

        答:最小长度是21个单词,最大长度是1458个单词,平均长度约209个单词。

        经验:分词的时候,max_len肯定不能设置小于21,初步考虑128~768(在Topline中设置为128)

二、Topline方法:预训练微调+特征融合+后处理

        若是追求高分数,可以优先考虑模型集成的方法。

        在代码中,我们提供了一个模型集成的整体流程框架,可以直接训练并推理多个模型的~

        但实际上,只需将单个模型进行微调便能达到满分的表现。

        模型的理论部分可参考Baseline“任务二:进阶实践 - 深度方法”-“BERT介绍”中的内容。(链接直达:AI夏令营第三期 - 基于论文摘要的文本分类与关键词抽取挑战赛教程 )其中,在此处所使用到的预训练模型为Bert的改进版——Roberta-base。它与Bert的区别在于:①Roberta在预训练的阶段中没有对下一句话进行预测(NSP)②采用了动态掩码 ③使用字符级和词级别表征的混合文本编码。(对Roberta感兴趣的同学,非常推荐你阅读一下原论文:https://arxiv.org/pdf/1907.11692.pdf)

        与常规的预训练模型接分类器不同,我对网络结构进行了更进一步的改进,具体细节如下:

        在模型结构上使用了以下两个特征:

          ①特征1:MeanPooling(768维) -> fc(128维)

          ②特征2:Last_hidden (768维) -> fc(128维)

        其中,特征1指的是将Roberta所输出的全部序列分词的表征向量先进行一个平均池化再接一个全连接层(fc,Fully Connected Layer);特征2指的是将Roberta的pooled_output接一个全连接层(fc,Fully Connected Layer)。(pooled_output = [CLS]的表征向量接入一个全连接层,再输入至Tanh激活函数)

        然后,将这两个特征进行加权并相加即可输进分类器进行训练。(在代码中,仅是将它们进行等权相加。后续当然也可以尝试分配不同的权重,看能否获得更好的性能)(Dropout层其实并不是一个必要项,可加可不加~)

        最后,将训练好的模型用于推理测试集,并根据标签数目的反馈,对预测阈值进行调整。(后处理)

        在代码部分中,主要分为四个模块:1.数据处理 2.模型训练 3.模型评估 4.测试集推理

以下是代码文件的目录结构:

        文件夹/代码文件的命名含义:

---------------------------------------------------------------------------------------------------------------------------

        checkpoints -> 存放模型在训练阶段的临时权重

        dataset -> 存放数据集,包含train.csv、testB.csv和testB_submit_exsample.csv文件

        evaluate_prediction -> 存放模型在评估阶段的临时文件(模型对验证集的推理概率文件)

        models_input_files -> 存放模型的输入文件

        models_prediction -> 存放模型对测试集的推理概率文件

        premodels -> 存放在线下载的预训练模型权重、config.json等文件

---------------------------------------------------------------------------------------------------------------------------

        data_process.py -> 数据处理

        models_training.py -> 模型训练

        evaluate_models.py -> 模型评估

        ensemble_and_submit.py -> 推理测试集(其实这里也包括模型集成的部分,不过这里没用到)

---------------------------------------------------------------------------------------------------------------------------

        我们这边已经提前将代码打包好啦~ 解压即可运行: https://wwvl.lanzout.com/ib9Wb15rxt0b

1、 运行代码 - 对数据进行处理

        在终端输入“python data_process.py”并回车。

2、运行代码 - 训练模型

        在终端输入“python models_training.py”并回车。

        (batch_size改为8可以达到跑分1的效果)

3、运行代码 - 测试集推理

        在终端输入“python ensemble_and_submit.py”并回车。

      (把阈值从0.001改为0.0005和0.999改为0.9995也可以使跑分为1)

结果显示:

 

三、还可继续优化/探索的地方(进阶/高阶玩法)

        问:用Topline都拿到满分了,为什么还要继续优化和探索?

        答:学无止境。

①调整超参数

        包括学习率、Batch_size、正则化系数等,可以使用网格搜索的方法(Grid search)来寻找模型更好的超参数组合。

②调整最大序列长度

        在数据处理阶段中,调整最大序列长度MAX_LEN。

③更改损失函数

        一般分类任务中,最常使用的损失函数是交叉熵(CE, Cross-Entropy),但最简单的也可以换成BCE。

④冻结模型部分参数

        如feature1在模型训练的前期,相对feature2而言可能并不能带来太好的表征,所以第一个epoch可以先将它的参数进行冻结(又或者将该特征对应全连接层的学习率调小),然后等到第二个epoch后再正常训练。

⑤融合更多特征

        融合更多特征,如考虑加入Glove、Word2Vec、Fasttext结合TextCNN、BiLSTM、LSTM+Attention等特征提取器所提取的文本特征。

⑥模型集成

        使用一或多个与本文模型能够互补优劣的模型,并进行集成。

⑦对比学习

        设计代理任务,加入对比损失函数,在训练阶段中获得更好的嵌入表示,提高模型性能。

⑧提示学习

        在预训练模型的基础上使用提示学习范式,通过硬提示/软提示的方法提高模型性能。

⑨......

方法4:大模型:微调ChatGLM2-6B解决文本二分类问题

  • clone微调脚本:git clone https://github.com/KMnO4-zx/xfg-paper.git

  • 下载chatglm2-6b模型:git clone https://huggingface.co/THUDM/chatglm2-6b,这行命令可能会失败,没关系多试几次!模型下载时间需要十几分钟,大家耐心等待哦~

  • (如果卡在中间也可以在确保git clone https://huggingface.co/THUDM/chatglm2-6b执行成功但无反应后,进入''chatglm2-6b''文件下,用如下命令下载模型【慎用】)

    此命令只用于下载模型文件,py文件不会下载。

     
       

    wget https://cloud.tsinghua.edu.cn/seafhttp/files/f3e22aa1-83d1-4f83-917e-cf0d19ad550f/pytorch_model-00001-of-00007.bin https://cloud.tsinghua.edu.cn/seafhttp/files/0b6a3645-0fb7-4931-812e-46bd2e8d8325/pytorch_model-00002-of-00007.bin https://cloud.tsinghua.edu.cn/seafhttp/files/f61456cb-5283-4529-a7bc-400355140e4b/pytorch_model-00003-of-00007.bin https://cloud.tsinghua.edu.cn/seafhttp/files/1a1f68c5-1a7d-489a-8f16-8432a099d782/pytorch_model-00004-of-00007.bin https://cloud.tsinghua.edu.cn/seafhttp/files/6357afba-bb40-4348-bc33-f08c1fcc2936/pytorch_model-00005-of-00007.bin https://cloud.tsinghua.edu.cn/seafhttp/files/ebec3ae2-5ae4-432c-83e4-df4b147026bb/pytorch_model-00006-of-00007.bin https://cloud.tsinghua.edu.cn/seafhttp/files/7d1aab8a-d255-47f7-87c9-4c0593379ee9/pytorch_model-00007-of-00007.bin https://cloud.tsinghua.edu.cn/seafhttp/files/4daca87e-0d34-4cff-bd43-5a40fcdf4ab1/tokenizer.model

    进入目录安装环境:cd ./xfg-paperpip install -r requirements.txt

  • 将脚本中的model_name_or_path更换为你本地的chatglm2-6b模型路径,然后运行脚本:sh xfg_train.sh

  • 微调过程大概需要两个小时(我使用阿里云A10-24G运行了两个小时左右),微调过程需要15G的显存,推荐使用16G、24G显存的显卡,比如3090,4090等。

  • 当然,我们已经把训练好的lora权重放在了仓库里,您可以直接运行下面的代码。

  • 也可以选择运行仓库内的jupyter notebook文件

CUDA_VISIBLE_DEVICES=0 python src/train_bash.py \
    --model_name_or_path chatglm2-6b \ 本地模型的目录
    --stage sft \ 微调方法
    --use_v2 \ 使用glm2模型微调,默认值true
    --do_train \ 是否训练,默认值true
    --dataset paper_label \ 数据集名字
    --finetuning_type lora \ 
    --lora_rank 8 \  LoRA 微调中的秩大小
    --output_dir ./output/label_xfg \ 输出lora权重存放目录
    --per_device_train_batch_size 4 \ 用于训练的批处理大小
    --gradient_accumulation_steps 4 \ 梯度累加次数
    --lr_scheduler_type cosine \
    --logging_steps 10 \ 日志输出间隔
    --save_steps 1000 \ 断点保存间隔
    --learning_rate 5e-5 \ 学习率
    --num_train_epochs 4.0 \ 训练轮数
    --fp16 是否使用 fp16 半精度 默认值:False

导入数据

# 导入 pandas 库,用于数据处理和分析
import pandas as pd
# 读取训练集和测试集
train_df = pd.read_csv('./csv_data/train.csv')
testB_df = pd.read_csv('./csv_data/testB.csv')

制作数据集

# 创建一个空列表来存储数据样本
res = []

# 遍历训练数据的每一行
for i in range(len(train_df)):
    # 获取当前行的数据
    paper_item = train_df.loc[i]
    # 创建一个字典,包含指令、输入和输出信息
    tmp = {
    "instruction": "Please judge whether it is a medical field paper according to the given paper title and abstract, output 1 or 0, the following is the paper title and abstract -->",
    "input": f"title:{paper_item[1]},abstract:{paper_item[3]}",
    "output": str(paper_item[5])
  }
    # 将字典添加到结果列表中
    res.append(tmp)

# 导入json包,用于保存数据集
import json
# 将制作好的数据集保存到data目录下
with open('./data/paper_label.json', mode='w', encoding='utf-8') as f:
    json.dump(res, f, ensure_ascii=False, indent=4)

修改data_info

  • 修改data目录下data_info文件

{
  "paper_label": {
    "file_name": "paper_label.json"
  }
}

加载训练好的LoRA权重,进行预测

# 导入所需的库和模块
from peft import PeftModel
from transformers import AutoTokenizer, AutoModel, GenerationConfig, AutoModelForCausalLM

# 定义预训练模型的路径
model_path = "../chatglm2-6b"
model = AutoModel.from_pretrained(model_path, trust_remote_code=True).half().cuda()
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
# 加载 label lora权重
model = PeftModel.from_pretrained(model, './output/label_xfg').half()
model = model.eval()
# 使用加载的模型和分词器进行聊天,生成回复
response, history = model.chat(tokenizer, "你好", history=[])
response

# 预测函数

def predict(text):
    # 使用加载的模型和分词器进行聊天,生成回复
    response, history = model.chat(tokenizer, f"Please judge whether it is a medical field paper according to the given paper title and abstract, output 1 or 0, the following is the paper title and abstract -->{text}", history=[],
    temperature=0.01)
    return response

制作submit

# 预测测试集
# 导入tqdm包,在预测过程中有个进度条
from tqdm import tqdm

# 建立一个label列表,用于存储预测结果
label = []

# 遍历测试集中的每一条样本
for i in tqdm(range(len(testB_df))):
    # 测试集中的每一条样本
    test_item = testB_df.loc[i]
    # 构建预测函数的输入:prompt
    test_input = f"title:{test_item[1]},author:{test_item[2]},abstract:{test_item[3]}"
    # 将预测结果存入lable列表
    label.append(int(predict(test_input)))

# 把label列表赋予testB_df
testB_df['label'] = label
# task1虽然只需要label,但需要有一个keywords列,用个随意的字符串代替
testB_df['Keywords'] = ['tmp' for _ in range(2000)]
# 制作submit,提交submit
submit = testB_df[['uuid', 'Keywords', 'label']]
submit.to_csv('submit.csv', index=False)
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
计算机夏令营面试中,常问的专业问题涵盖了计算机科学与技术的基础知识、实际应用能力以及专业发展方向等多个方面。以下是几个常见的问题及回答: 1. "请介绍一下你的专业背景和学习经历。" 回答时可以简要概括自己的专业背景,包括计算机科学相关的课程和项目经验。同时,突出个人学习方法和对计算机科学的热情,以及如何与团队合作等。 2. "你在哪个领域的计算机技术有专长?" 可以根据个人兴趣和学习经历选择一个具体领域,如人工智能、网络安全、数据分析等。接着可以分享相关项目经验,参与的竞赛或开源项目等,突出自己在该领域的学习和实践能力。 3. "你对编程语言的了解和应用如何?" 回答时可以先介绍熟悉的编程语言,如C++、Java等,并简单说明掌握的能力和项目开发经验。可以结合实例,讲述自己解决问题或优化程序的经验,以及对新兴编程语言或开发框架的学习能力。 4. "你在实际项目中遇到过哪些挑战,如何解决的?" 可以选择一个具体的项目案例,介绍自己在项目中所面临的难题,如性能优化、需求变更等,并展示自己的解决方案。可以突出自己的分析思路、团队合作能力和解决问题的效果。 5. "你未来的发展方向是什么?" 可以通过介绍自己对计算机科学与技术的热情和兴趣,以及在学校或实习中获得的经验来回答这个问题。可以提及个人的职业规划,如继续深耕某个领域、不断学习新技术或追求研究方向等。 6. "你在课外有参与过哪些计算机相关的活动或项目?" 可以分享一些有趣的课外活动或计算机相关的项目,如参加编程比赛、担任学生科技协会的职位、参与开源项目等。突出自己的团队合作能力、创新思维和对计算机技术的探索精神。 在面试中,回答问题时需简洁明了,突出自己的优势和个人特点,同时展示学习能力和团队合作能力。此外,可以提前了解常见的计算机领域知识和技术趋势,以便更好地回答相关问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

xingzhiyao123456

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

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

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

打赏作者

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

抵扣说明:

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

余额充值