十一、训练 Seq2Seq 模型
在上一章中,我们讨论了文档分类以及文档分类的一种特殊情况,称为情感分类。 这样做时,我们不得不谈论很多关于向量化的知识。
在本章中,我们将继续谈论解决 NLP 问题,但是除了分类之外,我们将生成新的单词序列。
我们将在本章介绍以下主题:
- 序列到序列模型
- 机器翻译
序列到序列模型
到目前为止,我们所研究的网络已经做了一些真正令人惊奇的事情。 但是它们都有一个很大的局限性:它们只能应用于输出具有固定且众所周知的大小的问题。
序列到序列模型能够将输入序列映射到具有可变长度的输出序列。
您可能还会看到术语序列到序列,甚至 Seq2Seq。 这些都是序列到序列模型的术语。
当使用序列到序列模型时,我们将引入一个序列并交换出一个序列。 这些序列的长度不必相同。 序列到序列模型使我们能够学习输入序列和输出序列之间的映射。
序列到序列模型可能在许多应用中有用,我们接下来将讨论这些应用。
序列到序列模型的应用
序列到序列模型具有许多实际应用。
也许最实际的应用是机器翻译。 我们可以使用机器翻译将一种语言的短语作为输入,并输出另一种语言的短语。 机器翻译是我们越来越依赖的一项重要服务。 得益于计算机视觉和机器翻译的进步,我们可以听不懂的语言,或者用不懂的语言查看标志,并且几乎可以立即在智能手机上获得不错的翻译。 序列到序列的网络确实使我们非常接近道格拉斯·亚当(Douglas Adam)想象的《银河系漫游指南》中的通天鱼。
问答也可以全部或部分通过序列到序列模型来完成,在这里我们可以将问题想象为输入序列,将答案想象为输出序列。 回答问题最普遍的应用是聊天。 如果您通过呼叫中心为企业提供支持,则每天会有成千上万甚至数百万个问题/答案对通过电话传递。 对于序列到序列聊天机器人来说,这是完美的训练。
我们可以利用这种问答方式的多种细微形式。 每天,我收到大约 34 亿封电子邮件。 其中,我可能只需要阅读 20-30(这是一个分类任务)。 但是,我对这些电子邮件的回复很少新颖。 我几乎可以肯定地创建一个序列到序列的网络,该网络可以为我写电子邮件,或者至少起草回复。 我认为我们已经开始看到这种行为已经内置在我们最喜欢的电子邮件程序中,并且肯定会出现更加全自动的响应。
序列到序列网络的另一个重要用途是自动文本摘要。 想象一下一组研究论文或大量期刊文章。 所有这些论文可能都有摘要。 这只是另一个翻译问题。 给定一些论文,我们可以使用序列到序列网络生成摘要。 网络可以学习以这种方式总结文档。
在本章的后面,我们将实现一个序列到序列的网络来进行机器翻译。 不过,在进行此操作之前,让我们了解一下这种网络架构是如何工作的。
序列到序列模型架构
理解序列到序列模型架构的关键是要理解该架构是为了允许输入序列的长度与输出序列的长度而变化的。 然后可以使用整个输入序列来预测长度可变的输出序列。
为此,网络被分为两个独立的部分,每个部分都包含一个或多个 LSTM 层,这些层负责一半的任务。 如果您想对其操作进行复习,我们在第 9 章“从头开始训练 RNN”中讨论了 LSTM。 我们将在以下各节中了解这两个部分。
编码器和解码器
序列到序列模型由两个单独的组件组成,一个编码器和一个解码器:
- 编码器:模型的编码器部分采用输入序列,并返回输出和网络的内部状态。 我们并不在乎输出。 我们只想保留编码器的状态,即输入序列的内存。
- 解码器:然后,模型的解码器部分将来自编码器的状态(称为上下文或条件)作为输入。 然后,根据前一个时间步长的输出,可以预测每个时间步长的目标序列。
然后,编码器和解码器如下图所示一起工作,获取输入序列并生成输出序列。 如您所见,我们使用特殊字符表示序列的开始和结束。
我们知道,一旦序列字符的结尾(我称之为<EOS>
)结束,就停止生成输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcfdVbNI-1681567952195)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/2a32ed66-4641-4bbf-9f24-77083ee3e768.png)]
尽管此示例涵盖了机器翻译,但是序列到序列学习的其他应用却以相同的方式工作。
字符与文本
可以在字符级别或单词级别建立序列到序列模型。 单词级序列到序列模型将单词作为输入的原子单位,而字符级模型将字符作为输入的原子单位。
那么,您应该使用哪个呢? 通常,最好的结果是从单词级模型中获得的。 就是说,预测序列中最可能出现的下一个单词需要softmax
层与问题的词汇量一样宽。 这导致了非常广泛的,高度尺寸的问题。
字符级模型要小得多。 字母表中有 26 个字母,但大约有 171,000 个英文单词是常用的。
对于本章中提出的问题,我将使用字符级模型,因为我重视您的 AWS 预算。 转换为单词非常简单,其中大部分复杂性都在数据准备中,这是留给读者的练习。
监督强迫
如上图所示,当预测序列y[t(n)]
某个位置的输出时,我们使用y[t(n-1)]
作为 LSTM 的输入。 然后,我们使用此时间步骤的输出来预测y[t(n 1)]
。
训练中这样做的问题是,如果y[t(n-1)]
错误,则y[t(n)]
将更加错误。 错误不断增加的链条会使事情变得非常缓慢。
解决该问题的一个显而易见的解决方案是将每个时间步长的每个序列预测替换为该时间步长的实际正确序列。 因此,我们将使用训练集中的实际值,而不是对y[t(n-1)]
使用 LSTM 预测。
通过使用这个概念,我们可以促进模型的训练过程,这恰好被称为监督强迫。
教师强迫有时会使我们的模型难以可靠地生成训练中看不到的序列,但总的来说,该技术可能会有所帮助。
注意
注意是可以在序列到序列模型中实现的另一种有用的训练技巧。 注意使解码器在输入序列的每个步骤中都能看到隐藏状态。 这使网络可以专注于(或关注)特定的输入,这可以加快训练速度并可以提高模型的准确率。 注意通常是一件好事。 但是,在撰写本文时,Keras 尚未内置注意力。尽管如此,Keras 目前确实有一个拉取请求正在等待自定义注意层。 我怀疑很快就会在 Keras 中建立对关注的支持。
翻译指标
知道翻译是否良好很难。 机器翻译质量的通用度量标准称为双语评估研究(BLEU),它最初是由 Papineni 等人在《BLEU:一种自动评估机器翻译的方法》中创建的。 BLEU 是基于 ngram 的分类精度的改进应用。 如果您想使用 BLEU 来衡量翻译质量,TensorFlow 团队已经发布了一个脚本,该脚本可以根据给定的地面真实翻译和机器预测翻译的语料来计算 BLEU 分数。 您可以在这里找到该脚本。
机器翻译
Je ne parle pasfrançais
,那就是你怎么说我不会说英语的法语。 大约两年前,我发现自己在巴黎,几乎不会说法语。 在我去之前,我已经看过一本书,听过一些 DVD,但是即使经过几个月的练习,我对法语的掌握还是很可悲的。 然后,在旅途的第一个早晨,我醒来,走进附近的boulangerie
(法国或法式面包店)吃早餐和早晨咖啡。 我说Bonjour, parlez-vous anglais?
,他们一点也不讲英语,或者也许他们正在享受我的奋斗。 无论哪种方式,当我的早餐取决于我对法语的掌握时,我都会比过去更有动力去争取Je voudrais un pain au chocolat
(翻译:我想要其中一种美味的巧克力面包)。 在最终成本函数(我的胃)的驱动下,我很快学会了在英语序列和法语序列之间进行映射。
在本案例研究中,我们将教计算机讲法语。 在几个小时的训练中,该模型将比我说法语更好。 考虑一下,这真是太神奇了。 我将训练一台计算机来执行我自己无法完成的任务。 当然,也许您确实会说法语,但这并不会给您留下深刻的印象,在这种情况下,我将美国著名演员亚当·桑德勒(Adam Sandler)称为比利·麦迪逊(Billy Madison):好吧,对我来说很难,所以退缩!
该示例的大部分来自于弗朗索瓦·乔勒(Francois Chollet)的博客文章,标题为《序列到序列学习的十分钟介绍》。 尽管我怀疑自己是否可以改进这项工作,但我希望使用本示例的目的是花一点点多一点的时间看一下序列到序列的网络,以使您掌握实现自己的所有知识。
与往常一样,本章的代码可以在本书的 Git 存储库中的Chapter11
下找到。 您可以在这个页面中找到此示例所需的数据,该文件将存档许多双语句子对的数据集,我们将在后面详细讨论。 我要使用的文件是 fra-eng.zip 。 这是英语/法语句子对的集合。 如果需要,您可以轻松选择其他语言,而无需进行太多修改。
在本案例研究中,我们将构建一个网络,该网络可以在给定一些英语句子的情况下学习法语句子。 这将是一个具有老师强迫作用的字符级序列到序列模型。
我希望最终得到的是看起来很像翻译服务的东西,您可以在网上找到它或下载到手机上。
了解数据
我们正在使用的数据是一个文本文件。 每行都有一个英文短语及其法语翻译,并用一个选项卡分隔,如以下代码所示:
代码语言:javascript复制Ignore Tom. Ignorez Tom.
(我不确定Tom
对数据集的作者做了什么…)
通常,每行英语翻译都有重复的法语翻译行。 当有多种常用方法翻译英语短语时,会发生这种情况。 看下面的代码例如:
代码语言:javascript复制Go now. Va, maintenant.
Go now. Allez-y maintenant.
Go now. Vas-y maintenant.
由于我们正在构建一个字符级序列到序列模型,因此需要将数据加载到内存中,然后对每个输入和输出在字符级进行热编码。 那是困难的部分。 让我们接下来做。
加载数据
加载此数据涉及很多工作。 阅读本文时,您可能想参考代码块。
以下代码中的第一个for
循环将遍历整个输入文件或调用load_data()
时指定的一些样本。 我这样做是因为您可能没有 RAM 来加载整个数据集。 多达 10,000 个示例,您可能会获得良好的结果; 但是,多多益善。
当我们逐行浏览输入文件时,我们一次要执行几项操作:
- 我们将每个法语翻译包装在
't'
中,以开始该短语,并在'n'
中,以结束它。 这对应于我在序列到序列图中使用的<SOS>
和<EOS>
标签。 当我们要生成翻译序列时,这将允许我们使用't'
作为输入来为解码器设定种子。 - 我们将每一行分为英语输入和其各自的法语翻译。 这些存储在列表
input_texts
和target_texts
中。 - 最后,我们将输入文本和目标文本的每个字符添加到一个集合中。 这些集称为
input_characters
和target_characters
。 当需要对短语进行热编码时,我们将使用这些集合。
循环完成后,我们会将字符集转换为排序列表。 我们还将创建名为num_encoder_tokens
和num_decoder_tokens
的变量,以保存每个列表的大小。 稍后我们也将需要这些以进行单热编码。
为了将输入和目标输入矩阵,我们需要像上一章一样,将短语填充到最长短语的长度。 为此,我们需要知道最长的短语。 我们将其存储在max_encoder_seq_length
和max_decoder_seq_length
中,如以下代码所示:
def load_data(num_samples=50000, start_char='t', end_char='n', data_path='data/fra-eng/fra.txt'):
input_texts = []
target_texts = []
input_characters = set()
target_characters = set()
lines = open(data_path, 'r', encoding='utf-8').read().split('n')
for line in lines[: min(num_samples, len(lines) - 1)]:
input_text, target_text = line.split('t')
target_text = start_char target_text end_char
input_texts.append(input_text)
target_texts.append(target_text)
for char in input_text:
if char not in input_characters:
input_characters.add(char)
for char in target_text:
if char not in target_characters:
target_characters.add(char)
input_characters = sorted(list(input_characters))
target_characters = sorted(list(target_characters))
num_encoder_tokens = len(input_characters)
num_decoder_tokens = len(target_characters)
max_encoder_seq_length = max([len(txt) for txt in input_texts])
max_decoder_seq_length = max([len(txt) for txt in target_texts])
print('Number of samples:', len(input_texts))
print('Number of unique input tokens:', num_encoder_tokens)
print('Number of unique output tokens:', num_decoder_tokens)
print('Max sequence length for inputs:', max_encoder_seq_length)
print('Max sequence length for outputs:', max_decoder_seq_length)
return {'input_texts': input_texts, 'target_texts': target_texts,
'input_chars': input_characters, 'target_chars':
target_characters, 'num_encoder_tokens': num_encoder_tokens,
'num_decoder_tokens': num_decoder_tokens,
'max_encoder_seq_length': max_encoder_seq_length,
'max_decoder_seq_length': max_decoder_seq_length}
加载数据后,我们将在字典中返回所有这些信息,这些信息可以传递给一个函数,该函数将对每个短语进行热编码。 让我们接下来做。
单热编码
在此函数中,我们将使用刚刚构建的字典,并对每个短语的文本进行热编码。
一旦完成,我们将剩下三个字典。 它们每个的尺寸为[文本数 * 最大序列长度 * 标记]
。 如果您停顿一下,回想一下第 10 章“使用单词嵌入从零开始训练 LSTM”的更简单的时间,您会发现这确实与我们在其他 NLP 模型中使用的相同,我们在输入端完成它。 我们将使用以下代码定义单热编码:
def one_hot_vectorize(data):
input_chars = data['input_chars']
target_chars = data['target_chars']
input_texts = data['input_texts']
target_texts = data['target_texts']
max_encoder_seq_length = data['max_encoder_seq_length']
max_decoder_seq_length = data['max_decoder_seq_length']
num_encoder_tokens = data['num_encoder_tokens']
num_decoder_tokens = data['num_decoder_tokens']
input_token_index = dict([(char, i) for i, char in
enumerate(input_chars)])
target_token_index = dict([(char, i) for i, char in
enumerate(target_chars)])
encoder_input_data = np.zeros((len(input_texts),
max_encoder_seq_length, num_encoder_tokens), dtype='float32')
decoder_input_data = np.zeros((len(input_texts),
max_decoder_seq_length, num_decoder_tokens), dtype='float32')
decoder_target_data = np.zeros((len(input_texts),
max_decoder_seq_length, num_decoder_tokens), dtype='float32')
for i, (input_text, target_text) in enumerate(zip(input_texts,
target_texts)):
for t, char in enumerate(input_text):
encoder_input_data[i, t, input_token_index[char]] = 1.
for t, char in enumerate(target_text):
# decoder_target_data is ahead of decoder_input_data by one
timestep
decoder_input_data[i, t, target_token_index[char]] = 1.
if t > 0:
# decoder_target_data will be ahead by one timestep
# and will not include the start character.
decoder_target_data[i, t - 1, target_token_index[char]] = 1.
data['input_token_index'] = input_token_index
data['target_token_index'] = target_token_index
data['encoder_input_data'] = encoder_input_data
data['decoder_input_data'] = decoder_input_data
data['decoder_target_data'] = decoder_target_data
return data
我们在此代码中创建了三个训练向量。 在继续之前,我想确保我们了解以下所有向量:
encoder_input_data
是形状为number_of_pairs
,max_english_sequence_length
,number_of_english_characters
的 3D 矩阵。decoder_input_data
是形状(number_of_pairs
,max_french_sequence_length
,number_of_french_characters
)的 3d 矩阵。decoder_output_data
与decoder_input_data
相同,仅向前移了一个时间步。 这意味着decoder_input_data[:, t 1, :]
等于decoder_output_data[:, t, :]
。
前面的每个向量都是字符层上整个短语的一个热编码表示。 这意味着,如果我们输入的短语是 Go! 向量的第一步是为文本中每个可能的英文字符包含一个元素。 除g
设置为 1 以外,其他每个元素都将设置为0
。
我们的目标是使用encoder_input_data
和decoder_input
数据作为输入特征,训练序列至序列模型来预测decoder_output_data
。
终于完成了数据准备,因此我们可以开始构建序列到序列的网络架构。
用于训练的网络架构
在此示例中,我们实际上将使用两种单独的架构,一种用于训练,另一种用于推理。 我们将从推理模型训练中使用训练过的层。 虽然实际上我们为每种架构使用了相同的部分,但是为了使事情更清楚,我将分别展示每个部分。 以下是我们将用来训练网络的模型:
代码语言:javascript复制encoder_input = Input(shape=(None, num_encoder_tokens), name='encoder_input')
encoder_outputs, state_h, state_c = LSTM(lstm_units, return_state=True,
name="encoder_lstm")(encoder_input)
encoder_states = [state_h, state_c]
decoder_input = Input(shape=(None, num_decoder_tokens), name='decoder_input')
decoder_lstm = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")
decoder_outputs, _, _ = decoder_lstm(decoder_input, initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax',
name='softmax_output')
decoder_output = decoder_dense(decoder_outputs)
model = Model([encoder_input, decoder_input], decoder_output)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
如果我们将放大编码器,则会看到相当标准的 LSTM。 不同之处在于,我们从编码器(return_state=True
)获取状态,如果将 LSTM 连接到密集层,通常不会这样做。 这些状态是我们将在encoder_states
中捕获的状态。 我们将使用它们为解码器提供上下文或条件。
在解码器方面,我们设置的decoder_lstm
与我们先前构建 Keras 层的方式略有不同,但实际上只是语法略有不同。
看下面的代码:
代码语言:javascript复制decoder_lstm = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")
decoder_outputs, _, _ = decoder_lstm(decoder_input, initial_state=encoder_states)
其功能与以下代码相同:
代码语言:javascript复制decoder_outputs, _, _ = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")(decoder_input, initial_state=encoder_states)
我这样做的原因在推理架构中将变得显而易见。
请注意,解码器将编码器的隐藏状态作为其初始状态。 然后将解码器输出传递到预测decoder_output_data
的softmax
层。
最后,我们将定义训练模型,我将其创造性地称为model
,该模型将encoder_input_data
和decoder_input
数据作为输入并预测decoder_output_data
。
用于推理的网络架构
为了在给定输入序列的情况下预测整个序列,我们需要稍微重新安排一下架构。 我怀疑在 Keras 的未来版本中,这将变得更简单,但是从今天起这是必需的步骤。
为什么需要有所不同? 因为我们没有推断的decoder_input_data
教师向量。 我们现在独自一人。 因此,我们将必须进行设置,以便我们不需要该向量。
让我们看一下这种推理架构,然后逐步执行代码:
代码语言:javascript复制encoder_model = Model(encoder_input, encoder_states)
decoder_state_input_h = Input(shape=(lstm_units,))
decoder_state_input_c = Input(shape=(lstm_units,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
decoder_input, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
[decoder_input] decoder_states_inputs,
[decoder_outputs] decoder_states)
首先,我们从构建编码器模型开始。 该模型将采用一个输入序列,并返回我们在先前模型中训练过的 LSTM 的隐藏状态。
然后,解码器模型具有两个输入,即h
和c
隐藏状态,这些状态限制了其从编码器模型派生的输出。 我们统称为decoder_states_inputs
。
我们可以从上面重用decoder_lstm
; 但是,这次我们不会丢弃状态state_h
和state_c
。 我们将把它们与目标的softmax
预测一起作为网络输出传递。
现在,当我们推断出一个新的输出序列时,我们可以在预测第一个字符之后获得这些状态,然后将它们通过softmax
预测传递回 LSTM,以便 LSTM 可以预测另一个字符。 我们将重复该循环,直到解码器生成一个'n'
信号为止,该信号已到达<EOS>
。
我们将很快看一下推理代码。 现在,让我们看看如何训练和序列化此模型集合。
放在一起
按照本书的传统,我将在这里向您展示该模型的整个架构如何融合在一起:
代码语言:javascript复制def build_models(lstm_units, num_encoder_tokens, num_decoder_tokens):
# train model
encoder_input = Input(shape=(None, num_encoder_tokens),
name='encoder_input')
encoder_outputs, state_h, state_c = LSTM(lstm_units,
return_state=True, name="encoder_lstm")(encoder_input)
encoder_states = [state_h, state_c]
decoder_input = Input(shape=(None, num_decoder_tokens),
name='decoder_input')
decoder_lstm = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")
decoder_outputs, _, _ = decoder_lstm(decoder_input,
initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax',
name='softmax_output')
decoder_output = decoder_dense(decoder_outputs)
model = Model([encoder_input, decoder_input], decoder_output)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
encoder_model = Model(encoder_input, encoder_states)
decoder_state_input_h = Input(shape=(lstm_units,))
decoder_state_input_c = Input(shape=(lstm_units,))
decoder_states_inputs = [decoder_state_input_h,
decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
decoder_input, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
[decoder_input] decoder_states_inputs,
[decoder_outputs] decoder_states)
return model, encoder_model, decoder_model
请注意,我们将在此处返回所有三个模型。 训练完训练模型后,我将使用keras model.save()
方法序列化这三个方法。
训练
我们终于准备好训练我们的序列到序列网络。 以下代码首先调用我们所有的数据加载函数,创建回调,然后拟合模型:
代码语言:javascript复制data = load_data()
data = one_hot_vectorize(data)
callbacks = create_callbacks("char_s2s")
model, encoder_model, decoder_model = build_models(256, data['num_encoder_tokens'], data['num_decoder_tokens'])
print(model.summary())
model.fit(x=[data["encoder_input_data"], data["decoder_input_data"]],
y=data["decoder_target_data"],
batch_size=64,
epochs=100,
validation_split=0.2,
callbacks=callbacks)
model.save('char_s2s_train.h5')
encoder_model.save('char_s2s_encoder.h5')
decoder_model.save('char_s2s_decoder.h5')
您会注意到,我以前没有像通常那样定义验证或测试集。 这次,按照博客文章中给出的示例,我将让 Keras 随机选择 20% 的数据作为验证,这在示例中可以很好地工作。 如果要使用此代码实际进行机器翻译,请使用单独的测试集。
训练模型适合后,我将保存所有三个模型,并将它们再次加载到为推理而构建的单独程序中。 我这样做是为了使代码保持简洁,因为推理代码本身非常复杂。
让我们来看看这个模型的 100 个周期的模型训练:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eL4Bg6fA-1681567952196)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/d6844cf9-3d55-4e3f-9722-4c54b3be05b5.png)]
如您所见,我们在第 20 个周期开始过拟合。虽然损失持续减少,但val_loss
却在增加。 在这种情况下,模型检查指向可能无法正常工作,因为在训练结束之前我们不会序列化推理模型。 因此,理想情况下,我们应该再训练一次,将训练的周期数设置为略大于 TensorBoard 中观察到的最小值。
推理
现在我们有了训练有素的模型,我们将实际生成一些翻译。
总体而言,推理步骤如下:
- 加载数据并再次向量化(我们需要字符到索引的映射以及一些转换进行测试)
- 使用字符对字典进行索引,我们将创建字符字典的反向索引,因此一旦我们预测了正确的字符,我们就可以从数字返回到字符
- 选择一些输入序列进行翻译,然后通过编码器运行,获取状态
- 将状态和
<SOS>
字符't'
发送到解码器。 - 循环,获取每个下一个字符,直到解码器生成
<EOS>
或'n'
加载数据
我们可以从训练脚本中导入load_data
和one_hot_vectorize
函数,以相同的方式调用这些方法,如以下代码所示:
data = load_data()
data = one_hot_vectorize(data)
创建反向索引
解码器将预测正确字符的索引,该索引将是解码器的softmax
输出的argmax
。 我们将需要能够将索引映射到字符。 您可能还记得,数据字典中已经有一个字符到索引的映射,所以我们只需要反转它即可。 逆转字典非常简单,如下所示:
def create_reverse_indicies(data):
data['reverse_target_char_index'] = dict(
(i, char) for char, i in data["target_token_index"].items())
return data
然后,我们可以如下调用此函数:
代码语言:javascript复制data = create_reverse_indicies(data)
载入模型
我们可以使用keras.models.load_model
加载保存在训练脚本中的模型。 我创建了此助手来完成该任务。 我们将使用以下代码加载模型:
def load_models():
model = load_model('char_s2s.h5')
encoder_model = load_model('char_s2s_encoder.h5')
decoder_model = load_model('char_s2s_decoder.h5')
return [model, encoder_model, decoder_model]
我们可以调用以下函数来加载所有三个模型:
代码语言:javascript复制model, encoder_model, decoder_model = load_models()
翻译序列
现在,我们准备对一些输入序列进行采样并进行翻译。 在示例代码中,我们使用前 100 个双语对进行翻译。 一个更好的测试可能是在整个空间中随机抽样,但是我认为这个简单的循环说明了这一过程:
代码语言:javascript复制for seq_index in range(100):
input_seq = data["encoder_input_data"][seq_index: seq_index 1]
decoded_sentence = decode_sequence(input_seq, data, encoder_model,
decoder_model)
print('-')
print('Input sentence:', data['input_texts'][seq_index])
print('Correct Translation:', data['target_texts']
[seq_index].strip("tn"))
print('Decoded sentence:', decoded_sentence)
在这段代码中,我们将encoder_input_data
的一个观察值用作decode_sequence
的输入。 decode_sequence
将传回解码器认为正确翻译的序列。 我们还需要将其传递给编码器和解码器模型,以便能够完成其工作。下面的翻译更加有趣,因为学习的短语未与
有了解码器预测后,就可以将其与输入和正确的转换进行比较。
当然,我们还没有完成,因为我们还没有探讨decode_sequence
方法的工作方式。 接下来。
解码序列
解码器需要执行以下两项操作:
- 来自编码器的状态。
- 输入信号开始预测的翻译。 我们将在一个热向量中向其发送
't'
,因为这是我们的<SOS>
字符。
为了获得编码器状态,我们只需要使用以下代码将要翻译的短语的向量化版本发送到编码器:
代码语言:javascript复制states_value = encoder_model.predict(input_seq)
为了启动解码器,我们还需要一个包含<SOS>
字符的热向量。 这段代码将我们带到了那里:
target_seq = np.zeros((1, 1, data['num_decoder_tokens']))
target_seq[0, 0, data['target_token_index']['t']] = 1.
现在,我们准备使用以下代码设置一个解码器循环,该循环将生成我们的翻译短语:
代码语言:javascript复制stop_condition = False
decoded_sentence = ''
while not stop_condition:
output_tokens, h, c = decoder_model.predict(
[target_seq] states_value)
sampled_token_index = np.argmax(output_tokens[0, -1, :])
sampled_char = data["reverse_target_char_index"][sampled_token_index]
decoded_sentence = sampled_char
if (sampled_char == 'n' or
len(decoded_sentence) > data['max_decoder_seq_length']):
stop_condition = True
target_seq = np.zeros((1, 1, data['num_decoder_tokens']))
target_seq[0, 0, sampled_token_index] = 1.
states_value = [h, c]
首先要注意的是,我们一直循环到stop_condition = True
。 这在解码器生成'n'
时发生。
第一次通过循环,我使用<SOS>
向量和我们在循环外部创建的编码器的状态调用了decoder_model
的预测方法。
当然,output_tokens
将包含解码器可以预测的每个字符的softmax
预测。 通过取output_tokens
的argmax
,我们将获得最大softmax
值的索引。 方便地,我可以使用之前创建的reverse_target_char_index
将其转换回关联的字符,这是一个在索引和字符之间转换的字典。
接下来,我们将该字符附加到decode_sequence
字符串。
接下来,我们可以检查该字符是否为'n'
并触发stop_condition
为True
。
最后,我们将创建一个新的target_seq
,其中包含解码器生成的最后一个字符,以及一个包含解码器隐藏状态的列表。 现在,我们准备再次重复循环。
我们的解码器将遵循此过程,直到生成解码序列为止。
翻译示例
只是为了好玩,我在这里提供了一些尝试的翻译。 所有这些都来自训练集的前面,这意味着我正在对training
数据集进行预测,因此这些转换可能会使模型看起来比实际更好。
我们的第一版翻译使您对我们的期望有所了解,并且该网络做得很好:
输入句子:Help!
正确翻译:À l'aide!
解码后的句子:À l'aide!
后续的翻译更加有趣,因为学习的短语未与任何训练短语相关联。 短语Vas-tu immédiatement!
转换为类似You go immediately
的字词。这非常相似,甚至可能正确:
输入句子:Go on.
正确的翻译: Poursuis.
解码后的句子: Vas-tu immédiatement!
输入句子:Go on.
正确的翻译:Continuez.
解码后的句子: Vas-tu immédiatement!
输入句子:Go on.
正确的翻译: Poursuivez.
解码后的句子: Vas-tu immédiatement!
当然,有很多方法可以说相同的事情,这使得网络变得更加困难:
输入句子:Come on!
正确的翻译: Allez !
解码后的句子: Allez !
输入句子:Come on!
正确的翻译: Allez !
解码后的句子: Allez !
输入句子:Come on.
正确的翻译:Viens!
解码后的句子: Allez!
输入句子:Come on.
正确的翻译:Venez!
解码后的句子: Allez!
总结
在本章中,我们介绍了序列到序列模型的基础知识,包括它们如何工作以及如何使用它们。 希望我们已经向您展示了一个功能强大的工具,可用于机器翻译,问题解答和聊天应用。
如果您已经做到了,那就好。 您已经看到了很多深度学习的应用,并且发现自己正处于深层神经网络应用的最先进的钟形曲线的右边。
在下一章中,我将向您展示另一个高级主题的示例,即深度强化学习或深度 Q 学习,并向您展示如何实现自己的深度 Q 网络。
在此之前,请放松!
十二、深度强化学习
在本章中,我们将以略有不同的方式使用深度神经网络。 我们将要构建一个智能体,而不是预测一个类的成员,估计一个值,甚至生成一个序列。 尽管机器学习和人工智能这两个术语经常互换使用,但在本章中,我们将讨论人工智能作为一种可以感知其环境的智能体,并采取步骤在该环境中实现某些目标。
想象一个可以玩象棋或围棋之类策略游戏的特工。 构建神经网络来解决此类游戏的一种非常幼稚的方法可能是使用一种网络架构,在该架构中,我们对每个可能的棋盘/棋子组合进行热编码,然后预测每个可能的下一个动作。 尽管该网络庞大而复杂,但可能做得并不好。 要很好地玩国际象棋,您不仅要考虑下一步,而且还要考虑接下来的步伐。 在不确定的情况下,我们的智能体将需要考虑给定未来行动的最佳下一步行动。
这是一个令人兴奋的领域。 正是在智能体领域,研究人员才朝着人工智能或强大的 AI 迈进,这是创建可以执行人类任何智力任务的智能体的崇高目标。 强 AI 的概念通常与弱 AI 形成对比,弱 AI 是解决某些单个任务或应用的能力。
对于作者(我)和读者(您)而言,本章将是一个挑战,因为强化学习理应拥有自己的书,并且需要总结在数学,心理学和计算机科学方面所做的工作。 因此,请原谅快速参考处理,并知道我在为您提供足够的信息,而在接下来的部分中将不多说。
强化学习,马尔可夫决策过程和 Q 学习是智能体的基础,我们接下来将讨论这些内容。
我们将在本章中讨论以下主题:
- 强化学习概述
- Keras 强化学习框架
- 在 Keras 中建立强化学习智能体
强化学习概述
强化学习基于智能体的概念。 智能体通过观察某种状态然后采取行动来与其环境进行交互。 当智能体采取行动在状态之间移动时,它会以奖励信号的形式接收有关其行动良好性的反馈。 这个奖励信号是强化学习中的强化。 这是一个反馈循环,智能体可以使用它来学习其选择的优势。 当然,奖励可以是正面的,也可以是负面的(惩罚)。
想象一下,无人驾驶汽车是我们正在制造的智能体。 在行驶过程中,它不断收到动作的奖励信号。 留在车道内可能会产生积极的报酬,而在行人上奔跑可能会给智能体带来非常消极的报酬。 当面临选择留在行人或撞到行人的选择时,智能体将希望学会以避开行人为代价,避开行人,损失车道线奖励,以避免更大的行人碰撞惩罚。
强化学习概念的核心是状态,行为和奖励的概念。 我已经讨论过奖励,所以让我们谈谈行动和状态。 动作是智能体在观察到某种状态时可以执行的操作。 如果我们的特工正在玩一个简单的棋盘游戏,那么该动作将由该特工轮到它来做。 然后转弯就是座席的状态。 为了解决这些问题,我们将在这里着眼于一个智能体可以采取的行动始终是有限的和离散的。 下图说明了此概念:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Io0Isbip-1681567952197)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/dea6219b-2da7-405e-877e-c72c52b20210.png)]
此反馈循环的一个步骤可以用数学方式表示为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJCqspQm-1681567952197)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/e39144a7-9a97-4e55-8500-6f6a64fc8537.png)]
动作会在原始状态s
和下一个状态s'
的智能体之间进行转换,智能体会在其中获得一些奖励r
。 智能体选择动作的方式称为智能体策略,通常称为pi
。
强化学习的目的是找到一系列动作,使行动者从一个状态到另一个状态,并获得尽可能多的报酬。
马尔可夫决策过程
我们构筑的这个世界恰好是马尔可夫决策过程(MDP),它具有以下属性:
- 它具有一组有限的状态,
S
- 它具有一组有限的动作
A
P[a](s, s')
是采取行动A
将在状态s
和状态s'
之间转换的概率R[a](s, s')
是s
和s'
之间过渡的直接奖励。γ ∈ [0, 1]
是折扣因子,这是我们相对于当前奖励对未来奖励的折扣程度(稍后会详细介绍)
一旦我们有了确定每个状态要采取的操作的策略函数pi
,MDP 就解决了,成为了马尔可夫链。
好消息是,有一个警告就完全有可能完美解决 MDP。 需要注意的是,必须知道 MDP 的所有回报和概率。 事实证明,这种警告相当重要,因为在大多数情况下,由于智能体的环境混乱或至少不确定,因此智能体不知道所有的回报和状态更改概率。
Q 学习
想象一下,我们有一些函数Q
,可以估计出采取行动的回报:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TxWaJdVI-1681567952198)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/c0927993-35eb-4977-a4bc-ccd338f9fc95.png)]
对于某些状态s
以及动作a
,它会根据状态为该动作生成奖励。 如果我们知道环境带来的所有回报,那么我们就可以遍历Q
并选择能够为我们带来最大回报的行动。 但是,正如我们在上一节中提到的那样,我们的智能体不知道所有的奖励状态和状态概率。 因此,我们的Q
函数需要尝试近似奖励。
我们可以使用称为 Bellman 公式的递归定义的Q
函数来近似此理想的Q
函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SwjNaNGu-1681567952198)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/92cb692e-ef61-427e-bd69-0e698f08f007.png)]
在这种情况下, r[0]
是下一个动作的奖励,然后在下一个动作上(递归地)递归使用Q
函数确定该行动的未来奖励。 为此,我们将γ
作为相对于当前奖励的未来奖励的折扣。 只要伽玛小于 1,它就不会使我们的奖励序列变得无限大。 更明显地,与当前状态下的相同奖励相比,未来状态下的奖励的值要低。 具体来说,如果有人今天给您 100 美元,明天给您 100 美元,您应该立即拿走这笔钱,因为明天不确定。
如果我们尽最大的努力让我们的智能体经历每种可能的状态转换,并使用此函数来估计我们的报酬,我们将得出我们试图近似的理想Q
函数。
无限状态空间
对Q
函数的讨论使我们陷入了传统强化学习的重要局限。 您可能还记得,它假设状态空间是有限且离散的。 不幸的是,这不是我们生活的世界,也不是我们的智能体在很多时候会发现自己的环境。 考虑一个可以打乒乓球的经纪人。 状态空间的重要组成部分是乒乓球的速度,它当然不是离散的。 像我们不久将要看到的那样,可以看到的特工会看到一个图像,该图像是一个很大的连续空间。
我们讨论的 Bellman 方程将要求我们在状态与状态之间转移时保持经验奖励的大矩阵。 但是,当面对连续的状态空间时,这是不可能的。 可能的状态本质上是无限的,我们不能创建无限大小的矩阵。
幸运的是,我们可以使用深度神经网络来近似Q
函数。 这可能不会让您感到惊讶,因为您正在阅读一本深度学习书,因此您可能猜测深度学习必须在某个地方出现。 就是那个地方
深度 Q 网络
深层 Q 网络(DQN)是近似Q
函数的神经网络。 他们将状态映射到动作,并学会估计每个动作的Q
值,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-igSxCHl8-1681567952199)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/33ff32cb-b608-417d-afd0-60e35ff0f6d4.png)]
我们可以使用深度神经网络作为函数来逼近该矩阵,而不是尝试存储一个无限大的矩阵,而是将奖励从连续状态空间映射到动作。 这样,我们可以将神经网络用作智能体的大脑。 但这一切都导致我们提出一个非常有趣的问题。 我们如何训练这个网络?
在线学习
当我们的智能体通过采取行动从一个状态过渡到另一个状态时,它会得到奖励。 智能体可以通过使用每个状态,动作和奖励作为训练输入来在线学习。 在执行每个操作后,该智能体将更新其神经网络权重,并希望在此过程中变得更聪明。 这是在线学习的基本思想。 智能体就像您和我一样,不断学习。
这种朴素的在线学习的缺点有些明显,有两个方面:
- 经历之后,我们就会放弃经验。
- 我们所经历的经验彼此高度相关,我们将过度适应最新的经验。 有趣的是,这也是人类遭受的苦难,称为可用性偏差。
我们可以通过使用内存和经验重放来解决这些问题。
记忆和经验重放
当我们引入有限存储空间的概念时,可以找到针对这两个问题的巧妙解决方案,该存储空间用于存储智能体具有的一组经验。 在每个状态下,我们都可以借此机会记住状态,行动和奖励。 然后,智能体可以通过从内存中采样一个随机小批量并使用该小批量更新 DQN 权重,定期重放这些经验。
这种重放机制使智能体能够以一般的方式从更长远的经验中学习,因为它是从内存中的那些经验中随机采样的,而不是仅使用最近的经验来更新整个网络。
利用与探索
通常,我们希望智能体遵循贪婪策略,这意味着我们希望智能体采取具有最大Q
值的操作。 在学习网络的同时,我们不希望它总是贪婪地表现。 如果这样做,它将永远不会探索新的选择,也不会学习新的东西。 因此,我们需要我们的智能体偶尔执行不符合规定的策略。
平衡这种探索的最佳方法是一个持续不断的研究主题,并且已经使用了很长时间。 但是,我们将使用的方法非常简单。 智能体每次执行操作时,我们都会生成一个随机数。 如果该数字等于或小于某个阈值ε
,则智能体将采取随机措施。 这称为 ε 贪婪策略。
智能体第一次启动时,对世界了解不多,应该探索更多。 随着智能体变得越来越聪明,它可能应该减少探索并更多地使用其对环境的了解。 为此,我们只需要在训练时逐渐降低ε
。 在我们的示例中,我们将每转降低ε
的衰减率,以使它随每个动作线性减小。
综上所述,我们有一个线性退火 ε - 贪心 Q 策略,说起来既简单又有趣。
DeepMind
至少没有提到 Mnih 等人的论文《和深度强化学习一起玩 Atari》,就不会完成关于强化学习的讨论。 然后是 DeepMind,现在是 Google。 在这篇具有里程碑意义的论文中,作者使用了卷积神经网络来训练深度 Q 网络来玩 Atari 2600 游戏。 他们从 Atari 2600 游戏中获取原始像素输出,将其缩小一点,将其转换为灰度,然后将其用作网络的状态空间输入。 为了使计算机了解屏幕上对象的速度和方向,他们使用了四个图像缓冲区作为深度 Q 网络的输入。
作者能够创建一个智能体,该智能体能够使用完全相同的神经网络架构玩 7 个 Atari 2600 游戏,并且在其中三个游戏上,该智能体要比人类更好。 后来又扩大到 49 场比赛,其中大多数比赛都比人类出色。 本文是迈向通用 AI 的非常重要的一步,它实际上是目前在强化学习中开展的许多研究的基础。
Keras 强化学习框架
在这一点上,我们应该有足够的背景知识来开始建立深层的 Q 网络,但是仍然需要克服很大的障碍。
实现利用深度强化学习的智能体可能是一个很大的挑战,但是最初由 Matthias Plappert 编写的 Keras-RL 库使其变得更加容易。 我将使用他的库来为本章介绍的智能体提供支持。
当然,如果没有环境,我们的经纪人将不会有太多的乐趣。 我将使用 OpenAI 体育馆,该体育馆提供许多环境,包括状态和奖励函数,我们可以轻松地使用它们来构建供智能体探索的世界。
安装 Keras-RL
Keras-RL 可以通过 PIP 安装。 但是,我建议从项目 GitHub 存储库中安装它,因为代码可能会更新一些。 为此,只需克隆存储库并按以下方式运行python setup.py install
:
git clone https://github.com/matthiasplappert/keras-rl.git
cd keras-rl
python setup.py install
安装 OpenAI Gym
OpenAI 体育场可作为点子安装。 我将使用他们的Box2D
和atari
环境中的示例。 您可以使用以下代码安装它们:
pip install gym
pip install gym[atari]
pip install gym[Box2D]
使用 OpenAI Gym
使用 OpenAI 体育场确实使深度强化学习变得容易。 Keras-RL 将完成大部分艰苦的工作,但是我认为值得单独走遍体育馆,这样您才能了解智能体如何与环境互动。
环境是可以实例化的对象。 例如,要创建CartPole-v0
环境,我们只需要导入体育场并创建环境,如以下代码所示:
import gym
env = gym.make("CartPole-v0")
现在,如果我们的智能体想要在那种环境中行动,它只需要发送一个action
并返回一个状态和一个reward
,如下所示:
next_state, reward, done, info = env.step(action)
该智能体可以通过使用循环与环境进行交互来播放整个剧集。 此循环的每次迭代都对应剧集中的单个步骤。 当智能体从环境接收到“完成”信号时,剧集结束。
在 Keras 中建立强化学习智能体
好消息,我们终于可以开始编码了。 在本部分中,我将演示两种名为 CartPole 和 Lunar Lander 的 Keras-RL 智能体。 我选择这些示例是因为它们不会消耗您的 GPU 和云预算来运行。 它们可以很容易地扩展到 Atari 问题,我在本书的 Git 存储库中也包括了其中之一。 您可以照常在Chapter12
文件夹中找到所有这些代码。 让我们快速讨论一下这两种环境:
- CartPole:CartPole 环境由平衡在推车上的杆组成。 智能体必须学习如何在立柱下方的推车移动时垂直平衡立柱。 给智能体指定了推车的位置,推车的速度,杆的角度和杆的旋转速度作为输入。 智能体可以在推车的任一侧施加力。 如果电线杆与垂直线的夹角下降超过 15 度,我们的经纪人就此告吹。
- Lunar Lander:Lunar Lander 的环境更具挑战性。 特工必须将月球着陆器降落在着陆垫上。 月亮的表面会发生变化,着陆器的方位也会在每个剧集发生变化。 该智能体将获得一个八维数组,用于描述每个步骤中的世界状态,并且可以在该步骤中执行四个操作之一。 智能体可以选择不执行任何操作,启动其主引擎,启动其左向引擎或启动其右向引擎。
CartPole
CartPole 智能体将使用一个相当适度的神经网络,即使没有 GPU,您也应该能够相当迅速地进行训练。 我们将一如既往地从模型架构开始。 然后,我们将定义网络的内存,探索策略,最后训练智能体。
CartPole 神经网络架构
三个具有 16 个神经元的隐藏层实际上可能足以解决这个简单的问题。 这个模型非常类似于我们在本书开始时使用的一些基本模型。 我们将使用以下代码来定义模型:
代码语言:javascript复制def build_model(state_size, num_actions):
input = Input(shape=(1,state_size))
x = Flatten()(input)
x = Dense(16, activation='relu')(x)
x = Dense(16, activation='relu')(x)
x = Dense(16, activation='relu')(x)
output = Dense(num_actions, activation='linear')(x)
model = Model(inputs=input, outputs=output)
print(model.summary())
return model
输入将是一个1 x 状态空间
向量,每个可能的动作都有一个输出神经元,它将预测每个步骤该动作的Q
值。 通过获取输出的argmax
,我们可以选择Q
值最高的动作,但是我们不必自己做,因为 Keras-RL 会为我们做。
记忆
Keras-RL 为我们提供了一个名为rl.memory.SequentialMemory
的类,该类提供了快速有效的数据结构,我们可以将智能体的经验存储在以下位置:
memory = SequentialMemory(limit=50000, window_length=1)
我们需要为此存储对象指定一个最大大小,它是一个超参数。 随着新的经验添加到该内存中并变得完整,旧的经验会被遗忘。
策略
Keras-RL 提供了一个称为rl.policy.EpsGreedyQPolicy
的 ε-贪婪 Q 策略,我们可以用来平衡利用与探索。 当智能体程序向世界前进时,我们可以使用rl.policy.LinearAnnealedPolicy
来衰减ε
,如以下代码所示:
policy = LinearAnnealedPolicy(EpsGreedyQPolicy(), attr='eps', value_max=1., value_min=.1, value_test=.05, nb_steps=10000)
在这里我们要说的是,我们要从ε
的值 1 开始,并且不小于 0.1,同时测试我们的随机数是否小于 0.05。 我们将步数设置为 .1 到 10,000 之间,Keras-RL 为我们处理衰减数学。
智能体
定义了模型,内存和策略后,我们现在就可以创建一个深度 Q 网络智能体,并将这些对象发送给该智能体。 Keras RL 提供了一个称为rl.agents.dqn.DQNAgent
的智能体类,我们可以为此使用它,如以下代码所示:
dqn = DQNAgent(model=model, nb_actions=num_actions, memory=memory, nb_steps_warmup=10,
target_model_update=1e-2, policy=policy)
dqn.compile(Adam(lr=1e-3), metrics=['mae'])
此时,其中两个参数target_model_update
和nb_steps_warmup
可能还不熟悉:
nb_steps_warmup
:确定我们开始进行经验重放之前需要等待的时间,如果您还记得的话,这是我们实际上开始训练网络的时间。 这使我们积累了足够的经验来构建适当的小批量生产。 如果您为此参数选择的值小于批量大小,则 Keras RL 将抽样替换。target_model_update
:Q
函数是递归的,当智能体更新它的网络以获取Q(s, a)
时,更新也影响其对Q(s', a)
所做的预测。 这会导致网络非常不稳定。 大多数深度 Q 网络实现解决此限制的方法是使用目标网络,该目标网络是未经训练的深度 Q 网络的副本,而经常被新副本替换。target_model_update
参数控制这种情况发生的频率。
训练
Keras RL 提供了多个类似 Keras 的回调,可以方便地进行模型检查指向和记录。 我将在下面使用这两个回调。 如果您想查看 Keras-RL 提供的更多回调,可以在以下位置找到它们。 您还可以找到可用于创建自己的 Keras-RL 回调的回调类。
我们将使用以下代码来训练我们的模型:
代码语言:javascript复制def build_callbacks(env_name):
checkpoint_weights_filename = 'dqn_' env_name '_weights_{step}.h5f'
log_filename = 'dqn_{}_log.json'.format(env_name)
callbacks = [ModelIntervalCheckpoint(checkpoint_weights_filename, interval=5000)]
callbacks = [FileLogger(log_filename, interval=100)]
return callbacks
callbacks = build_callbacks(ENV_NAME)
dqn.fit(env, nb_steps=50000,
visualize=False,
verbose=2,
callbacks=callbacks)
一旦构建了智能体的回调,我们就可以使用.fit()
方法来拟合DQNAgent
,就像使用 Keras 模型一样。 在此示例中,请注意visualize
参数。 如果将visualize
设置为True
,我们将能够观察智能体与环境的交互。 但是,这大大减慢了训练的速度。
结果
在前 250 个剧集之后,我们将看到剧集的总奖励接近 200,剧集步骤的总奖励也接近 200。这意味着智能体已学会平衡购物车上的杆位,直到环境结束最多 200 个步骤 。
观看我们的成功当然很有趣,因此我们可以使用DQNAgent
.test()
方法评估某些剧集。 以下代码用于定义此方法:
dqn.test(env, nb_episodes=5, visualize=True)
在这里,我们设置了visualize=True
,以便我们可以看到我们的智能体平衡杆位,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VtdDdRKw-1681567952199)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/b7cebe8d-49bf-4557-be22-f17d87109762.png)]
我们走了,那是一根平衡杆! 好吧,我知道,我承认平衡手推车上的电线杆并不是那么酷,所以让我们再做一个轻量级的例子。 在此示例中,我们将把月球着陆器降落在月球上,希望它将给您留下深刻的印象。
Lunar Lander
感谢 Keras-RL,我们用于 Lunar Lander 的智能体几乎与 CartPole 相同,除了实际的模型架构和一些超参数更改外。 Lunar Lander 的环境有八个输入而不是四个输入,我们的智能体现在可以选择四个操作而不是两个。
如果您受到这些示例的启发,并决定尝试构建 Keras-RL 网络,请记住,超参数选择非常非常重要。 对于 Lunar Lander 智能体,对模型架构的最小更改导致我的智能体无法学习环境解决方案。 使网络正确运行是一项艰巨的工作。
Lunar Lander 网络架构
我的 Lunar Lander 智能体程序的架构仅比 CartPole 的架构稍微复杂一点,对于相同的三个隐藏层仅引入了几个神经元。 我们将使用以下代码来定义模型:
代码语言:javascript复制def build_model(state_size, num_actions):
input = Input(shape=(1, state_size))
x = Flatten()(input)
x = Dense(64, activation='relu')(x)
x = Dense(32, activation='relu')(x)
x = Dense(16, activation='relu')(x)
output = Dense(num_actions, activation='linear')(x)
model = Model(inputs=input, outputs=output)
print(model.summary())
return model
在此问题的情况下,较小的架构会导致智能体学习控制和悬停着陆器,但实际上并未着陆。 当然,由于我们要对每个剧集的每个步骤进行小批量更新,因此我们需要仔细权衡复杂性与运行时和计算需求之间的关系。
记忆和策略
CartPole 的内存和策略可以重复使用。 我相信,通过进一步调整线性退火策略中的步骤,可能会提高智能体训练的速度,因为该智能体需要采取更多的步骤来进行训练。 但是,为 CartPole 选择的值似乎可以很好地工作,因此这是留给读者的练习。
智能体
从以下代码中可以看出,Lunar Lander DQNAgent
再次相同,只是学习率小得多。
dqn = DQNAgent(model=model, nb_actions=num_actions, memory=memory, nb_steps_warmup=10, target_model_update=1e-2, policy=policy)
dqn.compile(Adam(lr=0.00025), metrics=['mae'])
训练
在训练该特工时,您会注意到它学会做的第一件事是将着陆器悬停,并避免着陆。 当着陆器最终着陆时,它会收到非常高的奖励,成功着陆时为 100,坠毁时为 -100。 这种 -100 的奖励是如此之强,以至于智能体一开始宁愿因悬停而受到小额罚款。 我们的探员要花很多时间才能得出这样的提示:良好的着陆总比没有良好着陆好,因为坠机着陆非常糟糕。
可以塑造奖励信号来帮助座席更快地学习,但这超出了本书的范围。 有关更多信息,请查看奖励塑造。
由于这种对坠机着陆的极端负面反馈,网络需要花费相当长的一段时间才能学会着陆。 在这里,我们正在运行五十万个训练步骤,以传达我们的信息。 我们将使用以下代码来训练智能体:
代码语言:javascript复制callbacks = build_callbacks(ENV_NAME)
dqn.fit(env, nb_steps=1000000,
visualize=False,
verbose=2,
callbacks=callbacks)
您可以通过调整参数gamma
(默认值为 0.99)来进一步改进此示例。 如果您从Q
函数中调用,此参数会减少或增加Q
函数中将来奖励的影响。
结果
我在 Git 一章中包含了 Lunar Lander 的权重,并创建了一个脚本,该脚本在启用可视化的情况下运行这些权重dqn_lunar_lander_test.py
。 它加载经过训练的模型权重并运行 10 集。 在大多数情况下,特工能够以惊人的技能和准确率将月球着陆器降落在其着陆板上,如以下屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLZ2E9Gt-1681567952200)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/a22a91ff-1c32-4a61-90bb-3b52d6555309.png)]
希望这个例子可以说明,尽管深层 Q 网络并不是火箭科学,但仍可用于控制火箭。
总结
斯坦福大学只教授强化学习的整个课程。 可能只写了一本关于强化学习的书,实际上已经做了很多次。 我希望本章能够向您展示足够的知识,让您开始解决强化学习问题。
当我解决“月球着陆器”问题时,很容易让我的头脑从玩具问题到利用深层 Q 网络驱动的特工进行实际太空探索而徘徊。 我希望本章为您做同样的事情。
在下一章中,我将向您展示深度神经网络的最后一种用法,我们将研究可以生成新图像,数据点甚至音乐的网络,称为生成对抗网络。
十三、生成对抗网络
尽管我在本书中花了很多时间谈论分类或估计的网络,但在本章中,我将向您展示一些具有创建能力的深度神经网络。 生成对抗网络(GAN)通过两个内部深层网络之间的内部竞争来学习如何做到这一点,我们将在下面讨论。 在深度卷积生成对抗网络(DCGAN)的情况下,这是我将在本章中重点介绍的 GAN 类型,该网络将学习创建类似于训练数据集的图像。
我们将在本章介绍以下主题:
- GAN 概述
- 深度卷积 GAN 架构
- GAN 如何失败
- GAN 的安全选择
- 使用 Keras GAN 生成 MNIST 图像
- 使用 Keras GAN 生成 CIFAR-10 图像
GAN 概述
生成对抗网络都是关于生成新内容的。 GAN 能够学习一些分布并从该分布创建新样本。 该样本可能只是我们训练数据中未出现的直线上的新点,但也可能是非常复杂的数据集中的新点。 GAN 已用于生成新的音乐,声音和图像。 根据 Yann LeCun 所说,《对抗训练是切片以来最酷的事情》。 我不确定切片面包是否特别酷,但是 Yann LeCun 是一个非常酷的家伙,所以我会信守诺言。 无论如何,GAN 都非常受欢迎,虽然它可能不如我们在业务环境中涵盖的其他一些主题那么实用,但在我们对深度学习技术的调查中值得考虑。
2014 年,伊恩·古德费洛(Ian Goodfellow)等人。 撰写了一篇名为生成对抗网络的论文,提出了使用两个深度网络进行对抗训练的框架,每个都尝试打败对方。 该框架由两个独立的网络组成:判别器和生成器。
判别器正在查看来自训练集的真实数据和来自生成器的假数据。 它的工作是将每一个作为传入数据实例分类为真实还是伪造。
生成器试图使判别器误以为所生成的数据是真实的。
生成器和判别器被锁定在一个游戏中,它们各自试图超越彼此。 这种竞争驱使每个网络不断改进,直到最终判别器将生成器的输出与训练集中的数据区分开。 当生成器和判别器都正确配置时,它们将达到纳什均衡,在纳什均衡中,两者都无法找到优势。
深度卷积 GAN 架构
关于 GAN 的论文很多,每篇都提出了新的新颖架构和调整。 但是,它们中的大多数至少在某种程度上基于深度卷积 GAN(DCGAN)。 在本章的其余部分中,我们将重点介绍这种模型,因为当您采用此处未介绍的新的令人兴奋的 GAN 架构(例如条件 GAN(CGAN),Stack GAN,InfoGAN 或 Wasserstein GAN),或者可能还有一些其他的新变种,您可能会选择接下来看看。
DCGAN 由 Alex Radford,Luke Metz 和 Soumith Chintala 在论文《深度卷积生成对抗网络》中提出。
接下来让我们看一下 DCGAN 的总体架构。
对抗训练架构
GAN 的整体架构如下图所示。 生成器和判别器分别是单独的深度神经网络,为了易于使用,将它们简化为黑匣子。 我们将很快介绍它们的各个架构,但首先,我想着重介绍它们的交互方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Eyp3OCU-1681567952200)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/370d895f-0973-49cf-8e8a-a9746d082883.png)]
给生成器一个随机噪声向量(z
),并创建一个输出G(z)
(对于 DCGAN,这是一个图像),希望它能欺骗判别器。
判别器既得到实际训练数据(X
),又得到生成器输出G(z)
。 要做的是确定其输入实际上是真实的概率P(X)
。
判别器和生成器都在栈中一起训练。 随着一个方面的改进,另一个方面也有所改进,直到希望生成器产生如此好的输出,从而使判别器不再能够识别该输出与训练数据之间的差异。
当然,在您准备好构建自己的 GAN 之前,我们还要介绍更多细节。 接下来,让我们更深入地研究生成器。
生成器架构
在此示例中,我们使用适合于生成28 x 28
灰度图像的层大小,这正是我们稍后在 MNIST 示例中将要执行的操作。 如果您以前没有使用过生成器,那么生成器的算法可能会有些棘手,因此我们将在遍历每一层时进行介绍。 下图显示了架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fu3VO6Zv-1681567952200)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/6e7de048-2461-4be3-a702-f4d0a87d3c43.png)]
生成器的输入只是100 x 1
的随机向量,我们将其称为噪声向量。 当此噪声向量是从正态分布生成时,GAN 往往工作得最好。
网络的第一层是密集的并且完全连接。 它为我们提供了一种建立线性代数的方法,以便最终得到正确的输出形状。 对于每个卷积块,我们最终将第一轴和第二轴(最终将成为图像的高度和宽度的行和列)加倍,而通道数逐渐缩小到 1。我们最终需要输出的高度和宽度为 28。因此,我们将需要从7 x 7 x 128
张量开始,以便它可以移动到14 x 14
,然后最终是28 x 28
。 为此,我们将密集层的大小设置为128 x 7 x 7
神经元或 6,272 单元。 这使我们可以将密集层的输出重塑为7 x 7 x 128
。 如果现在看来这还不算什么,请不用担心,在编写代码后,这才有意义。
在完全连接的层之后,事情变得更加简单。 就像我们一直一样,我们正在使用卷积层。 但是,这次我们反向使用它们。 我们不再使用最大池来缩减样本量。 取而代之的是,我们进行上采样,在学习视觉特征时使用卷积来构建我们的网络,并最终输出适当形状的张量。
通常,生成器中最后一层的激活是双曲正切,并且训练图像矩阵中的元素被归一化为 -1 和 1 之间。这是我将在整章中提到的众多 GAN 黑魔法之一。 研究人员已经发现了一些经验证明可以帮助构建稳定的 GAN 的黑魔法,Soumith Chintala 可以在此 Git 上找到大多数黑客,而 Soumith Chintala 也是 DCGAN 原始论文的作者之一。 深度学习研究的世界无疑是一个很小的领域。
判别器架构
判别器的架构更像我们在前几章中已经看到的。 它实际上只是一个典型的图像分类器,如下图所示。 输出是 Sigmoid 的,因为判别器将预测输入图像是真实图像集的成员的概率。 判别器正在解决二分类问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAoOSH5E-1681567952201)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/68c3560e-2207-409d-94b3-aebcb4b7d247.png)]
现在,我们已经介绍了 DCGAN 的架构以及它的各个层次,下面让我们看一下如何训练框架。
DCGAN
DCGAN 框架是使用迷你批量来进行训练的,这与我之前在本书中对网络进行训练的方式相同。 但是,稍后在构建代码时,您会注意到我们正在构建一个训练循环,该循环明确控制每个更新批量的情况,而不仅仅是调用models.fit()
方法并依靠 Keras 为我们处理它。 我这样做是因为 GAN 训练需要多个模型来更新同一批次中的权重,所以它比我们以前所做的单个参数更新要稍微复杂一些。
对 DCGAN 进行训练的过程分为两步,每批次进行一次。
步骤 1 – 训练判别器
批量训练 DCGAN 的第一步是在实际数据和生成的数据上训练判别器。 赋予真实数据的标签显然是1
,而用于假数据的标签则是0
。
步骤 2 – 训练栈
判别器更新权重后,我们将判别器和生成器一起训练为一个模型。 这样做时,我们将使判别器的权重不可训练,将其冻结在适当的位置,但仍允许判别器将梯度反向传播到生成器,以便生成器可以更新其权重。
对于训练过程中的这一步,我们将使用噪声向量作为输入,这将导致生成器生成图像。 判别器将显示该图像,并要求预测该图像是否真实。 下图说明了此过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MWn9p1SG-1681567952201)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/c8cc7ae9-1801-46d0-af96-142955d6a9a2.png)]
判别器将提出一些预测,我们可以称之为y_hat
。 此栈的loss
函数将是二元交叉熵,并且我们将loss
函数的标签传递为 1,我们可以考虑y
。 如您在本书前面所提到的, y
和y_hat
之间的loss
转换为梯度,然后通过判别器传给生成器。 这将更新生成器权重,使它可以从判别者对问题空间的了解中受益,以便它可以学习创建更逼真的生成图像。
然后重复这两个步骤,直到生成器能够创建与训练集中的数据相似的数据,使得判别器无法再将两个数据集区分开,这成为了一个猜谜游戏。 判别器。 此时,生成器将不再能够改进。 当我们找到纳什均衡时,就对网络进行了训练。
GAN 如何失败
至少可以说,训练 GAN 是一件棘手的事情。 训练 GAN 失败的方法有很多种。 实际上,在撰写本章时,我发现自己大大扩展了亵渎向量的词汇量,同时还花了一点时间在云 GPU 上! 在本章稍后向您展示两个可用的 GAN 之前,让我们考虑可能发生的故障以及如何修复这些问题。
稳定性
训练 GAN 需要在判别器和生成器之间进行仔细的平衡。 判别器和生成器都在争夺深度网络优势。 另一方面,他们也需要彼此学习和成长。 为了使它起作用,任何一个都不能压倒另一个。
在不稳定的 GAN 中,判别器可能会使生成器过载,并绝对确定生成器是假的。 损失为零,并且没有可用于发送到生成器的梯度,因此它不再可以改善。 网络游戏结束。 解决此问题的最佳方法是降低判别器的学习率。 您也可以尝试减少整个判别器架构中神经元的数量。 但是,您可能会在训练过程的后期错过这些神经元。 最终,调整网络架构和超参数是避免这种情况的最佳方法。
当然,这可能是相反的方式,如模式崩溃的情况。
模式崩溃
模式崩溃是 GAN 失败的类似且相关的方式。 在模式崩溃中,生成器在多模式分布中学习一种模式,并选择始终使用该方法来利用判别器。 如果您的训练集中有鱼和小猫,并且您的生成器仅生成奇怪的小猫而没有鱼,则您经历了模式崩溃。 在这种情况下,增加判别器的威力可能会有所帮助。
GAN 的安全选择
我之前已经提到过 Soumith Chintala 的 GAN 黑魔法 Git,当您试图使 GAN 稳定时,这是一个很好的起点。 既然我们已经讨论了训练稳定的 GAN 会有多么困难,让我们来谈谈一些安全的选择,这些选择可能会帮助您成功找到自己的地方。 尽管有很多技巧,但以下是本章中尚未涵盖的我的主要建议:
- 批量规范:使用批量规范化时,请为真实数据和伪数据构造不同的微型批量,并分别进行更新。
- 泄漏的 ReLU:泄漏的 ReLU 是 ReLU 激活函数的变异。 回想一下 ReLU 函数是
f(x) = max(0, x)
。
但是,泄漏的 ReLU 可以表示为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ecnbOcRL-1681567952201)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/24a66098-c4cd-49c4-8322-9e65ae01d963.png)]
当设备不工作时,泄漏的 ReLU 允许非常小的非零梯度。 这可以消除消失的梯度,当我们像在判别器和生成器的组合中那样将多个层堆叠在一起时,这总是一个问题。
- 在生成器中使用丢弃:这将产生噪声并防止模式崩溃。
- 使用软标签:对于真实示例,请使用介于 0.7 和 1 之间的标签,对于伪示例,请使用介于 0 和 0.3 之间的标签。 这种噪声有助于保持信息从判别器流向生成器。
在本章的其他地方,我们还将介绍许多其他的 GAN 黑魔法。 但是,我认为在成功实现 GAN 时,这几项技巧是最重要的。
使用 Keras GAN 生成 MNIST 图像
我们之前曾与 MNIST 合作,但是这次我们将使用 GAN 生成新的 MNIST 图像。 训练 GAN 可能需要很长时间。 但是,此问题很小,可以在几个小时内在大多数笔记本电脑上运行,这是一个很好的例子。 稍后,我们将把这个例子扩展到 CIFAR-10 图像。
我在这里使用的网络架构已被许多人发现并进行了优化,包括 DCGAN 论文的作者以及像 ErikLinder-Norén 这样的人,他是 GAN 实现的优秀集合,称为 Keras GAN 作为我在此处使用的代码的基础。 如果您想知道我是如何在这里使用的架构选择的,这些就是我试图站在肩膀上的巨人。
加载数据集
MNIST
数据集由 60,000 个手绘数字(从 0 到 9)组成。Keras 为我们提供了一个内置加载程序,可将其分为 50,000 个训练图像和 10,000 个测试图像。 我们将使用以下代码加载数据集:
from keras.datasets import mnist
def load_data():
(X_train, _), (_, _) = mnist.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
X_train = np.expand_dims(X_train, axis=3)
return X_train
您可能已经注意到,我没有返回任何标签或测试数据集。 我将只使用训练数据集。 不需要标签,因为我要使用的唯一标签是0
代表假货,1
代表真货。 这些是真实的图像,因此将在判别器上将它们全部分配为标签 1。
创建生成器
生成器使用了一些新的层,我们将在本节中讨论这些层。 首先,有机会略读以下代码:
代码语言:javascript复制def build_generator(noise_shape=(100,)):
input = Input(noise_shape)
x = Dense(128 * 7 * 7, activation="relu")(input)
x = Reshape((7, 7, 128))(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(128, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(64, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(1, kernel_size=3, padding="same")(x)
out = Activation("tanh")(x)
model = Model(input, out)
print("-- Generator -- ")
model.summary()
return model
我们以前没有使用过UpSampling2D
层。 该层将增加输入张量的行和列,从而使通道保持不变。 它通过重复输入张量中的值来实现。 默认情况下,它将使输入加倍。 如果给UpSampling2D
层一个7 x 7 x 128
输入,它将给我们一个14 x 14 x 128
输出。
通常,当我们构建一个 CNN 时,我们从一个非常高和宽的图像开始,并使用卷积层来获得一个非常深但又不高又不宽的张量。 在这里,我将相反。 我将使用一个密集层并进行重塑,以7 x 7 x 128
张量开始,然后将其加倍两次后,剩下28 x 28
张量。 由于我需要灰度图像,因此可以使用具有单个单元的卷积层来获得28 x 28 x 1
输出。
这种生成器运算法则有点令人反感,乍一看似乎很尴尬,但是经过几个小时的痛苦之后,您就会掌握它了!
创建判别器
判别符实际上在很大程度上与我之前谈到的任何其他 CNN 相同。 当然,我们应该谈论一些新事物。 我们将使用以下代码来构建判别器:
代码语言:javascript复制def build_discriminator(img_shape):
input = Input(img_shape)
x =Conv2D(32, kernel_size=3, strides=2, padding="same")(input)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.25)(x)
x = Conv2D(64, kernel_size=3, strides=2, padding="same")(x)
x = ZeroPadding2D(padding=((0, 1), (0, 1)))(x)
x = (LeakyReLU(alpha=0.2))(x)
x = Dropout(0.25)(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(128, kernel_size=3, strides=2, padding="same")(x)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.25)(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(256, kernel_size=3, strides=1, padding="same")(x)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.25)(x)
x = Flatten()(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(input, out)
print("-- Discriminator -- ")
model.summary()
return model
首先,您可能会注意到形状奇怪的zeroPadding2D()
层。 第二次卷积后,我们的张量从28 x 28 x 3
变为7 x 7 x 64
。 这一层使我们回到偶数,在行和列的一侧都加零,这样我们的张量现在为8 x 8 x 64
。
更不寻常的是同时使用批量规范化和丢弃法。 通常,这两层不能一起使用。 但是,就 GAN 而言,它们似乎确实使网络受益。
创建栈式模型
现在我们已经组装了generator
和discriminator
,我们需要组装第三个模型,这是两个模型的栈,在discriminator
损失的情况下,我们可以用来训练生成器。
为此,我们可以创建一个新模型,这次使用以前的模型作为新模型中的层,如以下代码所示:
代码语言:javascript复制discriminator = build_discriminator(img_shape=(28, 28, 1))
generator = build_generator()
z = Input(shape=(100,))
img = generator(z)
discriminator.trainable = False
real = discriminator(img)
combined = Model(z, real)
注意,在建立模型之前,我们将判别器的训练属性设置为False
。 这意味着对于该模型,在反向传播期间,我们将不会更新判别器的权重。 正如我们在“栈式训练”部分中提到的,我们将冻结这些权重,仅将生成器的权重与栈一起移动。 判别器将单独训练。
现在,所有模型都已构建,需要对其进行编译,如以下代码所示:
代码语言:javascript复制gen_optimizer = Adam(lr=0.0002, beta_1=0.5)
disc_optimizer = Adam(lr=0.0002, beta_1=0.5)
discriminator.compile(loss='binary_crossentropy',
optimizer=disc_optimizer,
metrics=['accuracy'])
generator.compile(loss='binary_crossentropy', optimizer=gen_optimizer)
combined.compile(loss='binary_crossentropy', optimizer=gen_optimizer)
如果您会注意到,我们将创建两个自定义 Adam 优化器。 这是因为很多时候,我们只想更改判别器或生成器的学习率,从而减慢一个或另一个的学习速度,以至于我们得到一个稳定的 GAN,而后者却无法胜任另一个。 您还会注意到我正在使用beta_1 = 0.5
。 这是我发扬光大并取得成功的 DCGAN 原始论文的推荐。 从原始 DCGAN 论文中可以发现,0.0002 的学习率也是一个很好的起点。
训练循环
以前,我们曾很奢侈地在模型上调用.fit()
,让 Keras 处理将数据分成小批和为我们训练的痛苦过程。
不幸的是,因为我们需要为一个批量器对判别器和堆叠模型一起执行单独的更新,所以我们将不得不用老式的方式来做一些循环。 这就是过去一直做的事情,因此虽然可能需要做更多的工作,但它的确使我感到怀旧。 以下代码说明了训练技术:
代码语言:javascript复制num_examples = X_train.shape[0]
num_batches = int(num_examples / float(batch_size))
half_batch = int(batch_size / 2)
for epoch in range(epochs 1):
for batch in range(num_batches):
# noise images for the batch
noise = np.random.normal(0, 1, (half_batch, 100))
fake_images = generator.predict(noise)
fake_labels = np.zeros((half_batch, 1))
# real images for batch
idx = np.random.randint(0, X_train.shape[0], half_batch)
real_images = X_train[idx]
real_labels = np.ones((half_batch, 1))
# Train the discriminator (real classified as ones and
generated as zeros)
d_loss_real = discriminator.train_on_batch(real_images,
real_labels)
d_loss_fake = discriminator.train_on_batch(fake_images,
fake_labels)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
noise = np.random.normal(0, 1, (batch_size, 100))
# Train the generator
g_loss = combined.train_on_batch(noise, np.ones((batch_size, 1)))
# Plot the progress
print("Epoch %d Batch %d/%d [D loss: %f, acc.: %.2f%%] [G loss:
%f]" %
(epoch,batch, num_batches, d_loss[0], 100 * d_loss[1], g_loss))
if batch % 50 == 0:
save_imgs(generator, epoch, batch)
可以肯定,这里发生了很多事情。 和以前一样,让我们逐个细分。 首先,让我们看一下生成噪声向量的代码:
代码语言:javascript复制 noise = np.random.normal(0, 1, (half_batch, 100))
fake_images = generator.predict(noise)
fake_labels = np.zeros((half_batch, 1))
这段代码生成了一个噪声向量矩阵(我们之前将其称为z
)并将其发送到生成器。 它返回了一组生成的图像,我称之为伪图像。 我们将使用它们来训练判别器,因此我们要使用的标签为 0,表示这些实际上是生成的图像。
注意,这里的形状是half_batch x 28 x 28 x 1
。 half_batch
正是您所想的。 我们将创建一半的生成图像,因为另一半将是真实数据,我们将在下一步进行组装。 要获取真实图像,我们将在X_train
上生成一组随机索引,并将X_train
的切片用作真实图像,如以下代码所示:
idx = np.random.randint(0, X_train.shape[0], half_batch)
real_images = X_train[idx]
real_labels = np.ones((half_batch, 1))
是的,在这种情况下,我们正在抽样更换。 它确实可以解决,但可能不是实现小批量训练的最佳方法。 但是,它可能是最简单,最常见的。
由于我们正在使用这些图像来训练判别器,并且由于它们是真实图像,因此我们将它们分配为1
作为标签,而不是0
。 现在我们已经组装了判别器训练集,我们将更新判别器。 还要注意,我们没有使用我们之前讨论的软标签。 那是因为我想让事情尽可能地容易理解。 幸运的是,在这种情况下,网络不需要它们。 我们将使用以下代码来训练判别器:
# Train the discriminator (real classified as ones and generated as zeros)
d_loss_real = discriminator.train_on_batch(real_images, real_labels)
d_loss_fake = discriminator.train_on_batch(fake_images, fake_labels)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
请注意,这里我使用的是判别符的train_on_batch()
方法。 这是我第一次在本书中使用此方法。 train_on_batch()
方法正好执行一轮正向和反向传播。 每次我们调用它时,它都会从模型的先前状态更新一次模型。
另请注意,我正在分别对真实图像和伪图像进行更新。 这是我先前在“生成器架构”部分中引用的 GAN 黑魔法 Git 上给出的建议。 尤其是在训练的早期阶段,当真实图像和伪图像来自完全不同的分布时,如果我们将两组数据放在同一更新中,则批量归一化将导致训练问题。
现在,判别器已经更新,是时候更新生成器了。 这是通过更新组合栈间接完成的,如以下代码所示:
代码语言:javascript复制noise = np.random.normal(0, 1, (batch_size, 100))
g_loss = combined.train_on_batch(noise, np.ones((batch_size, 1)))
为了更新组合模型,我们创建了一个新的噪声矩阵,这次它将与整个批次一样大。 我们将其用作栈的输入,这将使生成器生成图像,并使用判别器评估该图像。 最后,我们将使用1
标签,因为我们想在实际图像和生成的图像之间反向传播误差。
最后,训练循环报告epoch
/batch
处的判别器和生成器损失,然后每epoch
中的每 50 批,我们将使用save_imgs
生成示例图像并将其保存到磁盘,如以下代码所示:
print("Epoch %d Batch %d/%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" %
(epoch,batch, num_batches, d_loss[0], 100 * d_loss[1], g_loss))
if batch % 50 == 0:
save_imgs(generator, epoch, batch)
save_imgs
函数使用生成器在运行时创建图像,因此我们可以看到工作的成果。 我们将使用以下代码来定义save_imgs
:
def save_imgs(generator, epoch, batch):
r, c = 5, 5
noise = np.random.normal(0, 1, (r * c, 100))
gen_imgs = generator.predict(noise)
gen_imgs = 0.5 * gen_imgs 0.5
fig, axs = plt.subplots(r, c)
cnt = 0
for i in range(r):
for j in range(c):
axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap='gray')
axs[i, j].axis('off')
cnt = 1
fig.savefig("images/mnist_%d_%d.png" % (epoch, batch))
plt.close()
它通过创建噪声矩阵并检索图像矩阵来仅使用生成器。 然后,使用matplotlib.pyplot
将这些图像保存到5 x 5
网格中的磁盘上。
模型评估
当您构建深层神经网络来创建图像时,好坏有点主观。 让我们看一下训练过程的一些示例,以便您可以亲自了解 GAN 如何开始学习如何生成 MNIST。
这是第一个周期的第一批网络。 显然,此时生成器对生成 MNIST 并不了解。 只是噪音,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xvgLFfw-1681567952201)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/100f93bf-4aee-47f2-9b5f-542ffd5ac42a.png)]
但是只有 50 个批次,正在发生一些事情,如下面的图像所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtzXs28K-1681567952202)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/6ccd70b9-a564-4bc3-9d65-99301b571751.png)]
在 200 个批次的周期 0 之后,我们几乎可以看到数字,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RcxsL4kP-1681567952202)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/69d0a788-d965-4e70-a6f5-e8589b810c9f.png)]
一个完整的周期后,这是我们的生成器。 我认为这些生成的数字看起来不错,而且我可以看到判别符可能会被它们欺骗。 在这一点上,我们可能会继续改善一点,但是随着计算机生成一些令人信服的 MNIST 数字,我们的 GAN 似乎已经发挥了作用,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvdqaprR-1681567952202)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/3b179ae1-40df-4859-8849-01c5011588f7.png)]
尽管大多数代码是相同的,但在结束本章之前,让我们再看一个使用彩色图像的示例。
使用 Keras GAN 生成 CIFAR-10 图像
虽然网络架构在很大程度上保持不变,但我认为有必要向您展示一个使用彩色图像的示例,并在 Git 中提供示例,以便在想要将 GAN 应用于您的 GAN 时有一些起点。 自己的数据。
CIFAR-10
是一个著名的数据集,包含 60,000 张32 x 32 x 3
RGB 彩色图像,分布在 10 个类别中。 这些类别是飞机,汽车,鸟类,猫,鹿,狗,青蛙,马,船和卡车。 希望以后看到生成的图像时,您可能会看到一些可以想象的东西,就像那些对象。
加载 CIFAR-10
加载数据集几乎完全相同,因为 Keras 还使用以下代码为CIFAR-10
提供了一个加载器:
from keras.datasets import cifar10
def load_data():
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
return X_train
创建生成器
生成器需要产生32 x 32 x 3
图像。 这需要对我们的网络架构进行两项细微更改,您可以在此处看到它们:
input = Input(noise_shape)
x = Dense(128 * 8 * 8, activation="relu")(input)
x = Reshape((8, 8, 128))(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(128, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(64, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(3, kernel_size=3, padding="same")(x)
out = Activation("tanh")(x)
model = Model(input, out)
由于我们需要在 32 处结束,并且我们将两次上采样,因此我们应该从 8 开始。这可以通过将密集层及其相应的重塑层从128 * 7 * 7
更改为128 * 8 * 8
来轻松实现。
由于我们的图像现在包含三个通道,因此最后的卷积层也需要包含三个通道,而不是一个。 这里的所有都是它的; 我们现在可以生成彩色图像!
创建判别器
判别符几乎完全不变。 输入层需要从28 x 28 x 1
更改为32 x 32 x 3
。 另外ZeroPadding2D
可以毫无问题地删除,因为没有它的层算术就可以工作。
训练循环
训练循环保持不变,区别器构建调用除外,该调用需要与 CIFAR-10 图像大小相对应的新尺寸,如以下代码所示:
代码语言:javascript复制discriminator = build_discriminator(img_shape=(32, 32, 3))
当从一个数据集移动到另一个数据集时,通常会需要调整我们的学习率或网络架构。 幸运的是,在此示例中并非如此。
模型评估
CIFAR-10
数据集当然更加复杂,并且网络具有更多的参数。 因此,事情将需要更长的时间。 这是在周期 0(批次 300)中我们的图像的样子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WDNBghMv-1681567952202)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/09db5d5d-b9fb-4d13-a89e-6f83e4c55da2.png)]
我可能开始看到一些边缘,但是看起来并不像什么。 但是,如果我们等待几个周期,我们显然处在松鼠和怪异的鱼类地区。 我们可以看到一些东西正在成形,只是有些模糊,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kS4ARsyD-1681567952203)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/72ffa712-8e86-49e4-8dc5-46daebb79db0.png)]
下图显示了 12 个周期后的生成器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6JXihp1s-1681567952203)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/7a6bfedb-d613-4204-badf-c62e7e5f00f4.png)]
我看到分辨率很低的鸟,鱼,甚至还有飞机和卡车。 当然,我们还有很长的路要走,但是我们的网络已经学会了创建图像,这非常令人兴奋。
总结
在本章中,我们研究了 GAN 以及如何将其用于生成新图像。 我们学习了一些很好地构建 GAN 的规则,甚至学习了模拟 MNIST 和 CIFAR-10 图像。 毫无疑问,您可能已经在媒体上看到了一些由 GANs 制作的惊人图像。 在阅读了本章并完成了这些示例之后,您将拥有执行相同操作的工具。 我希望您可以采纳这些想法并加以调整。 剩下的唯一限制是您自己的想象力,数据和 GPU 预算。
在这本书中,我们涵盖了深度学习的许多应用,从简单的回归到生成对抗网络。 我对这本书的最大希望是,它可以帮助您实际使用深度学习技术,而其中的许多技术已经存在于学术界和研究领域,而这超出了实践数据科学家或机器学习工程师的能力。 在此过程中,我希望我能就如何构建更好的深度神经网络以及何时使用深度网络(而不是更传统的模型)提供一些建议。 如果您在这 13 章中一直跟着我,请多多关照。
“我们都是手工艺品的学徒,没人能成为大师。”
——欧内斯特·海明威