NLP/Bert 源码解读 2

NLP/Bert 源码解读 2

在上一篇文章中 ,我们简单 了解了 transformers 的设计,从宏观上对整个结构有了了解。

今天,我们继续深入了解一下,在 Hugging Face 的 transformers 中,和 Bert 有关的类,都有哪些,

Bert 的组成

首先,从整体来看,Bert 的组成包括如下几个部分:

  • 基本模型和配置
    • BertModel
    • BertConfig
  • Tokenizer
    • BertTokenizer
    • BasicTokenizer
    • WordpieceTokenizer
    • BertTokenizerFast
  • 预训练模型
    • BertForPreTraining
    • BertForMaskedLM
    • BertForNextSentencePrediction
  • 针对特定任务进行 fine-tune 的模型,在 BertModel 的基础上添加了针对特定任务的网络层
    • BertForQuestionAnswering:问答系统
    • BertForTokenClassification:对每个 token 做分类
    • BertForMultipleChoice:对本文做多选任务
    • BertForSequenceClassification:文本分类

而关于训练好的模型的选择,常用的有如下这些(下面这些模型,没有在特定任务上进行 fine-tune):

  • bert-base-chinese:基于中文
  • bert-base-uncased:基于英语,不区分大小写
  • bert-base-cased:基于英语,区分大小写
  • bert-base-german-cased:基于德语,区分大小写
  • bert-base-multilingual-uncased:多语言,不区分大小写
  • bert-base-multilingual-cased:多语言,区分大小写
  • bert-large-cased:基于英语,更加大型的 Bert,区分大小写
  • bert-large-uncased:基于英语,更加大型的 Bert,不区分大小写

其中用的较多的是:bert-base-uncased

更多模型的选择,你可以查看 https://huggingface.co/

下面从基本模型和配置开始讲起。

基本模型和配置

BertModel

BertModel 是最基本的 Bert 模型,是很多模型的重要组件,由于代码较多,我们会在下一篇文章详细讲解 BertModel 的代码。这里先简单介绍一下。

BertModel 是最基本的 Bert 模型,不包括全连接层,没有对特定任务进行 fine-tune,输出是 hidden-states。

BertModel 可以作为编码器(encoder);也可以作为解码器(decoder),作为解码器时,在 self-attention 层之间会增加一个 cross-attention 层。

你可以通过BertConfig的参数 is_decoder=True ,来指定 BertModel 作为解码器(decoder)。

  • 当作为编码器(encoder)时,输入参数至少有 1 个:input_idsinput_embeds
    • input_ids 是经过 tokenization 后的序列,形状是 (batch_size, sequence_length)
    • input_embeds 是从 tokenization 后的序列转换为词向量的矩阵,形状是 (batch_size, sequence_length, hidden_size)
  • 当作为解码器(decoder)时,输入是encoder_hidden_states,来自于编码器(encoder)的输出。

构造方法

构造函数如下:

1
2
3
4
5
6
7
8
9
def __init__(self, config):
super().__init__(config)
self.config = config

self.embeddings = BertEmbeddings(config)
self.encoder = BertEncoder(config)
self.pooler = BertPooler(config)

self.init_weights()

可以看到包含了 BertEmbeddingsBertEncoderBertPooler

forward() 方法

forward() 方法参数如下:

  • input_ids:输入序列,形状是 (batch_size, sequence_length)
  • attention_mask:表示编码器输入序列对应的 mask,避免在 padding 的 token 上计算 attention,形状是(batch_size, sequence_length)。0 表示屏蔽(mask)的 token,1 表示没有屏蔽(mask)的 token。
  • token_type_ids:序列对应的 token_type_ids,形状是(batch_size, sequence_length)
  • position_ids:表示每个序列中每个 token 的位置,形状是 (batch_size, sequence_length),每个元素的大小范围是:[0, config.max_position_embeddings - 1]。为什么需要表示位置的向量?因为在 transformers 中,是不考虑 token 顺序的(在 RNN 中,是考虑顺序的)。如果你想要提供句子中 token 的顺序信息,可以传入这个参数。
  • head_mask:形状是 (num_heads,) 或者 (num_layers, num_heads)。0 表示屏蔽(mask)的一组注意力(attention head),1 表示没有屏蔽(mask)的一组注意力(attention head)。
  • inputs_embeds:输入序列的矩阵,可以替代 input_ids。形状是 (batch_size, sequence_length, hidden_size)。将 input_ids的元素转换为向量后,输入到模型。当你想自己控制这个转换过程的时候,在模型外面转换好,使用inputs_embeds 传给模型。
  • encoder_hidden_states:当BertConfigis_decoder=True ,指定 BertModel 作为解码器,那么 encoder_hidden_states 就是输入的参数,表示解码器的输出。形状是 (batch_size, sequence_length, hidden_size)
  • output_attentions:是否返回 attention,默认为 None。
  • encoder_attention_mask:表示解吗器输入序列对应的 mask,和 attention_mask 的作用类似。不过attention_mask会作用在编码器中,而 encoder_attention_mask会作用在解码器中。

