(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. 设计内容与要求
  3. 系统实现
  4. 模型评估与结果分析
  5. 讨论与总结
  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, args.face_num_max, args.path_name)

功能说明:

  • 摄像头捕捉:通过OpenCV打开指定摄像头,实时读取视频帧。
  • 人脸检测:使用Haar级联分类器检测视频帧中的人脸位置。
  • 图像保存:将检测到的人脸区域裁剪并保存为JPEG格式的图像文件,按顺序编号。
  • 界面显示:实时显示视频帧,标注人脸位置和已捕捉的图像数量。
  • 参数配置:通过命令行参数指定摄像头ID、最大捕捉数量和保存路径。

2. 数据处理

数据处理包括图像的读取、预处理、标签编码和数据集划分。使用PyTorch的Dataset和DataLoader类,提升数据加载效率和模块化管理。

# prepare.py
import os
import numpy as np
import cv2
import random
from torch.utils.data import DataLoader, Dataset

TRAIN_RATE = 0.8
VALID_RATE = 0.1
TEST_RATE = 0.1
SAMPLE_QUANTITY = 2000

IMAGE_SIZE = 227
MYPATH = "C:\\Users\\73559\\Desktop\\ml\\data"

class FaceDataset(Dataset):
    def __init__(self, data, transform=None):
        self.data = data
        self.transform = transform

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        img, label = self.data[idx]
        if self.transform:
            img = self.transform(img)
        # 确保标签为 torch.long 类型
        label = torch.tensor(label, dtype=torch.long)
        return img, label

def read_path(path_name):    
    images = []
    labels = []
    for dir_item in os.listdir(path_name):
        # 从初始路径开始叠加,合并成可识别的操作路径
        full_path = os.path.abspath(os.path.join(path_name, dir_item))        
        if os.path.isdir(full_path):    # 如果是文件夹,继续递归调用
            images_sub, labels_sub = read_path(full_path)
            images.extend(images_sub)
            labels.extend(labels_sub)
        else:   # 文件
            if dir_item.endswith('.jpg'):
                image = cv2.imread(full_path)                
                image = cv2.resize(image, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_AREA) # 修改图片的尺寸
                images.append(image)
                labels.append(os.path.basename(os.path.dirname(full_path)))                                                   
    return images, labels

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

if __name__ == "__main__":
    train_data, valid_data, test_data, label_dict = load_dataset(MYPATH)
    print(f'训练集大小: {len(train_data)}')
    print(f'验证集大小: {len(valid_data)}')
    print(f'测试集大小: {len(test_data)}')
    print(f'标签字典: {label_dict}')

功能说明:

  • 数据读取:递归读取指定目录下的所有JPEG图像,并提取图像数据和标签。
  • 图像预处理:对图像进行统一尺寸的调整,确保输入模型的一致性。
  • 标签编码:将类别名称映射为唯一的整数标签,便于模型训练。
  • 数据划分:将数据集随机划分为训练集、验证集和测试集,比例为80%:10%:10%。
  • 数据集类:定义PyTorch的Dataset类,便于与DataLoader结合使用,支持批量加载和数据增强。

3. 模型定义

模型定义是实现深度学习人脸识别的核心,涉及构建卷积层、全连接层以及其他关键组件。基于AlexNet架构,构建了一个适用于人脸识别的卷积神经网络模型。

# alexnet.py
import torch
import torch.nn as nn

class AlexNet(nn.Module):
    def __init__(self, num_classes=2):
        super(AlexNet, self).__init__()
        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),
        )
        self.fc = nn.Sequential(
            nn.Linear(256*6*6, 4096),
            nn.ReLU(),     
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Linear(4096, num_classes),
            # Softmax 层不需要显式添加,因为在损失函数中已经处理
        )
    
    def forward(self, x):
        x = self.conv(x)
        x = x.view(x.size(0), -1)  # 展平
        x = self.fc(x) 
        return x

功能说明:

  • 卷积层(Convolutional Layers):通过多层卷积操作提取图像的不同层次特征。
  • 批归一化层(Batch Normalization):加速训练过程,提高模型稳定性。
  • 激活函数(Activation Functions):引入非线性特性,提升模型表达能力。
  • 池化层(Pooling Layers):降低特征图的空间尺寸,减少计算量和参数数量。
  • 全连接层(Fully Connected Layers):将卷积层提取的特征映射到输出类别空间。
  • 输出层:输出每个类别的得分,损失函数将在训练过程中应用Softmax函数。

4. 自定义损失函数

为解决类别不平衡问题,设计并实现了Focal Loss损失函数。Focal Loss通过调整不同样本的损失权重,集中训练难分类样本,减轻易分类样本对总损失的影响。

# focal_loss.py
import torch
import torch.nn as nn
import torch.nn.functional as F

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

功能说明:

  • Focal Loss公式:

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

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

  • 实现细节:

    • 计算输入的对数概率(log_pt)。
    • 根据目标标签收集对应的对数概率。
    • 计算概率 ptp_t。
    • 计算Focal Loss,根据需要应用不同的归约方式(mean, sum, none)。

优势:

  • 应对类别不平衡:通过调整损失权重,减少易分类样本对总损失的影响,集中训练难分类样本。
  • 提升模型性能:在检测任务中尤其有效,能够显著提升小类别的识别率。

5. 模型训练

模型训练阶段包括将数据输入模型,计算损失,反向传播更新参数,并在每个epoch后进行验证评估。通过监控验证集的表现,调整训练过程,确保模型的泛化能力。

# train.py
import torch
import numpy as np
from torch.autograd import Variable
from prepare import load_dataset, MYPATH, FaceDataset
from alexnet import AlexNet
from focal_loss import FocalLoss  # 导入自定义的FocalLoss
from torch import nn, optim
from torch.utils.data import DataLoader
from torchvision import transforms

# 定义学习率
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  # 根据需要调整

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)

        # 确保标签为 torch.long 类型
        if labels.dtype != torch.long:
            labels = labels.long()

        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)
            
            # 确保标签为 torch.long 类型
            if labels.dtype != torch.long:
                labels = labels.long()

            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}')

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

