Skip to content

Latest commit

 

History

History
206 lines (148 loc) · 10 KB

BERT.md

File metadata and controls

206 lines (148 loc) · 10 KB

BERT

论文: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

最大贡献是验证了在 NLP 中也可以采用先无监督预训练后微调的方式,而且效果非常好。微调时候只需要简单的增加一些输出层即可对下游任务进行微调,不需要修改 BERT 架构。

图片来自 李宏毅 bert ppt 和 链接 感谢~

原理简析

https://huggingface.co/bert-base-uncased

示意图如下:

  • NSP: "next sentence prediction" 判断输入的两句话在语义上是否是连续的 (这两个句子可以是连续的语篇文本,也可以是随机选择的两个不相关的句子),注意并不是 next token prediction, 是一个2分类任务

BERT 是基于 GPT1 来进行改进的。GPT1 是单向的 Transformer,作者认为单向的 Transformer 会导致模型对于上下文的理解不够,因此提出了 BERT (当然从现在来看,其实 GPT 系列效果其实可以非常好)。 BERT 是双向的 Transformer。

BERT 通过使用 “掩码语言模型”(MLM)预训练目标来缓解前面提到的单向约束。掩码语言模型从输入中随机屏蔽一些标记,目标是基于上下文预测掩码处的原始词汇 id。除了掩码语言模型之外,还使用类似的 “下一句预测”任务来联合预训练文本对表示。

网络结构参数:

  • BERT_BASE (L=12, H=768, A=12, Total Parameters=110M)
  • BERT_LARGE (L=24, H=1024, A=16, Total Parameters=340M)
  • 最大支持长度为 512,即输入 token 最长是 512 个

A: the number of self-attention heads

为了让 BERT 可以尽可能的方便处理各种下游任务,作者在输入表示方面进行了一些设计,图示如下

典型的 NLP 任务例如 NSP,输入是一句话,但是对于判断两句话是否相似则输入是两句话。因此作者这里的输入句子不是真的是指的真正含义上的句子,也可以是两个句子然后通过特殊符号连接的一个句子作为输入。这样就可以统一输入形式,方便下游微调。

作者使用 WordPiece 嵌入算法进行分词和 30,000 个标记词汇,也就是词汇表大概是 3w。每个输入序列的第一个token始终是一个特殊的分类标记([CLS])。与该标记对应的最终隐藏状态被用作分类任务的聚合序列表示。如果是多个句子,则会打包成一个序列。作者以两种方式区分句子。首先,将它们与特殊标记 ([SEP]) 分开。其次,为每个标记添加一个可学习嵌入,指示它是否属于句子 A 或句子 B。

预训练过程

作者采用了两个任务联合训练的方式进行无监督预训练。

(1) Task #1: Masked LM

简单地随机屏蔽一定百分比的输入标记,然后预测这些掩码标记。与掩码标记对应的最终隐藏向量被输入到词汇表的输出 softmax 中进行训练。在我们所有的实验中,随机屏蔽每个序列中 15% 的所有 WordPiece 标记,并仅仅预测 masked token 而非整个句子。

但是这样会有一个问题:训练时候使用的 [MASK] token 在微调期间不会出现,这会导致预训练和微调期间的不一致。

解决办法是: 我们并不总是用实际的 [MASK] 标记替换“屏蔽”单词。训练数据生成器随机选择 15% 的标记位置进行预测。如果选择第 i 个令牌,我们将第 i 个令牌替换为

  • (1) 80% 的概率替换为 [MASK] token
  • (2) 10% 的概率替换为 随机token
  • (3) 10% 的概率未更改的第 i 个token 。然后,T_i 将用于预测具有交叉熵损失的原始标记

(2) Task #2: Next Sentence Prediction (NSP)

这个任务不是说文本生成。而是说给定两个句子,判断这两个句子是否是连续的。这个任务的目的是让模型学习到句子之间的关系。具体为在为每个预训练示例选择句子 A 和 B 时,50% 的时间 B 是遵循 A 的实际下一个句子(标记为 IsNext),50% 的时间是语料库中的随机句子(标记为 NotNext)。 所以实际上预训练时候这实际上是一个二分类问题。作者验证了虽然看起来好像非常简单,但是对于 Question Answering 等任务是非常有用的,因为这些任务要理解句子之间的联系。

对于预训练语料库,我们使用 BooksCorpus(8 亿字)和英语维基百科(2,500M 字)。对于 Wikipedia,我们只提取文本段落而忽略列表、表格和标题。使用文档级语料库而不是打乱的句子级语料库(例如十亿字基准)以提取长的连续序列至关重要。

