各位以為用人臉識別性別年齡就很厲害了嗎?這篇文章要來讓各位瞧瞧深度學習的黑科技可以黑到甚麼程度。只要透過手的照片,就能夠知道性別與年齡,透過卷積神經網路就能做到這麼神奇的效果。
本文將從數據到模型,透過微軟的深度學習框架CNTK以及臉書的深度學習框架Pytorch,來實作根據手的照片來判斷人的性別與年齡。以下解說部分張貼的皆為CNTK代碼,並附上兩個框架代碼。
首先關於使用的數據來自Google,他提供了一組數據包含了11,000張手部的照片(圖1),同時包含手的主人的性別年齡與種族。我們將會擷取裡面的性別(類別)與年齡(連續)數據來作為輸出變數,手部圖片作為輸入特徵。

由於下載後還要自己整理圖片有些許麻煩,因此我也已做好圖片的懶人包,將圖片數據向量化,以及比對好標籤檔的結果,並以Pickle檔案格式儲存。
圖2為解析完的結果,清單內的每個項目也是一個子清單,裡面分別包含兩個Ndarray,第一個是形狀3×64×64的向量代表圖檔。請注意,CNTK與Pytorch都是CHW格式:通道×高×寬。另一個則是形狀為3的向量,裡面三個數值分別為id(數值相同代表是同一個人的不同角度的手)、年齡(介於0~100)以及性別(0是表示女性,1是表示男性)。

CNTK與Pytorch的圖片格式要求是一樣的,在各家深度學習框架中,僅有Tensorflow的排列順序相反。向量維度的排列是CHW(通道×高×寬),顏色的排列順序是BGR(藍綠紅),也就都是依照字母順序排列。
關於圖片與向量的轉換方法如下:
def img2array(img: Image):
arr=np.array(img).astype(np.float32)
arr=arr.transpose(2, 0, 1)#轉成CHW
arr=arr[::-1] #顏色排序為BGR
return np.ascontiguousarray(arr)
def array2img(arr: np.ndarray):
sanitized_img=arr[::-1]#轉成RGB
sanitized_img=np.maximum(0, np.minimum(255, np.transpose(arr, (1, 2, 0))))#轉成HWC
img=Image.fromarray(sanitized_img.astype(np.uint8))
return img
為了供給建模使用的資料讀取器,同時也因為我想要畢其功於一役,讓兩種框架都可以一次適用,所以我寫了一個通用的讀取器來供應每個Minibatch所需要的數據。其中讀取圖片時,我將圖片向量除以255,而且讀取年齡時,我將數值除以100,都是為了確保數據可以介於0~1之間,以方便收斂。在這個範例中因為篇幅關係暫時不放數據增強(Data Augmentation)。利用函數,每次調用都可以回傳圖片以及所需要標籤。此外,要注意的是打亂圖片順序這個步驟很重要,Google的數據是有按照性別排序的。
在這裡要示範的卷積神經網路骨幹網路用的是我最近很推崇的一篇文章所介紹的新架構「DenseNet」,原始論文出處為「Densely Connected Convolutional Networks」。
傳統的卷積網路都是線性的,但當層數越多時,就有可能發生梯度彌散的問題,造成模型無法收斂。正因如此,微軟亞洲院2015年發展出的殘差神經網路(ResNet)就使用了跳轉連接(Skip Connection),以有效的將梯度挹注到後面神經層,這樣模型就可以做出超深的架構,也不用擔心難以收斂。
微軟2015年就以152層的ResNet獲得了當年的imageNet冠軍。但是深度學習在訓練的過程中,當卷積神經網路將梯度傳送到後層的時候,都會發生特徵被隨機遺失,這樣這個特徵就再也傳不下去而無用了。為了解決這個問題,DenseNet的基本概念就是,每一層的特徵都會傳送到後面的「每」一層,這樣就可以有效的確保訊號不被丟失。

DenseNet的基本結構稱之為稠密單元(Dense Block),它有幾個重要的超參數:
·k:稠密單元層數
·n_channel_start:初始通道數
·glowth_rate:通道成長數
以圖4為例,假設下圖是一個k=4(向下傳遞4次,所以共5層),初始通道數32,成長數為16的Dense Block,分別計算每一層的輸入通道數(從前面傳進來):

X1:32+16(來自於X0)=48
X2:48+16(來自於X1)=64
X3:64+16(來自於X0)+16(來自於X1)=96
X4:96+16(來自於X0)+16(來自於X1)+16(來自於X2)=144
Growth Rate有就是每次會傳遞多少通道到後面的層數,以上面說明案例固定數值為16,但該卷積層的通道數比這數字更大,因此等於是強迫每個卷積層要做一次特徵選取,將特徵精選之後傳至後方。這種「Save the Best for Last」的精神,可以高度保全有效特徵,以強化模型的收斂。DenseNet就是利用多個DenseBlock構成的網路(圖5)。

另外,CNTK與Pytorch都沒有預設的DenseNet,所以筆者用自訂網路的方式實作了兩個框架下的DenseNet。在該實作中,由於圖片只有64×64,經不起太多次圖片縮小,因此使用了5層k=4的Dense Block。同時,筆者也測試過3層,收斂速度快,但是結果測試集落差很大,顯著過擬合。由於想要同時預測性別與年齡,CNTK一個很神奇的特性就是可以在一個主要骨架下,同時接兩個輸出,只需要使用Combine函數,就可以將兩個輸出合併。未來更只要做一次預測,就能產出兩個預測結果,而且訓練時也只要訓練一次,而且骨幹部分特徵選取流程不需要做兩次,是不是很方便呢。但如果你是使用其他框架就只好做兩個模型了。
預測性別部分,使用的是長度為2的向量,最後一層全連接層活化函數使用Softmax已進行分類。預測年齡部分,由於讀取數據時已經將年齡除以100,因此年齡分布為0~1之間的常態分布,因此使用Sigmoid函數效果較好。
最後的訓練過程可以透過另一個函數來控制。首先宣告輸入變數以及兩個輸出變數(性別與年齡),然後宣告模型、損失函數以及正確率指標。優化器使用的是Adam,然後每50個Minibatch就用測試集測試一次。
筆者使用gtx-1080(minibatch size=64)跑完第3個epoch的結果如圖6,可以看出年齡誤差只有1.8%,性別目前仍有20.31%的錯誤率,看來手真的藏不住年齡啊!這個模型若是希望提升其泛化效果,應該要在輸入數據加入數據增強。
