Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)

Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)


前言

提示:本篇文章主要是关于李宏毅教授授课视频中的作业进行介绍,小白博主所作工作只是将现有的知识内容结合网路上一些优秀作者的总结以博文的形式加上自己的理解复述一遍。本文主要还是我的学习总结,因为网上的一些知识分布比较零散故作整理叙述。如果有不对的地方,还请帮忙指正。本文不产生任何收益,如有出现禁止转载、侵权的图片,亦或者文字内容请联系我进行修改。


  相关参考:  

  李宏毅2020机器学习深度学习(完整版)国语:链接: link.

  秋沐霖:基于卷积神经网络的面部表情识别(pytorch实现)链接: link

   liangchunjiang:详解CNN卷积神经网络 链接: link.

  Dean0Winchester.深度学习—图像卷积与反卷积(最完美的解释)链接: link.

  呆呆的猫.卷积神经网络超详细介绍 链接: link.

  斋藤康毅:深度学习入门(基于python的理论与实现)

  周志华:机器学习

  高扬,叶振斌:白话强化学习与pytorch

文章目录

  • Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)
  • 前言
  • 关于卷积神经网络(CNN)
    • 卷积神经网络介绍
      • 什么是卷积神经网络?
      • 机器如何理解一张图片?
      • 为什么用CNN做图像识别
      • 如何理解有监督学习?
    • CNN中的卷积
      • 用像素去理解图片
      • 图像与过滤器的卷积运算
    • CNN的三个基本思想
      • 局部感受野(local receptive fields)
      • 权值共享(shared weights)
      • 池化(pooling)
    • CNN整体结构
    • CNN的可视化
      • 第一层权重的可视化
      • 基于分层结构的信息提取
      • 卷积神经网络图片分类各层应用实例
  • 基于卷积神经网络的人脸识别
    • 数据集介绍
    • 数据预处理
      • 数据分离
      • 创建data-label对照表
    • 卷积神经网络搭建
      • 重写Dataset类
      • 网络模型搭建
      • 模型训练
    • 完整代码
    • 训练结果
  • 总结

关于卷积神经网络(CNN)

 

卷积神经网络介绍

什么是卷积神经网络?

  卷积神经网络(Convolutional Neural Network, CNN)是一种带有卷积结构的有监督深度神经网络模型,借助卷积层的应用减小了深层网络占有的内存量,通过让卷积核提取图像特征来实现对图像的识别分类。如果你不理解这段话,先不用着急,让我们从头讲起。

机器如何理解一张图片?

为什么用CNN做图像识别

  让我们从自身理解图片的方式出发,当我们去识别一只鸟时,我们会下意识地去寻找它的“鸟嘴”,它的“翅膀”,它的“爪子”…当我们去识别一辆汽车时,我们会下意识地去寻找它的“轮子”,它的“车窗”…

  同样我们可以将这种思路运用在机器识别图片上,当我们让机器去“看”图片时,我们不需要它用整张图的视野大小去看,因为这样太费“脑子”(过多地占用内存)。我们可以创建一个小的过滤窗口(又称为卷积核,如下图红框所示),假设这个红框是个“鸟嘴”过滤窗口(用于识别框内图像是不是一个“鸟嘴”),那么我们可以从左至右,从上至下,用这个窗口扫描整张图片来判断这张图片中是否含有“鸟嘴”,此后我们还可以利用“翅膀”窗口,“爪子”窗口依次进行扫描,借助过滤窗口过滤得到的信息来判断这张图片是不是一只鸟。
 
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第1张图片
 
  但实际上,我们并不会“告诉”机器它应该准备怎样的过滤器(卷积核)去识别图片,在机器进行学习之前的过滤器是随机初始化的,没有任何规律可言,也就是说在还没有开始学习图片之前,机器自身是并不知道它该借助怎么样的过滤器去识别图片的。

  那么我们该如何“教会”机器,学到正确的图片特征,形成好的过滤器呢。我们可以通过计算损失函数值然后利用误差反向传播算法来不断地调整,优化过滤器,直到我们的过滤器被优化得足够好,造成的损失足够小时,我们认为机器“找到”了正确的特征,能够利用这些特征来准确地识别图片。
 