功能说明:

  • 训练循环:对每个epoch进行训练和验证,输出损失和准确率。
  • 参数更新:通过优化器和损失函数,更新模型参数。
  • 验证评估:在每个epoch结束后,使用验证集评估模型性能,计算验证损失和准确率。
  • 模型保存:训练完成后,保存模型的参数。

优化策略:

  • 动量与权重衰减:通过引入动量和权重衰减,提高优化器的性能,减少震荡,加速收敛。
  • 自定义损失函数:使用Focal Loss应对类别不平衡问题,提升小类别的识别能力。

模型测试

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

a) 读取训练好的模型

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

# application.py
import numpy as np
import cv2
import sys
import torch
from torch.autograd import Variable
from alexnet import AlexNet
from focal_loss import FocalLoss  # 可选,若需要在应用中使用
from prepare import load_dataset, MYPATH

# 加载标签字典
_, _, _, label_dict = load_dataset(MYPATH)
idx2label = {v: k for k, v in label_dict.items()}

if __name__ == '__main__':
    if len(sys.argv) != 1:
        print("Usage:%s camera_id\r\n" % (sys.argv[0]))
        sys.exit(0)
    
    # 加载模型
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = AlexNet(num_classes=len(label_dict)).to(device)
    model.load_state_dict(torch.load('alexnet_focal_loss.pth'))
    model.eval()
    print('模型已加载')

    # 框住人脸的矩形边框颜色       
    color = (0, 255, 0)
    
    # 捕获指定摄像头的实时视频流
    cap = cv2.VideoCapture(0)
    
    # 人脸识别分类器本地存储路径
    cascade_path = "D:\\opencv\\build\\etc\\haarcascades\\haarcascade_frontalface_alt2.xml"    
    
    # 循环检测识别人脸
    while True:
        ret, frame = cap.read()   # 读取一帧视频
        
        if ret:
            # 图像灰化,降低计算复杂度
            frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        else:
            continue
        
        # 使用人脸识别分类器,读入分类器
        cascade = cv2.CascadeClassifier(cascade_path)                
 
        # 利用分类器识别出哪个区域为人脸
        faceRects = cascade.detectMultiScale(frame_gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))        
        if len(faceRects) > 0:                 
            for faceRect in faceRects: 
                x, y, w, h = faceRect
                
                # 截取脸部图像提交给模型识别
                img = frame[y - 10: y + h + 10, x - 10: x + w + 10]
                img = cv2.resize(img, (227, 227), interpolation=cv2.INTER_AREA)
                img = img.astype(np.float32) / 255.0  # 归一化
                img = (img - 0.5) / 0.5  # 标准化
                img = np.transpose(img, (2, 0, 1))  # HWC to CHW
                img = torch.from_numpy(img).unsqueeze(0).to(device)
                
                with torch.no_grad():
                    outputs = model(img)
                    _, predicted = torch.max(outputs, 1)
                    confidence = F.softmax(outputs, dim=1)[0][predicted].item()
                    label = idx2label[predicted.item()]
                
                # 显示置信度
                cv2.putText(frame, f'Conf: {confidence:.2f}', 
                            (x - 40, y - 40),                      # 坐标
                            cv2.FONT_HERSHEY_SIMPLEX,              # 字体
                            0.8,                                     # 字号
                            (255, 0, 255),                           # 颜色
                            2)                                     # 字的线宽
                
                # 绘制矩形框
                cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness=2)
                
                # 显示预测标签
                cv2.putText(frame, f'{label}', 
                            (x + 30, y + 30),                      # 坐标
                            cv2.FONT_HERSHEY_SIMPLEX,              # 字体
                            1,                                     # 字号
                            (255, 0, 255),                           # 颜色
                            2)                                     # 字的线宽
                            
        cv2.imshow("Face Recognition", frame)
        
        # 等待10毫秒看是否有按键输入
        k = cv2.waitKey(10)
        # 如果输入q则退出循环
        if k & 0xFF == ord('q'):
            break
 
    # 释放摄像头并销毁所有窗口
    cap.release()
    cv2.destroyAllWindows()

功能说明:

  • 实时视频捕捉:通过摄像头捕捉实时视频帧,进行人脸检测和识别。
  • 人脸检测:使用Haar级联分类器检测视频帧中的人脸位置。
  • 图像预处理:将检测到的人脸区域裁剪、调整尺寸、归一化和标准化,准备输入模型。
  • 模型预测:使用训练好的模型进行预测,获取预测标签和置信度。
  • 结果展示:在视频帧中标注预测标签和置信度,绘制人脸边框,增强用户体验。

模型评估与结果分析

评估模型的性能和准确率是验证模型有效性的重要步骤。通过在测试集上进行评估,计算准确率、混淆矩阵和分类报告,并可视化混淆矩阵,全面分析模型的识别能力。

1. 评估方法

评估方法包括以下几个方面:

  • 准确率(Accuracy):衡量模型在所有测试样本中的正确预测比例。
  • 混淆矩阵(Confusion Matrix):展示实际类别与预测类别之间的关系,帮助识别模型在不同类别上的表现。
  • 分类报告(Classification Report):包括精确率(Precision)、召回率(Recall)和F1分数(F1-Score),提供更细致的分类性能指标。

2. 评估结果

在测试集上进行模型评估,得到以下结果:

使用设备: cuda
模型已加载并设置为评估模式
测试集准确率: 1.0000
混淆矩阵:
[[94  0]
 [ 0 87]]
分类报告:
              precision    recall  f1-score   support

         cai       1.00      1.00      1.00        94
    jianming       1.00      1.00      1.00        87

    accuracy                           1.00       181
   macro avg       1.00      1.00      1.00       181
weighted avg       1.00      1.00      1.00       181

3. 混淆矩阵分析

混淆矩阵展示了模型在不同类别上的预测情况:

[[94  0]
 [ 0 87]]

