做h5页面的网站有哪些,怎么做属于自己的网址,谷歌网站流量分析,wordpress合并压缩一、绪论
1.1 研究背景
面部表情识别 (Facial Expression Recognition )
在日常工作和生活中#xff0c;人们情感的表达方式主要有#xff1a;语言、声音、肢体行为#xff08;如手势#xff09;、以及面部表情等。在这些行为方式中#xff0c;面部表情所携带的表达人类…一、绪论
1.1 研究背景
面部表情识别 (Facial Expression Recognition )
在日常工作和生活中人们情感的表达方式主要有语言、声音、肢体行为如手势、以及面部表情等。在这些行为方式中面部表情所携带的表达人类内心情感活动的信息最为丰富据研究表明人类的面部表情所携带的内心活动的信息在所有的上述的形式中比例最高大约占比55%。
人类的面部表情变化可以传达出其内心的情绪变化表情是人类内心世界的真实写照。上世纪70年代美国著名心理学家保罗•艾克曼经过大量实验之后将人类的基本表情定义为悲伤、害怕、厌恶、快乐、气愤和惊讶六种。同时他们根据不同的面部表情类别建立了相应的表情图像数据库。随着研究的深入中性表情也被研究学者加入基本面部表情中组成了现今的人脸表情识别研究中的七种基础面部表情。 由于不同的面部表情可以反映出在不同情景下人们的情绪变化以及心理变化因此面部表情的识别对于研究人类行为和心理活动具有十分重要的研究意义和实际应用价值。现如今面部表情识别主要使用计算机对人类面部表情进行分析识别从而分析认得情绪变化这在人机交互、社交网络分析、远程医疗以及刑侦监测等方面都具有重要意义。
1.2 研究意义
在计算机视觉中因为表情识别作为人机交互的一种桥梁可以更好的帮助机器了解、识别人类的内心活动从而更好的服务人类因此对人脸表情识别进行深入研究具有十分重要的意义。而在研究人脸表情的过程中如何有效地提取人脸表情特征是人脸表情识别中最为关键的步骤。人脸表情识别从出现到现在已历经数十载在过去的面部表情识别方法里还是主要依靠于人工设计的特征(比如边和纹理描述量)和机器学习技术(如主成分分析、线性判别分析或支持向量机)的结合。但是在无约束的环境中人工设计对不同情况的特征提取是很困难的同时容易受外来因素的干扰(光照、角度、复杂背景等)进而导致识别率下降。
随着科学技术的发展传统的一些基于手工提取人脸表情图像特征的方法因为需要研究人员具有比较丰富的经验具有比较强的局限性从而给人脸表情识别的研究造成了比较大的困难。随着深度学习的兴起作为深度学习的经典代表的卷积神经网络由于其具有自动提取人脸表情图像特征的优势使得基于深度学习的人脸表情特征提取方法逐渐兴起并逐步替代一些传统的人脸表情特征提取的方法。深度学习方法的主要优点在于它们可通过使用非常大的数据集进行训练学习从而获得表征这些数据的最佳功能。在深度学习中使用卷积神经网络作为人脸表情特征提取的工具可以更加完整的提取人脸表情特征解决了一些传统手工方法存在的提取人脸表情特征不充足的问题。
将人脸表情识别算法按照特征提取方式进行分类其主要分为两种一是基于传统的计算机视觉的提取算法。该类方法主要依赖于研究人员手工设计来提取人脸表情特征二是基于深度学习的算法。该方法使用卷积神经网络自动地提取人脸表情特征。卷积神经网络对原始图像进行简单的预处理之后可以直接输入到网络中使用端到端的学习方法即不经过传统的机器学习复杂的中间建模过程如在识别中对数据进行标注、翻转处理等直接一次性将数据标注好同时学习特征与进行分类这是深度学习方法与传统方法的重要区别。相比人工的选取与设计图像特征卷积神经网络通过自动学习的方式获得的样本数据的深度特征信息拥有更好的抗噪声能力、投影不变性、推广与泛化能力、抽象语义表示能力。
二、理论分析与研究
2.1 面部表情识别框架
面部表情识别通常可以划分为四个进程。包括图像获取面部检测图像预处理和表情分类。其中面部检测脸部特征提取和面部表情分类是面部表情识别的三个关键环节面部表情识别的基本框架如下图所示。 首先是获取图像并执行面部检测然后提取仅具有面部的图像部分。所提取的面部表情在比例和灰度上不均匀因此有必要对面部特征区域进行分割和归一化其中执行归一化主要是对面部光照和位置进行统一处理将图像统一重塑为标准大小如 48×48 像素的图片即图像预处理。然后对脸部图像提取面部表情特征值并进行分类。采用卷积神经网络(CNN)来完成特征提取和分类的任务因为 CNN 是模仿人脑工作并建立卷积神经网络结构模型的著名模型所以选择卷积神经网络作为构建模型体系结构的基础最后不断训练优化最后达到较准确识别出面部表情的结果。
图像预处理
采用多普勒扩展法的几何归一化分为两个主要步骤面部校正和面部修剪。
主要目的是将图像转化为统一大小。
具体步骤如下:
1找到特征点并对其进行标记首先选取两眼和鼻子作为三个特征点并采用一个函数对其进行标记这里选择的函数是xyginput(3)。这里重要的一点是获得特征点的坐标值可以用鼠标进行调整。
2两眼的坐标值可以看作参考点将两眼之间的距离设置为 d找到两眼间的中点并标记为 O然后根据参考点对图像进行旋转这步操作是为了保证将人脸图像调到一致。
3接下来以选定的 O 为基准分别向左右两个方向各剪切距离为 d 的区域在垂直方向剪切 05d 和 15d 的区域这样就可以根据面部特征点和几何模型对特征区域进行确定如下图所示 4为了更好的对表情进行提取可将表情的子区域图像裁剪成统一的 48×48 尺寸。
2.2 基于 CNN 的人脸面部表情识别算法
卷积神经网络(CNN)是一种前馈神经网络它包括卷积计算并具有较深的结构因此是深度学习的代表性算法之一。随着科技的不断进步人们在研究人脑组织时受启发创立了神经网络。神经网络由很多相互联系的神经元组成并且可以在不同的神经元之间通过调整传递彼此之间联系的权重系数 x 来增强或抑制信号。标准卷积神经网络通常由输入层、卷积层、池化层、全连接层和输出层组成如下图所示 上图中第一层为输入层大小为 28×28然后通过 20×24×24 的卷积层得到的结果再输入池化层中最后再通过图中第四层既全连接层直到最后输出。
下图为CNN常见的网络模型。其中包括 4 个卷积层3 个池化层池化层的大小为 3×3最终再通过两个全连接层到达输出层。网络模型中的输入层一般是一个矩阵卷积层池化层和全连接层可以当作隐藏层这些层通常具有不同的计算方法需要学习权重以找到最佳值。 从上述中可知标准卷积神经网络除了输入和输出外还主要具有三种类型池化层全连接层和卷积层。这三个层次是卷积神经网络的核心部分。
2.2.1 卷积层
卷积层是卷积神经网络的第一层由几个卷积单元组成。每个卷积单元的参数可以通过反向传播算法进行优化其目的是提取输入的各种特征但是卷积层的第一层只能提取低级特征例如边、线和角。更多层的可以提取更高级的特征利用卷积层对人脸面部图像进行特征提取。一般卷积层结构如下图所示卷积层可以包含多个卷积面并且每个卷积面都与一个卷积核相关联。 由上图可知每次执行卷积层计算时都会生成与之相关的多个权重参数这些权重参数的数量与卷积层的数量相关即与卷积层所用的函数有直接的关系。
2.2.2 池化层
在卷积神经网络中第二个隐藏层便是池化层在卷积神经网络中池化层通常会在卷积层之间由此对于缩小参数矩阵的尺寸有很大帮助也可以大幅减少全连接层中的参数数量。此外池化层在加快计算速度和防止过拟合方面也有很大的作用。在识别图像的过程中有时会遇到较大的图像此时希望减少训练参数的数量这时需要引入池化层。池化的唯一目的是减小图像空间的大小。常用的有 mean-pooling 和max-pooling。mean-pooling 即对一小块区域取平均值假设 pooling 窗的大小是 2×2那么就是在前面卷积层的输出的不重叠地进行 2×2 的取平均值降采样就得到 mean-pooling 的值。不重叠的 4 个 2×2 区域分别 mean-pooling 如下图所示。 max-pooling 即对一小块区域取最大值假设 pooling 的窗大小是 2×2就是在前面卷积层的输出的不重叠地进行 2×2 的取最大值降采样就得到 max-pooling 的值。不重叠的 4 个 2×2 区域分别max-pooling 如下图所示 2.2.3 全连接层
卷积神经网络中的最后一个隐藏层是全连接层。该层的角色与之前的隐藏层完全不同。卷积层和池化层的功能均用于面部图像的特征提取而全连接层的主要功能就是对图像的特征矩阵进行分类。根据不同的状况它可以是一层或多层。
通过该层的图片可以高度浓缩为一个数。由此全连接层的输出就是高度提纯的特征了便于移交给最后的分类器或者回归。
2.2.4 网络的训练
神经网络通过自学习的方式可以获得高度抽象的手工特征无法达到的特征在计算机视觉领域已经取得了革命性的突破。被广泛的应用于生活中的各方面。而要想让神经网络智能化必须对它进行训练在训练过程中一个重要的算法就是反向传播算法。反向传播算法主要是不断调整网络的权重和阈值以得到最小化网络的平方误差之和然后可以输出想要的结果。
2.2.5 CNN模型的算法评价
卷积神经网络由于强大的特征学习能力被应用于面部表情识别中从而极大地提高了面部表情特征提取的效率。与此同时卷积神经网络相比于传统的面部表情识别方法在数据的预处理和数据格式上得到了很大程度的简化。例如卷积神经网络不需要输入归一化和格式化的数据。基于以上优点卷积神经网络在人类面部表情识别这一领域中的表现要远远优于其他传统算法。
2.3 基于 VGG 的人脸面部表情识别算法
随着深度学习算法的不断发展众多卷积神经网络算法已经被应用到机器视觉领域中。尽管卷积神经网络极大地提高了面部表情特征提取的效率但是基于卷积神经网络的算法仍存在两个较为典型的问题
1忽略图像的二维特性。
2常规神经网络提取的表情特征鲁棒性较差。
因此我们需要寻找或设计一种对人类面部表情的识别更加优化并准确的深度卷积神经网络模型。
2.3.1 VGG模型原理
VGG模型的提出
VGGNet是由牛津大学视觉几何小组Visual Geometry Group, VGG提出的一种深层卷积网络结构网络名称VGGNet取自该小组名缩写。VGGNet是首批把图像分类的错误率降低到10%以内模型同时该网络所采用的3\times33×3卷积核的思想是后来许多模型的基础该模型发表在2015年国际学习表征会议International Conference On Learning Representations, ICLR后至今被引用的次数已经超过1万4千余次。 在原论文中的VGGNet包含了6个版本的演进分别对应VGG11、VGG11-LRN、VGG13、VGG16-1、VGG16-3和VGG19不同的后缀数值表示不同的网络层数VGG11-LRN表示在第一层中采用了LRN的VGG11VGG16-1表示后三组卷积块中最后一层卷积采用卷积核尺寸为 1\times11×1 相应的VGG16-3表示卷积核尺寸为 3\times33×3 。下面主要以的VGG16-3为例。 上图中的VGG16体现了VGGNet的核心思路使用 3\times33×3 的卷积组合代替大尺寸的卷积2个 3\times33×3 卷积即可与 5\times55×5 卷积拥有相同的感受视野。
感受野Receptive Field指的是神经网络中神经元“看到的”输入区域在卷积神经网络中feature map上某个元素的计算受输入图像上某个区域的影响这个区域即该元素的感受野。那么如果在我感受野相同的条件下我让中间层数更多那么能提取到的特征就越丰富效果就会更好。
VGG块的组成规律是连续使用数个相同的填充为1、窗口形状为 3\times33×3 的卷积层后接上一个步幅为2、窗口形状为 2\times22×2 的最大池化层。卷积层保持输入的高和宽不变而池化层则对其减半。
2.3.2 VGG模型的优点
1小卷积核: 将卷积核全部替换为3x3极少用了1x1,作用就是减少参数减小计算量。此外采用了更小的卷积核我们就可以使网络的层数加深就可以加入更多的激活函数更丰富的特征更强的辨别能力。卷积后都伴有激活函数更多的卷积核的使用可使决策函数更加具有辨别能力。其实最重要的还是多个小卷积堆叠在分类精度上比单个大卷积要好。
2小池化核: 相比AlexNet的3x3的池化核VGG全部为2x2的池化核。
3层数更深: 从作者给出的6个试验中我们也可以看到最后两个实验的的层数最深效果也是最好。
4卷积核堆叠的感受野: 作者在VGGnet的试验中只使用了两中卷积核大小1*1,3*3。并且作者也提出了一种想法两个3*3的卷积堆叠在一起获得的感受野相当于一个5*5卷积3个3x3卷积的堆叠获取到的感受野相当于一个7x7的卷积。 input83层conv3x3后output2等同于1层conv7x7的结果input82层conv3x3后output2等同于2层conv5x5的结果。
由上图可知输入的8个神经元可以想象为feature map的宽和高conv3 、conv5 、conv7 、对应stride1pad0 。从结果我们可以得出上面推断的结论。此外倒着看网络也就是 backprop 的过程每个神经元相对于前一层甚至输入层的感受野大小也就意味着参数更新会影响到的神经元数目。在分割问题中卷积核的大小对结果有一定的影响在上图三层的 conv3x3 中最后一个神经元的计算是基于第一层输入的7个神经元换句话说反向传播时该层会影响到第一层 conv3x3 的前7个参数。从输出层往回forward同样的层数下大卷积影响做参数更新时到的前面的输入神经元越多。
5全连接转卷积VGG另一个特点就是使用了全连接转全卷积它把网络中原本的三个全连接层依次变为1个conv7x72个conv1x1也就是三个卷积层。改变之后整个网络由于没有了全连接层网络中间的 feature map 不会固定所以网络对任意大小的输入都可以处理。
2.3.3 VGG模型的算法评价
综上所述VGG采用连续的小卷积核代替较大卷积核以获取更大的网络深度。 例如使用 个 ∗ 卷积核代替 ∗ 卷积核。这种方法使得在确保相同感知野的条件下 网络具有比一般的 更大的网络深度提升了神经网络特征提取及分类的效果。
2.4 基于 ResNet 的人脸面部表情识别算法
2.4.1 ResNet模型原理
ResNet模型的提出
ResNetResidual Neural Network由微软研究院的Kaiming He等四名华人提出通过使用ResNet Unit成功训练出了152层的神经网络并在ILSVRC2015比赛中取得冠军在top5上的错误率为3.57%同时参数量比VGGNet低效果非常突出。ResNet的结构可以极快的加速神经网络的训练模型的准确率也有比较大的提升。同时ResNet的推广性非常好甚至可以直接用到InceptionNet网络中。
下图是ResNet34层模型的结构简图 在ResNet网络中有如下几个亮点
提出residual结构残差结构并搭建超深的网络结构(突破1000层)使用Batch Normalization加速训练(丢弃dropout)
在ResNet网络提出之前传统的卷积神经网络都是通过将一系列卷积层与下采样层进行堆叠得到的。但是当堆叠到一定网络深度时就会出现两个问题。
梯度消失或梯度爆炸。退化问题(degradation problem)。
在ResNet论文中说通过数据的预处理以及在网络中使用BNBatch Normalization层能够解决梯度消失或者梯度爆炸问题。但是对于退化问题随着网络层数的加深效果还会变差如下图所示并没有很好的解决办法。 所以ResNet论文提出了residual结构残差结构来减轻退化问题。下图是使用residual结构的卷积网络可以看到随着网络的不断加深效果并没有变差反而变的更好了。 2.4.2 残差结构residual
残差指的是什么
其中ResNet提出了两种mapping一种是identity mapping指的就是下图中”弯弯的曲线”另一种residual mapping指的就是除了”弯弯的曲线“那部分所以最后的输出是 yF(x)x
identity mapping
顾名思义就是指本身也就是公式中的x而residual mapping指的是“差”也就是y−x所以残差指的就是F(x)部分。
下图是论文中给出的两种残差结构。左边的残差结构是针对层数较少网络例如ResNet18层和ResNet34层网络。右边是针对网络层数较多的网络例如ResNet101ResNet152等。为什么深层网络要使用右侧的残差结构呢。因为右侧的残差结构能够减少网络参数与运算量。同样输入一个channel为256的特征矩阵如果使用左侧的残差结构需要大约1170648个参数但如果使用右侧的残差结构只需要69632个参数。明显搭建深层网络时使用右侧的残差结构更合适。 下面先对左侧的残差结构针对ResNet18/34进行一个分析。如下图所示该残差结构的主分支是由两层3x3的卷积层组成而残差结构右侧的连接线是shortcut分支也称捷径分支注意为了让主分支上的输出矩阵能够与我们捷径分支上的输出矩阵进行相加必须保证这两个输出特征矩阵有相同的shape。如果刚刚仔细观察了ResNet34网络结构图应该能够发现图中会有一些虚线的残差结构。在原论文中作者只是简单说了这些虚线残差结构有降维的作用并在捷径分支上通过1x1的卷积核进行降维处理。而下图右侧给出了详细的虚线残差结构注意下每个卷积层的步距stride以及捷径分支上的卷积核的个数与主分支上的卷积核个数相同。 接着再来分析下针对ResNet50/101/152的残差结构如下图所示。在该残差结构当中主分支使用了三个卷积层第一个是1x1的卷积层用来压缩channel维度第二个是3x3的卷积层第三个是1x1的卷积层用来还原channel维度注意主分支上第一层卷积层和第二次卷积层所使用的卷积核个数是相同的第三次是第一层的4倍。该残差结构所对应的虚线残差结构如下图右侧所示同样在捷径分支上有一层1x1的卷积层它的卷积核个数与主分支上的第三层卷积层卷积核个数相同注意每个卷积层的步距。 下面这幅图是原论文给出的不同深度的ResNet网络结构配置注意表中的残差结构给出了主分支上卷积核的大小与卷积核个数表中的xN表示将该残差结构重复N次。那到底哪些残差结构是虚线残差结构呢。 对于我们ResNet18/34/50/101/152表中conv3_x, conv4_x, conv5_x所对应的一系列残差结构的第一层残差结构都是虚线残差结构。因为这一系列残差结构的第一层都有调整输入特征矩阵shape的使命将特征矩阵的高和宽缩减为原来的一半将深度channel调整成下一层残差结构所需要的channel。下面给出了简单标注了一些信息的ResNet34网络结构图。 对于ResNet50/101/152其实在conv2_x所对应的一系列残差结构的第一层也是虚线残差结构。因为它需要调整输入特征矩阵的channel根据表格可知通过3x3的max pool之后输出的特征矩阵shape应该是[56, 56, 64]但我们conv2_x所对应的一系列残差结构中的实线残差结构它们期望的输入特征矩阵shape是[56, 56, 256]因为这样才能保证输入输出特征矩阵shape相同才能将捷径分支的输出与主分支的输出进行相加。所以第一层残差结构需要将shape从[56, 56, 64] -- [56, 56, 256]。注意这里只调整channel维度高和宽不变而conv3_x, conv4_x, conv5_x所对应的一系列残差结构的第一层虚线残差结构不仅要调整channel还要将高和宽缩减为原来的一半。
2.4.3 ResNet模型的算法评价
ResNet已经被广泛运用于各种特征提取应用中它的出现解决了网络层数到一定的深度后分类性能和准确率不能提高的问题深度残差网络与传统卷积神经网络相比在网络中引入残差模块该模块的引入有效地缓解了网络模型训练时反向传播的梯度消失问题进而解决了深层网络难以训练和性能退化的问题。
三、人脸面部表情识别项目设计
3.1 项目简介
本项目是基于卷积神经网络模型开展表情识别的研究为了尽可能的提高最终表情识别的准确性需要大量的样本图片训练优化所以采用了 FER2013 数据集用来训练、测试此数据集由 35886 张人脸表情图片组成其中测试图 28708 张公共验证图和私有验证图各 3589 张所有图片中共有7种表情。在预处理时把图像归一化为 48×48 像素训练的网络结构是基于 CNN 网络结构的优化改进后的一个开源的网络结构下文中会具体介绍到通过不断地改进优化缩小损失率最终能达到较准确的识别出人的面部表情的结果。
3.2 数据集准备
本项目采用了FER2013数据库其数据集的下载地址如下
Challenges in Representation Learning: Facial Expression Recognition Challenge | Kaggle
FER2013数据集由28709张训练图3589张公开测试图和3589张私有测试图组成。每一张图都是像素为48*48的灰度图。FER2013数据库中一共有7中表情愤怒厌恶恐惧开心难过惊讶和中性。该数据库是2013年Kaggle比赛的数据由于这个数据库大多是从网络爬虫下载的存在一定的误差性。这个数据库的人为准确率是65% 士 5%。 3.3 数据集介绍
给定的数据集train.csv我们要使用卷积神经网络CNN根据每个样本的面部图片判断出其表情。在本项目中表情共分7类分别为0生气1厌恶2恐惧3高兴4难过5惊讶和6中立即面无表情无法归为前六类。因此项目实质上是一个7分类问题。 train.csv文件说明
1CSV文件大小为28710行X2305列
2在28710行中其中第一行为描述信息即“emotion”和“pixels”两个单词其余每行内含有一个样本信息即共有28709个样本
3在2305列中其中第一列为该样本对应的emotion取值范围为0到6。其余2304列为包含着每个样本大小为48X48人脸图片的像素值230448X48每个像素值取值范围在0到255之间 3.4 数据集分离
在原文件中emotion和pixels人脸像素数据是集中在一起的。为了方便操作决定利用pandas库进行数据分离即将所有emotion读出后写入新创建的文件emotion.csv将所有的像素数据读出后写入新创建的文件pixels.csv。
数据集分离的代码如下
# 将emotion和pixels像素数据分离
import pandas as pd# 注意修改train.csv为你电脑上文件所在的相对或绝对路劲地址。
path dataset/train.csv
# 读取数据
df pd.read_csv(path)
# 提取emotion数据
df_y df[[emotion]]
# 提取pixels数据
df_x df[[pixels]]
# 将emotion写入emotion.csv
df_y.to_csv(dataset/emotion.csv, indexFalse, headerFalse)
# 将pixels数据写入pixels.csv
df_x.to_csv(dataset/pixels.csv, indexFalse, headerFalse)
以上代码执行完毕后在dataset的文件夹下就会生成两个新文件emotion.csv以及pixels.csv。在执行代码前注意修改train.csv为你电脑上文件所在的相对或绝对路劲地址。 3.5 数据可视化
给定的数据集是csv格式的考虑到图片分类问题的常规做法决定先将其全部可视化还原为图片文件再送进模型进行处理。
在python环境下将csv中的像素数据还原为图片并保存下来有很多库都能实现类似的功能如pillowopencv等。这里我采用的是用opencv来实现这一功能。
将数据分离后人脸像素数据全部存储在pixels.csv文件中其中每行数据就是一张人脸。按行读取数据利用opencv将每行的2304个数据恢复为一张48X48的人脸图片并保存为jpg格式。在保存这些图片时将第一行数据恢复出的人脸命名为0.jpg第二行的人脸命名为1.jpg......以方便与label[0]、label[1]......一一对应。
数据可视化的代码如下
import cv2
import numpy as np# 指定存放图片的路径
path face_images
# 读取像素数据
data np.loadtxt(dataset/pixels.csv)# 按行取数据
for i in range(data.shape[0]):face_array data[i, :].reshape((48, 48)) # reshapecv2.imwrite(path // {}.jpg.format(i), face_array) # 写图片 以上代码虽短但涉及到大量数据的读取和大批图片的写入因此占用的内存资源较多且执行时间较长视机器性能而定一般要几分钟到十几分钟不等。代码执行完毕我们来到指定的图片存储路径就能发现里面全部是写好的人脸图片。 粗略浏览一下这些人脸图片就能发现这些图片数据来源较广且并不纯净。就前60张图片而言其中就包含了正面人脸如1.jpg侧面人脸如18.jpg倾斜人脸如16.jpg正面人头如7.jpg正面人上半身如55.jpg动漫人脸如38.jpg以及毫不相关的噪声如59.jpg。放大图片后仔细观察还会发现不少图片上还有水印。各种因素均给识别提出了严峻的挑战。
3.6 创建映射表
创建image图片名和对应emotion表情数据集的映射关系表。
首先我们需要划分一下训练集和验证集。在项目中共有28709张图片取前24000张图片作为训练集其他图片作为验证集。新建文件夹train_set和verify_set将0.jpg到23999.jpg放进文件夹train_set将其他图片放进文件夹verify_set。
在继承torch.utils.data.Dataset类定制自己的数据集时由于在数据加载过程中需要同时加载出一个样本的数据及其对应的emotion因此最好能建立一个image的图片名和对应emotion表情数据的关系映射表其中记录着image的图片名和其emotion表情数据的映射关系。
这里需要和大家强调一下大家在人脸可视化过程中每张图片的命名不是都和emotion的存放顺序是一一对应的。在实际操作的过程中才发现程序加载文件的机制是按照文件名首字母或数字来的即加载次序是0110100......而不是预想中的0123......因此加载出来的图片不能够和emotion[0]emotion[1]emotion[2]emotion[3]......一一对应所以建立image-emotion映射关系表还是相当有必要的。
建立image-emotion映射表的基本思路就是指定文件夹train_set或verify_set遍历该文件夹下的所有文件如果该文件是.jpg格式的图片就将其图片名写入一个列表同时通过图片名索引出其emotion将其emotion写入另一个列表。最后利用pandas库将这两个列表写入同一个csv文件。
image-emotion关系映射创建代码如下
import os
import pandas as pddef image_emotion_mapping(path):# 读取emotion文件df_emotion pd.read_csv(dataset/emotion.csv, header None)# 查看该文件夹下所有文件files_dir os.listdir(path)# 用于存放图片名path_list []# 用于存放图片对应的emotionemotion_list []# 遍历该文件夹下的所有文件for file_dir in files_dir:# 如果某文件是图片则将其文件名以及对应的emotion取出分别放入path_list和emotion_list这两个列表中if os.path.splitext(file_dir)[1] .jpg:path_list.append(file_dir)index int(os.path.splitext(file_dir)[0])emotion_list.append(df_emotion.iat[index, 0])# 将两个列表写进image_emotion.csv文件path_s pd.Series(path_list)emotion_s pd.Series(emotion_list)df pd.DataFrame()df[path] path_sdf[emotion] emotion_sdf.to_csv(path\\image_emotion.csv, indexFalse, headerFalse)def main():# 指定文件夹路径train_set_path face_images/train_setverify_set_path face_images/verify_setimage_emotion_mapping(train_set_path)image_emotion_mapping(verify_set_path)if __name__ __main__:main() 执行这段代码前注意修改相关文件路径。代码执行完毕后会在train_set和verify_set文件夹下各生成一个名为image-emotion.csv的关系映射表。
3.7 加载数据集
现在我们有了图片但怎么才能把图片读取出来送给模型呢一般在平常的时候我们第一个想到的是将所有需要的数据聚成一堆一堆然后通过构建list去读取我们的数据 假如我们编写了上述的图像加载数据集代码在训练中我们就可以依靠get_training_data()这个函数来得到batch_size个数据从而进行训练乍看下去没什么问题但是一旦我们的数据量超过1000
将所有的图像数据直接加载到numpy数据中会占用大量的内存由于需要对数据进行导入每次训练的时候在数据读取阶段会占用大量的时间只使用了单线程去读取读取效率比较低下拓展性很差如果需要对数据进行一些预处理只能采取一些不是特别优雅的做法
如果用opencv将所有图片读取出来最简单粗暴的方法就是直接以numpy中array的数据格式直接送给模型。如果这样做的话会一次性把所有图片全部读入内存占用大量的内存空间且只能使用单线程效率不高也不方便后续操作。
既然问题这么多到底说回来我们应该如何正确地加载数据集呢
其实在pytorch中有一个类torch.utils.data.Dataset是专门用来加载数据的我们可以通过继承这个类来定制自己的数据集和加载方法。
Dataset类是Pytorch中图像数据集中最为重要的一个类也是Pytorch中所有数据集加载类中应该继承的父类。其中父类中的两个私有成员函数必须被重载否则将会触发错误提示 def getitem(self, index): def len(self):
其中__len__应该返回数据集的大小而__getitem__应该编写支持数据集索引的函数例如通过dataset[i]可以得到数据集中的第i1个数据。
#源码
class Dataset(object):
An abstract class representing a Dataset.
All other datasets should subclass it. All subclasses should override
__len__, that provides the size of the dataset, and __getitem__,
supporting integer indexing in range from 0 to len(self) exclusive.
#这个函数就是根据索引迭代的读取路径和标签。因此我们需要有一个路径和标签的 ‘容器’供我们读
def __getitem__(self, index):raise NotImplementedError#返回数据的长度
def __len__(self):raise NotImplementedError
def __add__(self, other):return ConcatDataset([self, other])
我们通过继承Dataset类来创建我们自己的数据加载类命名为FaceDataset完整代码如下
import torch
from torch.utils import data
import numpy as np
import pandas as pd
import cv2# 我们通过继承Dataset类来创建我们自己的数据加载类命名为FaceDataset
class FaceDataset(data.Dataset):首先要做的是类的初始化。之前的image-emotion对照表已经创建完毕在加载数据时需用到其中的信息。因此在初始化过程中我们需要完成对image-emotion对照表中数据的读取工作。通过pandas库读取数据随后将读取到的数据放入list或numpy中方便后期索引。# 初始化def __init__(self, root):super(FaceDataset, self).__init__()self.root rootdf_path pd.read_csv(root \\image_emotion.csv, headerNone, usecols[0])df_label pd.read_csv(root \\image_emotion.csv, headerNone, usecols[1])self.path np.array(df_path)[:, 0]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。# 读取某幅图片item为索引号def __getitem__(self, item):face cv2.imread(self.root \\ self.path[item])# 读取单通道灰度图face_gray cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)# 高斯模糊# face_Gus cv2.GaussianBlur(face_gray, (3,3), 0)# 直方图均衡化face_hist cv2.equalizeHist(face_gray)# 像素值标准化face_normalized face_hist.reshape(1, 48, 48) / 255.0 # 为与pytorch中卷积神经网络API的设计相适配需reshape原图# 用于训练的数据需为tensor类型face_tensor torch.from_numpy(face_normalized) # 将python中的numpy数据类型转化为pytorch中的tensor数据类型face_tensor face_tensor.type(torch.FloatTensor) # 指定为torch.FloatTensor型否则送进模型后会因数据类型不匹配而报错label self.label[item]return face_tensor, label最后就是重写len()函数获取数据集大小了。self.path中存储着所有的图片名获取self.path第一维的大小即为数据集的大小。# 获取数据集样本个数def __len__(self):return self.path.shape[0]
3.8 网络模型搭建
这里采用的是基于 CNN 的优化模型这个模型是源于github一个做表情识别的开源项目可惜即使借用了这个项目的模型结构但却没能达到源项目中的精度acc在74%。下图为该开源项目中公布的两个模型结构这里我采用的是 Model B 且只采用了其中的卷积-全连接部分如果大家希望进一步提高模型的表现能力可以参考项目的说明文档考虑向模型中添加 Face landmarks HOG features 部分。
开源项目地址GitHub - amineHorseman/facial-expression-recognition-using-cnn: Deep facial expressions recognition using Opencv and Tensorflow. Recognizing facial expressions from images or camera stream 从下图我们可以看出在 Model B 的卷积部分输入图片 shape 为 48X48X1经过一个3X3X64卷积核的卷积操作再进行一次 2X 2的池化得到一个 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拉一个成长度为 6X6X2569216 的一维 tensor。随后数据经过 dropout 后被送进一层含有4096个神经元的隐层再次经过 dropout 后被送进一层含有 1024 个神经元的隐层之后经过一层含 256 个神经元的隐层最终经过含有7个神经元的输出层。一般再输出层后都会加上 softmax 层取概率最高的类别为分类结果。 接着我们可以通过继承nn.Module来定义自己的模型类。以下代码实现了上述的模型结构。需要注意的是在代码中数据经过最后含7个神经元的线性层后就直接输出了并没有经过softmax层。这是为什么呢其实这和Pytorch在这一块的设计机制有关。因为在实际应用中softmax层常常和交叉熵这种损失函数联合使用因此Pytorch在设计时就将softmax运算集成到了交叉熵损失函数CrossEntropyLoss()内部如果使用交叉熵作为损失函数就默认在计算损失函数前自动进行softmax操作不需要我们额外加softmax层。Tensorflow也有类似的机制。
模型代码如下
import torch
import torch.utils.data as data
import torch.nn as nn
import torch.optim as optim
import numpy as np
import pandas as pd
import cv2# 参数初始化
def gaussian_weights_init(m):classname m.__class__.__name__# 字符串查找find找不到返回-1不等-1即字符串中含有该字符if classname.find(Conv) ! -1:m.weight.data.normal_(0.0, 0.04)# 验证模型在验证集上的正确率
def validate(model, dataset, batch_size):val_loader data.DataLoader(dataset, batch_size)result, num 0.0, 0for images, labels in val_loader:pred model.forward(images)pred np.argmax(pred.data.numpy(), axis1)labels labels.data.numpy()result np.sum((pred labels))num len(images)acc result / numreturn accclass 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-32*1)/11 48nn.Conv2d(in_channels1, out_channels64, kernel_size3, stride1, padding1), # 卷积层nn.BatchNorm2d(num_features64), # 归一化nn.RReLU(inplaceTrue), # 激活函数# output(bitch_size, 64, 24, 24)nn.MaxPool2d(kernel_size2, stride2), # 最大值池化)# 第二次卷积、池化self.conv2 nn.Sequential(# input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-32*1)/11 24nn.Conv2d(in_channels64, out_channels128, kernel_size3, stride1, padding1),nn.BatchNorm2d(num_features128),nn.RReLU(inplaceTrue),# output:(bitch_size, 128, 12 ,12)nn.MaxPool2d(kernel_size2, stride2),)# 第三次卷积、池化self.conv3 nn.Sequential(# input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-32*1)/11 12nn.Conv2d(in_channels128, out_channels256, kernel_size3, stride1, padding1),nn.BatchNorm2d(num_features256),nn.RReLU(inplaceTrue),# output:(bitch_size, 256, 6 ,6)nn.MaxPool2d(kernel_size2, stride2),)# 参数初始化self.conv1.apply(gaussian_weights_init)self.conv2.apply(gaussian_weights_init)self.conv3.apply(gaussian_weights_init)# 全连接层self.fc nn.Sequential(nn.Dropout(p0.2),nn.Linear(in_features256*6*6, out_features4096),nn.RReLU(inplaceTrue),nn.Dropout(p0.5),nn.Linear(in_features4096, out_features1024),nn.RReLU(inplaceTrue),nn.Linear(in_features1024, out_features256),nn.RReLU(inplaceTrue),nn.Linear(in_features256, out_features7),)# 前向传播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
有了模型就可以通过数据的前向传播和误差的反向传播来训练模型了。在此之前还需要指定优化器即学习率更新的方式、损失函数以及训练轮数、学习率等超参数。
在本项目中采用的优化器是SGD即随机梯度下降其中参数weight_decay为正则项系数损失函数采用的是交叉熵可以考虑使用学习率衰减。
训练模型代码如下
def train(train_dataset, val_dataset, batch_size, epochs, learning_rate, wt_decay):# 载入数据并分割batchtrain_loader data.DataLoader(train_dataset, batch_size)# 构建模型model FaceCNN()# 损失函数loss_function nn.CrossEntropyLoss()# 优化器optimizer optim.SGD(model.parameters(), lrlearning_rate, weight_decaywt_decay)# 学习率衰减# scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.8)# 逐轮训练for epoch in range(epochs):# 记录损失值loss_rate 0# scheduler.step() # 学习率衰减model.train() # 模型训练for images, emotion in train_loader:# 梯度清零optimizer.zero_grad()# 前向传播output model.forward(images)# 误差计算loss_rate loss_function(output, emotion)# 误差的反向传播loss_rate.backward()# 更新参数optimizer.step()# 打印每轮的损失print(After {} epochs , the loss_rate is : .format(epoch1), 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(epoch1), acc_train)print(After {} epochs , the acc_val is : .format(epoch1), acc_val)return model
3.9 数据集的使用 完整的 model_CNN.py 代码如下
import torch
import torch.utils.data as data
import torch.nn as nn
import torch.optim as optim
import numpy as np
import pandas as pd
import cv2# 参数初始化
def gaussian_weights_init(m):classname m.__class__.__name__# 字符串查找find找不到返回-1不等-1即字符串中含有该字符if classname.find(Conv) ! -1:m.weight.data.normal_(0.0, 0.04)# 验证模型在验证集上的正确率
def validate(model, dataset, batch_size):val_loader data.DataLoader(dataset, batch_size)result, num 0.0, 0for images, labels in val_loader:pred model.forward(images)pred np.argmax(pred.data.numpy(), axis1)labels labels.data.numpy()result np.sum((pred labels))num len(images)acc result / numreturn acc# 我们通过继承Dataset类来创建我们自己的数据加载类命名为FaceDataset
class FaceDataset(data.Dataset):首先要做的是类的初始化。之前的image-emotion对照表已经创建完毕在加载数据时需用到其中的信息。因此在初始化过程中我们需要完成对image-emotion对照表中数据的读取工作。通过pandas库读取数据随后将读取到的数据放入list或numpy中方便后期索引。# 初始化def __init__(self, root):super(FaceDataset, self).__init__()self.root rootdf_path pd.read_csv(root \\image_emotion.csv, headerNone, usecols[0])df_label pd.read_csv(root \\image_emotion.csv, headerNone, usecols[1])self.path np.array(df_path)[:, 0]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。# 读取某幅图片item为索引号def __getitem__(self, item):face cv2.imread(self.root \\ self.path[item])# 读取单通道灰度图face_gray cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)# 高斯模糊# face_Gus cv2.GaussianBlur(face_gray, (3,3), 0)# 直方图均衡化face_hist cv2.equalizeHist(face_gray)# 像素值标准化face_normalized face_hist.reshape(1, 48, 48) / 255.0 # 为与pytorch中卷积神经网络API的设计相适配需reshape原图# 用于训练的数据需为tensor类型face_tensor torch.from_numpy(face_normalized) # 将python中的numpy数据类型转化为pytorch中的tensor数据类型face_tensor face_tensor.type(torch.FloatTensor) # 指定为torch.FloatTensor型否则送进模型后会因数据类型不匹配而报错label self.label[item]return face_tensor, label最后就是重写len()函数获取数据集大小了。self.path中存储着所有的图片名获取self.path第一维的大小即为数据集的大小。# 获取数据集样本个数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-32*1)/11 48nn.Conv2d(in_channels1, out_channels64, kernel_size3, stride1, padding1), # 卷积层nn.BatchNorm2d(num_features64), # 归一化nn.RReLU(inplaceTrue), # 激活函数# output(bitch_size, 64, 24, 24)nn.MaxPool2d(kernel_size2, stride2), # 最大值池化)# 第二次卷积、池化self.conv2 nn.Sequential(# input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-32*1)/11 24nn.Conv2d(in_channels64, out_channels128, kernel_size3, stride1, padding1),nn.BatchNorm2d(num_features128),nn.RReLU(inplaceTrue),# output:(bitch_size, 128, 12 ,12)nn.MaxPool2d(kernel_size2, stride2),)# 第三次卷积、池化self.conv3 nn.Sequential(# input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-32*1)/11 12nn.Conv2d(in_channels128, out_channels256, kernel_size3, stride1, padding1),nn.BatchNorm2d(num_features256),nn.RReLU(inplaceTrue),# output:(bitch_size, 256, 6 ,6)nn.MaxPool2d(kernel_size2, stride2),)# 参数初始化self.conv1.apply(gaussian_weights_init)self.conv2.apply(gaussian_weights_init)self.conv3.apply(gaussian_weights_init)# 全连接层self.fc nn.Sequential(nn.Dropout(p0.2),nn.Linear(in_features256*6*6, out_features4096),nn.RReLU(inplaceTrue),nn.Dropout(p0.5),nn.Linear(in_features4096, out_features1024),nn.RReLU(inplaceTrue),nn.Linear(in_features1024, out_features256),nn.RReLU(inplaceTrue),nn.Linear(in_features256, out_features7),)# 前向传播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 ydef train(train_dataset, val_dataset, batch_size, epochs, learning_rate, wt_decay):# 载入数据并分割batchtrain_loader data.DataLoader(train_dataset, batch_size)# 构建模型model FaceCNN()# 损失函数loss_function nn.CrossEntropyLoss()# 优化器optimizer optim.SGD(model.parameters(), lrlearning_rate, weight_decaywt_decay)# 学习率衰减# scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.8)# 逐轮训练for epoch in range(epochs):# 记录损失值loss_rate 0# scheduler.step() # 学习率衰减model.train() # 模型训练for images, emotion in train_loader:# 梯度清零optimizer.zero_grad()# 前向传播output model.forward(images)# 误差计算loss_rate loss_function(output, emotion)# 误差的反向传播loss_rate.backward()# 更新参数optimizer.step()# 打印每轮的损失print(After {} epochs , the loss_rate is : .format(epoch1), 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(epoch1), acc_train)print(After {} epochs , the acc_val is : .format(epoch1), acc_val)return modeldef main():# 数据集实例化(创建数据集)train_dataset FaceDataset(rootface_images/train_set)val_dataset FaceDataset(rootface_images/verify_set)# 超参数可自行指定model train(train_dataset, val_dataset, batch_size128, epochs100, learning_rate0.1, wt_decay0)# 保存模型torch.save(model, model/model_cnn.pkl)if __name__ __main__:main()
3.10 保存模型
运行model_CNN.py模型代码 生成模型并保存 3.11 模型的测试
3.11.1 加载模型 3.11.2 人脸面部表情识别测试
自己测试看看自己想表达的表情和识别的结果是否一致 使用视频进行测试 3.12 模型的优化
通过模型的测试对于自己的面部表情识别我自己想表达的表情和识别的结果匹配还是有些不一致的正确率有误差以及在视频测试中我认为视频里的人的表情和实际识别的结果也有差异对于测试中存在的问题有以下原因 训练的数据集还不够 训练的模型不够完善
因此我们可以用理论部分提出的另外两种模型VGG模型和ResNet模型对现有的项目进行优化
3.12.1 采用VGG模型优化
有关VGG模型的代码原型我在templates文件夹下整理好了给大家。数据集Flowers放在了dataset目录下大家记得在代码原型里面更改相关路径的一些配置。 接下来根据VGG模型的原理我们可以通过继承nn.Module来定义我们自己的基于VGG的模型类最后将我们自定义的VGG网络模型进行搭建。
模型的代码如下
class VGG(nn.Module):def __init__(self, *args):super(VGG, self).__init__()def forward(self, x):return x.view(x.shape[0],-1)def vgg_block(num_convs, in_channels, out_channels):blk []for i in range(num_convs):if i 0:blk.append(nn.Conv2d(in_channels, out_channels, kernel_size3, padding1))else:blk.append(nn.Conv2d(out_channels, out_channels, kernel_size3, padding1))blk.append(nn.ReLU())blk.append(nn.MaxPool2d(kernel_size2, stride2)) # 这里会使宽高减半return nn.Sequential(*blk)conv_arch ((2, 1, 32), (3, 32, 64), (3, 64, 128))
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 7
fc_features 128 * 6* 6 # c * w * h
fc_hidden_units 4096 # 任意def vgg(conv_arch, fc_features, fc_hidden_units):net nn.Sequential()# 卷积层部分for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):# 每经过一个vgg_block都会使宽高减半net.add_module(vgg_block_ str(i1), vgg_block(num_convs, in_channels, out_channels))# 全连接层部分net.add_module(fc, nn.Sequential(VGG(),nn.Linear(fc_features, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, 7)))return net
将模型搭建好之后我们可以来训练模型了训练模型的代码如下train_loss []
train_ac []
vaild_loss []
vaild_ac []
y_pred []def train(model,device,dataset,optimizer,epoch):model.train()correct 0for i,(x,y) in tqdm(enumerate(dataset)):x , y x.to(device), y.to(device)optimizer.zero_grad()output model(x)pred output.max(1,keepdimTrue)[1]correct pred.eq(y.view_as(pred)).sum().item()loss criterion(output,y) loss.backward()optimizer.step() train_ac.append(correct/len(data_train)) train_loss.append(loss.item())print(Epoch {} Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(epoch,loss,correct,len(data_train),100*correct/len(data_train)))def vaild(model,device,dataset):model.eval()correct 0with torch.no_grad():for i,(x,y) in tqdm(enumerate(dataset)):x,y x.to(device) ,y.to(device)output model(x)loss criterion(output,y)pred output.max(1,keepdimTrue)[1]global y_pred y_pred pred.view(pred.size()[0]).cpu().numpy().tolist()correct pred.eq(y.view_as(pred)).sum().item()vaild_ac.append(correct/len(data_vaild)) vaild_loss.append(loss.item())print(Test Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(loss,correct,len(data_vaild),100.*correct/len(data_vaild)))
训练后我们将训练好的模型进行保存由于我的电脑配置比较拉跨且用的是CPU所以训练模型时间需要特别久在这里就不等待模型最终的训练结果了大家可以自己去试试。 完整的model_VGG.py代码如下
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from tqdm import tqdmBATCH_SIZE 128
LR 0.01
EPOCH 60
DEVICE torch.device(cpu)path_train face_images/vgg_train_set
path_vaild face_images/vgg_vaild_settransforms_train transforms.Compose([transforms.Grayscale(),#使用ImageFolder默认扩展为三通道重新变回去就行transforms.RandomHorizontalFlip(),#随机翻转transforms.ColorJitter(brightness0.5, contrast0.5),#随机调整亮度和对比度transforms.ToTensor()
])
transforms_vaild transforms.Compose([transforms.Grayscale(),transforms.ToTensor()
])data_train torchvision.datasets.ImageFolder(rootpath_train,transformtransforms_train)
data_vaild torchvision.datasets.ImageFolder(rootpath_vaild,transformtransforms_vaild)train_set torch.utils.data.DataLoader(datasetdata_train,batch_sizeBATCH_SIZE,shuffleTrue)
vaild_set torch.utils.data.DataLoader(datasetdata_vaild,batch_sizeBATCH_SIZE,shuffleFalse)class VGG(nn.Module):def __init__(self, *args):super(VGG, self).__init__()def forward(self, x):return x.view(x.shape[0],-1)def vgg_block(num_convs, in_channels, out_channels):blk []for i in range(num_convs):if i 0:blk.append(nn.Conv2d(in_channels, out_channels, kernel_size3, padding1))else:blk.append(nn.Conv2d(out_channels, out_channels, kernel_size3, padding1))blk.append(nn.ReLU())blk.append(nn.MaxPool2d(kernel_size2, stride2)) # 这里会使宽高减半return nn.Sequential(*blk)conv_arch ((2, 1, 32), (3, 32, 64), (3, 64, 128))
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 7
fc_features 128 * 6* 6 # c * w * h
fc_hidden_units 4096 # 任意def vgg(conv_arch, fc_features, fc_hidden_units):net nn.Sequential()# 卷积层部分for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):# 每经过一个vgg_block都会使宽高减半net.add_module(vgg_block_ str(i1), vgg_block(num_convs, in_channels, out_channels))# 全连接层部分net.add_module(fc, nn.Sequential(VGG(),nn.Linear(fc_features, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, 7)))return netmodel vgg(conv_arch, fc_features, fc_hidden_units)
model.to(DEVICE)
optimizer optim.SGD(model.parameters(),lrLR,momentum0.9)#optim.Adam(model.parameters())
criterion nn.CrossEntropyLoss()train_loss []
train_ac []
vaild_loss []
vaild_ac []
y_pred []def train(model,device,dataset,optimizer,epoch):model.train()correct 0for i,(x,y) in tqdm(enumerate(dataset)):x , y x.to(device), y.to(device)optimizer.zero_grad()output model(x)pred output.max(1,keepdimTrue)[1]correct pred.eq(y.view_as(pred)).sum().item()loss criterion(output,y) loss.backward()optimizer.step() train_ac.append(correct/len(data_train)) train_loss.append(loss.item())print(Epoch {} Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(epoch,loss,correct,len(data_train),100*correct/len(data_train)))def vaild(model,device,dataset):model.eval()correct 0with torch.no_grad():for i,(x,y) in tqdm(enumerate(dataset)):x,y x.to(device) ,y.to(device)output model(x)loss criterion(output,y)pred output.max(1,keepdimTrue)[1]global y_pred y_pred pred.view(pred.size()[0]).cpu().numpy().tolist()correct pred.eq(y.view_as(pred)).sum().item()vaild_ac.append(correct/len(data_vaild)) vaild_loss.append(loss.item())print(Test Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(loss,correct,len(data_vaild),100.*correct/len(data_vaild)))def RUN():for epoch in range(1,EPOCH1):if epoch15 :LR 0.1optimizeroptimizer optim.SGD(model.parameters(),lrLR,momentum0.9)if(epoch30 and epoch%150):LR*0.1optimizeroptimizer optim.SGD(model.parameters(),lrLR,momentum0.9)#尝试动态学习率train(model,deviceDEVICE,datasettrain_set,optimizeroptimizer,epochepoch)vaild(model,deviceDEVICE,datasetvaild_set)#保存模型torch.save(model,model/model_vgg.pkl)if __name__ __main__:RUN()
最后我们只需要拿到我们训练好的model_vgg.pkl模型放到模型测试代码里面现实地测试一下同CNN一样测试自己和测试视频看看效果如何将识别结果和实际预期进行对比看看是否比原来地CNN模型更加准确。(在此不再演示)
完整的model_VGG_test.py代码如下
# -*- coding: utf-8 -*-
import cv2
import torch
import torch.nn as nn
import numpy as np
from statistics import mode# 人脸数据归一化,将像素值从0-255映射到0-1之间
def preprocess_input(images): preprocess input by substracting the train mean# Arguments: images or image of any shape# Returns: images or image with substracted train mean (129)images images/255.0return imagesclass VGG(nn.Module):def __init__(self, *args):super(VGG, self).__init__()def forward(self, x):return x.view(x.shape[0],-1)def vgg_block(num_convs, in_channels, out_channels):blk []for i in range(num_convs):if i 0:blk.append(nn.Conv2d(in_channels, out_channels, kernel_size3, padding1))else:blk.append(nn.Conv2d(out_channels, out_channels, kernel_size3, padding1))blk.append(nn.ReLU())blk.append(nn.MaxPool2d(kernel_size2, stride2)) # 这里会使宽高减半return nn.Sequential(*blk)conv_arch ((2, 1, 32), (3, 32, 64), (3, 64, 128))
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 7
fc_features 128 * 6* 6 # c * w * h
fc_hidden_units 4096 # 任意def vgg(conv_arch, fc_features, fc_hidden_units):net nn.Sequential()# 卷积层部分for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):# 每经过一个vgg_block都会使宽高减半net.add_module(vgg_block_ str(i1), vgg_block(num_convs, in_channels, out_channels))# 全连接层部分net.add_module(fc, nn.Sequential(VGG(),nn.Linear(fc_features, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, 7)))return net#opencv自带的一个面部识别分类器
detection_model_path model/haarcascade_frontalface_default.xmlclassification_model_path model/model_vgg.pkl# 加载人脸检测模型
face_detection cv2.CascadeClassifier(detection_model_path)# 加载表情识别模型
emotion_classifier torch.load(classification_model_path)frame_window 10#表情标签
emotion_labels {0: angry, 1: disgust, 2: fear, 3: happy, 4: sad, 5: surprise, 6: neutral}emotion_window []# 调起摄像头0是笔记本自带摄像头
video_capture cv2.VideoCapture(0)
# 视频文件识别
# video_capture cv2.VideoCapture(video/example_dsh.mp4)
font cv2.FONT_HERSHEY_SIMPLEX
cv2.startWindowThread()
cv2.namedWindow(window_frame)while True:# 读取一帧_, frame video_capture.read()frame frame[:,::-1,:]#水平翻转符合自拍习惯frame frame.copy()# 获得灰度图并且在内存中创建一个图像对象gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)# 获取当前帧中的全部人脸faces face_detection.detectMultiScale(gray,1.3,5)# 对于所有发现的人脸for (x, y, w, h) in faces:# 在脸周围画一个矩形框(255,0,0)是颜色2是线宽cv2.rectangle(frame,(x,y),(xw,yh),(84,255,159),2)# 获取人脸图像face gray[y:yh,x:xw]try:# shape变为(48,48)face cv2.resize(face,(48,48))except:continue# 扩充维度shape变为(1,48,48,1)#将148481转换成为(1,1,48,48)face np.expand_dims(face,0)face np.expand_dims(face,0)# 人脸数据归一化将像素值从0-255映射到0-1之间face preprocess_input(face)new_facetorch.from_numpy(face)new_new_face new_face.float().requires_grad_(False)# 调用我们训练好的表情识别模型预测分类emotion_arg np.argmax(emotion_classifier.forward(new_new_face).detach().numpy())emotion emotion_labels[emotion_arg]emotion_window.append(emotion)if len(emotion_window) frame_window:emotion_window.pop(0)try:# 获得出现次数最多的分类emotion_mode mode(emotion_window)except:continue# 在矩形框上部输出分类文字cv2.putText(frame,emotion_mode,(x,y-30), font, .7,(0,0,255),1,cv2.LINE_AA)try:# 将图片从内存中显示到屏幕上cv2.imshow(window_frame, frame)except:continue# 按q退出if cv2.waitKey(1) 0xFF ord(q):breakvideo_capture.release()
cv2.destroyAllWindows()
3.12.2 采用ResNet模型优化
有关ResNet模型的代码原型我也在templates文件夹下整理好了给大家。数据集Flowers放在了dataset目录下大家记得在代码原型里面更改相关路径的一些配置。 同样的根据ResNet模型的原理我们可以通过继承nn.Module来定义我们自己的基于ResNet的模型类最后将我们自定义的ResNet网络模型进行搭建。
模型的代码如下
class Reshape(nn.Module):def __init__(self, *args):super(Reshape, self).__init__()class GlobalAvgPool2d(nn.Module):# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现def __init__(self):super(GlobalAvgPool2d, self).__init__()def forward(self, x):return F.avg_pool2d(x, kernel_sizex.size()[2:])# 残差神经网络
class Residual(nn.Module): def __init__(self, in_channels, out_channels, use_1x1convFalse, stride1):super(Residual, self).__init__()self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, padding1, stridestride)self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1)if use_1x1conv:self.conv3 nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride)else:self.conv3 Noneself.bn1 nn.BatchNorm2d(out_channels)self.bn2 nn.BatchNorm2d(out_channels)def forward(self, X):Y F.relu(self.bn1(self.conv1(X)))Y self.bn2(self.conv2(Y))if self.conv3:X self.conv3(X)return F.relu(Y X)def resnet_block(in_channels, out_channels, num_residuals, first_blockFalse):if first_block:assert in_channels out_channels # 第一个模块的通道数同输入通道数一致blk []for i in range(num_residuals):if i 0 and not first_block:blk.append(Residual(in_channels, out_channels, use_1x1convTrue, stride2))else:blk.append(Residual(out_channels, out_channels))return nn.Sequential(*blk)resnet nn.Sequential(nn.Conv2d(1, 64, kernel_size7 , stride2, padding3),nn.BatchNorm2d(64), nn.ReLU(),nn.MaxPool2d(kernel_size3, stride2, padding1))
resnet.add_module(resnet_block1, resnet_block(64, 64, 2, first_blockTrue))
resnet.add_module(resnet_block2, resnet_block(64, 128, 2))
resnet.add_module(resnet_block3, resnet_block(128, 256, 2))
resnet.add_module(resnet_block4, resnet_block(256, 512, 2))
resnet.add_module(global_avg_pool, GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
resnet.add_module(fc, nn.Sequential(Reshape(), nn.Linear(512, 7)))
将模型搭建好之后我们可以来训练并模型了步骤和VGG的大同小异这里不赘述。由于我的电脑配置比较拉跨且用的是CPU所以训练模型时间需要特别久在这里就不等待模型最终的训练结果了大家可以自己去试试。 完整的model_ResNet.py训练模型的代码如下
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from tqdm import tqdmBATCH_SIZE 128
LR 0.01
EPOCH 60
DEVICE torch.device(cpu)path_train face_images/resnet_train_set
path_vaild face_images/resnet_vaild_settransforms_train transforms.Compose([transforms.Grayscale(),#使用ImageFolder默认扩展为三通道重新变回去就行transforms.RandomHorizontalFlip(),#随机翻转transforms.ColorJitter(brightness0.5, contrast0.5),#随机调整亮度和对比度transforms.ToTensor()
])
transforms_vaild transforms.Compose([transforms.Grayscale(),transforms.ToTensor()
])data_train torchvision.datasets.ImageFolder(rootpath_train,transformtransforms_train)
data_vaild torchvision.datasets.ImageFolder(rootpath_vaild,transformtransforms_vaild)train_set torch.utils.data.DataLoader(datasetdata_train,batch_sizeBATCH_SIZE,shuffleTrue)
vaild_set torch.utils.data.DataLoader(datasetdata_vaild,batch_sizeBATCH_SIZE,shuffleFalse)class ResNet(nn.Module):def __init__(self, *args):super(ResNet, self).__init__()def forward(self, x):return x.view(x.shape[0],-1)class GlobalAvgPool2d(nn.Module):# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现def __init__(self):super(GlobalAvgPool2d, self).__init__()def forward(self, x):return F.avg_pool2d(x, kernel_sizex.size()[2:])# 残差神经网络
class Residual(nn.Module): def __init__(self, in_channels, out_channels, use_1x1convFalse, stride1):super(Residual, self).__init__()self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, padding1, stridestride)self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1)if use_1x1conv:self.conv3 nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride)else:self.conv3 Noneself.bn1 nn.BatchNorm2d(out_channels)self.bn2 nn.BatchNorm2d(out_channels)def forward(self, X):Y F.relu(self.bn1(self.conv1(X)))Y self.bn2(self.conv2(Y))if self.conv3:X self.conv3(X)return F.relu(Y X)def resnet_block(in_channels, out_channels, num_residuals, first_blockFalse):if first_block:assert in_channels out_channels # 第一个模块的通道数同输入通道数一致blk []for i in range(num_residuals):if i 0 and not first_block:blk.append(Residual(in_channels, out_channels, use_1x1convTrue, stride2))else:blk.append(Residual(out_channels, out_channels))return nn.Sequential(*blk)resnet nn.Sequential(nn.Conv2d(1, 64, kernel_size7 , stride2, padding3),nn.BatchNorm2d(64), nn.ReLU(),nn.MaxPool2d(kernel_size3, stride2, padding1))
resnet.add_module(resnet_block1, resnet_block(64, 64, 2, first_blockTrue))
resnet.add_module(resnet_block2, resnet_block(64, 128, 2))
resnet.add_module(resnet_block3, resnet_block(128, 256, 2))
resnet.add_module(resnet_block4, resnet_block(256, 512, 2))
resnet.add_module(global_avg_pool, GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
resnet.add_module(fc, nn.Sequential(ResNet(), nn.Linear(512, 7))) model resnet
model.to(DEVICE)
optimizer optim.SGD(model.parameters(),lrLR,momentum0.9)#optim.Adam(model.parameters())
criterion nn.CrossEntropyLoss()train_loss []
train_ac []
vaild_loss []
vaild_ac []
y_pred []def train(model,device,dataset,optimizer,epoch):model.train()correct 0for i,(x,y) in tqdm(enumerate(dataset)):x , y x.to(device), y.to(device)optimizer.zero_grad()output model(x)pred output.max(1,keepdimTrue)[1]correct pred.eq(y.view_as(pred)).sum().item()loss criterion(output,y) loss.backward()optimizer.step() train_ac.append(correct/len(data_train)) train_loss.append(loss.item())print(Epoch {} Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(epoch,loss,correct,len(data_train),100*correct/len(data_train)))def vaild(model,device,dataset):model.eval()correct 0with torch.no_grad():for i,(x,y) in tqdm(enumerate(dataset)):x,y x.to(device) ,y.to(device)output model(x)loss criterion(output,y)pred output.max(1,keepdimTrue)[1]global y_pred y_pred pred.view(pred.size()[0]).cpu().numpy().tolist()correct pred.eq(y.view_as(pred)).sum().item()vaild_ac.append(correct/len(data_vaild)) vaild_loss.append(loss.item())print(Test Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(loss,correct,len(data_vaild),100.*correct/len(data_vaild)))def RUN():for epoch in range(1,EPOCH1):if epoch15 :LR 0.1optimizeroptimizer optim.SGD(model.parameters(),lrLR,momentum0.9)if(epoch30 and epoch%150):LR*0.1optimizeroptimizer optim.SGD(model.parameters(),lrLR,momentum0.9)#尝试动态学习率train(model,deviceDEVICE,datasettrain_set,optimizeroptimizer,epochepoch)vaild(model,deviceDEVICE,datasetvaild_set)torch.save(model,model/model_resnet.pkl)if __name__ __main__:RUN()
最后我们只需要拿到我们训练好的model_resnet.pkl模型放到模型测试代码里面现实地测试一下同CNN一样测试自己和测试视频看看效果如何将识别结果和实际预期进行对比看看是否比原来地CNN模型更加准确。(在此不再演示)
完整的model_ResNet_test.py代码如下
# -*- coding: utf-8 -*-
import cv2
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from statistics import mode# 人脸数据归一化,将像素值从0-255映射到0-1之间
def preprocess_input(images): preprocess input by substracting the train mean# Arguments: images or image of any shape# Returns: images or image with substracted train mean (129)images images/255.0return imagesclass ResNet(nn.Module):def __init__(self, *args):super(ResNet, self).__init__()def forward(self, x):return x.view(x.shape[0],-1)class GlobalAvgPool2d(nn.Module):# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现def __init__(self):super(GlobalAvgPool2d, self).__init__()def forward(self, x):return F.avg_pool2d(x, kernel_sizex.size()[2:])# 残差神经网络
class Residual(nn.Module): def __init__(self, in_channels, out_channels, use_1x1convFalse, stride1):super(Residual, self).__init__()self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, padding1, stridestride)self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1)if use_1x1conv:self.conv3 nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride)else:self.conv3 Noneself.bn1 nn.BatchNorm2d(out_channels)self.bn2 nn.BatchNorm2d(out_channels)def forward(self, X):Y F.relu(self.bn1(self.conv1(X)))Y self.bn2(self.conv2(Y))if self.conv3:X self.conv3(X)return F.relu(Y X)def resnet_block(in_channels, out_channels, num_residuals, first_blockFalse):if first_block:assert in_channels out_channels # 第一个模块的通道数同输入通道数一致blk []for i in range(num_residuals):if i 0 and not first_block:blk.append(Residual(in_channels, out_channels, use_1x1convTrue, stride2))else:blk.append(Residual(out_channels, out_channels))return nn.Sequential(*blk)resnet nn.Sequential(nn.Conv2d(1, 64, kernel_size7 , stride2, padding3),nn.BatchNorm2d(64), nn.ReLU(),nn.MaxPool2d(kernel_size3, stride2, padding1))
resnet.add_module(resnet_block1, resnet_block(64, 64, 2, first_blockTrue))
resnet.add_module(resnet_block2, resnet_block(64, 128, 2))
resnet.add_module(resnet_block3, resnet_block(128, 256, 2))
resnet.add_module(resnet_block4, resnet_block(256, 512, 2))
resnet.add_module(global_avg_pool, GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
resnet.add_module(fc, nn.Sequential(ResNet(), nn.Linear(512, 7))) #opencv自带的一个面部识别分类器
detection_model_path model/haarcascade_frontalface_default.xmlclassification_model_path model/model_resnet.pkl# 加载人脸检测模型
face_detection cv2.CascadeClassifier(detection_model_path)# 加载表情识别模型
emotion_classifier torch.load(classification_model_path)frame_window 10#表情标签
emotion_labels {0: angry, 1: disgust, 2: fear, 3: happy, 4: sad, 5: surprise, 6: neutral}emotion_window []# 调起摄像头0是笔记本自带摄像头
video_capture cv2.VideoCapture(0)
# 视频文件识别
# video_capture cv2.VideoCapture(video/example_dsh.mp4)
font cv2.FONT_HERSHEY_SIMPLEX
cv2.startWindowThread()
cv2.namedWindow(window_frame)while True:# 读取一帧_, frame video_capture.read()frame frame[:,::-1,:]#水平翻转符合自拍习惯frame frame.copy()# 获得灰度图并且在内存中创建一个图像对象gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)# 获取当前帧中的全部人脸faces face_detection.detectMultiScale(gray,1.3,5)# 对于所有发现的人脸for (x, y, w, h) in faces:# 在脸周围画一个矩形框(255,0,0)是颜色2是线宽cv2.rectangle(frame,(x,y),(xw,yh),(84,255,159),2)# 获取人脸图像face gray[y:yh,x:xw]try:# shape变为(48,48)face cv2.resize(face,(48,48))except:continue# 扩充维度shape变为(1,48,48,1)#将148481转换成为(1,1,48,48)face np.expand_dims(face,0)face np.expand_dims(face,0)# 人脸数据归一化将像素值从0-255映射到0-1之间face preprocess_input(face)new_facetorch.from_numpy(face)new_new_face new_face.float().requires_grad_(False)# 调用我们训练好的表情识别模型预测分类emotion_arg np.argmax(emotion_classifier.forward(new_new_face).detach().numpy())emotion emotion_labels[emotion_arg]emotion_window.append(emotion)if len(emotion_window) frame_window:emotion_window.pop(0)try:# 获得出现次数最多的分类emotion_mode mode(emotion_window)except:continue# 在矩形框上部输出分类文字cv2.putText(frame,emotion_mode,(x,y-30), font, .7,(0,0,255),1,cv2.LINE_AA)try:# 将图片从内存中显示到屏幕上cv2.imshow(window_frame, frame)except:continue# 按q退出if cv2.waitKey(1) 0xFF ord(q):breakvideo_capture.release()
cv2.destroyAllWindows()
3.13 模型的对比分析 CNNVGGResNet
模型的对比分析完整代码如下
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import numpy as np
import pandas as pd
from PIL import Image
import os
import matplotlib.pyplot as plt
from tqdm import tqdmBATCH_SIZE 128
LR 0.01
EPOCH 60
DEVICE torch.device(cpu)path_train 你选定模型的数据集
path_vaild 你选定模型的验证集transforms_train transforms.Compose([transforms.Grayscale(),#使用ImageFolder默认扩展为三通道重新变回去就行transforms.RandomHorizontalFlip(),#随机翻转transforms.ColorJitter(brightness0.5, contrast0.5),#随机调整亮度和对比度transforms.ToTensor()
])
transforms_vaild transforms.Compose([transforms.Grayscale(),transforms.ToTensor()
])data_train torchvision.datasets.ImageFolder(rootpath_train,transformtransforms_train)
data_vaild torchvision.datasets.ImageFolder(rootpath_vaild,transformtransforms_vaild)train_set torch.utils.data.DataLoader(datasetdata_train,batch_sizeBATCH_SIZE,shuffleTrue)
vaild_set torch.utils.data.DataLoader(datasetdata_vaild,batch_sizeBATCH_SIZE,shuffleFalse)class Reshape(nn.Module):def __init__(self, *args):super(Reshape, self).__init__()def forward(self, x):return x.view(x.shape[0],-1)class GlobalAvgPool2d(nn.Module):# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现def __init__(self):super(GlobalAvgPool2d, self).__init__()def forward(self, x):return F.avg_pool2d(x, kernel_sizex.size()[2:])CNN nn.Sequential(nn.Conv2d(1,64,3),nn.ReLU(True),nn.MaxPool2d(2,2),nn.Conv2d(64,256,3),nn.ReLU(True),nn.MaxPool2d(3,3),Reshape(),nn.Linear(256*7*7,4096),nn.ReLU(True),nn.Linear(4096,1024),nn.ReLU(True),nn.Linear(1024,7))def vgg_block(num_convs, in_channels, out_channels):blk []for i in range(num_convs):if i 0:blk.append(nn.Conv2d(in_channels, out_channels, kernel_size3, padding1))else:blk.append(nn.Conv2d(out_channels, out_channels, kernel_size3, padding1))blk.append(nn.ReLU())blk.append(nn.MaxPool2d(kernel_size2, stride2)) # 这里会使宽高减半return nn.Sequential(*blk)conv_arch ((2, 1, 32), (3, 32, 64), (3, 64, 128))
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 7
fc_features 128 * 6* 6 # c * w * h
fc_hidden_units 4096 # 任意def vgg(conv_arch, fc_features, fc_hidden_units):net nn.Sequential()# 卷积层部分for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):# 每经过一个vgg_block都会使宽高减半net.add_module(vgg_block_ str(i1), vgg_block(num_convs, in_channels, out_channels))# 全连接层部分net.add_module(fc, nn.Sequential(Reshape(),nn.Linear(fc_features, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, 7)))return net# 残差神经网络
class Residual(nn.Module): def __init__(self, in_channels, out_channels, use_1x1convFalse, stride1):super(Residual, self).__init__()self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, padding1, stridestride)self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1)if use_1x1conv:self.conv3 nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride)else:self.conv3 Noneself.bn1 nn.BatchNorm2d(out_channels)self.bn2 nn.BatchNorm2d(out_channels)def forward(self, X):Y F.relu(self.bn1(self.conv1(X)))Y self.bn2(self.conv2(Y))if self.conv3:X self.conv3(X)return F.relu(Y X)def resnet_block(in_channels, out_channels, num_residuals, first_blockFalse):if first_block:assert in_channels out_channels # 第一个模块的通道数同输入通道数一致blk []for i in range(num_residuals):if i 0 and not first_block:blk.append(Residual(in_channels, out_channels, use_1x1convTrue, stride2))else:blk.append(Residual(out_channels, out_channels))return nn.Sequential(*blk)resnet nn.Sequential(nn.Conv2d(1, 64, kernel_size7 , stride2, padding3),nn.BatchNorm2d(64), nn.ReLU(),nn.MaxPool2d(kernel_size3, stride2, padding1))
resnet.add_module(resnet_block1, resnet_block(64, 64, 2, first_blockTrue))
resnet.add_module(resnet_block2, resnet_block(64, 128, 2))
resnet.add_module(resnet_block3, resnet_block(128, 256, 2))
resnet.add_module(resnet_block4, resnet_block(256, 512, 2))
resnet.add_module(global_avg_pool, GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
resnet.add_module(fc, nn.Sequential(Reshape(), nn.Linear(512, 7))) # 用那个模型就切换注释即可
model CNN
#model resnet
#model vgg(conv_arch, fc_features, fc_hidden_units)
model.to(DEVICE)
optimizer optim.SGD(model.parameters(),lrLR,momentum0.9)#optim.Adam(model.parameters())
criterion nn.CrossEntropyLoss()print(model)train_loss []
train_ac []
vaild_loss []
vaild_ac []
y_pred []def train(model,device,dataset,optimizer,epoch):model.train()correct 0for i,(x,y) in tqdm(enumerate(dataset)):x , y x.to(device), y.to(device)optimizer.zero_grad()output model(x)pred output.max(1,keepdimTrue)[1]correct pred.eq(y.view_as(pred)).sum().item()loss criterion(output,y) loss.backward()optimizer.step() train_ac.append(correct/len(data_train)) train_loss.append(loss.item())print(Epoch {} Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(epoch,loss,correct,len(data_train),100*correct/len(data_train)))def vaild(model,device,dataset):model.eval()correct 0with torch.no_grad():for i,(x,y) in tqdm(enumerate(dataset)):x,y x.to(device) ,y.to(device)output model(x)loss criterion(output,y)pred output.max(1,keepdimTrue)[1]global y_pred y_pred pred.view(pred.size()[0]).cpu().numpy().tolist()correct pred.eq(y.view_as(pred)).sum().item()vaild_ac.append(correct/len(data_vaild)) vaild_loss.append(loss.item())print(Test Loss {:.4f} Accuracy {}/{} ({:.0f}%).format(loss,correct,len(data_vaild),100.*correct/len(data_vaild)))def RUN():for epoch in range(1,EPOCH1):if epoch15 :LR 0.1optimizeroptimizer optim.SGD(model.parameters(),lrLR,momentum0.9)if(epoch30 and epoch%150):LR*0.1optimizeroptimizer optim.SGD(model.parameters(),lrLR,momentum0.9)#尝试动态学习率train(model,deviceDEVICE,datasettrain_set,optimizeroptimizer,epochepoch)vaild(model,deviceDEVICE,datasetvaild_set)torch.save(model,m0.pth)RUN()
#vaild(model,deviceDEVICE,datasetvaild_set)def print_plot(train_plot,vaild_plot,train_text,vaild_text,ac,name): x [i for i in range(1,len(train_plot)1)]plt.plot(x,train_plot,labeltrain_text)plt.plot(x[-1],train_plot[-1],markero)plt.annotate(%.2f%%%(train_plot[-1]*100) if ac else %.4f%(train_plot[-1]),xy(x[-1],train_plot[-1]))plt.plot(x,vaild_plot,labelvaild_text)plt.plot(x[-1],vaild_plot[-1],markero)plt.annotate(%.2f%%%(vaild_plot[-1]*100) if ac else %.4f%(vaild_plot[-1]),xy(x[-1],vaild_plot[-1]))plt.legend()plt.savefig(name)#print_plot(train_loss,vaild_loss,train_loss,vaild_loss,False,loss.jpg)
#print_plot(train_ac,vaild_ac,train_ac,vaild_ac,True,ac.jpg)import seaborn as sns
from sklearn.metrics import confusion_matrixemotion [angry,disgust,fear,happy,sad,surprised,neutral]
sns.set()
f,axplt.subplots()
y_true [ emotion[i] for _,i in data_vaild]
y_pred [emotion[i] for i in y_pred]
C2 confusion_matrix(y_true, y_pred, labels[angry,disgust,fear,happy,sad,surprised,neutral])#[0, 1, 2,3,4,5,6])
#print(C2) #打印出来看看
sns.heatmap(C2,annotTrue ,fmt.20g,axax) #热力图ax.set_title(confusion matrix) #标题
ax.set_xlabel(predict) #x轴
ax.set_ylabel(true) #y轴
plt.savefig(matrix.jpg) 如果你还想看更多的不同模型的训练结果以及相关数据读取处理与分析可以去FER2013数据集官网下载别人的代码参考学习除了看不同模型的对比分析还能学习其他有关深度学习与计算机视觉的知识如基于Tensorflow的实现等 这里我也给大家下载了一些模板参考放在了templates文件夹下 四、总结
这是我校本课程选修课程深度学习与计算机视觉期末大作业。本次作业参考学习了很多文章的经验与方法自己也试着将其归纳总结。完成此次作业也可是不易但也锻炼了自己的学习能力虽然有些知识自己还不能够非常能完全掌握理解但我相信在后续的学习中自己对这方面的知识的理解也会加强许多学无止境希望和大家一起加油进步吧