如何理解有监督学习?

 
  有监督学习是机器学习中的一个分类,打个形象的比喻,有监督学习就好比老师(调参侠)让学生(机器)学会做选择题(分类),老师有很多考试卷(样本图片),这些卷子都是有答案(标签)的,我们让机器写这份卷子(对图片进行分类),将他写的答案(模型分类结果)与正确答案(正确标签)进行对比,给他扣分(计算损失函数),机器得到分数后回去“思考”该如何调整做题思路减少扣分(借助损失函数做梯度下降更新原来的模型参数)。
  而有监督就是指,机器是知道标准答案的(用于训练模型的样本是带标签的),从而可以借助“对答案”来及时调整自己的“答题思路”(更新模型参数)。
 

CNN中的卷积

用像素去理解图片

  我们知道所有的图片都由若干个横竖排列的像素点组合而成,其中每个像素点的取值范围为0~255,如果是一张彩色图片(RGB),那我们提取出来的像素矩阵将是3XmXn形式,如果是灰度图片,提取出来的像素矩阵将是1XmXn形式的。如下,我们有一个分辨率为4X4大小的灰色图片,我们将它用像素矩阵的形式进行表示。
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第2张图片

图像与过滤器的卷积运算

  如下图,我们想要用大小为(3,3)的过滤器(卷积核)去扫描我们大小为(4,4)大小的图片。
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第3张图片

  假设我们每次移动向右一步卷积核,那么形成卷积之后的新像素矩阵的过程如下图所示:
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第4张图片
  如果设置有偏置值,则还需要向应用了滤波器的元素加上一个固定值(即偏置大小):
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第5张图片
  如果不是灰度图片,而是三维RGB彩色图片,则我们只需要生成三个卷积核,对R、G、B三个通道进行卷积再求和即可。
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第6张图片

CNN的三个基本思想

 
  卷积神经网络的三个核心思想:局部感受野、权值共享、池化层,CNN借助这三个思想实现网络参数,内存占用的减少,并获得一定程度的位移,从而提升网络性能。

局部感受野(local receptive fields)

  通过上述举例,我们可以得知图片中的联系可以是局部的,当我们找“鸟嘴”时,每个神经元(CNN中的每个卷积核我们可以看作是一个参与搭建神经网络的神经元)不需要“看”整张图片,只需要“看”(感受)这张图片的局部特征即可,然后再在更高层将这些感受到的不同的局部神经元综合起来就可以得到全局(整张图片)的信息了,这样就可以减少连接的数目。
  而感受野其实就是指的这个窗口的大小,因为一种窗口只能学到一个特征,但一类图片不止一个特征,所以我们需要多个窗口去提取多种特征整合成全局信息。

权值共享(shared weights)

  
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第7张图片

  那我们再来理解一下权值共享:因为一个特征窗口我们只用来检测一种特征,那么我们可以把该窗口的权重值w,偏置值b(这里将窗口理解为一个神经元,我们可以根据之前所学习的神经元内部结构来理解这句话,不能理解的朋友可以保留疑问继续往下看)固定住,当窗口在扫描过程中发生移动时权重偏置参数不会发生改变,意味着一个窗口移动后仍然是同一个窗口,扫描检测的是同一种特征。
  注意,权值仅在同一窗口共享,不同窗口间不共享。

池化(pooling)

 池化可以理解为在卷积层上架了一个窗口,不用w,b训练,仅做求积、求max操作。这样可以有效压缩图片大小,节约内存空间。具体操作过程如下图所示:
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第8张图片

Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第9张图片