返回值有 4 个:

  • last_hidden_state:最后一层编码器或者解码器的输出。形状是 (batch_size, sequence_length, hidden_size)

  • pooler_output:第一个 token([CLS])对应的最后一层 hidden_state,经过一个线性层和 Tanh 激活函数,得到的结果。形状是 (batch_size, hidden_size)。其中,线性层的权重是 next sentence prediction (NSP)(分类) 任务中训练得到的。

    但根据论文,这个输出值不能很好地表示整个句子,通常更好的做法是:对一个序列中的所有 token 的最后一层 hidden_state 进行平均或者池化

  • hidden_states:当 output_hidden_states=True 或者 config.output_hidden_states=True 时,会返回这个值。是一个 tuple,包括两个元素,形状都是 (batch_size, sequence_length, hidden_size)

    • 第 1 个元素是 1 个 tuple,包含每层的 hidden_state。
    • 第 2 个元素是 最后 1 层的 hidden_state。
  • attentions:当 output_attentions=True 或者 config.output_attentions=True 时,会返回这个值。表示经过 Softmax 后的 attention,可以用于计算多头注意力的平均值。形状是 (batch_size, num_heads, sequence_length, sequence_length)

BertConfig

BertConfigBertModel 的配置类,用于配置一些超参数。继承自 PretrainedConfig

参数举例如下:

  • vocab_size:词汇表大小,默认为 30522
  • hidden_size:encoder 层和 pooler 层输出的向量维度,默认为 768
  • num_hidden_layers:encoder 的隐藏层数量,默认为 12
  • num_attention_heads:每个 encoder 中 attention 层的 head 数,默认为 12

默认加载的是类似于 bert-base-uncased 模型的结构。

关于更多参数,你可以查看https://huggingface.co/transformers/model_doc/bert.html?highlight=bertforsequenceclassification#bertconfig

Tokenizer

Tokenizer 的作用就是对文本进行预处理,存储了每个模型的词汇表,并且对文本进行预处理和编码,如把文本转换为数字,对文本进行 padding,添加 mask 等操作。

下面,我们会讲解和 Bert 有关的每个 Tokenizer。

如果你不想看下面关于每个 tokenizer 的内容,你可以先看 Tokenizer 的最佳实践。

Tokenizer 的最佳实践

首先,我们的序列经过 tokenization,一般需要得到 3 个参数:input_idstoken_type_idsattention_mask,这 3 个参数是模型必须的。

Tokenizer 提供了一种简便的用法,让我们可以一次性获得这 3 个变量。

例子如下:

1
2
3
4
5
6
7
from transformers import BertTokenizer, BertForSequenceClassification
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForSequenceClassification.from_pretrained("bert-base-uncased")
text="Hello, my dog is cute"
# 我们要得到 encoded_input,然后输入模型
encoded_input = tokenizer(text, max_length=100, add_special_tokens=True, truncation=True, padding=True, return_tensors="pt")
output = model(**encoded_input)

直接调用tokenizer(text),返回的 inputs 就是一个 dict,包括 input_idstoken_type_idsattention_mask。在下面的代码示例中,我们都是使用这种方法来进行 tokenization。

现在我们讲解 tokenizer(text) 的内部机制。

首先你需要了解 BertTokenizer 的继承关系,如下所示:

  • PreTrainedTokenizerBase
    • PreTrainedTokenizer
      • BertTokenizer

BertTokenizer 继承于 PreTrainedTokenizerPreTrainedTokenizer 继承于 PreTrainedTokenizerBase

我们调用tokenizer(text) ,会进入到 PreTrainedTokenizerBase__call__() 方法。

在这个方法里,会判断传入的数据是一个序列,还是一个 batch_size 的多个序列:

  • 如果是一个序列,会调用 encode_plus() 方法。
  • 如果是一个 batch_size 的多个序列,会调用 batch_encode_plus() 方法。

