GAN系列:代码阅读——Generative Adversarial Networks & 李宏毅老师GAN课程P1+P4

看了一上午简直要头疼死。GAN之前没接触过,学习的时候产生了很多乱七八糟的联想。从上篇文章开始,很多内容都是自己的理解,估计有很多错误,以后学习中发现了可能会回来修改的。

找的是机器之心i的代码:https://gitahub.com/jiqizhixin/ML-Tutorial-Experiment/blob/master/Experiments/Keras_GAN.ipynb,用Keras实现的,不然更看不懂了。笔记本上没安Tensorflow,打算晚上回去用实验室电脑跑一跑,如果顺利的话。

1. 导入部分:

from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Reshape
from keras.layers.core import Activation
from keras.layers.normalization import BatchNormalization
from keras.layers.convolutional import UpSampling2D
from keras.layers.convolutional import Conv2D, MaxPooling2D
from keras.layers.core import Flatten
from keras.optimizers import SGD
from keras.datasets import mnist
import numpy as np
from PIL import Image
import argparse
import math

关于网络模型:

生成器和判别器都是很简单的网络模型,因此只需要从导入序贯(Sequential)模型,网络层线性堆叠。

关于网络结构:

从keras.layers导入Dense(就是全连接层,是dense connect),导入Reshape(不过看Keras文档Reshape是在keras.layers.core目录下的);从keras.layers.core(常用层)导入Activation(激活层),Flatten(平铺);从keras.layers.normalization导入BatchNormalization(BN层);从keras.layers.convolutional导入UpSampling2D, Conv2D, MaxPooling2D(都是二维的)。

关于网络训练:

从keras.optimizers中导入SGD,算法中使用梯度下降法求解;从keras.datasets中导入mnist(手写数字的数据集,是灰度图像)。

其他:

PIL:Python Imaging Library,python中的图像处理库,看介绍就是有很强的图像处理能力,隐约中记得安装过;

argparse:还不太懂,貌似是能在命令行中提供更加用户友好的参数选择接口。

2. 生成器网络结构:

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

代码中的标注都是原作者的。生成器网络的输入是100维的向量,也就是生成器是要从100维的向量(相对于图像的向量应该算是低维向量了,它的分布肯定比图像向量所服从的分布简单多了)生成图像的向量(生成器网络实现的就是从低维向量到高维向量的映射)。至于它输出的图像向量的维度,通过网络具体结构计算,该生成器网络结构如下图所示:

MNIST数据集的图片大小就是28*28*1,说明以上output的推导没有问题,生成器的输出图片大小应该和MNIST中真实的图片大小相同。

因为是从低维到高维的映射,如何变化到高维呢:全连接层先一定程度上扩展一下维度,然后通过reshape转换为具有空间结构的矩阵,此时根据图片的特性(相邻像素的灰度值几乎相同)就可以2倍上采样,上采样一共用了两次,中间隔了一个卷积(连着用估计偏差太大)。同时虽然用了卷积层,但因为要保持图片大小,进行了padding,不然上采样白用了,越卷积越小。

Questions:

1)为什么激活函数选择tanh;

2)卷积层的作用:还是提取特征吗,理论上怎么理解啊,感觉和一般的图像分类的网络比是反过来了,有点晕。

3. 判别器网络结构:

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

该代码中好像都是‘channels_last’ 模式,把通道信息放在最后一个维度上了。判别器网络结构如下图所示:

判别器网络就相当于一个常见的分类网络,所以就是卷积激活池化这样常见的组合,一共有两次池化,不知道是不是和生成器中的两次上采样对应。Flatten层是卷积层和全连接层间的过渡,把具有空间结构的矩阵变成一维向量。第一个全连接层降维到1024(为什么全连接层都选择降维到1024呢),第二个全连接层直接降维到1,再经过激活层就是最后判别器的打分了。这里激活层函数和之前所有激活函数都不同,因为输出是介于0到1的。

4. 对抗网络结构:

def generator_containing_discriminator(g, d):
    #将前面定义的生成器架构和判别器架构组拼接成一个大的神经网络,用于判别生成的图片
    model = Sequential()
    #先添加生成器架构,再令d不可训练,即固定d
    #因此在给定d的情况下训练生成器,即通过将生成的结果投入到判别器进行辨别而优化生成器
    model.add(g)
    d.trainable = False
    model.add(d)
    return model