CNN整体结构

 
  相比较DNN,CNN 中新增了 Convolution(卷积) 层 和 Pooling(池化) 层,我们可以把卷积层中的每个卷积核理解成我们之前在DNN中学习到的神经元。CNN 的层的连接顺序是“Convolution - ReLU -(Pooling)”(Pooling层有时会被省略)。这可以理解为之前的“Affi ne - ReLU”连接被替换成了“Convolution -ReLU -(Pooling)”连接。
  如下图所示搭建一个简单的CNN网络,图片像素矩阵输入后,先进入卷积层进行初步的特征提取,利用ReLU层对卷积之后的矩阵进行激活,添加非线性变化。此处往往会再后面再添加一层归一化操作,因为像素点的取值范围为0-255,但随着不断地卷积进行,很可能会导致像素点值超过取值范围,因此我们要及时进行归一化操作,将像素值保持在0-1之间,这样即可以避免数值超标,又可以聚集散落的数值点,方便之后的梯度下降更新。
  经过归一化操作之后,我们还需要进行池化操作,按一定规则提取池化窗口中最具代表性的某个像素点,舍去其他像素点,达到“压缩”信息的目的。
  然后我们可以重复搭建刚才卷积-激活-归一-池化的网络架构若干次,使得卷积核提取的特征逐渐具体化。之后我们将得到的很多小的像素矩阵进行展平成一个一维矩阵,利用全连接层,利用DNN(深度神经网络)的思想,将展平后的一维数组作为输入,搭建若干隐藏层,最后得到一个神经元个数等于分类类别数的输出层,利用softma层对最大概率的分类进行筛选,得到分类结果。(没有看懂的朋友可以带着疑问往下看,理解的朋友可以跳过。)
 
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第10张图片
  卷积层:因为通过卷积运算我们可以提取出图像的特征,通过卷积运算可以使得原始信号的某些特征增强,并且降低噪声。用一个可训练的滤波器去卷积一个输入的图像(第一阶段是输入的图像,后面的阶段就是卷积特征了),然后加一个偏置,得到卷积层。
  池化层:因为对图像进行下采样,可以减少数据处理量同时保留有用信息,采样可以混淆特征的具体位置,因为某个特征找出来之后,它的位置已经不重要了,我们只需要这个特征和其他特征的相对位置,可以应对形变和扭曲带来的同类物体的变化。
  全连接层:采用softmax全连接,得到的激活值即卷积神经网络提取到的图片特征。
 
  如下图,省略中间的ReLU-Normlize(激活-归一),可以分成两段,一段是我们卷积池化的反复过程,另一段是展平后的全连接层。
  其中卷积和池化层可以重复多次。
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第11张图片

CNN的可视化

 

第一层权重的可视化

 
  CNN到底用卷积层“观察”到了什么呢?我们以下面这张图片为例进行举例,通过卷积层可视化来探索CNN中到底进行了什么处理。
 
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第12张图片
 
  学习前的滤波器是随机进行初始化的,所以在黑白的浓淡上没有规律可循,但学习后的滤波器变成了有规律的图像。我们发现,通过学习,滤波器被更新成了有规律的滤波器,比如从白到黑渐变的滤波器、含有块状区域(称为blob)的滤波器等。
 
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第13张图片

 
  如果要问下图中训练后的有规律的滤波器在“观察”什么,答案就是它在观察边缘(颜色变化的分界线)和斑块(局部的块状区域)等。比如,左半部分为白色、右半部分为黑色的滤波器的情况下,如图所示,会对垂直方向上的边缘有响应。
 
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第14张图片
 
  上图中显示了选择两个学习完的滤波器对输入图像进行卷积处理时的结果。我们发现“滤波器1”对垂直方向上的边缘有响应,“滤波器2”对水平方向上的边缘有响应。由此可知,卷积层的滤波器会提取边缘或斑块等原始信息。而刚才实现的CNN会将这些原始信息传递给后面的层。
 

