# BERT

## BERT的层级结构

BERT类的模型基本都由以下几种层级对接而成.

* 输入层
* Embedding层
* 主要层, 主要由Transformer结构和FeedForward结构组成
* 输出层

BERT模型的主要结构, 是由**主要层**堆叠而成, 例如在`bert-base`中就包含12层主要层. Bert类模型的主要层大同小异, 主要由Transformer layer, feed forward layer, layerNormalization等结构组成.

下面分析的是标准的Bert模型中, 每层的结构.

### 输入层

标准的Bert模型输入层由两部分组成:

* token input: 文本序列对应的token ids
* segment input: 每个token对应的segment ids

#### token ids

首先是token ids. Tokenizer原始文本得到token list, 每个token对应唯一的id, 最终得到token id list. 这个list的长度就是输入到模型中的长度.

标准Bert使用的tokenizer, 使用**BPE**(Byte-Pair Encoding)的编码方法, 将字符串文本分割为一个个的token. BPE是WordPiece的一种常见的方案. 简单来说是将一个单词(特指英文单词)继续划分, 将单词拆分到**Byte**层面, 也就是单个字符/字母(char)的层面, 然后对常见的byte组合, 得到byte-pair, 以此作为token的粒度.

例如`loved`, `loving`, `loves`三个单词, 如果以词为单位, 那么这三个单词的初始化是完全**正交**的, 同样的句子将对应位置的单词换成这三个单词, 理解的差异是很大的, 三者只能通过共现关系, 后续训练获得相关性. 但其实我们知道这三个单词是有着共同的意思的, 如果拆分成`lov`, `ed`, `ing`, `es`这种形式的token, 三个不同的句子将会共用`lov`部分, 直接就得到了语义的共通点. 且`ed`, `ing`, `es`也会出现在其他动词形态中, 不同动词也共享着`过去时`, `进行时`, `第三人称现在时`的语法.

另外还有着其他的好处:

* 同一语系的不同语言往往单词有着共同的词根, 代表着近似甚至相同的意思. 这对多语言任务提供了天然便利
* 可以起到**减少词表大小**的作用

然而对于中文, 最小的粒度**字**已经无法再拆分(拆分成部首甚至笔画的意义不大), 因此BPE也只是对英文作用. Bert的标准tokenizer对中文语句的处理, 基本上是按字进行tokenizer. 而且中文的字表是有限的, 词表却是可以无穷扩大的, 这点正好跟英文相反, 因此使用字的粒度作为token, 是非常合适的.

#### segment id

在Bert的训练过程中, 使用两个句子, 因此在输入中需要标注每个token所属的句子. 我们将每个句子称为segment, segment id list的长度与上面token id list的长度是相等的, 每个位置是一一对应的.

使用0表示第一个句子, 1表示第二个句子, 依次类推. 一般的任务中最多也就会使用两个句子, 0和1已经够用了.

需要注意的是使用两个句子时, 句子分界处的细节. Bert在将一个句子tokenize时, 会在句子的开始和结束分别补充`[CLS]`和`[SEP]`两个特殊的token, 代表开始和结束. 但`[CLS]`作为特殊token表示整体的开始, 只在第一个位置出现, 因此由两个句子组成的文本序列, tokenize会生成`[CLS] s11 s12 ... [SEP] s21 s22 ... [SEP]`的形式.

### Embedding层

Embedding层的作用, 就是将输入层的各种id输入, 通过**各自的Embedding矩阵**, 得到各自向量表达, 混合后输入到后续层中.

标准的Bert模型, embedding由**token embedding**, **position embedding**以及**segment embedding**三部分组成. 其中的token embedding和segment embedding会将上面的输入转换为相同长度的向量. position embedding会将的**绝对位置**转换为向量.

具体方法为给输入的token按`0, 1, ...`的顺序赋给每个token一个绝对位置的id, 然后查表将id转为向量.

然后对于每个的token, 将三种embedding结果相加, 就得到了这个token的混合表示.

需要注意以下几点.

#### position

位置在Transformer这种结构是必不可少的成分, 否则以Transformer的特性, 将两个token对调, 得到的结果会完全相同.

标准的Bert使用**绝对位置**, 但在其他的一些Bert类模型中(如T5, Nezha等), 会使用**相对位置**代替绝对位置. 得到相对位置的方法每种模型不同.

但如果使用**相对位置**, 就不在是得到id, 然后查表, 通过embedding矩阵转换为向量这种方法了. 而是得到一个$$n*n$$的矩阵($$n$$是token的数量), 直接作用在**attention矩阵**中(attention矩阵的大小也是$$n*n$$). 可以参考T5模型理解这种操作.

