赞
踩
在预训练阶段,通过不同的预训练任务在无标签数据上训练模型。
再微调训练阶段,BERT模型首先使用预训练的参数进行初始化,所有的参数都使用来自有标签的下流任务。
BERT则采用了Masked Language Model(MLM)和Next Sentence Prediction(NSP)两种预训练任务,使得模型能够同时利用左侧和右侧的上下文信息进行预测;
bert 在输入部分会较transform有些不同,它包括token_embeddings+segment embeddings+position embeddings三部分组成,包含了单词本身的信息、句子的信息和位置信息
#token_embeddings[n,n_vocab,model_dim] self.word_emb = nn.Embedding(n_vocab, model_dim) self.word_emb.weight.data.normal_(0, 0.1) #segment embeddings[n,max_seg(step),model_dim] self.segment_emb = nn.Embedding(num_embeddings=max_seg, embedding_dim=model_dim) self.segment_emb.weight.data.normal_(0, 0.1) #position embeddings[n,max_len,model_dim] self.position_emb = torch.empty(1, max_len, model_dim) # 使用 Kaiming 正态分布初始化方法对位置嵌入向量进行初始化 # 使用 fan_out 模式进行初始化,权重矩阵的每个输出通道将具有相同的方差,根据激活函数的非线性特性进行缩放 nn.init.kaiming_normal_(self.position_emb, mode='fan_out', nonlinearity='relu') self.position_emb = nn.Parameter(self.position_emb) def input_emb(self, seqs, segs):#输入 = 词向量+分割向量+位置向量 # device = next(self.parameters()).device # self.position_emb = self.position_emb.to(device) return self.word_emb(seqs) + self.segment_emb(segs) + self.position_emb
Bert有两个预训练任务:Masked LM 和 Next Sentence Prediction
在进行MLM任务时,BERT会对输入序列进行一次完整的正向传递和反向传递。在正向传递中,通过Transformer网络结构处理输入序列,获取每个位置的隐藏表示。在需要进行Mask预测的位置上,将单词替换为[MASK]标记。然后,在反向传递过程中,BERT模型根据上下文信息预测被遮盖的单词。
一般步骤 :
class MRPCData(tDataset): num_seg = 3 # 表示数据的分段数量 pad_id = PAD_ID def __init__(self, data_dir="./MRPC/", rows=None, proxy=None): maybe_download_mrpc(save_dir=data_dir, proxy=proxy) #下载 MRPC 数据集;proxy(默认为 None):可选的代理服务器地址 data, self.v2i, self.i2v = _process_mrpc(data_dir, rows) #处理 MRPC 数据集,返回处理后的数据、词汇表和反向词汇表 # 返回所有样本的最大长度 # zip 返回形式 data["train"]["s1id"]+data["test"]["s1id"] :data["train"]["s2id"] + data["test"]["s2id"] self.max_len = max([len(s1) + len(s2) + 3 for s1, s2 in zip( data["train"]["s1id"] + data["test"]["s1id"], data["train"]["s2id"] + data["test"]["s2id"])]) # 以列表的形式存储第一句子和第二句子的长度 self.xlen = np.array([ [ len(data["train"]["s1id"][i]), len(data["train"]["s2id"][i]) ] for i in range(len(data["train"]["s1id"]))], dtype=int) #x是【<GO>句子1<SEP>句子2<SEP>】的一个向量形式表示 x = [ [self.v2i["<GO>"]] + data["train"]["s1id"][i] + [self.v2i["<SEP>"]] + data["train"]["s2id"][i] + [ self.v2i["<SEP>"]] for i in range(len(self.xlen)) ] self.x = pad_zero(x, max_len=self.max_len) #将x进行填充 max-len = 70 # 使data["train"]["is_same"]维度变为二维数组; # [:, None] 是对数组进行切片和重塑的操作; : 表示选择所有行,而 None 表示增加一个维度 self.nsp_y = data["train"]["is_same"][:, None] # # 返回特征数据 self.x 的形状,self.num_seg - 1 表示了要使用的初始值 self.seg = np.full(self.x.shape, self.num_seg - 1, np.int32) # [160,70] # seg用来区别第一个句子和第二个句子 for i in range(len(x)): si = self.xlen[i][0] + 2 # +2是考虑到标记 self.seg[i, :si] = 0 # 将第一个句子的分割信息置为0 【用来区别第一个句子和第二个句子】 si_ = si + self.xlen[i][1] + 1 #第二个句子的长度,并加上1 self.seg[i, si:si_] = 1 # 将第二个句子标记为1 # 得到了一个不包含特殊词汇的词汇集合 self.word_ids = np.array(list(set(self.i2v.keys()).difference( [self.v2i[v] for v in ["<PAD>", "<MASK>", "<SEP>"]]))) # x[idx]【x是【<GO>句子1<SEP>句子2<SEP>】的一个向量形式表示】, seg[idx]【区别第一/二个句子】, # xlen[idx]【第一个句子和第二个句子长度】, nsp_y[idx] bool类型表示两个句子是否相等 def __getitem__(self, idx): return self.x[idx], self.seg[idx], self.xlen[idx], self.nsp_y[idx]
有 80% 的几率被替换成 [MASK]
有 10% 的几率被替换成任意一个其它的 token
有 10% 的几率原封不动
【为什么要这样呢???】:为了缓解微调和预训练的不匹配问题
# 区别是否进行掩码操作 def _get_loss_mask(len_arange, seq, pad_id): # 随机选择一些索引,作为掩码 rand_id = np.random.choice(len_arange, size=max(2, int(MASK_RATE * len(len_arange))), replace=False) loss_mask = np.full_like(seq, pad_id, dtype=bool) #生成与seq类型相同、并且所有元素都是 pad_id loss_mask[rand_id] = True return loss_mask[None, :], rand_id # loss_mask用于区别是否进行掩码操作 def do_mask(seq, len_arange, pad_id, mask_id): # len_arange 是一个索引数组的长度,seq 是一个序列,pad_id 是填充标记 loss_mask, rand_id = _get_loss_mask(len_arange, seq, pad_id) seq[rand_id] = mask_id #随机选择的索引位置的值替换为掩码标记 return loss_mask def do_replace(seq, len_arange, pad_id, word_ids): loss_mask, rand_id = _get_loss_mask(len_arange, seq, pad_id) seq[rand_id] = torch.from_numpy(np.random.choice(word_ids, size=len(rand_id))).type(torch.IntTensor) return loss_mask def do_nothing(seq, len_arange, pad_id): loss_mask, _ = _get_loss_mask(len_arange, seq, pad_id) return loss_mask #完成了根据随机概率生成不同类型的掩码或替换处理,并返回相应的结果。 def random_mask_or_replace(data, arange, dataset): # x[idx]【x是【<GO>句子1<SEP>句子2<SEP>】的一个向量形式表示】, seg[idx]【区别第一/二个句子】, # xlen[idx]【第一个句子和第二个句子长度】, nsp_y[idx] bool类型表示两个句子是否相等 seqs, segs, xlen, nsp_labels = data # [32,72],[32,72],[32,2],[32,1] seqs_ = seqs.data.clone() p = np.random.random() if p < 0.7: # 将需要被掩盖的位置对应的预测值排除在损失计算之外,以避免无效计算和损失干扰 loss_mask = np.concatenate([ do_mask( seqs[i], np.concatenate((arange[:xlen[i, 0]], arange[xlen[i, 0] + 1:xlen[i].sum() + 1])), #将数组 arange 中除了索引 xlen[i, 0] 对应的元素外的其他元素拼接在一起,生成一个新的数组 dataset.pad_id, dataset.mask_id ) for i in range(len(seqs))], axis=0) elif p < 0.85: # do nothing 直接拼接 loss_mask = np.concatenate([ do_nothing( seqs[i], np.concatenate((arange[:xlen[i, 0]], arange[xlen[i, 0] + 1:xlen[i].sum() + 1])), dataset.pad_id ) for i in range(len(seqs))], axis=0) else: # replace 对每个句子进行替换操作并拼接 loss_mask = np.concatenate([ do_replace( seqs[i], np.concatenate((arange[:xlen[i, 0]], arange[xlen[i, 0] + 1:xlen[i].sum() + 1])), dataset.pad_id, dataset.word_ids ) for i in range(len(seqs))], axis=0) loss_mask = torch.from_numpy(loss_mask).unsqueeze(2) return seqs, segs, seqs_, loss_mask, xlen, nsp_labels
让模型预测和还原被遮盖掉或替换掉的部分,计算损失的时候,只计算在第 1 步里被随机遮盖或替换的部分,其余部分不做损失,其余部分无论输出什么,都无所谓
def train(): MODEL_DIM = 256 N_LAYER = 4 LEARNING_RATE = 1e-4 dataset = utils.MRPCData("./MRPC", 1600) #这里只取了1600个数据 #统计数据集中所有样本的词汇 print("num word: ", dataset.num_word) model = BERT( model_dim=MODEL_DIM, max_len=dataset.max_len, num_layer=N_LAYER, num_head=4, n_vocab=dataset.num_word, lr=LEARNING_RATE, max_seg=dataset.num_seg, drop_rate=0.2, padding_idx=dataset.pad_id ) loader = DataLoader(dataset, batch_size=32, shuffle=True) arange = np.arange(0, dataset.max_len) #[0----71].shape=[72,1] for epoch in range(5): for batch_idx, batch in enumerate(loader): # batch_idx = num/32 seqs, segs, seqs_, loss_mask, xlen, nsp_labels = random_mask_or_replace(batch, arange, dataset) seqs, segs, seqs_, nsp_labels, loss_mask = seqs.type(torch.LongTensor).to(device), segs.type( torch.LongTensor).to(device), seqs_.type(torch.LongTensor).to(device), nsp_labels.to( device), loss_mask.to(device) loss, pred = model.step(seqs, segs, seqs_, loss_mask, nsp_labels) # pred = [32,72,2829] if batch_idx % 100 == 0: pred = pred[0].cpu().data.numpy().argmax(axis=1) # 是第几批次,一共有32批 print( "\n\nEpoch: ", epoch, "|batch: ", batch_idx, # 是这批次的第几个了 "| loss: %.3f" % loss, #seqs[0] 表示取出批次中第一个样本;取出目标第一个句子输出 "\n| tgt: ", " ".join([dataset.i2v[i] for i in seqs[0].cpu().data.numpy()[:xlen[0].sum() + 1]]), # 取出预测中第一个句子输出 "\n| prd: ", " ".join([dataset.i2v[i] for i in pred[:xlen[0].sum() + 1]]), # seqs_ 转换为一个字符串,并过滤掉掩码 "\n| tgt word: ", [dataset.i2v[i] for i in (seqs_[0] * loss_mask[0].view(-1)).cpu().data.numpy() if i != dataset.v2i["<PAD>"]], # 将预测词汇转换为一个字符串,并过滤掉掩码 "\n| prd word: ", [dataset.i2v[i] for i in pred * (loss_mask[0].view(-1).cpu().data.numpy()) if i != dataset.v2i["<PAD>"]], )
NSP任务的基本思想是:对于给定的一对句子A和B,BERT模型需要判断这两个句子是否是连续的。构造训练样本对,其中包含正确连续句子对(positive example)和随机连续句子对(negative example)。语料中50%的句子,选择其相应的下一句一起形成上下句,作为正样本;其余50%的句子随机选择一句非下一句一起形成上下句,作为负样本。这种设定,有利于sentence-level tasks。
具体过程如下:
在BERT中,通常使用两个主要的损失函数:掩码语言建模(Masked Language Modeling, MLM)损失和下一句预测(Next Sentence Prediction, NSP)损失。
(1)掩码语言建模(MLM)损失:
在BERT的预训练阶段,输入序列中的某些词或子词会被随机掩码(通常被替换为特殊的 [MASK] 标记)。MLM损失用于训练模型预测这些被掩码的词是什么。
(2)下一句预测(NSP)损失:
在BERT的预训练阶段,为了训练模型判断两个句子之间的关联性,需要使用下一句预测损失。
计算NSP损失的步骤如下:
使用二分类交叉熵损失函数来计算模型预测值与真实值之间的差异。目标是最小化交叉熵损失,使模型能够准确地判断句子关联性。
最终,BERT的总损失由MLM损失和NSP损失的加权和构成。权重可以根据具体任务的需求进行调整。
def step(self, seqs, segs, seqs_, loss_mask, nsp_labels):
device = next(self.parameters()).device
self.opt.zero_grad()
mlm_logits, nsp_logits = self(seqs, segs, training=True) # [n, step, n_vocab]=[32,72,11885], [n, n_cls] = [32,2]
mlm_loss = cross_entropy(
#根据loss_mask选择需要计算损失的位置的logits
torch.masked_select(mlm_logits, loss_mask).reshape(-1, mlm_logits.shape[2]),
torch.masked_select(seqs_, loss_mask.squeeze(2))
)
nsp_loss = cross_entropy(nsp_logits, nsp_labels.reshape(-1))
loss = mlm_loss + 0.2 * nsp_loss
loss.backward()
self.opt.step()
return loss.cpu().data.numpy(), mlm_logits
在具体任务中,微调所做的是不同的:
(1)分类任务:
分类任务的开头是cls,然后将该位置的 output,丢给 Linear Classifier,让其 predict 一个 class 即可
(2)词性标注
将句子中各个字对应位置的 output 分别送入不同的 Linear,预测出该字的标签。
(3)语言推理
给定一个前提,然后给出一个假设,模型要判断出这个假设是 正确、错误还是不知道。对 [CLS] 的 output 进行预测即可
(4)问答
首先将问题和文章通过 [SEP] 分隔,送入 BERT ,得到黄色部分的输出。训练两个 vector,即橙色和黄色的向量。进行 dot product,然后通过 softmax,看哪一个输出的值最大,得到最后输出
model_dim=256,
max_len=dataset.max_len = 70,
num_layer=4,
num_head=4,
n_vocab=dataset.num_word ,
lr=1e-4,
max_seg=dataset.num_seg = 3,
drop_rate=0.2,
padding_idx=dataset.pad_id = PAD_ID
bert是以句子为单位进行处理,transform以词为单位进行处理
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。