目录

  • BERT 模型使用
  • BERT 模型文本分类
  • BERT 模型处理 BOOLQ 数据集
  • 总结
  • References



    第一次写博客,简单记录下使用BERT的经历,也顺便熟悉下Markdown语法,希望以后能坚持下来吧。

BERT 模型使用

    BERT 的具体介绍,我就略过了,一是网上可以找到很多资料,二是我也只是刚使用了下 BERT,很多细节还不清楚,就不乱说话误导人了。老实说,最开始查资料,找相关工程时,看的头大,不知从何入手。现在总结下我认为的上手BERT的合适流程。

  • 了解 BERT 的本质。简单说,BERT 提供了更好的词向量表示,是一个加强版的 Word2Vec,我们只需要在自己的数据集上针对特定任务再进行 fine-tuning 就可以取得不错的效果。至于BERT是如何训练的,以及内部各个参数的含义,我认为可以之后再研究。
  • 了解 BERT 的输入编码格式。这一块是困扰我最久的地方,因为在针对不同的下游任务时,需要对 BERT 的输入输出部分进行相应的修改,而网上大部分资料却忽略了这个在实际使用 BERT 时非常重要的细节,吐血。
    先看一下加载预训练的 BERT 模型后,输入参数是什么
# 我参考的一个工程是从 pytorch_pretrained_bert 中加载的 BERT 预训练模型。现在更推荐使用 transformers 库
from pytorch_pretrained_bert import BertModel, BertTokenizer
self.bert = BertModel.from_pretrained(config.bert_path)
_, pooled = self.bert(input_ids=token_tensors, token_type_ids=segments_tensor, attention_mask=mask_tensor, output_all_encoded_layers=False)

先忽略output_all_encoded_layers 这个参数,重点关注input_ids, token_type_ids, attention_mask 这三个输入参数,这三个参数需要从输入文本中得到并正确设置,模型才能正确工作。下面介绍这三个参数的含义以及如何设置。

bert pytorch 多语言 pytorch加载bert预训练模型_深度学习

大多数地方只展示了上图中第二条分隔线之上 的内容,而实际在 PyTorch 中使用 BERT 时,第二条分隔线之下的内容才是最重要的。我们需要将文本转化为3种 id tensors:

tensors

含义

tokens_tensor

对应上文的 input_ids,存储每个 token 的索引值,用 tokenizer 转化文本获得。所有任务都需要有这个输入

segments_tensor

对应上文的 token_type_ids,用来识别句子界限。如果是单句子分类任务,默认为全0,可以不用设置该参数;如果是文本蕴含关系等句对任务,则第一个句子对应的位置全0,句子间分隔符 [SEP] 处置为0,第二个句子对应位置全1,句尾分隔符 [SEP] 处置为1, [PAD] 处全置0

mask_tensor

对应上文的 attention_mask,用来界定注意力机制范围。1表示让 BERT 关注该位置,0代表是 [PAD] 不用关注。即 [PAD]处为0,其余位置为1

在实际应用时,给定一段文本,我们只需要得到这段文本对应的3个 tensors,将其传入 BERT 模型即可,这就是 BERT 模型输入端的设置。举个例子

# 单句子任务
sentence = "I am fine."
SEP, PAD, CLS = '[SEP]', '[PAD]', '[CLS]'
pad_size = 20
# 切记为句子补上开头的 [CLS] 及 结尾的 [SEP],如果有padding操作,则需补上相应长度的 [PAD]
# 格式为 [CLS] + 句子 + [SEP] + [PAD] * N
token = [CLS] + config.tokenizer.tokenize(sentence) + [SEP]
length = len(token)
token += [PAD] * (pad_size - length)
tokens_tensor = config.tokenizer.convert_tokens_to_ids(token)  # 用 tokenizer 转化,传递给 input_ids
segments_tensor = [0] * pad_size  # 单句子任务可以不设置此tensor, token_type_ids 默认为全0,传递给 token_type_ids
mask_tensor = [1] * length + [0] * (pad_size - length) # [PAD] 处全置0,其余位置置1,传递给 attention_mask
# 将以上3个 tensor 作为输入传递给 BERT 模型
_, pooled = self.bert(input_ids=tokens_tensor, token_type_ids=segments_tensor,  attention_mask=mask_tensor, output_all_encoded_layers=False)
# 或者不设置 segments_tensor
_, pooled = self.bert(input_ids=tokens_tensor, attention_mask=mask_tensor, output_all_encoded_layers=False)
# 句子对任务
sentence_A = "I am fine."
sentence_B = "Thank you."
SEP, PAD, CLS = '[SEP]', '[PAD]', '[CLS]'
pad_size = 20
# 切记补上开头的 [CLS], 句子分隔处的 [SEP], 句末 [SEP],如果有padding操作,则需补上相应长度的 [PAD]
# 格式为 [CLS] + 句子A + [SEP] + 句子B + [SEP] + [PAD] * N
token_A = config.tokenizer.tokenize(sentence_A)
token_B = config.tokenizer.tokenize(sentence_B)
len_A, len_B = len(token_A), len(token_B)
token = [CLS] + token_A + [SEP] + token_B + [SEP]
length = len(token)
token += [PAD] * (pad_size - length)
token_sensor = config.tokenizer.convert_tokens_to_ids(token)  # 用 tokenizer 转化,传递给 input_ids
segments_tensor = [0] * (len_A + 2) + [1] * (len_B + 1) + [0] * (pad_size - length)  # 句子对任务需要设置此tensor, 传递给 token_type_ids
mask_tensor = [1] * length + [0] * (pad_size - length) # [PAD] 处全置0,其余位置置1,传递给 attention_mask
# 将以上3个 tensor 作为输入传递给 BERT 模型
_, pooled = self.bert(input_ids=tokens_tensor, token_type_ids=segments_tensor, attention_mask=mask_tensor, output_all_encoded_layers=False)

