基于深度学习的人脸识别系统设计与实现

时间:2024-12-18 07:37:51
(1) CNN模型搭建
  • a) 搭建卷基层
    博文中使用了AlexNet的卷积层结构,详细实现了多层卷积操作,包括不同的卷积核大小和通道数,符合要求。
  • b) 搭建池化层
    使用了MaxPool2d进行池化操作,按要求实现了池化层。
  • c) 选取激活函数
    采用了ReLU激活函数,明确说明了选择的激活函数,满足设计要求。
  • d) 选取优化器
    使用了SGD优化器,并设置了学习率,符合要求。
  • e) 自定义损失函数
    未实现。博文中使用了PyTorch内置的CrossEntropyLoss作为损失函数,未按照要求自定义损失函数。
(2) 训练模型
  • 数据划分
    博文中对数据集进行了训练集、验证集和测试集的划分,符合要求。
  • 设置相关参数
    设置了学习率、设备(CPU/GPU)等训练参数,并在训练循环中进行了迭代和优化。
  • 训练与验证
    实现了训练循环,并在每50个epoch打印损失值。同时,进行了验证集的评估,计算了验证损失和准确率。
  • 保存模型
    使用torch.save保存了训练好的模型,满足要求。
(3) 模型测试
  • 读取训练好的模型
    在模型应用部分,使用torch.load加载了保存的模型。
  • 模型测试
    实现了实时人脸识别,通过摄像头捕捉图像并使用训练好的模型进行预测,符合测试要求。

2. 需要改进的地方

  • 自定义损失函数
    根据课程要求,需要自定义一个损失函数。尽管CrossEntropyLoss是一个常用且有效的损失函数,但为了满足课程要求,您需要实现一个自定义的损失函数。例如,可以基于交叉熵损失进行修改,或者结合其他损失函数(如中心损失)以提升模型性能。

基于深度学习的人脸识别

摘要

人脸识别作为计算机视觉领域的重要研究方向,近年来在深度学习技术的推动下取得了显著的进展。本文旨在设计并实现一个基于深度学习的人脸识别算法,涵盖从数据采集、数据处理、模型构建、模型训练到模型测试的完整流程。通过构建卷积神经网络(CNN)模型、引入自定义损失函数、优化训练过程等方法,旨在提高人脸识别的准确率和鲁棒性。最终,通过对模型的评估和测试,验证其在实际应用中的有效性和性能。

目录

  1. 引言
  2. 设计内容与要求
    • \1. CNN模型搭建
      • a) 搭建卷基层
      • b) 搭建池化层
      • c) 选取激活函数
      • d) 选取优化器
      • e) 自定义损失函数
    • \2. 训练模型
      • a) 数据划分
      • b) 参数设置
      • c) 训练与验证
      • d) 保存模型
    • \3. 模型测试
      • a) 读取训练好的模型
      • b) 模型测试
  3. 系统实现
    • 1. 数据获取
    • 2. 数据处理
    • 3. 模型定义
    • 4. 自定义损失函数
    • 5. 模型训练
    • 6. 模型应用
  4. 模型评估与结果分析
    • 1. 评估方法
    • 2. 评估结果
    • 3. 混淆矩阵分析
    • 4. 样本预测展示
  5. 讨论与总结
    • 1. 结果讨论
    • 2. 存在的问题
    • 3. 未来工作
  6. 参考文献

引言

人脸识别技术作为一种重要的生物特征识别方法,广泛应用于安防监控、身份验证、智能营销等领域。传统的人脸识别方法主要依赖于手工特征提取和分类器设计,然而在复杂环境下表现不佳。随着深度学习技术的发展,基于卷积神经网络(CNN)的深度学习模型在人脸识别任务中展现出卓越的性能,能够自动学习图像特征,提高识别准确率和鲁棒性。

本项目旨在设计并实现一个基于深度学习的人脸识别系统,涵盖数据采集、数据处理、模型构建、训练与验证以及模型测试的全过程。通过引入自定义的损失函数,优化训练过程,提升模型在不同环境下的人脸识别能力。

设计内容与要求

1. CNN模型搭建

构建卷积神经网络模型是实现高效人脸识别的关键步骤。一个典型的CNN模型包括卷积层、池化层、激活函数、优化器以及损失函数等组成部分。以下将逐一介绍各个模块的设计与实现。

