本文将详细介绍基于 Pytorch 框架,利用全卷积网络进行人脸表情识别的完整过程,涵盖从数据集的准备、模型的设计与训练,再到模型的部署与预测,通过代码实现以及详细讲解,帮助读者深入理解并掌握这一技术。
人脸表情是人类情感交流的重要方式,不同的表情能够传达出丰富的情感信息。人脸表情识别在智能交互、安防监控、心理健康分析等众多领域有着广泛的应用前景。随着深度学习技术的发展,基于卷积神经网络的人脸表情识别方法取得了显著的成果。本篇博客将基于 Pytorch 框架,介绍全卷积网络在人脸表情识别中的应用,从数据、模型设计与训练、部署与预测等方面展开,带领读者一步步实现一个人脸表情识别系统。
人的面部由 44 块肌肉构成,可以产生超过 5000 多种表情,至少有 18 个独特的微表情,这些微表情能够反映人的心理活动与情绪。在人脸表情识别领域,有许多公开的数据库可供使用,例如 AffectNet,其官网地址为AffectNet – Mohammad H. Mahoor, PhD 。这些公开数据集为我们的研究和模型训练提供了丰富的数据资源1。
除了公开数据集,我们还可以创建自己的自定义表情数据集。本次使用的自定义表情数据集包含 4300 张人脸表情图像,涵盖了八种表情类别,分别是 ["neutral", "anger", "disdain", "disgust", "fear", "happy", "sadness", "surprise"],图像大小均为 64x64。以下是加载自定义数据集的代码示例:
import torch
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import os
class FaceEmotionDataset(Dataset):
def __init__(self, data_dir, transform=None):
self.data_dir = data_dir
self.transform = transform
self.image_paths = []
self.labels = []
emotion_labels = ["neutral", "anger", "disdain", "disgust", "fear", "happy", "sadness", "surprise"]
for emotion in emotion_labels:
emotion_dir = os.path.join(data_dir, emotion)
for file_name in os.listdir(emotion_dir):
image_path = os.path.join(emotion_dir, file_name)
self.image_paths.append(image_path)
self.labels.append(emotion_labels.index(emotion))
def __len__(self):
return len(self.image_paths)
def __getitem__(self, idx):
image_path = self.image_paths[idx]
image = Image.open(image_path).convert("RGB")
label = self.labels[idx]
if self.transform:
image = self.transform(image)
return image, label
# 示例用法
data_dir = "your_data_dir" # 替换为实际数据集路径
transform = torchvision.transforms.Compose([
torchvision.transforms.Resize((64, 64)),
torchvision.transforms.ToTensor()
])
dataset = FaceEmotionDataset(data_dir, transform=transform)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
上述代码首先定义了一个FaceEmotionDataset
类,继承自Dataset
类。在__init__
函数中,遍历数据集目录,获取所有图像的路径和对应的标签。__len__
函数返回数据集的大小,__getitem__
函数根据索引获取图像和标签,并在有变换操作时对图像进行变换。最后,通过示例展示了如何使用这个数据集类加载数据,并创建数据加载器。
在深入了解全卷积网络之前,先回顾一下深度神经网络的基本组成部分。深度神经网络中的卷积神经网络(CNN)由卷积层(Conv)和池化层(Pool)等组成,能够自动提取图像的特征。全连接层(FC)则用于对提取的特征进行分类等任务。常见的网络结构如Input -> Conv -> Pool -> Conv -> Pool -> FC -> FC -> Softmax
,输入图像经过卷积层和池化层的多次处理,提取到高级特征,再通过全连接层进行分类,最后经过 Softmax 函数进行概率分布,输出各类别的概率。
全卷积神经网络(FCN)是一种特殊的神经网络结构,它将传统卷积神经网络中的全连接层替换为卷积层。在本文的人脸表情识别模型中,全卷积神经网络的结构如下:
输入图像大小为224x224x3
,经过一系列的卷积层和 ReLU 激活函数,以及最大池化层的操作,图像的尺寸逐渐减小,通道数逐渐增加,具体变化为224x224x3 -> 224x224x64 -> 112x112x128 -> 56x56x256 -> 28x28x512 -> 14x14x512 -> 7x7x512 -> 1x1x4096 -> 1x1x1000
,最后通过 Softmax 函数进行分类。此外,模型中还包含残差 block 和输出头,输出头的形状为Nx8x1x1
,其中 8 对应八种表情类别。模型使用交叉熵损失作为分类损失函数789。
以下是构建全卷积神经网络模型的代码示例:
import torch
import torch.nn as nn
class FaceEmotionFCN(nn.Module):
def __init__(self):
super(FaceEmotionFCN, self).__init__()
self.conv_layers = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(512, 4096, kernel_size=7),
nn.ReLU(),
nn.Conv2d(4096, 1000, kernel_size=1),
nn.ReLU()
)
self.output_head = nn.Conv2d(1000, 8, kernel_size=1)
def forward(self, x):
x = self.conv_layers(x)
x = self.output_head(x)
return x
# 示例用法
model = FaceEmotionFCN()
input_tensor = torch.randn(1, 3, 224, 224)
output = model(input_tensor)
print(output.shape)
上述代码定义了FaceEmotionFCN
类,继承自nn.Module
。在__init__
函数中,构建了卷积层序列conv_layers
和输出头output_head
。forward
函数定义了数据的前向传播过程,前向传播计算传播计算损失率,反馈传播计算每个函数的梯度,来达到调节参数的目的。输入数据经过卷积层序列处理后,再通过输出头得到最终的输出。最后通过示例展示了如何创建模型实例,并输入一个随机张量,查看输出的形状。
在定义好模型结构后,我们需要对模型进行训练。训练过程主要包括以下几个步骤:
nn.CrossEntropyLoss()
作为模型的损失函数,优化器选择随机梯度下降(SGD),学习率设置为 0.001,动量设置为 0.9。import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
num_epochs = 10
for epoch in range(num_epochs):
running_loss = 0.0
for i, (inputs, labels) in enumerate(dataloader):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if (i + 1) % 100 == 0:
print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch + 1, num_epochs, i + 1, len(dataloader), running_loss / 100))
running_loss = 0.0
上述代码首先定义了训练的轮数num_epochs
,在每一轮训练中,遍历数据加载器dataloader
。在每次迭代中,先将优化器的梯度清零,然后将输入数据传入模型得到输出,计算损失函数,进行反向传播计算梯度,最后使用优化器更新模型参数。每 100 步打印一次训练的损失信息。
训练好模型后,我们可以将其导出为 ONNX 格式,以便在其他平台或框架中使用。以下是导出模型为 ONNX 格式的代码:
import torch
# 假设已经训练好的模型为model
dummy_input1 = torch.randn(1, 3, 64, 64)
torch.onnx.export(model, (dummy_input1), "face_emotions_model.onnx", verbose=True)
上述代码中,首先创建一个形状为(1, 3, 64, 64)
的随机张量dummy_input1
作为模型的输入示例,然后使用torch.onnx.export
函数将模型导出为face_emotions_model.onnx
文件,verbose=True
表示打印详细的导出信息。导出后的模型输入为input.1
,类型为float32[1,3,64,64]
,输出为output
,类型为float32[1,8]
。
在进行实时人脸表情识别时,首先需要检测图像中的人脸。我们可以借用 OpenCV4.x DNN 模块中的人脸检测模型来实现人脸检测。以下是使用 OpenCV 进行人脸检测的代码示例:
import cv2
# 加载人脸检测模型
net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "res10_300x300_ssd_iter_140000.caffemodel")
# 读取图像
image = cv2.imread("test_image.jpg")
(h, w) = image.shape[:2]
blob = cv2.dnn.blobFromImage(cv2.resize(image, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0))
# 进行人脸检测
net.setInput(blob)
detections = net.forward()
上述代码首先使用cv2.dnn.readNetFromCaffe
函数加载人脸检测模型,模型的配置文件为deploy.prototxt
,权重文件为res10_300x300_ssd_iter_140000.caffemodel
。然后读取一张测试图像,将其转换为模型所需的 blob 格式,通过net.setInput
设置输入,net.forward
进行前向传播,得到人脸检测结果detections
。
检测到人脸后,我们需要截取人脸的感兴趣区域(ROI),以便后续输入到表情预测模型中。以下是截取人脸 ROI 区域的代码:
for i in range(0, detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > 0.5:
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
(startX, startY, endX, endY) = box.astype("int")
face_roi = image[startY:endY, startX:endX]
上述代码遍历人脸检测结果,对于置信度大于 0.5 的检测框,获取其坐标信息,并从原始图像中截取人脸 ROI 区域。
截取到人脸 ROI 区域后,将其输入到训练好的表情预测模型中,预测人脸的表情,并在图像上显示出来。以下是完整的实时人脸表情检测代码:
import cv2
import torch
import torchvision.transforms as transforms
# 加载人脸检测模型
net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "res10_300x300_ssd_iter_140000.caffemodel")
# 加载表情预测模型
model = torch.load("face_emotions_model.pth")
model.eval()
# 定义图像变换
transform = transforms.Compose([
transforms.Resize((64, 64)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# 打开摄像头
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
(h, w) = frame.shape[:2]
blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0))
net.setInput(blob)
detections = net.forward()
for i in range(0, detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > 0.5:
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
(startX, startY, endX, endY) = box.astype("int")
face_roi = frame[startY:endY, startX:endX]
face_roi = cv2.cvtColor(face_roi, cv2.COLOR_BGR2RGB)
face_tensor = transform(face_roi).unsqueeze(0)
with torch.no_grad():
output = model(face_tensor)
_, predicted = torch.max(output, 1)
emotion = ["neutral", "anger", "disdain", "disgust", "fear", "happy", "sadness", "surprise"][predicted.item()]
cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 255, 0), 2)
cv2.putText(frame, emotion, (startX, startY - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
cv2.imshow("Face Emotion Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
上述代码首先加载人脸检测模型和表情预测模型,定义图像变换操作。然后打开摄像头,在每一帧图像中进行人脸检测,截取人脸 ROI 区域,将其转换为模型所需的张量格式,输入到表情预测模型中得到预测结果,最后在图像上绘制检测框和表情标签,并显示图像。按q
键可以退出程序。
在实际应用中,可能会遇到准确性问题,此时可以使用更大的数据集来训练模型,以提高模型的性能。同时,还可以尝试其他的模型结构和训练方法,进一步优化模型。
可视化:
本文基于 Pytorch 框架,详细介绍了全卷积网络在人脸表情识别中的应用,从人脸表情数据集的准备,到全卷积网络的设计与训练,再到模型的导出、部署与预测,通过完整的代码示例展示了整个过程。