一.前期学习经过

GAN(Generative Adversarial Nets)是生成对抗网络的简称,由生成器和判别器组成,在训练过程中通过生成器和判别器的相互对抗,来相互的促进、提高。最近一段时间对GAN进行了学习,并使用GAN做了一次实践,在这里做一篇笔记记录一下。

最初我参照JensLee大神的讲解,使用keras构造了一个DCGAN(深度卷积生成对抗网络)模型,来对数据集中的256张小狗图像进行学习,都是一些类似这样的狗狗照片:

使用Wasserstein GAN生成小狗图像-LMLPHP

 他的方法是通过随机生成的维度为1000的向量,生成大小为64*64的狗狗图。但经过较长时间的训练,设置了多种超参数进行调试,仍感觉效果不理想,总是在训练到一定程度之后,生成器的loss就不再改变,成为一个固定值,生成的图片也看不出狗的样子。

后续经过查阅资料,了解到DCGAN模型损失函数的定义会使生成器和判别器优化目标相背离,判别器训练的越好,生成器的梯度消失现象越严重,在之前DCGAN的实验中,生成器的loss长时间不变动就是梯度消失引起的。而Wasserstein GAN(简称WGAN)对其进行了改进,修改了生成器和判别器的损失函数,避免了当判别器训练程度较好时,生成器的梯度消失问题,并参照这篇博客,构建了WGAN网络对小狗图像数据集进行学习。

郑华滨大佬的这篇文章对WGAN的原理进行了细致的讲解,想要深入对模型原理进行挖掘的小伙伴可以去深入学习一下,本文重点讲实践应用。

二.模型实现

这里的代码是在TensorFlow框架(版本1.14.0)上实现的,python语言(版本3.6.4)

1.对TensorFlow的卷积、反卷积、全连接等操作进行封装,使其变量名称规整且方便调用。

 1 def conv2d(name, tensor,ksize, out_dim, stddev=0.01, stride=2, padding='SAME'):
 2     with tf.variable_scope(name):
 3         w = tf.get_variable('w', [ksize, ksize, tensor.get_shape()[-1],out_dim], dtype=tf.float32,
 4                             initializer=tf.random_normal_initializer(stddev=stddev))
 5         var = tf.nn.conv2d(tensor,w,[1,stride, stride,1],padding=padding)
 6         b = tf.get_variable('b', [out_dim], 'float32',initializer=tf.constant_initializer(0.01))
 7         return tf.nn.bias_add(var, b)
 8
 9 def deconv2d(name, tensor, ksize, outshape, stddev=0.01, stride=2, padding='SAME'):
10     with tf.variable_scope(name):
11         w = tf.get_variable('w', [ksize, ksize, outshape[-1], tensor.get_shape()[-1]], dtype=tf.float32,
12                             initializer=tf.random_normal_initializer(stddev=stddev))
13         var = tf.nn.conv2d_transpose(tensor, w, outshape, strides=[1, stride, stride, 1], padding=padding)
14         b = tf.get_variable('b', [outshape[-1]], 'float32', initializer=tf.constant_initializer(0.01))
15         return tf.nn.bias_add(var, b)
16
17 def fully_connected(name,value, output_shape):
18     with tf.variable_scope(name, reuse=None) as scope:
19         shape = value.get_shape().as_list()
20         w = tf.get_variable('w', [shape[1], output_shape], dtype=tf.float32,
21                                     initializer=tf.random_normal_initializer(stddev=0.01))
22         b = tf.get_variable('b', [output_shape], dtype=tf.float32, initializer=tf.constant_initializer(0.0))
23         return tf.matmul(value, w) + b
24
25 def relu(name, tensor):
26     return tf.nn.relu(tensor, name)
27
28 def lrelu(name,x, leak=0.2):
29     return tf.maximum(x, leak * x, name=name)

