深度学习示例(四)
原文:
annas-archive.org/md5/81c037237f3318d7e4e398047d4d8413译者:飞龙
第十二章:神经情感分析
在本章中,我们将讨论自然语言处理领域中的一个热门应用,即情感分析。如今,大多数人通过社交媒体平台表达他们的意见,利用这一海量文本来跟踪客户对某事的满意度,对于公司甚至政府来说都是非常重要的。
在本章中,我们将使用递归类型的神经网络来构建情感分析解决方案。本章将涉及以下主题:
-
一般的情感分析架构
-
情感分析——模型实现
一般的情感分析架构
在本节中,我们将重点讨论可以用于情感分析的一般深度学习架构。下图展示了构建情感分析模型所需的处理步骤。
所以,首先,我们将处理自然语言:
图 1:情感分析解决方案或基于序列的自然语言解决方案的一般管道
我们将使用电影评论来构建这个情感分析应用程序。这个应用程序的目标是根据输入的原始文本生成正面或负面评论。例如,如果原始文本是类似于这部电影很好的内容,那么我们需要模型为其生成一个正面情感。
一个情感分析应用程序将带我们经历许多必要的处理步骤,这些步骤是神经网络中处理自然语言所必需的,例如词嵌入。
所以在这种情况下,我们有一个原始文本,例如这不是一部好电影! 我们希望最终得到的是它是负面还是正面的情感。
在这种类型的应用程序中,有几个难点:
-
其中之一是序列可能具有不同的长度。这是一个非常短的序列,但我们会看到一些文本,它们的长度超过了 500 个单词。
-
另一个问题是,如果我们仅仅看单独的单词(例如,good),它表示的是正面情感。然而,它前面有一个not,所以现在它变成了负面情感。这可能变得更加复杂,我们稍后会看到一个例子。
正如我们在上一章所学的,神经网络无法直接处理原始文本,因此我们需要先将其转换成所谓的词元。这些基本上就是整数值,所以我们遍历整个数据集,统计每个词出现的次数。然后,我们创建一个词汇表,每个词在该词汇表中都会有一个索引。因此,单词this有一个整数 ID 或词元11,单词is的词元是6,not的词元是21,依此类推。现在,我们已经将原始文本转换成了一个由整数构成的列表,称为词元。
神经网络仍然无法处理这些数据,因为如果我们有一个包含 10,000 个单词的词汇表,那么这些标记的值可以在 0 到 9,999 之间变化,而它们之间可能没有任何关联。因此,单词编号 998 和单词编号 999 的语义可能完全不同。
因此,我们将使用上一章学习的表示学习或嵌入的概念。这个嵌入层将整数标记转换为实值向量,例如,标记11变为向量[0.67,0.36,...,0.39],如图 1所示。对于下一个标记 6 也是如此。
我们在上一章学习内容的简要回顾:前面图中的嵌入层学习的是标记(tokens)与其对应的实值向量之间的映射关系。同时,嵌入层还学习单词的语义含义,使得具有相似含义的单词在这个嵌入空间中会彼此接近。
从原始输入文本中,我们得到一个二维矩阵或张量,它现在可以作为输入传递给递归神经网络(RNN)。该网络可以处理任意长度的序列,其输出随后会传递到一个全连接层或密集层,并使用 sigmoid 激活函数。因此,输出值介于 0 和 1 之间,其中 0 表示负面情感。那么,如果 sigmoid 函数的值既不是 0 也不是 1 该怎么办?此时我们需要引入一个中间的切割值或阈值,当该值低于 0.5 时,认为对应的输入是负面情感,而当该值高于此阈值时,则认为是正面情感。
RNN——情感分析的背景
现在,让我们回顾一下 RNN 的基本概念,并在情感分析应用的背景下讨论它们。正如我们在 RNN 章节中提到的,RNN 的基本构建块是递归单元,如图所示:
图 2:RNN 单元的抽象概念
这张图是对递归单元内部运作的抽象。在这里,我们有输入数据,例如,单词good。当然,它需要被转换为嵌入向量。然而,我们暂时忽略这一部分。此外,这个单元还有一种内存状态,根据State和Input的内容,我们会更新这个状态并将新数据写入状态。例如,假设我们之前在输入中看到过单词not,我们将其写入状态,这样当我们在后续输入中看到单词good时,我们可以从状态中得知之前见过单词not。现在,我们看到单词good,因此,我们必须在状态中写入已见过not good这两个单词,这可能表明整个输入文本的情感是负面的。
从旧状态和输入到新状态内容的映射是通过所谓的门控来完成的,这些门控在不同版本的递归单元中有不同的实现方式。它基本上是一个矩阵运算加上激活函数,但正如我们稍后会看到的,反向传播梯度时会遇到问题。因此,RNN 必须以一种特殊方式设计,以避免梯度被过度扭曲。
在递归单元中,我们有一个类似的门控来生成输出,再次强调,递归单元的输出依赖于当前状态的内容和我们正在看到的输入。所以我们可以尝试展开递归单元中的处理过程:
图 3:递归神经网络的展开版本
现在,我们看到的是一个递归单元,但流程图展示了不同时间步发生的情况。所以:
-
在时间步 1,我们将单词this输入到递归单元中,它的内部记忆状态首先初始化为零。当我们开始处理新的数据序列时,TensorFlow 会执行此操作。所以我们看到单词this,而递归单元的状态是 0。因此,我们使用内部门控来更新记忆状态,this随后在时间步 2 被使用,在此时间步我们输入单词is,此时记忆状态已有内容。this这个词的意义并不大,因此状态可能仍然接近 0。
-
而且is也没有太多的意义,所以状态可能仍然接近 0。
-
在下一个时间步,我们看到单词not,这具有我们最终想要预测的意义,即整个输入文本的情感。这个信息需要存储在记忆中,以便递归单元中的门控能够看到该状态已经可能包含接近零的值。但现在它需要存储我们刚刚看到的单词not,因此它在该状态中保存了一些非零值。
-
然后,我们进入下一个时间步,看到单词a,这个也没有太多信息,所以可能会被忽略。它只是将状态复制过去。
-
现在,我们看到单词very,这表示任何情感可能是强烈的情感,因此递归单元现在知道我们已经看到了not和very。它以某种方式将其存储在内存状态中。
-
在下一个时间步,我们看到单词good,所以现在网络知道not very good,并且它想,*哦,这可能是负面情感!*因此,它将这个值存储在内部状态中。
-
然后,在最后一个时间步,我们看到movie,这实际上与情感无关,因此可能会被忽略。
-
接下来,我们使用递归单元中的另一个门控输出记忆状态的内容,然后通过 sigmoid 函数处理(这里没有展示)。我们得到一个介于 0 和 1 之间的输出值。
这个想法是,我们希望在互联网上的电影评论数据集(Internet Movie Database)上训练这个网络,其中,对于每个输入文本,我们都会提供正确的情感值——正面或负面。接着,我们希望 TensorFlow 能找出循环单元内部的门控应该是什么,以便它能够准确地将这个输入文本映射到正确的情感:
图 4:本章实现所用的架构
我们在这个实现中将使用的 RNN 架构是一个具有三层的 RNN 类型架构。在第一层,我们刚才解释的过程会发生,只是现在我们需要在每个时间步输出来自循环单元的值。然后,我们收集新的数据序列,即第一循环层的输出。接下来,我们可以将它输入到第二个循环层,因为循环单元需要输入数据的序列(而我们从第一层得到的输出和我们想要输入到第二个循环层的是一些浮点值,其含义我们并不完全理解)。这在 RNN 内部有其意义,但我们作为人类并不能理解它。然后,我们在第二个循环层中进行类似的处理。
所以,首先,我们将这个循环单元的内部记忆状态初始化为 0;然后,我们取第一个循环层的第一个输出并输入。我们用循环单元内部的门控处理它,更新状态,取第一个层循环单元的输出作为第二个词is的输入,并使用该输入和内部记忆状态。我们继续这样做,直到处理完整个序列,然后我们将第二个循环层的所有输出收集起来。我们将它们作为输入传递给第三个循环层,在那里我们进行类似的处理。但在这里,我们只需要最后一个时间步的输出,它是迄今为止输入的所有内容的摘要。然后,我们将它输出到一个全连接层,这里没有展示。最后,我们使用 sigmoid 激活函数,因此我们得到一个介于 0 和 1 之间的值,分别表示负面和正面情感。
梯度爆炸和梯度消失——回顾
正如我们在上一章提到的,存在一种现象叫做梯度爆炸和梯度消失,它在 RNN 中非常重要。让我们回过头来看一下图 1;该流程图解释了这个现象是什么。
假设我们有一个包含 500 个词的文本数据集,这将用于实现我们的情感分析分类器。在每个时间步,我们以递归方式应用循环单元内的门控;因此,如果有 500 个词,我们将在 500 次时间步中应用这些门控,以更新循环单元的内部记忆状态。
如我们所知,神经网络的训练方式是通过所谓的梯度反向传播,所以我们有一个损失函数,它获取神经网络的输出,然后是我们希望得到的该输入文本的真实输出。接下来,我们希望最小化这个损失值,以使神经网络的实际输出与此特定输入文本的期望输出相符。因此,我们需要计算这个损失函数关于这些递归单元内部权重的梯度,而这些权重用于更新内部状态并最终输出结果的门控。
现在,这个门可能会应用大约 500 次,如果其中有乘法运算,我们实际上得到的是一个指数函数。所以,如果你将一个值与其本身相乘 500 次,并且这个值略小于 1,那么它将很快消失或丢失。同样地,如果一个值略大于 1,并与其本身相乘 500 次,它将爆炸。
唯一能在 500 次乘法中生存的值是 0 和 1。它们将保持不变,所以递归单元实际上比你看到的要复杂得多。这是一个抽象的概念——我们希望以某种方式将内部记忆状态和输入映射,用于更新内部记忆状态并输出某个值——但实际上,我们需要非常小心地将梯度反向传播通过这些门,以防止在多次时间步中发生这种指数级的乘法。我们也鼓励你查看一些关于递归单元数学定义的教程。
情感分析 – 模型实现
我们已经了解了如何实现堆叠版本的 LSTM 变种 RNN。为了让事情更有趣,我们将使用一个更高级的 API,叫做 Keras。
Keras
"Keras 是一个高级神经网络 API,使用 Python 编写,可以在 TensorFlow、CNTK 或 Theano 上运行。它的开发重点是快速实验的实现。从想法到结果的转换延迟最小化是做出良好研究的关键。" – Keras 网站
所以,Keras 只是 TensorFlow 和其他深度学习框架的一个封装。它非常适合原型设计和快速构建,但另一方面,它让你对代码的控制较少。我们将尝试在 Keras 中实现这个情感分析模型,这样你可以在 TensorFlow 和 Keras 中获得一个动手实现。你可以使用 Keras 进行快速原型设计,而将 TensorFlow 用于生产环境的系统。
更有趣的消息是,你不需要切换到一个完全不同的环境。你现在可以在 TensorFlow 中将 Keras 作为模块访问,并像以下代码一样导入包:
from tensorflow.python.keras.models
import Sequential
from tensorflow.python.keras.layers
import Dense, GRU, Embedding
from tensorflow.python.keras.optimizers
import Adam
from tensorflow.python.keras.preprocessing.text
import Tokenizer
from tensorflow.python.keras.preprocessing.sequence
import pad_sequences
所以,让我们继续使用现在可以称之为更抽象的 TensorFlow 模块,它将帮助我们非常快速地原型化深度学习解决方案。因为我们只需几行代码就能写出完整的深度学习解决方案。
数据分析和预处理
现在,让我们进入实际的实现,我们需要加载数据。Keras 实际上有一个功能,可以用来从 IMDb 加载这个情感数据集,但问题是它已经将所有单词映射到整数令牌了。这是处理自然语言与神经网络之间非常关键的一部分,我真的想向你展示如何做到这一点。
此外,如果你想将这段代码用于其他语言的情感分析,你需要自己做这个转换,所以我们快速实现了一些下载这个数据集的函数。
让我们从导入一些必需的包开始:
%matplotlib inline
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
from scipy.spatial.distance import cdist
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Dense, GRU, Embedding
from tensorflow.python.keras.optimizers import Adam
from tensorflow.python.keras.preprocessing.text import Tokenizer
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
然后我们加载数据集:
import imdb
imdb.maybe_download_and_extract()
Output:
- Download progress: 100.0%
Download finished. Extracting files.
Done.
input_text_train, target_train = imdb.load_data(train=True)
input_text_test, target_test = imdb.load_data(train=False)
print("Size of the trainig set: ", len(input_text_train))
print("Size of the testing set: ", len(input_text_test))
Output:
Size of the trainig set: 25000
Size of the testing set: 25000
如你所见,训练集中有 25,000 个文本,测试集中也有。
我们来看看训练集中的一个例子,它是如何呈现的:
#combine dataset
text_data = input_text_train + input_text_test
input_text_train[1]
Output:
'This is a really heart-warming family movie. It has absolutely brilliant animal training and "acting" (if you can call it like that) as well (just think about the dog in "How the Grinch stole Christmas"... it was plain bad training). The Paulie story is extremely well done, well reproduced and in general the characters are really elaborated too. Not more to say except that this is a GREAT MOVIE!<br /><br />My ratings: story 8.5/10, acting 7.5/10, animals+fx 8.5/10, cinematography 8/10.<br /><br />My overall rating: 8/10 - BIG FAMILY MOVIE AND VERY WORTH WATCHING!'
target_train[1]
Output:
1.0
这是一个相当简短的文本,情感值为1.0,这意味着它是一个积极的情感,因此这是一篇关于某部电影的正面评价。
现在,我们进入了分词器,这也是处理这些原始数据的第一步,因为神经网络不能直接处理文本数据。Keras 实现了一个叫做分词器的工具,用来构建词汇表并将单词映射到整数。
此外,我们可以说我们希望最大使用 10,000 个单词,因此它将只使用数据集中最流行的 10,000 个单词:
num_top_words = 10000
tokenizer_obj = Tokenizer(num_words=num_top_words)
现在,我们将从数据集中获取所有文本,并在文本上调用这个函数fit:
tokenizer_obj.fit_on_texts(text_data)
分词器大约需要 10 秒钟,然后它将构建出词汇表。它看起来是这样的:
tokenizer_obj.word_index
Output:
{'britains': 33206,
'labcoats': 121364,
'steeled': 102939,
'geddon': 67551,
"rossilini's": 91757,
'recreational': 27654,
'suffices': 43205,
'hallelujah': 30337,
'mallika': 30343,
'kilogram': 122493,
'elphic': 104809,
'feebly': 32818,
'unskillful': 91728,
"'mistress'": 122218,
"yesterday's": 25908,
'busco': 85664,
'goobacks': 85670,
'mcfeast': 71175,
'tamsin': 77763,
"petron's": 72628,
"'lion": 87485,
'sams': 58341,
'unbidden': 60042,
"principal's": 44902,
'minutiae': 31453,
'smelled': 35009,
'history\x97but': 75538,
'vehemently': 28626,
'leering': 14905,
'kýnay': 107654,
'intendend': 101260,
'chomping': 21885,
'nietsze': 76308,
'browned': 83646,
'grosse': 17645,
"''gaslight''": 74713,
'forseeing': 103637,
'asteroids': 30997,
'peevish': 49633,
"attic'": 120936,
'genres': 4026,
'breckinridge': 17499,
'wrist': 13996,
"sopranos'": 50345,
'embarasing': 92679,
"wednesday's": 118413,
'cervi': 39092,
'felicity': 21570,
"''horror''": 56254,
'alarms': 17764,
"'ol": 29410,
'leper': 27793,
'once\x85': 100641,
'iverson': 66834,
'triply': 117589,
'industries': 19176,
'brite': 16733,
'amateur': 2459,
"libby's": 46942,
'eeeeevil': 120413,
'jbc33': 51111,
'wyoming': 12030,
'waned': 30059,
'uchida': 63203,
'uttter': 93299,
'irector': 123847,
'outriders': 95156,
'perd': 118465,
.
.
.}
所以,现在每个单词都与一个整数相关联;因此,单词the的编号是1:
tokenizer_obj.word_index['the']
Output:
1
这里,and的编号是2:
tokenizer_obj.word_index['and']
Output:
2
单词a的编号是3:
tokenizer_obj.word_index['a']
Output:
3
以此类推。我们看到movie的编号是17:
tokenizer_obj.word_index['movie']
Output:
17
并且film的编号是19:
tokenizer_obj.word_index['film']
Output:
19
所有这些的意思是,the是数据集中使用最多的单词,and是第二多的单词。因此,每当我们想要将单词映射到整数令牌时,我们将得到这些编号。
让我们以单词编号743为例,这就是单词romantic:
tokenizer_obj.word_index['romantic']
Output:
743
所以,每当我们在输入文本中看到单词romantic时,我们将其映射到令牌整数743。我们再次使用分词器将训练集中的所有单词转换为整数令牌:
input_text_train[1]
Output:
'This is a really heart-warming family movie. It has absolutely brilliant animal training and "acting" (if you can call it like that) as well (just think about the dog in "How the Grinch stole Christmas"... it was plain bad training). The Paulie story is extremely well done, well reproduced and in general the characters are really elaborated too. Not more to say except that this is a GREAT MOVIE!<br /><br />My ratings: story 8.5/10, acting 7.5/10, animals+fx 8.5/10, cinematography 8/10.<br /><br />My overall rating: 8/10 - BIG FAMILY MOVIE AND VERY WORTH WATCHING!
当我们将这些文本转换为整数令牌时,它就变成了一个整数数组:
np.array(input_train_tokens[1])
Output:
array([ 11, 6, 3, 62, 488, 4679, 236, 17, 9, 45, 419,
513, 1717, 2425, 2, 113, 43, 22, 67, 654, 9, 37,
12, 14, 69, 39, 101, 42, 1, 826, 8, 85, 1,
6418, 3492, 1156, 9, 13, 1042, 74, 2425, 1, 6419, 64,
6, 568, 69, 221, 69, 2, 8, 825, 1, 102, 23,
62, 96, 21, 51, 5, 131, 556, 12, 11, 6, 3,
78, 17, 7, 7, 56, 2818, 64, 723, 447, 156, 113,
702, 447, 156, 1598, 3611, 723, 447, 156, 633, 723, 156,
7, 7, 56, 437, 670, 723, 156, 191, 236, 17, 2,
52, 278, 147])
所以,单词this变成了编号 11,单词is变成了编号 59,以此类推。
我们还需要转换剩余的文本:
input_test_tokens = tokenizer_obj.texts_to_sequences(input_text_test)
现在,还有另一个问题,因为标记的序列长度根据原始文本的长度而有所不同,尽管循环神经网络(RNN)单元可以处理任意长度的序列。但是 TensorFlow 的工作方式是,批量中的所有数据必须具有相同的长度。
所以,我们可以确保数据集中的所有序列都具有相同的长度,或者编写一个自定义数据生成器,确保单个批次中的序列具有相同的长度。现在,确保数据集中的所有序列具有相同的长度要简单得多,但问题是有一些极端值。我们有一些句子,我认为,它们超过了 2,200 个单词。如果所有的短句子都超过 2,200 个单词,将极大地影响我们的内存。所以我们做的折衷是:首先,我们需要统计每个输入序列中的单词数,或者标记数。我们看到,序列中单词的平均数大约是 221:
total_num_tokens = [len(tokens) for tokens in input_train_tokens + input_test_tokens]
total_num_tokens = np.array(total_num_tokens)
#Get the average number of tokens
np.mean(total_num_tokens)
Output:
221.27716
我们看到,最大单词数超过了 2200 个:
np.max(total_num_tokens)
Output:
2208
现在,平均值和最大值之间有很大的差异,如果我们仅仅将数据集中的所有句子都填充到2208个标记,这将浪费大量的内存。尤其是如果你有一个包含百万级文本序列的数据集,这个问题就更加严重。
所以我们要做的折衷是,填充所有序列并截断那些太长的序列,使它们有544个单词。我们计算这一点的方式是——我们取了数据集中所有序列的平均单词数,并加上了两个标准差:
max_num_tokens = np.mean(total_num_tokens) + 2 * np.std(total_num_tokens)
max_num_tokens = int(max_num_tokens)
max_num_tokens
Output:
544
这样做的结果是什么?我们覆盖了数据集中文本的约 95%,所以只有大约 5%的文本超过了544个单词:
np.sum(total_num_tokens < max_num_tokens) / len(total_num_tokens)
Output:
0.94532
现在,我们调用 Keras 中的这些函数。它们会填充那些太短的序列(即只会添加零),或者截断那些太长的序列(如果文本过长,基本上会删除一些单词)。
现在,这里有一个重要的点:我们可以选择在预处理模式(pre mode)或后处理模式(post mode)下进行填充和截断。假设我们有一个整数标记的序列,并且我们希望填充它,因为它太短了。我们可以:
-
要么在开头填充所有这些零,这样我们就可以把实际的整数标记放在最后。
-
或者以相反的方式进行处理,将所有数据放在开头,然后将所有的零放在末尾。但是,如果我们回头看看前面的 RNN 流程图,记住它是一步一步地处理序列的,所以如果我们开始处理零,它可能没有任何意义,内部状态可能会保持为零。因此,每当它最终看到特定单词的整数标记时,它就会知道,好,现在开始处理数据了。
然而,如果所有的零都在末尾,那么我们将开始处理所有数据;接着,我们会在递归单元内部有一些内部状态。现在,我们看到的是一堆零,这实际上可能会破坏我们刚刚计算出来的内部状态。这就是为什么将零填充到开始处可能是个好主意。
但另一个问题是当我们截断文本时,如果文本非常长,我们会将其截断到544个单词,或者其他任何数字。现在,假设我们抓住了这句话,它在中间某个地方,并且它说的是这部非常好的电影或这不是。你当然知道,我们只有在处理非常长的序列时才会这样做,但很可能我们会丢失一些关键信息,无法正确分类这段文本。所以,当我们截断输入文本时,这是我们所做的妥协。更好的方法是创建一个批次,并在该批次中填充文本。所以,当我们看到非常非常长的序列时,我们将填充其他序列,使它们具有相同的长度。但我们不需要将所有这些数据都存储在内存中,因为其中大部分是浪费的。
让我们回到并转换整个数据集,使其被截断并填充;这样,它就变成了一个庞大的数据矩阵:
seq_pad = 'pre'
input_train_pad = pad_sequences(input_train_tokens, maxlen=max_num_tokens,
padding=seq_pad, truncating=seq_pad)
input_test_pad = pad_sequences(input_test_tokens, maxlen=max_num_tokens,
padding=seq_pad, truncating=seq_pad)
我们检查这个矩阵的形状:
input_train_pad.shape
Output:
(25000, 544)
input_test_pad.shape
Output:
(25000, 544)
那么,让我们来看一下在填充前后的特定样本标记:
np.array(input_train_tokens[1])
Output:
array([ 11, 6, 3, 62, 488, 4679, 236, 17, 9, 45, 419,
513, 1717, 2425, 2, 113, 43, 22, 67, 654, 9, 37,
12, 14, 69, 39, 101, 42, 1, 826, 8, 85, 1,
6418, 3492, 1156, 9, 13, 1042, 74, 2425, 1, 6419, 64,
6, 568, 69, 221, 69, 2, 8, 825, 1, 102, 23,
62, 96, 21, 51, 5, 131, 556, 12, 11, 6, 3,
78, 17, 7, 7, 56, 2818, 64, 723, 447, 156, 113,
702, 447, 156, 1598, 3611, 723, 447, 156, 633, 723, 156,
7, 7, 56, 437, 670, 723, 156, 191, 236, 17, 2,
52, 278, 147])
填充后,这个样本将如下所示:
input_train_pad[1]
Output:
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 11, 6, 3, 62, 488, 4679, 236, 17, 9,
45, 419, 513, 1717, 2425, 2, 113, 43, 22, 67, 654,
9, 37, 12, 14, 69, 39, 101, 42, 1, 826, 8,
85, 1, 6418, 3492, 1156, 9, 13, 1042, 74, 2425, 1,
6419, 64, 6, 568, 69, 221, 69, 2, 8, 825, 1,
102, 23, 62, 96, 21, 51, 5, 131, 556, 12, 11,
6, 3, 78, 17, 7, 7, 56, 2818, 64, 723, 447,
156, 113, 702, 447, 156, 1598, 3611, 723, 447, 156, 633,
723, 156, 7, 7, 56, 437, 670, 723, 156, 191, 236,
17, 2, 52, 278, 147], dtype=int32)
此外,我们需要一个功能来进行反向映射,使其能够将整数标记映射回文本单词;我们在这里只需要这个。它是一个非常简单的辅助函数,所以让我们继续实现它:
index = tokenizer_obj.word_index
index_inverse_map = dict(zip(index.values(), index.keys()))
def convert_tokens_to_string(input_tokens):
# Convert the tokens back to words
input_words = [index_inverse_map[token] for token in input_tokens if token != 0]
# join them all words.
combined_text = " ".join(input_words)
return combined_text
现在,举个例子,数据集中的原始文本是这样的:
input_text_train[1]
Output:
input_text_train[1]
'This is a really heart-warming family movie. It has absolutely brilliant animal training and "acting" (if you can call it like that) as well (just think about the dog in "How the Grinch stole Christmas"... it was plain bad training). The Paulie story is extremely well done, well reproduced and in general the characters are really elaborated too. Not more to say except that this is a GREAT MOVIE!<br /><br />My ratings: story 8.5/10, acting 7.5/10, animals+fx 8.5/10, cinematography 8/10.<br /><br />My overall rating: 8/10 - BIG FAMILY MOVIE AND VERY WORTH WATCHING!'
如果我们使用一个辅助函数将标记转换回文本单词,我们会得到以下文本:
convert_tokens_to_string(input_train_tokens[1])
'this is a really heart warming family movie it has absolutely brilliant animal training and acting if you can call it like that as well just think about the dog in how the grinch stole christmas it was plain bad training the paulie story is extremely well done well and in general the characters are really too not more to say except that this is a great movie br br my ratings story 8 5 10 acting 7 5 10 animals fx 8 5 10 cinematography 8 10 br br my overall rating 8 10 big family movie and very worth watching'
基本上是一样的,只是标点符号和其他符号不同。
构建模型
现在,我们需要创建 RNN,我们将在 Keras 中实现,因为它非常简单。我们使用所谓的sequential模型来实现这一点。
该架构的第一层将是所谓的嵌入层。如果我们回顾一下图 1中的流程图,我们刚才做的是将原始输入文本转换为整数标记。但我们仍然无法将其输入到 RNN 中,所以我们必须将其转换为嵌入向量,这些值介于-1 和 1 之间。它们可能在某种程度上超出这一范围,但通常情况下它们会在-1 和 1 之间,这些是我们可以在神经网络中进行处理的数据。
这有点像魔法,因为这个嵌入层与 RNN 同时训练,它看不到原始单词。它看到的是整数标记,但学会了识别单词如何一起使用的模式。所以它可以在某种程度上推断出一些单词或一些整数标记具有相似的意义,然后它将这些信息编码到看起来相似的嵌入向量中。
因此,我们需要决定每个向量的长度,例如,“11”这个标记将被转换成一个实值向量。在这个例子中,我们使用长度为 8 的向量,实际上它非常短(通常是 100 到 300 之间)。尝试改变这个嵌入向量中的元素数量,并重新运行这段代码,看看结果会是什么。
所以,我们将嵌入大小设置为 8,然后使用 Keras 将这个嵌入层添加到 RNN 中。它必须是网络中的第一个层:
embedding_layer_size = 8
rnn_type_model.add(Embedding(input_dim=num_top_words,
output_dim=embedding_layer_size,
input_length=max_num_tokens,
name='embedding_layer'))
然后,我们可以添加第一个循环层,我们将使用一个叫做门控循环单元(GRU)。通常,你会看到人们使用叫做LSTM的结构,但有些人似乎认为 GRU 更好,因为 LSTM 中有些门是冗余的。实际上,简单的代码在减少门的数量后也能很好地工作。你可以给 LSTM 加上更多的门,但那并不意味着它会变得更好。
所以,让我们定义我们的 GRU 架构;我们设定输出维度为 16,并且需要返回序列:
rnn_type_model.add(GRU(units=16, return_sequences=True))
如果我们看一下图 4中的流程图,我们想要添加第二个循环层:
rnn_type_model.add(GRU(units=8, return_sequences=True))
然后,我们有第三个也是最后一个循环层,它不会输出一个序列,因为它后面会跟随一个全连接层;它应该只给出 GRU 的最终输出,而不是一整个输出序列:
rnn_type_model.add(GRU(units=4))
然后,这里输出的结果将被输入到一个全连接或密集层,这个层应该只输出每个输入序列的一个值。它通过 sigmoid 激活函数处理,因此输出一个介于 0 和 1 之间的值:
rnn_type_model.add(Dense(1, activation='sigmoid'))
然后,我们说我们想使用 Adam 优化器,并设定学习率,同时损失函数应该是 RNN 输出与训练集中的实际类别值之间的二元交叉熵,这个值应该是 0 或 1:
model_optimizer = Adam(lr=1e-3)
rnn_type_model.compile(loss='binary_crossentropy',
optimizer=model_optimizer,
metrics=['accuracy'])
现在,我们可以打印出模型的摘要:
rnn_type_model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_layer (Embedding) (None, 544, 8) 80000
_________________________________________________________________
gru_1 (GRU) (None, None, 16) 1200
_________________________________________________________________
gru_2 (GRU) (None, None, 8) 600
_________________________________________________________________
gru_3 (GRU) (None, 4) 156
_________________________________________________________________
dense_1 (Dense) (None, 1) 5
=================================================================
Total params: 81,961
Trainable params: 81,961
Non-trainable params: 0
_________________________
如你所见,我们有嵌入层,第一个循环单元,第二、第三个循环单元和密集层。请注意,这个模型的参数并不多。
模型训练与结果分析
现在,是时候开始训练过程了,这里非常简单:
Output:
rnn_type_model.fit(input_train_pad, target_train,
validation_split=0.05, epochs=3, batch_size=64)
Output:
Train on 23750 samples, validate on 1250 samples
Epoch 1/3
23750/23750 [==============================]23750/23750 [==============================] - 176s 7ms/step - loss: 0.6698 - acc: 0.5758 - val_loss: 0.5039 - val_acc: 0.7784
Epoch 2/3
23750/23750 [==============================]23750/23750 [==============================] - 175s 7ms/step - loss: 0.4631 - acc: 0.7834 - val_loss: 0.2571 - val_acc: 0.8960
Epoch 3/3
23750/23750 [==============================]23750/23750 [==============================] - 174s 7ms/step - loss: 0.3256 - acc: 0.8673 - val_loss: 0.3266 - val_acc: 0.8600
让我们在测试集上测试训练好的模型:
model_result = rnn_type_model.evaluate(input_test_pad, target_test)
Output:
25000/25000 [==============================]25000/25000 [==============================] - 60s 2ms/step
print("Accuracy: {0:.2%}".format(model_result[1]))
Output:
Accuracy: 85.26%
现在,让我们看看一些被错误分类的文本示例。
所以首先,我们计算测试集中前 1,000 个序列的预测类别,然后取实际类别值。我们将它们进行比较,并得到一个索引列表,其中包含不匹配的地方:
target_predicted = rnn_type_model.predict(x=input_test_pad[0:1000])
target_predicted = target_predicted.T[0]
使用阈值来表示所有大于0.5的值将被认为是正类,其他的将被认为是负类:
class_predicted = np.array([1.0 if prob>0.5 else 0.0 for prob in target_predicted])
现在,我们来获取这 1,000 个序列的实际类别:
class_actual = np.array(target_test[0:1000])
让我们从输出中获取错误的样本:
incorrect_samples = np.where(class_predicted != class_actual)
incorrect_samples = incorrect_samples[0]
len(incorrect_samples)
Output:
122
我们看到有 122 个文本被错误分类,占我们计算的 1,000 个文本的 12.1%。让我们来看一下第一个被错误分类的文本:
index = incorrect_samples[0]
index
Output:
9
incorrectly_predicted_text = input_text_test[index]
incorrectly_predicted_text
Output:
'I am not a big music video fan. I think music videos take away personal feelings about a particular song.. Any song. In other words, creative thinking goes out the window. Likewise, Personal feelings aside about MJ, toss aside. This was the best music video of alltime. Simply wonderful. It was a movie. Yes folks it was. Brilliant! You had awesome acting, awesome choreography, and awesome singing. This was spectacular. Simply a plot line of a beautiful young lady dating a man, but was he a man or something sinister. Vincent Price did his thing adding to the song and video. MJ was MJ, enough said about that. This song was to video, what Jaguars are for cars. Top of the line, PERFECTO. What was even better about this was, that we got the real MJ without the thousand facelifts. Though ironically enough, there was more than enough makeup and costumes to go around. Folks go to Youtube. Take 14 mins. out of your life and see for yourself what a wonderful work of art this particular video really is.'
让我们看看这个样本的模型输出以及实际类别:
target_predicted[index]
Output:
0.1529513
class_actual[index]
Output:
1.0
现在,让我们测试一下我们训练好的模型,看看它在一组新数据样本上的表现:
test_sample_1 = "This movie is fantastic! I really like it because it is so good!"
test_sample_2 = "Good movie!"
test_sample_3 = "Maybe I like this movie."
test_sample_4 = "Meh ..."
test_sample_5 = "If I were a drunk teenager then this movie might be good."
test_sample_6 = "Bad movie!"
test_sample_7 = "Not a good movie!"
test_sample_8 = "This movie really sucks! Can I get my money back please?"
test_samples = [test_sample_1, test_sample_2, test_sample_3, test_sample_4, test_sample_5, test_sample_6, test_sample_7, test_sample_8]
现在,让我们将它们转换为整数标记:
test_samples_tokens = tokenizer_obj.texts_to_sequences(test_samples)
然后进行填充:
test_samples_tokens_pad = pad_sequences(test_samples_tokens, maxlen=max_num_tokens,
padding=seq_pad, truncating=seq_pad)
test_samples_tokens_pad.shape
Output:
(8, 544)
最后,让我们将模型应用于这些数据:
rnn_type_model.predict(test_samples_tokens_pad)
Output:
array([[0.9496784 ],
[0.9552593 ],
[0.9115685 ],
[0.9464672 ],
[0.87672734],
[0.81883633],
[0.33248223],
[0.15345531 ]], dtype=float32)
所以,接近零的值意味着负面情感,而接近 1 的值意味着正面情感;最后,这些数字会在每次训练模型时有所变化。
总结
在这一章中,我们介绍了一个有趣的应用——情感分析。情感分析被不同的公司用来追踪客户对其产品的满意度。甚至政府也使用情感分析解决方案来跟踪公民对他们未来想做的事情的满意度。
接下来,我们将重点关注一些可以用于半监督和无监督应用的先进深度学习架构。
第十三章:自编码器 – 特征提取与去噪
自编码器网络如今是广泛使用的深度学习架构之一。它主要用于无监督学习高效解码任务。它也可以通过学习特定数据集的编码或表示来进行降维。在本章中使用自编码器,我们将展示如何通过构建另一个具有相同维度但噪声较少的数据集来去噪你的数据集。为了将这一概念付诸实践,我们将从 MNIST 数据集中提取重要特征,并尝试查看如何通过这一方法显著提高性能。
本章将涵盖以下主题:
-
自编码器简介
-
自编码器示例
-
自编码器架构
-
压缩 MNIST 数据集
-
卷积自编码器
-
去噪自编码器
-
自编码器的应用
自编码器简介
自编码器是另一种深度学习架构,可以用于许多有趣的任务,但它也可以被看作是普通前馈神经网络的一种变体,其中输出与输入具有相同的维度。如 图 1 所示,自编码器的工作原理是将数据样本 (x[1],...,x[6]) 输入网络。它将在 L2 层学习该数据的较低表示,你可以把它看作是将数据集编码为较低表示的一种方式。然后,网络的第二部分(你可以称之为解码器)负责根据这个表示构建输出!。你可以将网络从输入数据中学习到的中间较低表示看作是它的压缩版本。
和我们迄今为止见过的其他深度学习架构并没有太大不同,自编码器使用反向传播算法。
自编码器神经网络是一种无监督学习算法,应用反向传播,将目标值设为与输入相同:
图 1:一般自编码器架构
自编码器示例
在本章中,我们将通过使用 MNIST 数据集演示一些不同类型的自编码器变体。作为一个具体例子,假设输入 x 是来自 28 x 28 图像的像素强度值(784 个像素);因此,输入数据样本的数量为 n=784。在 L2 层中有 s2=392 个隐藏单元。由于输出将与输入数据样本的维度相同,y ∈ R784。输入层中的神经元数量为 784,中间层 L2 中有 392 个神经元;因此,网络将是一个较低的表示,这是输出的压缩版本。然后,网络将把这个压缩的较低表示 a(L2) ∈ R392 输入到网络的第二部分,后者将尽力从这个压缩版本中重建输入像素 784。
自编码器依赖于输入样本由图像像素表示,这些像素在某种程度上是相关的,然后它将利用这一点来重建它们。因此,自编码器有点类似于降维技术,因为它们也学习输入数据的低维表示。
总结一下,典型的自编码器将由三个部分组成:
-
编码器部分,负责将输入压缩为低维表示
-
代码部分,即编码器的中间结果
-
解码器,负责使用该代码重建原始输入
以下图展示了典型自编码器的三个主要组成部分:
图 2:编码器如何在图像上发挥作用
正如我们所提到的,自编码器部分学习输入的压缩表示,然后将其馈送给第三部分,后者尝试重建输入。重建后的输入将类似于输出,但不会完全与原始输出相同,因此自编码器不能用于压缩任务。
自编码器架构
正如我们所提到的,典型的自编码器由三个部分组成。让我们更详细地探索这三部分。为了激励你,我们在本章中不会重新发明轮子。编码器-解码器部分不过是一个完全连接的神经网络,而代码部分是另一个神经网络,但它不是完全连接的。这个代码部分的维度是可控的,我们可以把它当作超参数来处理:
图 3:自编码器的一般编码器-解码器架构
在深入使用自编码器压缩 MNIST 数据集之前,我们将列出可以用于微调自编码器模型的一组超参数。主要有四个超参数:
-
代码部分大小:这是中间层的单元数。中间层的单元数越少,我们得到的输入压缩表示越多。
-
编码器和解码器的层数:正如我们所提到的,编码器和解码器不过是一个完全连接的神经网络,我们可以通过添加更多层使其尽可能深。
-
每层单元数:我们也可以在每一层使用不同的单元数。编码器和解码器的形状与 DeconvNets 非常相似,其中编码器的层数在接近代码部分时减少,然后在接近解码器的最终层时开始增加。
-
模型损失函数:我们也可以使用不同的损失函数,例如 MSE 或交叉熵。
在定义这些超参数并赋予它们初始值后,我们可以使用反向传播算法来训练网络。
压缩 MNIST 数据集
在本部分中,我们将构建一个简单的自动编码器,用于压缩 MNIST 数据集。因此,我们将把该数据集中的图像输入到编码器部分,编码器将尝试为它们学习一个压缩的低维表示;然后,我们将在解码器部分尝试重新构建输入图像。
MNIST 数据集
我们将通过使用 TensorFlow 的辅助函数获取 MNIST 数据集来开始实现。
让我们导入实现所需的必要包:
%matplotlib inline
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets('MNIST_data', validation_size=0)
Output:
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz
让我们先从绘制一些 MNIST 数据集中的示例开始:
# Plotting one image from the training set.
image = mnist_dataset.train.images[2]
plt.imshow(image.reshape((28, 28)), cmap='Greys_r')
Output:
图 4:来自 MNIST 数据集的示例图像
# Plotting one image from the training set.
image = mnist_dataset.train.images[2]
plt.imshow(image.reshape((28, 28)), cmap='Greys_r')
Output:
图 5:来自 MNIST 数据集的示例图像
构建模型
为了构建编码器,我们需要弄清楚每张 MNIST 图像包含多少像素,这样我们就能确定编码器输入层的大小。每张来自 MNIST 数据集的图像是 28 x 28 像素,因此我们将把该矩阵重塑为一个包含 28 x 28 = 784 个像素值的向量。我们不需要对 MNIST 图像进行归一化处理,因为它们已经是归一化的。
让我们开始构建模型的三个组件。在此实现中,我们将使用一个非常简单的架构,即单个隐藏层后接 ReLU 激活函数,如下图所示:
图 6:MNIST 实现的编码器-解码器架构
根据之前的解释,接下来我们将实现这个简单的编码器-解码器架构:
# The size of the encoding layer or the hidden layer.
encoding_layer_dim = 32
img_size = mnist_dataset.train.images.shape[1]
# defining placeholder variables of the input and target values
inputs_values = tf.placeholder(tf.float32, (None, img_size), name="inputs_values")
targets_values = tf.placeholder(tf.float32, (None, img_size), name="targets_values")
# Defining an encoding layer which takes the input values and incode them.
encoding_layer = tf.layers.dense(inputs_values, encoding_layer_dim, activation=tf.nn.relu)
# Defining the logit layer, which is a fully-connected layer but without any activation applied to its output
logits_layer = tf.layers.dense(encoding_layer, img_size, activation=None)
# Adding a sigmoid layer after the logit layer
decoding_layer = tf.sigmoid(logits_layer, name = "decoding_layer")
# use the sigmoid cross entropy as a loss function
model_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=logits_layer, labels=targets_values)
# Averaging the loss values accross the input data
model_cost = tf.reduce_mean(model_loss)
# Now we have a cost functiont that we need to optimize using Adam Optimizer
model_optimizier = tf.train.AdamOptimizer().minimize(model_cost)
现在我们已经定义了模型,并且使用了二元交叉熵,因为图像像素已经进行了归一化处理。
模型训练
在本节中,我们将启动训练过程。我们将使用 mnist_dataset 对象的辅助函数来从数据集中获取指定大小的随机批次;然后我们将在这一批图像上运行优化器。
让我们通过创建会话变量来开始本节内容,该变量将负责执行我们之前定义的计算图:
# creating the session
sess = tf.Session()
接下来,让我们启动训练过程:
num_epochs = 20
train_batch_size = 200
sess.run(tf.global_variables_initializer())
for e in range(num_epochs):
for ii in range(mnist_dataset.train.num_examples//train_batch_size):
input_batch = mnist_dataset.train.next_batch(train_batch_size)
feed_dict = {inputs_values: input_batch[0], targets_values: input_batch[0]}
input_batch_cost, _ = sess.run([model_cost, model_optimizier], feed_dict=feed_dict)
print("Epoch: {}/{}...".format(e+1, num_epochs),
"Training loss: {:.3f}".format(input_batch_cost))
Output:
.
.
.
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.089
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.092
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.089
Epoch: 20/20... Training loss: 0.090
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.088
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.090
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.092
Epoch: 20/20... Training loss: 0.092
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.093
在运行上述代码片段 20 个 epoch 后,我们将得到一个训练好的模型,它能够从 MNIST 数据的测试集中生成或重建图像。请记住,如果我们输入的图像与模型训练时使用的图像不相似,那么重建过程将无法正常工作,因为自动编码器是针对特定数据的。
让我们通过输入一些来自测试集的图像来测试训练好的模型,看看模型在解码器部分如何重建这些图像:
fig, axes = plt.subplots(nrows=2, ncols=10, sharex=True, sharey=True, figsize=(20,4))
input_images = mnist_dataset.test.images[:10]
reconstructed_images, compressed_images = sess.run([decoding_layer, encoding_layer], feed_dict={inputs_values: input_images})
for imgs, row in zip([input_images, reconstructed_images], axes):
for img, ax in zip(imgs, row):
ax.imshow(img.reshape((28, 28)), cmap='Greys_r')
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
fig.tight_layout(pad=0.1)
输出:
图 7:原始测试图像(第一行)及其重建(第二行)的示例
如你所见,重建后的图像与输入图像非常接近,但我们可能可以通过在编码器-解码器部分使用卷积层来获得更好的图像。
卷积自动编码器
之前的简单实现很好地完成了从 MNIST 数据集中重建输入图像的任务,但通过在自编码器的编码器和解码器部分添加卷积层,我们可以获得更好的性能。这个替换后得到的网络称为卷积自编码器(CAE)。这种能够替换层的灵活性是自编码器的一个巨大优势,使它们能够应用于不同的领域。
我们将用于 CAE 的架构将在网络的解码器部分包含上采样层,以获取图像的重建版本。
数据集
在这个实现中,我们可以使用任何类型的图像数据集,看看卷积版本的自编码器会带来什么变化。我们仍然将使用 MNIST 数据集,因此让我们开始使用 TensorFlow 辅助函数获取数据集:
%matplotlib inline
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets('MNIST_data', validation_size=0)
Output:
from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets('MNIST_data', validation_size=0)
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz
让我们展示数据集中的一个数字:
# Plotting one image from the training set.
image = mnist_dataset.train.images[2]
plt.imshow(image.reshape((28, 28)), cmap='Greys_r')
输出:
图 8:来自 MNIST 数据集的示例图像
构建模型
在这个实现中,我们将使用步幅为 1 的卷积层,并将填充参数设置为相同。这样,我们不会改变图像的高度或宽度。同时,我们使用了一组最大池化层来减少图像的宽度和高度,从而构建图像的压缩低维表示。
所以,让我们继续构建网络的核心部分:
learning_rate = 0.001
# Define the placeholder variable sfor the input and target values
inputs_values = tf.placeholder(tf.float32, (None, 28,28,1), name="inputs_values")
targets_values = tf.placeholder(tf.float32, (None, 28,28,1), name="targets_values")
# Defining the Encoder part of the netowrk
# Defining the first convolution layer in the encoder parrt
# The output tenosor will be in the shape of 28x28x16
conv_layer_1 = tf.layers.conv2d(inputs=inputs_values, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 14x14x16
maxpool_layer_1 = tf.layers.max_pooling2d(conv_layer_1, pool_size=(2,2), strides=(2,2), padding='same')
# The output tenosor will be in the shape of 14x14x8
conv_layer_2 = tf.layers.conv2d(inputs=maxpool_layer_1, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 7x7x8
maxpool_layer_2 = tf.layers.max_pooling2d(conv_layer_2, pool_size=(2,2), strides=(2,2), padding='same')
# The output tenosor will be in the shape of 7x7x8
conv_layer_3 = tf.layers.conv2d(inputs=maxpool_layer_2, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 4x4x8
encoded_layer = tf.layers.max_pooling2d(conv_layer_3, pool_size=(2,2), strides=(2,2), padding='same')
# Defining the Decoder part of the netowrk
# Defining the first upsampling layer in the decoder part
# The output tenosor will be in the shape of 7x7x8
upsample_layer_1 = tf.image.resize_images(encoded_layer, size=(7,7), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
# The output tenosor will be in the shape of 7x7x8
conv_layer_4 = tf.layers.conv2d(inputs=upsample_layer_1, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 14x14x8
upsample_layer_2 = tf.image.resize_images(conv_layer_4, size=(14,14), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
# The output tenosor will be in the shape of 14x14x8
conv_layer_5 = tf.layers.conv2d(inputs=upsample_layer_2, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 28x28x8
upsample_layer_3 = tf.image.resize_images(conv_layer_5, size=(28,28), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
# The output tenosor will be in the shape of 28x28x16
conv6 = tf.layers.conv2d(inputs=upsample_layer_3, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 28x28x1
logits_layer = tf.layers.conv2d(inputs=conv6, filters=1, kernel_size=(3,3), padding='same', activation=None)
# feeding the logits values to the sigmoid activation function to get the reconstructed images
decoded_layer = tf.nn.sigmoid(logits_layer)
# feeding the logits to sigmoid while calculating the cross entropy
model_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=targets_values, logits=logits_layer)
# Getting the model cost and defining the optimizer to minimize it
model_cost = tf.reduce_mean(model_loss)
model_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(model_cost)
现在我们可以开始了。我们已经构建了卷积神经网络的解码器-解码器部分,并展示了输入图像在解码器部分如何被重建。
模型训练
现在我们已经构建了模型,我们可以通过生成来自 MNIST 数据集的随机批次并将它们输入到之前定义的优化器中来启动学习过程。
让我们从创建会话变量开始;它将负责执行我们之前定义的计算图:
sess = tf.Session()
num_epochs = 20
train_batch_size = 200
sess.run(tf.global_variables_initializer())
for e in range(num_epochs):
for ii in range(mnist_dataset.train.num_examples//train_batch_size):
input_batch = mnist_dataset.train.next_batch(train_batch_size)
input_images = input_batch[0].reshape((-1, 28, 28, 1))
input_batch_cost, _ = sess.run([model_cost, model_optimizer], feed_dict={inputs_values: input_images,targets_values: input_images})
print("Epoch: {}/{}...".format(e+1, num_epochs),
"Training loss: {:.3f}".format(input_batch_cost))
Output:
.
.
.
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.105
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.097
Epoch: 20/20... Training loss: 0.102
在运行前面的代码片段 20 个周期后,我们将得到一个训练好的 CAE,因此我们可以继续通过输入来自 MNIST 数据集的相似图像来测试这个模型:
fig, axes = plt.subplots(nrows=2, ncols=10, sharex=True, sharey=True, figsize=(20,4))
input_images = mnist_dataset.test.images[:10]
reconstructed_images = sess.run(decoded_layer, feed_dict={inputs_values: input_images.reshape((10, 28, 28, 1))})
for imgs, row in zip([input_images, reconstructed_images], axes):
for img, ax in zip(imgs, row):
ax.imshow(img.reshape((28, 28)), cmap='Greys_r')
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
fig.tight_layout(pad=0.1)
Output:
图 9:原始测试图像(第一行)及其重建图像(第二行),使用卷积自编码器
去噪自编码器
我们可以进一步优化自编码器架构,迫使它学习关于输入数据的重要特征。通过向输入图像添加噪声,并以原始图像作为目标,模型将尝试去除这些噪声,并学习关于它们的重要特征,以便在输出中生成有意义的重建图像。这种 CAE 架构可以用于去除输入图像中的噪声。这种自编码器的特定变种称为去噪自编码器:
图 10:原始图像及添加少量高斯噪声后的相同图像示例
所以让我们从实现下图中的架构开始。我们在这个去噪自编码器架构中唯一添加的额外内容就是在原始输入图像中加入了一些噪声:
图 11:自编码器的通用去噪架构
构建模型
在这个实现中,我们将在编码器和解码器部分使用更多的层,原因在于我们给输入增加了新的复杂度。
下一个模型与之前的 CAE 完全相同,只是增加了额外的层,这将帮助我们从有噪声的图像中重建出无噪声的图像。
那么,让我们继续构建这个架构:
learning_rate = 0.001
# Define the placeholder variable sfor the input and target values
inputs_values = tf.placeholder(tf.float32, (None, 28, 28, 1), name='inputs_values')
targets_values = tf.placeholder(tf.float32, (None, 28, 28, 1), name='targets_values')
# Defining the Encoder part of the netowrk
# Defining the first convolution layer in the encoder parrt
# The output tenosor will be in the shape of 28x28x32
conv_layer_1 = tf.layers.conv2d(inputs=inputs_values, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 14x14x32
maxpool_layer_1 = tf.layers.max_pooling2d(conv_layer_1, pool_size=(2,2), strides=(2,2), padding='same')
# The output tenosor will be in the shape of 14x14x32
conv_layer_2 = tf.layers.conv2d(inputs=maxpool_layer_1, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 7x7x32
maxpool_layer_2 = tf.layers.max_pooling2d(conv_layer_2, pool_size=(2,2), strides=(2,2), padding='same')
# The output tenosor will be in the shape of 7x7x16
conv_layer_3 = tf.layers.conv2d(inputs=maxpool_layer_2, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 4x4x16
encoding_layer = tf.layers.max_pooling2d(conv_layer_3, pool_size=(2,2), strides=(2,2), padding='same')
# Defining the Decoder part of the netowrk
# Defining the first upsampling layer in the decoder part
# The output tenosor will be in the shape of 7x7x16
upsample_layer_1 = tf.image.resize_images(encoding_layer, size=(7,7), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
# The output tenosor will be in the shape of 7x7x16
conv_layer_4 = tf.layers.conv2d(inputs=upsample_layer_1, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 14x14x16
upsample_layer_2 = tf.image.resize_images(conv_layer_4, size=(14,14), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
# The output tenosor will be in the shape of 14x14x32
conv_layer_5 = tf.layers.conv2d(inputs=upsample_layer_2, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 28x28x32
upsample_layer_3 = tf.image.resize_images(conv_layer_5, size=(28,28), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
# The output tenosor will be in the shape of 28x28x32
conv_layer_6 = tf.layers.conv2d(inputs=upsample_layer_3, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)
# The output tenosor will be in the shape of 28x28x1
logits_layer = tf.layers.conv2d(inputs=conv_layer_6, filters=1, kernel_size=(3,3), padding='same', activation=None)
# feeding the logits values to the sigmoid activation function to get the reconstructed images
decoding_layer = tf.nn.sigmoid(logits_layer)
# feeding the logits to sigmoid while calculating the cross entropy
model_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=targets_values, logits=logits_layer)
# Getting the model cost and defining the optimizer to minimize it
model_cost = tf.reduce_mean(model_loss)
model_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(model_cost)
现在我们有了一个更复杂或更深的卷积模型版本。
模型训练
现在是时候开始训练这个更深的网络了,这将需要更多的时间来收敛,通过从有噪声的输入中重建无噪声的图像。
所以让我们先创建会话变量:
sess = tf.Session()
接下来,我们将启动训练过程,但使用更多的训练轮次:
num_epochs = 100
train_batch_size = 200
# Defining a noise factor to be added to MNIST dataset
mnist_noise_factor = 0.5
sess.run(tf.global_variables_initializer())
for e in range(num_epochs):
for ii in range(mnist_dataset.train.num_examples//train_batch_size):
input_batch = mnist_dataset.train.next_batch(train_batch_size)
# Getting and reshape the images from the corresponding batch
batch_images = input_batch[0].reshape((-1, 28, 28, 1))
# Add random noise to the input images
noisy_images = batch_images + mnist_noise_factor * np.random.randn(*batch_images.shape)
# Clipping all the values that are above 0 or above 1
noisy_images = np.clip(noisy_images, 0., 1.)
# Set the input images to be the noisy ones and the original images to be the target
input_batch_cost, _ = sess.run([model_cost, model_optimizer], feed_dict={inputs_values: noisy_images,
targets_values: batch_images})
print("Epoch: {}/{}...".format(e+1, num_epochs),
"Training loss: {:.3f}".format(input_batch_cost))
Output:
.
.
.
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.096
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.096
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.097
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.101
现在我们已经训练好了模型,能够生成无噪声的图像,这使得自编码器适用于许多领域。
在接下来的代码片段中,我们不会直接将 MNIST 测试集的原始图像输入到模型中,因为我们需要先给这些图像添加噪声,看看训练好的模型如何生成无噪声的图像。
在这里,我给测试图像加入噪声,并通过自编码器传递它们。即使有时很难分辨原始的数字是什么,模型仍然能够出色地去除噪声:
#Defining some figures
fig, axes = plt.subplots(nrows=2, ncols=10, sharex=True, sharey=True, figsize=(20,4))
#Visualizing some images
input_images = mnist_dataset.test.images[:10]
noisy_imgs = input_images + mnist_noise_factor * np.random.randn(*input_images.shape)
#Clipping and reshaping the noisy images
noisy_images = np.clip(noisy_images, 0., 1.).reshape((10, 28, 28, 1))
#Getting the reconstructed images
reconstructed_images = sess.run(decoding_layer, feed_dict={inputs_values: noisy_images})
#Visualizing the input images and the noisy ones
for imgs, row in zip([noisy_images, reconstructed_images], axes):
for img, ax in zip(imgs, row):
ax.imshow(img.reshape((28, 28)), cmap='Greys_r')
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
fig.tight_layout(pad=0.1)
Output:
图 12:原始测试图像中加入一些高斯噪声(顶部行)以及基于训练好的去噪自编码器重建后的图像示例
自编码器的应用
在之前从较低表示构建图像的示例中,我们看到它与原始输入非常相似,而且我们还看到在去噪噪声数据集时,卷积自编码网络(CANs)的优势。我们上面实现的这种示例对于图像构建应用和数据集去噪非常有用。因此,你可以将上述实现推广到你感兴趣的任何其他示例。
此外,在本章中,我们还展示了自编码器架构的灵活性,以及我们如何对其进行各种更改。我们甚至测试了它在解决更复杂的图像去噪问题中的表现。这种灵活性为自编码器的更多应用开辟了大门。
图像上色
自编码器——尤其是卷积版本——可以用于更难的任务,例如图像上色。在接下来的示例中,我们给模型输入一张没有颜色的图像,经过自编码器模型重建后的图像将会被上色:
图 13:CAE 模型训练进行图像上色
图 14:上色论文架构
现在我们的自编码器已经训练完成,我们可以用它给我们之前从未见过的图片上色!
这种应用可以用于给拍摄于早期相机时代的非常老旧图像上色。
更多应用
另一个有趣的应用是生成更高分辨率的图像,或者像下图所示的神经图像增强。
这些图展示了理查德·张提供的更真实的图像上色版本:
图 15:理查德·张(Richard Zhang)、菲利普·伊索拉(Phillip Isola)和亚历克塞·A·埃夫罗斯(Alexei A. Efros)提供的彩色图像上色
这张图展示了自编码器在图像增强中的另一个应用:
图 16:亚历克西(Alexjc)提供的神经增强(github.com/alexjc/neural-enhance)
总结
在本章中,我们介绍了一种全新的架构,可以用于许多有趣的应用。自编码器非常灵活,所以可以随意提出你自己在图像增强、上色或构建方面的问题。此外,还有自编码器的更多变体,称为变分自编码器。它们也被用于非常有趣的应用,比如图像生成。
第十四章:生成对抗网络
生成对抗网络(GANs)是深度神经网络架构,由两个相互对立的网络组成(因此得名对抗)。
GANs 是在 2014 年由 Ian Goodfellow 和其他研究人员,包括 Yoshua Bengio,在蒙特利尔大学的一篇论文中提出的(arxiv.org/abs/1406.2661)。Facebook 的 AI 研究总监 Yann LeCun 曾提到,对抗训练是过去 10 年中在机器学习领域最有趣的想法。
GANs 的潜力巨大,因为它们可以学习模仿任何数据分布。也就是说,GANs 可以被训练生成在任何领域与我们现实世界极为相似的世界:图像、音乐、语音或散文。从某种意义上说,它们是机器人艺术家,它们的输出令人印象深刻(www.nytimes.com/2017/08/14/arts/design/google-how-ai-creates-new-music-and-new-artists-project-magenta.html)——并且也令人感动。
本章将涵盖以下内容:
-
直观介绍
-
GANs 的简单实现
-
深度卷积 GANs
直观介绍
本节将以非常直观的方式介绍 GANs。为了理解 GANs 的工作原理,我们将采用一个虚拟情境——获得一张派对门票的过程。
故事从一个非常有趣的派对或活动开始,这个活动在某个地方举行,你非常想参加。你听说这个活动时已经很晚了,所有的门票都卖光了,但你会不惜一切代价进入派对。所以,你想到了一个主意!你将尝试伪造一张与原始门票完全相同或者非常相似的票。但因为生活总是充满挑战,还有一个难题:你不知道原始门票长什么样子。所以,根据你参加过类似派对的经验,你开始想象这张门票可能是什么样子的,并开始根据你的想象设计这张票。
你将尝试设计这张票,然后去活动现场,向保安展示这张票。希望他们会被说服,并让你进去。但你不想多次向保安展示自己的脸,于是你决定寻求朋友的帮助,朋友会拿着你对原始门票的初步猜测去向保安展示。如果他们没有让他进去,他会根据看到其他人用真正的门票进入的情况,给你一些信息,告诉你这张票可能是什么样的。你会根据朋友的反馈不断完善门票,直到保安允许他进入。到那时——只有到那时——你才会设计出一张完全相同的票,并让自己也顺利进入。
不要过多思考这个故事有多不现实,但 GAN 的工作原理与这个故事非常相似。如今 GAN 非常流行,人们将其用于计算机视觉领域的许多应用。
你可以将 GAN 用于许多有趣的应用,我们将在实现并提到其中的一些应用。
在 GAN 中,有两个主要组成部分在许多计算机视觉领域中取得了突破。第一个组件被称为生成器,第二个组件被称为判别器:
-
生成器将尝试从特定的概率分布中生成数据样本,这与那个试图复制活动票的人的行为非常相似。
-
判别器将判断(就像安保人员试图找出票上的缺陷,以决定它是原始的还是伪造的)输入是来自原始训练集(原始票)还是来自生成器部分(由试图复制原始票的人设计):
图 1:GAN——通用架构
简单的 GAN 实现
从伪造活动票的故事来看,GAN 的概念似乎非常直观。为了清楚地理解 GAN 如何工作以及如何实现它们,我们将展示一个在 MNIST 数据集上实现简单 GAN 的例子。
首先,我们需要构建 GAN 网络的核心,主要由两个部分组成:生成器和判别器。正如我们所说,生成器将尝试从特定的概率分布中生成或伪造数据样本;判别器则能够访问和看到实际的数据样本,它将判断生成器的输出是否在设计上存在缺陷,或者它与原始数据样本有多么接近。类似于事件的场景,生成器的整个目的是尽力说服判别器,生成的图像来自真实数据集,从而试图欺骗判别器。
训练过程的结果类似于活动故事的结局;生成器最终将成功生成看起来与原始数据样本非常相似的图像:
图 2:MNIST 数据集的 GAN 通用架构
任何 GAN 的典型结构如图 2所示,将会在 MNIST 数据集上进行训练。图中的潜在样本部分是一个随机的思维或向量,生成器利用它来用假图像复制真实图像。
如我们所述,判别器充当一个判断者,它会尝试将生成器设计的假图像与真实图像区分开来。因此,这个网络的输出将是二值的,可以通过一个 sigmoid 函数表示,0 表示输入是一个假图像,1 表示输入是一个真实图像。
让我们继续并开始实现这个架构,看看它在 MNIST 数据集上的表现。
让我们从导入实现所需的库开始:
%matplotlib inline
import matplotlib.pyplot as plt
import pickle as pkl
import numpy as np
import tensorflow as tf
我们将使用 MNIST 数据集,因此我们将使用 TensorFlow 辅助工具获取数据集并将其存储在某个地方:
from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets('MNIST_data')
Output:
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz
模型输入
在深入构建 GAN 的核心部分(由生成器和鉴别器表示)之前,我们将定义计算图的输入。如 图 2 所示,我们需要两个输入。第一个是实际图像,将传递给鉴别器。另一个输入称为 潜在空间,它将传递给生成器并用于生成假图像:
# Defining the model input for the generator and discrimator
def inputs_placeholders(discrimator_real_dim, gen_z_dim):
real_discrminator_input = tf.placeholder(tf.float32, (None, discrimator_real_dim), name="real_discrminator_input")
generator_inputs_z = tf.placeholder(tf.float32, (None, gen_z_dim), name="generator_input_z")
return real_discrminator_input, generator_inputs_z
图 3:MNIST GAN 实现的架构
现在是时候开始构建我们架构的两个核心组件了。我们将从构建生成器部分开始。如 图 3 所示,生成器将包含至少一个隐藏层,该层将作为近似器工作。此外,我们将使用一种称为 leaky ReLU 的激活函数,而不是使用普通的 ReLU 激活函数。这样,梯度值就可以在层之间流动,而没有任何限制(关于 leaky ReLU 的更多内容将在下一节介绍)。
变量作用域
变量作用域是 TensorFlow 的一个功能,帮助我们完成以下任务:
-
确保我们有一些命名约定以便稍后能检索它们,例如,可以让它们以“generator”或“discriminator”开头,这将帮助我们在训练网络时。我们本可以使用作用域命名功能,但这个功能对第二个目的帮助不大。
-
为了能够重用或重新训练相同的网络,但使用不同的输入。例如,我们将从生成器中采样假图像,以查看生成器在复制原始图像方面的表现如何。另外,鉴别器将能够访问真实图像和假图像,这将使我们能够重用变量,而不是在构建计算图时创建新变量。
以下语句将展示如何使用 TensorFlow 的变量作用域功能:
with tf.variable_scope('scopeName', reuse=False):
# Write your code here
你可以在www.tensorflow.org/programmers_guide/variable_scope#the_problem阅读更多关于使用变量作用域功能的好处。
Leaky ReLU
我们提到过,我们将使用一个不同版本的 ReLU 激活函数,称为 leaky ReLU。传统版本的 ReLU 激活函数会选择输入值与零的最大值,换句话说,将负值截断为零。而 leaky ReLU,即我们将使用的版本,允许某些负值存在,因此得名 leaky ReLU。
有时,如果我们使用传统的 ReLU 激活函数,网络会陷入一个叫做“死亡状态”的常见状态,这时网络对于所有输出都会产生零值。
使用 Leaky ReLU 的目的是通过允许一些负值通过,防止出现“死亡”状态。
使生成器正常工作的整个思路是接收来自判别器的梯度值,如果网络处于“死亡”状态,则学习过程无法发生。
以下图示展示了传统 ReLU 和其 Leaky 版本之间的区别:
图 4:ReLU 函数
图 5:Leaky ReLU 激活函数
Leaky ReLU 激活函数在 TensorFlow 中没有实现,因此我们需要自己实现。该激活函数的输出如果输入为正,则为正值,如果输入为负,则为一个受控的负值。我们将通过一个名为 alpha 的参数来控制负值的大小,从而通过允许一些负值通过来为网络引入容忍度。
以下方程表示我们将要实现的 Leaky ReLU:
生成器
MNIST 图像的值已归一化在 0 和 1 之间,在这个范围内,sigmoid 激活函数表现最佳。但是,实际上,发现 tanh 激活函数的性能优于其他任何函数。因此,为了使用 tanh 激活函数,我们需要将这些图像的像素值范围重新缩放到 -1 和 1 之间:
def generator(gen_z, gen_out_dim, num_hiddern_units=128, reuse_vars=False, leaky_relu_alpha=0.01):
''' Building the generator part of the network
Function arguments
---------
gen_z : the generator input tensor
gen_out_dim : the output shape of the generator
num_hiddern_units : Number of neurons/units in the hidden layer
reuse_vars : Reuse variables with tf.variable_scope
leaky_relu_alpha : leaky ReLU parameter
Function Returns
-------
tanh_output, logits_layer:
'''
with tf.variable_scope('generator', reuse=reuse_vars):
# Defining the generator hidden layer
hidden_layer_1 = tf.layers.dense(gen_z, num_hiddern_units, activation=None)
# Feeding the output of hidden_layer_1 to leaky relu
hidden_layer_1 = tf.maximum(hidden_layer_1, leaky_relu_alpha*hidden_layer_1)
# Getting the logits and tanh layer output
logits_layer = tf.layers.dense(hidden_layer_1, gen_out_dim, activation=None)
tanh_output = tf.nn.tanh(logits_layer)
return tanh_output, logits_layer
现在生成器部分已经准备好了,让我们继续定义网络的第二个组件。
判别器
接下来,我们将构建生成对抗网络中的第二个主要组件,即判别器。判别器与生成器非常相似,但我们将使用 sigmoid 激活函数,而不是 tanh 激活函数;它将输出一个二进制结果,表示判别器对输入图像的判断:
def discriminator(disc_input, num_hiddern_units=128, reuse_vars=False, leaky_relu_alpha=0.01):
''' Building the discriminator part of the network
Function Arguments
---------
disc_input : discrminator input tensor
num_hiddern_units : Number of neurons/units in the hidden layer
reuse_vars : Reuse variables with tf.variable_scope
leaky_relu_alpha : leaky ReLU parameter
Function Returns
-------
sigmoid_out, logits_layer:
'''
with tf.variable_scope('discriminator', reuse=reuse_vars):
# Defining the generator hidden layer
hidden_layer_1 = tf.layers.dense(disc_input, num_hiddern_units, activation=None)
# Feeding the output of hidden_layer_1 to leaky relu
hidden_layer_1 = tf.maximum(hidden_layer_1, leaky_relu_alpha*hidden_layer_1)
logits_layer = tf.layers.dense(hidden_layer_1, 1, activation=None)
sigmoid_out = tf.nn.sigmoid(logits_layer)
return sigmoid_out, logits_layer
构建 GAN 网络
在定义了构建生成器和判别器部分的主要函数后,接下来是将它们堆叠起来,并定义模型的损失函数和优化器进行实现。
模型超参数
我们可以通过更改以下超参数集来微调 GAN:
# size of discriminator input image
#28 by 28 will flattened to be 784
input_img_size = 784
# size of the generator latent vector
gen_z_size = 100
# number of hidden units for the generator and discriminator hidden layers
gen_hidden_size = 128
disc_hidden_size = 128
#leaky ReLU alpha parameter which controls the leak of the function
leaky_relu_alpha = 0.01
# smoothness of the label
label_smooth = 0.1
定义生成器和判别器
在定义了生成虚假 MNIST 图像的架构的两个主要部分后(这些图像看起来和真实的完全一样),现在是时候使用我们目前定义的函数来构建网络了。为了构建网络,我们将按照以下步骤进行:
-
定义我们的模型输入,这将由两个变量组成。其中一个变量是真实图像,将被输入到判别器中,另一个是潜在空间,将被生成器用于复制原始图像。
-
调用定义好的生成器函数来构建网络的生成器部分。
-
调用定义的判别器函数来构建网络的判别器部分,但我们将调用这个函数两次。第一次调用将用于真实数据,第二次调用将用于来自生成器的假数据。
-
通过重用变量保持真实图像和假图像的权重相同:
tf.reset_default_graph()
# creating the input placeholders for the discrminator and generator
real_discrminator_input, generator_input_z = inputs_placeholders(input_img_size, gen_z_size)
#Create the generator network
gen_model, gen_logits = generator(generator_input_z, input_img_size, gen_hidden_size, reuse_vars=False, leaky_relu_alpha=leaky_relu_alpha)
# gen_model is the output of the generator
#Create the generator network
disc_model_real, disc_logits_real = discriminator(real_discrminator_input, disc_hidden_size, reuse_vars=False, leaky_relu_alpha=leaky_relu_alpha)
disc_model_fake, disc_logits_fake = discriminator(gen_model, disc_hidden_size, reuse_vars=True, leaky_relu_alpha=leaky_relu_alpha)
判别器和生成器的损失
在这一部分,我们需要定义判别器和生成器的损失,这可以看作是该实现中最棘手的部分。
我们知道生成器试图复制原始图像,而判别器充当一个判断者,接收来自生成器和原始输入图像的两种图像。所以,在设计每个部分的损失时,我们需要关注两个方面。
首先,我们需要网络的判别器部分能够区分由生成器生成的假图像和来自原始训练示例的真实图像。在训练时,我们将为判别器部分提供一个批次,该批次分为两类。第一类是来自原始输入的图像,第二类是生成器生成的假图像。
所以,判别器的最终总损失将是其将真实样本识别为真实和将假样本识别为假的能力之和;然后最终的总损失将是:
tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits_layer, labels=labels))
所以,我们需要计算两个损失来得出最终的判别器损失。
第一个损失,disc_loss_real,将根据我们从判别器得到的logits值和labels计算,这里标签将全部为 1,因为我们知道这个小批次中的所有图像都来自 MNIST 数据集的真实输入图像。为了增强模型在测试集上的泛化能力并提供更好的结果,实践中发现将 1 的值改为 0.9 更好。对标签值进行这样的修改引入了标签平滑:
labels = tf.ones_like(tensor) * (1 - smooth)
对于判别器损失的第二部分,即判别器检测假图像的能力,损失将是判别器从生成器得到的 logits 值与标签之间的差异;这些标签全为 0,因为我们知道这个小批次中的所有图像都是来自生成器,而不是来自原始输入图像。
现在我们已经讨论了判别器损失,我们还需要计算生成器的损失。生成器损失将被称为gen_loss,它是disc_logits_fake(判别器对于假图像的输出)和标签之间的损失(由于生成器试图通过其设计的假图像来说服判别器,因此标签将全部为 1):
# calculating the losses of the discrimnator and generator
disc_labels_real = tf.ones_like(disc_logits_real) * (1 - label_smooth)
disc_labels_fake = tf.zeros_like(disc_logits_fake)
disc_loss_real = tf.nn.sigmoid_cross_entropy_with_logits(labels=disc_labels_real, logits=disc_logits_real)
disc_loss_fake = tf.nn.sigmoid_cross_entropy_with_logits(labels=disc_labels_fake, logits=disc_logits_fake)
#averaging the disc loss
disc_loss = tf.reduce_mean(disc_loss_real + disc_loss_fake)
#averaging the gen loss
gen_loss = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(
labels=tf.ones_like(disc_logits_fake),
logits=disc_logits_fake))
优化器
最后是优化器部分!在这一部分,我们将定义训练过程中使用的优化标准。首先,我们将分别更新生成器和鉴别器的变量,因此我们需要能够获取每部分的变量。
对于第一个优化器,即生成器优化器,我们将从计算图的可训练变量中获取所有以generator开头的变量;然后,我们可以通过变量的名称来查看每个变量代表什么。
我们对鉴别器的变量也做相同的处理,允许所有以discriminator开头的变量。之后,我们可以将需要优化的变量列表传递给优化器。
所以,TensorFlow 的变量范围功能使我们能够获取以特定字符串开头的变量,然后我们可以得到两份不同的变量列表,一份是生成器的,一份是鉴别器的:
# building the model optimizer
learning_rate = 0.002
# Getting the trainable_variables of the computational graph, split into Generator and Discrimnator parts
trainable_vars = tf.trainable_variables()
gen_vars = [var for var in trainable_vars if var.name.startswith("generator")]
disc_vars = [var for var in trainable_vars if var.name.startswith("discriminator")]
disc_train_optimizer = tf.train.AdamOptimizer().minimize(disc_loss, var_list=disc_vars)
gen_train_optimizer = tf.train.AdamOptimizer().minimize(gen_loss, var_list=gen_vars)
模型训练
现在,让我们开始训练过程,看看 GAN 是如何生成与 MNIST 图像相似的图像的:
train_batch_size = 100
num_epochs = 100
generated_samples = []
model_losses = []
saver = tf.train.Saver(var_list = gen_vars)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for e in range(num_epochs):
for ii in range(mnist_dataset.train.num_examples//train_batch_size):
input_batch = mnist_dataset.train.next_batch(train_batch_size)
# Get images, reshape and rescale to pass to D
input_batch_images = input_batch[0].reshape((train_batch_size, 784))
input_batch_images = input_batch_images*2 - 1
# Sample random noise for G
gen_batch_z = np.random.uniform(-1, 1, size=(train_batch_size, gen_z_size))
# Run optimizers
_ = sess.run(disc_train_optimizer, feed_dict={real_discrminator_input: input_batch_images, generator_input_z: gen_batch_z})
_ = sess.run(gen_train_optimizer, feed_dict={generator_input_z: gen_batch_z})
# At the end of each epoch, get the losses and print them out
train_loss_disc = sess.run(disc_loss, {generator_input_z: gen_batch_z, real_discrminator_input: input_batch_images})
train_loss_gen = gen_loss.eval({generator_input_z: gen_batch_z})
print("Epoch {}/{}...".format(e+1, num_epochs),
"Disc Loss: {:.3f}...".format(train_loss_disc),
"Gen Loss: {:.3f}".format(train_loss_gen))
# Save losses to view after training
model_losses.append((train_loss_disc, train_loss_gen))
# Sample from generator as we're training for viegenerator_inputs_zwing afterwards
gen_sample_z = np.random.uniform(-1, 1, size=(16, gen_z_size))
generator_samples = sess.run(
generator(generator_input_z, input_img_size, reuse_vars=True),
feed_dict={generator_input_z: gen_sample_z})
generated_samples.append(generator_samples)
saver.save(sess, './checkpoints/generator_ck.ckpt')
# Save training generator samples
with open('train_generator_samples.pkl', 'wb') as f:
pkl.dump(generated_samples, f)
Output:
.
.
.
Epoch 71/100... Disc Loss: 1.078... Gen Loss: 1.361
Epoch 72/100... Disc Loss: 1.037... Gen Loss: 1.555
Epoch 73/100... Disc Loss: 1.194... Gen Loss: 1.297
Epoch 74/100... Disc Loss: 1.120... Gen Loss: 1.730
Epoch 75/100... Disc Loss: 1.184... Gen Loss: 1.425
Epoch 76/100... Disc Loss: 1.054... Gen Loss: 1.534
Epoch 77/100... Disc Loss: 1.457... Gen Loss: 0.971
Epoch 78/100... Disc Loss: 0.973... Gen Loss: 1.688
Epoch 79/100... Disc Loss: 1.324... Gen Loss: 1.370
Epoch 80/100... Disc Loss: 1.178... Gen Loss: 1.710
Epoch 81/100... Disc Loss: 1.070... Gen Loss: 1.649
Epoch 82/100... Disc Loss: 1.070... Gen Loss: 1.530
Epoch 83/100... Disc Loss: 1.117... Gen Loss: 1.705
Epoch 84/100... Disc Loss: 1.042... Gen Loss: 2.210
Epoch 85/100... Disc Loss: 1.152... Gen Loss: 1.260
Epoch 86/100... Disc Loss: 1.327... Gen Loss: 1.312
Epoch 87/100... Disc Loss: 1.069... Gen Loss: 1.759
Epoch 88/100... Disc Loss: 1.001... Gen Loss: 1.400
Epoch 89/100... Disc Loss: 1.215... Gen Loss: 1.448
Epoch 90/100... Disc Loss: 1.108... Gen Loss: 1.342
Epoch 91/100... Disc Loss: 1.227... Gen Loss: 1.468
Epoch 92/100... Disc Loss: 1.190... Gen Loss: 1.328
Epoch 93/100... Disc Loss: 0.869... Gen Loss: 1.857
Epoch 94/100... Disc Loss: 0.946... Gen Loss: 1.740
Epoch 95/100... Disc Loss: 0.925... Gen Loss: 1.708
Epoch 96/100... Disc Loss: 1.067... Gen Loss: 1.427
Epoch 97/100... Disc Loss: 1.099... Gen Loss: 1.573
Epoch 98/100... Disc Loss: 0.972... Gen Loss: 1.884
Epoch 99/100... Disc Loss: 1.292... Gen Loss: 1.610
Epoch 100/100... Disc Loss: 1.103... Gen Loss: 1.736
在运行模型 100 个周期后,我们得到了一个训练好的模型,它能够生成与我们输入给鉴别器的原始图像相似的图像:
fig, ax = plt.subplots()
model_losses = np.array(model_losses)
plt.plot(model_losses.T[0], label='Disc loss')
plt.plot(model_losses.T[1], label='Gen loss')
plt.title("Model Losses")
plt.legend()
输出:
图 6:鉴别器和生成器的损失
如前图所示,可以看到模型的损失(由鉴别器和生成器的曲线表示)正在收敛。
生成器从训练中获得的样本
让我们测试一下模型的表现,甚至看看生成器在接近训练结束时,生成技能(为活动设计票据)是如何增强的:
def view_generated_samples(epoch_num, g_samples):
fig, axes = plt.subplots(figsize=(7,7), nrows=4, ncols=4, sharey=True, sharex=True)
print(gen_samples[epoch_num][1].shape)
for ax, gen_image in zip(axes.flatten(), g_samples[0][epoch_num]):
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
img = ax.imshow(gen_image.reshape((28,28)), cmap='Greys_r')
return fig, axes
在绘制训练过程中最后一个周期生成的一些图像之前,我们需要加载包含每个周期生成样本的持久化文件:
# Load samples from generator taken while training
with open('train_generator_samples.pkl', 'rb') as f:
gen_samples = pkl.load(f)
现在,让我们绘制出训练过程中最后一个周期生成的 16 张图像,看看生成器是如何生成有意义的数字(如 3、7 和 2)的:
_ = view_generated_samples(-1, gen_samples)
图 7:最终训练周期的样本
我们甚至可以看到生成器在不同周期中的设计能力。所以,让我们可视化它在每 10 个周期生成的图像:
rows, cols = 10, 6
fig, axes = plt.subplots(figsize=(7,12), nrows=rows, ncols=cols, sharex=True, sharey=True)
for gen_sample, ax_row in zip(gen_samples[::int(len(gen_samples)/rows)], axes):
for image, ax in zip(gen_sample[::int(len(gen_sample)/cols)], ax_row):
ax.imshow(image.reshape((28,28)), cmap='Greys_r')
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
图 8:网络训练过程中每 10 个周期生成的图像
正如你所看到的,生成器的设计能力以及其生成假图像的能力在最开始时非常有限,之后随着训练过程的进行有所增强。
从生成器采样
在上一节中,我们展示了一些在这个 GAN 架构训练过程中生成的示例图像。我们还可以通过加载我们保存的检查点,并给生成器提供一个新的潜在空间,让它用来生成全新的图像:
# Sampling from the generator
saver = tf.train.Saver(var_list=g_vars)
with tf.Session() as sess:
#restoring the saved checkpints
saver.restore(sess, tf.train.latest_checkpoint('checkpoints'))
gen_sample_z = np.random.uniform(-1, 1, size=(16, z_size))
generated_samples = sess.run(
generator(generator_input_z, input_img_size, reuse_vars=True),
feed_dict={generator_input_z: gen_sample_z})
view_generated_samples(0, [generated_samples])
图 9:来自生成器的样本
在实现这个例子时,你可以得出一些观察结果。在训练过程的最初几个周期里,生成器没有能力生成与真实图像相似的图像,因为它不知道这些图像是什么样子的。即使是判别器也不知道如何区分由生成器生成的假图像和真实图像。在训练开始时,会出现两种有趣的情况。首先,生成器不知道如何创建像我们最初输入到网络中的真实图像一样的图像。第二,判别器不知道真实图像和假图像之间的区别。
随着训练的进行,生成器开始生成一定程度上有意义的图像,这是因为生成器会学习到原始输入图像的来源数据分布。与此同时,判别器将能够区分真假图像,最终在训练结束时被生成器所欺骗。
摘要
现在,GANs 已经被应用于许多有趣的领域。GANs 可以用于不同的设置,例如半监督学习和无监督学习任务。此外,由于大量研究人员正在致力于 GANs,这些模型日益进步,生成图像或视频的能力越来越强。
这些模型可以用于许多有趣的商业应用,比如为 Photoshop 添加一个插件,可以接受像让我的笑容更迷人这样的命令。它们还可以用于图像去噪。
第十五章:人脸生成与处理缺失标签
我们可以使用 GAN 的有趣应用无穷无尽。在本章中,我们将演示 GAN 的另一个有前途的应用——基于 CelebA 数据库的人脸生成。我们还将演示如何在半监督学习设置中使用 GAN,其中我们有一个标签不全的数据集。
本章将涵盖以下主题:
-
人脸生成
-
使用生成对抗网络(GAN)的半监督学习
人脸生成
如我们在上一章所提到的,生成器和判别器由反卷积网络(DNN:www.quora.com/How-does-a-deconvolutional-neural-network-work)和卷积神经网络(CNN:cs231n.github.io/convolutional-networks/)组成:
-
CNN 是一种神经网络,它将图像的数百个像素编码成一个小维度的向量(z),该向量是图像的摘要。
-
DNN 是一种网络,它学习一些滤波器,以从 z 中恢复原始图像。
此外,判别器会输出 1 或 0,表示输入的图像是来自真实数据集,还是由生成器生成。另一方面,生成器会尝试根据潜在空间 z 复制与原始数据集相似的图像,这些图像可能遵循高斯分布。因此,判别器的目标是正确区分真实图像,而生成器的目标是学习原始数据集的分布,从而欺骗判别器,使其做出错误的决策。
在本节中,我们将尝试教导生成器学习人脸图像的分布,以便它能够生成逼真的人脸。
生成类人面孔对于大多数图形公司来说至关重要,这些公司总是在为其应用程序寻找新的面孔,这也让我们看到人工智能在生成逼真人脸方面接近现实的程度。
在本示例中,我们将使用 CelebA 数据集。CelebFaces 属性数据集(CelebA)是一个大规模的面部属性数据集,包含约 20 万张名人图像,每张图像有 40 个属性标注。数据集涵盖了大量的姿势变化,以及背景杂乱,因此 CelebA 非常多样且注释完备。它包括:
-
10,177 个身份
-
202,599 张人脸图像
-
每张图像有五个地标位置和 40 个二元属性注释
我们可以将此数据集用于许多计算机视觉应用,除了人脸生成,还可以用于人脸识别、定位或人脸属性检测。
该图展示了生成器误差,或者说学习人脸分布,在训练过程中如何逐渐接近现实:
图 1:使用 GAN 从名人图像数据集中生成新面孔
获取数据
在这一部分,我们将定义一些辅助函数,帮助我们下载 CelebA 数据集。我们将通过导入实现所需的包开始:
import math
import os
import hashlib
from urllib.request import urlretrieve
import zipfile
import gzip
import shutil
import numpy as np
from PIL import Image
from tqdm import tqdm
import utils
import tensorflow as tf
接下来,我们将使用 utils 脚本下载数据集:
#Downloading celebA dataset
celebA_data_dir = 'input'
utils.download_extract('celeba', celebA_data_dir)
Output:
Downloading celeba: 1.44GB [00:21, 66.6MB/s]
Extracting celeba...
探索数据
CelebA 数据集包含超过 20 万张带注释的名人图像。由于我们将使用 GAN 生成类似的图像,因此值得看一些来自数据集的图像,看看它们的效果。在这一部分,我们将定义一些辅助函数,用于可视化 CelebA 数据集中的一组图像。
现在,让我们使用utils脚本从数据集中显示一些图像:
#number of images to display
num_images_to_show = 25
celebA_images = utils.get_batch(glob(os.path.join(celebA_data_dir, 'img_align_celeba/*.jpg'))[:num_images_to_show], 28,
28, 'RGB')
pyplot.imshow(utils.images_square_grid(celebA_images, 'RGB'))
Output:
图 2:从 CelebA 数据集中绘制一组图像
这个计算机视觉任务的主要目标是使用 GAN 生成类似于名人数据集中图像的图像,因此我们需要专注于图像的面部部分。为了聚焦于图像的面部部分,我们将去除不包含名人面孔的部分。
构建模型
现在,让我们开始构建我们实现的核心——计算图;它主要包括以下组件:
-
模型输入
-
判别器
-
生成器
-
模型损失
-
模型优化器
-
训练模型
模型输入
在这一部分,我们将实现一个辅助函数,定义模型的输入占位符,这些占位符将负责将数据输入到计算图中。
这些函数应该能够创建三个主要的占位符:
-
来自数据集的实际输入图像,尺寸为(批量大小,输入图像宽度,输入图像高度,通道数)
-
潜在空间 Z,将被生成器用来生成假图像
-
学习率占位符
辅助函数将返回这三个输入占位符的元组。现在,让我们定义这个函数:
# defining the model inputs
def inputs(img_width, img_height, img_channels, latent_space_z_dim):
true_inputs = tf.placeholder(tf.float32, (None, img_width, img_height, img_channels),
'true_inputs')
l_space_inputs = tf.placeholder(tf.float32, (None, latent_space_z_dim), 'l_space_inputs')
model_learning_rate = tf.placeholder(tf.float32, name='model_learning_rate')
return true_inputs, l_space_inputs, model_learning_rate
判别器
接下来,我们需要实现网络的判别器部分,用于判断输入是来自真实数据集还是由生成器生成的。我们将再次使用 TensorFlow 的tf.variable_scope功能为一些变量添加前缀“判别器”,以便我们能够检索和重用它们。
那么,让我们定义一个函数,返回判别器的二进制输出以及 logit 值:
# Defining the discriminator function
def discriminator(input_imgs, reuse=False):
# using variable_scope to reuse variables
with tf.variable_scope('discriminator', reuse=reuse):
# leaky relu parameter
leaky_param_alpha = 0.2
# defining the layers
conv_layer_1 = tf.layers.conv2d(input_imgs, 64, 5, 2, 'same')
leaky_relu_output = tf.maximum(leaky_param_alpha * conv_layer_1, conv_layer_1)
conv_layer_2 = tf.layers.conv2d(leaky_relu_output, 128, 5, 2, 'same')
normalized_output = tf.layers.batch_normalization(conv_layer_2, training=True)
leay_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)
conv_layer_3 = tf.layers.conv2d(leay_relu_output, 256, 5, 2, 'same')
normalized_output = tf.layers.batch_normalization(conv_layer_3, training=True)
leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)
# reshaping the output for the logits to be 2D tensor
flattened_output = tf.reshape(leaky_relu_output, (-1, 4 * 4 * 256))
logits_layer = tf.layers.dense(flattened_output, 1)
output = tf.sigmoid(logits_layer)
return output, logits_layer
生成器
现在,轮到实现网络的第二部分,它将尝试使用潜在空间z来复制原始输入图像。我们也将使用tf.variable_scope来实现这个功能。
那么,让我们定义一个函数,返回生成器生成的图像:
def generator(z_latent_space, output_channel_dim, is_train=True):
with tf.variable_scope('generator', reuse=not is_train):
#leaky relu parameter
leaky_param_alpha = 0.2
fully_connected_layer = tf.layers.dense(z_latent_space, 2*2*512)
#reshaping the output back to 4D tensor to match the accepted format for convolution layer
reshaped_output = tf.reshape(fully_connected_layer, (-1, 2, 2, 512))
normalized_output = tf.layers.batch_normalization(reshaped_output, training=is_train)
leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)
conv_layer_1 = tf.layers.conv2d_transpose(leaky_relu_output, 256, 5, 2, 'valid')
normalized_output = tf.layers.batch_normalization(conv_layer_1, training=is_train)
leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)
conv_layer_2 = tf.layers.conv2d_transpose(leaky_relu_output, 128, 5, 2, 'same')
normalized_output = tf.layers.batch_normalization(conv_layer_2, training=is_train)
leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)
logits_layer = tf.layers.conv2d_transpose(leaky_relu_output, output_channel_dim, 5, 2, 'same')
output = tf.tanh(logits_layer)
return output
模型损失
接下来是比较棘手的部分,我们在前一章中讲过,即计算判别器和生成器的损失。
所以,让我们定义这个函数,它将使用之前定义的generator和discriminator函数:
# Define the error for the discriminator and generator
def model_losses(input_actual, input_latent_z, out_channel_dim):
# building the generator part
gen_model = generator(input_latent_z, out_channel_dim)
disc_model_true, disc_logits_true = discriminator(input_actual)
disc_model_fake, disc_logits_fake = discriminator(gen_model, reuse=True)
disc_loss_true = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=disc_logits_true, labels=tf.ones_like(disc_model_true)))
disc_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
logits=disc_logits_fake, labels=tf.zeros_like(disc_model_fake)))
gen_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
logits=disc_logits_fake, labels=tf.ones_like(disc_model_fake)))
disc_loss = disc_loss_true + disc_loss_fake
return disc_loss, gen_loss
模型优化器
最后,在训练我们的模型之前,我们需要实现该任务的优化标准。我们将使用之前使用的命名约定来检索判别器和生成器的可训练参数并训练它们:
# specifying the optimization criteria
def model_optimizer(disc_loss, gen_loss, learning_rate, beta1):
trainable_vars = tf.trainable_variables()
disc_vars = [var for var in trainable_vars if var.name.startswith('discriminator')]
gen_vars = [var for var in trainable_vars if var.name.startswith('generator')]
disc_train_opt = tf.train.AdamOptimizer(
learning_rate, beta1=beta1).minimize(disc_loss, var_list=disc_vars)
update_operations = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
gen_updates = [opt for opt in update_operations if opt.name.startswith('generator')]
with tf.control_dependencies(gen_updates):
gen_train_opt = tf.train.AdamOptimizer(
learning_rate, beta1).minimize(gen_loss, var_list=gen_vars)
return disc_train_opt, gen_train_opt
训练模型
现在,是时候训练模型并观察生成器如何在一定程度上欺骗判别器,通过生成与原始 CelebA 数据集非常接近的图像。
但首先,让我们定义一个辅助函数,它将展示生成器生成的一些图像:
# define a function to visualize some generated images from the generator
def show_generator_output(sess, num_images, input_latent_z, output_channel_dim, img_mode):
cmap = None if img_mode == 'RGB' else 'gray'
latent_space_z_dim = input_latent_z.get_shape().as_list()[-1]
examples_z = np.random.uniform(-1, 1, size=[num_images, latent_space_z_dim])
examples = sess.run(
generator(input_latent_z, output_channel_dim, False),
feed_dict={input_latent_z: examples_z})
images_grid = utils.images_square_grid(examples, img_mode)
pyplot.imshow(images_grid, cmap=cmap)
pyplot.show()
然后,我们将使用之前定义的辅助函数来构建模型输入、损失和优化标准。我们将它们堆叠在一起,并开始基于 CelebA 数据集训练我们的模型:
def model_train(num_epocs, train_batch_size, z_dim, learning_rate, beta1, get_batches, input_data_shape, data_img_mode):
_, image_width, image_height, image_channels = input_data_shape
actual_input, z_input, leaningRate = inputs(
image_width, image_height, image_channels, z_dim)
disc_loss, gen_loss = model_losses(actual_input, z_input, image_channels)
disc_opt, gen_opt = model_optimizer(disc_loss, gen_loss, learning_rate, beta1)
steps = 0
print_every = 50
show_every = 100
model_loss = []
num_images = 25
with tf.Session() as sess:
# initializing all the variables
sess.run(tf.global_variables_initializer())
for epoch_i in range(num_epocs):
for batch_images in get_batches(train_batch_size):
steps += 1
batch_images *= 2.0
z_sample = np.random.uniform(-1, 1, (train_batch_size, z_dim))
_ = sess.run(disc_opt, feed_dict={
actual_input: batch_images, z_input: z_sample, leaningRate: learning_rate})
_ = sess.run(gen_opt, feed_dict={
z_input: z_sample, leaningRate: learning_rate})
if steps % print_every == 0:
train_loss_disc = disc_loss.eval({z_input: z_sample, actual_input: batch_images})
train_loss_gen = gen_loss.eval({z_input: z_sample})
print("Epoch {}/{}...".format(epoch_i + 1, num_epocs),
"Discriminator Loss: {:.4f}...".format(train_loss_disc),
"Generator Loss: {:.4f}".format(train_loss_gen))
model_loss.append((train_loss_disc, train_loss_gen))
if steps % show_every == 0:
show_generator_output(sess, num_images, z_input, image_channels, data_img_mode)
启动训练过程,这可能会根据你的主机机器规格需要一些时间:
# Training the model on CelebA dataset
train_batch_size = 64
z_dim = 100
learning_rate = 0.002
beta1 = 0.5
num_epochs = 1
celeba_dataset = utils.Dataset('celeba', glob(os.path.join(data_dir, 'img_align_celeba/*.jpg')))
with tf.Graph().as_default():
model_train(num_epochs, train_batch_size, z_dim, learning_rate, beta1, celeba_dataset.get_batches,
celeba_dataset.shape, celeba_dataset.image_mode)
输出:
Epoch 1/1... Discriminator Loss: 0.9118... Generator Loss: 12.2238
Epoch 1/1... Discriminator Loss: 0.6119... Generator Loss: 3.2168
Epoch 1/1... Discriminator Loss: 0.5383... Generator Loss: 2.8054
Epoch 1/1... Discriminator Loss: 1.4381... Generator Loss: 0.4672
Epoch 1/1... Discriminator Loss: 0.7815... Generator Loss: 14.8220
Epoch 1/1... Discriminator Loss: 0.6435... Generator Loss: 9.2591
Epoch 1/1... Discriminator Loss: 1.5661... Generator Loss: 10.4747
Epoch 1/1... Discriminator Loss: 1.5407... Generator Loss: 0.5811
Epoch 1/1... Discriminator Loss: 0.6470... Generator Loss: 2.9002
Epoch 1/1... Discriminator Loss: 0.5671... Generator Loss: 2.0700
图 3:此时训练的生成输出样本
Epoch 1/1... Discriminator Loss: 0.7950... Generator Loss: 1.5818
Epoch 1/1... Discriminator Loss: 1.2417... Generator Loss: 0.7094
Epoch 1/1... Discriminator Loss: 1.1786... Generator Loss: 1.0948
Epoch 1/1... Discriminator Loss: 1.0427... Generator Loss: 2.8878
Epoch 1/1... Discriminator Loss: 0.8409... Generator Loss: 2.6785
Epoch 1/1... Discriminator Loss: 0.8557... Generator Loss: 1.7706
Epoch 1/1... Discriminator Loss: 0.8241... Generator Loss: 1.2898
Epoch 1/1... Discriminator Loss: 0.8590... Generator Loss: 1.8217
Epoch 1/1... Discriminator Loss: 1.1694... Generator Loss: 0.8490
Epoch 1/1... Discriminator Loss: 0.9984... Generator Loss: 1.0042
图 4:此时训练的生成输出样本
经过一段时间的训练后,你应该会得到类似这样的结果:
图 5:此时训练的生成输出样本
使用生成对抗网络(GAN)进行半监督学习
鉴于此,半监督学习是一种技术,其中使用标注数据和未标注数据来训练分类器。
这种类型的分类器采用一小部分标注数据和大量未标注数据(来自同一领域)。目标是将这些数据源结合起来,训练一个深度卷积神经网络(DCNN),以学习一个推断函数,能够将新的数据点映射到其期望的结果。
在这个领域,我们提出了一种 GAN 模型,用于使用非常小的标注训练集对街景房号进行分类。实际上,该模型使用了原始 SVHN 训练标签的大约 1.3%,即 1000 个标注样本。我们使用了在论文Improved Techniques for Training GANs from OpenAI中描述的一些技术(arxiv.org/abs/1606.03498)。
直觉
在构建生成图像的 GAN 时,我们同时训练生成器和判别器。训练后,我们可以丢弃判别器,因为我们只在训练生成器时使用了它。
图 6:用于 11 类分类问题的半监督学习 GAN 架构
在半监督学习中,我们需要将判别器转变为一个多类分类器。这个新模型必须能够在测试集上很好地泛化,尽管我们没有很多带标签的训练样本。此外,这一次,训练结束时,我们实际上可以抛弃生成器。请注意,角色发生了变化。现在,生成器仅用于在训练过程中帮助判别器。换句话说,生成器充当了一个不同的信息来源,判别器从中获取未经标签的原始训练数据。正如我们将看到的,这些未经标签的数据对于提高判别器的性能至关重要。此外,对于一个常规的图像生成 GAN,判别器只有一个角色:计算其输入是否真实的概率——我们将其称为 GAN 问题。
然而,要将判别器转变为一个半监督分类器,除了 GAN 问题,判别器还必须学习原始数据集每个类别的概率。换句话说,对于每个输入图像,判别器必须学习它属于某个类别(如 1、2、3 等)的概率。
回顾一下,对于图像生成 GAN 的判别器,我们有一个单一的 sigmoid 单元输出。这个值代表了输入图像是真实的概率(接近 1),还是假的概率(接近 0)。换句话说,从判别器的角度来看,接近 1 的值意味着样本很可能来自训练集。同样,接近 0 的值则意味着样本更有可能来自生成器网络。通过使用这个概率,判别器能够将信号传回生成器。这个信号使生成器能够在训练过程中调整其参数,从而有可能提高生成真实图像的能力。
我们必须将判别器(来自之前的 GAN)转换为一个 11 类分类器。为此,我们可以将其 sigmoid 输出转换为具有 11 个类别输出的 softmax,前 10 个输出表示 SVHN 数据集各个类别的概率(0 至 9),第 11 类则表示所有来自生成器的假图像。
请注意,如果我们将第 11 类的概率设为 0,那么前 10 个概率的总和就等于使用 sigmoid 函数计算的相同概率。
最后,我们需要设置损失函数,使得判别器能够同时完成两项任务:
-
帮助生成器学习生成真实图像。为了做到这一点,我们必须指示判别器区分真实样本和假样本。
-
使用生成器的图像以及带标签和不带标签的训练数据,帮助分类数据集。
总结一下,判别器有三种不同的训练数据来源:
-
带标签的真实图像。这些是像任何常规监督分类问题一样的图像标签对。
-
没有标签的真实图像。对于这些图像,分类器只能学习到这些图像是“真实的”。
-
来自生成器的图像。为了使用这些图像,判别器学习将其分类为假。
这些不同数据源的结合将使分类器能够从更广泛的角度学习。反过来,这使得模型的推理性能比仅使用 1,000 个标注样本进行训练时更为精准。
数据分析和预处理
对于这个任务,我们将使用 SVHN 数据集,它是斯坦福大学的街景房屋号码(Street View House Numbers)数据集的缩写(ufldl.stanford.edu/housenumbers/)。所以,让我们通过导入实现所需的包开始实现:
# Lets start by loading the necessary libraries
%matplotlib inline
import pickle as pkl
import time
import matplotlib.pyplot as plt
import numpy as np
from scipy.io import loadmat
import tensorflow as tf
import os
接下来,我们将定义一个辅助类来下载 SVHN 数据集(记得你需要首先手动创建input_data_dir目录):
from urllib.request import urlretrieve
from os.path import isfile, isdir
from tqdm import tqdm
input_data_dir = 'input/'
input_data_dir = 'input/'
if not isdir(input_data_dir):
raise Exception("Data directory doesn't exist!")
class DLProgress(tqdm):
last_block = 0
def hook(self, block_num=1, block_size=1, total_size=None):
self.total = total_size
self.update((block_num - self.last_block) * block_size)
self.last_block = block_num
if not isfile(input_data_dir + "train_32x32.mat"):
with DLProgress(unit='B', unit_scale=True, miniters=1, desc='SVHN Training Set') as pbar:
urlretrieve(
'http://ufldl.stanford.edu/housenumbers/train_32x32.mat',
input_data_dir + 'train_32x32.mat',
pbar.hook)
if not isfile(input_data_dir + "test_32x32.mat"):
with DLProgress(unit='B', unit_scale=True, miniters=1, desc='SVHN Training Set') as pbar:
urlretrieve(
'http://ufldl.stanford.edu/housenumbers/test_32x32.mat',
input_data_dir + 'test_32x32.mat',
pbar.hook)
train_data = loadmat(input_data_dir + 'train_32x32.mat')
test_data = loadmat(input_data_dir + 'test_32x32.mat')
Output:
trainset shape: (32, 32, 3, 73257)
testset shape: (32, 32, 3, 26032)
让我们了解一下这些图像的样子:
indices = np.random.randint(0, train_data['X'].shape[3], size=36)
fig, axes = plt.subplots(6, 6, sharex=True, sharey=True, figsize=(5,5),)
for ii, ax in zip(indices, axes.flatten()):
ax.imshow(train_data['X'][:,:,:,ii], aspect='equal')
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
plt.subplots_adjust(wspace=0, hspace=0)
Output:
图 7:来自 SVHN 数据集的样本图像。
接下来,我们需要将图像缩放到-1 到 1 之间,这对于使用tanh()函数是必要的,因为该函数将压缩生成器输出的值:
# Scaling the input images
def scale_images(image, feature_range=(-1, 1)):
# scale image to (0, 1)
image = ((image - image.min()) / (255 - image.min()))
# scale the image to feature range
min, max = feature_range
image = image * (max - min) + min
return image
class Dataset:
def __init__(self, train_set, test_set, validation_frac=0.5, shuffle_data=True, scale_func=None):
split_ind = int(len(test_set['y']) * (1 - validation_frac))
self.test_input, self.valid_input = test_set['X'][:, :, :, :split_ind], test_set['X'][:, :, :, split_ind:]
self.test_target, self.valid_target = test_set['y'][:split_ind], test_set['y'][split_ind:]
self.train_input, self.train_target = train_set['X'], train_set['y']
# The street house number dataset comes with lots of labels,
# but because we are going to do semi-supervised learning we are going to assume that we don't have all labels
# like, assume that we have only 1000
self.label_mask = np.zeros_like(self.train_target)
self.label_mask[0:1000] = 1
self.train_input = np.rollaxis(self.train_input, 3)
self.valid_input = np.rollaxis(self.valid_input, 3)
self.test_input = np.rollaxis(self.test_input, 3)
if scale_func is None:
self.scaler = scale_images
else:
self.scaler = scale_func
self.train_input = self.scaler(self.train_input)
self.valid_input = self.scaler(self.valid_input)
self.test_input = self.scaler(self.test_input)
self.shuffle = shuffle_data
def batches(self, batch_size, which_set="train"):
input_name = which_set + "_input"
target_name = which_set + "_target"
num_samples = len(getattr(dataset, target_name))
if self.shuffle:
indices = np.arange(num_samples)
np.random.shuffle(indices)
setattr(dataset, input_name, getattr(dataset, input_name)[indices])
setattr(dataset, target_name, getattr(dataset, target_name)[indices])
if which_set == "train":
dataset.label_mask = dataset.label_mask[indices]
dataset_input = getattr(dataset, input_name)
dataset_target = getattr(dataset, target_name)
for jj in range(0, num_samples, batch_size):
input_vals = dataset_input[jj:jj + batch_size]
target_vals = dataset_target[jj:jj + batch_size]
if which_set == "train":
# including the label mask in case of training
# to pretend that we don't have all the labels
yield input_vals, target_vals, self.label_mask[jj:jj + batch_size]
else:
yield input_vals, target_vals
构建模型
在本节中,我们将构建所有必要的组件以进行测试,因此我们首先定义将用于向计算图输入数据的输入。
模型输入
首先,我们将定义模型输入函数,该函数将创建用于输入数据的模型占位符:
# defining the model inputs
def inputs(actual_dim, z_dim):
inputs_actual = tf.placeholder(tf.float32, (None, *actual_dim), name='input_actual')
inputs_latent_z = tf.placeholder(tf.float32, (None, z_dim), name='input_latent_z')
target = tf.placeholder(tf.int32, (None), name='target')
label_mask = tf.placeholder(tf.int32, (None), name='label_mask')
return inputs_actual, inputs_latent_z, target, label_mask
生成器
在本节中,我们将实现 GAN 网络的第一个核心部分。该部分的架构和实现将遵循原始的 DCGAN 论文:
def generator(latent_z, output_image_dim, reuse_vars=False, leaky_alpha=0.2, is_training=True, size_mult=128):
with tf.variable_scope('generator', reuse=reuse_vars):
# define a fully connected layer
fully_conntected_1 = tf.layers.dense(latent_z, 4 * 4 * size_mult * 4)
# Reshape it from 2D tensor to 4D tensor to be fed to the convolution neural network
reshaped_out_1 = tf.reshape(fully_conntected_1, (-1, 4, 4, size_mult * 4))
batch_normalization_1 = tf.layers.batch_normalization(reshaped_out_1, training=is_training)
leaky_output_1 = tf.maximum(leaky_alpha * batch_normalization_1, batch_normalization_1)
conv_layer_1 = tf.layers.conv2d_transpose(leaky_output_1, size_mult * 2, 5, strides=2, padding='same')
batch_normalization_2 = tf.layers.batch_normalization(conv_layer_1, training=is_training)
leaky_output_2 = tf.maximum(leaky_alpha * batch_normalization_2, batch_normalization_2)
conv_layer_2 = tf.layers.conv2d_transpose(leaky_output_2, size_mult, 5, strides=2, padding='same')
batch_normalization_3 = tf.layers.batch_normalization(conv_layer_2, training=is_training)
leaky_output_3 = tf.maximum(leaky_alpha * batch_normalization_3, batch_normalization_3)
# defining the output layer
logits_layer = tf.layers.conv2d_transpose(leaky_output_3, output_image_dim, 5, strides=2, padding='same')
output = tf.tanh(logits_layer)
return output
判别器
现在,是时候构建 GAN 网络的第二个核心部分——判别器了。在之前的实现中,我们提到判别器会产生一个二元输出,表示输入图像是否来自真实数据集(1)还是由生成器生成(0)。在这里,情况有所不同,因此判别器将变为一个多类别分类器。
现在,让我们继续构建架构中的判别器部分:
# Defining the discriminator part of the network
def discriminator(input_x, reuse_vars=False, leaky_alpha=0.2, drop_out_rate=0., num_classes=10, size_mult=64):
with tf.variable_scope('discriminator', reuse=reuse_vars):
# defining a dropout layer
drop_out_output = tf.layers.dropout(input_x, rate=drop_out_rate / 2.5)
# Defining the input layer for the discriminator which is 32x32x3
conv_layer_3 = tf.layers.conv2d(input_x, size_mult, 3, strides=2, padding='same')
leaky_output_4 = tf.maximum(leaky_alpha * conv_layer_3, conv_layer_3)
leaky_output_4 = tf.layers.dropout(leaky_output_4, rate=drop_out_rate)
conv_layer_4 = tf.layers.conv2d(leaky_output_4, size_mult, 3, strides=2, padding='same')
batch_normalization_4 = tf.layers.batch_normalization(conv_layer_4, training=True)
leaky_output_5 = tf.maximum(leaky_alpha * batch_normalization_4, batch_normalization_4)
conv_layer_5 = tf.layers.conv2d(leaky_output_5, size_mult, 3, strides=2, padding='same')
batch_normalization_5 = tf.layers.batch_normalization(conv_layer_5, training=True)
leaky_output_6 = tf.maximum(leaky_alpha * batch_normalization_5, batch_normalization_5)
leaky_output_6 = tf.layers.dropout(leaky_output_6, rate=drop_out_rate)
conv_layer_6 = tf.layers.conv2d(leaky_output_6, 2 * size_mult, 3, strides=1, padding='same')
batch_normalization_6 = tf.layers.batch_normalization(conv_layer_6, training=True)
leaky_output_7 = tf.maximum(leaky_alpha * batch_normalization_6, batch_normalization_6)
conv_layer_7 = tf.layers.conv2d(leaky_output_7, 2 * size_mult, 3, strides=1, padding='same')
batch_normalization_7 = tf.layers.batch_normalization(conv_layer_7, training=True)
leaky_output_8 = tf.maximum(leaky_alpha * batch_normalization_7, batch_normalization_7)
conv_layer_8 = tf.layers.conv2d(leaky_output_8, 2 * size_mult, 3, strides=2, padding='same')
batch_normalization_8 = tf.layers.batch_normalization(conv_layer_8, training=True)
leaky_output_9 = tf.maximum(leaky_alpha * batch_normalization_8, batch_normalization_8)
leaky_output_9 = tf.layers.dropout(leaky_output_9, rate=drop_out_rate)
conv_layer_9 = tf.layers.conv2d(leaky_output_9, 2 * size_mult, 3, strides=1, padding='valid')
leaky_output_10 = tf.maximum(leaky_alpha * conv_layer_9, conv_layer_9)
...
我们将不再在最后应用全连接层,而是执行所谓的全局平均池化(GAP),该操作在特征向量的空间维度上取平均值;这将把张量压缩为一个单一的值:
...
# Flatten it by global average pooling
leaky_output_features = tf.reduce_mean(leaky_output_10, (1, 2))
...
例如,假设经过一系列卷积操作后,我们得到一个形状为的输出张量:
[BATCH_SIZE, 8, 8, NUM_CHANNELS]
要应用全局平均池化,我们计算[8x8]张量片的平均值。该操作将产生一个形状如下的张量:
[BATCH_SIZE, 1, 1, NUM_CHANNELS]
这可以重塑为:
[BATCH_SIZE, NUM_CHANNELS].
在应用全局平均池化后,我们添加一个全连接层,该层将输出最终的 logits。这些 logits 的形状为:
[BATCH_SIZE, NUM_CLASSES]
这将表示每个类别的得分。为了获得这些类别的概率得分,我们将使用softmax激活函数:
...
# Get the probability that the input is real rather than fake
softmax_output = tf.nn.softmax(classes_logits)s
...
最终,判别器函数将如下所示:
# Defining the discriminator part of the network
def discriminator(input_x, reuse_vars=False, leaky_alpha=0.2, drop_out_rate=0., num_classes=10, size_mult=64):
with tf.variable_scope('discriminator', reuse=reuse_vars):
# defining a dropout layer
drop_out_output = tf.layers.dropout(input_x, rate=drop_out_rate / 2.5)
# Defining the input layer for the discrminator which is 32x32x3
conv_layer_3 = tf.layers.conv2d(input_x, size_mult, 3, strides=2, padding='same')
leaky_output_4 = tf.maximum(leaky_alpha * conv_layer_3, conv_layer_3)
leaky_output_4 = tf.layers.dropout(leaky_output_4, rate=drop_out_rate)
conv_layer_4 = tf.layers.conv2d(leaky_output_4, size_mult, 3, strides=2, padding='same')
batch_normalization_4 = tf.layers.batch_normalization(conv_layer_4, training=True)
leaky_output_5 = tf.maximum(leaky_alpha * batch_normalization_4, batch_normalization_4)
conv_layer_5 = tf.layers.conv2d(leaky_output_5, size_mult, 3, strides=2, padding='same')
batch_normalization_5 = tf.layers.batch_normalization(conv_layer_5, training=True)
leaky_output_6 = tf.maximum(leaky_alpha * batch_normalization_5, batch_normalization_5)
leaky_output_6 = tf.layers.dropout(leaky_output_6, rate=drop_out_rate)
conv_layer_6 = tf.layers.conv2d(leaky_output_6, 2 * size_mult, 3, strides=1, padding='same')
batch_normalization_6 = tf.layers.batch_normalization(conv_layer_6, training=True)
leaky_output_7 = tf.maximum(leaky_alpha * batch_normalization_6, batch_normalization_6)
conv_layer_7 = tf.layers.conv2d(leaky_output_7, 2 * size_mult, 3, strides=1, padding='same')
batch_normalization_7 = tf.layers.batch_normalization(conv_layer_7, training=True)
leaky_output_8 = tf.maximum(leaky_alpha * batch_normalization_7, batch_normalization_7)
conv_layer_8 = tf.layers.conv2d(leaky_output_8, 2 * size_mult, 3, strides=2, padding='same')
batch_normalization_8 = tf.layers.batch_normalization(conv_layer_8, training=True)
leaky_output_9 = tf.maximum(leaky_alpha * batch_normalization_8, batch_normalization_8)
leaky_output_9 = tf.layers.dropout(leaky_output_9, rate=drop_out_rate)
conv_layer_9 = tf.layers.conv2d(leaky_output_9, 2 * size_mult, 3, strides=1, padding='valid')
leaky_output_10 = tf.maximum(leaky_alpha * conv_layer_9, conv_layer_9)
# Flatten it by global average pooling
leaky_output_features = tf.reduce_mean(leaky_output_10, (1, 2))
# Set class_logits to be the inputs to a softmax distribution over the different classes
classes_logits = tf.layers.dense(leaky_output_features, num_classes + extra_class)
if extra_class:
actual_class_logits, fake_class_logits = tf.split(classes_logits, [num_classes, 1], 1)
assert fake_class_logits.get_shape()[1] == 1, fake_class_logits.get_shape()
fake_class_logits = tf.squeeze(fake_class_logits)
else:
actual_class_logits = classes_logits
fake_class_logits = 0.
max_reduced = tf.reduce_max(actual_class_logits, 1, keep_dims=True)
stable_actual_class_logits = actual_class_logits - max_reduced
gan_logits = tf.log(tf.reduce_sum(tf.exp(stable_actual_class_logits), 1)) + tf.squeeze(
max_reduced) - fake_class_logits
softmax_output = tf.nn.softmax(classes_logits)
return softmax_output, classes_logits, gan_logits, leaky_output_features
模型损失
现在是时候定义模型的损失函数了。首先,判别器的损失将分为两部分:
-
一个将表示 GAN 问题的部分,即无监督损失
-
第二部分将计算每个实际类别的概率,这就是监督损失
对于判别器的无监督损失,它必须区分真实训练图像和生成器生成的图像。
和常规 GAN 一样,一半时间,判别器将从训练集获取未标记的图像作为输入,另一半时间,从生成器获取虚假未标记的图像。
对于判别器损失的第二部分,即监督损失,我们需要基于判别器的 logits 来构建。因此,我们将使用 softmax 交叉熵,因为这是一个多分类问题。
正如《增强训练 GAN 的技术》论文中提到的,我们应该使用特征匹配来计算生成器的损失。正如作者所描述的:
“特征匹配是通过惩罚训练数据集上一组特征的平均值与生成样本上该组特征的平均值之间的绝对误差来实现的。为此,我们从两个不同的来源提取一组统计数据(矩),并迫使它们相似。首先,我们取出从判别器中提取的特征的平均值,这些特征是在处理真实训练小批量数据时得到的。其次,我们以相同的方式计算矩,但这次是针对当判别器分析来自生成器的虚假图像小批量时的情况。最后,利用这两个矩的集合,生成器的损失是它们之间的平均绝对差。换句话说,正如论文强调的那样:我们训练生成器使其匹配判别器中间层特征的期望值。”
最终,模型的损失函数将如下所示:
def model_losses(input_actual, input_latent_z, output_dim, target, num_classes, label_mask, leaky_alpha=0.2,
drop_out_rate=0.):
# These numbers multiply the size of each layer of the generator and the discriminator,
# respectively. You can reduce them to run your code faster for debugging purposes.
gen_size_mult = 32
disc_size_mult = 64
# Here we run the generator and the discriminator
gen_model = generator(input_latent_z, output_dim, leaky_alpha=leaky_alpha, size_mult=gen_size_mult)
disc_on_data = discriminator(input_actual, leaky_alpha=leaky_alpha, drop_out_rate=drop_out_rate,
size_mult=disc_size_mult)
disc_model_real, class_logits_on_data, gan_logits_on_data, data_features = disc_on_data
disc_on_samples = discriminator(gen_model, reuse_vars=True, leaky_alpha=leaky_alpha,
drop_out_rate=drop_out_rate, size_mult=disc_size_mult)
disc_model_fake, class_logits_on_samples, gan_logits_on_samples, sample_features = disc_on_samples
# Here we compute `disc_loss`, the loss for the discriminator.
disc_loss_actual = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_data,
labels=tf.ones_like(gan_logits_on_data)))
disc_loss_fake = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_samples,
labels=tf.zeros_like(gan_logits_on_samples)))
target = tf.squeeze(target)
classes_cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=class_logits_on_data,
labels=tf.one_hot(target,
num_classes + extra_class,
dtype=tf.float32))
classes_cross_entropy = tf.squeeze(classes_cross_entropy)
label_m = tf.squeeze(tf.to_float(label_mask))
disc_loss_class = tf.reduce_sum(label_m * classes_cross_entropy) / tf.maximum(1., tf.reduce_sum(label_m))
disc_loss = disc_loss_class + disc_loss_actual + disc_loss_fake
# Here we set `gen_loss` to the "feature matching" loss invented by Tim Salimans.
sampleMoments = tf.reduce_mean(sample_features, axis=0)
dataMoments = tf.reduce_mean(data_features, axis=0)
gen_loss = tf.reduce_mean(tf.abs(dataMoments - sampleMoments))
prediction_class = tf.cast(tf.argmax(class_logits_on_data, 1), tf.int32)
check_prediction = tf.equal(tf.squeeze(target), prediction_class)
correct = tf.reduce_sum(tf.to_float(check_prediction))
masked_correct = tf.reduce_sum(label_m * tf.to_float(check_prediction))
return disc_loss, gen_loss, correct, masked_correct, gen_model
模型优化器
现在,让我们定义模型优化器,它与我们之前定义的非常相似:
def model_optimizer(disc_loss, gen_loss, learning_rate, beta1):
# Get weights and biases to update. Get them separately for the discriminator and the generator
trainable_vars = tf.trainable_variables()
disc_vars = [var for var in trainable_vars if var.name.startswith('discriminator')]
gen_vars = [var for var in trainable_vars if var.name.startswith('generator')]
for t in trainable_vars:
assert t in disc_vars or t in gen_vars
# Minimize both gen and disc costs simultaneously
disc_train_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(disc_loss,
var_list=disc_vars)
gen_train_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(gen_loss, var_list=gen_vars)
shrink_learning_rate = tf.assign(learning_rate, learning_rate * 0.9)
return disc_train_optimizer, gen_train_optimizer, shrink_learning_rate
模型训练
最后,在将所有内容组合在一起后,让我们开始训练过程:
class GAN:
def __init__(self, real_size, z_size, learning_rate, num_classes=10, alpha=0.2, beta1=0.5):
tf.reset_default_graph()
self.learning_rate = tf.Variable(learning_rate, trainable=False)
model_inputs = inputs(real_size, z_size)
self.input_actual, self.input_latent_z, self.target, self.label_mask = model_inputs
self.drop_out_rate = tf.placeholder_with_default(.5, (), "drop_out_rate")
losses_results = model_losses(self.input_actual, self.input_latent_z,
real_size[2], self.target, num_classes,
label_mask=self.label_mask,
leaky_alpha=0.2,
drop_out_rate=self.drop_out_rate)
self.disc_loss, self.gen_loss, self.correct, self.masked_correct, self.samples = losses_results
self.disc_opt, self.gen_opt, self.shrink_learning_rate = model_optimizer(self.disc_loss, self.gen_loss,
self.learning_rate, beta1)
def view_generated_samples(epoch, samples, nrows, ncols, figsize=(5, 5)):
fig, axes = plt.subplots(figsize=figsize, nrows=nrows, ncols=ncols,
sharey=True, sharex=True)
for ax, img in zip(axes.flatten(), samples[epoch]):
ax.axis('off')
img = ((img - img.min()) * 255 / (img.max() - img.min())).astype(np.uint8)
ax.set_adjustable('box-forced')
im = ax.imshow(img)
plt.subplots_adjust(wspace=0, hspace=0)
return fig, axes
def train(net, dataset, epochs, batch_size, figsize=(5, 5)):
saver = tf.train.Saver()
sample_z = np.random.normal(0, 1, size=(50, latent_space_z_size))
samples, train_accuracies, test_accuracies = [], [], []
steps = 0
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for e in range(epochs):
print("Epoch", e)
num_samples = 0
num_correct_samples = 0
for x, y, label_mask in dataset.batches(batch_size):
assert 'int' in str(y.dtype)
steps += 1
num_samples += label_mask.sum()
# Sample random noise for G
batch_z = np.random.normal(0, 1, size=(batch_size, latent_space_z_size))
_, _, correct = sess.run([net.disc_opt, net.gen_opt, net.masked_correct],
feed_dict={net.input_actual: x, net.input_latent_z: batch_z,
net.target: y, net.label_mask: label_mask})
num_correct_samples += correct
sess.run([net.shrink_learning_rate])
training_accuracy = num_correct_samples / float(num_samples)
print("\t\tClassifier train accuracy: ", training_accuracy)
num_samples = 0
num_correct_samples = 0
for x, y in dataset.batches(batch_size, which_set="test"):
assert 'int' in str(y.dtype)
num_samples += x.shape[0]
correct, = sess.run([net.correct], feed_dict={net.input_real: x,
net.y: y,
net.drop_rate: 0.})
num_correct_samples += correct
testing_accuracy = num_correct_samples / float(num_samples)
print("\t\tClassifier test accuracy", testing_accuracy)
gen_samples = sess.run(
net.samples,
feed_dict={net.input_latent_z: sample_z})
samples.append(gen_samples)
_ = view_generated_samples(-1, samples, 5, 10, figsize=figsize)
plt.show()
# Save history of accuracies to view after training
train_accuracies.append(training_accuracy)
test_accuracies.append(testing_accuracy)
saver.save(sess, './checkpoints/generator.ckpt')
with open('samples.pkl', 'wb') as f:
pkl.dump(samples, f)
return train_accuracies, test_accuracies, samples
别忘了创建一个名为 checkpoints 的目录:
real_size = (32,32,3)
latent_space_z_size = 100
learning_rate = 0.0003
net = GAN(real_size, latent_space_z_size, learning_rate)
dataset = Dataset(train_data, test_data)
train_batch_size = 128
num_epochs = 25
train_accuracies, test_accuracies, samples = train(net,
dataset,
num_epochs,
train_batch_size,
figsize=(10,5))
最后,在Epoch 24时,你应该得到如下结果:
Epoch 24
Classifier train accuracy: 0.937
Classifier test accuracy 0.67401659496
Step time: 0.03694915771484375
Epoch time: 26.15842580795288
图 8:使用特征匹配损失由生成器网络创建的示例图像
fig, ax = plt.subplots()
plt.plot(train_accuracies, label='Train', alpha=0.5)
plt.plot(test_accuracies, label='Test', alpha=0.5)
plt.title("Accuracy")
plt.legend()
图 9:训练过程中训练与测试的准确率
尽管特征匹配损失在半监督学习任务中表现良好,但生成器生成的图像不如上一章中创建的图像那么好。不过,这个实现主要是为了展示我们如何将 GAN 应用于半监督学习设置。
概述
最后,许多研究人员认为无监督学习是通用人工智能系统中的缺失环节。为了克服这些障碍,尝试通过使用更少标注数据来解决已知问题是关键。在这种情况下,GAN 为使用较少标注样本学习复杂任务提供了真正的替代方案。然而,监督学习和半监督学习之间的性能差距仍然相当大。我们可以肯定地预期,随着新方法的出现,这一差距将逐渐缩小。