基于分层结构的信息提取

 
  上面的结果是针对第1层的卷积层得出的。第1层的卷积层中提取了边缘或斑块等“低级”信息,那么在堆叠了多层的CNN中,各层中又会提取什么样的信息呢?根据深度学习的可视化相关的研究,随着层次加深,提取的信息(正确地讲,是反映强烈的神经元)也越来越抽象。图中展示了进行一般物体识别(车或狗等)的8层CNN(5层卷积层+2层全连接层+softmax 层)。这个网络结构的名称是AlexNet。AlexNet网络结构堆叠了多层卷积层和池化层,最后经过全连接层输出结果。图中的方块表示的是中间数据,对于这些中间数据,会连续应用卷积运算。
 
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第15张图片
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第16张图片
  CNN的卷积层中提取的信息。第1层的神经元对边缘或斑块有响应,第3层对纹理有响应,第5层对物体部件有响应,最后的全连接层对物体的类别(狗或车)有响应。

卷积神经网络图片分类各层应用实例

Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第17张图片

基于卷积神经网络的人脸识别

此处主要参考:秋沐霖:基于卷积神经网络的面部表情识别(pytorch实现)

数据集介绍

  数据集地址:数据集地址:https://pan.baidu.com/s/1hwrq5Abx8NOUse3oew3BXg ,提取码:ukf7
  数据集train.csv,大小为28710行X2305列,每一行包含有label,feature两部分。
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第18张图片

  其中label为图片标签,取值范围为0~6,分别代表了七种表情:(0)生气,(1)厌恶,(2)恐惧,(3)高兴,(4)难过,(5)惊讶和(6)中立(无表情)。如下图所示:
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第19张图片
  feature部分包含2304个数值,代表48X48(分辨率)的人脸图片像素值,其中每个像素值的取值范围为0~255。利用openCV库读取像素矩阵转存为图片如下所示:
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第20张图片
  那我们要做的就是搭建一个CNN网络,让我们训练出来的模型可以尽可能准确地识别图像表情标签。

数据预处理

  因为我们现在手里的文件是csv格式,我们需要先将像素数据还原成图片进行保存,python提供专业的图像处理库openCV,我们将采用openCV库实现我们的图片还原。

数据分离

  在原文件中,标签label与人脸像素数据是在一起的,为了方便之后的训练操作,我们利用数据处理库pandas进行数据分离,我们利用read_csv()函数将csv文件以DataFrame的数据类型读取出来,进行缺失值的值填充,之后将label与feature标签进行拆分,分别保存为label.csv和data.csv两个文件,并指定一个路径,借助openCV库进行图片还原保存:

import pandas as pd
import cv2

def DataProcess():
    # 数据预处理
    # 将label与人脸数据作拆分
    path = 'D:\\python_code\\train.csv'  # 文件路径
    df = pd.read_csv(path)  # pd阅读器打开csv文件
    df = df.fillna(0)  # 空值填充

    # 分别提取标签和特征数据
    df_y = df[['label']]
    df_x = df[['feature']]

    # 将label,feature数据写入csv文件
    df_y.to_csv('label.csv', index=False, header=False)  # 不保存索引(0-N),不保存列名('label')
    df_x.to_csv('data.csv', index=False, header=False)

    # 指定存放图片的路径
    path = 'D:\\python_code\\face'
    # 读取像素数据
    data = np.loadtxt('data.csv')

    # 按行取数据
    for i in range(data.shape[0]):  # 按行读取
        face_array = data[i, :].reshape((48, 48))  # reshape 转成图像矩阵给cv2处理
        cv2.imwrite(path + '//' + '{0}.jpg'.format(i), face_array)  # csv文件转jpg写图片

创建data-label对照表

  首先,我们需要划分一下训练集和验证集。在本次作业中,共有28709张图片,取前24000张图片作为训练集,其他图片作为验证集。新建文件夹train和val,将0.jpg到23999.jpg放进文件夹train_data,将其他图片放进文件夹test_data。
Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第21张图片

  在继承torch.utils.data.Dataset类定制自己的数据集时,由于在数据加载过程中需要同时加载出一个样本的数据及其对应的label,因此最好能建立一个data-label对照表,其中记录着data和label的对应关系。

  有童鞋看到这里就会提出疑问了:在人脸可视化过程中,每张图片的命名不都和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对应表是有必要的。

  建立data-label对照表的基本思路就是:指定文件夹(train或val),遍历该文件夹下的所有文件,如果该文件是.jpg格式的图片,就将其图片名写入一个列表,同时通过图片名索引出其label,将其label写入另一个列表。最后利用pandas库将这两个列表写入同一个csv文件。

  执行这段代码前,注意修改相关文件路径。代码执行完毕后,会在train_data和label_data文件夹下各生成一个名为dataset.csv的data-label对照表。

import pandas as pd
import os

def data_label(path):
    # 读取label文件
    df_label = pd.read_csv('label.csv',header = None)

    # 查看文件夹下所有文件
    files_dir = os.listdir(path)

    # 用于存放图片名
    path_list = []

    # 用于存放图片对应的label
    label_list = []

    # 遍历该文件夹下的所有文件
    for files_dir in files_dir:
        # 如果某文件是图片,则其文件名以及对应的label取出,分别放入path_list和label_list这两个列表中
        if os.path.splitext(files_dir)[1] == ".jpg":     # 路径切割,将文件名和后缀名作切割后保存为列表形式
            path_list.append(files_dir)                  # 如果是.jpg文件就添加入path_list 路径列表
            index = int(os.path.splitext(files_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)    # df保存,命名为dataset.csv

卷积神经网络搭建

重写Dataset类

  Dataset类是Pytorch中图像数据集中最为重要的一个类,也是Pytorch中所有数据集加载类中应该继承的父类。其中父类中的两个私有成员函数getitem()和len()必须被重载,否则将会触发错误提示。其中getitem()可以通过索引获取数据,len()可以获取数据集的大小。
  我们着重重写getitem方法。在该方法内完成一些图像预处理操作,并最后将用于训练的数据转换为训练所需的tensor数据类型。

import pandas as pd
import torch
import numpy as np
import cv2

class FaceDataset(data.Dataset):     # 父类继承,注意继承dataset父类必须重写getitem,len否则报错.
    # 初始化
    def __init__(self, root):     # root为train,val文件夹地址
        super(FaceDataset, self).__init__()        # 调用父类的初始化函数
        self.root = root
        # 读取data - label 对照表中的内容
        df_path = pd.read_csv(root + '\\dataset.csv', header = None, usecols=[0])
        df_label = pd.read_csv(root + '\\dataset.csv', header= None, usecols=[1])
        # 将其中内容放入numpy, 方便后期索引
        self.path = np.array(df_path)[:, 0]
        self.label = np.array(df_label)[:, 0]

    # 读取某幅图片, item为索引号
    def __getitem__(self, item):
        face = cv2.imread(self.root + '\\' + self.path[item])          # 读取图片

        # 读取单通道灰度图
        face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)    # 单通道=灰度,三通道-RGB彩色

        # 高斯模糊
        # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)

        # 直方图均衡化
        face_hist = cv2.equalizeHist(face_gray)

        # 像素值标准化,0-255的像素范围转成0-1范围来描述
        face_normalized = face_hist.reshape(1, 48, 48) / 255.0

        # 用于训练的数据需要为tensor类型
        face_tensor = torch.from_numpy(face_normalized)  # 将numpy中的ndarray转换成pytorch中的tensor
        face_tensor = face_tensor.type('torch.FloatTensor')  # Tensor转FloatTensor
        label = self.label[item]
        return face_tensor, label

    # 获取数据集样本个数
    def __len__(self):
        return self.path.shape[0]

网络模型搭建

  本文代码模型借助的是下图model B的上部分。由三层卷积和三层全连接层以及softmax层构成。
  我们输入图片大小为为48X48X1(48X48像素,灰度单通道),经过一个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层,取概率最高的类别为分类结果。