在卷积函数(conv2d)和反卷积函数(deconv2d)中,变量'w'就是指卷积核,他们的维度分布时有差异的,卷积函数中,卷积核的维度为[卷积核高,卷积核宽,输入通道维度,输出通道维度],而反卷积操作中的卷积核则将最后两个维度顺序调换,变为[卷积核高,卷积核宽,输出通道维度,输入通道维度]。反卷积是卷积操作的逆过程,通俗上可理解为:已知卷积结果矩阵(维度y*y),和卷积核(维度k*k),获得卷积前的原始矩阵(维度x*x)这么一个过程。

不论是卷积操作还是反卷积操作,都需要对输出的维度进行计算,并作为函数的参数(即out_dim和outshape变量)输入到函数中。经过个人总结,卷积(反卷积)操作输出维度的计算公式如下(公式为个人总结,如有错误欢迎指出):

正向卷积维度计算:

使用Wasserstein GAN生成小狗图像-LMLPHP

其中,'⌊⌋'是向下取整符号,y是卷积后边长,x是卷积前边长,k指的是卷积核宽/高,stride指步长。

反向卷积维度计算:

 使用Wasserstein GAN生成小狗图像-LMLPHP

其中,y是反卷积前的边长,x是反卷积后的边长,k指的是卷积核宽/高,stride指步长。

relu()和lrelu()函数是两个激活函数,lrelu()其中LeakyRelu激活函数的实现,它能够减轻RELU的稀疏性。

2.对判别器进行构建。 

 1 def Discriminator(name,inputs,reuse):
 2     with tf.variable_scope(name, reuse=reuse):
 3         output = tf.reshape(inputs, [-1, pic_height_width, pic_height_width, inputs.shape[-1]])
 4         output1 = conv2d('d_conv_1', output, ksize=5, out_dim=DEPTH) #32*32
 5         output2 = lrelu('d_lrelu_1', output1)
 6
 7         output3 = conv2d('d_conv_2', output2, ksize=5, padding="VALID",stride=1,out_dim=DEPTH) #28*28
 8         output4 = lrelu('d_lrelu_2', output3)
 9
10         output5 = conv2d('d_conv_3', output4, ksize=5, out_dim=2*DEPTH) #14*14
11         output6 = lrelu('d_lrelu_3', output5)
12
13         output7 = conv2d('d_conv_4', output6, ksize=5, out_dim=4*DEPTH) #7*7
14         output8 = lrelu('d_lrelu_4', output7)
15
16         output9 = conv2d('d_conv_5', output8, ksize=5, out_dim=6*DEPTH) #4*4
17         output10 = lrelu('d_lrelu_5', output9)
18
19         output11 = conv2d('d_conv_6', output10, ksize=5, out_dim=8*DEPTH) #2*2
20         output12 = lrelu('d_lrelu_6', output11)
21
22         chanel = output12.get_shape().as_list()
23         output13 = tf.reshape(output12, [batch_size, chanel[1]*chanel[2]*chanel[3]])
24         output0 = fully_connected('d_fc', output13, 1)
25         return output0

判别器的作用是输入一个固定大小的图像,经过多层卷积、激活函数、全连接计算后,得到一个值,根据这个值可以判定该输入图像是否是狗。

首先是命名空间问题,函数参数name即规定了生成器的命名空间,下面所有新生成的变量都在这个命名空间之内,另外每一步卷积、激活函数、全连接操作都需要手动赋命名空间,这些命名不能重复,如变量output1的命名空间为'd_conv_1',变量output5的命名空间为'd_conv_3',不重复的命名也方便后续对模型进行保存加载。

先将输入图像inputs形变为[-1,64, 64, 3]的tensor,其中第一个维度-1是指任意数量,即任意数量的图片,每张图片长宽各64个像素,有3个通道(RGB),形变后的output维度为[batch_size,64,64,3]。

接下来开始卷积操作得到变量output1,由于默认的stride=2、padding="SAME",根据上面的正向卷积维度计算公式,得到卷积后的图像大小为32*32,而通道数则由一个全局变量DEPTH来确定,这样计算每一层的输出维度,卷积核大小都是5*5,只有在output3这一行填充方式和步长进行了调整,以将其从32*32的图像卷积成28*28。

