反卷積是指,通過測量輸出和已知輸入重構未知輸入的過程。在神經網絡中,反卷積過程並不具備學習的能力,僅僅是用於可視化一個已經訓練好的卷積神經網絡,沒有學習訓練的過程。反卷積有着許多特別的應用,一般可以用於信道均衡、圖像恢復、語音識別、地震學、無損探傷等未知輸入估計和過程辨識方面的問題。
在神經網絡的研究中,反卷積更多的是充當可視化的作用,對於一個復雜的深度卷積網絡,通過每層若干個卷積核的變換,我們無法知道每個卷積核關注的是什么,變換后的特征是什么樣子。通過反卷積的還原,可以對這些問題有個清晰的可視化,以各層得到的特征圖作為輸入,進行反卷積得到反卷積結果,以驗證顯示各層提取到的特征圖。
一 反卷積原理
反卷積可以理解為卷積操作的逆操作,這里千萬不要當成反卷積操作可以復原卷積操作的輸入值,反卷積並沒有那個功能,它僅僅是將卷積變換過程中的步驟反向變換一次而已,通過將卷積核轉置,與卷積后的結果再做一遍卷積,所以它還有一個名字叫做轉置卷積。
舉個例子:假如你想要查看Alexnet 的conv5提取到了什么東西,我們就用conv5的特征圖后面接一個反卷積網絡,然后通過:反池化、反激活、反卷積,這樣的一個過程,把本來一張13*13大小的特征圖(conv5大小為13*13),放大回去,最后得到一張與原始輸入圖片一樣大小的圖片(227*227)。
雖然它不能還原出原來卷積的樣子,但是在作用上有類似的效果,你可以將帶有小部分缺失的信息最大化的恢復,也可以用來恢復被卷積生成后的原始輸入。
反卷積的具體操作比較復雜,這里不介紹如何具體實現反卷積,在tensorflow中反卷積的是通過函數tf.nn.conv2d_transpose()來實現的:
def conv2d_transpose(value, filter, output_shape, strides, padding="SAME", data_format="NHWC", name=None):
具體參數說明如下:
- value:代表通過卷積操作之后的張量,一般用NHWC類型。如果是NHWC類型,形狀[batch, height, width, in_channels],如果是NCHW類型,形狀為[batch, in_channels, height, width]。
- filter:代表卷積核,形狀為[height, width, output_channels, in_channels]。
- output_shape:反卷積輸出的張量形狀,它必須是能夠生成value參數的原數據的形狀,如果輸出形狀不對,函數會報錯。
- strides:代表原數據生成value時使用的步長。
- padding:代表原數據生成value時使用的填充方式,是用來檢查輸入形狀和輸出形狀是否合規的。
- data_format: 'NHWC' and 'NCHW' 類型。
- name:名稱。
返回反卷積后的形狀,按照output_shape指定的形狀。
查看該函數的實現代碼,我們可以看到反卷積的操作其實是使用了gen_nn_ops.conv2d_backprop_input()函數來實現的,相當於在TensorFlow中利用了卷積操作在反向傳播的處理函數中做反卷積操作,即卷積操作的反向傳播就是反卷積操作。
注意:在使用反卷積的網絡中,定義占位符中不能存在None,必須指定具體的數,不然會報錯。
二 反卷積實例
我們通過對模擬數據進行卷積核反卷積的操作,來比較卷積與反卷積中padding在SAME和VALID下的變化。先定義一個[1,4,4,1]的矩陣,矩陣里的元素值都為1,與濾波器大小為2x2,步長為2x2,分別使用padding為SAME和VALID兩種情況生成卷積數據,然后將結果再進行反卷積運算,打印輸出的結果
''' 一 反卷積實例 ''' import tensorflow as tf import numpy as np #模擬數據 img = tf.Variable(tf.constant(1.0,shape=[1,4,4,1])) kernel =tf.Variable(tf.constant([1.0,0,-1,-2],shape=[2,2,1,1])) #分別進行VALID和SAME操作 conv = tf.nn.conv2d(img,kernel,strides=[1,2,2,1],padding='VALID') cons = tf.nn.conv2d(img,kernel,strides=[1,2,2,1],padding='SAME') #VALID填充計算方式 (n - f + 1)/s向上取整 print(conv.shape) #SAME填充計算方式 n/s向上取整 print(cons.shape) #在進行反卷積操作 contv = tf.nn.conv2d_transpose(conv,kernel,[1,4,4,1],strides=[1,2,2,1],padding='VALID') conts = tf.nn.conv2d_transpose(cons,kernel,[1,4,4,1],strides=[1,2,2,1],padding='SAME') with tf.Session() as sess: sess.run(tf.global_variables_initializer()) print('kernel:\n',sess.run(kernel)) print('conv:\n',sess.run(conv)) print('cons:\n',sess.run(cons)) print('contv:\n',sess.run(contv)) print('conts:\n',sess.run(conts))
我們可以把padding為VALID方式時的卷積核反卷積操作繪制出來,如下圖:
三 反池化原理
反池化屬於池化的逆操作,是無法通過池化的結果還原出全部的原始數據,因此池化的過程只保留主要信息,舍去部分信息。如果想從池化后的這些主要信息恢復出全部信息,由於存在着信息缺失,這時只能通過補位來實現最大程度的信息完整。
池化層常用的有最大池化和平均池化,其反池化也需要與其對應。
- 平均池化比較簡單。首先還原成原來的大小,然后將池化結果中的每個值都填入其對應於原始數據區域中的相應位置即可。如下圖:
- 最大池化的反池化會復雜一些。要求在池化過程中記錄最大激活池的坐標位置,然后在反池化時,只把池化過程中最大激活值所在位置坐標的值激活,其它位置為0.當然,這個過程只是一種近似,因為在池化的過程中,除了最大值所在的位置,其他的值也是不為0的。如下圖:
-
四 反池化實例
TensorFlow中目前還沒有反池化操作的函數。對於最大池化層,也不支持輸出最大激活值得位置,但是同樣有個池化的反向傳播函數tf.nn.max_pool_with_argmax()。該函數可以找出位置,需要開發者利用這個函數做一些改動,自己封裝一個最大池化操作,然后再根據mask寫出反池化函數。
''' 二 反池化操作 ''' def max_pool_with_argmax(net,stride): ''' 重定義一個最大池化函數,返回最大池化結果以及每個最大值的位置(是個索引,形狀和池化結果一致) args: net:輸入數據 形狀為[batch,in_height,in_width,in_channels] stride:步長,是一個int32類型,注意在最大池化操作中我們設置窗口大小和步長大小是一樣的 ''' #使用mask保存每個最大值的位置 這個函數只支持GPU操作 _, mask = tf.nn.max_pool_with_argmax( net,ksize=[1, stride, stride, 1], strides=[1, stride, stride, 1],padding='SAME') #將反向傳播的mask梯度計算停止 mask = tf.stop_gradient(mask) #計算最大池化操作 net = tf.nn.max_pool(net, ksize=[1, stride, stride, 1],strides=[1, stride, stride, 1], padding='SAME') #將池化結果和mask返回 return net,mask def un_max_pool(net,mask,stride): ''' 定義一個反最大池化的函數,找到mask最大的索引,將max的值填到指定位置 args: net:最大池化后的輸出,形狀為[batch, height, width, in_channels] mask:位置索引組數組,形狀和net一樣 stride:步長,是一個int32類型,這里就是max_pool_with_argmax傳入的stride參數 ''' ksize = [1, stride, stride, 1] input_shape = net.get_shape().as_list() # calculation new shape output_shape = (input_shape[0], input_shape[1] * ksize[1], input_shape[2] * ksize[2], input_shape[3]) # calculation indices for batch, height, width and feature maps one_like_mask = tf.ones_like(mask) batch_range = tf.reshape(tf.range(output_shape[0], dtype=tf.int64), shape=[input_shape[0], 1, 1, 1]) b = one_like_mask * batch_range y = mask // (output_shape[2] * output_shape[3]) x = mask % (output_shape[2] * output_shape[3]) // output_shape[3] feature_range = tf.range(output_shape[3], dtype=tf.int64) f = one_like_mask * feature_range # transpose indices & reshape update values to one dimension updates_size = tf.size(net) indices = tf.transpose(tf.reshape(tf.stack([b, y, x, f]), [4, updates_size])) values = tf.reshape(net, [updates_size]) ret = tf.scatter_nd(indices, values, output_shape) return ret #定義一個形狀為4x4x2的張量 img = tf.constant([ [[0.0,4.0],[0.0,4.0],[0.0,4.0],[0.0,4.0]], [[1.0,5.0],[1.0,5.0],[1.0,5.0],[1.0,5.0]], [[2.0,6.0],[2.0,6.0],[2.0,6.0],[2.0,6.0]], [[3.0,7.0],[3.0,7.0],[3.0,7.0],[3.0,7.0]], ]) img = tf.reshape(img,[1,4,4,2]) #最大池化操作 pooling1 = tf.nn.max_pool(img,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME') #帶有最大值位置的最大池化操作 pooling2,mask = max_pool_with_argmax(img,2) #反最大池化 img2 = un_max_pool(pooling2,mask,2) with tf.Session() as sess: print('image:') image = sess.run(img) print(image) #默認的最大池化輸出 result = sess.run(pooling1) print('max_pool:\n',result) #帶有最大值位置的最大池化輸出 result,mask2 = sess.run([pooling2,mask]) print('max_pool_with_argmax:\n',result,mask2) #反最大池化輸出 result = sess.run(img2) print('un_max_pool',result)
這里我們自己定義了兩個函數,一個是帶有最大值位置的最大池化函數,一個反最大池化函數,程序運行后,我們應該可以看到自己定義的最大池化與原來的版本輸出是一樣的,由於tf.nn.max_pool_with_argmax()函數只支持GPU操作,不能在CPU機器上運行,所以我沒法運行這段程序。mask的值是將整個數組flat后的索引,並保持與池化結果一致的shape。
五 偏導計算
在反向傳播的過程中,神經網絡需要對每個代價函數對應的學習參數求偏導,計算出的這個值叫做梯度,用來乘以學習率然后更新學習參數使用的。它是通過tf.gradients()函數來實現的,這個函數的第一個參數為需要求導的公式,第二個參數為指定公式中的哪個變量來求偏導。如果對一個不存在的變量求偏導,會返回None。
''' 三 偏導計算 ''' w1 = tf.Variable([[1,2]]) #1x2 w2 = tf.Variable([[3,4]]) #1x2 y = tf.matmul(w1,[[9],[10]]) #2x1 #求w1的梯度 grads = tf.gradients(y,w1) #1x2 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) gradval = sess.run(grads) print(gradval)
可以看到我們計算得到的結果為[[9,10]],形狀為1x2,即[[9],[10]]的轉置。至於為什么是其轉置,你需要了解一下矩陣如何求偏導的知識。
import numpy as np x = np.array([[9,10]]) print(x.shape) x = np.array([[9],[10]]) print(x.shape)
tf.gradients()函數還可以同時對多個子式求關於多個變量的偏導:
tf.reset_default_graph() w1 = tf.get_variable('w1',shape=[2]) w2 = tf.get_variable('w2',shape=[2]) w3 = tf.get_variable('w3',shape=[2]) w4 = tf.get_variable('w4',shape=[2]) y1 = w1 + w2 + w3 y2 = w3 + w4 gradients = tf.gradients([y1,y2],[w1,w2,w3,w4],grad_ys=[tf.convert_to_tensor([1.,2.]),tf.convert_to_tensor([3.,4.])]) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) gradval = sess.run(gradients) print(gradval)
上面的程序有兩個op,4個參數,演示了使用tf.gradients()函數同時為兩個式子4個參數求梯度。
這里使用了tf.gradients()函數的第三個參數,即給定公式結果的值,來求參數梯度,這里相當於y1為[1.,2.],y2為[3.,4.]。對於y1來講,求關於w1的梯度時,會認為w2和w3為常數,所以w2,w3的導數為0,即w1的梯度就為[1.,2.]。同理可以得出w2,w3均為[1.,2.],接着求y2的梯度,得到w3和w4均為[3.,4.]。然后將兩個式子中的w3結果加起來,所以w3就為[4.,6.]。(這一塊我也沒有懂,一臉懵逼)
六 梯度停止
對於反向傳播過程中某種情況需要停止梯度的運算時,在TensorFlow中提供了一個tf.stop_gradient()函數,被它定義過得節點將沒有梯度運算的功能。
''' 四 梯度停止 ''' w1 = tf.Variable(2.0) w2 = tf.Variable(2.0) a = tf.multiply(w1, 3.0) #停止a節點梯度運算的功能 a_stoped = tf.stop_gradient(a) b = tf.multiply(a_stoped, w2) gradients = tf.gradients(b, xs=[w1, w2]) print(gradients)
可見,一個節點
被 stop
之后,這個節點上的梯度,就無法再向前BP
了。由於w1
變量的梯度只能來自a
節點,由於停止了1節點梯度運算的功能,所以,計算梯度返回的是None
。
a = tf.Variable(1.0) b = tf.Variable(1.0) c = tf.add(a, b) #停止c節點梯度運算的功能 c_stoped = tf.stop_gradient(c) d = tf.add(a, b) e = tf.add(c_stoped, d) gradients = tf.gradients(e, xs=[a, b]) with tf.Session() as sess: tf.global_variables_initializer().run() print(sess.run(gradients))
雖然 c
節點被stop
了,但是a,b
還有從d
傳回的梯度,所以還是可以輸出梯度值的。
完整代碼:

