使用Pytorch实现FER2013人脸表情识别——从FNN到CNN

学完了深度学习的理论和框架,接下来就是实践啦!相信大家在实践的时候都会去找各种的项目,本文是kaggle上面的一个深度学习小项目,目的是实现人脸表情的分类。接下来我将具体讲解实现过程,相信你只要学了深度学习的理论知识,对pytorch框架有所了解,就能够看得懂。话不多说,开讲!

深度学习首先要做的就是准备数据集,本项目的数据集可以直接从kaggle上面下载,附上链接(FER-2013数据集)。数据集由训练集和测试集组成,训练集包含28709张人脸图片,按照不同表情分为愤怒,厌恶,恐惧,快乐,悲伤,惊讶,中性七个类别,放在不同的文件夹中。测试集包含3589图片,也一样的分好类放在不同的文件夹中,其中每张图片都是48*48像素的灰度图像。

数据集准备好之后,接下来开始搭建模型。众所周知,深度学习分为模型,学习准则,优化算法三个模块,后面我会一一介绍。本文分别用FNN(前馈神经网络),CNN(卷积神经网络)进行了项目的实现,先介绍FNN。

第一步,导包,这里我们要用到四个包,后面用的时候再详细介绍。

import torch  # 导入pytorch
from torch.utils.data import DataLoader  # 加载数据集的包
from torchvision import transforms  # 对数据进行处理
from torchvision.datasets import ImageFolder  # 按文件夹对自动给图片打标签

深度学习在实现过程中,无非就是四个步骤,导入数据集、搭建训练模型、训练、测试。首先讲第一步:前面已经下载好了数据集,我们要把train和test数据集放在pycharm创建的project同一个文件夹里面,方便训练的时候导入数据。用transforms准备两个数据处理器,一个用来处理训练集,一个用来处理测试集,两个处理器的配置不一样。transforms在处理图像的时候,可以对图像进行翻转,旋转,缩放,裁剪等操作,所以可以用来对训练集进行数据增强,防止过拟合,而测试集不需要做数据增强,所以对两个数据集的处理器配置要不一样。对训练集本人加了翻转和旋转操作,当然读者也可以做别的数据增强,具体的可以去看transforms的使用。

TrainTransforms = transforms.Compose([transforms.RandomHorizontalFlip(p=0.5),  # 以0.5的概率随机翻转
                                      transforms.RandomRotation((-10, 10)),  # 在(-10,10)度的范围内旋转
                                      transforms.Grayscale(num_output_channels=1),  # 参数为1表示转换为灰度图像
                                      transforms.ToTensor(),  # 把图像转化为张量
                                      transforms.Normalize(0.5, 0.5)])  # 归一化,参数为均值和方差

TestTransforms = transforms.Compose([transforms.Grayscale(num_output_channels=1),  
                                     transforms.ToTensor(),
                                     transforms.Normalize(0.5, 0.5)])

配置好了数据处理器之后,接下来就是要加载数据集,这里我们要用到DataLoader模块。但是在加载数据集之前,我们还有一个工作,我们下载下来的图片都是没有标签的,不像手写数字集MINST那样每张图片都进行了标注,这里只是按照不同的类别将人脸表情图像放在了不同的文件夹中而已。所以,我们在加载数据集之前必须要对图片进行标注。这里有一个很方便的模块叫做ImageFolder,可以按照文件夹顺序给文件夹中的每张图片进行标注,例如angry中的每张图片都标注为0,disgust中的图片都为1。

给数据打好标签之后,我们把它放入DataLoader,训练过程中,我们采用小批量随机梯度下降算法,将batch_size设置为64,shuffle=True,每次随机抽取64张图片参与训练。

train_dir = './train'  # “.”表示当前目录
test_dir = './test'

TrainData = ImageFolder(train_dir, transform=TrainTransforms)  # ImageFolder把图片按照文件夹分类,可以自动打标签
TestData = ImageFolder(test_dir, transform=TestTransforms)

