五、AIGC大模型_03BERT论文与实战

1、BERT论文

论文网址:https://arxiv.org/pdf/1810.04805

1.1 论文正文内容翻译与总结

1.1.1 引言

BERT(Bidirectional Encoder Representations from Transformers)是一种新的语言表示模型,旨在通过无监督的预训练方法学习深度双向语言表示,与以往的语言模型(如ELMo和OpenAI GPT)不同,BERT能够同时利用上下文的左右信息进行预训练

预训练完成后,BERT可以通过添加一个额外的输出层进行微调(fine-tuning),从而在多种自然语言处理任务(如问答和语言推理)上达到最先进的性能,而无需对任务特定架构进行大量修改

BERT的主要贡献包括:

  • 提出了一种深度双向Transformer架构,通过“掩码语言模型”(Masked Language Model, MLM)和“下一句预测”(Next Sentence Prediction, NSP)任务进行预训练

  • 展示了预训练语言表示在多种任务上的有效性,尤其是在减少对任务特定架构依赖方面

  • 在11个自然语言处理任务上取得了新的最佳结果,包括GLUE基准测试、SQuAD问答任务等

1.1.2 相关工作

BERT的提出基于以往的语言模型预训练方法,包括基于特征的方法(如ELMo)和基于微调的方法(如OpenAI GPT),这些方法通常使用单向语言模型进行预训练,而BERT通过掩码语言模型实现了深度双向预训练,从而在性能上取得了显著提升

1.1.3 BERT模型

BERT的架构基于多层双向Transformer编码器,预训练包括两个任务:

  • 掩码语言模型(MLM):随机掩盖输入文本中的一些词,并预测这些被掩盖的词。这种方法允许模型在预训练阶段同时利用上下文的左右信息

  • 下一句预测(NSP):通过预测两个句子是否是连续的文本,训练模型理解句子之间的关系

BERT的输入表示能够处理单句或多句(如问答对),并通过特殊的标记(如[CLS]和[SEP])区分不同的句子

1.1.4 实验

BERT在多个自然语言处理任务上进行了实验,包括GLUE基准测试、SQuAD问答任务、SWAG常识推理任务等,实验结果表明:BERT在这些任务上均取得了显著的性能提升,尤其是在需要理解句子关系的任务上

  • GLUE基准测试:BERT在多个子任务上取得了最佳性能,平均准确率比之前的最佳方法提升了4.5%-7.0%

  • SQuAD问答任务:BERT在SQuAD v1.1和v2.0版本上均取得了接近人类水平的性能

  • SWAG任务:BERT在常识推理任务上也表现出色,显著优于之前的最佳系统

1.1.5 消融研究

BERT的消融研究验证了其双向预训练和两个预训练任务的重要性:

  • 双向预训练的重要性:与单向预训练模型(如OpenAI GPT)相比,BERT的双向预训练在多个任务上取得了更好的性能

  • 模型大小的影响:更大的模型(如BERT-Large)在预训练后能够更好地适应小规模任务,即使这些任务的数据量很少

  • 基于特征的方法:BERT也可以作为特征提取器,为其他任务提供上下文嵌入,而无需微调。这种方法在某些任务上也取得了接近最佳的性能

1.1.6 结论

BERT通过深度双向预训练,在多种自然语言处理任务上取得了显著的性能提升,其成功证明了双向预训练在语言理解任务中的重要性,并为未来的研究提供了新的方向

1.2 论文附录内容翻译与总结

附件内容是对BERT(Bidirectional Encoder Representations from Transformers)模型的详细介绍,包括其预训练任务、预训练过程、微调过程以及与其他模型的比较。以下是翻译和总结:

