Roberta
1. 动态MASK VS. 静态MASK
上文我们提到在MLM任务中模型会随机挑选Token进行MASK,这一步是在数据处理的过程中进行的,这就是静态MASK。为了充分利用数据,文中会对每句话复制10次。同时文中还对静态MASK进行了改进,采用了动态MASK在每次输入数据的时候进行MASK,这样可以避免在每个epoch中每个sequence被MASK的方式相同。
作者把文中的实验方式和Bert-Base的效果进行对比,可以发现文中对数据进行处理之后,效果有了些许提升,尤其是动态MASK。这可能是因为更换了数据被MASK的方式,提高了模型输入的数据的随机性,使得模型可以学习到更多地pattern。
2. NSP任务
Bert中任务除了MLM还有NSP任务,NSP任务判断两句话是不是连续的一句话,正例是从文章中挑选的连续的两句话,负例是从不同的文章中挑选的两句话。在训练数据中50%的句子是连续的话,剩下的50%的句子是负例。为了判断NSP任务是否对模型的效率有所提升,RoBERTa做了以下实验:
- SEGMENT-PAIR+NSP:传统的Bert的输入和NSP任务,每一对Pair的长度都小于512。
- SENTENCE-PAIR+NSP:输入和Bert相同,但是每一对Pair的长度远小于512,因此采用的batch远大于512。
- FULL-SENTENCES:不会截断句子,句子的边界可能会跨过Document,不采用NSP Loss
- DOC-SENTENCES:数据和Full-Sentence类似,句子不会超过Document,不采用NSP Loss,同时采用动态调整batch size。
作者将以上改进和Bert-base进行了对比,发现不采用NSP LOSS可以轻微提升模型的效率,同时我们发现Doc-sentence的效果优于Full-Sentence效果,因为Doc-sentence任务中的batch size是动态调整的。
3. 更大的batch
在机器翻译任务中,有人发现采用更大的batch可以提高模型的训练速度和下游任务的效率,Bert-base中采用256的batch size训练1M步,这和训练125K步,每步2Kbatch以及训练31K步每步8Kbatch相同。作者对比了不同的batch size的下游任务的困惑度,发现大的batch size模型可以获得更好的效果。
4. Text Encoding
Bert-base中采用WordPiece Encoding,wordpiece encoding就是把单词拆成一片一片的,这中编码在处理数字的时候非常明显,对于长度比较长的数字例如912378,通过WordPiece Encoding就会变成[912, #378]这样做的好处是,可以通过常见的word piece拼接成输入的字符,这样可以减小词典的大小。
WordPiece Encoding的一种主要实现方式就是BPE(Byte-Pair Encoding)双字节编码,有关BPE的算法大家可以上网搜索。
BPE算法的词表大小通常在10K-100K之间,词表大多数元素都是unicode编码,论文《Language models are unsupervised multitask learners》采用byte编码替代BPE中的unicode编码,可以将词表的大小缩小到50K。
在Bert-base使用的词表大小是30K,本文作者使用了《Language models are unsupervised multitask learners》中的编码方法,采用更大的数据集训练了50K大小的词表,不过实验表明两种编码方法对模型的效果并不大。
总体效果
上文中作者分别从batch size,mask,nsp任务和text encoding方式进行了实验,这部分作者将上文对Bert的实验效果整合在一起——采用动态mask,Full-Sentence没有NSP Loss,large mini-batches和上文中50K大小的词表的byte编码BPE算法。鉴于XLNet采用了10倍Bert的预训练数据集,文中作者也采用了Bert-large的结构,在BOOKCORPUS + WIKIPEDIA上训练100K步,那么效果如何呢?
下图是作者的实验结构,可以看到Bert采用160GB的数据,训练500K step之后的在SquAD上的效果已经小幅超过XLNet。
问题1:bert的具体网络结构,以及训练过程,bert为什么火,它在什么的基础上改进了些什么?
bert是用了transformer的encoder侧的网络,作为一个文本编码器,使用大规模数据进行预训练,预训练使用两个loss,一个是mask LM,遮蔽掉源端的一些字(可能会被问到mask的具体做法,15%概率mask词,这其中80%用[mask]替换,10%随机替换一个其他字,10%不替换,至于为什么这么做,那就得问问BERT的作者了{捂脸}),然后根据上下文去预测这些字,一个是next sentence,判断两个句子是否在文章中互为上下句,然后使用了大规模的语料去预训练。在它之前是GPT,GPT是一个单向语言模型的预训练过程(它和gpt的区别就是bert为啥叫双向 bi-directional),更适用于文本生成,通过前文去预测当前的字。下图为transformer的结构,bert的网络结构则用了左边的encoder。
问题2,讲讲multi-head attention的具体结构
BERT由12层transformer layer(encoder端)构成,首先word emb , pos emb(可能会被问到有哪几种position embedding的方式,bert是使用的哪种), sent emb做加和作为网络输入,每层由一个multi-head attention, 一个feed forward 以及两层layerNorm构成,一般会被问到multi-head attention的结构,具体可以描述为,
step1
一个768的hidden向量,被映射成query, key, value。 然后三个向量分别切分成12个小的64维的向量,每一组小向量之间做attention。不妨假设batchsize为32,seqlen为512,隐层维度为768,12个head
hidden(32 x 512 x 768) -> query(32 x 512 x 768) -> 32 x 12 x 512 x 64
hidden(32 x 512 x 768) -> key(32 x 512 x 768) -> 32 x 12 x 512 x 64
hidden(32 x 512 x 768) -> val(32 x 512 x 768) -> 32 x 12 x 512 x 64
**_step2*
然后query和key之间做attention,得到一个32 x 12 x 512 x 512的权重矩阵,然后根据这个权重矩阵加权value中切分好的向量,得到一个32 x 12 x 512 x 64 的向量,拉平输出为768向量。
32 x 12 x 512 x 64(query_hidden) 32 x 12 x 64 x 512(key_hidden) -> 32 x 12 x 512 x 512
32 x 12 x 64 x 512(value_hidden) * 32 x 12 x 512 x 512 (权重矩阵) -> 32 x 12 x 512 x 64
然后再还原成 -> 32 x 512 x 768
简言之是12个头,每个头都是一个64维度分别去与其他的所有位置的hidden embedding做attention然后再合并还原。
问题2.5: Bert 采用哪种Normalization结构,LayerNorm和BatchNorm区别,LayerNorm结构有参数吗,参数的作用?
采用LayerNorm结构,和BatchNorm的区别主要是做规范化的维度不同,BatchNorm针对一个batch里面的数据进行规范化,针对单个神经元进行,比如batch里面有64个样本,那么规范化输入的这64个样本各自经过这个神经元后的值(64维),LayerNorm则是针对单个样本,不依赖于其他数据,常被用于小mini-batch场景、动态网络场景和 RNN,特别是自然语言处理领域,就bert来说就是对每层输出的隐层向量(768维)做规范化,图像领域用BN比较多的原因是因为每一个卷积核的参数在不同位置的神经元当中是共享的,因此也应该被一起规范化。
class BertLayerNorm(nn.Module): def init(self, hiddensize, eps=1e-5): super(BertLayerNorm, self)._init() self.weight = nn.Parameter(torch.ones(hidden_size)) self.bias = nn.Parameter(torch.zeros(hidden_size)) self.variance_epsilon = eps def forward(self, x): u = x.mean(-1, keepdim=True) s = (x - u).pow(2).mean(-1, keepdim=True) x = (x - u) / torch.sqrt(s + self.variance_epsilon) return self.weight * x + self.bias
帖一个LayerNorm的实现,可以看到module中有weight和bias参数
归一化不是就是把上层的输出约束为一个正态分布,为什么还有个w和b的参数?
参考百面机器学习220-221页,为了搞清楚为什么归一化层有一个w,b这两个参数归一化是为了让本层网络的输出进行额外的约束,但如果每个网络的输出被限制在这个约束内,就会限制模型的表达能力。 通俗的讲,不管你上层网络输出的是什么牛鬼蛇神的向量,都先给我归一化到正态分布(均值为0,标准差为1),但我不能给下游传这个向量,如果这样下游网络看到的只能是归一化后的,限制了模型。(先归一化,在适当还原成下游网络合适的分布,通过模型学习)
问题3:transformer attention的时候为啥要除以根号D
至于attention后的权重为啥要除以 ,作者在论文中的解释是点积后的结果大小是跟维度成正比的,所以经过softmax以后,梯度就会变很小,除以
后可以让attention的权重分布方差为1,而不是
。
具体细节(简而言之就是softmax如果某个输入太大的话就会使得权重太接近于1,梯度很小)
transformer中的attention为什么scaled?1733 赞同 · 53 评论回答
问题4:wordpiece的作用
wordpiece其核心思想是将单词打散为字符,然后根据片段的组合频率,最后单词切分成片段处理。和原有的分词相比,能够极大的降低OOV的情况,例如cosplayer, 使用分词的话如果出现频率较低则是UNK,但bpe可以把它切分吃cos play er, 模型可以词根以及前缀等信息,学习到这个词的大致信息,而不是一个OOV。
NLP三大Subword模型详解:BPE、WordPiece、ULM163 赞同 · 3 评论文章
wordpiece与BPE(Byte Pair Encoding)算法类似,也是每次从词表中选出两个子词合并成新的子词。与BPE的最大区别在于,如何选择两个子词进行合并:BPE选择频数最高的相邻子词合并,而WordPiece选择能够提升语言模型概率最大的相邻子词加入词表。
问题5: 如何优化BERT效果:
1 感觉最有效的方式还是数据。
2 把现有的大模型ERNIE_2.0_large, Roberta,roberta_wwm_ext_large、roberta-pair-large等进行ensemble,然后蒸馏原始的bert模型,这是能有效提高的,只是操作代价比较大。
3 BERT上面加一些网络结构,比如attention,rcnn等,个人得到的结果感觉和直接在上面加一层transformer layer的效果差不多,模型更加复杂,效果略好,计算时间略增加。
4 改进预训练,在特定的大规模数据上预训练,相比于开源的用百科,知道等数据训练的更适合你的任务(经过多方验证是一种比较有效的提升方案)。以及在预训练的时候去mask低频词或者实体词(听说过有人这么做有收益,但没具体验证)。
5 文本对抗,作者了解的不多。感兴趣可以看看⬇️
https://arxiv.org/pdf/2004.09984.pdfarxiv.org/pdf/2004.09984.pdf
6 其他的欢迎留言补充
也可以去参考一下一些竞赛中的trick,但有可能在自己任务上并没提升。
https://github.com/huanghuidmml/epidemicTextMatchgithub.com/huanghuidmml/epidemicTextMatch
问题6 如何优化BERT性能
1 压缩层数,然后蒸馏,直接复用12层bert的前4层或者前6层,效果能和12层基本持平,如果不蒸馏会差一些。
2 双塔模型(短文本匹配任务),将bert作为一个encoder,输入query编码成向量,输入title编码成向量,最后加一个DNN网络计算打分即可。离线缓存编码后的向量,在线计算只需要计算DNN网络。
3 int8预估,在保证模型精度的前提下,将Float32的模型转换成Int8的模型。
4 提前结束,大致思想是简单的case前面几层就可以输出分类结果,比较难区分的case走完12层,但这个在batch里面计算应该怎么优化还没看明白,有的提前结束有的最后结束,如果在一个batch里面的话就不太好弄。感兴趣的可以看看⬇️。
https://arxiv.org/pdf/2006.04152.pdfarxiv.org/pdf/2006.04152.pdf
5 ALBERT 做了一些改进优化,主要是不同层之间共享参数,以及用矩阵分解降低embedding的参数,可以看看⬇️。
如何看待瘦身成功版BERT——ALBERT?293 赞同 · 34 评论回答
问题7 self-attention相比lstm优点是什么?
bert通过使用self-attention + position embedding对序列进行编码,lstm的计算过程是从左到右从上到下(如果是多层lstm的话),后一个时间节点的emb需要等前面的算完,而bert这种方式相当于并行计算,虽然模型复杂了很多,速度其实差不多。
问题8 BERT的一些改进。
- ERNIE_1.0(baidu) : 模型结构不变,预训练的时候第一阶段基于切词mask,第二阶段基于实体mask,让模型在预训练的过程中根据上下文去学到一些词级别,实体级别,短语级别的信息。
- ERNIE_2.0:模型结构加入task embedding(作者将预训练任务分为:Word-aware, Structure-aware,Semantic-aware),不同类型的任务选取不同的task embedding,然后加了很多预训练任务,很多数据。
- ALBERT
- Factorized Embedding Parameterization,矩阵分解降低embedding参数量。
- Cross-layer Parameter Sharing,不同层之间共享参数。
- Sentence Order Prediction(SOP),next sentence任务的负样本增强。
个人感觉降低参数量相对于优化效果以及计算速度来说,并不是特别重要。
为什么BERT在第一句前会加一个[CLS]标志?
论文中说:
The first token of every sequence is always a special classification token [CLS]. The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks.
因为这个无明显语义信息的符号会更“公平”地融合文本中各个词的语义信息,从而更好的表示整句话的语义。
BERT的三个Embedding直接相加会对语义有影响吗?
Embedding的数学本质,就是以one-hot为输入的单层全连接。也就是说,世界上本没什么Embedding,有的只是one-hot。(苏剑林)
在这里用一个例子再解释一下:
假设 token Embedding 矩阵维度是 [200000,768];position Embedding 矩阵维度是 [512,768];segment Embedding 矩阵维度是 [2,768]。
对于一个token,假设它的 token one-hot 是[1,0,0,0,…0];它的 position one-hot 是[1,0,0,…0];它的 segment one-hot 是[1,0]。那这个token最后的 word Embedding,就是上面三种 Embedding 的加和。
如此得到的 word Embedding,和concat后的特征:[1,0,0,0…0,1,0,0…0,1,0],再过维度为 [200000+512+2,768] 的全连接层,得到的向量其实就是一样的。
在BERT中,token分3种情况做mask,分别的作用是什么?
15%token做mask;其中80%用[MASK]替换,10%用random token替换,10%不变。其实这个就是典型的Denosing Autoencoder的思路,那些被Mask掉的单词就是在输入侧加入的所谓噪音
这么做的主要原因是:① 在后续finetune任务中语句中并不会出现 [MASK] 标记;②预测一个词汇时,模型并不知道输入对应位置的词汇是否为正确的词汇( 10% 概率),这就迫使模型更多地依赖于上下文信息去预测词汇,并且赋予了模型一定的纠错能力。
原文:
Although this (15% mask) allows us to obtain a bidirectional pre-trained model, a downside is that we are creating a mismatch between pre-training and fine-tuning, since the [MASK] token does not appear during fine-tuning. To mitigate this, we do not always replace “masked” word with the actual [MASK] token.
BERT的输入是什么,哪些是必须的,为什么position id不用给,type_id 和 attention_mask没有给定的时候,默认会是什么
class BertEmbeddings(nn.Module):
“””Construct the embeddings from word, position and token_type embeddings.”””
def __init__(self, config):
super().__init__()
self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)
# self.LayerNorm is not snake-cased to stick with TensorFlow model variable name and be able to load
# any TensorFlow checkpoint file
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
# position_ids (1, len position emb) is contiguous in memory and exported when serialized
self.position_embedding_type = getattr(config, "position_embedding_type", "absolute")
self.register_buffer("position_ids", torch.arange(config.max_position_embeddings).expand((1, -1)))
if version.parse(torch.__version__) > version.parse("1.6.0"):
self.register_buffer(
"token_type_ids",
torch.zeros(self.position_ids.size(), dtype=torch.long),
persistent=False,
)
def forward(
self, input_ids=None, token_type_ids=None, position_ids=None, inputs_embeds=None, past_key_values_length=0
):
if input_ids is not None:
input_shape = input_ids.size()
else:
input_shape = inputs_embeds.size()[:-1]
seq_length = input_shape[1]
if position_ids is None:
position_ids = self.position_ids[:, past_key_values_length : seq_length + past_key_values_length]
if token_type_ids is None:
if hasattr(self, "token_type_ids"):
buffered_token_type_ids = self.token_type_ids[:, :seq_length]
buffered_token_type_ids_expanded = buffered_token_type_ids.expand(input_shape[0], seq_length)
token_type_ids = buffered_token_type_ids_expanded
else:
token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=self.position_ids.device)
if inputs_embeds is None:
inputs_embeds = self.word_embeddings(input_ids)
token_type_embeddings = self.token_type_embeddings(token_type_ids)
embeddings = inputs_embeds + token_type_embeddings
if self.position_embedding_type == "absolute":
position_embeddings = self.position_embeddings(position_ids)
embeddings += position_embeddings
embeddings = self.LayerNorm(embeddings)
embeddings = self.dropout(embeddings)
return embeddings
BERT和transformer encoder的区别是,在输入端增加segment embedding;position embedding不再使用三角函数的方法生成,而是采用可学习的embedding table。
BERT训练时使用的学习率 warm-up 策略是怎样的?为什么要这么做?
warmup 需要在训练最初使用较小的学习率来启动,并很快切换到大学习率而后进行常见的 decay。
这是因为,刚开始模型对数据的“分布”理解为零,或者是说“均匀分布”(当然这取决于你的初始化);在第一轮训练的时候,每个数据点对模型来说都是新的,模型会很快地进行数据分布修正,如果这时候学习率就很大,极有可能导致开始的时候就对该数据“过拟合”,后面要通过多轮训练才能拉回来,浪费时间。当训练了一段时间(比如两轮、三轮)后,模型已经对每个数据点看过几遍了,或者说对当前的batch而言有了一些正确的先验,较大的学习率就不那么容易会使模型学偏,所以可以适当调大学习率。这个过程就可以看做是warmup。那么为什么之后还要decay呢?当模型训到一定阶段后(比如十个epoch),模型的分布就已经比较固定了,或者说能学到的新东西就比较少了。如果还沿用较大的学习率,就会破坏这种稳定性,用我们通常的话说,就是已经接近loss的local optimal了,为了靠近这个point,我们就要慢慢来。
神经网络中 warmup 策略为什么有效;有什么理论解释么?
1268 赞同 · 19 评论回答
BERT训练过程中的损失函数是什么?
BERT的损失函数由两部分组成,第一部分是来自 MLM 的单词级别分类任务,另一部分是来自NSP的句子级别的分类任务。通过这两个任务的联合学习(是同时训练,而不是前后训练),可以使得 BERT 学习到的表征既有 token 级别信息,同时也包含了句子级别的语义信息。具体损失函数如下:
其中 是 BERT 中 Encoder 部分的参数,
是 MLM 任务中在 Encoder 上所接的输出层中的参数,
则是句子预测任务中在 Encoder 接上的分类器参数。因此,在第一部分的损失函数中,如果被 mask 的词集合为 M,因为它是一个词典大小 |V| 上的多分类问题,那么具体说来有:
在句子预测任务中,也是一个分类问题的损失函数:
因此,两个任务联合学习的损失函数是:
为什么BERT比ELMo效果好?ELMo和BERT的区别是什么?
因为LSTM抽取特征的能力远弱于Transformer,即使是拼接双向特征,其融合能力也偏弱;BERT的训练数据以及模型参数均多于ELMo。
ELMO给下游提供的是每个单词的embedding,所以这一类预训练的方法被称为“Feature-based Pre-Training”。而BERT模型是“基于Fine-tuning的模式”,这种做法的下游任务需要将模型改造成BERT模型,才可利用BERT模型预训练好的参数。