对抗网络是由生成器网络和判别器网络串联而成的,生成器网络在前,对抗网络输入是100维的低维向量,输出是介于0到1的标量。这里固定判别器的操作(d.trainable = False)有一点点不懂。

5. 生成图片拼接:

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

这部分有点不懂,从后面的代码看generated_images的第一个维度应该是图片在一个batch里的编号,所以这是把一个batch里的图像拼一起成为一张大图?有什么意义吗(哦,是不是为了最后的显示啊,我看网上大家跑出来的结果展示都是一堆手写数字图片挨在一起的一张大图)?这部分就是先计算了大图的尺寸(height*shape[0], width*shape[1]),然后根据这个尺寸生成一个全为0的矩阵,再把生成的小图片们一个个覆盖上去取代0。

6. 对抗网络训练:

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()
    #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)

这部分就很长了,一块一块看:

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:])

首先是加载数据,本来图像的像素值应该是0到255吧,把它们转换成了-1到1之间,因为后面生成器的输入(100维的随机噪声)也是介于-1到1的(怎么感觉这个解释又不对了)。然后又给数据增加了一个维度,出现在numpy array中的None作用就是增加一个维度,下面注释的这行应该是同样的作用,不过没看懂是怎么实现这一功能的(维度看的我蒙了,X_train.shape应该是个元组吧,应该是三个元素(height,width,channel) ,那和后面的1又组成一个有两个元素的元组(第一个元素就是包含了三个元素的元组)?然后后面X_train.shape[1:]应该是有两个元素的元组。两个元组相加就是元素串联,应该是生成四个元素的元组,可是第一个元素是个元组啊,这表示啥啊)。

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)

接下来就是一些模型的设置了:编译这里loss直接用的二元分类交叉熵,是把生成器和判别器都看作了二分类网络吧(那个目标函数V(G,D)确实类似于分类问题中的目标函数,感觉判别器没问题,但是生成器训练时不是函数V的前半部分不是没用了吗,有影响吗?)。

之前定义对抗网络模型的时候把d.trainable设置为False了,所以这里要改回来(为什么前面要那么设置呢,难道是因为最后的目的是用生成器产生图片,那时候判别器就不用了,直接从网络中间部分取出生成器产生的图片就行?但是那不是predict就行吗,也不涉及训练啊)。

然后就可以开始训练了,先训练判别器:

print("Number of batches", int(X_train.shape[0]/BATCH_SIZE))

既然有BATCH,就是用的批量梯度下降(每次梯度下降都遍历所有训练数据计算量太大而且太慢了,所以把训练集分为多个BATCH,每次梯度下降时都只用其中一个BATCH的数据,所有BATCH用完后就是一个epoch结束了)。接下来就是在一个epoch中进行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")

生成图片时把小图片拼接了,并且还原了像素值大小,保存在了./GAN/目录下,命名为"+str(epoch)+"_"+str(index)+".png。fromarray的作用没搞懂。跳出这个迭代100次的条件判断,回到外面的每一次迭代:

X = np.concatenate((image_batch, generated_images))
y = [1] * BATCH_SIZE + [0] * BATCH_SIZE
d_loss = d.train_on_batch(X, y)

把每一次生成的图片和数据集中的真实图片连起来,再加上标签,所有生成的图片都给标签0,真实图片给标签1。有了数据和标签就可以训练了,train_on_batch返回值是标量loss。判别器训练后就要被固定了,然后重新采样噪声向量来训练生成器(看来这个代码里判别器和生成器训练次数相同):

d.trainable = False
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次保存一下权重,保存权重的次数(index % 10 == 9)刚好是用来产生上面输出图片(index % 10 == 0)的前一次,就是产生那些图片的权重:

if index % 100 == 9:
    g.save_weights('generator', True)
    d.save_weights('discriminator', True)

7. 生成图片:

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")

生成器加载之前保存的参数,generate函数的参数nice=False直接进入else部分程序:生成随机噪声,输入到生成器中进行predict,组成大图片并恢复像素值,最后保存在./GAN目录下,命名为generated_image.png。

猜你喜欢

转载自blog.csdn.net/lynlindasy/article/details/87775541
今日推荐