數據批量的概念
通常來說,深度學習中所有數據張量的第一個軸(也就是軸0,因為索引從0開始)都是樣本軸[samples axis,有時也叫樣本維度(samples dimension)?]?。深度學習模型不會一次性處理整個數據集,而是將數據拆分成小批量。下面是MNIST數據集的一個批量,批量大小為128。
batch = train_images[:128]
對于這種批量張量,第一個軸(軸0)叫作批量軸(batch axis)或批量維度(batch dimension)?。在使用Keras和其他深度學習庫時,你會經常遇到“批量軸”這個術語。
現實世界中的數據張量實例
向量數據:形狀為(samples, features)的2階張量,每個樣本都是一個數值(?“特征”?)向量。時間序列數據或序列數據:形狀為(samples, timesteps, features)的3階張量,每個樣本都是特征向量組成的序列(序列長度為timesteps)?。圖像數據:形狀為(samples, height, width, channels)的4階張量,每個樣本都是一個二維像素網格,每個像素則由一個“通道”?(channel)向量表示。視頻數據:形狀為(samples, frames, height, width, channels)的5階張量,每個樣本都是由圖像組成的序列(序列長度為frames)?。
向量數據
這是最常見的一類數據。對于這種數據集,每個數據點都被編碼為一個向量,因此一個數據批量就被編碼為一個2階張量(由向量組成的數組)?,其中第1個軸是樣本軸,第2個軸是特征軸(features axis)?。
時間序列數據或序列數據
當時間(或序列順序)對數據很重要時,應該將數據存儲在帶有時間軸的3階張量中。每個樣本可被編碼為一個向量序列(2階張量)?,因此一個數據批量就被編碼為一個3階張量。
圖像數據
圖像通常具有3個維度:高度、寬度和顏色深度。雖然灰度圖像只有一個顏色通道,因此可以保存在2階張量中,但按照慣例,圖像張量都是3階張量。
圖像張量的形狀有兩種約定:通道在后(channels-last)的約定(這是TensorFlow的標準)和通道在前(channels-first)的約定(使用這種約定的人越來越少)?。
視頻數據
視頻數據是現實世界中為數不多的需要用到5階張量的數據類型。視頻可以看作幀的序列,每一幀都是一張彩色圖像。由于每一幀都可以保存在一個形狀為(height, width,color_depth)的3階張量中,因此一個視頻(幀的序列)可以保存在一個形狀為(frames, height, width, color_depth)的4階張量中,由多個視頻組成的批量則可以保存在一個形狀為(samples, frames, height, width, color_depth)的5階張量中。
神經網絡的“齒輪”?:張量運算
所有計算機程序最終都可以簡化為對二進制輸入的一些二進制運算(AND、OR、NOR等)?,與此類似,深度神經網絡學到的所有變換也都可以簡化為對數值數據張量的一些張量運算(tensor operation)或張量函數(tensor function)?,如張量加法、張量乘法等。下面是一個Keras層的實例。
keras.layers.Dense(512, activation="relu")
這個層理解為一個函數,其輸入是一個矩陣,返回的是另一個矩陣,即輸入張量的新表示。這個函數具體如下(其中W是一個矩陣,b是一個向量,二者都是該層的屬性)?。
output = relu(dot(input, W) + b)
這里有3個張量運算。輸入張量和張量W之間的點積運算(dot)?。由此得到的矩陣與向量b之間的加法運算(+)?。relu運算。relu(x)就是max(x, 0),relu代表“修正線性單元”?(rectified linear unit)?。
逐元素運算
relu運算和加法都是逐元素(element-wise)運算,即該運算分別應用于張量的每個元素。也就是說,這些運算非常適合大規模并行實現(向量化實現)?。如果你想對逐元素運算編寫一個簡單的Python實現,那么可以使用for循環。下列代碼是對逐元素relu運算的簡單實現。
def naive_relu(x):#x是一個2階NumPy張量assert len(x.shape) == 2#避免覆蓋輸入張量x = x.copy()for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] = max(x[i, j], 0)return x
對于加法,可采用同樣的實現方法。
def naive_add(x, y):#x和y是2階NumPy張量assert len(x.shape) == 2assert x.shape == y.shape#避免覆蓋輸入張量x = x.copy()for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] += y[i, j]return x
利用同樣的方法,可以實現逐元素的乘法、減法等。在實踐中處理NumPy數組時,這些運算都是優化好的NumPy內置函數。這些函數將大量運算交給基礎線性代數程序集(Basic Linear Algebra Subprograms,BLAS)實現。BLAS是低層次(low-level)?、高度并行、高效的張量操作程序,通常用Fortran或C語言來實現。因此,在NumPy中可以直接進行下列逐元素運算,速度非常快。
import numpy as np
#逐元素加法
#z = x + y
# 逐元素relu
#z = np.maximum(z, 0.)
我們來看一下兩種方法運行時間的差別。
import timex = np.random.random((20, 100))
y = np.random.random((20, 100))
t0 = time.time()
for _ in range(1000):z = x + yz = np.maximum(z, 0.)
print("Took: {0:.2f} s".format(time.time() - t0))
只需要0.00秒。與之相對,前面手動編寫的簡單實現耗時長達0.77秒。
t0 = time.time()
for _ in range(1000):z = naive_add(x, y)z = naive_relu(z)
print("Took: {0:.2f} s".format(time.time() - t0))
同樣,在GPU上運行TensorFlow代碼,逐元素運算都是通過完全向量化的CUDA來完成的,可以最大限度地利用高度并行的GPU芯片架構。
廣播
naive_add的簡單實現僅支持兩個形狀相同的2階張量相加,但在Dense層中,我們將一個2階張量與一個向量相加。如果將兩個形狀不同的張量相加,會發生什么?在沒有歧義且可行的情況下,較小的張量會被廣播(broadcast)?,以匹配較大張量的形狀。廣播包含以下兩步。(1)向較小張量添加軸[叫作廣播軸(broadcast axis)?]?,使其ndim與較大張量相同。(2)將較小張量沿著新軸重復,使其形狀與較大張量相同。我們來看一個具體的例子。假設X的形狀是(32, 10),y的形狀是(10,)。
import numpy as np
#X是一個形狀為(32, 10)的隨機矩陣
X = np.random.random((32, 10))
#y是一個形狀為(10,)的隨機向量
y = np.random.random((10,))
首先,我們向y添加第1個軸(空的)?,這樣y的形狀變為(1, 10)。
#現在y的形狀變為(1, 10)
y = np.expand_dims(y, axis=0)
然后,我們將y沿著這個新軸重復32次,這樣得到的張量Y的形狀為(32, 10),并且Y[i,:] == y for i in range(0, 32)
#將y沿著軸0重復32次后得到Y,其形狀為(32, 10)
Y = np.concatenate([y] * 32, axis=0)
現在,我們可以將X和Y相加,因為它們的形狀相同。在實際的實現過程中并不會創建新的2階張量,因為那樣做非常低效。重復操作完全是虛擬的,它只出現在算法中,而沒有出現在內存中。但想象將向量沿著新軸重復10次,是一種很有用的思維模型。下面是一種簡單實現。
def naive_add_matrix_and_vector(x, y):#x是一個2階NumPy張量assert len(x.shape) == 2 # y是一個NumPy向量assert len(y.shape) == 1 assert x.shape[1] == y.shape[0]#避免覆蓋輸入張量x = x.copy() for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] += y[j]return x
如果一個張量的形狀是(a, b, …, n, n+1, …, m),另一個張量的形狀是(n, n+1, …, m),那么通常可以利用廣播對這兩個張量做逐元素運算。廣播會自動應用于從a到n-1的軸。下面這個例子利用廣播對兩個形狀不同的張量做逐元素maximum運算。
import numpy as np
#x是一個形狀為(64, 3, 32, 10)的隨機張量
x = np.random.random((64, 3, 32, 10))
#y是一個形狀為(32, 10)的隨機張量
y = np.random.random((32, 10))
#輸出z的形狀為(64, 3, 32, 10),與x相同
z = np.maximum(x, y)
本文代碼匯總:
def naive_add(x, y):#x和y是2階NumPy張量assert len(x.shape) == 2assert x.shape == y.shape#避免覆蓋輸入張量x = x.copy()for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] += y[i, j]return ximport numpy as np
#逐元素加法
#z = x + y
# 逐元素relu
#z = np.maximum(z, 0.)import timex = np.random.random((20, 100))
y = np.random.random((20, 100))
t0 = time.time()
for _ in range(1000):z = x + yz = np.maximum(z, 0.)
print("Took: {0:.2f} s".format(time.time() - t0))t0 = time.time()
for _ in range(1000):z = naive_add(x, y)z = naive_relu(z)
print("Took: {0:.2f} s".format(time.time() - t0))import numpy as np
#X是一個形狀為(32, 10)的隨機矩陣
X = np.random.random((32, 10))
#y是一個形狀為(10,)的隨機向量
y = np.random.random((10,))#現在y的形狀變為(1, 10)
y = np.expand_dims(y, axis=0)#將y沿著軸0重復32次后得到Y,其形狀為(32, 10)
Y = np.concatenate([y] * 32, axis=0)def naive_add_matrix_and_vector(x, y):#x是一個2階NumPy張量assert len(x.shape) == 2# y是一個NumPy向量assert len(y.shape) == 1assert x.shape[1] == y.shape[0]#避免覆蓋輸入張量x = x.copy()for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] += y[j]return ximport numpy as np
#x是一個形狀為(64, 3, 32, 10)的隨機張量
x = np.random.random((64, 3, 32, 10))
#y是一個形狀為(32, 10)的隨機張量
y = np.random.random((32, 10))
#輸出z的形狀為(64, 3, 32, 10),與x相同
z = np.maximum(x, y)