碎碎念
不知道哪年辈子订阅的书突然上架了,昨天就熬夜看了一遍,今天跑作者仓库看了一下代码。顺路参加一下金石计划。因为作者代码是两年前的了,有的已经跑不通了,所以改了改。
使用BERT预训练模型
第一节是如何使用预训练好的BERT模型,我们这里是直接使用huggingface🤗为我们提供的预训练模型。
Hugging Face 是一家人工智能公司,成立于2016年,主要致力于自然语言处理 (NLP) 技术的研究和开发。该公司的主要产品是开源软件库 Transformers,该库提供了一系列强大的预训练模型和工具,可以用于多种 NLP 任务,如文本分类、命名实体识别、问答等。
Transformers 库基于 PyTorch 和 TensorFlow 框架,提供了易于使用的 API,可以帮助开发人员快速地使用和微调预训练模型。该库还提供了许多有用的工具,如 Tokenizers、Datasets 和 Metrics,可以帮助用户更轻松地处理和分析文本数据。
除了 Transformers 库外,Hugging Face 还开发了一些其他的 NLP 工具和应用程序,如 Datasets、Tokenizers 和 Trainers 等,这些工具可以帮助开发人员更好地处理文本数据和构建自己的 NLP 模型。
代码
以Ann likes to eat chocolate
这个句子为例,假设需要提取句子中每个词的上下文嵌入。我们将数据集中的所有句子的标记长度设为10。
我们首先需要对句子进行标记,并将这些标记送入预训练的BERT模型,该模型将返回每个标记的嵌入。除了获得标记级(词级)的特征外,还可以获得句级的特征。
pip install transformers
使用pip安装Transformers库
from transformers import BertModel, BertTokenizer
import torch
导入必要的库模块
model = BertModel.from_pretrained('bert-base-uncased', output_hidden_states = True)
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
这是使用 Hugging Face 的 Transformers 库加载预训练的 BERT 模型和分词器的代码。
-
BertModel.from_pretrained()
方法加载了一个预训练的 BERT 模型,'bert-base-uncased'
是指加载的是 BERT 模型的基础版本(即不包含其他特定任务的微调)。output_hidden_states = True
则指定在使用该模型进行推理时,将返回所有的隐藏状态。 -
BertTokenizer.from_pretrained()
方法加载了一个与 BERT 模型相对应的分词器,'bert-base-uncased'
表示加载的是 BERT 模型的基础版本的分词器,该分词器将输入的文本转换为一系列对应的 token,以便 BERT 模型进行处理。这个分词器是用来对待处理文本进行划分和编码,使其能够与BERT模型进行输入。
sentence = 'Ann likes to eat chocolate'
tokens = tokenizer.tokenize(sentence)
tokens = ['[CLS]'] + tokens + ['[SEP]']
注意,下边这几段加特殊标签啊,attention mask啊都是可以同tokenizer完成的,这里实现一下仅仅是为了回顾一下BERT的工作流程,下一篇文章微调模型的时候就直接用tokenizer一步实现了。
这段代码使用了之前加载的 BERT 分词器,对输入的英文句子进行了分词处理。
-
使用了
tokenizer.tokenize()
方法对输入句子进行划分,将其转换为一个 token 列表 -
添加一些特殊的标记
[CLS]
和[SEP]
。-
[CLS]
标记是 BERT 模型中用于表示句子开始的特殊标记 -
[SEP]
标记是用于表示句子结束的特殊标记。
-
在该代码段中,首先使用 [CLS]
标记将输入句子的开头进行标记化,然后将分词处理后的结果添加到 token 列表中,最后再添加一个 [SEP]
标记来表示句子的结尾,以便 BERT 模型能够正确地处理整个句子。
这样处理后,tokens 就是一个包含了特殊标记和分词结果的 token 列表,可以直接用作输入 BERT 模型的参数。
tokens = tokens + ['[PAD]'] + ['[PAD]'] + ['[PAD]']
attention_mask = [1 if i!= '[PAD]' else 0 for i in tokens]
我们可以看到,标记列表的开头处有一个[CLS]
标记,其结尾处有一个[SEP]
标记,且标记长度为5。假设需要将标记长度设为10,那么需要在列表最后添加3个[PAD]
标记来满足长度要求,如下所示。
创建注意力掩码。如果标记不是[PAD]
,那么将注意力掩码值设置为1,否则我们将其设置为0。
token_ids = tokenizer.convert_tokens_to_ids(tokens)
token_ids = torch.tensor(token_ids).unsqueeze(0)
attention_mask = torch.tensor(attention_mask).unsqueeze(0)
将所有标记转换为它们的标记ID,将所有标记转换为它们的标记ID。
output = model(token_ids, attention_mask = attention_mask)
last_hidden_state = output.last_hidden_state
pooler_output = output.pooler_output
hidden_states = output.hidden_states
这里对人家原来的代码进行了更改,原来代码跑不通了。可能是因为随着版本更改BERT的返回值改了。
在定义模型时,我们设置了output_hidden_states = True
,以获得所有编码器层的嵌入。
-
last_hidden_state
包含从最后的编码器(编码器12)中获得的所有标记的特征。 -
pooler_output
表示来自最后的编码器的[CLS]标记的特征,它被一个线性激活函数和tanh激活函数进一步处理。 -
hidden_states
包含从所有编码器层获得的所有标记的特征。
last_hidden_state.shape
它仅有从最后的编码器(编码器12)中获得的所有标记的特征。
输出数组[1, 10, 768]
表示[batch_size, sequence_length, hidden_size]
,其表明批量大小为1,序列长度等于标记长度,即10。隐藏层的大小等于特征向量(嵌入向量)的大小,在BERT-base模型中,其大小为768。
每个标记的嵌入如下所示。
-
last_hidden_state[0][0]
给出了第1个标记[CLS]
的特征。 -
last_hidden_state[0][1]
给出了第2个标记Ann
的特征。 -
last_hidden_state[0][2]
给出了第3个标记likes
的特征
pooler_output.shape
下面来看pooler_output
,它包含来自最后的编码器的[CLS]
标记的特征,并将被线性激活函数和tanh激活函数进一步处理。
len(hidden_states)
输出数组[1, 768]
表示[batch_size, hidden_size]
。前面已知,[CLS]
标记持有该句子的总特征,因此,可以用pooler_output
作为Ann likes to eat chocolate
这个句子的特征。
hidden_states[0].shape
hidden_states
包含从所有编码器层获得的所有标记的特征。它是一个包含13个值的元组,含有所有编码器层(隐藏层)的特征,即从输入嵌入层h0h_0到最后的编码器层h12h_{12}
输出数组[1, 10, 768]
表示[batch_size, sequence_length, hidden_size]
。
有啥用?
通过这种方式,我们就可以获得所有编码器层的标记嵌入。
开头我们说:
我们首先需要对句子进行标记,并将这些标记送入预训练的BERT模型,该模型将返回每个标记的嵌入。除了获得标记级(词级)的特征外,还可以获得句级的特征。
我们认为[CLS]
捕获了句子级特征,而每个词的输出是捕获到标记级(词级)的特征。
所以我们可以拿[CLS]
的信息去做简单的分类任务,比如情感分析。
这是试用最后一层的嵌入,我们取之前层的嵌入可以在一些命名实体识别任务中试用。 举个栗子:
BERT的研究人员尝试了从不同的编码器层中提取嵌入。例如,对于命名实体识别任务,研究人员使用预训练的BERT模型来提取特征。他们没有只使用来自顶层编码器(最后的隐藏层)的嵌入作为特征,而是尝试使用来自其他编码器层(其他的隐藏层)的嵌入作为特征,所得到的F1分数,看👇图。
将最后4个编码器层(最后4个隐藏层)的嵌入连接起来可以得到最高的F1分数,即96.1。这说明可以使用其他编码器层的嵌入,而不只是提取顶层编码器(最后的隐藏层)的嵌入。
本文正在参加「金石计划」