而这两个方法是在 PreTrainedTokenizer 中实现的。

换言之,调用tokenizer(),和调用 encode_plus() 方法(或者 batch_encode_plus() 方法), 是一样的。

encode_plus() 方法

当你的数据是一个序列时,使用这个方法。

下面来了解一下 encode_plus() 方法的部分参数。

  • text:第 1 个序列(第一句话)

  • text_pair:第 2 个序列(第二句话)

  • add_special_tokens:是否在序列前添加 [CLS],结尾添加 [SEP]

  • max_length:序列的最大长度

  • truncation_strategy:当传入 max_length 时,这个参数才生效,表示截断策略

    • 'longest_first':直接把两个序列拼接起来,把后面超出 max_length 的部分截断。适用于输入两个文本的情况,处理后两个文本的序列长度之和是max_length
    • 'only_first':仅截断第 1 个序列
    • 'only_second':仅截断第 2 个序列
    • 'do_not_truncate':不截断,如果输入序列大于 max_length, 则会报错
  • return_tensors:返回的张量类型

    • 'tf':返回 PyTorch 的 Tensor(需要装好 PyTorch)
    • 'pt':返回 Tensorflow 的 Tensor(需要装好 Tensorflow)

encode_plus() 方法默认会返回一个 dict,包括 input_idstoken_type_idsattention_mask

如果你只需要 input_ids,可以调用encode()方法,encode() 方法的代码很简洁,实际上就是用了 encode_plus() 方法,但是只选择返回 input_ids。代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  def encode(
self,
text: Union[TextInput, PreTokenizedInput, EncodedInput],
text_pair: Optional[Union[TextInput, PreTokenizedInput, EncodedInput]] = None,
add_special_tokens: bool = True,
padding: Union[bool, str] = False,
truncation: Union[bool, str] = False,
max_length: Optional[int] = None,
stride: int = 0,
return_tensors: Optional[Union[str, TensorType]] = None,
**kwargs
):
encoded_inputs = self.encode_plus(
text,
text_pair=text_pair,
add_special_tokens=add_special_tokens,
padding=padding,
truncation=truncation,
max_length=max_length,
stride=stride,
return_tensors=return_tensors,
**kwargs,
)
# 只选择返回 input_ids
return encoded_inputs["input_ids"]

batch_encode_plus() 方法

当你的数据是一个 batch_size 的形式时,使用这个方法。

batch_encode_plus() 方法的参数,大部分和 encode_plus() 方法一样,除了输入的序列。

  • batch_text_or_text_pairs:表示多个序列的 list
    • 如果只有一个 list,那么直接传入 list 即可。
    • 如果有 2 个 list:text, text_pair。第 1 个 list text表示第 1 个序列,第 2 个 list text_pair表示第 2 个序列。你可以传入list(zip(text, text_pair))
  • add_special_tokens:是否在序列前添加 [CLS],结尾添加 [SEP]
  • max_length:序列的最大长度
  • truncation_strategy:当传入 max_length 时,这个参数才生效,表示截断策略
    • 'longest_first':直接把两个序列拼接起来,把后面超出 max_length 的部分截断。适用于输入两个文本的情况,处理后两个文本的序列长度之和是max_length
    • 'only_first':仅截断第 1 个序列
    • 'only_second':仅截断第 2 个序列
    • 'do_not_truncate':不截断,如果输入序列大于 max_length, 则会报错
  • return_tensors:返回的张量类型
    • 'tf':返回 PyTorch 的 Tensor(需要装好 PyTorch)
    • 'pt':返回 Tensorflow 的 Tensor(需要装好 Tensorflow)

小结

既然,我只需要调用tokenizer() 或者 batch_encode_plus() 方法,就能够得到想要的输出。那么还有必要了解下面关于其他 tokenizer 的内容吗?

还是有必要的。

因为库中提供的 tokenizer 的方法,不一定能够满足你的需要。如果这些定义好的方法,不能满足你的文本预处理需求,那么你就需要自定义 tokenizer。而自定义 tokenizer,就需要了解 tokenizer 的一些原理机制。

在后面的文章中,我会提供一个实战案例,使用自定义 tokenizer,来完成文本预处理。

BertTokenizer

BertTokenizer 是针对 Bert 模型,基于 WordPiece 的 tokenizer,继承自 PreTrainedTokenizer

参数

