基於卷積神經網絡的面部表情識別(Pytorch實現)----臺大李宏毅機器學習做業3(HW3)

1、項目說明

  給定數據集train.csv,要求使用卷積神經網絡CNN,根據每一個樣本的面部圖片判斷出其表情。在本項目中,表情共分7類,分別爲:(0)生氣,(1)厭惡,(2)恐懼,(3)高興,(4)難過,(5)驚訝和(6)中立(即面無表情,沒法歸爲前六類)。因此,本項目實質上是一個7分類問題。html

數據集介紹:python

  (1)、CSV文件,大小爲28710行X2305列;git

  (2)、在28710行中,其中第一行爲描述信息,即「label」和「feature」兩個單詞,其他每行內含有一個樣本信息,即共有28709個樣本;github

  (3)、在2305列中,其中第一列爲該樣本對應的label,取值範圍爲0到6。其他2304列爲包含着每一個樣本大小爲48X48人臉圖片的像素值(2304=48X48),每一個像素值取值範圍在0到255之間;web

  (4)、數據集地址:https://pan.baidu.com/s/1hwrq5Abx8NOUse3oew3BXg ,提取碼:ukf7 。spring

 2、思路分析及代碼實現

  給定的數據集是csv格式的,考慮到圖片分類問題的常規作法,決定先將其所有可視化,還原爲圖片文件再送進模型進行處理。express

  藉助深度學習框架Pytorch1.0 CPU(窮逼)版本,搭建模型,因爲需用到本身的數據集,所以咱們須要重寫其中的數據加載部分,其他用現成的API便可。網絡

  做業要求使用CNN實現功能,所以基本只能在調參階段自由發揮(不要鄙視調參,經過此次做業才發現,參數也不是人人都能調得好的,好比我)。app

2.1 數據可視化

  咱們須要將csv中的像素數據還原爲圖片並保存下來,在python環境下,不少庫都能實現相似的功能,如pillow,opencv等。因爲筆者對opencv較爲熟悉,且opencv又是專業的圖像處理庫,所以決定採用opencv實現這一功能。框架

2.1.1 數據分離

  原文件中,label和人臉像素數據是集中在一塊兒的。爲了方便操做,決定利用pandas庫進行數據分離,即將全部label 讀出後,寫入新建立的文件label.csv;將全部的像素數據讀出後,寫入新建立的文件data.csv。

 1 # 將label和像素數據分離
 2 import pandas as pd
 3 
 4 # 修改成train.csv在本地的相對或絕對地址
 5 path = './/ml2019spring-hw3//train.csv'
 6 # 讀取數據
 7 df = pd.read_csv(path)
 8 # 提取label數據
 9 df_y = df[['label']]
10 # 提取feature(即像素)數據
11 df_x = df[['feature']]
12 # 將label寫入label.csv
13 df_y.to_csv('label.csv', index=False, header=False)
14 # 將feature數據寫入data.csv
15 df_x.to_csv('data.csv', index=False, header=False)

  以上代碼執行完畢後,在該代碼腳本所在的文件夾下,就會生成兩個新文件label.csv以及data.csv。在執行代碼前,注意修改train.csv在本地的路徑。

2.1.2 數據可視化

  將數據分離後,人臉像素數據所有存儲在data.csv文件中,其中每行數據就是一張人臉。按行讀取數據,利用opencv將每行的2304個數據恢復爲一張48X48的人臉圖片,並保存爲jpg格式。在保存這些圖片時,將第一行數據恢復出的人臉命名爲0.jpg,第二行的人臉命名爲1.jpg......,以方便與label[0]、label[1]......一一對應。

 1 import cv2
 2 import numpy as np
 3 
 4 # 指定存放圖片的路徑
 5 path = './/face'
 6 # 讀取像素數據
 7 data = np.loadtxt('data.csv')
 8 
 9 # 按行取數據
