概述
代表模型BERT。BERT是2018年10月由Google AI研究院提出的一种预训练模型.
- BERT的全称是 Bidirectional Encoder Representation from Transformers.
- BERT在机器阅读理解顶级水平测试SQuAD1.1中表现出惊人的成绩: 全部两个衡量指标上全面超越人类, 并且在11种不同NLP测试中创出SOTA表现. 包括将GLUE基准推高至80.4% (绝对改进7.6%), MultiNLI准确度达到86.7% (绝对改进5.6%). 成为NLP发展史上的里程碑式的模型成就.

架构
总体架构: 如下图所示, 最左边的就是BERT的架构图, 可以很清楚的看到BERT采用了Transformer Encoder block进行连接, 因为是一个典型的双向编码模型.

从上面的架构图中可以看到, 宏观上BERT分三个主要模块:
- 最底层黄色标记的Embedding模块.
- 中间层蓝色标记的Transformer模块.
- 最上层绿色标记的预微调模块.
2.1 输入部分

- Token Embeddings 是词嵌入张量, 第一个单词是CLS标志, 可以用于之后的分类任务.
- Segment Embeddings 是句子分段嵌入张量, 是为了服务后续的两个句子为输入的预训练任务.
- Position Embeddings 是位置编码张量, 此处注意和传统的Transformer不同, 不是三角函数计算的固定位置编码, 而是通过学习得出来的.
2.2 多头注意力

2.3 层归一化
层归一化,对输入数据缩放,有效的控制方差,提升训练的稳定性
$$LayerNorm(x) = \gamma \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta$$2.4 前馈网络层
Feed Forward,线性层+激活层+线性层。激活函数使用GELU。
$$FFN(x) = GELU(xW_1 + b_1)W_2 + b_2$$2.5 激活函数
在前馈网络(FFN)使用 GELU作为激活函数

GELU函数的精确定义为:
$GELU(x) = x \cdot \Phi(x)$
其中$\Phi(x)$是标准正态分布的累积分布函数(CDF),Bert实际上做了近似处理,计算效率更高。
$GELU \simeq 0.5x(1+\tanh(\frac 2 \pi(x+0.044715x^3))) $
注意RoBERTa / GPT-1/2 均采用该近似版本。
Bert为什么选择GELU而不是RELU

面试一句话总结:
BERT 使用的激活函数是 GELU,它是一种基于高斯分布的平滑激活函数,相比 ReLU 在负值区间处理更柔和,有助于提升模型表达能力和训练稳定性。
预训练任务
MaskedLM(完形填空)
NSP(是否下句子预测)


微调任务
4.1 单句分类任务
应用的任务包括
- 意图识别
- 新闻文本分类
- 情感分类
计算原理
- 取[CLS]向量,维度为[B, H]
- 接入分类层,本质上做线形变换$W\in \mathbb R ^{H\times C}$
- 最终激活函数使用softmax/sigmoid,得到归一化概率
- 概率最大的作为分类标签

4.2 句子对分类任务
应用的任务包括
- 句子对的相似度计算
- 文本是否匹配
计算原理
- 取[CLS]向量,维度为[B, H]
- 接入分类层,本质上做线形变换$W\in \mathbb R ^{H\times C}$
- 最终激活函数使用softmax/sigmoid,得到归一化概率
- 概率最大的作为分类标签

4.3 序列标注任务
应用的任务包括
- 实体识别NER、分词、词性标注
计算原理
- 取每个token向量,维度为[B, L, H]
- 接入分类层,本质上做线形变换$W\in \mathbb R ^{H\times C}$
- 最终激活函数使用softmax/sigmoid,得到归一化概率
- 针对token分类,概率最大的作为分类标签

4.4 基于原文的问答任务
基本不用了,不用看

源码分析
使用句子分类模型加载权重,查看模型的架构
from transformers import BertTokenizer, BertForSequenceClassification
model_path = "/Users/chan/projects/models/bert-base-chinese"
# 加载分词器
tokenizer = BertTokenizer.from_pretrained(model_path)
# 加载分类模型
model = BertForSequenceClassification.from_pretrained(model_path, num_labels=5)
print(model)
BertForSequenceClassification(
(bert): BertModel(
(embeddings): BertEmbeddings(
(word_embeddings): Embedding(21128, 768, padding_idx=0) # 词嵌入向量
(position_embeddings): Embedding(512, 768) # 位置编码向量
(token_type_embeddings): Embedding(2, 768) # 句子归属向量
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(encoder): BertEncoder( # 编码器
(layer): ModuleList(
(0-11): 12 x BertLayer(
(attention): BertAttention( # 注意力机制
(self): BertSdpaSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True) # 查询
(key): Linear(in_features=768, out_features=768, bias=True) # 键
(value): Linear(in_features=768, out_features=768, bias=True) # 值
(dropout): Dropout(p=0.1, inplace=False)
)
(output): BertSelfOutput( # 注意力机制输出
(dense): Linear(in_features=768, out_features=768, bias=True) # 输出密集层
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
(intermediate): BertIntermediate( # FFN前馈层
(dense): Linear(in_features=768, out_features=3072, bias=True) # 第一次线性变化
(intermediate_act_fn): GELUActivation() # 激活函数
)
(output): BertOutput(
(dense): Linear(in_features=3072, out_features=768, bias=True) # 第二次线性变换
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
)
)
(pooler): BertPooler( # 池化层 取[CLS]的token输出
(dense): Linear(in_features=768, out_features=768, bias=True)
(activation): Tanh()
)
)
(dropout): Dropout(p=0.1, inplace=False)
(classifier): Linear(in_features=768, out_features=5, bias=True) # 线性分类层
)
分类任务
inputs = "hello world!"
inputs = tokenizer(inputs, return_tensors='pt')
output = model(**inputs) ## 前向传播
logits = output.logits # 未归一化的概率
# 取概率最大的标签,就是分类标签
logits.argmax()
MLM任务
from transformers import BertForMaskedLM
mlm = BertForMaskedLM.from_pretrained(model_path)
mlm_inputs = "[MASK]京是中国的首都"
mlm_inputs = tokenizer(mlm_inputs, return_tensors='pt')
output = mlm(**mlm_inputs)
logits = output.logits
index = 1 # [MASK] 位置在1索引
logits[:, index, :]
mlm_input_id = logits[:, index, :].argmax()
tokenizer.decode(mlm_input_id)
# [MASK] ===> 北
总结
本节内容主要讲解bert的原理,包括架构、预训练任务、微调任务、如何做分类任务,如何完成掩码词预测任务。重点需要掌握bert的架构以及微调分类任务。