TensorFlow 使用VAE模型進行手寫數學圖片的創建

如何使用VAE模型進行手寫數字圖片的創建

其實主要依賴於兩個發生器,一個是編碼,一個是解碼


實現步驟如下:

  1. 將數字手寫的圖片庫傳入到VAE中
  2. 經過編碼器(卷積神經網絡)得到一個語義值(包括手寫數字識別的特徵,這裏我們設置4個維度,分別代表:粗細,彎曲程度等)
  3. 將獲得的四個值進行平均數和標準差的求解,(目的是爲了符合正態分佈,爲什麼使用正態分佈做隨機數,因爲正態分佈形成的隨機數比較精準)
  4. 使用隨機分佈發生器進行隨機數的生成
  5. 將獲得的隨機數進行解碼操作,(就是反捲積操作)
  6. 最後通過全連接得到想要的圖片

代碼實現過程:
定義編碼器

之前說過編碼器就是一個卷積的過程,所以定義如下:
定義一個encode方法

def encode(self, x, vec_size):

其中x所代表的是輸入的手寫數字識別的圖片,vec_size所代表的是獲得語義值的長度(之前說過我們僅僅打算獲得4個語義值,因此這裏的vec_size所傳入的值就是4)
定義特徵數:filters

filters = 16

爲什麼特徵數選擇16

  1. 本系統所選擇的數據量比較小
  2. 電腦硬件的影響

開始進行卷積操作(卷積操作可以使用通用的,但在系統中選擇使用卷積池來實現卷積)
第一步:增大圖片的特徵數

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是輸入的圖片經過編碼器所獲得的值(符合上訴,我們所做的過程)

首先是平均數的求解:

這裏的平均數和往常所學的平均數有些不同,這裏的平均數的求解是夾雜着動量的原理

比如:x1x2x3...xnx_1、x_2、x_3...x_n

夾雜着動量的平均數的公式是:

xn=nxn+xn+1n+1\frac{}{x_n} = \frac{n\frac{}{x_n} + x_{n+1}}{n+1}

化簡公式爲:

xn=axn+(1a)xn+1\frac{}{x_n} = a*\frac{}{x_n}+(1-a)*x_{n+1} 其中aa是慣性系數(也被稱爲衰減率)

這樣我們就得到了求解平均值得公式了

首先,按照公式,我們先求出輸入語義的平均值

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進行正向傳播。

同理,我們也可以安照同樣的方式來推導出標準差的求法

首先,平均值的使用:x=imxim\frac{}{x} =\frac{\sum_i^{m}x_i}{m}

計算方差:

S2=im(xix)2mS^2=\frac{\sum_i^m(x_i-\frac{}{x})^2}{m}

=im(xi22xix+(x)2)m=\frac{\sum_i^m(x_i^2-2x_i\frac{}{x}+(\frac{}{x})^2)}{m}

=imxi22ximxim+x2=\frac{\sum_i^mx_i^2-2\frac{}{x}\sum_i^mx_i}{m}+\frac{}{x}^2

=imxi2mx2=\frac{\sum_i^mx_i^2}{m}-\frac{}{x}^2

通過以上的公式,我們就可以計算標準差了

首先將獲得的語義值進行平方處理

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

運行結果如下:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章