TensorFlow 实战(八)
原文:
zh.annas-archive.org/md5/63f1015b3af62117a4a51b25a6d19428译者:飞龙
附录 A:设置环境
在这个附录中,您将配置计算机上的开发和运行时环境。提供了两个安装过程:一个用于基于 Unix 的环境,另一个用于 Windows 环境。请注意,我们将把 Unix 环境的讨论主要集中在 Ubuntu 上,而不是 MacOS 上。这是因为,对于机器学习和深度学习来说,Ubuntu 比 MacOS 更受欢迎,也得到了更好的支持。但是,我们将列出在 MacOS 上运行此项所需的资源。
A.1 在基于 Unix 的环境中
我们的讨论将分为三个部分。在第一部分中,我们将讨论设置虚拟 Python 环境以安装运行代码所需的库的步骤。接下来,我们将讨论需要 GPU 支持的事项。最后,我们将讨论在 MacOS 上执行相同操作的情况。
A.1.1 使用 Anaconda 发行版创建虚拟 Python 环境(Ubuntu)
在本节中,我们将讨论在 Ubuntu 中设置 conda 环境(通过 Anaconda 软件包创建的虚拟 Python 环境的术语)的步骤:
-
在 Linux 系统上安装 Anaconda(
docs.anaconda.com/anaconda/install/linux/)。 -
打开终端并用您喜欢的文本编辑器打开 ~/.bashrc 文件(例如,对于 vim,请键入 vim ~/.bashrc)。
-
将以下行添加到文件末尾(带有您路径填充的占位符):
if ! [[ "$PATH" == *"anaconda3"* ]]; then export PATH=${PATH}:<your anaconda3 installation path>/bin fi -
保存并关闭编辑器。
-
打开一个新的命令行终端。
-
运行 conda create -n manning.tf2 python=3.9 设置一个新的 conda 虚拟环境。
-
(推荐)在您的主目录中创建一个名为 code 的文件夹,您将在其中存储本地代码,并使用 cd~/code 进入该文件夹。
-
使用 git clone github.com/thushv89/ma… 克隆托管在 Github 上的代码存储库。确保您的操作系统上已安装了 Git。
-
使用 cd manning_tf2_in_action 进入克隆的代码存储库。
-
使用以下命令激活环境
-
Anaconda < 4.4:source activate manning.tf2
-
Anaconda >= 4.4:conda activate manning.tf2
-
-
使用 pip install -r requirements.txt 安装所需的库。
A.1.2 GPU 支持的先决条件(Ubuntu)
安装 NVIDIA 驱动程序
确保您已安装了适用于您的 GPU 的最新 NVIDIA 图形驱动程序。您可以在 mng.bz/xnKe 找到驱动程序安装程序。如果您没有安装最新驱动程序,可能会在随后的步骤中遇到获取 TensorFlow GPU 支持的问题。
安装 CUDA
在本节中,我们将安装 CUDA 11.2,因为我们使用的 TensorFlow 版本高于 2.5.0。但是,您需要选择适合您 TensorFlow 版本的正确 CUDA 版本,如在 www.tensorflow.org/install/source#gpu 中指定的。最新 TensorFlow 版本的 CUDA 版本列在表 A.1 中。
表 A.1 最新 TensorFlow 版本支持的 CUDA 版本
| TensorFlow 版本 | CUDA 版本 |
|---|---|
| 2.4.x | 11.0 |
| 2.8.x | 11.2 |
| 2.9.x | 11.2 |
要安装所需的 CUDA 版本,请按照以下步骤操作:
-
转到
developer.nvidia.com/cuda-toolkit-archive页面。这将显示您可以下载的所有 CUDA 版本。 -
点击所需的 CUDA 版本,您将看到类似于图 A.1 的页面。例如,图 A.1 描绘了用于 Ubuntu 发行版下载 CUDA 版本 11.7 的选项。
-
确保您对下载的文件拥有执行权限(例如,在 Ubuntu 上,您可以通过终端运行 chmod a+x <路径到下载的文件>来提供执行权限)。
-
通过命令行终端打开下载的软件包进行安装(例如,在 Ubuntu 上,只需转到下载目录并使用./<文件名>运行安装)。
图 A.1 CUDA 下载页面(Ubuntu 安装)
安装完成后,需要将安装路径添加到特殊的环境变量中:
-
打开终端,使用您喜欢的文本编辑器(例如,对于 vim,请键入 vim~/.bashrc)打开~/.bashrc 文件。
-
将以下行添加到文件末尾。例如,路径可能类似于/usr/local/cuda-11.0:
if ! [[ "$PATH" == *"cuda"* ]]; then export PATH=${PATH}:<path to CUDA>/bin fi export LD_LIBRARY_PATH=<path to CUDA>/lib64 -
保存并关闭编辑器。
安装 CuDNN
与 CUDA 类似,需要仔细选择 cuDNN 版本。表 A.2 列出了最新 TensorFlow 版本支持的 cuDNN 版本。要获取完整列表,请访问www.tensorflow.org/install/source#gpu。
表 A.2 最新 TensorFlow 版本支持的 cuDNN 版本
| TensorFlow 版本 | cuDNN 版本 |
|---|---|
| 2.4.x | 8.0 |
| 2.6.x | 8.1 |
| 2.9.x | 8.1 |
首先,按照developer.nvidia.com/cudnn上的说明和提示下载首选的 cuDNN 软件包。要安装 cuDNN,请按照mng.bz/AyQK提供的指南进行操作。
A.1.3 MacOS 注意事项
不幸的是,由于 NVIDIA 不认为 CUDA 是 CUDA 相关开发工作的主要开发环境,因此 CUDA 不再得到积极支持(mng.bz/ZAlO)。您仍然可以安装 Anaconda、创建虚拟环境并安装 TensorFlow 来进行开发工作。但是,您可能无法在 NVIDIA GPU 上运行执行 CUDA 实现的任何 TensorFlow 计算。
在 MacOS 上安装 Anaconda,请按照docs.anaconda.com/anaconda/install/mac-os/ 提供的指南进行操作。管理 conda 环境的指南在mng.bz/R4V0中提供。
A.2 在 Windows 环境中
在本节中,我们将讨论如何在 Windows 上安装虚拟环境,并确保 GPU 支持。
A.2.1 创建一个虚拟 Python 环境(Anaconda)
此节讨论了在 Windows 主机上创建 conda 环境的步骤:
-
在 Windows 系统上安装 Anaconda(
docs.anaconda.com/anaconda/install/linux/),这也将安装一个用于执行 Anaconda 特定命令的 CLI(命令行接口)。 -
在开始菜单的搜索栏中输入 Anaconda Prompt,打开 Anaconda Prompt(如图 A.2 所示)。
-
在终端中运行 conda create -n manning.tf2 python=3.9 以设置 conda 虚拟环境。
-
(建议)在您的主文件夹(例如,C:\Users<username>\Documents)中创建一个名为 code 的文件夹,在其中我们将本地存储代码,并使用 cd C:\Users<username>\Documents 进入该文件夹。
-
如果尚未安装,请为 Windows 安装 Git(例如,
git-scm.com/download/win)。 -
使用 git clone github.com/thushv89/ma… 克隆托管在 Github 上的代码库。
-
使用 cd manning_tf2_in_action 进入克隆代码库。
-
使用 conda activate manning.tf2 激活环境。
-
使用 pip install -r requirements.txt 安装所需的库。
图 A.2:在 Windows 上打开 Anaconda Prompt
A.2.2 GPU 支持的先决条件
在本节中,我们将讨论确保 GPU 被识别并正常工作的几个先决条件。
安装 NVIDIA 驱动程序
确保您已为您的 GPU 安装了最新的 NVIDIA 图形驱动程序。您可以在mng.bz/xnKe找到驱动程序安装程序。如果您不安装最新的驱动程序,您可能会在获取 TensorFlow 的 GPU 支持的后续步骤中遇到问题。
安装 CUDA
在本节中,我们将安装 CUDA 11.2,因为我们使用的是高于 2.5.0 版本的 TensorFlow。但是,您需要选择适合您 TensorFlow 版本的正确 CUDA 版本,如www.tensorflow.org/install/source#gpu中所述。
要安装所需的 CUDA 版本,请完成以下步骤:
-
转到
developer.nvidia.com/cuda-toolkit-archive页面。这将显示您可以下载的所有 CUDA 版本。 -
通过单击所需的 CUDA 版本,进入页面如图 A.3 所示。例如,图 A.3 描述了选择 Windows 操作系统获取 CUDA 11.7 的选项。
-
以管理员身份运行下载的 .exe 文件,并按照提示进行操作。
图 A.3:CUDA 下载页面(Windows 安装)
安装完成后,需要将安装路径添加到特殊环境变量中:
-
通过从开始菜单中选择“编辑系统环境变量”来打开“环境变量”窗口(图 A.4)。
-
根据表 A.3 中的说明,将以下路径添加到路径变量中。 图 A.5 显示了如何在 Windows 上添加/修改环境变量。
图 A.4 打开系统属性窗口
表 A.3 需要添加和修改的路径变量
| PATH | \bin |
|---|---|
| CUDA_PATH |
图 A.5 添加/修改路径变量的步骤
安装 CuDNN
与 CUDA 类似,需要仔细选择 cuDNN 版本。 表 A.4 列出了最新 TensorFlow 版本支持的 cuDNN 版本。 要获取完整列表,请访问www.tensorflow.org/install/source#gpu。
表 A.4 最新 TensorFlow 版本支持的 cuDNN 版本
| TensorFlow version | cuDNN version |
|---|---|
| 2.4.x | 8.1 |
| 2.5.x | 8.1 |
| 2.6.x | 8.0 |
首先,按照developer.nvidia.com/cudnn上的说明和提示下载首选的 cuDNN 软件包。 要安装 cuDNN,请按照mng.bz/AyQK上提供的说明操作。
A.3 激活和停用 conda 环境
一旦 conda 环境被创建,完成以下步骤来激活或停用环境。
在 Windows 上(通过 Anaconda Prompt)(图 A.6)
-
运行 conda activate 以激活环境。
-
运行 conda deactivate 以停用当前活动环境。
图 A.6 激活 conda 环境
在 Ubuntu 上(通过终端)
-
运行 source activate (Anaconda < 4.4)或 conda activate (Anaconda >= 4.4)以激活环境。
-
运行 conda deactivate 以停用当前活动环境。
A.4 运行 Jupyter Notebook 服务器并创建笔记本
我们将使用 Jupyter Notebook 服务器编写代码并执行它。 具体来说,我们将启动 Jupyter Notebook 服务器,它将为您提供一个仪表板(网页)以创建 Jupyter Notebook。 Jupyter Notebook 是一个交互式 Python 运行时环境。 这意味着您可以在 Jupyter Notebooks 中编写代码,并根据需要运行不同的代码片段。 这是因为代码可以分隔成所谓的notebook cells。 让我们看看如何启动 Jupyter Notebook 服务器并开始编码:
-
打开命令行终端(例如,Ubuntu 终端或 Windows Anaconda Prompt),并激活虚拟环境 manning.tf2(如果尚未激活)。
-
在 CLI 中使用 cd 命令进入您下载代码的目录(例如,cd C:\Users<user>\Documents\code\manning_tf2_in_action)。
-
在 CLI 中运行命令 jupyter notebook。
-
这将在您默认的浏览器上打开 Jupyter Notebook 服务器的首页。
-
现在,您可以在该目录中浏览文件夹结构,打开任何笔记本,并运行它(图 A.7)。
-
一旦打开了一个笔记本,您就可以进行各种操作,如创建代码单元格、运行代码单元格等(图 A.8)。
图 A.7 Jupyter Notebook 服务器创建的首页
图 A.8 Jupyter Notebook 概览
A.5 杂项注释
为了让 TensorFlow/Keras 提供的绘图功能正常工作,您安装了一个名为 graphviz 的 Python 包。您可能需要将该库的路径(例如,如果您使用了 Anaconda 安装,则为 \envs\manning.tf2\Library\bin\graphviz)添加到操作系统的 PATH 变量中。
附录 B:计算机视觉
B.1 Grad-CAM:解释计算机视觉模型
Grad-CAM(代表梯度类激活映射)在第七章介绍过,是由 Ramprasaath R. Selvaraju 等人在“Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization”(arxiv.org/pdf/1610.02391.pdf)中介绍的一种深度神经网络模型解释技术。深度网络以其难以解释的特性而臭名昭著,因此被称为黑盒子。因此,我们必须进行一些分析,并确保模型按预期工作。
让我们在第七章实现的模型上刷新一下记忆:一个名为 InceptionResNet v2 的预训练模型,其顶部是一个具有 200 个节点的 softmax 分类器(即我们的图像分类数据集 TinyImageNet 中的类别数量相同;请参阅下面的列表)。
清单 B.1 我们在第七章定义的 InceptionResNet v2 模型
import tensorflow as tf
import tensorflow.keras.backend as K
from tensorflow.keras.applications import InceptionResNetV2
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout
K.clear_session()
def get_inception_resnet_v2_pretrained():
model = Sequential([ ❶
Input(shape=(224,224,3)), ❷
InceptionResNetV2(include_top=False, pooling='avg'), ❸
Dropout(0.4), ❹
Dense(200, activation='softmax') ❺
])
loss = tf.keras.losses.CategoricalCrossentropy()
adam = tf.keras.optimizers.Adam(learning_rate=0.0001)
model.compile(loss=loss, optimizer=adam, metrics=['accuracy'])
return model
model = get_inception_resnet_v2_pretrained()
model.summary()
❶ 使用 Sequential API 定义一个模型。
❷ 定义一个输入层来接收大小为 224 × 224 × 3 的图像批次。
❸ 下载并使用预训练的 InceptionResNetV2 模型(不包括内置分类器)。
❹ 添加一个 dropout 层。
❺ 添加一个具有 200 个节点的新分类器层。
如果你打印此模型的摘要,你将得到以下输出:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
inception_resnet_v2 (Model) (None, 1536) 54336736
_________________________________________________________________
dropout (Dropout) (None, 1536) 0
_________________________________________________________________
dense (Dense) (None, 200) 307400
=================================================================
Total params: 54,644,136
Trainable params: 54,583,592
Non-trainable params: 60,544
_________________________________________________________________
如您所见,InceptionResNet v2 模型被视为我们模型中的单个层。换句话说,它是一个嵌套模型,其中外层模型(sequential)有一个内层模型(inception_resnet_v2)。但是我们需要更多的透明度,因为我们将要访问 inception_resnet_v2 模型内的特定层,以实现 Grad-CAM。因此,我们将“解开”或移除此嵌套,并且只描述模型的层。我们可以使用以下代码实现这一点:
K.clear_session()
model = load_model(os.path.join('models','inception_resnet_v2.h5'))
def unwrap_model(model):
inception = model.get_layer('inception_resnet_v2')
inp = inception.input
out = model.get_layer('dropout')(inception.output)
out = model.get_layer('dense')(out)
return Model(inp, out)
unwrapped_model = unwrap_model(model)
unwrapped_model.summary()
实质上我们正在做的是取现有模型并略微更改其输入。在取得现有模型后,我们将输入更改为 inception_resnet_v2 模型的输入层。然后,我们定义一个新模型(本质上使用与旧模型相同的参数)。然后你将看到以下输出。没有更多的模型在模型内部:
Model: "model"
___________________________________________________________________________
➥ ________________
Layer (type) Output Shape Param # Connected
➥ to
===========================================================================
➥ ================
input_2 (InputLayer) [(None, None, None, 0
___________________________________________________________________________
➥ ________________
conv2d (Conv2D) (None, None, None, 3 864
➥ input_2[0][0]
___________________________________________________________________________
➥ ________________
batch_normalization (BatchNorma (None, None, None, 3 96
➥ conv2d[0][0]
___________________________________________________________________________
➥ ________________
activation (Activation) (None, None, None, 3 0
➥ batch_normalization[0][0]
___________________________________________________________________________
➥ ________________
...
___________________________________________________________________________
➥ ________________
conv_7b (Conv2D) (None, None, None, 1 3194880
➥ block8_10[0][0]
___________________________________________________________________________
➥ ________________
conv_7b_bn (BatchNormalization) (None, None, None, 1 4608
➥ conv_7b[0][0]
___________________________________________________________________________
➥ ________________
conv_7b_ac (Activation) (None, None, None, 1 0
➥ conv_7b_bn[0][0]
___________________________________________________________________________
➥ ________________
global_average_pooling2d (Globa (None, 1536) 0
➥ conv_7b_ac[0][0]
___________________________________________________________________________
➥ ________________
dropout (Dropout) (None, 1536) 0
➥ global_average_pooling2d[0][0]
___________________________________________________________________________
➥ ________________
dense (Dense) (None, 200) 307400
➥ dropout[1][0]
===========================================================================
➥ ================
Total params: 54,644,136
Trainable params: 54,583,592
Non-trainable params: 60,544
___________________________________________________________________________
➥ ________________
接下来,我们将进行一次更改:向我们的模型引入一个新输出。请记住,我们使用功能 API 来定义我们的模型。这意味着我们可以在我们的模型中定义多个输出。我们需要的输出是 inception_resnet_v2 模型中最后一个卷积层产生的特征图。这是 Grad-CAM 计算的核心部分。您可以通过查看解开模型的模型摘要来获得最后一个卷积层的层名称:
last_conv_layer = 'conv_7b' # This is the name of the last conv layer of the model
grad_model = Model(
inputs=unwrapped_model.inputs,
outputs=[
unwrapped_model.get_layer(last_conv_layer).output,
unwrapped_model.output
]
)
有了我们的模型准备好后,让我们转向数据。我们将使用验证数据集来检查我们的模型。特别地,我们将编写一个函数(见清单 B.2)来接收以下内容:
-
image_path(str)- 数据集中图像的路径。
-
val_df(pd.DataFrame)—一个包含从图像名称到 wnid(即 WordNet ID)的映射的 pandas 数据框。请记住,wnid 是用于识别特定对象类的特殊编码。
-
class_indices(dict)—一个 wnid(字符串)到类别(0-199 之间的整数)的映射。这保留了关于哪个 wnid 在模型的最终输出层中由哪个索引表示的信息。
-
words(pd.DataFrame)—一个包含从 wnid 到类别的可读描述的映射的 pandas 数据框。
清单 B.2 检索转换后的图像、类别索引和人类可读标签
img_path = 'data/tiny-imagenet-200/val/images/val_434.JPEG'
val_df = pd.read_csv( ❶
os.path.join('data','tiny-imagenet-200', 'val', 'val_annotations.txt'),
sep='\t', index_col=0, header=None
)
with open(os.path.join('data','class_indices'),'rb') as f: ❷
class_indices = pickle.load(f)
words = pd.read_csv( ❸
os.path.join('data','tiny-imagenet-200', 'words.txt'),
sep='\t', index_col=0, header=None
)
def get_image_class_label(img_path, val_df, class_indices, words):
""" Returns the normalized input, class (int) and the label name for a given image"""
img = np.expand_dims( ❹
np.asarray(
Image.open(img_path).resize((224,224) ❺
)
img /= 127.5 ❻
img -= 1 ❻
if img.ndim == 3:
img = np.repeat(np.expand_dims(img, axis=-1), 3, axis=-1) ❼
_, img_name = os.path.split(img_path)
wnid = val_df.loc[img_name,1] ❽
cls = class_indices[wnid] ❾
label = words.loc[wnid, 1] ❿
return img, cls, label
# Test the function with a test image
img, cls, label = get_image_class_label(img_path, val_df, class_indices, words)⓫
❶ 读取 val_annotations.txt。这将创建一个数据框,其中包含从图像文件名到 wnid(即 WordNet ID)的映射。
❷ 加载将 wnid 映射到类索引(整数)的类索引。
❸ 这将创建一个数据框,其中包含从 wnid 到类描述的映射。
❹ 加载由文件路径给出的图像。首先,我们添加一个额外的维度来表示批次维度。
❺ 将图像调整大小为 224×224 大小的图像。
❻ 将图像像素值调整到[-1, 1]的范围内。
❼ 如果图像是灰度的,则在通道维度上将图像重复三次,以与 RGB 图像具有相同的格式。
❽ 获取图像的 wnid。
❾ 获取图像的类别索引。
❿ 获取类的字符串标签。
⓫ 对一个示例图像运行该函数。
get_image_class_label()函数使用指定的参数并加载由 image_path 给出的图像。首先,我们将图像调整大小为 224×224 大小的图像。我们还在开始时添加了一个额外的维度来表示图像作为一个图像批次。然后,它执行特定的数值转换(即,逐元素除以 127.5 并减去 1)。这是用于训练 InceptionResNet v2 模型的特殊转换。然后,我们使用传递给函数的数据框和 class_indices 获取该类的类索引(即,整数)和人类可读的标签。最后,它返回转换后的图像、类索引和图像所属类的标签。
下一个清单显示了如何为图像计算 Grad-CAMs。我们将使用 10 个图像分别计算 Grad-CAMs。
清单 B.3 为 10 个图像计算 Grad-CAM
# Define a sample probe set to get Grad-CAM
image_fnames = [
os.path.join('data','tiny-imagenet-200', 'val','images',f) \
for f in [
'val_9917.JPEG', 'val_9816.JPEG', 'val_9800.JPEG', 'val_9673.JPEG',
➥ 'val_9470.JPEG',
'val_4.JPEG', 'val_127.JPEG', 'val_120.JPEG', 'val_256.JPEG',
➥ 'val_692.JPEG'
]
]
grad_info = {}
for fname in image_fnames: ❶
img, cls, label = get_image_class_label(fname, val_df, class_indices, words)❷
with tf.GradientTape() as tape: ❸
conv_output, preds = grad_model(img) ❸
loss = preds[:, cls] ❹
grads = tape.gradient(loss, conv_output) ❺
weights = tf.reduce_mean(grads, axis=(1, 2), keepdims=True) ❻
grads *= weights ❻
grads = tf.reduce_sum(grads, axis=(0,3)) ❼
grads = tf.nn.relu(grads) ❼
grads /= tf.reduce_max(grads) ❽
grads = tf.cast(grads*255.0, 'uint8') ❽
grad_info[fname] = {'image': img, 'class': cls, 'label':label, 'gradcam':
➥ grads} ❾
❶ 获取每个图像的标准化输入、类别(整数)和标签(字符串)。
❷ 在 GradientTape 环境中计算模型的输出。
❸ 这将使我们能够稍后访问在计算过程中出现的梯度。
❹ 我们只考虑输入图像的类索引对应的损失。
❺ 获取与最后一个卷积特征图相关的损失的梯度。
❻ 计算并应用权重。
❼ 将特征图折叠为单个通道,以获得最终的热图。
❽ 将值归一化为 0-255 的范围内。
❾ 将计算出的 GradCAMs 存储在字典中以便稍后可视化。
要计算一张图像的 Grad-CAM,我们遵循以下步骤。首先,我们获得给定图像路径的转换图像、类索引和标签。
接下来是此计算的最重要步骤!您知道,给定一张图像和一个标签,最终损失被计算为所有可用类别的类特定损失的总和。也就是说,如果您想象一个独热编码的标签和模型输出的概率向量,我们计算每个输出节点之间的损失。这里每个节点代表一个单独的类别。为了计算梯度映射,我们首先仅针对该图像的真实标签计算类特定损失的梯度,关于最后一个卷积层的输出。这给出了一个与最后一个卷积层的输出大小相同的张量。重要的是注意我们使用的典型损失和这里使用的损失之间的区别。通常,我们将所有类别的损失求和,而在 Grad-CAM 中,我们仅考虑与输入的真实类对应的特定节点的损失。
注意我们如何计算梯度。我们使用了一种称为 GradientTape 的东西(mng.bz/wo1Q)。这是 TensorFlow 中的一项创新技术。每当在 GradientTape 的上下文中计算某些东西时,它将记录所有这些计算的梯度。这意味着当我们在 GradientTape 的上下文中计算输出时,我们可以稍后访问该计算的梯度。
然后我们进行一些额外的转换。首先,我们计算输出特征图的每个通道的权重。这些权重简单地是该特征图的均值。然后将特征图的值乘以这些权重。然后我们对所有通道的输出进行求和。这意味着我们将得到一个宽度和高度都为一个通道的输出。这本质上是一个热图,其中高值表示在给定像素处更重要。为了将负值剪切为 0,然后在输出上应用 ReLU 激活。作为最终的归一化步骤,我们将所有值带到 0-255 的范围,以便我们可以将其作为热图叠加在实际图像上。然后只需使用 matplotlib 库绘制图像,并将我们生成的 Grad-CAM 输出叠加在图像上。如果您想查看此代码,请参阅 Ch07-Improving-CNNs-and-Explaining/7.3.Interpreting_CNNs_ Grad-CAM.ipynb 笔记本。最终输出将如图 B.1 所示。
图 B.1 几个探测图像的 Grad-CAM 输出可视化。图像中越红的区域,模型就越关注该图像的那部分。您可以看到我们的模型已经学会了理解一些复杂的场景,并分离出需要关注的模型。
B.2 图像分割:U-Net 模型
在第八章中,我们讨论了 DeepLab v3:一个图像分割模型。在本节中,我们将讨论另一个被称为 U-Net 的图像分割模型。它的架构与 DeepLab 模型不同,并且在实际中非常常用。因此,这是一个值得学习的模型。
B.2.1 理解和定义 U-Net 模型
U-Net 模型本质上是两个镜像反射的全卷积网络,充当编码器和解码器,还有一些额外的连接,将编码器的部分连接到解码器的部分。
U-Net 的背景
U-Net 是在论文“U-Net: Convolution Networks for Biomedical Image Segmentation”(arxiv.org/pdf/1505.04597.pdf)中被介绍的,其起源于生物医学图像分割。U-Net 的名称源自于网络的形状。它仍然是生物学/医学领域分割任务中常用的选择,并且已经被证明在更一般的任务中也能很好地工作。
首先,我们将看一下原始的 U-Net 模型,该模型在论文中被介绍。然后,我们稍微调整我们的讨论方向,使其更适合当前问题。原始模型使用的是一个 572 × 572 × 1 大小的图像(即灰度图像),并输出一个 392 × 392 × 2 大小的图像。该网络经过训练,可以识别/分割细胞边界。因此,输出中的两个通道表示像素是否属于细胞边界的二进制输出。
编码器由多个下采样模块组成,逐渐对输入进行下采样。一个下采样模块包括两个卷积层和一个最大池化层。具体来说,一个下采样模块包括:
-
一个 3 × 3 卷积层(valid padding)× 2
-
一个 2 × 2 最大池化层(除了最后一个下采样模块)
一系列这样的下采样层将大小为 572 × 572 × 1 的输入转换为大小为 28 × 28 × 1024 的输出。
接下来,解码器由多个上采样层组成。具体来说,每个解码器上采样模块包括:
-
一个 2 × 2 转置卷积层
-
一个 3 × 3 卷积层(valid padding)× 2
你可能已经想知道,什么是转置卷积层?转置卷积是反向计算卷积层中发生的计算得到的结果。转置卷积不是缩小输出的卷积运算(即使用步长),而是增加输出的大小(即上采样输入)。这也被称为分数步长,因为使用转置卷积时,增加步长会产生更大的输出。如图 B.2 所示。
图 B.2 标准卷积与转置卷积的对比。标准卷积的正向步长会导致输出更小,而转置卷积的正向步长会导致输出更大的图像。
最后,有跳跃连接将编码器的中间层连接到解码器的中间层。这是一个重要的架构设计,因为它为解码器提供了所需的空间/上下文信息,否则这些信息将会丢失。特别地,编码器的第 i^(th)级输出与解码器的第 n-i^(th)级输入连接起来(例如,第一级的输出[大小为 568 × 568 × 64]被连接到解码器的最后一级输入上 [大小为 392 × 392 × 64];图 B.3)。为了做到这一点,编码器的输出首先需要稍微裁剪一下,以匹配相应的解码器层的输出。
图 B.3 原始 U-Net 模型。浅色块代表编码器,深色块代表解码器。垂直数字表示给定位置的输出大小(高度和宽度),顶部的数字表示滤波器数量。
B.2.2 比编码器更好的是什么?一个预训练的编码器
如果你直接将原始网络用于 Pascal VOC 数据集,你可能会对其性能感到非常失望。这种行为背后可能有几个原因:
-
Pascal VOC 中的数据比原始 U-Net 设计的要复杂得多。例如,与黑白图像中包含简单细胞结构不同,我们有包含现实世界复杂场景的 RGB 图像。
-
作为一个完全卷积网络,U-Net 具有很高的正则化程度(由于参数数量较少)。这个参数数量不足以以足够的准确度解决我们所面临的复杂任务。
-
作为一个从随机初始化开始的网络,它需要学会在没有来自预训练模型的预训练知识的情况下解决任务。
按照这种推理,让我们讨论一下我们将对原始 U-Net 架构进行的一些改变。我们将实现一个具有
-
预训练的编码器
-
每个解码器模块中的滤波器数量更多
我们将使用的预训练编码器是一个 ResNet-50 模型(arxiv.org/pdf/1512.03385.pdf)。几年前,它是计算机视觉社区中引起轰动的开创性残差网络之一。我们只会简单地介绍 ResNet-50,因为我们将在 DeepLab v3 模型的部分详细讨论该模型。ResNet-50 模型由多个卷积块组成,后跟一个全局平均池化层和一个具有 softmax 激活的完全连接的最终预测层。卷积块是该模型的创新部分(在图 B.4 中用 B 表示)。原始模型有 16 个卷积块组织成 5 组。我们将仅使用前 13 个块(即前 4 组)。单个块由三个卷积层(步幅为 2 的 1 × 1 卷积层、3 × 3 卷积层和 1 × 1 卷积层)、批量归一化和残差连接组成,如图 B.4 所示。我们在第七章深入讨论了残差连接。
图 B.4 修改后的 U-Net 架构(最佳查看彩色)。此版本的 U-Net 将 ResNet-50 模型的前四个块作为编码器,并将解码器规格(例如,滤波器数量)增加到与匹配的编码器层的规格相匹配。
实现修改后的 U-Net
通过对模型及其不同组件进行深入的概念理解,是时候在 Keras 中实现它了。我们将使用 Keras 函数式 API。首先,我们定义网络的编码器部分:
inp = layers.Input(shape=(512, 512, 3))
# Defining the pretrained resnet 50 as the encoder
encoder = tf.keras.applications.ResNet50 (
include_top=False, input_tensor=inp,pooling=None
)
接下来,我们讨论解码器的花哨之处。解码器由多个上采样层组成,这些层具有两个重要功能:
-
将输入上采样到更大的输出
-
复制、裁剪和连接匹配的编码器输入
下面的列表中显示的函数封装了我们概述的计算。
列表 B.4 修改后的 UNet 解码器的上采样层
def upsample_conv(inp, copy_and_crop, filters):
""" Up sampling layer of the U-net """
# 2x2 transpose convolution layer
conv1_out = layers.Conv2DTranspose(
filters, (2,2), (2,2), activation='relu'
)(inp)
# Size of the crop length for one side
crop_side = int((copy_and_crop.shape[1]-conv1_out.shape[1])/2)
# Crop if crop side is > 0
if crop_side > 0:
cropped_copy = layers.Cropping2D(crop_side)(copy_and_crop)
else:
cropped_copy = copy_and_crop
# Concat the cropped encoder output and the decoder output
concat_out = layers.Concatenate(axis=-1)([conv1_out, cropped_copy])
# 3x3 convolution layer
conv2_out = layers.Conv2D(
filters, (3,3), activation='relu', padding='valid'
)(concat_out)
# 3x3 Convolution layer
out = layers.Conv2D(
filters, (3,3), activation='relu', padding='valid'
)(conv2_out)
return out
让我们分析我们编写的函数。它接受以下参数:
-
输入—层的输入
-
copy_and_crop—从编码器复制过来的输入
-
filters—执行转置卷积后的输出滤波器数量
首先,我们执行转置卷积,如下所示:
conv1_out = layers.Conv2DTranspose(
filters=filters, kernel_size=(2,2),
strides=(2,2), activation='relu'
)(inp)
Conv2DTranspose 的语法与我们多次使用的 Conv2D 相同。它有一些滤波器、卷积核大小(高度和宽度)、步长(高度和宽度)、激活函数和填充(默认为 valid)。我们将根据转置卷积输出的大小和编码器的输入来计算裁剪参数。然后,根据需要使用 Keras 层 Cropping2D 进行裁剪:
crop_side = int((copy_and_crop.shape[1]-conv1_out.shape[1])/2)
if crop_side > 0:
cropped_copy = layers.Cropping2D(crop_side)(copy_and_crop)
else:
cropped_copy = copy_and_crop
在这里,我们首先计算从一侧裁剪多少,方法是从上采样输出 conv1_out 中减去编码器的大小。然后,如果大小大于零,则通过将 crop_side 作为参数传递给 Cropping2D Keras 层来计算 cropped_copy。然后将裁剪后的编码器输出和上采样的 conv1_out 连接起来以产生单个张量。这通过两个具有 ReLU 激活和有效填充的 3 × 3 卷积层产生最终输出。我们现在完全定义解码器(请参见下一个清单)。解码器由三个上采样层组成,这些层使用前一层的输出以及复制的编码器输出。
清单 B.5 修改后的 U-Net 模型的解码器
def decoder(inp, encoder):
""" Define the decoder of the U-net model """
up_1 = upsample_conv(inp, encoder.get_layer("conv3_block4_out").output,
➥ 512) # 32x32
up_2 = upsample_conv(up_1,
➥ encoder.get_layer("conv2_block3_out").output, 256) # 64x64
up_3 = upsample_conv(up_2, encoder.get_layer("conv1_relu").output, 64)
➥ # 128 x 128
return up_3
跨越预定义模型的中间输出的复制不是我们以前做过的事情。因此,值得进一步调查。我们不能够诉诸于先前定义的代表编码器输出的变量,因为这是一个通过 Keras 下载的预定义模型,没有用于创建模型的实际变量的引用。
但是访问中间输出并使用它们创建新连接并不那么困难。你只需要知道要访问的层的名称即可。这可以通过查看 encoder.summary() 的输出来完成。例如,在这里(根据图 B.4),我们获得了 conv3、conv2 和 conv1 模块的最后输出。要获取 conv3_block4_out 的输出,你需要做的就是
encoder.get_layer("conv3_block4_out").output
将其传递给我们刚刚定义的上采样卷积层。能够执行这样复杂的操作证明了 Keras 函数 API 有多么灵活。最后,你可以在下一个清单中的函数 unet_pretrained_encoder() 中定义完整修改后的 U-Net 模型。
清单 B.6 完整修改后的 U-Net 模型
def unet_pretrained_encoder():
""" Define a pretrained encoder based on the Resnet50 model """
# Defining an input layer of size 384x384x3
inp = layers.Input(shape=(512, 512, 3))
# Defining the pretrained resnet 50 as the encoder
encoder = tf.keras.applications.ResNet50 (
include_top=False, input_tensor=inp,pooling=None
)
# Encoder output # 8x8
decoder_out = decoder(encoder.get_layer("conv4_block6_out").output, encoder)
# Final output of the model (note no activation)
final_out = layers.Conv2D(num_classes, (1,1))(decoder_out)
# Final model
model = models.Model(encoder.input, final_out)
return model
这里发生的情况非常清楚。我们首先定义一个大小为 512 × 512 × 3 的输入,将其传递给编码器。我们的编码器是一个没有顶部预测层或全局池化的 ResNet-50 模型。接下来,我们定义解码器,它将 conv4_block6_out 层的输出作为输入(即 ResNet-50 模型的 conv4 块的最终输出),然后逐渐使用转置卷积操作上采样它。此外,解码器复制、裁剪和连接匹配的编码器层。我们还定义一个产生最终输出的 1 × 1 卷积层。最后,我们使用 Keras 函数 API 定义端到端模型。
附录 C:自然语言处理
C.1 环游动物园:遇见其他 Transformer 模型
在第十三章,我们讨论了一种强大的基于 Transformer 的模型,称为 BERT(双向编码器表示来自 Transformer)。但 BERT 只是一波 Transformer 模型的开始。这些模型变得越来越强大,更好,要么通过解决 BERT 的理论问题,要么重新设计模型的各个方面以实现更快更好的性能。让我们了解一些流行的模型,看看它们与 BERT 的不同之处。
C.1.1 生成式预训练(GPT)模型(2018)
故事实际上甚至早于 BERT。OpenAI 在 Radford 等人的论文“通过生成式预训练改善语言理解”中引入了一个模型称为 GPT(mng.bz/1oXV)。它的训练方式类似于 BERT,首先在大型文本语料库上进行预训练,然后进行有区分性的任务微调。与 BERT 相比,GPT 模型是一个Transformer 解码器。它们的区别在于,GPT 模型具有从左到右(或因果)的注意力,而 BERT 在计算自注意力输出时使用双向(即从左到右和从右到左)注意力。换句话说,GPU 模型在计算给定单词的自注意力输出时只关注其左侧的单词。这与我们在第五章讨论的掩码注意力组件相同。因此,GPT 也被称为自回归模型,而 BERT 被称为自编码器。此外,与 BERT 不同,将 GPT 适应不同的任务(如序列分类、标记分类或问题回答)需要进行轻微的架构更改,这很麻烦。GPT 有三个版本(GPT-1、GPT-2 和 GPT-3);每个模型都变得更大,同时引入轻微的改进以提高性能。
注意 OpenAI,TensorFlow:github.com/openai/gpt-2。
C.1.2 DistilBERT(2019)
跟随 BERT,DistilBERT 是由 Hugging Face 在 Sanh 等人的论文“DistilBERT, a distilled version of BERT: Smaller, faster, cheaper and lighter”(arxiv.org/pdf/1910.01108v4.pdf)中介绍的模型。DistilBERT 的主要焦点是在保持性能相似的情况下压缩 BERT。它是使用一种称为知识蒸馏(mng.bz/qYV2)的迁移学习技术进行训练的。其思想是有一个教师模型(即 BERT),和一个更小的模型(即 DistilBERT),试图模仿教师的输出。DistilBERT 模型相比于 BERT 更小,只有 6 层,而不是 BERT 的 12 层。DistilBERT 模型是以 BERT 的每一层的初始化为基础进行初始化的(因为 DistilBERT 恰好有 BERT 层的一半)。DistilBERT 的另一个关键区别是它只在掩码语言建模任务上进行训练,而不是在下一个句子预测任务上进行训练。
注意 Hugging Face 的 Transformers:huggingface.co/transformers/model_doc/distilbert.xhtml。
C.1.3 RoBERT/ToBERT(2019)
RoBERT(递归 BERT)和 ToBERT(基于 BERT 的 Transformer)是由 Pappagari 等人在论文“Hierarchical Transformer Models for Long Document Classification”(arxiv.org/pdf/1910.10781.pdf)中介绍的两个模型。这篇论文解决的主要问题是 BERT 在长文本序列(例如,电话转录)中的性能下降或无法处理。这是因为自注意力层对长度为 n 的序列具有 O(n²) 的计算复杂度。这些模型提出的解决方案是将长序列分解为长度为 k 的较小段(有重叠),并将每个段馈送到 BERT 以生成汇集输出(即 [CLS] 标记的输出)或来自任务特定分类层的后验概率。然后,堆叠 BERT 为每个段返回的输出,并将其传递给像 LSTM(RoBERT)或较小 Transformer(ToBERT)这样的递归模型。
注意 Hugging Face 的 Transformers:huggingface.co/transformers/model_doc/roberta.xhtml。
C.1.4 BART(2019)
BART(双向和自回归 Transformer)由 Lewis 等人在“BART:去噪序列到序列预训练用于自然语言生成、翻译和理解”(arxiv.org/pdf/1910.13461.pdf)中提出,是一个序列到序列模型。我们在第 11 和 12 章中已经讨论了序列到序列模型,BART 也借鉴了这些概念。BART 有一个编码器和一个解码器。如果你还记得第五章,Transformer 模型也有一个编码器和一个解码器,可以视为序列到序列模型。Transformer 的编码器具有双向注意力,而 Transformer 的解码器具有从左到右的注意力(即是自回归的)。
与原始 Transformer 模型不同,BART 使用了几种创新的预训练技术(文档重建)来预训练模型。特别地,BART 被训练成为一个去噪自编码器,其中提供了一个有噪声的输入,并且模型需要重建真实的输入。在这种情况下,输入是一个文档(一系列句子)。这些文件使用表 C.1 中列出的方法进行损坏。
表 C.1 文档损坏所采用的各种方法。真实文档为“I was hungry. I went to the café.”下划线字符(_)代表遮蔽标记。
| 方法 | 描述 | 例子 |
|---|---|---|
| 记号遮蔽 | 句子中的记号随机遮蔽。 | 我饿了。我去了 _ 咖啡馆。 |
| 记号删除 | 随机删除记号。 | 我饿了。我去了咖啡馆。 |
| 句子排列 | 更改句子顺序。 | 我去了咖啡馆。我饿了。 |
| 文档旋转 | 旋转文档,以便文档的开头和结尾发生变化。 | 咖啡馆。我饿了。我去了 |
| 文本补齐 | 使用单一遮蔽标记遮蔽跨度标记。一个长度为 0 的跨度将插入遮蔽标记。 | 我饿了。我 _ 去了咖啡馆。 |
使用损坏逻辑,我们生成输入到 BART 的数据,目标就是没有损坏的真实文档。初始时,已经损坏的文档被输入到编码器,然后解码器被要求递归地预测出真实的序列,同时使用先前预测出的输出作为下一个输入。这类似于第十一章使用机器翻译模型预测翻译的方法。
模型预训练后,你可以将 BART 用于 Transformer 模型通常用于的任何 NLP 任务。例如,可以按以下方式将 BART 用于序列分类任务(例如,情感分析):
-
把记号序列(例如,电影评论)输入到编码器和解码器中。
-
在给解码器输入时,在序列末尾添加一个特殊记号(例如,[CLS])。我们在使用 BERT 时将特殊记号添加到序列开头。
-
通过解码器得到特殊标记的隐藏表示结果,并将其提供给下游分类器以预测最终输出(例如,积极/消极的预测结果)。
如果要将 BART 用于序列到序列的问题(例如机器翻译),请按照以下步骤进行:
-
将源序列输入编码器。
-
向目标序列的开头和结尾分别添加起始标记(例如,[SOS])和结束标记(例如,[EOS])。
-
在训练时,使用除了最后一个标记以外的所有目标序列标记作为输入,使用除了第一个标记以外的所有标记作为目标(即,teacher forcing)来训练解码器。
-
在推理时,将起始标记提供给解码器作为第一个输入,并递归地预测下一个输出,同时将前一个输出(件)作为输入(即自回归)。
注意 Hugging Face’s Transformers: mng.bz/7ygy。
C.1.5 XLNet (2020)
XLNet 是由杨飞等人在 2020 年初发布的论文 "XLNet: Generalized Autoregressive Pretraining for Language Understanding" 中推出的。它的主要重点是捕捉基于自编码器模型(例如 BERT)和自回归模型(例如 GPT)两种方法的优点,这很重要。
作为自编码器模型,BERT 具有的一个关键优势是,任务特定的分类头包含了由双向上下文丰富化的标记的隐藏表示。正如你所想像的那样,了解给定标记之前和之后的内容能够得到更好的下游任务效果。相反地,GPT 只关注给定单词的左侧来生成表示。因此,GPT 的标记表示是次优的,因为它们只关注(左侧)上下文的单向 注意力。
另一方面,BERT 的预训练方法涉及引入特殊标记 [MASK]。虽然这个标记出现在预训练的上下文中,但它在微调的上下文中从未出现过,造成了预训练和微调之间的差异。
BERT 中存在一个更为关键的问题。BERT 假设掩盖标记是单独构建的(即独立假设),这在语言建模中是错误的。换句话说,如果你有句子 "I love [MASK][1] [MASK][2] city",第二个掩盖标记是独立于 [MASK][1] 的选择而生成的。这是错误的,因为要生成一个有效的城市名称,必须在生成 [MASK][2] 之前先了解 [MASK][1] 的值。然而,GPT 的自回归性质允许模型先预测 [MASK][1] 的值,然后使用其与其左侧的其他单词一起生成城市的第一个单词的值,然后生成第二个单词的值(即上下文感知)。
XLNet 将这两种语言建模方法融合为一体,因此你可以从 BERT 使用的方法中得到双向上下文,以及从 GPT 的方法中得到上下文感知。这种新方法被称为置换语言建模。其思想如下。考虑一个长度为 T 的单词序列。对于该序列,有 T!种排列方式。例如,句子“Bob 爱猫”将有 3! = 3 × 2 × 1 = 6 种排列方式:
Bob loves cats
Bob cats loves
loves Bob cats
loves cats Bob
cats Bob loves
cats loves Bob
如果用于学习的语言模型的参数在所有排列中共享,我们不仅可以使用自回归方法来学习它,还可以捕获给定单词的文本两侧的信息。这是论文中探讨的主要思想。
注意 Hugging Face 的 Transformers:mng.bz/mOl2。
C.1.6 Albert(2020)
Albert 是 BERT 模型的一种变体,其性能与 BERT 相媲美,但参数更少。Albert 做出了两项重要贡献:减少模型大小和引入一种新的自监督损失,有助于模型更好地捕捉语言。
嵌入层的因式分解
首先,Albert 对 BERT 中使用的嵌入矩阵进行了因式分解。在 BERT 中,嵌入是大小为 V × H 的度量,其中 V 是词汇表大小,H 是隐藏状态大小。换句话说,嵌入大小(即嵌入向量的长度)与最终隐藏表示大小之间存在紧密耦合。然而,嵌入(例如 BERT 中的 WordPiece 嵌入)并不是设计用来捕捉上下文的,而隐藏状态是在考虑了标记及其上下文的情况下计算的。因此,增大隐藏状态大小 H 是有意义的,因为隐藏状态比嵌入更能捕获标记的信息性表示。但是,由于存在紧密耦合,这样做会增加嵌入矩阵的大小。因此,Albert 建议对嵌入矩阵进行因式分解为两个矩阵,V × E 和 E × H,从而解耦嵌入大小和隐藏状态大小。通过这种设计,可以增加隐藏状态大小,同时保持嵌入大小较小。
跨层参数共享
跨层参数共享是 Albert 中引入的另一种减少参数空间的技术。由于 BERT 中的所有层(以及 Transformer 模型一般)从顶部到底部都具有统一的层,参数共享是微不足道的。参数共享可以通过以下三种方式之一实现:
-
在所有自注意子层之间
-
在所有完全连接的子层之间
-
在自注意和完全连接的子层之间(分开)
Albert 将跨层共享所有参数作为默认策略。通过使用这种策略,Albert 实现了 71%到 86%的参数减少,而不会显著影响模型的性能。
句序预测而非下一句预测
论文的作者最终认为,BERT 中基于下一句预测的预训练任务所增加的价值是值得怀疑的,这一点得到了多项先前研究的支持。因此,他们提出了一种新的、更具挑战性的模型,主要关注语言的一致性:句子顺序预测。在这个模型中,模型使用一个二元分类头来预测给定的一对句子是否是正确的顺序。数据可以很容易地生成,正样本是按照顺序排列的相邻句子,而负样本则是通过交换两个相邻句子生成的。作者认为这比基于下一句预测的任务更具挑战性,导致比 BERT 更具见解的模型。
备注 TFHub: (tfhub.dev/google/albert_base/3)。Hugging Face 的 Transformers: (mng.bz/5QM1)。
C.1.7 Reformer (2020)
Reformer 是最近加入 Transformer 家族的模型之一。Reformer 的主要思想是能够扩展到包含数万个标记的序列。Reformer 在 2020 年初由 Kitaev 等人在论文“Reformer: The Efficient Transformer”中提出(arxiv.org/pdf/2001.04451.pdf)。
防止普通 Transformer 被用于长序列的主要限制是自注意力层的计算复杂性。它需要对每个词查看所有其他词,以生成最终的表示,这对含有 L 个标记的序列具有 O(L²) 的复杂性。Reformer 使用局部敏感哈希(LSH)将这种复杂性降低到 O(L logL)。LSH 的思想是为每个输入分配一个哈希;具有相同哈希的输入被认为是相似的,并被分配到同一个桶。这样,相似的输入就会被放在同一个桶中。为此,我们需要对自注意力子层进行若干修改。
自注意力层中的局部敏感哈希
首先,我们需要确保 Q 和 K 矩阵是相同的。这是必要的,因为其思想是计算查询和键之间的相似性。这可以通过共享 Q 和 K 的权重矩阵的权重来轻松实现。接下来,需要开发一个哈希函数,可以为给定的查询/键生成哈希,使得相似的查询/键(共享-qk)获得相似的哈希值。同时需要记住,这必须以可微的方式完成,以确保模型的端到端训练。使用了以下哈希函数:
h(x) = argmax([xR; - xR])
其中,R 是大小为[d_model, b/2]的随机矩阵,用于用户定义的 b(即,桶的数量),x 是形状为[b, L, d_model]的输入。通过使用这个散列函数,您可以得到批处理中每个输入标记在给定位置的桶 ID。要了解更多关于这个技术的信息,请参考原始论文“Practical and Optimal LSH for Angular Distance” by Andoni et al.(arxiv.org/pdf/1509.02897.pdf)。根据桶 ID,共享的 qk 项被排序。
然后,排序后的共享的 qk 项使用固定的块大小进行分块。更大的块大小意味着更多的计算(即,会考虑更多单词来给定标记),而较小的块大小可能意味着性能不佳(即,没有足够的标记可供查看)。
最后,自注意力的计算如下。对于给定的标记,查看它所在的相同块以及前一个块,并关注这两个块中具有相同桶 ID 的单词。这将为输入中提供的所有标记产生自注意力输出。这样,模型不必为每个标记查看每个其他单词,可以专注于给定标记的子集单词或标记。这使得模型可伸缩到长达几万个标记的序列。
注意 Hugging Face's Transformers: mng.bz/6XaD。