Training DeepID1 Network for Face Comparison with Google Colab+Tensorflow

本文由2019年6月《软件工程》必修课的课程设计报告的AI部分改编

主要介绍了“员工考勤管理系统”课程设计中的员工人脸打卡子系统
该系统使用了Google CoLab提供的在线Tensorflow GPU平台训练得到的DeepID人脸特征提取比对模型,
以及基于该模型搭建的Tensorflow+OpenCV+Flask人脸比对Python服务器

中文标题:使用Google CoLab+Tensorflow训练DeepID1人脸比对模型

项目地址

  1. Google CoLab(需要访问国外网站的能力)
  2. GitHub(待发布)

简介

deepid

图1.1 DeepID的网络结构
其中DeepID层能够提取出160维特征向量

DeepID是香港中文大学王晓刚教授团队在CVPR2014上发表的论文《Deep Learning Face Representation from Predicting 10,000 Classes》提出的方法,全称为Deep hidden IDentity feature(DeepID)。

该方法是一种特征提取的算法,对于一个多层卷积-池化网络进行多分类任务训练后,在其中一层中间层DeepID层能够提取出输入的任意人脸图片的160维深层次特征向量(实际上是160x2x60维的特征向量)。

而这种特征提取的能力是面向任意的(需要经过预先裁剪和对齐后的)人脸图像的,因此作者做出了一个形象的比喻:即使是分10000个类,网络也能够有效区分出每个类别的人脸的显著特征(从而通过特征之间的距离,识别出两张人脸是否为同一人)。

因此,这一方法体现出的以下特性,使得我们最终在众多人脸特征提取方法中选取了DeepId:

  1. 方法实现的仅需一次训练即可获得的人脸特征提取能力,十分适合企业员工人脸考勤环境下员工人脸库经常性变动、待对比人脸图像来源较为复杂的应用场景。
  2. 方法的网络结构简单,易于理解和实现。同时,网络层数较少,相应地也能够减少训练所消耗的时间和硬件资源,便于我们在短周期(8周,AI子系统开发仅一周)的软件工程课程设计开发过程中安排进度。最终,该算法的训练时长在Google CoLab上为50000次/2小时。
  3. 方法的准确率较高,在Tensorflow的实现+YouTube Aligned Faces数据集上的测试集人脸比对识别准确率能够达到96%。

当然,这一方法作为一个2014年提出的方法,(也是DeepID三代中的第一个版本)也存在着一定的缺陷:

  1. 仅适用于提取图像中的正脸,也就是通过摄像头正对人脸拍摄的、或者是通过一定图像处理算法重新对齐的人脸。对于侧脸、带有一定歪斜的人脸等日常生活中常见的人脸图像,识别能力大打折扣。也正因如此,GitHub上DeepID的Tensorflow实现采用了Youtube Aligned Faces数据集,已经做过了人脸对齐的预处理,用来训练DeepID较为方便。
  2. 在实际使用的过程中,笔者发现这一模型对于裁剪得出的人脸图像的光线明暗、是否佩戴眼镜等变化是敏感的,只有在光照条件、脸部配饰等状况近似于人脸图像采集时的情况下,才能够被识别为同一人。

因此,目前主流的人脸特征比对方法都聚焦在人脸检测阶段的多特征点提取、侧脸特征点的重新对齐、人脸3D模型识别(一个最著名的案例,就是Apple在iPhone上用于FaceID的3D结构光特征点识别方案)等研究方向。
至于Google Colab,是谷歌打造的的一个在线深度学习平台,基于Jupyter Notebook+Tensorflow,能够通过简单的配置,使用Google免费提供的云端GPU资源,从而无需本地硬件资源地轻松训练自己的神经网络。在很久之前的一次计设校赛上曾经使用过这一平台,因此本项目也继续使用这个平台对DeepID网络进行训练。

训练环境搭建

访问 https://colab.research.google.com,如果没有谷歌账号可以先去注册一个,列表中是已有的Jupyter Notebook文件,创建的文件一般会放在Google 云端硬盘的/colab notebook文件夹下。一般是创建Python 3笔记本,

colab

图2.1 Google Colab的初始界面

Colab的环境初始化结束后,呈现的是经典的jupyter notebook界面,先点击“代码执行程序-更改运行时类型”,将“硬件加速器”从“None”修改为“GPU”,这样就可以免费使用基于谷歌提供的云端Nvidia GTX Tesla T4 GPU的Tensorflow GPU版本,显存15GB,比自己笔记本的4G独显性能高多了。

注意!千万不要选择TPU!

虽然TPU是Google推出的号称Tensorflow专用的GPU平台,但是其训练速度真的难以接受,在下文我会附上GPU和TPU训练DeepID网络时的Tensorboard检测到的数据,足以体现两者之间的性能差异。

colab

图2.2 在Colab选取GPU

之后可以在左侧边栏中,查看文件目录,会发现一个“挂在Google云端硬盘”的选项,点击之后就会生成一个cell。内容大致为

1
2
3
# 运行此单元格即可装载您的 Google 云端硬盘。
from google.colab import drive
drive.mount('/content/drive')

运行之后,会生成一个链接拿到Google 云端硬盘生成的授权码,输入到这个cell中,即可成功挂在你的Google 云硬盘。

