Bidirectional Encoder Representations from Transformers,即Bert;
学习如何使用预训练的BERT模型:
从头开始预训练BERT模型是很费力的,因此可以下载预训练的BERT模型并直接使用(从Github仓库直接下载);
L表示编码器层数,H表示隐藏神经元的数量(特征大小),BERT-base的L=12,H=768;
预训练模型可以使用不区分大小写(BERT-uncased)的格式和区分大小写(BERT-cased)的格式;
不区分大小写的模型是最常用的模型,但对一些特定任务如命名实体识别,则必须保留大小写,就需要使用区分大小写模型;
预训练模型的应用场景:
示例句子 ->
标记句子 ->
送入模型 ->
返回每个标记的词级嵌入 以及 句级特征;
构建分类数据集,作正反向观点的二分类:
训练分类器做情感分类:
标记句子:
[CLS]
标记,在结尾添加[SEP]
,为统一所有句子的标记长度(假设是512),那么不足512的会使用标记[PAD]
来重复填充;[PAD]
只是用于匹配长度,而不是实际标记的一部分,需要引入注意力掩码,将所有位置的注意力掩码值设为1,再将标记[PAD]
的位置设为0;[CLS]
标记对应的ID为101;标记[PAD]
的仍为0;接下来把 token_ids和 attention_mask一起输入预训练的BERT模型,并获得每个标记的特征向量;
最终输出的 R[CLS]
就是标记[CLS]
的嵌入,它可以代表整个句子的总特征;如果使用的是BERT-base模型配置,那么每个标记的特征向量大小为768;
采用类似的方法,就可计算出训练集所有句子的特征向量,一旦有了训练集所有句子的特征,就可以把这些特征作为输入,训练一个分类器;
值的注意的是,使用
[CLS]
标记的特征代表整个句子的特征并不总是一个好主意;要获得一个句子的特征,最好基于所有标记的特征进行平均或者汇聚;
Hugging Face是一个致力于通过自然语言将AI技术大众化的组织,它提供了开源的Transformers库对一些自然语言处理任务和自然语言理解(NLU)任务非常有效;Transformers库包含了百余种语言的数千个预训练模型,而且还可以与Pytroch和TF兼容;
安装命令:pip install Transformers==3.5.1
!pip install Transformers==4.27.4 --ignore-installed PyYAML
# 问题:cannot import name 'is_tokenizers_available' from 'transformers.utils'
# 参考:https://discuss.huggingface.co/t/how-to-resolve-the-hugging-face-error-importerror-cannot-import-name-is-tokenizers-available-from-transformers-utils/23957/2
# 示意代码 仅做指示
from transformers import BertModel, BertTokenizer
import torch
def get_tokens_and_attention_mask(tokens_a):
# 标记更新
tokens = []
# 上下句掩码(当前示例任务 没什么用)
segment_ids = []
tokens.append("[CLS]")
segment_ids.append(0)
for token in tokens_a:
tokens.append(token)
segment_ids.append(0)
if tokens[-1] != "[SEP]":
tokens.append("[SEP]")
segment_ids.append(0)
# 将标记转换为它们的标记ID
token_ids = tokenizer.convert_tokens_to_ids(tokens)
# 输入的注意力掩码
attention_mask = [1] * len(token_ids)
while len(token_ids) < max_seq_length:
token_ids.append(0)
attention_mask.append(0) # 这里加的0 实际对应的就是标记 [PAD],这里补0,实际就不用处理添加[PAD]标记的逻辑
segment_ids.append(0)
assert len(token_ids) == max_seq_length
return token_ids, attention_mask
# 下载并加载预训练模型
model = BertModel.from_pretrained("bert-tiny-uncased")
# 下载并加载用于预训练模型的词元分析器
tokenizer = BertTokenizer.from_pretrained("bert-tiny-uncased")
sentence = "I am Good"
# 分词并获取标记
tokens_a = tokenizer.tokenize(sentence)
token_ids, attention_mask = get_tokens_and_attention_mask(tokens_a)
# 转为张量
token_ids = torch.tensor(token_ids).unsqueeze(0)
attention_mask = torch.tensor(attention_mask).unsqueeze(0)
# 送入模型
hidden_rep, cls_head = model(token_ids, attention_mask=attention_mask)
hidden_rep:
torch.size([1, 10, 128])
;[1, 10, 128]
分别对应 [batch_size, sequence_length, hidden_size]
;隐藏层的大小等于特征向量大小;
[CLS]
的特征:hidden_rep[0][0]
I
的特征:hidden_rep[0][1]
cls_head:
[CLS]
标记的特征,shape为torch.size([1, 128])
;可以用cls_head作为句子的整句特征;
前面介绍的是如何从预训练的BERT模型的顶层编码器提取嵌入,此外,也可以考虑从所有的编码器层获得嵌入;
使用h0表示输入嵌入层,h1则表示第一个编码器层(第一个隐藏层),研究人员使用预训练的BERT-base模型的不同层编码器的嵌入作为特征,应用在命名实体识别任务,所得的F1分数(调和均值)发现,将最后4个编码器的嵌入 连接起来可以得到最高的F1,这说明可以使用其他层所提取的嵌入,而不必只用顶层编码器的嵌入;
# 示意代码 仅做指示
from transformers import BertModel, BertTokenizer
import torch
# output_hidden_states 可以控制输出所有编码器的嵌入
model = BertModel.from_pretrained("bert-base-uncased", output_hidden_states = True)
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
sentence = "I am Good"
tokens_a = tokenizer.tokenize(sentence)
token_ids, attention_mask = get_tokens_and_attention_mask(tokens_a)
# 转为张量
token_ids = torch.tensor(token_ids).unsqueeze(0)
attention_mask = torch.tensor(attention_mask).unsqueeze(0)
# 送入模型
last_hidden_state, pooler_output, hidden_states = model(token_ids, attention_mask=attention_mask)
[1, 10 ,768]
对应[batch_size, sequence_length, hidden_size]
;[CLS]
标记的特征,它被一个线性激活函数和tanh激活函数进一步处理,shape=[1, 768]
,可被用作句子的特征;hidden_states[0]
:输入嵌入层h0获得的所有标记的特征;hidden_states[12]
:最后一个编码器层h12获得的所有标记的特征,shape=[1, 10 ,768]
;通过对返回值的解析,就可以获得所有编码器层的标记嵌入;
到目前为止,我们已经学会了如何使用BERT模型,再看如何针对下游任务进行微调;
在提取句子的嵌入
R[CLS]
后,可以将其送入一个分类器并训练其进行分类;类似的在微调过程中,也可以这样做(对R[CLS]
使用softmax激活函数的前馈网络层)
微调的两种调整权重的方式:
安装必要的库:pip install nlp
# 示意代码 仅做指示(当时实际用的是pytorch_pretrained_bert的库,也是hugging face的)
# from pytorch_pretrained_bert.tokenization import BertTokenizer
# from pytorch_pretrained_bert.modeling import BertForSequenceClassification
# from pytorch_pretrained_bert.optimization import BertAdam
from transformers import BertForSequenceClassification, BertTokenizerFast, Trainer, TrainingArguments
from nlp import load_dataset
from torch
import numpy as np
# 使用nlp库加载并下载数据集
dataset = dataset.train_test_split(test_size=0.3)
# {
# "test": Dataset(text, label),
# "train": Dataset(text, label)
# }
train_set = dataset["train"]
test_set = dataset["test"]
model = BertForSequenceClassification.from_pretrained("bert-base-uncased")
# 下载并加载用于预训练模型的词元分析器,注意这里使用BertTokenizerFast类
tokenizer = BertTokenizerFast.from_pretrained("bert-tiny-uncased")
# 这里词元分析器会帮我们完成get_tokens_and_attention_mask函数的功能
tokenizer("I am Good")
# 返回
# {
# 'input_ids':[101, 1000, 1001, 1002, 102],
# 'token_type_ids':[0, 0, 0, 0, 0],
# 'attention_mask': [1, 1, 1, 1, 1]
# }
# 词元分析器,可以输入任意数量的句子,并动态地进行补长和填充,只需将padding=True, max_length=10
# tokenizer(["I am Good","I am boy"], padding=True, max_length=10)
# 返回(输入两个句子)
# {
# 'input_ids':[[], []],
# 'token_type_ids':[[], []],
# 'attention_mask': [[], []]
# }
# 使用词元分析器预处理数据集
# 可以定义一个名为 preprocess的函数来处理数据集
def preprocess(data):
return tokenizer(data["text"], padding=True, truncation=True) # 截断
train_set.set_format("torch", columns=["input_ids", "attention_mask", "label"])
test_set.set_format("torch", columns=["input_ids", "attention_mask", "label"])
# 训练模型
batch_size = 8 # 批量大小
epochs = 2 # 迭代次数
warmup_steps = 500 # 预热步骤
weight_decay = 0.01 # 权重衰减
# 设置训练参数
training_args = TrainingArguments(
output_dir = "./results",
num_train_epochs = epochs,
per_device_train_batch_size = batch_size,
per_device_eval_batch_size = batch_size,
warmip_steps = warmup_steps,
weight_decay = weight_decay,
evaluate_during_training = True,
logging_dir = "./logs"
)
trainer = Trainer(
model = model,
args = training_args,
train_dataset = train_set,
eval_dataset = test_set
)
# 开始模型训练
trainer.train()
# 训练结束后 评估模型
trainer.evaluate()
# {'epoch': 1.0, 'eval_loss': 0.68}
# {'epoch': 2.0, 'eval_loss': 0.50}
# ...
在该任务中,在确定的“前提”下,推定假设是“真”、“假”还是“未定的”;即模型的目标是:确定一个句子对(前提-假设对)是真、是假、还是中性;
[CLS]
标记,每句结尾添加[SEP]
标记;[CLS]
标记的特征(即整个句子对的特征);R[CLS]
送入分类器;问答任务重,对一个问题,模型会返回一个答案,目标是让模型返回正确答案;
虽然这个任务可以使用生成式的任务模型,但这里是基于微调BERT实现的,思路不同;
BERT模型输入是一个问题和一个段落,这个段落需要是一个含有答案的段落,BERT必须从该段落中提取答案;
要通过微调BERT模型来完成这项任务,模型必须要了解给定段落中包含答案的文本段的起始索引和结束索引;要找到这两个索引,模型应该返回“该段落中每个标记是答案的起始标记和结束标记的概率”;
这里引入两个向量:
两个向量的值,将通过训练获得;
为了计算这个概率:
# 微调BERT模型用于问答任务
from transformers import BertForQuestionAnswering, BertTokenizer
# 下载模型
model = BertForQuestionAnswering.from_pretrained("bert-large-uncased-whole-word-masking-fine-tuned-squad") # 该模型基于斯坦福问答数据集(SQyAD)微调而得
# 下载并加载词元分析器
tokenizer = BertTokenizer.from_pretrained("bert-large-uncased-whole-word-masking-fine-tuned-squad")
question = "A"
paragraph = "AAABBBBBBAAAA"
question = "[CLS]" + question + "[SEP]"
paragraph = paragraph + "[SEP]"
# 转为 input_ids
question_tokens = tokenizer.tokenize(question)
paragraph_tokens = tokenizer.tokenize(paragraph)
# 设置segment_ids
segment_ids = [0] * len(question_tokens)
segment_ids = segment_ids + [1] * len(paragraph_tokens)
# 转张量
input_ids = torch.tensor([input_ids])
segment_ids = torch.tensor([segment_ids])
start_scores, end_scores = model(input_ids, token_type_ids = segment_ids)
start_index = torch.argmax(start_scores)
end_index = torch.argmax(end_scores)
# 答案
' '.join(tokens[start_index: end_index + 1])
关于问答任务BERT微调的一些疑问?
- 问题1:起始向量和结束向量 是怎么来的?
R[CLS]
和R[SEP]
是否就是这两个向量?- 问题2:上面的示例代码段,很明显并不是一个微调,而是一个应用,虽然是应用一个已经微调好的模型,但如何训练却未讲明;
可以展开思考(待查明)
任务目标是将命名实体划分到预设的类别中,如某一句子种出现了人名和地名(还有其他词)的词汇,能将相应词汇进行准确归类;
好吧,这里说的依旧很简略,但至少上我们知道了大致的实现逻辑;
todo