bert模型的输入输出
模型输入
3个embedding:token embedding字向量,position embedding位置向量,segement embedding文本向量
- 关于token embedding
token embedding要将各个词转换成固定维度的向量。在BERT中,每个词会被转换成768维的向量表示。输入文本在送入token embeddings 层之前要先进行tokenization处理。
tokenization使用的方法是WordPiece tokenization。两个特殊的token会被插入到tokenization的结果的开头 ([CLS])和结尾 ([SEP])
- 关于position embedding
- 关于segement embedding
-
分词
(参考原文链接:https://blog.csdn.net/u010099080/article/details/102587954)
BERT 源码中 tokenization.py 就是预处理进行分词的程序,主要有两个分词器:BasicTokenizer 和 WordpieceTokenizer,另外一个 FullTokenizer 是这两个的结合:先进行BasicTokenizer 得到一个分得比较粗的 token 列表,然后再对每个 token 进行一次 WordpieceTokenizer,得到最终的分词结果。
BasicTokenizer
是一个初步的分词器。对于一个待分词字符串,流程大致就是转成 unicode-->去除各种奇怪字符-->处理中文(判断是否为中文,中文按字符分隔)-->空格分词-->去除多余字符和标点分词-->再次空格分词
WordpieceTokenizer是在BT结果的基础上进行再一次切分,得到子词(subword,以 ## 开头),词汇表就是在此时引入的。该类只有两个方法:一个初始化方法__init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=200),一个分词方法tokenize(self, text)。tokenize(self, text)
:该方法就是主要的分词方法了,大致分词思路是按照从左到右的顺序,将一个词拆分成多个子词,每个子词尽可能长。 按照源码中的说法,该方法称之为 greedy longest-match-first algorithm,贪婪最长优先匹配算法。
样本方面
def convert_single_example( max_seq_length, tokenizer,text_a, text_b=None): tokens_a = tokenizer.tokenize(text_a) tokens_b = None if text_b: tokens_b = tokenizer.tokenize(text_b)# 这里主要是将中文分字 if tokens_b: # 如果有第二个句子,那么两个句子的总长度要小于 max_seq_length - 3 # 因为要为句子补上[CLS], [SEP], [SEP] _truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3) else: # 如果只有一个句子,只用在前后加上[CLS], [SEP] 所以句子长度要小于 max_seq_length - 3 if len(tokens_a) > max_seq_length - 2: tokens_a = tokens_a[0:(max_seq_length - 2)] # 转换成bert的输入,注意下面的type_ids 在源码中对应的是 segment_ids # (a) 两个句子: # tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP] # type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1 # (b) 单个句子: # tokens: [CLS] the dog is hairy . [SEP] # type_ids: 0 0 0 0 0 0 0 # # 这里 "type_ids" 主要用于区分第一个第二个句子。 # 第一个句子为0,第二个句子是1。在预训练的时候会添加到单词的的向量中,但这个不是必须的 # 因为[SEP] 已经区分了第一个句子和第二个句子。但type_ids 会让学习变的简单 tokens = [] segment_ids = [] tokens.append("[CLS]") segment_ids.append(0) for token in tokens_a: tokens.append(token) segment_ids.append(0) tokens.append("[SEP]") segment_ids.append(0) if tokens_b: for token in tokens_b: tokens.append(token) segment_ids.append(1) tokens.append("[SEP]") segment_ids.append(1) input_ids = tokenizer.convert_tokens_to_ids(tokens)# 将中文转换成ids # 创建mask input_mask = [1] * len(input_ids) # 对于输入进行补0 while len(input_ids) < max_seq_length: input_ids.append(0) input_mask.append(0) segment_ids.append(0) assert len(input_ids) == max_seq_length assert len(input_mask) == max_seq_length assert len(segment_ids) == max_seq_length return input_ids,input_mask,segment_ids # 对应的就是创建bert模型时候的input_ids,input_mask,segment_ids 参数
- 对于文本分类任务,BERT模型在文本前插入一个[CLS]符号,并将该符号对应的输出向量作为整篇文本的语义表示
-
- 语句对分类任务:该任务的实际应用场景包括:问答(判断一个问题与一个答案是否匹配)、语句匹配(两句话是否表达同一个意思)等。对于该任务,BERT模型除了添加[CLS]符号并将对应的输出作为文本的语义表示,还对输入的两句话用一个[SEP]符号作分割,并分别对两句话附加两个不同的文本向量以作区分
- 序列标注任务:该任务的实际应用场景包括:中文分词&新词发现(标注每个字是词的首字、中间字或末字)、答案抽取(答案的起止位置)等。对于该任务,BERT模型利用文本中每个字对应的输出向量对该字进行标注(分类),如下图所示(B、I、E分别表示一个词的第一个字、中间字和最后一个字)。
模型输出
获取bert模型的输出,使用 model.get_sequence_output()
和model.get_pooled_output()
两个方法。
output_layer = model.get_sequence_output()# 这个获取每个token的output 输出[batch_size, seq_length, embedding_size]
"""Gets final hidden layer of encoder.
Returns:
float Tensor of shape [batch_size, seq_length, hidden_size] corresponding
to the final hidden of the transformer encoder.
"""
output_layer = model.get_pooled_output() # 这个获取句子的output
mask问题
在 BERT 的 Masked LM 训练任务中, 会用 [MASK] token 去替换语料中 15% 的词,然后在最后一层预测。但是下游任务中不会出现 [MASK] token,导致预训练和 fine-tune 出现了不一致,为了减弱不一致性给模型带来的影响,在这被替换的 15% 语料中:
1. 80% 的 tokens 会被替换为 [MASK] token
2. 10% 的 tokens 会称替换为随机的 token
3. 10% 的 tokens 会保持不变但需要被预测
第一点中的替换:是 Masked LM 中的主要部分,可以在不泄露 label 的情况下融合真双向语义信息;
第二点的随机替换:因为需要在最后一层随机替换的这个 token 位去预测它真实的词,而模型并不知道这个 token 位是被随机替换的,就迫使模型尽量在每一个词上都学习到一个 全局语境下的表征,因而也能够让 BERT 获得更好的语境相关的词向量(这正是解决一词多义的最重要特性);
第三点的保持不变:也就是真的有 10% 的情况下是 泄密的(占所有词的比例为15% * 10% = 1.5%),这样能够给模型一定的bias,相当于是额外的奖励,将模型对于词的表征能够拉向词的真实表征(此时输入层是待预测词的真实 embedding,在输出层中的该词位置得到的embedding,是经过层层 Self-attention 后得到的,这部分 embedding 里多少依然保留有部分输入embedding 的信息,而这部分就是通过输入一定比例的真实词所带来的额外奖励,最终会使得模型的输出向量朝输入层的真实 embedding 有一个偏移)。 而如果全用 mask 的话,模型只需要保证输出层的分类准确,对于输出层的向量表征并不关心,因此可能会导致最终的向量输出效果并不好。
被随机选择15%的词当中以10%的概率用任意词替换去预测正确的词,相当于文本纠错任务,为BERT模型赋予了一定的文本纠错能力;被随机选择15%的词当中以10%的概率保持不变,缓解了finetune时候与预训练时候输入不匹配的问题(预训练时候输入句子当中有mask,而finetune时候输入是完整无缺的句子,即为输入不匹配问题)
模型结构
关于attention机制
Attention机制将目标字和上下文各个字的语义向量表示作为输入,首先通过线性变换获得目标字的Query向量表示、上下文各个字的Key向量表示以及目标字与上下文各个字的原始Value表示,然后计算Query向量与各个Key向量的相似度作为权重,加权融合目标字的Value向量和各个上下文字的Value向量,作为Attention的输出,即:目标字的增强语义向量表示。
Self-Attention:对于输入文本,我们需要对其中的每个字分别增强语义向量表示,因此,我们分别将每个字作为Query,加权融合文本中所有字的语义信息,得到各个字的增强语义向量,如下图所示。在这种情况下,Query、Key和Value的向量表示均来自于同一输入文本,因此,该Attention机制也叫Self-Attention
Multi-head Self-Attention:为了增强Attention的多样性,文章作者进一步利用不同的Self-Attention模块获得文本中每个字在不同语义空间下的增强语义向量,并将每个字的多个增强语义向量进行线性组合,从而获得一个最终的与原始字向量长度相同的增强语义向量
使用多头注意力,能够从不同角度提取信息,提高信息提取的全面性
【Multi-head Self-Attention的输入和输出在形式上完全相同,输入为文本中各个字的原始向量表示,输出为各个字融合了全文语义信息后的增强向量表示】
Transformer Encoder在Multi-head Self-Attention之上又添加了三种关键操作:
- 残差连接(ResidualConnection):将模块的输入与输出直接相加,作为最后的输出。这种操作背后的一个基本考虑是:修改输入比重构整个输出更容易(“锦上添花”比“雪中送炭”容易多了!)。这样一来,可以使网络更容易训练。解决网络层数比较多时产生的梯度消失的问题
-
Layer Normalization:对某一层神经网络节点作0均值1方差的标准化。
layer normalization 和batch normalization的区别
计算的维度不同,BN是基于不同batch的同一个channel计算的,LN是基于同一个batch的不同字符之间计算的
- 线性转换:对每个字的增强语义向量再做两次线性变换,以增强整个模型的表达能力。这里,变换后的向量与原向量保持长度相同。
【Transformer Encoder的输入和输出在形式上还是完全相同,因此,Transformer Encoder同样可以表示为将输入文本中各个字的语义向量转换为相同长度的增强语义向量】
BERT使用Transformer-encoder来编码输入,encoder中的Self-attention机制在编码一个token的时候同时利用了其上下文的token,其中‘同时利用上下文’即为双向的体现