1
2
3
4
5
Go to this URL in a browser: https://accounts.google.com/o/oauth2/auth?....

Enter your authorization code:
··········
Mounted at /content/drive

之所以需要挂载Google云硬盘,是基于这样的考虑:

Google Colab有一个“防挖矿”机制,为了防止自己免费开放的GPU资源被矿工拿来挂机挖矿,Colab会自动回收那些运行了很久或者和网页端断线很久的项目的所有资源:包括GPU和所有文件

因此尽量不要尝试在训练的过程中关闭浏览器,然后等时间到了再次打开浏览器查看结果,很有可能早已训练结束,模型文件已经生成,但是由于Colab的这个机制导致文件被删除。

所以在训练过程中,需要挂载Google 云端硬盘,将模型文件和训练生成的Tensorboard日志的路径放在云端硬盘里,就算谷歌回收了资源也能够及时保存。

但是,需要注意的是,数据集最好不要放在Google 云端硬盘里,因为网上有人试过了,Colab从Google云端硬盘上获取文件时不是直接读取文件系统,而是发送请求进行文件分块下载的,这个网络IO带来的延迟会极大地拖慢训练的速度。

此外,这个数据集直接上传到Google Colab上的速度也是堪忧。但是,值得称赞的是,在Colab里直接用Shell命令下载在线的数据集,速度极快,能够达到15M/s。以下是下载YouTube Aligned Faces数据集的输出,30秒完成~

数据集下载

图2.3 下载YouTube Aligned Faces数据集的输出

还有一个需要注意的地方,就是Colab上已经装好了Tensorflow 1.14、OpenCV以及matplot、numpy、PIL等深度学习常用的python库,若需要其他库也是直接执行shell命令pip install即可。这里的Tensorflow 1.14与目前常用的1.x版本相比,在API上有着许多区别,如果直接复制他人的代码,会出现许多的问题。

笔者也因此几乎是把GitHub上的DeepID实现从头开始添加中文注释和改写API,学到了很多搭建Tensorflow训练框架的相关API用法(例如session、variable和namescope),也算是继续了之前《人工智能》大作业的“注释阅读法”的个人习惯。

以上就是一些搭建Colab环境的注意事项,如果你已经看懂了这些,而且熟悉Jupyter Notebook,就可以开始着手编写训练代码了。

编写训练代码

下载YouTube Aligned Faces数据集

1
2
3
4
5
# 下载youtube aligned face数据集
!wget --http-user=wolftau --http-password=wtal997 http://www.cslab.openu.ac.il/download/wolftau/aligned_images_DB.tar.gz
# 解压下载的数据集
!mkdir -p data
!tar -zxf aligned_images_DB.tar.gz -C Training-DeepID1-Network-for-Face-Comparison-with-Google-Colab-Tensorflow/data

裁剪数据集图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
'''
此处开始为DeepID人脸特征提取、比对代码

来源为:https://github.com/jinze1994/DeepID1
主要工作:增加了详细中文注释、更新了部分tensorflow2.0的新API
'''

'''
crop.py

裁剪训练数据集图片,图片已经经过了对齐预处理
所谓对齐就是裁剪到只剩下人脸,且已经事先将带有倾斜的人脸对齐过了
因此此处只需裁剪并缩放到 (55,47) 的像素即可
这样的处理适合被检测对象配合、也就是主动进行识别的场景
'''
import os
from PIL import Image

'''
crop_img_by_half_center

从1/4处开始裁剪1/2尺寸的图像并缩放
'''
def crop_img_by_half_center(src_file_path, dest_file_path):
# 打开图像
im = Image.open(src_file_path)
# 获取图像尺寸
x_size, y_size = im.size
# 开始裁剪的坐标
start_point_xy = x_size / 4
# 裁剪结束时的坐标
end_point_xy = x_size / 4 + x_size / 2
# 生成方形框
box = (start_point_xy, start_point_xy, end_point_xy, end_point_xy)
# 裁剪
new_im = im.crop(box)
# 缩放为(55,47)
new_new_im = new_im.resize((47,55))
# 保存
new_new_im.save(dest_file_path)

'''
walk_through_the_folder_for_crop

遍历数据集文件夹,进行图像的处理,生成目标文件夹
'''
def walk_through_the_folder_for_crop(aligned_db_folder, result_folder):
# 若不存在目标文件夹,新建一个
if not os.path.exists(result_folder):
os.mkdir(result_folder)

# 打开每一个youtube人物文件夹
for people_folder in os.listdir(aligned_db_folder):
src_people_path = aligned_db_folder + people_folder + '/'
dest_people_path = result_folder + people_folder + '/'
# 创建每一个人物文件夹对应的目标文件夹
if not os.path.exists(dest_people_path):
os.mkdir(dest_people_path)
# 打开每一个人物文件夹下的视频文件夹
for video_folder in os.listdir(src_people_path):
src_video_path = src_people_path + video_folder + '/'
dest_video_path = dest_people_path + video_folder + '/'
# 创建每一个视频文件夹对应的目标文件夹
if not os.path.exists(dest_video_path):
os.mkdir(dest_video_path)
# 对于每一个视频文件夹下的图片文件,进行处理
for img_file in os.listdir(src_video_path):
src_img_path = src_video_path + img_file
dest_img_path = dest_video_path + img_file
crop_img_by_half_center(src_img_path, dest_img_path)
'''
裁剪模块的主程序
'''
if __name__ == '__main__':
# 数据集路径和目标文件夹路径
aligned_db_folder = "data/aligned_images_DB"
result_folder = "data/crop_images_DB"
if not aligned_db_folder.endswith('/'):
aligned_db_folder += '/'
if not result_folder.endswith('/'):
result_folder += '/'
# 开始处理
walk_through_the_folder_for_crop(aligned_db_folder, result_folder)

