原文:Hands-On Deep Learning Architectures with Python
译者:飞龙
本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。
不要担心自己的形象,只关心如何实现目标。——《原则》,生活原则 2.3.c
第 3 节:序列建模
在本节中,我们将学习两个重要的 DL 模型以及这些模型的演化路径。 我们将通过一些示例探索它们的架构和各种工程最佳实践。
本节将涵盖以下章节:
- “第 6 章”,“循环神经网络”
六、循环神经网络
在本章中,我们将解释最重要的深度学习模型之一,即循环神经网络(RNNs)。 我们将首先回顾什么是 RNN,以及为什么它们非常适合处理顺序数据。 在简要介绍了 RNN 模型的发展路径之后,我们将说明根据不同形式的输入和输出数据以及工业示例进行分类的各种 RNN 架构。 我们将找出问题的答案,例如“我们如何仅生成一个输出?”,“输出可以是序列吗?”,和“仅对一个输入单元有效吗?”。
接下来,我们将讨论按循环层分类的几种架构。 首先,我们将应用基本的 RNN 架构来编写我们自己的《战争与和平》。 具有原始架构的 RNN 不能很好地保存长期依赖的信息。 为了解决这个问题,我们将学习内存增强型架构,包括长短期内存和门控循环单元。 我们还将在股票价格预测中采用门控架构。 最后,由于对捕获过去的信息不满意,我们将引入一种双向架构,该架构允许该模型从序列的过去和将来上下文中保留信息。 具有 LSTM 的双向模型将用于对电影评论的情感进行分类。
在本章中,我们将介绍以下主题:
- 什么是 RNN?
- RNN 的演进路径
- RNN 架构按输入和输出(一对多,多对一,同步和不同步的多对多)
- 原始 RNN 架构
- 用于文本生成的原始 RNN
- 长期记忆
- 用于文本生成的 LSTM RNN
- 门控循环单元
- 用于股价预测的 GRU RNN
- 双向 RNN
- 用于情感分类的 BRNN
什么是 RNN?
回想一下,在前几章中讨论的深度前馈网络,自编码器神经网络和 CNN 中,数据从输入层到输出层是单向流动的。 但是,深度学习模型允许数据沿任何方向进行,甚至可以循环回到输入层,并且不仅限于前馈架构。 从上一个输出循环返回的数据成为下一个输入数据的一部分。 RNN 就是很好的例子。 下图描述了 RNN 的一般形式,在本章中,我们将研究 RNN 的几种变体:
如上图所示,来自先前时间点的数据进入了当前时间点的训练。 循环架构使模型可以很好地与时间序列(例如产品销售,股票价格)或顺序输入(例如文章中的单词-DNA 序列)配合使用。
假设我们在传统的神经网络中有一些输入x[t]
(其中t
代表时间步长或顺序顺序),如下图所示。 假设不同t
处的输入彼此独立。 可以将任何时间的网络输出写为h[t] = f(x[t])
,如下所示:
在 RNN 中,反馈回路将当前状态的信息传递到下一个状态,如下图的网络展开版本所示。 RNN 网络在任何时间的输出都可以写成h[t] = f(h[t-1], x[t])
。 对序列的每个元素执行相同的任f
,并且输出h[t]
取决于先前计算的输出h[t-1]
。 得益于这种类似链的架构,或到目前为止所计算出的额外的存储器捕获,在将 RNN 应用于时间序列和序列数据方面取得了巨大的成功。 在将 RNN 的变体应用于各种问题之前,首先,我们将了解 RNN 的历史及其演变路径:
RNN 的演进路径
RNN 实际上具有悠久的历史,最早是在 1980 年代开发的。 霍普菲尔德网络是第一个具有循环链接的神经网络,它是约翰·霍普菲尔德(John Hopfield)在《Neurons with graded response have collective computational properties like those of two-state neurons》中发明的。
受 Hopfield 网络的启发,在《及时发现结构》中引入了全连接神经网络 -- Elman 网络。 Elman 网络具有一个隐藏层和一组连接到该隐藏层的上下文单元。 在每个时间步,上下文单元都会跟踪隐藏单元的先前值。
1992 年,Schmidhuber 由于记住了长期依赖性而发现了梯度消失的问题。 五年后,Schmidhuber 和 Hochreiter 在《LSTM》中提出了长短期记忆(LSTM)。 遗忘门单元增强了 LSTM 的功能,它可以删除旧的和无关的内存,从而避免梯度消失。
在 1997 年,RNN 扩展为双向版本(在《双向循环神经网络》中发表,该模型在正向(从头到尾)和负向(从头到尾)时间方向上训练。
《分层控制如何在人工自适应系统中进行自组织》中介绍的分层 RNN 同时具有水平和垂直循环连接,从而分解复杂和自适应信息。
自 2007 年以来,LSTM 开始盛行:在《循环神经网络的判别性关键词发现应用》中,它们在某些语音识别任务中的表现优于传统模型。 在 2009 年,通过连通性时间分类(CTC)训练的 LSTM 模型用于语音音频中的连接手写识别和音素识别等任务。 LSTM 模型也成为机器翻译和语言建模的最新解决方案。 如《Show and Tell:神经图像字幕生成器》中所述,LSTM 甚至与 CNN 结合以自动进行图像字幕。
早在 2014 年,GRU RNN 被引入,是对常规 RNN 的另一项改进,类似于 LSTM。 GRU RNN 在许多任务上的表现与 LSTM 相似。 但是,它们在较小的数据集上表现出更好的表现,部分原因是它们需要调整的参数较少,而架构中的门却较少。
按照承诺,我们将详细研究 RNN 的变体,然后将其应用于实际问题。
RNN 架构和应用
RNN 可以分为多对一,一对多,多对多(同步)和多对多(基于它们的输入和输出)。 从隐藏层的角度来看,最常用的 RNN 架构包括基本的原始 RNN 和双向的 LSTM 和 GRU。 我们将专注于 RNN 的这四种架构,并将首先通过输入和输出简要提及这四个类别。
不同输入和输出的架构
多对一:多对一架构可能是最直观的。 我们可以在序列中输入尽可能多的元素或时间步长,但是模型在经历整个序列后仅产生一个输出。 下图显示了其一般结构,其中f
表示一个或多个循环层:
多对一架构可用于情感分析,其中该模型读取整个客户评论(例如)并输出情感分数。 类似地,它可以用于在遍历整个音频流之后识别歌曲的流派。
一对多:与多对一 RNN 相反,一对多架构仅接受一个输入并产生一个输出序列。 一对多架构可以表示如下:
像这样的 RNN 通常用于生成序列。 例如,我们可以使用该模型来生成带有起始音符或流派作为唯一输入的音乐。 以类似的方式,我们甚至可以用指定的起始词以莎士比亚风格写诗。
多对多(同步):第三种架构多对多(同步)使每个输入需要一个输出。 正如我们在以下网络流中看到的那样,每个输出都取决于所有先前的输出和当前输入:
多对多(同步)架构的常见用例之一是时间序列预测,在这种情况下,我们希望在给定当前和所有先前输入的情况下,在每个时间步长执行滚动预测。 此架构还广泛用于自然语言处理(NLP)问题,例如命名实体识别(NER),词性(PoS)标记和语音识别。
多对多(不同步):对于多对多架构的不同步版本,该模型在完成读取整个输入序列之前不会生成输出。 结果,输出的数量可以与输入的数量不同。 如下图所示,输出序列Ty
的长度不必等于输入序列Tx
的长度:
在机器翻译中最常见的是不同步的多对多架构。 例如,模型读取英语的整个句子,然后开始生成法语的翻译句子。 另一个流行的用例是提前进行多步预测,要求我们根据所有先前的输入来预测提前几个时间步。
到目前为止,我们已经通过模型输入和输出了解了四种 RNN 架构,我们将在本章的其余部分的实际示例中结合其中的一些,我们将主要讨论隐藏层中的架构,更具体地说,是循环层。
让我们从原始 RNN 开始,这是循环架构的最基本形式。
原始神经网络
标注了权重且展开后的版本的基本 RNN 模型如下所示:
这里,U
是连接输入层和隐藏层的权重,V
是隐藏层和输出层之间的权重,W
是循环层,即反馈层的权重; s[t]
是时间步t
的隐藏状态,x[t]
和h[t]
分别是时间步t
的输入和输出。
请注意,为简便起见,我们从现在开始仅使用一个循环层,但是我们可以将多个循环层堆叠在一起,我们将很快看到。
层之间的关系可以描述如下:
- 基于当前输入
x[t]
和通过s[t] = a(U x[t] + W s[t-1])
的先前隐藏状态s[t-1]
计算时间步长t
和s[t]
的隐藏状态,其中a
是激活函数。 RNN 中隐藏层的激活函数的典型选择包括 tanh 和 ReLU。 - 同样,
s[t-1]
取决于s[t-2]: s[t-1] = a(U x[t] + W s[t-2])
,依此类推。s[1]
也依赖于s[0]
,按照惯例,该s[0]
设置为全零。 - 由于对时间步长具有这种依赖性,因此可以将隐藏状态视为网络的内存,从以前的时间步长中捕获信息。
- 将时间步长
t
的输出计算为h[t] = g(V s[t])
,其中g
是激活函数。 根据执行的任务,它可以是用于二分类的 Sigmoid 函数,用于多类分类的 softmax 函数以及用于回归的简单线性函数。
与传统的神经网络类似,所有的权重U
,V
和W
均使用反向传播算法进行训练。 但是不同之处在于,在当前时间t
上,我们需要计算除当前时间之外的所有先前t-1
个时间步的损失。 这是因为权重由所有时间步共享,并且一个时间步的输出间接依赖于所有先前的时间步,例如权重的梯度。 例如,如果要计算时间步t = 5
的梯度,则需要向后传播前四个时间步,并对五个时间步的梯度求和。 这种特殊的反向传播算法称为时间上的反向传播(BPTT)。
从理论上讲,RNN 可以从输入序列的开头捕获信息,从而增强了时间序列或序列建模的预测能力。 但是,由于梯度梯度消失,原始 RNN 并非如此。 我们将在后面对此进行详细说明,并将了解专门设计用于解决此问题的其他架构,例如 LSTM 和 GRU。 但是现在,让我们假设原始 RNN 在许多情况下都能正常工作,并且获得了一些实践经验,因为它们是任何 RNN 的基本组成部分。
用于文本生成的原始 RNN
如前所述,RNN 通常在 NLP 域中用作语言模型,它在单词序列上分配概率分布,例如机器翻译,PoS 标记和语音识别。 我们将使用一种相当有趣的语言来对问题文本生成进行建模,其中 RNN 模型用于学习指定域的文本序列,然后在所需域中生成全新且合理的文本序列。
基于 RNN 的文本生成器可以接受任何输入文本,例如《哈利波特》等小说,莎士比亚的诗歌以及《星球大战》的电影剧本,并可以生成自己的《哈利波特》,莎士比亚的诗歌和《星球大战》电影剧本。 如果对模型进行了很好的训练,那么人工文本应该是合理的,并且阅读起来应与原始文本相似。 在本部分中,我们将以《战争与和平》和俄罗斯作家列夫·托尔斯泰的小说为例。 随意使用您喜欢的任何书籍进行训练。 我们建议从没有版权保护的书中下载文本数据。 古腾堡计划是一个很好的资源,拥有超过 5.7 万本版权已过期的免费优秀书籍。
首先,我们需要直接从这里下载《战争与和平》的.txt
文件。 或者,我们可以从 Gutenberg 项目下载该文件,但是我们将需要进行一些清理,例如,从文件以及目录中删除开头部分Project Gutenberg EBook
,以及结尾的End of the Project
。
然后,我们读取文件,将文本转换为小写,并通过打印出前 100 个字符来快速查看它:
>>> training_file = 'warpeace_input.txt'
>>> raw_text = open(training_file, 'r').read()
>>> raw_text = raw_text.lower()
>>> raw_text[:100]
'ufeff"well, prince, so genoa and lucca are now just family estates of thenbuonapartes. but i warn you, i'
现在,我们需要计算总共有多少个字符:
>>> n_chars = len(raw_text)
>>> print('Total characters: {}'.format(n_chars))
Total characters: 3196213
然后,我们可以获得唯一的字符和词汇量:
>>> chars = sorted(list(set(raw_text)))
>>> n_vocab = len(chars)
>>> print('Total vocabulary (unique characters): {}'.format(n_vocab))
Total vocabulary (unique characters): 57
>>> print(chars)
['n', ' ', '!', '"', "'", '(', ')', '*', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '=', '?', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '!!CDP!E.agrave!!', '!!CDP!E.auml!!', '!!CDP!E.eacute!!', '!!CDP!E.ecirc!!', 'ufeff']
现在,我们有了一个原始的训练数据集,其中包含超过 300 万个字符和 57 个唯一字符。 但是我们如何将其提供给 RNN 模型呢?
回想一下,在同步多对多架构中,该模型接受序列并同时生成序列。 在我们的例子中,我们可以给模型提供固定长度的字符序列。 输出序列的长度与输入序列的长度相同,并且一个字符从其输入序列偏移。 假设我们从learning
的单词设置序列长度为5
。 现在,我们可以使用输入learn
和输出earni
来构造训练样本。 我们可以在网络中对此进行可视化,如下所示:
我们只是构造了一个训练样本。 对于整个训练集,我们可以将原始文本数据分成相等长度的序列,例如 100。每个字符序列都是训练样本的输入。
接下来,我们以相同的方式将原始文本数据拆分为序列,但是这次从第二个字符开始。 每个结果序列都是训练样本的输出。 例如,给定原始文本deep learning architectures
和5
作为序列长度,我们可以创建五个训练样本,如下所示:
输入 | 输出 |
---|---|
deep_ |
eep_l |
learn |
earni |
ing_a |
ng_ar |
rchit |
chite |
ectur |
cture |
在此,_
表示空间。
请注意,最后一个子序列es
不够长,因此我们可以简单地忽略它。
由于神经网络模型仅吸收数字数据,因此字符的输入和输出序列由单热编码的向量表示。 我们通过将 57 个字符映射到从0
到56
的索引以及另一个相反的索引来创建字典:
>>> index_to_char = dict((i, c) for i, c in enumerate(chars))
>>> char_to_index = dict((c, i) for i, c in enumerate(chars))
>>> print(char_to_index)
{'n': 0, ' ': 1, '!': 2, '"': 3, "'": 4, '(': 5, ')': 6, '*': 7, ',': 8, '-': 9, '.': 10, '/': 11, '0': 12, '1': 13, '2': 14, '3': 15, '4': 16, '5': 17, '6': 18, '7': 19, '8': 20, '9': 21, ':': 22, ';': 23, '=': 24, '?': 25, 'a': 26, 'b': 27, 'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 'h': 33, 'i': 34, 'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 'p': 41, 'q': 42, 'r': 43, 's': 44, 't': 45, 'u': 46, 'v': 47, 'w': 48, 'x': 49, 'y': 50, 'z': 51, '!!CDP!E.agrave!!': 52, '!!CDP!E.auml!!': 53, '!!CDP!E.eacute!!': 54, '!!CDP!E.ecirc!!': 55, 'ufeff': 56}
例如,字符e
成为长度为 57 的向量,索引为30
的为1
,所有其他索引的值为 0s。 准备好角色查找表后,我们可以如下构建训练数据集:
>>> import numpy as np
>>> seq_length = 100
>>> n_seq = int(n_chars / seq_length)
将序列长度设置为 100,我们将获得n_seq
训练样本。 接下来,我们初始化训练输入和输出:
请注意,序列长度具有形状(样本数,序列长度,特征维数)。 由于我们将使用 Keras 进行 RNN 模型训练,因此需要这种形式。
>>> X = np.zeros((n_seq, seq_length, n_vocab))
>>> Y = np.zeros((n_seq, seq_length, n_vocab))
组装每个n_seq
样本:
>>> for i in range(n_seq):
... x_sequence = raw_text[i * seq_length : (i + 1) * seq_length]
... x_sequence_ohe = np.zeros((seq_length, n_vocab))
... for j in range(seq_length):
... char = x_sequence[j]
... index = char_to_index[char]
... x_sequence_ohe[j][index] = 1.
... X[i] = x_sequence_ohe
... y_sequence =
raw_text[i * seq_length + 1 : (i + 1) * seq_length + 1]
... y_sequence_ohe = np.zeros((seq_length, n_vocab))
... for j in range(seq_length):
... char = y_sequence[j]
... index = char_to_index[char]
... y_sequence_ohe[j][index] = 1.
... Y[i] = y_sequence_ohe
>>> X.shape
(31962, 100, 57)
>>> Y.shape
(31962, 100, 57)
同样,每个样本都由单热编码字符的100
元素组成。 我们终于准备好了训练数据集,现在是时候构建我们的原始 RNN 模型了。 让我们训练一个具有两个循环层的模型,如下所示:
每层包含 800 个单元,其中0.3
和 ReLU 的丢弃率作为激活函数。 首先,导入所有必需的模块:
>>> from keras.models import Sequential
>>> from keras.layers.core import Dense, Activation, Dropout
>>> from keras.layers.recurrent import SimpleRNN
>>> from keras.layers.wrappers import TimeDistributed
>>> from keras import optimizers
现在,指定其他超参数,包括批量大小和周期数,以及隐藏层和单元数以及丢弃率:
>>> batch_size = 100
>>> n_layer = 2
>>> hidden_units = 800
>>> n_epoch= 300
>>> dropout = 0.3
现在,创建并编译网络:
>>> model = Sequential()
>>> model.add(SimpleRNN(hidden_units, input_shape=
(None, n_vocab),return_sequences=True, activation='relu'))
>>> model.add(Dropout(dropout))
>>> for i in range(n_layer - 1):
... model.add(SimpleRNN(hidden_units, return_sequences=True,
activation='relu'))
... model.add(Dropout(dropout))
>>> model.add(TimeDistributed(Dense(n_vocab)))
>>> model.add(Activation('softmax'))
关于我们刚刚建立的模型,需要注意以下几点:
return_sequences=True
:循环层的输出变成一个序列,从而实现了我们想要的多对多架构。 否则,它将变成多对一,最后一个元素作为输出。TimeDistributed
:由于循环层的输出是一个序列,而下一层-密集层-不接受顺序输入,因此TimeDistributed
换行用作适配器来解决此问题。Softmax
:之所以使用这种激活,是因为该模型生成了一个单编码字符向量。
至于优化器,我们将选择 RMSprop,其学习率为0.001
:
>>> optimizer = optimizers.RMSprop(lr=0.001, rho=0.9,
epsilon=1e-08, decay=0.0)
>>> model.compile(loss="categorical_crossentropy",optimizer=optimizer)
添加了多类交叉熵的损失度量之后,我们就完成了模型的构建。 我们可以通过使用以下代码来查看模型的摘要:
>>> print(model.summary()) _________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_1 (SimpleRNN) (None, None, 800) 686400
_________________________________________________________________
dropout_1 (Dropout) (None, None, 800) 0
_________________________________________________________________
simple_rnn_2 (SimpleRNN) (None, None, 800) 1280800
_________________________________________________________________
dropout_2 (Dropout) (None, None, 800) 0
_________________________________________________________________
time_distributed_1 (TimeDist (None, None, 57) 45657
_________________________________________________________________
activation_1 (Activation) (None, None, 57) 0
=================================================================
Total params: 2,012,857
Trainable params: 2,012,857
Non-trainable params: 0
_________________________________________________________________
我们有超过 200 万个参数需要训练。 但是,在开始漫长的训练过程之前,最好建立一些回调,以便在训练过程中跟踪模型的统计信息和内部状态。 我们将采用的回调函数包括:
- 模型检查点,它在每个周期后保存模型,以便我们加载最新保存的模型,并在模型意外停止时从那里继续训练。
- 尽早停止,当失去的表现不再改善时,停止训练。
- 定期检查文本生成结果。 我们想看看生成的文本是多么合理,并且训练损失不够明显。
这些函数的定义或初始化如下:
>>> from keras.callbacks import Callback, ModelCheckpoint, EarlyStopping
>>> file_path =file_path =
"weights/weights_epoch_{epoch:03d}_loss_{loss:.4f}.hdf5"
>>> checkpoint = ModelCheckpoint(file_path, monitor='loss',
verbose=1, save_best_only=True, mode='min')
模型检查点将与周期号一起保存,而训练损失则保存在文件名中。 我们还同时监视验证损失,并查看其是否在50
个连续的周期内停止下降:
>>> early_stop = EarlyStopping(monitor='loss', min_delta=0,
patience=50, verbose=1, mode='min')
接下来,我们有用于质量监控的回调。 首先,我们编写一个辅助函数,该函数在给定 RNN 模型的情况下生成任意长度的文本:
>>> def generate_text(model, gen_length, n_vocab, index_to_char):
... """
... Generating text using the RNN model
... @param model: current RNN model
... @param gen_length: number of characters we want to generate
... @param n_vocab: number of unique characters
... @param index_to_char: index to character mapping
... @return: string of text generated
... """
... # Start with a randomly picked character
... index = np.random.randint(n_vocab)
... y_char = [index_to_char[index]]
... X = np.zeros((1, gen_length, n_vocab))
... for i in range(gen_length):
... X[0, i, index] = 1.
... indices = np.argmax(model.predict(
X[:, max(0, i - seq_length -1):i + 1, :])[0], 1)
... index = indices[-1]
... y_char.append(index_to_char[index])
... return ('').join(y_char)
它以随机选择的字符开头。 然后,输入模型根据过去生成的字符来预测剩余的每个gen_length-1
字符,这些字符的长度最大为100
(序列长度)。
现在,我们可以定义callback
类,该类为每个N
个周期生成文本:
>>> class ResultChecker(Callback):
... def __init__(self, model, N, gen_length):
... self.model = model
... self.N = N
... self.gen_length = gen_length
...
... def on_epoch_end(self, epoch, logs={}):
... if epoch % self.N == 0:
... result = generate_text(self.model, self.gen_length,
n_vocab, index_to_char)
... print('nMy War and Peace:n' + result)
现在所有组件都准备就绪,让我们开始训练模型:
>>> model.fit(X, Y, batch_size=batch_size, verbose=1, epochs=n_epoch,
callbacks=[ResultChecker(model, 10, 200), checkpoint, early_stop])
生成器为每个10
周期写入200
字符。 我们可以看到每个周期的进度条,其详细设置为1
(0
是静默模式,2
没有进度条)。
以下是周期1
,11
,51
和101
的结果:
Epoch 1
:
Epoch 1/300
8000/31962 [======>.......................] - ETA: 51s - loss: 2.8891 31962/31962 [==============================] - 67s 2ms/step - loss: 2.1955 My War and Peace:
5 the count of the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the
Epoch 00001: loss improved from inf to 2.19552, saving model to weights/weights_epoch_001_loss_2.19552.hdf5
Epoch 11
:
Epoch 11/300
100/31962 [..............................] - ETA: 1:26 - loss: 1.2321 31962/31962 [==============================] - 66s 2ms/step - loss: 1.2493 My War and Peace:
?" said the countess was a strange the same time the countess was already been and said that he was so strange to the countess was already been and the same time the countess was already been and said Epoch 00011: loss improved from 1.26144 to 1.24933, saving model to weights/weights_epoch_011_loss_1.2493.hdf5
Epoch 51
:
Epoch 51/300
31962/31962 [==============================] - 66s 2ms/step - loss: 1.1562 My War and Peace:
!!CDP!E.agrave!! to see him and the same thing is the same thing to him and the same thing the same thing is the same thing to him and the same thing the same thing is the same thing to him and the same thing the sam Epoch 00051: loss did not improve from 1.14279
Epoch 101
:
Epoch 101/300
31962/31962 [==============================] - 67s 2ms/step - loss: 1.1736 My War and Peace:
= the same thing is to be a soldier in the same way to the soldiers and the same thing is the same to me to see him and the same thing is the same to me to see him and the same thing is the same to me Epoch 00101: loss did not improve from 1.11891
训练在周期203
提前停止:
Epoch 00203: loss did not improve from 1.10864
Epoch 00203: early stopping
在 Tesla K80 GPU 上,每个周期大约需要 1 分钟。 经过约 3.5 小时的训练,损失从2.19552
减少到1.10864
。
它在周期151
生成以下文本:
which was a strange and serious expression of his face and shouting and said that the countess was standing beside him.
"what a battle is a strange and serious and strange and so that the countess was
我们的《战争与和平》读起来不错,尽管有点荒谬。 通过调整此原始 RNN 模型的超参数,我们可以做得更好吗? 绝对可以,但这是不值得的。 正如我们前面提到的,训练一个普通的 RNN 模型来解决需要学习长期依赖关系的问题非常困难-距离较远的步骤之间的依赖关系通常对于预测至关重要。 但是,由于梯度消失的问题,原始 RNN 仅能够捕获序列中几个早期步骤之间的时间依赖性。 LSTM 和 GRU 之类的架构是专门为解决此问题而设计的。 我们将在以下两个部分中说明它们如何随着时间的推移在内存中维护信息。
LSTM RNN
LSTM 的架构的神奇之处在于:在普通循环单元的顶部,增加了一个存储单元和三个信息门以处理长期依赖关系。 LSTM 的循环单元如下所示(我们还提出了一种原始用于比较):
在上图中从左到右,主要组成部分说明如下:
c[t]
是存储单元,它从输入序列的最开始就存储上下文。f
表示遗忘门,它控制来自前一存储状态c[t-1]
的多少信息可以向前传递。 与遗忘门相关的权重包括W[f]
,它与先前的隐藏状态S[t-1]
连接,和u[f]
,它与当前输入x[t]
连接。i
代表输入门,它确定当前输入可以通过多少信息。 权重W[i]
和U[i]
分别将其与先前的隐藏状态和当前输入相连。- tanh 只是隐藏状态的激活函数,并且基于当前输入
x[t]
和先前的隐藏状态s[t-1]
及其相应的权重W[c]
和U[c]
进行计算。 它与原始 RNN 中的a
完全相同。 o
用作输出门,它定义了将内部存储器中的多少信息用作整个循环单元的输出。 同样,W[o]
和U[o]
是关联的权重。
因此,这些组件之间的关系可以概括如下:
- 在时间步
t
处的遗忘门f
的输出被计算为f = sigmoid(U[f] x[t] + W[f] s[t-1])
。 - 将在时间步
t
处输入门i
的输出计算为i = sigmoid(U[i] x[t] + W[i] s[t-1])
。 - 将在时间步
t
处的 tanh 激活c'
的输出计算为c' = sigmoid(U[c] x[t] + W[c] s[t-1])
。 - 在时间步
t
处的输出门o
的输出被计算为o = sigmoid(U[o] x[t] + W[o] s[t-1])
。 - 在时间步
t
处的存储单元c[t]
由c[t] = f .* c[t-1] + i .* c'
更新,其中.*
表示逐元素乘法。 值得注意的是,f
和i
中的 Sigmoid 函数将其输出转换为从0
到1
的范围,从而控制分别通过的先前存储器c[t - 1]
和当前存储器输入c'
的数据比例。 - 最后,在时间步
t
的隐藏状态s[t]
被更新为s[t] = o .* c[t]
。 同样,o
确定用作整个单元输出的更新存储单元c[t]
的比例。
使用随时间的反向传播训练所有四组权重U
和W
,这与原始 RNN 相同。 通过学习三个信息门的权重,网络显式地对长期依赖关系进行建模。 接下来,我们将利用 LSTM 架构并发明更强大的文本生成器。
用于文本生成的 LSTM RNN
在基于 LSTM 的文本生成器中,我们将序列长度增加到 160 个字符,因为它可以更好地处理长序列。 记住要用新的seq_length
= 160 重新生成训练集X
和Y
。
为了轻松地将该模型的表现与以前的原始模型进行比较,我们将保留一个相似的结构-两个循环层,两个循环层均包含800
单元,0.4
的丢弃率和 tanh (默认情况下)作为激活函数:
>>> from keras.layers.recurrent import LSTM
>>> batch_size = 100
>>> n_layer = 2
>>> hidden_units = 800
>>> n_epoch= 300
>>> dropout = 0.4
现在,创建并编译网络:
>>> model = Sequential() >>> model.add(LSTM(hidden_units, input_shape=(None, n_vocab),
return_sequences=True)) >>> model.add(Dropout(dropout)) >>> for i in range(n_layer - 1): ... model.add(LSTM(hidden_units, return_sequences=True)) ... model.add(Dropout(dropout)) >>> model.add(TimeDistributed(Dense(n_vocab))) >>> model.add(Activation('softmax'))
优化器RMSprop
的学习速度为0.001
:
>>> optimizer = optimizers.RMSprop(lr=0.001, rho=0.9,
epsilon=1e-08, decay=0.0) >>> model.compile(loss="categorical_crossentropy", optimizer=optimizer)
让我们总结一下我们刚刚组装的 LSTM 模型:
>>> print(model.summary())
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm_1 (LSTM) (None, None, 800) 2745600
_________________________________________________________________
dropout_1 (Dropout) (None, None, 800) 0
_________________________________________________________________
lstm_2 (LSTM) (None, None, 800) 5123200
_________________________________________________________________
dropout_2 (Dropout) (None, None, 800) 0
_________________________________________________________________
time_distributed_1 (TimeDist (None, None, 57) 45657
_________________________________________________________________
activation_1 (Activation) (None, None, 57) 0
=================================================================
Total params: 7,914,457
Trainable params: 7,914,457
Non-trainable params: 0
_________________________________________________________________
有 800 万个参数需要训练,几乎是原始模型中训练参数的四倍。 让我们开始训练他们:
>>> model.fit(X, Y, batch_size=batch_size, verbose=1, epochs=n_epoch,
callbacks=[ResultChecker(model, 10, 500), checkpoint, early_stop])
生成器为每个10
周期写入500
个字符长的文本。
以下是周期151
,201
和251
的结果:
Epoch 151
:
Epoch 151/300
19976/19976 [==============================] - 250s 12ms/step - loss: 0.7300
My War and Peace:
ing to the countess. "i have nothing to do with him and i have nothing to do with the general," said prince andrew.
"i am so sorry for the princess, i am so since he will not be able to say anything. i saw him long ago. i am so sincerely that i am not to
blame for it. i am sure that something is so much talk about the emperor alexander's personal attention."
"why do you say that?" and she recognized in his son's presence.
"well, and how is she?" asked pierre.
"the prince is very good to make
Epoch 00151: loss improved from 0.73175 to 0.73003, saving model to weights/weights_epoch_151_loss_0.7300.hdf5
Epoch 201
:
Epoch 201/300
19976/19976 [==============================] - 248s 12ms/step - loss: 0.6794 My War and Peace:
was all the same to him. he received a story proved that the count had not yet seen the countess and the other and asked to be able to start a tender man than the world. she was not a family affair and was at the same time as in the same way. a few minutes later the count had been at home with his smile and said: "i am so glad! well, what does that mean? you will see that you are always the same." "you know i have not come to the conclusion that i should like to
send my private result. the prin Epoch 00201: loss improved from 0.68000 to 0.67937, saving model to weights/weights_epoch_151_loss_0.6793.hdf5
Epoch 251
:
Epoch 251/300
19976/19976 [==============================] - 249s 12ms/step - loss: 0.6369 My War and Peace:
nd the countess was sitting in a single look on
her face. "why should you be ashamed?" "why do you say that?" said princess mary. "why didn't you say a word of this?" said prince andrew with a smile. "you would not like that for my sake, prince vasili's son, have you seen the rest of the two?" "well, i am suffering," replied the princess with a sigh. "well, what a delightful norse?" he shouted. the convoy and driving away the flames of the battalions of the first
day of the orthodox russian Epoch 00251: loss improved from 0.63715 to 0.63689, saving model to weights/weights_epoch_251_loss_0.6368.hdf5
最后,在周期300
,训练因0.6001
损失而停止。
每个周期大约需要四到五分钟,才能在 Tesla K80 GPU 上完成。 借助大约 22 个小时的训练,文本生成器借助 LSTM 架构能够编写出更逼真有趣的《战争与和平》脚本。
此外,用于字符生成的 LSTM RNN 不限于文本。 他们可以从任何字符数据中学习,例如源代码,HTML,LaTex,并希望自动编写软件程序,网页和科学论文。
GRU RNN
在 LSTM 之后的十多年中,发明了具有门控机制的替代架构 GRU。 GRU 和 LSTM 的表现相当,在某些情况下一个要优于另一个。 但是,GRU 只有两个信息门,这比 LSTM 稍微复杂一些。 GRU 的循环单元描述如下:
上图中从左到右的关键组件说明如下:
r
表示复位门,它控制要忘记前一存储器s[t-1]
的多少信息。 给定连接到先前隐藏状态s[t-1]
的权重W[r]
和连接到当前输入x[t]
的U[r]
,复位门r
的输出在时间步t
计算为r = sigmoid(U[r] x[t] + W[r] s[t-1])
。p
代表更新门,它确定可以从前一个内存中传递多少信息。 将权重W[p]
和U[p]
分别连接到先前的存储状态和当前输入,将时间t
的更新门p
的输出计算为p = sigmoid(U[p] x[t] + W[p] s[t-1])
。- tanh 是隐藏状态的激活函数,并基于当前输入
x[t]
和先前存储状态的重置进行计算。 给定它们的相应权重W[c]
和U[c]
,将当前存储器c'
在时间步t
的输出计算为c = tanh(U[c] x[t] + W[c] (r .* s[t-1]))
。 - 最后,在时间步
t
的隐藏状态s[t]
被更新为s[t] = (1 - p) .* c' + p .* s[t-1]
。 同样,p
决定用于更新当前内存的先前内存的比例–越接近1
,则保留的内存就越多; 距离0
越近,发生的当前存储器越多。 - 有趣的是,如果
p
是全零向量,而r
是全一向量(例如我们没有明确保留任何先前的内存),则该网络只是一个普通的 RNN。
总体而言,GRU 与 LSTM 非常相似。 它们都使用光栅机制进行长期建模,并且与门相关的参数通过 BPTT 进行训练。 但是,有一些区别值得注意:
- LSTM 中有三个信息门,而 GRU 中只有两个。
- GRU 中的更新门负责输入门和 LSTM 中的遗忘门。
- GRU 中的重置门直接应用于先前的隐藏状态。
- LSTM 显式地对存储单元
c[t]
进行建模,而 GRU 则不。 - 附加的非线性将应用于 LSTM 中更新的隐藏状态。
- LSTM 于 1997 年推出,近年来已得到研究和广泛使用。 GRU 于 2014 年发明,至今尚未得到充分的探索。 这就是为什么 LSTM 比 GRU 更受欢迎的原因,即使不能保证一个 LSTM 胜过另一个。
- 通常认为,与 GSTM 相比,训练 GRU RNN 相对更快并且需要更少的数据,因为 GRU RNN 具有较少的参数。 因此,有人认为 GRU 在训练量较小的情况下效果更好。
尽管还有许多谜团,我们还是将 GRU RNN 应用于十亿(或万亿)美元的问题:股价预测。
用于股价预测的 GRU RNN
预测股票会使许多人感兴趣。 多年以来,已经开发出了大量使用机器学习技术预测股票价格的方法。 例如,在《Python 机器学习示例的》的“第 7 章”中,线性回归,随机森林和支持向量机被用于预测股票价格。 在像这样的传统机器学习解决方案中,特征工程可能是最费力的阶段。 这是手动创建特定于域的特征或信号的过程,这些特征或信号对于定向预测比原始输入更为重要。 典型的发明特征包括x
天移动平均线,一段时间内的波动率和x
天回报率。 相反,基于 RNN 的深度学习解决方案不涉及以手工为特色的手工制作,而是自己找出及时或顺序的关系。 我们将通过使用 GRU RNN 预测道琼斯工业平均指数(DJIA)来展示循环架构的强大功能。
尽管我们强调了深度学习的优势,但我们并未断言深度学习方法优于传统的机器学习方法。 在机器学习中,没有一种适合所有的。
DJIA 由 30 只大型和重要股票(例如 Apple,IBM,GE 和 Goldman Sachs)组成,是全球投资者最常关注的市场指数之一。 它代表了整个美国市场价值的四分之一,这使该项目更加令人兴奋。
我们可以在这个页面上查看其历史每日数据。 其中一些数据的屏幕截图如下:
五个值说明了交易日内股票在一段时间内的走势:开盘价和收盘价,即交易日的起始价和最终价,低点和高位,即股票交易价格范围和交易量,即在一个交易日交易的股票总数。 例如,我们将重点关注使用历史收盘价来预测未来收盘价。 但是,合并其他四个指标也是可行的。
让我们从数据获取和探索开始。 首先,我们将数据从 2001-01-01 下载到 2017-12-31:在同一网页上,将时间段更改为 2001 年 1 月 1 日至 2017 年 12 月 31 日。单击“应用”按钮,最后单击“下载数据”。 然后,我们可以加载并查看数据:
>>> import numpy as np >>> import matplotlib.pyplot as plt >>> import pandas as pd >>> raw_data = pd.read_csv('^DJI.csv') >>> raw_data.head()
Date Open High Low Close 0 2001-01-02 10790.919922 10797.019531 10585.360352 10646.150391 1 2001-01-03 10637.419922 11019.049805 10581.089844 10945.750000 2 2001-01-04 10944.940430 11028.000000 10888.419922 10912.410156 3 2001-01-05 10912.809570 10919.419922 10627.750000 10662.009766 4 2001-01-08 10658.730469 10700.849609 10516.019531 10621.349609
Adj Close Volume 0 10646.150391 253300000 1 10945.750000 420720000 2 10912.410156 382800000 3 10662.009766 272650000 4 10621.349609 225780000
使用以下代码行绘制收盘价数据:
>>> data = raw_data.Close.values >>> len(data) 4276 >>> plt.plot(data) >>> plt.xlabel('Time period') >>> plt.ylabel('Price') >>> plt.show()
前面的代码将创建以下图形:
一年平均有 252 个交易日。 这就是为什么在 17 年中仅选择了 4,276 个数据点的原因。
接下来,我们需要从原始时间序列构造顺序输入,以便提供 RNN 模型,这与我们在文本生成中所做的类似。 回想一下,在“多对一”架构中,该模型采用一个序列,并经过序列中的所有时间步长后产生一个输出。 在我们的案例中,我们可以将过去T
天的价格序列提供给 RNN 模型,并输出第二天的价格。
将价格时间序列表示为x[1]
,x[2]
,...,x[n]
(N = 4276
),并以T = 5
为例。 通过这样做,我们可以创建训练样本,如下所示:
输入 | 输出 |
---|---|
{x[1], x[2], x[3], x[4], x[5]} |
x[6] |
{x[2], x[3], x[4], x[5], x[6]} |
x[7] |
{x[3], x[4], x[5], x[6], x[7]} |
x[8] |
...... | ... |
{x[n - 1], x[n - 2], x[n - 3], x[n - 4], x[n - 5]} |
x[n] |
在这里,我们通过回顾前 5 个交易日(一周)来预测第二天的价格。 我们还在网络中对其进行了描述:
因此,我们实现了序列生成函数:
>>> def generate_seq(data, window_size): ... """ ... Transform input series into input sequences and outputs based
on a specified window size ... @param data: input series ... @param window_size: int ... @return: numpy array of input sequences, numpy array of outputs ... """ ... X, Y = [], [] ... for i in range(window_size, len(data)): ... X.append(data[i - window_size:i]) ... Y.append(data[i]) ... return np.array(X),np.array(Y)
然后,我们以T = 10
构造输入和输出数据集(回溯 2 周):
>>> window_size = 10 >>> X, Y = generate_seq(data, window_size) >>> X.shape (4266, 10) >>> Y.shape (4266,)
接下来,我们将数据分为 70% 的训练和 30% 的测试:
>>> train_ratio = 0.7
>>> train_n = int(len(Y) * train_ratio)
>>> X_train = X[:train_n]
>>> Y_train = Y[:train_n]
>>> X_test = X[train_n:]
>>> Y_test = Y[train_n:]
我们现在可以开始对训练数据进行建模吗? 当然不需要-需要数据缩放或预处理。 从上图可以看出,测试数据与训练数据不成比例,更不用说将来的数据了。 回归模型无法预测超出范围的值。 为了解决这个问题,我们通常使用最大最小缩放x_scaled = (x - x_min) / (x_max / x_min)
将数据缩放到给定范围,例如 0 到 1。 但是,没有可靠的方法来预测股票的x_max
(或x_min
。 这与已知最小值和最大值(例如,图像预测中的 0 和 255)的情况不同。 为了解决这个问题,我们将每个窗口内的价格标准化。 我们只需将时间窗口中的每个价格除以最近的已知价格即可。 再次使用前面的T = 5
示例,我们可以如下预处理训练样本:
输入 | 输出 |
---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
...... | ... |
![]() |
![]() |
我们基本上将绝对值转换为相对值。 预处理函数实现如下:
>>> def scale(X, Y): ... """ ... Scaling the prices within each window ... @param X: input series ... @param Y: outputs ... @return: scaled input series and outputs ... """ ... X_processed, Y_processed = np.copy(X), np.copy(Y) ... for i in range(len(X)): ... x = X[i, -1] ... X_processed[i] /= x ... Y_processed[i] /= x ... return X_processed, Y_processed
扩展训练和测试数据:
>>> X_train_scaled, Y_train_scaled = scale(X_train, Y_train) >>> X_test_scaled, Y_test_scaled = scale(X_test, Y_test)
终于到了构建 GRU RNN 模型的时候了:
>>> from keras.models import Sequential >>> from keras.layers import Dense, GRU >>> from keras import optimizers >>> model = Sequential() >>> model.add(GRU(256, input_shape=(window_size, 1))) >>> model.add(Dense(1))
在这里,由于我们只有 2986 个训练样本,因此我们正在设计一个相对简单的模型,该模型具有一个 256 个单元的循环层。 对于优化器,使用 RMSprop,学习率为 0.006,以最小化均方误差:
>>> optimizer = optimizers.RMSprop(lr=0.0006, rho=0.9,
epsilon=1e-08, decay=0.0) >>> model.compile(loss='mean_squared_error', optimizer=optimizer)
除了早期停止和模型检查点之外,我们还使用 TensorBoard 作为回调函数。 TensorBoard 是 TensorFlow 的表现可视化工具,可提供用于训练和验证指标的动态图:
>>> from keras.callbacks import TensorBoard, EarlyStopping, ModelCheckpoint >>> tensorboard = TensorBoard(log_dir='./logs/run1/', write_graph=True, write_images=False)
验证损失是在提前停止和模型检查点中使用的度量标准:
>>> early_stop = EarlyStopping(monitor='val_loss', min_delta=0, patience=100, verbose=1, mode='min') >>> model_file = "weights/best_model.hdf5" >>> checkpoint = ModelCheckpoint(model_file, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
与往常一样,重塑输入数据以提供 Keras RNN 模型:
>>> X_train_reshaped = X_train_scaled.reshape( (X_train_scaled.shape[0], X_train_scaled.shape[1], 1)) >>> X_test_reshaped = X_test_scaled.reshape( (X_test_scaled.shape[0], X_test_scaled.shape[1], 1))
该模型适合训练集并通过测试集进行了验证,最大历时为300
,批量为100
:
>>> model.fit(X_train_reshaped, Y_train_scaled, validation_data=
(X_test_reshaped, Y_test_scaled), epochs=300, batch_size=100,
verbose=1, callbacks=[tensorboard, early_stop, checkpoint])
以下是周期1
,11
,52
和99
的结果:
Epoch 1
:
Epoch 1/300 2986/2986 [==============================] - 1s 386us/step - loss: 0.0641 - val_loss: 0.0038 Epoch 00001: val_loss improved from inf to 0.00383, saving model to weights/best_model.hdf5
Epoch 11
:
Epoch 11/300 2986/2986 [==============================] - 1s 353us/step - loss: 0.0014 - val_loss: 9.0839e-04 Epoch 00011: val_loss improved from 0.00128 to 0.00091, saving model to weights/best_model.hdf5
Epoch 52
:
Epoch 52/300 2986/2986 [==============================] - 1s 415us/step - loss: 4.2122e-04 - val_loss: 6.0911e-05 Epoch 00052: val_loss improved from 0.00010 to 0.00006, saving model to weights/best_model.hdf5
Epoch 99
:
Epoch 99/300 2986/2986 [==============================] - 1s 391us/step - loss: 2.1644e-04 - val_loss: 5.2291e-05 Epoch 00099: val_loss improved from 0.00005 to 0.00005, saving model to weights/best_model.hdf5
每个周期大约需要 1 秒才能在 CPU(Core i7)上完成。 训练在周期 242 停止,因为验证损失不再减少:
Epoch 241/300 2986/2986 [==============================] - 1s 370us/step - loss: 1.9895e-04 - val_loss: 7.5277e-05 Epoch 00241: val_loss did not improve Epoch 242/300 2986/2986 [==============================] - 1s 368us/step - loss: 1.9372e-04 - val_loss: 9.1636e-05 Epoch 00242: val_loss did not improve Epoch 00242: early stopping
同时,我们可以在终端中输入以下命令行来检出 TensorBoard:
tensorboard --logdir=logs
它返回以下输出:
Starting TensorBoard b'41' on port 6006 (You can navigate to http://192.168.0.12:6006)
如果转到http://192.168.0.12:6006
,您将能够查看一段时间内的训练损失和验证损失。
平滑为 0(无指数平滑)时的训练损失:
平滑为 0(无指数平滑)时的验证损失:
学习进展顺利,两种损失都随着时间而减少。 通过将其与基本事实进行比较,我们可以进一步将预测可视化。 首先,加载我们刚刚获得的最佳模型,并为训练数据和测试数据计算预测:
>>> from keras.models import load_model >>> model = load_model(model_file) >>> pred_train_scaled = model.predict(X_train_reshaped) >>> pred_test_scaled = model.predict(X_test_reshaped)
我们还需要将按比例缩放的预测转换回其原始比例。 我们可以编写一个函数来简化此操作:
>>> def reverse_scale(X, Y_scaled): ... """ ... Convert the scaled outputs to the original scale ... @param X: original input series ... @param Y_scaled: scaled outputs ... @return: outputs in original scale ... """ ... Y_original = np.copy(Y_scaled) ... for i in range(len(X)): ... x = X[i, -1] ... Y_original[i] *= x ... return Y_original
将反向缩放应用于缩放的预测:
>>> pred_train = reverse_scale(X_train, pred_train_scaled) >>> pred_test = reverse_scale(X_test, pred_test_scaled)
最后,绘制预测以及基本事实:
>>> plt.plot(Y) >>> plt.plot(np.concatenate([pred_train, pred_test])) >>> plt.xlabel('Time period') >>> plt.ylabel('Price') >>> plt.legend(['original series','prediction'],loc='center left') >>> plt.show()
前面的代码将创建以下图形:
结果图表明预测是非常准确的。 为了进行比较,我们还使用了仅经过 10 个周期训练的模型,得出了效果不佳的结果:
应该注意的是,该项目主要是为了演示 GRU RNN 的应用,而不是用于实际的股票交易。 实际上,它要复杂得多,应考虑许多外部和内部因素,例如基本面,技术模式,利率,波动性,周期,新闻和情感。
双向 RNN
到目前为止,在 RNN 架构中,输入序列的信息是从过去到当前状态再到未来的一个方向学习的。 它限制了当前状态以利用将来的输入信息。 让我们看一个缺失单词生成的简单示例:
He said, "Machine __ combines computer science and statistics."
对于仅学习前三个单词的 RNN 模型来说,很难生成适合整个句子的下一个单词。 但是,如果给出了剩余单词,则该模型将更好地捕获上下文,并且更有可能预测下一个单词,即learning
。 为了克服单向 RNN 的局限性,引入了双向 RNN(BRNN)。
在 BRNN 中,隐藏层由两个独立的循环层组成。 这两层是相反的方向:一层为正时间方向,也称为正向,其中输入信息从过去流向当前状态。 另一个为负时间方向,也称为反向,其中从将来到当前状态处理输入信息。 下图描述了 BRNN 的一般结构:
在此,f ->
和f <-
分别表示正向和反向循环层。 它们连接在一起,形成隐藏层f
,并保留来自过去和将来状态的信息。
当需要并提供完整的上下文(包括过去和将来的信息)时,BRNN 特别有用。 例如,在词性标记,实体识别或手写识别中,可以通过了解当前单词或字母之后的单词或字母来提高性能。 其他出色的用例包括语音识别,机器翻译和图像字幕。
接下来,我们将 BRNN 和 LSTM 结合起来用于情感分类。 我们将看到捕获移动评论的完整上下文信息是否有助于增强其情感极性。
用于情感分类的双向 RNN
Keras 包含来自 IMDb 的 50,000 条电影评论的数据集,并用情感极性(1
为正,0
为负)标记。 评论已经过预处理,每个词都由字典中的相应索引表示。 字典中的单词将根据整个数据集中的频率进行排序。 例如,编码为4
的单词是数据中第 4 个最常见的单词。 您可以猜测1
代表the
,2
代表and
,并且最高索引用于停用词。
可以通过以下代码获得单词索引字典:
>>> from keras.datasets import imdb >>> word_to_id = imdb.get_word_index()
我们可以使用load_data
函数加载数据,该数据返回两个元组,即训练数据集和测试数据集:
>>> max_words = 5000 >>> (x_train, y_train), (x_test, y_test) =
imdb.load_data(num_words=max_words, skip_top=10, seed=42)
在这里,我们仅考虑前 5,000 个最常用的词,但排除了前 10 个最常用的词。 种子用于复制目的:
>>> print(len(y_train), 'training samples') 25000 training samples >>> print(len(y_test), 'testing samples') 25000 testing samples
您会在某些输入样本中找到许多 2。 这是因为2
用于表示由于我们应用了频率过滤器而被切掉的单词,包括前 10 个最常用的单词和第 5,000 个最常用的单词。 实际单词从索引3
开始。
您可能会注意到输入样本的长度不同; 例如,第一个训练样本中有467
个单词,第二个训练样本中有138
个单词。 但是,训练 RNN 模型需要输入相同长度的样本。 我们需要将序列填充到相同的长度:短样本的末尾用 0 填充,而长样本则被截断。 我们使用pad_sequences
函数,将序列长度指定为200
:
>>> from keras.preprocessing import sequence >>> maxlen = 200 >>> x_train = sequence.pad_sequences(x_train, maxlen=maxlen) >>> x_test = sequence.pad_sequences(x_test, maxlen=maxlen) >>> print('x_train shape:', x_train.shape) x_train shape: (25000, 200) >>> print('x_test shape:', x_test.shape) x_test shape: (25000, 200)
现在,我们有 200 个单词的输入序列,每个单词由0
至4999
的整数表示。 我们可以进行传统的单热编码,但是所生成的 3,000 维稀疏输出将使训练相应的 RNN 模型过慢。 取而代之的是,我们执行单词嵌入以将单词索引转换为较低维的密集向量。 在 Keras 中,我们将嵌入层用作模型的第一层:
>>> from keras.models import Sequential >>> from keras.layers import Embedding >>> model = Sequential() >>> model.add(Embedding(max_words, 128, input_length=maxlen))
嵌入层将5000
(max_words
)值的索引输入转换为 128 维向量。 接下来,我们将双向 RNN 与 LSTM 相结合:
>>> from keras.layers import Dense, Embedding, LSTM, Bidirectional >>> model.add(Bidirectional(LSTM(128, dropout=0.2, recurrent_dropout=0.2)))
我们仅在 LSTM 层上应用Bidirectional
包装器。 LSTM 层具有 128 个隐藏的单元,其中输入单元的丢弃率为 20%,循环连接的丢弃率为 20%。
最后一层生成逻辑输出:
>>> model.add(Dense(1, activation='sigmoid'))
对于优化器,RMSprop
与0.001
的学习率一起使用,以最小化二分类的交叉熵:
>>> optimizer = optimizers.RMSprop(0.001) >>> model.compile(optimizer=optimizer,loss='binary_crossentropy', metrics=['accuracy'])
最后,我们将测试集作为验证数据训练模型,并根据验证损失提前停止训练:
>>> from keras.callbacks import EarlyStopping >>> early_stop = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1, mode='min') >>> model.fit(x_train, y_train, batch_size=32, epochs=100, validation_data=[x_test, y_test], callbacks=[early_stop])
让我们看一下周期1
,5
,8
和15
的日志:
Epoch 1
:
Train on 25000 samples, validate on 25000 samples
Epoch 1/100
5504/25000 [=====>........................] - ETA: 15:04 - loss: 0.6111 - acc: 0.6672 25000/25000 [==============================] - 1411s 56ms/step - loss: 0.4730 - acc: 0.7750 - val_loss: 0.3765 - val_acc: 0.8436
Epoch 5
:
Epoch 5/100
5088/25000 [=====>........................] - ETA: 15:22 - loss: 0.2395 - acc: 0.9025 25000/25000 [==============================] - 1407s 56ms/step - loss: 0.2367 - acc: 0.9070 - val_loss: 0.2869 - val_acc: 0.8848 Epoch 00005: val_loss did not improve from 0.27994
Epoch 8
:
Epoch 8/100
5088/25000 [=====>........................] - ETA: 15:16 - loss: 0.1760 - acc: 0.9347 25000/25000 [==============================] - 1404s 56ms/step - loss: 0.1815 - acc: 0.9314 - val_loss: 0.2703 - val_acc: 0.8960
Epoch 15
:
Epoch 15/100
5408/25000 [=====>........................] - ETA: 15:08 - loss: 0.0936 - acc: 0.9680 25000/25000 [==============================] - 1413s 57ms/step - loss: 0.0975 - acc: 0.9656 - val_loss: 0.3588 - val_acc: 0.8816 Epoch 00015: val_loss did not improve from 0.27034
训练在周期18
停止,因为提前停止触发:
Epoch 00018: val_loss did not improve from 0.27034
Epoch 00018: early stopping
使用 BRNN 和 LSTM 可以达到 89.6% 的测试精度。
总结
我们刚刚完成了关于 DL 架构-RNN 的学习旅程的重要部分! 在本章中,我们更加熟悉了 RNN 及其变体。 我们从 RNN 是什么,RNN 的发展路径以及它们如何成为顺序建模的最新解决方案入手。 我们还研究了四种 RNN 架构,并按输入和输出数据的形式进行了分类,并提供了一些工业示例。
接下来,我们讨论按循环层分类的各种架构,包括原始 RNN,LSTM,GRU 和双向 RNN。 首先,我们应用了原始架构来编写我们自己的《战争与和平》 ,尽管有点荒谬。 我们通过使用 LSTM 架构 RNN 生成了更好的版本。 股票价格预测中采用了另一种内存增强型架构 GRU。
最后,除了过去的信息外,我们还引入了双向架构,该模型允许模型保留序列的过去和将来上下文中的信息。 我们还使用双向 RNN 和 LSTM 进行电影评论情感分类。 在下一章中,我们将探讨 DL 模型的另一项伟大发明:生成对抗网络。
第 4 节:生成对抗网络(GAN)
在本章中,您将学习 GAN 及其基准。 连同 GAN 模型的演变路径,将通过工业示例来说明架构和工程最佳实践。 我们还将探讨如何仅生成一个输出,一个输出是否可以是一个序列以及它是否仅适用于一个输入元素。
本节将介绍以下章节:
- “第 7 章”,“生成对抗网络”
七、生成对抗网络
在本章中,我们将解释最有趣的深度学习模型之一,即生成对抗网络(GAN)。 我们将首先回顾 GAN 是什么以及它们的用途。 在简要介绍了 GAN 模型的演化路径之后,我们将说明各种 GAN 架构以及图像生成示例。
想象一下,您正在模仿一个最初并不了解的艺术品(例如梵高的《星空》)。 您可以随意参加多次。 每次您提交作品时,评委都会向您反馈真实艺术品的外观以及复制品的接近程度。 在最初的几次尝试中,由于您对原始作品的了解非常有限,因此您的作品得分不高。 经过几番尝试,由于法官提供了有用的提示,您的作品越来越接近真实的艺术品。 您会不断尝试和改进,并将法官的反馈纳入您的尝试中,而在最近的几次尝试中,您的工作看起来与原始工作非常接近。 希望您最终能赢得比赛。
GAN 在合成图像,声波,视频和任何其他信号方面几乎具有相同的作用,我们将在本章中对此进行深入探讨。
在本章中,我们将介绍以下主题:
- 什么是 GAN?
- 生成模型
- 对抗训练
- GAN 的演进路径
- 原始 GAN 架构
- 实现原始 GAN
- 生成图像
- 深度卷积 GAN 架构
- 实现深度卷积 GAN
- 条件 GAN 架构
- 实现条件 GAN
- 信息最大化 GAN 架构
- 实现信息最大化 GAN
什么是 GAN?
GAN 可能是深度神经网络最有趣的类型之一。 自从 Goodfellow 等人首次引入 GAN 以来。 2014 年,围绕它们开展的研究项目和应用越来越多,其中一些确实很有趣。 以下是我们挑选的有趣的东西:
- 图像生成,例如猫图片,假名人面孔,甚至是现代艺术品
- 音频或视频合成,例如 DeepMind 的 WaveNets,它们能够为天文图像生成人类语音和 3D 重建
- 时间序列生成,例如用于股票市场预测的医学数据和高频数据
- 统计推断,例如通过分析一堆图片来设计服装的亚马逊算法
那么,为什么 GAN 如此强大? 对于 GAN,最好先讨论生成模型,因为 GAN 基本上是生成模型。
生成模型
机器学习中有两种主要类型的模型,即生成模型和判别模型。 顾名思义,判别模型试图在两个(或多个)类之间区分数据。 例如,我们在“第 4 章”,“CNN 架构”中讨论过的 CNN 模型,学会告诉我们一个图像是猫还是狗,给定其中一个图像, 以及 “第 6 章”,“循环神经网络”中的 RNN 模型经过训练,可以输出给定段落的正面或负面情感。 判别模型着重于根据数据的特征预测数据类别。 相反,生成模型不尝试将特征映射到类,而是在给定特定类的情况下生成特征。 例如,训练高斯混合模型(GMM)以生成适合训练集分布的新数据。 生成模型对给定单个类的特征分布进行建模。 也就是说,可以使用生成模型对数据进行分类,例如朴素贝叶斯和玻尔兹曼机,我们在“第 3 章”,“受限玻尔兹曼机和自编码器”中进行了讨论。 但是,他们的首要任务是弄清楚某些特征的可能性,而不是识别标签。 然后将学习到的特征分布用于分类。 如果仍然感到困惑,这是区分生成模型和判别模型的简单方法:
- 判别模型对寻找边界或规则来分离数据感兴趣
- 生成模型侧重于对数据分布进行建模
GAN 模型由两个网络组成。 一个称为生成器的网络负责生成新的数据样本,从而生成 GAN 生成模型。 另一个网络称为判别器,对生成的数据进行真实性评估。 具体而言,它确定单独生成的样本是否属于真实训练数据集。 再次,GAN 仍然是生成模型,因为它们专注于生成特别感兴趣的数据分布,并且添加了判别器以提供反馈以更好地生成数据。 让我们看看它是如何工作的。
对抗 - 以对抗方式进行训练
如果我们还记得梵高(Van Gogh)的例子,那么 GAN 模型就在做类似的事情。 我们模仿艺术品,而 GAN 中的生成器生成候选对象,例如感兴趣的特定数据分布; 法官评估我们的副本,而 GAN 中的判别器则通过从真实数据分布中区分生成的候选对象和实例来评估生成的候选对象。
GAN 中的生成器和判别器以对抗性方式进行训练,也就是说,它们在零和框架中相互竞争,以便生成模拟真实数据分布的数据。 就像在美术比赛中一样,我们不断改进复制品,以期获得法官的高分,并且法官不断评估我们的工作并提供反馈。 GAN 中的生成器旨在生成被视为来自真实数据分布的合成实例,即使它们是伪造的,而判别器的目标是识别单个实例是否是伪造的(已合成) 或真实的。 从优化的角度来看,生成器的训练目标是增加判别器的错误-判别器犯的错误越多,生成器的表现就越好。 判别器的目的是减少其误差,这是显而易见的。 在每次迭代中,两个网络都使用梯度下降来实现其目标,这并不是什么新鲜事。 有趣的是,每个网络都在试图击败另一个网络,即生成器试图欺骗判别器,而判别器则试图不被欺骗。 最终,来自生成器的合成数据(例如图像,音频,视频,时间序列)(希望)能够欺骗最复杂的判别器,类似于艺术品复制品竞赛,我们能够从最严格的标准中获得最高分但最有帮助的裁判。
实际上,生成器从具有高斯分布的多元高斯分布的预定义分布中抽取随机样本作为最受欢迎的输入,并生成看起来好像可能来自目标分布的数据。 这与复制品竞赛非常相似,在复制品竞赛中,我们最初对艺术品一无所知,并一直朝着像样的复制品努力。
以图像生成为例,下图可以表示 GAN 模型:
GAN 采取的步骤如下:
-
生成器网络从高斯分布中获取随机样本并输出图像。
-
这些生成的图像然后被馈送到判别器网络。
-
判别器网络接收生成的图像和从实际数据集中获取的图像。
-
判别器输出概率,其中上限 1 表示输入图像被认为是真实的,下限 0 表示输入图像被认为是假的。
-
生成器的损失(成本函数)是基于判别器认为真实的伪图像的交叉熵来计算的。
-
判别器的损失(成本函数)是根据被认为是伪造的伪图像的交叉熵加上被认为是真实的真实图像的交叉熵来计算的。
-
对于每个周期,两个网络都经过优化,分别将其各自的损失降至最低。
-
在某个时候,当融合判别器将融合生成器生成的图像视为真实图像时,该模型将得到很好的训练。
-
最终,训练有素的生成器会生成图像作为最终输出,从而模仿真实图像的输入。
GAN 的演进路径
对抗训练的思想可以追溯到 1990 年代早期的工作,例如 Schmidhuber 的《通过可预测性最小化学习阶乘代码》。 2013 年,在《通过受控相互作用学习动物行为的协同进化方法》中提出了无模型推断的对抗模型。 2014 年,Goodfellow 等人在《生成对抗网络》中首次引入了 GAN。
Li 等人(提出动物行为推断的同一作者)在《图灵学习:一种无度量的行为推断方法及其在群体中的应用》中于 2016 年提出了图灵学习 一词。 图灵学习与图灵测试相关,并且是 GAN 的概括,如《概括 GAN:图灵的视角》中所总结。 在图灵学习中,模型不仅限于 GAN 或神经网络; 判别器会影响生成器的输入,在图灵测试中充当询问器。
提出的第一个 GAN 由用于生成器和判别器的全连接层组成。 自从原始架构以来,已经开发了许多新的创新方法,例如,深度卷积 GAN,条件 GAN 和信息最大化的 GAN。 在下一节中,我们将详细研究这些架构,并从头开始实现每个架构。
GAN 架构和实现
如所承诺的,我们将仔细研究我们在前几节中详细提到的 GAN 的变体,并将其应用于实际问题。 最常用的 GAN 包括深度卷积 GAN,条件 GAN 和信息最大化 GAN。 让我们从最基本的架构开始。
原始 GAN
在最基本的 GAN 模型中,生成器和判别器都是全连接神经网络。 原始 GAN 的架构可以描述如下:
生成器的输入是来自特定分布的随机样本,我们通常将其称为噪声或潜在变量。 第二层和后面的几层是隐藏层,在这种情况下,它们是全连接层。 隐藏层通常比其先前的隐藏层具有更多的单元。 输出层的大小与预期的生成大小相同,与实际数据的大小相同。 对于判别器,其输入是真实或生成的数据,其后是一个或多个隐藏层,以及一个单元的输出层。 每个隐藏层通常比其先前的隐藏层具有更少的单元。 通常,生成器和判别器具有相同数量的隐藏层。 而且,两组隐藏层通常是对称的。 例如,原始 GAN 的生成器和判别器如下所示:
现在我们已经了解了什么是普通 GAN,我们可以开始在 TensorFlow 中从头开始实现它们了。 从这里开始,我们将以 MNIST 手写数字数据集为例,以便我们可以应用 GAN 生成我们自己的 MNIST。
我们将从加载用于模型训练的 MNIST 数据集开始:
>>> import numpy as np
>>> import tensorflow as tf
>>> def load_dataset():
... (x_train, y_train), (x_test, y_test) =
tf.keras.datasets.mnist.load_data('./mnist_data')
... train_data = np.concatenate((x_train, x_test), axis=0)
... train_data = train_data / 255.
... train_data = train_data * 2\. - 1
... train_data = train_data.reshape([-1, 28 * 28])
... return train_data
该函数读取并合并原始训练和测试集(不包括标签),因为在原始模型中不需要它们。 它还将数据从[0, 255]
到[-1, 1]
的范围重新缩放,这是神经网络模型预处理的非常重要的一部分,并且还将单个样本重塑为一维样本。
调用此函数并检查已加载数据的大小:
>>> data = load_dataset()
>>> print("Training dataset shape:", data.shape)
Training dataset shape: (70000, 784)
总共有 70,000 个训练样本,每个样本有 784 个尺寸(28 x 28
)。 如果您忘记了 MNIST 数据的外观,我们将使用定义如下的函数显示一些示例:
>>> import matplotlib.pyplot as plt
>>> def display_images(data, image_size=28):
... fig, axes = plt.subplots(4, 10, figsize=(10, 4))
... for i, ax in enumerate(axes.flatten()):
... img = data[i, :]
... img = (img - img.min()) / (img.max() - img.min())
... ax.imshow(img.reshape(image_size, image_size), cmap='gray')
... ax.xaxis.set_visible(False)
... ax.yaxis.set_visible(False)
... plt.subplots_adjust(wspace=0, hspace=0)
... plt.show()
此函数在 4 行 10 列中显示 40 张图像。 稍后将重新使用它以显示生成的图像。 看一下前 40 个真实样本:
>>> display_images(data)
请参考以下屏幕截图以获取最终结果:
在评估我们生成的图像的真实性时,您将需要稍后返回这些图像。
现在,让我们开始构建 GAN 模型。 首先,我们为全连接层定义包装函数,因为它在原始 GAN 中最常使用:
>>> def dense(x, n_outputs, activation=None):
... return tf.layers.dense(x, n_outputs, activation=activation,
kernel_initializer=
tf.random_normal_initializer(mean=0.0,stddev=0.02))
通过放置一些密集层,我们构建了生成器:
>>> def generator(z, alpha=0.2):
... """
... Generator network
... @param z: input of random samples
... @param alpha: leaky relu factor
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... fc1 = dense(z, 256)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 1024)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 28 * 28)
... out = tf.tanh(out)
... return out
生成器将输入的随机噪声依次馈入三个隐藏层,分别具有 256、512 和 1,024 个隐藏单元。 请注意,每个隐藏层的激活函数是泄漏的 ReLU,这是 ReLU 的变体。 发明它是为了解决即将死去的 ReLU 问题,即对于该函数的任何负输入,其输出变为零。 它定义为f(x) = max(x, ax)
,其中a
是介于 0 到 1 之间的斜率因子(但较小的值更常见)。 下图显示了 ReLU 和泄漏版本之间的比较(例如leak = 0.2
):
在三个隐藏层之后,输出层随后进行 tanh 激活,将数据映射到期望图像的相同大小和范围。 类似地,我们用四个密集层构建判别器,其中三个是隐藏层,其大小与生成器中隐藏层的顺序相反:
>>> def discriminator(x, alpha=0.2):
... """
... Discriminator network
... @param x: input samples, can be real or generated samples
... @param alpha: leaky relu factor
... @return: output logits
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... fc1 = dense(x, 1024)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 256)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 1)
... return out
输出层将数据映射到单个单元对率。 现在,我们可以为大小为 784 的实际输入数据和大小为 100 的噪声输入数据定义占位符:
>>> noise_size = 100
>>> tf.reset_default_graph()
>>> X_real = tf.placeholder(tf.float32, (None, 28 * 28), name='input_real')
>>> z = tf.placeholder(tf.float32, (None, noise_size), name='input_noise')
将生成器应用于输入噪声,并将判别器应用于生成的图像以及真实图像数据:
>>> g_sample = generator(z)
>>> d_real_out = discriminator(X_real)
>>> d_fake_out = discriminator(g_sample)
利用网络的所有这些输出,我们为生成器开发了loss
计算,该计算基于被认为是真实的伪造图像:
>>> g_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(logits=d_fake_out,
labels=tf.ones_like(d_fake_out)))
>>> tf.summary.scalar('generator_loss', g_loss)
我们还记录了损失,以便使用 TensorBoard 可视化学习进度。
接下来是用于判别器的loss
计算,该计算基于两个组件:真实图像被视为伪造的图像和伪图像被视为真实的图像:
>>> d_real_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(logits=d_real_out,
labels=tf.ones_like(d_real_out)))
>>> d_fake_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(logits=d_fake_out,
labels=tf.zeros_like(d_fake_out)))
>>> d_loss = d_real_loss + d_fake_loss
>>> tf.summary.scalar('discriminator_loss', d_loss)
同样,我们记录了判别器损失。 然后,我们为两个网络定义优化器,如下所示:
>>> train_vars = tf.trainable_variables()
>>> d_vars = [var for var in train_vars
if var.name.startswith('discriminator')]
>>> g_vars = [var for var in train_vars
if var.name.startswith('generator')]
>>> learning_rate = 0.0002
>>> beta1 = 0.5
>>> with tf.control_dependencies(
tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
... d_opt = tf.train.AdamOptimizer(learning_rate,
beta1=beta1).minimize(d_loss, var_list=d_vars)
... g_opt = tf.train.AdamOptimizer(learning_rate,
beta1=beta1).minimize(g_loss, var_list=g_vars)
优化器实现 Adam 算法,学习率为 0.0002,第一矩衰减率为 0.5。 在进行模型优化之前,不要忘记定义一个函数,该函数返回用于训练的批量数据:
>>> def gen_batches(data, batch_size, shuffle=True):
... """
... Generate batches for training
... @param data: training data
... @param batch_size: batch size
... @param shuffle: shuffle the data or not
... @return: batches generator
... """
... n_data = data.shape[0]
... if shuffle:
... idx = np.arange(n_data)
... np.random.shuffle(idx)
... data = data[idx]
... for i in range(0, n_data, batch_size):
... batch = data[i:i + batch_size]
... yield batch
准备好所有组件之后,我们就可以开始训练 GAN 模型了。 对于每 100 步,我们记录生成器损失和判别器损失。 为了进行表现检查,我们创建了一组噪声输入,并显示了当前生成器针对每个周期生成的图像:
>>> epochs = 100
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter(
'./logdir/vanilla', sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for batch_x in gen_batches(data, batch_size):
... batch_z = np.random.uniform(
-1, 1, size=(batch_size,noise_size))
... _, summary, d_loss_batch = sess.run(
[d_opt, merged, d_loss],
feed_dict={z: batch_z, X_real: batch_x})
... sess.run(g_opt, feed_dict={z: batch_z})
... _, g_loss_batch = sess.run(
[g_opt, g_loss], feed_dict={z: batch_z})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss:
{:.4f}, generator Loss: {:.4f}".format(
... epoch + 1, epochs, d_loss_batch,g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z), feed_dict={z:sample_z})
... display_images(gen_samples)
注意,在每个周期,生成器更新两次,而判别器仅更新一次。 这是因为优化判别器比生成器容易得多,这很直观。 任意图像只是毫不费力地被认为是伪造的。 如果判别器在早期出现收敛,则不完整的生成器将产生垃圾。 您还可以为两个网络指定不同的学习率,例如,生成器的学习率稍高一些,0.001,判别器的学习率则为 0.0002。
请参阅以下屏幕截图以获取第 25 阶段的最终结果:
以下屏幕截图显示了周期 50 的输出:
以下屏幕截图显示了周期 75 的输出:
以下屏幕截图显示了周期 100 的输出:
我们的第一个 GAN 模型能够合成手写数字,并且大多数看起来都是合法的! 我们还来看看 TensorBoard 中的学习图。 要运行 TensorBoard,请在终端中输入以下命令:
tensorboard --logdir=logdir/
然后,在浏览器中转到http://localhost:6006/
; 我们将看到判别器的图表:
以下是生成器的示意图:
您可能会注意到,有些数字看起来很奇怪。 在全连接原始模型的基础上,我们可以做哪些改进? 对于计算机视觉,使用卷积层可能是最直观的方法。
深度卷积 GAN
卷积层已成为解决图像问题的必备条件。 使用 GAN 生成图像也不例外。 因此,Radford 等人在《使用深度卷积生成对抗网络进行无监督表示学习》提出了深度卷积生成对抗网络(DCGAN)。
很容易理解 DCGAN 中的判别器。 它也非常类似于用于分类的标准 CNN,其中使用一个或多个卷积层,每个卷积层之后是一个非线性层,最后是一个全连接层。 例如,我们可以具有以下架构:
卷积层共有三个,分别由 64 个,128 个和 256 个5 x 5
过滤器组成。
如前所述,生成器通常与判别器对称。 DCGAN 中的判别器通过卷积层解释输入图像并生成数字输出。 因此,生成器需要使用转置的卷积层将数字噪声输入转换为图像,这与卷积层完全相反。 卷积层执行下采样,而转置卷积层执行上采样。 例如,我们可以为生成器使用以下架构:
考虑到所有这些概念,让我们实现 DCGAN。 同样,我们从定义卷积和转置卷积层的包装函数开始,因为它们在 DCGAN 中使用最频繁:
>>> def conv2d(x, n_filters, kernel_size=5):
... return tf.layers.conv2d(inputs=x, filters=n_filters,
kernel_size=kernel_size, strides=2, padding="same",
kernel_initializer=tf.random_normal_initializer(
mean=0.0, stddev=0.02))
>>> def transpose_conv2d(x, n_filters, kernel_size=5):
... return tf.layers.conv2d_transpose(inputs=x,
filters=n_filters, kernel_size=kernel_size, strides=2,
padding='same', kernel_initializer=
tf.random_normal_initializer(mean=0.0, stddev=0.02))
我们定义的密集函数可用于全连接层。 同样,我们在卷积层的输出上应用批量归一化。 批量规范化背后的思想类似于输入数据规范化,可加快学习速度。 通过从激活层的输出中减去批量平均值,然后将其除以批量标准差来执行批量标准化。 我们为批量标准化定义了一个包装器函数,如下所示:
>>> def batch_norm(x, training, epsilon=1e-5, momentum=0.9):
... return tf.layers.batch_normalization(x, training=training,
epsilon=epsilon, momentum=momentum)
现在,我们可以使用刚刚定义的组件来构造判别器:
>>> def discriminator(x, alpha=0.2, training=True):
... """
... Discriminator network for DCGAN
... @param x: input samples, can be real or generated samples
... @param alpha: leaky relu factor
... @param training: whether to return the output in training mode
(normalized with statistics of the current batch)
... @return: output logits
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... conv1 = conv2d(x, 64)
... conv1 = tf.nn.leaky_relu(conv1, alpha)
... conv2 = conv2d(conv1, 128)
... conv2 = batch_norm(conv2, training=training)
... conv2 = tf.nn.leaky_relu(conv2, alpha)
... conv3 = conv2d(conv2, 256)
... conv3 = batch_norm(conv3, training=training)
... conv3 = tf.nn.leaky_relu(conv3, alpha)
... fc = tf.layers.flatten(conv3)
... out = dense(fc, 1)
... return out
这很容易。 三个卷积层分别包含 64、128 和 256 个5 x 5
过滤器。
开发生成器有点棘手。 回想一下,我们需要首先将输入的一维噪声整形为三维图像,以启用转置卷积。 我们知道,由于两个网络的对称性,第三维是 256。 那么,前两个维度是什么? 它们是2 x 2
,在第一个转置的卷积层之后变为4 x 4
,第二个之后的为8 x 8
,第三个之后的为16 x 16
,如果它是3 x 3
,则与我们的28 x 28
的目标相去甚远。 同样,它在第三个转置的卷积层之后变为24 x 24
,这又不够大。 如果它是4 x 4
,则在第三个转置的卷积层之后变为32 x 32
。 因此,将线性输入重塑为4 x 4
图像就足够了。 请注意,现在生成的图像输出的大小为32 x 32
,这与我们的真实图像的大小4 x 4
不同。要确保对判别器的输入恒定,我们只需要在真实图像上填充零即可。 在load_dataset
函数的顶部实现了实图像的零填充:
>>> def load_dataset_pad():
... (x_train, y_train), (x_test, y_test)=
tf.keras.datasets.mnist.load_data('./mnist_data')
... train_data = np.concatenate((x_train, x_test), axis=0)
... train_data = train_data / 255.
... train_data = train_data * 2\. - 1
... train_data = train_data.reshape([-1, 28, 28, 1])
... train_data = np.pad(train_data, ((0,0),(2,2),(2,2),(0,0)),
'constant', constant_values=0.)
... return train_data
由于 DCGAN 中的判别器接受三维图像输入,因此训练数据也被重塑为(70000, 32, 32, 1)
:
>>> data = load_dataset_pad()
>>> print("Training dataset shape:", data.shape)
Training dataset shape: (70000, 32, 32, 1)
加载数据后,我们可以继续定义生成器:
>>> def generator(z, n_channel, training=True):
... """
... Generator network for DCGAN
... @param z: input of random samples
... @param n_channel: number of output channels
... @param training: whether to return the output in training mode (normalized with statistics of the current batch)
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... fc = dense(z, 256 * 4 * 4, activation=tf.nn.relu)
... fc = tf.reshape(fc, (-1, 4, 4, 256))
... trans_conv1 = transpose_conv2d(fc, 128)
... trans_conv1 = batch_norm(trans_conv1, training=training)
... trans_conv1 = tf.nn.relu(trans_conv1)
... trans_conv2 = transpose_conv2d(trans_conv1, 64)
... trans_conv2 = batch_norm(trans_conv2, training=training)
... trans_conv2 = tf.nn.relu(trans_conv2)
... trans_conv3 = transpose_conv2d(trans_conv2, n_channel)
... out = tf.tanh(trans_conv3)
... return out
首先,它将噪声输入映射到具有 4,096 个单元的全连接层,以便它可以重塑大小为4 x 4 x 256
的三维数据,然后由三个转置的卷积层消耗。
现在,我们可以为尺寸为28 x 28 x 1
的实际输入数据定义占位符:
>>> image_size = data.shape[1:]
>>> tf.reset_default_graph()
>>> X_real = tf.placeholder(
tf.float32, (None,) + image_size, name='input_real')
噪声输入数据和其余参数与上一节中的相同,因此我们跳过重复相同的代码。 接下来,我们将生成器应用于输入噪声:
>>> g_sample = generator(z, image_size[2])
其余部分,包括图像判别器,损失计算和优化器,将重用上一节中的内容。
准备好所有组件之后,我们现在就可以开始训练我们的 DCGAN 模型了。 同样,我们记录每 100 步的损失,并显示每个周期(此时总共 50 个周期)的合成图像:
>>> epochs = 50
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter(
'./logdir/dcgan', sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for batch_x in gen_batches(data, batch_size):
... batch_z = np.random.uniform(
-1, 1, size=(batch_size, noise_size))
... _, summary, d_loss_batch = sess.run(
[d_opt, merged,d_loss], feed_dict=
{z: batch_z, X_real: batch_x})
... sess.run(g_opt, feed_dict={z: batch_z, X_real:batch_x})
... _, g_loss_batch = sess.run([g_opt, g_loss], feed_dict=
{z: batch_z, X_real: batch_x})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss: {:.4f},
generator Loss: {:.4f}".format(epoch + 1, epochs,
d_loss_batch, g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z, image_size[2],
training=False), feed_dict={z: sample_z})
... display_images(gen_samples, 32)
请参阅以下屏幕截图以获取第 25 阶段的最终结果:
最后,请参考以下屏幕截图,以获取第 50 阶段的最终结果:
TensorBoard 中显示的学习图如下所示:
下图显示了生成器损失:
从我们的 DCGAN 模型生成的图像看起来比从普通 GAN 生成的图像更真实。 我们还将它们与真实图像一起放置; 没有提示,您能告诉哪个集是真集还是假集?
到目前为止,就我们无法控制要产生的 0 到 9 而言,我们生成的数字是相当随机的。 这是因为原始 GAN 和 DCGAN 中的生成器仅吸收随机噪声,只要结果看起来是真实的,就不再限制生成什么。 我们将看到条件 GAN 和 infoGAN 如何启用此功能。
条件 GAN
条件 GAN(CGAN)通过将标签信息馈送到生成器和判别器,从而希望生成特定标签的数据,从而使我们可以控制要生成的内容。 下图显示了 CGAN 的架构:
如我们所见,标签数据是 CGAN 中生成器和判别器的输入空间的扩展。 注意,标签数据表示为一热向量。 例如,MNIST 数据集中的数字 2 变为[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
。 CGAN 的其他部分,例如cost
函数,与常规 GAN 相似。 因此,实现 CGAN 应该很容易。 我们可以开发一个全连接 CGAN,但是 CGAN 中的隐藏层不限于全连接层。 您可以尝试实现卷积版本作为练习。
首先,我们需要修改data load
函数以包含标签:
>>> def load_dataset_label():
... from keras.utils import np_utils
... (x_train, y_train), (x_test, y_test)
=tf.keras.datasets.mnist.load_data('./mnist_data')
... x_data = np.concatenate((x_train, x_test), axis=0)
... y_train = np_utils.to_categorical(y_train)
... y_test = np_utils.to_categorical(y_test)
... y_data = np.concatenate((y_train, y_test), axis=0)
... x_data = x_data / 255.
... x_data = x_data * 2\. - 1
... x_data = x_data.reshape([-1, 28 * 28])
... return x_data, y_data
该函数还将标签数据从一维转换为一热编码的十维:
>>> x_data, y_data = load_dataset_label()
>>> print("Training dataset shape:", x_data.shape) Training dataset shape: (70000, 784)
因此,batch
生成函数也需要更新,以便它返回一批图像和标签:
>>> def gen_batches_label(x_data, y_data, batch_size, shuffle=True):
... """
... Generate batches including label for training
... @param x_data: training data
... @param y_data: training label
... @param batch_size: batch size
... @param shuffle: shuffle the data or not
... @return: batches generator
... """
... n_data = x_data.shape[0]
... if shuffle:
... idx = np.arange(n_data)
... np.random.shuffle(idx)
... x_data = x_data[idx]
... y_data = y_data[idx]
... for i in range(0, n_data - batch_size, batch_size):
... x_batch = x_data[i:i + batch_size]
... y_batch = y_data[i:i + batch_size]
... yield x_batch, y_batch
稍后,我们将标签数据的占位符定义为新输入:
>>> n_classes = 10
>>> y = tf.placeholder(tf.float32, shape=[None, n_classes],name='y_classes')
生成器获取标签数据,并将其与输入噪声连接起来:
>>> def generator(z, y, alpha=0.2):
... """
... Generator network for CGAN
... @param z: input of random samples
... @param y: labels of the input samples
... @param alpha: leaky relu factor
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... z_y = tf.concat([z, y], axis=1)
... fc1 = dense(z_y, 256)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 1024)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 28 * 28)
... out = tf.tanh(out)
... return out
判别器做同样的事情:
>>> def discriminator(x, y, alpha=0.2):
... """
... Discriminator network for CGAN
... @param x: input samples, can be real or generated samples
... @param y: labels of the input samples
... @param alpha: leaky relu factor
... @return: output logits
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... x_y = tf.concat([x, y], axis=1)
... fc1 = dense(x_y, 1024)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 256)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 1)
... return out
现在,我们将y
标签提供给generator
和discriminator
:
>>> g_sample = generator(z, y)
>>> d_real_out = discriminator(X_real, y)
>>> d_fake_out = discriminator(g_sample, y)
为了进行质量检查,我们在给定噪声输入的每个周期合成图像,并带有 10 个类别的集合的标签。 样本标签定义如下:
>>> n_sample_display = 40
>>> sample_y = np.zeros(shape=(n_sample_display, n_classes))
>>> for i in range(n_sample_display):
... j = i % 10
... sample_y[i, j] = 1
训练部分之前的其余代码与原始 GAN 模型中的代码相同。
准备好所有组件之后,我们就可以开始训练 CGAN 模型了:
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter('./logdir/cgan',sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for batch_x, batch_y in gen_batches_label(
x_data, y_data,batch_size):
... batch_z = np.random.uniform(-1, 1,
size=(batch_size, noise_size))
... _, summary, d_loss_batch = sess.run([d_opt, merged,d_loss],
feed_dict={z: batch_z,
X_real: batch_x, y: batch_y})
... sess.run(g_opt, feed_dict={z: batch_z, y: batch_y})
... _, g_loss_batch = sess.run([g_opt, g_loss], feed_dict=
{z: batch_z, y: batch_y})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss: {:.4f},
generator Loss: {:.4f}".format(
... epoch + 1, epochs, d_loss_batch, g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z, y),
feed_dict={z:sample_z, y: sample_y})
... display_images(gen_samples)
请参阅以下屏幕截图,以获取第 50 阶段的最终结果:
并参考以下屏幕截图以获取第 100 个周期的最终结果:
TensorBoard 中显示的学习图如下所示:
下图显示了生成器损失:
使用 CGAN,我们可以完全控制要生成的数字,并且还可以使用 InfoGAN 来控制其他属性,例如宽度或旋转度。
InfoGAN
InfoGANs(最大化生成对抗网络信息的缩写)在某种意义上类似于 CGAN,因为两个生成器网络都接受一个附加参数,并且条件变量c
,例如标签信息。 他们都试图学习相同的条件分布P(X | z, c
。 InfoGAN 与 CGAN 的区别在于它们处理条件变量的方式。
CGAN 认为条件变量是已知的。 因此,在训练期间将条件变量显式馈送到判别器。 相反,InfoGAN 假设条件变量是未知的且是潜在的,我们需要根据训练数据来推断条件变量。 InfoGAN 中的判别器负责推导后验P(c | X)
。 下图显示了 InfoGAN 的架构:
由于我们不需要将条件变量提供给将自动推断出条件的判别器,因此我们可以将其基本分配给任何与数据相关的事物。 它不仅限于标签。 它可以是边缘的宽度,旋转角度和特定样式。 此外,它不仅限于一个变量,也不限于诸如标签之类的分类值。 我们可以有多个变量或一个标签变量,以及一个或多个连续变量作为潜在特征。
那么,InfoGAN 如何学习潜在特征? 顾名思义,它们是通过最大化信息来实现的,信息是指信息论中的互信息。 我们希望最大化c
与生成器生成的输出之间的相互信息。 InfoGAN 的loss
函数可以概括如下:
L_InfoGAN = (D, G) = L(D, G) - I(c, G(z, c))
此处,L(D, G)
是常规 GAN 中的loss
函数,I(c | G(z, c))
是c
和生成的输出之间的互信息。 更准确地,将P(c | G(z, c))
预测为更高的I(c | G(z, c))
。
互信息I(a, b)
衡量如果我们知道b
,我们对a
的了解程度。P(a | b)
(或P(b | a)
)预测得越准确,I(a, b)
越高。 I(a, b) = 0
表示a
和b
完全无关。
取决于潜在变量是什么,可以不同地计算I(c | G(z, c))
。 对于分类变量,它是通过cross
熵来衡量的。 对于连续变量,可以将其计算为其分布(例如高斯分布)之间的方差。
考虑到所有这些概念,我们可以开始开发 InfoGAN 模型。 回想一下,我们不需要在 InfoGAN 中提供标签数据。 因此,我们可以将load_dataset_pad
和gen_batches
函数用作 DCGAN。 让我们像往常一样先加载数据:
>>> data = load_dataset_pad()
>>> print("Training dataset shape:", data.shape)
>>> Training dataset shape: (70000, 32, 32, 1)
现在,我们为条件变量定义一个占位符,作为生成器的额外输入:
>>> n_classes = 10
>>> n_cont = 1
>>> c = tf.placeholder(tf.float32, shape=[None, n_classes + n_cont],
name='conditional_variable')
此示例中的潜在特征包括 10 维一热编码特征和一维连续热特征。 现在,我们定义 InfoGAN 的生成器,它生成条件变量并将其与输入噪声连接起来:
>>> def generator(z, c, n_channel, training=True):
... """
... Generator network for InfoGAN
... @param z: input of random samples
... @param c: latent features for the input samples
... @param n_channel: number of output channels
... @param training: whether to return the output in training mode
(normalized with statistics of the current batch)
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... z_c = tf.concat([z, c], axis=1)
... fc = dense(z_c, 256 * 4 * 4, activation=tf.nn.relu)
... fc = tf.reshape(fc, (-1, 4, 4, 256))
... trans_conv1 = transpose_conv2d(fc, 128)
... trans_conv1 = batch_norm(trans_conv1, training=training)
... trans_conv1 = tf.nn.relu(trans_conv1)
... trans_conv2 = transpose_conv2d(trans_conv1, 64)
... trans_conv2 = batch_norm(trans_conv2, training=training)
... trans_conv2 = tf.nn.relu(trans_conv2)
... trans_conv3 = transpose_conv2d(trans_conv2, n_channel)
... out = tf.tanh(trans_conv3)
... return out
至于判别器,其前半部分与 DCGAN 的判别器相同,后者由三组卷积层组成。 它的后半部分由两个全连接层组成,后面是三组输出:判别器对率(用于确定图像是真实的还是伪造的),连续变量的后验和分类变量的后验:
>>> def discriminator(x, n_classes, n_cont=1, alpha=0.2, training=True):
... """
... Discriminator network for InfoGAN
... @param x: input samples, can be real or generated samples
... @param n_classes: number of categorical latent variables
... @param n_cont: number of continuous latent variables
... @param alpha: leaky relu factor
... @param training: whether to return the output in training mode
(normalized with statistics of the current batch)
... @return: discriminator logits, posterior for the continuous
variable, posterior for the categorical variable
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... conv1 = conv2d(x, 64)
... conv1 = tf.nn.leaky_relu(conv1, alpha)
... conv2 = conv2d(conv1, 128)
... conv2 = batch_norm(conv2, training=training)
... conv2 = tf.nn.leaky_relu(conv2, alpha)
... conv3 = conv2d(conv2, 256)
... conv3 = batch_norm(conv3, training=training)
... conv3 = tf.nn.leaky_relu(conv3, alpha)
... fc1 = tf.layers.flatten(conv3)
... fc1 = dense(fc1, 1024)
... fc1 = batch_norm(fc1, training=training)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 128)
... d_logits = dense(fc2, 1)
... cont = dense(fc2, n_cont)
... classes = dense(fc2, n_classes)
... return d_logits, cont, classes
现在,我们将生成器和判别器应用于输入数据以及条件变量:
>>> g_sample = generator(z, c, image_size[2])
>>> d_real_logits, d_real_cont, d_real_cat = discriminator(
X_real, n_classes, n_cont)
>>> d_fake_logits, d_fake_cont, d_fake_cat = discriminator(
g_sample, n_classes, n_cont)
回想一下,InfoGAN 中的损失函数由两部分组成。 第一部分与标准 GAN 相同:
>>> g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
logits=d_fake_logits, labels=tf.ones_like(d_fake_logits)))
>>> d_real_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(
logits=d_real_logits, labels=tf.ones_like(d_real_logits)))
>>> d_fake_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
logits=d_fake_logits,
labels=tf.zeros_like(d_fake_logits)))
>>> d_loss = d_real_loss + d_fake_loss
第二部分是相互信息。 对于单热编码的分类变量,通过交叉熵来度量:
>>> cat = c[:, n_cont:]
>>> d_cat_loss = tf.reduce_mean(
... tf.nn.softmax_cross_entropy_with_logits(
logits=d_fake_cat, labels=cat))
对于连续变量,可以将其计算为其分布(例如高斯分布)之间的方差:
>>> d_cont_loss = tf.reduce_sum(tf.square(d_fake_cont))
信息损失(与互信息相反)是这两种损失的加权和:
>>> lambda_cont = 0.1
>>> lambda_cat = 1.0
>>> d_info_loss = lambda_cont * d_cont_loss + lambda_cat * cat_loss
生成器和判别器的最终损失如下:
>>> g_loss += d_info_loss
>>> tf.summary.scalar('generator_loss', g_loss)
>>> d_loss += d_info_loss
>>> tf.summary.scalar('discriminator_loss', d_loss)
我们还需要开发一个函数来生成用于训练的随机条件变量:
>>> def gen_condition_variable(n_size, n_classes, n_cont):
... cont = np.random.randn(n_size, n_cont)
... cat = np.zeros((n_size, n_classes))
... cat[range(n_size), np.random.randint(0, n_classes, n_size)] = 1
... return np.concatenate((cont, cat), axis=1)
为了进行质量检查,我们在给定噪声输入的每个周期合成图像,并与一组 10 类的条件变量和一组恒定连续变量进行合成。 样本条件变量的定义如下:
>>> n_sample_display = 40
>>> sample_c = np.zeros((n_sample_display, n_cont + n_classes))
>>> for i in range(n_sample_display):
... j = i % 10
... sample_c[i, j + 1] = 1
... sample_c[i, 0] = -3 + int(i / 10) * 2
第 1,第 11 ,第 21 和第 31 个样本被赋予标签0
,而第 2,第 12,第 22 和第 32 个样本被赋予标签1
,依此类推。 前 10 个样本被赋予连续值-3
,接下来的 10 个样本被赋予-1
,然后被赋予1
,最后被赋予最后的 10 个样本3
。 训练超过 50 个周期如下:
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter('./logdir/infogan',sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for x in gen_batches(data, batch_size):
... batch_z = np.random.uniform(
-1, 1, size=(batch_size, noise_size))
... batch_c = gen_condition_variable(
batch_size, n_classes, n_cont)
... _, summary, d_loss_batch = sess.run([d_opt, merged,
d_loss], feed_dict=
{z: batch_z, X_real: x, c: batch_c})
... sess.run(g_opt, feed_dict=
{z: batch_z, X_real: x, c: batch_c})
... _, g_loss_batch = sess.run([g_opt, g_loss],
feed_dict={z: batch_z, X_real: x, c: batch_c})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss: {:.4f},
generator Loss: {:.4f}".format(
... epoch + 1, epochs, d_loss_batch, g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z, c,image_size[2],
training=False),
feed_dict={z: sample_z, c: sample_c})
... display_images(gen_samples, 32)
请参阅以下屏幕截图,以获取第 20 阶段的最终结果:
有关周期 40 的最终结果,请参考以下屏幕截图:
有关最后一个周期的最终结果,请参考以下屏幕截图:
您可能已经注意到,生成的图像不是 0 到 9 的顺序。我们做错了吗? 幸运的是,没有。 回想一下,条件变量c
在模型之前是未知的,并且是在训练期间推断出来的。 标签 0 不一定表示模型的数字 0。 但是模型获得的知识是类别 0 与任何其他类别都不同。 因此,从 0 到 9 的标签仅代表 10 个不同的类别。 事实证明,在我们的案例中,类别 0 到 9 代表数字 3、0、2、6、4、7、5、8、9、1。连续变量呢? 我们可以看到生成的图像的旋转行为逐行不同,尤其是 0、7、8、9 和 1。第一行(输入连续值-3
的前 10 张图像)显示从垂直轴开始逆时针旋转 20 度。 最后一行(具有输入连续值3
的最后 10 张图像)显示从垂直轴顺时针旋转 20 度。
使用 InfoGAN,除了生成的图像类别之外,我们还扩展了对宽度或旋转等属性的控制。
总结
我们刚刚完成了有关深度学习架构 GAN 的学习旅程的重要部分! 在本章中,我们更加熟悉 GAN 及其变体。 我们从 GAN 入手。 GAN 的演进路径; 以及它们如何在数据合成(例如图像生成,音频和视频生成)中如此流行。 我们还研究了四种 GAN 架构,即原始 GAN,深度卷积 GAN,条件 GAN 和信息最大化的 GAN。 我们从头开始实现了每个 GAN 模型,并使用它们来生成看起来是真实的数字图像。
GAN 是近几年来深度学习的一项伟大发明。 在下一章中,我们将讨论深度学习的其他最新进展,包括贝叶斯神经网络,胶囊网络和元学习。
第 5 节:深度学习和高级人工智能的未来
在本节中,我们想谈谈一些深度学习方面的想法,这些想法我们今年很有影响力,并且将来会更加突出。
本节将介绍以下章节:
- “第 8 章”,“深度学习的新趋势”
八、深度学习的新趋势
在本书的前七章中,具有各种架构的深度神经网络展示了它们从图像,文本和事务数据中学习的能力。 尽管近年来深度学习发展迅速,但它的发展似乎并不会很快减速。 我们看到几乎每个月都会提出新的深度学习架构,并且新的解决方案时不时地成为最新技术。 因此,在最后一章中,我们想谈一谈深度学习中的一些想法,这些想法今年对我们很有影响,并且在将来会更加突出。
在本章中,我们将研究以下主题:
- 贝叶斯神经网络
- 深度学习模型的局限性
- 贝叶斯神经网络的实现
- 胶囊网络
- 卷积神经网络(CNN)的局限性
- 元学习
- 深度学习中的挑战
- 元学习模型的实现
深度学习的新趋势
除了我们在前几章中提到的以外,还有许多其他有趣的深度学习模型和架构难以分类,同时,它们是深度学习的新趋势,并将在未来几年中产生巨大影响 。 在 NLP 中, BERT(代表转换器的双向编码器表示)成为了最新的语言模型(有关更多详细信息,请参阅以下论文,由 Google 发布)。 在计算机视觉方面,GAN 不断获得普及和改进。 他们的发明者 Ian Goodfellow 提出了注意力生成对抗网络,用于更精细地生成图像,其中包括以下三个新趋势:
- 贝叶斯神经网络
- 胶囊网络
- 元学习
贝叶斯神经网络
贝叶斯深度学习结合了贝叶斯学习和深度学习的优点。 它提供了一个深度学习框架,可以实现最先进的表现,同时捕获和建模不确定性。 首先让我们开始理解不确定性的含义,然后我们将继续探讨贝叶斯深度学习如何从不确定性的角度看待事物。
我们的深度学习模型不知道的东西 - 不确定性
不确定性是由于知识有限而无法准确描述未来结果的状态。 在机器学习或深度学习的上下文中,这与预测结果的含糊性有关,或者与人的主观定义和概念有关,而不是自然的客观事实。 不确定性很重要,因为它为我们提供了有关预测的自信程度的信息-如果预测不足或过分虚假,我们可能会拒绝我们的预测。
我们的深度学习模型通常无法估计这种不确定性。 他们产生预测,并盲目接受它们的准确率。 您可能会争辩说,最后一个 Sigmoid 或 softmax 层提供了预测结果的可能性。 这不是信心吗? 概率越高,置信度越高? 不幸的是,这不是信心,因为概率描述了一个结果相对于另一个的结果,但并不能解释整体的信心。
在许多情况下,不知道不确定性可能会带来问题。 例如,2016 年 5 月,一辆 AI 辅助车辆在未能识别出明亮的天空下的白色拖拉机拖车时坠毁。 早在 2016 年,Google 相册中的图像分类系统将两名非洲人标记为大猩猩。 如果将不确定性纳入算法,则可能会避免错误的决策,因为低信度的预测将被拒绝。
我们如何获取不确定性信息 – 贝叶斯神经网络
贝叶斯深度学习将预测与不确定性信息相关联。 让我们看看它是如何工作的。
在我们的常规深度学习模型中,包括权重w
和偏差b
的参数是通过最大似然估计优化的 (MLE):
w = argmax[w] logP(x, y | w)
训练模型后,参数的每个系数(例如w[1], w[2], ..., b[1], b[2], ...
)都是标量,例如w[1] = 1
,w[2] = 3
。
相反,在贝叶斯学习中,每个系数都与一个分布相关联。 例如,它们可以处于高斯分布w[1] ~ N(1, 1), w[2] ~ N(3, 2)
,如下图所示:
关于单个值进行分布的好处是,它可以衡量预测的可信度。 如果使用从分布中采样的不同参数集生成一致的预测,则可以说这些预测具有很高的置信度。 相反,如果在各种样本上的预测不一致,则可以说预测的置信度较低。
估计参数的分布等效于参数的最大后验估计(MAP):
w = argmax[w] logP(w | x, y)
根据贝叶斯规则,可以如下计算P(w | x, y)
:
困难的部分是计算分母,这就是证据。 这需要对w
的所有可能值进行积分。 幸运的是,可以使用蒙特卡洛或变分推断之类的技术对其进行近似。 我们不会在此处提供有关这些技术的详细信息,因为它们不在本书的讨论范围之内。 取而代之的是,我们将使用 TensorFlow 实现贝叶斯神经网络,并演示拥有不确定性信息的力量。
Edward 是我们将用来实现贝叶斯推理的库。 它建立在 TensorFlow 之上,专为概率建模和推断而设计,包括变分推断和蒙特卡洛。 在撰写本文时(2018 年底),Edward 仅与 TensorFlow 1.7 兼容。 因此,在安装 Edward 之前,我们将必须卸载当前版本的 TensorFlow 并安装 1.7 版:
pip uninstall tensorflow
pip install tensorflow==1.7.0.
pip install edward
不要忘了在此部分之后安装最新版本的 TensorFlow。 现在,我们可以执行以下步骤来实现贝叶斯神经网络:
- 让我们导入必要的包:
>>> import numpy as np
>>> import tensorflow as tf
>>> from edward.models import Categorical, Normal
>>> import edward as ed
- 加载训练和测试数据。 在示例中,我们将使用 MNIST 数据集:
>>> def load_dataset():
... (x_train, y_train), (x_test, y_test) =
tf.keras.datasets.mnist.load_data('./mnist_data')
... x_train = x_train / 255.
... x_train = x_train.reshape([-1, 28 * 28])
... x_test = x_test / 255.
... x_test = x_test.reshape([-1, 28 * 28])
... return (x_train, y_train), (x_test, y_test)
>>> (x_train, y_train), (x_test, y_test) = load_dataset()
- 定义一些占位符和变量:
>>> batch_size = 100
>>> n_features = 28 * 28
>>> n_classes = 10
>>> x = tf.placeholder(tf.float32, [None, n_features])
>>> y_ph = tf.placeholder(tf.int32, [batch_size])
- 为简单起见,我们将使用仅具有一个隐藏层的神经网络,以及
w
和b
。 我们分别使用高斯分布为权重和偏差设置先验:
>>> w = Normal(loc=tf.zeros([n_features, n_classes]),
scale=tf.ones([n_features, n_classes]))
>>> b = Normal(loc=tf.zeros(n_classes), scale=tf.ones(n_classes))
loc
参数指定分布的平均值,而scale
参数指定标准差。 然后,我们可以计算预测输出:
>>> y = Categorical(tf.matmul(x, w) + b)
由于模型的参数是与单个值相反的分布,因此输出也应具有分布-特别是分类分布。
- 如前所述,我们使用变分推断来近似
w
和b
的后验。 我们建立了近似分布Q(w)
和Q(b)
,它们将被优化以匹配w
和b
。 两组分布之间的差异通过 Kullback-Leibler(KL)散度来衡量,我们尝试将其最小化。
同样,近似分布Q(w)
和Q(b)
也是高斯分布,但初始位置随机:
>>> qw = Normal(loc=tf.Variable(tf.random_normal(
[n_features, n_classes])), scale=tf.nn.softplus(
tf.Variable(tf.random_normal([n_features, n_classes]))))
>>> qb = Normal(loc=tf.Variable(tf.random_normal([n_classes])),
scale=tf.nn.softplus(tf.Variable(
tf.random_normal([n_classes]))))
- 接下来,我们定义并初始化与 KL 散度的变分推论:
>>> inference = ed.KLqp({w: qw, b: qb}, data={y: y_ph})
>>> inference.initialize(n_iter=100, scale=
{y: float(x_train.shape[0]) / batch_size})
变分推断带有 100 次迭代。
- 不要忘记启动 TensorFlow 会话并初始化该会话的所有变量:
>>> sess = tf.InteractiveSession()
>>> tf.global_variables_initializer().run()
- 现在,我们可以以小批量的方式开始训练贝叶斯网络模型,在此我们可以重用我们在“第 7 章”,“生成对抗网络”中定义的批量生成函数:
>>> def gen_batches_label(x_data, y_data, batch_size, shuffle=True):
... """
... Generate batches including label for training
... @param x_data: training data
... @param y_data: training label
... @param batch_size: batch size
... @param shuffle: shuffle the data or not
... @return: batches generator
... """
... n_data = x_data.shape[0]
... if shuffle:
... idx = np.arange(n_data)
... np.random.shuffle(idx)
... x_data = x_data[idx]
... y_data = y_data[idx]
... for i in range(0, n_data - batch_size, batch_size):
... x_batch = x_data[i:i + batch_size]
... y_batch = y_data[i:i + batch_size]
... yield x_batch, y_batch
>>> for _ in range(inference.n_iter):
... for X_batch, Y_batch in gen_batches_label( x_train, y_train, batch_size):
... inference.update(feed_dict={x: X_batch, y_ph: Y_batch})
- 训练完成后,我们在测试集上评估模型(优化的近似分布
Q(w)
和Q(b)
)。 因为它们是分布,所以我们可以对w
和b
的各种集合进行采样,并计算相应的预测。 我们训练样本 30 次,但总的来说,训练越多越好:
>>> n_samples = 30
>>> pred_samples = []
>>> for _ in range(n_samples):
... w_sample = qw.sample()
... b_sample = qb.sample()
... prob = tf.nn.softmax(
tf.matmul(x_test.astype(np.float32), w_sample)
+ b_sample)
... pred = np.argmax(prob.eval(), axis=1).astype(np.float32)
... pred_samples.append(pred)
- 来自每个采样参数的预测记录在
pred_samples
中。 现在,我们可以计算每组预测的准确率:
>>> acc_samples = []
>>> for pred in pred_samples:
... acc = (pred == y_test).mean() * 100
... acc_samples.append(acc)
30 组预测的准确率如下:
>>> print('The classification accuracy for each sample of w and b:',
acc_samples)
The classification accuracy for each sample of w and b: [90.86999999999999, 90.86, 91.84, 90.88000000000001, 91.33, 91.14999999999999, 90.42, 90.59, 91.36, 91.18, 90.25, 91.22, 89.36, 90.99000000000001, 90.99000000000001, 91.33, 91.2, 91.38, 90.56, 90.75, 90.75, 91.01, 90.96, 91.17, 91.29, 91.03, 91.12, 91.64, 91.44, 90.71000000000001]
单个样本的准确率非常一致,约为 91%。 这表明该模型具有很高的置信度。 我们还可以仔细看一个图像示例来验证这一点。 我们需要拍摄第一个测试图像(即标签7
)并显示它:
>>> image_test_ind = 0
>>> image_test = x_test[image_test_ind]
>>> label_test = y_test[image_test_ind]
>>> print('The label of the image is:', label_test)
The label of the image is: 7
>>> import matplotlib.pyplot as plt
>>> plt.imshow(image_test.reshape((28, 28)),cmap='Blues')
>>> plt.show()
请参考以下屏幕截图以获取最终结果:
此示例的预测如下:
>>> pred_samples_test = [pred[image_test_ind] for pred in pred_samples]
>>> print('The predictions for the example are:', pred_samples_test)
The predictions for the example are: [7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0]
我们还制作了预测的直方图,以更好地可视化它们:
>>> plt.hist(pred_samples_test, bins=range(10))
>>> plt.xticks(np.arange(0,10))
>>> plt.xlim(0, 10)
>>> plt.xlabel("Predictions for the example")
>>> plt.ylabel("Frequency")
>>> plt.show()
请参考以下屏幕截图以获取最终结果:
这些预测是一致且充满信心的。 如果预测不一致,将会是什么样? 我们可以使用 notMNIST 数据集看到这一点。 notMNIST 数据集包含从A
到J
的 529,119 张28 x 28
字母的灰度图像。 请参阅以下屏幕快照以获取数据集:
我们可以从这里下载数据集。 单击MNIST_small.tar.gz
的链接,该链接包含数据集的一小部分,并解压缩该文件。 举一个例子并显示它:
>>> from scipy import ndimage
>>> image_file = 'notMNIST_small/A/MDRiXzA4LnR0Zg==.png'
>>> image_not = ndimage.imread(image_file).astype(float)
>>> plt.imshow(image_not, cmap='Blues')
>>> plt.show()
请参考以下屏幕截图以获取最终结果:
像之前一样对图像进行预处理:
>>> image_not = image_not / 255.
>>> image_not = image_not.reshape([-1, 28 * 28])
同样,我们对经过训练的模型进行采样,并使用每组采样参数进行预测:
>>> pred_samples_not = []
>>> for _ in range(n_samples):
... w_sample = qw.sample()
... b_sample = qb.sample()
... prob = tf.nn.softmax(tf.matmul(
image_not.astype(np.float32),w_sample) + b_sample)
... pred = np.argmax(prob.eval(), axis=1).astype(np.float32)
... pred_samples_not.append(pred[0])
该 notMNIST 示例(A
)的预测如下:
>>> print('The predictions for the notMNIST example are:',
pred_samples_not)
The predictions for the notMNIST example are: [2.0, 5.0, 2.0, 2.0, 2.0, 2.0, 2.0, 3.0, 5.0, 5.0, 8.0, 2.0, 5.0, 5.0, 5.0, 3.0, 2.0, 5.0, 6.0, 2.0, 2.0, 5.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 8.0, 2.0]
再次,我们对预测进行直方图,以更好地可视化它们:
>>> plt.hist(pred_samples_not, bins=range(10))
>>> plt.xticks(np.arange(0,10))
>>> plt.xlim(0,10)
>>> plt.xlabel("Predictions for the notMNIST example")
>>> plt.ylabel("Frequency")
>>> plt.show()
请参考以下屏幕截图以获取最终结果:
这些预测是非常不一致的。 如我们所见,2
,3
,5
,6
和8
到处都是 。 这是我们所期望的,因为基本事实是模型知道的A
,不是 10 类(0
至9
)中的任何一个。 在贝叶斯学习中,预测具有很高的不确定性,因此我们将拒绝我们的预测。 在常规学习中,模型将给出不提供置信度或确定性信息的预测。
胶囊网络
胶囊网络是一种新型的深度神经网络。 它们以类似于 3D 计算机图形的方式处理视觉信息。 它们保留了对象之间的层次关系,而 CNN 无法做到。 尽管 CNN 仍然是大多数计算机视觉任务的首选解决方案,但它们仍面临一些挑战。 让我们谈谈胶囊网络是如何解救的。
卷积神经网络无法做什么
不要误会我的意思-CNN 非常棒,仍然是当今大多数计算机视觉问题的主要解决方案。 通过回顾我们在前几章中研究的内容,CNN 擅长从卷积过滤器激活的像素中检测有效特征,从而模拟人类视觉系统过程图像。 它们在较早的层中检测低级特征(例如边缘和线条),在较后的层中检测高水平特征(例如轮廓和形状)。 但是,它具有根本的局限性-缺乏空间层次结构。
假设我们正在使用 CNN 模型进行人脸检测项目。 训练有素的网络的某些卷积可能识别眼睛,但有些可能是由鼻子,一些耳朵或嘴巴触发的。 只要我们具有所有组件,包括两只眼睛和耳朵,一只鼻子和一只嘴,无论它们的相对位置如何,全连接层都可以识别一张脸。 那就对了! 因此,以下两个图像 A 和 B 都将通过 CNN 人脸检测器:
图片 A:
图片 B:
显然,图像 B 不是脸,因为它的眼睛,鼻子,嘴巴和耳朵遍布各处。 但是,由于 CNN 仅关心某些组件的存在,而不关心这些组件的相对位置及其方向,因此将其分类为人脸。 胶囊网络是由 Geoffrey Hinton 发明的(《胶囊之间的动态路由》)来合并有关组件之间的相对空间关系及其方向的信息。 。
胶囊网络 – 融合方向和相对的空间关系
胶囊网络的发明受到计算机图形学的启发,在计算机图形学中,图像是根据对象的内部几何信息构造的。 此数据存储为代表这些对象的层次结构和相对位置及其方向的几何对象和矩阵的数组。
在 CNN 中,神经元输出标量。 在胶囊网络中,神经元输出一个向量,该向量以下列方式编码所有重要的几何信息:
- 向量的长度表示检测到物体(例如,眼睛,耳朵和鼻子或某种形状)的可能性
- 向量的方向编码对象的方向
以下检测摩天大楼的示例可以帮助您更好地了解向量:
在此简化示例中,两个向量(黑色和蓝色)代表水平矩形和三角形。 我们可以在生成的向量表示图中观察到这一点:存在摩天大楼的左上区域具有最长的向量,并且其方向与摩天大楼的方向匹配; 没有摩天大楼的区域的向量较短,其方向与摩天大楼的方向不同。
这种表示法的优点在于,该模型能够从所有不同的视角检测对象。 假设我们有一个向量,用于检测长度为 0.8 的摩天大楼的水平矩形。 如果我们更改图像中摩天大楼的视点,则向量将随着检测到的对象状态的变化而相应地旋转。 但是,向量的长度保持为 0.8,因为仍然可以检测到对象。 胶囊网络的这种特性称为活动等价性或不变性,而 CNN 不能通过合并来实现。
使用向量而不是比例作为神经元的输出,胶囊网络通过编码从不同层提取的特征之间的层次和几何关系来扩展 CNN。 特征不再是在二维平面上,而是在三维空间内,类似于计算机图形图像。 现在,您可能想知道胶囊如何消耗和产生向量。 让我们比较和总结胶囊中的计算以及下表中传统神经元中的计算:
胶囊不是直接进行加权和,而是首先运行矩阵乘法以获得更高级别的特征。 让我们重用我们的摩天大楼检测示例,并假设我们有一个输入向量u[1]
用于检测矩形,u[2]
用于检测三角形。 然后将这两个向量乘以仿射变换矩阵W
的相应权重向量。 权重向量可能会编码矩形和摩天大楼之间的相对位置,而另一个权重向量可能会尝试了解三角形和摩天大楼之间关系的方向。 进行仿射变换后,我们可以获得较高级别的特征u_hat[1]
和u_hat[2]
,它们根据三角形和矩形的位置和方向表示摩天大楼的位置和方式。
此后,下一步是执行加权和,这听起来很熟悉。 在传统的神经元中,权重是通过反向传播进行优化和计算的。 但是,在捕获中使用动态路由学习权重。 动态路由的主要思想是使用多数表决来确定最可能的对象应该喜欢什么。 接近真实预测的向量的权重c
应具有较高的值,与远离正确预测的向量相对应的权重c
应具有较低的值。
最后,胶囊中的激活函数也是新的东西。 回想一下,在传统的神经元中,典型的激活函数包括 Sigmoid,tanh 和 ReLU,其主要目的是增加非线性并重新调整输入。 同样,对于胶囊,激活函数的后半部分将输入向量的长度转换为 1,但不改变其方向; 激活函数的前半部分(称为压缩)进一步缩放长度,使其不等于 1,同时增加了一些非线性。
这就是胶囊中计算的工作方式。 总而言之,胶囊网络采用功能强大的表示形式-向量-对不同层上重要的特征层次信息进行编码,这在使用标量的传统 CNN 中不可用。 最后但并非最不重要的一点是,如果您对自己实现胶囊网络感兴趣,请随时查看有关 Keras 和 CapsLayer 的以下链接:
元学习
元学习是深度学习研究中另一个令人兴奋的趋势。 就像传统学习一样,它不仅仅针对特定任务在庞大的数据集上进行训练。 它试图利用从任务分配中学到的过去经验来模仿人类的学习过程。 即使只有少量训练样本,它也可以实现良好的表现。 但是,传统的深度学习方法无法做到这一点。
深度学习的一大挑战 – 训练数据
您可能已经看到了以下图表,在给定各种训练数据的情况下,将深度学习与传统机器学习算法之间的表现进行了比较:
仅有少量训练数据可用,深度学习算法通常不会比传统机器学习算法起作用或表现更差。 有了足够的数据,深度学习就开始击败传统学习。 随着数据量的增加,它对表现差距的影响也越来越大。 这里的明确信息是,深度学习通常需要足够数量的训练数据才能获得良好的表现。
深度学习之所以需要巨大的数据集,是因为它使用该数据集从头开始训练模型(或模型的某些关键部分)。 您在这里看到问题了吗? 显然,与我们人类不同,传统的深度学习算法无法从少量数据样本中快速学习,而人类则可以利用过去的经验来快速学习。 我们可以从以下两个方面总结一下机器学习(ML)/ 深度学习(DL)与人类:
- 样本效率:深度学习的样本效率低,因为它需要来自各个类别的足够的样本用于分类任务,而对于回归任务则需要足够的变化。 例如,我们通常在每位数数千个样本上训练一个手写数字识别器。
- 可移植性:对于特定任务,深度学习不会从以前的经验中学习,也不会利用从同一任务中学到的知识。 您可能会争辩说,迁移学习就像利用以前的经验一样,但是请记住,迁移的知识是从其他任务中获得的,我们仍然需要使用庞大的数据集来微调模型的关键部分。
那么,我们如何才能使深度学习接近人类的学习方式-学习呢? 当我们没有足够的训练数据时,如何还能有效地学习? 答案是元学习。
元学习 – 学习如何学习
元学习是机器学习中的现代子领域。 它使用有关机器学习实验的元数据来自动解决具有类似属性的新学习问题。 在元学习中,训练仅限于来自单个分布的样本,并且不会因训练规模小而受到损害。 就像孩子们只看过几次之后如何分辨猫和狗一样。 另一方面,元学习超出了一项任务。 它了解有关任务的分布情况,因此它仍可用于从未介绍过的任务。 就像孩子一样,有了从猫和狗那里学到的知识,他们就可以区分老虎和狼,而以前却看不到任何老虎或狼。 简而言之,元学习学习如何学习。 它试图建立在如何解决任务的知识(元数据)上。
在元学习中,模型经过各种任务训练。 每个学习任务都与包含输入特征和目标变量的数据集相关联。 学习的目的是优化模型,以使这些任务的分配损失最小。 请注意,此处的模型表示高级优化器,可更新低级模型的权重。 即,训练高级模型以学习低级模型。 一项任务的一个数据集在整个过程中被视为一个数据样本。
元学习有许多不同的方法,因为元学习仍然是一个宽松术语,是一个相对较新的概念。 最受欢迎的包括:
- 基于模型学习的元学习:这是一个顺序模型,只需几个步骤即可更新其参数。 可以在记忆增强神经网络(MANN)中找到典型的模型学习架构,如下图所示,其外观与 RNN 相似。 在 MANN 中,模型的最后一步将提取要预测的样本。 在此之前,每个步骤都需要获取训练样本,并且样本的标签会在上一步中输入。 有了这种显式的存储缓冲区(也称为一步偏移),网络就可以记住学习经验,而不仅仅是从特征到标签的简单映射。 而且,由于我们可以通过随机选择不同的训练数据点
(X, Y)
并按顺序排列它们来轻松生成大量的训练案例,因此不必担心这种训练规模的限制。 下图描述了一个基于模型的元学习示例:
- 基于度量的学习,我们将在下一节中详细讨论。
基于指标的元学习
在有限的训练数据的情况下,我们应该做的是提取最重要的信息,而不要过度这样做。 基于度量的元学习试图实现相同的目标。 它利用度量或距离函数在正确的级别上提取特征。
典型的基于度量的学习模型是连体神经网络。 如下图的架构所示,它由两个相同的网络组成,它们共享相同的权重和参数以分别从两个输入中提取特征:
然后,从连体网络中提取的特征将输入判别器,以确定两个输入数据样本是否属于同一类。 判别器首先计算特征对之间的距离(例如 L1 距离,L2 距离,余弦相似度)。 然后,该距离将通过 Sigmoid 激活传递到全连接层,以产生两个输入来自同一类别的概率。
简而言之,连体网络试图学习有效的特征,以便揭示两个输入之间的关系。 为了帮助更好地了解这个基于度量的网络,我们将实现它,并将其应用于数据量有限的人脸识别项目。
标准的面部识别系统应该能够仅使用系统中该人的几张照片来识别该人的身份。 显然,为了积累足够的训练数据,不能强迫他拍摄数百张照片。 尽管这是一个多类别的分类问题,但在这种情况下,CNN 不能在一个很小的训练集上真正起作用,因为每个类别的样本要多得多。 而且,如果我们采用典型的多类分类路线,则每次新用户加入系统时都必须对模型进行重新训练,这是不切实际的。 幸运的是,连体神经网络擅长处理少样本学习问题。
我们将使用 AT&T 的人脸数据库(也称为 ORL 人脸数据库)的一个例子。 从这里下载数据集并解压缩。 提取的文件夹有 40 个子文件夹,从s1
到s40
,它们代表 40 个主题。 每个子文件夹包含 10 个图像文件1.pgm
,2.pgm
,...,10.pgm
。 因此,总共只有来自 40 个个体的 400 个图像样本。 同样,要使典型的 CNN 分类器仅使用 400 个训练样本几乎是不可能的。
每个图像的尺寸为92 * 112
,并具有 256 个灰度等级。 图像文件为 PGM 格式。 让我们开始使用PIL
包阅读并显示一个样本。 PIL
代表 Python Imaging Library,如果尚未安装,则可以使用以下命令进行安装:
pip install Pillow
现在,阅读并显示一个图像样本:
>>> from PIL import Image
>>> img = Image.open('./orl_faces/s1/1.pgm')
>>> print(img.size)
(92, 112)
>>> img.show()
首先,我们编写一个函数以将所有图像及其主题 ID 加载到字典中,其中键是主题 ID,值是 10 张图像的列表:
>>> image_size = [92, 112, 1]
>>> def load_images_ids(path='./orl_faces'):
... id_image = {}
... for id in range(1, 41):
... id_image[id] = []
... for image_id in range(1, 11):
... img = Image.open('{}/s{}/{}.pgm'.format(
path, id, image_id))
... img = np.array(img).reshape(image_size)
... id_image[id].append(img)
... return id_image
注意,每个图像矩阵都被重塑为尺寸[92, 112, 1]
。
现在,加载所有图像及其主题 ID:
>>> id_image = load_images_ids()
我们仅用 400 个样本处理该项目的方法是将其从多类分类转换为二分类问题。 我们不是直接预测图像属于 40 个对象中的哪个,而是预测图像属于各个对象的概率,而概率最高的那个将成为最终结果。 图像属于对象的概率是根据图像与对象样本之间的距离得出的。 下图说明了预测过程:
给定来自未知主题的图像,我们将其分别与来自三个主题的每个图像进行比较(假设系统中只有三个主题)。 我们将每对图像馈送到连体神经网络并获得概率分数。 具有对象 1 的图像的图像对获得最高概率。 因此,查询图像很可能来自主题 1。使用这种策略,我们将能够从 400 张图像中生成更多的训练样本(或者具体来说,是成对的),以适合连体神经网络。 。 例如,我们最多可以为每个主题创建10 * 9/2 = 45
个唯一阳性对,或者对于所有 40 个主题总共创建45 * 40 = 1800
个唯一阳性对。 类似地,我们可以为每对主题形成10 * 10 = 100
个唯一的阴性对,或者对于所有可能的主题对总共形成100 * (40 * 39/2) = 78,000
个唯一的阴性对。 最后,我们可以获得数百个训练样本,这确实提高了人脸识别表现。
现在,让我们通过执行以下步骤在 Keras 中构建连体神经网络:
- 首先,导入所需的所有模块:
>>> from keras import backend as K
>>> from keras.layers import Activation
>>> from keras.layers import Input, Lambda, Dense, Dropout,
Convolution2D, MaxPooling2D, Flatten
>>> from keras.models import Sequential, Model
- 我们的连体网络由卷积层组成,因为我们正在处理图像,最大池化层和全连接层。 这种架构的连体网络被称为卷积连体神经网络:
>>> def siamese_network():
... seq = Sequential()
... nb_filter = 16
... kernel_size = 6
... # Convolution layer
... seq.add(Convolution2D(nb_filter, (kernel_size, kernel_size), input_shape=image_size, border_mode='valid'))
... seq.add(Activation('relu'))
... seq.add(MaxPooling2D(pool_size=(2, 2)))
... seq.add(Dropout(.25))
... # flatten
... seq.add(Flatten())
... seq.add(Dense(50, activation='relu'))
... seq.add(Dropout(0.1))
... return seq
- 然后,我们可以定义输入占位符,两个图像,并将它们传递给连体网络:
>>> img_1 = Input(shape=image_size)
>>> img_2 = Input(shape=image_size)
>>> base_network = siamese_network()
>>> feature_1 = base_network(img_1)
>>> feature_2 = base_network(img_2)
- 然后,我们需要计算这两个结果特征向量之间的距离(我们使用 L1 距离),并将结果映射到概率输出:
>>> distance_function = lambda x: K.abs(x[0] - x[1])
>>> distance = Lambda(distance_function, output_shape=
lambda x: x[0])([feature_1, feature_2])
>>> prediction = Dense(1, activation='sigmoid')(distance)
- 最后,我们需要用输入和输出占位符包装 Keras 模型,并使用基于 Adam 的优化器和交叉熵作为损失函数对其进行编译:
>>> model = Model(input=[img_1, img_2], output=prediction)
>>> from keras.losses import binary_crossentropy
>>> from keras.optimizers import Adam
>>> optimizer = Adam(lr=0.001)
>>> model.compile(loss=binary_crossentropy, optimizer=optimizer)
- 现在该模型已准备好进行训练,我们需要使用以下函数构建训练集,其中从各个受试者中随机选择正对,从两个不同受试者中随机选择负对。 正负比率保持在 50:50:
>>> np.random.seed(42)
>>> def gen_train_data(n, id_image):
... X_1, X_2 = [], []
... Y = [1] * (n // 2) + [0] * (n // 2)
... # generate positive samples
... ids = np.random.choice(range(1, 41), n // 2)
... for id in ids:
... two_image_ids = np.random.choice(range(10), 2, False)
... X_1.append(id_image[id][two_image_ids[0]])
... X_2.append(id_image[id][two_image_ids[1]])
... # generate negative samples, by randomly selecting two
images from two ids
... for _ in range(n // 2):
... two_ids = np.random.choice(range(1, 41), 2, False)
... two_image_ids = np.random.randint(0, 10, 2)
... X_1.append(id_image[two_ids[0]][two_image_ids[0]])
... X_2.append(id_image[two_ids[1]][two_image_ids[1]])
... X_1 = np.array(X_1).reshape([n] + image_size) / 255
... X_2 = np.array(X_2).reshape([n] + image_size) / 255
... Y = np.array(Y)
... return [X_1, X_2], Y
我们获得 8,000 个训练样本:
>>> X_train, Y_train = gen_train_data(8000, id_image)
- 现在,我们可以将模型拟合到训练集上,具有 10 个周期和 10% 的数据拆分以进行验证:
>>> epochs = 10
>>> model.fit(X_train, Y_train, validation_split=0.1, batch_size=64, verbose=1, epochs=epochs)
Epoch 1/10
7200/7200 [==============================] - 71s 10ms/step - loss: 0.5168 - val_loss: 0.3305
Epoch 2/10
7200/7200 [==============================] - 62s 9ms/step - loss: 0.3259 - val_loss: 0.2210
Epoch 3/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.2467 - val_loss: 0.2219
Epoch 4/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.2089 - val_loss: 0.1669
Epoch 5/10
7200/7200 [==============================] - 60s 8ms/step - loss: 0.1920 - val_loss: 0.1521
Epoch 6/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.1534 - val_loss: 0.1441
Epoch 7/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.1509 - val_loss: 0.1124
Epoch 8/10
7200/7200 [==============================] - 58s 8ms/step - loss: 0.1408 - val_loss: 0.1323
Epoch 9/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.1281 - val_loss: 0.1360
Epoch 10/10
7200/7200 [==============================] - 58s 8ms/step - loss: 0.1215 - val_loss: 0.1736
训练和验证损失看起来都不错,但是我们仍然需要在真实环境中进行测试。 假设有n
个受试者,则在一个测试用例中将有n
个样本。 在这些n
个样本中,一个样本是地面真实对,由查询图像和同一主题的图像组成; 其他n-1
个样本由查询图像和其他主题的图像组成。 例如,假设我们有四个主题,A
,B
,C
和D
,及其图像a
,b
,c
和d
。 给定未知图像x
,一个测试案例可以是[x, a]
,[x, b]
,[x, c]
和[x, d]
。 在元学习中,给定n
唯一类,这样的任务称为四路学习或 N 路学习。
首先,让我们定义一个生成测试用例的函数:
>>> def gen_test_case(n_way):
... ids = np.random.choice(range(1, 41), n_way)
... id_1 = ids[0]
... image_1 = np.random.randint(0, 10, 1)[0]
... image_2 = np.random.randint(image_1 + 1, 9 + image_1, 1)[0] % 10
... X_1 = [id_image[id_1][image_1]]
... X_2 = [id_image[id_1][image_2]]
... for id_2 in ids[1:]:
... image_2 = np.random.randint(0, 10, 1)[0]
... X_1.append(id_image[id_1][image_1])
... X_2.append(id_image[id_2][image_2])
... X_1 = np.array(X_1).reshape([n_way] + image_size) / 255
... X_2 = np.array(X_2).reshape([n_way] + image_size) / 255
... return [X_1, X_2]
请注意,为方便起见,我们将地面真值样本放在第一位,而错误的样本放在其余部分。
我们将以 4、9、16、25、36 和 40 方式的学习方式评估模型。 在每种学习方式中,我们重复实验 1,000 次并计算准确率。 为了进行比较,我们使用 K 最近邻(KNN)作为基准模型来计算识别表现,我们定义如下:
>>> def knn(X):
... distances = [np.linalg.norm(x_1 - x_2)
for x_1, x_2 in zip(X[0], X[1])]
... pred = np.argmin(distances)
... return pred
最后,我们开始测试:
>>> n_experiment = 1000
>>> for n_way in [4, 9, 16, 25, 36, 40]:
... n_correct_snn = 0
... n_correct_knn = 0
... for _ in range(n_experiment):
... X_test = gen_test_case(n_way)
... pred = model.predict(X_test)
... pred_id = np.argmax(pred)
... if pred_id == 0:
... n_correct_snn += 1
... if knn(X_test) == 0:
... n_correct_knn += 1
... print('{}-way few shot learning accuracy: {}'.format(
n_way, n_correct_snn / n_experiment))
... print('Baseline accuracy with knn: {}\n'.format(
n_correct_knn / n_experiment))
4-way few shot learning accuracy: 0.963
Baseline accuracy with knn: 0.876
9-way few shot learning accuracy: 0.931
Baseline accuracy with knn: 0.752
16-way few shot learning accuracy: 0.845
Baseline accuracy with knn: 0.663
25-way few shot learning accuracy: 0.767
Baseline accuracy with knn: 0.55
36-way few shot learning accuracy: 0.679
Baseline accuracy with knn: 0.497
40-way few shot learning accuracy: 0.659
Baseline accuracy with knn: 0.478
我们的连体元学习模型大大优于基线。 我们还可以观察到,n
越高,要考虑的类别越多,因此预测正确的类别越困难。
总结
这是我们 DL 架构的最后一站,也是 DL 历程中的新趋势。 在本章中,我们了解到贝叶斯深度学习结合了贝叶斯学习和深度学习的优点。 它对不确定性进行建模,以某种方式告诉我们我们对预测的信任程度。 胶囊网络捕获对象之间的东方和相对空间关系。 我们相信,它们将来会变得更加成熟和流行。
元学习(即学习如何学习)是 DL 研究社区中令人兴奋的主题。 我们已经实现了元学习模型,即带有 Keras 的连体神经网络,并将其应用于人脸识别问题。 实际上,DL 中还有许多其他有趣的事情值得研究,例如深度强化学习,主动学习和自动机器学习。 在阅读本书时,您还发现其他趋势吗?
标签:指南,loss,架构,...,Python,batch,tf,data,我们 From: https://www.cnblogs.com/apachecn/p/17325460.html