TensorFlow 实战(二)(2)https://developer.aliyun.com/article/1522700
5.2.4 使用标量理解自注意力
目前还不太清楚为什么设计了这样的计算方式。为了理解和可视化这个层正在做什么,我们将假设特征维度为 1. 也就是说,一个单词由一个值(即标量)表示。图 5.6 可视化了如果我们假设单一输入序列和输入的维度(d[model])、查询长度(d[q])、键长度(d[k])和值长度(d[v])的维度都为 1. 在我们所做的假设下,W[q]、W[k] 和 W[v] 将是标量。用于计算 q、k 和 v 的矩阵乘法本质上变成了标量乘法:
q = (q[1], q[2],…, q[7]),其中 q[i] = x[i] W[q]
k = (k[1], k[2],…, k[7]),其中 k[i] = x[i] W[k]
v = (v[1], v[2],…, v[7]),其中 v[i] = x[i] W[v]
接下来,我们需要计算 P = softmax ((Q.K^T) / √(d[k])) 组件。Q.K^T 本质上是一个 n × n 的矩阵,它代表了每个查询和键组合的项(图 5.6)。Q.K[(i,j)]^T 的第 i 行和第 j 列是按如下计算的
Q.K[(i,j)]^T =q [i] × k [j]
然后,通过应用 softmax 函数,该矩阵被转换为行向量的概率分布。你可能已经注意到 softmax 转换中出现了一个常数 √(d[k])。这是一个归一化常数,有助于防止梯度值过大并实现稳定的梯度。在我们的示例中,你可以忽略这个因为 √(d[k]) = 1。
最后,我们计算最终输出 h = (h[1],h[2],…,h[7]),其中
h[i] = P[(i],[1)] v[1] + P[(i],[2)] v[2] +…+ P[(i],[7)] v[7]
在这里,我们可以更清楚地看到 q、k 和 v 之间的关系。当计算最终输出时,q 和 k 被用于计算 v 的软索引机制。例如,当计算第四个输出(即 h[4])时,我们首先对第四行进行硬索引(跟随 q[4]),然后根据该行的列(即 k 值)给出的软索引(即概率),混合各种 v 值。现在更清楚 q、k 和 v 的作用是什么了:
- 查询—帮助构建最终用于索引值(v)的概率矩阵。查询影响矩阵的行,并表示正在处理的当前单词的索引。
- 键—帮助构建最终用于索引值(v)的概率矩阵。键影响矩阵的列,并表示根据查询单词需要混合的候选单词。
- 值—通过使用查询和键创建的概率矩阵进行索引,用于计算最终输出的隐藏(即关注)表示。
您可以轻松地将图 5.6 中的大灰色框放置在自注意子层上,并仍然产生输出形状(如图 5.5 中所示)(图 5.7)。
图 5.6 自注意层中的计算。自注意层从输入序列开始,并计算查询、键和值向量的序列。然后将查询和键转换为概率矩阵,该矩阵用于计算值的加权和。
图 5.7(顶部)和图 5.6(底部)。您可以从底部获取灰色框,并将其插入到顶部的自注意子层中,然后可以看到产生相同的输出序列。
现在让我们扩展我们的自注意层,并重新审视其背后的具体计算及其重要性。回到我们先前的表示法,我们从一个具有 n 个元素的单词序列开始。然后,在嵌入查找之后,为每个单词检索一个嵌入向量,我们有一个大小为 n × d[model] 的矩阵。接下来,我们有权重和偏差来计算每个查询、键和值向量:
q = xW[q],其中 x ∈ ℝ^(n×dmodel)。W[q] ∈ ℝ^(dmodel×dq),而 q ∈ ℝ^(n×d)q
k = xW[k],其中 x ∈ ℝ^(n×dmodel)。W[k] ∈ ℝ^(dmodel×dk),而 k ∈ ℝ^(n×d)k
v = xW[v],其中 x ∈ ℝ^(n×dmodel)。W[v] ∈ ℝ^(dmodel×dv),而 v ∈ ℝ^(n×d)v
例如,查询,或 q,是一个大小为 n × d[q] 的向量,通过将大小为 n × d[model] 的输入 x 与大小为 d[model] × d[q] 的权重矩阵 W[q] 相乘获得。还要记住,正如在原始 Transformer 论文中一样,我们确保查询、键和值向量的所有输入嵌入大小相同。换句话说,
d[model] = d[q] = d[k] = d[v] = 512
接下来,我们使用我们获得的 q 和 k 值计算概率矩阵:
最后,我们将这个概率矩阵与我们的值矩阵相乘,以获得自注意力层的最终输出:
自注意力层接受一批词序列(例如,一批具有固定长度的句子),其中每个词由一个向量表示,并产生一批隐藏输出序列,其中每个隐藏输出是一个向量。
自注意力与循环神经网络(RNNs)相比如何?
在 Transformer 模型出现之前,RNNs 主导了自然语言处理的领域。RNNs 在 NLP 问题中很受欢迎,因为大多数问题本质上都是时间序列问题。你可以将句子/短语视为一系列单词(即每个单词由一个特征向量表示)在时间上的分布。RNN 通过这个序列,一次消耗一个单词(同时保持一个记忆/状态向量),并在最后产生一些输出(或一系列输出)。但是你会发现,随着序列长度的增加,RNN 的表现越来越差。这是因为当 RNN 到达序列末尾时,它可能已经忘记了开始时看到的内容。
你可以看到,自注意力机制缓解了这个问题,它允许模型在给定时间内查看完整的序列。这使得 Transformer 模型比基于 RNN 的模型表现得好得多。
5.2.5 自注意力作为烹饪比赛
自注意力的概念可能仍然有点难以捉摸,这使得理解自注意力子层中究竟发生了什么变得困难。以下类比可能会减轻负担并使其更容易理解。假设你参加了一个与其他六位选手(总共七位选手)一起的烹饪节目。游戏如下。
你在超市里拿到一件印有号码(从 1 到 7)的 T 恤和一个手推车。超市有七个过道。你必须飞奔到印有 T 恤上号码的过道,墙上会贴着某种饮料的名称(例如,苹果汁,橙汁,酸橙汁)。你需要挑选制作该饮料所需的物品,然后飞奔到你分配的桌子上,制作那种饮料。
假设你是号码 4 并且拿到了橙汁,所以你会前往 4 号过道并收集橙子,一点盐,一颗酸橙,糖等等。现在假设你旁边的对手(编号 3)要制作酸橙汁;他们会挑选酸橙,糖和盐。正如你所看到的,你们选取了不同的物品以及相同物品的不同数量。例如,你的对手没有选择橙子,但你选择了,并且你可能选择了较少的酸橙,与你正在制作酸橙汁的对手相比。
这与自注意力层中发生的情况非常相似。你和你的竞争对手是模型的输入(在一个时间步上)。通道是查询,你需要选择的货品是键。就像通过查询和键来索引概率矩阵以获得“混合系数”(即注意力权重)来获取值一样,你可以通过分配给你的通道号(即查询)和通道中每个货品的数量(即键)来索引你所需要的货品。最后,你制作的饮料就是值。请注意,这个类比并不完全对应于自注意力子层中的计算。然而,你可以在抽象层面上发现这两个过程之间的显著相似之处。我们发现的相似之处如图 5.8 所示。
图 5.8 以烹饪比赛为背景描述的自注意力。选手是查询,货品是你需要选择的食材,值是你制作的最终饮料。
接下来我们将讨论什么是蒙版自注意力层。
5.2.6 蒙版自注意力层
正如你已经看到的,解码器有一个特殊的额外的自注意子层,称为蒙版自注意力。正如我们已经提到的,这个想法是防止模型通过关注不应关注的单词(也就是模型预测位置之前的单词)来“作弊”。为了更好地理解这一点,假设有两个人在教一个学生从英语翻译成法语。第一个人给出一个英语句子,要求学生逐词翻译,同时给出到目前为止已经翻译的反馈。第二个人给出一个英语句子,要求学生翻译,但提前提供完整的翻译。在第二种情况下,学生很容易作弊,提供高质量的翻译,虽然对语言几乎一无所知。现在让我们从机器学习的角度来理解关注不应关注的单词的潜在危险。
假设我们要将句子 “dogs are great” 翻译为 “les chiens sont super。” 当处理句子 “Dogs are great” 时,模型应该能够关注该句子中的任何单词,因为这是模型在任何给定时间完全可用的输入。但是,在处理句子 “Les chiens sont super” 时,我们需要注意向模型展示什么和不展示什么。例如,在训练模型时,我们通常一次性提供完整的输出序列,而不是逐字节地提供单词,以增强计算效率。在向解码器提供完整输出序列时,我们必须屏蔽当前正在处理的单词之前的所有单词,因为让模型在看到该单词之后的所有内容后预测单词 “chiens” 是不公平的。这是必须做的。如果不这样做,代码会正常运行。但最终,当你将其带到现实世界时,性能会非常差。强制执行这一点的方法是将概率矩阵 p 设为下三角矩阵。这将在注意力/输出计算期间基本上为混合输入的任何内容赋予零概率。标准自注意力和蒙版自注意力之间的差异如图 5.9 所示。
图 5.9 标准自注意力与蒙版自注意力方法。在标准注意力方法中,给定步骤可以看到来自当前时间步之前或之后的任何其他时间步的输入。然而,在蒙版自注意力方法中,当前时间步只能看到当前输入和之前的时间步。
让我们学习如何在 TensorFlow 中实现这一点。我们对 call() 函数进行了非常简单的更改,引入了一个新参数 mask,该参数表示模型不应该看到的项目,用 1 表示,其余项目用 0 表示。然后,对于模型不应该看到的元素,我们添加一个非常大的负数(即 - 10⁹),以便在应用 softmax 时它们变成零(见清单 5.2)。
清单 5.2 蒙版自注意力子层
import tensorflow as tf class SelfAttentionLayer(layers.Layer): def __init__(self, d): ... def build(self, input_shape): ... def call(self, q_x, k_x, v_x, mask=None): ❶ q = tf.matmul(x,self.Wq) k = tf.matmul(x,self.Wk) v = tf.matmul(x,self.Wv) p = tf.matmul(q, k, transpose_b=True)/math.sqrt(self.d) p = tf.squeeze(p) if mask is None: p = tf.nn.softmax(p) ❷ else: p += mask * -1e9 ❸ p = tf.nn.softmax(p) ❸ h = tf.matmul(p, v) return h,p
❶ call 函数接受额外的蒙版参数(即 0 和 1 的矩阵)。
❷ 现在,SelfAttentionLayer 支持蒙版和非蒙版输入。
❸ 如果提供了蒙版,添加一个大的负值以使最终概率为零,以防止看到的单词。
创建蒙版很容易;您可以使用 tf.linalg.band_part() 函数创建三角矩阵
mask = 1 - tf.linalg.band_part(tf.ones((7, 7)), -1, 0)
给出
>>> tf.Tensor( [[0\. 1\. 1\. 1\. 1\. 1\. 1.] [0\. 0\. 1\. 1\. 1\. 1\. 1.] [0\. 0\. 0\. 1\. 1\. 1\. 1.] [0\. 0\. 0\. 0\. 1\. 1\. 1.] [0\. 0\. 0\. 0\. 0\. 1\. 1.] [0\. 0\. 0\. 0\. 0\. 0\. 1.] [0\. 0\. 0\. 0\. 0\. 0\. 0.]], shape=(7, 7), dtype=float32)
我们可以通过查看概率矩阵 p 来轻松验证屏蔽是否起作用。它必须是一个下三角矩阵
layer = SelfAttentionLayer(512) h, p = layer(x, x, x, mask) print(p.numpy())
给出
>>> [[1\. 0\. 0\. 0\. 0\. 0\. 0\. ] [0.37 0.63 0\. 0\. 0\. 0\. 0\. ] [0.051 0.764 0.185 0\. 0\. 0\. 0\. ] [0.138 0.263 0.072 0.526 0\. 0\. 0\. ] [0.298 0.099 0.201 0.11 0.293 0\. 0\. ] [0.18 0.344 0.087 0.25 0.029 0.108 0\. ] [0.044 0.044 0.125 0.284 0.351 0.106 0.045]]
现在,在计算值时,模型无法看到或关注到它在处理当前单词时尚未看到的单词。
5.2.7 多头注意力
原始 Transformer 论文中讨论了一种称为多头注意力的方法,它是自注意力层的扩展。一旦理解了自注意机制,这个想法就很简单。多头注意力创建多个并行的自注意力头。这样做的动机是,当模型有机会为输入序列学习多个注意力模式(即多组权重)时,它的性能更好。
记住,在单个注意力头中,所有的查询、键和值的维度都设置为 512。换句话说,
d[q] = d[k] = d[v] = 512
使用多头注意力,假设我们使用八个注意力头,
d[q] = d[k] = d[v] = 512/8 = 64
然后将所有注意力头的最终输出连接起来,形成最终输出,它的维度将为 64 × 8 = 512
H = Concat (h¹, h², … , h⁸)
其中h^i 是第i个注意力头的输出。使用刚刚实现的 SelfAttentionLayer,代码变为
multi_attn_head = [SelfAttentionLayer(64) for i in range(8)] outputs = [head(x, x, x)[0] for head in multi_attn_head] outputs = tf.concat(outputs, axis=-1) print(outputs.shape)
得到
>>> (1, 7, 512)
如你所见,它仍然具有之前的相同形状(没有多个头)。然而,此输出是使用多个头进行计算的,这些头的维度比原始的自注意层要小。
5.2.8 全连接层
与我们刚刚学习的内容相比,全连接层更加简单。到目前为止,自注意力层产生了一个n×d[v]大小的输出(忽略批处理维度)。全连接层将输入数据进行以下转换
h[1] = ReLU(xW[1] + b[1])
这里,W[1]是一个d[v] × d[ff1]的矩阵,b[1]是一个d[ff1]大小的向量。因此,这个操作产生一个n×d[ff1]大小的张量。结果输出传递到另一层,进行以下计算
h[2] = h[1] W[2] + b [2]
这里W[2]是一个d[ff1] × d[ff2]大小的矩阵,b[2]是一个d[ff2]大小的向量。该操作得到一个大小为n×d[ff2]的张量。在 TensorFlow 中,我们可以将这些计算再次封装成一个可重用的 Keras 层(见下一个列表)。
列表 5.3 全连接子层
import tensorflow as tf class FCLayer(layers.Layer): def __init__(self, d1, d2): super(FCLayer, self).__init__() self.d1 = d1 ❶ self.d2 = d2 ❷ def build(self, input_shape): self.W1 = self.add_weight( ❸ shape=(input_shape[-1], self.d1), initializer='glorot_uniform',❸ trainable=True, dtype='float32' ❸ ) self.b1 = self.add_weight( ❸ shape=(self.d1,), initializer='glorot_uniform', ❸ trainable=True, dtype='float32' ❸ ) self.W2 = self.add_weight( ❸ shape=(input_shape[-1], self.d2), initializer='glorot_uniform',❸ trainable=True, dtype='float32' ❸ ) self.b2 = self.add_weight( ❸ shape=(self.d2,), initializer='glorot_uniform', ❸ trainable=True, dtype='float32' ❸ ) def call(self, x): ff1 = tf.nn.relu(tf.matmul(x,self.W1)+self.b1) ❹ ff2 = tf.matmul(ff1,self.W2)+self.b2 ❺ return ff2
❶ 第一个全连接计算的输出维度
❷ 第二个全连接计算的输出维度
❸ 分别定义 W1、b1、W2 和 b2。我们使用 glorot_uniform 作为初始化器。
❹ 计算第一个全连接计算
❺ 计算第二个全连接计算
在这里,你可以使用 tensorflow.keras.layers.Dense()层来实现此功能。然而,我们将使用原始的 TensorFlow 操作进行练习,以熟悉低级 TensorFlow。在这个设置中,我们将改变 FCLayer,如下面的列表所示。
列表 5.4 使用 Keras Dense 层实现的全连接层
import tensorflow as tf import tensorflow.keras.layers as layers class FCLayer(layers.Layer): def __init__(self, d1, d2): super(FCLayer, self).__init__() self.dense_layer_1 = layer.Dense(d1, activation='relu') ❶ self.dense_layer_2 = layers.Dense(d2) ❷ def call(self, x): ff1 = self.dense_layer_1(x) ❸ ff2 = self.dense_layer_2(ff1) ❹ return ff2
❶ 在子类化层的 init 函数中定义第一个全连接层
❷ 定义第二个稠密层。注意我们没有指定激活函数。
❸ 调用第一个稠密层以获取输出
❹ 使用第一个稠密层的输出调用第二个稠密层以获取最终输出
现在你知道了 Transformer 架构中进行的计算以及如何使用 TensorFlow 实现它们。但请记住,原始 Transformer 论文中解释了各种细微的细节,我们还没有讨论。这些细节大多将在后面的章节中讨论。
练习 2
假设你被要求尝试一种新型的多头注意力机制。与其将较小头的输出(大小为 64)连接起来,而是将输出(大小为 512)相加。使用 SelfAttentionLayer 编写 TensorFlow 代码以实现此效果。您可以使用 tf.math.add_n() 函数按元素对张量列表求和。
将所有内容放在一起 5.2.9
让我们将所有这些元素放在一起创建一个 Transformer 网络。首先让我们创建一个编码器层,其中包含一组 SelfAttentionLayer 对象(每个头一个)和一个 FCLayer(请参阅下一个列表)。
列表 5.5 编码器层
import tensorflow as tf class EncoderLayer(layers.Layer): def __init__(self, d, n_heads): super(EncoderLayer, self).__init__() self.d = d self.d_head = int(d/n_heads) self.n_heads = n_heads self.attn_heads = [ SelfAttentionLayer(self.d_head) for i in range(self.n_heads) ] ❶ self.fc_layer = FCLayer(2048, self.d) ❷ def call(self, x): def compute_multihead_output(x): ❸ outputs = [head(x, x, x)[0] for head in self.attn_heads] outputs = tf.concat(outputs, axis=-1) return outputs h1 = compute_multihead_output(x) ❹ y = self.fc_layer(h1) ❺ return y
❶ 创建多个注意力头。每个注意力头具有 d/n_heads 大小的特征维度。
❷ 创建完全连接的层,其中中间层有 2,048 个节点,最终子层有 d 个节点。
❸ 创建一个函数,给定一个输入来计算多头注意力输出。
❹ 使用定义的函数计算多头注意力。
❺ 获取层的最终输出。
在初始化 EncoderLayer 时,EncoderLayer 接受两个参数:d(输出的维度)和 n_heads(注意力头的数量)。然后,在调用层时,传递一个单一的输入 x。首先计算注意力头(SelfAttentionLayer)的关注输出,然后是完全连接层(FCLayer)的输出。这就包装了编码器层的关键点。接下来,我们创建一个解码器层(请参阅下一个列表)。
列表 5.6 解码器层
import tensorflow as tf class DecoderLayer(layers.Layer): def __init__(self, d, n_heads): super(DecoderLayer, self).__init__() self.d = d self.d_head = int(d/n_heads) self.dec_attn_heads = [ SelfAttentionLayer(self.d_head) for i in range(n_heads) ] ❶ self.attn_heads = [ SelfAttentionLayer(self.d_head) for i in range(n_heads) ] ❷ self.fc_layer = FCLayer(2048, self.d) ❸ def call(self, de_x, en_x, mask=None): def compute_multihead_output(de_x, en_x, mask=None): ❹ outputs = [ head(en_x, en_x, de_x, mask)[0] for head in ➥ self.attn_heads] ❺ outputs = tf.concat(outputs, axis=-1) return outputs h1 = compute_multihead_output(de_x, de_x, mask) ❻ h2 = compute_multihead_output(h1, en_x) ❼ y = self.fc_layer(h2) ❽ return y
❶ 创建处理解码器输入的注意力头。
❷ 创建同时处理编码器输出和解码器输入的注意力头。
❸ 最终完全连接的子层
❹ 计算多头注意力的函数。此函数接受三个输入(解码器的先前输出、编码器输出和可选的掩码)。
❺ 每个头将函数的第一个参数作为查询和键,并将函数的第二个参数作为值。
❻ 计算第一个受关注的输出。这仅查看解码器输入。
❼ 计算第二个受关注的输出。这将查看先前的解码器输出和编码器输出。
❽ 通过完全连接的子层将输出计算为层的最终输出。
解码器层与编码器层相比有几个不同之处。它包含两个多头注意力层(一个被屏蔽,一个未被屏蔽)和一个全连接层。首先计算第一个多头注意力层(被屏蔽)的输出。请记住,我们会屏蔽任何超出当前已处理的解码器输入的解码器输入。我们使用解码器输入来计算第一个注意力层的输出。然而,第二层中发生的计算有点棘手。做好准备!第二个注意力层将编码器网络的最后一个被关注的输出作为查询和键;然后,为了计算值,使用第一个注意力层的输出。将这一层看作是一个混合器,它混合了被关注的编码器输出和被关注的解码器输入。
有了这个,我们可以用两个编码器层和两个解码器层创建一个简单的 Transformer 模型)。我们将使用 Keras 函数式 API(见下一个列表)。
列表 5.7 完整的 Transformer 模型
import tensorflow as tf n_steps = 25 ❶ n_en_vocab = 300 ❶ n_de_vocab = 400 ❶ n_heads = 8 ❶ d = 512 ❶ mask = 1 - tf.linalg.band_part(tf.ones((n_steps, n_steps)), -1, 0) ❷ en_inp = layers.Input(shape=(n_steps,)) ❸ en_emb = layers.Embedding(n_en_vocab, 512, input_length=n_steps)(en_inp) ❹ en_out1 = EncoderLayer(d, n_heads)(en_emb) ❺ en_out2 = EncoderLayer(d, n_heads)(en_out1) de_inp = layers.Input(shape=(n_steps,)) ❻ de_emb = layers.Embedding(n_de_vocab, 512, input_length=n_steps)(de_inp) ❼ de_out1 = DecoderLayer(d, n_heads)(de_emb, en_out2, mask) ❽ de_out2 = DecoderLayer(d, n_heads)(de_out1, en_out2, mask) de_pred = layers.Dense(n_de_vocab, activation='softmax')(de_out2) ❾ transformer = models.Model( inputs=[en_inp, de_inp], outputs=de_pred, name='MinTransformer' ❿ ) transformer.compile( loss='categorical_crossentropy', optimizer='adam', metrics=['acc'] )
❶ Transformer 模型的超参数
❷ 用于屏蔽解码器输入的掩码
❸ 编码器的输入层。它接受一个批量的单词 ID 序列。
❹ 嵌入层将查找单词 ID 并返回该 ID 的嵌入向量。
❺ 计算第一个编码器层的输出。
❻ 解码器的输入层。它接受一个批量的单词 ID 序列。
❼ 解码器的嵌入层
❽ 计算第一个解码器层的输出。
❾ 预测正确输出序列的最终预测层
❿ 定义模型。注意我们为模型提供了一个名称。
在深入细节之前,让我们回顾一下 Transformer 架构的外观(图 5.10)。
图 5.10 Transformer 模型架构
由于我们已经相当深入地探讨了底层元素,因此网络应该非常易于理解。我们所要做的就是设置编码器模型,设置解码器模型,并通过创建一个 Model 对象来适当地组合这些内容。最初我们定义了几个超参数。我们的模型接受长度为 n_steps 的句子。这意味着如果给定句子的长度小于 n_steps,则我们将填充一个特殊的标记使其长度为 n_steps。如果给定句子的长度大于 n_steps,则我们将截断句子至 n_steps 个词。n_steps 值越大,句子中保留的信息就越多,但模型消耗的内存也越多。接下来,我们有编码器输入的词汇表大小(即,馈送给编码器的数据集中唯一单词的数量)(n_en_vocab)、解码器输入的词汇表大小(n_de_vocab)、头数(n_heads)和输出维度(d)。
有了这个,我们定义了编码器输入层,它接受一个批次的 n_steps 长句子。在这些句子中,每个词都将由一个唯一的 ID 表示。例如,句子“The cat sat on the mat”将被转换为[1, 2, 3, 4, 1, 5]。接下来,我们有一个称为嵌入(Embedding)的特殊层,它为每个词提供了一个 d 元素长的表示(即,词向量)。在这个转换之后,您将得到一个大小为(批量大小,n_steps,d)的输出,这是应该进入自注意力层的输出格式。我们在第三章(第 3.4.3 节)中简要讨论了这种转换。嵌入层本质上是一个查找表。给定一个唯一的 ID(每个 ID 代表一个词),它会给出一个 d 元素长的向量。换句话说,这一层封装了一个大小为(词汇量大小,d)的大矩阵。当定义嵌入层时,您可以看到:
layers.Embedding(n_en_vocab, 512, input_length=n_steps)
我们需要提供词汇量大小(第一个参数)和输出维度(第二个参数),最后,由于我们正在处理长度为 n_steps 的输入序列,我们需要指定 input_length 参数。有了这个,我们就可以将嵌入层的输出(en_emb)传递给一个编码器层。您可以看到我们的模型中有两个编码器层。
下一步,转向解码器,从高层面看,一切都与编码器相同,除了两个不同之处:
- 解码器层将编码器输出(en_out2)和解码器输入(de_emb 或 de_out1)作为输入。
- 解码器层还有一个最终的稠密层,用于生成正确的输出序列(例如,在机器翻译任务中,这些将是每个时间步长的翻译词的概率)。
您现在可以定义和编译模型为
transformer = models.Model( inputs=[en_inp, de_inp], outputs=de_pred, name=’MinTransformer’ ) transformer.compile( loss='categorical_crossentropy', optimizer='adam', metrics=['acc'] )
请注意,在定义模型时,我们可以为其提供一个名称。我们将我们的模型命名为“MinTransformer”。作为最后一步,让我们看一下模型摘要,
transformer.summary()
这将提供以下输出:
Model: "MinTransformer" _____________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ============================================================================================= input_1 (InputLayer) [(None, 25)] 0 _____________________________________________________________________________________________ embedding (Embedding) (None, 25, 512) 153600 input_1[0][0] _____________________________________________________________________________________________ input_2 (InputLayer) [(None, 25)] 0 _____________________________________________________________________________________________ encoder_layer (EncoderLayer) (None, 25, 512) 2886144 embedding[0][0] _____________________________________________________________________________________________ embedding_1 (Embedding) (None, 25, 512) 204800 input_2[0][0] _____________________________________________________________________________________________ encoder_layer_1 (EncoderLayer) (None, 25, 512) 2886144 encoder_layer[0][0] _____________________________________________________________________________________________ decoder_layer (DecoderLayer) (None, 25, 512) 3672576 embedding_1[0][0] encoder_layer_1[0][0] _____________________________________________________________________________________________ decoder_layer_1 (DecoderLayer) (None, 25, 512) 3672576 decoder_layer[0][0] encoder_layer_1[0][0] _____________________________________________________________________________________________ dense (Dense) (None, 25, 400) 205200 decoder_layer_1[0][0] ============================================================================================= Total params: 13,681,040 Trainable params: 13,681,040 Non-trainable params: 0 _____________________________________________________________________________________________
工作坊参与者将高高兴兴地离开这个工作坊。您已经介绍了 Transformer 网络的基本要点,同时教导参与者实现自己的网络。我们首先解释了 Transformer 具有编码器-解码器架构。然后,我们看了编码器和解码器的组成,它们由自注意力层和全连接层组成。自注意力层允许模型在处理给定输入词时关注其他输入词,这在处理自然语言时非常重要。我们还看到,在实践中,模型在单个注意力层中使用多个注意力头以提高性能。接下来,全连接层创建了所关注输出的非线性表示。在理解基本要素之后,我们使用我们为自注意力层(SelfAttentionLayer)和全连接层(FCLayer)创建的可重用自定义层实现了一个基本的小规模 Transformer 网络。
下一步是在 NLP 数据集上训练这个模型(例如机器翻译)。然而,训练这些模型是一个单独章节的主题。 Transformers 比我们讨论的还要复杂得多。例如,有预训练的基于 Transformer 的模型,你可以随时使用它们来解决 NLP 任务。我们将在后面的章节再次讨论 Transformers。
总结
- Transformer 网络在几乎所有 NLP 任务中都表现优于其他模型。
- Transformer 是一种主要用于学习 NLP 任务的编码器 - 解码器型神经网络。
- 使用 Transformer,编码器和解码器由两个计算子层组成:自我注意层和完全连接层。
- 自我注意层根据处理当前位置时,与序列中其他位置之间的相对重要性产生一个给定时间步长的输入的加权和。
- 完全连接层对自我注意层产生的注意输出进行了非线性表示。
- 解码器在其自我注意层中使用掩码,以确保在产生当前预测时,解码器不会看到任何未来的预测。
练习答案
练习 1
Wq = tf.Variable(np.random.normal(size=(256,512))) Wk = tf.Variable (np.random.normal(size=(256,512))) Wv = tf.Variable (np.random.normal(size=(256,512)))
练习 2
multi_attn_head = [SelfAttentionLayer(512) for i in range(8)] outputs = [head(x)[0] for head in multi_attn_head] outputs = tf.math.add_n(outputs)
第二部分:瞧,无需双手!深度网络在现实世界中
一个精通机器学习的从业者是一个多面手。他们不仅需要对现代深度学习框架如 TensorFlow 有很好的理解,还需要能够熟练运用其提供的复杂 API 来实现复杂的深度学习模型,以解决计算机视觉和自然语言处理等领域常见的一些机器学习问题。
在第二部分,我们将看一下计算机视觉和自然语言处理中的真实世界问题。首先,我们来看图像分类和图像分割,这是两个流行的计算机视觉任务。对于这些任务,我们分析了在给定问题上表现良好的现代复杂深度学习模型。我们不仅会从头开始实现这些模型,还会理解核心设计决策背后的推理和它们带来的优势。
接下来,我们转向自然语言处理。我们首先看一下情感分析任务以及深度学习如何解决它。我们还探讨解决方案的各个角落,例如基本的 NLP 预处理步骤以及使用词向量来提升性能。然后,我们看一下语言建模:这是一个预训练任务,为现代 NLP 模型带来了巨大的语言理解能力。在这次讨论中,我们再次探讨了语言建模中融入的各种技术,以提高预测质量。
第六章:教机器看图像分类和 CNN
本章涵盖内容
- 在 Python 中对图像数据进行探索性数据分析
- 预处理和通过图像流水线提供数据
- 使用 Keras 功能 API 实现复杂的 CNN 模型
- 训练和评估 CNN 模型
我们已经对 CNN 做了相当多的工作。CNN 是一种可以处理二维数据(如图像)的网络类型。CNN 使用卷积操作通过在图像上移动一个核(即一个小的值网格)来创建图像(即像素的网格)的特征图,从而产生新的值。CNN 具有多个这样的层,随着它们的深入,它们生成越来越高级的特征图。您还可以在卷积层之间使用最大或平均汇聚层来减少特征图的维数。汇聚层也会在特征图上移动核以创建输入的较小表示。最终的特征图连接到一系列完全连接的层,其中最后一层产生预测结果(例如,图像属于某个类别的概率)。
我们使用 Keras Sequential API 实现了 CNN。我们使用了各种 Keras 层,如 Conv2D、MaxPool2D 和 Dense,以便轻松地实现 CNN。我们已经学习了与 Conv2D 和 MaxPool2D 层相关的各种参数,如窗口大小、步幅和填充方式。
在本章中,我们将更接近地看到卷积神经网络(CNN)在解决令人兴奋的问题时在真实世界数据上的表现。机器学习不仅仅是实现一个简单的 CNN 来学习高度策划的数据集,因为真实世界的数据往往是杂乱无序的。您将学习到探索性数据分析,这是机器学习生命周期的核心。您将探索一个图像数据集,目标是识别图像中的对象(称为图像分类)。然后,我们将深入研究计算机视觉领域的一个最先进的模型,即 inception 模型。在深度学习中,广泛认可的神经网络架构(或模板)在特定任务上表现良好。inception 模型是一种在图像数据上表现出色的模型之一。我们将研究模型的架构以及其中使用的几个新颖设计概念的动机。最后,我们将训练在我们探索过的数据集上的模型,并依靠准确性等指标分析模型的性能。
我们走了很长一段路。我们理解了那里存在的主要深度学习算法的技术方面,并且对我们正确执行探索性数据分析的能力充满信心,因此以信心进入模型阶段。然而,深度网络很快就会变得非常庞大。复杂的网络会牵扯到各种计算和性能问题。因此,任何希望在实际问题中使用这些算法的人都需要学习那些在复杂学习任务中已被证明执行良好的现有模型。
6.1 将数据置于显微镜下:探索性数据分析
你正在与一组数据科学家合作构建一个多才多艺的图像分类模型。最终目标是将此模型用作智能购物助手的一部分。用户可以上传家里内部的照片,助手将根据他们的风格找到合适的产品。团队决定从图像分类模型开始。你需要回到团队,拿到一个很棒的数据集并解释数据的样子以及为什么这个数据集很棒。数据集包含在现实世界中拍摄的日常物品,你将进行探索性数据分析并查看数据集的各种属性(例如,可用类别,数据集大小,图像属性)来了解数据,并识别和解决潜在问题。
探索性数据分析(EDA)是数据科学项目中你将要做的技术发展的基石。该过程的主要目标是通过消除离群值和噪音等烦人问题,最终获得高质量干净的数据集。为了拥有这样的数据集,你需要仔细审查数据,并找出是否存在
- 类别不平衡(在分类问题中)
- 损坏的数据
- 缺失的特征
- 离群值
- 需要各种转换的特征(例如,标准化,独热编码)
这绝不是一份详尽的需要注意的事项清单。你进行的探索越多,数据质量就会越好。
在进行探索性数据分析之前发生了什么?
机器学习问题总是源于业务问题。一旦问题得到适当的确认和理解,你可以开始考虑数据:我们有什么数据?我们训练模型来预测什么?这些预测如何转化为为公司带来好处的可操作见解?在勾选这些问题之后,你可以通过探索性数据分析来检索并开始处理数据。毕竟,机器学习项目中的每一步都需要有目的性地完成。
你已经花了几天时间研究,找到了一个适合你问题的数据集。为了开发一个能够理解客户风格偏好的智能购物助手,它应该能够从客户上传的照片中识别尽可能多的家居物品。为此,你计划使用 tiny-imagenet-200 (www.kaggle.com/c/tiny-imagenet
)数据集。
ImageNet 数据集
Tiny ImageNet 是原始 ImageNet 数据集(www.kaggle.com/competitions/imagenet-object-localization-challenge
)的一个规模较小的重制版,它是年度 ImageNet 大规模视觉识别挑战(ILSVRC)的一部分。每年,全球各地的研究团队竞争开发最先进的图像分类和检测模型。这个数据集拥有大约 1.2 百万张标记的图像,分布在 1,000 个类别中,已成为计算机视觉领域最大的标记图像数据集之一。
这个数据集包含属于 200 个不同类别的图像。图 6.1 展示了一些可用类别的图像。
图 6.1 tiny-imagenet-200 的一些样本图像。你可以看到这些图像属于各种不同的类别。
首先,我们需要下载数据集。下面的代码将在你的工作目录中创建一个名为 data 的文件夹,下载包含数据的 zip 文件,并为你解压缩。最终,你应该在 data 文件夹中有一个名为 tiny-imagenet-200 的文件夹:
import os import requests import zipfile if not os.path.exists(os.path.join('data','tiny-imagenet-200.zip')): url = "http:/ /cs231n.stanford.edu/tiny-imagenet-200.zip" r = requests.get(url) if not os.path.exists('data'): os.mkdir('data') with open(os.path.join('data','tiny-imagenet-200.zip'), 'wb') as f: f.write(r.content) with zipfile.ZipFile( os.path.join('data','tiny-imagenet-200.zip'), 'r' ) as zip_ref: zip_ref.extractall('data') else: print("The file already exists.")
TensorFlow 实战(二)(4)https://developer.aliyun.com/article/1522703