10 for i in range(data.shape[0]):
11     face_array = data[i, :].reshape((48, 48)) # reshape
12     cv2.imwrite(path + '//' + '{}.jpg'.format(i), face_array) # 寫圖片

  以上代碼雖短,但涉及到大量數據的讀取和大批圖片的寫入,所以佔用的內存資源較多,且執行時間較長(視機器性能而定,通常要幾分鐘到十幾分鐘不等)。代碼執行完畢,咱們來到指定的圖片存儲路徑,就能發現裏面所有是寫好的人臉圖片。

  粗略瀏覽一下這些人臉圖片,就能發現這些圖片數據來源較廣,且並不純淨。就前60張圖片而言,其中就包含了正面人臉,如1.jpg;側面人臉,如18.jpg;傾斜人臉,如16.jpg;正面人頭,如7.jpg;正面人上半身,如55.jpg;動漫人臉,如38.jpg;以及絕不相關的噪聲,如59.jpg。放大圖片後仔細觀察,還會發現很多圖片上還有水印。種種因素均給識別提出了嚴峻的挑戰。

2.2 在pytorch下建立數據集

  如今咱們有了圖片,但怎麼才能把圖片讀取出來送給模型呢?

  最簡單粗暴的方法就是直接用opencv將全部圖片讀取出來,以numpy中array的數據格式直接送給模型。若是這樣作的話,會一次性把全部圖片所有讀入內存,佔用大量的內存空間,且只能使用單線程,效率不高,也不方便後續操做。

  其實在pytorch中,有一個類(torch.utils.data.Dataset)是專門用來加載數據的,咱們能夠經過繼承這個類來定製本身的數據集和加載方法。如下爲基本流程。

2.2.1 建立data-label對照表

  首先,咱們須要劃分一下訓練集和驗證集。在本次做業中,共有28709張圖片,取前24000張圖片做爲訓練集,其餘圖片做爲驗證集。新建文件夾train和val,將0.jpg到23999.jpg放進文件夾train,將其餘圖片放進文件夾val。

  在繼承torch.utils.data.Dataset類定製本身的數據集時,因爲在數據加載過程當中須要同時加載出一個樣本的數據及其對應的label,所以最好能創建一個data-label對照表,其中記錄着data和label的對應關係(「data-lable對照表」並不是官方名詞,這個技術流程是筆者參考了他人的博客後本身摸索的,這個名字也是筆者給命的名)。

  有童鞋看到這裏就會提出疑問了:在人臉可視化過程當中,每張圖片的命名不都和label的存放順序是一一對應關係嗎,爲何還要畫蛇添足,再從新創建data-label對照表呢?筆者在剛開始的時候也是這麼想的,按順序(0.jpg, 1.jpg, 2.jpg......)加載圖片和label(label[0], label[1], label[2]......),豈不是方便、快捷又高效?結果在實際操做的過程當中才發現,程序加載文件的機制是按照文件名首字母(或數字)來的,即加載次序是0,1,10,100......,而不是預想中的0,1,2,3......,所以加載出來的圖片不可以和label[0],label[1],lable[2],label[3]......一一對應,因此創建data-label對照表仍是至關有必要的。

  創建data-label對照表的基本思路就是:指定文件夾(train或val),遍歷該文件夾下的全部文件,若是該文件是.jpg格式的圖片,就將其圖片名寫入一個列表,同時經過圖片名索引出其label,將其label寫入另外一個列表。最後利用pandas庫將這兩個列表寫入同一個csv文件。 

  執行這段代碼前,注意修改相關文件路徑。代碼執行完畢後,會在train和val文件夾下各生成一個名爲dataset.csv的data-label對照表。 

 1 import os
 2 import pandas as pd
 3 
 4 def data_label(path):
 5     # 讀取label文件
 6     df_label = pd.read_csv('label.csv', header = None)
 7     # 查看該文件夾下全部文件
 8     files_dir = os.listdir(path)
 9     # 用於存放圖片名
