Bootstrap

【NLP】Datawhale-AI夏令营Day4打卡:预训练+微调范式

⭐️ 最近参加了由Datawhale主办、联合科大讯飞、阿里云天池发起的 AI夏令营(第三期),我参与了深度学习实践-NLP(自然语言处理)方向 😄
⭐️ 作为NLP小白,我希望能通过本次夏令营的学习实践,对NLP有初步的了解,学习大模型,动手完成NLP项目内容,同时通过社区交流学习,提升调参优化等能力
⭐️ 今天是打卡的第四天! ✊✊✊
⭐️ 按照日程安排,8月19日-22日主要学习深度学习方法,完成任务二,同时尝试跑通大模型Topline。
⭐️ 今天我主要学习预训练+微调范式、Transformer 和 Attention,并跑通深度学习方法baseline。

🚩【NLP】Datawhale-AI夏令营Day1打卡:文本特征提取
第一天学习了Python 数据分析相关的库(pandas和sklearn),文本特征提取的方法(基于TF-IDF提取和基于BOW提取,以及停用词的用法),划分数据集的方法,以及机器学习的模型,并尝试跑通了机器学习方法baseline。

🚩【NLP】Datawhale-AI夏令营Day2打卡:数据分析
第二天学习了数据探索、数据清洗、特征工程、模型训练与验证部分。

🚩【NLP】Datawhale-AI夏令营Day3打卡:Bert模型
第三天学习了Bert模型(预训练+微调范式),并尝试跑通了深度学习方法baseline。

1. 学习内容

AI夏令营第三期–基于论文摘要的文本分类与关键词抽取挑战赛教程

✅ 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不仅在文本分类任务上表现优异,还在文本生成、文本摘要、机器翻译、问答系统等任务上刷新了多项记录,成为了自然语言处理领域的一个里程碑技术。
🔗 什么是AI大模型:大规模预训练+微调

✅ Transformer

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

在 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 (即计算注意力的另一方)越重要的部分得到的权重就越高。

✅ Attention

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% 被还原为原单词。

2. 实践项目

同之前博客介绍的,仍然是任务一,文本分类任务。

【NLP】Datawhale-AI夏令营Day3打卡:Bert模型

3. 实践代码

深度学习方法(Bert模型):

由于本地环境出了一些问题,今天重新安装了 Anaconda + Pytorch,稍微调了一下batch_size和epochs,再次跑了一下baseline代码。

代码包括以下几个部分:

1️⃣ 导入模块
导入我们本次Baseline代码所需的模块
2️⃣ 设置全局配置
3️⃣ 数据收集与准备
在赛题主页下载数据,读取数据集,数据预处理(考虑数据扩增)
4️⃣ 构建训练所需的dataloader与dataset
定义dataset: Pytorch 中自定义dataset需要继承 torch.utils.data.Dataset,继承Dataset这个类时,它要求你必须重写 getitem(self, index)、len(self) 两个方法,前者通过提供索引返回数据,也就是提供 DataLoader获取数据的方式;后者返回数据集的长度,DataLoader依据 len 确定自身索引采样器的长度。
而本次教程中,我们在初始化类方法 init 中引入我们的训练数据,重写 getitem(self, index)方法,保证能够取出index对应行的text 与label 值,也就是我们训练需要的数据以及训练希望得到的结果,在 len 方法中,我们需要返回数据集的总长度,这里直接利用Dataframe数据的len()方法就能完成。
构造Dataloader: 在Dataloader中,我们需要利用Dataloader来加载训练数据与训练目标,需要注意的是,加载完成后的数据需要为tensor(张量)形式,因此在下文中我们定义了collate_fn来帮助完成batch组装以及将文本内容向量化,而文本内容向量化这部分内容,我们利用bert模型来完成,而label值的向量化我们直接使用torch.LongTensor()方法完成。
5️⃣ 定义模型
pytorch中定义模型需要继承nn.Module类,其中需要至少定义两个方法,一个是初始化模型结构的方法__init__,另一个方法forward来完成推理流程。
6️⃣ 模型训练、评估
7️⃣ 结果输出

#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 = 8
# 文本的最大长度
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. 遇到的问题和解决方法

都是小问题

问题1:
显存不足

解决1:
改小 batch_size
关掉正在运行的其他进程

问题2:
ModuleNotFoundError: No module named ‘sklearn’

解决2:
conda install scikit-learn

5. 实践成绩

8.19凌晨 bert模型返回分数:0.89562(batch_size=1, epochs = 10)
8.19 bert模型返回分数:0.99452 (batch_size=8, epochs = 10)

在这里插入图片描述

相比之前的机器学习方法,效果显著提升,但运行时间也变长了

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,这里就不展示了。

在这里插入图片描述

;