参数如下:

  • vocab_file:包含词汇表的文件路径
  • do_lower_case:是否转换为小写,默认为 True
  • do_basic_tokenize:在使用 WordpieceTokenizer之前,是否先使用 BasicTokenizer,默认为 True
  • never_split:当 do_basic_tokenize=True 时才生效。一个列表,列表里面出现的词不会进行 tokenization
  • tokenize_chinese_chars:是否对中文的字前后添加空格,默认为 True。当对日文进行 tokenization 时,这个值要设置为 False

关于更多参数,你可以查看 https://huggingface.co/transformers/model_doc/bert.html?highlight=bertforsequenceclassification#berttokenizer

方法

BertTokenizer 内部,有3 个比较重要的方法:

  • _tokenize()
  • create_token_type_ids_from_sequences()
  • build_inputs_with_special_tokens()

最重要的是 _tokenize()方法,在_tokenize()方法里调用了 BasicTokenizerWordpieceTokenizer,下面会详细讲解 BasicTokenizerWordpieceTokenizer

_tokenize()

_tokenize() 方法的流程如下:

  • 如果do_basic_tokenize 为 True,那么先使用 BasicTokenizer,得到的词之后。
    • 如果词在 never_split里,那么就不使用WordpieceTokenizer
    • 如果词不在 never_split里,再使用 WordpieceTokenizer 分割。
  • 如果do_basic_tokenize 为 False,只使用 WordpieceTokenizer

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def _tokenize(self, text):
split_tokens = []
# 如果do_basic_tokenize 为 True,那么先使用 BasicTokenizer,再使用 WordpieceTokenizer
if self.do_basic_tokenize:
for token in self.basic_tokenizer.tokenize(text, never_split=self.all_special_tokens):

# If the token is part of the never_split set
if token in self.basic_tokenizer.never_split:
split_tokens.append(token)
else:
split_tokens += self.wordpiece_tokenizer.tokenize(token)
# 否则,只使用 WordpieceTokenizer
else:
split_tokens = self.wordpiece_tokenizer.tokenize(text)
return split_tokens

create_token_type_ids_from_sequences()

作用是创建句子的 mask 序列,用于 sequence-pair classification 任务,也称为 Next sentence prediction (NSP)

输入两个句子,输出句子的 token type ids。类似下面,0 对应位置的单词表示第一个句子,1 对应位置的单词表示第二个句子。

1
2
0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1
| first sequence | second sequence |

参数如下:

  • token_ids_0:第 1 个序列
  • token_ids_1:第 2 个序列

create_token_type_ids_from_sequences() 方法的流程如下:

  • 如果token_ids_1为空,那么输入是 1 个序列,输出是全为 0 的 token type ids向量(表示一个序列)。

  • 如果token_ids_1不为空,那么输入是 2 个序列,输出是两个序列的 token type ids向量。

代码如下:

1
2
3
4
5
6
7
8
def create_token_type_ids_from_sequences(
self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None
) -> List[int]:
sep = [self.sep_token_id]
cls = [self.cls_token_id]
if token_ids_1 is None:
return len(cls + token_ids_0 + sep) * [0]
return len(cls + token_ids_0 + sep) * [0] + len(token_ids_1 + sep) * [1]

build_inputs_with_special_tokens()

作用是:在序列开头添加 [CLS],在序列末尾添加 [SEP]

参数如下:

  • token_ids_0:第 1 个序列
  • token_ids_1:第 2 个序列

