本文爲《深度學習入門 基於Python的理論與實現》的部分讀書筆記
代碼以及圖片均參考此書
目錄
加深層的動機
感受野(receptive field)
參考:https://blog.csdn.net/program_developer/article/details/80958716
https://blog.csdn.net/u010725283/article/details/78593410/
理解感受野是理解卷積神經網絡工作的基礎,尤其是對於使用Anchor作爲強先驗區域的物體檢測算法,如Faster RCNN和SSD。如何設置Anchor的大小,Anchor應該對應特徵圖的哪一層,都應該考慮感受野。通常來講,Anchor的大小應該與感受野相匹配,尤其是有效感受野,過大或過小都不好。
定義
感受野(Receptive Field)的定義是卷積神經網絡每一層輸出的特徵圖(feature map)上的像素點在輸入圖片上映射的區域大小。再通俗點的解釋是,特徵圖上的一個點對應輸入圖上的區域
由上圖可以看出,經過幾個卷積層之後,特徵圖的大小逐漸變小,一個特徵所表示的信息量越來越多
感受野的計算
注意上圖中Layer1到Layer2的過程中,padding=1,這裏的1是指特徵所佔的區域,即一個特徵所佔的感受野,所以Conv2這張圖上纔會在外面加上三個格。stride=2也是同樣的道理,2表示跨過兩個特徵。
從上圖可以整理出以下公式:
其中爲第層的感受野大小,爲卷積核大小,爲第層中兩個特徵相隔的距離
設爲第層的感受野大小,爲卷積核大小,爲第層的卷積核步長
整理一下就可以得到感受野的計算公式:
由上述公式計算的感受野通常很大,而因爲輸入層中邊緣點的使用次數明顯比中間點少,因此做出的貢獻不同。經過多層的卷積堆疊之後,輸入層對於特徵圖點做出的貢獻分佈呈高斯分佈形狀。
因此,實際的有效感受野 Effective Receptive Field 往往小於理論感受野。
減少網絡的參數數量
說得詳細一點,就是與沒有加深層的網絡相比,加深了層的網絡可以用更少的參數達到同等水平(或者更強)的表現力。這一點結合卷積運算中的濾波器大小來思考就好理解了。
使用 的濾波器每個輸出節點都是從輸入數據的某個 的區域算出來的。
由上圖可以看出,重複兩次 的卷積層後,輸出數據同樣是觀察了輸入數據的某個 的區域後計算出來的
也就是說,一次 的卷積運算的區域可以看作由兩次 的卷積運算抵充,而且參數數量更少了。這個參數數量之差會隨着層的加深而變大。比如,重複三次 的卷積運算時,參數的數量總共是27。而爲了用一次卷積運算“觀察”與之相同的區域,需要一個 的濾波器,此時的參數數量是49。
由此可以看出疊加小型濾波器來加深網絡的好處:減少參數的數量,擴大感受野(receptive field,給神經元施加變化的某個局部空間區域)。並且,通過疊加層,將ReLU 等激活函數夾在卷積層的中間,進一步提高了網絡的表現力。這是因爲向網絡添加了基於激活函數的“非線性”表現力,通過非線性函數的疊加,可以表現更加複雜的東西。
使學習更加高效
卷積層中,神經元會對邊緣等簡單的形狀有響應,隨着層的加深,開始對紋理、物體部件等更加複雜的東西有響應。CNN的卷積層會分層次地提取信息,從而高效地進行學習。
例如,考慮一下 “狗”的識別問題。要用淺層網絡解決這個問題的話,卷積層需要一下子理解很多“狗”的特徵。“狗”有各種各樣的種類,根據拍攝環境的不同,外觀變化也很大。因此,要理解“狗”的特徵,需要大量富有差異性的學習數據,而這會導致學習需要花費很多時間。不過,通過加深網絡,就可以分層次地分解需要學習的問題。因此,各層需要學習的問題就變成了更簡單的問題。比如,最開始的層只要專注於學習邊緣就好,這樣一來,只需用較少的學習數據就可以高效地進行學習。這是爲什麼呢?因爲和印有“狗”的照片相比,包含邊緣的圖像數量衆多,並且邊緣的模式比“狗”的模式結構更簡單。
深度學習的高速化
基於GPU的高速化
百度的AI Studio現在每天都可以白嫖12小時的GPU使用時間 😏真香
GPU原本是作爲圖像專用的顯卡使用的,但最近不僅用於圖像處理,也用於通用的數值計算。由於GPU可以高速地進行並行數值計算,因此GPU計算的目標就是將這種壓倒性的計算能力用於各種用途。
深度學習中需要進行大量的乘積累加運算(或者大型矩陣的乘積運算)。這種大量的並行運算正是GPU所擅長的(反過來說,CPU比較擅長連續的、複雜的計算)。因此,與使用單個CPU相比,使用GPU進行深度學習的運算可以達到驚人的高速化。
GPU主要由NVIDIA和AMD兩家公司提供。雖然兩家的GPU都可以用於通用的數值計算,但與深度學習比較“親近”的是NVIDIA的GPU。實際上,大多數深度學習框架只受益於NVIDIA的GPU。這是因爲深度學習的框架中使用了NVIDIA提供的CUDA這個面向GPU計算的綜合開發環境。
下圖中中出現的cuDNN是在CUDA上運行的庫,它裏面實現了爲深度學習最優化過的函數等。
- 同時要注意Numpy這個庫是不會主動檢測並使用GPU的,如果要使用GPU進行運算,可以使用它的替代品minpy或者直接使用其他的深度學習框架。因爲之前的實現一直都使用的是Numpy,因此只能用CPU進行網絡訓練
分佈式學習
爲了進一步提高深度學習所需的計算的速度,可以考慮在多個GPU或者多臺機器上進行分佈式計算。
“如何進行分佈式計算”是一個非常難的課題。它包含了機器間的通信、數據的同步等多個無法輕易解決的問題。可以將這些難題都交給TensorFlow 等優秀的框架。
運算精度的位數縮減
在深度學習的高速化中,除了計算量之外,內存容量、總線帶寬等也有可能成爲瓶頸。關於內存容量,需要考慮將大量的權重參數或中間數據放在內存中。關於總線帶寬,當流經GPU(或者CPU)總線的數據超過某個限制時,就會成爲瓶頸。考慮到這些情況,我們希望儘可能減少流經網絡的數據的位數。
深度學習並不那麼需要數值精度的位數。這是神經網絡的一個重要性質。這個性質是基於神經網絡的健壯性而產生的。這裏所說的健壯性是指,比如,即便輸入圖像附有一些小的噪聲,輸出結果也仍然保持不變。可以認爲,正是因爲有了這個健壯性,流經網絡的數據即便有所“劣化”,對輸出結果的影響也較小。
根據以往的實驗結果,在深度學習中,即便是16位的半精度浮點數(half float),也可以順利地進行學習。實際上,NVIDIA的下一代GPU框架Pascal也支持半精度浮點數的運算,由此可以認爲今後半精度浮點數將被作爲標準使用。
以往的深度學習的實現中並沒有注意數值的精度,不過Python 中一般使用64 位的浮點數。NumPy中提供了16 位的半精度浮點數類型(不過,只有16位類型的存儲,運算本身不用16位進行),即便使用NumPy的半精度浮點數,識別精度也不會下降。
特別是在面向嵌入式應用程序中使用深度學習時,位數縮減非常重要。
實現深層CNN
這個網絡結構參考了VGG。卷積層全部使用 的小型濾波器,(有一層的卷積層濾波器以保證輸入數據在經過最後一層池化層之前長寬均爲偶數),使輸入數據每經過一個卷積層,長寬不變而通道數變大(通道數從前面的層開始按順序以16、16、32、32、64、64的方式增加)。池化層用於逐漸減小中間數據的空間大小,使用 的濾波器,,使輸入數據每經過一個池化層,通道數不變而長寬均減半。同時也加入了Dropout層,用於抑制過擬合。
import sys
file_path = __file__.replace('\\', '/')
dir_path = file_path[: file_path.rfind('/')] # 當前文件夾的路徑
pardir_path = dir_path[: dir_path.rfind('/')]
sys.path.append(pardir_path) # 添加上上級目錄到python模塊搜索路徑
import numpy as np
from func.gradient import numerical_gradient, gradient_check
from layer.common import *
from collections import OrderedDict
import os
import pickle
class DeepConvNet:
"""
網絡結構如下所示
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
affine - relu - dropout - affine - dropout - softmax
輸入數據(1,28,28)的形狀變化
(16, 28, 28) - (16, 28, 28) - (16, 14, 14) -
(32, 14, 14) - (32, 16, 16) - (32, 8, 8) -
(64, 8, 8) - (64, 8, 8) - (64, 4, 4) -
(50) - (10)
如果要改變卷積核的參數或者全連接層中的神經元個數的話,則不僅需要調整初始化方法中的參數,還要在代碼中手動更改初始化權重的標準差以及第一個隱藏層的輸入數據形狀
"""
def __init__(self, input_dim=(1, 28, 28),
conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
hidden_size=50, output_size=10, dropout_ratio=0.5,
pretrain_flag=True, pkl_file_name=None):
self.pkl_file_name = pkl_file_name
if pretrain_flag == 1 and os.path.exists(self.pkl_file_name):
self.load_pretrain_model()
else:
# 初始化權重===========
# 各層的神經元平均與前一層的幾個神經元有連接
# conv1 conv2 conv3 conv4 conv5 conv6 affine1 affine2
pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
wight_init_scales = np.sqrt(2.0 / pre_node_nums) # 使用ReLU的情況下推薦的初始值
self.params = {}
pre_channel_num = input_dim[0]
for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
self.params['W' + str(idx+1)] = wight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
pre_channel_num = conv_param['filter_num']
self.params['W7'] = wight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
self.params['b7'] = np.zeros(hidden_size)
self.params['W8'] = wight_init_scales[7] * np.random.randn(hidden_size, output_size)
self.params['b8'] = np.zeros(output_size)
# 生成層===========
self.layers = []
self.layers.append(Convolution(self.params['W1'], self.params['b1'],
conv_param_1['stride'], conv_param_1['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W2'], self.params['b2'],
conv_param_2['stride'], conv_param_2['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Convolution(self.params['W3'], self.params['b3'],
conv_param_3['stride'], conv_param_3['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W4'], self.params['b4'],
conv_param_4['stride'], conv_param_4['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Convolution(self.params['W5'], self.params['b5'],
conv_param_5['stride'], conv_param_5['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W6'], self.params['b6'],
conv_param_6['stride'], conv_param_6['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Affine(self.params['W7'], self.params['b7']))
self.layers.append(Relu())
self.layers.append(Dropout(dropout_ratio))
self.layers.append(Affine(self.params['W8'], self.params['b8']))
self.layers.append(Dropout(dropout_ratio))
self.last_layer = SoftmaxWithLoss()
def load_pretrain_model(self):
with open(self.pkl_file_name, 'rb') as f:
model = pickle.load(f)
for key in ('params', 'layers', 'last_layer'):
exec('self.' + key + '=model.' + key)
print('params loaded!')
def predict(self, x, train_flg=False):
for layer in self.layers:
if isinstance(layer, Dropout):
x = layer.forward(x, train_flg)
else:
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x, train_flg=True)
return self.last_layer.forward(y, t)
def accuracy(self, x, t, batch_size=100):
if t.ndim != 1:
t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx, train_flg=False)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
# 設定
grads = {}
for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
grads['W' + str(i+1)] = self.layers[layer_idx].dW
grads['b' + str(i+1)] = self.layers[layer_idx].db
return grads
if __name__ == '__main__':
from dataset.mnist import load_mnist
from trainer.trainer import Trainer
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=False, one_hot_label=True, shuffle_data=True)
# setting
train_flag = 1 # 進行訓練還是預測
gradcheck_flag = 0 # 對已訓練的網絡進行梯度檢驗
pkl_file_name = dir_path + '/deep_convnet.pkl'
fig_name = dir_path + '/deep_convnet.png'
net = DeepConvNet(input_dim=(1, 28, 28),
conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
hidden_size=50, output_size=10, dropout_ratio=0.5,
pretrain_flag=True, pkl_file_name=pkl_file_name)
trainer = Trainer(net, x_train, t_train, x_test, t_test,
epochs=5, mini_batch_size=128,
optimizer='Adam', optimizer_param={},
save_model_flag=True, pkl_file_name=pkl_file_name, plot_flag=True, fig_name=fig_name,
evaluate_sample_num_per_epoch=1000, verbose=True)
if gradcheck_flag == 1:
gradient_check(net, x_train[:2], t_train[:2])
if train_flag:
trainer.train()
else:
acc = net.accuracy(x_train, t_train)
print('accuracy:', acc)
訓練了3個epoch之後,精度成功突破了99%,沒有過擬合現象發生
=============== Final Test Accuracy ===============
test acc:0.9917
深度學習的應用案例(簡介)
物體識別
物體檢測
圖像分割
圖像分割是指在像素水平上對圖像進行分類。如下圖所示,使用以像素爲單位對各個對象分別着色的監督數據進行學習。然後,在推理時,對輸入圖像的所有像素進行分類。
要基於神經網絡進行圖像分割,最簡單的方法是以所有像素爲對象,對每個像素執行推理處理。比如,準備一個對某個矩形區域中心的像素進行分類的網絡,以所有像素爲對象執行推理處理。正如大家能想到的,這樣的方法需要按照像素數量進行相應次forward處理,因而需要耗費大量的時間(正確地說,卷積運算中會發生重複計算很多區域的無意義的計算)。爲了解決這個無意義的計算問題,有人提出了一個名爲FCN(Fully Convolutional Network)的方法。該方法通過一次forward處理,對所有像素進行分類
相對於一般的CNN包含全連接層,FCN將全連接層替換成發揮相同作用的卷積層。在物體識別中使用的網絡的全連接層中,中間數據的空間容量被作爲排成一列的節點進行處理,而只由卷積層構成的網絡中,空間容量可以保持原樣直到最後的輸出。
全連接層中,輸出和全部的輸入相連。使用卷積層也可以實現與此結構完全相同的連接。比如,針對輸入大小是32×10×10(通道數32、高10、長10)的數據的全連接層可以替換成濾波器大小爲32×10×10 的卷積層。如果全連接層的輸出節點數是100,那麼在卷積層準備100 個32×10×10 的濾波器就可以實現完全相同的處理。像這樣,全連接層可以替換成進行相同處理的卷積層。
如下圖所示,FCN的特徵在於最後導入了擴大空間大小的處理。基於這個處理,變小了的中間數據可以一下子擴大到和輸入圖像一樣的大小。FCN最後進行的擴大處理是基於雙線性插值法的擴大(雙線性插值擴大)。FCN中,這個雙線性插值擴大是通過去卷積(逆卷積運算)來實現的