GAN 启发自博弈论中的二人零和博弈(two-player game), 论文:Generative Adversarial Networks(arxiv:https://arxiv.org/abs/1406.2661) GAN 模型中的两位博弈方分别由生成式模型G(generative model)和判别式模型N(discriminative model)组成。
基本原理 G是一个生成图片的网络,它接收一个随机的噪声z,通过这个噪声生成图片,记做G(z)。 D是一个判别网络,判别一张图片是不是“真实的”。它的输入参数是x,x代表一张图片,输出D(x)代表x为真实图片的概率,如果为1,就代表100%是真实的图片,而输出为0,就代表不可能是真实的图片。
在训练过程中,生成网络G的目标就是尽量生成真实的图片去欺骗判别网络D。而D的目标就是尽量把G生成的图片和真实的图片分别开来。这样,G和D构成了一个动态的“博弈过程”。
最后对抗网网络的目标:对于输入一个随机噪声z,生成网络G生成的假样本G(z)进去了判别网络以后,判别网络D给出的结果D(G(z))是一个接近 0.5 的值,也就是说无法分辨是真(1)还是假(0),达到了平衡。从而我们的G可以生成以假乱真的图片。
模型 判别模型,直观来看就是一个简单的神经网络结构,输入就是一副图像,输出就是一个概率值,用于判断真假使用(概率值大于 0.5 那就是真,小于 0.5 那就是假)。 生成模型,生成模型同样也可以看成是一个神经网络模型,输入是一组随机噪声z,输出是一个仿样本G(z)。
实现方法
生成模型与对抗模型可以说是完全独立的两个模型
但是训练时让他们:单独交替迭代训练
先用真实数据集和生成模型产生的数据集进行标签,来训练判别模型。
而训练生成模型时,把生成数据标签成真实数据,让判别模型去判别。(注意,不更新判别模型里面的参数)把误差一直传,传到生成网络那块后更新生成网络的参数。
重复上述过程。
简单来说生成器的结果会有两种应用,一种是标明为假,和真实数据混合起来为判别模型。一种把自己标注为真,用当前的判别模型来进行训练
目标公式
公式详解在下图
整个式子由两部分构成。x表示真实图片,z表示输入G网络的噪声,而G(z)表示G网络生成的图片。 D(x)表示D网络判断真实图片是否真实的概率(因为x就是真实的,所以对于D来说,这个值越接近1越好)。而D(G(z))是D网络判断G生成的图片的是否真实的概率。
单独优化D时,想要让真实数据x的输出让D(x)越大越好,让生成数据D(G(z))越小越好。公式中将D(G(z))写为1-D(G(z))是可以让整个公式统一,V(D)越大 越好。
单独优化G时,只和公式第二部分相关,其目标就是让D(G(z)越大越好,为了整合统一写为1-D(G(z),就是V(G)越小 越好。
用tensorflow实现简单的GAN about tensorflow:tensorflow过程主要分为前向传播和反向传播。
前向传播搭建网络结构。
反向传播训练网络参数。
本次目的:实现通过mnist数据集,训练得到生成数字图像
每训练1000 次保存16张生成图片
共训练500000轮。
参考代码:https://github.com/liuzheng007/GAN/blob/master/gan.py
KL散度又称相对熵,用来衡量两个分布的差异。 KL 散度有很多有用的性质,最重要的是它是非负的。KL 散度为 0时,是 当且仅当 P 和 Q 在离散型变量的情况下是相同的分布,或者在连续型变量的情况下是 『几乎处处』 相同的。因为 KL 散度是非负的并且衡量的是两个分布之间的差异,它经常被用作分布之间的某种距离。然而,它并不是真的距离因为它不是对称的。
最优判别器 在上面极小极大博弈的第一步中,给定生成器 G,最大化 V(D,G) 而得出最优判别器 D。其中,最大化 V(D,G) 评估了 P(G(z)) 和 P(x) 之间的差异或距离。因为在原论文中价值函数可写为在 x 上的积分,即将数学期望展开为积分形式: 其实求积分的最大值可以转化为求被积函数的最大值。而求被积函数的最大值是为了求得最优判别器 D,因此不涉及判别器的项都可以看作为常数项。如图,因此被积函数可表示为 aD(x)+b log(1-D(x)) 【 a,b∈(0,1)】
令判别器 D(x) 等于 y,那么被积函数可以写为: 为了找到最优的极值点,如果 a+b≠0,我们可以对y求一阶导,令导数为零。 继续求表达式 f(y) 在该驻点的二阶导, 其中 a,b∈(0,1)。因为一阶导等于零、二阶导小于零,所以我们知道 a/(a+b) 为极大值。将 a=P(x)、b=P(G(x)) 代入该极值,最优判别器 D(x)=P(x)/(P(x)+P(G(x))),此时价值函数 V(G,D) 取极大值。 其实该最优的 D 在实践中并不是可计算的,但在数学上十分重要。我们并不知道先验的 P(x),所以我们在训练中永远不会用到它。另一方面,它的存在令我们可以证明最优的 G 是存在的,并且在训练中我们只需要逼近 D(x)。
代码解析: 生成模型 首先需要定义一个生成器 G,该生成器需要将输入的随机噪声变换为图像。以下是定义的生成模型,该模型首先输入有 100 个元素的向量,该向量随机生成于某分布。随后利用两个全连接层接连将该输入向量扩展到 1024 维和 1287 7 维,后面就开始将全连接层所产生的一维张量重新塑造成二维张量,即 MNIST 中的灰度图。我们注意到该模型采用的激活函数为 tanh,所以也尝试过将其转换为 relu 函数,但发现生成模型如果转化为 relu 函数,那么它的输出就会成为一片灰色。
由全连接传递的数据会经过几个上采样层和卷积层,我们注意到最后一个卷积层所采用的卷积核为 1,所以经过最后卷积层所生成的图像是一张二维灰度图像,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def generator_model(): #下面搭建生成器的架构,首先导入序贯模型(sequential),即多个网络层的线性堆叠 model = Sequential() #添加一个全连接层,输入为100维向量,输出为1024维 model.add(Dense(input_dim=100, output_dim=1024)) #添加一个激活函数tanh model.add(Activation('tanh')) #添加一个全连接层,输出为128×7×7维度 model.add(Dense(128*7*7)) #添加一个批量归一化层,该层在每个batch上将前一层的激活值重新规范化,即使得其输出数据的均值接近0,其标准差接近1 model.add(BatchNormalization()) model.add(Activation('tanh')) #Reshape层用来将输入shape转换为特定的shape,将含有128*7*7个元素的向量转化为7×7×128张量 model.add(Reshape((7, 7, 128), input_shape=(128*7*7,))) #2维上采样层,即将数据的行和列分别重复2次 model.add(UpSampling2D(size=(2, 2))) #添加一个2维卷积层,卷积核大小为5×5,激活函数为tanh,共64个卷积核,并采用padding以保持图像尺寸不变 model.add(Conv2D(64, (5, 5), padding='same')) model.add(Activation('tanh')) model.add(UpSampling2D(size=(2, 2))) #卷积核设为1即输出图像的维度 model.add(Conv2D(1, (5, 5), padding='same')) model.add(Activation('tanh')) return model
拼接 前面定义的是可生成图像的模型 G(z;θ_g),而我们在训练生成模型时,需要固定判别模型 D 以极小化价值函数而寻求更好的生成模型,这就意味着我们需要将生成模型与判别模型拼接在一起,并固定 D 的权重以训练 G 的权重。下面就定义了这一过程,我们先添加前面定义的生成模型,再将定义的判别模型拼接在生成模型下方,并且我们将判别模型设置为不可训练。因此,训练这个组合模型才能真正更新生成模型的参数。
1 2 3 4 5 6 7 8 9 def generator_containing_discriminator(g, d): #将前面定义的生成器架构和判别器架构组拼接成一个大的神经网络,用于判别生成的图片 model = Sequential() #先添加生成器架构,再令d不可训练,即固定d #因此在给定d的情况下训练生成器,即通过将生成的结果投入到判别器进行辨别而优化生成器 model.add(g) d.trainable = False model.add(d) return model
判别模型 判别模型相对来说就是比较传统的图像识别模型,前面我们可以按照经典的方法采用几个卷积层与最大池化层,而后再展开为一维张量并采用几个全连接层作为架构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def discriminator_model(): #下面搭建判别器架构,同样采用序贯模型 model = Sequential() #添加2维卷积层,卷积核大小为5×5,激活函数为tanh,输入shape在‘channels_first’模式下为(samples,channels,rows,cols) #在‘channels_last’模式下为(samples,rows,cols,channels),输出为64维 model.add( Conv2D(64, (5, 5), padding='same', input_shape=(28, 28, 1)) ) model.add(Activation('tanh')) #为空域信号施加最大值池化,pool_size取(2,2)代表使图片在两个维度上均变为原长的一半 model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(128, (5, 5))) model.add(Activation('tanh')) model.add(MaxPooling2D(pool_size=(2, 2))) #Flatten层把多维输入一维化,常用在从卷积层到全连接层的过渡 model.add(Flatten()) model.add(Dense(1024)) model.add(Activation('tanh')) #一个结点进行二值分类,并采用sigmoid函数的输出作为概念 model.add(Dense(1)) model.add(Activation('sigmoid')) return model
训练 训练过程可简述为:
加载 MNIST 数据 将数据分割为训练与测试集,并赋值给变量 设置训练模型的超参数 编译模型的训练过程 在每一次迭代内,抽取生成图像与真实图像,并打上标注 随后将数据投入到判别模型中,并进行训练与计算损失 固定判别模型,训练生成模型并计算损失,结束这一次迭代
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 def train(BATCH_SIZE): (X_train, y_train), (X_test, y_test) = mnist.load_data() #iamge_data_format选择"channels_last"或"channels_first",该选项指定了Keras将要使用的维度顺序。 #"channels_first"假定2D数据的维度顺序为(channels, rows, cols),3D数据的维度顺序为(channels, conv_dim1, conv_dim2, conv_dim3) #转换字段类型,并将数据导入变量中 X_train = (X_train.astype(np.float32) - 127.5)/127.5 X_train = X_train[:, :, :, None] X_test = X_test[:, :, :, None] # X_train = X_train.reshape((X_train.shape, 1) + X_train.shape[1:]) #将定义好的模型架构赋值给特定的变量 d = discriminator_model() g = generator_model() d_on_g = generator_containing_discriminator(g, d) #定义生成器模型判别器模型更新所使用的优化算法及超参数 d_optim = SGD(lr=0.001, momentum=0.9, nesterov=True) g_optim = SGD(lr=0.001, momentum=0.9, nesterov=True) #编译三个神经网络并设置损失函数和优化算法,其中损失函数都是用的是二元分类交叉熵函数。编译是用来配置模型学习过程的 g.compile(loss='binary_crossentropy', optimizer="SGD") d_on_g.compile(loss='binary_crossentropy', optimizer=g_optim) #前一个架构在固定判别器的情况下训练了生成器,所以在训练判别器之前先要设定其为可训练。 d.trainable = True d.compile(loss='binary_crossentropy', optimizer=d_optim) #下面在满足epoch条件下进行训练 for epoch in range(30): print("Epoch is", epoch) #计算一个epoch所需要的迭代数量,即训练样本数除批量大小数的值取整;其中shape[0]就是读取矩阵第一维度的长度 print("Number of batches", int(X_train.shape[0]/BATCH_SIZE)) #在一个epoch内进行迭代训练 for index in range(int(X_train.shape[0]/BATCH_SIZE)): #随机生成的噪声服从均匀分布,且采样下界为-1、采样上界为1,输出BATCH_SIZE×100个样本;即抽取一个批量的随机样本 noise = np.random.uniform(-1, 1, size=(BATCH_SIZE, 100)) #抽取一个批量的真实图片 image_batch = X_train[index*BATCH_SIZE:(index+1)*BATCH_SIZE] #生成的图片使用生成器对随机噪声进行推断;verbose为日志显示,0为不在标准输出流输出日志信息,1为输出进度条记录 generated_images = g.predict(noise, verbose=0) #每经过100次迭代输出一张生成的图片 if index % 100 == 0: image = combine_images(generated_images) image = image*127.5+127.5 Image.fromarray(image.astype(np.uint8)).save( "./GAN/"+str(epoch)+"_"+str(index)+".png") #将真实的图片和生成的图片以多维数组的形式拼接在一起,真实图片在上,生成图片在下 X = np.concatenate((image_batch, generated_images)) #生成图片真假标签,即一个包含两倍批量大小的列表;前一个批量大小都是1,代表真实图片,后一个批量大小都是0,代表伪造图片 y = [1] * BATCH_SIZE + [0] * BATCH_SIZE #判别器的损失;在一个batch的数据上进行一次参数更新 d_loss = d.train_on_batch(X, y) print("batch %d d_loss : %f" % (index, d_loss)) #随机生成的噪声服从均匀分布 noise = np.random.uniform(-1, 1, (BATCH_SIZE, 100)) #固定判别器 d.trainable = False #计算生成器损失;在一个batch的数据上进行一次参数更新 g_loss = d_on_g.train_on_batch(noise, [1] * BATCH_SIZE) #令判别器可训练 d.trainable = True print("batch %d g_loss : %f" % (index, g_loss)) #每100次迭代保存一次生成器和判别器的权重 if index % 100 == 9: g.save_weights('generator', True) d.save_weights('discriminator', True)