10     path_list = []
11     # 用於存放圖片對應的label
12     label_list = []
13     # 遍歷該文件夾下的全部文件
14     for file_dir in files_dir:
15         # 若是某文件是圖片,則將其文件名以及對應的label取出,分別放入path_list和label_list這兩個列表中
16         if os.path.splitext(file_dir)[1] == ".jpg":
17             path_list.append(file_dir)
18             index = int(os.path.splitext(file_dir)[0])
19             label_list.append(df_label.iat[index, 0])
20 
21     # 將兩個列表寫進dataset.csv文件
22     path_s = pd.Series(path_list)
23     label_s = pd.Series(label_list)
24     df = pd.DataFrame()
25     df['path'] = path_s
26     df['label'] = label_s
27     df.to_csv(path+'\\dataset.csv', index=False, header=False)
28 
29 
30 def main():
31     # 指定文件夾路徑
32     train_path = 'F:\\0gold\\ML\\LHY_class\\FaceData\\train'
33     val_path = 'F:\\0gold\\ML\\LHY_class\\FaceData\\val'
34     data_label(train_path)
35     data_label(val_path)
36 
37 if __name__ == "__main__":
38     main()

  OK,代碼執行完畢,讓咱們來看一看data-label對照表裏面具體是什麼樣子吧! 

2.2.2 重寫Dataset類

  首先介紹一下Pytorch中Dataset類:Dataset類是Pytorch中圖像數據集中最爲重要的一個類,也是Pytorch中全部數據集加載類中應該繼承的父類。其中父類中的兩個私有成員函數getitem()和len()必須被重載,不然將會觸發錯誤提示。其中getitem()能夠經過索引獲取數據,len()能夠獲取數據集的大小。在Pytorch源碼中,Dataset類的聲明以下:

 1 class Dataset(object):
 2     """An abstract class representing a Dataset.
 3 
 4     All other datasets should subclass it. All subclasses should override
 5     ``__len__``, that provides the size of the dataset, and ``__getitem__``,
 6     supporting integer indexing in range from 0 to len(self) exclusive.
 7     """
 8 
 9     def __getitem__(self, index):
10         raise NotImplementedError
11 
12     def __len__(self):
13         raise NotImplementedError
14 
15     def __add__(self, other):
16         return ConcatDataset([self, other])

   咱們經過繼承Dataset類來建立咱們本身的數據加載類,命名爲FaceDataset。

1 import torch
2 from torch.utils import data
3 import numpy as np
4 import pandas as pd
5 import cv2
6 
7 class FaceDataset(data.Dataset):

   首先要作的是類的初始化。以前的data-label對照表已經建立完畢,在加載數據時需用到其中的信息。所以在初始化過程當中,咱們須要完成對data-label對照表中數據的讀取工做。

  經過pandas庫讀取數據,隨後將讀取到的數據放入list或numpy中,方便後期索引。

 1 # 初始化
 2 def __init__(self, root):
 3     super(FaceDataset, self).__init__()
 4     # root爲train或val文件夾的地址    
 5     self.root = root
 6     # 讀取data-label對照表中的內容
 7     df_path = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[0]) # 讀取第一列文件名
 8     df_label = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[1]) # 讀取第二列label
 9     # 將其中內容放入numpy,方便後期索引
10     self.path = np.array(df_path)[:, 0]
11     self.label = np.array(df_label)[:, 0]

  接着就要重寫getitem()函數了,該函數的功能是加載數據。在前面的初始化部分,咱們已經獲取了全部圖片的地址,在這個函數中,咱們就要經過地址來讀取數據。

  因爲是讀取圖片數據,所以仍然藉助opencv庫。須要注意的是,以前可視化數據部分將像素值恢復爲人臉圖片並保存,獲得的是3通道的灰色圖(每一個通道都徹底同樣),而在這裏咱們只須要用到單通道,所以在圖片讀取過程當中,即便原圖原本就是灰色的,但咱們仍是要加入參數從cv2.COLOR_BGR2GARY,保證讀出來的數據是單通道的。讀取出來以後,能夠考慮進行一些基本的圖像處理操做,如經過高斯模糊降噪、經過直方圖均衡化來加強圖像等(經試驗證實,在本次做業中,直方圖均衡化並無什麼卵用,而高斯降噪甚至會下降正確率,多是由於圖片分辨率原本就較低,模糊後基本上什麼都看不清了吧)。讀出的數據是48X48的,然後續卷積神經網絡中nn.Conv2d() API所接受的數據格式是(batch_size, channel, width, higth),本次圖片通道爲1,所以咱們要將48X48 reshape爲1X48X48。

 1 # 讀取某幅圖片,item爲索引號
 2 def __getitem__(self, item):
 3     face = cv2.imread(self.root + '\\' + self.path[item])
 4     # 讀取單通道灰度圖
 5     face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) 
 6     # 高斯模糊
 7     # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)
 8     # 直方圖均衡化
 9     face_hist = cv2.equalizeHist(face_gray)
