TensorFlow 实战(六)(4)https://developer.aliyun.com/article/1522937
视觉 Transformer(ViT)
想法是将图像分解成 16×16 个小块,并将每个小块视为一个单独的令牌。每个图像路径被展平为一个 1D 向量,并使用位置编码机制对其位置进行编码,类似于原始的 Transformer。需要注意的是,原始 Transformer 中的位置编码是 1D 的。然而,图像是 2D 的。作者认为 1D 的位置编码已足够,且 1D 和 2D 位置编码之间没有太大的性能差异。一旦图像被分成 16×16 的块并展平,每个图像就可以被表示为一个令牌序列,就像文本输入序列一样。
然后,该模型以一种自监督的方式在一个名为 JFT-300M(paperswithcode.com/dataset/jft-300m
)的视觉数据集中进行预训练。在视觉中制定自监督任务并不是一件微不足道的事情,就像在 NLP 中那样。在 NLP 中,您可以简单地形成一个目标,即预测文本序列中的掩蔽令牌。但是,在计算机视觉的上下文中,令牌是一系列连续的值(归一化的像素值)。因此,ViT 预先训练以预测给定图像补丁的平均三位 RGB 颜色。每个通道(即红色,绿色和蓝色)都用三位表示(即每个位的值为 0 或 1),这给出 512 个可能或类。换句话说,对于给定的图像,补丁将被随机遮盖(使用与 BERT 相同的方法),并且要求模型预测该图像补丁的平均三位 RGB 颜色。
首先,Dosovitskiy 等人在 2020 年 10 月发表了题为“一张图片值 16x16 个单词:用于大规模图像识别的变压器”(An Image Is Worth 16X16 Words: Transformers for Image Recognition at Scale)的文章(arxiv.org/pdf/2010.11929.pdf
)。这可以被认为是走向视觉变换器的第一步。在这篇论文中,作者将原始 Transformer 模型适应于计算机视觉,对架构进行了最小限度的修改。这个模型被称为视觉变换器(ViT)。
在预训练后,可以通过在 ViT 的顶部适配一个分类或回归头来针对特定任务的问题进行微调,就像 BERT 一样。ViT 还在序列的开头有[CLS]令牌,该令牌将用作插入到 ViT 上面的下游视觉模型的输入表示。以下图示说明了 ViT 的机制。
ViT 的原始代码是使用一个称为 Jax 的框架编写的(github.com/google-research/vision_transformer
)。然而,该模型有几个第三方 TensorFlow 包装器(例如github.com/emla2805/vision-transformer
)。如果使用第三方版本,请确保阅读代码并验证正确性,因为第三方与原始研究团队没有关联。
视觉 Transformer(ViT)模型架构及其在下游图像分类任务中的使用方式
统一 Transformer(UniT)
随后,Facebook AI 的一篇更具突破性的论文问世,题为“Transformer 就是你所需要的一切:统一 Transformer 实现多模态多任务学习”(作者:Hu 等)(arxiv.org/pdf/2102.10772.pdf
)。该模型被称为统一 Transformer(UniT)。UniT 可以在计算机视觉和自然语言处理领域执行大量任务,只需改变分类头即可。
即使模型很复杂,整体上还是很直观的。有三个 Transformer 模型。一个 Transformer 对图像输入(如果有)进行编码,以生成图像的编码表示。下一个 Transformer 对文本输入(如果有)进行编码。最后,另一个 Transformer 将任务索引作为输入,获取嵌入并将其传递给一个跨自注意力层,该层以连接的图像和文本编码作为查询和键,并将任务嵌入(经过自注意力层后)作为值。这类似于 Transformer 解码器在其编码器-解码器注意力层中使用最后一个编码器输出生成查询和键,并使用解码器的输入作为值。该模型在下一页的图中表示。
UniT 在涉及八个数据集的七项任务上进行了评估。这些任务包括对象检测、视觉问答、对象注释和四个仅语言任务。这四个仅语言任务来自 GLUE 基准数据集,其中包括以下内容:
任务
对象检测—模型预测图像中存在的对象的矩形坐标(数据集 COCO 和 Visual Genome Detection [VGD])。
视觉问答(VQAv2)—给定一幅图像和一个问题,然后模型预测问题的答案,该答案可以在图像中找到(数据集:VQAv2)。
视觉蕴涵任务—一种视觉蕴涵任务,其中给定一幅图像和一个文本序列,模型预测句子是否语义上蕴含图像(SNLI-VE)。
问题自然语言推理(QNLI)—通过从给定上下文中提取答案来回答问题。
Quora 问题对(QQP)—从给定的问题对中识别重复的问题。
文本蕴涵—文本蕴涵着重于预测句子 A 是否蕴含/与句子 B 矛盾/与句子 B 中性(数据集 MNLI)。
情感分析—为给定的评论/文本预测情感(正面/负面/中性)(数据集 Stanford Sentiment Treebank [SST-2])。
UniT 的整体架构。该模型由三个组件组成:图像编码器、文本编码器和任务解码器。最后,有几个分类头安装在任务解码器之上。
接下来,我们将训练我们刚刚定义的模型。
13.3.4 训练模型
我们一直在耐心地解决这个问题,最终,我们可以训练我们刚刚定义的模型,使用之前创建的 train_dataset 数据集,并使用 valid_dataset 来监视模型的准确度:
model_v2.fit( train_dataset, validation_data=valid_dataset, epochs=3 )
这将打印以下输出:
Epoch 1/3 WARNING:tensorflow:The parameters `output_attentions`, ➥ `output_hidden_states` and `use_cache` cannot be updated when calling a ➥ model.They have to be set to True/False in the config object (i.e.: ➥ `config=XConfig.from_pretrained('name', output_attentions=True)`). WARNING:tensorflow:The parameter `return_dict` cannot be set in graph mode ➥ and will always be set to `True`. 9700/9700 [==============================] - 3308s 340ms/step - loss: ➥ 4.3138 - tf_distil_bert_for_question_answering_loss: 2.2146 - ➥ tf_distil_bert_for_question_answering_1_loss: 2.0992 - ➥ tf_distil_bert_for_question_answering_sparse_categorical_accuracy: ➥ 0.4180 - ➥ tf_distil_bert_for_question_answering_1_sparse_categorical_accuracy: ➥ 0.4487 - val_loss: 2.3849 - ➥ val_tf_distil_bert_for_question_answering_loss: 1.2053 - ➥ val_tf_distil_bert_for_question_answering_1_loss: 1.1796 - ➥ val_tf_distil_bert_for_question_answering_sparse_categorical_accuracy: ➥ 0.6681 - ➥ val_tf_distil_bert_for_question_answering_1_sparse_categorical_accuracy ➥ : 0.6909 ... Epoch 3/3 9700/9700 [==============================] - 3293s 339ms/step - loss: ➥ 1.6349 - tf_distil_bert_for_question_answering_loss: 0.8647 - ➥ tf_distil_bert_for_question_answering_1_loss: 0.7703 - ➥ tf_distil_bert_for_question_answering_sparse_categorical_accuracy: ➥ 0.7294 - ➥ tf_distil_bert_for_question_answering_1_sparse_categorical_accuracy: ➥ 0.7672 - val_loss: 2.4049 - ➥ val_tf_distil_bert_for_question_answering_loss: 1.2048 - ➥ val_tf_distil_bert_for_question_answering_1_loss: 1.2001 - ➥ val_tf_distil_bert_for_question_answering_sparse_categorical_accuracy: ➥ 0.6975 - ➥ val_tf_distil_bert_for_question_answering_1_sparse_categorical_accuracy ➥ : 0.7200
训练更新内容相当长,因此让我们将它们分解一下。有两个损失:
- tf_distil_bert_for_question_answering_loss——测量起始索引预测头的损失
- tf_distil_bert_for_question_answering_1_loss——测量结束索引预测头的损失
正如之前提到的,对于问答问题,我们有两个分类头:一个用于预测起始索引,另一个用于预测结束索引。对于准确度也有类似的情况。有两个准确度来衡量各自头部的性能:
- tf_distil_bert_for_question_answering_sparse_categorical_accuracy——测量分类头预测起始索引的准确度
- tf_distil_bert_for_question_answering_1_sparse_categorical_accuracy——测量分类头预测终结索引的准确度
我们可以看到模型对于起始和结束索引预测的训练准确度大约为 77%,而验证准确度分别为 70%和 72%。鉴于我们只对该模型进行了三次 epochs 的训练,这些准确度是不错的。
注意在一台配备了 NVIDIA GeForce RTX 2070 8GB 的 Intel Core i5 机器上,训练大约需要 2 小时 45 分钟来运行三个 epochs。
您可以看到模型训练过程中产生了几个警告。由于这是一个新库,非常重要的一点是要注意这些警告是否在我们使用这些模型的上下文中具有意义。如果看到错误,你不必担心这些警告,因为警告并不总是指示问题。根据我们要解决的问题,一些警告是不适用的,可以安全地忽略。第一个警告表示在调用模型时无法更新参数 output_attentions、output_hidden_states 和 use_cache,而需要作为 config 对象传递。我们对此并不担心,因为我们对模型不感兴趣引入任何自定义修改,并且我们使用的模型已经设计用于问答问题。
第二个警告表示 return_dict 将始终设置为 TRUE。设置 return_dict=True 意味着 Transformer 模型将返回一个 TensorFlow 或 Keras 无法理解的 ModelOutput 对象。当我们希望使用 Keras API 与模型一起使用时,这将在后续过程中造成问题。这就是我们创建 tf_wrap_model()函数的原因之一:确保我们获得一个总是输出元组而不是 ModelOutput 对象的 tf.keras.Model。
最后,我们将保存模型:
import os # Create folders if not os.path.exists('models'): os.makedirs('models') if not os.path.exists('tokenizers'): os.makedirs('tokenizers') tokenizer.save_pretrained(os.path.join('tokenizers', 'distilbert_qa')) model_v2.get_layer( "tf_distil_bert_for_question_answering").save_pretrained( os.path.join('models', 'distilbert_qa') ) )
确保保存分词器和模型。要保存分词器,你可以简单地调用 save_pretrained() 函数并提供一个文件夹路径。分词器将保存在该目录中。保存模型需要更多的工作。我们无法直接保存模型(model_v2),因为当你的模型有一个自定义层时,为了正确保存,该层需要实现 get_config() 函数并指定该层的所有属性。然而,对于作为自定义层存在的 Transformer 模型,这样做将会非常困难。因此,我们只会通过调用 model_v2.get_layer() 函数和层名称(即 tf_distil_bert_for_question_answering)来保存 Transformer 模型,然后使用文件夹路径调用 save_pretrained() 方法。每当我们需要构建完整模型时,我们只需在保存的模型上调用 tf_wrap_model() 函数即可。
13.3.5 询问 BERT 一个问题
评估模型也是调用 model_v2.evaluate() 函数与我们之前创建的测试数据集相关的事情:
model_v2.evaluate(test_dataset)
这将打印
1322/1322 [==============================] - 166s 126ms/step - loss: 2.4756 ➥ - tf_distil_bert_for_question_answering_loss: 1.2702 - ➥ tf_distil_bert_for_question_answering_1_loss: 1.2054 - ➥ tf_distil_bert_for_question_answering_sparse_categorical_accuracy: ➥ 0.6577 - ➥ tf_distil_bert_for_question_answering_1_sparse_categorical_accuracy: ➥ 0.6942
这是个好消息!我们在预测答案起始索引时达到了约 65.7%的准确率,而模型能够以大约 69.4%的准确率预测结束索引。需要观察的两件事是起始和结束的准确率都相似,这意味着模型能够以这个准确率正确地获取答案(从开始到结束)。最后,这个准确率接近验证准确率,这意味着我们没有发生异常的过拟合。
正如我多次提到的,仅仅看一个数字通常不足以判断一个模型的性能。在评估对象时,视觉检查一直是人类的自然倾向。因此,作为一个严谨的数据科学家或机器学习工程师,尽可能地将其纳入机器学习工作流程是必要的。在下一个清单中,我们将向我们的模型提供来自测试集的一个问题,看看模型会产生什么。
清单 13.9 推断给定问题的模型的文本答案
i = 5 sample_q = test_questions[i] ❶ sample_c = test_contexts[i] ❶ sample_a = test_answers[i] ❶ sample_input = ( test_encodings["input_ids"][i:i+1], test_encodings["attention_mask"][i:i+1] ) def ask_bert(sample_input, tokenizer): out = model_v2.predict(sample_input) ❷ pred_ans_start = tf.argmax(out[0][0]) ❸ pred_ans_end = tf.argmax(out[1][0]) ❹ print( "{}-{} token ids contain the answer".format( pred_ans_start, pred_ans_end ) ) ans_tokens = sample_input[0][0][pred_ans_start:pred_ans_end+1] ❺ return " ".join(tokenizer.convert_ids_to_tokens(ans_tokens)) ❻ print("Question") ❼ print("\t", sample_q, "\n") ❼ print("Context") ❼ print("\t", sample_c, "\n") ❼ print("Answer (char indexed)") ❼ print("\t", sample_a, "\n") ❼ print('='*50,'\n') sample_pred_ans = ask_bert(sample_input, tokenizer) ❽ print("Answer (predicted)") ❾ print(sample_pred_ans) ❾ print('='*50,'\n')
❶ 定义一个示例问题、上下文和答案。
❷ 对示例输入进行模型预测。这将返回起始和结束索引的概率向量。
❸ 通过从起始索引概率向量中获取最大索引来获得预测的起始索引。
❹ 通过从结束索引概率向量中获取最大索引来获得预测的结束索引。
❺ 通过获取起始/结束索引之间的文本来获得字符串答案标记。
❻ 返回一个单一字符串形式的标记列表。
❼ 打印模型的输入。
❽ 在定义的输入上调用 ask_bert 函数。
❾ 打印答案。
让我们详细说明清单 13.9 中代码的细节。首先,我们定义一个索引 i。这个索引将用于从测试集中检索一个样本。sample_q、sample_c 和 sample_a 代表我们选择的样本的问题、上下文和答案。有了这些,我们可以定义 sample_input,它将包含模型理解的输入的编码表示。函数 ask_bert()接受一个使用 tokenizer 为模型准备的输入(用 sample_input 表示),以将答案的标记 ID 转换回可读的标记。该函数首先预测输入的输出,并获取答案的起始和结束标记 ID。最后,该函数将这些 ID 以及其中的单词转换为一个可理解的答案并返回文本。如果你打印这个过程的输出,你会得到以下结果:
Question What was the theme of Super Bowl 50? Context Super Bowl 50 was an American football game to determine the ➥ champion of the National Football League (NFL) for the 2015 season. The ➥ American Football Conference (AFC) champion Denver Broncos defeated the ➥ National Football Conference (NFC) champion Carolina Panthers 24-10 to ➥ earn their third Super Bowl title. The game was played on February 7, ➥ 2016, at Levi's Stadium in the San Francisco Bay Area at Santa Clara, ➥ California. As this was the 50th Super Bowl, the league emphasized the ➥ "golden anniversary" with various gold-themed initiatives, as well as ➥ temporarily suspending the tradition of naming each Super Bowl game ➥ with Roman numerals (under which the game would have been known as ➥ "Super Bowl L"), so that the logo could prominently feature the Arabic ➥ numerals 50\. Answer (char indexed) {'answer_start': 487, 'text': '"golden anniversary"', ➥ 'answer_end': 507} ================================================== 98-99 token ids contain the answer Answer (predicted) golden anniversary ==================================================
这结束了我们关于使用 Hugging Face 的 Transformer 库实现 Transformer 模型的讨论。我们已经逐步介绍了您在解决任何 NLP 任务时可能遇到的所有步骤。Hugging Face 的 transformers 库仍然以在 TensorFlow 或 PyTorch 中实现 Transformer 模型而享有盛誉。
可视化注意力头
每当我们有机会解释深度学习模型并理解模型为何做出某种决定时,充分利用这个机会就显得很重要。拥有解剖和解释模型的能力有助于在用户之间建立信任。由于存在自注意力层,解释 Transformer 模型变得非常容易。使用自注意力层,我们可以找出模型在生成一个令牌的隐藏表示时注意到了哪些单词。
我们可以使用 bert_viz 库(github.com/jessevig/bertviz
)来可视化任意层中任意注意力头中的注意力模式。重要的是要注意,bert_viz 不支持 TensorFlow,而是使用 PyTorch 库。尽管有这个小小的技术差异,但使用 bert_viz 很容易和直观。
首先,导入所需的库:
import torch from bertviz import head_view
接下来,定义一个 BERT 模型。确保将 output_attentions=True 配置传递给输出注意力输出,因为默认情况下它是关闭的:
config = BertConfig.from_pretrained( 'bert-base-uncased', output_attentions=True ) bert = TFBertModel.from_pretrained( "bert-base-uncased", config=config )
编码输入文本,然后获取模型的输出:
encoded_input = tokenizer(text, return_tensors='tf') output = model(encoded_input)
最后调用 head_view()函数。你可以通过简单调用 output.attentions 来获取注意力输出,它将返回一个包含 12 个张量的元组。每个张量对应于 BERT 中的一个单独层。此外,确保将它们转换为 torch 张量。否则,该函数会出错。输出在图 13.10 中可视化。
head_view( [torch.from_numpy(layer_attn.numpy()) for layer_attn in output.attentions], encoded_tokens )
这结束了我们关于 Transformer 模型的讨论。然而,重要的是要记住,Transformer 模型正在不断发展并变得更好。在下一章中,我们将讨论一个重要的可视化工具,称为 TensorBoard,它与 TensorFlow 一起提供。
图 13.10 bert_viz 库的注意力输出。您可以从下拉菜单中选择不同的层。不同的阴影代表该层中不同的注意力头,可以打开或关闭。两列之间的线表示模型在生成给定标记的隐藏表示时关注哪些词。
练习 3
您被要求实现一个命名实体识别模型。命名实体识别是一个标记分类任务,其中为每个标记分配一个标签(例如,人物、组织、地理、其他等)。有七种不同的标签。如果您想要使用 distilbert-base-uncased 模型进行此操作,您将如何定义模型?请记住,在 transformers 库中,您可以将 num_labels 作为关键字传递以定义输出类的数量。例如,如果您有一个要设置为 “abc” 的配置 “a”,您可以这样做
<model>.from_pretrained(<model_tag>, *"*a*"*= *"*abc*"*)
概要
- Transformer 模型的主要子组件包括嵌入(标记和位置)、自注意力子层、完全连接子层、残差连接和层归一化子层。
- BERT 是一个 Transformer 编码器模型,为输入传递的每个标记生成一个隐藏的(关注的)表示。
- BERT 使用特殊标记,如 [CLS](表示开始并用于生成分类头的输出)、[SEP](用于标记两个子序列之间的分隔;例如,在问答中标记问题和上下文之间的分隔)、[PAD](表示填充的标记,将所有序列都调整为固定长度)、以及 [MASK](用于在输入序列中屏蔽标记;例如,填充的标记)。
- BERT 可以通过在 BERT 的最终输出之上适配一个分类头(例如,逻辑回归层)来用于下游、任务特定的分类任务。根据任务类型,可能需要多个分类头,并且分类头的利用可能有所不同。
- Hugging Face 的 transformers 库提供了所有与 NLP 相关的 Transformer 模型的实现,并且可以轻松下载和在工作流中使用。可下载的预训练模型具有两个主要组件:分词器,将提供的字符串标记为标记序列;模型,接受标记序列以生成最终的隐藏输出。
练习答案
练习 1
PE(pos,2i ) = sin(pos/10000^(21/d[model]))
import tensorflow as tf # Defining some hyperparameters n_steps = 25 # Sequence length n_en_vocab = 300 # Encoder's vocabulary size n_heads = 8 # Number of attention heads d = 512 # The feature dimensionality of each layer # Encoder input layer en_inp = tf.keras.layers.Input(shape=(n_steps,)) # Encoder input embedddings en_emb = tf.keras.layers.Embedding( n_en_vocab, 512, input_length=n_steps )(en_inp) pos_inp = tf.constant( [[p/(10000**(2*i/d)) for p in range(d)] for i in range(n_steps)] ) pos_inp = tf.expand_dims(pos_inp, axis=0) en_pos_emb = tf.math.sin(pos_inp) en_final_emb = en_emb + en_pos_emb # Two encoder layers en_out1 = EncoderLayer(d, n_heads)(en_emb) en_out2 = EncoderLayer(d, n_heads)(en_out1) model = tf.keras.models.Model(inputs=en_inp, output=en_out2)
练习 2
hub_classifier, hub_encoder = bert.bert_models.classifier_model( bert_config=bert_config, hub_module_url=bert_url, num_labels=5 )
练习 3
from transformers import TFDistilBertForTokenClassification model = TFDistilBertForTokenClassification.from_pretrained( "distilbert-base-uncased", num_labels=7 *)*