# -*- coding: utf-8 -*- """ Created on Fri May 4 17:04:22 2018 @author: zy """ ''' 一 反卷積實例 ''' import tensorflow as tf import numpy as np #模擬數據 img = tf.Variable(tf.constant(1.0,shape=[1,4,4,1])) kernel =tf.Variable(tf.constant([1.0,0,-1,-2],shape=[2,2,1,1])) #分別進行VALID和SAME操作 conv = tf.nn.conv2d(img,kernel,strides=[1,2,2,1],padding='VALID') cons = tf.nn.conv2d(img,kernel,strides=[1,2,2,1],padding='SAME') #VALID填充計算方式 (n - f + 1)/s向上取整 print(conv.shape) #SAME填充計算方式 n/s向上取整 print(cons.shape) #在進行反卷積操作 contv = tf.nn.conv2d_transpose(conv,kernel,[1,4,4,1],strides=[1,2,2,1],padding='VALID') conts = tf.nn.conv2d_transpose(cons,kernel,[1,4,4,1],strides=[1,2,2,1],padding='SAME') with tf.Session() as sess: sess.run(tf.global_variables_initializer()) print('kernel:\n',sess.run(kernel)) print('conv:\n',sess.run(conv)) print('cons:\n',sess.run(cons)) print('contv:\n',sess.run(contv)) print('conts:\n',sess.run(conts)) ''' 二 反池化操作 ''' def max_pool_with_argmax(net,stride): ''' 重定義一個最大池化函數,返回最大池化結果以及每個最大值的位置(是個索引,形狀和池化結果一致) args: net:輸入數據 形狀為[batch,in_height,in_width,in_channels] stride:步長,是一個int32類型,注意在最大池化操作中我們設置窗口大小和步長大小是一樣的 ''' #使用mask保存每個最大值的位置 這個函數只支持GPU操作 _, mask = tf.nn.max_pool_with_argmax( net,ksize=[1, stride, stride, 1], strides=[1, stride, stride, 1],padding='SAME') #將反向傳播的mask梯度計算停止 mask = tf.stop_gradient(mask) #計算最大池化操作 net = tf.nn.max_pool(net, ksize=[1, stride, stride, 1],strides=[1, stride, stride, 1], padding='SAME') #將池化結果和mask返回 return net,mask def un_max_pool(net,mask,stride): ''' 定義一個反最大池化的函數,找到mask最大的索引,將max的值填到指定位置 args: net:最大池化后的輸出,形狀為[batch, height, width, in_channels] mask:位置索引組數組,形狀和net一樣 stride:步長,是一個int32類型,這里就是max_pool_with_argmax傳入的stride參數 ''' ksize = [1, stride, stride, 1] input_shape = net.get_shape().as_list() # calculation new shape output_shape = (input_shape[0], input_shape[1] * ksize[1], input_shape[2] * ksize[2], input_shape[3]) # calculation indices for batch, height, width and feature maps one_like_mask = tf.ones_like(mask) batch_range = tf.reshape(tf.range(output_shape[0], dtype=tf.int64), shape=[input_shape[0], 1, 1, 1]) b = one_like_mask * batch_range y = mask // (output_shape[2] * output_shape[3]) x = mask % (output_shape[2] * output_shape[3]) // output_shape[3] feature_range = tf.range(output_shape[3], dtype=tf.int64) f = one_like_mask * feature_range # transpose indices & reshape update values to one dimension updates_size = tf.size(net) indices = tf.transpose(tf.reshape(tf.stack([b, y, x, f]), [4, updates_size])) values = tf.reshape(net, [updates_size]) ret = tf.scatter_nd(indices, values, output_shape) return ret ''' #定義一個形狀為4x4x2的張量 img = tf.constant([ [[0.0,4.0],[0.0,4.0],[0.0,4.0],[0.0,4.0]], [[1.0,5.0],[1.0,5.0],[1.0,5.0],[1.0,5.0]], [[2.0,6.0],[2.0,6.0],[2.0,6.0],[2.0,6.0]], [[3.0,7.0],[3.0,7.0],[3.0,7.0],[3.0,7.0]], ]) img = tf.reshape(img,[1,4,4,2]) #最大池化操作 pooling1 = tf.nn.max_pool(img,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME') #帶有最大值位置的最大池化操作 pooling2,mask = max_pool_with_argmax(img,2) #反最大池化 img2 = un_max_pool(pooling2,mask,2) with tf.Session() as sess: print('image:') image = sess.run(img) print(image) #默認的最大池化輸出 result = sess.run(pooling1) print('max_pool:\n',result) #帶有最大值位置的最大池化輸出 result,mask2 = sess.run([pooling2,mask]) print('max_pool_with_argmax:\n',result,mask2) #反最大池化輸出 result = sess.run(img2) print('un_max_pool',result) ''' ''' 三 偏導計算 ''' w1 = tf.Variable([[1,2]]) #1x2 w2 = tf.Variable([[3,4]]) #1x2 y = tf.matmul(w1,[[9],[10]]) #2x1 #求w1的梯度 grads = tf.gradients(y,w1) #1x2 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) gradval = sess.run(grads) print(gradval) tf.reset_default_graph() w1 = tf.get_variable('w1',shape=[2]) w2 = tf.get_variable('w2',shape=[2]) w3 = tf.get_variable('w3',shape=[2]) w4 = tf.get_variable('w4',shape=[2]) y1 = w1 + w2 + w3 y2 = w3 + w4 gradients = tf.gradients([y1,y2],[w1,w2,w3,w4],grad_ys=[tf.convert_to_tensor([1.,2.]),tf.convert_to_tensor([3.,4.])]) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) gradval = sess.run(gradients) print(gradval) ''' 四 梯度停止 ''' w1 = tf.Variable(2.0) w2 = tf.Variable(2.0) a = tf.multiply(w1, 3.0) #停止a節點梯度運算的功能 a_stoped = tf.stop_gradient(a) b = tf.multiply(a_stoped, w2) gradients = tf.gradients(b, xs=[w1, w2]) print(gradients) a = tf.Variable(1.0) b = tf.Variable(1.0) c = tf.add(a, b) #停止c節點梯度運算的功能 c_stoped = tf.stop_gradient(c) d = tf.add(a, b) e = tf.add(c_stoped, d) gradients = tf.gradients(e, xs=[a, b]) with tf.Session() as sess: tf.global_variables_initializer().run() print(sess.run(gradients))
參考文章
[2]tensorflow學習筆記(三十):tf.gradients 與 tf.stop_gradient() 與 高階導數