a) 搭建卷基层

卷积层是CNN的核心组成部分,通过滑动卷积核提取图像的局部特征。每个卷积层包含多个卷积核(或称滤波器),能够检测不同类型的特征,如边缘、纹理等。卷积操作通过与输入图像的局部区域进行点积运算,生成特征图(Feature Map)。

在本项目中,首个卷积层设置为96个卷积核,尺寸为11×11,步幅为4。这一设置能够在保持一定的感受野的同时,降低特征图的尺寸。后续卷积层逐步增加深度,提取更高级别的特征。

self.conv = nn.Sequential(
    nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4),
    nn.BatchNorm2d(96),
    nn.ReLU(),        
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Conv2d(96, 256, 5, padding=2),
    nn.BatchNorm2d(256),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Conv2d(256, 384, 3, padding=1),
    nn.ReLU(),
    nn.Conv2d(384, 384, 3, padding=1),
    nn.ReLU(),
    nn.Conv2d(384, 256, 3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
)
b) 搭建池化层

池化层用于降低特征图的空间尺寸,减少计算量和参数数量,同时提取主要特征。常见的池化操作包括最大池化和平均池化。最大池化通过取局部区域的最大值,能够保留最显著的特征。

在本项目中,使用了最大池化层(MaxPool2d),池化窗口大小为3×3,步幅为2。此设置有效地减少了特征图的尺寸,同时保留了关键特征信息。

c) 选取激活函数

激活函数引入非线性特性,使得神经网络能够学习和表示复杂的函数关系。常见的激活函数包括ReLU、Sigmoid、Tanh等。

本项目中,选择了ReLU(Rectified Linear Unit)作为激活函数。ReLU函数定义为f(x)=max(0, x),具有计算简单、收敛速度快、缓解梯度消失问题等优点。因此,ReLU被广泛应用于深度神经网络中。

nn.ReLU()
d) 选取优化器

优化器在训练过程中负责更新模型参数,最小化损失函数。常用的优化器包括SGD、Adam、RMSprop等。

本项目选择了SGD(Stochastic Gradient Descent)优化器,结合动量(momentum)和权重衰减(weight_decay)进行参数更新。SGD优化器具有较好的收敛性和稳定性,适用于大规模数据集和复杂模型。

optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9, weight_decay=5e-4)
e) 自定义损失函数

损失函数用于衡量模型预测值与真实值之间的差异,指导模型参数的优化。常见的损失函数包括交叉熵损失(Cross-Entropy Loss)、均方误差损失(MSE Loss)等。

在本项目中,自定义了Focal Loss(焦点损失函数),旨在解决类别不平衡问题。Focal Loss通过调整难易样本的权重,减少易分类样本的影响,集中训练难分类样本,从而提升模型在不平衡数据集上的表现。

Focal Loss的公式为:

FL(pt)=−α(1−pt)γlog⁡(pt)\text{FL}(p_t) = -\alpha (1 - p_t)^\gamma \log(p_t)

其中,ptp_t 是模型对正确类别的预测概率,α\alpha 是平衡因子,γ\gamma 是聚焦参数。

class FocalLoss(nn.Module):
    """
    Focal Loss for multi-class classification
    """

    def __init__(self, alpha=1, gamma=2, reduction='mean'):
        """
        :param alpha: weight for the rare class
        :param gamma: focusing parameter
        :param reduction: 'none' | 'mean' | 'sum'
        """
        super(FocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction

    def forward(self, inputs, targets):
        # 计算交叉熵损失
        log_pt = F.log_softmax(inputs, dim=1)
        log_pt = log_pt.gather(1, targets.view(-1,1))
        log_pt = log_pt.view(-1)
        pt = log_pt.exp()
        # 计算focal loss
        loss = -1 * (1 - pt) ** self.gamma * log_pt
        if self.alpha is not None:
            loss = self.alpha * loss
        if self.reduction == 'mean':
            return loss.mean()
        elif self.reduction == 'sum':
            return loss.sum()
        else:
            return loss

训练模型

训练深度学习模型是实现高效人脸识别的关键步骤。训练过程包括数据划分、参数设置、模型训练与验证以及模型的保存。以下将详细介绍每个环节的设计与实现。

a) 数据划分