解释:

  • 真阳性(True Positive, TP):cai类被正确预测为cai类的数量为94。
  • 真阴性(True Negative, TN):jianming类被正确预测为jianming类的数量为87。
  • 假阳性(False Positive, FP):cai类被错误预测为jianming类的数量为0。
  • 假阴性(False Negative, FN):jianming类被错误预测为cai类的数量为0。

分析:

  • 完美分类:混淆矩阵显示所有样本都被正确分类,说明模型在测试集上的表现极佳。
  • 类别均衡:测试集中cai类和jianming类的样本数量较为均衡,模型能够在不同类别上保持高准确率。
  • 无误分类:没有任何假阳性或假阴性,表明模型在当前测试集上没有分类错误。

4. 样本预测展示

除了定量评估,展示模型对样本的预测结果也有助于直观了解模型的识别能力。在evaluate.py中,展示了部分正确和错误预测的样本图像,并保存为sample_predictions.png。

示例展示:

由于模型在测试集上实现了100%的准确率,所有展示的样本均为正确预测。以下为示例展示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

解释:

  • 正确预测样本:展示了几张被正确识别为cai和jianming的样本图像,验证了模型的高准确率。
  • 错误预测样本:在本次测试中未出现错误预测,但在实际应用中应展示部分错误预测的样本,以分析模型的不足。

讨论与总结

1. 结果讨论

本项目通过构建基于深度学习的CNN模型,实现了高效的人脸识别系统。模型在训练集和验证集上均取得了100%的准确率,且在测试集上也达到了100%的准确率,表现出色。

优势:

  • 高准确率:在测试集上实现了100%的准确率,表明模型具有极高的识别能力。
  • 类别区分明显:模型能够有效区分不同类别的人脸,误分类率为零。
  • 鲁棒性强:通过引入Focal Loss,应对了类别不平衡问题,提升了模型在少数类上的表现。

不足:

  • 过拟合风险:训练集和验证集上的100%准确率可能表明模型存在过拟合风险,尤其是在数据集较小时。
  • 数据多样性不足:如果数据集样本来自有限的环境或个体,模型的泛化能力可能受限。
  • 实时性能:在实时应用中,模型的推理速度和资源消耗需进一步优化。

2. 存在的问题

尽管模型在测试集上表现优异,但仍存在一些潜在的问题:

  • 数据集规模:训练数据量相对较小,可能导致模型在更大规模或复杂数据上的泛化能力不足。
  • 数据质量:数据集中图像质量、光照条件、角度变化等因素的多样性有限,可能影响模型在不同环境下的表现。
  • 类别不平衡:尽管Focal Loss缓解了类别不平衡问题,但数据集的类别分布仍需进一步平衡。

3. 未来工作

为了进一步提升人脸识别系统的性能和实用性,可以考虑以下几个方面的改进:

a) 扩大数据集

收集更多样化的人脸图像,涵盖不同光照、姿态、表情等条件,增强模型的泛化能力。同时,增加不同个体的样本数量,减少个体之间的相似性影响。

b) 数据增强

在数据预处理阶段,应用更多的数据增强技术,如随机旋转、缩放、裁剪、颜色抖动等,增加数据的多样性,防止模型过拟合。

transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(10),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 对RGB三通道分别归一化
])
c) 模型优化
  • 引入正则化方法:在模型中添加Dropout层,进一步减少过拟合风险。
  • 尝试不同的网络架构:如ResNet、VGG等,比较不同模型在任务上的表现,选择最优架构。
  • 参数调优:通过网格搜索或随机搜索优化学习率、批量大小等超参数,提升模型性能。
d) 评估方法扩展

除了准确率、混淆矩阵和分类报告,还可以引入其他评估指标,如ROC曲线、AUC值、Precision-Recall曲线等,全面评估模型性能。

e) 实时应用优化

优化模型的推理速度和资源消耗,以满足实时应用的需求。可以通过模型剪枝、量化等技术,减少模型参数,提高运行效率。

# 示例:模型量化
model.eval()
model = torch.quantization.quantize_dynamic(
    model, {nn.Linear}, dtype=torch.qint8
)
torch.save(model.state_dict(), 'quantized_alexnet_focal_loss.pth')
f) 安全性与隐私保护

在实际应用中,人脸识别系统涉及用户隐私,需考虑数据的安全性和隐私保护措施,如数据加密、匿名化处理等。

g) 多任务学习

将人脸识别与其他任务(如表情识别、年龄估计等)结合,通过多任务学习提升模型的综合能力。

参考文献

  1. He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. Proceedings of the IEEE conference on computer vision and pattern recognition, 770-778.
  2. Simonyan, K., & Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. arXiv preprint arXiv:1409.1556.
  3. Lin, T. Y., Goyal, P., Girshick, R., He, K., & Dollar, P. (2017). Focal loss for dense object detection. Proceedings of the IEEE international conference on computer vision, 2980-2988.
  4. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.
  5. Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic optimization. arXiv preprint arXiv:1412.6980.
  6. He, K., Zhang, X., Ren, S., & Sun, J. (2015). Delving deep into rectifiers: Surpassing human-level performance on imagenet classification. Proceedings of the IEEE international conference on computer vision, 1026-1034.
  7. Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). ImageNet classification with deep convolutional neural networks. Advances in neural information processing systems, 1097-1105.
  8. OpenCV Documentation. (n.d.). Haar Cascades. Retrieved from https://docs.opencv.org/master/d7/d00/tutorial_meanshift.html

结论

本项目设计并实现了一个基于深度学习的人脸识别系统,涵盖了数据采集、数据处理、模型构建、训练与验证以及模型测试的全过程。通过构建卷积神经网络模型、引入自定义的Focal Loss损失函数、优化训练过程,模型在训练集、验证集和测试集上均表现出色,达到了100%的准确率。然而,数据集规模和多样性有限,存在过拟合的风险,未来需要通过扩展数据集、引入更多的数据增强技术以及优化模型架构等方式,进一步提升模型的泛化能力和实际应用的鲁棒性。整体而言,本项目展示了深度学习技术在实际人脸识别任务中的强大潜力和应用价值。

