Skip to content

双向编码器BERT

一、什么是 BERT

BERT 的全称是 “来自 Transformers 的双向编码器表示”(Bidirectional Encoder Representations from Transformers),它结合了 ELMo 和 GPT 的优点:

  1. 它像 ELMo 一样,能对上下文进行双向编码,也就是能同时考虑一个词前面和后面的内容,更全面地理解词的语义;
  2. 它又像 GPT 一样,是任务无关的,在处理不同的自然语言处理任务时,只需要对模型做很小的改动,不用重新设计整个模型。 elmo-gpt-bert.svg

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-input.svg

三、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

六、小结

  1. BERT 是一个双向的预训练语言模型,能同时考虑词的前后上下文,更全面地理解语义;

  2. BERT 的输入有特殊的格式,能处理单个文本和文本对,还通过片段嵌入和位置嵌入来区分不同的文本和词的位置;

  3. BERT 的预训练任务包括掩蔽语言模型和下一句预测,分别让模型学习到词的语义和句子之间的关系;

  4. BERT 是任务无关的,在处理不同的 NLP 任务时,只需要添加简单的输出层,然后微调模型即可。

(注:文档部分内容可能由 AI 生成) 源地址

京ICP备2024093538号-1