1. 学习内容
AI夏令营第三期–基于论文摘要的文本分类与关键词抽取挑战赛教程
1.1 文本分类的两种实现思路
✅ 特征提取 + 机器学习:
- 数据预处理: 首先,对文本数据进行预处理,包括文本清洗(如去除特殊字符、标点符号)、分词等操作。可以使用常见的NLP工具包(如NLTK或spaCy)来辅助进行预处理。
- 特征提取: 使用TF-IDF(词频-逆文档频率)或BOW(词袋模型)方法将文本转换为向量表示。TF-IDF可以计算文本中词语的重要性,而BOW则简单地统计每个词语在文本中的出现次数。可以使用scikit-learn库的TfidfVectorizer或CountVectorizer来实现特征提取。
- 构建训练集和测试集: 将预处理后的文本数据分割为训练集和测试集,确保数据集的样本分布均匀。
- 选择机器学习模型: 根据实际情况选择适合的机器学习模型,如朴素贝叶斯、支持向量机(SVM)、随机森林等。这些模型在文本分类任务中表现良好。可以使用scikit-learn库中相应的分类器进行模型训练和评估。
- 模型训练和评估: 使用训练集对选定的机器学习模型进行训练,然后使用测试集进行评估。评估指标可以选择准确率、精确率、召回率、F1值等。
- 调参优化: 如果模型效果不理想,可以尝试调整特征提取的参数(如词频阈值、词袋大小等)或机器学习模型的参数,以获得更好的性能。
前天和昨天采用的方法就是上述的特征提取+机器学习方法。
✅ Bert模型(深度学习方法之一):
- 数据预处理: 首先,对文本数据进行预处理,包括文本清洗(如去除特殊字符、标点符号)、分词等操作。可以使用常见的NLP工具包(如NLTK或spaCy)来辅助进行预处理。
- 构建训练所需的dataloader与dataset: 构建Dataset类时,需要定义三个方法
__init__
,__getitem__
,__len__
, 其中__init__
方法完成类初始化,__getitem__
要求返回返回内容和label,__len__
方法返回数据长度 - 构造Dataloader: 在其中完成对句子进行编码、填充、组装batch等动作:
- 定义预测模型利用预训练的BERT模型: 使用BERT模型编码中的 [CLS]向量 来完成二分类任务,[CLS]就是classification的意思,可以理解为用于下游的分类任务。
单文本分类任务:对于文本分类任务,BERT模型在文本前插入一个[CLS]符号,并将该符号对应的输出向量作为整篇文本的语义表示,用于文本分类。可以理解为:与文本中已有的其它字/词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个字/词的语义信息。
在模型设计中思路就体现为我们取出文本数据经过向量化后的[CLS]向量,然后经过二分类预测层得到最终的结果。
outputs = self.bert(**src).last_hidden_state[:, 0, :] self.predictor(outputs) self.predictor = nn.Sequential( nn.Linear(768, 256), nn.ReLU(), nn.Linear(256, 1), nn.Sigmoid() )
- 模型训练和评估: 使用训练集对选定的机器学习模型进行训练,然后使用测试集进行评估。评估指标可以选择准确率、精确率、召回率、F1值等。
- 调参优化: 如果模型效果不理想,可以尝试调整特征提取的参数(如词频阈值、词袋大小等)或机器学习模型的参数,以获得更好的性能。
1.2 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 仍然是自然语言处理领域最常用、最重要的预训练模型之一。
✏️:BERT(Bidirectional Encoder Representations from Transformers)的意思是基于双向Transformer的编码器表示,BERT的核心思想是使用双向Transformer来编码文本数据,从而获得文本中每个词的上下文相关的向量表示,然后将这些向量表示作为输入,用于不同的下游任务,如文本分类、文本生成、文本摘要等。
✏️:BERT的 预训练(Pre-training) 过程是指在大规模的通用数据集上进行无监督或自监督的学习,目的是让模型学习到通用的知识和能力,如词汇、语法、语义、逻辑、常识等。
BERT使用了两种预训练任务,分别是:
掩码语言模型(Masked Language Model,MLM):这个任务是指在输入的文本中随机地遮盖一些词,然后让模型根据上下文来预测被遮盖的词。这个任务可以让模型学习到词汇和语法的知识。
下一个句子预测(Next Sentence Prediction,NSP):这个任务是指给定两个句子A和B,让模型判断B是否是A的下一个句子。这个任务可以让模型学习到语义和逻辑的知识。
BERT使用了数TB甚至数PB的数据集来进行预训练,如英文维基百科、书籍语料库等。BERT使用了数千甚至数万个GPU或TPU等高性能计算设备来进行并行计算和优化。BERT预训练后得到了一个通用的编码器模型,它可以将任意长度的文本转换为固定长度的向量表示。
✏️:BERT的 微调(Fine-tuning) 过程是指在有标注的数据上进行有监督的学习,目的是让模型适应特定的任务和场景,如文本分类、文本生成、文本摘要等。BERT使用了一种简单而有效的微调方法,即在预训练好的编码器模型上添加一个简单的输出层,然后根据不同的任务和场景来调整输出层的结构和参数。例如,在文本分类任务中,输出层可以是一个全连接层或者一个softmax层;在文本生成任务中,输出层可以是一个解码器或者一个线性层等。
BERT使用了少量标注好的数据来进行微调,如GLUE、SQuAD等公开数据集。BERT使用了相对较少的计算资源来进行微调,一般只需要几个小时或几天就可以完成。BERT微调后得到了一个针对特定任务和场景的模型,它可以根据输入的文本来产生相应的输出或行为。
✏️:以上就是BERT处理文本分类任务的预训练和微调过程。从这个过程中可以看出,BERT利用了“大规模预训练+微调”的范式,在预训练阶段学习到通用的知识和能力,在微调阶段适应特定的任务和场景,在各种领域和场景中都能够展现出惊人的效果。事实上,BERT不仅在文本分类任务上表现优异,还在文本生成、文本摘要、机器翻译、问答系统等任务上刷新了多项记录,成为了自然语言处理领域的一个里程碑技术。
1.3 预训练 + 微调范式
自然语言处理最早的范式是文本表示+ 机器学习,如 Baseline 所演示的方法,通过将自然语言文本表示为数值向量,再建立统计机器学习模型实行下游任务。
深度学习的研究方法,主要是通过多层的神经网络来端到端处理下游任务,将文本表示、特征工程、建模预测都融合在深度神经网络中,减少了人工特征构建的过程,显著提升了自然语言处理能力。神经网络词向量是其中的核心部分,即文本通过神经网络后的向量表示,这些向量表示能够蕴含深层语义且维度合适,后续研究往往可以直接使用以替代传统的文本表示方法,典型的应用如 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 预训练任务,从而将预训练模型推向了一个高潮。
✏️ :语义表征的计算可分为三个阶段,分别是:
一、特征工程(规则)阶段,以词袋模型(BOW)为典型代表,语义局限于字面相同与否。
二、浅层表征(深度学习)阶段,以word2vec为典型代表,词的特征表达具有聚类性质和线性性质,体现了语义,但没有体现上下文语义。
三、深层表征(预训练语言模型)阶段,以基于transformer的Bert为典型代表,在大量语料上进行自监督训练,表征参数和特征提取组件的一体化。
✏️ :NLP预训练技术演进特点:
一、模型从浅变深,从简单NNLM变化到深层的transformer。
二、预训练任务逐渐复杂,从上下文预测演进成完形填空。
三、任务从拆分静态词向量向 深度语义向量一体化演进。
四、语义从表面向深层,语义从孤立到上下文情景敏感。
2. 实践项目
任务平台
本次夏令营的代码运行平台是百度的 AI Studio,运行结果提交至讯飞开放平台进行验证评分。
题目要求
机器通过对论文摘要等信息的理解,判断该论文是否属于医学领域的文献。
任务示例
输入:
论文信息,格式如下:
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,分数越高,效果越好。
模型评估的指标可以按照不同的任务进行分类,主要包括:
分类任务评估指标: 准确率(Accuracy)、精确率(Precision)、召回率(Recall)、PR 曲线、F1 值、ROC
曲线、AUC、对数损失
回归任务评估指标:平均绝对误差(MAE)、均方误差(MSE)、均方根误差(RMSE)、归一化均方根误差(NRMSE)、决定系数(R2)
聚类任务评估指标:纯度、NMI、兰德系数、调整兰德系数
目标检测任务评估指标:IOU
3. 实践代码
以下代码是参照baseline的,由于在本地跑,而且显卡很一般,所以batch_size从100降到1了。
#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 = 1 # 文本的最大长度 text_max_length = 128 # 总训练的epochs数 epochs = 10 # 学习率 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("./Dataset") 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('./Dataset/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('./Dataset/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. 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) if hasattr(torch.cuda, 'empty_cache'): torch.cuda.empty_cache() # 首先将模型调成训练模式 model.train() # 清空一下cuda缓存 if torch.cuda.is_available(): torch.cuda.empty_cache() # 定义几个变量,帮助打印loss total_loss = 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('submit_task_bert.csv', index=None)
4. 实践成绩
8.19凌晨 bert模型返回分数:0.89562
相比之前的机器学习方法,效果显著提升
8.14 submit_task1.csv 得分:0.67116
第一次测试是基于逻辑回归模型的,没有考虑停用词;
8.16 submit_task_LR.csv 得分:0.67435
第二次测试也是基于逻辑回归模型的,但考虑了停用词;
8.16 submit_task_SVM.csv 得分:0.6778
第三次测试是基于SVM模型的,考虑了停用词;
8.16 submit_task_AdaBoost.csv 得分:0.76263
第四次测试是基于AdaBoost模型的,考虑了停用词;
8.17 尝试了XGBoost模型、GBDT模型,并且稍微调整了一下AdaBoost参数,但效果不行,得分比baseline低了将近0.01,这里就不展示了。