最后将维度为[batch_size,2,2,8*DEPTH]的变量形变为[batch_size,2*2*8*DEPTH]的变量,在进行全连接操作(矩阵乘法),得到变量output0(维度为[batch_size,1]),即对输入中batch_size个图像的判别结果。

一般的判别器会在全连接层后方加一个sigmoid激活函数,将数值归并到[0,1]之间,以直观的显示该图片是狗的概率,但WGAN使用Wasserstein距离来计算损失,因此需要去掉sigmoid激活函数。

3.对生成器进行构建。

 1 def generator(name, reuse=False):
 2     with tf.variable_scope(name, reuse=reuse):
 3         noise = tf.random_normal([batch_size, 128])#.astype('float32')
 4
 5         noise = tf.reshape(noise, [batch_size, 128], 'noise')
 6         output = fully_connected('g_fc_1', noise, 2*2*8*DEPTH)
 7         output = tf.reshape(output, [batch_size, 2, 2, 8*DEPTH], 'g_conv')
 8
 9         output = deconv2d('g_deconv_1', output, ksize=5, outshape=[batch_size, 4, 4, 6*DEPTH])
10         output = tf.nn.relu(output)
11         # output = tf.reshape(output, [batch_size, 4, 4, 6*DEPTH])
12
13         output = deconv2d('g_deconv_2', output, ksize=5, outshape=[batch_size, 7, 7, 4* DEPTH])
14         output = tf.nn.relu(output)
15
16         output = deconv2d('g_deconv_3', output, ksize=5, outshape=[batch_size, 14, 14, 2*DEPTH])
17         output = tf.nn.relu(output)
18
19         output = deconv2d('g_deconv_4', output, ksize=5, outshape=[batch_size, 28, 28, DEPTH])
20         output = tf.nn.relu(output)
21
22         output = deconv2d('g_deconv_5', output, ksize=5, outshape=[batch_size, 32, 32, DEPTH],stride=1, padding='VALID')
23         output = tf.nn.relu(output)
24
25         output = deconv2d('g_deconv_6', output, ksize=5, outshape=[batch_size, OUTPUT_SIZE, OUTPUT_SIZE, 3])
26         # output = tf.nn.relu(output)
27         output = tf.nn.sigmoid(output)
28         return tf.reshape(output,[-1,OUTPUT_SIZE,OUTPUT_SIZE,3])

生成器的作用是随机产生一个随机值向量,并通过形变、反卷积等操作,将其转变为[64,64,3]的图像。

首先生成随机值向量noise,其维度为[batch_size,128],接下来的步骤和判别器完全相反,先通过全连接层,将其转换为[batch_size, 2*2*8*DEPTH]的变量,并形变为[batch_size, 2, 2, 8*DEPTH]。

之后,通过不断的反卷积操作,将变量维度变为[batch_size, 4, 4, 6*DEPTH]→[batch_size, 7, 7, 4* DEPTH]→[batch_size, 14, 14, 2*DEPTH]→[batch_size, 28, 28, DEPTH]→[batch_size, 32, 32, DEPTH]→[batch_size, 64, 64, 3]。

与卷积层不同之处在于,每一步反卷积操作的输出维度,需要手动规定,具体计算方法参加上方的反向卷积维度计算公式。最终得到的output变量即batch_size张生成的图像。

4.数据预处理。

 1 def load_data(path):
 2     X_train = []
 3     img_list = glob.glob(path + '/*.jpg')
 4     for img in img_list:
 5         _img = cv2.imread(img)
 6         _img = cv2.resize(_img, (pic_height_width, pic_height_width))
 7         X_train.append(_img)
 8     print('训练集图像数目:',len(X_train))
 9     # print(X_train[0],type(X_train[0]),X_train[0].shape)
