TensorFlow 实战(六)(1)https://developer.aliyun.com/article/1522932
摘要
- 在序列到序列模型中使用注意力可以极大地提高其性能。
- 在每个解码时间步中使用注意力,解码器可以看到编码器的所有历史输出,并选择并混合这些输出,以产生一个综合的(例如,求和)表示,这给出了编码器输入的整体视图。
- 在注意力计算的中间产物之一是归一化的能量值,它给出了每个编码位置对于每个解码步骤的解码时间步骤的重要性的概率分布。换句话说,这是一个矩阵,对于每个编码器时间步和解码器时间步的组合都有一个值。这可以可视化为热图,并且可以用于解释解码器在翻译解码器中的某个令牌时关注了哪些单词。
练习答案
练习 1
e_inp = tf.keras.Input( shape=(1,), dtype=tf.string, name='e_input_final' ) fwd_state, bwd_state, en_states = encoder(e_inp) d_inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='d_input') d_vectorized_out = vectorizer(d_inp) d_emb_layer = tf.keras.layers.Embedding( n_vocab+2, 128, mask_zero=True, name='d_embedding' ) d_emb_out = d_emb_layer(d_vectorized_out) d_init_state = tf.keras.layers.Concatenate(axis=-1)([fwd_state, bwd_state]) gru_out = tf.keras.layers.GRU(256, return_sequences=True)( d_emb_out, initial_state=d_init_state ) attn_out = AttentionX()([en_states, gru_out]) d_dense_layer_1 = tf.keras.layers.Dense( 512, activation='relu', name='d_dense_1' ) d_dense1_out = d_dense_layer_1(attn_out) d_final_layer = tf.keras.layers.Dense( n_vocab+2, activation='softmax', name='d_dense_final' ) d_final_out = d_final_layer(d_dense1_out)
练习 2
im = ax.imshow(attention_matrix) ax.set_xticks(np.arange(attention_matrix.shape[1])) ax.set_yticks(np.arange(attention_matrix.shape[0])) ax.set_xticklabels(german_text_labels) ax.set_yticklabels(english_text_labels)
第十三章:个 Transformer
本章涵盖
- 实现一个包含所有组件的完整 Transformer 模型
- 使用 TFHub 中预训练的 BERT 模型实现垃圾邮件分类器
- 使用 Hugging Face 的 Transformer 库实现问答模型
在第十一章和第十二章,你学习了序列到序列模型,这是一类强大的模型家族,允许我们将任意长度的序列映射到另一个任意长度的序列。我们通过一个机器翻译任务来举例说明这种能力。序列到序列模型由一个编码器和一个解码器组成。编码器接收输入序列(源语言中的句子)并创建该序列的紧凑表示(称为上下文向量)。解码器接收上下文向量以生成最终的目标(即目标语言中的句子)。但是我们看到上下文向量的限制如何限制了模型,并研究了两种改进模型性能的技术。首先,教师强制允许解码器不仅在给定时间内查看上下文向量,还可以查看目标语言中先前的单词。这为解码器提供了更多的信息,以准确地生成最终的预测。其次,注意机制允许解码器窥视编码器输出历史中的任何部分,并使用该信息来生成输出。然而,LSTM 和 GRU 模型仍然相当受限制,因为它们一次只能看到一个序列输出,并且需要依赖有限的状态向量(即记忆)来记住它们所看到的内容。
但是现在镇上出现了一个新的竞争对手。如果有一个词可以概括最近最先进的自然语言处理(NLP)和计算机视觉研究,那就是 Transformer。Transformer 模型是最新型的深度学习模型,通过被冠以 NLP 许多任务的最新技术而令人难忘,击败了先前的领导者,如基于 LSTM 和 GRU 的模型。受到他们在 NLP 领域取得的空前成功的启发,它们现在正在被引入来解决各种计算机视觉问题。
按设计,Transformer 模型使得记忆或使用长序列数据(例如单词序列)中的信息变得轻而易举。与 LSTM 模型不同,后者必须一次查看一个时间步长,Transformer 模型可以一次看到整个序列。这使得 Transformer 模型比其他模型更好地理解语言。此外,由于纵向(即时间)计算的最小化需要对文本进行顺序处理,Transformer 模型享有高度的可并行性。
在本章中,延续我们在第五章的对话,我们将讨论 Transformer 模型的一些更多细节,以便我们对其有一个全面的理解。我们将看到 Transformer 模型如何使用多个嵌入来表示标记以及这些标记在序列中的位置。然后我们将学习 BERT,这是 Transformer 模型的一个变体,它已经在大型文本语料库上进行了训练,可以作为基础层轻松解决下游 NLP 任务,而不需要复杂的模型。BERT 本质上是 Transformer 模型的编码器部分,使用了两种技术进行大量文本的预训练:掩码语言建模(即,序列中的单词被随机掩码,BERT 必须预测掩码单词)和下一个句子预测(即,给定两个句子 A 和 B,预测 B 是否暗示 A)。当我们使用它在 TensorFlow 中实现一个垃圾邮件分类器时,我们将看到 BERT 在实际中的运行情况。接下来,Hugging Face 的 transformers 库(huggingface.co/transformers/
)是实现最先进的 Transformer 模型的热门选择,易于使用。如果您打算在 TensorFlow 中实现 Transformer 模型,那么这是最有价值的库之一。出于这个原因,我们将使用 Hugging Face 的 Transformers 库实现一个问答模型。最后,我们将结束本章,讨论 Transformer 模型在计算机视觉中的应用。
作为第一步,让我们重新审视一下我们已经学过的关于 Transformer 模型的知识(见图 13.1),并进一步扩展我们的理解。
图 13.1 Transformer 模型如何解决 NLP 问题
13.1 更详细的 Transformer
你是一名数据科学家,有人建议你在 NLP 工作流中使用 Transformer 模型。你看了 TensorFlow 提供的 Transformer 模型。然而,你很难从文档中理解这个模型。你认为从零开始实现一个 Transformer 网络是理解赋予 Transformer 模型生命的概念的好方法。因此,你决定按照论文“Attention Is All You Need”(arxiv.org/pdf/1706.03762.pdf
)中指定的所有组件来实现一个 Transformer 模型。这个模型将具有诸如嵌入层(标记和位置嵌入)、自注意层、归一化层等组件。
13.1.1 重新审视 Transformer 的基本组件
在第五章,我们讨论了 Transformer 模型的基础,以实现一个简化的 Transformer。现在让我们深入讨论并查看存在于 Transformer 模型中的所有组件。Transformer 模型是基于编码器-解码器的模型。编码器接受一系列输入(例如,Dogs are great)以创建这些标记的隐藏(或潜在)表示。接下来,解码器使用编码器生成的输入标记的表示,并生成一个输出(例如,输入的法语翻译)。
编码器由一堆层组成,其中每一层包含两个子层:
- 自注意力层—为序列中的每个输入标记生成潜在表示。对于每个输入标记,此层查看整个输入序列,并选择序列中的其他标记,以丰富为该标记生成的隐藏输出的语义(即,关注表示)。
- 全连接层—生成与关注表示相关的逐元素隐藏表示。
解码器由三个子层组成:
- 掩码自注意力层—对于每个输入标记,它查看其左侧的所有标记。解码器需要屏蔽右侧的单词,以防止模型看到未来的单词,使得解码器的预测任务变得简单。
- 编码器-解码器注意力层—对于解码器中的每个输入标记,它查看编码器的输出以及解码器的掩码关注输出,以生成一个语义丰富的隐藏输出。
- 全连接层—生成解码器关注表示的逐元素隐藏表示。
在我们之前的讨论中,最难理解的是自注意力层。因此,值得重新审视自注意力层中发生的计算。自注意力层中的计算围绕着三个权重矩阵展开:
- 查询权重矩阵(W[q])
- 键权重矩阵(W[k])
- 值权重矩阵(W[v])
这些权重矩阵中的每一个对于给定输入序列中的给定标记(在位置i)产生三个输出:查询、键和值。让我们刷新一下我们在第五章中对这些实体所说的话:
- 查询(q[i])—帮助构建最终用于索引值(v)的概率矩阵。查询影响矩阵的行,并表示正在处理的当前单词的索引。
- 关键(k[i])—帮助构建最终用于索引值(v)的概率矩阵。关键影响矩阵的列,并表示需要根据查询词混合的候选词。
- Value(v[i]) ——输入的隐藏(即潜在)表示,用于通过查询和密钥创建的概率矩阵索引计算最终输出。正如前面解释的那样,在位置 i 处的最终输出不仅使用第 i 个令牌,还使用输入序列中的其他令牌,这增强了最终表示中捕获的语义。
这些元素的高层目的是生成一个被关注的表示(即给定令牌的潜在或隐藏表示,其由输入序列中其他令牌的信息增强)。为此,模型
- 为输入序列中的每个位置生成一个查询
- 对于每个查询,确定每个密钥应该贡献多少(密钥也代表个别令牌)
- 基于给定查询的密钥的贡献,混合与这些密钥对应的值以生成最终的关注表示
查询、密钥和值都是通过将可训练的权重矩阵与输入令牌的数值表示相乘而生成的。所有这些都需要以可微分的方式进行,以确保梯度可以通过模型进行反向传播。论文提出了以下计算来计算输入令牌的自注意力层的最终表示:
这里,Q 表示查询,K 表示密钥,V 表示批量数据中所有输入和每个输入中所有令牌的值。这就是使 Transformer 模型如此强大的原因:与 LSTM 模型不同,Transformer 模型将所有令牌在一个序列中聚合到一个矩阵乘法中,使得这些模型高度可并行化。
Transformer 中的嵌入
当我们讨论 Transformer 模型时,有一件事情被忽略了,那就是它使用的嵌入。我们简要地提到了使用的词嵌入。让我们在这里更详细地讨论这个话题。词嵌入根据单词的上下文提供了语义保持的表示。换句话说,如果两个单词在相同的上下文中使用,它们将具有相似的单词向量。例如,“猫”和“狗”将具有相似的表示,而“猫”和“火山”将具有完全不同的表示。
单词向量最初在 Mikolov 等人的论文中被介绍,题为“Efficient Estimation of Word Representations in Vector Space” (arxiv.org/pdf/1301.3781.pdf
)。它有两个变种,skip-gram 和 continuous bag-of-words(CBOW)。由于 skip-gram 稍微比 CBOW 更广泛地被接受,让我们讨论 skip-gram 算法的要点。
第一步是定义一个大小为 V × E 的大矩阵,其中 V 是词汇表的大小,E 是嵌入的大小。嵌入的大小(E)是用户定义的超参数,其中更大的 E 通常会导致更强大的词嵌入。在实践中,你不需要使嵌入的大小超过 300。
接下来,完全无监督地创建输入和目标。给定大量的文本语料库,选择一个单词形式作为输入(探针词),并以探针词周围的单词作为目标。通过定义固定大小的探针词周围窗口来捕获周围的单词。例如,针对窗口大小为 2(在探针词的每一侧),你可以从句子 “angry John threw a pizza at me.” 生成以下输入-目标对。
(John, angry), (John, threw), (John, a), (threw, angry), (threw, John), (threw, a), (threw, pizza), ..., (at, a), (at, pizza), (at, me)
有了带标签的数据,你可以将学习词嵌入的问题框定为分类问题。换句话说,你训练一个模型(即一个词嵌入矩阵的函数),以输入的词为基础预测目标词。该模型包括两个部分,嵌入矩阵和完全连接层,通过 softmax 激活输出预测结果。一旦学习了嵌入,你可以丢弃其周围的其他内容(例如完全连接层),并使用嵌入矩阵用于下游 NLP 任务,例如情感分析、机器翻译等。只需要查找与单词相对应的嵌入向量,即可获得该单词的数字表示。
现代深度学习模型受到原始词向量算法的启发,将学习词嵌入和实际的决策支持 NLP 问题融合到单个模型训练任务中。换句话说,以下一般方法用于将词嵌入纳入到机器学习模型中:
- 定义一个随机初始化的词嵌入矩阵(或提供免费下载的预训练的嵌入)。
- 定义使用单词嵌入作为输入并产生输出(例如情感、语言翻译等)的模型(随机初始化)。
- 在任务上训练整个模型(嵌入 + 模型)。
Transformer 模型中也使用了相同的技术。但是,Transformer 模型中有两个不同的嵌入:
- Token 嵌入(为模型在输入序列中看到的每个 token 提供唯一的表示)
- 位置嵌入(为输入序列中的每个位置提供唯一的表示)
Token 嵌入(为模型在输入序列中看到的每个 token 提供唯一的表示)
位置嵌入被用来告诉模型一个标记出现的位置。其主要目的是为了让位置嵌入服务器告诉变压器模型一个单词出现的位置。这是因为,与 LSTMs/GRUs 不同,变压器模型没有序列的概念,因为它们一次处理整个文本。此外,单词位置的改变可能会改变句子或单词的含义。例如,在两个版本中
Ralph loves his tennis ball. It likes to chase the ball Ralph loves his tennis ball. Ralph likes to chase it
单词“it”指的是不同的东西,单词“it”的位置可以用作识别这种差异的线索。原始的变压器论文使用以下方程式生成位置嵌入:
PE(pos,2i) = sin(pos/10000^(21/d[model]))
PE(pos,2i + 1) = cos(pos/10000^(21/d[model]))
其中 pos 表示序列中的位置,i表示i^(th)特征维度(0 ≤ i < d[model])。偶数特征使用正弦函数,而奇数特征使用余弦函数。图 13.2 展示了当时间步长和特征位置变化时位置嵌入的变化。可以看到,具有较高索引的特征位置具有较低频率的正弦波。作者确切的方程式并不完全清楚。但是,他们确实提到他们没有看到前一个方程式和让模型在训练期间联合学习位置嵌入之间有显著的性能差异。
图 13.2 位置嵌入随时间步长和特征位置的变化。偶数特征位置使用正弦函数,而奇数位置使用余弦函数。此外,信号的频率随着特征位置的增加而降低。
需要注意的是,标记嵌入和位置嵌入都具有相同的维度(即d[model])。最后,作为模型的输入,标记嵌入和位置嵌入被求和以形成单个混合嵌入向量(图 13.3)。
图 13.3 在变压器模型中生成的嵌入以及如何计算最终嵌入
13.1.3 残差和规范化
变压器模型的另一个重要特征是残差连接和单个层之间的规范化层的存在。当我们讨论图像分类的高级技术时,我们在第七章中深入讨论了残差连接。让我们简要地重新讨论残差连接的机制和动机。
残差连接是通过将给定层的输出添加到前面一个或多个层的输出而形成的。这反过来形成了模型中的“快捷连接”,并通过减少所谓的梯度消失现象提供了更强的梯度流(见图 13.4)。梯度消失导致最靠近输入的层的梯度非常小,以至于这些层的训练受到阻碍。
图 13.4 残差连接的数学视角
在 Transformer 模型中,每个层次都会创建残差连接,具体如下:
- 多头自注意力子层的输入被添加到多头自注意力子层的输出中。
- 完全连接子层的输入被添加到完全连接子层的输出中。
接下来,通过残差连接增强的输出经过一层层归一化层。层归一化,类似于批归一化,是减少神经网络中“协变量转移”的一种方式,使其能够更快地训练并达到更好的性能。协变量转移是指神经网络激活分布的变化(由数据分布的变化引起),这些变化在模型训练过程中发生。这种分布的变化会在模型训练期间影响一致性,并对模型产生负面影响。层归一化是由 Ba 等人在论文“Layer Normalization”中介绍的(arxiv.org/pdf/1607.06450.pdf
)。
批归一化计算激活的均值和方差作为批次中样本的平均值,导致其性能依赖于用于训练模型的小批量大小。
然而,层归一化计算激活的均值和方差(即归一化项)的方式是这样的,即归一化项对每个隐藏单元都是相同的。换句话说,层归一化对于层中的所有隐藏单元都有一个单一的均值和方差值。这与批归一化不同,后者对层中的每个隐藏单元维护单独的均值和方差值。此外,与批归一化不同,层归一化不会对批次中的样本进行平均,而是留下了平均化,对不同的输入具有不同的归一化项。通过每个样本具有一个均值和方差,层归一化摆脱了对小批量大小的依赖。有关此方法的更多细节,请参阅原始论文。
TensorFlow/Keras 中的层归一化
TensorFlow 提供了层归一化算法的方便实现,网址为 mng.bz/YGRB
。你可以使用 TensorFlow Keras API 定义的任何模型来使用这个层。
图 13.5 展示了在 Transformer 模型中如何使用残差连接和层归一化。
图 13.5 残差连接和层归一化层在 Transformer 模型中的使用方式
讨论关于 Transformer 模型中的组件到此结束。我们已经讨论了 Transformer 模型的所有要点,即自注意力层、全连接层、嵌入(标记和位置)、层归一化和残差连接。在下一节中,我们将讨论如何使用一个称为 BERT 的预训练 Transformer 模型来解决垃圾邮件分类任务。
练习 1
你被给定了以下 Transformer 编码器的代码
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, d, input_length=n_steps )(en_inp) # 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)
其中 EncoderLayer 定义了一个典型的 Transformer 编码器层,其中包含自注意力子层和全连接子层。你被要求使用以下方程式集成位置编码
PE(pos, 2i) = sin(pos/10000^(2i/d[model]))
pos 从 0 到 511(d=512 特征),i 从 0 到 24(n_steps=25 时间步),表示时间步。换句话说,我们的位置编码将是一个形状为 [n_steps, d] 的张量。你可以使用 tf.math.sin() 逐元素生成张量的 sin 值。你可以将位置嵌入定义为张量,而不是 tf.keras.layers.Layer 的乘积。最终嵌入应通过将标记嵌入和位置嵌入相加来生成。你会如何做?
13.2 使用预训练的 BERT 进行垃圾邮件分类
你正在为一家邮件服务公司担任数据科学家,公司渴望实现垃圾邮件分类功能。他们希望在公司内部实现此功能并节省成本。通过阅读关于 BERT 及其在解决 NLP 任务中的强大性能的文章,你向团队解释,你需要做的就是下载 BERT 模型,在 BERT 顶部拟合一个分类层,并在标记的数据上端到端地训练整个模型。标记的数据包括一个垃圾消息和一个指示消息是否为垃圾或正常的标签。你被委托负责实现此模型。
现在我们已经讨论了 Transformer 架构的所有移动元素,这使得我们非常有能力理解 BERT。BERT 是一种基于 Transformer 的模型,由 Devlin 等人在论文 “BERT: Pre-Training of Deep Bidirectional Transformers for Language Understanding” 中介绍(arxiv.org/pdf/1810.04805.pdf
),它代表了自然语言处理历史上的一个非常重要的里程碑,因为它是一个先驱性模型,证明了在 NLP 领域应用 “迁移学习”的能力。
BERT 是一个在大量文本数据上以无监督方式预训练的 Transformer 模型。因此,你可以使用 BERT 作为基础,获得丰富、语义上准确的文本输入序列的数字表示,这些表示可以直接提供给下游 NLP 模型。由于 BERT 提供的丰富文本表示,你可以让你的决策支持模型不再需要理解语言,而是可以直接专注于手头的问题。从技术角度来看,如果你正在用 BERT 解决分类问题,你所需要做的就是
- 在 BERT 之上拟合一个分类器(例如逻辑回归层),将 BERT 的输出作为输入
- 在判别性任务上(即 BERT + 分类器)端到端地训练模型
BERT 的历史
在像 BERT 这样的模型出现之前,解决自然语言处理(NLP)任务既重复又耗时。每次都需要从头开始训练一个模型。更糟糕的是,大多数模型都无法处理长文本序列,限制了它们理解语言的能力。
2017 年,NLP 任务的 Transformer 模型在论文“Attention Is All You Need”(arxiv.org/pdf/1706.03762.pdf
)中提出。Transformer 模型在一系列 NLP 任务上击败了之前的主导者,如 LSTMs 和 GRUs。与逐字逐句查看并维护状态(即内存)的循环模型不同,Transformer 模型一次查看整个序列。
然后,在 2018 年,NLP(即在 NLP 中进行迁移学习)迎来了“ImageNet 时刻”。ImageNet 时刻是指 ML 从业者意识到,在其他任务(如目标检测、图像分割)上使用已经在大型 ImageNet 图像分类数据集上训练过的计算机视觉模型,可以更快地获得更好的性能。这实际上催生了在计算机视觉领域广泛使用的迁移学习概念。因此,直到 2018 年,NLP 领域还没有一个非常好的方法来利用迁移学习来提升任务性能。论文“通用语言模型微调用于文本分类”(arxiv.org/pdf/1801.06146.pdf
)介绍了在语言建模任务上预训练然后在判别性任务上训练模型的思想(例如,分类问题)。这种方法的优势在于,你不需要像从头训练模型那样多的样本。
2018 年,BERT 被引入。这是自然语言处理历史上两个最出色时刻的结合。换句话说,BERT 是一个在大量文本数据上以无监督方式预训练的 Transformer 模型。
现在我们将更详细地了解 BERT 模型。
13.2.1 理解 BERT
现在让我们更微观地检查 BERT。正如我之前提到的,BERT 是一个 Transformer 模型。确切地说,它是 Transformer 模型的编码器部分。这意味着 BERT 接受一个输入序列(一组标记)并生成一个编码的输出序列。图 13.6 描述了 BERT 的高层架构。
图 13.6 BERT 的高层架构。它接受一组输入标记并生成使用几个隐藏层生成的隐藏表示的序列。
当 BERT 接受一个输入时,它会在输入中插入一些特殊的标记。首先,在开始时,它插入一个[CLS](分类的缩写形式)标记,用于生成特定类型任务(例如,序列分类)的最终隐藏表示。它表示在关注序列中的所有标记后的输出。接下来,根据输入类型,它还会插入一个[SEP](即“分隔”)标记。[SEP]标记标记了输入中不同序列的结束和开始。例如,在问答中,模型接受问题和可能包含答案的上下文(例如,段落)作为输入,并且[SEP]在问题和上下文之间使用。
接下来,使用三种不同的嵌入空间生成标记的最终嵌入。标记嵌入为词汇表中的每个标记提供了一个独特的向量。位置嵌入编码了每个标记的位置,如前面讨论的。最后,段落嵌入为输入的每个子组件提供了一个不同的表示。例如,在问答中,问题将具有作为其段落嵌入向量的唯一向量,而上下文将具有不同的嵌入向量。这通过在输入序列中的每个不同组件的n个不同嵌入向量来完成。根据输入中为每个标记指定的组件索引,检索相应的段落嵌入向量。n需要事先指定。
BERT 的真正价值来自于它是以自监督方式在大型语料库上预训练的事实。在预训练阶段,BERT 在两个不同的任务上进行训练:
- 掩码语言建模(MLM)
- 下一句预测(NSP)
掩码语言建模(MLM)任务灵感来自填空题或填空测验,其中学生被给出一句带有一个或多个空白的句子,并被要求填写空白。类似地,给定一个文本语料库,单词从句子中被掩码,然后模型被要求预测掩码的标记。例如,句子
I went to the bakery to buy bread
可能变成
I went to the [MASK] to buy bread
注意:已经有大量基于 Transformer 的模型,每个模型都在前一个模型的基础上进行了构建。您可以在附录 C 中了解更多关于这些模型的信息。
图 13.7 显示了在遮蔽语言建模任务训练过程中 BERT 的主要组成部分。BERT 使用特殊标记([MASK])来表示被遮蔽的单词。然后模型的目标将是单词"bakery"。但这给模型带来了实际问题。特殊标记[MASK]在实际文本中不会出现。这意味着模型在关键问题的微调阶段(即在分类问题上训练时)看到的文本将与在预训练阶段看到的文本不同。这有时被称为预训练-微调不一致性。因此,BERT 的作者建议采取以下方法来处理这个问题。当屏蔽一个单词时,做以下之一:
- 使用[MASK]标记(使用 80%的概率)。
- 使用随机单词(以 10%的概率)。
- 使用真实的单词(以 10%的概率)。
图 13.7 显示了预训练 BERT 使用的方法。BERT 在两个任务上进行预训练:遮蔽语言建模任务和下一句预测任务。在遮蔽语言建模任务中,输入中的标记被遮蔽,模型被要求预测被遮蔽的标记。在下一句预测任务中,模型被要求预测两个句子是否相邻。
接下来,在下一句预测任务中,模型会得到一对句子 A 和 B(按照顺序),并被要求预测 B 是否是 A 之后的下一句。可以通过在 BERT 之上拟合一个二元分类器,并在选择的句子对上端到端地训练整个模型来完成。以无监督的方式生成模型的输入对是不难的:
- 通过选择相邻的两个句子生成标签为 TRUE 的样本。
- 通过随机选择不相邻的两个句子生成标签为 FALSE 的样本。
按照这种方法,为下一句预测任务生成了一个带标签的数据集。然后,使用带标签的数据集对 BERT 和二元分类器进行端到端的训练。图 13.7 突出显示了下一句预测任务中的数据和模型架构。
您可能已经注意到图 13.6 中输入到 BERT 的是特殊标记。除了我们已讨论过的[MASK]标记之外,还有两个特殊的标记具有特殊的用途。
[CLS]标记被附加到输入到 BERT 的任何输入序列上。它表示输入的开始。它还为放置在 BERT 顶部的分类头上使用的输入提供基础。正如您所知,BERT 对序列中的每个输入标记生成一个隐藏表示。按照惯例,与[CLS]标记对应的隐藏表示被用作放置在 BERT 之上的分类模型的输入。
BERT 解决的任务特定 NLP 任务可以分为四种不同的类别。这些基于 General Language Understanding Evaluation(GLUE)基准任务套件中的任务 (gluebenchmark.com
):
- 序列分类—这里,给定一个单一的输入序列,并要求模型为整个序列预测一个标签(例如,情感分析,垃圾邮件识别)。
- 令牌分类—这里,给定一个单一的输入序列,并要求模型为序列中的每个令牌预测一个标签(例如,命名实体识别,词性标注)。
- 问答—这里,输入包括两个序列:一个问题和一个上下文。问题和上下文之间由一个 [SEP] 令牌分隔。模型被训练来预测属于答案的令牌范围的起始和结束索引。
- 多选题—这里的输入由多个序列组成:一个问题,后跟可能是或可能不是问题答案的多个候选项。这些多个序列由令牌 [SEP] 分隔,并作为单个输入序列提供给模型。模型被训练来预测该问题的正确答案(即,类标签)。
BERT 的设计使得它能够在不对基础模型进行任何修改的情况下用于解决这些任务。在涉及多个序列的任务中(例如,问答,多选题),您需要单独告诉模型不同的输入(例如,问题的令牌和问题回答任务中的上下文的令牌)。为了做出这种区分,使用 [SEP] 令牌。[SEP] 令牌在不同序列之间插入。例如,如果您正在解决一个问答任务,您可能会有一个输入如下:
Question: What color is the ball? Paragraph: Tippy is a dog. She loves to play with her red ball.
然后,BERT 的输入可能如下所示
[CLS] What color is the ball [SEP] Tippy is a dog She loves to play with her red ball [SEP]
BERT 还使用段落嵌入空间来表示一个令牌属于哪个序列。例如,只有一个序列的输入对于所有令牌具有相同的段落嵌入向量(例如,垃圾邮件分类任务)。具有两个或更多序列的输入使用第一个或第二个空间,取决于令牌属于哪个序列。例如,在问答中,模型将使用唯一的段落嵌入向量来编码问题的令牌,其中将使用不同的段落嵌入向量来编码上下文的令牌。现在我们已经讨论了使用 BERT 成功解决下游 NLP 任务所需的所有要素。让我们重申一下有关 BERT 的关键要点:
- BERT 是一种基于编码器的 Transformer,经过大量文本的预训练。
- BERT 使用掩码语言建模和下一句预测任务进行模型的预训练。
- BERT 为输入序列中的每个令牌输出隐藏表示。
- BERT 有三个嵌入空间:令牌嵌入,位置嵌入和段落嵌入。
- BERT 使用特殊标记[CLS]来表示输入的开始,并用作下游分类模型的输入。
- BERT 旨在解决四种类型的 NLP 任务:序列分类、标记分类、自由文本问答和多项选择题答案。
- BERT 使用特殊标记[SEP]来分隔序列 A 和序列 B。
接下来,我们将学习如何使用 BERT 对垃圾邮件进行分类。
使用 BERT 在 TensorFlow 中对垃圾邮件进行分类
现在是展示您的技能并以最小的努力实现垃圾邮件分类器的时候了。首先,让我们下载数据。我们将在这个练习中使用的数据是一组垃圾邮件和 ham(非垃圾邮件)短信消息,可在mng.bz/GE9v
获取。下载数据的 Python 代码已在 Ch13-Transormers-with-TF2-and-Huggingface/13.1_Spam_Classification_with_BERT.ipynb 笔记本中提供。
理解数据
一旦您下载并提取数据,我们就可以快速查看数据中的内容。它将是一个单个的制表符分隔的文本文件。文件的前三个条目如下:
ham Go until jurong point, crazy.. Available only in bugis n great ➥ world la e buffet... Cine there got amore wat... ham Ok lar... Joking wif u oni... spam Free entry in 2 a wkly comp to win FA Cup final tkts 21st ➥ May 2005 ...
如图所示,每行以单词 ham 或 spam 开头,表示是否安全或垃圾邮件。然后给出消息中的文本,后跟一个制表符。我们的下一个任务是将此数据加载到内存中,并将输入和标签存储在 NumPy 数组中。以下清单显示了执行此操作的步骤。
清单 13.1 将数据从文本文件加载到 NumPy 数组中
inputs = [] ❶ labels = [] ❷ n_ham, n_spam = 0,0 ❸ with open(os.path.join('data', 'SMSSpamCollection'), 'r') as f: for r in f: ❹ if r.startswith('ham'): ❺ label = 0 ❻ txt = r[4:] ❼ n_ham += 1 ❽ # Spam input elif r.startswith('spam'): ❾ label = 1 ❿ txt = r[5:] ⓫ n_spam += 1 inputs.append(txt) ⓬ labels.append(label) ⓭ # Convert them to arrays inputs = np.array(inputs).reshape(-1,1) ⓮ labels = np.array(labels) ⓯
❶ 输入(消息)存储在这里。
❷ 标签(0/1)存储在这里。
❸ 计算 ham/spam 示例的总数
❹ 读取文件中的每一行。
❺ 如果行以 ham 开头,则为一个 ham 示例。
❻ 将其标记为 0。
❼ 输入是该行中的文本(除了以 ham 开头的单词)。
❽ 增加 n_ham 的计数。
❾ 如果行以 spam 开头,则为垃圾邮件。
❿ 将其标记为 1。
⓫ 输入是该行中的文本(除了以 spam 开头的单词)。
⓬ 将输入文本附加到输入。
⓭ 将标签附加到标签。
⓮ 将输入转换为 NumPy 数组(并将其重新整形为具有一列的矩阵)。
⓯ 将标签列表转换为 NumPy 数组。
您可以打印 n_ham 和 n_spam 变量,并验证有 4827 个 ham 示例和 747 个 spam 示例。换句话说,垃圾邮件示例比 ham 示例少。因此,在训练模型时,我们必须确保考虑到这种不平衡。
处理数据中的类别不平衡
为了抵消类别不平衡,让我们创建平衡的训练/验证和测试数据集。为此,我们将使用 imbalanced-learn 库,这是一个用于操纵不平衡数据集(例如,从不同类别中抽样不同数量的数据)的优秀库。恢复数据集平衡的两种主要策略是:
- 对多数类进行欠采样(从该类中选择较少的样本以用于最终数据集)
- 对少数类进行过采样(为最终数据集生成更多来自该类的样本)
我们将在这里使用第一种策略(即欠采样多数类)。更具体地说,我们将首先
- 通过从数据集中随机抽样数据来创建平衡的测试和验证数据集(每个类别 n 个示例)
- 将剩余的数据分配给训练集,并使用一种称为near-miss算法的算法对训练集中的多数类进行欠采样。
创建训练验证和测试数据的过程如图 13.8 所示。
图 13.8 从原始数据集创建训练、验证和测试数据集的过程
首先,让我们从库和 NumPy 库中导入一些欠采样器:
from imblearn.under_sampling import NearMiss, RandomUnderSampler import numpy as np
接下来,我们将定义一个变量 n,它表示在验证和测试数据集中每个类别要保留多少个样本:
n=100 # Number of instances for each class for test/validation sets random_seed = 4321
接下来我们将定义一个随机欠采样器。最重要的参数是 sampling_strategy 参数,它接受一个字典,其中键是标签,值是该标签所需的样本数量。我们还将通过 random_state 参数传递 random_seed 以确保每次运行代码时都获得相同的结果:
rus = RandomUnderSampler( sampling_strategy={0:n, 1:n}, random_state=random_seed )
然后我们调用欠采样器的 fit_resample()函数,使用我们创建的 inputs 和 labels 数组来采样数据:
rus.fit_resample(inputs, labels)
一旦您适应了欠采样器,您可以使用欠采样器的 sample_indices_ 属性获取所选样本的索引。使用这些索引,我们将创建一对新的数组 test_x 和 test_y 来保存测试数据:
test_inds = rus.sample_indices_ test_x, test_y = inputs[test_inds], np.array(labels)[test_inds]
不在测试数据集中的索引被分配到不同的数组:rest_x 和 rest_y。这些将被用来创建验证数据集和训练数据集:
rest_inds = [i for i in range(inputs.shape[0]) if i not in test_inds] rest_x, rest_y = inputs[rest_inds], labels[rest_inds]
和之前的方法类似,我们从 rest_x 和 rest_y 中欠采样数据来创建验证数据集(valid_x 和 valid_y)。请注意,我们不使用 inputs 和 labels 数组,而是使用这些数组分离出测试数据后剩余的数据:
rus.fit_resample(rest_x, rest_y) valid_inds = rus.sample_indices_ valid_x, valid_y = rest_x[valid_inds], rest_y[valid_inds]
TensorFlow 实战(六)(3)https://developer.aliyun.com/article/1522935