TensorFlow 实战(六)(3)https://developer.aliyun.com/article/1522935
13.3 使用 Hugging Face 的 Transformers 进行问答
你的朋友计划启动一个使用 ML 来找出开放领域问题答案的初创公司。缺乏 ML 背景,他转向你询问是否可以使用 ML 实现这一点。知道问题回答是机器可学习的,只要有标记数据,你决定使用 BERT 变体创建一个问答原型并进行演示。你将使用 SQUAD v1 问答数据集,并在该数据集上训练 DistilBERT(BERT 的一个变体)。为此,你将使用 Hugging Face 的 transformers 库(huggingface.co/transformers/
)。Hugging Face 的 transformers 库提供了不同 Transformer 模型的实现和易于使用的 API 来训练/评估数据集上的模型。
BERT 旨在解决两种不同类型的任务:
- 任务只有一个文本序列作为输入
- 任务有两个文本序列(A 和 B)作为输入
垃圾邮件分类属于第一类。问题回答是一种具有两个输入序列的任务类型。在问题回答中,你有一个问题和一个内容(段落,句子等),其中可能包含问题的答案。然后训练一个模型来预测给定问题和内容的答案。让我们更好地了解一下过程。数据集中的每个记录将包含以下元素:
- 一个问题(文本序列)
- 一个内容(文本序列)
- 答案在内容中的起始索引(整数)
- 答案在内容中的结束索引(整数)
首先,我们需要将问题和内容结合起来,并添加几个特殊标记。在开头,我们需要添加一个[CLS]标记,然后添加一个[SEP]来分隔问题和内容,以及一个[SEP]来标记输入的结尾。此外,问题和内容将使用模型的分词器分解为标记(即子词)。对于具有
问题:狗对什么吠叫
答案:狗吠的是邮递员
如果我们将单个单词视为标记,输入将如下所示:
[CLS], What, did, the, dog, barked, at, [SEP], The, dog, barked, at, the, ➥ mailman, [SEP]
接下来,这些标记被转换为 ID 并馈送到 BERT 模型。BERT 模型的输出连接到两个下游分类层:一个层预测答案的起始索引,而另一个层预测答案的结束索引。这两个分类层各自有自己的权重和偏差。
BERT 模型将为输入序列中的每个标记输出一个隐藏表示。跨越整个内容范围的标记输出表示被馈送给下游模型。每个分类层然后预测每个标记作为答案的起始/结束标记的概率。这些层的权重在时间维度上共享。换句话说,相同的权重矩阵应用于每个输出表示以预测概率。
对于本节,我们将使用 Hugging Face 的 transformers 库。有关更多详细信息,请参阅侧边栏。
Hugging Face 的 transformers 库
Hugging Face 是一家专注于解决 NLP 问题的公司。该公司提供了用于训练 NLP 模型以及托管数据集并在公开可访问的存储库中训练模型的库。我们将使用 Hugging Face 提供的两个 Python 库:transformers 和 datasets。
在撰写本书时,transformers 库(huggingface.co/transformers/
)是最通用的 Python 库,提供了对许多已发布的 Transformer 模型(例如,BERT,XLNet,DistilBERT,Albert,RoBERT 等)以及社区发布的 NLP 模型(huggingface.co/models
)的即时访问,transformers 库支持 TensorFlow 和 PyTorch 两种深度学习框架。PyTorch(pytorch.org/
)是另一个类似于 TensorFlow 的深度学习框架,提供了全面的功能套件来实现和生产化深度学习模型。我在 transformers 库中看到的关键优势如下:
- 一个易于理解的 API,用于预训练和微调模型,这对所有模型都是一致的
- 能够下载各种 Transformer 模型的 TensorFlow 版本,并将其用作 Keras 模型
- 能够转换 TensorFlow 和 PyTorch 模型
- 强大的功能,如 Trainer(
mng.bz/Kx9j
),允许用户以非常灵活的方式创建和训练模型 - 高度活跃的社区-贡献模型和数据集
13.3.1 理解数据
如前所述,我们将使用 SQuAD v1 数据集(rajpurkar.github.io/SQuAD-explorer/
)。这是由斯坦福大学创建的问答数据集。您可以使用 Hugging Face 的 datasets 库轻松下载数据集,如下所示:
from datasets import load_dataset dataset = load_dataset("squad")
让我们打印数据集并查看可用的属性:
print(dataset)
这将返回
DatasetDict({ train: Dataset({ features: ['id', 'title', 'context', 'question', 'answers'], num_rows: 87599 }) validation: Dataset({ features: ['id', 'title', 'context', 'question', 'answers'], num_rows: 10570 }) })
训练示例有 87,599 个,验证样本有 10,570 个。我们将使用这些示例来创建训练/验证/测试集分割。我们只对特征部分的最后三列感兴趣(即上下文,问题和答案)。其中,上下文和问题只是简单的字符串,而答案是一个字典。让我们进一步分析答案。您可以打印一些答案,如下所示:
dataset["train"]["answers"][:5]
这给了
[{'answer_start': [515], 'text': ['Saint Bernadette Soubirous']}, {'answer_start': [188], 'text': ['a copper statue of Christ']}, {'answer_start': [279], 'text': ['the Main Building']}, {'answer_start': [381], 'text': ['a Marian place of prayer and reflection']}, {'answer_start': [92], 'text': ['a golden statue of the Virgin Mary']}]
我们可以看到每个答案都有一个起始索引(基于字符)和包含答案的文本。有了这些信息,我们可以轻松计算答案的结束索引(即,结束索引 = 起始索引 + 文本长度)。
GLUE 基准任务套件
GLUE 基准测试(gluebenchmark.com/tasks
)是一个用于评估自然语言处理模型的流行任务集合。它在多种任务上测试模型的自然语言理解能力。GLUE 任务集合包括以下任务。
13.3.2 处理数据
此数据集存在几个完整性问题需要解决。我们将解决这些问题,然后创建一个 tf.data 流水线来传输数据。需要解决的第一个问题是给定的 answer_start 和实际的 answer_start 之间的对齐问题。一些示例往往在给定的 answer_start 和实际的 answer_start 位置之间存在大约两个字符的偏移。我们将编写一个函数来纠正此偏移,以及添加结束索引。下一节概述了执行此操作的代码。
列表 13.5 修正答案起始/结束索引中的不必要偏移量
def correct_indices_add_end_idx(answers, contexts): """ Correct the answer index of the samples (if wrong) """ n_correct, n_fix = 0, 0 ❶ fixed_answers = [] ❷ for answer, context in zip(answers, contexts): ❸ gold_text = answer['text'][0] ❹ answer['text'] = gold_text ❹ start_idx = answer['answer_start'][0] ❺ answer['answer_start'] = start_idx ❺ if start_idx <0 or len(gold_text.strip())==0: print(answer) end_idx = start_idx + len(gold_text) ❻ # sometimes squad answers are off by a character or two - fix this if context[start_idx:end_idx] == gold_text: ❼ answer['answer_end'] = end_idx n_correct += 1 elif context[start_idx-1:end_idx-1] == gold_text: ❽ answer['answer_start'] = start_idx - 1 answer['answer_end'] = end_idx - 1 n_fix += 1 elif context[start_idx-2:end_idx-2] == gold_text: ❾ answer['answer_start'] = start_idx - 2 answer['answer_end'] = end_idx - 2 n_fix +=1 fixed_answers.append(answer) print( ❿ "\t{}/{} examples had the correct answer indices".format( n_correct, len(answers) ) ) print( ❿ "\t{}/{} examples had the wrong answer indices".format( n_fix, len(answers) ) ) return fixed_answers, contexts ⓫
❶ 记录正确并已修正的数量。
❷ 新修正的答案将存储在此变量中。
❸ 迭代每个答案上下文对。
❹ 将答案从字符串列表转换为字符串。
❺ 将答案的起始部分从整数列表转换为整数。
❻ 通过将答案的长度加到 start_idx 上来计算结束索引。
❼ 如果从 start_idx 到 end_idx 的片段与答案文本完全匹配,则不需要更改。
❽ 如果从 start_idx 到 end_idx 的片段需要偏移 1 才能与答案匹配,则相应地偏移。
❾ 如果从 start_idx 到 end_idx 的片段需要偏移 2 才能与答案匹配,则相应地偏移。
❿ 打印正确答案的数量(不需要更改)。
⓫ 打印需要修正的答案数量。
现在我们可以在数据集的两个子集(训练和验证)上调用此函数。我们将使用验证子集作为我们的测试集(未见过的示例)。将一部分训练示例保留为验证样本:
train_questions = dataset["train"]["question"] train_answers, train_contexts = correct_indices_add_end_idx( dataset["train"]["answers"], dataset["train"]["context"] ) test_questions = dataset["validation"]["question"] test_answers, test_contexts = correct_indices_add_end_idx( dataset["validation"]["answers"], dataset["validation"]["context"] )
当您运行此代码时,您将看到以下统计信息:
- 训练数据修正
- 87,341/87,599 个示例的答案索引是正确的。
- 258/87,599 个示例的答案索引是错误的。
- 验证数据修正
- 10,565/10,570 个示例的答案索引是正确的。
- 5/10,570 个示例的答案索引是错误的。
我们必须确保所需的修正数量不是非常高。如果修正数量显着高,通常意味着代码中存在错误或者数据加载逻辑存在问题。在这里,我们可以清楚地看到需要修正的示例数量很少。
定义和使用标记器
解决问题所需的所有数据都已经提供给我们了。现在是时候像以前一样对数据进行分词了。请记住,这些预训练的自然语言处理模型分为两部分:分词器和模型本身。分词器将文本分词为较小的部分(例如,子词),并以 ID 序列的形式呈现给模型。然后,模型接收这些 ID,并在它们上执行嵌入查找,以及各种计算,以得出最终的标记表示,这将作为输入传递给下游分类模型(即问答分类层)。在 transformers 库中,你有分词器对象和模型对象:
from transformers import DistilBertTokenizerFast tokenizer = DistilBertTokenizerFast.from_pretrained('distilbert-base-uncased')
你可以看到我们正在使用一个名为 DistilBertTokenizerFast 的分词器。这个分词器来自一个称为 DistilBERT 的模型。DistilBERT 是 BERT 的一个较小版本,表现与 BERT 相似但体积较小。它使用了一种称为知识蒸馏的迁移学习技术进行训练(devopedia.org/knowledge-distillation
)。要获得这个分词器,我们只需要调用 DistilBertTokenizerFast.from_pretrained() 函数,传入模型标签(即 distilbert-base-uncased)。这个标签表示模型是一个 DistilBERT 类型的模型,基本大小为(有不同大小的模型可用),并且忽略字符的大小写(由 uncased 表示)。模型标签指向 Hugging Face 的模型仓库中可用的一个模型,并为我们下载它。它将被存储在你的计算机上。
Hugging Face 提供了两种不同的分词器变体:标准分词器(PreTrainedTokenizer 对象;mng.bz/95d7
)和快速分词器(PreTrainedTokenizerFast 对象;mng.bz/j2Xr
)。你可以在mng.bz/WxEa
中了解它们的区别)。在批量编码(即将字符串转换为标记序列)时,它们的速度明显更快。此外,快速分词器还有一些额外的方法,将帮助我们轻松地处理输入,以供问答模型使用。
什么是 DistilBERT?
跟随 BERT,DistilBERT 是 Hugging Face 在 2019 年的论文“DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter” by Sanh et al. (arxiv.org/pdf/1910.01108v4.pdf
)中介绍的模型。它是使用知识蒸馏的迁移学习技术训练的。其核心思想是拥有一个教师模型(即 BERT),其中一个较小的模型(即 DistilBERT)试图模仿教师的输出,这成为 DistilBERT 的学习目标。DistilBERT 比 BERT 要小,只有六层,而 BERT 有 12 层。DistilBERT 另一个关键的不同之处在于它仅在遮蔽语言建模任务上进行训练,而不是下一个句子预测任务。这个决定是基于一些研究的支持,这些研究质疑下一个句子预测任务(相比于遮蔽语言建模任务)对于自然语言理解的贡献。
有了下载的分词器,让我们通过提供一些示例文本来检查分词器以及它是如何转换输入的:
context = "This is the context" question = "This is the question" token_ids = tokenizer(context, question, return_tensors='tf') print(token_ids)
这将返回
{'input_ids': <tf.Tensor: shape=(1, 11), dtype=int32, numpy= array([[ 101, 2023, 2003, 1996, 6123, 102, 2023, 2003, 1996, 3160, 102]], dtype=int32)>, 'attention_mask': <tf.Tensor: shape=(1, 11), dtype=int32, numpy=array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32)> }
接下来,打印与 ID 相对应的令牌
print(tokenizer.convert_ids_to_tokens(token_ids['input_ids'].numpy()[0]))
这将给出
['[CLS]', 'this', 'is', 'the', 'context', '[SEP]', 'this', 'is', 'the', 'question', '[SEP]']
现在我们可以使用接下来清单中的代码对所有训练和测试数据进行编码。
清单 13.6 编码训练和测试数据
train_encodings = tokenizer( ❶ train_contexts, train_questions, truncation=True, padding=True, ➥ return_tensors='tf' ) print( "train_encodings.shape: {}".format(train_encodings["input_ids"].shape) ) test_encodings = tokenizer( test_contexts, test_questions, truncation=True, padding=True, ➥ return_tensors='tf' ❷ ) print("test_encodings.shape: {}".format(test_encodings["input_ids"].shape))
❶ 编码 train 数据。
❷ 编码测试数据。
注意,我们在调用分词器时使用了几个参数。当启用截断和填充(即设置为 True)时,分词器将根据需要对输入序列进行填充/截断。你可以在创建分词器时传递一个参数(model_max_length)来将文本填充或截断到一定的长度。如果没有提供这个参数,它将使用在预训练时设置的默认长度作为分词器的配置之一。启用填充/截断后,你的输入将经历以下变化之一:
- 如果序列比长度短,则在序列末尾添加特殊令牌[PAD]直到它达到指定的长度。
- 如果序列比长度长,就会被截断。
- 如果序列的长度恰好相同,则不会引入任何更改。
当你运行清单 13.6 中的代码时,会打印
train_encodings.shape: (87599, 512) test_encodings.shape: (10570, 512)
我们可以看到,所有序列都被填充或截断,直到达到在模型预训练期间设置的 512 的长度。让我们看一下在使用 transformers 库定义分词器时需要注意的一些重要参数:
- model_max_length (int, optional)—输入进行填充的最大长度(令牌数)。
- padding_side (str, optional)—模型应用填充的一侧。可以是 [‘right’, ‘left’]。默认值从同名的类属性中选择。
- model_input_names(List[string],可选)—模型前向传递接受的输入列表(例如,“token_type_ids”或“attention_mask”)。默认值从同名类属性中选取。
- bos_token(str 或 tokenizers.AddedToken,可选)—表示句子开头的特殊标记。
- eos_token(str 或 tokenizers.AddedToken,可选)—表示句子结尾的特殊标记。
- unk_token(str 或 tokenizers.AddedToken,可选)—表示词汇表外单词的特殊标记。如果模型遇到以前没有见过的单词,词汇表外单词就很重要。
大多数这些参数可以安全地忽略,因为我们使用的是预训练的分词器模型,在训练之前已经设置了这些属性。
不幸的是,我们还需要做一些事情。我们必须做的一个重要转换是如何表示模型答案的开始和结束。正如我之前所说的,我们给出了答案的起始和结束字符位置。但是我们的模型只理解标记级别的分解,而不是字符级别的分解。因此,我们必须从给定的字符位置找到标记位置。幸运的是,快速分词器提供了一个方便的函数:char_to_token()。请注意,此函数仅适用于快速分词器(即 PreTrainedTokenizerFast 对象),而不适用于标准分词器(即 PreTrainedTokenizer 对象)。char_to_token()函数接受以下参数:
- batch_or_char_index(int)—批处理中的示例索引。如果批处理有一个示例,则将其用作我们感兴趣的要转换为标记索引的字符索引。
- char_index(int,可选—如果提供了批索引,则表示我们要转换为标记索引的字符索引。
- sequence_index(int,可选—如果输入有多个序列,则表示字符/标记所属的序列。
使用此函数,我们将编写 update_char_to_token_positions_inplace()函数将基于字符的索引转换为基于标记的索引(见下一个列表)。
列表 13.7 将 char 索引转换为基于标记的索引
def update_char_to_token_positions_inplace(encodings, answers): start_positions = [] end_positions = [] n_updates = 0 for i in range(len(answers)): ❶ start_positions.append( encodings.char_to_token(i, answers[i]['answer_start']) ❷ ) end_positions.append( encodings.char_to_token(i, answers[i]['answer_end'] - 1) ❷ ) if start_positions[-1] is None or end_positions[-1] is None: n_updates += 1 ❸ # if start position is None, the answer passage has been truncated # In the guide, ➥ https:/ /huggingface.co/transformers/custom_datasets.xhtml#qa-squad # they set it to model_max_length, but this will result in NaN ➥ losses as the last # available label is model_max_length-1 (zero-indexed) if start_positions[-1] is None: start_positions[-1] = tokenizer.model_max_length -1 ❹ if end_positions[-1] is None: end_positions[-1] = tokenizer.model_max_length -1 ❺ print("{}/{} had answers truncated".format(n_updates, len(answers))) encodings.update({ 'start_positions': start_positions, 'end_positions': end_positions }) ❻ update_char_to_token_positions_inplace(train_encodings, train_answers) update_char_to_token_positions_inplace(test_encodings, test_answers)
❶ 遍历所有答案。
❷ 获取起始和结束字符位置的标记位置。
❸ 跟踪缺少答案的样本数量。
❹ 如果找不到起始位置,请将其设置为最后一个可用索引。
❺ 如果找不到结束位置,请将其设置为最后一个可用索引。
❻ 在原地更新编码。
这将打印
10/87599 had answers truncated 8/10570 had answers truncated
在列表 13.7 中的代码中,我们遍历数据集中的每个答案,并对每个答案的起始和结束(字符索引)调用 char_to_token()方法。 答案的新起始和结束标记索引分配给新的键 start_positions 和 end_positions。 此外,您可以看到有一个验证步骤,检查起始或结束索引是否为 None(即,在预处理时未找到合适的标记 ID)。 如果是这种情况,我们将序列的最后一个索引分配为位置。
有多少个烂鸡蛋?
您可以看到,我们正在打印需要修改的示例数量(例如,需要纠正)或已损坏的示例数量(截断的答案)。 这是一个重要的健全性检查,因为很高的数字可能表明数据质量存在问题或数据处理工作流中存在错误。 因此,始终打印这些数字并确保它们低到可以安全忽略为止。
我们现在将看到如何定义 tf.data 流水线。
从标记到 tf.data 流水线
经过所有的清洁和必要的转换,我们的数据集就像新的一样好。 我们所剩下的就是从数据创建一个 tf.data.Dataset。 我们的数据流水线将非常简单。 它将创建一个训练数据集,该数据集将被分成两个子集,训练集和验证集,并对数据集进行批处理。 然后创建并批处理测试数据集。 首先让我们导入 TensorFlow:
import tensorflow as tf
然后我们将定义一个生成器,将产生模型训练所需的输入和输出。 正如您所看到的,我们的输入是两个项目的元组。 它有
- 填充的输入标记 ID(形状为[<数据集大小>,512])
- 注意力掩码(形状为[<数据集大小>,512])
输出将由以下组成
- 起始令牌位置(形状为[<数据集大小>])
- 结束令牌位置(形状为[<数据集大小>]
def data_gen(input_ids, attention_mask, start_positions, end_positions): for inps, attn, start_pos, end_pos in zip( input_ids, attention_mask, start_positions, end_positions ): yield (inps, attn), (start_pos, end_pos)
我们的数据生成器以非常特定的格式返回数据。 它返回一个输入元组和一个输出元组。 输入元组按顺序具有标记 ID(input_ids)由标记器返回和注意力掩码(attention_mask)。 输出元组具有所有输入的答案的起始位置(start_positions)和结束位置(end_positions)。
我们必须将我们的数据生成器定义为可调用的(即,它返回一个函数,而不是生成器对象)。 这是我们将要定义的 tf.data.Dataset 的要求。 要从我们之前定义的生成器获取可调用函数,让我们使用 partial()函数。 partial()函数接受一个可调用的、可选的可调用关键字参数,并返回一个部分填充的可调用函数,您只需要提供缺少的参数(即,在部分调用期间未指定的参数):
from functools import partial train_data_gen = partial( data_gen, input_ids=train_encodings['input_ids'], attention_mask=train_encodings['attention_mask'], start_positions=train_encodings['start_positions'], end_positions=train_encodings['end_positions'] )
train_data_gen 可以视为一个没有参数的函数,因为我们已经在部分调用中提供了所有参数。由于我们已经将我们的数据定义为一个生成器的形式,我们可以使用 tf.data.Dataset.from_generator()函数生成数据。请记住,在通过生成器定义数据时,我们必须传递 output_types 参数。我们所有的输出都是 int32 类型。但它们作为一对元组出现:
train_dataset = tf.data.Dataset.from_generator( train_data_gen, output_types=(('int32', 'int32'), ('int32', 'int32')) )
接下来,我们对数据进行洗牌以确保没有顺序。确保传递 buffer_size 参数,该参数指定了有多少样本被带入内存进行洗牌。由于我们计划使用其中的 10000 个样本作为验证样本,我们将 buffer_size 设置为 20000:
train_dataset = train_dataset.shuffle(20000)
是时候将 train_dataset 分割为训练和验证数据了,因为我们将使用原始验证数据子集作为测试数据。为了将 train_dataset 分割为训练和验证子集,我们将采取以下方法。在洗牌后,将前 10000 个样本定义为验证数据集。我们将使用 tf.data.Dataset.batch()函数进行数据批处理,批处理大小为 8:
valid_dataset = train_dataset.take(10000) valid_dataset = valid_dataset.batch(8)
跳过前 10000 个数据点,因为它们属于验证集,将其余部分作为 train_dataset。我们将使用 tf.data.Dataset.batch()函数对数据进行批处理,批处理大小为 8:
train_dataset = train_dataset.skip(10000) train_dataset = train_dataset.batch(8)
最后,使用相同的数据生成器定义测试数据:
test_data_gen = partial(data_gen, input_ids=test_encodings['input_ids'], attention_mask=test_encodings['attention_mask'], start_positions=test_encodings['start_positions'], end_positions=test_encodings['end_positions'] ) test_dataset = tf.data.Dataset.from_generator( test_data_gen, output_types=(('int32', 'int32'), ('int32', 'int32')) ) test_dataset = test_dataset.batch(8)
接下来我们将着手定义模型。
13.3.3 定义 DistilBERT 模型
我们仔细查看了数据,使用标记器进行了清理和处理,并定义了一个 tf.data.Dataset,以便快速检索模型将接受的格式的示例批次。现在是定义模型的时候了。为了定义模型,我们将导入以下模块:
from transformers import TFDistilBertForQuestionAnswering
transformers 库为您提供了一个出色的现成模型模板选择,您可以下载并在任务上进行训练。换句话说,您不必费心琢磨如何在预训练 transformers 之上插入下游模型。例如,我们正在解决一个问答问题,我们想要使用 DistilBERT 模型。transformers 库具有 DistilBERT 模型的内置问答适配。您只需导入模块并调用 from_pretrained()函数并提供模型标签即可下载它:
model = TFDistilBertForQuestionAnswering.from_pretrained("distilbert-base-uncased")
这将下载该模型并保存在您的本地计算机上。
Transformers 库还提供了哪些其他现成模型可用?
您可以查看mng.bz/8Mwz
以了解您可以轻松完成的 DistilBERT 模型的操作。transformers 是一个完整的库,您可以使用它来解决几乎所有常见的 NLP 任务,使用 Transformer 模型。在这里,我们将查看 DistilBERT 模型的选项。
TFDistilBertForMaskedLM
这使您可以使用掩码语言建模任务对 DistilBERT 模型进行预训练。在掩码语言建模任务中,给定文本语料库,单词将被随机屏蔽,并要求模型预测屏蔽的单词。
TFDistilBertForSequenceClassification
如果您的问题具有单个输入序列,并且您希望为输入预测标签(例如,垃圾邮件分类、情感分析等),您可以使用此模型端到端地训练模型。
TFDistilBertForMultipleChoice
使用此变体,DistilBERT 可用于解决多项选择问题。输入包括一个问题和几个答案。这些通常组合成单个序列(即,[CLS] [Question] [SEP] [Answer 1] [SEP] [Answer 2] [SEP]等),并且模型被要求预测问题的最佳答案,通常通过将[CLS]标记的表示传递给分类层来完成,该分类层将预测正确答案的索引(例如,第一个答案、第二个答案等)。
TFDistilBertForTokenClassification
命名实体识别或词性标注等任务需要模型为输入序列中的每个标记预测一个标签(例如,人物、组织等)。对于这样的任务,可以简单地使用这种类型的模型。
TFDistilBertForQuestionAnswering
这是我们场景中将使用的模型。我们有一个问题和一个上下文,模型需要预测答案(或答案在上下文中的起始/结束位置)。可以使用此模块解决此类问题。
表 13.1 总结了此侧边栏中的模型及其用法。
表 13.1 Hugging Face 的 transformers 库中不同模型及其用法总结
博客jalammar.github.io/illustrated-bert/
提供了一张非常详细的图表,展示了 BERT-like 模型如何用于不同的任务(如“特定任务模型”部分所述)。
对这些模型的训练和评估将非常类似于您如果将它们用作 Keras 模型时的使用方式。只要数据格式符合模型期望的正确格式,您就可以对这些模型调用 model.fit()、model.predict()或 model.evaluate()。
要训练这些模型,您还可以使用高级 Trainer(mng.bz/EWZd
)。我们将在稍后更详细地讨论 Trainer 对象。
这一部分是我在使用该库时的一个警示。通常,一旦定义了模型,你可以像使用 Keras 模型一样使用它。这意味着你可以使用 tf.data.Dataset 调用 model.fit() 并训练模型。当训练模型时,TensorFlow 和 Keras 期望模型输出为张量或张量元组。然而,Transformer 模型的输出是特定对象(继承自 transformers.file_utils.ModelOutput 对象),如 mng.bz/N6Rn
中所述。这将引发类似以下的错误
TypeError: The two structures don't have the same sequence type. Input structure has type <class 'tuple'>, while shallow structure has type <class 'transformers.modeling_tf_outputs.TFQuestionAnsweringModelOutput'>.
为了修复这个问题,transformers 库允许你设置一个名为 return_dict 的特定配置,并确保模型返回一个元组,而不是一个对象。然后,我们定义一个 Config 对象,该对象具有 return_dict=False,并用新的 config 对象覆盖模型的默认 config。例如,对于 DistilBERT 模型,可以这样做
from transformers import DistilBertConfig, TFDistilBertForQuestionAnswering config = DistilBertConfig.from_pretrained( "distilbert-base-uncased", return_dict=False ) model = TFDistilBertForQuestionAnswering.from_pretrained( "distilbert-base-uncased", config=config )
不幸的是,我无法通过使用这种配置使模型表现出我预期的行为。这表明,在编写代码时,即使使用一流的库,你也需要预料到可能出现的问题。最简单的解决方法是让 transformers 库输出一个 ModelOutput 对象,并编写一个包装函数,该函数将提取该对象的所需输出,并从这些输出创建一个 tf.keras.Model。以下清单中的函数为我们执行了这个任务。
将模型包装在 tf.keras.models.Model 对象中以防止错误
def tf_wrap_model(model): """ Wraps the huggingface's model with in the Keras Functional API """ # Define inputs input_ids = tf.keras.layers.Input( [None,], dtype=tf.int32, name="input_ids" ) ❶ attention_mask = tf.keras.layers.Input( [None,], dtype=tf.int32, name="attention_mask" ❷ ) out = model([input_ids, attention_mask]) ❸ wrap_model = tf.keras.models.Model( [input_ids, attention_mask], outputs=(out.start_logits, out.end_logits) ❹ ) return wrap_model
❶ 定义一个输入层,该层将接收一个令牌序列的批次。
❷ 定义一个输入,用于编码时返回的 attention mask。
❸ 给定输入 id 和 attention_mask 获取模型输出。
❹ 定义一个包装模型,该模型以定义的输入作为输入,并输出开始和结束索引预测层的对数。
你可以通过调用清单 13.8 中的函数来生成生成校正输出的模型:
model_v2 = tf_wrap_model(model)
最后,使用损失函数(稀疏分类交叉熵,因为我们使用整数标签)、度量(稀疏准确性)和优化器(Adam 优化器)对模型进行编译:
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) acc = tf.keras.metrics.SparseCategoricalAccuracy() optimizer = tf.keras.optimizers.Adam(learning_rate=1e-5) model_v2.compile(optimizer=optimizer, loss=loss, metrics=[acc])
视觉中的 Transformer
Transformer 模型通常在自然语言处理领域表现出色。直到最近,人们才开始努力了解它们在计算机视觉领域的位置。然后谷歌和 Facebook AI 发表了几篇重要论文,研究了 Transformer 模型如何在计算机视觉领域中使用。
TensorFlow 实战(六)(5)https://developer.aliyun.com/article/1522939