TrainLoad = DataLoader(TrainData, batch_size=64, shuffle=True, pin_memory=True)  # pin_memory:如果设置为True,
                                                                                 # 那么data loader将会在返回它们之前,将tensors拷贝到CUDA中的固定内存
TestLoad = DataLoader(TestData, batch_size=64, shuffle=True, pin_memory=True)

数据准备工作就绪,接下来开始搭建模型。首先是FNN前馈神经网络,也就是多隐层全连接神经网络。输入数据是64X(48*48)的二维张量,所以在设置模型超参数时,维度要对应起来。在设置超参数的时候,除了第一个隐藏层的输入维度要设置为48*48,最后一个隐藏层的输出要设置维7(七个类别)之外,其他的超参数都可以自由设置,读者也可以自己更改网络层数。

class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()  # 所有继承自torch.nn.Module的类都需要写这个初始化方法
        self.Linear1 = torch.nn.Linear(48 * 48, 512)  # 注意这个L是大写
        self.Linear2 = torch.nn.Linear(512, 64)
        self.Linear3 = torch.nn.Linear(64, 7)
        self.Activate = torch.nn.ReLU()  # ReLu在神经网络中相当于一个通用的激活函数

    def forward(self, x):  # 前馈神经网络中方法名必须为forward,不能更改
        x = x.view(x.size(0), -1)  # 把输入数据转化为64行,自动获取列数,因为x.size的第0维是64
        x = self.Activate(self.Linear1(x))
        x = self.Activate(self.Linear2(x))
        x = self.Linear3(x)
        return x  # 用交叉熵损失函数就不需要softmax了,因为交叉熵会自动做softmax处理

下一步工作就是学习准则和优化算法的选择了,学习准则这里采用经验风险最小化准则,优化算法为小批量随机梯度下降算法,这里我们选用pytorch中的Adam优化器。在进行训练之前,我们把数据迁移至GPU进行加速训练。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Model().to(device)  # 把模型迁移至GPU
criterion = torch.nn.CrossEntropyLoss()  # 多分类用交叉熵损失函数
optimizer = torch.optim.Adam(model.parameters())  # 也可以用别的优化器对比效果

一切准备就绪,开始训练。通过DataLoader返回的数据是一个包含两个数据的列表,第一个数据是图片张量,第二个数据是标签,所以可以直接用image和label获取数据。

def Train():
    run_loss = 0
    total = 0
    for image, label in TrainLoad:
        image = image.to(device)  # 迁移至GPU
        label = label.to(device)
        optimizer.zero_grad()  # 梯度清零
        pred = model(image)  # 预测
        loss = criterion(pred, label)  # 计算损失
        run_loss += loss.item()  # loss是一个张量。取值要用item
        total += label.size(0)
        loss.backward()  # 反向传播
        optimizer.step()  # 更新梯度
    print(run_loss/total)  # 输出每个样本上的损失值

每训练一轮,我们在测试集上进行一次测试。测试过程中不需要使用梯度,所以要设置no_grad()。

def Test():
    total = 0
    correct = 0
    with torch.no_grad():
        for image, label in TestLoad:
            image = image.to(device)
            label = label.to(device)
            pred = model(image)
            _, predict = torch.max(pred.data, dim=1)  # torch.max返回张量中的最大值和索引,dim=1按照行计算,不需要使用的值用下划线储存
            total += label.size(0)  # size返回label的行数和列数,0表示行数
            number = (predict == label).sum().item()  # sum对64维的布尔型张量求值得到一个只有一个值的张量,item把这个张量转换为int
    print("accuracy on test is %d %%" % (100*correct/total))

最后添加程序入口,设置训练轮数,项目完成!

if __name__ == '__main__':
    for i in range(10):  # 训练10轮
        Train()
        Test()

下面是CNN(卷积神经网络)的代码,直接附上。CNN的优势在于可以保存图像的空间信息,其关键在于要会计算每一次卷积和池化之后的维度。如果不会计算,那你或许需要再去巩固一下CNN的理论知识,也可以去B站看一下Pytorch框架的视频,这里推荐刘二大人,直接看卷积神经网络。