10     return np.array(X_train, dtype=np.uint8)
11
12 def normalization(input_matirx):
13     input_shape = input_matirx.shape
14     total_dim = 1
15     for i in range(len(input_shape)):
16         total_dim = total_dim*input_shape[i]
17     big_vector = input_matirx.reshape(total_dim,)
18     out_vector = []
19     for i in range(len(big_vector)):
20         out_vector.append(big_vector[i]/256)    # 0~256值归一化
21     out_vector = np.array(out_vector)
22     out_matrix = out_vector.reshape(input_shape)
23     return out_matrix
24
25 def denormalization(input_matirx):
26     input_shape = input_matirx.shape
27     total_dim = 1
28     for i in range(len(input_shape)):
29         total_dim = total_dim*input_shape[i]
30     big_vector = input_matirx.reshape(total_dim,)
31     out_vector = []
32     for i in range(len(big_vector)):
33         out_vector.append(big_vector[i]*256)    # 0~256值还原
34     out_vector = np.array(out_vector)
35     out_matrix = out_vector.reshape(input_shape)
36     return out_matrix

这些函数主要用于加载原始图像,并根据我们WGAN模型的要求,对原始图像进行预处理。load_data()函数用来加载数据文件夹中的所有狗狗图像,并将载入的图像缩放到64*64像素的大小。

normalization()函数和denormalization()函数用来对图像数据进行归一化和反归一化,每个像素在单个通道中的取值在[0~256]之间,我们需要将其归一化到[0,1]范围内,否则在模型训练过程中loss会出现较大波动;在使用生成器得到生成结果之后,每个像素内的数值都在[0,1]之间,需要将其反归一化到[0~256]。这一步也必不可少,最开始的几次试验没有对数据进行归一化,导致loss巨大,且难以收敛。

5.模型训练。

 1 def train():
 2     with tf.variable_scope(tf.get_variable_scope()):
 3         real_data = tf.placeholder(tf.float32, shape=[batch_size,pic_height_width,pic_height_width,3])
 4         with tf.variable_scope(tf.get_variable_scope()):
 5             fake_data = generator('gen',reuse=False)
 6             disc_real = Discriminator('dis_r',real_data,reuse=False)
 7             disc_fake = Discriminator('dis_r',fake_data,reuse=True)
 8         """获取变量列表,d_vars为判别器参数,g_vars为生成器的参数"""
 9         t_vars = tf.trainable_variables()
10         d_vars = [var for var in t_vars if 'd_' in var.name]
11         g_vars = [var for var in t_vars if 'g_' in var.name]
12         '''计算损失'''
13         gen_cost = -tf.reduce_mean(disc_fake)
14         disc_cost = tf.reduce_mean(disc_fake) - tf.reduce_mean(disc_real)
15
16         alpha = tf.random_uniform(
17             shape=[batch_size, 1],minval=0.,maxval=1.)
18         differences = fake_data - real_data
19         interpolates = real_data + (alpha * differences)
20         gradients = tf.gradients(Discriminator('dis_r',interpolates,reuse=True), [interpolates])[0]
21         slopes = tf.sqrt(tf.reduce_sum(tf.square(gradients), reduction_indices=[1]))
22         gradient_penalty = tf.reduce_mean((slopes - 1.) ** 2)
23         disc_cost += LAMBDA * gradient_penalty
24         """定义优化器optimizer"""
25         with tf.variable_scope(tf.get_variable_scope(), reuse=None):
26             gen_train_op = tf.train.RMSPropOptimizer(
27                 learning_rate=1e-4,decay=0.9).minimize(gen_cost,var_list=g_vars)
28             disc_train_op = tf.train.RMSPropOptimizer(
29                 learning_rate=1e-4,decay=0.9).minimize(disc_cost,var_list=d_vars)
30         saver = tf.train.Saver()
31         sess = tf.InteractiveSession()
32         coord = tf.train.Coordinator()
33         threads = tf.train.start_queue_runners(sess=sess, coord=coord)
34         """初始化参数"""
35         init = tf.global_variables_initializer()
36         sess.run(init)
37         '''获得数据'''
38         dog_data = load_data(data_path)
39         dog_data = normalization(dog_data)
40         for epoch in range (1, EPOCH):
41             for iters in range(IDXS):
42                 if(iters%4==3):
43                     img = dog_data[(iters%4)*batch_size:]
44                 else:
45                     img = dog_data[(iters%4)*batch_size:((iters+1)%4)*batch_size]
46                 for x in range(1):           # TODO 在对一批数据展开训练时,训练几次生成器
47                     _, g_loss = sess.run([gen_train_op, gen_cost])
48                 for x in range(0,3):        # TODO 训练一次生成器,训练几次判别器...
49                     _, d_loss = sess.run([disc_train_op, disc_cost], feed_dict={real_data: img})
50                 print("[%4d:%4d/%4d] d_loss: %.8f, g_loss: %.8f"%(epoch, iters, IDXS, d_loss, g_loss))
51
52             with tf.variable_scope(tf.get_variable_scope()):
53                 samples = generator('gen', reuse=True)
54                 samples = tf.reshape(samples, shape=[batch_size,pic_height_width,pic_height_width,3])
55                 samples=sess.run(samples)
56                 samples = denormalization(samples)  # 还原0~256 RGB 通道数值
57                 save_images(samples, [8,8], os.getcwd()+'/img/'+'sample_%d_epoch.png' % (epoch))
58
59             if epoch%10==9:
60                 checkpoint_path = os.path.join(os.getcwd(),
61                                                './models/WGAN/my_wgan-gp.ckpt')
62                 saver.save(sess, checkpoint_path, global_step=epoch)
63                 print('*********    model saved    *********')
64         coord.request_stop()
65         coord.join(threads)
66         sess.close()

