用卷積神經(jīng)網(wǎng)絡(luò )檢測臉部關(guān)鍵點(diǎn)的教程(一)
這是一個(gè)手把手教你學(xué)習深度學(xué)校的教程。一步一步,我們將要嘗試去解決Kaggle challenge中的臉部關(guān)鍵點(diǎn)的檢測問(wèn)題。
本文引用地址:http://dyxdggzs.com/article/201710/365583.htm這份教程介紹了Lasagne,一個(gè)比較新的基于Python和Theano的神經(jīng)網(wǎng)絡(luò )庫。我們將用Lasagne去模擬一系列的神經(jīng)網(wǎng)絡(luò )結構,討論一下數據增強(data augmentaTIon)、流失(dropout)、結合動(dòng)量(momentum)和預先訓練(pre-training)。這里有很多方法可以將我們的結果改善不少。
我假設諸位已經(jīng)知道了一些關(guān)于神經(jīng)網(wǎng)絡(luò )的只是。所以我們就不介紹神經(jīng)網(wǎng)絡(luò )的背景知識了。這里也提供一些好的介紹神經(jīng)網(wǎng)絡(luò )的書(shū)籍和視頻,如Neural Networks and Deep Learning online book。Alec Radford的演講Deep Learning with Python’s Theano library也是一個(gè)快速介紹的好例子。以及ConvNetJS Browser Demos
預先準備
如果你只需要看懂的話(huà),則不需要自己寫(xiě)一個(gè)代碼然后去執行。這里提供一些安裝的教程給那些配置好CUDA的GPU并且想要運行試驗的那些人。
我假設你們已經(jīng)安裝了CUDA toolkit, Python 2.7.x, numpy, pandas, matplotlib, 和scikit-learn。安裝剩下的依賴(lài)包,比如Lasagne和Theano都可以運行下面的指令
pip install -r https://raw.githubusercontent.com/dnouri/kfkd-tutorial/master/requiremen...
注意,為了簡(jiǎn)潔起見(jiàn),我沒(méi)有在命令中創(chuàng )建虛擬環(huán)境,但是你需要的。
譯者:我是在windows10上面配置這個(gè)環(huán)境的,安裝anaconda(再用此環(huán)境安裝依賴(lài)包)、VS2013(不推薦2015)、CUDA工具即可。
如果一切都順利的話(huà),你將會(huì )在你的虛擬環(huán)境下的src/lasagne/examples/目錄中找到mnist.py并運行MNIST例子。這是一個(gè)對于神經(jīng)網(wǎng)絡(luò )的“Hello world”程序。數據中有十個(gè)分類(lèi),分別是0~9的數字,輸入時(shí)28&TImes;28的手寫(xiě)數字圖片。
cd src/lasagne/examples/
python mnist.py
此命令將在三十秒左右后開(kāi)始打印輸出。 這需要一段時(shí)間的原因是,Lasagne使用Theano做重型起重; Theano反過(guò)來(lái)是一個(gè)“優(yōu)化GPU元編程代碼生成面向數組的優(yōu)化Python數學(xué)編譯器”,它將生成需要在訓練發(fā)生前編譯的C代碼。 幸運的是,我們組需要在第一次運行時(shí)支付這個(gè)開(kāi)銷(xiāo)的價(jià)格。
譯者:如果沒(méi)有配置GPU,用的是CPU的話(huà),應該是不用這么久的編譯時(shí)間,但是執行時(shí)間有一些長(cháng)。如果用GPU,在第一次跑一些程序的時(shí)候,會(huì )有提示正在編譯的內容。
當訓練開(kāi)始的時(shí)候,你會(huì )看到
Epoch 1 of 500
training loss: 1.352731
validaTIon loss: 0.466565
validaTIon accuracy: 87.70 %
Epoch 2 of 500
training loss: 0.591704
validation loss: 0.326680
validation accuracy: 90.64 %
Epoch 3 of 500
training loss: 0.464022
validation loss: 0.275699
validation accuracy: 91.98 %
…
如果你讓訓練運行足夠長(cháng),你會(huì )注意到,在大約75代之后,它將達到大約98%的測試精度。
如果你用的是GPU,你想要讓Theano去使用它,你要在用戶(hù)的主文件夾下面創(chuàng )建一個(gè).theanorc文件。你需要根據自己安裝環(huán)境以及自己操作系統的配置使用不同的配置信息:
[global]
floatX = float32
device = gpu0
[lib]
cnmem = 1
譯者:這是我的配置文件。
[cuba]
root = C:Program FilesNVIDIA GPU Computing ToolkitCUDAv8.0
[global]
openmp = False
device = gpu
floatX = float32
allow_input_downcast = True
[nvcc]
fastmath = True
flags = -IC:Anaconda2libs
compiler_bindir = C:Program Files (x86)Microsoft Visual Studio 12.0VCbin
base_compiledir = path_to_a_directory_without_such_characters
[blas]
ldflags =
[gcc]
cxxflags = -IC:Anaconda2MinGW
數據
面部關(guān)鍵點(diǎn)檢測的訓練數據集包括7049(96x96)個(gè)灰度圖像。 對于每個(gè)圖像,我們應該學(xué)習找到15個(gè)關(guān)鍵點(diǎn)的正確位置(x和y坐標),例如
left_eye_center
right_eye_outer_corner
mouth_center_bottom_lip
一個(gè)臉部標記出三個(gè)關(guān)鍵點(diǎn)的例子。
數據集的一個(gè)有趣的變化是,對于一些關(guān)鍵點(diǎn),我們只有大約2,000個(gè)標簽,而其他關(guān)鍵點(diǎn)有7,000多個(gè)標簽可用于訓練。
讓我們編寫(xiě)一些Python代碼,從所提供的CSV文件加載數據。 我們將編寫(xiě)一個(gè)可以加載訓練和測試數據的函數。 這兩個(gè)數據集的區別在于測試數據不包含目標值; 這是預測這些問(wèn)題的目標。 這里是我們的load()函數:
# file kfkd.py
import os
import numpy as np
from pandas.io.parsers import read_csv
from sklearn.utils import shuffle
FTRAIN = ~/data/kaggle-facial-keypoint-detection/training.csv
FTEST = ~/data/kaggle-facial-keypoint-detection/test.csv
def load(test=False, cols=None):
Loads data from FTEST if *test* is True, otherwise from FTRAIN.
Pass a list of *cols* if youre only interested in a subset of the
target columns.
fname = FTEST if test else FTRAIN
df = read_csv(os.path.expanduser(fname)) # load pandas dataframe
# The Image column has pixel values separated by space; convert
# the values to numpy arrays:
df[Image] = df[Image].apply(lambda im: np.fromstring(im, sep= ))
if cols: # get a subset of columns
df = df[list(cols) + [Image]]
print(df.count()) # prints the number of values for each column
df = df.dropna() # drop all rows that have missing values in them
X = np.vstack(df[Image].values) / 255. # scale pixel values to [0, 1]
X = X.astype(np.float32)
if not test: # only FTRAIN has any target columns
y = df[df.columns[:-1]].values
y = (y - 48) / 48 # scale target coordinates to [-1, 1]
X, y = shuffle(X, y, random_state=42) # shuffle train data
y = y.astype(np.float32)
else:
y = None
return X, y
X, y = load()
print(X.shape == {}; X.min == {:.3f}; X.max == {:.3f}.format(
X.shape, X.min(), X.max()))
print(y.shape == {}; y.min == {:.3f}; y.max == {:.3f}.format(
y.shape, y.min(), y.max()))
你沒(méi)有必要看懂這個(gè)函數的每一個(gè)細節。 但讓我們看看上面的腳本輸出:
$ python kfkd.py
left_eye_center_x 7034
left_eye_center_y 7034
right_eye_center_x 7032
right_eye_center_y 7032
left_eye_inner_corner_x 2266
left_eye_inner_corner_y 2266
left_eye_outer_corner_x 2263
left_eye_outer_corner_y 2263
right_eye_inner_corner_x 2264
right_eye_inner_corner_y 2264
…
mouth_right_corner_x 2267
mouth_right_corner_y 2267
mouth_center_top_lip_x 2272
mouth_center_top_lip_y 2272
mouth_center_bottom_lip_x 7014
mouth_center_bottom_lip_y 7014
Image 7044
dtype: int64
X.shape == (2140, 9216); X.min == 0.000; X.max == 1.000
y.shape == (2140, 30); y.min == -0.920; y.max == 0.996
首先,它打印出了CSV文件中所有列的列表以及每個(gè)列的可用值的數量。 因此,雖然我們有一個(gè)圖像的訓練數據中的所有行,我們對于mouth_right_corner_x只有個(gè)2,267的值等等。
load()返回一個(gè)元組(X,y),其中y是目標矩陣。 y的形狀是n×m的,其中n是具有所有m個(gè)關(guān)鍵點(diǎn)的數據集中的樣本數。 刪除具有缺失值的所有行是這行代碼的功能:
df = df.dropna() # drop all rows that have missing values in them
這個(gè)腳本輸出的y.shape == (2140, 30)告訴我們,在數據集中只有2140個(gè)圖像有著(zhù)所有30個(gè)目標值。
一開(kāi)始,我們將僅訓練這2140個(gè)樣本。 這使得我們比樣本具有更多的輸入大?。?,216); 過(guò)度擬合可能成為一個(gè)問(wèn)題。當然,拋棄70%的訓練數據也是一個(gè)壞主意。但是目前就這樣,我們將在后面談?wù)摗?/p>
第一個(gè)模型:一個(gè)單隱層
現在我們已經(jīng)完成了加載數據的工作,讓我們使用Lasagne并創(chuàng )建一個(gè)帶有一個(gè)隱藏層的神經(jīng)網(wǎng)絡(luò )。 我們將從代碼開(kāi)始:
# add to kfkd.py
from lasagne import layers
from lasagne.updates import nesterov_momentum
from nolearn.lasagne import NeuralNet
net1 = NeuralNet(
layers=[ # three layers: one hidden layer
(input, layers.InputLayer),
(hidden, layers.DenseLayer),
(output, layers.DenseLayer),
],
# layer parameters:
input_shape=(None, 9216), # 96x96 input pixels per batch
hidden_num_units=100, # number of units in hidden layer
output_nonlinearity=None, # output layer uses identity function
output_num_units=30, # 30 target values
# optimization method:
update=nesterov_momentum,
update_learning_rate=0.01,
update_momentum=0.9,
regression=True, # flag to indicate were dealing with regression problem
max_epochs=400, # we want to train this many epochs
verbose=1,
)
X, y = load()
net1.fit(X, y)
我們使用相當多的參數來(lái)初始化NeuralNet。讓我們看看他們。首先是三層及其參數:
layers=[ # 三層神經(jīng)網(wǎng)絡(luò ):一個(gè)隱層
(input, layers.InputLayer),
(hidden, layers.DenseLayer),
(output, layers.DenseLayer),
],
# 層的參數:
input_shape=(None, 9216), # 每個(gè)批次96x96個(gè)輸入樣例
hidden_num_units=100, # 隱層中的單元數
output_nonlinearity=None, # 輸出用的激活函數
output_num_units=30, # 30個(gè)目標值
這里我們定義輸入層,隱藏層和輸出層。在層參數中,我們命名并指定每個(gè)層的類(lèi)型及其順序。參數input_shape,hidden_??num_units,output_nonlinearity和output_num_units是特定層的參數。它們通過(guò)它們的前綴引用層,使得input_shape定義輸入層的shape參數,hidden_??num_units定義隱藏層的num_units等等。(看起來(lái)有點(diǎn)奇怪,我們必須指定像這樣的參數,但結果是它讓我們對于受使用scikit-learn的管道和參數搜索功能擁有更好的兼容性。)
我們將input_shape的第一個(gè)維度設置為None。這轉換為可變批量大小。如果你知道批量大小的話(huà),也可以設置成固定值,如果為None,則是可變值。
我們將output_nonlinearity設置為None。因此,輸出單元的激活僅僅是隱藏層中的激活的線(xiàn)性組合。
DenseLayer使用的默認非線(xiàn)性是rectifier,它其實(shí)就是返回max(0, x)。它是當今最受歡迎的激活功能選擇。通過(guò)不明確設置hidden_??nonlinearity,我們選擇rectifier作為我們隱藏層的激活函數。
神經(jīng)網(wǎng)絡(luò )的權重用具有巧妙選擇的間隔的均勻分布來(lái)初始化。也就是說(shuō),Lasagne使用“Glorot-style”初始化來(lái)計算出這個(gè)間隔。
還有幾個(gè)參數。 所有以update開(kāi)頭的參數用來(lái)表示更新方程(或最優(yōu)化方法)的參數。 更新方程將在每個(gè)批次后更新我們網(wǎng)絡(luò )的權重。 我們將使用涅斯捷羅夫動(dòng)量梯度下降優(yōu)化方法(nesterov_momentum gradient descent optimization method)來(lái)完成這項工作。Lasagne實(shí)現的其他方法有很多,如adagrad和rmsprop。我們選擇nesterov_momentum,因為它已經(jīng)證明對于大量的問(wèn)題很好地工作。
”’ optimization method: ””
update=nesterov_momentum,
update_learning_rate=0.01,
update_momentum=0.9,
update_learning_rate定義了梯度下降更新權重的步長(cháng)。我們稍后討論學(xué)習率和momentum參數,現在的話(huà),這種健全的默認值已經(jīng)足夠了。
上圖是不同的最優(yōu)化方法的對比(animation by?Alec Radford)。星標位置為全局最優(yōu)值。注意到不添加動(dòng)量的隨機梯度下降是收斂最慢的,我們在教程中從頭到尾都是用Nesterov加速過(guò)的梯度下降。
在我們的NeuralNet的定義中,我們沒(méi)有指定一個(gè)目標函數來(lái)實(shí)現最小化。這里使用的還有一個(gè)默認值:對于回歸問(wèn)題,它是均方誤差(MSE)。
最后一組參數聲明我們正在處理一個(gè)回歸問(wèn)題(而不是分類(lèi)),400是我們愿意訓練的時(shí)期數,并且我們想在訓練期間通過(guò)設置verbose = 1:
regression=True, # flag to indicate were dealing with regression problem
max_epochs=400, # we want to train this many epochs
verbose=1,
最后兩行加載了數據,然后用數據訓練了我們的第一個(gè)神經(jīng)網(wǎng)絡(luò )。
X, y = load()
net1.fit(X, y)
運行這兩行會(huì )輸出一個(gè)表格,每次完成一代就輸出一行。每一行里,我們可以看到當前的訓練損失和驗證損失(最小二乘損失),以及兩者的比率。NeuroNet將會(huì )自動(dòng)把輸入數據X分成訓練集和測試集,用20%的數據作驗證。(比率可以通過(guò)參數eval_size=0.2調整)
$ python kfkd.py
...
InputLayer (None, 9216) produces 9216 outputs
DenseLayer (None, 100) produces 100 outputs
DenseLayer (None, 30) produces 30 outputs
Epoch | Train loss | Valid loss | Train / Val
--------|--------------|--------------|----------------
1 | 0.105418 | 0.031085 | 3.391261
2 | 0.020353 | 0.019294 | 1.054894
3 | 0.016118 | 0.016918 | 0.952734
4 | 0.014187 | 0.015550 | 0.912363
5 | 0.013329 | 0.014791 | 0.901199
...
200 | 0.003250 | 0.004150 | 0.783282
201 | 0.003242 | 0.004141 | 0.782850
202 | 0.003234 | 0.004133 | 0.782305
203 | 0.003225 | 0.004126 | 0.781746
204 | 0.003217 | 0.004118 | 0.781239
205 | 0.003209 | 0.004110 | 0.780738
...
395 | 0.002259 | 0.003269 | 0.690925
396 | 0.002256 | 0.003264 | 0.691164
397 | 0.002254 | 0.003264 | 0.690485
398 | 0.002249 | 0.003259 | 0.690303
399 | 0.002247 | 0.003260 | 0.689252
400 | 0.002244 | 0.003255 | 0.689606
在相對較快的GPU上訓練,我們能夠在1分鐘之內完成400個(gè)epoch的訓練。注意測試損失會(huì )一直減小。(如果你訓練得足夠長(cháng)時(shí)間,它將會(huì )有很小很小的改進(jìn))
現在我們有了一個(gè)很好的結果了么?我們看到測試誤差是0.0032,和競賽基準比試一下。記住我們將目標除以了48以將其縮放到-1到1之間,也就是說(shuō),要是想計算均方誤差和排行榜的結果比較,必須把我們上面得到的0.003255還原到原來(lái)的尺度。
>>> import numpy as np
>>> np.sqrt(0.003255) * 48
2.7385251505144153
這個(gè)值應該可以代表我們的成績(jì)了。當然,這得假設測試集合的數據和訓練集合的數據符合相同的分布,但事實(shí)卻并非如此。
測試網(wǎng)絡(luò )
我們剛剛訓練的net1對象已經(jīng)保存了訓練時(shí)打印在控制臺桌面中的記錄,我們可以獲取這個(gè)記錄通過(guò)train_history_相關(guān)屬性,讓我們畫(huà)出這兩個(gè)曲線(xiàn)。
train_loss = np.array([i[train_loss] for i in net1.train_history_])
valid_loss = np.array([i[valid_loss] for i in net1.train_history_])
pyplot.plot(train_loss, linewidth=3, label=train)
pyplot.plot(valid_loss, linewidth=3, label=valid)
pyplot.grid()
pyplot.legend()
pyplot.xlabel(epoch)
pyplot.ylabel(loss)
pyplot.ylim(1e-3, 1e-2)
pyplot.yscale(log)
pyplot.show()
我們能夠看到我們的網(wǎng)絡(luò )過(guò)擬合了,但是結果還不錯。事實(shí)上,我們找不到驗證錯誤開(kāi)始上升的點(diǎn),所以那種通常用來(lái)避免過(guò)擬合的early stopping方法在現在還沒(méi)有什么用處。注意我們沒(méi)有采用任何正則化手段,除了選擇節點(diǎn)比較少的隱層——這可以讓過(guò)擬合保持在可控范圍內。
那么網(wǎng)絡(luò )的預測結果是什么樣的呢?讓我們選擇一些樣例來(lái)看一看。
def plot_sample(x, y, axis):
img = x.reshape(96, 96)
axis.imshow(img, cmap=gray)
axis.scatter(y[0::2] * 48 + 48, y[1::2] * 48 + 48, marker=x, s=10)
X, _ = load(test=True)
y_pred = net1.predict(X)
fig = pyplot.figure(figsize=(6, 6))
fig.subplots_adjust(
left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
for i in range(16):
ax = fig.add_subplot(4, 4, i + 1, xticks=[], yticks=[])
plot_sample(X[i], y_pred[i], ax)
pyplot.show()
第一個(gè)模型預測的結果(從測試集抽出了16個(gè)樣例)
預測結果看起來(lái)還不錯,但是有點(diǎn)時(shí)候還是有一點(diǎn)偏。讓我們試著(zhù)做的更好一些。
評論