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

本文涉及的产品
NLP 自学习平台,3个模型定制额度 1个月
NLP自然语言处理_高级版,每接口累计50万次
NLP自然语言处理_基础版,每接口每天50万次
简介: 【NLP】Datawhale-AI夏令营Day4打卡:预训练+微调范式


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

相关文章
|
12天前
|
机器学习/深度学习 人工智能 机器人
何恺明新作出炉!异构预训练Transformer颠覆本体视觉学习范式,AI性能暴涨超20%
【10月更文挑战第29天】在机器人学习领域,训练通用模型面临数据异构性的挑战。近期研究“Scaling Proprioceptive-Visual Learning with Heterogeneous Pre-trained Transformers”提出异构预训练Transformer(HPT),通过大规模预训练学习跨不同本体和任务的共享表示,显著提升了性能。实验结果显示,HPT在未见过的任务上表现优异,性能提升超过20%。
33 6
|
29天前
|
人工智能 自然语言处理
从迷茫到精通:揭秘模型微调如何助你轻松驾驭AI新热点,解锁预训练模型的无限潜能!
【10月更文挑战第13天】本文通过简单的问题解答形式,结合示例代码,详细介绍了模型微调的全流程。从选择预训练模型、准备新任务数据集、设置微调参数,到进行微调训练和评估调优,帮助读者全面理解模型微调的技术细节和应用场景。
67 6
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
揭开模型微调Fine-Tuning的神秘面纱:如何在预训练基础上巧妙调整,解锁定制AI解决方案的秘密武器
【10月更文挑战第8天】模型微调是在预训练模型基础上,利用特定领域数据进一步训练,以优化模型在特定任务上的表现。此方法广泛应用于自然语言处理和计算机视觉等领域,通过调整预训练模型的部分或全部参数,结合适当的正则化手段,有效提升模型性能。例如,使用Hugging Face的Transformers库对BERT模型进行微调,以改善文本匹配任务的准确率。
54 1
|
1月前
|
人工智能 自然语言处理
【NLP自然语言处理】NLP中的常用预训练AI模型
【NLP自然语言处理】NLP中的常用预训练AI模型
|
3月前
|
数据采集 人工智能 物联网
Datawhale X 魔搭 AI夏令营task 3笔记
Datawhale X 魔搭 AI夏令营task 3笔记
78 2
|
3月前
|
机器学习/深度学习 人工智能 自然语言处理
【模型微调】AI Native应用中模型微调概述、应用及案例分析
在AI Native应用中,模型微调是一个关键步骤,它允许开发者使用特定领域的数据对预训练模型进行二次训练过程,从而使其更好地适应特定任务或数据集。模型微调通过调整模型的参数,使模型在特定任务上达到更高的性能。这种技术广泛应用于自然语言处理、图像识别、语音识别等领域
78 1
|
3月前
|
人工智能 物联网
Datawhale X 魔搭 AI夏令营task 3笔记
Datawhale X 魔搭 AI夏令营task 3笔记
|
3月前
|
机器学习/深度学习 自然语言处理 数据处理
|
3月前
|
数据采集 人工智能 物联网
我们给大模型去掉了“AI味”-大模型微调全链路实战
本文档旨在概述使用ModelScope生态进行LLM训练的全链路最佳实践,涵盖数据下载、数据预处理、模型训练、模型评估完整流程。
|
3月前
|
机器学习/深度学习 自然语言处理
自然语言处理 Paddle NLP - 预训练语言模型及应用
自然语言处理 Paddle NLP - 预训练语言模型及应用
28 0

热门文章

最新文章