Pytorch基于卷积神经网络的人脸表情识别(白话CNN+数据集+代码实战)_第22张图片
  我们可以通过继承nn.Module来定义自己的模型类。以下代码实现了上述的模型结构。需要注意的是,在代码中,数据经过最后含7个神经元的线性层后就直接输出了,并没有经过softmax层。这是为什么呢?其实这和Pytorch在这一块的设计机制有关。因为在实际应用中,softmax层常常和交叉熵这种损失函数联合使用,因此Pytorch在设计时,就将softmax运算集成到了交叉熵损失函数CrossEntropyLoss()内部,如果使用交叉熵作为损失函数,就默认在计算损失函数前自动进行softmax操作,不需要我们额外加softmax层。Tensorflow也有类似的机制。

import pandas as pd
import torch
import numpy as np
import cv2
import os
import torch.utils.data as data
import torch.optim as optim
import torch.nn as nn

class FaceCNN(nn.Module):
    # 初始化网络结构
    def __init__(self):
        super(FaceCNN, self).__init__()

        # 第一次卷积, 池化
        self.conv1 = nn.Sequential(
            # 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,
            # 卷积核大小kernel_size,步长stride,对称填0行列数padding
            # input:(bitch_size, 1, 48, 48),
            # output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48
            # 卷积层
            nn.Conv2d(in_channels= 1, out_channels= 64, kernel_size= 3, stride= 1, padding= 1),

            # 数据归一化处理,使得数据在Relu之前不会因为数据过大而导致网络性能不稳定
            # 做归一化让数据形成一定区间内的正态分布
            # 不做归一化会导致不同方向进入梯度下降速度差距很大
            nn.BatchNorm2d(num_features = 64),      # 归一化可以避免出现梯度散漫的现象,便于激活。
            nn.RReLU(inplace = True),     # 激活函数


            nn.MaxPool2d(kernel_size= 2,stride = 2), # 最大值池化# output(bitch_size, 64, 24, 24)
        )

        # 第二次卷积, 池化
        self.conv2 = nn.Sequential(
            # input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 12, 12),
            nn.Conv2d(in_channels= 64, out_channels= 128, kernel_size= 3, stride= 1, padding= 1),

            nn.BatchNorm2d(num_features= 128),
            nn.RReLU(inplace= True),

            nn.MaxPool2d(kernel_size= 2, stride = 2),
        )

        # 第三次卷积, 池化
        self.conv3 = nn.Sequential(
            # input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12),
            nn.Conv2d(in_channels=128 , out_channels=256, kernel_size=3, stride=1, padding=1),

            nn.BatchNorm2d(num_features= 256),
            nn.RReLU(inplace=True),

            nn.MaxPool2d(kernel_size=2, stride=2),
            # 最后一层不需要添加激活函数
        )

        # 全连接层
        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),
        )

    # 前向传播
    # 使用sequential模块后无需再在forward函数中添加激活函数
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)

        # 数据扁平化
        x = x.view(x.shape[0], -1)   # 输出维度,-1表示该维度自行判断
        y = self.fc(x)
        return y

模型训练

  有了模型,就可以通过数据的前向传播和误差的反向传播来训练模型了。在此之前,还需要指定优化器(即学习率更新的方式)、损失函数以及训练轮数、学习率等超参数。

  在本次作业中,我们采用的优化器是SGD,即随机梯度下降,其中参数weight_decay为正则项系数(使用结构化风险函数,考虑到模型复杂度可以减轻过拟合);损失函数采用的是交叉熵;可以考虑使用学习率衰减。

# 训练模型
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)
    # 学习率衰减
    #scheduler = optim.lr_scheduler.StepLR(optimizer, step_size= 10, gamma= 0.8)

    # 逐轮训练
    for epoch in range(epochs):
        # 记录损失值
        loss_rate = 0
        # scheduler.step()
        # 注意dropout网络结构下训练和test模式下是不一样的结构
        model.train()  # 模型训练,调用Modlue类提供的train()方法切换到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 , '.format(epoch + 1))
        print('After {} epochs , the loss_rate is : '.format(epoch + 1), loss_rate.item())
        if epoch % 5 == 0:
            model.eval()  # 模型评估,切换到test状态继续执行
            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 model

完整代码

import pandas as pd
import torch
import numpy as np
import cv2
import os
import torch.utils.data as data
import torch.optim as optim
import torch.nn as nn

