一、问题定义
人脸关键点检测,是输入一张人脸图片,模型会返回人脸关键点的一系列坐标,从而定位到人脸的关键信息。
二、数据准备
Dataset定义较为简单,主要完成标签和图像数据读取。
# 按照Dataset的使用规范,构建人脸关键点数据集
from paddle.io import Dataset
class FacialKeypointsDataset(Dataset):
# 人脸关键点数据集
"""
步骤一:继承paddle.io.Dataset类
"""
def __init__(self, csv_file, root_dir, transform=None):
"""
步骤二:实现构造函数,定义数据集大小
Args:
csv_file (string): 带标注的csv文件路径
root_dir (string): 图片存储的文件夹路径
transform (callable, optional): 应用于图像上的数据处理方法
"""
self.key_pts_frame = pd.read_csv(csv_file) # 读取csv文件
self.root_dir = root_dir # 获取图片文件夹路径
self.transform = transform # 获取 transform 方法
def __getitem__(self, idx):
"""
步骤三:实现__getitem__方法,定义指定index时如何获取数据,并返回单条数据(训练数据,对应的标签)
"""
# 实现 __getitem__
image_name=os.path.join(self.root_dir, self.key_pts_frame.iloc[idx,0])
image=mpimg.imread(image_name)
if(image.shape[2]==4):
image=image[:,:,0:3]
# 获取关键点信息
key_pts = self.key_pts_frame.iloc[idx, 1:].as_matrix()
key_pts = key_pts.astype('float').reshape(-1) # [136, 1]
# 如果定义了 transform 方法,使用 transform方法
if self.transform:
image, key_pts = self.transform([image, key_pts])
# 转为 numpy 的数据格式
image = np.array(image, dtype='float32')
key_pts = np.array(key_pts, dtype='float32')
return image, key_pts
def __len__(self):
"""
步骤四:实现__len__方法,返回数据集总数目
"""
# 实现 __len__
return len(self.key_pts_frame)
继续transforms归一化等处置,特别主要的是重置尺寸等,涉及到标签数据。
class Resize(object):
# 将输入图像调整为指定大小
def __init__(self, output_size):
assert isinstance(output_size, (int, tuple))
self.output_size = output_size
def __call__(self, data):
image = data[0] # 获取图片
key_pts = data[1] # 获取标签
image_copy = np.copy(image)
key_pts_copy = np.copy(key_pts)
h, w = image_copy.shape[:2]
if isinstance(self.output_size, int):
if h > w:
new_h, new_w = self.output_size * h / w, self.output_size
else:
new_h, new_w = self.output_size, self.output_size * w / h
else:
new_h, new_w = self.output_size
new_h, new_w = int(new_h), int(new_w)
img = F.resize(image_copy, (new_h, new_w))
# scale the pts, too
key_pts_copy[::2] = key_pts_copy[::2] * new_w / w
key_pts_copy[1::2] = key_pts_copy[1::2] * new_h / h
return img, key_pts_copy
3、模型组建
根据前文的分析可知,人脸关键点检测和分类,可以使用同样的网络结构,如LeNet、Resnet50等完成特征的提取,只是在原来的基础上,需要修改模型的最后部分,将输出调整为 人脸关键点的数量*2,即每个人脸关键点的横坐标与纵坐标,就可以完成人脸关键点检测任务了,具体可以见下面的代码,也可以参考官网案例:人脸关键点检测
网络结构如下:
import paddle.nn as nn
from paddle.vision.models import resnet50
class SimpleNet(nn.Layer):
def __init__(self, key_pts):
super(SimpleNet, self).__init__()
# 实现 __init__
self.backbone = paddle.vision.models.resnet101(pretrained=True)
# 添加第一个线性变换层
self.linear1 = nn.Linear(in_features=1000, out_features=512)
# 使用 ReLU 激活函数
self.act1 = nn.ReLU()
# 添加第二个线性变换层作为输出,输出元素的个数为 key_pts*2,代表每个关键点的坐标
self.linear2 = nn.Linear(in_features=512, out_features=key_pts*2)
def forward(self, x):
# 实现 forward
x = self.backbone(x)
x = self.linear1(x)
x = self.act1(x)
x = self.linear2(x)
return x
4.模型训练
训练模型前,需要设置训练模型所需的优化器,损失函数和评估指标。
- 优化器:Adam优化器,快速收敛。
- 损失函数:SmoothL1Loss
- 评估指标:NME
https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/02_paddle2.0_develop/07_customize_cn.html#metric
from paddle.metric import Metric
class NME(Metric):
"""
1. 继承paddle.metric.Metric
"""
def __init__(self, name='nme', *args, **kwargs):
"""
2. 构造函数实现,自定义参数即可
"""
super(NME, self).__init__(*args, **kwargs)
self._name = name
self.rmse = 0
self.sample_num = 0
def name(self):
"""
3. 实现name方法,返回定义的评估指标名字
"""
return self._name
def update(self, preds, labels):
"""
4. 实现update方法,用于单个batch训练时进行评估指标计算。
- 当`compute`类函数未实现时,会将模型的计算输出和标签数据的展平作为`update`的参数传入。
"""
N = preds.shape[0]
preds = preds.reshape((N, -1, 2))
labels = labels.reshape((N, -1, 2))
self.rmse = 0
for i in range(N):
pts_pred, pts_gt = preds[i, ], labels[i, ]
interocular = np.linalg.norm(pts_gt[36, ] - pts_gt[45, ])
self.rmse += np.sum(np.linalg.norm(pts_pred - pts_gt, axis=1)) / (interocular * preds.shape[1])
self.sample_num += 1
return self.rmse / N
def accumulate(self):
"""
5. 实现accumulate方法,返回历史batch训练积累后计算得到的评价指标值。
每次`update`调用时进行数据积累,`accumulate`计算时对积累的所有数据进行计算并返回。
结算结果会在`fit`接口的训练日志中呈现。
"""
return self.rmse / self.sample_num
def reset(self):
"""
6. 实现reset方法,每个Epoch结束后进行评估指标的重置,这样下个Epoch可以重新进行计算。
"""
self.rmse = 0
self.sample_num = 0
# 使用 paddle.Model 封装模型
model = paddle.Model(SimpleNet(key_pts=68))
# 定义Adam优化器
optimizer = paddle.optimizer.Adam(learning_rate=0.001,
weight_decay=5e-4,
parameters=model.parameters())
# 定义SmoothL1Loss
loss = nn.SmoothL1Loss()
# 使用自定义metrics
metric = NME()
# 配置模型
model.prepare(optimizer=optimizer, loss=loss, metrics=metric)
# 模型训练
# model.fit(train_dataset, epochs=100, batch_size=128, verbose=1)
损失函数的选择:L1Loss、L2Loss、SmoothL1Loss的对比
- L1Loss: 在训练后期,预测值与ground-truth差异较小时, 损失对预测值的导数的绝对值仍然为1,此时如果学习率不变,损失函数将在稳定值附近波动,难以继续收敛达到更高精度。
- L2Loss: 在训练初期,预测值与ground-truth差异较大时,损失函数对预测值的梯度十分大,导致训练不稳定。
- SmoothL1Loss: 在x较小时,对x梯度也会变小,而在x很大时,对x的梯度的绝对值达到上限 1,也不会太大以至于破坏网络参数。
model.fit(train_dataset, epochs=100, batch_size=64, verbose=1)
checkpoints_path = './checkpoints/models'
model.save(checkpoints_path)
五、模型预测
根据预测的点位,进行特效处置。