使用tensorflow實現深度卷積生成對抗網絡,並使用DCGAN 生成手寫數字(超詳細)

本文繼上一篇文章繼續研究深度卷積生成對抗網絡(DCGAN) ,本文主要講解實現細節,使用 DCGAN 實現手寫數字生成任務,通過這一個例子,讀者可以進一步鞏固上一篇博客所講內容,同時對生成對抗網絡會有更加詳細的認識。
完整項目代碼在本人github上面已經開源,具體用法可以參見本人github

完整參考代碼可查看 這裏

效果展示

使用如下超參數訓練 1000次:

batch_size=128     訓練時候的批次大小,默認是128
learning_rate=0.002     默認是0.002
img_sizet=32    生成圖片的大小(和訓練圖片的大小保持一致)
z_dim=100       輸入生成器的隨機向量的大小,默認是100
g_channels=[128,64,32,1]     生成器的通道數目變化列表,用於構建生成器結構
d_channels=[32,64,128,256]      判別器的通道樹木變化列表,用來構建判別器
init_conv_size=4        隨機向量z經過全連接之後進行reshape 生成三維矩陣的初始邊長,默認是 4 
beta1=0.5       AdamOptimizer 指數衰減率估計,默認是0.5

中間結果展示:

訓練200次:
生成圖片:
在這裏插入圖片描述
真實圖片:
在這裏插入圖片描述
訓練500次:
生成圖片:
在這裏插入圖片描述
真實圖片:
在這裏插入圖片描述
訓練1000次:
生成圖片:
在這裏插入圖片描述
真實圖片:
在這裏插入圖片描述
訓練3500次:
生成圖片:
在這裏插入圖片描述
真實圖片:
在這裏插入圖片描述
訓練5000次:
生成圖片:
在這裏插入圖片描述
真實圖片:
在這裏插入圖片描述
可以看到的是訓練 5000 次之後生成的圖片和真實的圖片已經非常像。

加載訓練用的數據集

因爲要生成手寫數字,則首先需要一個手寫數字的數據集來訓練GAN,這裏使用常見的快被用爛了的MNIST數據集,下面是加載數據集的工具文件:

dataset_loader.py

"""
create by qianqianjun
2019.12.19
"""
import os
import struct
import numpy as np

def load_mnist(path,train=True):
    """
    加載mnist 數據集的函數
    :param path:  數據集的位置
    :param train:  是否加載訓練數據,是返回train 用的image和lable,否則返回test用的images和label
    :return: 返回訓練或者測試用的images 和 labels 
    """
    def get_urls(files,type='train'):
        """
        獲取訓練數據或者測試數據的二進制文件地址
        :param files:  讀取的數據集目錄文件列表
        :param type:  訓練或者測試標識
        :return:  返回二進制文件的完整地址
        """
        images_path = None
        labels_path = None
        for file in files:
            if file.find(type) != -1:
                if file.find("images") != -1:
                    images_path = os.path.join(path, file)
                else:
                    labels_path = os.path.join(path, file)

        if images_path == None or labels_path == None:
            raise Exception("請檢查數據集!")
        return images_path,labels_path
    def load_data_and_label(data_path,label_path):
        """
        加載訓練或者測試數據的lable 和 data
        :param data_path:  訓練或者測試圖片數據的二進制文件地址
        :param label_path:  訓練或者測試label數據的二進制文件地址
        :return:  返回讀取的圖片 和 label 的 ndarray 數組
        """
        images = None
        labels = None
        with open(label_path,'rb') as label_file:
            struct.unpack('>II', label_file.read(8))
            labels=np.fromfile(label_file,dtype=np.uint8)
        with open(data_path,'rb') as img_file:
            struct.unpack('>IIII', img_file.read(16))
            images=np.fromfile(img_file,dtype=np.uint8).reshape(len(labels),784)
        return images,labels
    
    # 查看數據集文件夾中有多少文件。
    files = os.listdir(path)
    if train:
        data_path,label_path=get_urls(files,type='train')
        return load_data_and_label(data_path,label_path)
    else:
        data_path,label_path=get_urls(files,type='t10k')
        return load_data_and_label(data_path, label_path)

