TensorFlow 实战(二)(2)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: TensorFlow 实战(二)

TensorFlow 实战(二)(1)https://developer.aliyun.com/article/1522699

4.3 一步一步地:递归神经网络(RNNs)

您是国家气象局的机器学习顾问。他们拥有过去三十年的 CO2 浓度数据。您被委托开发一个机器学习模型,预测未来五年的 CO2 浓度。您计划实现一个简单的 RNN,它接受 CO2 浓度序列(在本例中,过去 12 个月的值)并预测序列中的下一个值。

很明显,我们面对的是一个时间序列问题。这与我们以往解决的任务非常不同。在以前的任务中,一个输入不依赖于先前的输入。换句话说,您认为每个输入都是 i.i.d(独立同分布)的输入。然而,在这个问题中,情况并非如此。今天的 CO2 浓度将取决于过去几个月的 CO2 浓度。

典型的前馈网络(即全连接网络、CNNs)在没有特殊适应的情况下无法从时间序列数据中学习。然而,有一种特殊类型的神经网络专门设计用于从时间序列数据中学习。这些网络通常被称为 RNNs。RNNs 不仅使用当前输入进行预测,而且在给定时间步长时还使用网络的记忆,从过去的时间步长。图 4.14 描述了前馈网络和 RNN 在预测几个月内 CO2 浓度时的差异。正如您所看到的,如果您使用前馈网络,它必须仅基于上个月来预测下个月的 CO2 水平,而 RNN 则会查看所有以前的月份。


图 4.14 以 CO2 浓度水平预测任务为例,前馈网络和 RNN 之间的操作差异

4.3.1 理解数据

数据集非常简单(从datahub.io/core/co2-ppm/r/co2-mm-gl.csv下载)。每个数据点都有一个日期(YYYY-MM-DD 格式)和一个浮点值,表示 CSV 格式中的 CO2 浓度。数据以 CSV 文件的形式提供给我们。让我们按如下方式下载文件:

import requests
import os
def download_data():
    """ This function downloads the CO2 data from 
    https:/ /datahub.io/core/co2-ppm/r/co2-mm-gl.csv
    if the file doesn't already exist
    """
    save_dir = "data"
    save_path = os.path.join(save_dir, 'co2-mm-gl.csv')
    # Create directories if they are not there
    if not os.path.exists(save_dir):
        os.makedirs(save_dir)
    # Download the data and save
    if not os.path.exists(save_path):
        url = "https:/ /datahub.io/core/co2-ppm/r/co2-mm-gl.csv"
        r = requests.get(url)
        with open(save_path, 'wb') as f:
            f.write(r.content)
    else:
        print("co2-mm-gl.csv already exists. Not downloading.")
    return save_path
# Downloading the data
save_path = download_data()

我们可以使用 pandas 轻松加载这个数据集:

import pandas as pd
data = pd.read_csv(save_path)

现在我们可以看一下数据的样子,使用 head() 操作,它将提供数据框中的前几个条目:

data.head()

这将得到类似图 4.15 的东西。


图 4.15 数据集中的示例数据

在这个数据集中,我们唯一感兴趣的两列是日期列和平均列。其中,日期列仅用于可视化目的。让我们将日期列设置为数据框的索引。这样,当我们绘制数据时,x 轴将自动注释相应的日期:

data = data.set_index('Date')

现在我们可以通过创建一条线图来可视化数据(图 4.16):

data[["Average"]].plot(figsize=(12,6))


图 4.16 CO2 浓度随时间变化的图示

数据的明显特征是它呈上升趋势和短周期性重复。让我们看看我们可以对这些数据做什么样的改进。数据明显的上升趋势构成了一个问题。这意味着数据在一个一致的范围内没有分布。随着时间线的推移,范围不断增加。如果你把数据直接输入模型,通常模型的性能会下降,因为模型必须预测的任何新数据都超出了训练期间看到的数据范围。但是如果你忘记绝对值,思考这些数据与前一个值的相对关系,你会发现它在一个非常小的值范围内波动(大约为-2.0 到+1.5)。事实上,我们可以很容易地测试这个想法。我们将创建一个名为 Average Diff 的新列,其中将包含两个连续时间步之间的相对差异:

data["Average Diff"]=data["Average"] - data["Average"].shift(1).fillna(method='bfill')

如果你在这个阶段执行 data.head(),你会看到类似表 4.2 的东西。

表 4.2 引入平均差异列后数据集中的示例数据

日期 十进制日期 平均值 趋势 平均差异
1980-01-01 1980.042 338.45 337.83 0.00
1980-02-01 1980.125 339.15 338.10 0.70
1980-03-01 1980.208 339.48 338.13 0.33
1980-04-01 1980.292 339.87 338.25 0.39
1980-05-01 1980.375 340.30 338.78 0.43

这里,我们正在从原始平均列中减去一个平移一个时间步长的值的版本的平均列。图 4.17 在视觉上描述了这个操作。


图 4.17 从原始平均系列到平均差异系列的转换

最后,我们可以可视化值的行为(图 4.18)使用 data[“Average Diff”].plot(figsize=(12,6)) 行。


图 4.18 CO2 浓度值的相对变化(即,Average[t]-Average[t-1])随时间的变化

你能看到区别吗?从不断增长的数据流中,我们已经转变成了在短时间内发生变化的数据流。下一步是为模型创建数据批处理。我们如何为时间序列问题创建数据批处理呢?请记住,我们不能简单地随机采样数据,因为每个输入都取决于其前序输入。

假设我们想要使用过去 12 个 CO2 浓度值(即 12 个时间步)来预测当前的 CO2 浓度值。时间步数是一个必须仔细选择的超参数。为了自信地选择这个超参数,你必须对数据和所使用的模型的内存限制有扎实的了解。

我们首先随机选择序列中的一个位置,并从该位置开始取 12 个值作为输入,并将第 13 个值作为我们感兴趣的要预测的输出,以便每次采样的总序列长度(n_seq)为 13。如果你这样做 10 次,你将得到一个具有 10 个元素的数据批处理。正如你所看到的,这个过程利用了随机性,同时保留了数据的时间特性,并向模型提供数据。图 4.19 对这个过程进行了可视化描述。


图 4.19 批处理时间序列数据。n_seq 表示我们在给定时间内看到的时间步数,以创建单个输入和输出。

要在 Python 中执行此操作,让我们编写一个函数,以单个数据集的形式给出所有位置的数据。换句话说,该函数返回所有可能的具有 12 个元素的连续序列作为 x,并将每个序列的相应下一个值作为 y。在将数据提供给模型时可以执行洗牌操作,如下一个清单所示。

清单 4.4 用于为模型生成时间序列数据序列的代码

import numpy as np
def generate_data(co2_arr,n_seq):
    x, y = [],[]
    for i in range(co2_arr.shape[0]-n_seq):
        x.append(co2_arr[i:i+n_seq-1])         ❶
        y.append(co2_arr[i+n_seq-1:i+n_seq])   ❷
    x = np.array(x)                            ❸
    y = np.array(y)                            ❸
    return x,y

❶ 提取长度为 n_seq 的值序列

❷ 将序列中的下一个值提取为输出

❸ 将所有内容组合成一个数组

4.3.2 实现模型

了解数据后,我们可以开始实现网络。我们将实现一个具有以下内容的网络:

  • 具有 64 个隐藏单元的 rnn 层
  • 具有 64 个隐藏单元和 ReLU 激活的密集层
  • 具有单输出和线性激活的密集层
from tensorflow.keras import layers, models
rnn = models.Sequential([
    layers.SimpleRNN(64),
    layers.Dense(64, activation='relu'),
    layers.Dense(1)
])

请注意,网络的超参数(例如,隐藏单元的数量)已经经验性地选择,以便在给定问题上良好地工作。第一层是网络中最关键的组件,因为它是从时间序列数据中学习的要素。SimpleRNN 层封装了图 4.20 中所示的功能。


图 4.20 SimpleRNN 单元的功能。该单元在每个时间步长产生一个内存,从一个输入到另一个输入。下一步会消耗当前输入以及上一个时间步长的内存。

在 RNN 中发生的计算比在 FCN 中更复杂。RNN 按给定顺序(即 x1、x2、x3)从一个输入到另一个输入。在每个步骤中,递归层产生一个输出(即 o1、o2、o3),并将隐藏计算(h0、h1、h2、h3)传递到下一个时间步长。在这里,第一个隐藏状态(h0)通常设为零。

在给定的时间步长上,递归层计算一个隐藏状态,就像 Dense 层一样。然而,涉及的具体计算更加复杂,超出了本书的范围。隐藏状态的大小是递归层的另一个超参数。递归层接受当前输入以及细胞计算的先前隐藏状态。更大尺寸的隐藏状态有助于保持更多内存,但增加了网络的内存需求。由于隐藏状态依赖于上一个时间步长的自身,这些网络被称为 RNNs。

使用 SimpleRNN 的算法

SimpleRNN 层模仿的计算也称为Elman 网络。要了解递归层中发生的具体计算,你可以阅读 J.L. Elman(1990)的论文“Finding Structure in Time”。要了解 RNN 的后续变体及其区别的更高级概述,请参阅mng.bz/xnJgmng.bz/Ay2g

默认情况下,SimpleRNN 不会将隐藏状态暴露给开发者,并且会在时间步长之间自动传播。对于这个任务,我们只需要每个时间步长产生的最终输出,这默认情况下是该层的输出。因此,你可以简单地将 SimpleRNN 连接到 Sequential API 中的一个 Dense 层,而无需进行任何额外的工作。

你是否注意到我们没有为第一层提供 input_shape?只要你在模型拟合期间提供正确形状的数据即可。Keras 会懒惰地构建层,因此在你向模型提供数据之前,模型不需要知道输入大小。但为了避免错误,最好在模型的第一层设置 input_shape 参数。例如,在我们定义的模型中,第一层(即 SimpleRNN 层)可以更改为 layers.SimpleRNN(64, input_shape=x),其中 x 是包含模型接受的数据形状的元组。

这个模型的另一个重要区别是它是一个回归模型,而不是分类模型。在分类模型中,有不同的类别(由输出节点表示),我们尝试将给定的输入与不同的类别(或节点)关联起来。回归模型预测一个连续的值作为输出。在我们的回归模型中,输出中没有类的概念,而是表示 CO2 浓度的实际连续值。因此,我们必须适当地选择损失函数。在这种情况下,我们将使用均方误差(MSE)作为损失。MSE 是回归问题的非常常见的损失函数。我们将使用 MSE 损失和 adam 优化器编译 rnn:

rnn.compile(loss='mse', optimizer='adam')

让我们祈祷并训练我们的模型:

x, y = generate_data(data[“Average Diff”], n_seq=13)
rnn.fit(x, y, shuffle=True, batch_size=64, epochs=25)

你将得到以下异常:

ValueError: 
➥ Input 0 of layer sequential_1 is incompatible with the layer: 
➥ expected ndim=3, found ndim=2\. Full shape received: [None, 12]

看起来我们做错了什么。我们刚刚运行的那行导致了一个异常,它说给层 sequential_1(即 SimpleRNN 层)提供的数据的维度出了问题。具体来说,sequential_1 层期望一个三维输入,但却有一个二维输入。我们需要调查这里发生了什么,并解决这个问题。

问题在于 SimpleRNN(或 tf.keras 中的任何其他顺序层)只接受非常特定格式的数据。数据需要是三维的,按照以下顺序的维度:

  1. 批处理维度
  2. 时间维度
  3. 特征维度

即使对于这些维度中的任何一个,你只有一个元素,它们也需要以大小为 1 的维度存在于数据中。让我们通过打印 x.shape 来查看 x 的维度。你将会得到 x.shape = (429, 12)。现在我们知道了问题所在。我们尝试传递一个二维数据集,但我们应该传递一个三维数据集。在这种情况下,我们需要将 x 重塑为形状为 (492, 12, 1) 的张量。让我们修改我们的 generate_data(…) 函数以反映以下清单中的这种变化。

列表 4.5 具有正确形状数据的先前 generate_data() 函数

import numpy as np
def generate_data(co2_arr,n_seq):
    x, y = [],[]                              ❶
    for i in range(co2_arr.shape[0]-n_seq):   ❷
        x.append(co2_arr[i:i+n_seq-1])        ❸
        y.append(co2_arr[i+n_seq-1:i+n_seq])  ❸
    x = np.array(x).reshape(-1,n_seq-1,1)     ❹
    y = np.array(y) 
    return x,y

❶ 创建两个列表来保存输入序列和标量输出目标。

❷ 遍历数据中所有可能的起始点,以用作输入序列。

❸ 创建第 i 个位置的输入序列和输出目标。

❹ 将 x 从列表转换为数组,并使 x 成为 RNN 可接受的 3D 张量。

现在让我们尝试训练我们的模型:

x, y = generate_data(data[“Average Diff”], n_seq=13)
rnn.fit(x, y, shuffle=True, batch_size=64, epochs=25)

你应该看到模型的 MSE 在下降:

Train on 429 samples
Epoch 1/25
429/429 [==============================] - 1s 2ms/sample - loss: 0.4951
Epoch 2/25
429/429 [==============================] - 0s 234us/sample - loss: 0.0776
...
Epoch 24/25
429/429 [==============================] - 0s 234us/sample - loss: 0.0153
Epoch 25/25
429/429 [==============================] - 0s 234us/sample - loss: 0.0152

我们从大约 0.5 的损失开始,最终损失大约为 0.015。这是一个非常积极的迹象,因为它表明模型正在学习数据中存在的趋势。

4.3.3 使用经过训练的模型预测未来的 CO2 值

到目前为止,我们已经专注于分类任务。对于分类任务,评估模型要比回归任务容易得多。在分类任务中(假设数据集平衡),通过计算数据的总体准确性,我们可以得到一个体现模型表现的不错的代表性数字。在回归任务中,情况并不那么简单。我们无法对回归值进行准确度测量,因为预测的是实际值,而不是类别。例如,均方损失的大小取决于我们正在回归的值,这使它们难以客观解释。为了解决这个问题,我们预测未来五年的数值,并直观地检查模型的预测情况(见下一列表)。

列表 4.6 使用训练模型的未来 CO2 水平预测逻辑

history = data["Average Diff"].values[-12:].reshape(1,-1,1)     ❶
true_vals = []
prev_true = data["Average"].values[-1]                          ❷
for i in range(60):                                             ❸
    p_diff = rnn.predict(history).reshape(1,-1,1)               ❹
    history = np.concatenate((history[:,1:,:],p_diff),axis=1)   ❺
    true_vals.append(prev_true+p_diff[0,0,0])                   ❻
    prev_true = true_vals[-1]                                   ❼

❶ 从中获取开始预测的第一个数据序列,重塑为 SimpleRNN 接受的正确形状

❷ 保存最后一个绝对 CO2 浓度值,以计算相对预测的实际值。

❸ 预测接下来的 60 个月。

❹ 使用数据序列进行预测。

❺ 修改历史记录,以包括最新的预测。

❻ 计算绝对 CO2 浓度。

❼ 更新 prev_true,以便在下一个时间步骤中计算绝对 CO2 浓度。

让我们回顾一下我们所做的事情。首先,我们从我们的训练数据中提取最后 12 个 CO2 值(从平均差值列中)来预测第一个未来的 CO2 值,并将其重塑为模型期望数据的正确形状:

history = data["Average Diff"].values[-12:].reshape(1,-1,1)

然后,我们将预测的 CO2 值记录在 true_vals 列表中。请记住,我们的模型只预测 CO2 值相对于先前 CO2 值的相对运动。因此,在模型预测之后,为了得到绝对 CO2 值,我们需要最后一个 CO2 值。prev_true 捕获了这一信息,最初包含数据的平均列中的最后一个值:

prev_true = data["Average"].values[-1]

现在,接下来的 60 个月(或 5 年),我们可以递归预测 CO2 值,同时使最后预测的值成为网络的下一个输入。要做到这一点,我们首先使用 Keras 提供的 predict(…)方法预测一个值。然后,我们需要确保预测也是一个三维张量(尽管它只是一个单一值)。然后我们修改 history 变量:

history = np.concatenate((history[:,1:,:],p_diff),axis=1)

我们把历史中除了第一个值之外的所有值,并将最后预测的值附加到末尾。然后,我们通过添加 prev_true 值到 p_diff 来附加绝对预测的 CO2 值:

true_vals.append(prev_true+p_diff[0,0,0])

最后,我们将 prev_true 更新为我们预测的最后一个绝对 CO2 值:

prev_true = true_vals[-1]

通过递归执行这组操作,我们可以获得接下来 60 个月的预测值(保存在 true_vals 变量中)。如果我们可视化预测的值,它们应该看起来像图 4.21。


图 4.21 在接下来的五年里预测的 CO2 浓度。虚线代表当前数据的趋势,实线代表预测的趋势。

做得好!考虑到模型的简单性,预测看起来非常有前景。该模型肯定捕捉到了二氧化碳浓度的年度趋势,并学会了二氧化碳水平将继续上升。你现在可以去找你的老板,事实性地解释为什么我们应该担心未来气候变化和危险水平的二氧化碳。我们在这里结束了对不同神经网络的讨论。

练习 3

受到你在预测二氧化碳浓度方面工作的印象,你的老板给了你数据,并要求你改进模型以预测二氧化碳和温度值。保持其他超参数不变,你会如何改变模型以完成这个任务?确保指定第一层的 input_shape 参数。

总结

  • 完全连接网络(FCNs)是最简单直接的神经网络之一。
  • FCNs 可以使用 Keras Dense 层来实现。
  • 卷积神经网络(CNNs)是计算机视觉任务的热门选择。
  • TensorFlow 提供了各种层,如 Conv2D、MaxPool2D 和 Flatten,这些层帮助我们快速实现 CNNs。
  • CNNs 有一些参数,如卷积核大小、步幅和填充,必须小心设置。如果不小心,这可能导致张量形状不正确和运行时错误。
  • 循环神经网络(RNNs)主要用于学习时间序列数据。
  • 典型的 RNN 期望数据组织成具有批次、时间和特征维度的三维张量。
  • RNN 看的时间步数是一个重要的超参数,应该根据数据进行选择。

练习答案

练习 1: 你可以使用 Sequential API 来做到这一点,你只需要使用 Dense 层。

练习 2

autoencoder = models.Sequential(
    [layers.Dense(32, activation='sigmoid', input_shape=(512,)),
    layers.Dense(16, activation='sigmoid'),
    layers.Dense(512, activation='sigmoid')]
)

练习 3

rnn = models.Sequential([
    layers.SimpleRNN(64, input_shape=(12, 2)),
    layers.Dense(64, activation='relu'),
    layers.Dense(2)
])

第五章:深度学习的最新技术:Transformer

本章内容包括:

  • 为机器学习模型以数值形式表示文本
  • 使用 Keras sub-classing API 构建 Transformer 模型

到目前为止,我们已经看到了许多不同的深度学习模型,包括全连接网络、卷积神经网络和循环神经网络。我们使用全连接网络来重建受损图像,使用卷积神经网络来对车辆进行分类,最后使用 RNN 来预测未来的 CO2 浓度值。在本章中,我们将讨论一种新型的模型,即 Transformer。

Transformer 是最新一代的深度网络。瓦斯瓦尼等人在他们的论文《Attention Is All You Need》(arxiv.org/pdf/1706.03762.pdf)中普及了这个想法。他们创造了“Transformer”这个术语,并解释了它在未来有很大潜力。在随后的几年里,谷歌、OpenAI 和 Facebook 等领先的科技公司实施了更大更好的 Transformer 模型,这些模型在 NLP 领域显著优于其他模型。在这里,我们将参考瓦斯瓦尼等人在论文中介绍的模型来学习它。虽然 Transformer 也存在于其他领域(例如计算机视觉),我们将重点介绍 Transformer 在 NLP 领域中的应用,特别是在机器翻译任务中(即使用机器学习模型进行语言翻译)。本章将省略原始 Transformer 论文中的一些细节,以提高清晰度,但这些细节将在后面的章节中进行介绍。

想要在使用深度学习模型解决实际问题时出类拔萃,了解 Transformer 模型的内部工作原理是必不可少的。如前所述,Transformer 模型在机器学习领域迅速普及。这主要是因为它在解决复杂机器学习问题方面展现出的性能。

5.1 将文本表示为数字