A.1 预训练任务说明

  • Masked LM(掩码语言模型)和掩码过程

    • 假设句子为“my dog is hairy”,随机选择第4个词(“hairy”)进行掩码

    • 80%的情况下,用[MASK]替换该词,如“my dog is [MASK]”

    • 10%的情况下,用随机词替换,如“my dog is apple”

    • 10%的情况下,保持原词不变,如“my dog is hairy”

    • 目的是让模型在不知道哪些词会被预测或替换的情况下,保持每个输入词的上下文分布表示

    • 随机替换仅占1.5%的词(15%的10%),不会损害模型的语言理解能力

  • Next Sentence Prediction(下一句预测)

    • 输入示例:

      • [CLS] the man went to [MASK] store [SEP]

      • he bought a gallon [MASK] milk [SEP]

      • 标签:IsNext(下一句)

      • [CLS] the man [MASK] to the store [SEP]

      • penguin [MASK] are flight##less birds [SEP]

      • 标签:NotNext(非下一句)

A.2 预训练过程

  • 从语料库中采样两段文本(称为“句子”),第一句使用A嵌入,第二句使用B嵌入

  • 50%的情况下,B是A的实际下一句;50%的情况下,B是随机句子,用于下一句预测任务

  • 采样长度不超过512个词

  • 使用WordPiece分词,掩码率为15%,不考虑部分词元

  • 使用256个序列的批量大小(256个序列×512个词=每批128,000个词),训练1,000,000步,约40个epoch

  • 使用Adam优化器,学习率1e-4,β1=0.9,β2=0.999,L2权重衰减0.01,前10,000步学习率预热,线性衰减学习率

  • 使用0.1的dropout概率

  • 使用gelu激活函数

  • 训练损失是掩码LM似然和下一句预测似然的总和

  • BERTBASE在4个Cloud TPUs(总共16个TPU芯片)上训练,BERTLARGE在16个Cloud TPUs(总共64个TPU芯片)上训练,每个预训练过程需要4天

A.3 微调过程

  • 微调时,大多数模型超参数与预训练相同,但批量大小、学习率和训练周期数不同

  • Dropout概率始终为0.1

  • 大型数据集(如100k+标记训练样本)对超参数选择不敏感,而小型数据集则更敏感

  • 微调通常很快,建议对上述参数进行穷举搜索,选择在开发集上表现最佳的模型

A.4 BERT、ELMo和OpenAI GPT的比较

  • 模型架构

    • BERT:双向Transformer

    • OpenAI GPT:左到右的Transformer

    • ELMo:独立训练的左到右和右到左的LSTM的连接

    • BERT和OpenAI GPT是微调方法,而ELMo是基于特征的方法

  • 训练数据

    • GPT:仅使用BooksCorpus(8亿词)

    • BERT:使用BooksCorpus(8亿词)和Wikipedia(25亿词)

  • 特殊标记

    • GPT:在微调时引入[SEP]和[CLS]标记

    • BERT:在预训练期间学习[SEP]、[CLS]和句子A/B嵌入

  • 训练步骤和批量大小

    • GPT:1M步,批量大小为32,000词

    • BERT:1M步,批量大小为128,000词

  • 学习率

    • GPT:微调时使用相同的5e-5学习率

    • BERT:选择在开发集上表现最佳的任务特定微调学习率

A.5 不同任务的微调示例

  • BERT在不同任务上的微调模型通过添加一个额外的输出层来形成,因此需要从头学习的参数数量最少

  • 任务包括序列级任务(如MNLI、QQP)和标记级任务(如SQuAD、NER)

B.1 GLUE基准测试的详细描述

  • GLUE基准测试包括以下数据集:

    • MNLI:多体裁自然语言推理,预测两个句子之间的蕴含关系

    • QQP:Quora问题对,判断两个问题是否语义等价

    • QNLI:斯坦福问答数据集的二分类版本,判断问题和句子是否包含答案

    • SST-2:斯坦福情感树库,判断电影评论的情感倾向

    • CoLA:语言可接受性语料库,判断句子是否“可接受”

    • STS-B:语义文本相似性基准,判断两个句子的语义相似度

    • MRPC:微软研究释义语料库,判断句子对是否语义等价

    • RTE:文本蕴含识别,与MNLI类似,但训练数据较少

    • WNLI:Winograd自然语言推理,由于数据集构建问题,通常不包括在GLUE中