分割数据集为训练集、验证集和测试集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
'''
split.py

对剪裁后的数据集文件按照 8:1:1 的规模进行切分,分别作为训练集、验证集和测试集。
每个人保留固定数目的图片(100张)进行训练。
为生成测试集,对每个人构造 5 对同一个人的图片 pair,再构造 5 对不同人的图片 pair,作为测试集。
一个pair作为每次测试时输入的组合,用来测试同一个人是否能正确匹配、不同人是否能够分出不同的人脸比对效果
'''
import os
import os.path
import random

'''
fatch_pics_for_one_user

获取一个youtube用户的所有图片
'''
def fatch_pics_for_one_user(people_path):
people_imgs = []
# 从文件夹中遍历
for video_folder in os.listdir(people_path):
for video_file_name in os.listdir(os.path.join(people_path, video_folder)):
people_imgs.append(os.path.join(people_path, video_folder, video_file_name))
random.shuffle(people_imgs)
return people_imgs

'''
build_dataset

创建训练集、验证集和测试集
'''
def build_dataset(src_folder):
# 总人数,总图片张数
total_people, total_picture = 0, 0
# 测试用户列表、验证集、训练集
test_people, valid_set, train_set = [], [], []
# 标签数量
label = 0

# 用户文件夹遍历
for people_folder in os.listdir(src_folder):
# 获取一个youtube用户的所有图片
people_imgs = fatch_pics_for_one_user(os.path.join(src_folder, people_folder))
total_people += 1
total_picture += len(people_imgs)
# 若数量在100张以内,则全部放入测试用户列表
# 保证测试集中的用户不会出现在训练集和验证集中
if len(people_imgs) < 100:
test_people.append(people_imgs)
# 否则分割到验证集和训练集中,1:9
else:
valid_set += zip(people_imgs[:10], [label]*10)
train_set += zip(people_imgs[10:100], [label]*90)
label += 1

# 测试集
test_set = []
# 从测试用户列表中,构造5对同一个人的照片、5对不同人的照片
for i, people_imgs in enumerate(test_people):
# 5对同一个人的照片
for k in range(5):
same_pair = random.sample(people_imgs, 2)
test_set.append((same_pair[0], same_pair[1], 1))
# 5对不同人的照片
for k in range(5):
j = i;
while j == i:
j = random.randint(0, len(test_people)-1)
test_set.append((random.choice(test_people[i]), random.choice(test_people[j]), 0))

# 打乱各个数据集的顺序
random.shuffle(test_set)
random.shuffle(valid_set)
random.shuffle(train_set)

# 输出各数据集的统计信息
print('\tpeople\tpicture')
print('total:\t%6d\t%7d' % (total_people, total_picture))
print('test:\t%6d\t%7d' % (len(test_people), len(test_set)))
print('valid:\t%6d\t%7d' % (label, len(valid_set)))
print('train:\t%6d\t%7d' % (label, len(train_set)))
return test_set, valid_set, train_set

'''
set_to_csv_file

保存到csv文件中
'''
def set_to_csv_file(data_set, file_name):
with open(file_name, "w") as f:
for item in data_set:
print(" ".join(map(str, item)), file=f)

'''
数据集切分模块的主程序
'''
if __name__ == '__main__':
random.seed(7)
# 原始数据集路径以及各数据集保存列表文件
src_folder = "data/crop_images_DB"
test_set_file = "data/test_set.csv"
valid_set_file = "data/valid_set.csv"
train_set_file = "data/train_set.csv"
if not src_folder.endswith('/'):
src_folder += '/'

test_set, valid_set, train_set = build_dataset(src_folder)
set_to_csv_file(test_set, test_set_file)
set_to_csv_file(valid_set, valid_set_file)
set_to_csv_file(train_set, train_set_file)

向量化数据集,便于读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
'''
vec.py

将数据格式化为向量形式,存入 data/dataset.pkl。便于训练时直接从该文件读取数据。
'''
import pickle
import numpy as np
from PIL import Image

'''
vectorize_imgs

将图像向量化,事实上就是将图像转化为浮点数格式的数组
'''
def vectorize_imgs(img_path):
with Image.open(img_path) as img:
arr_img = np.asarray(img, dtype='float32')
return arr_img