假设你正在参加一个游戏节目。游戏中的一个挑战叫做单词盒子。有一个由透明盒子组成的矩阵(3 行,5 列,10 深度)。你也有一些上面涂有 0 或 1 的球。你被给予了三个句子,你的任务是用 1 和 0 填充所有的盒子来表示这些句子。此外,你可以在一分钟内写一条简短的信息,帮助其他人在以后破译这一信息。之后,另一个队员看着盒子,写下最初给你的原始句子中的尽可能多的单词。

这个挑战本质上是如何将文本转换成数字,用于机器翻译模型。这也是在了解任何 NLP 模型之前需要解决的重要问题。到目前为止我们看到的数据都是数值型数据结构。例如,一张图像可以被表示为一个 3D 数组(高度,宽度和通道维度),其中每个值表示像素强度(即,取值范围在 0 至 255 之间)。但文本呢?我们怎么让计算机理解字符、单词或句子呢?我们将在自然语言处理(NLP)的情境中学习如何用 Transformer 完成这一点。

您有以下一组句子:

  • 我去了海滩。
  • 天气很冷。
  • 我回到了房子。

你要做的第一件事是给词汇表中的每个单词分配一个从 1 开始的 ID。我们将保留数字 0 给我们稍后会看到的特殊标记。假设你分配了以下 ID:

  • I → 1
  • went → 2
  • to → 3
  • the → 4
  • beach → 5
  • 它 → 6
  • was → 7
  • cold → 8
  • came → 9
  • back → 10
  • house → 11

将单词映射到相应的 ID 后,我们的句子变为了下面这个样子:

  • [1, 2, 3, 4, 5]
  • [6, 7, 8]
  • [1, 9, 10, 3, 4, 11]

请记住,您需要填写所有方框,并且最多长度为 5。请注意我们的最后一句有六个单词。这意味着所有句子都需要表示为固定长度。深度学习模型面临类似的问题。它们以批处理的方式处理数据,并且为了高效处理数据,批处理的序列长度需要是固定的。真实世界的句子在长度上可能差异很大。因此,我们需要

  • 用特殊标记(ID 为 0)填充短句
  • 截断长句

使它们具有相同的长度。如果我们填充短句并截断长句,使长度为 5,我们得到以下结果:

  • [1, 2, 3, 4, 5]
  • [6, 7, 8, 0, 0]
  • [1, 9, 10, 3, 4]

这里,我们有一个大小为 3×5 的 2D 矩阵,它表示我们的一批句子。最后要做的一件事是将这些 ID 表示为向量。因为我们的球有 1 和 0,你可以用 11 个球(我们有 10 个不同的单词和特殊标记)代表每个单词,其中由单词 ID 指示的位置上的球为 1,其余为 0。这种方法称为 one-hot 编码。例如,

以下分别代表着各自的 ID:

1 → [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

。 。 。

10 → [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]

11 → [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]

现在你可以用 1 和 0 填写方框,使得你得到类似图 5.1 的结果。这样,任何一位有这些 ID 映射的人(提供在一张纸上)都可以解密最初所提供的大部分单词(除了被截断的单词)。


图 5.1 词盒游戏中的方框。阴影方框代表一个词(即第一个句子中的第一个词“I”,它的 ID 是 1)。你可以看到它被一个 1 和九个 0 所表示。

同样,这是在 NLP 问题中对单词进行的转换。你可能会问:“为什么不直接提供单词 ID?”存在两个问题:

  • 神经网络看到的值范围非常大(0-100,000+)对于一个现实世界的问题。这会导致不稳定性并使训练困难。
  • 输入 ID 会错误地表明具有相似 ID 的单词应该是相似的(例如,单词 ID 4 和 5)。这从未发生过,会使模型混淆并导致性能下降。

因此,将单词转换为某种向量表示是很重要的。有许多将单词转换为向量的方法,例如独热编码和词嵌入。你已经看到了独热编码的工作原理,我们将稍后详细讨论词嵌入。当我们将单词表示为向量时,我们的 2D 矩阵变为 3D 矩阵。例如,如果我们将向量长度设置为 4,你将得到一个 3 × 6 × 4 的 3D 张量。图 5.2 描述了最终矩阵的外观。


图 5.2 表示一个单词序列批次的 3D 矩阵,其中每个单词由一个向量表示(即矩阵中的阴影块)。有三个维度:批次、序列(时间)和特征。

接下来,我们将讨论流行的 Transformer 模型的各个组成部分,这将为我们提供对这些模型内部执行的基础。

5.2 理解 Transformer 模型

你目前是一名深度学习研究科学家,并最近受邀在当地 TensorFlow 大会上进行有关 Transformer 的研讨会。Transformer 是一类新型的深度学习模型,在众多任务中已经超越了它们的老对手。你计划首先解释 Transformer 网络的架构,然后带领参与者完成几个练习,在这些练习中,他们将使用 Keras 的子类化层实现在 Transformers 中找到的基本计算,最后使用这些计算来实现一个基本的小规模 Transformer。

5.2.1 Transformer 的编码器-解码器视图

Transformer 网络基于编码器-解码器架构。编码器-解码器模式在某些类型的深度学习任务中很常见(例如,机器翻译、问答、无监督图像重建)。其思想是编码器将输入映射到某种潜在(或隐藏)表示(通常较小),而解码器使用潜在表示构建有意义的输出。例如,在机器翻译中,语言 A 的句子被映射到一个潜在向量,解码器从中构建语言 B 中该句子的翻译。你可以将编码器和解码器视为两个独立的机器学习模型,其中解码器依赖于编码器的输出。这个过程如图 5.3 所示。在给定的时间点,编码器和解码器同时处理一批词序列(例如,一批句子)。由于机器学习模型不理解文本,因此该批次中的每个单词都由一个数字向量表示。这是通过一种过程来实现的,例如独热编码,类似于我们在第 5.1 节中讨论的内容。


图 5.3 机器翻译任务的编码器-解码器架构

编码器-解码器模式在现实生活中也很常见。假设你是法国的导游,带着一群游客去一家餐厅。服务员用法语解释菜单,你需要为团队将其翻译成英语。想象一下你会如何做。当服务员用法语解释菜肴时,你处理这些词语并创建出菜肴的心理图像,然后将该心理图像翻译成一系列英语词语。

现在让我们更深入地了解各个组件及其构成。

5.2.2 更深入地探讨

自然地,你可能会问自己:“编码器和解码器由什么组成?”这是本节的主题。请注意,此处讨论的编码器和解码器与你在第三章中看到的自编码器模型有很大不同。正如之前所述,编码器和解码器分别像多层深度神经网络一样工作。它们由多个层组成,每个层包含子层,封装了对输入进行的某些计算以产生输出。前一层的输出作为下一层的输入。还需要注意的是,编码器和解码器的输入和输出是序列,例如句子。这些模型中的每个层都接收一个元素序列并输出另一个元素序列。那么,编码器和解码器中的单个层包含什么?

每个编码器层包含两个子层:

  • 自注意力层
  • 全连接层

自注意力层的最终输出与全连接层类似(即使用矩阵乘法和激活函数)。典型的全连接层将处理输入序列中的所有元素,并分别处理它们,然后输出一个元素以替换每个输入元素。但自注意力层可以选择和组合输入序列中的不同元素以输出给定元素。这使得自注意力层比典型的全连接层更加强大(见图 5.4)。


图 5.4 自注意力子层和全连接子层之间的区别。自注意力子层查看序列中的所有输入,而全连接子层只查看正在处理的输入。

为什么以这种方式选择和组合不同的输入元素有好处?在自然语言处理的上下文中,自注意力层使模型在处理某个单词时能够查看其他单词。但这对模型意味着什么?这意味着在编码器处理句子“I kicked the ball and it disappeared”中的单词“it”时,模型可以关注单词“ball”。通过同时看到“ball”和“it”两个单词(学习依赖关系),消除歧义的单词变得更容易。这样的能力对语言理解至关重要。

我们可以通过一个现实世界的例子了解自注意力如何方便地帮助我们解决任务。假设你正在和两个人玩游戏:A 和 B。A 手持写有问题的板子,你需要说出答案。假设 A 一次只透露一个单词,直到问题的最后一个单词被揭示,才揭示你正在回答问题。对于长而复杂的问题,这是具有挑战性的,因为你不能物理上看到完整的问题,必须严重依赖记忆来回答问题。这就是没有自注意力时的感觉。另一方面,假设 B 将整个问题一次性展示在板上,而不是逐字逐句地展示。现在回答问题要容易得多,因为你可以一次看到整个问题。如果问题很复杂,需要复杂的答案,你可以在提供完整答案的各个部分时查看问题的不同部分。这就是自注意力层的作用。

接下来,全连接层以逐元素的方式接受自注意力子层产生的输出元素,并为每个输出元素生成一个隐藏表示。这使得模型更加深入,从而表现更好。

让我们更详细地看一下数据如何通过模型流动,以更好地理解层和子层的组织。假设要将句子“Dogs are great”(英语)翻译成“Les chiens sont super”(法语)。首先,编码器接收完整的句子“Dogs are great”,并为句子中的每个单词生成一个输出。自注意力层选择每个位置的最重要的单词,计算一个输出,并将该信息发送到全连接层以产生更深层的表示。解码器迭代地生成输出单词,一个接着一个。为此,解码器查看编码器的最终输出序列以及解码器预测的所有先前单词。假设最终预测是 les chiens sont super 。这里, 标记了句子的开始, 标记了句子的结束。它接收的第一个输入是一个特殊标记,表示句子的开始(),以及编码器的输出,并产生翻译中的下一个单词:“les”。然后解码器消耗 和 “les” 作为输入,生成单词“chiens”,并继续直到模型达到翻译的末尾(由 标记)。图 5.5 描述了这个过程。

在原始的 Transformer 论文中,编码器有六个层,并且一个单层有一个自注意力子层和一个全连接子层,按顺序排列。首先,自注意力层将英文单词作为时间序列输入。然而,在将这些单词馈送到编码器之前,您需要为每个单词创建一个数值表示,如前面所讨论的。在论文中,词嵌入(附加一些编码)用于表示这些单词。每个嵌入都是一个 512 长的向量。然后自注意力层计算输入句子中每个单词的隐藏表示。如果我们忽略一些实现细节,这个时间步长 t 的隐藏表示可以被看作是所有输入的加权和(在一个单一序列中),其中输入位置 i 的权重由在处理编码器输入中的单词 ew[t] 时选择(或关注)编码器单词 ew[i] 在输入序列中的重要性来确定。编码器在输入序列中的每个位置 t 上都做出这样的决定。例如,在处理句子“我踢了 并且 消失了”中的单词“它”时,编码器需要更多地关注单词“球”而不是单词“the”。自注意力子层中的权重被训练以展示这样的属性。这样,自注意力层为每个编码器输入生成了一个隐藏表示。我们称之为 关注表示/输出

全连接子层然后接管并且非常直观。它有两个线性层,并且在这两个层之间有一个 ReLU 激活函数。它接收自注意力层的输出,并将其转换为隐藏输出使用。

h[1] = ReLU(xW[1] + b[1])

h[2] = h[1]W[2] + b[2]

请注意,第二层没有非线性激活函数。接下来,解码器也有六个层,每个层都有三个子层:

  • 一个掩码自注意力层
  • 一个编码器-解码器注意力层
  • 一个全连接层

掩码自注意力层的操作方式与自注意力层类似。然而,在处理第 s 个单词(即dw[s])时,它会屏蔽 dw[s] 之前的单词。例如,在处理单词“chiens”时,它只能关注单词“”和“les”。这很重要,因为解码器必须能够预测正确的单词,只给出它先前预测的单词,所以强制解码器只关注它已经看到的单词是有意义的。

接下来,编码器-解码器注意力层获取编码器输出和掩码自注意力层产生的输出,并产生一系列输出。该层的目的是计算时间 s 处的隐藏表示(即一个受关注的表示),作为编码器输入的加权和,其中位置 j 的权重由处理解码器单词 dw[s] 时关注编码器输入 e w[j]的重要性确定。

最后,与编码器层相同的全连接层接收自注意力层的输出以生成层的最终输出。图 5.5 以高层次描述了本节讨论的层和操作。


图 5.5 编码器和解码器中的各个层以及编码器内部、解码器内部和编码器与解码器之间形成的各种连接。方框表示模型的输入和输出。长方形阴影框表示子层的临时输出。

在下一节中,我们将讨论自注意力层的外观。

5.2.3 自注意力层

我们已经在抽象级别上介绍了自注意力层的目的。在处理时间步 t 的单词w[t]时,其目的是确定关注输入序列中的第 i 个单词(即w[i])对理解当前单词有多重要。换句话说,该层需要确定对于每个单词(由 t 索引)所有其他单词(由 i 索引)的重要性。现在让我们以更细粒度的方式理解涉及此过程的计算。

首先,计算涉及三个不同的实体:

  • 查询 — 查询的目的是表示当前正在处理的单词。
  • — 键的目的是表示在处理当前单词时要关注的候选单词。
  • — 值的目的是计算序列中所有单词的加权和,其中每个单词的权重基于它对理解当前单词的重要性。

对于给定的输入序列,需要为输入的每个位置计算查询、键和值。这些是由与每个实体相关联的权重矩阵计算的。

请注意,这是它们关系的简化,实际关系有些复杂和混乱。但这种理解为什么我们需要三个不同的实体来计算自注意力输出提供了动机。

接下来,我们将了解自注意力层如何从输入序列到查询、键和值张量,最终到输出序列。首先,将输入的单词序列使用单词嵌入查找转换为数值表示。单词嵌入本质上是一个巨大的矩阵,其中词汇表中的每个单词都有一个浮点向量(即嵌入向量)。通常,这些嵌入是几百个元素长。对于给定的输入序列,我们假设输入序列的长度为 n 元素,并且每个单词向量的长度为 d[model] 元素。然后我们有一个 n × d[model] 矩阵。在原始 Transformer 论文中,单词向量长度为 512 个元素。

自注意力层中有三个权重矩阵:查询权重(W[q])、键权重(W[k])和值权重(W[v]),分别用于计算查询、键和值向量。W[q] 是 d[model] × d[q],W[k] 是 d[model] × d[k],W[v] 是 d[model] × d[v]。让我们假设这些元素在 TensorFlow 中的维度为 512,就像原始 Transformer 论文中一样。即,

d[model] = d[q] = d[k] = d[v] = 512

我们首先将我们的输入 x 定义为一个 tf.constant,它有三个维度(批量、时间、特征)。Wq、Wk 和 Wv 声明为 tf.Variable 对象,因为这些是自注意力层的参数。

import tensorflow as tf
import numpy as np
n_seq = 7
x = tf.constant(np.random.normal(size=(1,n_seq,512)))
Wq = tf.Variable(np.random.normal(size=(512,512)))
Wk = tf.Variable (np.random.normal(size=(512,512)))
Wv = tf.Variable (np.random.normal(size=(512,512)))

其形状为

>>> x.shape=(1, 7, 512)
>>> Wq.shape=(1, 512)
>>> Wk.shape=(1, 512)
>>> Wv.shape=(1, 512)

接下来,qkv 计算如下:

q = xW[q];形状变换:n × d[model]。d[model] × d[q] = n × d[q]

k = xW[k];形状变换:n × d[model]。d[model] × d[k] = n × d[k]

v = xW[v];形状变换:n × d[model]。d[model] × d[v] = n × d[v]

很明显,计算 qkv 只是一个简单的矩阵乘法。请记住,所有输入(即 x)和输出张量(即 q、k 和 v)前面都有一个批处理维度,因为我们处理数据批次。但为了避免混乱,我们将忽略批处理维度。然后我们按以下方式计算自注意力层的最终输出:


在这里,组件 (将被称为 P)是一个概率矩阵。这就是自注意力层的全部内容。使用 TensorFlow 实现自注意力非常简单。作为优秀的数据科学家,让我们将其创建为可重复使用的 Keras 层,如下所示。

列表 5.1 自注意力子层

import tensorflow as tf
import tensorflow.keras.layers as layers
class SelfAttentionLayer(layers.Layer):
    def __init__(self, d):
        super(SelfAttentionLayer, self).__init__()
        self.d = d                                                                ❶
    def build(self, input_shape):
        self.Wq = self.add_weight(                                                ❷
            shape=(input_shape[-1], self.d), initializer='glorot_uniform',        ❷
            trainable=True, dtype='float32'                                       ❷
        )        
        self.Wk = self.add_weight(                                                ❷
            shape=(input_shape[-1], self.d), initializer='glorot_uniform',        ❷
            trainable=True, dtype='float32'                                       ❷
        )
        self.Wv = self.add_weight(                                                ❷
            shape=(input_shape[-1], self.d), initializer='glorot_uniform',        ❷
            trainable=True, dtype='float32'                                       ❷
        )
    def call(self, q_x, k_x, v_x):
        q = tf.matmul(q_x,self.Wq)                                                ❸
        k = tf.matmul(k_x,self.Wk)                                                ❸
        v = tf.matmul(v_x,self.Wv)                                                ❸
        p = tf.nn.softmax(tf.matmul(q, k, transpose_b=True)/math.sqrt(self.d))    ❹
        h = tf.matmul(p, v)                                                       ❺
        return h,p

❶ 定义自注意力输出的输出维度

❷ 定义计算查询、键和值实体的变量

❸ 计算查询、键和值张量

❹ 计算概率矩阵

❺ 计算最终输出

这是一个快速的复习:

  • init(self, d)—定义层的任何超参数
  • build(self, input_shape)—创建层的参数作为变量
  • call(self, v_x, k_x, q_x)—定义层中发生的计算

如果你看一下 call(self, v_x, k_x, q_x) 函数,它接受三个输入:分别用于计算值、键和查询。在大多数情况下,这些输入是相同的。然而,也有一些情况下,不同的输入被用于这些计算(例如,解码器中的一些计算)。此外,请注意我们同时返回 h(即最终输出)和 p(即概率矩阵)。概率矩阵是一个重要的视觉辅助工具,它帮助我们理解模型何时以及在哪里关注了单词。如果你想获取层的输出,可以执行以下操作

layer = SelfAttentionLayer(512)
h, p = layer(x, x, x)
print(h.shape)

将返回

>>> (1, 7, 512)

练习 1

给定以下输入

x = tf.constant(np.random.normal(size=(1,10,256)))

并假设我们需要一个大小为 512 的输出,编写代码创建 Wq、Wk 和 Wv 作为 tf.Variable 对象。使用 np.random.normal() 函数设置初始值。

TensorFlow 实战(二)(3)https://developer.aliyun.com/article/1522701

相关文章
|
27天前
|
机器学习/深度学习 自然语言处理 TensorFlow
TensorFlow 实战(六)(2)
TensorFlow 实战(六)
24 0
|
13天前
|
机器学习/深度学习 TensorFlow API
TensorFlow与Keras实战:构建深度学习模型
本文探讨了TensorFlow和其高级API Keras在深度学习中的应用。TensorFlow是Google开发的高性能开源框架,支持分布式计算,而Keras以其用户友好和模块化设计简化了神经网络构建。通过一个手写数字识别的实战案例,展示了如何使用Keras加载MNIST数据集、构建CNN模型、训练及评估模型,并进行预测。案例详述了数据预处理、模型构建、训练过程和预测新图像的步骤,为读者提供TensorFlow和Keras的基础实践指导。
147 59
|
27天前
|
机器学习/深度学习 自然语言处理 TensorFlow
TensorFlow 实战(五)(5)
TensorFlow 实战(五)
19 1
|
27天前
|
自然语言处理 算法 TensorFlow
TensorFlow 实战(六)(3)
TensorFlow 实战(六)
21 0
|
27天前
|
机器学习/深度学习 数据可视化 TensorFlow
TensorFlow 实战(六)(1)
TensorFlow 实战(六)
25 0
|
27天前
|
存储 自然语言处理 TensorFlow
TensorFlow 实战(五)(4)
TensorFlow 实战(五)
21 0
|
27天前
|
数据可视化 TensorFlow 算法框架/工具
TensorFlow 实战(八)(4)
TensorFlow 实战(八)
25 1
|
27天前
|
TensorFlow API 算法框架/工具
TensorFlow 实战(八)(3)
TensorFlow 实战(八)
25 1
|
27天前
|
机器学习/深度学习 自然语言处理 TensorFlow
TensorFlow 实战(八)(5)
TensorFlow 实战(八)
25 0
|
27天前
|
并行计算 TensorFlow 算法框架/工具
TensorFlow 实战(八)(2)
TensorFlow 实战(八)
21 0