C.1 预训练步数的影响

  • 图5展示了从预训练了k步的模型参数开始微调后的MNLI准确率

  • 结论:

    • BERT确实需要大量的预训练(128,000词/批×1,000,000步)才能达到高微调准确率。BERTBASE在训练1M步时比500k步多出1.0%的准确率

    • MLM模型的收敛速度略慢于LTR模型,但MLM模型在绝对准确率上几乎立即开始优于LTR模型

C.2 不同掩码策略的消融研究

  • BERT在预训练时使用了混合策略来掩码目标词

  • 结果显示:

    • 微调对不同的掩码策略相当鲁棒

    • 仅使用[MASK]策略在NER的基于特征的方法中存在问题,因为模型没有机会调整表示

    • 仅使用随机替换(RND)策略的性能远低于BERT的策略

2、BERT掩码过程(完形填空)示例

# 导入pipeline(pipeline是transformers提供的一个高级接口,用于快速加载和使用预训练模型来完成特定任务,如文本生成、情感分析、命名实体识别等)
from transformers import pipeline

# 加载掩码填充任务的预训练模型
unmasker = pipeline(task='fill-mask', model='bert-base-chinese')

# 输入文本并预测
unmasker(inputs="大[MASK]很可爱,我打算养一只!")

jupyter中的运行结果:

3、BERT实战案例

3.1 案例概述

现有一个名为《samples.tsv》的语料文件,其中包含5999行数据,每行数据包含两列内容:

(1)用户的评论语句

(2)该语句的情感性质(正面/负面)

内容大致如下图所示

现要求将上述语料送入BERT模型中进行训练,在训练之后可对用户输入的新语句进行情感性质判断

3.2 代码实现

3.2.1 模型准备

先到魔搭社区https://www.modelscope.cn下载bert-base-chinese模型,使其与代码文件、语料文件均处于同一级目录(如下图所示)

3.2.2 代码实现

(1)读取数据

# pandas 适合表格类数据读取
import pandas as pd
import numpy as np

# 用tab标签作为分隔符
data = pd.read_csv(filepath_or_buffer="samples.tsv", sep="\t").to_numpy()

# 打乱样本顺序
np.random.shuffle(data)

(2)打包数据

# 深度学习框架
import torch
# 深度学习中的封装层
from torch import nn
# 引入数据集
from torch.utils.data import Dataset
# 引入数据集加载器
from torch.utils.data import DataLoader

class SentiDataset(Dataset):
    """
        自定义数据集
    """
    def __init__(self, data):
        """
            初始化
        """
        self.data = data

    def __getitem__(self, idx):
        """
            按索引获取单个样本
        """
        x, y = self.data[idx]
        return x, y

    def __len__(self):
        """
            返回数据集中的样本个数
        """
        return len(self.data)

# 训练集(前4500个作为训练集)
train_dataset = SentiDataset(data=data[:4500])
train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=32)
# 测试集(4500之后的作为测试集)
test_dataset = SentiDataset(data=data[4500:])
test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=32)

# 数据集中数据简单预览
for x, y in train_dataloader:    
    print(x)
    print(y)
    break

(3)构建模型

# 用于加载 BERT 分词器
from transformers import BertTokenizer
# 用于加载 BERT 序列分类器
from transformers import BertForSequenceClassification

# 模型地址
model_dir = "bert-base-chinese"

# 加载分词器
tokenizer = BertTokenizer.from_pretrained(model_dir)
# 查看分词器
print(tokenizer)

# 设备判断
device = "cuda:0" if torch.cuda.is_available() else "cpu"

# 二分类的分类模型
model = BertForSequenceClassification.from_pretrained(model_dir, num_labels=2)
# 数据搬家
model.to(device = device)