def DataProcess():
    # 数据预处理
    # 将label与人脸数据作拆分
    path = 'D:\\python_code\\train.csv'  # 文件路径
    df = pd.read_csv(path)  # pd阅读器打开csv文件
    df = df.fillna(0)  # 空值填充

    # 分别提取标签和特征数据
    df_y = df[['label']]
    df_x = df[['feature']]

    # 将label,feature数据写入csv文件
    df_y.to_csv('label.csv', index=False, header=False)  # 不保存索引(0-N),不保存列名('label')
    df_x.to_csv('data.csv', index=False, header=False)

    # 指定存放图片的路径
    path = 'D:\\python_code\\face'
    # 读取像素数据
    data = np.loadtxt('data.csv')

    # 按行取数据
    for i in range(data.shape[0]):  # 按行读取
        face_array = data[i, :].reshape((48, 48))  # reshape 转成图像矩阵给cv2处理
        cv2.imwrite(path + '//' + '{0}.jpg'.format(i), face_array)  # csv文件转jpg写图片

def data_label(path):
    # 读取label文件
    df_label = pd.read_csv('label.csv',header = None)

    # 查看文件夹下所有文件
    files_dir = os.listdir(path)

    # 用于存放图片名
    path_list = []

    # 用于存放图片对应的label
    label_list = []

    # 遍历该文件夹下的所有文件
    for files_dir in files_dir:
        # 如果某文件是图片,则其文件名以及对应的label取出,分别放入path_list和label_list这两个列表中
        if os.path.splitext(files_dir)[1] == ".jpg":     # 路径切割,将文件名和后缀名作切割后保存为列表形式
            path_list.append(files_dir)                  # 如果是.jpg文件就添加入path_list 路径列表
            index = int(os.path.splitext(files_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)    # df保存,命名为dataset.csv

# 验证模型在验证集上的正确率
def validate(model, dataset, batch_size):
    val_loader = data.DataLoader(dataset, batch_size)
    result, num = 0.0, 0
    for images, labels in val_loader:
        pred = model.forward(images)
        pred = np.argmax(pred.data.numpy(), axis = 1)
        labels = labels.data.numpy()
        result += np.sum((pred == labels))
        num += len(images)

    acc = result / num
    return acc

class FaceDataset(data.Dataset):     # 父类继承,注意继承dataset父类必须重写getitem,len否则报错.
    # 初始化
    def __init__(self, root):     # root为train,val文件夹地址
        super(FaceDataset, self).__init__()        # 调用父类的初始化函数
        self.root = root
        # 读取data - label 对照表中的内容
        df_path = pd.read_csv(root + '\\dataset.csv', header = None, usecols=[0])
        df_label = pd.read_csv(root + '\\dataset.csv', header= None, usecols=[1])
        # 将其中内容放入numpy, 方便后期索引
        self.path = np.array(df_path)[:, 0]
        self.label = np.array(df_label)[:, 0]

    # 读取某幅图片, item为索引号
    def __getitem__(self, item):
        face = cv2.imread(self.root + '\\' + self.path[item])          # 读取图片

        # 读取单通道灰度图
        face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)    # 单通道=灰度,三通道-RGB彩色

        # 高斯模糊
        # face_Gus = cv2.GaussianBlur(face_gray, (3,3), 0)

        # 直方图均衡化
        face_hist = cv2.equalizeHist(face_gray)

        # 像素值标准化,0-255的像素范围转成0-1范围来描述
        face_normalized = face_hist.reshape(1, 48, 48) / 255.0

        # 用于训练的数据需要为tensor类型
        face_tensor = torch.from_numpy(face_normalized)  # 将numpy中的ndarray转换成pytorch中的tensor
        face_tensor = face_tensor.type('torch.FloatTensor')  # Tensor转FloatTensor
        label = self.label[item]
        return face_tensor, label

    # 获取数据集样本个数
    def __len__(self):
        return self.path.shape[0]