#### 长度限制

模型预训练得到的权重文件中, 包含**token embedding**, **position embedding**, **segment embedding**这三个矩阵的参数值. 因此在使用时, 读取预训练得到的权值作为初始化的值.

但所有开源的Bert预训练权重文件中, 对应的**position embedding**都只包含长度范围为**512**的权重, 对于更长的序列, 对应的position id就超出了范围, 因此无法得到对应的位置向量表示.

即使另外初始化超出范围的位置对应的embedding, 也会造成pre-training和fine-tune不一致的问题, 较大程度的影响模型的表现.

因此标准的Bert类模型, 只能处理长度不大于512的序列. 如果要处理更长的序列, 就需要另找办法处理.

### 主要层

主要层是Bert最重要的部分, 也是优秀表征的来源.

标准Bert模型的主要层, 是按照如下的顺序拼接而成的.

`MultiHead Attention` >>> `Add` >>> `Layer Normalization` >>> `Feed Forward` >>> `Add` >>> `Layer Normalization`

其中的`Add`指的是将上一层得到结果与进行上一层转换之前的输入相加. 而`Feed Forward`层其实就是两个`Dense`层叠加, 其中第一个`Dense`的放大size, 且激活函数如`relu`, 第二个`Dense`缩小size, 并且没有激活函数.

### 输出层

输出层输出的内容是多种多样的, 应对不同的任务需要灵活使用不同的输出.

#### logit output

最基础的, 就是输出每个位置token的表达向量, 输出一个$$n \* m$$的矩阵, 其中$$n$$表示序列长度, $$m$$表示表征向量的长度.

注意标准Bert这部分的输出是没有经过`softmax`作用的, 因此得到的是logit, 而非`probability`.

#### pooler output

Pooler部分其实指的就是第一个token, `[CLS]`对应的向量, 并没有进行如果CNN一般的池化操作.

很多分类任务多使用这个向量构建下游结构. 因此在标准的Bert中, pooler output在输出前, 可以进行一个向量长度不变的Dense, 并作用指定的激活函数, 默认是使用`tanh`函数进行激活.

#### nsp output

Next Sentence Prediction任务需要的输出, 基本只在Bert的预训练中使用. 将两个句子合并在一起输入, 判断两个句子是否有顺序关系. 作为一个简单的分类任务, 组成预训练中loss的一部分.

nsp output其实就是对pooler output(经过激活的)再经过一个`Dense`层映射为长度为2的向量, 并经过`softmax`函数激活, 因此每个位置表示二分类的一个概率预测结果.

#### mlm output

Masked Language Model输出. 这部分作为预训练loss的主要成分, 转换稍微复杂些. 预测被mask的token具体是什么, 就需要对应位置的一个概率向量, 长度为字典的大小, 表示每个token的概率.

将logit output首先经过`Dense`层映射, 并经过`relu`激活, 转换为向量长度不变的新表征. 然后再通过`Layer Normalization`正则化输出, 得到一个$$n \* m$$的矩阵, 其中$$n$$表示序列长度, $$m$$表示表征向量的长度.

将这个矩阵与Embedding层中`token embedding`矩阵权值相乘, 得到$$n \* v$$的矩阵, 其中$$v$$表示字典大小. 再经过`softmax`按相应为维度进行激活, 就得到了每个位置token对应的每个可能token的概率值. 这就是mlm output, 一个大小为$$n \* v$$的矩阵.

**需要注意的是**:

* 这里有着参数的共享. 共享使用了Embedding层中`token embedding`矩阵权值
* 使用`Layer Normalization`的原因是大致将向量归一化, 后面与token embedding中每个向量点乘的结果, 更多的由夹角确定, 排除模长的影响

#### 预训练的权值

需要注意的是, 输出层里的很多输出结果都引入了额外的参数, 如`Dense`, `Layer Normalization`等, 不同的预训练权值文件中, 有的可能没有包含输出层的这些参数, 因此只能使用随机数初始化这些权值, 无法享受预训练带来的结果. 特别是在预测mask位置token的任务时, 影响尤为显著.

对于中文预训练模型, 哈工大开源的large版本[RoBERTa-wwm-ext-large](https://github.com/ymcui/Chinese-BERT-wwm)是**不包含**MLM权重的, 但base版本却包含. 腾讯开源的[UER](https://github.com/dbiir/UER-py)是都包含MLM权重的.

## 参考资料

* [bert4keras](https://github.com/bojone/bert4keras/blob/master/bert4keras/models.py)
