使用预训练的BERT模型解决文本二分类和关键词提取

一. 使用预训练的BERT模型解决文本二分类问题

1. 导入相关的包

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

2. 定义相关参数

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)

3. 进行数据读取与数据预处理

文本分类需要数据来训练模型,所以先读取数据集并进行预处理。这里我加载了训练集和验证集的CSV文件,主要进行了以下处理:

  • 填充空值
  • 拼接标题、作者、摘要等字段得到文本内容
  • 从训练集中采样部分数据作为验证集

先查看一下当前所在路径
%pwd

然后,读取数据集,进行数据处理

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)]

4. 构建训练所需的dataloader与dataset

4.1 自定义的Dataset类

文本不能直接输入BERT模型,需要先通过tokenizer转换为id序列。这里我加载了AutoTokenizer,并构建了自定义的Dataset类,主要完成:

  • tokenizer分词、截断、padding
  • 定义__getitem__和__len__方法,方便后续生成batch(pytorch框架中Dataset类的标准写法)
# 构建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)

```python
train_dataset = MyDataset('train')
validation_dataset = MyDataset('validation')

我们使用__getitem__函数返回索引0的数据看看其内容:
train_dataset.__getitem__(0)

output:
('Accessible Visual Artworks for Blind and Visually Impaired People: Comparing a Multimodal Approach with Tactile Graphics Quero, Luis Cavazos; Bartolome, Jorge Iranzo; Cho, Jundong Despite the use of tactile graphics and audio guides, blind and visually impaired people still face challenges to experience and understand visual artworks independently at art exhibitions. Art museums and other art places are increasingly exploring the use of interactive guides to make their collections more accessible. In this work, we describe our approach to an interactive multimodal guide prototype that uses audio and tactile modalities to improve the autonomous access to information and experience of visual artworks. The prototype is composed of a touch-sensitive 2.5D artwork relief model that can be freely explored by touch. Users can access localized verbal descriptions and audio by performing touch gestures on the surface while listening to themed background music along. We present the design requirements derived from a formative study realized with the help of eight blind and visually impaired participants, art museum and gallery staff, and artists. We extended the formative study by organizing two accessible art exhibitions. There, eighteen participants evaluated and compared multimodal and tactile graphic accessible exhibits. Results from a usability survey indicate that our multimodal approach is simple, easy to use, and improves confidence and independence when exploring visual artworks. accessibility technology; multimodal interaction; auditory interface; touch interface; vision impairment', 0)

可以看到输出包含文本和标签

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

扫描二维码关注公众号,回复: 16839868 查看本文章

4.2 构造Dataloader

#接着构造我们的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)

构造训练集和验证集的dataloader,并打印输出看一下loader里每个样本的内容:

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)

5. 模型定义

加载预训练的BERT模型,构建一个神经网络,包含BERT编码器和新增的分类头,如线性层等,完成文本特征提取和分类。

#定义预测模型,该模型由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)

6. 定义损失函数和优化器

指定训练过程的优化器、loss函数、策略等,如Adam优化器、交叉熵损失函数等。在训练循环中,完成模型的训练、验证、保存最佳模型等。

#定义出损失函数和优化器。这里使用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

7. 定义验证方法

定义一个验证方法,获取到验证集的精准率和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)

8. 模型训练

# 首先将模型调成训练模式
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

9. 预测

加载最好的模型,然后进行测试集的预测

model = torch.load(model_dir / f"model_best.pt")
model = model.eval()

10.输出结果

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[['uuid', 'label']].to_csv('submit_task1.csv', index=None)

此时,针对任务一标签labels的预测结果就保存到 submit_task1.csv 文件中了。

将模型分类预测的结果保存为CSV文件提交。

这样,使用BERT做文本分类的主要流程就介绍完毕了。这种预训练-微调的方式结合了BERT强大的语义表示能力,可以显著提升文本分类的效果。后续还可以进一步优化模型结构、超参等,进一步挖掘模型效果。

二. 使用预训练的BERT模型解决关键词提取

当我们想从特定文档中了解关键信息时,通常会转向关键词提取(keyword extraction)。关键词提取(keyword extraction)是提取与输入文本最相关的词汇、短语的自动化过程。
目前已经有很多的集成工具库类似KeyBERT,已经能够达到非常好的效果,但是在这里我们选择使用BERT创建自己的关键词提取模型,来帮助大家更好的完成文本关键词提取的整个流程

1.导入相关库


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

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

# 导入计算相似度前置库,为了计算候选者和文档之间的相似度,我们将使用向量之间的余弦相似度,因为它在高维度下表现得相当好。
from sklearn.metrics.pairwise import cosine_similarity

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

2. 读取数据集并处理

这里使用的是B榜的测试集,因为7月24号B榜开放后,任务一和任务二就合二为一了,单纯用以前的测试集去预测标签对最终的提交没意义。

# 读取数据集
test = pd.read_csv('../../原始数据/testB.csv')
test['title'] = test['title'].fillna('')
test['abstract'] = test['abstract'].fillna('')

test['text'] = test['title'].fillna('') + ' ' +test['abstract'].fillna('')

停用词过滤是提高关键词质量的重要手段。我提供了一个停用词表,将无实际语义的词过滤掉。建议根据文本领域调整停用词表,使滤除效果更好。

# 定义停用词,去掉出现较多,但对文章不关键的词语
stops =[i.strip() for i in open(r'./stop.txt',encoding='utf-8').readlines()] 

3. 加载BERT模型

这里我们选择了distiluse-base-multilingual-cased这个预训练BERT模型。这是一个 distilled版本的多语言BERT,效果较好且体积较小。我们使用它来编码文本,获取文本的语义表示向量。由于transformer模型有token长度限制,所以在输入大型文档时,你可能会遇到一些错误。在这种情况下,您可以考虑将您的文档分割成几个小的段落,并对其产生的向量进行平均池化(mean pooling ,要取平均值)。

model = SentenceTransformer(r'xlm-r-distilroberta-base-paraphrase-v1')

4. 提取关键词

针对输入文本,我们使用TF-IDF等方法提取一系列关键词候选。同时设置ngram范围,控制返回的关键词长度。

这里使用的思路是获取文本内容的embedding,同时与文本标题的embedding进行比较,文章的关键词往往与标题内容有很强的相似性,为了计算候选者和文档之间的相似度,我们将使用向量之间的余弦相似度,因为它在高维度下表现得相当好。

但这种方法可能在某些文本中是存在一定的问题,例如文本标题不一定含有所有的关键词

分享一位"尼莫"大佬的思路,很有启发:

我简单分享一下我探索任务2的路程吧,之后有时间完善学习笔记的博客会有比较详细的做法。

一开始我做任务2是用的抽取式的做法。用的是baseline的代码,先用词频统计得出候选关键词,再将候选关键词和标题向量化,通过比较余弦相似度来提取关键词。因为之前有做过数据分析,关键词的组成单词可能不止一个,所以最开始候选关键词的提取我用的是1~5Gram,但是在A榜测试上效果不好,我就改用1Gram、2Gram、……、5Gram,分别提取由不同数量单词组成的关键词作为独立的候选组,每个候选组都通过余弦相似度抽取关键词,此时A榜正确率达到了20%左右。此时,我发现了一个bug,按照现行的任务2评价标准,只要关键词足够多,总是能把所有关键词蒙对的,所以我向组织方反应了这个问题。本以为他们会改进评价标准,结果直接限制关键词提取数量了。

之后进一步观察数据集,发现很多样本的关键词不在标题和摘要中,此时用抽取式的关键词提取就很难拿到高分了。因此,我转向了生成式关键词提取,生成式关键词提取的做法和昨天直播分享的做法是一样的,大家看视频就好,在此不赘述。

后来,我认为抽取式方法和生成式方法可以互补,就继续研究了TextRank算法。最终形成了余弦相似度关键词提取(抽取式)+TextRank关键词提取(抽取式)+BART生成式关键词提取的融合方案。

猜你喜欢

转载自blog.csdn.net/qq_42859625/article/details/131955775