'''
read_csv_file

读取csv文件
'''
def read_csv_file(csv_file):
x, y = [], []
with open(csv_file, "r") as f:
for line in f.readlines():
path, label = line.strip().split()
x.append(vectorize_imgs(path))
y.append(int(label))
return np.asarray(x, dtype='float32'), np.asarray(y, dtype='int32')
'''
read_csv_pair_file

读取成对数据(也就是一个label对应两张图)的csv文件
事实上就是读取测试集数据
'''
def read_csv_pair_file(csv_file):
x1, x2, y = [], [], []
with open(csv_file, "r") as f:
for line in f.readlines():
p1, p2, label = line.strip().split()
x1.append(vectorize_imgs(p1))
x2.append(vectorize_imgs(p2))
y.append(int(label))
return np.asarray(x1, dtype='float32'), np.asarray(x2, dtype='float32'), np.asarray(y, dtype='int32')

'''
向量化主程序,将csv文件转换为pkl文件
'''
if __name__ == '__main__':
testX1, testX2, testY = read_csv_pair_file('data/test_set.csv')
validX, validY = read_csv_file('data/valid_set.csv')
trainX, trainY = read_csv_file('data/train_set.csv')

print(testX1.shape, testX2.shape, testY.shape)
print(validX.shape, validY.shape)
print(trainX.shape, trainY.shape)

# 导入向量化的数据到pkl文件中
with open('data/dataset.pkl', 'wb') as f:
pickle.dump(testX1, f, pickle.HIGHEST_PROTOCOL)
pickle.dump(testX2, f, pickle.HIGHEST_PROTOCOL)
pickle.dump(testY , f, pickle.HIGHEST_PROTOCOL)
pickle.dump(validX, f, pickle.HIGHEST_PROTOCOL)
pickle.dump(validY, f, pickle.HIGHEST_PROTOCOL)
pickle.dump(trainX, f, pickle.HIGHEST_PROTOCOL)
pickle.dump(trainY, f, pickle.HIGHEST_PROTOCOL)

运行tensorboard监视训练过程

在经过了以上漫长的数据集裁剪、分割和向量化过程(第1、2步各需要20分钟)之后,就开始了训练。这里可以选用Colab内置的Tensorboard进行训练过程的监视。首先需要升级,否则无法读取训练过程中生成的日志文件。

在实际使用过程中,若训练生成的日志放在了Colab的文件目录中,Tensorboard在训练开始后过一段时间会与训练程序断开连接,
因此同样需要将训练程序代码中的日志文件路径设为Google 云端硬盘的路径。这样就算掉线了也能够在本地运行一个Tensorboard,手动下载Google 云端硬盘上不断更新的日志文件进行监视(或者有下载Google云端硬盘的客户端,可以使用文件夹同步功能实时更新本地的日志文件)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'''
训练之前,运行tensorboard监视训练过程

尝试了无数次,读不到日志文件,无论是绝对路径还是相对路径
在mac上本地查看日志文件,是能用的,
后来发现升级一下tensorboard就好了,
'''

# 升级,升级后首次使用会报错,清除一下报错里面提示的info文件即可
# !pip install --upgrade tensorboard
# !rm /tmp/.tensorboard-info/pid-*.info

%reload_ext tensorboard
%tensorboard --logdir "/content/drive/My Drive/Colab Notebooks/deepid/log"

训练DeepID网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
'''
deepid1.py

DeepID网络训练主程序
'''
import pickle
import numpy as np
import tensorflow as tf

'''
load_data

从pkl向量文件中导出数据
'''
def load_data():
with open('data/dataset.pkl', 'rb') as f:
testX1 = pickle.load(f)
testX2 = pickle.load(f)
testY = pickle.load(f)
validX = pickle.load(f)
validY = pickle.load(f)
trainX = pickle.load(f)
trainY = pickle.load(f)
return testX1, testX2, testY, validX, validY, trainX, trainY

# 导入向量数据
testX1, testX2, testY, validX, validY, trainX, trainY = load_data()
# 类型数量=训练集数量,也就是认为每一个训练集数据均为一类
# 因为本网络只负责特征提取而非分类,所以可以这么做
class_num = np.max(trainY) + 1
# 清除一下当前的作用域
tf.reset_default_graph();


'''
weight_variable

初始化权重,shape事实上是卷积核尺寸
'''
def weight_variable(shape):
with tf.name_scope('weights'):
# 从截断的正态分布中输出随机值,以初始化权重。
return tf.Variable(tf.truncated_normal(shape, stddev=0.1))
'''
bias_variable

初始化偏置,也就是wx+b中的b,bias
'''
def bias_variable(shape):
with tf.name_scope('biases'):
# 使用全零向量初始化偏置
return tf.Variable(tf.zeros(shape))

'''
Wx_plus_b

求wx+b
'''
def Wx_plus_b(weights, x, biases):
with tf.name_scope('Wx_plus_b'):
return tf.matmul(x, weights) + biases

'''
nn_layer

n*n的全连接层,可选激活函数
'''
def nn_layer(input_tensor, input_dim, output_dim, layer_name, act=tf.nn.relu):
# 进入对应层的命名空间
with tf.name_scope(layer_name):
# 权重
weights = weight_variable([input_dim, output_dim])
# 偏置
biases = bias_variable([output_dim])
# 预激活
# 可以这么翻译,个人认为是激活前的预处理
preactivate = Wx_plus_b(weights, input_tensor, biases)
# 若传入了激活函数,则让它激活
if act != None:
activations = act(preactivate, name='activation')
return activations
else:
# 否则就输出预激活量
return preactivate