# 讀取訓練用的圖片數據和訓練用的labels 標籤
train_images,train_labels=load_mnist("./MNIST",train=True)
# 讀取測試用的圖片數據和測試用的labels 標籤
test_images,test_labels=load_mnist("./MNIST",train=False)

數據集provider工具

這一個文件主要用來在訓練的時候分批次的取數據,對數據集進行打亂,洗牌工作,防止模型學習到數據之間的順序關聯。
data_provider.py

"""
write by qianqianjun
2019.12.20
"""
import numpy as np
from PIL import Image
class MnistData(object):
    def __init__(self,images_data,z_dim,img_size):
        """
        建立一個data provider
        :param images_data:  傳進來的圖像數據的集合
        :param z_dim:  生成器輸入的隨機向量的長度
        :param img_size:  傳進來的圖像的大小
        """
        self._data=images_data
        self.images_num=len(self._data)
        # 生成隨機向量的矩陣,爲每一張圖像都生成一個隨機向量。
        self._z_data=np.random.standard_normal((self.images_num,z_dim))
        self._offset=0
        self.init_mnist(img_size)
        self.random_shuffer()

    def random_shuffer(self):
        """
        數據集進行打亂操作,防止模型學習到訓練數據之間的順序性質
        :return:
        """
        p=np.random.permutation(self.images_num)
        self._z_data=self._z_data[p]
        self._data=self._data[p]

    def init_mnist(self,img_size):
        """
        調整數據集到指定的shape
        :param img_size: 指定大小的邊長
        :return:
        """
        # 將訓練數據進行resize,使其成爲圖片
        data=np.reshape(self._data,(self.images_num,28,28))
        new_data=[]
        for i in range(self.images_num):
            img=data[i]
            # 使用PIL 進行圖像縮放變換
            img=Image.fromarray(img)
            img=img.resize((img_size,img_size))
            img=np.asarray(img)
            # 將圖片轉換爲有通道的形式方便訓練(3維矩陣,只有一個通道)
            img=img.reshape((img_size,img_size,1))
            new_data.append(img)
        # 將列表轉換爲 ndarray
        new_data=np.asarray(new_data,dtype=np.float32)
        # 對圖像數據進行歸一化,方便訓練
        new_data=new_data / 127.5 -1
        # 更新數據
        self._data=new_data
    def next_batch(self,batch_size):
        """
        用來分批次的取數據
        :param batch_size:  每一批取數據的個數
        :return:  返回一批數據和一批隨機向量
        """
        if batch_size> self.images_num:
            raise Exception("batch size is more than train images amount!")
        end_offset=self._offset+batch_size
        if end_offset >self.images_num:
            self.random_shuffer()
            self._offset=0
            end_offset=self._offset+batch_size

        # 取出一批數據和一批隨機向量。
        batch_data=self._data[self._offset:end_offset]
        batch_z=self._z_data[self._offset:end_offset]
        self._offset=end_offset
        return batch_data,batch_z

定義生成器結構

generator.py

"""
write by qianqianjun
2019.12.19
生成器模型實現
"""
import tensorflow as tf
def conv2d_transpose(inputs,out_channel,name,training,with_bn_relu=True):
    """
    反捲積的封裝
    :param inputs:
    :param output_channel: 輸出通道數目
    :param name: 名字
    :param training: bool類型 ,指示是否在訓練
    :param with_bn_relu: 是否需要使用 batch_normalization
    :return: 反捲積之後的矩陣
    """
    with tf.variable_scope(name):
        conv2d_trans = tf.layers.conv2d_transpose(
            inputs, out_channel, [5, 5],
            strides=(2, 2),
            padding='SAME'
        )
        if with_bn_relu:
            bn = tf.layers.batch_normalization(conv2d_trans, training=training)
            return tf.nn.relu(bn)
        else:
            return conv2d_trans

class Generator(object):
    def __init__(self,channels,init_conv_size):
        """
        創建生成器模型
        :param channels: 生成器反捲積過程中使用的通道數 數組
        :param init_conv_size:  使用的卷積核大小
        """
        self._channels = channels
        self._init_conv_size = init_conv_size
        self._reuse = False
    def __call__(self, inputs,training):
        """
        一個魔法函數,用來將對象當函數使用
        :param inputs: 輸入的隨機向量矩陣,shape 爲 【batch_size ,z_dim]
        :param training:  是否是訓練過程
        :return: 返回生成的圖像
        """
        inputs=tf.convert_to_tensor(inputs)
        with tf.variable_scope('generator',reuse=self._reuse):
            """
            下面代碼實現的轉換是: random vector-> fc全連接層-> 
            self.channels[0] * self._init_conv_size **2 ->
            reshpe -> [init_conv_size,init_conv_size,self.channels[0] ]
            """
            with tf.variable_scope("input_conv"):
                fc=tf.layers.dense(
                    inputs,
                    self._channels[0] * (self._init_conv_size **2 )
                )
                conv0=tf.reshape(fc,[-1,self._init_conv_size,
                                     self._init_conv_size,self._channels[0]])

                bn0=tf.layers.batch_normalization(conv0,training=training)
                relu0=tf.nn.relu(bn0)

            # 經過全連接和BN歸一化和 relu 激活,可以看做是某一個卷積層的輸出
            # 下面就可以進行反捲積操作了。
            deconv_inputs=relu0
            # 構建 decoder 網絡層
            for i in range(1,len(self._channels)):
                with_bn_relu=(i!=len(self._channels)-1)
                deconv_inputs=conv2d_transpose(
                    deconv_inputs,
                    self._channels[i],
                    "deconv-%d" % i,
                    training,
                    with_bn_relu=with_bn_relu)
            img_inputs=deconv_inputs
            with tf.variable_scope('generate_imgs'):
                imgs=tf.tanh(img_inputs,name='imgs')

        self.reuse=True
        self.variables=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
                                         scope='generator')
        return imgs

判別器實現

discriminator.py

"""
write by qianqianjun
2019.12.20
判別器簡單實現
"""
import tensorflow as tf
def conv2d(inputs,output_channel,name,training):
    """
    卷積操作的封裝
    :param inputs: 輸入的圖像或者feature map
    :param output_channel:  輸出feature map 的channel 數目
    :param name:  varibale_scope 名稱
    :param training:  是否是訓練過程。
    :return:  返回經過卷積層之後的結果
    """
    def leaky_relu(x,leak=0.2,name=''):
        return tf.maximum(x,x*leak,name=name)

    with tf.variable_scope(name):
        conv2d_output=tf.layers.conv2d(
            inputs,output_channel,
            [5,5],strides=(2,2),
            padding='SAME'
        )
        bn=tf.layers.batch_normalization(conv2d_output,training=training)
        return leaky_relu(bn,name='outputs')

class Discriminator(object):
    def __init__(self,channels):
        """
        創建判別器模型結構
        :param channels:  輸出通道數目
        """
        self._channels=channels
        self._reuse=False
    def __call__(self,inputs,training):
        """
        使用判別器輸出判別的結果,
        :param inputs:  輸入的batch_images data
        :param training:  是否在訓練。
        :return:
        """
        inputs=tf.convert_to_tensor(inputs,dtype=tf.float32)
        conv_inputs=inputs
        with tf.variable_scope('discriminator',reuse=self._reuse):
            # 根據卷積通道數組來建立卷積神經網絡結構:
            for i in range(len(self._channels)):
                conv_inputs=conv2d(conv_inputs,self._channels[i],
                                   'conv-%d'%i,
                                   training=training)
            fc_inputs=conv_inputs
            # 將卷積神經網絡輸出的 feature map 展平並進行全連接。
            with tf.variable_scope('fc'):
                flatten=tf.layers.flatten(fc_inputs)
                # 全連接輸出大小爲 2
                # 其實可以理解爲一個分類的問題,真圖片還是假圖片,一共兩類。
                logits=tf.layers.dense(flatten,2,name='logits')
        self._reuse=True
        self.variables=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
                                         scope='discriminator')
        return logits

定義DCGAN網絡架構

DCGAN.py