数据划分是确保模型能够在不同数据集上泛化的重要步骤。通常将数据集划分为训练集、验证集和测试集,比例为80%:10%:10%。训练集用于模型参数的优化,验证集用于调参和早停,测试集用于最终模型的评估。

在本项目中,数据划分通过随机打乱数据并按比例划分实现:

def load_dataset(path_name):
    images, labels = read_path(path_name)

    # 假设每个子文件夹代表一个类别,给标签编码
    label_set = sorted(list(set(labels)))
    label_dict = {label: idx for idx, label in enumerate(label_set)}
    labels = np.array([label_dict[label] for label in labels])        

    # 简单交叉验证
    combined = list(zip(images, labels))
    random.shuffle(combined)
    images, labels = zip(*combined)

    train_size = int(SAMPLE_QUANTITY * TRAIN_RATE)
    valid_size = int(SAMPLE_QUANTITY * VALID_RATE)
    test_size = SAMPLE_QUANTITY - train_size - valid_size

    train_data = combined[:train_size]
    valid_data = combined[train_size:train_size + valid_size]
    test_data = combined[train_size + valid_size:SAMPLE_QUANTITY]

    return train_data, valid_data, test_data, label_dict

b) 参数设置

参数设置包括学习率、批量大小、优化器选择、损失函数选择等,这些参数直接影响模型的训练效果和收敛速度。

在本项目中,设置的主要参数如下:

  • 学习率(Learning Rate):0.001
  • 批量大小(Batch Size):32
  • 优化器:SGD,动量0.9,权重衰减5e-4
  • 损失函数:自定义Focal Loss
# 定义学习率
learning_rate = 0.001
# 是否使用GPU训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f'使用设备: {device}')

# 定义转换
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 对RGB三通道分别归一化
])

# 数据载入
train_data, valid_data, test_data, label_dict = load_dataset(MYPATH)

train_dataset = FaceDataset(train_data, transform=transform)
valid_dataset = FaceDataset(valid_data, transform=transform)
test_dataset = FaceDataset(test_data, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=32, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# 初始化模型
model = AlexNet(num_classes=len(label_dict)).to(device)

# 定义自定义Focal Loss
criterion = FocalLoss(alpha=1, gamma=2, reduction='mean')
# 定义优化器,可以尝试不同的优化器
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9, weight_decay=5e-4)
# 或者使用Adam优化器
# optimizer = optim.Adam(model.parameters(), lr=learning_rate)

num_epochs = 30  # 根据需要调整

c) 训练与验证

训练过程包括前向传播、计算损失、反向传播和参数更新。在每个epoch结束后,通过在验证集上评估模型性能,监控模型的泛化能力,防止过拟合。

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for batch_idx, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()

        if (batch_idx + 1) % 50 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{batch_idx+1}/{len(train_loader)}], Loss: {loss.item():.4f}')
    
    # 每个epoch结束后在验证集上评估
    model.eval()
    eval_loss = 0.0
    eval_acc = 0
    with torch.no_grad():
        for images, labels in valid_loader:
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)
            eval_loss += loss.item() * images.size(0)
            _, preds = torch.max(outputs, 1)
            eval_acc += (preds == labels).sum().item()
    
    eval_loss /= len(valid_dataset)
    eval_acc /= len(valid_dataset)
    print(f'End of Epoch {epoch+1}, Validation Loss: {eval_loss:.4f}, Validation Accuracy: {eval_acc:.4f}')

d) 保存模型

训练完成后,保存模型的参数,以便在后续测试和应用中使用。采用state_dict的保存方式,具有更高的灵活性和可扩展性。

# 保存训练好的模型
torch.save(model.state_dict(), 'alexnet_focal_loss.pth')
print('模型已保存为 alexnet_focal_loss.pth')

模型测试

模型测试阶段旨在评估训练好的模型在未见过的数据上的表现。通过加载保存的模型参数,对测试集进行预测,并计算相关评估指标,如准确率、混淆矩阵、精确率、召回率和F1分数。

a) 读取训练好的模型

首先,初始化模型结构,并加载保存的模型参数。确保模型与训练时的结构一致。

# 加载模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AlexNet(num_classes=len(label_dict)).to(device)