'''
conv_pool_layer

卷积+池化层,在deepid中一共有3层
也可以定制only_conv=True来满足deepid第四层只有卷积

卷积:局部感知,基于相邻部分的相关性原理;权值共享、因此可以设计多核卷积
池化:这里使用最大池化,则说明是提取显著特征
'''
def conv_pool_layer(x, w_shape, b_shape, layer_name, act=tf.nn.relu, only_conv=False):
with tf.name_scope(layer_name):
W = weight_variable(w_shape)
b = bias_variable(b_shape)
# 输入到卷积层
conv = tf.nn.conv2d(
# 输入x和卷积核W的大小、权重
x, W,
# 卷积步长,tf中前后两个1不能改,中间两个为水平滑动和垂直滑动步长
strides=[1, 1, 1, 1],
# VALID方式丢弃小于窗口大小的
# SAME方式相反会填充到窗口大小
padding='VALID',
name='conv2d'
)
h = conv + b
# 加入偏置,激活
relu = act(h, name='relu')
if only_conv == True:
return relu
# 若存在池化层则再进行池化
# ksize参数确定了池化窗口大小
# 值得注意的是这里的最大池化没有使用激活函数,也就是仅仅提取线性的显著特征
pool = tf.nn.max_pool(relu, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='VALID', name='max-pooling')
return pool

'''
accuracy

在验证集上测试阶段的准确度计算,由模型预测值和实际值计算得出
'''
def accuracy(y_estimate, y_real):
with tf.name_scope('accuracy'):
with tf.name_scope('correct_prediction'):
# 在测试阶段的准确度计算
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
with tf.name_scope('accuracy'):
# 对每个批次计算总的准确度均值
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 记录准确度信息
tf.summary.scalar('accuracy', accuracy)
return accuracy

'''
train_step

训练梯度,也就是需要计算梯度下降了
这里采用了ADAM优化器,其他优化器的特征:
Momentum冲量算法增加冲量、
Adagrad对低频变化的参数以更大步长更新、
RMSProp更新时只更新梯度平方的期望(移动的均值)

ADAM优化器对梯度的一阶矩估计(均值)和二阶矩估计(方差)两个方面适应性调节
'''
def train_step(loss):
with tf.name_scope('train'):
# 初始学习率1e-4,之后同样会动态调整,一般是逐步衰减,减少趋近最优时的震荡
# minimize才是更新梯度,之前是计算梯度
return tf.train.AdamOptimizer(1e-4).minimize(loss)

# 输入,tf.placeholder为形参,在执行时再赋具体的值
with tf.name_scope('input'):
h0 = tf.placeholder(tf.float32, [None, 55, 47, 3], name='x')
y_ = tf.placeholder(tf.float32, [None, class_num], name='y')

# 第1个卷积-池化层,4x4,当前通道数3,卷积核数量(下一层通道数)20,偏置大小20
h1 = conv_pool_layer(h0, [4, 4, 3, 20], [20], 'Conv_layer_1')
# 第2个卷积-池化层,3x3,当前通道数20,卷积核数量40,偏置大小40
h2 = conv_pool_layer(h1, [3, 3, 20, 40], [40], 'Conv_layer_2')
# 第3个卷积-池化层,3x3,当前通道数40,卷积核数量60,偏置大小60
h3 = conv_pool_layer(h2, [3, 3, 40, 60], [60], 'Conv_layer_3')
# 第4个卷积层,2x2,当前通道数60,卷积核数量80,偏置大小80
h4 = conv_pool_layer(h3, [2, 2, 60, 80], [80], 'Conv_layer_4', only_conv=True)

# 最后一个deepid层
with tf.name_scope('DeepID1'):
# deepid层直接与第3层相连,
# 使用reshape能够拉平这两层的输出为1维数组
# -1即为任意维,后跟的是每一维度实际尺寸大小,
# 该大小即为整个层所有神经元个数(比实际还偏大一点),因此是拉平了的
h3r = tf.reshape(h3, [-1, 5*4*60])
# deepid层与第4层相连
h4r = tf.reshape(h4, [-1, 4*3*80])
# 初始化两次相连的权重
W1 = weight_variable([5*4*60, 160])
W2 = weight_variable([4*3*80, 160])
b = bias_variable([160])
# 直接带权重一起相加
h = tf.matmul(h3r, W1) + tf.matmul(h4r, W2) + b
# relu激活
h5 = tf.nn.relu(h)

# 计算损失函数
with tf.name_scope('loss'):
# n*n的全连接层,将拉平的第3、4层全连接到一个160个神经元的全连接层上
y = nn_layer(h5, 160, class_num, 'nn_layer', act=None)
# softmax层
# 1. 将logits(也就是输入y),计算为(0,1)范围的概率值
# 2. 计算损失loss,这里计算的是交叉熵损失,y_认为是对应的标签
'''
增加Soft-max layer的输出数量(即分类数,或识别的个体数)可以提升人脸验证的准确率。
即分类的类别数越多,DeepConv-Net学到的DeepID特征(160维)越有效。
此外,作者强调用于人脸验证的一定是160维长度的DeepID特征,而不是Softmax Layer的输出。
如果用SoftmaxLayer输出的结果(例如用4348个不同人的数据训练DeepID,Softmax输出是4348维)
进行人脸验证特征,采用联合贝叶斯人脸验证方法得到的准确率约为66%,而神经网络人脸验证方法则完全失效

摘录自:https://www.cnblogs.com/venus024/p/5632243.html

笔者(本人)注:可以说这个神经网络只是利用了多分类训练(可以在代码中看出同一个人的类别标签还是相同的)的形式,
训练神经网络在提取特征时的权重参数,从而达到提取特征、加以比对的目的
'''
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits = y, labels = y_))
# 记录当前损失
tf.summary.scalar('loss', loss)

