深度學習系列 Part(3)
這是《GPU學習深度學習》系列文章的第三篇,主要是接著上一講提到的如何自己構建深度神經網絡框架中的功能模塊,進一步詳細介紹 Tensorflow 中 Keras 工具包提供的幾種深度神經網絡模塊。本系列文章主要介紹如何使用 騰訊雲GPU服務器 進行深度學習運算,前面主要介紹原理部分,後期則以實踐為主。
往期內容:
- GPU 學習深度學習系列Part 1:傳統機器學習的回顧
- GPU 學習深度學習系列Part 2:Tensorflow 簡明原理
上一講中,我們用最簡單的代碼,實現了最簡單的深度學習框架,然後進一步的實現了 Input
Linear
Sigmoid
Tanh
以及 MSE
這幾個模塊,並且用這幾個模塊搭建了一個最簡單的兩層深度學習網絡。
當然,我們沒有必要自己親自關註這些底層的部分,接下來的內容,我們將基於現在最火的深度學習框架 Tensorflow
,這裏以 r1.1 版本為例,詳細介紹一下更多的模塊的原理,談一談怎麽使用這些零件搭建深度學習網絡。在 r1.1版本的 Tensorflow
中,已經集成了以前的 Keras 模塊,使得搭建基本的 Tensorflow
模塊更加簡單、方便。
我們可以簡單的將深度神經網絡的模塊,分成以下的三個部分,即深度神經網絡上遊的基於生成器的 輸入模塊,深度神經網絡本身,以及深度神經網絡下遊基於批量梯度下降算法的 凸優化模塊:
- 批量輸入模塊
- 各種深度學習零件搭建的深度神經網絡
- 凸優化模塊
其中,搭建深度神經網絡的零件又可以分成以下類別:
- list text here各種深度學習零件搭建的深度神經網絡
- list text here常用層
- Dense
- Activation
- Dropout
- Flatten
- 卷積層
- Conv2D
- Cropping2D
- ZeroPadding2D
- 池化層
- MaxPooling2D
- AveragePooling2D
- GlobalAveragePooling2D
- 正則化層
- BatchNormalization
- 反卷積層(Keras中在卷積層部分)
- UpSampling2D
- list text here常用層
需要強調一下,這些層與之前一樣,都 同時包括了正向傳播、反向傳播兩條通路。我們這裏只介紹比較好理解的正向傳播過程,基於其導數的反向過程同樣也是存在的,其代碼已經包括在 Tensorflow 的框架中對應的模塊裏,可以直接使用。
當然還有更多的零件,具體可以去keras文檔中參閱。
接下來的部分,我們將首先介紹這些深度神經網絡的零件,然後再分別介紹上遊的批量輸入模塊,以及下遊的凸優化模塊。
1. 深度神經網絡的基本零件
1.1 常用層:
1.1.1. Dense
Dense 層,就是我們上一篇文章裏提到的 Linear
層,即 y=wx+b ,計算乘法以及加法。
1.1.2. Activation
Activation 層在我們上一篇文章中,同樣出現過,即 Tanh
層以及Sigmoid
層,他們都是 Activation 層的一種。當然 Activation 不止有這兩種形式,比如有:
圖片來源 激活函數與caffe及參數
這其中 relu
層可能是深度學習時代最重要的一種激發函數,在2011年首次被提出。由公式可見,relu
相比早期的 tanh
與 sigmoid
函數, relu
有兩個重要的特點,其一是在較小處都是0(sigmoid,relu
)或者-5(tanh
),但是較大值relu
函數沒有取值上限。其次是relu
層在0除不可導,是一個非線性的函數:
即 y=x*(x>0)
對其求導,其結果是:
1.1.3. Dropout
Dropout
層,指的是在訓練過程中,每次更新參數時將會隨機斷開一定百分比(rate)的輸入神經元,這種方式可以用於防止過擬合。
圖片來源Dropout: A Simple Way to Prevent Neural Networks from Overfitting
1.1.4. Flatten
Flatten
層,指的是將高維的張量(Tensor, 如二維的矩陣、三維的3D矩陣等)變成一個一維張量(向量)。Flatten
層通常位於連接深度神經網絡的 卷積層部分 以及 全連接層部分。
1.2 卷積層
提到卷積層,就必須講一下卷積神經網絡。我們在第一講的最後部分提高深度學習模型預測的準確性部分,提了一句 “使用更復雜的深度學習網絡,在圖片中挖出數以百萬計的特征”。這種“更復雜的神經網絡”,指的就是卷積神經網絡。卷積神經網絡相比之前基於 Dense 層建立的神經網絡,有所區別之處在於,卷積神經網絡可以使用更少的參數,對局部特征有更好的理解。
1.2.1. Conv2D
我們這裏以2D 的卷積神經網絡為例,來逐一介紹卷積神經網絡中的重要函數。比如我們使用一個形狀如下的卷積核:
1 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 1 |
掃描這樣一個二維矩陣,比如一張圖片:
1 | 1 | 1 | 0 | 0 |
0 | 1 | 1 | 1 | 0 |
0 | 0 | 1 | 1 | 1 |
0 | 0 | 1 | 1 | 0 |
0 | 1 | 1 | 0 | 0 |
其過程與結果會是這樣:
當然,這裏很重要的一點,就是正如我們上一講提到的, Linear
函數的 w
, b
兩個參數都是變量,會在不斷的訓練中,不斷學習更新。卷積神經網絡中,卷積核其實也是一個變量。這裏的
1 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 1 |
可能只是初始值,也可能是某一次叠代時選用的值。隨著模型的不斷訓練,將會不斷的更新成其他值,結果也將會是一個不規則的形狀。具體的更新方式,同上一講提到的 Linear
等函數模塊相同,卷積層也有反向傳播函數,基於反向函數計算梯度,即可用來更新現有的卷積層的值,具體方法可參考CNN的反向傳導練習。舉一個經過多次學習得到的卷積神經網絡的卷積核為例:
圖片來源Alexnet 2012年文章
清楚了其原理,卷積神經網絡還需要再理解幾個輸入參數:
Conv2D(filters, kernel_size, strides=(1, 1), padding=‘valid‘, ...)
其中:
-
filters
指的是輸出的卷積層的層數。如上面的動圖,只輸出了一個卷積層,filters = 1,而實際運用過程中,一次會輸出很多卷積層。 -
kernel_size
指的是卷積層的大小,是一個 二維數組,分別代表卷積層有幾行、幾列。 -
strides
指的是卷積核在輸入層掃描時,在 x,y 兩個方向,每間隔多長掃執行一次掃描。 -
padding
這裏指的是是否掃描邊緣。如果是valid
,則僅僅掃描已知的矩陣,即忽略邊緣。而如果是same
,則將根據情況在邊緣補上0,並且掃描邊緣,使得輸出的大小等於 input_size / strides。
1.2.2. Cropping2D
這裏 Cropping2D
就比較好理解了,就是特地選取輸入圖像的某一個固定的小部分。比如車載攝像頭檢測路面的馬路線時,攝像頭上半部分拍到的天空就可以被 Cropping2D
函數直接切掉忽略不計。
圖片來源Udacity自動駕駛課程
1.2.3. ZeroPadding2D
1.2.1部分提到輸入參數時,提到 padding
參數如果是same
,掃描圖像邊緣時會補上0,確保輸出數量等於 input / strides。這裏 ZeroPadding2D 的作用,就是在圖像外層邊緣補上幾層0。如下圖,就是對原本 32x32x3 的圖片進行 ZeroPadding2D(padding=(2, 2))
操作後的結果:
圖片來源Adit Deshpande博客
1.3. 池化層
1.3.1. MaxPooling2D
可能大家在上一部分會意識到一點,就是通過與一個相同的、大小為11x11的卷積核做卷積操作,每次移動步長為1,則相鄰的結果會非常接近,正是由於結果接近,有很多信息是冗余的。
因此,MaxPooling
就是一種減少模型冗余程度的方法。以 2x 2 MaxPooling
為例。圖中如果是一個 4x4 的輸入矩陣,則這個 4x4 的矩陣,會被分割成由兩行、兩列組成的 2x2 子矩陣,然後每個 2x2 子矩陣取一個最大值作為代表,由此得到一個兩行、兩列的結果:
圖片來源 斯坦福CS231課程
1.3.2. AveragePooling2D
AveragePooling
與 MaxPooling
類似,不同的是一個取最大值,一個是平均值。如果上圖的 MaxPooling
換成 AveragePooling2D
,結果會是:
3.25 | 5.25 |
2 | 2 |
1.3.3. GlobalAveragePooling2D
GlobalAveragePooling
,其實指的是,之前舉例 MaxPooling
提到的 2x2 Pooling,對子矩陣分別平均,變成了對整個input 矩陣求平均值。
這個理念其實和池化層關系並不十分緊密,因為他扔掉的信息有點過多了,通常只會出現在卷積神經網絡的最後一層,通常是作為早期深度神經網絡 Flatten
層 + Dense
層結構的替代品:
前面提到過 Flatten
層通常位於連接深度神經網絡的 卷積層部分 以及 全連接層部分,但是這個連接有一個大問題,就是如果是一個 1k x 1k 的全連接層,一下就多出來了百萬參數,而這些參數實際用處相比卷積層並不高。造成的結果就是,早期的深度神經網絡占據內存的大小,反而要高於後期表現更好的神經網絡:
圖片來源Training ENet on ImageNet
更重要的是,全連接層由於參數偏多,更容易造成 過擬合——前文提到的 Dropout
層就是為了避免過擬合的一種策略,進而由於過擬合,妨礙整個網絡的泛化能力。於是就有了用更多的卷積層提取特征,然後不去 Flatten
這些 k x k 大小卷積層,直接把這些 k x k 大小卷積層變成一個值,作為特征,連接分類標簽。
1.4. 正則化層
除了之前提到的 Dropout
策略,以及用 GlobalAveragePooling
取代全連接層的策略,還有一種方法可以降低網絡的過擬合,就是正則化,這裏著重介紹下 BatchNormalization
。
1.4.1. BatchNormalization
BatchNormalization
確實適合降低過擬合,但他提出的本意,是為了加速神經網絡訓練的收斂速度。比如我們進行最優值搜索時,我們不清楚最優值位於哪裏,可能是上千、上萬,也可能是個負數。這種不確定性,會造成搜索時間的浪費。
BatchNormalization
就是一種將需要進行最優值搜索數據,轉換成標準正態分布,這樣optimizer
就可以加速優化:
輸入:一批input 數據: B
期望輸出: β,γ
具體如何實現正向傳播和反向傳播,可以看這裏。
1.5. 反卷積層
最後再談一談和圖像分割相關的反卷積層。
之前在 1.2 介紹了卷積層,在 1.3 介紹了池化層。相信讀者大概有了一種感覺,就是卷積、池化其實都是在對一片區域計算平均值、最大值,進而忽略這部分信息。換言之,卷積+池化,就是對輸入圖片打馬賽克。
但是馬賽克是否有用?我們知道老司機可以做到“圖中有碼,心中無碼”,就是說,圖片即便是打了馬賽克、忽略了細節,我們仍然可以大概猜出圖片的內容。這個過程,就有點反卷積的意思了。
利用反卷積層,可以基於 卷積層+全連接層結構,構建新的、用於圖像分割的神經網絡 結構。這種結構不限制輸入圖片的大小,
圖片來源:Fully Convolutional Networks for Semantic Segmentation
1.5.1. UpSampling2D
上圖在最後階段使用了 Upsampling
模塊,這個同樣在 Tensorflow 的 keras 模塊可以找到。用法和 MaxPooling2D
基本相反,比如:
UpSampling2D(size=(2, 2))
就相當於將輸入圖片的長寬各拉伸一倍,整個圖片被放大了。
當然,Upsampling
實際上未必在網絡的最後才使用,我們後面文章提到的 unet 網絡結構,每一次進行卷積操作縮小圖片大小,後期都會使用 Upsampling
函數增大圖片。
圖片來源 U-Net: Convolutional Networks for Biomedical Image Segmentation
2. 深度神經網絡的上下遊結構
介紹完深度神經網絡的基本結構以後,讀者可能已經意識到了,1.3.3 部分提到的深度神經網絡的參數大小動輒幾十M、上百M,如何合理訓練這些參數是個大問題。這就需要在這個網絡的上下遊,合理處理這個問題。
海量參數背後的意義是,深度神經網絡可以獲取海量的特征。第一講中提到過,深度學習是脫胎於傳統機器學習的,兩者之間的區別,就是深度學習可以在圖像處理中,自動進行特征工程,如我們第一講所言:
想讓計算機幫忙挖掘、標註這些更多的特征,這就離不開 更優化的模型 了。事實上,這幾年深度學習領域的新進展,就是以這個想法為基礎產生的。我們可以使用更復雜的深度學習網絡,在圖片中挖出數以百萬計的特征。
這時候,問題也就來了。機器學習過程中,是需要一個輸入文件的。這個輸入文件的行、列,分別指代樣本名稱以及特征名稱。如果是進行百萬張圖片的分類,每個圖片都有數以百萬計的特征,我們將拿到一個 百萬樣本 x 百萬特征 的巨型矩陣。傳統的機器學習方法拿到這個矩陣時,受限於計算機內存大小的限制,通常是無從下手的。也就是說,傳統機器學習方法,除了在多數情況下不會自動產生這麽多的特征以外,模型的訓練也會是一個大問題。
深度學習算法為了實現對這一量級數據的計算,做了以下算法以及工程方面的創新:
- 將全部所有數據按照樣本拆分成若幹批次,每個批次大小通常在十幾個到100多個樣本之間。(詳見下文 輸入模塊)
- 將產生的批次逐一參與訓練,更新參數。(詳見下文 凸優化模塊)
- 使用 GPU 等計算卡代替 CPU,加速並行計算速度。
這就有點《愚公移山》的意思了。我們可以把訓練深度神經網絡的訓練任務,想象成是搬走一座大山。成語故事中,愚公的辦法是既然沒有辦法直接把山搬走,那就子子孫孫,每人每天搬幾筐土走,山就會越來越矮,總有一天可以搬完——這種任務分解方式就如同深度學習算法的分批訓練方式。同時,隨著科技進步,可能搬著搬著就用翻鬥車甚至是高達來代替背筐,就相當於是用 GPU 等高並行計算卡代替了 CPU。
於是,我們這裏將主要提到的上遊輸入模塊,以及下遊凸優化模塊,實際上就是在說如何使用愚公移山的策略,用 少量多次 的方法,去“搬”深度神經網絡背後大規模計算量這座大山。
2.2. 輸入模塊
這一部分實際是在說,當我們有成千上萬的圖片,存在硬盤中時,如何實現一個函數,每調用一次,就會讀取指定張數的圖片(以n=32為例),將其轉化成矩陣,返回輸出。
有 Python 基礎的人可能意識到了,這裏可能是使用了 Python 的 生成器 特性。其具體作用如廖雪峰博客所言:
創建一個包含100萬個元素的 list,不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那後面絕大多數元素占用的空間都白白浪費了。 所以,如果 list 元素可以按照某種算法推算出來,那我們是否可以在循環的過程中不斷推算出後續的元素呢?這樣就不必創建完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱為生成器:generator。
其關鍵的寫法,是把傳統函數的 return
換成 yield
:
def generator(samples, batch_size=32):
num_samples = len(samples)
while 1:
sklearn.utils.shuffle(samples)
for offset in range(0, num_samples, batch_size):
batch_samples = samples.iloc[offset:offset+batch_size]
images = []
angles = []
for idx in range(batch_samples.shape[0]):
name = ‘./data/‘+batch_samples.iloc[idx][‘center‘]
center_image = cv2.cvtColor( cv2.imread(name), cv2.COLOR_BGR2RGB )
center_angle = float(batch_samples.iloc[idx][‘dir‘])
images.append(center_image)
angles.append(center_angle)
# trim image to only see section with road
X_train = np.array(images)
y_train = np.array(angles)
yield sklearn.utils.shuffle(X_train, y_train)
然後調用時,使用
next(generator)
即可一次返回 32 張圖像以及對應的標註信息。
當然,keras
同樣提供了這一模塊,ImageDataGenerator,並且還是加強版,可以對圖片進行 增強處理(data argument)(如旋轉、反轉、白化、截取等)。圖片的增強處理在樣本數量不多時增加樣本量——因為如果圖中是一只貓,旋轉、反轉、顏色調整之後,這張圖片可能會不太相同,但它仍然是一只貓:
datagen = ImageDataGenerator(
featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False,
width_shift_range=0.1,
height_shift_range=0.1,
horizontal_flip=True,
vertical_flip=False)
# compute quantities required for featurewise normalization
datagen.fit(X_train)
2.3 凸優化模塊
這一部分談的是,如何使用基於批量梯度下降算法的凸優化模塊,優化模型參數。
前面提到,深度學習的“梯度下降”計算,可以理解成搬走一座大山,而“批量梯度下降”,則是一群人拿著土筐,一點一點把山上的土給搬下山。那麽這一點具體應該如何實現呢?其實在第二講,我們就實現了一個隨機批量梯度下降(Stochastic gradient descent, SGD),這裏再回顧一下:
def sgd_update(trainables, learning_rate=1e-2):
for t in trainables:
t.value = t.value - learning_rate * t.gradients[t]
#訓練神經網絡的過程
for i in range(epochs):
loss = 0
for j in range(steps_per_epoch):
# 輸入模塊,將全部所有數據按照樣本拆分成若幹批次
X_batch, y_batch = resample(X_, y_, n_samples=batch_size)
# 各種深度學習零件搭建的深度神經網絡
forward_and_backward(graph)
# 凸優化模塊
sgd_update(trainables, 0.1)
當然,SGD 其實並不是一個很好的方法,有很多改進版本,可以用下面這張gif圖概況:
Keras 裏,可以直接使用 SGD, Adagrad, Adadelta, RMSProp 以及 Adam 等模塊。其實在優化過程中,直接使用 Adam 默認參數,基本就可以得到最優的結果:
from keras.optimizers import Adam
adam = Adam()
model.compile(loss=‘categorical_crossentropy‘,
optimizer=adam,
metrics=[‘accuracy‘])
3. 實戰項目——CIFAR-10 圖像分類
最後我們用一個keras 中的官方示例,來結束本講。
首先做一些前期準備:
# 初始化
from __future__ import print_function
import numpy as np
from keras.callbacks import TensorBoard
from keras.models import Sequential
from keras.optimizers import Adam
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPool2D
from keras.utils import np_utils
from keras import backend as K
from keras.callbacks import ModelCheckpoint
from keras.preprocessing.image import ImageDataGenerator
from keras.datasets import cifar10
from keras.backend.tensorflow_backend import set_session
import tensorflow as tf
config = tf.ConfigProto()
config.gpu_options.allow_growth=True
set_session(tf.Session(config=config))
np.random.seed(0)
print("Initialized!")
# 定義變量
batch_size = 32
nb_classes = 10
nb_epoch = 50
img_rows, img_cols = 32, 32
nb_filters = [32, 32, 64, 64]
pool_size = (2, 2)
kernel_size = (3, 3)
#
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
X_train = X_train.astype("float32") / 255
X_test = X_test.astype("float32") / 255
y_train = y_train
y_test = y_test
input_shape = (img_rows, img_cols, 3)
Y_train = np_utils.to_categorical(y_train, nb_classes)
Y_test = np_utils.to_categorical(y_test, nb_classes)
上遊部分, 基於生成器的批量生成輸入模塊:
datagen = ImageDataGenerator(
featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False,
rotation_range=0,
width_shift_range=0.1,
height_shift_range=0.1,
horizontal_flip=True,
vertical_flip=False)
datagen.fit(X_train)
核心部分,用各種零件搭建深度神經網絡:
model = Sequential()
model.add(Conv2D(nb_filters[0], kernel_size, padding=‘same‘,input_shape=X_train.shape[1:]))
model.add(Activation(‘relu‘))
model.add(Conv2D(nb_filters[1], kernel_size))
model.add(Activation(‘relu‘))
model.add(MaxPool2D(pool_size=pool_size))
model.add(Dropout(0.25))
model.add(Conv2D(nb_filters[2], kernel_size, padding=‘same‘))
model.add(Activation(‘relu‘))
model.add(Conv2D(nb_filters[3], kernel_size))
model.add(Activation(‘relu‘))
model.add(MaxPool2D(pool_size=pool_size))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(512))
model.add(Activation(‘relu‘))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation(‘softmax‘))
構建的模型如下:
下遊部分,使用凸優化模塊:
adam = Adam(lr=0.0001)
model.compile(loss=‘categorical_crossentropy‘,
optimizer=adam,
metrics=[‘accuracy‘])
最後,開始訓練模型,並且評估模型準確性:
#訓練模型
best_model = ModelCheckpoint("cifar10_best.h5", monitor=‘val_loss‘, verbose=0, save_best_only=True)
tb = TensorBoard(log_dir="./logs")
model.fit_generator(datagen.flow(X_train, Y_train, batch_size=batch_size),
steps_per_epoch=X_train.shape[0] // batch_size,
epochs=nb_epoch, verbose=1,
validation_data=(X_test, Y_test), callbacks=[best_model,tb])
# 模型評分
score = model.evaluate(X_test, Y_test, verbose=0)
# 輸出結果
print(‘Test score:‘, score[0])
print("Accuracy: %.2f%%" % (score[1]*100))
print("Compiled!")
以上代碼本人使用 Pascal TitanX 執行,50個 epoch 中,每個 epoch 用時 12s 左右,總計用時在十五分鐘以內,約25 epoch 後,驗證集的準確率數會逐步收斂在0.8左右。
本篇是繼上一篇“如何造輪子”的主題的一個延續,介紹了 Tensorflow 中 Keras 工具包有哪些現成的輪子可以拿來直接用。
深度學習系列 Part(3)