LeNet-5卷积神经网络。

import torch
from torch.utils.data import DataLoader
from torchvision.datasets import ImageFolder
from torchvision import transforms

train_dir = './train'
test_dir = './test'

TrainTransforms = transforms.Compose([transforms.RandomHorizontalFlip(p=0.5),
                                      transforms.RandomRotation(10),  # 旋转角度为(-10,10)
                                      transforms.Grayscale(num_output_channels=1),  # 转化为灰度图像
                                      transforms.ToTensor(),
                                      transforms.Normalize(0.5, 0.5)])

TestTransforms = transforms.Compose([transforms.Grayscale(num_output_channels=1),
                                     transforms.ToTensor(),
                                     transforms.Normalize(0.5, 0.5)])

TrainData = ImageFolder(train_dir, transform=TrainTransforms)
TestData = ImageFolder(test_dir, transform=TestTransforms)

TrainLoader = DataLoader(TrainData, batch_size=64, shuffle=True, pin_memory=True)
TestLoader = DataLoader(TestData, batch_size=64, shuffle=True, pin_memory=True)


class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.Con1 = torch.nn.Conv2d(1, 10, kernel_size=5, stride=1)  # Conv2d是指定大小的二维卷积核,Conv1d是一维
        self.Con2 = torch.nn.Conv2d(10, 20, kernel_size=5, stride=1)
        self.Con3 = torch.nn.Conv2d(20, 10, kernel_size=5, stride=1)
        self.Pooling = torch.nn.MaxPool2d(2)  # Maxpool2d(二维池化),Maxpool1d(一维池化)
        self.Activate = torch.nn.ReLU()
        self.fc = torch.nn.Linear(250, 7)

    def forward(self, x):
        batch_size = x.size(0)
        x = self.Activate(self.Pooling(self.Con1(x)))
        x = self.Activate(self.Pooling(self.Con2(x)))
        x = self.Activate(self.Con3(x))  # 注意这里的维度变成了64*250
        x = x.view(batch_size, -1)  # 注意全连接层的维度
        return self.fc(x)


device = ('cuda' if torch.cuda.is_available() else 'cpu')
model = Model().to(device)  # 把模型迁移到GPU
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)


def Train():
    run_loss = 0
    count_labels = 0
    for images, labels in TrainLoader:
        images = images.to(device)
        labels = labels.to(device)
        predict = model(images)
        loss = criterion(predict, labels)
        count_labels += labels.size(0)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        run_loss += loss.item()
    print(run_loss/count_labels)


def Test():
    total = 0
    correct = 0
    with torch.no_grad():  # 不要漏掉这里的括号
        for images, labels in TestLoader:
            images = images.to(device)
            labels = labels.to(device)
            pred = model(images)
            _, predict = torch.max(pred.data, dim=1)
            correct += (predict == labels).sum().item()
            total += labels.size(0)  # size(0)是什么意思
    print('accuracy on test is %d %%' % (100*correct/total))


if __name__ == '__main__':
    for epoch in range(20):
        Train()
        Test()

Inception卷积神经网络。

import torch
from torch.utils.data import DataLoader
from torchvision.datasets import ImageFolder
from torchvision import transforms
import torch.nn.functional as F

train_dir = './train'
test_dir = './test'

TrainTransforms = transforms.Compose([transforms.RandomHorizontalFlip(p=0.5),
                                      transforms.RandomRotation(10),  # 旋转角度为(-10,10)
                                      transforms.Grayscale(num_output_channels=1),  # 转化为灰度图像
                                      transforms.ToTensor(),
                                      transforms.Normalize(0.5, 0.5)])

TestTransforms = transforms.Compose([transforms.Grayscale(num_output_channels=1),
                                     transforms.ToTensor(),
                                     transforms.Normalize(0.5, 0.5)])

TrainData = ImageFolder(train_dir, transform=TrainTransforms)
TestData = ImageFolder(test_dir, transform=TestTransforms)