# 初始化精确度
accuracy = accuracy(y, y_)
# 初始化优化器
optimizer = train_step(loss)

# 合并所有的记录,给session用来回调运行
merged = tf.summary.merge_all()
# 保存模型的回调
saver = tf.train.Saver()

'''
训练主函数
'''
if __name__ == '__main__':

# 获取一个batch的输入
def get_batch(data_x, data_y, start):
end = (start + 1024) % data_x.shape[0]
if start < end:
return data_x[start:end], data_y[start:end], end
return np.vstack([data_x[start:], data_x[:end]]), np.vstack([data_y[start:], data_y[:end]]), end

with tf.Session() as sess:
# 注意,trainX和trainY为具体数据和标签
data_x = trainX
data_y = (np.arange(class_num) == trainY[:,None]).astype(np.float32)
validY = (np.arange(class_num) == validY[:,None]).astype(np.float32)

# 日志文件目录
logdir = '/content/drive/My Drive/Colab Notebooks/deepid/log'
if tf.gfile.Exists(logdir):
tf.gfile.DeleteRecursively(logdir)
tf.gfile.MakeDirs(logdir)

# 创建训练线程
sess = tf.Session()
# 初始化所有参数,开始训练
sess.run(tf.global_variables_initializer())
# 写入训练日志和测试日志
train_writer = tf.summary.FileWriter(logdir + '/train', sess.graph)
test_writer = tf.summary.FileWriter(logdir + '/test', sess.graph)

# 开始训练,训练次数50000次
idx = 0
for i in range(50001):
# 获取一个batch的输入
batch_x, batch_y, idx = get_batch(data_x, data_y, idx)
# 优化器
_ = sess.run(optimizer, {h0: batch_x, y_: batch_y})
# 运行,h0赋值为batchX,也就是图像,y_赋值为batch_y,也就是标签
summary = sess.run(merged, {h0: batch_x, y_: batch_y})

train_writer.add_summary(summary, i)

# 每100次进行验证集测试
if i % 100 == 0:
summary = sess.run(merged, {h0: validX, y_: validY})
test_writer.add_summary(summary, i)
# 每5000次保存一次模型
if i % 5000 == 0 and i != 0:
saver.save(sess, '/content/drive/My Drive/Colab Notebooks/deepid/checkpoint/%05d.ckpt' % i)

这里展示一下tensorboard采集的使用TPU(由于速度过慢,未训练完)和Tesla T4 GPU(2小时训练结束)的进行50000次训练的准确率和损失率图表:

对于TPU:

TPU-acc

图2.4 TPU训练时的准确度统计图

TPU-acc-1

图2.5 TPU训练时的准确度与耗时(放大后)

可以看出TPU训练时的准确度上升缓慢,而且过了3个小时后,准确度仍然在0.6,而且才训练了不到4k次。

TPU-loss

图2.6 TPU训练时的损失率统计图

TPU-loss-1

图2.7 TPU训练时的损失率与耗时(放大后)

同样地,TPU训练时损失率下降也十分缓慢。

对于GPU:

GPU-acc

图2.8 GPU训练时的准确度统计图

GPU-acc-1

图2.9 GPU训练时的准确度与耗时(放大后)

可以看出TPU训练时的准确度上升呈对数曲线,在训练次数到15k~30k时就已经趋于稳定,在1小时27分时就已经结束了训练。

GPU-loss

图2.10 GPU训练时的损失率统计图

GPU-loss

图2.10 GPU训练时的损失率与耗时(放大后)

同样地,GPU训练时损失率下降也十分迅速。

综上,可以看出Google Colab提供的免费GPU性能十分地强劲,能够满足快速训练简单的深度学习模型的需求。这款GPU通过nvidia-smi命令查询的情况如下所示,据查,该款显卡的价格约两万元人民币,可以看出谷歌为了推广深度学习付出了巨大的成本。

nvidia-smi

图2.11 nvidia-smi命令得到的GPU信息:Tesla T4

在测试集上使用模型文件预测,获取余弦距离阈值

这里运行测试集除了检验模型的预测效果,更重要的是获取余弦距离的阈值,也就是(true_mean + false_mean)/2,意思是:小于同类组+不同类组的平均组内距离的两者平均(有点拗口,但是确实是以此为阈值)。根据这个阈值,就能判断任意两个人脸之间的距离代表的是同一个人还是不同的人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
'''
predict.py

预测模块,训练结束后即可使用模型文件预测
'''

import pickle
import tensorflow as tf
from scipy.spatial.distance import cosine, euclidean

saver = tf.train.Saver()

'''
predict

预测
'''

