如何使用VAE模型進行手寫數字圖片的創建
其實主要依賴於兩個發生器,一個是編碼,一個是解碼
實現步驟如下:
- 將數字手寫的圖片庫傳入到VAE中
- 經過編碼器(卷積神經網絡)得到一個語義值(包括手寫數字識別的特徵,這裏我們設置4個維度,分別代表:粗細,彎曲程度等)
- 將獲得的四個值進行平均數和標準差的求解,(目的是爲了符合正態分佈,爲什麼使用正態分佈做隨機數,因爲正態分佈形成的隨機數比較精準)
- 使用隨機分佈發生器進行隨機數的生成
- 將獲得的隨機數進行解碼操作,(就是反捲積操作)
- 最後通過全連接得到想要的圖片
代碼實現過程:
定義編碼器
之前說過編碼器就是一個卷積的過程,所以定義如下:
定義一個encode方法
def encode(self, x, vec_size):
其中x所代表的是輸入的手寫數字識別的圖片,vec_size所代表的是獲得語義值的長度(之前說過我們僅僅打算獲得4個語義值,因此這裏的vec_size所傳入的值就是4)
定義特徵數:filters
filters = 16
爲什麼特徵數選擇16
- 本系統所選擇的數據量比較小
- 電腦硬件的影響
開始進行卷積操作(卷積操作可以使用通用的,但在系統中選擇使用卷積池來實現卷積)
第一步:增大圖片的特徵數
x = tf.layers.conv2d(x,filters,3,1,'same', name='conv1',activation=tf.nn.relu)
第二步:設置卷積池進行卷積
for i in range(2):
filters *= 2
x = tf.layers.conv2d(x,filters,3,1,'same',activation=tf.nn.relu,name='conv2_%d'%i)
x = tf.layers.max_pooling2d(x,2,2)
完成卷積操作,此時所獲得的圖片的大小是:[-1,7,7,64]
如果想要得到語義值,還需要一步全連接的操作,在這裏我們使用卷積的形式來實現全連接
x = tf.layers.conv2d(x,vec_size,7,1,name='conv3')
這樣獲得的語義的形狀是:[-1,1,1,vec_size]
在返回值得時候需要將x的形狀轉變成二維的形狀
return tf.reshape(x,[-1,vec_size])
編碼器的整體代碼:
def encode(self,x,vec_size):
filters = 16
x = tf.layers.conv2d(x,filters,3,1,'same',name = 'conv1', activation = tf.nn.relu)
for i in range(2):
filters *= 2
x = tf.layers.conv2d(x,filters,3,1,'same',activation=tf.nn.relu,name='conv2_%d'%i)
x = tf.layers.max_pooling2d(x,2,2)
x = tf.layers.conv2d(x,vec_size,7,1,name='conv3')
return tf.reshape(x,[-1,vec_size])
編碼器定義完畢後,我們還需要定義解碼器
定義解碼器
解碼的過程實質就是反捲積的過程,相對於編碼器,解碼器就是將編碼器的過程反過來執行。
首先我們獲得的語義的形狀是:[-1,vec_size]
而我們要獲得的結果值的形狀是:[-1,28,28,1]
首先我們將獲得的語義進行全連接,轉換爲:[-1,7,7,64]的形狀
y = tf.layers.dense(vec,7*7*64,tf.nn.relu,name='dense1')
y = tf.reshape(y,[-1,7,7,64])
此時語義的形狀就是經過三次卷積之後的形狀,不難看出我們需要進行三次反捲積就可以得到最終的圖像了。
反捲積的操作可以和卷積的操作進行對比,僅僅表現的就是函數的使用時不同的
首先定義filters的值爲64
然後按照編碼器的過程反着來一遍
for i in range(2):
filters //= 2
y = tf.layers.conv2d_transpose(y,filters,3,2,'same',name='deconv1_%d' % i,activation=tf.nn.relu)
這樣我們就可以得到:[-1,28,28,16]的形狀了
同時爲了還原形狀,我們還需要將圖片的特徵通道值改爲1,也就是圖片中的灰度值
y = tf.layers.conv2d_transpose(y,1,3,1,'same',name='deconv2')#[-1,28,28,1]
最終我們返回y的值
譯碼器的整體代碼
def decode(self,vec):
"""
Decode the semantics vector.
:param vec: [-1, vec_size]
:return: [-1, 28, 28, 1]
"""
y = tf.layers.dense(vec,7*7*64,tf.nn.relu,name='dense1')
y = tf.reshape(y,[-1,7,7,64])
filters = 64
for i in range(2):
filters //= 2
y = tf.layers.conv2d_transpose(y,filters,3,2,'same',name='deconv1_%d' % i,activation=tf.nn.relu)
#y:[-1,28,28,16]
y = tf.layers.conv2d_transpose(y,1,3,1,'same',name='deconv2')#[-1,28,28,1]
return y
當然在使用解碼器之前,我們還需要計算語義的平均值和標準差,從而方便的得到正態分佈隨機數
定義運算器–計算平均值和標準差
def process_normal(self,vec)
其中vec是輸入的圖片經過編碼器所獲得的值(符合上訴,我們所做的過程)
首先是平均數的求解:
這裏的平均數和往常所學的平均數有些不同,這裏的平均數的求解是夾雜着動量的原理
比如:
夾雜着動量的平均數的公式是:
化簡公式爲:
其中是慣性系數(也被稱爲衰減率)
這樣我們就得到了求解平均值得公式了
首先,按照公式,我們先求出輸入語義的平均值
mean = tf.reduce_mean(vec,axis=0)
其次,我們還需要獲得語義輸入的維度數,也就是要確定獲得的平均數的維度是多少
vector_size = vec.shape[1].value
這裏的vector_size的大小是4
到這裏我們就可以使用公式:
首先定義一個輸出的變量值,也就是所要求的平均值(要注意其維度爲4)
self.final_mean = tf.get_variable('mean' ,[vector_size],tf.float32,tf.initializers.zeros,trainable=False)
我們需要定義一個慣性系數:a(在這我們定義的是mom)
mom = self.config.momentum # momentum 是在Config類中進行定義的慣性系數,常用的慣性系數是0.99
使用公式爲:
assign = tf.assign(self.final_mean, self.final_mean * mom + mean * (1 - mom))
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS,assign)
上面的函數中,我們使用了一個集合的工具,其目的是
我們都明白使用梯度下降的時候,使用的是反向傳播的原理
但是assign是不參與梯度下降的,也就是assign和train_op沒有實質的聯繫
所以我們需要定義一個正向傳播來實現assign 和 train_op之間的聯繫,相當於讓assign參與梯度下降
這就相當與我們將assign和train_op進行正向傳播。
同理,我們也可以安照同樣的方式來推導出標準差的求法
首先,平均值的使用:
計算方差:
通過以上的公式,我們就可以計算標準差了
首先將獲得的語義值進行平方處理
msd = tf.reduce_mean(tf.square(vec), axis=0)# msd : mean square difference(方差) 計算的維度依然是0維
其次定義平方差的變量
self.final_msd = tf.get_variable('msd',[vector_size], tf.float32, tf.initializers.zeros, trainable=False)
最後和計算平均數類似計算帶有動量的平方的平均數
assign = tf.assign(self.final_msd, self.final_msd*mom + msd*(1-mom))
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, assign)
到此運算器部分的方法基本全部實現了
整體代碼如下:
def process_normal(self,vec): # 處理正態分佈
"""
vec:[-1, vector_size]
"""
mean = tf.reduce_mean(vec, axis=0) # [vector_size] 不可訓練變量{計算平均值}
msd = tf.reduce_mean(tf.square(vec), axis=0)# msd : mean square difference(方差) 計算的維度依然是0維
vector_size = vec.shape[1].value # 獲取數值
self.final_mean = tf.get_variable('mean',[vector_size], tf.float32, tf.initializers.zeros, trainable=False)
self.final_msd = tf.get_variable('msd',[vector_size], tf.float32, tf.initializers.zeros, trainable=False)
# 用mean更新final_mean
mom = self.config.momentum # 慣性系數
# 用tf的賦值表達式來做這件事
assign = tf.assign(self.final_mean,self.final_mean*mom+mean*(1-mom))
# 將train_op和assign進行(正向傳播都走一遍)(反向傳播走實線)
# 使用集合的工具,將train_op獲取到與assign掛鉤
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, assign)
assign = tf.assign(self.final_msd, self.final_msd*mom + msd*(1-mom))
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, assign)
到這裏,我們就要對上面所定義的幾個方法進行整理和調用了
我們將這幾個方法統一的放到Tensors的類中
在Tensors類中的構造方法中定義一下屬性
首先定義輸入的樣本變量
x = tf.placeholder(tf.float32,[None,784],'x')
定義進行梯度下降的學習步長
lr = tf.placeholder(tf.float32,[],'lr')
由於輸入的x的形狀是一條直線,也就是一維的,我們需要將其轉換爲二維的圖片
x = tf.reshape(x,[-1,28,28,1])
這樣輸入樣本的變量x就已經定義好了,我們接下來要做的就是使用編碼器、運算器、以及解碼器
self.vec = self.encode(x, config.vector_size) # [-1,4]
self.process_normal(self.vec)
self.y = self.decode(self.vec) # [-1, 28, 28, 1]
這樣我們就得到得了構建後的圖片了,然後進行損失函數的求解(在這裏我們使用平方差的形式)
loss = tf.reduce_mean(tf.square(self.y-x))
注意一點,如果變量的名字沒有更改則模型是不需要進行重新訓練的
進行梯度下降處理,使創造出來的圖片能夠更加的準確
opt = tf.train.AdamOptimizer(lr)
由於assign和梯度下降沒有關聯,所以,我們需要進行正向傳播,但是如何進行正向傳播呢?
with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
self.train_op = opt.minimize(loss) # 依賴者
以上的代碼,就可以解決這個問題
其中train_op是依賴者
在這裏,我們定義一個損失函數的彙總
self.summary = tf.summary.scalar('loss', tf.sqrt(loss))
最後我們還需要將獲得的,也就通過VAE模型創造出來的圖片進行形狀的轉換
self.y = tf.reshape(self.y,[-1,28,28])
這樣整個VAE模型的構建就完全結束了。
整體代碼如下:
class MyTensors:
def __init__(self,config:MyConfig):
self.config = config
with tf.device('/gpu:0'):
x = tf.placeholder(tf.float32,[None,784],'x')
lr = tf.placeholder(tf.float32,[],'lr')
label = tf.placeholder(tf.int32, [None], 'label')
self.inputs = [x,lr,label]
x = tf.reshape(x,[-1,28,28,1])
self.vec = self.encode(x, config.vector_size) # [-1,4]
self.process_normal(self.vec)
self.y = self.decode(self.vec) # [-1, 28, 28, 1]
loss = tf.reduce_mean(tf.square(self.y-x)) # 變量的名字沒有更改不會導致模型重新訓練
opt = tf.train.AdamOptimizer(lr)
with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
self.train_op = opt.minimize(loss) # 依賴者
self.summary = tf.summary.scalar('loss', tf.sqrt(loss))
self.y = tf.reshape(self.y,[-1,28,28])
#----------------------------------------------------------------------
def process_normal(self,vec): # 處理正態分佈
mean = tf.reduce_mean(vec, axis=0) # [vector_size] 不可訓練變量{計算平均值}
msd = tf.reduce_mean(tf.square(vec), axis=0)# msd : mean square difference(方差) 計算的維度依然是0維
vector_size = vec.shape[1].value # 獲取數值
self.final_mean = tf.get_variable('mean',[vector_size], tf.float32, tf.initializers.zeros, trainable=False)
self.final_msd = tf.get_variable('msd',[vector_size], tf.float32, tf.initializers.zeros, trainable=False)
mom = self.config.momentum # 慣性系數
assign = tf.assign(self.final_mean,self.final_mean*mom+mean*(1-mom))
# 將train_op和assign進行(正向傳播都走一遍)(反向傳播走實線)
# 使用集合的工具,將train_op獲取到與assign掛鉤
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, assign)
assign = tf.assign(self.final_msd, self.final_msd*mom + msd*(1-mom))
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, assign)
#----------------------------------------------------------------------
def encode(self,x,vec_size):
"""
encode the x to a vector which size is vec_size
x : [-1, 28, 28, 1]
the input tensor, which shape is [-1, 28, 28, 1]
the size of the semantics vector
the semantics vector which shape is [-1, vec_size]
卷積--拍扁--全連接
"""
filters = 16
x = tf.layers.conv2d(x,filters,3,1,'same',activation=tf.nn.relu,name='conv1')
# 卷積,池化(縮小大小),激活
for i in range(2):
filters *= 2
x = tf.layers.conv2d(x,filters,3,1,'same',activation=tf.nn.relu, name='conv2_%d'%i)
# 池化激活前後沒有影響
x = tf.layers.max_pooling2d(x,2,2) # 窗口的長度,移動的步數
# x:[-1,7,7,64]
# kern : 7*7 卷積
x = tf.layers.conv2d(x,vec_size,7,1,name='conv3') # [-1,1,1,vec_size] same as dense
return tf.reshape(x,[-1,vec_size])
#----------------------------------------------------------------------
def decode(self,vec):
y = tf.reshape(y,[-1,7,7,64])
filters = 64
size = 7
for i in range(2):
# y 輸入,通道數
filters //= 2
size *= 2
y = tf.layers.conv2d_transpose(y,filters,3,2,'same',name='deconv1_%d'%i, activation=tf.nn.relu)
y = tf.layers.conv2d_transpose(y,1,3,1,'same',name='deconv2')
return y
運行結果如下: