基于MobileNetv3实现人脸面部表情识别

前言

大家好,我是阿光。

本专栏整理了《PyTorch深度学习项目实战100例》,内包含了各种不同的深度学习项目,包含项目原理以及源码,每一个项目实例都附带有完整的代码+数据集。

正在更新中~ ✨

🚨 我的项目环境:

  • 平台:Windows10
  • 语言环境:python3.7
  • 编译器:PyCharm
  • PyTorch版本:1.8.1

💥 项目专栏:【PyTorch深度学习项目实战100例】


一、基于MobileNetv3实现人脸面部表情识别

人脸表情是人类信息交流的重要方式,它所富含的人体行为信息与人的情感状态、精神状态、健康状态等有着极为密切的关联。因此,通过对于人脸表情的识别可以获得很多有价值的信息,从而分析人类的心理活动和精神状态,并为各种机器视觉和人工智能控制系统的应用提供了解决方案。

所以本项目在研究人脸面部表情识别过程中,借助人工智能算法的优势,开展基于深度神经网络的图像分类实验。借助MobileNetv3模型进行迁移学习,经迭代100次后分类准确率达到90.54%。

在这里插入图片描述

二、数据集介绍

数据集介绍

128K张MMA面部表情图像数据集
MMAFEDB包含用于培训,验证和测试的目录。
每个目录包含对应于七个面部表情类别的七个子目录。

在这里插入图片描述

数据说明:

  • angry-愤怒
  • disgust-厌恶
  • fear-恐惧
  • happy-快乐
  • neutral-中性
  • sad-悲伤
  • surprise-惊讶

数据集下载链接:

https://www.heywhale.com/mw/dataset/5ee06333b772f5002d731821/content

三、MobileNetV3模型

在这里插入图片描述

相对重量级网络而言,轻量级网络的特点是参数少、计算量小、推理时间短。更适用于存储空间和功耗受限的场景,例如移动端嵌入式设备等边缘计算设备。因此轻量级网络受到了广泛的关注,其中MobileNet可谓是其中的佼佼者。MobileNetV3经过了V1和V2前两代的积累,性能和速度都表现优异,受到学术界和工业界的追捧,无疑是轻量级网络的“抗把子“。MobileNetV3 参数是由NAS(network architecture search)搜索获取的,又继承的V1和V2的一些实用成果,并引人SE通道注意力机制,可谓集大成者。

在这里插入图片描述

四、加载预训练模型

这里我们加载pytorch中已经训练好的GoogLeNet模型,需要一定时间下载训练好的模型权重。

由于预训练的模型与我们的任务需要不一样,所以需要我们将要修改最后一层的全连接层,将输出维度修改为我们的任务要求中的四分类。

但是需要注意需要冻结其它层的参数,防止训练过程中将其进行改动,然后进行训练微调最后一层即可。

# 5.加载MobileNetv3模型
model = torchvision.models.mobilenet_v3_small(pretrained=True) # 加载预训练好的MobileNetv3模型

# 冻结模型参数
for param in model.parameters():
    param.requires_grad = False
    
# 修改最后一层的全连接层
model.classifier[3] = nn.Linear(model.classifier[3].in_features, 7)

# 将模型加载到cpu中
model = model.to('cpu')

# criterion = nn.CrossEntropyLoss() # 损失函数
criterion = MyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # 优化器

五、自定义损失函数

由于我们的任务为多分类问题,所以损失函数需要使用为交叉熵损失函数(Cross Entropy),但是这里我们没有采用框架自带的,而是自己实现了一个损失函数,其实功能是一样的,这里只是为了熟悉如何进行自定义损失函数,虽说大多数情况下,框架自带的损失函数能够满足我们的需求,但是对于一些特定任务,是无法满足的需要我们进行自定义。

定义函数需要继承 nn.Module 子类,然后定义好参数还有所需的变量,在forward中进行编写计算损失函数的过程,然后PyTorch会自动的计算反向传播需要的梯度,不需我们自己进行计算。

# 自定义损失函数,需要在forward中定义过程
class MyLoss(nn.Module):
    def __init__(self):
        super(MyLoss, self).__init__()
    
    # 参数为传入的预测值和真实值,返回所有样本的损失值,自己只需定义计算过程,反向传播PyTroch会自动记录,最好用PyTorch进行计算
    def forward(self, pred, label):
        # pred:[32, 4] label:[32, 1] 第一维度是样本数
        
        exp = torch.exp(pred)
        tmp1 = exp.gather(1, label.unsqueeze(-1)).squeeze()
        tmp2 = exp.sum(1)
        softmax = tmp1 / tmp2
        log = -torch.log(softmax)
        return log.mean()

六、训练过程

train epoch[1/10] loss:1.931: 100%|████████████████████████████████████████████████████| 27/27 [00:28<00:00,  1.06s/it]
【EPOCH:1
训练损失为55.50532126426697
训练精度为23.61%
train epoch[2/10] loss:2.162: 100%|████████████████████████████████████████████████████| 27/27 [00:28<00:00,  1.06s/it]
【EPOCH:2
训练损失为50.870723247528076
训练精度为29.49%
train epoch[3/10] loss:1.760: 100%|████████████████████████████████████████████████████| 27/27 [00:36<00:00,  1.36s/it]
【EPOCH:3
训练损失为51.71602666378021
训练精度为29.61%
train epoch[4/10] loss:2.810: 100%|████████████████████████████████████████████████████| 27/27 [00:35<00:00,  1.33s/it]
【EPOCH:4
训练损失为52.830843567848206
训练精度为30.78%
train epoch[5/10] loss:1.724: 100%|████████████████████████████████████████████████████| 27/27 [00:39<00:00,  1.46s/it]
【EPOCH:5
训练损失为50.44184374809265
训练精度为32.19%
train epoch[6/10] loss:2.046: 100%|████████████████████████████████████████████████████| 27/27 [00:33<00:00,  1.26s/it]
【EPOCH:6
训练损失为49.46258223056793
训练精度为31.72%
train epoch[7/10] loss:1.607: 100%|████████████████████████████████████████████████████| 27/27 [00:29<00:00,  1.08s/it]
【EPOCH:7
训练损失为53.17816519737244
训练精度为29.84%
train epoch[8/10] loss:1.872: 100%|████████████████████████████████████████████████████| 27/27 [00:28<00:00,  1.06s/it]
【EPOCH:8
训练损失为54.47881722450256
训练精度为29.14%
train epoch[9/10] loss:1.677: 100%|████████████████████████████████████████████████████| 27/27 [00:26<00:00,  1.00it/s]
【EPOCH:9
训练损失为52.271769404411316
训练精度为30.78%
train epoch[10/10] loss:2.269: 100%|███████████████████████████████████████████████████| 27/27 [00:29<00:00,  1.09s/it]
【EPOCH:10
训练损失为52.047794222831726
训练精度为31.84%
Finished Training

完整源码

import torchvision
from torch import nn
import numpy as np
import os
import json
import pickle

import torch
import torch.optim as optim
import torch.nn.functional as F
from torchvision import transforms, datasets
import torchvision.models as models
from tqdm import tqdm
from PIL import Image

import matplotlib.pyplot as plt

epochs = 10
lr = 0.03
batch_size = 32
image_path = './data'
save_path = './checkpoints/best_model.pkl'

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# 1.数据转换
data_transform = {
    # 训练中的数据增强和归一化
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224), # 随机裁剪
        transforms.RandomHorizontalFlip(), # 左右翻转
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 均值方差归一化
    ])
}

# 2.形成训练集
train_dataset = datasets.ImageFolder(root=os.path.join(image_path),
                                     transform=data_transform['train'])


# 3.形成迭代器
train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size,
                                           True)

print('using {} images for training.'.format(len(train_dataset)))

# 4.建立分类标签与索引的关系
cloth_list = train_dataset.class_to_idx
class_dict = {}
for key, val in cloth_list.items():
    class_dict[val] = key
with open('class_dict.pk', 'wb') as f:
    pickle.dump(class_dict, f)

# 自定义损失函数,需要在forward中定义过程
class MyLoss(nn.Module):
    def __init__(self):
        super(MyLoss, self).__init__()
    
    # 参数为传入的预测值和真实值,返回所有样本的损失值,自己只需定义计算过程,反向传播PyTroch会自动记录,最好用PyTorch进行计算
    def forward(self, pred, label):
        # pred:[32, 4] label:[32, 1] 第一维度是样本数
        
        exp = torch.exp(pred)
        tmp1 = exp.gather(1, label.unsqueeze(-1)).squeeze()
        tmp2 = exp.sum(1)
        softmax = tmp1 / tmp2
        log = -torch.log(softmax)
        return log.mean()

# 5.加载MobileNetv3模型
model = torchvision.models.mobilenet_v3_small(pretrained=True) # 加载预训练好的MobileNetv3模型

# 冻结模型参数
for param in model.parameters():
    param.requires_grad = False
    
# 修改最后一层的全连接层
model.classifier[3] = nn.Linear(model.classifier[3].in_features, 7)

# 将模型加载到cpu中
model = model.to('cpu')

# criterion = nn.CrossEntropyLoss() # 损失函数
criterion = MyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # 优化器

# 6.模型训练
best_acc = 0 # 最优精确率
best_model = None # 最优模型参数

for epoch in range(epochs):
    model.train()
    running_loss = 0 # 损失
    epoch_acc = 0  # 每个epoch的准确率
    epoch_acc_count = 0  # 每个epoch训练的样本数
    train_count = 0  # 用于计算总的样本数,方便求准确率
    train_bar = tqdm(train_loader)
    for data in train_bar:
        images, labels = data
        optimizer.zero_grad()
        output = model(images.to(device))
        loss = criterion(output, labels.to(device))
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                 epochs,
                                                                 loss)
        # 计算每个epoch正确的个数
        epoch_acc_count += (output.argmax(axis=1) == labels.view(-1)).sum()
        train_count += len(images)

    # 每个epoch对应的准确率
    epoch_acc = epoch_acc_count / train_count

    # 打印信息
    print("【EPOCH: 】%s" % str(epoch + 1))
    print("训练损失为%s" % str(running_loss))
    print("训练精度为%s" % (str(epoch_acc.item() * 100)[:5]) + '%')

    if epoch_acc > best_acc:
        best_acc = epoch_acc
        best_model = model.state_dict()

    # 在训练结束保存最优的模型参数
    if epoch == epochs - 1:
        # 保存模型
        torch.save(best_model, save_path)

print('Finished Training')

# 加载索引与标签映射字典
with open('class_dict.pk', 'rb') as f:
    class_dict = pickle.load(f)

# 数据变换
data_transform = transforms.Compose(
    [transforms.Resize(256),
     transforms.CenterCrop(224),
     transforms.ToTensor()])

# 图片路径
img_path = r'./data/test.jpg'

# 打开图像
img = Image.open(img_path)

# 对图像进行变换
img = data_transform(img)

plt.imshow(img.permute(1,2,0))
plt.show()

# 将图像升维,增加batch_size维度
img = torch.unsqueeze(img, dim=0)

# 获取预测结果
pred = class_dict[model(img).argmax(axis=1).item()]
print('【预测结果分类】:%s' % pred)