10     # 像素值標準化
11     face_normalized = face_hist.reshape(1, 48, 48) / 255.0 # 爲與pytorch中卷積神經網絡API的設計相適配,需reshape原圖
12     # 用於訓練的數據需爲tensor類型
13     face_tensor = torch.from_numpy(face_normalized) # 將python中的numpy數據類型轉化爲pytorch中的tensor數據類型
14     face_tensor = face_tensor.type('torch.FloatTensor') # 指定爲'torch.FloatTensor'型,不然送進模型後會因數據類型不匹配而報錯
15     label = self.label[item]
16     return face_tensor, label

   最後就是重寫len()函數獲取數據集大小了。self.path中存儲着全部的圖片名,獲取self.path第一維的大小,即爲數據集的大小。

1 # 獲取數據集樣本個數
2 def __len__(self):
3     return self.path.shape[0]

  完整代碼:

 1 class FaceDataset(data.Dataset):
 2     # 初始化
 3     def __init__(self, root):
 4         super(FaceDataset, self).__init__()
 5         self.root = root
 6         df_path = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[0])
 7         df_label = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[1])
 8         self.path = np.array(df_path)[:, 0]
 9         self.label = np.array(df_label)[:, 0]
10 
11     # 讀取某幅圖片,item爲索引號
12     def __getitem__(self, item):
13         face = cv2.imread(self.root + '\\' + self.path[item])
14         # 讀取單通道灰度圖
15         face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) 
16         # 高斯模糊
17         # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)
18         # 直方圖均衡化
19         face_hist = cv2.equalizeHist(face_gray)
20         # 像素值標準化
21         face_normalized = face_hist.reshape(1, 48, 48) / 255.0 # 爲與pytorch中卷積神經網絡API的設計相適配,需reshape原圖
22         # 用於訓練的數據需爲tensor類型
23         face_tensor = torch.from_numpy(face_normalized) # 將python中的numpy數據類型轉化爲pytorch中的tensor數據類型
24         face_tensor = face_tensor.type('torch.FloatTensor') # 指定爲'torch.FloatTensor'型,不然送進模型後會因數據類型不匹配而報錯
25         label = self.label[item]
26         return face_tensor, label
27 
28     # 獲取數據集樣本個數
29     def __len__(self):
30         return self.path.shape[0]
View Code

2.2.3 數據集的使用

  到此爲止,咱們已經成功地寫好了本身的數據集加載類。那麼這個類該如何使用呢?下面筆者將以訓練集(train文件夾下的數據)加載爲例,講一下整個數據集加載類在模型訓練過程當中的使用方法。

  首先,咱們須要將這個類實例化。

1 # 數據集實例化(建立數據集)
2 train_dataset = FaceDataset(root='E:\\WSD\\HW3\\FaceData\\train')

  train_dataset即爲咱們實例化的訓練集,要想加載其中的數據,還須要DataLoader類的輔助。DataLoader類老是配合Dataset類一塊兒使用,DataLoader類能夠幫助咱們分批次讀取數據,也能夠經過這個類選擇讀取數據的方式(順序 or 隨機亂序),還能夠選擇並行加載數據等,這個類並不要咱們重寫。

1 # 載入數據並分割batch
2 train_loader = data.DataLoader(train_dataset, batch_size)

  最後,咱們就能直接從train_loader中直接加載出數據和label了,並且每次都會加載出一個批次(batch)的數據和label。