"""
write by qianqianjun 
2019.12.20
DCGAN 網絡架構實現
"""
from generator import Generator
from discriminater import Discriminator
import tensorflow as tf
class DCGAN(object):
    def __init__(self,hps):
        """
        建立一個DCGAN的網絡架構
        :param hps:  網絡的所有超參數的集合
        """
        g_channels=hps.g_channels
        d_channels=hps.d_channels
        self._batch_size=hps.batch_size
        self._init_conv_size=hps.init_conv_size
        self._z_dim=hps.z_dim
        self._img_size=hps.img_size
        self._generator=Generator(g_channels,self._init_conv_size)
        self._discriminator=Discriminator(d_channels)

    def build(self):
        """
        構建整個計算圖
        :return:
        """
        # 創建隨機向量和圖片的佔位符
        self._z_placeholder=tf.placeholder(tf.float32,
                                           (self._batch_size,self._z_dim))
        self._img_placeholder=tf.placeholder(tf.float32,
                                             (self._batch_size,
                                              self._img_size,
                                              self._img_size,1))
        # 將隨機向量輸入生成器生成圖片
        generated_imgs=self._generator(self._z_placeholder,training=True)

        # 將來生成的圖片經過判別器來得到 生成圖像的logits
        fake_img_logits=self._discriminator(
            generated_imgs,training=True
        )
        # 將真實的圖片經過判別器得到真實圖像的 logits
        real_img_logits=self._discriminator(
            self._img_placeholder,training=True
        )

        """
        定義損失函數
        包括生成器的損失函數和判別器的損失函數。
        生成器的目的是使得生成圖像經過判別器之後儘量被判斷爲真的
        判別器的目的是使得生成器生成的圖像被判斷爲假的,同時真實圖像經過判別器要被判斷爲真的
        """

        ## 生層器的損失函數,只需要使得假的圖片被判斷爲真即可
        fake_is_real_loss=tf.reduce_mean(
            tf.nn.sparse_softmax_cross_entropy_with_logits(
                labels=tf.ones([self._batch_size],dtype=tf.int64),
                logits=fake_img_logits
            )
        )

        ## 判別器的損失函數,只需要使得生成的圖像被判斷爲假的,真實的圖像被判斷爲真的即可
        # 真的被判斷爲真的:
        real_is_real_loss=tf.reduce_mean(
            tf.nn.sparse_softmax_cross_entropy_with_logits(
                labels=tf.ones([self._batch_size],dtype=tf.int64),
                logits=real_img_logits
            )
        )
        # 假的被判斷爲假的:
        fake_is_fake_loss=tf.reduce_mean(
            tf.nn.sparse_softmax_cross_entropy_with_logits(
                labels=tf.zeros([self._batch_size],dtype=tf.int64),
                logits=fake_img_logits
            )
        )

        # 將損失函數集中管理:
        tf.add_to_collection('g_losses',fake_is_real_loss)
        tf.add_to_collection('d_losses',real_is_real_loss)
        tf.add_to_collection('d_losses',fake_is_fake_loss)

        loss={
            'g':tf.add_n(tf.get_collection('g_losses'),name='total_g_loss'),
            'd':tf.add_n(tf.get_collection('d_losses'),name='total_d_loss')
        }
        return (self._z_placeholder,self._img_placeholder,generated_imgs,loss)
    def build_train_op(self,losses,learning_rate,beta1):
        """
        定義訓練過程
        :param losses:  損失函數集合
        :param learning_rate:  學習率
        :param beta1:  指數衰減率估計
        :return:
        """
        g_opt=tf.train.AdamOptimizer(learning_rate=learning_rate,beta1=beta1)
        d_opt=tf.train.AdamOptimizer(learning_rate=learning_rate,beta1=beta1)

        g_opt_op=g_opt.minimize(
            losses['g'],
            var_list=self._generator.variables
        )

        d_opt_op=d_opt.minimize(
            losses['d'],
            var_list=self._discriminator.variables
        )

        with tf.control_dependencies([g_opt_op,d_opt_op]):
            return tf.no_op(name='train')

定義超參數集合

train_argparse.py