这一部分代码中,在34行"初始化参数"之前,还都属于计算图绘制阶段,包括定义占位符,定义损失函数,定义优化器等。需要注意的是获取变量列表这一步,需要对计算图中的所有变量进行筛选,根据命名空间,将所有生成器所包含的参数存入g_vars,将所有判别器所包含的参数存入d_vars。在定义生成器优化器的时候,指定var_list=g_vars;在定义判别器优化器的时候,指定var_list=d_vars

在训练的过程中,两个全局变量控制训练的程度,EPOCH即训练的轮次数目,IDXS则为每轮训练中训练多少个批次。第46行,48行的for循环用来控制对生成器、判别器的训练程度,本例中训练程度为1:3,即训练1次生成器,训练3次判别器,这个比例可以自己设定,WGAN模型其实对这个比例不是特别敏感。

根据上述构建好的WGAN模型,设置EPOCH=200,IDXS=1000,在自己的电能上训练了24个小时,观察每一轮次的图像生成结果,可以看出生成器不断的进化,从一片混沌到初具狗的形状。下方4幅图像分别是训练第1轮,第5轮,第50轮,第200轮的生成器模型的输出效果,每张图像中包含64个小图,可以看出狗的外形逐步显现出来。

使用Wasserstein GAN生成小狗图像-LMLPHP使用Wasserstein GAN生成小狗图像-LMLPHP

使用Wasserstein GAN生成小狗图像-LMLPHP使用Wasserstein GAN生成小狗图像-LMLPHP

 这次实验的代码在github上进行了保存:https://github.com/NosenLiu/Dog-Generator-by-TensorFlow   除了这个WGAN模型的训练代码外,还有对进行模型、加载、再训练的相关内容。有兴趣的朋友可以关注(star☆)一下。

 

.总结感悟

 通过这次实践,对WGAN模型有了一定程度的理解,尤其是对生成器的训练是十分到位的,比较容易出效果。但是由于它的判别器最后一层没有sigmoid函数,单独应用这个判别器对一个图像进行计算,根据计算结果,很难直接的得到这张图片中是狗图的概率。

另外由于没有使用目标识别来对数据集进行预处理,WGAN会认为数据集中照片中的所有内容都是狗,这也就导致了后面生成的部分照片中有较大区域的绿色,应该是生成器将数据集中的草地认作了狗的一部分。

参考

https://blog.csdn.net/LEE18254290736/article/details/97371930

https://zhuanlan.zhihu.com/p/25071913

https://blog.csdn.net/xg123321123/article/details/78034859

08-27 10:52