def predict(ckpt):
with tf.Session() as sess:
saver.restore(sess, ckpt)
# 计算测试集的两对数据特征值列表
h1 = sess.run(h5, {h0: testX1})
h2 = sess.run(h5, {h0: testX2})

# 计算两个特征值列表对应两项的余弦距离
# 事实上是1-余弦距离,距离越近,数值越小,符合直觉
# 因此范围也从-1~1变为了0~2
pre_y = np.array([cosine(x, y) for x, y in zip(h1, h2)])

# 求余弦距离阈值
def part_mean(x, mask):
# mask事实上是测试集的标签,若是testY,1就代表对应的两张图为同类,0为不同类,1-testY反之
# 以testY为例,在这一乘法下,留下的非零项目即为同类项
z = x * mask
# 同类组余弦距离总和/同类组数量
# 对所有非零项目求和=同类组距离总和
# 非零项目个数=同类组数量
# 两者相除则为同类组的平均组内距离
# 1-testY时则为不同类组的平均组内距离
return float(np.sum(z) / np.count_nonzero(z))

true_mean = part_mean(pre_y, testY)
false_mean = part_mean(pre_y, 1-testY)
print(true_mean, false_mean)

# 筛选出pre_y也就是余弦距离结果中,符合小于同类组+不同类组的平均组内距离的两者平均这一条件的项目,与testY中的对应项目进行比对
# 由于testY中对应项目为1也就是True的元素代表两张图为同类,因此当pre_y中元素小于这一条件时,也代表为同类
# 反之,pre_y中的元素大于这一条件时,代表非同类
# 所以最终得到的矩阵是一个同类、非同类的预测值与测试集标签之间的对应关系,只有正确的才能留下来
# 对此计算均值,即可获取模型在测试集上的准确率
print(np.mean((pre_y < (true_mean + false_mean)/2) == testY.astype(bool)))

'''
预测主函数
'''
if __name__ == '__main__':
# 输入模型路径
predict('/content/drive/My Drive/Colab Notebooks/deepid/checkpoint/30000.ckpt');

预测服务搭建

对于已经训练好的Tensorflow模型的预测服务搭建,在网络上有许多的方法,事实上最好的方法是使用frozen_graph工具对checkpoint进行固化处理,笔者这里是直接调用了checkpoint来恢复现场,事实上效果类似。
笔者在这里使用的是docker进行预测服务搭建,具体使用的镜像是yoanlin/opencv-python3-tensorflow,自带python3、opencv和tensorflow1.x。由于tensorflow仅仅是使用1.14生成的模型,所以不存在兼容性问题。
具体命令如下

1
2
3
4
5
6
7
8
9
10
# 拉取镜像
docker pull yoanlin/opencv-python3-tensorflow
# 生成容器,配置端口映射和文件夹映射
# 8888端口是tensorboard,8080是flask
# faces文件夹映射为人脸图像路径,我是使用软工项目中的Spring boot来接收图像的,所以flask就没写接收代码,直接从文件路径里面取,server文件夹映射为flask的程序文件
docker run -itd --name=tf-cv -p 7777:8888 -p 8081:8080 -v /tf-cv/faces:/faces -v /tf-cv/server/:/server yoanlin/opencv-python3-tensorflow
# 若需要进入镜像内部安装flask等其他python库,运行以下命令
docker exec -it tf-cv bash
# flask无法独立启动守护进程,需要使用gunicorn,其中gunicorn.conf.py写有基本配置,此处可自行搜索相关教程
gunicorn app:app -c gunicorn.conf.py -D

若需要部署到服务器,可以使用以下命令

1
2
3
4
5
6
7
# 将容器提交为新的镜像
docker commit tf-cv tf-cv:server
# 将新的镜像打包为tar压缩文件,之后用scp命令传到服务器上
docker save > tf-cv.tar tf-cv:server
# 在服务器上解压镜像
docker load < tf-cv.tar
# 再次使用docker run命令生成新的容器,并在新的容器内部运行flask,参见以上命令

编写预测代码

OpenCV人脸检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
'''
HAAR特征检测人脸

用opencv的方式(HAAR特征)检测人脸,效果不是很好。
最优方案是MTCNN,需要人脸特征点数据集多次训练,比较繁琐
'''

# coding:utf-8
import cv2
from PIL import Image
import numpy as np

def detect_face(img_path):
# 获取训练好的人脸参数数据,此处引用GitHub上的opencv库中的默认值
face_cascade = cv2.CascadeClassifier(r'/root/haarcascade_frontalface_default.xml')

# 读取图片,并处理成灰度图
image = cv2.imread(img_path)

# 未读取到图片,返回
if image is None:
return "提示:未读取到图片"

# 转换为灰度图像
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# haar模型检测人脸
faces = face_cascade.detectMultiScale(
gray,
scaleFactor = 1.15,
minNeighbors = 5,
minSize = (5, 5),
flags = cv2.CASCADE_SCALE_IMAGE
)

# 未检测到人脸,返回
if len(faces) <= 0:
return "提示:未检测到人脸"

# 裁剪人脸图像
face_images = []

for(x,y,w,h) in faces:
face_img = image[y:y+h, x:x+w]
face_images.append(face_img)

# 若有多个人脸,则选出面积最大(也就是最靠前)的人脸
face_images = sorted(face_images, key=lambda img:img.size, reverse=True)

