NLP/Bert 源码解读 1

NLP/Bert 源码解读 1

上一篇文章中,我用图解详细讲述了 Bert 的组成部分和内部原理。

今天这篇文章,我们来看 Bert 的源码。下面使用的 Bert 源码,来自于 Hugging Face 的 transformers。这个项目一开始的名字是:pytorch-pretrained-bert,只包含 Bert。

后来加入了 GPT-2,RoBERTa,XLM,DistilBert,XLNet,T5,CTRL 等模型,改名为 transformers。你可以点击 model-architectures 来查看所有的模型。

transformers 的代码实现包括 PyTorch 和 Tensorflow,我这里只讲其中的 PyTorch 的源码。

你可以使用 pip install transformers 来安装这个库。

设计理念

为了让你对这个库有一个整体的认识,我先从顶层设计,来概览一下整个库包括的模块。

首先,这个库屏蔽了很多底层实现的细节,从上层使用来讲,你只需要知道 3 个大类:ConfigurationModel,以及 Tokenizer。这 3 个类都都一个方法from_pretrained(),这个方法可以加载预训练好的模型结构、权重参数以及词汇表。

  • Model 是指已经预训练好的模型,包括超过 30 个模型。
  • Configuration 用于加载模型的权重,设置模型的一些超参数,如网络层数,hidden_state 向量的长度,多头注意力的数量等。
  • Tokenizer 存储了每个模型的词汇表,并且对文本进行预处理和编码,如把文本转换为数字,对文本进行 padding,添加 mask 等操作。

这个库提供了非常多预训练好的模型,调用from_pretrained()方法加载模型,调用save_pretrained() 保存模型,这两个函数说明如下:

  • from_pretrained()
    • 传入名字,加载预训练好的模型(如果本地没有对应的模型,那么自动下载模型)。
    • 传入路径,加载自己训练的模型。
  • save_pretrained() 保存自己训练的模型。

除此之外,还提供了 2 个更顶层的 API pipeline()Trainer(),将上面 3 个类也封装起来了。

下面来看看如何使用 pipeline

使用 pipeline

pipeline提供了处理 8 种 NLP 任务的能力。

  • 情感分析:判断文本的情感是积极的,还是消极的
  • 文本生成(英语):提供一个文本开头,模型会生成接下来的文字
  • 名称实体识别:在一个句子中,标记每个单词表示的实体(人、地点等)
  • 问答系统:为模型提供一些文本和一个问题,从上文本提取答案。
  • 完形填空:给定一个句子,有些单词是空的(用 [MASK] 表示),填充空白的地方
  • 生成摘要:给定一段长文本,生成摘要
  • 翻译:把一种语言翻译为另一种语言
  • 特征提取:返回一段文本的向量表示

下面的例子是情感分析任务,你可以通过 task summary 查看更多的任务例子:

1
2
from transformers import pipeline
classifier = pipeline('sentiment-analysis')

当我们第一次使用sentiment-analysis,程序会自动下载预训练好的模型和 tokenizer,然后我们就可以调用classifier来进行情感分类。

1
2
classifier('We are very happy to show you the 🤗 Transformers library.')
# [{'label': 'POSITIVE', 'score': 0.9997795224189758}]

上面加载的模型默认是 distilbert-base-uncased-finetuned-sst-2-english,是 Bert 模型在文本分类任务上经过微调后得到的。如果你想加载其他模型,你可以使用model参数。例如下面代码加载了nlptown/bert-base-multilingual-uncased-sentiment,这个模型是在多语言数据集上经过训练的,支持英语、法语、荷兰语、德语、意大利语以及西班牙语。

1
classifier = pipeline('sentiment-analysis', model="nlptown/bert-base-multilingual-uncased-sentiment")

关于所有支持的模型,你可以通过 model hub 查看。

