Tensorflow–一維離散卷積
一維離散卷積的運算是一種主要基於向量的計算方式
一.一維離散卷積的計算原理
一維離散卷積通常有三種卷積類型:full卷積,same卷積和valid卷積
1.full卷積
full卷積的計算過程如下:K沿着I順序移動,每移動一個固定位置,對應位置的值相乘,然后對其求和
其中K稱為卷積核或者濾波器或者卷積掩碼
2.valid卷積
從full卷積的計算過程可知,如果K靠近I,就會有部分延伸到I之外,valid卷積只考慮能完全覆蓋K內的情況,即K在I內部移動的情況
3.same卷積
首先在卷積核K上指定一個錨點,然后將錨點順序移動到輸入張量I的每一個位置處,對應位置相乘然后求和,卷積核錨點的位置一般有以下規則,假設卷積核的長度為FL:
如果FL為奇數,則錨點的位置在(FL-1)/2處
如果FL為偶數,則錨點的位置在(FL-2)/2處
4.full,same,valid卷積的關系
假設一個長度為L的一維張量與一個長度為FL的卷積核卷積,其中Fa代表計算same卷積時,錨點的位置索引,則兩者的full卷積與same卷積的關系如下:
Csame=Cfull[FL-Fa-1,FL-Fa+L-2]
full卷積C與valid卷積C的關系如下:
C=C[FL-1,L-1]
same卷積C與valid卷積C的關系如下:
C=C[FL-Fa-2,L-Fa_2]
注意:大部分書籍中對卷積運算的定義分為兩步。第1步是將卷積核翻轉180;第2步是將翻轉的結果沿輸入張量順序移動,每移動到一個固定位置,對應位置相乘然后求和,如Numpy中實現的卷積函數convolve和Scipy中實現的卷積函數convolve,函數內部都進行了以上兩步運算。可見,最本質的卷積運算還是在第2步。Tensorflow中實現的卷積函數
tf.nn.conv1d(value,filters,stride,padding,use_cudnn_on_gpu=None,data_format=None,name=None)
其內部就沒有進行第1步操作,而是直接進行了第2步操作
import tensorflow as tf
# 輸入張量I
I=tf.constant(
[
[[3],[4],[1],[5],[6]]
]
,tf.float32
)
# 卷積核
K=tf.constant(
[
[[-1]],
[[-2]],
[[2]],
[[1]]
]
,tf.float32
)
I_conv1d_K=tf.nn.conv1d(I,K,1,'SAME')
session=tf.Session()
print(session.run(I_conv1d_K))
[[[ 3.]
[ -4.]
[ 10.]
[ 1.]
[-17.]]]
函數tf.nn.conv1d只實現了same卷積和valid卷積,它就是為更方便地搭建卷積神經網絡而設計的。利用Numpy或者Scipy中的卷積函數convolve實現上述示例的full卷積代碼如下:
import numpy as np
from scipy import signal
I=np.array([3,4,1,5,6],np.float32)
K=np.array([-1,-2,2,1],np.float32)
# 卷積核K翻轉180
K_reverse=np.flip(K,0)
# r=np.convolve(I,K_reverse,mode='full')
r=signal.convolve(I,K_reverse,mode='full')
print(r)
[ 3. 10. 3. -4. 10. 1. -17. -6.]
注意:如果卷積核的長度是偶數,函數convolve和tf.nn.conv1d在實現same卷積時,其結果會略有不同,但也只是在邊界處(兩端)的值有所不同,這是因為這兩個函數對卷積核錨點的位置定義不同,本質上就是從full卷積結果中取的區域不一樣
二.一維卷積定理
1.一維離散傅里葉變換
Tensorflow通過函數fft和ifft分別實現一維離散的傅里葉變換及逆變換
import tensorflow as tf
# 輸入長度為3的一維張量
f=tf.constant([4,5,6],tf.complex64)
session=tf.Session()
# 一維傅里葉變換
F=tf.fft(f)
print("傅里葉變換F的值:")
print(session.run(F))
# 計算F的傅里葉逆變換(顯然與輸入的f是相等的)
F_ifft=tf.ifft(F)
print("打印F的傅里葉逆變換的值:")
print(session.run(F_ifft))
傅里葉變換F的值:
[15. -5.9604645e-08j -1.4999998+8.6602545e-01j
-1.4999999-8.6602563e-01j]
打印F的傅里葉逆變換的值:
[4.+3.9736431e-08j 5.-3.1789145e-07j 6.+0.0000000e+00j]
2.卷積定理
假設有長度為L的一維張量I,I(l)代表I的第l個數,其中0≤l<L,有長度為FL的一維卷積核K,那么I與K的full卷積結果的尺寸為L+FL-1
首先,在I的末尾補零,將I的尺寸擴充到與full卷積尺寸相同,即
然后,將卷積核K翻轉180得到K_rotate180,在末尾進行補零操作,且將K_rotate180的尺寸擴充到和full卷積相同,即
假設fft_Ip和fft_Krp分別是I_padded和K_rotate180_padded的傅里葉變換,那么I☆K的傅里葉變換等於fft_Ipfft_Krp,即
其中代表對應元素相乘,即對應位置的兩個復數相乘,該關系通常稱為卷積定理
從卷積定理中可以看出分別有對張量的補零操作(或稱為邊界擴充)和翻轉操作。這兩種操作在Tensorflow中有對應的函數實現,我們先介紹實現邊界擴充的函數:
pad(tensor,padding,mode='CONSTANT',name=None,constant_value=0)
以長度為2的一維張量為例,上側補1個0,下側補2個0
import tensorflow as tf
x=tf.constant([2,1],tf.float32)
r=tf.pad(x,[[1,2]],mode='CONSTANT')
session=tf.Session()
print(session.run(r))
[0. 2. 1. 0. 0.]
當使用常數進行擴充時,也可以選擇其他常數,通過參數constant_value進行設置,默認缺省值為0
import tensorflow as tf
x=tf.constant(
[
[1,2,3],
[4,5,6]
]
,tf.float32
)
# 常數邊界擴充,上側補1行10,下側補2行10,右側補1列10
r=tf.pad(x,[[1,2],[0,1]],mode='CONSTANT',constant_values=10)
session=tf.Session()
print(session.run(r))
[[10. 10. 10. 10.]
[ 1. 2. 3. 10.]
[ 4. 5. 6. 10.]
[10. 10. 10. 10.]
[10. 10. 10. 10.]]
除了常數邊界擴充,還有其他擴充方式,可以通過參數mode設置,當mode='SYMMETRIC’時,代表鏡像方式的邊界擴充;當mode='REFLECT’時,代表反射方式的邊界擴充,可以修改以上程序觀察打印結果
Tensorflow通過函數reverse(tensor,axis,name=None)實現張量的翻轉,二維張量的每一列翻轉(沿"0"方向),則稱為水平鏡像;對每一行翻轉(沿"1"方向),則稱為垂直鏡像
import tensorflow as tf
t=tf.constant(
[
[1,2,3],
[4,5,6]
]
,tf.float32
)
# 水平鏡像
rh=tf.reverse(t,axis=[0])
# 垂直鏡像
rv=tf.reverse(t,axis=[1])
# 逆時針翻轉180:先水平鏡像在垂直鏡像(或者先垂直再水平)
r=tf.reverse(t,[0,1])
session=tf.Session()
print("水平鏡像的結果:")
print(session.run(rh))
print("垂直鏡像的結果:")
print(session.run(rv))
print("逆時針翻轉180的結果:")
print(session.run(r))
水平鏡像的結果:
[[4. 5. 6.]
[1. 2. 3.]]
垂直鏡像的結果:
[[3. 2. 1.]
[6. 5. 4.]]
逆時針翻轉180的結果:
[[6. 5. 4.]
[3. 2. 1.]]
掌握了張量邊界擴充和翻轉的對應函數后,利用卷積定理計算前面中x和K的full卷積
import tensorflow as tf
# 長度為5的輸入張量
I=tf.constant(
[3,4,1,5,6],tf.complex64
)
# 長度為4的卷積核
K=tf.constant(
[-1,-2,2,1],tf.complex64
)
# 補0操作
I_padded=tf.pad(I,[[0,3]])
# 將卷積核翻轉180
K_rotate180=tf.reverse(K,axis=[0])
# 翻轉進行0操作
K_rotate180_padded=tf.pad(K_rotate180,[[0,4]])
# 傅里葉變換
I_padded_fft=tf.fft(I_padded)
# 傅里葉變換
K_rotate180_padded_fft=tf.fft(K_rotate180_padded)
# 將以上兩個傅里葉變換點乘操作
IK_fft=tf.multiply(I_padded_fft,K_rotate180_padded_fft)
# 傅里葉逆變換
IK=tf.ifft(IK_fft)
# 因為輸入的張量和卷積核都是實數,對以上傅里葉逆變換進行取實部的操作
IK_real=tf.real(IK)
session=tf.Session()
print(session.run(IK_real))
[ 2.9999998 10. 3. -4. 10. 1.
-17. -6. ]
三.具備深度的一維離散卷積
1.具備深度的張量與卷積核的卷積
張量x可以理解為是一個長度為3,深度為3的張量,K可以理解為是一個長度為2,深度為3的張量,兩者same卷積的過程就是錨點順序移動到輸入張量的每一個位置處,然后對應位置相乘,求和
注意:輸入張量的深度和卷積核的深度是相等的
import tensorflow as tf
# 1個長度為3,深度為3的張量
x=tf.constant(
[
[[2,5,2],[6,1,-1],[7,9,-5]]
]
,tf.float32
)
# 1個長度為2,深度為3的卷積核
k=tf.constant(
[
[[-1],[5],[4]],[[2],[1],[6]]
]
,tf.float32
)
# 一維same卷積
v_conv1d_k=tf.nn.conv1d(x,k,1,'SAME')
session=tf.Session()
print(session.run(v_conv1d_k))
[[[ 38.]
[-12.]
[ 18.]]]
2.具備深度的張量分別與多個卷積核的卷積
同一個張量與多個卷積核的卷積本質上是該張量分別與每一個卷積核卷積,然后將每一個卷積結果在深度方向上連接在一起。以長度為3,深度為3的輸入張量與2個長度為2,深度為3的卷積核卷積為例
import tensorflow as tf
x=tf.constant(
[
[[2,5,2],[6,1,-1],[7,9,-5]]
]
,tf.float32
)
# 2個長度為2,深度為3的卷積核
k=tf.constant(
[
[[-1,1],[5,3],[4,7]],[[2,-2],[1,-1],[6,9]]
]
,tf.float32
)
v_conv1d_k=tf.nn.conv1d(x,k,1,'SAME')
session=tf.Session()
print(session.run(v_conv1d_k))
[[[ 38. 9.]
[-12. -66.]
[ 18. -1.]]]
1個深度為C的張量與M個深度為C的卷積核的卷積結果的深度為M,即最后輸出結果的深度與卷積核的個數相等
3.多個具備深度的張量分別與多個卷積核的卷積
計算3個長度為3,深度為3的張量與2個長度為2,深度為3的卷積核的卷積
import tensorflow as tf
# 3個長度為3,深度為3的張量
x=tf.constant(
[
[[2,5,2],[6,1,-1],[7,9,-5]], # 第1個
[[1,3,2],[5,2,-2],[8,4,3]], # 第2個
[[4,5,-1],[1,9,5],[2,7,0]] # 第3個
]
,tf.float32
)
# 2個長度為2,深度為3的卷積核
k=tf.constant(
[
[[-1,1],[5,3],[4,7]],[[2,-2],[1,-1],[6,9]]
]
,tf.float32
)
v_conv1d_k=tf.nn.conv1d(x,k,1,'SAME')
session=tf.Session()
print(session.run(v_conv1d_k))
[[[ 38. 9.]
[-12. -66.]
[ 18. -1.]]
[[ 22. -6.]
[ 35. 4.]
[ 24. 41.]]
[[ 58. 46.]
[ 75. 52.]
[ 33. 23.]]]
函數tf.nn.conv1d可以實現任意多個輸入量分別與任意多個卷積核的卷積,輸入張量的深度和卷積核的深度是相等的