TensorFlow 实战(八)(5)

简介: TensorFlow 实战(八)

TensorFlow 实战(八)(4)https://developer.aliyun.com/article/1522971

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 用于序列分类任务(例如,情感分析):

  1. 把记号序列(例如,电影评论)输入到编码器和解码器中。
  2. 在给解码器输入时,在序列末尾添加一个特殊记号(例如,[CLS])。我们在使用 BERT 时将特殊记号添加到序列开头。
  3. 通过解码器得到特殊标记的隐藏表示结果,并将其提供给下游分类器以预测最终输出(例如,积极/消极的预测结果)。

如果要将 BART 用于序列到序列的问题(例如机器翻译),请按照以下步骤进行:

  1. 将源序列输入编码器。
  2. 向目标序列的开头和结尾分别添加起始标记(例如,[SOS])和结束标记(例如,[EOS])。
  3. 在训练时,使用除了最后一个标记以外的所有目标序列标记作为输入,使用除了第一个标记以外的所有标记作为目标(即,teacher forcing)来训练解码器。
  4. 在推理时,将起始标记提供给解码器作为第一个输入,并递归地预测下一个输出,同时将前一个输出(件)作为输入(即自回归)。

注意 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

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