生成式对抗网络(GAN)自2014年提出以来已经成为最受欢迎的生成模型。本文借鉴机器之心对 2014 GAN 论文的解读,在本机运行该Keras项目。
传送门: 机器之心GitHub项目:GAN完整理论推导与实现,Perfect!
接下来主要讲一下如何实现的:
1. 定义一个生成模型:
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
2. 定义一个判别模型:
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
3. 拼接:
def generator_containing_discriminator(g, d):
#将前面定义的生成器架构和判别器架构组拼接成一个大的神经网络,用于判别生成的图片
model = Sequential()
#先添加生成器架构,再令d不可训练,即固定d
#因此在给定d的情况下训练生成器,即通过将生成的结果投入到判别器进行辨别而优化生成器
model.add(g)
d.trainable = False
model.add(d)
return model
4. 生成拼接的图片(即将一个batch所有生成图片放到一个图片中):
def combine_images(generated_images):
#生成图片拼接
num = generated_images.shape[0]
width = int(math.sqrt(num))
height = int(math.ceil(float(num)/width))
shape = generated_images.shape[1:3]
image = np.zeros((height*shape[0], width*shape[1]),
dtype=generated_images.dtype)
for index, img in enumerate(generated_images):
i = int(index/width)
j = index % width
image[i*shape[0]:(i+1)*shape[0], j*shape[1]:(j+1)*shape[1]] = \
img[:, :, 0]
return image
5. 训练:
def train(BATCH_SIZE):
# 国内好像不能直接导入数据集,试了几次都不行,后来将数据集下载到本地'~/.keras/datasets/',也就是当前目录(我的是用户文件夹下)下的.keras文件夹中。
#下载的地址为:https://s3.amazonaws.com/img-datasets/mnist.npz
(X_train, y_train), (X_test, y_test) = mnist.load_data()
#image_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] # None将3维的X_train扩展为4维
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)
#print(np.shape(generated_images)) # (BATCH_SIZE,28,28,1) # 表示用BATCH_SIZE个100维向量生成BATCH_SIZE个图像的过程
#每经过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))
# print(np.shape(X)) # # (2*BATCH_SIZE,28,28,1)
#生成图片真假标签,即一个包含两倍批量大小的列表;前一个批量大小都是1,代表真实图片,后一个批量大小都是0,代表伪造图片
y = [1] * BATCH_SIZE + [0] * BATCH_SIZE
#判别器的损失;在一个batch的数据上进行一次参数更新
d_loss = d.train_on_batch(X, y) # (2*BATCH_SIZE,28,28,1) -> (2*BATCH_SIZE,1)
print("batch %d d_loss : %f" % (index, d_loss)) # 理论上,d的loss越来越大,因为生成图片和真实图片越来越像
#随机生成的噪声服从均匀分布
noise = np.random.uniform(-1, 1, (BATCH_SIZE, 100))
#固定判别器
d.trainable = False
#计算生成器损失;在一个batch的数据上进行一次参数更新
#生成器的目标是愚弄辨别器蒙混过关,需要达到的目标是对于生成的图片,输出为1(正好和鉴别器相反).
g_loss = d_on_g.train_on_batch(noise, [1] * BATCH_SIZE) # (BATCH_SIZE,100) -> (BATCH_SIZE,28,28,1) -> (BATCH_SIZE,1)
#令判别器可训练
d.trainable = True
print("batch %d g_loss : %f" % (index, g_loss)) # 理论上,g的loss越来越小,因为生成图像越接近真实,生成图像的label接近1
#每100次迭代保存一次生成器和判别器的权重
if index % 100 == 9:
g.save_weights('generator', True)
d.save_weights('discriminator', True)
注意:运行加载MNIST
数据集,调用mnist.load_data()
函数需要翻墙。如果不翻墙,可在其他地方找到要加载的mnist.npz
文件,把它放到Keras
安装目录下的~/.keras/datasets/
,也可以。不要试图用Tensorflow
加载MNIST
数据集的那个模块,因为那个模块对MNIST
采取了one-hot
的编码格式,得到的值都是归一化的数值。而Keras
的函数mnist.load_data()
加载的MNIST
数据集是原始的像素值。
6. 生成:
def generate(BATCH_SIZE, nice= False ):
#训练完模型后,可以运行该函数生成图片
g = generator_model()
g.compile(loss='binary_crossentropy', optimizer="SGD")
g.load_weights('generator')
if nice:
d = discriminator_model()
d.compile(loss='binary_crossentropy', optimizer="SGD")
d.load_weights('discriminator')
noise = np.random.uniform(-1, 1, (BATCH_SIZE*20, 100))
generated_images = g.predict(noise, verbose=1)
d_pret = d.predict(generated_images, verbose=1)
index = np.arange(0, BATCH_SIZE*20)
index.resize((BATCH_SIZE*20, 1))
pre_with_index = list(np.append(d_pret, index, axis=1))
pre_with_index.sort(key=lambda x: x[0], reverse=True)
nice_images = np.zeros((BATCH_SIZE,) + generated_images.shape[1:3], dtype=np.float32)
nice_images = nice_images[:, :, :, None]
for i in range(BATCH_SIZE):
idx = int(pre_with_index[i][1])
nice_images[i, :, :, 0] = generated_images[idx, :, :, 0]
image = combine_images(nice_images)
else:
noise = np.random.uniform(-1, 1, (BATCH_SIZE, 100))
generated_images = g.predict(noise, verbose=0)
image = combine_images(generated_images)
image = image*127.5+127.5
Image.fromarray(image.astype(np.uint8)).save(
"./GAN/generated_image.png")
以上代码在支持 Tensorflow、Keras
的ipython notebook
中运行。
先训练模型(迭代30次):
train(100) # 100为batch大小,可以随意指定。
迭代的效果如下:
9_500.png
19_500.png
29_500.png
再生成模型:
generate(132) # 132为batch大小,可以随意指定。该值大小也决定了生成的图片中含有多少个数字。
generate(32) # 32为batch大小,可以随意指定。该值大小也决定了生成的图片中含有多少个数字。
生成的效果如下:
generated_image132.png
generated_image32.png
训练过程分析:
将 MNIST 数据集(60000)分块训练(如 BITCH_SIZE = 200),则一个 Epoch 就会循环 300 次。每一次循环,生成器 g 先根据 BITCH_SIZE 个 100 维的随机噪声生成 BITCH_SIZE 张和真实图像同样大小的图像,然后将这 BITCH_SIZE 张图像和真实的 BITCH_SIZE 张图像拼接起来,给它们打上标签,计算判别器 d 的损失值。接下来,同样生成 BITCH_SIZE 个 100 维的随机噪声,打上和真实图像一样的标签(因为这是 g 所期望的),放入 d_on_g 中。在 d_on_g 中,先由 g 生成 BITCH_SIZE 张图像图像,再固定 d,再由 d 计算这 BITCH_SIZE 张图像的损失值,即生成器 g 的损失值。循环 300 次后,这才是 1 个 Epoch。如果想要生成更真实的图像, 要有多个 Epoch(代码中是30次)。
损失函数分析:
正常情况下,生成模型的损失和判别模型的损失会在一定范围内交替上升与下降。因为判别模型损失小,意味着更容易区分真实图像和假图像;相反,此时的生成模型与它的目标(真实图像)相差很大,损失函数会增大。反之亦然。最终理想的生成器的损失函数应该和判别器的损失函数一样大,这时判别器无法区分真实图像和假图像,生成器也达到了它所能到达的最小损失值。
更多实现细节可以参考我的github
:
网友评论