"""
write by qianqianjun
2019.12.20
命令行參數解釋程序
如果不清楚可以參考博客:
https://blog.csdn.net/qq_38863413/article/details/103305449
"""
import argparse
parser=argparse.ArgumentParser()
parser.description="指定DCGAN網絡在訓練時候的超參數,使用help命令獲取詳細的幫助"
parser.add_argument("--batch_size",type=int,default=128,help="訓練時候的批次大小,默認是128")
parser.add_argument("--learning_rate",type=float,default=0.002,help="訓練時候的學習率,默認是0.002")
parser.add_argument("--img_size",type=int,default=32,help="生成圖片的大小(和訓練圖片的大小保持一致)")
parser.add_argument("--z_dim",type=int,default=100,help="輸入生成器的隨機向量的大小,默認是100")
parser.add_argument("--g_channels",type=list,default=[128,64,32,1],help="生成器的通道數目變化列表,用於構建生成器結構")
parser.add_argument("--d_channels",type=list,default=[32,64,128,256],help="判別器的通道樹木變化列表,用來構建判別器")
parser.add_argument("--init_conv_size",type=int,default=4,help="隨機向量z經過全連接之後進行reshape 生成三維矩陣的初始邊長,默認是 4 ")
parser.add_argument("--beta1",type=float,default=0.5,help="AdamOptimizer 指數衰減率估計,默認是0.5")

hps=parser.parse_args()

編寫程序入門文件

mian.py

import os
import tensorflow as tf
from train_argparse import hps
from dataset_loader import train_images
from data_provider import MnistData
from DCGAN import DCGAN
from utils import combine_imgs

output_dir='./out'
if not os.path.exists(output_dir):
    os.mkdir(output_dir)
dcgan=DCGAN(hps)
z_placeholder,img_placeholder,generated_imgs,losses=dcgan.build()
train_op=dcgan.build_train_op(losses,hps.learning_rate,hps.beta1)
init_op=tf.global_variables_initializer()
train_steps=200
mnist_data=MnistData(train_images,hps.z_dim,hps.img_size)
with tf.Session() as sess:
    sess.run(init_op)
    for step in range(train_steps):
        batch_imgs,batch_z=mnist_data.next_batch(hps.batch_size)
        fetches=[train_op,losses['g'],losses['d']]
        should_sample=(step+1) %100 ==0
        if should_sample:
            fetches+= [generated_imgs]
        output_values=sess.run(
            fetches,feed_dict={
                z_placeholder:batch_z,
                img_placeholder:batch_imgs,
            }
        )
        _,g_loss_val,d_loss_val=output_values[0:3]
        if (step+1) %200==0:
            print('step: %4d , g_loss: %4.3f , d_loss: %4.3f' % (step, g_loss_val, d_loss_val))
        if should_sample:
            gen_imgs_val=output_values[3]
            gen_img_path=os.path.join(output_dir,'%05d-gen.jpg' % (step+1))
            gt_img_path=os.path.join(output_dir,'%05d-gt.jpg' % (step+1))
            gen_img=combine_imgs(gen_imgs_val,hps.img_size)
            gt_img=combine_imgs(batch_imgs,hps.img_size)
            gen_img.save(gen_img_path)
            gt_img.save(gt_img_path)

其它工具類

utils.py

"""
write by qianqianjun
2019,12,20

工具文件
這裏使用了 numpy 的一些維度變換,如果不清楚可以參考博客:
https://blog.csdn.net/qq_38863413/article/details/103526645
"""
import numpy as np
from PIL import Image
def combine_imgs(batch_images,img_size,rows=8,cols=16):
    """
    用於在訓練過程中展示一批數據(將一批圖像拼接成一張大圖)
    :param batch_images:  批次圖像數據
    :param img_size:  圖像大小
    :param rows:  一共有多行。
    :param cols:  一行放置多少圖片
    :return:  返回拼接之後的大圖
    """
    #batch_img: [batch_size,img_size,img_size,1]
    result_big_img=[]
    for i in range(rows):
        row_imgs=[]
        for j in range(cols):
            img=batch_images[cols*i+j]
            img=img.reshape((img_size,img_size))
            # 反歸一化
            img=(img+1) * 127.5
            row_imgs.append(img)
        row_imgs=np.hstack(row_imgs)
        result_big_img.append(row_imgs)
    result_big_img=np.vstack(result_big_img)
    result_big_img=np.asarray(result_big_img,np.uint8)
    result_big_img=Image.fromarray(result_big_img)
    return result_big_img
發佈了57 篇原創文章 · 獲贊 29 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章