SklearnTensorFlow与Keras机器学习实用指南第三版(七)绝不原创的飞龙

我们能否构建一台能够掌握书面和口头语言的机器?这是自然语言处理研究的终极目标,但实际上研究人员更专注于更具体的任务,比如文本分类、翻译、摘要、问答等等。

让我们从一个简单而有趣的模型开始,这个模型可以像莎士比亚一样写作(某种程度上)。

潘达鲁斯:

唉,我想他将会被接近并且这一天

当一点点智慧被获得而从未被喂养时,

而谁不是一条链,是他死亡的主题,

我不应该睡觉。

虽然不是杰作,但仍然令人印象深刻,模型能够学习单词、语法、正确的标点符号等,只是通过学习预测句子中的下一个字符。这是我们的第一个语言模型示例;本章后面讨论的类似(但更强大)的语言模型是现代自然语言处理的核心。在本节的其余部分,我们将逐步构建一个char-RNN,从创建数据集开始。

>>>print(shakespeare_text[:80])FirstCitizen:Beforeweproceedanyfurther,hearmespeak.All:Speak,speak.看起来像是莎士比亚的作品!

接下来,我们将使用tf.keras.layers.TextVectorization层(在第十三章介绍)对此文本进行编码。我们设置split="character"以获得字符级别的编码,而不是默认的单词级别编码,并且我们使用standardize="lower"将文本转换为小写(这将简化任务):

text_vec_layer=tf.keras.layers.TextVectorization(split="character",standardize="lower")text_vec_layer.adapt([shakespeare_text])encoded=text_vec_layer([shakespeare_text])[0]现在,每个字符都映射到一个整数,从2开始。TextVectorization层将值0保留给填充标记,将值1保留给未知字符。目前我们不需要这两个标记,所以让我们从字符ID中减去2,并计算不同字符的数量和总字符数:

defto_dataset(sequence,length,shuffle=False,seed=None,batch_size=32):ds=tf.data.Dataset.from_tensor_slices(sequence)ds=ds.window(length+1,shift=1,drop_remainder=True)ds=ds.flat_map(lambdawindow_ds:window_ds.batch(length+1))ifshuffle:ds=ds.shuffle(buffer_size=100_000,seed=seed)ds=ds.batch(batch_size)returnds.map(lambdawindow:(window[:,:-1],window[:,1:])).prefetch(1)这个函数开始得很像我们在第十五章中创建的to_windows()自定义实用函数:

图16-1总结了数据集准备步骤:它展示了长度为11的窗口,批量大小为3。每个窗口的起始索引在其旁边标出。

现在我们准备创建训练集、验证集和测试集。我们将大约使用文本的90%进行训练,5%用于验证,5%用于测试:

length=100tf.random.set_seed(42)train_set=to_dataset(encoded[:1_000_000],length=length,shuffle=True,seed=42)valid_set=to_dataset(encoded[1_000_000:1_060_000],length=length)test_set=to_dataset(encoded[1_060_000:],length=length)提示我们将窗口长度设置为100,但您可以尝试调整它:在较短的输入序列上训练RNN更容易更快,但RNN将无法学习任何长于length的模式,所以不要将其设置得太小。

就是这样!准备数据集是最困难的部分。现在让我们创建模型。

由于我们的数据集相当大,而建模语言是一个相当困难的任务,我们需要不止一个简单的具有几个循环神经元的RNN。让我们构建并训练一个由128个单元组成的GRU层的模型(如果需要,稍后可以尝试调整层数和单元数):

model=tf.keras.Sequential([tf.keras.layers.Embedding(input_dim=n_tokens,output_dim=16),tf.keras.layers.GRU(128,return_sequences=True),tf.keras.layers.Dense(n_tokens,activation="softmax")])model.compile(loss="sparse_categorical_crossentropy",optimizer="nadam",metrics=["accuracy"])model_ckpt=tf.keras.callbacks.ModelCheckpoint("my_shakespeare_model",monitor="val_accuracy",save_best_only=True)history=model.fit(train_set,validation_data=valid_set,epochs=10,callbacks=[model_ckpt])让我们仔细看看这段代码:

这个模型不处理文本预处理,所以让我们将其包装在一个最终模型中,包含tf.keras.layers.TextVectorization层作为第一层,加上一个tf.keras.layers.Lambda层,从字符ID中减去2,因为我们暂时不使用填充和未知标记:

shakespeare_model=tf.keras.Sequential([text_vec_layer,tf.keras.layers.Lambda(lambdaX:X-2),#noortokensmodel])现在让我们用它来预测句子中的下一个字符:

>>>y_proba=shakespeare_model.predict(["Tobeornottob"])[0,-1]>>>y_pred=tf.argmax(y_proba)#choosethemostprobablecharacterID>>>text_vec_layer.get_vocabulary()[y_pred+2]'e'太好了,模型正确预测了下一个字符。现在让我们使用这个模型假装我们是莎士比亚!

使用char-RNN模型生成新文本时,我们可以将一些文本输入模型,让模型预测最有可能的下一个字母,将其添加到文本末尾,然后将扩展后的文本提供给模型猜测下一个字母,依此类推。这被称为贪婪解码。但在实践中,这经常导致相同的单词一遍又一遍地重复。相反,我们可以随机采样下一个字符,概率等于估计的概率,使用TensorFlow的tf.random.categorical()函数。这将生成更多样化和有趣的文本。categorical()函数会根据类别对数概率(logits)随机采样随机类别索引。例如:

>>>log_probas=tf.math.log([[0.5,0.4,0.1]])#probas=50%,40%,and10%>>>tf.random.set_seed(42)>>>tf.random.categorical(log_probas,num_samples=8)#draw8samples为了更好地控制生成文本的多样性,我们可以将logits除以一个称为温度的数字,我们可以根据需要进行调整。接近零的温度偏好高概率字符,而高温度使所有字符具有相等的概率。在生成相对严格和精确的文本(如数学方程式)时,通常更喜欢较低的温度,而在生成更多样化和创意性的文本时,更喜欢较高的温度。以下next_char()自定义辅助函数使用这种方法选择要添加到输入文本中的下一个字符:

defnext_char(text,temperature=1):y_proba=shakespeare_model.predict([text])[0,-1:]rescaled_logits=tf.math.log(y_proba)/temperaturechar_id=tf.random.categorical(rescaled_logits,num_samples=1)[0,0]returntext_vec_layer.get_vocabulary()[char_id+2]接下来,我们可以编写另一个小的辅助函数,它将重复调用next_char()以获取下一个字符并将其附加到给定的文本中:

defextend_text(text,n_chars=50,temperature=1):for_inrange(n_chars):text+=next_char(text,temperature)returntext现在我们准备生成一些文本!让我们尝试不同的温度值:

首先,注意到有状态的RNN只有在批次中的每个输入序列从上一个批次中对应序列的确切位置开始时才有意义。因此,我们构建有状态的RNN需要做的第一件事是使用顺序且不重叠的输入序列(而不是我们用来训练无状态RNN的洗牌和重叠序列)。在创建tf.data.Dataset时,因此在调用window()方法时必须使用shift=length(而不是shift=1)。此外,我们必须不调用shuffle()方法。

不幸的是,为有状态的RNN准备数据集时,批处理比为无状态的RNN更加困难。实际上,如果我们调用batch(32),那么32个连续窗口将被放入同一个批次中,接下来的批次将不会继续每个窗口的位置。第一个批次将包含窗口1到32,第二个批次将包含窗口33到64,因此如果您考虑,比如说,每个批次的第一个窗口(即窗口1和33),您会发现它们不是连续的。这个问题的最简单解决方案就是只使用批量大小为1。以下的to_dataset_for_stateful_rnn()自定义实用函数使用这种策略来为有状态的RNN准备数据集:

defto_dataset_for_stateful_rnn(sequence,length):ds=tf.data.Dataset.from_tensor_slices(sequence)ds=ds.window(length+1,shift=length,drop_remainder=True)ds=ds.flat_map(lambdawindow:window.batch(length+1)).batch(1)returnds.map(lambdawindow:(window[:,:-1],window[:,1:])).prefetch(1)stateful_train_set=to_dataset_for_stateful_rnn(encoded[:1_000_000],length)stateful_valid_set=to_dataset_for_stateful_rnn(encoded[1_000_000:1_060_000],length)stateful_test_set=to_dataset_for_stateful_rnn(encoded[1_060_000:],length)图16-2总结了这个函数的主要步骤。

批处理更加困难,但并非不可能。例如,我们可以将莎士比亚的文本分成32个等长的文本,为每个文本创建一个连续输入序列的数据集,最后使用tf.data.Dataset.zip(datasets).map(lambda*windows:tf.stack(windows))来创建正确的连续批次,其中批次中的第n个输入序列从上一个批次中的第n个输入序列结束的地方开始(请参阅笔记本获取完整代码)。

现在,让我们创建有状态的RNN。在创建每个循环层时,我们需要将stateful参数设置为True,因为有状态的RNN需要知道批量大小(因为它将为批次中的每个输入序列保留一个状态)。因此,我们必须在第一层中设置batch_input_shape参数。请注意,我们可以将第二维度留空,因为输入序列可以具有任意长度:

model=tf.keras.Sequential([tf.keras.layers.Embedding(input_dim=n_tokens,output_dim=16,batch_input_shape=[1,None]),tf.keras.layers.GRU(128,return_sequences=True,stateful=True),tf.keras.layers.Dense(n_tokens,activation="softmax")])在每个时期结束时,我们需要在回到文本开头之前重置状态。为此,我们可以使用一个小的自定义Keras回调:

classResetStatesCallback(tf.keras.callbacks.Callback):defon_epoch_begin(self,epoch,logs):self.model.reset_states()现在我们可以编译模型并使用我们的回调函数进行训练:

model.compile(loss="sparse_categorical_crossentropy",optimizer="nadam",metrics=["accuracy"])history=model.fit(stateful_train_set,validation_data=stateful_valid_set,epochs=10,callbacks=[ResetStatesCallback(),model_ckpt])提示训练完这个模型后,只能用它来对与训练时相同大小的批次进行预测。为了避免这个限制,创建一个相同的无状态模型,并将有状态模型的权重复制到这个模型中。

有趣的是,尽管char-RNN模型只是训练来预测下一个字符,但这看似简单的任务实际上也需要它学习一些更高级的任务。例如,要找到“Greatmovie,Ireally”之后的下一个字符,了解到这句话是积极的是有帮助的,所以下一个字符更有可能是“l”(代表“loved”)而不是“h”(代表“hated”)。事实上,OpenAI的AlecRadford和其他研究人员在一篇2017年的论文中描述了他们如何在大型数据集上训练了一个类似于大型char-RNN模型,并发现其中一个神经元表现出色地作为情感分析分类器:尽管该模型在没有任何标签的情况下进行了训练,但他们称之为情感神经元达到了情感分析基准测试的最新性能。这预示并激励了NLP中的无监督预训练。

但在探索无监督预训练之前,让我们将注意力转向单词级模型以及如何在监督方式下用它们进行情感分析。在这个过程中,您将学习如何使用掩码处理可变长度的序列。

让我们使用TensorFlowDatasets库加载IMDb数据集(在第十三章中介绍)。我们将使用训练集的前90%进行训练,剩下的10%用于验证:

为了为这个任务构建一个模型,我们需要预处理文本,但这次我们将其分成单词而不是字符。为此,我们可以再次使用tf.keras.layers.TextVectorization层。请注意,它使用空格来识别单词边界,在某些语言中可能效果不佳。例如,中文书写不使用单词之间的空格,越南语甚至在单词内部也使用空格,德语经常将多个单词连接在一起,没有空格。即使在英语中,空格也不总是分词的最佳方式:想想“SanFrancisco”或“#ILoveDeepLearning”。

然而,对于英语中的IMDb任务,使用空格作为标记边界应该足够好。因此,让我们继续创建一个TextVectorization层,并将其调整到训练集。我们将词汇表限制为1,000个标记,包括最常见的998个单词以及一个填充标记和一个未知单词的标记,因为很少见的单词不太可能对这个任务很重要,并且限制词汇表大小将减少模型需要学习的参数数量:

vocab_size=1000text_vec_layer=tf.keras.layers.TextVectorization(max_tokens=vocab_size)text_vec_layer.adapt(train_set.map(lambdareviews,labels:reviews))最后,我们可以创建模型并训练它:

使用Keras让模型忽略填充标记很简单:在创建Embedding层时简单地添加mask_zero=True。这意味着所有下游层都会忽略填充标记(其ID为0)。就是这样!如果对先前的模型进行几个时期的重新训练,您会发现验证准确率很快就能达到80%以上。

接下来,如果该层的supports_masking属性为True,那么掩码会自动传播到下一层。只要层具有supports_masking=True,它就会继续这样传播。例如,当return_sequences=True时,循环层的supports_masking属性为True,但当return_sequences=False时,它为False,因为在这种情况下不再需要掩码。因此,如果您有一个具有多个return_sequences=True的循环层,然后是一个return_sequences=False的循环层的模型,那么掩码将自动传播到最后一个循环层:该层将使用掩码来忽略被掩码的步骤,但不会进一步传播掩码。同样,如果在我们刚刚构建的情感分析模型中创建Embedding层时设置了mask_zero=True,那么GRU层将自动接收和使用掩码,但不会进一步传播,因为return_sequences没有设置为True。

一些层在将掩码传播到下一层之前需要更新掩码:它们通过实现compute_mask()方法来实现,该方法接受两个参数:输入和先前的掩码。然后计算更新后的掩码并返回。compute_mask()的默认实现只是返回先前的掩码而没有更改。

许多Keras层支持掩码:SimpleRNN、GRU、LSTM、Bidirectional、Dense、TimeDistributed、Add等(都在tf.keras.layers包中)。然而,卷积层(包括Conv1D)不支持掩码——它们如何支持掩码并不明显。

LSTM和GRU层具有基于Nvidia的cuDNN库的优化实现。但是,此实现仅在所有填充标记位于序列末尾时支持遮罩。它还要求您使用几个超参数的默认值:activation、recurrent_activation、recurrent_dropout、unroll、use_bias和reset_after。如果不是这种情况,那么这些层将退回到(速度慢得多的)默认GPU实现。

如果要实现支持遮罩的自定义层,应在call()方法中添加一个mask参数,并显然使方法使用该遮罩。此外,如果遮罩必须传播到下一层,则应在构造函数中设置self.supports_masking=True。如果必须在传播之前更新遮罩,则必须实现compute_mask()方法。

使用遮罩层和自动遮罩传播对简单模型效果最好。对于更复杂的模型,例如需要将Conv1D层与循环层混合时,并不总是适用。在这种情况下,您需要显式计算遮罩并将其传递给适当的层,可以使用函数式API或子类API。例如,以下模型与之前的模型等效,只是使用函数式API构建,并手动处理遮罩。它还添加了一点辍学,因为之前的模型略微过拟合:

inputs=tf.keras.layers.Input(shape=[],dtype=tf.string)token_ids=text_vec_layer(inputs)mask=tf.math.not_equal(token_ids,0)Z=tf.keras.layers.Embedding(vocab_size,embed_size)(token_ids)Z=tf.keras.layers.GRU(128,dropout=0.2)(Z,mask=mask)outputs=tf.keras.layers.Dense(1,activation="sigmoid")(Z)model=tf.keras.Model(inputs=[inputs],outputs=[outputs])遮罩的最后一种方法是使用不规则张量来向模型提供输入。实际上,您只需在创建TextVectorization层时设置ragged=True,以便将输入序列表示为不规则张量:

>>>text_vec_layer_ragged=tf.keras.layers.TextVectorization(...max_tokens=vocab_size,ragged=True)...>>>text_vec_layer_ragged.adapt(train_set.map(lambdareviews,labels:reviews))>>>text_vec_layer_ragged(["Greatmovie!","ThisisDiCaprio'sbestrole."])将这种不规则张量表示与使用填充标记的常规张量表示进行比较:

>>>text_vec_layer(["Greatmovie!","ThisisDiCaprio'sbestrole."])Keras的循环层内置支持不规则张量,因此您无需执行其他操作:只需在模型中使用此TextVectorization层。无需传递mask_zero=True或显式处理遮罩——这一切都已为您实现。这很方便!但是,截至2022年初,Keras中对不规则张量的支持仍然相对较新,因此存在一些问题。例如,目前无法在GPU上运行时将不规则张量用作目标(但在您阅读这些内容时可能已解决)。

使用预训练词嵌入在几年内很受欢迎,但这种方法有其局限性。特别是,一个词无论上下文如何,都有一个表示。例如,“right”这个词在“leftandright”和“rightandwrong”中以相同的方式编码,尽管它们表示两个非常不同的含义。为了解决这个限制,MatthewPeters在2018年引入了来自语言模型的嵌入(ELMo):这些是从深度双向语言模型的内部状态中学习到的上下文化词嵌入。与仅在模型中使用预训练嵌入不同,您可以重用预训练语言模型的一部分。

例如,让我们基于通用句子编码器构建一个分类器,这是由谷歌研究人员团队在2018年介绍的模型架构。这个模型基于transformer架构,我们将在本章后面讨论。方便的是,这个模型可以在TensorFlowHub上找到。

请注意,TensorFlowHub模块URL的最后部分指定我们想要模型的第4个版本。这种版本控制确保如果TFHub上发布了新的模块版本,它不会破坏我们的模型。方便的是,如果你只在Web浏览器中输入这个URL,你将得到这个模块的文档。

还要注意,在创建hub.KerasLayer时,我们设置了trainable=True。这样,在训练期间,预训练的UniversalSentenceEncoder会进行微调:通过反向传播调整一些权重。并非所有的TensorFlowHub模块都是可微调的,所以确保查看你感兴趣的每个预训练模块的文档。

到目前为止,我们已经看过使用char-RNN进行文本生成,以及使用基于可训练嵌入的单词级RNN模型进行情感分析,以及使用来自TensorFlowHub的强大预训练语言模型。在接下来的部分中,我们将探索另一个重要的NLP任务:神经机器翻译(NMT)。

简而言之,架构如下:英语句子作为输入馈送给编码器,解码器输出西班牙语翻译。请注意,西班牙语翻译也在训练期间作为解码器的输入使用,但是向后移动了一步。换句话说,在训练期间,解码器被给予上一步应该输出的单词作为输入,而不管它实际输出了什么。这被称为“教师强迫”——一种显著加速训练并提高模型性能的技术。对于第一个单词,解码器被给予序列开始(SOS)标记,期望解码器以序列结束(EOS)标记结束句子。

每个单词最初由其ID表示(例如,单词“soccer”的ID为854)。接下来,一个Embedding层返回单词嵌入。然后这些单词嵌入被馈送给编码器和解码器。

在每一步中,解码器为输出词汇表(即西班牙语)中的每个单词输出一个分数,然后softmax激活函数将这些分数转换为概率。例如,在第一步中,“Me”这个词可能有7%的概率,“Yo”可能有1%的概率,依此类推。具有最高概率的单词被输出。这非常类似于常规的分类任务,事实上你可以使用"sparse_categorical_crossentropy"损失来训练模型,就像我们在char-RNN模型中所做的那样。

请注意,在推断时(训练后),你将没有目标句子来馈送给解码器。相反,你需要将它刚刚输出的单词作为上一步的输入,如图16-4所示(这将需要一个在图中未显示的嵌入查找)。

让我们构建并训练这个模型!首先,我们需要下载一个英语/西班牙语句子对的数据集:

importnumpyasnptext=text.replace("","").replace("","")pairs=[line.split("\t")forlineintext.splitlines()]np.random.shuffle(pairs)sentences_en,sentences_es=zip(*pairs)#separatesthepairsinto2lists让我们看一下前三个句子对:

>>>foriinrange(3):...print(sentences_en[i],"=>",sentences_es[i])...Howboring!=>Quéaburrimiento!Ilovesports.=>Adoroeldeporte.Wouldyouliketoswapjobs=>Tegustaríaqueintercambiemoslostrabajos接下来,让我们创建两个TextVectorization层——每种语言一个,并对文本进行调整:

vocab_size=1000max_length=50text_vec_layer_en=tf.keras.layers.TextVectorization(vocab_size,output_sequence_length=max_length)text_vec_layer_es=tf.keras.layers.TextVectorization(vocab_size,output_sequence_length=max_length)text_vec_layer_en.adapt(sentences_en)text_vec_layer_es.adapt([f"startofseq{s}endofseq"forsinsentences_es])这里有几件事需要注意:

让我们检查两种词汇表中的前10个标记。它们以填充标记、未知标记、SOS和EOS标记(仅在西班牙语词汇表中)、然后按频率递减排序的实际单词开始:

>>>text_vec_layer_en.get_vocabulary()[:10]['','[UNK]','the','i','to','you','tom','a','is','he']>>>text_vec_layer_es.get_vocabulary()[:10]['','[UNK]','startofseq','endofseq','de','que','a','no','tom','la']接下来,让我们创建训练集和验证集(如果需要,您也可以创建一个测试集)。我们将使用前100,000个句子对进行训练,其余用于验证。解码器的输入是西班牙语句子加上一个SOS标记前缀。目标是西班牙语句子加上一个EOS后缀:

X_train=tf.constant(sentences_en[:100_000])X_valid=tf.constant(sentences_en[100_000:])X_train_dec=tf.constant([f"startofseq{s}"forsinsentences_es[:100_000]])X_valid_dec=tf.constant([f"startofseq{s}"forsinsentences_es[100_000:]])Y_train=text_vec_layer_es([f"{s}endofseq"forsinsentences_es[:100_000]])Y_valid=text_vec_layer_es([f"{s}endofseq"forsinsentences_es[100_000:]])好的,现在我们准备构建我们的翻译模型。我们将使用功能API,因为模型不是顺序的。它需要两个文本输入——一个用于编码器,一个用于解码器——所以让我们从这里开始:

encoder_inputs=tf.keras.layers.Input(shape=[],dtype=tf.string)decoder_inputs=tf.keras.layers.Input(shape=[],dtype=tf.string)接下来,我们需要使用我们之前准备的TextVectorization层对这些句子进行编码,然后为每种语言使用一个Embedding层,其中mask_zero=True以确保自动处理掩码。嵌入大小是一个您可以调整的超参数,像往常一样:

embed_size=128encoder_input_ids=text_vec_layer_en(encoder_inputs)decoder_input_ids=text_vec_layer_es(decoder_inputs)encoder_embedding_layer=tf.keras.layers.Embedding(vocab_size,embed_size,mask_zero=True)decoder_embedding_layer=tf.keras.layers.Embedding(vocab_size,embed_size,mask_zero=True)encoder_embeddings=encoder_embedding_layer(encoder_input_ids)decoder_embeddings=decoder_embedding_layer(decoder_input_ids)提示当语言共享许多单词时,您可能会获得更好的性能,使用相同的嵌入层用于编码器和解码器。

现在让我们创建编码器并传递嵌入输入:

encoder=tf.keras.layers.LSTM(512,return_state=True)encoder_outputs,*encoder_state=encoder(encoder_embeddings)为了保持简单,我们只使用了一个LSTM层,但您可以堆叠几个。我们还设置了return_state=True以获得对层最终状态的引用。由于我们使用了一个LSTM层,实际上有两个状态:短期状态和长期状态。该层分别返回这些状态,这就是为什么我们必须写*encoder_state来将两个状态分组在一个列表中。现在我们可以使用这个(双重)状态作为解码器的初始状态:

decoder=tf.keras.layers.LSTM(512,return_sequences=True)decoder_outputs=decoder(decoder_embeddings,initial_state=encoder_state)接下来,我们可以通过具有softmax激活函数的Dense层将解码器的输出传递,以获得每个步骤的单词概率:

output_layer=tf.keras.layers.Dense(vocab_size,activation="softmax")Y_proba=output_layer(decoder_outputs)就是这样!我们只需要创建KerasModel,编译它并训练它:

deftranslate(sentence_en):translation=""forword_idxinrange(max_length):X=np.array([sentence_en])#encoderinputX_dec=np.array(["startofseq"+translation])#decoderinputy_proba=model.predict((X,X_dec))[0,word_idx]#lasttoken'sprobaspredicted_word_id=np.argmax(y_proba)predicted_word=text_vec_layer_es.get_vocabulary()[predicted_word_id]ifpredicted_word=="endofseq":breaktranslation+=""+predicted_wordreturntranslation.strip()该函数只是逐步预测一个单词,逐渐完成翻译,并在达到EOS标记时停止。让我们试试看!

>>>translate("Ilikesoccerandalsogoingtothebeach")'megustaelfútbolyavecesmismoalbus'翻译说:“我喜欢足球,有时甚至喜欢公共汽车”。那么你如何改进呢?一种方法是增加训练集的大小,并在编码器和解码器中添加更多的LSTM层。但这只能让你走得更远,所以让我们看看更复杂的技术,从双向循环层开始。

在Keras中实现双向循环层,只需将循环层包装在tf.keras.layers.Bidirectional层中。例如,以下Bidirectional层可以用作我们翻译模型中的编码器:

只有一个问题。这一层现在将返回四个状态而不是两个:前向LSTM层的最终短期和长期状态,以及后向LSTM层的最终短期和长期状态。我们不能直接将这个四重状态用作解码器的LSTM层的初始状态,因为它只期望两个状态(短期和长期)。我们不能使解码器双向,因为它必须保持因果关系:否则在训练过程中会作弊,而且不起作用。相反,我们可以连接两个短期状态,并连接两个长期状态:

encoder_outputs,*encoder_state=encoder(encoder_embeddings)encoder_state=[tf.concat(encoder_state[::2],axis=-1),#short-term(0&2)tf.concat(encoder_state[1::2],axis=-1)]#long-term(1&3)现在让我们看看另一种在推理时可以极大提高翻译模型性能的流行技术:束搜索。

假设您已经训练了一个编码器-解码器模型,并且您使用它将句子“Ilikesoccer”翻译成西班牙语。您希望它会输出正确的翻译“megustaelfútbol”,但不幸的是它输出了“megustanlosjugadores”,意思是“我喜欢球员”。看着训练集,您注意到许多句子如“Ilikecars”,翻译成“megustanlosautos”,所以模型在看到“Ilike”后输出“megustanlos”并不荒谬。不幸的是,在这种情况下是一个错误,因为“soccer”是单数。模型无法回头修正,所以它尽力完成句子,这种情况下使用了“jugadores”这个词。我们如何让模型有机会回头修正之前的错误呢?最常见的解决方案之一是beamsearch:它跟踪一个最有希望的句子列表(比如说前三个),在每个解码器步骤中尝试扩展它们一个词,只保留*k个最有可能的句子。参数k被称为beamwidth*。

例如,假设您使用模型来翻译句子“Ilikesoccer”,使用beamsearch和beamwidth为3(参见图16-6)。在第一个解码器步骤中,模型将为翻译句子中每个可能的第一个词输出一个估计概率。假设前三个词是“me”(75%的估计概率),“a”(3%)和“como”(1%)。这是我们目前的短列表。接下来,我们使用模型为每个句子找到下一个词。对于第一个句子(“me”),也许模型为“gustan”这个词输出36%的概率,“gusta”这个词输出32%的概率,“encanta”这个词输出16%的概率,依此类推。请注意,这些实际上是条件概率,假设句子以“me”开头。对于第二个句子(“a”),模型可能为“mi”这个词输出50%的条件概率,依此类推。假设词汇表有1,000个词,我们将得到每个句子1,000个概率。

接下来,我们计算我们考虑的3,000个两个词的句子的概率(3×1,000)。我们通过将每个词的估计条件概率乘以它完成的句子的估计概率来做到这一点。例如,“me”的句子的估计概率为75%,而“gustan”这个词的估计条件概率(假设第一个词是“me”)为36%,所以“megustan”的估计概率为75%×36%=27%。在计算了所有3,000个两个词的句子的概率之后,我们只保留前3个。在这个例子中,它们都以“me”开头:“megustan”(27%),“megusta”(24%)和“meencanta”(12%)。目前,“megustan”这个句子领先,但“megusta”还没有被淘汰。

然后我们重复相同的过程:我们使用模型预测这三个句子中的下一个词,并计算我们考虑的所有3,000个三个词的句子的概率。也许现在前三个是“megustanlos”(10%),“megustael”(8%)和“megustamucho”(2%)。在下一步中,我们可能得到“megustaelfútbol”(6%),“megustamuchoel”(1%)和“megustaeldeporte”(0.2%)。请注意,“megustan”已经被淘汰,正确的翻译现在领先。我们在没有额外训练的情况下提高了我们的编码器-解码器模型的性能,只是更明智地使用它。

TensorFlowAddons库包含一个完整的seq2seqAPI,让您可以构建带有注意力的编码器-解码器模型,包括beamsearch等等。然而,它的文档目前非常有限。实现beamsearch是一个很好的练习,所以试一试吧!查看本章的笔记本,了解可能的解决方案。

通过这一切,您可以为相当短的句子获得相当不错的翻译。不幸的是,这种模型在翻译长句子时会表现得非常糟糕。问题再次出在RNN的有限短期记忆上。注意力机制是解决这个问题的划时代创新。

考虑一下从单词“soccer”到其翻译“fútbol”的路径,回到图16-3:这是相当长的!这意味着这个单词的表示(以及所有其他单词)需要在实际使用之前经过许多步骤。我们难道不能让这条路径变短一点吗?

NMT中最常用的度量标准是双语评估助手(BLEU)分数,它将模型产生的每个翻译与人类产生的几个好翻译进行比较:它计算出现在任何目标翻译中的n-gram(n个单词序列)的数量,并调整分数以考虑在目标翻译中产生的n-gram的频率。

如果输入句子有n个单词,并假设输出句子长度大致相同,那么这个模型将需要计算大约n2个权重。幸运的是,这种二次计算复杂度仍然可行,因为即使是长句子也不会有成千上万个单词。

h~(t)=∑iα(t,i)y(i)withα(t,i)=expe(t,i)∑i'expe(t,i')ande(t,i)=h(t)y(i)doth(t)Wy(i)generalvtanh(W[h(t);y(i)])concat

Keras为Luongattention提供了tf.keras.layers.Attention层,为Bahdanauattention提供了AdditiveAttention层。让我们将Luongattention添加到我们的编码器-解码器模型中。由于我们需要将所有编码器的输出传递给Attention层,所以在创建编码器时,我们首先需要设置return_sequences=True:

encoder=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(256,return_sequences=True,return_state=True))接下来,我们需要创建注意力层,并将解码器的状态和编码器的输出传递给它。然而,为了在每一步访问解码器的状态,我们需要编写一个自定义的记忆单元。为简单起见,让我们使用解码器的输出而不是其状态:实际上这也很有效,并且编码更容易。然后我们直接将注意力层的输出传递给输出层,就像Luong注意力论文中建议的那样:

attention_layer=tf.keras.layers.Attention()attention_outputs=attention_layer([decoder_outputs,encoder_outputs])output_layer=tf.keras.layers.Dense(vocab_size,activation="softmax")Y_proba=output_layer(attention_outputs)就是这样!如果训练这个模型,你会发现它现在可以处理更长的句子。例如:

>>>translate("Ilikesoccerandalsogoingtothebeach")'megustaelfútbolytambiéniralaplaya'简而言之,注意力层提供了一种让模型集中注意力于输入的一部分的方法。但是还有另一种方式来思考这个层:它充当了一个可微分的记忆检索机制。

例如,假设编码器分析了输入句子“Ilikesoccer”,并且成功理解了单词“I”是主语,单词“like”是动词,因此在这些单词的输出中编码了这些信息。现在假设解码器已经翻译了主语,并且认为接下来应该翻译动词。为此,它需要从输入句子中提取动词。这类似于字典查找:就好像编码器创建了一个字典{"subject":"They","verb":"played",...},解码器想要查找与键“verb”对应的值。

然而,模型没有离散的令牌来表示键(如“主语”或“动词”);相反,它具有这些概念的矢量化表示,这些表示在训练期间学习到,因此用于查找的查询不会完全匹配字典中的任何键。解决方案是计算查询与字典中每个键之间的相似度度量,然后使用softmax函数将这些相似度分数转换为总和为1的权重。正如我们之前看到的那样,这正是注意力层所做的。如果代表动词的键与查询最相似,那么该键的权重将接近1。

接下来,注意力层计算相应值的加权和:如果“动词”键的权重接近1,那么加权和将非常接近单词“played”的表示。

这就是为什么Keras的Attention和AdditiveAttention层都期望输入一个包含两个或三个项目的列表:queries,keys,以及可选的values。如果不传递任何值,则它们会自动等于键。因此,再次查看前面的代码示例,解码器输出是查询,编码器输出既是键也是值。对于每个解码器输出(即每个查询),注意力层返回与解码器输出最相似的编码器输出(即键/值)的加权和。

关键是,注意力机制是一个可训练的内存检索系统。它非常强大,以至于您实际上可以仅使用注意力机制构建最先进的模型。进入Transformer架构。

简而言之,图16-8的左侧是编码器,右侧是解码器。每个嵌入层输出一个形状为[批量大小,序列长度,嵌入大小]的3D张量。之后,随着数据流经Transformer,张量逐渐转换,但形状保持不变。

如果您将Transformer用于NMT,则在训练期间必须将英语句子馈送给编码器,将相应的西班牙语翻译馈送给解码器,并在每个句子开头插入额外的SOS令牌。在推理时,您必须多次调用Transformer,逐字产生翻译,并在每轮将部分翻译馈送给解码器,就像我们之前在translate()函数中所做的那样。

编码器的作用是逐渐转换输入——英文句子的单词表示——直到每个单词的表示完美地捕捉到单词的含义,在句子的上下文中。例如,如果你用句子“Ilikesoccer”来喂给编码器,那么单词“like”将以一个相当模糊的表示开始,因为这个单词在不同的上下文中可能有不同的含义:想想“Ilikesoccer”和“It’slikethat”。但是经过编码器后,单词的表示应该捕捉到给定句子中“like”的正确含义(即喜欢),以及可能需要用于翻译的任何其他信息(例如,它是一个动词)。

解码器的作用是逐渐将翻译句子中的每个单词表示转换为翻译中下一个单词的单词表示。例如,如果要翻译的句子是“Ilikesoccer”,解码器的输入句子是“megustaelfútbol”,那么经过解码器后,“el”的单词表示将最终转换为“fútbol”的表示。同样,“fútbol”的表示将被转换为EOS标记的表示。

经过解码器后,每个单词表示都经过一个带有softmax激活函数的最终Dense层,希望能够输出正确下一个单词的高概率和所有其他单词的低概率。预测的句子应该是“megustaelfútbol”。

那是大局观;现在让我们更详细地走一遍图16-8:

图16-8中每个多头注意力层的前两个箭头代表键和值,第三个箭头代表查询。在自注意力层中,所有三个都等于前一层输出的单词表示,而在解码器的上层注意力层中,键和值等于编码器的最终单词表示,查询等于前一层输出的单词表示。

让我们更详细地了解Transformer架构中的新颖组件,从位置编码开始。

位置编码是一个密集向量,用于编码句子中单词的位置:第i个位置编码被添加到句子中第i个单词的单词嵌入中。实现这一点的最简单方法是使用Embedding层,并使其对批处理中从0到最大序列长度的所有位置进行编码,然后将结果添加到单词嵌入中。广播规则将确保位置编码应用于每个输入序列。例如,以下是如何将位置编码添加到编码器和解码器输入的方法:

max_length=50#maxlengthinthewholetrainingsetembed_size=128pos_embed_layer=tf.keras.layers.Embedding(max_length,embed_size)batch_max_len_enc=tf.shape(encoder_embeddings)[1]encoder_in=encoder_embeddings+pos_embed_layer(tf.range(batch_max_len_enc))batch_max_len_dec=tf.shape(decoder_embeddings)[1]decoder_in=decoder_embeddings+pos_embed_layer(tf.range(batch_max_len_dec))请注意,此实现假定嵌入表示为常规张量,而不是不规则张量。23编码器和解码器共享相同的Embedding层用于位置编码,因为它们具有相同的嵌入大小(这通常是这种情况)。

Transformer论文的作者选择使用基于正弦和余弦函数在不同频率下的固定位置编码,而不是使用可训练的位置编码。位置编码矩阵P在方程16-2中定义,并在图16-9的顶部(转置)表示,其中P[p,i]是句子中位于第p位置的单词的编码的第i个分量。

这个解决方案可以提供与可训练位置编码相同的性能,并且可以扩展到任意长的句子,而不需要向模型添加任何参数(然而,当有大量预训练数据时,通常会优先选择可训练位置编码)。在这些位置编码添加到单词嵌入之后,模型的其余部分可以访问句子中每个单词的绝对位置,因为每个位置都有一个唯一的位置编码(例如,句子中位于第22个位置的单词的位置编码由图16-9左上角的垂直虚线表示,您可以看到它是唯一的)。此外,选择振荡函数(正弦和余弦)使模型能够学习相对位置。例如,相距38个单词的单词(例如,在位置p=22和p=60处)在编码维度i=100和i=101中始终具有相同的位置编码值,如图16-9所示。这解释了为什么我们需要每个频率的正弦和余弦:如果我们只使用正弦(i=100处的蓝色波),模型将无法区分位置p=22和p=35(由十字标记)。

在TensorFlow中没有PositionalEncoding层,但创建一个并不太困难。出于效率原因,我们在构造函数中预先计算位置编码矩阵。call()方法只是将这个编码矩阵截断到输入序列的最大长度,并将其添加到输入中。我们还设置supports_masking=True以将输入的自动掩码传播到下一层:

classPositionalEncoding(tf.keras.layers.Layer):def__init__(self,max_length,embed_size,dtype=tf.float32,**kwargs):super().__init__(dtype=dtype,**kwargs)assertembed_size%2==0,"embed_sizemustbeeven"p,i=np.meshgrid(np.arange(max_length),2*np.arange(embed_size//2))pos_emb=np.empty((1,max_length,embed_size))pos_emb[0,:,::2]=np.sin(p/10_000**(i/embed_size)).Tpos_emb[0,:,1::2]=np.cos(p/10_000**(i/embed_size)).Tself.pos_encodings=tf.constant(pos_emb.astype(self.dtype))self.supports_masking=Truedefcall(self,inputs):batch_max_length=tf.shape(inputs)[1]returninputs+self.pos_encodings[:,:batch_max_length]让我们使用这个层将位置编码添加到编码器的输入中:

pos_embed_layer=PositionalEncoding(max_length,embed_size)encoder_in=pos_embed_layer(encoder_embeddings)decoder_in=pos_embed_layer(decoder_embeddings)现在让我们更深入地看一下Transformer模型的核心,即多头注意力层。

要理解多头注意力层的工作原理,我们首先必须了解它基于的缩放点积注意力层。它的方程式在方程式16-3中以矢量化形式显示。它与Luong注意力相同,只是有一个缩放因子。

注意力(Q,K,V)=softmaxQK

在这个方程中:

如果在创建tf.keras.layers.Attention层时设置use_scale=True,那么它将创建一个额外的参数,让该层学习如何正确地降低相似性分数。Transformer模型中使用的缩放后的点积注意力几乎相同,只是它总是将相似性分数按相同因子缩放,即1/(dkeys)。

请注意,Attention层的输入就像Q、K和V一样,只是多了一个批处理维度(第一个维度)。在内部,该层仅通过一次调用tf.matmul(queries,keys)计算批处理中所有句子的所有注意力分数:这使得它非常高效。实际上,在TensorFlow中,如果A和B是具有两个以上维度的张量,比如形状为[2,3,4,5]和[2,3,5,6],那么tf.matmul(A,B)将把这些张量视为2×3数组,其中每个单元格包含一个矩阵,并将相应的矩阵相乘:A中第i行和第j列的矩阵将与B中第i行和第j列的矩阵相乘。由于一个4×5矩阵与一个5×6矩阵的乘积是一个4×6矩阵,tf.matmul(A,B)将返回一个形状为[2,3,4,6]的数组。

现在我们准备看一下多头注意力层。其架构如图16-10所示。

但是为什么?这种架构背后的直觉是什么?好吧,再次考虑一下句子“Ilikesoccer”中的单词“like”。编码器足够聪明,能够编码它是一个动词的事实。但是单词表示还包括其在文本中的位置,这要归功于位置编码,它可能还包括许多其他对其翻译有用的特征,比如它是现在时。简而言之,单词表示编码了单词的许多不同特征。如果我们只使用一个缩放后的点积注意力层,我们只能一次性查询所有这些特征。

这就是为什么多头注意力层应用多个不同的线性变换值、键和查询:这使得模型能够将单词表示的许多不同特征投影到不同的子空间中,每个子空间都专注于单词的某些特征。也许其中一个线性层将单词表示投影到一个只剩下单词是动词信息的子空间,另一个线性层将提取出它是现在时的事实,依此类推。然后缩放后的点积注意力层实现查找阶段,最后我们将所有结果连接起来并将它们投影回原始空间。

Keras包括一个tf.keras.layers.MultiHeadAttention层,因此我们现在拥有构建Transformer其余部分所需的一切。让我们从完整的编码器开始,它与图16-8中的完全相同,只是我们使用两个块的堆叠(N=2)而不是六个,因为我们没有一个庞大的训练集,并且我们还添加了一点辍学:

N=2#insteadof6num_heads=8dropout_rate=0.1n_units=128#forthefirstdenselayerineachfeedforwardblockencoder_pad_mask=tf.math.not_equal(encoder_input_ids,0)[:,tf.newaxis]Z=encoder_infor_inrange(N):skip=Zattn_layer=tf.keras.layers.MultiHeadAttention(num_heads=num_heads,key_dim=embed_size,dropout=dropout_rate)Z=attn_layer(Z,value=Z,attention_mask=encoder_pad_mask)Z=tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z,skip]))skip=ZZ=tf.keras.layers.Dense(n_units,activation="relu")(Z)Z=tf.keras.layers.Dense(embed_size)(Z)Z=tf.keras.layers.Dropout(dropout_rate)(Z)Z=tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z,skip]))这段代码应该大多数都很简单,除了一个问题:掩码。在撰写本文时,MultiHeadAttention层不支持自动掩码,因此我们必须手动处理。我们该如何做?

然而,该层将为每个单独的查询标记计算输出,包括填充标记。我们需要掩盖与这些填充标记对应的输出。回想一下,在Embedding层中我们使用了mask_zero,并且在PositionalEncoding层中我们将supports_masking设置为True,因此自动掩码一直传播到MultiHeadAttention层的输入(encoder_in)。我们可以利用这一点在跳过连接中:实际上,Add层支持自动掩码,因此当我们将Z和skip(最初等于encoder_in)相加时,输出将自动正确掩码。天啊!掩码需要比代码更多的解释。

现在开始解码器!再次,掩码将是唯一棘手的部分,所以让我们从那里开始。第一个多头注意力层是一个自注意力层,就像在编码器中一样,但它是一个掩码多头注意力层,这意味着它是因果的:它应该忽略未来的所有标记。因此,我们需要两个掩码:一个填充掩码和一个因果掩码。让我们创建它们:

现在让我们构建解码器:

encoder_outputs=Z#let'ssavetheencoder'sfinaloutputsZ=decoder_in#thedecoderstartswithitsowninputsfor_inrange(N):skip=Zattn_layer=tf.keras.layers.MultiHeadAttention(num_heads=num_heads,key_dim=embed_size,dropout=dropout_rate)Z=attn_layer(Z,value=Z,attention_mask=causal_mask&decoder_pad_mask)Z=tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z,skip]))skip=Zattn_layer=tf.keras.layers.MultiHeadAttention(num_heads=num_heads,key_dim=embed_size,dropout=dropout_rate)Z=attn_layer(Z,value=encoder_outputs,attention_mask=encoder_pad_mask)Z=tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z,skip]))skip=ZZ=tf.keras.layers.Dense(n_units,activation="relu")(Z)Z=tf.keras.layers.Dense(embed_size)(Z)Z=tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z,skip]))对于第一个注意力层,我们使用causal_mask&decoder_pad_mask来同时掩盖填充标记和未来标记。因果掩码只有两个维度:它缺少批处理维度,但这没关系,因为广播确保它在批处理中的所有实例中被复制。

对于第二个注意力层,没有特别之处。唯一需要注意的是我们使用encoder_pad_mask而不是decoder_pad_mask,因为这个注意力层使用编码器的最终输出作为其值。

我们快要完成了。我们只需要添加最终的输出层,创建模型,编译它,然后训练它:

Y_proba=tf.keras.layers.Dense(vocab_size,activation="softmax")(Z)model=tf.keras.Model(inputs=[encoder_inputs,decoder_inputs],outputs=[Y_proba])model.compile(loss="sparse_categorical_crossentropy",optimizer="nadam",metrics=["accuracy"])model.fit((X_train,X_train_dec),Y_train,epochs=10,validation_data=((X_valid,X_valid_dec),Y_valid))恭喜!您已经从头开始构建了一个完整的Transformer,并对其进行了自动翻译的训练。这变得相当高级了!

但领域并没有就此停止。现在让我们来探讨一些最近的进展。

2018年被称为NLP的“ImageNet时刻”。从那时起,进展一直令人震惊,基于巨大数据集训练的基于Transformer的架构越来越大。

掩码语言模型(MLM)

句子中的每个单词有15%的概率被掩盖,模型经过训练,以预测被掩盖的单词。例如,如果原始句子是“她在生日聚会上玩得很开心”,那么模型可能会得到句子“她在聚会上玩得很开心”,它必须预测单词“had”和“birthday”(其他输出将被忽略)。更准确地说,每个选择的单词有80%的概率被掩盖,10%的概率被替换为随机单词(为了减少预训练和微调之间的差异,因为模型在微调过程中不会看到标记),以及10%的概率被保留(以偏向模型正确答案)。

下一个句子预测(NSP)

该模型经过训练,以预测两个句子是否连续。例如,它应该预测“狗在睡觉”和“它打呼噜”是连续的句子,而“狗在睡觉”和“地球绕着太阳转”不是连续的。后来的研究表明,NSP并不像最初认为的那么重要,因此在大多数后来的架构中被放弃了。

该模型同时在这两个任务上进行训练(参见图16-11)。对于NSP任务,作者在每个输入的开头插入了一个类标记(),相应的输出标记表示模型的预测:句子B跟在句子A后面,或者不是。这两个输入句子被连接在一起,只用一个特殊的分隔标记()分开,然后作为输入提供给模型。为了帮助模型知道每个输入标记属于哪个句子,每个标记的位置嵌入上面添加了一个段嵌入:只有两种可能的段嵌入,一个用于句子A,一个用于句子B。对于MLM任务,一些输入词被屏蔽(正如我们刚才看到的),模型试图预测这些词是什么。损失仅在NSP预测和被屏蔽的标记上计算,而不是在未被屏蔽的标记上。

在对大量文本进行无监督预训练阶段之后,该模型然后在许多不同的任务上进行微调,每个任务的变化都很小。例如,对于文本分类(如情感分析),所有输出标记都被忽略,除了第一个,对应于类标记,一个新的输出层取代了以前的输出层,以前的输出层只是一个用于NSP的二元分类层。

巨型模型的这种趋势不幸地导致只有经济实力雄厚的组织才能负担得起训练这样的模型:成本很容易就能达到几十万美元甚至更高。训练单个模型所需的能量相当于一个美国家庭几年的电力消耗;这一点根本不环保。许多这些模型甚至太大,无法在常规硬件上使用:它们无法适应内存,速度也会非常慢。最后,有些模型成本如此之高,以至于不会公开发布。

DistilBERT是使用蒸馏(因此得名)进行训练的:这意味着将知识从一个教师模型转移到一个通常比教师模型小得多的学生模型。通常通过使用教师对每个训练实例的预测概率作为学生的目标来实现。令人惊讶的是,蒸馏通常比在相同数据集上从头开始训练学生更有效!事实上,学生受益于教师更加微妙的标签。

但是通过思维链提示,示例答案包括导致结论的所有推理步骤。例如,不是“A:11”,提示包含“A:Roger从5个球开始。2罐每罐3个网球,总共6个网球。5+6=11。”这鼓励模型给出对实际问题的详细答案,比如“John照顾10只狗。每只狗每天需要0.5小时散步和照顾自己的事务。所以是10×0.5=5小时每天。5小时每天×7天每周=35小时每周。答案是每周35小时。”这是论文中的一个实际例子!

这个模型不仅比使用常规提示更频繁地给出正确答案——我们鼓励模型深思熟虑——而且还提供了所有推理步骤,这对于更好地理解模型答案背后的原理是有用的。

transformers已经在NLP领域占据了主导地位,但它们并没有止步于此:它们很快也扩展到了计算机视觉领域。

归纳偏差是模型由于其架构而做出的隐含假设。例如,线性模型隐含地假设数据是线性的。CNN隐含地假设在一个位置学习到的模式在其他位置也可能有用。RNN隐含地假设输入是有序的,并且最近的标记比较重要。模型具有的归纳偏差越多,假设它们是正确的,模型所需的训练数据就越少。但是,如果隐含的假设是错误的,那么即使在大型数据集上训练,模型也可能表现不佳。

仅仅两个月后,Facebook的一个研究团队发布了一篇论文,介绍了数据高效图像变换器(DeiTs)。他们的模型在ImageNet上取得了竞争性的结果,而无需额外的训练数据。该模型的架构与原始ViT几乎相同,但作者使用了一种蒸馏技术,将来自最先进的CNN模型的知识转移到他们的模型中。

这些惊人的进步使一些研究人员认为人类水平的AI已经近在眼前,认为“规模就是一切”,并且一些模型可能“稍微有意识”。其他人指出,尽管取得了惊人的进步,这些模型仍然缺乏人类智能的可靠性和适应性,我们推理的符号能力,基于单个例子进行泛化的能力等等。

正如您所看到的,transformers无处不在!好消息是,通常您不必自己实现transformers,因为许多优秀的预训练模型可以通过TensorFlowHub或HuggingFace的模型中心轻松下载。您已经看到如何使用TFHub中的模型,所以让我们通过快速查看HuggingFace的生态系统来结束本章。

今天谈论transformers时不可能不提到HuggingFace,这是一家为NLP、视觉等构建了一整套易于使用的开源工具的人工智能公司。他们生态系统的核心组件是Transformers库,它允许您轻松下载一个预训练模型,包括相应的分词器,然后根据需要在自己的数据集上进行微调。此外,该库支持TensorFlow、PyTorch和JAX(使用Flax库)。

使用Transformers库的最简单方法是使用transformers.pipeline()函数:只需指定您想要的任务,比如情感分析,它会下载一个默认的预训练模型,准备好使用——真的再简单不过了:

fromtransformersimportpipelineclassifier=pipeline("sentiment-analysis")#manyothertasksareavailableresult=classifier("Theactorswereveryconvincing".)结果是一个Python列表,每个输入文本对应一个字典:

>>>result[{'label':'POSITIVE','score':0.9998071789741516}]在此示例中,模型正确地发现句子是积极的,置信度约为99.98%。当然,您也可以将一批句子传递给模型:

>>>classifier(["IamfromIndia.","IamfromIraq."])[{'label':'POSITIVE','score':0.9896161556243896},{'label':'NEGATIVE','score':0.9811071157455444}]pipeline()函数使用给定任务的默认模型。例如,对于文本分类任务,如情感分析,在撰写本文时,默认为distilbert-base-uncased-finetuned-sst-2-english——一个在英文维基百科和英文书籍语料库上训练的带有小写标记器的DistilBERT模型,并在斯坦福情感树库v2(SST2)任务上进行了微调。您也可以手动指定不同的模型。例如,您可以使用在多种自然语言推理(MultiNLI)任务上进行微调的DistilBERT模型,该任务将两个句子分类为三类:矛盾、中性或蕴含。以下是如何操作:

pipelineAPI非常简单方便,但有时您需要更多控制。对于这种情况,Transformers库提供了许多类,包括各种标记器、模型、配置、回调等。例如,让我们使用TFAutoModelForSequenceClassification和AutoTokenizer类加载相同的DistilBERT模型及其对应的标记器:

fromtransformersimportAutoTokenizer,TFAutoModelForSequenceClassificationtokenizer=AutoTokenizer.from_pretrained(model_name)model=TFAutoModelForSequenceClassification.from_pretrained(model_name)接下来,让我们标记一对句子。在此代码中,我们激活填充,并指定我们希望使用TensorFlow张量而不是Python列表:

token_ids=tokenizer(["Ilikesoccer.[SEP]Wealllovesoccer!","Joelivedforaverylongtime.[SEP]Joeisold."],padding=True,return_tensors="tf")提示在将"Sentence1[SEP]Sentence2"传递给标记器时,您可以等效地传递一个元组:("Sentence1","Sentence2")。

输出是BatchEncoding类的类似字典实例,其中包含标记ID序列,以及包含填充标记的掩码为0:

>>>token_ids{'input_ids':,'attention_mask':}当调用标记器时设置return_token_type_ids=True,您还将获得一个额外的张量,指示每个标记属于哪个句子。这对某些模型是必需的,但对DistilBERT不是。

接下来,我们可以直接将这个BatchEncoding对象传递给模型;它返回一个包含其预测类logits的TFSequenceClassifierOutput对象:

>>>outputs=model(token_ids)>>>outputsTFSequenceClassifierOutput(loss=None,logits=[],[...])最后,我们可以应用softmax激活函数将这些logits转换为类概率,并使用argmax()函数预测每个输入句子对的具有最高概率的类:

>>>Y_probas=tf.keras.activations.softmax(outputs.logits)>>>Y_probas>>>Y_pred=tf.argmax(Y_probas,axis=1)>>>Y_pred#0=contradiction,1=entailment,2=neutral在此示例中,模型正确将第一对句子分类为中性(我喜欢足球并不意味着每个人都喜欢),将第二对句子分类为蕴含(乔确实应该很老)。

如果您希望在自己的数据集上微调此模型,您可以像通常使用Keras一样训练模型,因为它只是一个常规的Keras模型,具有一些额外的方法。但是,由于模型输出的是logits而不是概率,您必须使用tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)损失,而不是通常的"sparse_categorical_crossentropy"损失。此外,模型在训练期间不支持BatchEncoding输入,因此您必须使用其data属性来获取一个常规字典:

在下一章中,我们将讨论如何使用自编码器以无监督的方式学习深度表示,并使用生成对抗网络来生成图像等!

艾伦·图灵,“计算机机器和智能”,心灵49(1950年):433-460。

当然,单词chatbot出现得更晚。图灵称其测试为模仿游戏:机器A和人类B通过文本消息与人类审问者C聊天;审问者提出问题以确定哪一个是机器(A还是B)。如果机器能够愚弄审问者,那么它通过了测试,而人类B必须尽力帮助审问者。

由于输入窗口重叠,因此在这种情况下时代的概念并不那么清晰:在每个时代(由Keras实现),模型实际上会多次看到相同的字符。

RicoSennrich等人,“使用子词单元进行稀有词的神经机器翻译”,计算语言学年会第54届年会论文集1(2016年):1715-1725。

TakuKudo,“子词规范化:改进神经网络翻译模型的多个子词候选”,arXiv预印本arXiv:1804.10959(2018)。

TakuKudo和JohnRichardson,“SentencePiece:用于神经文本处理的简单且语言无关的子词标记器和去标记器”,arXiv预印本arXiv:1808.06226(2018)。

YonghuiWu等人,“谷歌的神经机器翻译系统:弥合人类和机器翻译之间的差距”,arXiv预印本arXiv:1609.08144(2016)。

不规则张量在第十二章中被介绍,详细内容在附录C中。

1MatthewPeters等人,“深度上下文化的词表示”,2018年北美计算语言学分会年会论文集:人类语言技术1(2018):2227–2237。

11JeremyHoward和SebastianRuder,“文本分类的通用语言模型微调”,计算语言学年会第56届年会论文集1(2018):328–339。

12DanielCer等人,“通用句子编码器”,arXiv预印本arXiv:1803.11175(2018)。

13IlyaSutskever等人,“使用神经网络进行序列到序列学习”,arXiv预印本(2014)。

1SamyBengio等人,“使用循环神经网络进行序列预测的计划抽样”,arXiv预印本arXiv:1506.03099(2015)。

1在Python中,如果运行a,*b=[1,2,3,4],那么a等于1,b等于[2,3,4]。

1SébastienJean等人,“在神经机器翻译中使用非常大的目标词汇”,计算语言学年会第53届年会和亚洲自然语言处理联合国际会议第7届年会论文集1(2015):1–10。

1DzmitryBahdanau等人,“通过联合学习对齐和翻译的神经机器翻译”,arXiv预印本arXiv:1409.0473(2014)。

1Minh-ThangLuong等人,“基于注意力的神经机器翻译的有效方法”,2015年自然语言处理经验方法会议论文集(2015):1412–1421。

2AshishVaswani等人,“注意力就是一切”,第31届国际神经信息处理系统会议论文集(2017):6000–6010。

22这是“注意力就是一切”论文中的图1,经作者的亲切许可再现。

23如果您使用最新版本的TensorFlow,可以使用不规则张量。

2目前Z+skip不支持自动屏蔽,这就是为什么我们不得不写tf.keras.layers.Add()([Z,skip])的原因。再次强调,当您阅读本文时,情况可能已经发生变化。

2AlecRadford等人,“通过生成式预训练改进语言理解”(2018年)。

2例如,“简在朋友的生日派对上玩得很开心”意味着“简喜欢这个派对”,但与“每个人都讨厌这个派对”相矛盾,与“地球是平的”无关。

2JacobDevlin等人,“BERT:深度双向Transformer的预训练”,2018年北美计算语言学协会会议论文集:人类语言技术1(2019年)。

31AlecRadford等人,“语言模型是无监督多任务学习者”(2019年)。

32WilliamFedus等人,“SwitchTransformers:通过简单高效的稀疏性扩展到万亿参数模型”(2021年)。

33VictorSanh等人,“DistilBERT,Bert的精简版本:更小、更快、更便宜、更轻”,arXiv预印本arXiv:1910.01108(2019年)。

3ColinRaffel等人,“探索统一文本到文本Transformer的迁移学习极限”,arXiv预印本arXiv:1910.10683(2019年)。

3AakankshaChowdhery等人,“PaLM:使用路径扩展语言建模”,arXiv预印本arXiv:2204.02311(2022年)。

3JasonWei等人,“思维链提示引发大型语言模型的推理”,arXiv预印本arXiv:2201.11903(2022年)。

MarcoTulioRibeiro等人,“‘为什么我应该相信你?’:解释任何分类器的预测”,第22届ACMSIGKDD国际知识发现与数据挖掘会议论文集(2016年):1135–1144。

1NicolasCarion等人,“使用Transformer进行端到端目标检测”,arXiv预印本arxiv:2005.12872(2020年)。

2AlexeyDosovitskiy等人,“一幅图像价值16x16个词:大规模图像识别的Transformer”,arXiv预印本arxiv:2010.11929(2020年)。

3HugoTouvron等人,“训练数据高效的图像Transformer和通过注意力蒸馏”,arXiv预印本arxiv:2012.12877(2020年)。

AndrewJaegle等人,“Perceiver:带有迭代注意力的通用感知”,arXiv预印本arxiv:2103.03206(2021)。

MathildeCaron等人,“自监督视觉Transformer中的新兴属性”,arXiv预印本arxiv:2104.14294(2021)。

XiaohuaZhai等人,“缩放视觉Transformer”,arXiv预印本arxiv:2106.04560v1(2021)。

AlecRadford等人,“从自然语言监督中学习可转移的视觉模型”,arXiv预印本arxiv:2103.00020(2021)。

AdityaRamesh等人,“零样本文本到图像生成”,arXiv预印本arxiv:2102.12092(2021)。

AdityaRamesh等人,“具有CLIP潜变量的分层文本条件图像生成”,arXiv预印本arxiv:2204.06125(2022)。

1Jean-BaptisteAlayrac等人,“Flamingo:用于少样本学习的视觉语言模型”,arXiv预印本arxiv:2204.14198(2022)。

2ScottReed等人,“通用主体代理”,arXiv预印本arxiv:2205.06175(2022)。

自编码器是人工神经网络,能够学习输入数据的密集表示,称为潜在表示或编码,而无需任何监督(即,训练集未标记)。这些编码通常比输入数据的维度低得多,使得自编码器在降维方面非常有用(参见第八章),特别是用于可视化目的。自编码器还充当特征检测器,并且可以用于深度神经网络的无监督预训练(正如我们在第十一章中讨论的那样)。最后,一些自编码器是生成模型:它们能够随机生成看起来非常类似于训练数据的新数据。例如,您可以在人脸图片上训练一个自编码器,然后它将能够生成新的人脸。

生成学习领域的一个较新的成员是扩散模型。在2021年,它们成功生成了比GANs更多样化和高质量的图像,同时训练也更容易。然而,扩散模型运行速度较慢。

自编码器、GANs和扩散模型都是无监督的,它们都学习潜在表示,它们都可以用作生成模型,并且有许多类似的应用。然而,它们的工作方式非常不同:

在本章中,我们将深入探讨自编码器的工作原理以及如何将其用于降维、特征提取、无监督预训练或生成模型。这将自然地引导我们到GAN。我们将构建一个简单的GAN来生成假图像,但我们会看到训练通常相当困难。我们将讨论对抗训练中遇到的主要困难,以及一些解决这些困难的主要技术。最后,我们将构建和训练一个DDPM,并用它生成图像。让我们从自编码器开始!

你觉得以下哪个数字序列最容易记住?

乍一看,第一个序列似乎更容易,因为它要短得多。然而,如果你仔细看第二个序列,你会注意到它只是从50到14的偶数列表。一旦你注意到这个模式,第二个序列比第一个容易记忆得多,因为你只需要记住模式(即递减的偶数)和起始和结束数字(即50和14)。请注意,如果你能快速轻松地记住非常长的序列,你就不会太在意第二个序列中的模式。你只需要把每个数字背下来,就这样。难以记忆长序列的事实使得识别模式变得有用,希望这解释清楚了为什么在训练期间对自编码器进行约束会促使其发现和利用数据中的模式。

就像这个记忆实验中的国际象棋选手一样,自编码器查看输入,将其转换为高效的潜在表示,然后输出与输入非常接近的内容(希望如此)。自编码器始终由两部分组成:一个编码器(或识别网络),将输入转换为潜在表示,然后是一个解码器(或生成网络),将内部表示转换为输出(参见图17-1)。

正如您所看到的,自编码器通常具有与多层感知器(MLP;参见第十章)相同的架构,只是输出层中的神经元数量必须等于输入数量。在这个例子中,有一个由两个神经元组成的隐藏层(编码器),以及一个由三个神经元组成的输出层(解码器)。输出通常被称为重构,因为自编码器试图重构输入。成本函数包含一个重构损失,当重构与输入不同时,惩罚模型。

因为内部表示的维度比输入数据低(是2D而不是3D),所以自编码器被称为欠完备。欠完备自编码器不能简单地将其输入复制到编码中,但它必须找到一种输出其输入的方式。它被迫学习输入数据中最重要的特征(并丢弃不重要的特征)。

让我们看看如何实现一个非常简单的欠完备自编码器进行降维。

如果自编码器仅使用线性激活函数,并且成本函数是均方误差(MSE),那么它最终会执行主成分分析(PCA;参见第八章)。

以下代码构建了一个简单的线性自编码器,用于在3D数据集上执行PCA,将其投影到2D:

importtensorflowastfencoder=tf.keras.Sequential([tf.keras.layers.Dense(2)])decoder=tf.keras.Sequential([tf.keras.layers.Dense(3)])autoencoder=tf.keras.Sequential([encoder,decoder])optimizer=tf.keras.optimizers.SGD(learning_rate=0.5)autoencoder.compile(loss="mse",optimizer=optimizer)这段代码与我们在过去章节中构建的所有MLP并没有太大的不同,但有几点需要注意:

现在让我们在与我们在第八章中使用的相同简单生成的3D数据集上训练模型,并使用它对该数据集进行编码(即将其投影到2D):

X_train=[...]#generatea3Ddataset,likeinChapter8history=autoencoder.fit(X_train,X_train,epochs=500,verbose=False)codings=encoder.predict(X_train)请注意,X_train既用作输入又用作目标。图17-2显示了原始3D数据集(左侧)和自编码器的隐藏层的输出(即编码层,右侧)。正如您所看到的,自编码器找到了最佳的2D平面来投影数据,尽可能保留数据中的方差(就像PCA一样)。

您可以将自编码器视为执行一种自监督学习,因为它基于一种带有自动生成标签的监督学习技术(在本例中简单地等于输入)。

就像我们讨论过的其他神经网络一样,自编码器可以有多个隐藏层。在这种情况下,它们被称为堆叠自编码器(或深度自编码器)。添加更多层有助于自编码器学习更复杂的编码。也就是说,必须小心不要使自编码器过于强大。想象一个如此强大的编码器,它只学习将每个输入映射到一个单一的任意数字(解码器学习反向映射)。显然,这样的自编码器将完美地重构训练数据,但它不会在过程中学习任何有用的数据表示,并且不太可能很好地推广到新实例。

堆叠自编码器的架构通常关于中心隐藏层(编码层)是对称的。简单来说,它看起来像三明治。例如,时尚MNIST的自编码器(在第十章介绍)可能有784个输入,然后是具有100个神经元的隐藏层,然后是具有30个神经元的中心隐藏层,然后是具有100个神经元的另一个隐藏层,最后是具有784个神经元的输出层。这个堆叠自编码器在图17-3中表示。

您可以实现一个堆叠自编码器,非常类似于常规的深度MLP:

stacked_encoder=tf.keras.Sequential([tf.keras.layers.Flatten(),tf.keras.layers.Dense(100,activation="relu"),tf.keras.layers.Dense(30,activation="relu"),])stacked_decoder=tf.keras.Sequential([tf.keras.layers.Dense(100,activation="relu"),tf.keras.layers.Dense(28*28),tf.keras.layers.Reshape([28,28])])stacked_ae=tf.keras.Sequential([stacked_encoder,stacked_decoder])stacked_ae.compile(loss="mse",optimizer="nadam")history=stacked_ae.fit(X_train,X_train,epochs=20,validation_data=(X_valid,X_valid))让我们来看看这段代码:

确保自编码器得到正确训练的一种方法是比较输入和输出:差异不应太大。让我们绘制一些验证集中的图像,以及它们的重构:

importnumpyasnpdefplot_reconstructions(model,images=X_valid,n_images=5):reconstructions=np.clip(model.predict(images[:n_images]),0,1)fig=plt.figure(figsize=(n_images*1.5,3))forimage_indexinrange(n_images):plt.subplot(2,n_images,1+image_index)plt.imshow(images[image_index],cmap="binary")plt.axis("off")plt.subplot(2,n_images,1+n_images+image_index)plt.imshow(reconstructions[image_index],cmap="binary")plt.axis("off")plot_reconstructions(stacked_ae)plt.show()图17-4显示了生成的图像。

现在我们已经训练了一个堆叠自编码器,我们可以使用它来降低数据集的维度。对于可视化,与其他降维算法(如我们在第八章中讨论的算法)相比,这并不会产生很好的结果,但自编码器的一个重要优势是它们可以处理具有许多实例和许多特征的大型数据集。因此,一种策略是使用自编码器将维度降低到合理水平,然后使用另一个降维算法进行可视化。让我们使用这种策略来可视化时尚MNIST。首先,我们将使用堆叠自编码器的编码器将维度降低到30,然后我们将使用Scikit-Learn的t-SNE算法实现将维度降低到2以进行可视化:

fromsklearn.manifoldimportTSNEX_valid_compressed=stacked_encoder.predict(X_valid)tsne=TSNE(init="pca",learning_rate="auto",random_state=42)X_valid_2D=tsne.fit_transform(X_valid_compressed)现在我们可以绘制数据集:

plt.scatter(X_valid_2D[:,0],X_valid_2D[:,1],c=y_valid,s=10,cmap="tab10")plt.show()图17-5显示了生成的散点图,通过显示一些图像进行美化。t-SNE算法识别出几个与类别相匹配的簇(每个类别由不同的颜色表示)。

因此,自编码器可以用于降维。另一个应用是无监督的预训练。

正如我们在第十一章中讨论的,如果你正在处理一个复杂的监督任务,但没有太多标记的训练数据,一个解决方案是找到一个执行类似任务的神经网络,并重复使用其较低层。这样可以使用少量训练数据训练高性能模型,因为你的神经网络不需要学习所有低级特征;它只需重复使用现有网络学习的特征检测器。

同样,如果你有一个大型数据集,但其中大部分是未标记的,你可以首先使用所有数据训练一个堆叠自编码器,然后重复使用较低层来创建一个用于实际任务的神经网络,并使用标记数据进行训练。例如,图17-6展示了如何使用堆叠自编码器为分类神经网络执行无监督预训练。在训练分类器时,如果你确实没有太多标记的训练数据,可能需要冻结预训练层(至少是较低的层)。

拥有大量未标记数据和少量标记数据是很常见的。构建一个大型未标记数据集通常很便宜(例如,一个简单的脚本可以从互联网上下载数百万张图片),但标记这些图片(例如,将它们分类为可爱或不可爱)通常只能由人类可靠地完成。标记实例是耗时且昂贵的,因此通常只有少量人类标记的实例,甚至更少。

实现上没有什么特别之处:只需使用所有训练数据(标记加未标记)训练一个自编码器,然后重复使用其编码器层来创建一个新的神经网络(请参考本章末尾的练习示例)。

接下来,让我们看一些训练堆叠自编码器的技术。

当一个自编码器是整齐对称的,就像我们刚刚构建的那样,一个常见的技术是将解码器层的权重与编码器层的权重绑定在一起。这样可以减半模型中的权重数量,加快训练速度并限制过拟合的风险。具体来说,如果自编码器总共有N层(不包括输入层),而W[L]表示第L层的连接权重(例如,第1层是第一个隐藏层,第N/2层是编码层,第N层是输出层),那么解码器层的权重可以定义为W[L]=W[N–L+1]^(其中L=N/2+1,…,N)。

使用Keras在层之间绑定权重,让我们定义一个自定义层:

classDenseTranspose(tf.keras.layers.Layer):def__init__(self,dense,activation=None,**kwargs):super().__init__(**kwargs)self.dense=denseself.activation=tf.keras.activations.get(activation)defbuild(self,batch_input_shape):self.biases=self.add_weight(name="bias",shape=self.dense.input_shape[-1],initializer="zeros")super().build(batch_input_shape)defcall(self,inputs):Z=tf.matmul(inputs,self.dense.weights[0],transpose_b=True)returnself.activation(Z+self.biases)这个自定义层就像一个常规的Dense层,但它使用另一个Dense层的权重,经过转置(设置transpose_b=True等同于转置第二个参数,但更高效,因为它在matmul()操作中实时执行转置)。然而,它使用自己的偏置向量。现在我们可以构建一个新的堆叠自编码器,与之前的模型类似,但解码器的Dense层与编码器的Dense层绑定:

dense_1=tf.keras.layers.Dense(100,activation="relu")dense_2=tf.keras.layers.Dense(30,activation="relu")tied_encoder=tf.keras.Sequential([tf.keras.layers.Flatten(),dense_1,dense_2])tied_decoder=tf.keras.Sequential([DenseTranspose(dense_2,activation="relu"),DenseTranspose(dense_1),tf.keras.layers.Reshape([28,28])])tied_ae=tf.keras.Sequential([tied_encoder,tied_decoder])这个模型实现了与之前模型大致相同的重构误差,使用了几乎一半的参数数量。

与我们刚刚做的整个堆叠自编码器一次性训练不同,可以一次训练一个浅层自编码器,然后将它们堆叠成一个单一的堆叠自编码器(因此得名),如图17-7所示。这种技术现在不太常用,但你可能仍然会遇到一些论文讨论“贪婪逐层训练”,所以了解其含义是很有必要的。

在训练的第一阶段,第一个自编码器学习重建输入。然后我们使用这个第一个自编码器对整个训练集进行编码,这给我们一个新的(压缩的)训练集。然后我们在这个新数据集上训练第二个自编码器。这是训练的第二阶段。最后,我们构建一个大的三明治,使用所有这些自编码器,如图17-7所示(即,我们首先堆叠每个自编码器的隐藏层,然后反向堆叠输出层)。这给我们最终的堆叠自编码器(请参阅本章笔记本中“逐个训练自编码器”部分以获取实现)。通过这种方式,我们可以轻松训练更多的自编码器,构建一个非常深的堆叠自编码器。

自编码器不仅限于密集网络:你也可以构建卷积自编码器。现在让我们来看看这些。

conv_encoder=tf.keras.Sequential([tf.keras.layers.Reshape([28,28,1]),tf.keras.layers.Conv2D(16,3,padding="same",activation="relu"),tf.keras.layers.MaxPool2D(pool_size=2),#output:14×14x16tf.keras.layers.Conv2D(32,3,padding="same",activation="relu"),tf.keras.layers.MaxPool2D(pool_size=2),#output:7×7x32tf.keras.layers.Conv2D(64,3,padding="same",activation="relu"),tf.keras.layers.MaxPool2D(pool_size=2),#output:3×3x64tf.keras.layers.Conv2D(30,3,padding="same",activation="relu"),tf.keras.layers.GlobalAvgPool2D()#output:30])conv_decoder=tf.keras.Sequential([tf.keras.layers.Dense(3*3*16),tf.keras.layers.Reshape((3,3,16)),tf.keras.layers.Conv2DTranspose(32,3,strides=2,activation="relu"),tf.keras.layers.Conv2DTranspose(16,3,strides=2,padding="same",activation="relu"),tf.keras.layers.Conv2DTranspose(1,3,strides=2,padding="same"),tf.keras.layers.Reshape([28,28])])conv_ae=tf.keras.Sequential([conv_encoder,conv_decoder])还可以使用其他架构类型创建自编码器,例如RNNs(请参阅笔记本中的示例)。

好的,让我们退后一步。到目前为止,我们已经看过各种类型的自编码器(基本、堆叠和卷积),以及如何训练它们(一次性或逐层)。我们还看过一些应用:数据可视化和无监督预训练。

迄今为止,为了强迫自编码器学习有趣的特征,我们限制了编码层的大小,使其欠完备。实际上还有许多其他类型的约束可以使用,包括允许编码层与输入一样大,甚至更大,从而产生过完备自编码器。接下来,我们将看一些其他类型的自编码器:去噪自编码器、稀疏自编码器和变分自编码器。

噪声可以是添加到输入的纯高斯噪声,也可以是随机关闭的输入,就像dropout中一样(在第十一章中介绍)。图17-8展示了这两种选项。

实现很简单:这是一个常规的堆叠自编码器,附加了一个Dropout层应用于编码器的输入(或者您可以使用一个GaussianNoise层)。请记住,Dropout层仅在训练期间激活(GaussianNoise层也是如此):

图17-9显示了一些嘈杂的图像(一半像素关闭),以及基于dropout的去噪自编码器重建的图像。请注意,自编码器猜测了实际输入中不存在的细节,例如白色衬衫的顶部(底部行,第四幅图)。正如您所看到的,去噪自编码器不仅可以用于数据可视化或无监督预训练,就像我们迄今讨论过的其他自编码器一样,而且还可以非常简单高效地从图像中去除噪声。

另一种通常导致良好特征提取的约束是稀疏性:通过向成本函数添加适当的项,自编码器被推动减少编码层中活跃神经元的数量。例如,它可能被推动使编码层中平均只有5%的显著活跃神经元。这迫使自编码器将每个输入表示为少量激活的组合。结果,编码层中的每个神经元通常最终代表一个有用的特征(如果您每个月只能说几个词,您可能会尽量使它们值得倾听)。

一个简单的方法是在编码层中使用sigmoid激活函数(将编码限制在0到1之间),使用一个大的编码层(例如,具有300个单元),并向编码层的激活添加一些[1]正则化。解码器只是一个常规的解码器:

sparse_l1_encoder=tf.keras.Sequential([tf.keras.layers.Flatten(),tf.keras.layers.Dense(100,activation="relu"),tf.keras.layers.Dense(300,activation="sigmoid"),tf.keras.layers.ActivityRegularization(l1=1e-4)])sparse_l1_decoder=tf.keras.Sequential([tf.keras.layers.Dense(100,activation="relu"),tf.keras.layers.Dense(28*28),tf.keras.layers.Reshape([28,28])])sparse_l1_ae=tf.keras.Sequential([sparse_l1_encoder,sparse_l1_decoder])这个ActivityRegularization层只是返回其输入,但作为副作用,它会添加一个训练损失,等于其输入的绝对值之和。这只影响训练。同样,您可以删除ActivityRegularization层,并在前一层中设置activity_regularizer=tf.keras.regularizers.l1(1e-4)。这种惩罚将鼓励神经网络生成接近0的编码,但由于如果不能正确重建输入也会受到惩罚,因此它必须输出至少几个非零值。使用[1]范数而不是[2]范数将推动神经网络保留最重要的编码,同时消除不需要的编码(而不仅仅是减少所有编码)。

另一种方法,通常会产生更好的结果,是在每次训练迭代中测量编码层的实际稀疏度,并在测量到的稀疏度与目标稀疏度不同时对模型进行惩罚。我们通过计算编码层中每个神经元的平均激活值来实现这一点,整个训练批次上。批次大小不能太小,否则平均值将不准确。

一旦我们有了每个神经元的平均激活,我们希望通过向成本函数添加稀疏损失来惩罚那些激活过多或不足的神经元。例如,如果我们测量到一个神经元的平均激活为0.3,但目标稀疏度为0.1,那么它必须受到惩罚以减少激活。一种方法可能是简单地将平方误差(0.3-0.1)2添加到成本函数中,但实际上更好的方法是使用Kullback–Leibler(KL)散度(在第四章中简要讨论),它比均方误差具有更强的梯度,如您可以在图17-10中看到的那样。

给定两个离散概率分布P和Q,这些分布之间的KL散度,记为DKL,可以使用方程17-1计算。

DKL(P∥Q)=∑iP(i)logP(i)Q(i)

在我们的情况下,我们想要衡量编码层中神经元激活的目标概率p和通过测量整个训练批次上的平均激活来估计的实际概率q之间的差异。因此,KL散度简化为方程17-2。

DKL(p∥q)=plogpq+(1-p)log1-p1-q

一旦我们计算了编码层中每个神经元的稀疏损失,我们将这些损失相加并将结果添加到成本函数中。为了控制稀疏损失和重构损失的相对重要性,我们可以将稀疏损失乘以一个稀疏权重超参数。如果这个权重太高,模型将严格遵循目标稀疏度,但可能无法正确重构输入,使模型无用。相反,如果权重太低,模型将主要忽略稀疏目标,并且不会学习任何有趣的特征。

现在我们有了所有需要基于KL散度实现稀疏自编码器的东西。首先,让我们创建一个自定义正则化器来应用KL散度正则化:

kl_divergence=tf.keras.losses.kullback_leibler_divergenceclassKLDivergenceRegularizer(tf.keras.regularizers.Regularizer):def__init__(self,weight,target):self.weight=weightself.target=targetdef__call__(self,inputs):mean_activities=tf.reduce_mean(inputs,axis=0)returnself.weight*(kl_divergence(self.target,mean_activities)+kl_divergence(1.-self.target,1.-mean_activities))现在我们可以构建稀疏自编码器,使用KLDivergenceRegularizer来对编码层的激活进行正则化:

kld_reg=KLDivergenceRegularizer(weight=5e-3,target=0.1)sparse_kl_encoder=tf.keras.Sequential([tf.keras.layers.Flatten(),tf.keras.layers.Dense(100,activation="relu"),tf.keras.layers.Dense(300,activation="sigmoid",activity_regularizer=kld_reg)])sparse_kl_decoder=tf.keras.Sequential([tf.keras.layers.Dense(100,activation="relu"),tf.keras.layers.Dense(28*28),tf.keras.layers.Reshape([28,28])])sparse_kl_ae=tf.keras.Sequential([sparse_kl_encoder,sparse_kl_decoder])在FashionMNIST上训练这个稀疏自编码器后,编码层的稀疏度大约为10%。

VAEs在这些特定方面与我们迄今讨论过的所有自编码器都有很大不同:

这两个特性使得VAEs与RBM相当相似,但它们更容易训练,采样过程也更快(对于RBM,您需要等待网络稳定到“热平衡”状态,然后才能对新实例进行采样)。正如它们的名字所暗示的,变分自编码器执行变分贝叶斯推断,这是进行近似贝叶斯推断的有效方法。回想一下,贝叶斯推断意味着根据新数据更新概率分布,使用从贝叶斯定理推导出的方程。原始分布称为先验,而更新后的分布称为后验。在我们的情况下,我们想要找到数据分布的一个很好的近似。一旦我们有了这个,我们就可以从中进行采样。

让我们看看VAEs是如何工作的。图17-11(左)展示了一个变分自编码器。您可以认出所有自编码器的基本结构,具有一个编码器后面跟着一个解码器(在这个例子中,它们都有两个隐藏层),但有一个转折:编码器不是直接为给定输入产生编码,而是产生一个均值编码μ和一个标准差σ。然后,实际编码随机地从均值μ和标准差σ的高斯分布中采样。之后解码器正常解码采样的编码。图的右侧显示了一个训练实例通过这个自编码器的过程。首先,编码器产生μ和σ,然后一个编码被随机采样(请注意它并不完全位于μ),最后这个编码被解码;最终输出类似于训练实例。

正如您在图中所看到的,尽管输入可能具有非常复杂的分布,但变分自编码器倾向于产生看起来像是从简单的高斯分布中采样的编码:在训练期间,成本函数(下面讨论)推动编码逐渐在编码空间(也称为潜在空间)内迁移,最终看起来像一个高斯点云。一个很大的结果是,在训练变分自编码器之后,您可以非常容易地生成一个新实例:只需从高斯分布中随机采样一个随机编码,解码它,然后就完成了!

现在,让我们看一下成本函数。它由两部分组成。第一部分是通常的重构损失,推动自编码器重现其输入。我们可以使用MSE来实现这一点,就像我们之前做的那样。第二部分是潜在损失,推动自编码器具有看起来像是从简单高斯分布中抽样的编码:这是目标分布(即高斯分布)与编码的实际分布之间的KL散度。数学比稀疏自编码器更复杂,特别是由于高斯噪声,它限制了可以传输到编码层的信息量。这推动自编码器学习有用的特征。幸运的是,方程简化了,因此可以使用Equation17-3计算潜在损失。

L=-12∑i=1n1+log(σi2)-σi2-μi2

在这个方程中,是潜在损失,n是编码的维度,μ[i]和σ[i]是编码的第i个分量的均值和标准差。向量μ和σ(包含所有μ[i]和σ[i])由编码器输出,如Figure17-11(左)所示。

变分自编码器架构的常见调整是使编码器输出γ=log(σ2)而不是σ。然后可以根据Equation17-4计算潜在损失。这种方法在数值上更稳定,加快了训练速度。

L=-12∑i=1n1+γi-exp(γi)-μi2

现在,让我们开始为FashionMNIST构建一个变分自编码器(如Figure17-11所示,但使用γ调整)。首先,我们需要一个自定义层来根据μ和γ抽样编码:

classSampling(tf.keras.layers.Layer):defcall(self,inputs):mean,log_var=inputsreturntf.random.normal(tf.shape(log_var))*tf.exp(log_var/2)+mean这个Sampling层接受两个输入:mean(μ)和log_var(γ)。它使用函数tf.random.normal()从均值为0,标准差为1的高斯分布中抽样一个随机向量(与γ形状相同)。然后将其乘以exp(γ/2)(数学上等于σ,您可以验证),最后加上μ并返回结果。这样从均值为μ,标准差为σ的高斯分布中抽样一个编码向量。

接下来,我们可以创建编码器,使用函数式API,因为模型不是完全顺序的:

codings_size=10inputs=tf.keras.layers.Input(shape=[28,28])Z=tf.keras.layers.Flatten()(inputs)Z=tf.keras.layers.Dense(150,activation="relu")(Z)Z=tf.keras.layers.Dense(100,activation="relu")(Z)codings_mean=tf.keras.layers.Dense(codings_size)(Z)#μcodings_log_var=tf.keras.layers.Dense(codings_size)(Z)#γcodings=Sampling()([codings_mean,codings_log_var])variational_encoder=tf.keras.Model(inputs=[inputs],outputs=[codings_mean,codings_log_var,codings])注意,输出codings_mean(μ)和codings_log_var(γ)的Dense层具有相同的输入(即第二个Dense层的输出)。然后,我们将codings_mean和codings_log_var都传递给Sampling层。最后,variational_encoder模型有三个输出。只需要codings,但我们也添加了codings_mean和codings_log_var,以防我们想要检查它们的值。现在让我们构建解码器:

decoder_inputs=tf.keras.layers.Input(shape=[codings_size])x=tf.keras.layers.Dense(100,activation="relu")(decoder_inputs)x=tf.keras.layers.Dense(150,activation="relu")(x)x=tf.keras.layers.Dense(28*28)(x)outputs=tf.keras.layers.Reshape([28,28])(x)variational_decoder=tf.keras.Model(inputs=[decoder_inputs],outputs=[outputs])对于这个解码器,我们可以使用顺序API而不是功能API,因为它实际上只是一个简单的层堆栈,与我们迄今构建的许多解码器几乎相同。最后,让我们构建变分自编码器模型:

_,_,codings=variational_encoder(inputs)reconstructions=variational_decoder(codings)variational_ae=tf.keras.Model(inputs=[inputs],outputs=[reconstructions])我们忽略编码器的前两个输出(我们只想将编码输入解码器)。最后,我们必须添加潜在损失和重构损失:

latent_loss=-0.5*tf.reduce_sum(1+codings_log_var-tf.exp(codings_log_var)-tf.square(codings_mean),axis=-1)variational_ae.add_loss(tf.reduce_mean(latent_loss)/784.)我们首先应用方程17-4来计算批处理中每个实例的潜在损失,对最后一个轴求和。然后我们计算批处理中所有实例的平均损失,并将结果除以784,以确保它具有适当的比例,与重构损失相比。实际上,变分自编码器的重构损失应该是像素重构误差的总和,但是当Keras计算"mse"损失时,它计算所有784个像素的平均值,而不是总和。因此,重构损失比我们需要的要小784倍。我们可以定义一个自定义损失来计算总和而不是平均值,但将潜在损失除以784(最终损失将比应该的大784倍,但这只是意味着我们应该使用更大的学习率)更简单。

最后,我们可以编译和拟合自编码器!

variational_ae.compile(loss="mse",optimizer="nadam")history=variational_ae.fit(X_train,X_train,epochs=25,batch_size=128,validation_data=(X_valid,X_valid))生成时尚MNIST图像现在让我们使用这个变分自编码器生成看起来像时尚物品的图像。我们只需要从高斯分布中随机采样编码,并解码它们:

codings=tf.random.normal(shape=[3*7,codings_size])images=variational_decoder(codings).numpy()图17-12显示了生成的12张图像。

变分自编码器使得执行语义插值成为可能:不是在像素级别插值两个图像,看起来就像两个图像只是叠加在一起,我们可以在编码级别进行插值。例如,让我们在潜在空间中沿着任意线取几个编码,并解码它们。我们得到一系列图像,逐渐从裤子变成毛衣(见图17-13):

现在让我们转向GANs:它们更难训练,但当你设法让它们工作时,它们会产生非常惊人的图像。

生成器

以随机分布(通常是高斯)作为输入,并输出一些数据——通常是图像。您可以将随机输入视为要生成的图像的潜在表示(即编码)。因此,正如您所看到的,生成器提供了与变分自编码器中的解码器相同的功能,并且可以以相同的方式用于生成新图像:只需将一些高斯噪声输入,它就会输出一个全新的图像。但是,它的训练方式非常不同,您很快就会看到。

鉴别器

以生成器的假图像或训练集中的真实图像作为输入,必须猜测输入图像是假还是真。

在训练过程中,生成器和鉴别器有着相反的目标:鉴别器试图区分假图像和真实图像,而生成器试图生成看起来足够真实以欺骗鉴别器的图像。由于GAN由具有不同目标的两个网络组成,因此无法像训练常规神经网络那样进行训练。每个训练迭代被分为两个阶段:

生成器实际上从未看到任何真实图像,但它逐渐学会生成令人信服的假图像!它所得到的只是通过鉴别器反向传播的梯度。幸运的是,鉴别器越好,这些二手梯度中包含的关于真实图像的信息就越多,因此生成器可以取得显著进展。

让我们继续构建一个简单的FashionMNISTGAN。

首先,我们需要构建生成器和鉴别器。生成器类似于自编码器的解码器,鉴别器是一个常规的二元分类器:它以图像作为输入,最终以包含单个单元并使用sigmoid激活函数的Dense层结束。对于每个训练迭代的第二阶段,我们还需要包含生成器后面的鉴别器的完整GAN模型:

codings_size=30Dense=tf.keras.layers.Densegenerator=tf.keras.Sequential([Dense(100,activation="relu",kernel_initializer="he_normal"),Dense(150,activation="relu",kernel_initializer="he_normal"),Dense(28*28,activation="sigmoid"),tf.keras.layers.Reshape([28,28])])discriminator=tf.keras.Sequential([tf.keras.layers.Flatten(),Dense(150,activation="relu",kernel_initializer="he_normal"),Dense(100,activation="relu",kernel_initializer="he_normal"),Dense(1,activation="sigmoid")])gan=tf.keras.Sequential([generator,discriminator])接下来,我们需要编译这些模型。由于鉴别器是一个二元分类器,我们可以自然地使用二元交叉熵损失。gan模型也是一个二元分类器,因此它也可以使用二元交叉熵损失。然而,生成器只会通过gan模型进行训练,因此我们根本不需要编译它。重要的是,在第二阶段之前鉴别器不应该被训练,因此在编译gan模型之前我们将其设置为不可训练:

discriminator.compile(loss="binary_crossentropy",optimizer="rmsprop")discriminator.trainable=Falsegan.compile(loss="binary_crossentropy",optimizer="rmsprop")注意trainable属性只有在编译模型时才会被Keras考虑,因此在运行此代码后,如果我们调用其fit()方法或train_on_batch()方法(我们将使用),则discriminator是可训练的,而在调用这些方法时gan模型是不可训练的。

由于训练循环是不寻常的,我们不能使用常规的fit()方法。相反,我们将编写一个自定义训练循环。为此,我们首先需要创建一个Dataset来迭代图像:

batch_size=32dataset=tf.data.Dataset.from_tensor_slices(X_train).shuffle(buffer_size=1000)dataset=dataset.batch(batch_size,drop_remainder=True).prefetch(1)现在我们准备编写训练循环。让我们将其封装在一个train_gan()函数中:

deftrain_gan(gan,dataset,batch_size,codings_size,n_epochs):generator,discriminator=gan.layersforepochinrange(n_epochs):forX_batchindataset:#phase1-trainingthediscriminatornoise=tf.random.normal(shape=[batch_size,codings_size])generated_images=generator(noise)X_fake_and_real=tf.concat([generated_images,X_batch],axis=0)y1=tf.constant([[0.]]*batch_size+[[1.]]*batch_size)discriminator.train_on_batch(X_fake_and_real,y1)#phase2-trainingthegeneratornoise=tf.random.normal(shape=[batch_size,codings_size])y2=tf.constant([[1.]]*batch_size)gan.train_on_batch(noise,y2)train_gan(gan,dataset,batch_size,codings_size,n_epochs=50)正如之前讨论的,您可以在每次迭代中看到两个阶段:

就是这样!训练后,您可以随机从高斯分布中抽取一些编码,并将它们馈送给生成器以生成新图像:

codings=tf.random.normal(shape=[batch_size,codings_size])generated_images=generator.predict(codings)如果显示生成的图像(参见图17-15),您会发现在第一个时期结束时,它们已经开始看起来像(非常嘈杂的)时尚MNIST图像。

不幸的是,图像从未真正比那更好,甚至可能会出现GAN似乎忘记了它学到的东西的时期。为什么会这样呢?原来,训练GAN可能是具有挑战性的。让我们看看为什么。

在训练过程中,生成器和鉴别器不断试图在零和博弈中互相智胜。随着训练的进行,游戏可能会进入博弈论家称之为纳什均衡的状态,以数学家约翰·纳什命名:这是当没有玩家会因为改变自己的策略而变得更好,假设其他玩家不改变自己的策略。例如,当每个人都在道路的左侧行驶时,就达到了纳什均衡:没有司机会因为成为唯一一个换边的人而变得更好。当然,还有第二种可能的纳什均衡:当每个人都在道路的右侧行驶时。不同的初始状态和动态可能导致一个或另一个均衡。在这个例子中,一旦达到均衡状态(即,与其他人一样在同一侧行驶),就会有一个单一的最佳策略,但是纳什均衡可能涉及多种竞争策略(例如,捕食者追逐猎物,猎物试图逃跑,双方都不会因为改变策略而变得更好)。

最大的困难被称为模式坍塌:这是指生成器的输出逐渐变得不那么多样化。这是如何发生的呢?假设生成器在制作令人信服的鞋子方面比其他任何类别都更擅长。它会用鞋子更多地欺骗鉴别器,这将鼓励它生成更多的鞋子图像。逐渐地,它会忘记如何制作其他任何东西。与此同时,鉴别器将看到的唯一假图像将是鞋子,因此它也会忘记如何鉴别其他类别的假图像。最终,当鉴别器设法将假鞋子与真实鞋子区分开来时,生成器将被迫转向另一个类别。然后它可能擅长衬衫,忘记鞋子,鉴别器也会跟随。GAN可能逐渐在几个类别之间循环,从未真正擅长其中任何一个。

此外,由于生成器和鉴别器不断相互对抗,它们的参数可能最终会振荡并变得不稳定。训练可能开始正常,然后由于这些不稳定性,突然出现无明显原因的分歧。由于许多因素影响这些复杂的动态,GAN对超参数非常敏感:您可能需要花费大量精力对其进行微调。实际上,这就是为什么在编译模型时我使用RMSProp而不是Nadam:使用Nadam时,我遇到了严重的模式崩溃。

简而言之,这仍然是一个非常活跃的研究领域,GAN的动态仍然没有完全被理解。但好消息是取得了巨大进展,一些结果真的令人惊叹!因此,让我们看一些最成功的架构,从几年前的深度卷积GAN开始。然后我们将看一下两个更近期(更复杂)的架构。

这些准则在许多情况下都适用,但并非总是如此,因此您可能仍需要尝试不同的超参数。实际上,仅仅改变随机种子并再次训练完全相同的模型有时会奏效。以下是一个在时尚MNIST上表现相当不错的小型DCGAN:

X_train_dcgan=X_train.reshape(-1,28,28,1)*2.-1.#reshapeandrescale鉴别器看起来很像用于二元分类的常规CNN,只是不是使用最大池化层来对图像进行下采样,而是使用步幅卷积(strides=2)。请注意,我们使用了泄漏的ReLU激活函数。总体而言,我们遵守了DCGAN的指导方针,只是将鉴别器中的BatchNormalization层替换为Dropout层;否则,在这种情况下训练会不稳定。随意调整这个架构:您将看到它对超参数非常敏感,特别是两个网络的相对学习率。

最后,要构建数据集,然后编译和训练这个模型,我们可以使用之前的相同代码。经过50个训练周期后,生成器产生的图像如图17-16所示。它还不完美,但其中许多图像相当令人信服。

如果您扩大这个架构并在大量人脸数据集上进行训练,您可以获得相当逼真的图像。事实上,DCGAN可以学习相当有意义的潜在表示,如图17-17所示:生成了许多图像,手动选择了其中的九个(左上角),包括三个戴眼镜的男性,三个不戴眼镜的男性和三个不戴眼镜的女性。对于这些类别中的每一个,用于生成图像的编码被平均,然后基于结果的平均编码生成图像(左下角)。简而言之,左下角的三幅图像分别代表位于其上方的三幅图像的平均值。但这不是在像素级别计算的简单平均值(这将导致三个重叠的脸),而是在潜在空间中计算的平均值,因此图像看起来仍然像正常的脸。令人惊讶的是,如果您计算戴眼镜的男性,减去不戴眼镜的男性,再加上不戴眼镜的女性——其中每个术语对应于一个平均编码——并生成对应于此编码的图像,您将得到右侧面孔网格中心的图像:一个戴眼镜的女性!其周围的其他八幅图像是基于相同向量加上一点噪音生成的,以展示DCGAN的语义插值能力。能够在人脸上进行算术运算感觉像是科幻!

然而,DCGAN并不完美。例如,当您尝试使用DCGAN生成非常大的图像时,通常会出现局部令人信服的特征,但整体上存在不一致,比如一只袖子比另一只长得多的衬衫,不同的耳环,或者眼睛看向相反的方向。您如何解决这个问题?

例如,当将生成器的输出从4×4增加到8×8时(参见图17-18),在现有卷积层(“Conv1”)中添加了一个上采样层(使用最近邻过滤)以生成8×8特征图。这些被馈送到新的卷积层(“Conv2”),然后馈送到新的输出卷积层。为了避免破坏Conv1的训练权重,我们逐渐淡入两个新的卷积层(在图17-18中用虚线表示),并淡出原始输出层。最终输出是新输出(权重为α)和原始输出(权重为1-α)的加权和,从0逐渐增加α到1。当向鉴别器添加新的卷积层时(后跟一个平均池化层进行下采样),也使用类似的淡入/淡出技术。请注意,所有卷积层都使用"same"填充和步幅为1,因此它们保留其输入的高度和宽度。这包括原始卷积层,因此它现在产生8×8的输出(因为其输入现在是8×8)。最后,输出层使用核大小为1。它们只是将它们的输入投影到所需数量的颜色通道(通常为3)。

该论文还介绍了几种旨在增加输出多样性(以避免模式崩溃)并使训练更稳定的技术:

小批量标准差层

添加到鉴别器的末尾附近。对于输入中的每个位置,它计算批次中所有通道和所有实例的标准差(S=tf.math.reduce_std(inputs,axis=[0,-1]))。然后,这些标准差在所有点上进行平均以获得单个值(v=tf.reduce_mean(S))。最后,在批次中的每个实例中添加一个额外的特征图,并填充计算出的值(tf.concat([inputs,tf.fill([batch_size,height,width,1],v)],axis=-1))。这有什么帮助呢?如果生成器生成具有很少变化的图像,那么在鉴别器的特征图中将会有很小的标准差。由于这一层,鉴别器将更容易访问这个统计数据,使得它不太可能被生成器欺骗,生成器产生的多样性太少。这将鼓励生成器产生更多样化的输出,减少模式崩溃的风险。

均衡学习率

像素级归一化层

在生成器的每个卷积层之后添加。它根据同一图像和位置处的所有激活进行归一化,但跨所有通道(除以均方激活的平方根)。在TensorFlow代码中,这是inputs/tf.sqrt(tf.reduce_mean(tf.square(X),axis=-1,keepdims=True)+1e-8)(需要平滑项1e-8以避免除以零)。这种技术避免了由于生成器和鉴别器之间的激烈竞争而导致激活爆炸。

映射网络

一个将潜在表示z(即编码)映射到向量w的八层MLP。然后,将该向量通过多个仿射变换(即没有激活函数的Dense层,在图17-19中用“A”框表示)发送,从而产生多个向量。这些向量控制生成图像的风格在不同层次上,从细粒度纹理(例如头发颜色)到高级特征(例如成人或儿童)。简而言之,映射网络将编码映射到多个风格向量。

合成网络

负责生成图像。它有一个恒定的学习输入(明确地说,这个输入在训练之后将是恒定的,但在训练期间,它会通过反向传播不断调整)。它通过多个卷积和上采样层处理这个输入,就像之前一样,但有两个变化。首先,在输入和所有卷积层的输出(在激活函数之前)中添加了一些噪音。其次,每个噪音层后面都跟着一个自适应实例归一化(AdaIN)层:它独立地标准化每个特征图(通过减去特征图的均值并除以其标准差),然后使用风格向量确定每个特征图的比例和偏移(风格向量包含每个特征图的一个比例和一个偏置项)。

独立于编码添加噪音的想法非常重要。图像的某些部分是相当随机的,比如每个雀斑或头发的确切位置。在早期的GAN中,这种随机性要么来自编码,要么是生成器本身产生的一些伪随机噪音。如果来自编码,这意味着生成器必须将编码的表征能力的相当一部分用于存储噪音,这是相当浪费的。此外,噪音必须能够流经网络并到达生成器的最终层:这似乎是一个不必要的约束,可能会减慢训练速度。最后,一些视觉伪影可能会出现,因为在不同级别使用相同的噪音。如果生成器尝试生成自己的伪随机噪音,这种噪音可能看起来不太令人信服,导致更多的视觉伪影。此外,生成器的一部分权重将被用于生成伪随机噪音,这再次似乎是浪费的。通过添加额外的噪音输入,所有这些问题都可以避免;GAN能够利用提供的噪音为图像的每个部分添加适量的随机性。

每个级别的添加噪音都是不同的。每个噪音输入由一个充满高斯噪音的单个特征图组成,该特征图被广播到所有特征图(给定级别的)并使用学习的每个特征比例因子进行缩放(这由图17-19中的“B”框表示)然后添加。

有这么多种类的GAN,需要一本整书来覆盖它们。希望这个介绍给您带来了主要思想,最重要的是让您有学习更多的愿望。继续实现您自己的GAN,如果一开始学习有困难,请不要灰心:不幸的是,这是正常的,需要相当多的耐心才能使其正常运行,但结果是值得的。如果您在实现细节上遇到困难,有很多Keras或TensorFlow的实现可以参考。实际上,如果您只是想快速获得一些惊人的结果,那么您可以使用预训练模型(例如,有适用于Keras的预训练StyleGAN模型可用)。

现在我们已经研究了自编码器和GANs,让我们看看最后一种架构:扩散模型。

随着我们在正向过程中不断添加更多高斯噪声,像素值的分布变得越来越高斯。我遗漏的一个重要细节是,每一步像素值都会被稍微重新缩放,缩放因子为1-βt。这确保了像素值的均值逐渐接近0,因为缩放因子比1稍微小一点(想象一下反复将一个数字乘以0.99)。这也确保了方差将逐渐收敛到1。这是因为像素值的标准差也会被1-βt缩放,因此方差会被1-β[t](即缩放因子的平方)缩放。但是方差不能收缩到0,因为我们在每一步都添加方差为β[t]的高斯噪声。而且由于当你对高斯分布求和时方差会相加,您可以看到方差只能收敛到1-β[t]+β[t]=1。

前向扩散过程总结在Equation17-5中。这个方程不会教你任何新的关于前向过程的知识,但理解这种数学符号是有用的,因为它经常在机器学习论文中使用。这个方程定义了给定x[t–1]的概率分布q中x[t]的概率分布,其均值为x[t–1]乘以缩放因子,并且具有等于β[t]I的协方差矩阵。这是由β[t]乘以单位矩阵I得到的,这意味着噪音是各向同性的,方差为β[t]。

q(xt|xt-1)=N(1-βtxt-1,βtI)

有趣的是,前向过程有一个快捷方式:可以在不必先计算x[1],x[2],…,x[t–1]的情况下,给定x[0]来采样图像x[t]。实际上,由于多个高斯分布的和也是一个高斯分布,所有的噪音可以在一次性使用Equation17-6中的公式添加。这是我们将要使用的方程,因为它速度更快。

q(xt|x0)=Nαˉtx0,(1-αˉt)I

当然,我们的目标不是让猫淹没在噪音中。相反,我们想要创造许多新的猫!我们可以通过训练一个能够执行逆过程的模型来实现这一点:从x[t]到x[t–1]。然后我们可以使用它从图像中去除一点噪音,并重复这个操作多次,直到所有的噪音都消失。如果我们在包含许多猫图像的数据集上训练模型,那么我们可以给它一张完全充满高斯噪音的图片,模型将逐渐使一个全新的猫出现(见Figure17-20)。

βt=1-αˉtαˉt-1,其中αˉt=f(t)f(0)和f(t)=cos(t/T+s1+s·π2)2

在这些方程中:

让我们创建一个小函数来计算α[t],β[t]和α[t],并使用T=4,000调用它:

defprepare_batch(X):X=tf.cast(X[...,tf.newaxis],tf.float32)*2-1#scalefrom–1to+1X_shape=tf.shape(X)t=tf.random.uniform([X_shape[0]],minval=1,maxval=T+1,dtype=tf.int32)alpha_cm=tf.gather(alpha_cumprod,t)alpha_cm=tf.reshape(alpha_cm,[X_shape[0]]+[1]*(len(X_shape)-1))noise=tf.random.normal(X_shape)return{"X_noisy":alpha_cm**0.5*X+(1-alpha_cm)**0.5*noise,"time":t,},noise让我们看一下这段代码:

通过这种设置,模型将预测应从输入图像中减去的噪声,以获得原始图像。为什么不直接预测原始图像呢?嗯,作者尝试过:它简单地效果不如预期。

接下来,我们将创建一个训练数据集和一个验证集,将prepare_batch()函数应用于每个批次。与之前一样,X_train和X_valid包含像素值从0到1的时尚MNIST图像:

现在我们可以正常训练模型了。作者指出,使用MAE损失比MSE效果更好。您也可以使用Huber损失:

model=build_diffusion_model()model.compile(loss=tf.keras.losses.Huber(),optimizer="nadam")history=model.fit(train_set,validation_data=valid_set,epochs=100)一旦模型训练完成,您可以使用它生成新图像。不幸的是,在反向扩散过程中没有捷径,因此您必须从均值为0,方差为1的高斯分布中随机抽样x[T],然后将其传递给模型预测噪声;使用方程17-8从图像中减去它,然后您会得到x[T–1]。重复这个过程3999次,直到得到x[0]:如果一切顺利,它应该看起来像一个常规的时尚MNIST图像!

xt-1=1αtxt-βt1-αˉtθ(xt,t)+βtz

让我们编写一个实现这个反向过程的函数,并调用它生成一些图像:

defgenerate(model,batch_size=32):X=tf.random.normal([batch_size,28,28,1])fortinrange(T,0,-1):noise=(tf.random.normalift>1elsetf.zeros)(tf.shape(X))X_noise=model({"X_noisy":X,"time":tf.constant([t]*batch_size)})X=(1/alpha[t]**0.5*(X-beta[t]/(1-alpha_cumprod[t])**0.5*X_noise)+(1-alpha[t])**0.5*noise)returnXX_gen=generate(model)#generatedimages这可能需要一两分钟。这是扩散模型的主要缺点:生成图像很慢,因为模型需要被多次调用。通过使用较小的T值或者同时使用相同模型预测多个步骤,可以加快这一过程,但生成的图像可能不那么漂亮。尽管存在这种速度限制,扩散模型确实生成高质量和多样化的图像,正如您在图17-22中所看到的。

此外,研究人员还采用了各种调节技术来引导扩散过程,使用文本提示、图像或任何其他输入。这使得快速生成一个漂亮的高分辨率图像成为可能,比如一只读书的蝾螈,或者你可能喜欢的其他任何东西。您还可以使用输入图像来调节图像生成过程。这使得许多应用成为可能,比如外部绘制——在输入图像的边界之外扩展——或内部绘制——填充图像中的空洞。

在下一章中,我们将转向深度强化学习的一个完全不同的分支。

1WilliamG.Chase和HerbertA.Simon,“国际象棋中的感知”,认知心理学4,第1期(1973年):55-81。

2YoshuaBengio等,“深度网络的贪婪逐层训练”,第19届神经信息处理系统国际会议论文集(2006):153-160。

3JonathanMasci等,“用于分层特征提取的堆叠卷积自编码器”,第21届国际人工神经网络会议论文集1(2011):52-59。

PascalVincent等,“使用去噪自编码器提取和组合稳健特征”,第25届国际机器学习会议论文集(2008):1096-1103。

PascalVincent等,“堆叠去噪自编码器:使用局部去噪标准在深度网络中学习有用的表示”,机器学习研究杂志11(2010):3371-3408。

DiederikKingma和MaxWelling,“自编码变分贝叶斯”,arXiv预印本arXiv:1312.6114(2013)。

变分自编码器实际上更通用;编码不限于高斯分布。

IanGoodfellow等,“生成对抗网络”,第27届神经信息处理系统国际会议论文集2(2014):2672-2680。

11MarioLucic等,“GAN是否平等?大规模研究”,第32届神经信息处理系统国际会议论文集(2018):698-707。

12AlecRadford等,“使用深度卷积生成对抗网络进行无监督表示学习”,arXiv预印本arXiv:1511.06434(2015)。

1MehdiMirza和SimonOsindero,“有条件生成对抗网络”,arXiv预印本arXiv:1411.1784(2014)。

1TeroKarras等,“用于改善质量、稳定性和变化的GAN的渐进增长”,国际学习表示会议论文集(2018)。

1变量的动态范围是其可能取的最高值和最低值之间的比率。

1TeroKarras等人,“基于风格的生成对抗网络架构”,arXiv预印本arXiv:1812.04948(2018)。

1JaschaSohl-Dickstein等人,“使用非平衡热力学进行深度无监督学习”,arXiv预印本arXiv:1503.03585(2015)。

2JonathanHo等人,“去噪扩散概率模型”(2020)。

21AlexNichol和PrafullaDhariwal,“改进的去噪扩散概率模型”(2021)。

22OlafRonneberger等人,“U-Net:用于生物医学图像分割的卷积网络”,arXiv预印本arXiv:1505.04597(2015)。

23RobinRombach,AndreasBlattmann等人,“使用潜在扩散模型进行高分辨率图像合成”,arXiv预印本arXiv:2112.10752(2021)。

THE END
1.中国教育监督网在沉浸式学习中普法,成都市关工委开展青少 教育三乱 更多>> 多名中小学校长被查!教育系统反腐受到关注 治理“三乱”,敲掉破坏营商环境的“拦路虎 隐性校园欺凌不可忽视,需始终加强防范 教育部:持续推进“四新”学科建设 近八成受访家长直言 “隐形变异”学科类校 http://zgjyjdw.cn/
2.监督学习下载app监督学习下载2024官方安卓版最新1.2版下载3、可以很好的提高用户们的专注力,提高自己的工作和学习效率。 监督学习更新日志: 日日夜夜的劳作只为你可以更快乐 嘛咪嘛咪哄~bug通通不见了! 华军小编推荐: 监督学习作为学习教育里面十分出色的软件,小编强力向您推荐,下载了就知道有多好。小编还为您准备了ABCmouse、作业帮直播课、作业帮在线拍照解题、研报客、驾http://www.onlinedown.net/soft/10111002.htm
3.怎么在互联网上匿名举报学校违法行为在网安界面找到并点击“网络违法犯罪举报”后的“申报”按钮,填写相关信息进行举报。 三、拨打投诉电话或写信举报 可以通过拨打当地教育局或相关政府部门的投诉电话进行举报。在电话中详细说明违法情况,并提供个人联系方式以便后续沟通。 还可以选择写信的方式进行举报,将信件寄送至相关教育部门或政府职能部门,并在信中详细https://agents.baidu.com/content/question/89446b0ae402f20eb386dac0
4.网络违法犯罪举报网站《网络违法犯罪举报网站》不具备现场、紧急报警的受理功能,如您的情况紧急,请立即拨打报警电话“110”。 我要举报 版权所有:公安部网络安全保卫局 京公网安备 11010102000001号京ICP备05070602号 建议您使用IE9及其以上版本,Edge、Chrome、FireFox和360等主流浏览器浏览本网站 https://cyberpolice.mps.gov.cn/wfjb/html/xxgg/20200825/4739.shtml
5.举报须知教育App是指以教职工、学生、家长为主要用户,以教育、学习为主要应用场景,服务于学校教学与管理、学生学习与生活以及家校互动等方面的互联网移动应用。 违法违规和违反政策文件的行为主要包括以下四类: (一)未获得备案或许可。 (二)存在互联网违法或不良信息。 https://app.eduyun.cn/bmp-web/rpt/rptReportGuide
6.机器学习——推荐系统和强化学习强化学习推荐系统3.3.3 学习状态值函数 3.3.4 算法改进:改进的神经网络架构 3.3.5 算法改进:E-贪婪策略(Epsilon-greedy策略) 3.3.6 算法改进:小批量和软更新 3.3.7 强化学习的状态 3.3.8 总结 第三课 第一周1.1 欢迎来到第三课_哔哩哔哩_bilibili (1)Unsupervised Learning:无监督学习 https://blog.csdn.net/qq_47941078/article/details/126216950
7.号称地表最强的神经机器翻译,为什么还是不尽如人意?用神经网络来进行机器翻译,首先解决的就是难以形成语句和必须依赖人工资料的问题。NMT的基本思想,是以每一个句子作为独立的神经元,从而打破基于短语的翻译障碍。并且翻译系统可以实现监督训练,不必完全依赖固定数据,这可以在专业领域等资料稀少的环节,获得更好的翻译结果 。 https://www.tmtpost.com/2785604.html
8.cvpr2021有的放矢,用图像分割与像素投票找在定义了真实地标分割图和真实方向投票图后,我们可以监督所提出的 VS-Net 预测这两个图。经过训练,VS-Net 可以预测查询图像的分割图和投票图,我们可以据此建立精确的二维到三维的对应关系,以实现稳健的视觉定位。 基于原型的在线学习三元监督投票分割网络: https://www.sensetime.com/cn/technology-new-detail/41164696?categoryId=48
9.2021届计算机科学方向毕业设计(论文)阶段性汇报针对图神经网络的两阶段式负载均衡策略 第一个阶段是在预训练阶段使用metis算法的多约束、层次化功能形成一个大概均衡的图划分;第二阶段是动态监控各设备的训练速度相应调整批次大小,是实时动态调整。 陆晗 面向时空预测网络的自监督持续学习方法 1.实现了持续学习的测试框架,并且验证了数据集KTH 和Moving-Mnist https://zhiyuan.sjtu.edu.cn/html/zhiyuan/announcement_view.php?id=3943
10.全栈金融工程师算法技术解构数据降维 特征选择与稀疏学习 半监督学习 隐马尔科夫模型 规则学习 强化学习 5、机器学习Spark RDD弹性分布式数据集,记录被分配到一个集群的多个节点上。 推荐引擎:推荐引擎,是主动发现用户当前或潜在需求,并主动推送信息给用户的信息网络。挖掘用户的喜好和需求,主动向用户推荐其感兴趣或者需要的对象。 https://www.jianshu.com/p/6c3888c2e846
11.银行安全生产专项整治行动工作总结(通用13篇)严格落实有关网络安全方面的各项规定,采取了多种措施防范安全有关事件的发生,总体上看,我行网络安全工作做得比较扎实,效果也比较好,近年来未发现泄密问题。 一、计算机涉密信息管理情况 今年以来,我行加强组织领导,强化宣传教育,落实工作责任,加强日常监督检查,将涉密计算机管理抓在手上。对于计算机磁介质(软盘、U盘、https://www.oh100.com/a/202210/5387610.html
12.2020年学刊第1期派驻纪检监察组要充分发挥近距离、全天候、常态化的优势,必须始终坚守监督这个基本职责、第一职责,不断提升监督实效,发挥好“探头”作用,把派驻监督这一制度优势转化为工作优势,把“三不”一体化推进落到实处,推动驻在单位营造风清气正的政治生态和干事创业的良好氛围。为此,驻市卫健委纪检监察组学习和实践相结合,https://www.smxdx.cn/xk/6939.html
13.学习笔记:神经网络学习算法腾讯云开发者社区主流的神经网络学习算法(或者说学习方式)可分为三大类:有监督学习(SupervisedLearning)、无监督学习(Unsupervised Learning)和强化学习(Reinforcement Learning),如下图所示。 注:有监督学习、无监督学习和强化学习并不是某一种特定的算法,而是一类算法的统称。 https://cloud.tencent.com/developer/article/1610502
14.详解基于图卷积的半监督学习(附代码)机器之心GCN可以找到标记的节点和没有标记的节点表示,并在训练中利用这两种信息来进行半监督学习。具体地,在半监督学习中,GCN通过聚合节点的标签和节点未标记的邻居来产生节点的特征表示。训练过程中,我们反向传播二进制交叉熵损失,以更新所有节点之间的共享权重。而这种损失取决于标记节点的特征表示,而该特征表示又取决于有https://www.jiqizhixin.com/articles/2019-03-07-10
15.头条文章对于表征学习,以双向生成式对抗网络(BiGAN)、深度聚集(DeepCluster)、逆约束自编码机插值(ACAI)和动量对比(MoCo)为代表的方法充分结合了深度神经网络强大的特征表示能力,利用无监督数据进行表征学习,为后续更复杂多样的监督学习任务打下了坚实的数据表征基础。https://card.weibo.com/article/m/show/id/2309404598738399395890
16.大学英语学习计划(精选17篇)新理念平台:任课教师可运用与课本配套的新理念平台布置各个单元的课文及生词预习、课后练习等相关学习计划,教师可在教学业务中的班级学习查看模块里查看并监督学生学习计划完成情况。 ⑵网络作业: 蓝鸽平台:任课教师可运用蓝鸽语言学科平台的作业系统模块布置《全新版大学英语综合教程1、2》教学单元的作业以及大学英语四级https://www.ruiwen.com/ziliao/english/4971798.html