Fine-tuning BERT

对于每个任务,我们只需将特定于任务的输入和输出插入 BERT 中并端到端微调所有参数。作者在 11 个任务上进行了微调。

典型任务构造方式:

单句/句子对分类任务:直接使用 [CLS] 的 hidden state 过一层分类层+ softmax 进行预测;

QA 任务:

给定一段内容和问题,输出答案的起始和结束位置。这里的输出是两个位置,所以需要两个分类层。 红蓝向量是新增的可学习 embedding。

代码详解

我们选择最常用的 https://huggingface.co/bert-base-uncased 来进行源码分析。也有中文版的 https://huggingface.co/bert-base-chinese

uncased 表示不区分大小写,统一转为小写输入和输出。

from transformers import BertTokenizer, BertModel
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
print(tokenizer.vocab_size)  # 30522
print(tokenizer.get_vocab()) # 第 0 个是 [PAD],中间是 100 个 [unused], 然后是 '[CLS]': 101, '[SEP]': 102, '[MASK]': 103,后面一大堆又是 [unused]

架构可以查看文件 config.json

{
  "architectures": [
    "BertForMaskedLM"
  ],
  "attention_probs_dropout_prob": 0.1,
  "gradient_checkpointing": false,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072, # feedforward 层的维度
  "layer_norm_eps": 1e-12,
  "max_position_embeddings": 512, # 最大输入长度
  "model_type": "bert",
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "pad_token_id": 0,
  "position_embedding_type": "absolute", # 位置编码方式
  "transformers_version": "4.6.0.dev0",
  "type_vocab_size": 2,
  "use_cache": true,
  "vocab_size": 30522
}

只有编码器。详细结构见 bert model,具体代码见 code

下面提供了一个简单的 demo,并进行实现说明

from transformers import pipeline
unmasker = pipeline('fill-mask', model='bert-base-uncased')
o = unmasker("Hello I'm a [MASK] model.")
print(o)
# [{'score': 0.10731077939271927, 'token': 4827, 'token_str': 'fashion', 'sequence': "hello i'm a fashion model."},
# {'score': 0.0877445638179779, 'token': 2535, 'token_str': 'role', 'sequence': "hello i'm a role model."},
# {'score': 0.05338413640856743, 'token': 2047, 'token_str': 'new', 'sequence': "hello i'm a new model."},
# {'score': 0.046672213822603226, 'token': 3565, 'token_str': 'super', 'sequence': "hello i'm a super model."},
# {'score': 0.027095887809991837, 'token': 2986, 'token_str': 'fine', 'sequence': "hello i'm a fine model."}]

实际上内部调用的是 FillMaskPipeline 类,并依次调用 preprocess _forward postprocess 三个方法

def preprocess(self, inputs, return_tensors=None, **preprocess_parameters) -> Dict[str, GenericTensor]:
    if return_tensors is None:
        return_tensors = self.framework
    # Token 过程
    model_inputs = self.tokenizer(inputs, return_tensors=return_tensors)
    self.ensure_exactly_one_mask_token(model_inputs)
    return model_inputs
def _forward(self, model_inputs):
    # 就是 BertForMaskedLM =BertModel+分类层BertOnlyMLMHead,结构见 hf_transformer/bert_base_for_maskedlm.txt
    model_outputs = self.model(**model_inputs)
    model_outputs["input_ids"] = model_inputs["input_ids"]
    return model_outputs

(cls): BertOnlyMLMHead(
  (predictions): BertLMPredictionHead(
    (transform): BertPredictionHeadTransform(
      (dense): Linear(in_features=768, out_features=768, bias=True)
      (transform_act_fn): GELUActivation()
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
    )
    (decoder): Linear(in_features=768, out_features=30522, bias=True)
  )
)

输出 30522 维度

def postprocess(self, model_outputs, top_k=5, target_ids=None):
    
    input_ids = model_outputs["input_ids"][0]
    outputs = model_outputs["logits"]
    
    # 取出哪个 token 位置才是 mask 标记
    masked_index = torch.nonzero(input_ids == self.tokenizer.mask_token_id, as_tuple=False).squeeze(-1)
    
    # 只需要这个位置预测就可以 (1,30522)
    logits = outputs[0, masked_index, :]
    
    probs = logits.softmax(dim=-1)
    if target_ids is not None:
        probs = probs[..., target_ids]
    # 取 token 个预测
    values, predictions = probs.topk(top_k)
    
    # 把预测的 masked token 和原来的 token 合并,重新解码,得到最终结果
    # hello i'm a fashion model.
    sequence = self.tokenizer.decode(tokens, skip_special_tokens=single_mask)