附录

附录A:代码清单

A.1 数据采集 (data_capture.py)
# 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, args.face_num_max, args.path_name)
A.2 数据处理 (prepare.py)
# prepare.py
import os
import numpy as np
import cv2
import random
from torch.utils.data import DataLoader, Dataset
import torch

TRAIN_RATE = 0.8
VALID_RATE = 0.1
TEST_RATE = 0.1
SAMPLE_QUANTITY = 2000

IMAGE_SIZE = 227
MYPATH = "C:\\Users\\73559\\Desktop\\ml\\data"

class FaceDataset(Dataset):
    def __init__(self, data, transform=None):
        self.data = data
        self.transform = transform

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        img, label = self.data[idx]
        if self.transform:
            img = self.transform(img)
        # 确保标签为 torch.long 类型
        label = torch.tensor(label, dtype=torch.long)
        return img, label

def read_path(path_name):    
    images = []
    labels = []
    for dir_item in os.listdir(path_name):
        # 从初始路径开始叠加,合并成可识别的操作路径
        full_path = os.path.abspath(os.path.join(path_name, dir_item))        
        if os.path.isdir(full_path):    # 如果是文件夹,继续递归调用
            images_sub, labels_sub = read_path(full_path)
            images.extend(images_sub)
            labels.extend(labels_sub)
        else:   # 文件
            if dir_item.endswith('.jpg'):
                image = cv2.imread(full_path)                
                image = cv2.resize(image, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_AREA) # 修改图片的尺寸
                images.append(image)
                labels.append(os.path.basename(os.path.dirname(full_path)))                                                   
    return images, labels

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

if __name__ == "__main__":
    train_data, valid_data, test_data, label_dict = load_dataset(MYPATH)
    print(f'训练集大小: {len(train_data)}')
    print(f'验证集大小: {len(valid_data)}')
    print(f'测试集大小: {len(test_data)}')
    print(f'标签字典: {label_dict}')
    
    # 打印标签分布
    train_labels = [label for _, label in train_data]
    valid_labels = [label for _, label in valid_data]
    test_labels = [label for _, label in test_data]
    
    print(f'训练集标签分布: {np.bincount(train_labels)}')
    print(f'验证集标签分布: {np.bincount(valid_labels)}')
    print(f'测试集标签分布: {np.bincount(test_labels)}')
A.3 模型定义 (alexnet.py)
# alexnet.py
import torch
import torch.nn as nn

class AlexNet(nn.Module):
    def __init__(self, num_classes=2):
        super(AlexNet, self).__init__()
        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),
        )
        self.fc = nn.Sequential(
            nn.Linear(256*6*6, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),  # 添加 Dropout
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),  # 添加 Dropout
            nn.Linear(4096, num_classes),
            # Softmax 层不需要显式添加,因为在损失函数中已经处理
        )
    
    def forward(self, x):
        x = self.conv(x)
        x = x.view(x.size(0), -1)  # 展平
        x = self.fc(x) 
        return x
A.4 自定义损失函数 (focal_loss.py)
# focal_loss.py
import torch
import torch.nn as nn
import torch.nn.functional as F

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.5 模型训练 (train.py)
# train.py
import torch
import numpy as np
from torch.autograd import Variable
from prepare import load_dataset, MYPATH, FaceDataset
from alexnet import AlexNet
from focal_loss import FocalLoss  # 导入自定义的FocalLoss
from torch import nn, optim
from torch.utils.data import DataLoader
from torchvision import transforms

# 定义学习率
learning_rate = 0.001
# 是否使用GPU训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f'使用设备: {device}')

# 定义转换
transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(10),
    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  # 根据需要调整

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)

        # 确保标签为 torch.long 类型
        if labels.dtype != torch.long:
            labels = labels.long()

        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)
            
            # 确保标签为 torch.long 类型
            if labels.dtype != torch.long:
                labels = labels.long()

            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}')

# 保存训练好的模型
torch.save(model.state_dict(), 'alexnet_focal_loss.pth')
print('模型已保存为 alexnet_focal_loss.pth')
A.6 模型应用 (application.py)
# application.py
import numpy as np
import cv2
import sys
import torch
from torch.autograd import Variable
from alexnet import AlexNet
from prepare import load_dataset, MYPATH
import torch.nn.functional as F  # 添加这一行

# 加载标签字典
_, _, _, label_dict = load_dataset(MYPATH)
idx2label = {v: k for k, v in label_dict.items()}

def plot_confusion_matrix(cm, classes, title='Confusion Matrix'):
    """
    绘制混淆矩阵的热力图并保存为文件
    """
    import matplotlib
    matplotlib.use('Agg')  # 使用非交互式后端
    import matplotlib.pyplot as plt
    import seaborn as sns

    plt.figure(figsize=(8,6))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
                xticklabels=classes, yticklabels=classes)
    plt.ylabel('True Label')
    plt.xlabel('Predicted Label')
    plt.title(title)
    plt.savefig('confusion_matrix.png')  # 保存图像
    # plt.show()  # 移除或注释掉

