本文繼上一篇文章繼續研究深度卷積生成對抗網絡(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