try:
    # 使用 torch.load 的 map_location 参数以确保在不同设备上的兼容性
    model.load_state_dict(torch.load('alexnet_focal_loss.pth', map_location=device))
except RuntimeError as e:
    print(f"加载模型权重时出错: {e}")
    sys.exit()

model.eval()
print('模型已加载并设置为评估模式')

b) 模型测试

在测试集上进行预测,收集所有预测结果和真实标签,计算准确率、混淆矩阵和分类报告,并可视化混淆矩阵。

all_preds = []
all_labels = []

with torch.no_grad():
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        
        outputs = model(images)
        _, preds = torch.max(outputs, 1)
        
        all_preds.extend(preds.cpu().numpy())
        all_labels.extend(labels.cpu().numpy())

# 计算准确率
accuracy = accuracy_score(all_labels, all_preds)
print(f'测试集准确率: {accuracy:.4f}')

# 计算混淆矩阵
cm = confusion_matrix(all_labels, all_preds)
print('混淆矩阵:')
print(cm)

# 打印分类报告(包括精确率、召回率和F1分数)
report = classification_report(all_labels, all_preds, target_names=label_dict.keys())
print('分类报告:')
print(report)

# 绘制混淆矩阵热力图
plot_confusion_matrix(cm, classes=list(label_dict.keys()), title='Confusion Matrix')

系统实现

本节将详细介绍系统的各个模块实现,包括数据获取、数据处理、模型定义、自定义损失函数、模型训练以及模型应用。

1. 数据获取

数据获取是构建人脸识别系统的第一步,涉及从摄像头采集人脸图像,并将其保存到指定目录。使用OpenCV进行人脸检测和图像捕捉。

# data_capture.py
import cv2
import sys
import os

def CatchPICFromVideo(window_name, camera_idx, catch_pic_num, path_name):
    cv2.namedWindow(window_name)
    
    # 视频来源,可以来自一段已存好的视频,也可以直接来自USB摄像头
    cap = cv2.VideoCapture(camera_idx)                
    
    # 告诉OpenCV使用人脸识别分类器
    classfier = cv2.CascadeClassifier("D:\\opencv\\build\\etc\\haarcascades\\haarcascade_frontalface_alt2.xml")
    
    # 识别出人脸后要画的边框的颜色,RGB格式
    color = (0, 255, 0)
    
    num = 0    
    while cap.isOpened():
        ok, frame = cap.read() # 读取一帧数据
        if not ok:            
            break                

        grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 将当前桢图像转换成灰度图像            
        
        # 人脸检测,1.2和2分别为图片缩放比例和需要检测的有效点数
        faceRects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
        if len(faceRects) > 0:          # 大于0则检测到人脸                                   
            for faceRect in faceRects:  # 单独框出每一张人脸
                x, y, w, h = faceRect                        
                
                # 将当前帧保存为图片
                img_name = os.path.join(path_name, f'{num}.jpg')                
                image = frame[y - 10: y + h + 10, x - 10: x + w + 10]
                cv2.imwrite(img_name, image)                                
                                
                num += 1                
                if num >= catch_pic_num:   # 如果超过指定最大保存数量退出循环
                    break
                
                # 画出矩形框
                cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)
                
                # 显示当前捕捉到了多少人脸图片
                font = cv2.FONT_HERSHEY_SIMPLEX
                cv2.putText(frame, f'num:{num}', (x + 30, y + 30), font, 1, (255, 0, 255), 4)                

        # 超过指定最大保存数量结束程序
        if num >= catch_pic_num: 
            break                
                   
        # 显示图像
        cv2.imshow(window_name, frame)        
        c = cv2.waitKey(10)
        if c & 0xFF == ord('q'):
            break        
        
    # 释放摄像头并销毁所有窗口
    cap.release()
    cv2.destroyAllWindows() 

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser(description="人脸数据采集")
    parser.add_argument('--camera_id', type=int, default=0, help='摄像头ID')
    parser.add_argument('--face_num_max', type=int, default=1000, help='最大人脸数量')
    parser.add_argument('--path_name', type=str, default='C:\\Users\\73559\\Desktop\\ml\\pic3', help='图片保存路径')
    args = parser.parse_args()

    # 确保保存路径存在
    os.makedirs(args.path_name, exist_ok=True)

    CatchPICFromVideo("截取人脸", args.camera_id