-
Notifications
You must be signed in to change notification settings - Fork 32
6、用Facenet模型提取人脸特征
之前用自己训练的神经网络来识别人脸有大量的参数需要拟合,而我准备的训练数据比较少,这种情况其实更适合使用迁移学习的方案,也就是用一个已经由大量数据预训练好的深度CNN模型,利用自己的少量数据只对最后一层或几层的参数进行微调,从而训练出适合自己问题的模型。 这里我选用预训练好的Facenet模型提取128维的人脸特征,在最后一层用传统机器学习的KNN(k-近邻算法)模型比对人脸特征,从而实现人脸识别。Facenet是Google提出的一个用于人脸识别的深度卷积神经网络,其具体模型其实是一个类似于Inception Net的CNN模型,只是其最后一层不是通过传统的Softmax layer来训练,而是通过提取128维的人脸特征用Triplet Loss来训练,我所需要的正是Facenet模型通过大量人脸数据学到的128维高级特征。
这个方案里,人脸识别的整个方案架构依然不变,只是模型由简单CNN改为Facenet+knn,还是先说一下整体的思路:
- 导入Facenet预训练模型;
- 读取图片,并用Facenet预训练模型将图片数据转化为128维向量作为训练数据;
- 建立KNN模型,通过K折交叉验证选择最佳的超参数k,并存储最佳的模型;
- 用摄像头实时提取人脸,生成128维特征向量,并通过存储的模型对人脸进行识别。
首先需要一个Keras实现的Facenet预训练模型,我尝试过吴恩达深度学习课程人脸识别编程作业里的模型,那个模型是通过载入预训练好的权重参数来生成模型,实际使用的时候比较慢,还有的模型是Python2实现的,而我需要Python3实现的模型,最终我用到的模型来自keras-facenet。把模型下载好后放到工作目录下的model文件夹下,新建一个face_knn_classifier.py
,其作用类似于之前的face_train_keras.py
:
from keras.models import load_model
from load_face_dataset import load_dataset, IMAGE_SIZE, resize_image
import numpy as np
from fr_utils import img_to_encoding
from sklearn.model_selection import cross_val_score, ShuffleSplit, KFold
from sklearn.neighbors import KNeighborsClassifier
from sklearn.externals import joblib
import matplotlib.pyplot as plt
# 建立facenet模型
facenet = load_model('./model/facenet_keras.h5') # bad marshal data (unknown type code),用Python2实现的模型时会报这个错
#facenet.summary()
class Dataset:
# http://www.runoob.com/python3/python3-class.html
# 很多类都倾向于将对象创建为有初始状态的。
# 因此类可能会定义一个名为 __init__() 的特殊方法(构造方法),类定义了 __init__() 方法的话,类的实例化操作会自动调用 __init__() 方法。
# __init__() 方法可以有参数,参数通过 __init__() 传递到类的实例化操作上,比如下面的参数path_name。
# 类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。
# self 代表的是类的实例,代表当前对象的地址,而 self.class 则指向类。
def __init__(self, path_name):
# 训练集
self.X_train = None
self.y_train = None
# 数据集加载路径
self.path_name = path_name
# 加载数据集
def load(self, img_rows = IMAGE_SIZE, img_cols = IMAGE_SIZE, img_channels = 3, model = facenet):
# 加载数据集到内存
images, labels = load_dataset(self.path_name)
# 生成128维特征向量
X_embedding = img_to_encoding(images, model) # 考虑这里分批执行,否则可能内存不够,这里在img_to_encoding函数里通过predict的batch_size参数实现
# 输出训练集、验证集和测试集的数量
print('X_train shape', X_embedding.shape)
print('y_train shape', labels.shape)
print(X_embedding.shape[0], 'train samples')
# 这里对X_train就不再进一步normalization了,因为已经在facenet里有了l2_norm
self.X_train = X_embedding
self.y_train = labels
上面列出了加载Facenet模型和准备训练数据的代码,这里和之前准备训练数据就不一样了,首先还是通过load_dataset
函数来读取图片和图片类别标签,不过load_dataset
函数有一些修改,在load_face_dataset.py
文件里,删掉了read_path
函数,load_dataset
函数改为:
def load_dataset(data_dir):
images = [] # 用来存放图片
labels = [] # 用来存放类别标签
sample_nums = [] # 用来存放不同类别的人脸数据量
classes = os.listdir(data_dir) # 通过数据集路径下文件夹的数量得到所有类别
category = 0 # 分类标签计数
for person in classes: # person是不同分类人脸的文件夹名
person_dir = os.path.join(data_dir, person) # person_dir是某一分类人脸的路径名
person_pics = os.listdir(person_dir) # 某一类人脸路径下的全部人脸数据文件
for face in person_pics: # face是某一分类文件夹下人脸图片数据的文件名
img = cv2.imread(os.path.join(person_dir, face)) # 通过os.path.join得到人脸图片的绝对路径
if img is None: # 遇到部分数据有点问题,报错'NoneType' object has no attribute 'shape'
pass
else:
img = resize_image(img, IMAGE_SIZE, IMAGE_SIZE)
images.append(img) # 得到某一分类下的所有图片
labels.append(category) # 给某一分类下的所有图片赋予分类标签值
sample_nums.append(len(person_pics)) # 得到某一分类下的样本量
category += 1
images = np.array(images)
labels = np.array(labels)
print("Number of classes: ", len(classes)) # 输出分类数
for i in range(len(sample_nums)):
print("Number of the sample of class ", i, ": ", sample_nums[i]) # 输出每个类别的样本量
return images, labels
之前的load_dataset
函数通过判断图片文件路径名的不同来给分类标签赋值:
labels = np.array([0 if label.endswith('me') else 1 for label in labels])
这种方式不方便类别的扩展,假如有三类以上,就不好实现了,修改后的load_dataset
函数则通过遍历数据集路径下的不同人脸文件夹得到不同类别,然后遍历每个人脸文件夹得到每一类人脸数据,用一个category
变量储存类别标签,每遍历一类人脸category
变量就加一。这种方式可以很方便地扩展类别。另外我还输出了类别数以及每一类的样本数。这里需要额外了解的一个知识点是os.listdir()
方法得到的所有文件夹和文件是按文件(夹)名的首字母按字母顺序升序(a→z)排序的,因此当有多个人脸类别的时候就要按想要的类别顺序来给人脸文件夹命名。
接下来再回到face_knn_classifier.py
程序里,加载了图片数据后我用一个img_to_encoding
函数将图片数据转化为128维特征向量,这个函数我单独写在了fr_utils.py
文件里:
def img_to_encoding(images, model):
# 这里image的格式就是opencv读入后的格式
images = images[...,::-1] # Color image loaded by OpenCV is in BGR mode. But Matplotlib displays in RGB mode. 这里的操作实际是对channel这一dim进行reverse,从BGR转换为RGB
images = np.around(images/255.0, decimals=12) # np.around是四舍五入,其中decimals是保留的小数位数,这里进行了归一化
# https://stackoverflow.com/questions/44972565/what-is-the-difference-between-the-predict-and-predict-on-batch-methods-of-a-ker
if images.shape[0] > 1:
embedding = model.predict(images, batch_size = 128) # predict是对多个batch进行预测,这里的128是尝试后得出的内存能承受的最大值
else:
embedding = model.predict_on_batch(images) # predict_on_batch是对单个batch进行预测
# 报错,operands could not be broadcast together with shapes (2249,128) (2249,),因此要加上keepdims = True
embedding = embedding / np.linalg.norm(embedding, axis = 1, keepdims = True) # 注意这个项目里用的keras实现的facenet模型没有l2_norm,因此要在这里加上
return embedding
这里对img_to_encoding
函数需要说明几点:
- OpenCV读取得到的图片数据像素排列是BGR模式,而CNN模型中要求的图片数据是RGB模式,这里通过
images = images[...,::-1]
对channel维度反序即可,这里用到的numpy多维数组的操作可以参考我的另一篇手记:Python列表切片小结; - 在生成128维特征向量的时候,图片数量大于一的情况是用于准备训练数据,用到的是
model.predict()
方法,这个方法可以指定batch_size
参数的大小,从而利用神经网络的向量化计算加速,当然batch_size
的具体大小要根据电脑内存的承受能力来设置;图片数量为一的情况是用于实时人脸的识别,这时用的是model.predict_on_batch()
方法,这个方法没有batch_size
参数,把输入的全部数据作为一整个batch来计算,实时人脸识别的时候就是对一帧画面中提取的单张人脸图片进行计算; - 完整的Facenet模型对最终的128维特征向量有一个L2 normalization的操作,而我所使用的Facenet预训练模型是没有这一步的,这个可以通过前面的
facenet.summary
语句输出模型的具体结构看到,也可以看模型结构的原始代码。因此,这里要自己在img_to_encoding
函数里加上这一步。后面训练KNN模型的时候我发现加了L2 normalization这一步,KNN模型的准确率有显著提升,说明这一步还是很必要的。
接着再回到face_knn_classifier.py
程序里,得到128维的特征向量后我就直接把他们作为训练数据了,没有像之前一样划分为训练集和测试集,至于为什么这样做,我会在下一篇介绍KNN的手记里继续讲解。
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.