class FaceCNN(nn.Module):
    # 初始化网络结构
    def __init__(self):
        super(FaceCNN, self).__init__()

        # 第一次卷积, 池化
        self.conv1 = nn.Sequential(
            # 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,
            # 卷积核大小kernel_size,步长stride,对称填0行列数padding
            # input:(bitch_size, 1, 48, 48),
            # output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48
            # 卷积层
            nn.Conv2d(in_channels= 1, out_channels= 64, kernel_size= 3, stride= 1, padding= 1),

            # 数据归一化处理,使得数据在Relu之前不会因为数据过大而导致网络性能不稳定
            # 做归一化让数据形成一定区间内的正态分布
            # 不做归一化会导致不同方向进入梯度下降速度差距很大
            nn.BatchNorm2d(num_features = 64),      # 归一化可以避免出现梯度散漫的现象,便于激活。
            nn.RReLU(inplace = True),     # 激活函数


            nn.MaxPool2d(kernel_size= 2,stride = 2), # 最大值池化# output(bitch_size, 64, 24, 24)
        )

        # 第二次卷积, 池化
        self.conv2 = nn.Sequential(
            # input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 12, 12),
            nn.Conv2d(in_channels= 64, out_channels= 128, kernel_size= 3, stride= 1, padding= 1),

            nn.BatchNorm2d(num_features= 128),
            nn.RReLU(inplace= True),

            nn.MaxPool2d(kernel_size= 2, stride = 2),
        )

        # 第三次卷积, 池化
        self.conv3 = nn.Sequential(
            # input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12),
            nn.Conv2d(in_channels=128 , out_channels=256, kernel_size=3, stride=1, padding=1),

            nn.BatchNorm2d(num_features= 256),
            nn.RReLU(inplace=True),

            nn.MaxPool2d(kernel_size=2, stride=2),
            # 最后一层不需要添加激活函数
        )

        # 全连接层
        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),
        )

    # 前向传播
    # 使用sequential模块后无需再在forward函数中添加激活函数
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)

        # 数据扁平化
        x = x.view(x.shape[0], -1)   # 输出维度,-1表示该维度自行判断
        y = self.fc(x)
        return y

# 训练模型
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)
    # 学习率衰减
    #scheduler = optim.lr_scheduler.StepLR(optimizer, step_size= 10, gamma= 0.8)

    # 逐轮训练
    for epoch in range(epochs):
        # 记录损失值
        loss_rate = 0
        # scheduler.step()
        # 注意dropout网络结构下训练和test模式下是不一样的结构
        model.train()  # 模型训练,调用Modlue类提供的train()方法切换到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 , '.format(epoch + 1))
        print('After {} epochs , the loss_rate is : '.format(epoch + 1), loss_rate.item())
        if epoch % 5 == 0:
            model.eval()  # 模型评估,切换到test状态继续执行
            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 model

def main():
    # 数据预处理
    DataProcess()
    train_path = 'D:\\python_code\\face\\train_data'   # 找到拆分后train_data.csv的保存路径
    val_path = 'D:\\python_code\\face\\test_data'
    data_label(train_path)      # 读取图片相应的label
    data_label(val_path)

    # 数据集的使用
    # 数据集实例化
    train_dataset = FaceDataset(root = 'D:\\python_code\\face\\train_data')
    val_dataset = FaceDataset(root='D:\\python_code\\face\\test_data')

    # 超参数可自行指定
    model = train(train_dataset, val_dataset, batch_size = 128, epochs = 10, learning_rate =0.1,wt_decay = 0)

    # 保存模型
    torch.save(model, 'model_net1.pk1')


if __name__ == '__main__':
    main()

训练结果

总结

 
  博主只是出于完成课时作业的目的完成了这篇博客,许多地方写的比较粗糙,很多不足之处希望多多包涵,之后会陆续补齐李教授的其他作业讲解,希望能够帮助到一些和我一样刚入门的朋友,也希望大佬可以帮忙提意见帮助我改进不足之处。

你可能感兴趣的:(笔记)