if __name__ == '__main__':
    if len(sys.argv) != 1:
        print("Usage:%s camera_id\r\n" % (sys.argv[0]))
        sys.exit(0)
    
    # 加载模型
    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 TypeError:
        # 对于较旧的 PyTorch 版本,不支持 weights_only 参数
        model.load_state_dict(torch.load('alexnet_focal_loss.pth'))
    
    model.eval()
    print('模型已加载')
    
    # 框住人脸的矩形边框颜色       
    color = (0, 255, 0)
    
    # 捕获指定摄像头的实时视频流
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("无法打开摄像头")
        sys.exit()

    # 人脸识别分类器本地存储路径
    cascade_path = "D:\\opencv\\build\\etc\\haarcascades\\haarcascade_frontalface_alt2.xml"    
    
    # 加载分类器
    cascade = cv2.CascadeClassifier(cascade_path)
    if cascade.empty():
        print("无法加载人脸分类器")
        sys.exit()
    
    # 循环检测识别人脸
    while True:
        ret, frame = cap.read()   # 读取一帧视频
        
        if not ret:
            print("无法读取摄像头帧")
            break
        
        # 图像灰化,降低计算复杂度
        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        
        # 利用分类器识别出哪个区域为人脸
        faceRects = cascade.detectMultiScale(frame_gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
        
        if len(faceRects) > 0:                 
            for faceRect in faceRects: 
                x, y, w, h = faceRect
                
                # 确保裁剪区域在图像边界内
                x1 = max(x - 10, 0)
                y1 = max(y - 10, 0)
                x2 = min(x + w + 10, frame.shape[1])
                y2 = min(y + h + 10, frame.shape[0])
                
                # 截取脸部图像
                img = frame[y1:y2, x1:x2]
                
                if img.size == 0:
                    print("裁剪后的图像为空,跳过")
                    continue
                
                try:
                    img = cv2.resize(img, (227, 227), interpolation=cv2.INTER_AREA)
                except cv2.error as e:
                    print(f"图像调整大小失败: {e}")
                    continue
                
                img = img.astype(np.float32) / 255.0  # 归一化
                img = (img - 0.5) / 0.5  # 标准化
                img = np.transpose(img, (2, 0, 1))  # HWC to CHW
                img = torch.from_numpy(img).unsqueeze(0).to(device)
                
                with torch.no_grad():
                    outputs = model(img)
                    _, predicted = torch.max(outputs, 1)
                    confidence = F.softmax(outputs, dim=1)[0][predicted].item()
                    label = idx2label[predicted.item()]
                
                # 显示置信度
                cv2.putText(frame, f'Conf: {confidence:.2f}', 
                            (x1 - 40, y1 - 40),                      # 坐标
                            cv2.FONT_HERSHEY_SIMPLEX,                  # 字体
                            0.8,                                       # 字号
                            (255, 0, 255),                             # 颜色
                            2)                                         # 字的线宽
                
                # 绘制矩形框
                cv2.rectangle(frame, (x1, y1), (x2, y2), color, thickness=2)
                
                # 显示预测标签
                cv2.putText(frame, f'{label}', 
                            (x1 + 30, y1 + 30),                      # 坐标
                            cv2.FONT_HERSHEY_SIMPLEX,                  # 字体
                            1,                                         # 字号
                            (255, 0, 255),                             # 颜色
                            2)                                         # 字的线宽
                            
        cv2.imshow("Face Recognition", frame)
        
        # 等待10毫秒看是否有按键输入
        k = cv2.waitKey(10)
        # 如果输入q则退出循环
        if k & 0xFF == ord('q'):
            break

    # 释放摄像头并销毁所有窗口
    cap.release()
    cv2.destroyAllWindows()

2. 完整的 evaluate.py 示例

为了确保全面评估模型性能,下面提供了一个完整的evaluate.py示例,包括绘制混淆矩阵和展示样本预测。

# evaluate.py

import os
import numpy as np
import torch
from torch.utils.data import DataLoader
from torchvision import transforms
from prepare import load_dataset, MYPATH, FaceDataset
from alexnet import AlexNet
from focal_loss import FocalLoss  # 如果需要,可以在评估中使用
import torch.nn.functional as F
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score
import matplotlib
matplotlib.use('Agg')  # 使用非交互式后端
import matplotlib.pyplot as plt
import seaborn as sns

def plot_confusion_matrix(cm, classes, title='Confusion Matrix'):
    """
    绘制混淆矩阵的热力图并保存为文件
    """
    plt.figure(figsize=(8,6))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
                xticklabels=classes, yticklabels=classes)
    plt.ylabel('True Label')
    plt.xlabel('Predicted Label')
    plt.title(title)
    plt.savefig('confusion_matrix.png')  # 保存图像
    # plt.show()  # 移除或注释掉

def main():
    # 设置设备
    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)
    
    # 打印数据集大小和标签分布
    print(f'训练集大小: {len(train_data)}')
    print(f'验证集大小: {len(valid_data)}')
    print(f'测试集大小: {len(test_data)}')
    print(f'标签字典: {label_dict}')
    
    train_labels = [label for _, label in train_data]
    valid_labels = [label for _, label in valid_data]
    test_labels = [label for _, label in test_data]
    
    print(f'训练集标签分布: {np.bincount(train_labels)}')
    print(f'验证集标签分布: {np.bincount(valid_labels)}')
    print(f'测试集标签分布: {np.bincount(test_labels)}')
    
    test_dataset = FaceDataset(test_data, transform=transform)
    test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
    
    # 初始化模型
    model = AlexNet(num_classes=len(label_dict)).to(device)
    
    # 加载训练好的模型权重
    model_path = 'alexnet_focal_loss.pth'
    if not os.path.exists(model_path):
        print(f"模型文件 {model_path} 不存在。请先训练模型。")
        return
    
    try:
        # 使用 torch.load 的 map_location 参数以确保在不同设备上的兼容性
        model.load_state_dict(torch.load(model_path, map_location=device))
    except RuntimeError as e:
        print(f"加载模型权重时出错: {e}")
        return
    
    model.eval()
    print('模型已加载并设置为评估模式')
    
    all_preds = []
    all_labels = []
    
    correct_preds = []
    incorrect_preds = []
    
    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())
            
            # 收集正确和错误预测
            for img, pred, label in zip(images, preds, labels):
                if pred == label:
                    correct_preds.append((img.cpu(), pred.cpu(), label.cpu()))
                else:
                    incorrect_preds.append((img.cpu(), pred.cpu(), label.cpu()))
    
    # 计算准确率
    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')
    
    # 展示一些正确预测和错误预测的样本
    num_correct = min(5, len(correct_preds))
    num_incorrect = min(5, len(incorrect_preds))
    
    plt.figure(figsize=(15,6))
    
    for i in range(num_correct):
        img, pred, label = correct_preds[i]
        img = img.numpy().transpose(1, 2, 0)
        img = (img * 0.5) + 0.5  # 反标准化
        plt.subplot(2, 5, i+1)
        plt.imshow(img)
        plt.title(f'Pred: {list(label_dict.keys())[pred.item()]}')
        plt.axis('off')
    
    for i in range(num_incorrect):
        img, pred, label = incorrect_preds[i]
        img = img.numpy().transpose(1, 2, 0)
        img = (img * 0.5) + 0.5  # 反标准化
        plt.subplot(2, 5, num_correct + i + 1)
        plt.imshow(img)
        plt.title(f'Pred: {list(label_dict.keys())[pred.item()]} \nTrue: {list(label_dict.keys())[label.item()]}')
        plt.axis('off')
    
    plt.tight_layout()
    plt.savefig('sample_predictions.png')  # 保存样本预测图像
    # plt.show()  # 移除或注释掉
    print('样本预测图像已保存为 sample_predictions.png')
    
    # 可选:保存评估结果到文件
    with open('evaluation_report.txt', 'w') as f:
        f.write(f'Test Accuracy: {accuracy:.4f}\n\n')
        f.write('Confusion Matrix:\n')
        f.write(np.array2string(cm))
        f.write('\n\nClassification Report:\n')
        f.write(report)
    
    print('评估报告已保存为 evaluation_report.txt')
    print('混淆矩阵图像已保存为 confusion_matrix.png')
    print('样本预测图像已保存为 sample_predictions.png')