# 转换图像
face = Image.fromarray(face_images[0])

# 缩放为(55,47)
resize_face = face.resize((47,55))

# 转换为array
return np.asarray(resize_face)

Tensorflow人脸特征比对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# import tensorflow as tf
import cv2
import numpy as np
from . import detector as dt
import tensorflow as tf
from scipy.spatial.distance import cosine

'''
预测
'''
def predict(src_img_path, dst_img_path):
# 对输入的图像分别检测人脸
src_image = dt.detect_face(src_img_path)
dst_image = dt.detect_face(dst_img_path)

# 若返回了错误信息,不再检测
if isinstance(src_image, str):
return src_image
elif isinstance(dst_image, str):
return dst_image

# 载入tensorflow模型,开始检测
with tf.Session() as sess:
saver=tf.train.import_meta_graph('/root/50000.ckpt.meta')
saver.restore(sess,"/root/50000.ckpt")
graph = tf.get_default_graph()

# 计算160维的人脸特征
h1 = sess.run("DeepID1/Relu:0", feed_dict={"input/x:0": [src_image]})
h2 = sess.run("DeepID1/Relu:0", feed_dict={"input/x:0": [dst_image]})

# 计算人脸之间的余弦距离(事实上是1-余弦),范围0~1,越小越接近
pre_y = np.array([cosine(x, y) for x, y in zip(h1, h2)])

# 在测试集上测试模型的过程中,得到了余弦距离的阈值为0.47189
# 因此,比该阈值小的即为同一个人,大的则不是同一个人
return { 'msg': { 'isSame': bool((pre_y < 0.47189)[0]), 'predict': pre_y[0] } }

Flask后端服务器主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from flask import Flask
from flask import request
from flask import jsonify
import json
from predict.main import predict

app = Flask(__name__)

@app.route('/face', methods=['POST'])
def hello():
data = json.loads(request.get_data(as_text=True))
src_face = data['src_face']
dst_face = data['dst_face']
res = predict(src_face, dst_face)

# 返回一下两个人脸图像的路径,便于验证是否正确
if isinstance(res, str):
return jsonify({ 'success': False, 'msg': res, 'src_face': src_face, 'dst_face': dst_face })
else:
return jsonify({ 'success': True, 'msg': res['msg'], 'src_face': src_face, 'dst_face': dst_face })


if __name__ == '__main__':
app.run(debug=True)

以上的flask服务器的业务流程是:

  • 在主程序中路由/face上接收POST请求,收到待比对的两张人脸图片的文件路径。
  • 在detect_face函数中,使用OpenCV的HAAR模型,检测图片中的人脸,并且裁剪成当时训练时使用的(55,47)尺寸输入。若检测不到人脸,或者图片文件无法找到,直接返回错误信息。
  • 在predict函数中,调用tensorflow恢复(restore)模型的参数,输入这两张人脸,获取每张人脸的特征值,计算两者特征值的余弦距离,与之前在测试集上获取的余弦距离阈值进行比对,判断出是否为同一个人,返回结果。

最终,在前端小程序的手机前置摄像头调用和用户界面的配合下,该系统的最终效果如下所示:

weapp-1

图2.12 地图定位界面

weapp-2
图2.13 人脸识别成功,正在比对人脸

weapp-3
图2.14 未检测到人脸

weapp-4
图2.15 比对人脸为同一人后,打卡成功的结果

总结

本次项目实践了使用OpenCV、Tensorflow、Tensorboard以及docker、Jupyter Notebook等深度学习模型训练的常用工具,并尝试将训练得出的模型进行Python flask后端+小程序前端应用落地。在这一过程中,笔者不但熟悉了从数据集预处理、模型训练框架搭建、模型训练过程监控再到模型实际应用的全过程,也通过编写中文注释、以及对Tensorflow不同版本API的移植重写,进一步熟悉深度学习的常用术语和内在含义,可以说是一次收获颇丰的实践案例。

在此,特别感谢Google Colab免费提供的Nvidia GTX Tesla T4高性能GPU硬件资源以及在线训练平台,感谢他们为深度学习的推广和应用做出的无数努力和贡献。最后,感谢USTB的《机器学习》(自动化学院)、《人工智能》、《模式识别》、《软件工程》等相关课程老师的辛勤教学,是各位老师传授的宝贵知识和设置的一系列大作业帮助着我进一步理解、学习AI各个方向的知识并加以实践,为未来的研究和工作打下了知识基础。感谢大家!

参考资料

  1. GitHub上DeepID的Tensorflow实现(本文在此基础上修改了调用的TensorflowAPI到1.14,并添加中文注释):
    https://github.com/jinze1994/DeepID1
  2. DeepID1论文《Deep Learning Face Representation from Predicting 10,000 Classes》:
    https://www.cv-foundation.org/openaccess/content_cvpr_2014/papers/Sun_Deep_Learning_Face_2014_CVPR_paper.pdf
  3. Google Colab官网:https://colab.research.google.com
  4. DeepID1、2算法解读:https://www.cnblogs.com/venus024/p/5632243.html
  5. 人脸特征提取DeepID 1.0深度网络解读:
    https://blog.csdn.net/jiajinrang93/article/details/72566130/