以上就是使用 BERT 模型时,需要准备的输入格式,例子里3个 tensor 都是手动生成(除 input_ids外),实际上利用 hugging face 提供的 transformers,可以更方便的获得这3个 tensor,不过我认为理解这3个 tensor 是有必要的。

  • 在不同的下游任务中使用 BERT。上文中讨论了使用 BERT 时,模型输入的构造方式,实际上在针对不同的下游任务时,网络结构和输出端也要进行调整。具体如下图所示
    总结:首先需要明确下游任务类型,然后根据类型,选择合适的网络结构,输入端按要求生成对应的3个 tensor,输出端选择相应的结构,如线性分类器或 DIY 结构,就可以在指定数据集上进行 fine-tuning 了。

BERT 模型文本分类

    在 PyTorch 中使用 BERT,可以利用 Hugging Face 提供的 transformers,提供了很多 BERT 预训练模型,包括一些针对特定下游任务的模型,如 bertForSequenceClassification,可以避免重复造轮子。

# 使用 transformers 提供的序列分类模型 BertForSequenceClassification
from transformers import BertForSequenceClassification

PRETRAINED_MODEL_NAME = "bert-base-chinese"
NUM_LABELS = 3

model = BertForSequenceClassification.from_pretrained(
    PRETRAINED_MODEL_NAME, num_labels=NUM_LABELS)
# 在 BertModel 基础上,自己添加 Dropout 层和 线性分类层,实际效果等价于 transformers 提供的 BertForSequenceClassification
class Model(nn.Module):

    def __init__(self, config):
        super(Model, self).__init__()
        self.bert = BertModel.from_pretrained(config.bert_path)
        for param in self.bert.parameters():
            param.requires_grad = True
        self.dropout = nn.Dropout(0.3)
        self.fc = nn.Linear(config.hidden_size, config.num_classes)

    def forward(self, x):
        context = x[0]  # 输入的句子
        mask = x[2]  # 对padding部分进行mask
        seg_ids = x[3]  # 句子间的分隔情况
        _, pooled = self.bert(input_ids=context, token_type_ids=seg_ids,  attention_mask=mask, output_all_encoded_layers=False)
        out = self.dropout(pooled)
        out = self.fc(out)
        return out
 
 model = Model(config)

    自己尝试了一个 单句子分类模型,在 SST-2 数据集和 THUCNews 数据集上进行了验证。参考了Github上的项目 使用Bert,ERNIE,进行中文文本分类,其中包含了数据集封装和模型定义。

BERT 模型处理 BOOLQ 数据集

    BoolQ 数据集中,每条训练数据是一个四元组 (question, title, answer, passage),question 是一个复杂且非事实性的 query,要求基于 passage 给出 YES/NO 的回答,在 BoolQ: Exploring the Surprising Difficulty of Natural Yes/No Questions 中提出。文中给出了一系列基于迁移学习的 baseline,其中效果最好的方式是利用 BERT Large 模型在 MultiNLI 数据集预训练后,再在 BoolQ 数据集上 fine-tuning,在 devset 上准确率为 76.9%。
    我的做法是将该任务看作一个 Sequence Classification 任务,将 passage 与 question 拼接起来,作为文本输入,answer作为标签,构建一个二分类模型。

bert pytorch 多语言 pytorch加载bert预训练模型_python_02

直接在 train set 数据集上进行 fine-tuning,运行 3 个 epoch 后,在 dev set 上的准确率为71.71%,与论文中描述的

We find training models on our train set alone to be relatively ineffective. Our best model reaches 69.6% accuracy, only 8% better than the majority baseline.

情况吻合。可以预见,如果先在 MultiNLI 数据集上进行预训练,再迁移到 BoolQ 数据集上,效果大概率会有改善。但我个人对这套方法持怀疑态度,这类模型把 BoolQ 问题简化为了一个类似文本蕴含的二分类问题,而真正的推理过程可能需要更加复杂的网络结构和创新,我认为这是解决文本推理问题的关键。而当前的模型更多的是利用了 BERT 或 XLNET 等网络强大的表征能力,捕捉到了数据集中本身存在的一些“虚假的统计学线索 (Spurious Statistical Cues) ”,而并不是掌握了推理本身。
    另外在训练时,遇到了 dev set 上 loss 和 accuracy 同时上升的情况,stackoverflowgithub 上都有一些讨论,常见的观点有:模型已经出现过拟合了;模型的正则化正在起作用(对抗过拟合);dev set 过小导致的震荡;模型变得极度自信,在准确率上升的同时,某些样例却错的离谱而拉高了 loss,无法看出模型是否过拟合;或者干脆认为 loss 的实现有 BUG(我不太认同这个解释)。不过目前似乎没有一个公认的解释?

总结

    记录反思了下自己初次使用 BERT 的经历教训。关键是要理解 BERT 模型的输入输出格式,确定自己任务的类型并选择相应的网络结构,封装好数据后,就可以愉快的 fine-tuning 和迁移啦。不过现在还只了解些皮毛,或者压根连皮毛也不算,更深层和细节的内容仍然有待学习和研究,才能更灵活的使用这些工具。强烈推荐在了解 BERT 的基础用法后,使用 Hugging Face 的 transformers 库 :)