TrainLoader = DataLoader(TrainData, batch_size=64, shuffle=True, pin_memory=True)
TestLoader = DataLoader(TestData, batch_size=64, shuffle=True, pin_memory=True)


class InceptionA(torch.nn.Module):
    def __init__(self, in_channels):
        super(InceptionA, self).__init__()
        self.branch1x1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)

        self.branch5x5_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
        self.branch5x5_2 = torch.nn.Conv2d(16, 24, kernel_size=5, padding=2)

        self.branch3x3_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
        self.branch3x3_2 = torch.nn.Conv2d(16, 24, kernel_size=3, padding=1)
        self.branch3x3_3 = torch.nn.Conv2d(24, 24, kernel_size=3, padding=1)

        self.branch_pool = torch.nn.Conv2d(in_channels, 24, kernel_size=1)

    def forward(self, x):
        branch1x1 = self.branch1x1(x)

        branch5x5 = self.branch5x5_1(x)
        branch5x5 = self.branch5x5_2(branch5x5)

        branch3x3 = self.branch3x3_1(x)
        branch3x3 = self.branch3x3_2(branch3x3)
        branch3x3 = self.branch3x3_3(branch3x3)

        branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
        branch_pool = self.branch_pool(branch_pool)

        outputs = [branch1x1, branch5x5, branch3x3, branch_pool]
        return torch.cat(outputs, dim=1)  # b,c,w,h  c对应的是dim=1


class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.Con1 = torch.nn.Conv2d(1, 10, kernel_size=5, stride=1)
        self.Con2 = torch.nn.Conv2d(88, 20, kernel_size=5, stride=1)
        self.Con3 = torch.nn.Conv2d(88, 10, kernel_size=5, stride=1)
        self.incep1 = InceptionA(in_channels=10)
        self.incep2 = InceptionA(in_channels=20)
        self.Pooling = torch.nn.MaxPool2d(2)
        self.Activate = torch.nn.ReLU()
        self.fc = torch.nn.Linear(250, 7)

    def forward(self, x):
        batch_size = x.size(0)
        x = self.Activate(self.Pooling(self.Con1(x)))
        x = self.incep1(x)
        x = self.Activate(self.Pooling(self.Con2(x)))
        x = self.incep2(x)
        x = self.Activate(self.Con3(x))  # 注意这里的维度变成了64*250
        x = x.view(batch_size, -1)  # 注意全连接层的维度
        return self.fc(x)


device = ('cuda' if torch.cuda.is_available() else 'cpu')
model = Model().to(device)  # 把模型迁移到GPU
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)


def Train():
    run_loss = 0
    count_labels = 0
    for images, labels in TrainLoader:
        images = images.to(device)
        labels = labels.to(device)
        predict = model(images)
        loss = criterion(predict, labels)
        count_labels += labels.size(0)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        run_loss += loss.item()
    print(run_loss/count_labels)


def Test():
    total = 0
    correct = 0
    with torch.no_grad():  # 不要漏掉这里的括号
        for images, labels in TestLoader:
            images = images.to(device)
            labels = labels.to(device)
            pred = model(images)
            _, predict = torch.max(pred.data, dim=1)
            correct += (predict == labels).sum().item()
            total += labels.size(0)  # size(0)是什么意思
    print('accuracy on test is %d %%' % (100*correct/total))


if __name__ == '__main__':
    for epoch in range(20):
        Train()
        Test()

最后总结:在训练完成之后,卷积神经网络比前馈神经网络的精度提高了15%以上。说明卷积神经网络在处理图像方面确实比前馈神经网络要好。模型训练之后的总体精度不是很高,分析其原因在于:1.数据集中有很多不同分类的图像,其本身差异不大,导致模型无法正确分类;2.训练样本数据太少,导致模型过拟合,这个问题可以通过数据增强等方法解决。

最后,感谢大家的阅读,如果觉得我讲的明白,还请大家收藏加关注,感激不尽!(代码后续也会在github进行开源,敬请关注)

物联沃分享整理
物联沃-IOTWORD物联网 » 使用Pytorch实现FER2013人脸表情识别——从FNN到CNN

发表评论