Appearance
双向编码器BERT
一、什么是 BERT
BERT 的全称是 “来自 Transformers 的双向编码器表示”(Bidirectional Encoder Representations from Transformers),它结合了 ELMo 和 GPT 的优点:
- 它像 ELMo 一样,能对上下文进行双向编码,也就是能同时考虑一个词前面和后面的内容,更全面地理解词的语义;
- 它又像 GPT 一样,是任务无关的,在处理不同的自然语言处理任务时,只需要对模型做很小的改动,不用重新设计整个模型。
BERT 改进了 11 种自然语言处理任务的性能,涵盖了文本分类、文本对分类、问答、文本标记等多个领域,比如情感分析、自然语言推断、命名实体识别等任务都能用 BERT 来提升效果。
二、BERT 的输入表示
BERT 能处理两种输入:单个文本(比如一段影评)和文本对(比如两个句子,判断它们的关系),它的输入有特殊的格式:
1. 单个文本的表示
如果输入是单个文本,BERT 的输入序列是:特殊类别词元<cls> + 文本的词元 + 特殊分隔词元<sep>。比如输入文本是 “我喜欢看电影”,输入序列就是<cls> 我 喜 欢 看 电 影 <sep>。
2. 文本对的表示
如果输入是文本对,比如 “我喜欢看电影” 和 “这部电影很好看”,BERT 的输入序列是:<cls> + 第一个文本的词元 + <sep> + 第二个文本的词元 + <sep>,也就是<cls> 我 喜 欢 看 电 影 <sep> 这 部 电 影 很 好 看 <sep>。
3. 片段嵌入和位置嵌入
片段嵌入:为了区分文本对里的两个文本,BERT 会给第一个文本的每个词元加上片段嵌入
e_A,给第二个文本的每个词元加上片段嵌入e_B;如果是单个文本,就只加e_A。位置嵌入:因为文本是有顺序的,BERT 会给每个词元加上位置嵌入,用来表示这个词在序列里的位置,而且这个位置嵌入是可以学习的,模型会自己调整不同位置的表示。
我们可以用一个简单的函数来生成 BERT 的输入序列和对应的片段索引:
python
def get_tokens_and_segments(tokens_a, tokens_b=None):
"""获取BERT输入序列的词元和片段索引"""
tokens = ['<cls>'] + tokens_a + ['<sep>']
# 0和1分别标记片段A和B
segments = [0] * (len(tokens_a) + 2)
if tokens_b is not None:
tokens += tokens_b + ['<sep>']
segments += [1] * (len(tokens_b) + 1)
return tokens, segments三、BERT 的模型结构
BERT 的核心是BERTEncoder,它的结构很清晰,就像一个多层的双向特征提取器:
1. 嵌入层
首先是三个嵌入层的叠加:
词嵌入:把每个词元转换成对应的向量,也就是把文字变成计算机能理解的数字;
片段嵌入:用来区分不同的文本片段;
位置嵌入:用来表示词元的位置。
这三个嵌入加起来,就得到了每个词元的初始表示。
2. Transformer 编码器堆叠
然后是多层的 Transformer 编码器,每个编码器都能提取更高级的特征,就像一层一层地把文本的语义信息提炼出来。
用 PyTorch 实现的 BERT 编码器代码如下:
python
import torch
from torch import nn
from d2l import torch as d2l
#@save
class BERTEncoder(nn.Module):
"""BERT编码器"""
def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,
ffn_num_hiddens, num_heads, num_layers, dropout,
max_len=1000, key_size=768, query_size=768, value_size=768,
**kwargs):
super(BERTEncoder, self).__init__(**kwargs)
# 词嵌入层
self.token_embedding = nn.Embedding(vocab_size, num_hiddens)
# 片段嵌入层,区分两个文本片段
self.segment_embedding = nn.Embedding(2, num_hiddens)
# 堆叠多个Transformer编码器块
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module(f"{i}", d2l.EncoderBlock(
key_size, query_size, value_size, num_hiddens, norm_shape,
ffn_num_input, ffn_num_hiddens, num_heads, dropout, True))
# 可学习的位置嵌入
self.pos_embedding = nn.Parameter(torch.randn(1, max_len,
num_hiddens))
def forward(self, tokens, segments, valid_lens):
# 把三个嵌入加起来,得到每个词元的初始表示
X = self.token_embedding(tokens) + self.segment_embedding(segments)
X = X + self.pos_embedding.data[:, :X.shape[1], :]
# 每个编码器块提取特征
for blk in self.blks:
X = blk(X, valid_lens)
return X四、BERT 的预训练任务
BERT 在使用之前需要先预训练,预训练有两个任务,用来让模型学习到语言的语义信息:
1. 掩蔽语言模型(Masked Language Modeling)
这个任务的思路很简单:随机遮住输入序列里 15% 的词元,然后让模型预测这些被遮住的词元。比如输入 “我喜欢看电影”,随机遮住 “喜欢”,变成 “我 看电影”,让模型猜<mask>是什么词。
不过为了让预训练和后续的微调更匹配,BERT 对被遮住的词元有三种处理方式:
80% 的时间用
<mask>替换被遮住的词元;10% 的时间用一个随机的词元替换被遮住的词元;
10% 的时间保持被遮住的词元不变。
这样模型就能学习到双向的上下文信息,因为要猜出被遮住的词,需要同时看这个词前面和后面的内容。
2. 下一句预测(Next Sentence Prediction)
这个任务是让模型判断两个句子是不是连续的。比如给模型两个句子,模型要预测第二个句子是不是第一个句子的下一句。比如:
正样本:“我喜欢看电影” 和 “这部电影的剧情很精彩”(这两个句子是连续的);
负样本:“我喜欢看电影” 和 “今天天气很好”(这两个句子不是连续的)。
这个任务能让模型学习到句子之间的关系,对问答、自然语言推断等任务很有帮助。
五、完整的 BERT 模型
把编码器和预训练任务结合起来,就得到了完整的 BERT 模型:
python
class MaskLM(nn.Module):
"""掩蔽语言模型任务"""
def __init__(self, vocab_size, num_hiddens, mlm_in_features):
super(MaskLM, self).__init__()
self.mlp = nn.Sequential(
nn.Linear(mlm_in_features, num_hiddens),
nn.ReLU(),
nn.LayerNorm(num_hiddens),
nn.Linear(num_hiddens, vocab_size)
)
def forward(self, X, pred_positions):
# 获取需要预测的位置的表示
num_pred_positions = pred_positions.shape[1]
pred_positions = pred_positions.reshape(-1)
batch_size = X.shape[0]
batch_idx = torch.arange(0, batch_size)
batch_idx = batch_idx.repeat_interleave(num_pred_positions)
masked_X = X[batch_idx, pred_positions]
masked_X = masked_X.reshape((batch_size, num_pred_positions, -1))
# 预测被遮住的词
mlm_Y_hat = self.mlp(masked_X)
return mlm_Y_hat
class NextSentencePred(nn.Module):
"""下一句预测任务"""
def __init__(self, nsp_in_features):
super(NextSentencePred, self).__init__()
self.output = nn.Linear(nsp_in_features, 2)
def forward(self, X):
# 用<cls>词元的表示来预测
return self.output(X)
class BERTModel(nn.Module):
"""完整的BERT模型"""
def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,
ffn_num_hiddens, num_heads, num_layers, dropout,
max_len=1000, key_size=768, query_size=768, value_size=768,
hid_in_features=768, mlm_in_features=768,
nsp_in_features=768):
super(BERTModel, self).__init__()
# BERT编码器
self.encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape,
ffn_num_input, ffn_num_hiddens, num_heads,
num_layers, dropout, max_len=max_len,
key_size=key_size, query_size=query_size,
value_size=value_size)
# 用来生成<cls>词元的表示
self.hidden = nn.Sequential(nn.Linear(hid_in_features, num_hiddens),
nn.Tanh())
# 掩蔽语言模型任务
self.mlm = MaskLM(vocab_size, num_hiddens, mlm_in_features)
# 下一句预测任务
self.nsp = NextSentencePred(nsp_in_features)
def forward(self, tokens, segments, valid_lens=None,
pred_positions=None):
# 编码输入序列
encoded_X = self.encoder(tokens, segments, valid_lens)
# 预测被遮住的词
if pred_positions is not None:
mlm_Y_hat = self.mlm(encoded_X, pred_positions)
else:
mlm_Y_hat = None
# 预测下一句
nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :]))
return encoded_X, mlm_Y_hat, nsp_Y_hat六、小结
BERT 是一个双向的预训练语言模型,能同时考虑词的前后上下文,更全面地理解语义;
BERT 的输入有特殊的格式,能处理单个文本和文本对,还通过片段嵌入和位置嵌入来区分不同的文本和词的位置;
BERT 的预训练任务包括掩蔽语言模型和下一句预测,分别让模型学习到词的语义和句子之间的关系;
BERT 是任务无关的,在处理不同的 NLP 任务时,只需要添加简单的输出层,然后微调模型即可。
(注:文档部分内容可能由 AI 生成) 源地址