if __name__ == '__main__':
    main()

3. 总结

通过系统的设计与实现,本项目成功构建了一个基于深度学习的人脸识别系统。通过数据采集、数据处理、模型构建、训练与验证、模型测试等环节,系统能够高效地识别和分类不同个体的人脸图像。自定义的Focal Loss有效应对了类别不平衡问题,进一步提升了模型的识别准确率。然而,模型在测试集上达到了100%的准确率,表明数据集可能较小或存在过拟合现象,未来需要通过扩展数据集规模、引入更多的数据增强技术、优化模型架构等方式,进一步提升模型的泛化能力和实际应用的鲁棒性。

附录

附录A:代码清单

A.1 数据采集 (data_capture.py)
# 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, args.face_num_max, args.path_name)
A.2 数据处理 (prepare.py)
# prepare.py
import os
import numpy as np
import cv2
import random
from torch.utils.data import DataLoader, Dataset
import torch

TRAIN_RATE = 0.8
VALID_RATE = 0.1
TEST_RATE = 0.1
SAMPLE_QUANTITY = 2000

IMAGE_SIZE = 227
MYPATH = "C:\\Users\\73559\\Desktop\\ml\\data"

class FaceDataset(Dataset):
    def __init__(self, data, transform=None):
        self.data = data
        self.transform = transform

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        img, label = self.data[idx]
        if self.transform:
            img = self.transform(img)
        # 确保标签为 torch.long 类型
        label = torch.tensor(label, dtype=torch.long)
        return img, label

def read_path(path_name):    
    images = []
    labels = []
    for dir_item in os.listdir(path_name):
        # 从初始路径开始叠加,合并成可识别的操作路径
        full_path = os.path.abspath(os.path.join(path_name, dir_item))        
        if os.path.isdir(full_path):    # 如果是文件夹,继续递归调用
            images_sub, labels_sub = read_path(full_path)
            images.extend(images_sub)
            labels.extend(labels_sub)
        else:   # 文件
            if dir_item.endswith('.jpg'):
                image = cv2.imread(full_path)                
                image = cv2.resize(image, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_AREA) # 修改图片的尺寸
                images.append(image)
                labels.append(os.path.basename(os.path.dirname(full_path)))                                                   
    return images, labels

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

if __name__ == "__main__":
    train_data, valid_data, test_data, label_dict = load_dataset(MYPATH)
    print(f'训练集大小: {len(train_data)}')
    print(f'验证集大小: {len(valid_data)}')
    print(f'测试集大小: {len(test_data)}')
    print(f'标签字典: {label_dict}')
    
    # 打印标签分布
    train_labels = [label for _, label in train_data]
    valid_labels = [label for _, label in valid_data]
    test_labels = [label for _, label in test_data]
    
    print(f'训练集标签分布: {np.bincount(train_labels)}')
    print(f'验证集标签分布: {np.bincount(valid_labels)}')
    print(f'测试集标签分布: {np.bincount(test_labels)}')
A.3 模型定义 (alexnet.py)
# alexnet.py
import torch
import torch.nn as nn

class AlexNet(nn.Module):
    def __init__(self, num_classes=2):
        super(AlexNet, self).__init__()
        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),
        )
        self.fc = nn.Sequential(
            nn.Linear(256*6*6, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),  # 添加 Dropout
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),  # 添加 Dropout
            nn.Linear(4096, num_classes),
            # Softmax 层不需要显式添加,因为在损失函数中已经处理
        )
    
    def forward(self, x):
        x = self.conv(x)
        x = x.view(x.size(0), -1)  # 展平
        x = self.fc(x) 
        return x
A.4 自定义损失函数 (focal_loss.py)
# focal_loss.py
import torch
import torch.nn as nn
import torch.nn.functional as F

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.5 模型训练 (train.py)
# train.py
import torch
import numpy as np
from torch.autograd import Variable
from prepare import load_dataset, MYPATH, FaceDataset
from alexnet import AlexNet
from focal_loss import FocalLoss  # 导入自定义的FocalLoss
from torch import nn, optim
from torch.utils.data import DataLoader
from torchvision import transforms

# 定义学习率
learning_rate = 0.001
# 是否使用GPU训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f'使用设备: {device}')

# 定义转换
transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(10),
    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  # 根据需要调整

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)

        # 确保标签为 torch.long 类型
        if labels.dtype != torch.long:
            labels = labels.long()

        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)
            
            # 确保标签为 torch.long 类型
            if labels.dtype != torch.long:
                labels = labels.long()

            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}')

