1 简介
LFW人脸数据集主要用于测试模型准确率。数据库中共有13233张图像和5749人。 每张图片都是250x250 jpg。目前LFW数据集不用作训练,主要用于测试。因此本文主要讲解其测试协议。
程序使用pytorch
LFW官方网址:http://vis-www.cs.umass.edu/lfw/
本次实验测试MobileFaceNet模型在LFW上的准确率。
lfw数据集(已经对齐并且大小为112×112):
链接:https://pan.baidu.com/s/1Sy0EkKRfb0xHRgq1iao2qg
提取码:1rf3
2 数据集准备
2.1 LFW数据集下载
下载地址:http://vis-www.cs.umass.edu/lfw/lfw.tgz
2.2 人脸预处理
下载的LFW数据集是250×250的图像,我们需要将其进行人脸预处理操作(人脸检测+人脸对齐),具体方法可以看我另一个博客。
https://blog.csdn.net/qq_41684249/article/details/111302649
三、测试协议
3.1 pair.txt
下载地址:http://vis-www.cs.umass.edu/lfw/pairs.txt
该文件将数据库随机分为10组,我们随机选择300个匹配对和300个每组中不匹配的对。使用此拆分,可以使用10倍交叉验证得出数据库的性能。
pair.txt文件的格式如下:
- 第一行显示套数后跟每套匹配对的数量(相等到每组不匹配对的数量)。
- 匹配对格式如下:
name n1 n2
表示匹配对name人的第n1和第n2张图。 - 不匹配对格式如下:
name1 n1 name2 n2
表示不匹配对name1人的第n1张图和name2人的第n2张图。
3.2 具体流程
- 处理LFW原始数据集,具体方法见:https://blog.csdn.net/qq_41684249/article/details/111302649
- 使用python,pytorch进行数据读取。
- 送入预训练网络MobileFaceNet,得到特征。输入112×112的对齐人脸图像,提取的特征为512维。
- 将LFW测试集所有图像进行特征提取。
- 通过cosine距离/欧式距离计算两张人脸的相似度。通过最优阈值得到准确率。
- 最优阈值的选取:先将训练数据分为10组,然后选取其中一组,计算其余九组的最大ROC得到最优阈值,这个最优阈值作为该组的阈值,算出该组的准确率。这样重复十次。得到最终的准确率。具体请看程序。
四、程序
4.1 MobileFaceNet
预训练模型见博客。
博客链接:https://blog.csdn.net/qq_41684249/article/details/115357756?spm=1001.2014.3001.5501
4.2 配置文件config.py
只需将这个配置文件对应修改就行。
img_list.txt和pairs.txt文件见第一部分。
# 测试协议txt文件
PAIRS_FILE_PATH = "/home/malidong/workspace/dataset/lfw/pairs.txt"
# 预处理后的数据集地址
CROPPED_FACE_FOLDER = "/home/malidong/workspace/dataset/lfw/112×112"
# 包含数据集所有相对路径的txt文件地址
IMAGE_LIST_FILE_PATH = "/home/malidong/workspace/dataset/lfw/img_list.txt"
# 模型输出维度
FEAT_DIM = 512
# 深度卷积层核大小
OUT_H = 7
OUT_W = 7
# 预训练模型地址
MODEL_PATH = "/home/malidong/workspace/mobilefacenet/Epoch_17.pt"
# 设备选择
DEVICE = "cuda"
4.3 数据集加载文件test_dataset.py
import os
import cv2
import numpy as np
from torch.utils.data import Dataset
import torchvision.transforms as transforms
class CommonTestDataset(Dataset):
def __init__(self, image_root, image_list_file):
'''
普通测试数据集加载
:param image_root: 数据集根目录
:param image_list_file: txt文件,包含所有图片相对路径
'''
self.image_root = image_root
self.image_list = []
image_list_buf = open(image_list_file) # 开始读取文件
line = image_list_buf.readline().strip() # 读取一行
while line:
self.image_list.append(line)
line = image_list_buf.readline().strip() # 继续读取下一行
# 预处理
self.transform = transforms.Compose([
transforms.ToTensor(), # 0-1
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # 归一化
])
def __len__(self):
return len(self.image_list)
def __getitem__(self, index):
short_image_path = self.image_list[index] # 图片相对路径
image_path = os.path.join(self.image_root, short_image_path) # 图片路径
image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_UNCHANGED) # 读取图片
#image = cv2.resize(image, (128, 128))
image = self.transform(image) # 预处理
return image, short_image_path # 图片 相对路径(相当于图片身份,用于查询)
4.4 lfw评估程序lfw_evaluator.py
import torch
import numpy as np
import torch.nn.functional as F
class LFWEvaluator(object):
def __init__(self, data_loader, pairs_file, device):
'''
LFW测试协议 先将所有的数据集进行特征提取,然后在测试
:param data_loader: 数据集加载
:param pairs_file: 测试txt文件地址
:param device: 设备
'''
self.data_loader = data_loader
self.device = device
self.pair_list = self.pairsParser(pairs_file)
def pairsParser(self, pairs_file):
'''
读取pair.txt文件内容
:param pairs_file: 测试txt文件地址
:return: (图1 图2 标签),是一个list列表文件。标签为0表示不同,标签为1表示相同。
'''
test_pair_list = [] # 创建一个list
pairs_file_buf = open(pairs_file) # 读取文件
line = pairs_file_buf.readline() # 跳过第一行 因为第一行是无关的内容
line = pairs_file_buf.readline().strip() # 读取一行,去除首尾空格
while line: # 只要文件有内容,就会读取
line_strs = line.split('\t') # 按空格分割
if len(line_strs) == 3: # 如果是3元素,则表示两张人脸是同一个人
person_name = line_strs[0] # 第一个元素是姓名
image_index1 = line_strs[1] # 第二个元素是第一张图的索引
image_index2 = line_strs[2] # 第三个元素是第二张图的索引
image_name1 = person_name + '/' + person_name + '_' + image_index1.zfill(4) + '.jpg' # 得到第一张人脸的地址
image_name2 = person_name + '/' + person_name + '_' + image_index2.zfill(4) + '.jpg' # 得到第二张人脸的地址
label = 1 # 标签为1表示是同一个身份
elif len(line_strs) == 4: # 表示两张人脸是不同的人
person_name1 = line_strs[0] # 第一个人的姓名
image_index1 = line_strs[1] # 第一个人的索引
person_name2 = line_strs[2] # 第二个人的姓名
image_index2 = line_strs[3] # 第二个人的索引
image_name1 = person_name1 + '/' + person_name1 + '_' + image_index1.zfill(4) + '.jpg' # 得到第一张人脸的地址
image_name2 = person_name2 + '/' + person_name2 + '_' + image_index2.zfill(4) + '.jpg' # 得到第二张人脸的地址
label = 0 # 标签为0表示不同身份
else:
raise Exception('Line error: %s.' % line)
test_pair_list.append((image_name1, image_name2, label)) # 存入list中
line = pairs_file_buf.readline().strip() # 读取下一行
return test_pair_list
def extract_feature(self, model, data_loader):
'''
提取所有测试集人脸特征
:param model: 要测试的模型
:param data_loader: 数据集
:return: 字典形式 图片名字(相对路径):提取的特征
'''
model.eval() # 测试模式,如果不写,会对bn层有影响
image_name2feature = {} # 字典
with torch.no_grad(): # 不进行梯度下降
for batch_idx, (images, filenames) in enumerate(data_loader): # 读取数据
images = images.to(self.device) # 图片
features = model(images) # 特征
features = F.normalize(features) # 特征归一化 用于求余弦相似度
features = features.cpu().numpy()
for filename, feature in zip(filenames, features):
image_name2feature[filename] = feature # 存入字典
return image_name2feature
def test(self, model):
'''
测试程序
:param model: 测试模型
:return: 准确率和方差
'''
image_name2feature = self.extract_feature(model, self.data_loader)
mean, std = self.test_one_model(self.pair_list, image_name2feature)
return mean, std
def test_one_model(self, test_pair_list, image_name2feature, is_normalize = True):
'''
获取模型的LFW测试准确率
:param test_pair_list: 测试pair.txt文件
:param image_name2feature: 存入特征的字典
:param is_normalize: 是否已经归一化,如果已经归一化,就True,无需在进行归一化。
:return: 准确率、方差
'''
subsets_score_list = np.zeros((10, 600), dtype = np.float32) # 余弦相似度
subsets_label_list = np.zeros((10, 600), dtype = np.int8) # 标签
for index, cur_pair in enumerate(test_pair_list): # 图1 图2 标签
cur_subset = index // 600 # 对600取整 一共6000对,分成10组
cur_id = index % 600 # 对600取余
image_name1 = cur_pair[0] # 图1
image_name2 = cur_pair[1] # 图2
label = cur_pair[2] # 标签 0表示不同 1表示相同
subsets_label_list[cur_subset][cur_id] = label # 存入标签
feat1 = image_name2feature[image_name1] # 图1的特征
feat2 = image_name2feature[image_name2] # 图2的特征
if not is_normalize: # 归一化
feat1 = feat1 / np.linalg.norm(feat1)
feat2 = feat2 / np.linalg.norm(feat2)
cur_score = np.dot(feat1, feat2) # 余弦相似度(即得分)
subsets_score_list[cur_subset][cur_id] = cur_score # 存入余弦相似度
subset_train = np.array([True] * 10)
accu_list = []
for subset_idx in range(10): # 一组一组算
test_score_list = subsets_score_list[subset_idx] # 一组的余弦相似度
test_label_list = subsets_label_list[subset_idx] # 一组的标签
subset_train[subset_idx] = False # 改为False
train_score_list = subsets_score_list[subset_train].flatten() # 其余9组的余弦相似度
train_label_list = subsets_label_list[subset_train].flatten() # 其余9组的标签
subset_train[subset_idx] = True # 改为True
best_thres = self.getThreshold(train_score_list, train_label_list) # 用其余9组得出阈值
positive_score_list = test_score_list[test_label_list == 1] # 将标签为1的放入一个余弦相似度列表
negtive_score_list = test_score_list[test_label_list == 0] # 将标签为0的放入一个余弦相似度列表
true_pos_pairs = np.sum(positive_score_list > best_thres) # 计算结果正确的数量
true_neg_pairs = np.sum(negtive_score_list < best_thres) # 计算结果正确的数量
accu_list.append((true_pos_pairs + true_neg_pairs) / 600) # 计算每组最后结果
mean = np.mean(accu_list) # 取平均值
std = np.std(accu_list, ddof=1) / np.sqrt(10) # 方差
return mean, std
def getThreshold(self, score_list, label_list, num_thresholds=1000):
'''
得到最佳阈值
:param score_list: 余弦相似度list
:param label_list: 标签list
:param num_thresholds: 只需n次得到最优roc
:return:
'''
pos_score_list = score_list[label_list == 1] # 将标签为1的放入一个余弦相似度列表
neg_score_list = score_list[label_list == 0] # 将标签为0的放入一个余弦相似度列表
pos_pair_nums = pos_score_list.size # 数量
neg_pair_nums = neg_score_list.size # 数量
score_max = np.max(score_list) # 最大余弦相似度
score_min = np.min(score_list) # 最小余弦相似度
score_span = score_max - score_min # 相减
step = score_span / num_thresholds # 切分成num_thresholds个
threshold_list = score_min + step * np.array(range(1, num_thresholds + 1)) # 得到一个切分后的阈值list
fpr_list = []
tpr_list = []
for threshold in threshold_list: # 一个个试
fpr = np.sum(neg_score_list > threshold) / neg_pair_nums # 错误/负样本数
tpr = np.sum(pos_score_list > threshold) /pos_pair_nums # 正确/正样本数
fpr_list.append(fpr)
tpr_list.append(tpr)
fpr = np.array(fpr_list)
tpr = np.array(tpr_list)
best_index = np.argmax(tpr-fpr) # 取最大值
best_thres = threshold_list[best_index]
return best_thres # 得到最大阈值
4.5 主程序test_lfw.py
import os
from prettytable import PrettyTable
from torch.utils.data import DataLoader
from test_dataset import CommonTestDataset
from MobileFaceNets import MobileFaceNet
import config
from lfw_evaluator import LFWEvaluator
if __name__ == '__main__':
# 参数预加载
pairs_file_path = config.PAIRS_FILE_PATH
cropped_face_folder = config.CROPPED_FACE_FOLDER
image_list_file_path = config.IMAGE_LIST_FILE_PATH
feat_dim = config.FEAT_DIM
out_h = config.OUT_H
out_w = config.OUT_W
model_path = config.MODEL_PATH
device = config.DEVICE
# 设置gpu
os.environ["CUDA_VISIBLE_DEVICES"] = "0, 1"
# 测试数据集加载
data_loader = DataLoader(CommonTestDataset(cropped_face_folder, image_list_file_path),
batch_size=1024, num_workers=16, shuffle=False)
# 模型加载
model = MobileFaceNet(feat_dim, out_h, out_w)
model = model.load_model(model, model_path)
# lfw评估类加载
lfw_evaluator = LFWEvaluator(data_loader, pairs_file_path, device)
# 评估
mean, std = lfw_evaluator.test(model)
# 显示
accu_list = [(os.path.basename(model_path), mean, std)]
pretty_tabel = PrettyTable(["model_name", "mean accuracy", "standard error"])
for accu_item in accu_list:
pretty_tabel.add_row(accu_item)
print(pretty_tabel)