1 for images, labels in train_loader:
2     '''
3     經過images和labels訓練模型
4     '''

2.3 網絡模型搭建

   經過Pytorch搭建基於卷積神經網絡的分類器。剛開始是本身設計的網絡模型,在訓練時發現準確度一直上不去,折騰一週後走投無路,後來在github上找到了一個作表情識別的開源項目,用的是這個項目的模型結構,但仍是沒能達到項目中的精度(acc在74%)。下圖爲該開源項目中公佈的兩個模型結構,筆者用的是Model B ,且只採用了其中的卷積-全鏈接部分,若是你們但願進一步提升模型的表現能力,能夠考慮向模型中添加Face landmarks + HOG features 部分。

  能夠看出,在Model B 的卷積部分,輸入圖片shape爲48X48X1,通過一個3X3X64卷積核的卷積操做,再進行一次2X2的池化,獲得一個24X24X64的feature map 1(以上卷積和池化操做的步長均爲1,每次卷積前的padding爲1,下同)。將feature map 1通過一個3X3X128卷積核的卷積操做,再進行一次2X2的池化,獲得一個12X12X128的feature map 2。將feature map 2通過一個3X3X256卷積核的卷積操做,再進行一次2X2的池化,獲得一個6X6X256的feature map 3。卷積完畢,數據即將進入全鏈接層。進入全鏈接層以前,要進行數據扁平化,將feature map 3拉一個成長度爲6X6X256=9216的一維tensor。隨後數據通過dropout後被送進一層含有4096個神經元的隱層,再次通過dropout後被送進一層含有1024個神經元的隱層,以後通過一層含256個神經元的隱層,最終通過含有7個神經元的輸出層。通常再輸出層後都會加上softmax層,取機率最高的類別爲分類結果。

  咱們能夠經過繼承nn.Module來定義本身的模型類。如下代碼實現了上述的模型結構。須要注意的是,在代碼中,數據通過最後含7個神經元的線性層後就直接輸出了,並無通過softmax層。這是爲何呢?其實這和Pytorch在這一塊的設計機制有關。由於在實際應用中,softmax層經常和交叉熵這種損失函數聯合使用,所以Pytorch在設計時,就將softmax運算集成到了交叉熵損失函數CrossEntropyLoss()內部,若是使用交叉熵做爲損失函數,就默認在計算損失函數前自動進行softmax操做,不須要咱們額外加softmax層。Tensorflow也有相似的機制。

 1 class FaceCNN(nn.Module):
 2     # 初始化網絡結構
 3     def __init__(self):
 4         super(FaceCNN, self).__init__()
 5         
 6         # 第一次卷積、池化
 7         self.conv1 = nn.Sequential(
 8             # 輸入通道數in_channels,輸出通道數(即卷積核的通道數)out_channels,卷積核大小kernel_size,步長stride,對稱填0行列數padding
 9             # input:(bitch_size, 1, 48, 48), output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48
10             nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1), # 卷積層
11             nn.BatchNorm2d(num_features=64), # 歸一化
12             nn.RReLU(inplace=True), # 激活函數
13             # output(bitch_size, 64, 24, 24)
14             nn.MaxPool2d(kernel_size=2, stride=2), # 最大值池化
15         )
16         
17         # 第二次卷積、池化
18         self.conv2 = nn.Sequential(
19             # input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-3+2*1)/1+1 = 24
20             nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
21             nn.BatchNorm2d(num_features=128),
22             nn.RReLU(inplace=True),
23             # output:(bitch_size, 128, 12 ,12)
24             nn.MaxPool2d(kernel_size=2, stride=2),
25         )
26         
27         # 第三次卷積、池化
28         self.conv3 = nn.Sequential(
29             # input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-3+2*1)/1+1 = 12
30             nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
31             nn.BatchNorm2d(num_features=256),
32             nn.RReLU(inplace=True),
33             # output:(bitch_size, 256, 6 ,6)
34             nn.MaxPool2d(kernel_size=2, stride=2),
35         )
36 
37         # 參數初始化
38         self.conv1.apply(gaussian_weights_init)
39         self.conv2.apply(gaussian_weights_init)
40         self.conv3.apply(gaussian_weights_init)
41 
42         # 全鏈接層
43         self.fc = nn.Sequential(
44             nn.Dropout(p=0.2),
45             nn.Linear(in_features=256*6*6, out_features=4096),
46             nn.RReLU(inplace=True),
47             nn.Dropout(p=0.5),
48             nn.Linear(in_features=4096, out_features=1024),
49             nn.RReLU(inplace=True),
50             nn.Linear(in_features=1024, out_features=256),
51             nn.RReLU(inplace=True),
52             nn.Linear(in_features=256, out_features=7),
53         )
54 
55     # 前向傳播
56     def forward(self, x):
57         x = self.conv1(x)
58         x = self.conv2(x)
59         x = self.conv3(x)
60         # 數據扁平化
61         x = x.view(x.shape[0], -1)
62         y = self.fc(x)
63         return y

 2.4 訓練模型

  有了模型,就能夠經過數據的前向傳播和偏差的反向傳播來訓練模型了。在此以前,還須要指定優化器(即學習率更新的方式)、損失函數以及訓練輪數、學習率等超參數。

  在本次做業中,咱們採用的優化器是SGD,即隨機梯度降低,其中參數weight_decay爲正則項係數;損失函數採用的是交叉熵;能夠考慮使用學習率衰減。

 1 def train(train_dataset, batch_size, epochs, learning_rate, wt_decay):
 2     # 載入數據並分割batch
 3     train_loader = data.DataLoader(train_dataset, batch_size)
 4     # 構建模型
 5     model = FaceCNN()
 6     # 損失函數
 7     loss_function = nn.CrossEntropyLoss()
 8     # 優化器
 9     optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=wt_decay)
