1.背景介绍
2020年,已经进入了全面复苏乏力、物质生产与消费不平衡、人口老龄化加剧、城市空气污染、公共卫生健康等众多社会问题的时期。如何有效地保障人民群众生命财产安全,成为国家重点工程。智能安防也成为人们关心的话题之一。越来越多的人对自动驾驶汽车、无人机、机器人、互联网技术、传感器、图像处理、生物识别等领域的突破性科技保持着极大的兴趣。而机器学习、深度学习、强化学习、优化算法等前沿技术正为这个方向带来新的发展机会。借助于以上技术的革命性进步,自动驾驶汽车、无人机等人机交互系统将迎来新一轮的发展。
现如今,随着计算机视觉、自然语言处理、模式识别、图形学、生物信息学等领域的深入研究和应用,基于机器学习的智能安防系统已经逐渐成熟,在低成本、高精度的同时具备了较好的隐蔽性和鲁棒性。可以预见的是,未来的智能安防系统将呈现出更为复杂、更具实时性、更具有自主学习能力的特征。
根据当前的热点需求,针对智能安防领域的核心技术,国内外研究者相继提出了许多创新型的方法、模型、算法和工具,并取得了理论与实践上的巨大进展。其中包括深度学习、强化学习、单目摄像头+边缘计算、视觉模式识别、语音理解、传感网络、非凸优化算法等。因此,编写一篇符合专业水准且有深度思考、丰富案例、详尽的原理和算法实现的技术博客文章,对于其他读者也是十分有益的。
2.核心概念与联系
2019年上半年,英伟达发布了第二代神经网络推理引擎Deep Learning Inference Engine(简称DL-IE),提供一种可编程的、高度可扩展的、高性能的神经网络推理框架。此后,随着英伟达的深度学习技术的迅猛发展,深度学习模型的计算能力也在快速增长。另外,由于有限的内存资源限制,只能支持固定大小的神经网络模型。因此,为了解决超大模型的问题,NVIDIA推出了混合精度训练,通过采用低精度数据类型(float16或bfloat16)来减少内存占用,同时保持模型精度的同时提升训练效率。
基于GPU加速的深度学习方法,诞生了众多深度学习框架。如PyTorch、TensorFlow、MXNet等。这些框架提供了便利的接口及高效的运行速度。另一方面,微软正在加紧布局其人工智能云平台Azure,目前已有超过7万名工程师投入到该领域进行研究开发。除此之外,国内也有诸如百度、腾讯、华为等众多公司基于自身优势,推出了不同类型的AI产品和服务。
综合以上信息,可以得出结论:
- 深度学习算法的发展已经催生了许多“智能安防”相关的创新项目,采用不同的方法、模型、算法进行设计。
- 深度学习技术正以惊人的速度发展,将会带来前所未有的计算能力。
- 在本文中,我们将主要关注基于深度学习的方法、模型、算法,并围绕这些核心技术进行介绍。
3.核心算法原理和具体操作步骤以及数学模型公式详细讲解
- 目标检测算法YOLO (You Only Look Once)
YOLO是一个基于卷积神经网络的目标检测算法,该算法能够实时的进行目标的检测。YOLO对原始图片进行预测,先从图片中把有用的信息提取出来,然后生成一个网格,每个网格都可以认为是一个区域,再判断该网格里面是否包含物体,并标记它对应的位置。这样,YOLO能够很快的完成一次目标检测任务,同时能够同时检测多个目标。
具体流程如下:
- 对输入图片进行调整大小;
- 将输入图片转换成符合尺寸的feature map;
- 使用一个三层的神经网络结构预测bounding box坐标和类别概率分布;
- 将预测结果进行解码,得到目标位置和类别概率;
- 根据置信度阈值进行非极大抑制(Non Maximum Suppression,NMS),去掉重复框和假阳性。
YOLO算法的损失函数由两个部分组成:
- 定位损失,即最小化预测出的box与真实标注的box之间的偏差;
- 分类损失,即最小化预测出的类别概率分布与真实标注类别一致的概率。
YOLO算法在一定程度上可以消除孤立的目标影响,但也存在一些局限性。例如,对小物体的检测效果不太好;当多个目标聚集在一起时,效果可能会受到影响;对于背景相当模糊或者变化多样的场景,效果可能会变差。另外,YOLO算法在内存、计算、通信等方面的开销比较大,所以对于实际应用来说,还需要进一步提高算法的效率。
- SSD(Single Shot MultiBox Detector)
SSD(Single Shot MultiBox Detector)是2016年提出的目标检测算法,与YOLO相比,它有以下几个显著的优点:
- 更快:SSD只在第一次卷积网络处理时就把所有候选框都生成出来,而不是像YOLO一样在每个特征层上生成;
- 更准确:SSD算法考虑到了物体的尺寸、形状、姿态,使用不同尺寸的卷积核对特征图的不同位置进行检测,相比于YOLO这种仅依靠颜色和空间的检测方式,能获得更加准确的预测框;
- 降低内存消耗:SSD算法只在训练的时候才把所有候选框都保存下来,因为计算量非常大,因此SSD在内存上的消耗就会大幅降低;
- 模型大小小:SSD算法只有两个卷积层和两个全连接层,因此它的模型大小相比于YOLO小很多。
SSD算法的具体流程如下:
- 从输入图片生成多个尺度的特征图;
- 在每一个特征图上,利用不同尺寸的卷积核进行探测,将特征图划分成固定数量的默认框;
- 每个默认框代表一类特定大小、宽高比的目标;
- 通过边界框回归预测器,修正预测框的位置;
- 通过分类器预测各个类别的概率;
- 非极大值抑制(NMS)消除冗余检测框;
- 输出最终的检测结果。
SSD算法的损失函数由四个部分组成:
- 定位损失,将预测框与真实框的位置误差计算出来;
- 类别损失,将预测类别与真实类别的损失计算出来;
- 正负样本权重,保证正负样本权重的均匀分布;
- 置信度损失,将分类概率与真实标签之间的误差计算出来。
SSD算法的缺点是速度慢,但是由于它只在训练阶段才生成所有候选框,所以速度上的限制不是很大,另外,SSD算法不需要进行非极大值抑制(NMS),相比于YOLO,它有更加细致的目标检测结果。
- Faster R-CNN
Faster R-CNN是2015年提出的一种目标检测算法,它的特点就是速度快。它的核心思想是引入 Region Proposal Network(RPN)。RPN的作用是在整个图片中生成不同尺度的候选框,这些候选框代表了图片中的潜在物体。然后,利用候选框对整张图片进行卷积运算,从而生成固定维度的特征向量。这样,Faster R-CNN就可以直接对生成的特征向量进行预测,而不需要再次进行卷积运算。
具体流程如下:
- 从输入图片生成多个尺度的特征图;
- 使用Region Proposal Network生成多个候选框;
- 以这些候选框作为输入,对整张图片进行卷积运算,生成固定维度的特征向量;
- 对特征向量进行分类和回归预测,得到目标位置和类别概率;
- 用NMS消除冗余检测框。
Faster R-CNN的损失函数与YOLO类似。
4.具体代码实例和详细解释说明
下面,我们将用Python实现YOLO、SSD和Faster R-CNN算法,并对比它们的性能和效果。这里使用的图片来源于COCO数据集。首先下载训练集和验证集,并准备好数据加载。
import os
import cv2
import numpy as np
from matplotlib import pyplot as plt
from PIL import Image
from keras_yolo3.model import yolo_eval, yolo_body, tiny_yolo_body
from keras_yolo3.utils import get_random_data
def load_coco_dataset():
"""Load COCO dataset"""
# Path of train and validation images
coco_dir = 'D:/Project/Dataset/coco/'
img_train_dir = os.path.join(coco_dir, 'images', 'train2017')
ann_file = os.path.join(coco_dir, 'annotations',
'instances_train2017.json')
img_valid_dir = os.path.join(coco_dir, 'images', 'val2017')
return img_train_dir, ann_file, img_valid_dir
def preprocess_image(img):
"""Preprocess the input image"""
# Resize to target size
h, w = TARGET_SIZE
if h!= w:
resize_ratio = min(h / img.shape[0], w / img.shape[1])
resized_img = cv2.resize(
img, None, fx=resize_ratio, fy=resize_ratio, interpolation=cv2.INTER_CUBIC)
else:
resized_img = cv2.resize(
img, (w, h), interpolation=cv2.INTER_CUBIC)
# Convert color space from BGR to RGB
rgb_img = cv2.cvtColor(resized_img, cv2.COLOR_BGR2RGB)
# Normalize pixel values to [0, 1]
normalized_img = rgb_img / 255.0
return normalized_img
接下来,我们定义三个函数分别用于训练YOLO、SSD和Faster R-CNN模型。为了简单起见,我们只训练一个类别的目标检测。
def train_yolo(epochs):
"""Train YOLO model"""
# Load training data set
img_train_dir, ann_file, _ = load_coco_dataset()
class_names = ['person']
# Define YOLO model
anchors = [(1.3221, 1.73145), (3.19275, 4.00944),
(5.05587, 8.09892), (9.47112, 4.84053), (11.2364, 10.0071)]
model = yolo_body(len(class_names), anchors,
len(anchors)//3, alpha=0.5, weight_decay=0., use_bias=True)
model.load_weights('D:/Project/models/yolov3.h5', by_name=True, skip_mismatch=True)
print('Load weights success.')
# Compile the model with a SGD optimizer and a loss function that
# targets both classification and regression errors
model.compile(optimizer='adam',
loss={'yolo_loss': lambda y_true, y_pred: y_pred},
metrics=['accuracy'])
print('Compile model success.')
batch_size = 32
val_split = 0.1
num_samples = sum([len(files) for r, d, files in os.walk(img_train_dir)])
steps_per_epoch = int((num_samples - num_samples*val_split)/batch_size) + 1
print('steps_per_epoch:', steps_per_epoch)
# Train the model on the train dataset
callbacks = [keras.callbacks.ModelCheckpoint(
'D:/Project/models/yolov3_{epoch}.h5', save_weights_only=False, period=10)]
history = model.fit_generator(
generator=data_generator(img_train_dir, ann_file, class_names,
batch_size, shuffle=True),
steps_per_epoch=steps_per_epoch,
epochs=epochs,
verbose=1,
workers=1,
max_queue_size=10,
callbacks=callbacks,
validation_data=data_generator(img_train_dir, ann_file, class_names,
batch_size, shuffle=False))
# Plot learning curves
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='validation')
plt.title('Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
def train_ssd(epochs):
"""Train SSD model"""
# Load training data set
img_train_dir, ann_file, _ = load_coco_dataset()
classes = ["background", "aeroplane", "bicycle", "bird", "boat",
"bottle", "bus", "car", "cat", "chair", "cow", "diningtable",
"dog", "horse", "motorbike", "person", "pottedplant",
"sheep", "sofa", "train", "tvmonitor"]
# Define SSD model
model = ssd_model.create_model(input_shape=(None, None, 3),
num_classes=len(classes),
base="vgg")
model.load_weights("D:/Project/models/vgg_ssd300.h5", by_name=True)
print('Load weights success.')
# Freeze layers of VGG
freeze_layers(model, pattern=r"^conv\d*$|.*bn\d*$|.*dense.*$|.*softmax.*$")
# Add custom Layers
model.get_layer("predictions")._init_graph_network(
outputs={"loc": model.get_layer("cls1_fc_loc").output,
"conf": model.get_layer("cls1_fc_conf").output})
anchor_boxes = generate_default_boxes(img_size=(300, 300))
prior_boxes = bbox_util.generate_prior_boxes(anchor_boxes, clip_boxes=False,
variances=[0.1, 0.1, 0.2, 0.2])
layer = AnchorBoxes(name="mbox_priorbox")(model.output)
model = Model(inputs=model.input, outputs=layer)
model._make_predict_function()
print('Add extra layers success.')
# Compile the model with a Adam optimizer and smooth L1 loss
adam = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
smooth_l1 = SmoothL1(sigma=0.5)
model.compile(optimizer=adam,
loss={"mbox_loc": smooth_l1,
"mbox_conf": "categorical_crossentropy"},
metrics={
"mbox_loc": [metrics.mean_absolute_error,
metrics.mean_squared_error,
tf.keras.metrics.MeanIoU()],
"mbox_conf": "accuracy"})
print('Compile model success.')
batch_size = 32
val_split = 0.1
num_samples = sum([len(files) for r, d, files in os.walk(img_train_dir)])
steps_per_epoch = int((num_samples - num_samples*val_split)/batch_size) + 1
print('steps_per_epoch:', steps_per_epoch)
# Train the model on the train dataset
callbacks = [keras.callbacks.ModelCheckpoint(
"D:/Project/models/ssd300_{epoch}.h5", monitor="val_loss", verbose=1, save_best_only=True, mode="min")]
history = model.fit_generator(
generator=data_generator(img_train_dir, ann_file, classes,
batch_size, is_training=True),
steps_per_epoch=steps_per_epoch,
epochs=epochs,
verbose=1,
workers=1,
max_queue_size=10,
callbacks=callbacks,
validation_data=data_generator(img_train_dir, ann_file, classes,
batch_size, is_training=False))
# Plot learning curves
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='validation')
plt.title('Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
def train_faster_rcnn(epochs):
"""Train Faster RCNN model"""
# Load training data set
img_train_dir, ann_file, _ = load_coco_dataset()
num_classes = 1
# Define Faster R-CNN model
config = ConfigProto()
config.gpu_options.allow_growth = True
session = InteractiveSession(config=config)
K.set_learning_phase(1)
backbone = ResNet50(input_tensor=Input(shape=(None, None, 3)), weights="imagenet", include_top=False)
model = FasterRCNN(backbone=backbone, num_classes=num_classes)
model_path = "D:/Project/models/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5"
model.load_weights(model_path, by_name=True, skip_mismatch=True)
print('Load weights success.')
# Unfreeze all layers except last two blocks
n = len(model.layers)-1
for i in range(n-4):
model.layers[i].trainable = False
model.summary()
# Compile the model with a SGD optimizer and a classification loss
sgd = SGD(lr=0.001, momentum=0.9, nesterov=True)
model.compile(optimizer=sgd,
loss='sparse_categorical_crossentropy',
metrics=["accuracy"])
print('Compile model success.')
batch_size = 32
val_split = 0.1
num_samples = sum([len(files) for r, d, files in os.walk(img_train_dir)])
steps_per_epoch = int((num_samples - num_samples*val_split)/batch_size) + 1
print('steps_per_epoch:', steps_per_epoch)
# Train the model on the train dataset
callbacks = [ModelCheckpoint(filepath='D:/Project/models/fasterrcnn_{epoch}.h5',
monitor='val_loss', verbose=1, save_best_only=True)]
history = model.fit_generator(
generator=data_generator(img_train_dir, ann_file, num_classes,
batch_size, shuffle=True),
steps_per_epoch=steps_per_epoch,
epochs=epochs,
verbose=1,
workers=1,
max_queue_size=10,
callbacks=callbacks,
validation_data=data_generator(img_train_dir, ann_file, num_classes,
batch_size, shuffle=False))
# Plot learning curves
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='validation')
plt.title('Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
最后,我们定义一个函数用于评估模型的性能。
def evaluate_model(model_type):
"""Evaluate the performance of trained model"""
_, _, img_valid_dir = load_coco_dataset()
img_paths = []
gt_boxes = []
pred_scores = []
pred_labels = []
img_width = IMAGE_SIZE[1]
img_height = IMAGE_SIZE[0]
for filename in os.listdir(os.path.join(img_valid_dir)):
continue
# Read the image file
im = Image.open(os.path.join(img_valid_dir, filename))
# Preprocess the image
img = np.array(im, dtype='uint8')
inputs = preprocess_image(img).reshape((1,) + img.shape+(3,))
# Predict objects using the trained model
if model_type == 'yolo':
yolo_outputs = yolo_head(model.output, anchors, len(class_names))
boxes, scores, classes = yolo_eval(
yolo_outputs,
inputs[0].shape[1:],
score_threshold=0.3,
iou_threshold=0.45,
max_boxes=200)
elif model_type =='ssd':
features = FeatureExtractor(model)({'image': inputs})
predictions = head(features)
boxes, labels, probs = decode_detections(predictions,
confidence_thresh=0.5,
iou_threshold=0.5,
top_k=200)[0]
elif model_type == 'faster_rcnn':
detections = model.detect([inputs], verbose=0)[0]['rois'][:, :-1]
# Save ground truth annotations
annot_path = os.path.join(ann_dir, '{}.xml'.format(filename[:-4]))
tree = ET.parse(annot_path)
root = tree.getroot()
width = float(root.findtext('./size/width'))
height = float(root.findtext('./size/height'))
for obj in root.findall('./object'):
cls_name = obj.findtext('name')
xmin = float(obj.findtext('bndbox/xmin')) / width * img_width
ymin = float(obj.findtext('bndbox/ymin')) / height * img_height
xmax = float(obj.findtext('bndbox/xmax')) / width * img_width
ymax = float(obj.findtext('bndbox/ymax')) / height * img_height
gt_boxes.append([[xmin, ymin], [xmax, ymax]])
# Filter out low confident predictions
indices = np.where(probs > 0.5)
filtered_boxes = boxes[indices]
filtered_labels = labels[indices]
filtered_probs = probs[indices]
# Keep only one predicted object per bounding box (non-max suppression)
final_boxes = []
final_labels = []
final_probs = []
while len(filtered_boxes) > 0:
max_idx = np.argmax(filtered_probs)
curr_box = filtered_boxes[max_idx]
final_boxes.append(curr_box)
final_labels.append(filtered_labels[max_idx])
final_probs.append(filtered_probs[max_idx])
overlap = compute_overlap(np.expand_dims(curr_box, axis=0),
filtered_boxes[:max_idx]+filtered_boxes[max_idx+1:])
filtered_boxes = filtered_boxes[max_idx+1:]
filtered_labels = filtered_labels[max_idx+1:]
filtered_probs = filtered_probs[max_idx+1:]
filtered_probs *= np.greater(overlap, 0.45)
# Store the results
img_paths.append(os.path.join(img_valid_dir, filename))
pred_scores.extend(final_probs)
pred_labels.extend(final_labels)
# Compute mAP
APs = []
for c in range(len(class_names)):
AP = average_precision_score(gt_boxes, pred_scores, pred_labels,
gt_class=c)
APs.append(AP)
mean_ap = np.mean(APs)
print('{} Mean AP@0.5IOU: {:.4f}'.format(model_type, mean_ap))
通过比较训练得到的模型在验证集上的性能,我们可以分析它们的区别,选择最适合的模型。
if __name__ == '__main__':
TRAINING = False
EPOCHS = 20
MODEL_TYPE = 'yolo'
if TRAINING:
if MODEL_TYPE == 'yolo':
train_yolo(EPOCHS)
elif MODEL_TYPE =='ssd':
train_ssd(EPOCHS)
elif MODEL_TYPE == 'faster_rcnn':
train_faster_rcnn(EPOCHS)
evaluate_model(MODEL_TYPE)