使用 Keras 和 CNN 进行自定义 AI 人脸识别





5.00/5 (4投票s)
本文中,我们将组合并训练我们的 CNN 用于人脸识别。
引言
如果你看过电影《少数派报告》,你可能会记得汤姆·克鲁斯走进一家 Gap 商店的场景。一个视网膜扫描仪读取他的眼睛,并为他播放定制广告。现在是 2020 年。我们不需要视网膜扫描仪,因为我们有人工智能 (AI) 和机器学习 (ML)!
在本系列中,我们将向你展示如何使用深度学习进行人脸识别,然后——根据已识别的人脸——使用神经网络文本转语音 (TTS) 引擎播放定制广告。欢迎你在 CodeProject 上浏览代码,或下载 .zip 文件到你的机器上浏览代码。
我们假设你熟悉 AI/ML 的基本概念,并且能够使用 Python。
在本文中,我们将讨论 CNN,然后设计一个并使用 Keras 在 Python 中实现它。
什么是 CNN?
CNN 是一种神经网络 (NN),常用于图像分类任务,例如人脸识别,以及输入具有网格状拓扑的任何其他问题。在 CNN 中,并非每个节点都连接到下一层的所有节点;换句话说,它们不是全连接的 NN。这有助于防止在全连接 NN 中出现的过拟合问题,更不用说由于 NN 中连接过多导致的收敛速度极慢。
CNN 的概念依赖于一种称为卷积的数学运算,这在数字信号处理领域非常常见。卷积定义为两个函数的乘积——第三个函数——表示前两个函数之间的重叠量。在 CNN 领域,卷积是通过在图像中滑动滤波器(又称核)来实现的。
在人脸识别中,卷积运算允许我们检测图像中的不同特征。不同的滤波器可以检测垂直和水平边缘、纹理、曲线以及其他图像特征。这就是为什么任何 CNN 中的第一层之一都是卷积层。
CNN 中常见的另一个层是池化层。池化用于减小图像表示的大小,这转化为参数数量的减少,并最终减少计算工作量。最常见的池化类型是“最大池化”,它使用滑动窗口——类似于卷积操作中的滑动窗口——在每个位置从匹配的单元组中获取最大值。最后,它从获取的最大值构建新的图像表示。
最常见的 CNN 架构通常以卷积层开始,后跟激活层,然后是池化层,最后是一个传统的全连接网络,例如多层 NN。这种层一个接一个排列的模型被称为顺序模型。为什么最后是全连接网络?是为了学习变换后的图像(经过卷积和池化之后)中特征的非线性组合。
设计 CNN
这是我们将在 CNN 中实现的架构:
- 输入层——一个 NumPy 数组 (img_width, img_height, 1);“1”表示我们处理的是灰度图像;对于 RGB 图像,它将是 (img_width, img_height, 3)
- Conv2D 层——32 个滤波器,滤波器大小为 3
- 激活层——必须使用非线性函数进行学习,在本例中,该函数是 ReLU
- Conv2D 层——32 个滤波器,滤波器大小为 3,步长为 3
- 使用 ReLU 函数的激活层
- MaxPooling2D 层——应用 (2, 2) 池化窗口
- DropOut 层,比例为 25%——通过随机丢弃前一层的一些值(将它们设置为 0)来防止过拟合;又称稀释技术
- Conv2D 层——64 个滤波器,滤波器大小为 3
- 使用 ReLU 函数的激活层
- Conv2D 层——64 个滤波器,滤波器大小为 3,步长为 3
- 使用 ReLU 函数的激活层
- MaxPooling2D 层——应用 (2, 2) 池化窗口
- DropOut 层,比例为 25%
- Flatten 层——转换数据以用于下一层
- Dense 层——代表一个全连接的传统 NN
- 使用 ReLU 函数的激活层
- DropOut 层,比例为 25%
- Dense 层,节点数量与问题中的类别数量匹配——耶鲁数据集为 15
- 使用 ReLU 函数的激活层
上述架构非常常见;层参数已通过实验进行微调。
实现 CNN
现在让我们在代码中实现我们的 CNN 架构——我们选择的层集。为了创建一个易于扩展的解决方案,我们将使用带有抽象方法的 ML 模型。
class MLModel(metaclass=abc.ABCMeta):
def __init__(self, dataSet=None):
if dataSet is not None:
self.objects = dataSet.objects
self.labels = dataSet.labels
self.obj_validation = dataSet.obj_validation
self.labels_validation = dataSet.labels_validation
self.number_labels = dataSet.number_labels
self.n_classes = dataSet.n_classes
self.init_model()
@abstractmethod
def init_model(self):
pass
@abstractmethod
def train(self):
pass
@abstractmethod
def predict(self, object):
pass
@abstractmethod
def evaluate(self):
score = self.get_model().evaluate(self.obj_validation, self.labels_validation, verbose=0)
print("%s: %.2f%%" % (self.get_model().metrics_names[1], score[1] * 100))
@abstractmethod
def get_model(self):
pass
在我们的例子中,`dataset` 是本系列上一篇文章中描述的 `FaceDataSet` 类的一个实例。`ConvolutionalModel` 类继承自 `MLModel` 并实现了其所有抽象方法,它将包含我们的 CNN 架构。代码如下:
class ConvolutionalModel(MLModel):
def __init__(self, dataSet=None):
if dataSet is None:
raise Exception("DataSet is required in this model")
self.shape = numpy.array([constant.IMG_WIDTH, constant.IMG_HEIGHT, 1])
super().__init__(dataSet)
self.cnn.compile(loss=constant.LOSS_FUNCTION,
optimizer=Common.get_sgd_optimizer(),
metrics=[constant.METRIC_ACCURACY])
def init_model(self):
self.cnn = Sequential()
self.cnn.add(Convolution2D(32, 3, padding=constant.PADDING_SAME, input_shape=self.shape))
self.cnn.add(Activation(constant.RELU_ACTIVATION_FUNCTION))
self.cnn.add(Convolution2D(32, 3, 3))
self.cnn.add(Activation(constant.RELU_ACTIVATION_FUNCTION))
self.cnn.add(MaxPooling2D(pool_size=(2, 2)))
self.cnn.add(Dropout(constant.DROP_OUT_O_25))
self.cnn.add(Convolution2D(64, 3, padding=constant.PADDING_SAME))
self.cnn.add(Activation(constant.RELU_ACTIVATION_FUNCTION))
self.cnn.add(Convolution2D(64, 3, 3))
self.cnn.add(Activation(constant.RELU_ACTIVATION_FUNCTION))
self.cnn.add(MaxPooling2D(pool_size=(2, 2)))
self.cnn.add(Dropout(constant.DROP_OUT_O_25))
self.cnn.add(Flatten())
self.cnn.add(Dense(constant.NUMBER_FULLY_CONNECTED))
self.cnn.add(Activation(constant.RELU_ACTIVATION_FUNCTION))
self.cnn.add(Dropout(constant.DROP_OUT_0_50))
self.cnn.add(Dense(self.n_classes))
self.cnn.add(Activation(constant.SOFTMAX_ACTIVATION_FUNCTION))
self.cnn.summary()
def train(self, n_epochs=20, batch=32):
self.cnn.fit(self.objects, self.labels,
batch_size=batch,
epochs=n_epochs, shuffle=True)
def get_model(self):
return self.cnn
def predict(self, image):
image = Common.to_float(image)
result = self.cnn.predict(image)
print(result)
def evaluate(self):
super(ConvolutionalModel, self).evaluate()
在构造函数中,我们设置了 `self.shape` 变量,它定义了输入层的形状。在我们的例子中,对于高 320 像素、宽 243 像素的耶鲁数据集图像,`self.shape=(320, 243, 1)`。
然后我们调用 `super()` 来设置所有来自父构造函数的数据集相关变量,并调用 `init_model()` 方法来初始化模型。
最后,我们调用 `compile` 方法,它配置模型进行训练,并将要在 `loss` 参数中使用的目标函数设置为。目标函数在训练过程中被优化——最小化或最大化。`accuracy` 参数定义了在训练期间评估模型的指标。`optimizer` 参数定义了权重的计算方式;最常见的优化器是梯度下降。
我们的 CNN 模型被定义为顺序模型,所有层都按照架构要求添加。`train()` 方法使用 `sequential` 类的 `fit` 方法(代表层的排列)来训练 CNN。此方法将用于训练 CNN 的数据、此数据的正确分类以及一些可选参数(例如要运行的 epochs 数量)作为输入。
训练 CNN
现在代码已经准备好了——是时候训练我们的 CNN 了。让我们实例化 `ConvolutionalModel` 类,在耶鲁数据集上进行训练,并调用 evaluate 方法。
cnn = ConvolutionalModel(dataSet)
cnn.train(n_epochs=50)
cnn.evaluate()
经过 50 个 epoch 的训练,我们对测试图像的准确率达到了近 85%。
这意味着我们的 CNN 现在将以 85% 的概率识别数据集中 15 个对象中的每一个。对于一个简单的练习来说,还不错,是吧?
现在我们已经训练了 CNN,如果我们要预测新的传入数据,也就是图像中的新面孔,我们可以使用前面详细介绍的 ConvolutionalModel 类中的 predict(image) 方法来实现。它将如何工作?调用将如下所示,并且应符合某些假设。
cnn.predict(np.expand_dims(image, axis=0))
首先,输入图像需要与之前训练过的 CNN 的输入层具有相同的尺寸或形状。其次,它应该是相同类型的输入,即像素值矩阵,在 `predict()` 方法内部我们对数据进行归一化,因此无需提供归一化后的图像像素矩阵。第三,我们可能需要为输入的面部图像添加一个维度,因为在训练过的 CNN 中,我们为数据集中的样本数量考虑了第四个维度。这可以使用 numpy 的 `expand_dims()` 方法实现。第四,假设将提供一个面部图像,如果图片较大,前面文章中提供的面部检测方法可能会很有用。
最后,`predict()` 方法的输出可以在上图中看到。此方法将输出面部属于每个可能类别或个体(对于训练数据集为 15 个)的概率。在这种情况下,我们可以看到最高概率将是类别 4,这正是输入面部图像所指的类别或人。
下一步?
现在我们知道如何从头开始构建自己的 CNN。在下一篇文章中,我们将探讨另一种方法——利用预训练模型。我们将采用一个在包含数百万图像的数据集上预训练用于人脸识别的 CNN,并将其调整以解决我们的问题。敬请期待!