build_inputs_with_special_tokens()` 方法的流程如下:

  • 如果token_ids_1为空,那么输入是 1 个序列 X,输出是 [CLS] X [SEP]

  • 如果token_ids_1不为空,那么输入是 2 个序列 AB,输出是 [CLS] A [SEP] B [SEP]

代码如下:

1
2
3
4
5
6
7
def build_inputs_with_special_tokens(self, token_ids_0, token_ids_1=None):
output = [self.cls_token_id] + token_ids_0 + [self.sep_token_id]

if token_ids_1:
output += token_ids_1 + [self.sep_token_id]

return output

BertTokenizerFast

针对 Bert 模型,基于 WordPiece 的 tokenizer,继承自 PreTrainedTokenizer

和类似,不同之处是:没有 _tokenize() 方法,只有 create_token_type_ids_from_sequences() 方法和 build_inputs_with_special_tokens() 方法。

下面来讲解 BasicTokenizerWordpieceTokenizer

BasicTokenizer

BasicTokenizer 的作用是:根据空格分割词汇和符号。

参数

  • do_lower_case:是否将字母转换为小写,默认为 True
  • never_split:一个列表,列表里面出现的词不会进行 tokenization
  • tokenize_chinese_chars:是否对中文的字前后添加空格,默认为 True

方法

最重要的是 tokenize() 方法,流程如下:

  • tokenize_chinese_chars=True,会首先在中文的每个字前后增加空格。
  • 调用whitespace_tokenize(),通过空格分割单词、符号,以及中文的字
  • 然后调用_run_strip_accents(),从单词中去除重音字母
  • 最后再调用whitespace_tokenize() 分割单词、符号,以及中文的字,返回结果

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  def tokenize(self, text, never_split=None):
""" Basic Tokenization of a piece of text.
Split on "white spaces" only, for sub-word tokenization, see WordPieceTokenizer.

Args:
**never_split**: (`optional`) list of str
Kept for backward compatibility purposes.
Now implemented directly at the base class level (see :func:`PreTrainedTokenizer.tokenize`)
List of token not to split.
"""
# union() returns a new set by concatenating the two sets.
never_split = self.never_split.union(set(never_split)) if never_split else self.never_split

# This was added on November 1st, 2018 for the multilingual and Chinese
# models. This is also applied to the English models now, but it doesn't
# matter since the English models were not trained on any Chinese data
# and generally don't have any Chinese data in them (there are Chinese
# characters in the vocabulary because Wikipedia does have some Chinese
# words in the English Wikipedia.).
# 当`tokenize_chinese_chars=True`,会首先在中文的每个字前后增加空格。
if self.tokenize_chinese_chars:
text = self._tokenize_chinese_chars(text)
# 调用`whitespace_tokenize()`,通过空格分割单词和中文的字
orig_tokens = whitespace_tokenize(text)
split_tokens = []
for token in orig_tokens:
if self.do_lower_case and token not in never_split:
# 转换为小写
token = token.lower()
# 调用`_run_strip_accents()`,从单词中去除重音字母
token = self._run_strip_accents(token)
split_tokens.extend(self._run_split_on_punc(token, never_split))
# 最后再调用`whitespace_tokenize()`分割单词和中文的字,返回结果
output_tokens = whitespace_tokenize(" ".join(split_tokens))
return output_tokens

WordpieceTokenizer

WordpieceTokenizer 的作用是:对每个单词进行 WordPiece tokenization。

WordPiece 的字面理解,就是把 word 拆成 piece(一片),主要使用双字节编码(BPE,Byte-Pair Encoding)的方式实现。

BPE 的过程可以理解为把一个单词拆分为更小的粒度,使得我们的词汇表更加精简,并且寓意更加清晰。

比如 lovedlovingloves 这三个单词。本身的语义都是“爱”的意思,只是后缀不一样,如果我们以单词为单位,那它们就是不一样的词。而在英语中,意思相近不同后缀的词非常的多,就会使得词表变的很大,训练速度变慢,训练的效果也不是太好。

BPE 算法通过训练,能够把一个单词拆分成更小的单元,称为 token。

例如:

  • lovedlovingloves 这三个单词拆分为 ["lov", "##ed", "##ing", "##es"],表示 lovedinges
  • unaffable 拆分为 ["un", "##aff", "##able"],表示 unaffable

这样可以把词的本身的意思和时态分开,有效的减少了词汇表的数量。

参数

  • vocab:WordPiece tokenization 的词汇表
  • unk_token:当一个字没有出现在词汇表中,用这个 unk_token 表示,比如用 "[UNK]" 表示未知字符
  • max_input_chars_per_word:每个单词最大的字符数,默认100。如果单词的字母数量超过这个长度,就不进行 WordPiece tokenization,而是用 unk_token 表示这个单词

方法

tokenize() 方法流程如下:

  • 首先判断每个单词的长度,如果单词的字母数量超过 max_input_chars_per_word,就不进行 WordPiece tokenization,而是用 unk_token 表示这个单词
  • 然后使用贪婪最长匹配优先算法,找出每一个 piece
  • 最后把 piece 添加到输出的列表中

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def tokenize(self, text):
"""Tokenizes a piece of text into its word pieces.

This uses a greedy longest-match-first algorithm to perform tokenization
using the given vocabulary.

For example:
input = "unaffable"
output = ["un", "##aff", "##able"]

Args:
text: A single token or whitespace separated tokens. This should have
already been passed through `BasicTokenizer`.

Returns:
A list of wordpiece tokens.
"""

output_tokens = []
for token in whitespace_tokenize(text):
chars = list(token)
# 如果单词的字母数量超过 max_input_chars_per_word,就不进行 WordPiece tokenization,而是用 unk_token 表示这个单词
if len(chars) > self.max_input_chars_per_word:
output_tokens.append(self.unk_token)
continue

is_bad = False
start = 0
sub_tokens = []
while start < len(chars):
end = len(chars)
cur_substr = None、
# 使用贪婪最长匹配优先算法 找出每一个 piece
while start < end:
substr = "".join(chars[start:end])
if start > 0:
substr = "##" + substr
if substr in self.vocab:
# 如果这个piece 在词汇表里,那么就赋值给 cur_substr
cur_substr = substr
break
end -= 1
if cur_substr is None:
is_bad = True
break
sub_tokens.append(cur_substr)
start = end

if is_bad:
output_tokens.append(self.unk_token)
else:
# 把 cur_substr 添加到输出的列表中
output_tokens.extend(sub_tokens)
return output_tokens

针对特定任务进行 fine-tune 的模型

下面这 4 个模型,都是在 BertModel的基础上,添加了线性层,进行分类或者回归。

你可以认为这 4 个模型,在 BertModel的基础上套了一层壳,以适应特定的任务。

BertForSequenceClassification

BertForSequenceClassification 是一个用于文本分类任务的模型。

构造方法

构造函数如下:

1
2
3
4
5
6
7
8
9
10
11
  def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels
# 定义 BertModel 实例
self.bert = BertModel(config)
# 添加 Dropout 层
self.dropout = nn.Dropout(config.hidden_dropout_prob)
# 添加 Linear 层
self.classifier = nn.Linear(config.hidden_size, config.num_labels)

self.init_weights()

可以看到,BertForSequenceClassification 是在 BertModel 的基础上,添加了一个 Dropout 层和线性层。

forward() 方法

forward() 方法的参数,在 BertModel的基础上,添加了一个 labels 参数,表示标签。

  • labels:标签值,形状是 (batch_size,)。如果 config.num_labels == 1,那么会计算回归损失(均方误差);如果 config.num_labels > 1,那么会计算分类损失(交叉熵)。

返回值有 4 个。其中,只有 logits 是确定会返回的,其他 3 个不一定有返回。

  • loss:损失函数的值。只有传入 labels参数时,才会计算 loss 返回。如果 config.num_labels == 1,那么会计算回归损失(均方误差);如果 config.num_labels > 1,那么会计算分类损失(交叉熵)。
  • logits:返回 Softmax 前一层的输出,形状是 (batch_size, config.num_labels)
  • hidden_states:当 output_hidden_states=True 或者 config.output_hidden_states=True 时,会返回这个值。是一个 tuple,包括两个元素,形状都是 (batch_size, sequence_length, hidden_size)
    • 第 1 个元素是 1 个 tuple,包含每层的 hidden_state。
    • 第 2 个元素是 最后 1 层的 hidden_state。
  • attentions:当 output_attentions=True 或者 config.output_attentions=True 时,会返回这个值。表示经过 Softmax 后的 attention,可以用于计算多头注意力的平均值。形状是 (batch_size, num_heads, sequence_length, sequence_length)

forward() 方法的流程如下:

  • 首先调用 BertModel,得到输出。
  • 取出其中的 pooled_output,表示第一个 [CLS] 对应的 hidden state,经过 Dropout 层、线性层,得到 logits
  • 如果有 label,那么计算 loss
  • 返回 (loss), logits, (hidden_states), (attentions)。其中,只有 logits 是确定会返回的,其他 3 个不一定有返回。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
  def forward(
self,
input_ids=None,
attention_mask=None,
token_type_ids=None,
position_ids=None,
head_mask=None,
inputs_embeds=None,
labels=None,
output_attentions=None,
output_hidden_states=None,
):

outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
)
# BertModel 的输出是:(last_hidden_state, pooler_output, hidden_states, attentions)
# 这里取出 pooler_output,也就是第一个 [CLS] 对应的 hidden state,形状是 (batch_size, hidden_size)
pooled_output = outputs[1]

pooled_output = self.dropout(pooled_output)
# 经过 线性层,得到 logits
logits = self.classifier(pooled_output)
# 把 logits, hidden_states, attentions 添加到 outputs 中
outputs = (logits,) + outputs[2:] # add hidden states and attention if they are here

if labels is not None:
# 如果只有 1 类,计算 MSE
if self.num_labels == 1:
# We are doing regression
loss_fct = MSELoss()
loss = loss_fct(logits.view(-1), labels.view(-1))
else:
# 如果超过 1 类,计算 CrossEntropyLoss
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
outputs = (loss,) + outputs

return outputs # (loss), logits, (hidden_states), (attentions)

例子

1
2
3
4
5
6
7
8
from transformers import BertTokenizer, BertForSequenceClassification
import torch
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForSequenceClassification.from_pretrained('bert-base-uncased')
inputs = tokenizer("Hello, my dog is cute", return_tensors="pt")
labels = torch.tensor([1]).unsqueeze(0) # Batch size 1
outputs = model(**inputs, labels=labels)
loss, logits = outputs[:2]

BertForTokenClassification

BertForTokenClassification 是一个用于对每个 token 进行分类的模型。

构造方法

它的构造函数,和 BertForSequenceClassification 一样,在 BertModel 的基础上,添加了一个 Dropout 层和线性层。

forward() 方法

forward() 方法的参数,在 BertModel的基础上,添加了一个 labels 参数,表示标签。

  • labels:表示每个 token 的标签,形状是 (batch_size, sequence_length)。注意这里 labels 的形状和 BertForSequenceClassification 的 labels 形状不一样。

forward() 方法的流程,和 BertForSequenceClassificationforward() 方法基本一样 ,不同的是:

  • BertForSequenceClassification 中,使用的是 outputs[1]的输出,也就是表示第一个 [CLS] 对应的 hidden state,形状是 (batch_size, hidden_size)

  • 而在 BertForTokenClassification 中,使用的是 outputs[0]的输出,也就是最后一层所有 token 的 hidden state。形状是 (batch_size, sequence_length, hidden_size)

另外,由于是 token 级别的分类,因此计算 loss 时,使用了 attention_mask 来跳过那些不需要计算的位置(一般是指 padding 的位置)。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  def forward(
self,
input_ids=None,
attention_mask=None,
token_type_ids=None,
position_ids=None,
head_mask=None,
inputs_embeds=None,
labels=None,
output_attentions=None,
output_hidden_states=None,
):

outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
)
# BertModel 的输出是:(last_hidden_state, pooler_output, hidden_states, attentions)
# 这里与 BertForSequenceClassification 不同。取出的是 last_hidden_state,最后一层编码器或者解码器的输出。形状是 (batch_size, sequence_length, hidden_size)
sequence_output = outputs[0]

sequence_output = self.dropout(sequence_output)
# 线性层
logits = self.classifier(sequence_output)

outputs = (logits,) + outputs[2:] # add hidden states and attention if they are here
if labels is not None:
# 定义损失函数
loss_fct = CrossEntropyLoss()
# Only keep active parts of the loss
if attention_mask is not None:
# 如果 mask 不为空,那么只计算 mask=1 的位置的损失,不计算那些 padding 的损失
# attention_mask: (batch_size, sequence_length)
# active_loss: (batch_size * sequence_length)
# 把 1 转换为 True, 0 转换为 False
active_loss = attention_mask.view(-1) == 1
# active_logits: (batch_size * sequence_length, num_labels)
active_logits = logits.view(-1, self.num_labels)
# 把 active_loss 为 1 的位置,替换为 label;
# 把 active_loss 为 0 的位置,替换为 loss_fct.ignore_index;
active_labels = torch.where(
active_loss, labels.view(-1), torch.tensor(loss_fct.ignore_index).type_as(labels)
)
loss = loss_fct(active_logits, active_labels)
else:
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
outputs = (loss,) + outputs

return outputs # (loss), scores, (hidden_states), (attentions)

例子

1
2
3
4
5
6
7
8
from transformers import BertTokenizer, BertForTokenClassification
import torch
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForTokenClassification.from_pretrained('bert-base-uncased')
inputs = tokenizer("Hello, my dog is cute", return_tensors="pt")
labels = torch.tensor([1] * inputs["input_ids"].size(1)).unsqueeze(0) # Batch size 1
outputs = model(**inputs, labels=labels)
loss, logits = outputs[:2]

BertForQuestionAnswering

BertForQuestionAnswering 是基于文本提取的问答系统。

输入是一段文本和问题,输出是 2 个整数,表示答案在文本中开始位置和结束位置。

构造方法

构造函数如下:

1
2
3
4
5
6
7
class BertForQuestionAnswering(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels

self.bert = BertModel(config)
self.qa_outputs = nn.Linear(config.hidden_size, config.num_labels)

BertModel 的基础上,添加了一个线性层对每个 token 做 2 分类,没有 Dropout 层。

forward() 方法

forward() 方法的参数,在 BertModel的基础上,添加了 2 个参数。

  • start_positions:答案在文本中的开始位置。如果位置超过文本长度范围,那么不计算 loss。
  • end_positions:答案在文本中的结束位置。如果位置超过文本长度范围,那么不计算 loss。

有 5 个返回值:

  • loss:损失函数的值。只有传入 start_positions参数和 end_positions 时,才会计算 loss 返回。
  • start_scores:答案在文本中开始位置对应的分数(没有经过 Softmax)。
  • end_scores:答案在文本中结束位置对应的分数(没有经过 Softmax)。
  • hidden_states:当 output_hidden_states=True 或者 config.output_hidden_states=True 时,会返回这个值。是一个 tuple,包括两个元素,形状都是 (batch_size, sequence_length, hidden_size)
    • 第 1 个元素是 1 个 tuple,包含每层的 hidden_state。
    • 第 2 个元素是 最后 1 层的 hidden_state。
  • attentions:当 output_attentions=True 或者 config.output_attentions=True 时,会返回这个值。表示经过 Softmax 后的 attention,可以用于计算多头注意力的平均值。形状是 (batch_size, num_heads, sequence_length, sequence_length)

forward() 方法的流程是:

  • 首先调用 BertModel,得到输出。
  • 取出其中的 last_hidden_state,也就是最后一层所有 token 的 hidden state。
  • last_hidden_state,每个 token 做 2 分类,得到 logits,形状为 (batch_size, sequence_length, 2)
  • 使用 logits.split(1, dim=-1),划分为start_logitsend_logits,形状均为 (batch_size, sequence_length, 1)start_logits 表示开始位置,end_logits表示结束位置。
  • 如果 start_positionsend_positions 都不为空,那么分别计算start_logitsend_logits 的 loss,取平均返回。
  • 返回 (loss), start_logits, end_logits, (hidden_states), (attentions)。其中,只有 start_logitsend_logits是确定会返回的,其他 3 个不一定有返回。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
  def forward(
self,
input_ids=None,
attention_mask=None,
token_type_ids=None,
position_ids=None,
head_mask=None,
inputs_embeds=None,
start_positions=None,
end_positions=None,
output_attentions=None,
output_hidden_states=None,
):

outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
)

# BertModel 的输出是:(last_hidden_state, pooler_output, hidden_states, attentions)
# 这里取出的是 last_hidden_state,最后一层编码器或者解码器的输出。形状是 (batch_size, sequence_length, hidden_size)
sequence_output = outputs[0]

logits = self.qa_outputs(sequence_output)
# logits: (batch_size, sequence_length, 2)
# 根据最后一维分割
# start_logits: (batch_size, sequence_length, 1)
# end_logits: (batch_size, sequence_length, 1)
start_logits, end_logits = logits.split(1, dim=-1)
start_logits = start_logits.squeeze(-1)
end_logits = end_logits.squeeze(-1)

outputs = (start_logits, end_logits,) + outputs[2:]
if start_positions is not None and end_positions is not None:
# If we are on multi-GPU, split add a dimension
if len(start_positions.size()) > 1:
start_positions = start_positions.squeeze(-1)
if len(end_positions.size()) > 1:
end_positions = end_positions.squeeze(-1)
# sometimes the start/end positions are outside our model inputs, we ignore these terms
# ignored_index = sequence_length
ignored_index = start_logits.size(1)
# 如果元素 < 0,则置为 0
# 如果元素 > ignored_index,则置为 ignored_index
start_positions.clamp_(0, ignored_index)
end_positions.clamp_(0, ignored_index)

loss_fct = CrossEntropyLoss(ignore_index=ignored_index)
# 计算 loss
start_loss = loss_fct(start_logits, start_positions)
end_loss = loss_fct(end_logits, end_positions)
total_loss = (start_loss + end_loss) / 2
outputs = (total_loss,) + outputs

return outputs # (loss), start_logits, end_logits, (hidden_states), (attentions)

例子

1
2
3
4
5
6
7
8
9
from transformers import BertTokenizer, BertForQuestionAnswering
import torch
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForQuestionAnswering.from_pretrained('bert-base-uncased')
inputs = tokenizer("Hello, my dog is cute", return_tensors="pt")
start_positions = torch.tensor([1])
end_positions = torch.tensor([3])
outputs = model(**inputs, start_positions=start_positions, end_positions=end_positions)
loss, start_logits, end_logits = outputs[:3]

评论