Pytorch實現基於卷積神經網路的面部表情識別(詳細步驟)(轉載)
文章目錄
一、專案背景
二、資料處理
1、標籤與特徵分離
2、資料視覺化
3、訓練集和測試集
三、模型搭建
四、模型訓練
五、完整程式碼
一、專案背景
資料集cnn_train.csv包含人類面部表情的圖片的label和feature。在這裡,面部表情識別相當於一個分類問題,共有7個類別。
其中label包括7種類型表情:
一共有28709個label,說明包含了28709張表情包嘿嘿。
每一行就是一張表情包48*48=2304個畫素,相當於4848個灰度值(intensity)(0為黑, 255為白)
二、資料處理
1、標籤與特徵分離
這一步為了後面方便讀取資料集,對原資料進行處理,分離後分別儲存為cnn_label.csv和cnn_data.csv.
# cnn_feature_label.py 將label和畫素資料分離 import pandas as pd path = 'cnn_train.csv'# 原資料路徑 # 讀取資料 df = pd.read_csv(path) # 提取label資料 df_y = df[['label']] # 提取feature(即畫素)資料 df_x = df[['feature']] # 將label寫入label.csv df_y.to_csv('cnn_label.csv', index=False, header=False) # 將feature資料寫入data.csv df_x.to_csv('cnn_data.csv', index=False, header=False)
執行之後生成結果檔案:
2、資料視覺化
完成與標籤分離後,下一步我們對特徵進一步處理,也就是將每個資料行的2304個畫素值合成每張48*48的表情圖。
# face_view.py 資料視覺化 import cv2 import numpy as np # 指定存放圖片的路徑 path = './/face' # 讀取畫素資料 data = np.loadtxt('cnn_data.csv') # 按行取資料 for i in range(data.shape[0]): face_array = data[i, :].reshape((48, 48)) # reshape cv2.imwrite(path + '//' + '{}.jpg'.format(i), face_array) # 寫圖片
這段程式碼將寫入28709張表情圖,執行需要一小段時間。
結果如下:
3、訓練集和測試集
第一步,我們要訓練模型,需要劃分一下訓練集和驗證集。一共有28709張圖片,我取前24000張圖片作為訓練集,其他圖片作為驗證集。新建資料夾cnn_train和cnn_val,將0.jpg到23999.jpg放進資料夾cnn_train,將其他圖片放進資料夾cnn_val。
第二步,對每張圖片標記屬於哪一個類別,存放在dataset.csv中,分別在剛剛訓練集和測試集執行標記任務。
# cnn_picture_label.py 表情圖片和類別標註 import os import pandas as pd def data_label(path): # 讀取label檔案 df_label = pd.read_csv('cnn_label.csv', header=None) # 檢視該資料夾下所有檔案 files_dir = os.listdir(path) # 用於存放圖片名 path_list = [] # 用於存放圖片對應的label label_list = [] # 遍歷該資料夾下的所有檔案 for file_dir in files_dir: # 如果某檔案是圖片,則將其檔名以及對應的label取出,分別放入path_list和label_list這兩個列表中 if os.path.splitext(file_dir)[1] == ".jpg": path_list.append(file_dir) index = int(os.path.splitext(file_dir)[0]) label_list.append(df_label.iat[index, 0]) # 將兩個列表寫進dataset.csv檔案 path_s = pd.Series(path_list) label_s = pd.Series(label_list) df = pd.DataFrame() df['path'] = path_s df['label'] = label_s df.to_csv(path + '\\dataset.csv', index=False, header=False) def main(): # 指定資料夾路徑 train_path = 'D:\\PyCharm_Project\\deep learning\\model\\cnn_train' val_path = 'D:\\PyCharm_Project\\deep learning\\model\\cnn_val' data_label(train_path) data_label(val_path) if __name__ == "__main__": main()
完成之後如圖:
第三步,重寫Dataset類,它是Pytorch中影象資料集載入的一個基類,原始碼如下,我們需要重寫類來實現載入上面的影象資料集。
import bisect import warnings from torch._utils import _accumulate from torch import randperm class Dataset(object): r"""An abstract class representing a :class:`Dataset`. All datasets that represent a map from keys to data samples should subclass it. All subclasses should overwrite :meth:`__getitem__`, supporting fetching a data sample for a given key. Subclasses could also optionally overwrite :meth:`__len__`, which is expected to return the size of the dataset by many :class:`~torch.utils.data.Sampler` implementations and the default options of :class:`~torch.utils.data.DataLoader`. .. note:: :class:`~torch.utils.data.DataLoader` by default constructs a index sampler that yields integral indices. To make it work with a map-style dataset with non-integral indices/keys, a custom sampler must be provided. """ def __getitem__(self, index): raise NotImplementedError def __add__(self, other): return ConcatDataset([self, other]) # No `def __len__(self)` default? # See NOTE [ Lack of Default `__len__` in Python Abstract Base Classes ] # in pytorch/torch/utils/data/sampler.py
重寫之後如下,自定義類名為FaceDataset:
class FaceDataset(data.Dataset): # 初始化 def __init__(self, root): super(FaceDataset, self).__init__() self.root = root df_path = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[0]) df_label = pd.read_csv(root + '\\dataset.csv', header=None, usecols=[1]) self.path = np.array(df_path)[:, 0] self.label = np.array(df_label)[:, 0] # 讀取某幅圖片,item為索引號 def __getitem__(self, item): # 影象資料用於訓練,需為tensor型別,label用numpy或list均可 face = cv2.imread(self.root + '\\' + self.path[item]) # 讀取單通道灰度圖 face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) # 直方圖均衡化 face_hist = cv2.equalizeHist(face_gray) """ 畫素值標準化 讀出的資料是48X48的,而後續卷積神經網路中nn.Conv2d() API所接受的資料格式是(batch_size, channel, width, higth), 本次圖片通道為1,因此我們要將48X48 reshape為1X48X48。 """ face_normalized = face_hist.reshape(1, 48, 48) / 255.0 face_tensor = torch.from_numpy(face_normalized) face_tensor = face_tensor.type('torch.FloatTensor') label = self.label[item] return face_tensor, label # 獲取資料集樣本個數 def __len__(self): return self.path.shape[0]
到此,就實現了資料集載入的過程,下面準備使用這個類將資料餵給模型訓練了。
這是Github上面部表情識別的一個開源專案的模型結構,我們使用model B搭建網路模型。使用RRelu(隨機修正線性單元)作為啟用函式。卷積神經網路模型如下:
class FaceCNN(nn.Module): # 初始化網路結構 def __init__(self): super(FaceCNN, self).__init__() # 第一層卷積、池化 self.conv1 = nn.Sequential( nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1), # 卷積層 nn.BatchNorm2d(num_features=64), # 歸一化 nn.RReLU(inplace=True), # 啟用函式 nn.MaxPool2d(kernel_size=2, stride=2), # 最大值池化 ) # 第二層卷積、池化 self.conv2 = nn.Sequential( nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(num_features=128), nn.RReLU(inplace=True), # output:(bitch_size, 128, 12 ,12) nn.MaxPool2d(kernel_size=2, stride=2), ) # 第三層卷積、池化 self.conv3 = nn.Sequential( nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(num_features=256), nn.RReLU(inplace=True), # output:(bitch_size, 256, 6 ,6) nn.MaxPool2d(kernel_size=2, stride=2), ) # 引數初始化 self.conv1.apply(gaussian_weights_init) self.conv2.apply(gaussian_weights_init) self.conv3.apply(gaussian_weights_init) # 全連線層 self.fc = nn.Sequential( nn.Dropout(p=0.2), nn.Linear(in_features=256 * 6 * 6, out_features=4096), nn.RReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(in_features=4096, out_features=1024), nn.RReLU(inplace=True), nn.Linear(in_features=1024, out_features=256), nn.RReLU(inplace=True), nn.Linear(in_features=256, out_features=7), ) # 前向傳播 def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) # 資料扁平化 x = x.view(x.shape[0], -1) y = self.fc(x) return y
引數解析:
輸入通道數in_channels,輸出通道數(即卷積核的通道數)out_channels,卷積核大小kernel_size,步長stride,對稱填0行列數padding。
第一層卷積:input:(bitch_size, 1, 48, 48), output(bitch_size, 64, 24, 24)
第二層卷積:input:(bitch_size, 64, 24, 24), output(bitch_size, 128, 12, 12)
第三層卷積:input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 6, 6)
四、模型訓練
損失函式使用交叉熵,優化器是隨機梯度下降SGD,其中weight_decay為正則項係數,每輪訓練列印損失值,每5輪訓練列印準確率。
def train(train_dataset, val_dataset, batch_size, epochs, learning_rate, wt_decay): # 載入資料並分割batch train_loader = data.DataLoader(train_dataset, batch_size) # 構建模型 model = FaceCNN() # 損失函式 loss_function = nn.CrossEntropyLoss() # 優化器 optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=wt_decay) # 逐輪訓練 for epoch in range(epochs): # 記錄損失值 loss_rate = 0 # scheduler.step() # 學習率衰減 model.train() # 模型訓練 for images, labels in train_loader: # 梯度清零 optimizer.zero_grad() # 前向傳播 output = model.forward(images) # 誤差計算 loss_rate = loss_function(output, labels) # 誤差的反向傳播 loss_rate.backward() # 更新引數 optimizer.step() # 列印每輪的損失 print('After {} epochs , the loss_rate is : '.format(epoch + 1), loss_rate.item()) if epoch % 5 == 0: model.eval() # 模型評估 acc_train = validate(model, train_dataset, batch_size) acc_val = validate(model, val_dataset, batch_size) print('After {} epochs , the acc_train is : '.format(epoch + 1), acc_train) print('After {} epochs , the acc_val is : '.format(epoch + 1), acc_val) return modelView Code
以上程式程式碼的執行過程需要較長時間,目前我只能在CPU上跑程式,速度慢,算力不足,我差不多用了1天時間訓練100輪,訓練時間看不同電腦裝置配置,如果在GPU上跑會快很多。
下面擷取幾個訓練結果:
從結果可以看出,訓練在60輪的時候,模型在訓練集上的準確率達到99%以上,而在測試集上只有60%左右,很明顯出現過擬合的情況,還可以進一步優化引數,使用正則等方法防止過擬合。另外,後面幾十輪訓練的提升很低,還需要找出原因。
這個過程我還在學習中,上面是目前達到的結果,希望之後能夠把這個模型進一步優化,提高準確率。
小結:
學習了機器學習和深度學習有一段時間,基本上看的是李巨集毅老師講解的理論知識,還未真正去實現訓練一個模型。這篇記錄我第一次學習的專案過程,多有不足,還需不斷實踐。目前遇到的問題是:1、基本的理論知識能夠理解,但是在公式推導和模型選擇還未很好掌握。2、未具備訓練一個模型的經驗(程式碼實現),後續需要學習實戰專案。
參考資料:
機器學習-李巨集毅(2019)視訊
https://ntumlta2019.github.io/ml-web-hw3/
https://www.cnblogs.com/HL-space/p/10888556.html
https://github.com/amineHorseman/facial-expression-recognition-using-cnn
原文連結:https://blog.csdn.net/Charzous/article/details/107452464