# 计算一个可训练参数(即需要梯度更新的参数)的总数
num_params = 0
for param in model.parameters():
    if param.requires_grad:
        num_params += param.numel()
num_params

# 类别字典
label2idx = {"正面": 0, "负面": 1}
idx2label = {0: "正面", 1: "负面"}

(4)训练

# 损失函数
loss_fn = nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.AdamW(params=model.parameters(), lr=1e-4)
# 定义训练轮次
epochs = 5

def get_acc(dataloader):
    """
        计算准确率
    """
    # 设置为评估模式
    model.eval()
    accs = []
    # 构建一个无梯度的环境
    with torch.no_grad():
        # 逐个批次计算
        for X, y in train_dataloader:
            # 编码
            X = tokenizer.batch_encode_plus(batch_text_or_text_pairs=X, 
                                            padding=True, 
                                            truncation=True,
                                            max_length=100,
                                            return_tensors="pt")
            # 转张量
            y = torch.tensor(data=[label2idx.get(label) for label in y], dtype=torch.long).cuda()
            # 1. 正向传播
            y_pred = model(input_ids=X["input_ids"].to(device=device), 
                           attention_mask=X["attention_mask"].to(device=device))
            # 2. 计算准确率
            acc = (y_pred.logits.argmax(dim=-1) == y).to(dtype=torch.float).mean().item()
            accs.append(acc)
    return sum(accs) / len(accs)

def train():
    """
        训练过程
    """
    # 训练之前:先看看准确率
    train_acc = get_acc(train_dataloader)
    test_acc = get_acc(test_dataloader)
    print(f"初始:Train_Acc: {train_acc}, Test_Acc: {test_acc}")
    # 遍历每一轮
    for epoch in range(epochs):
        model.train()
        # 遍历每个批次
        for X, y in train_dataloader:
            # 编码
            X = tokenizer.batch_encode_plus(batch_text_or_text_pairs=X, 
                                            padding=True, 
                                            truncation=True,
                                            max_length=100,
                                            return_tensors="pt")
            # 转张量
            y = torch.tensor(data=[label2idx.get(label) for label in y], dtype=torch.long).cuda()

            # 1. 正向传播
            y_pred = model(input_ids=X["input_ids"].to(device=device), 
                           attention_mask=X["attention_mask"].to(device=device))

            # 2. 计算损失
            loss = loss_fn(y_pred.logits, y)

            # 3. 反向传播
            loss.backward()

            # 4. 优化一步
            optimizer.step()

            # 5. 清空梯度
            optimizer.zero_grad()
        # 每轮都计算一下准备率
        train_acc = get_acc(train_dataloader)
        test_acc = get_acc(test_dataloader)
        print(f"Epoch: {epoch +1}, Train_Acc: {train_acc}, Test_Acc: {test_acc}")

# 执行训练函数
train()

(5)保存模型

# 保存训练好的模型
model.save_pretrained(save_directory="./sentiment_model")
# 保存分词器
tokenizer.save_pretrained(save_directory="./sentiment_model")

(6)预测

# 加载分词器
tokenizer = BertTokenizer.from_pretrained("./sentiment_model")
# 加载模型
model = BertForSequenceClassification.from_pretrained("./sentiment_model").cuda()

def predict(text="服务挺好的"):
    """
        预测方法
    """
    # 设置为评估模式
    model.eval()
    with torch.no_grad():
        inputs = tokenizer(text=text,
                           padding=True, 
                           truncation=True,
                           max_length=100,
                           return_tensors="pt")
        y_pred = model(input_ids=inputs["input_ids"].to(device=device), 
                       attention_mask=inputs["attention_mask"].to(device=device))
        y_pred = y_pred.logits.argmax(dim=-1).cpu().numpy()
        result = idx2label.get(y_pred[0])
        return result

# 预测效果
predict(text="有老鼠,厕所脏")

3.3 运行结果

猜你喜欢

转载自blog.csdn.net/weixin_43767064/article/details/145620928
今日推荐