# 保存训练好的模型
torch.save(model.state_dict(), 'alexnet_focal_loss.pth')
print('模型已保存为 alexnet_focal_loss.pth')
A.6 模型应用 (application.py)
# application.py
import numpy as np
import cv2
import sys
import torch
from torch.autograd import Variable
from alexnet import AlexNet
from prepare import load_dataset, MYPATH
import torch.nn.functional as F  # 添加这一行

# 加载标签字典
_, _, _, label_dict = load_dataset(MYPATH)
idx2label = {v: k for k, v in label_dict.items()}

def plot_confusion_matrix(cm, classes, title='Confusion Matrix'):
    """
    绘制混淆矩阵的热力图并保存为文件
    """
    import matplotlib
    matplotlib.use('Agg')  # 使用非交互式后端
    import matplotlib.pyplot as plt
    import seaborn as sns

    plt.figure(figsize=(8,6))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
                xticklabels=classes, yticklabels=classes)
    plt.ylabel('True Label')
    plt.xlabel('Predicted Label')
    plt.title(title)
    plt.savefig('confusion_matrix.png')  # 保存图像
    # plt.show()  # 移除或注释掉

if __name__ == '__main__':
    if len(sys.argv) != 1:
        print("Usage:%s camera_id\r\n" % (sys.argv[0]))
        sys.exit(0)
    
    # 加载模型
    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 TypeError:
        # 对于较旧的 PyTorch 版本,不支持 weights_only 参数
        model.load_state_dict(torch.load('alexnet_focal_loss.pth'))
    
    model.eval()
    print('模型已加载')
    
    # 框住人脸的矩形边框颜色       
    color = (0, 255, 0)
    
    # 捕获指定摄像头的实时视频流
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("无法打开摄像头")
        sys.exit()

    # 人脸识别分类器本地存储路径
    cascade_path = "D:\\opencv\\build\\etc\\haarcascades\\haarcascade_frontalface_alt2.xml"    
    
    # 加载分类器
    cascade = cv2.CascadeClassifier(cascade_path)
    if cascade.empty():
        print("无法加载人脸分类器")
        sys.exit()
    
    # 循环检测识别人脸
    while True:
        ret, frame = cap.read()   # 读取一帧视频
        
        if not ret:
            print("无法读取摄像头帧")
            break
        
        # 图像灰化,降低计算复杂度
        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        
        # 利用分类器识别出哪个区域为人脸
        faceRects = cascade.detectMultiScale(frame_gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
        
        if len(faceRects) > 0:                 
            for faceRect in faceRects: 
                x, y, w, h = faceRect
                
                # 确保裁剪区域在图像边界内
                x1 = max(x - 10, 0)
                y1 = max(y - 10, 0)
                x2 = min(x + w + 10, frame.shape[1])
                y2 = min(y + h + 10, frame.shape[0])
                
                # 截取脸部图像
                img = frame[y1:y2, x1:x2]
                
                if img.size == 0:
                    print("裁剪后的图像为空,跳过")
                    continue
                
                try:
                    img = cv2.resize(img, (227, 227), interpolation=cv2.INTER_AREA)
                except cv2.error as e:
                    print(f"图像调整大小失败: {e}")
                    continue
                
                img = img.astype(np.float32) / 255.0  # 归一化
                img = (img - 0.5) / 0.5  # 标准化
                img = np.transpose(img, (2, 0, 1))  # HWC to CHW
                img = torch.from_numpy(img).unsqueeze(0).to(device)
                
                with torch.no_grad():
                    outputs = model(img)
                    _, predicted = torch.max(outputs, 1)
                    confidence = F.softmax(outputs, dim=1)[0][predicted].item()
                    label = idx2label[predicted.item()]
                
                # 显示置信度
                cv2.putText(frame, f'Conf: {confidence:.2f}', 
                            (x1 - 40, y1 - 40),                      # 坐标
                            cv2.FONT_HERSHEY_SIMPLEX,                  # 字体
                            0.8,                                       # 字号
                            (255, 0, 255),                             # 颜色
                            2)                                         # 字的线宽
                
                # 绘制矩形框
                cv2.rectangle(frame, (x1, y1), (x2, y2), color, thickness=2)
                
                # 显示预测标签
                cv2.putText(frame, f'{label}', 
                            (x1 + 30, y1 + 30),                      # 坐标
                            cv2.FONT_HERSHEY_SIMPLEX,                  # 字体
                            1,                                         # 字号
                            (255, 0, 255),                             # 颜色
                            2)                                         # 字的线宽
                            
        cv2.imshow("Face Recognition", frame)
        
        # 等待10毫秒看是否有按键输入
        k = cv2.waitKey(10)
        # 如果输入q则退出循环
        if k & 0xFF == ord('q'):
            break

    # 释放摄像头并销毁所有窗口
    cap.release()
    cv2.destroyAllWindows()

附录B:评估脚本 (evaluate.py)

# evaluate.py

import os
import numpy as np
import torch
from torch.utils.data import DataLoader
from torchvision import transforms
from prepare import load_dataset, MYPATH, FaceDataset
from alexnet import AlexNet
from focal_loss import FocalLoss  # 如果需要,可以在评估中使用
import torch.nn.functional as F
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score
import matplotlib
matplotlib.use('Agg')  # 使用非交互式后端
import matplotlib.pyplot as plt
import seaborn as sns

def plot_confusion_matrix(cm, classes, title='Confusion Matrix'):
    """
    绘制混淆矩阵的热力图并保存为文件
    """
    plt.figure(figsize=(8,6))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
                xticklabels=classes, yticklabels=classes)
    plt.ylabel('True Label')
    plt.xlabel('Predicted Label')
    plt.title(title)
    plt.savefig('confusion_matrix.png')  # 保存图像
    # plt.show()  # 移除或注释掉