10     # 學習率衰減
11     # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.8)
12     # 逐輪訓練
13     for epoch in range(epochs):
14         # 記錄損失值
15         loss_rate = 0
16         # scheduler.step() # 學習率衰減
17         model.train() # 模型訓練
18         for images, labels in train_loader:
19             # 梯度清零
20             optimizer.zero_grad()
21             # 前向傳播
22             output = model.forward(images)
23             # 偏差計算
24             loss_rate = loss_function(output, labels)
25             # 偏差的反向傳播
26             loss_rate.backward()
27             # 更新參數
28             optimizer.step()

 2.5 模型的保存與加載

  咱們訓練的這個模型相對較小,所以能夠直接保存整個模型(包括結構和參數)。

1 # 模型保存
2 torch.save(model, 'model_net1.pkl')
1 # 模型加載
2 model_parm =  'model_net1.pkl'
3 model = torch.load(net_parm)

 3、源碼分享

3.1 源代碼

  代碼在CPU上跑起來較慢,視超參數和機器性能不一樣,通常跑完需耗時幾小時到幾十小時不等。代碼執行時,每輪輸出一次損失值,每5輪輸出一次在訓練集和驗證集上的正確率。有條件的能夠在GPU上嘗試。

  1 import torch
  2 import torch.utils.data as data
  3 import torch.nn as nn
  4 import torch.optim as optim
  5 import numpy as np
  6 import pandas as pd
  7 import cv2
  8 
  9 # 參數初始化
 10 def gaussian_weights_init(m):
 11     classname = m.__class__.__name__
 12     # 字符串查找find,找不到返回-1,不等-1即字符串中含有該字符
 13     if classname.find('Conv') != -1:
 14         m.weight.data.normal_(0.0, 0.04)
 15 
 16 # 人臉旋轉,嘗試過但效果並很差,本次並未用到
 17 def imgProcess(img):
 18     # 通道分離
 19     (b, g, r) = cv2.split(img)
 20     # 直方圖均衡化
 21     bH = cv2.equalizeHist(b)
 22     gH = cv2.equalizeHist(g)
 23     rH = cv2.equalizeHist(r)
 24 
 25     # 順時針旋轉15度矩陣
 26     M0 = cv2.getRotationMatrix2D((24,24),15,1)
 27     # 逆時針旋轉15度矩陣
 28     M1 = cv2.getRotationMatrix2D((24,24),15,1)
 29     # 旋轉
 30     gH = cv2.warpAffine(gH, M0, (48, 48))
 31     rH = cv2.warpAffine(rH, M1, (48, 48))
 32     # 通道合併
 33     img_processed = cv2.merge((bH, gH, rH))
 34     return img_processed
 35 
 36 # 驗證模型在驗證集上的正確率
 37 def validate(model, dataset, batch_size):
 38     val_loader = data.DataLoader(dataset, batch_size)
 39     result, num = 0.0, 0
 40     for images, labels in val_loader:
 41         pred = model.forward(images)
 42         pred = np.argmax(pred.data.numpy(), axis=1)
 43         labels = labels.data.numpy()       
 44         result += np.sum((pred == labels))
 45         num += len(images)
 46     acc = result / num
 47     return acc
 48 
 49 class FaceDataset(data.Dataset):
 50     # 初始化
 51     def __init__(self, root):
 52         super(FaceDataset, self).__init__()
 53         self.root = root
 54         df_path = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[0])
 55         df_label = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[1])
 56         self.path = np.array(df_path)[:, 0]
 57         self.label = np.array(df_label)[:, 0]
 58 
 59     # 讀取某幅圖片,item爲索引號
 60     def __getitem__(self, item):
 61         # 圖像數據用於訓練,需爲tensor類型,label用numpy或list都可
 62         face = cv2.imread(self.root + '\\' + self.path[item])
 63         # 讀取單通道灰度圖
 64         face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) 
 65         # 高斯模糊
 66         # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)
 67         # 直方圖均衡化
 68         face_hist = cv2.equalizeHist(face_gray)
 69         # 像素值標準化
 70         face_normalized = face_hist.reshape(1, 48, 48) / 255.0
 71         face_tensor = torch.from_numpy(face_normalized)
 72         face_tensor = face_tensor.type('torch.FloatTensor')
 73         label = self.label[item]
 74         return face_tensor, label
 75 
 76     # 獲取數據集樣本個數
 77     def __len__(self):
 78         return self.path.shape[0]
 79 
 80 class FaceCNN(nn.Module):
 81     # 初始化網絡結構
 82     def __init__(self):
 83         super(FaceCNN, self).__init__()
 84         
 85         # 第一次卷積、池化
 86         self.conv1 = nn.Sequential(
 87             # 輸入通道數in_channels,輸出通道數(即卷積核的通道數)out_channels,卷積核大小kernel_size,步長stride,對稱填0行列數padding
 88             # input:(bitch_size, 1, 48, 48), output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48
 89             nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1), # 卷積層
 90             nn.BatchNorm2d(num_features=64), # 歸一化
 91             nn.RReLU(inplace=True), # 激活函數
 92             # output(bitch_size, 64, 24, 24)
 93             nn.MaxPool2d(kernel_size=2, stride=2), # 最大值池化
 94         )
 95         
 96         # 第二次卷積、池化
 97         self.conv2 = nn.Sequential(
 98             # input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-3+2*1)/1+1 = 24
 99             nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
100             nn.BatchNorm2d(num_features=128),
101             nn.RReLU(inplace=True),
102             # output:(bitch_size, 128, 12 ,12)
103             nn.MaxPool2d(kernel_size=2, stride=2),
104         )
105         
106         # 第三次卷積、池化
107         self.conv3 = nn.Sequential(
108             # input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-3+2*1)/1+1 = 12
109             nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
110             nn.BatchNorm2d(num_features=256),
111             nn.RReLU(inplace=True),
112             # output:(bitch_size, 256, 6 ,6)
113             nn.MaxPool2d(kernel_size=2, stride=2),
114         )
115 
116         # 參數初始化
117         self.conv1.apply(gaussian_weights_init)
118         self.conv2.apply(gaussian_weights_init)
119         self.conv3.apply(gaussian_weights_init)
120 
121         # 全鏈接層
122         self.fc = nn.Sequential(
123             nn.Dropout(p=0.2),
124             nn.Linear(in_features=256*6*6, out_features=4096),
125             nn.RReLU(inplace=True),
126             nn.Dropout(p=0.5),
127             nn.Linear(in_features=4096, out_features=1024),
128             nn.RReLU(inplace=True),
129             nn.Linear(in_features=1024, out_features=256),
130             nn.RReLU(inplace=True),
131             nn.Linear(in_features=256, out_features=7),
132         )
133 
134     # 前向傳播
135     def forward(self, x):
136         x = self.conv1(x)
137         x = self.conv2(x)
138         x = self.conv3(x)
139         # 數據扁平化
140         x = x.view(x.shape[0], -1)
141         y = self.fc(x)
142         return y
143 
144 def train(train_dataset, val_dataset, batch_size, epochs, learning_rate, wt_decay):
145     # 載入數據並分割batch
146     train_loader = data.DataLoader(train_dataset, batch_size)
147     # 構建模型
148     model = FaceCNN()
149     # 損失函數
150     loss_function = nn.CrossEntropyLoss()
151     # 優化器
152     optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=wt_decay)
153     # 學習率衰減
154     # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.8)
155     # 逐輪訓練
156     for epoch in range(epochs):
157         # 記錄損失值
158         loss_rate = 0
159         # scheduler.step() # 學習率衰減
160         model.train() # 模型訓練
161         for images, labels in train_loader:
162             # 梯度清零
163             optimizer.zero_grad()
164             # 前向傳播
165             output = model.forward(images)
166             # 偏差計算
167             loss_rate = loss_function(output, labels)
168             # 偏差的反向傳播
169             loss_rate.backward()
170             # 更新參數
171             optimizer.step()
172             
173         # 打印每輪的損失
174         print('After {} epochs , the loss_rate is : '.format(epoch+1), loss_rate.item())
175         if epoch % 5 == 0:
176             model.eval() # 模型評估
177             acc_train = validate(model, train_dataset, batch_size)
178             acc_val = validate(model, val_dataset, batch_size)
179             print('After {} epochs , the acc_train is : '.format(epoch+1), acc_train)
180             print('After {} epochs , the acc_val is : '.format(epoch+1), acc_val)
181 
182     return model
183 
184 def main():
185     # 數據集實例化(建立數據集)
186     train_dataset = FaceDataset(root='E:\\WSD\\HW3\\FaceData\\train')
187     val_dataset = FaceDataset(root='E:\\WSD\\HW3\\FaceData\\val')
188     # 超參數可自行指定
189     model = train(train_dataset, val_dataset, batch_size=128, epochs=100, learning_rate=0.1, wt_decay=0)
190     # 保存模型
191     torch.save(model, 'model_net1.pkl')
192 
193 
194 if __name__ == '__main__':
195     main()
View Code

 3.2 說明

  這是臺灣大學李宏毅老師機器學習課程(2019年春季)第三次做業。在該數據集上,只用卷積神經網絡和其餘輔助手段,能達到的最高分類正確率在75%左右。我先後折騰了近3周,一方面由於能力有限,無人交流指導,另外一方面是由於算力有限(窮逼一個,沒有GPU),最終正確率也僅有63%。上面的源代碼不是個人最終模型,一是由於個人模型原本就很差,過擬合有點嚴重;二是由於我但願你們能本身動手體驗一波調參的樂趣。在此拋磚引玉,要是有哪一個小夥伴有好的改進方法,歡迎來和我交流鴨~

 

參考資料:

  本次做業發佈地址:https://ntumlta2019.github.io/ml-web-hw3/

  面部表情識別GitHub地址:https://github.com/amineHorseman/facial-expression-recognition-using-cnn

  Pytorch製做數據集:https://ptorch.com/news/215.html

            http://www.javashuo.com/article/p-qhlcfgjv-kn.html

相關文章
相關標籤/搜索