注意,有些预训练好的模型,对特定任务进行了微调。如果你加载的模型没有对特定任务进行微调,那么只会加载 Transformer 里面基本的网络层,而不会加载额外的 head 层(一般用于最终的分类输出),而 head 层的参数会随机初始化,这会导致每次输出的结果都不一样。

例如 bert-base-uncased 模型,这个模型来自于 Bert 的原始论文 BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding,没有对文本分类做微调,加载这个模型来做情感分类,会导致输出结果是随机的。

1
classifier = pipeline('sentiment-analysis', model="bert-base-uncased")

你可以直接使用 AutoModelForSequenceClassificationAutoTokenizerfrom_pretrained() 方法,来加载你想要的模型,再传入 pipeline

1
2
3
4
5
6
from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_name = "nlptown/bert-base-multilingual-uncased-sentiment"
model = AutoModelForSequenceClassification.(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
pipe = pipeline('sentiment-analysis', model=model, tokenizer=tokenizer)

使用 Model、Tokenizer、Configuration

我们在上面说了,pipeline其实就是封装了ModelTokenizer 以及 Configuration

现在我们不使用 pipeline,而是使用 ModelTokenizer 以及 Configuration来构建一个模型。

首先,还是加载ModelTokenizer

1
2
3
4
from transformers import AutoTokenizer, AutoModelForSequenceClassification
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
pt_model = AutoModelForSequenceClassification.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

上面的代码中,使用了AutoModelForSequenceClassificationAutoTokenizer,这两个类会根据模型名称,自动加载对应的实现类。如果你知道自己使用的模型,可以使用具体的类名。例如你要使用 Bert,那么你可以使用 BertForSequenceClassificationBertTokenizer,如下所示:

1
2
3
4
from transformers import BertTokenizer, BertForSequenceClassification
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
pt_model = BertForSequenceClassification.from_pretrained(model_name)
tokenizer = BertTokenizer.from_pretrained(model_name)

Tokenizer 的作用

上面提到,Tokenizer 的作用是预处理文本数据,主要包括 2 部分:

  • 把文本划分为词。每一个模型的划分方法都不一样,都有自己对应的的 Tokenzier,因此,我们需要通过from_pretrained() 方法加载对应模型的 Tokenizer。你可以通过 tokenizer_summary 查看所有的 Tokenizer
  • 把词转换为数字,这个步骤需要词汇表。每个训练好的模型所使用的数据集都不一样,词汇表也不一样。因此,我们需要通过from_pretrained() 方法加载对应模型的 Tokenizer,才能把文本转换为正确的数字,构建张量,输入到模型。

Tokenizer 初始化完成之后,调用如下代码把文本转换为张量,返回的inputs 是一个 dict,包括 input_idsattention_mask

  • input_ids:表示文本的张量
  • attention_mask:计算 attention 时使用的张量,默认为 1。如果使用了 padding,那么补齐的位置对应的 attention_mask 为 0
1
2
3
inputs = tokenizer("We are very happy to show you the 🤗 Transformers library.")
print(inputs)
# {'input_ids': [101, 2057, 2024, 2200, 3407, 2000, 2265, 2017, 1996, 100, 19081, 3075, 1012, 102], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

你可以传进一个文本 list,参数padding=True,表示对齐长度;return_tensors="pt"表示返回 PyTorch 的张量(return_tensors="tf"表示返回 Tensorflow 的张量)。

1
2
3
4
5
6
7
8
9
10
11
pt_batch = tokenizer(
["We are very happy to show you the 🤗 Transformers library.", "We hope you don't hate it."],
padding=True,
return_tensors="pt"
)

for key, value in pt_batch.items():
print(f"{key}: {value.numpy().tolist()}")

# input_ids: [[101, 2057, 2024, 2200, 3407, 2000, 2265, 2017, 1996, 100, 19081, 3075, 1012, 102], [101, 2057, 3246, 2017, 2123, 1005, 1056, 5223, 2009, 1012, 102, 0, 0, 0]]
# attention_mask: [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]]

然后把数据输入到模型(注意需要用**解包参数),关于 Python 中 ***的作用,请查看 这篇文章

1
2
3
pt_outputs = pt_model(**pt_batch)
print(pt_outputs)
# (tensor([[-4.0833, 4.3364], [ 0.0818, -0.0418]], grad_fn=<AddmmBackward>),)

返回的结果是一个 tuple,表示最后一个 Softmax 前一层的的输出。

你可以调用 Softmax 来获得最终的输出,并和真实的标签计算 loss。

1
2
3
4
import torch.nn.functional as F
pt_predictions = F.softmax(pt_outputs[0], dim=-1)
print(pt_predictions)
# tensor([[2.2043e-04, 9.9978e-01], [5.3086e-01, 4.6914e-01]], grad_fn=<SoftmaxBackward>)

你也可以把标签转入模型里,那么返回的 tuple 会包含 loss 和模型最后一层的输出。

1
2
import torch
pt_outputs = pt_model(**pt_batch, labels = torch.tensor([1, 0]))

你也可以在自己的数据集上训练(微调)这个网络。

当你微调完模型后,你可以使用save_pretrained()方法保存模型和 tokenizer。其中save_directory是保存的路径。

1
2
tokenizer.save_pretrained(save_directory)
model.save_pretrained(save_directory)

然后你可以使用from_pretrained()方法加载保存的模型。

1
2
tokenizer = AutoTokenizer.from_pretrained(save_directory)
model = TFAutoModel.from_pretrained(save_directory, from_pt=True)

你也可以让模型返回 hidden states 和 attention weights。

1
2
pt_outputs = pt_model(**pt_batch, output_hidden_states=True, output_attentions=True)
all_hidden_states, all_attentions = pt_outputs[-2:]

Configuration 的作用

如果你想模型的一些超参数(如自定义 hidden state 向量的长度、dropout rate、多头注意力的数量等),但是又需要利用训练好的模型的权重参数,那么就可以使用 Configuration。

代码如下所示,加载了distilbert-base-uncased模型,通过DistilBertConfig定义了一些超参数。

1
2
3
4
5
6
7
8
from transformers import DistilBertConfig, DistilBertTokenizer, DistilBertForSequenceClassification
# 这里定义:
# 多头注意力 n_heads=8
# 输入的 dim=512
# 隐层向量长度 hidden_dim=4*512
config = DistilBertConfig(n_heads=8, dim=512, hidden_dim=4*512)
tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')
model = DistilBertForSequenceClassification(config)

如果你想修改类别的数量,可以在 from_pretrained() 函数中使用 num_labels 修改类别数量。代码如下所示:

1
2
3
4
5
from transformers import DistilBertConfig, DistilBertTokenizer, DistilBertForSequenceClassification
model_name = "distilbert-base-uncased"
# 在 from_pretrained() 函数中使用 num_labels 修改类别数量
model = DistilBertForSequenceClassification.from_pretrained(model_name, num_labels=10)
tokenizer = DistilBertTokenizer.from_pretrained(model_name)

总结

虽然 pipeline 的用法非常简洁,只需要最少的代码就可以调用模型,但是也缺少了模型的定制性。

一般情况下,我们使用比较多的还是 Model、Tokenizer 以及 Configuration 的组合。这里总结一下它们 3 者的功能。

  • Model 可以加载已经训练好的模型,包括超过 30 个模型。
  • Configuration 用于设置模型的一些超参数,如网络层数,hidden_state 向量的长度,多头注意力的数量、dropout rate 等。
  • Tokenizer 存储了每个模型的词汇表,并且对文本进行预处理和编码,如把文本转换为数字,对文本进行 padding,添加 mask 等操作。

其中,Model 和 Tokenizer 是必须的,而 Configuration 只有在你需要修改一些超参数的时候才会用到。


在下一篇文章,我会讲解在 transformers 中,提供了哪些 Bert 有关的模型。


如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。

我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学


评论