def main():
    # 设置设备
    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)
    
    # 打印数据集大小和标签分布
    print(f'训练集大小: {len(train_data)}')
    print(f'验证集大小: {len(valid_data)}')
    print(f'测试集大小: {len(test_data)}')
    print(f'标签字典: {label_dict}')
    
    train_labels = [label for _, label in train_data]
    valid_labels = [label for _, label in valid_data]
    test_labels = [label for _, label in test_data]
    
    print(f'训练集标签分布: {np.bincount(train_labels)}')
    print(f'验证集标签分布: {np.bincount(valid_labels)}')
    print(f'测试集标签分布: {np.bincount(test_labels)}')
    
    test_dataset = FaceDataset(test_data, transform=transform)
    test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
    
    # 初始化模型
    model = AlexNet(num_classes=len(label_dict)).to(device)
    
    # 加载训练好的模型权重
    model_path = 'alexnet_focal_loss.pth'
    if not os.path.exists(model_path):
        print(f"模型文件 {model_path} 不存在。请先训练模型。")
        return
    
    try:
        # 使用 torch.load 的 map_location 参数以确保在不同设备上的兼容性
        model.load_state_dict(torch.load(model_path, map_location=device))
    except RuntimeError as e:
        print(f"加载模型权重时出错: {e}")
        return
    
    model.eval()
    print('模型已加载并设置为评估模式')
    
    all_preds = []
    all_labels = []
    
    correct_preds = []
    incorrect_preds = []
    
    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())
            
            # 收集正确和错误预测
            for img, pred, label in zip(images, preds, labels):
                if pred == label:
                    correct_preds.append((img.cpu(), pred.cpu(), label.cpu()))
                else:
                    incorrect_preds.append((img.cpu(), pred.cpu(), label.cpu()))
    
    # 计算准确率
    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')
    
    # 展示一些正确预测和错误预测的样本
    num_correct = min(5, len(correct_preds))
    num_incorrect = min(5, len(incorrect_preds))
    
    plt.figure(figsize=(15,6))
    
    for i in range(num_correct):
        img, pred, label = correct_preds[i]
        img = img.numpy().transpose(1, 2, 0)
        img = (img * 0.5) + 0.5  # 反标准化
        plt.subplot(2, 5, i+1)
        plt.imshow(img)
        plt.title(f'Pred: {list(label_dict.keys())[pred.item()]}')
        plt.axis('off')
    
    for i in range(num_incorrect):
        img, pred, label = incorrect_preds[i]
        img = img.numpy().transpose(1, 2, 0)
        img = (img * 0.5) + 0.5  # 反标准化
        plt.subplot(2, 5, num_correct + i + 1)
        plt.imshow(img)
        plt.title(f'Pred: {list(label_dict.keys())[pred.item()]} \nTrue: {list(label_dict.keys())[label.item()]}')
        plt.axis('off')
    
    plt.tight_layout()
    plt.savefig('sample_predictions.png')  # 保存样本预测图像
    # plt.show()  # 移除或注释掉
    print('样本预测图像已保存为 sample_predictions.png')
    
    # 可选:保存评估结果到文件
    with open('evaluation_report.txt', 'w') as f:
        f.write(f'Test Accuracy: {accuracy:.4f}\n\n')
        f.write('Confusion Matrix:\n')
        f.write(np.array2string(cm))
        f.write('\n\nClassification Report:\n')
        f.write(report)
    
    print('评估报告已保存为 evaluation_report.txt')
    print('混淆矩阵图像已保存为 confusion_matrix.png')
    print('样本预测图像已保存为 sample_predictions.png')

if __name__ == '__main__':
    main()

附录C:评估结果示例

测试集准确率:

测试集准确率: 1.0000

混淆矩阵:

[[94  0]
 [ 0 87]]

分类报告:

              precision    recall  f1-score   support

         cai       1.00      1.00      1.00        94
    jianming       1.00      1.00      1.00        87

    accuracy                           1.00       181
   macro avg       1.00      1.00      1.00       181
weighted avg       1.00      1.00      1.00       181

混淆矩阵热力图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

样本预测展示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

参考文献

  1. He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. Proceedings of the IEEE conference on computer vision and pattern recognition, 770-778.
  2. Simonyan, K., & Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. arXiv preprint arXiv:1409.1556.
  3. Lin, T. Y., Goyal, P., Girshick, R., He, K., & Dollar, P. (2017). Focal loss for dense object detection. Proceedings of the IEEE international conference on computer vision, 2980-2988.
  4. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.
  5. Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic optimization. arXiv preprint arXiv:1412.6980.
  6. He, K., Zhang, X., Ren, S., & Sun, J. (2015). Delving deep into rectifiers: Surpassing human-level performance on imagenet classification. Proceedings of the IEEE international conference on computer vision, 1026-1034.
  7. Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). ImageNet classification with deep convolutional neural networks. Advances in neural information processing systems, 1097-1105.
  8. OpenCV Documentation. (n.d.). Haar Cascades. Retrieved from https://docs.opencv.org/master/d7/d00/tutorial_meanshift.html

附录D:评估报告 (evaluation_report.txt)

Test Accuracy: 1.0000

Confusion Matrix:
[[94  0]
 [ 0 87]]

Classification Report:
              precision    recall  f1-score   support

         cai       1.00      1.00      1.00        94
    jianming       1.00      1.00      1.00        87

    accuracy                           1.00       181
   macro avg       1.00      1.00      1.00       181
weighted avg       1.00      1.00      1.00       181

致谢

感谢指导老师在项目设计与实现过程中提供的宝贵建议和技术支持。同时,感谢同学们在数据采集和代码测试阶段的协助,使本项目得以顺利完成。

声明

本文所述内容为本人原创,所有引用的文献和资源已在参考文献中列出。未经许可,不得转载或用于商业用途。

联系方式

如有任何问题或建议,欢迎通过以下方式联系本人:


结束语

本报告详细介绍了基于深度学习的人脸识别系统的设计与实现过程,从数据采集到模型训练,再到模型评估与应用,全面覆盖了项目的各个环节。通过系统的设计与优化,模型在测试集上实现了卓越的识别准确率,展示了深度学习技术在计算机视觉领域的强大潜力。未来,通过进一步扩展数据集、优化模型架构和提升系统性能,能够更好地满足实际应用需求,推动人脸识别技术的发展与应用。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