DCGAN也是GAN领域内比较经典的内容,涉及到了GAN网络结构上的很多改变,所以结合代码看了看论文。
论文:UNSUPERVISED REPRESENTATION LEARNING WITH DEEP CONVOLUTIONAL GENERATIVE ADVERSARIAL NETWORKS
在上一篇论文Generative Adversarial Networks中,主要学习的是GAN的原理和对抗思想,对于生成器和判别器的网络结构没有仔细研究。因此借这篇论文看一看CNN最开始是如何融入到GAN中的(其实是因为看了天池最近的一个比赛,发现给的示例中判别器用了Inceptionv1等网络,但自己没什么改进的方向,还是对GAN的具体内容了解不多)。
代码来自:https://github.com/carpedm20/DCGAN-tensorflow
这里有两个数据集:MNIST和celebA,分别对应有无condition的情况。
该论文最重要的就是对GAN的网络结构进行了修改:生成器和判别器中都以卷积为主(生成器中要升维,所以是反卷积;判别器中降维,所以是普通卷积),卷积的特性使网络提取到很好的特征。带步长的卷积还可以取代池化实现下采样,池化是一个非参数的过程,但卷积是一个带参数的可学习的过程。因为卷积层的存在,生成器中必须把z向量变成具有图像空间结构的矩阵,这里用reshape实现。
注:论文中说舍弃了全连接层,判别器和生成器都是全卷积网络,但个人觉得linear的操作和全连接层没什么区别啊,不太明白。
除此之外,生成器和判别器中每一层都加入了BN(除了判别器的输入层和生成器的输出层),该方法一定程度上避免了梯度消失。同时,激活函数的选择也能够改善结果,生成器中除了输出层全部用relu激活,判别器中则用leaky relu。接下来对不同的数据集所对应的网络进行分析:
若是输入数据集为MNIST,用函数load_mnist获取并处理图像数据,返回值为像素值介于0-1的图像矩阵(每张图像为一行,共70000行)和经过one-hot编码的标签(也是70000行,每行10个元素分别表示10个类别),load_mnist函数代码如下:
def load_mnist(self):
# 获取数据保存的文件夹目录
data_dir = os.path.join(self.data_dir, self.dataset_name)
# 获取训练集图像
fd = open(os.path.join(data_dir,'train-images-idx3-ubyte'))
loaded = np.fromfile(file=fd,dtype=np.uint8)
# 从第16个数字开始才是图片?loaded中是一维向量
# 60000张图片,28*28*1的尺寸
trX = loaded[16:].reshape((60000,28,28,1)).astype(np.float)
# 获取训练集标签
fd = open(os.path.join(data_dir,'train-labels-idx1-ubyte'))
loaded = np.fromfile(file=fd,dtype=np.uint8)
# 标签从第八位开始
trY = loaded[8:].reshape((60000)).astype(np.float)
# 测试集图片
fd = open(os.path.join(data_dir,'t10k-images-idx3-ubyte'))
loaded = np.fromfile(file=fd,dtype=np.uint8)
teX = loaded[16:].reshape((10000,28,28,1)).astype(np.float)
# 测试集标签
fd = open(os.path.join(data_dir,'t10k-labels-idx1-ubyte'))
loaded = np.fromfile(file=fd,dtype=np.uint8)
teY = loaded[8:].reshape((10000)).astype(np.float)
# 把标签转换为ndarray形式
trY = np.asarray(trY)
teY = np.asarray(teY)
# 把训练集和测试集串联
X = np.concatenate((trX, teX), axis=0)
# 标签转换为int
y = np.concatenate((trY, teY), axis=0).astype(np.int)
# 将图像和标签按同样的随机性打乱,仍是一一对应的
seed = 547
np.random.seed(seed)
np.random.shuffle(X)
np.random.seed(seed)
np.random.shuffle(y)
# y_dim = 10
y_vec = np.zeros((len(y), self.y_dim), dtype=np.float)
# y_vec是标签one-hot编码结果
for i, label in enumerate(y):
y_vec[i,y[i]] = 1.0
# 把图像像素值归一到0-1
return X/255.,y_vec
标签作为condition共同输入生成器中,相当于监督学习,生成器具体结构如代码所示:
s_h, s_w = self.output_height, self.output_width
s_h2, s_h4 = int(s_h/2), int(s_h/4)
s_w2, s_w4 = int(s_w/2), int(s_w/4)
# yb = tf.expand_dims(tf.expand_dims(y, 1),2)
yb = tf.reshape(y, [self.batch_size, 1, 1, self.y_dim])
z = concat([z, y], 1)
# gfc_dim:fc dimension for generator
# 把z扩展到全连接层的长度
h0 = tf.nn.relu(
self.g_bn0(linear(z, self.gfc_dim, 'g_h0_lin')))
h0 = concat([h0, y], 1)
h1 = tf.nn.relu(self.g_bn1(
linear(h0, self.gf_dim*2*s_h4*s_w4, 'g_h1_lin')))
h1 = tf.reshape(h1, [self.batch_size, s_h4, s_w4, self.gf_dim * 2])
h1 = conv_cond_concat(h1, yb)
h2 = tf.nn.relu(self.g_bn2(deconv2d(h1,
[self.batch_size, s_h2, s_w2, self.gf_dim * 2], name='g_h2')))
h2 = conv_cond_concat(h2, yb)
return tf.nn.sigmoid(
deconv2d(h2, [self.batch_size, s_h, s_w, self.c_dim], name='g_h3'))
根据代码画出了下图,其中s_h=s_w=28,c_dim=1因为是灰度图像。根据下图一行行分析:生成器的输入是100维的随机向量z与10维的condition进行concatenate的结果(因此有110列),首先通过矩阵进行线性变换升维至gfc_dim=1024(这个1024就是一般的全连接层的神经元个数),然后和condition进行concatenate变成1034维,再次通过矩阵线性变换升维至(gf_dim*2*s_h4*s_w4),这个维度是根据最后的输出维度s_h和s_w决定的。这时将二维矩阵进行reshape变成符合图像矩阵的四维形式[batch_num, s_h4, s_w4, gf_dim*2],并再次与condition进行concatenate变成(gf_dim*2+10)。接下来通过两次反卷积扩大特征图,达到最后的图像尺寸[batch_num, s_h, s_w, c_dim],并最后利用sigmoid函数激活。
有一点疑问是,为什么condition每次都要进行concatenate?看了一下Conditional GAN的论文,只是在输入层进行了concatenate呀,不过打算实验看一下每一层都concatenate和只在输入concatenate的差距。
还要注意的是,MNIST的图片不经过裁剪(crop),保持本身28*28的大小。
对应的判别器代码为:
yb = tf.reshape(y, [self.batch_size, 1, 1, self.y_dim])
x = conv_cond_concat(image, yb)
h0 = lrelu(conv2d(x, self.c_dim + self.y_dim, name='d_h0_conv'))
h0 = conv_cond_concat(h0, yb)
h1 = lrelu(self.d_bn1(conv2d(h0, self.df_dim + self.y_dim, name='d_h1_conv')))
h1 = tf.reshape(h1, [self.batch_size, -1])
h1 = concat([h1, y], 1)
h2 = lrelu(self.d_bn2(linear(h1, self.dfc_dim, 'd_h2_lin')))
h2 = concat([h2, y], 1)
h3 = linear(h2, 1, 'd_h3_lin')
return tf.nn.sigmoid(h3), h3
判别器的输出有两个,一个是未经过sigmoid分类的结果h3,一个是经过了sigmoid的结果。根据代码画出的网络结构图如下: