Backpropagation in Neural Network

摘要: 反向传播毋庸置疑是整个神经网络的精髓,正是由于它的提出标志着深度神经网络的训练在有限算力基础上成为可能,但反向传播本身的原理同样值得品读和思考。

本文主要总结神经网络中反向传播算法的推导流程并挖掘一些深层次的原理。反向传播算法在1970年就已经提出,直到1986年 David Rumelhart, Geoffrey Hinton, and Ronald Williams在一篇论文中对其实现的分析才得以普及。

1. 表达式的定义

  • $w_{jk}^l$ 代表第l−1层第k个神经元,与第l层第j个神经元之间的权重(注意j与k的顺序);
  • $b_j^l$ 代表第l层中第j个神经元的偏移;
  • $a_j^l$ 代表第l层中第j个神经元的激活函数值;
  • $L$ 代表神经网络的总层数;
  • $J(W,b)$ 简写为$J$ 代表神经网络的代价函数;

假设我们有$m$ 个训练样本${(x^{(1)},y^{(1)}),…, (x^{(m)},y^{(m)})}$ ,对于每个训练样本$(x,y)$ 定义代价函数为:

对于$m$个训练样本,总的代价函数为:

根据神经网络层与层之间关系的定义,我们有如下表达式(向量形式):

其中

利用梯度下降法进行目标优化使用的主要梯度更新公式:

因为神经网络的复杂性导致$\frac{\partial J}{\partial w^l}$ 无法直接计算或者计算代价太大(每个权重的计算都需要进行一次前向传播),反向传播的目的在于提供一种更加高效的手段,完成上述梯度的更新操作。

2. 反向传播的直观理解

反向传播是用于理解改变网络中的任一权重如何影响网络代价函数的过程。假设神经网络中某个权重$w_{jk}^l$

产生了轻微的扰动误差 $\Delta w_{jk}^l$ ,扰动误差导致该神经元的输出产生误差 $\Delta z_j^l$ ,这个扰动将使对应的神经元$a_j^l$ 产生一个扰动误差 $\Delta a_j^l$ ,该误差将逐步向后层传递,直至达到输出层,并最终影响代价函数,生成一个代价误差 $\Delta J = \frac{\partial J}{\partial z_j^l}\Delta z_j^l$ 。我们可以给出如下式子来为通过误差近似计算梯度提供方向:

其中,$\delta_j^l$ 可以定义为$l$层第$j$个神经元上的误差。

我们先从计算最后一层误差$\delta_j^L$ 开始,

公式E1(向量形式):

证明

现在把问题转变为如何利用反向传播由后往前逐步计算误差$\delta_j^l$ ,为计算不同层之间误差之间的关系,利用链式法则给出如下推理过程:

公式E2

有了以上误差的反向传播计算方法,我们可以利用计算的误差计算权重的梯度如下:

公式E3

公式E4

其中公式(4)也可以写成:

证明:

另一种求解方式

如何我们令$\delta_j^l =\frac{\partial J}{\partial a_j^l}$ 也可以按照上述过程类似的方法推到出相关公式

3. 反向传播算法的计算流程

  1. 输入$x$,令$a^1=z^1=x$;
  2. 前向传播: 对于每层$l=2,3,…,L$计算$z^l$和$a^l$;
  3. 根据公式E1计算输出误差:$\delta^L = \nabla_a J \odot \sigma’(z^L)$ ;
  4. 反向传播:对于每层$l=L-1,L-2,…,2$ 利用公式E2计算误差:$\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma’(z^l)$ ;
  5. 利用公式E3E4计算参数梯度;
  6. 更新权重

参考

  1. CS231n讲义:Backpropagation, Intuitions
  2. Neural Networks and Deep Learning
  3. Machine Learning Cousera Course

使用TensorFlow Object Detection API识别仪表表盘

前面用到了Tensorflow的物体识别API做了一个检测仪表表盘的实践,记录实践过程中的技巧。

Update: 2019/04/16,使用新版Tensorflow接口

1. 概述

本文主要介绍如何使用Tensorflow 物体识别API应用自己业务场景进行物体识别。将结合从事的一些实际经验,分享一个仪表表盘识别的案例。我们在一个利用机器视觉技术自动识别仪表表读数的项目中,需要首先识别各种不同类型表盘的显示屏位置。本案例分析将针对面板识别中采用的关键技术进行分析,详细阐述如何利用物体识别技术和已训练好的模型快速实现使用用户数据设计一个面向特定物体识别的深度神经网络。

目标: 从给定的水表图片中将关键的数字面板给扣取出来。

如下图所示,如果采用通用OCR技术对水表图片面板进行检测,将同时提取很多特征项,对实际的检测值造成比较大的干扰。

ocr-demo

1.1 模型及算法选型

在方案设计初期我们分别使用公有云服务、现有的成熟OCR软件、开源的OCR方案对目标对象进行了初步识别及分析。通过实测,现有方案无法满足我们的任务需求。为此希望能够利用深度学习在物体识别领域的成熟方案,构建一个面向水表图片面板识别的神经网络模型。

Model name Speed (ms) Pascal mAP@0.5 (ms) Outputs
faster_rcnn_resnet101_kitti 79 87 Boxes
Model name Speed (ms) Open Images mAP@0.52 Outputs
faster_rcnn_inception_resnet_v2_atrous_oid 727 37 Boxes
faster_rcnn_inception_resnet_v2_atrous_lowproposals_oid 347 Boxes

几种模型主要在精度和速度方面进行了取舍,如果需要一个高精度的模型可以选择faster R-CNN,如果希望速度快比如实时检测,则可以选择SSD模型。鉴于本方案中设计检测目标特征将为简单,可采用最轻量级的MobileSSD进行优化。

2. 实现流程

2.1 数据准备

数据准备的环节是除了训练过程之外最为耗时的环节,准备数据的质量和数量将直接决定了训练模型的好坏。图片数据最好在光线、角度、清晰度等方面能最大化的泛化实际的业务场景。由于在这个案例中我们只需要输出一个分类对象,而且输入对象被限定在水表图片上,所以整体涉及需要提取的特征参数空间不是很大,少量经过处理好的明显可供辨识的水表图片即可。目前可用水表图片攻击229张,我们采用80%用于训练,20%用于测试的方式进行划分。

如果分类较多,数据有限,可以选择从互联网上下载或者在开源数据集中获得所需的数据。

另外需要注意图片的大小,图片太大一方面影响训练过程的处理时间,另外大量图片载入内存将很容导致内存溢出,所以如果图像特征粒度不是特别精细可以采用低分辨率图片进行分析。

数据标记

对于物体识别而言,数据标记过程是一个相对复杂的过程,目前除了人工标记没有太好的自动或半监督手段,幸好针对图片的标记已经有了几款很好用的工具:

  • LabelImg

    • 这是一个可以直接在图片上做注释框自动生成标记信息的软件,注释信息将被保存为PASCAL VOC 格式的XML文件(ImageNet的文件格式)

    labelImage

  • FIAT (Fast Image Data Annotation Tool)

    • 该工具生成csv格式的注释文件

      data annotation

  • ImageMagick

    • 图片预处理工具
    • Use ImageMagick® to create, edit, compose, or convert bitmap images. It can read and write images in a variety of formats (over 200) including PNG, JPEG, GIF, HEIC, TIFF, DPX, EXR, WebP, Postscript, PDF, and SVG. Use ImageMagick to resize, flip, mirror, rotate, distort, shear and transform images, adjust image colors, apply various special effects, or draw text, lines, polygons, ellipses and Bézier curves.

在本方案中我们使用工具LabelImage将229张水表图片进行了标注,同时生成了PASCAL格式的XML文件,名字为000001.jpg的图片注释格式如下:

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
<annotation>
<folder>imgs</folder>
<filename>000001.jpg</filename>
<source>
<database>VOC</database>
<annotation>PASCAL VOC</annotation>
</source>
<size>
<width>2448</width>
<height>3264</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>panel</name>
<pose>Frontal</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>739</xmin>
<ymin>430</ymin>
<xmax>1475</xmax>
<ymax>796</ymax>
</bndbox>
</object>
</annotation>

我们在根目录新建一个annotations的文件夹存储229张图片的XML描述文件,同时根目录images文件夹用于存储所有的训练数据和测试数据。

数据描述格式

  • 在TensorFlow 物体检测API中使用 TFRecord file format格式对图像标记信息进行描述,所以我们无论采取下面的那种标记方式,最终需要生成TFRcord格式的文件格式。
  • TFRecord格式要去如下:

For every example in your dataset, you should have the following information:

  1. An RGB image for the dataset encoded as jpeg or png.
  2. A list of bounding boxes for the image. Each bounding box should contain:
    1. A bounding box coordinates (with origin in top left corner) defined by 4floating point numbers [ymin, xmin, ymax, xmax]. Note that we store thenormalized coordinates (x / width, y / height) in the TFRecord dataset.
    2. The class of the object in the bounding box.
  • TensorFlow针对主流的物体识别类数据集格式提供了转换工具,包括 PASCAL VOC datasetOxford Pet dataset等;

    • PASCAL VOC
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # From tensorflow/models/research/
    wget http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar
    tar -xvf VOCtrainval_11-May-2012.tar
    python object_detection/dataset_tools/create_pascal_tf_record.py \
    --label_map_path=object_detection/data/pascal_label_map.pbtxt \
    --data_dir=VOCdevkit --year=VOC2012 --set=train \
    --output_path=pascal_train.record
    python object_detection/dataset_tools/create_pascal_tf_record.py \
    --label_map_path=object_detection/data/pascal_label_map.pbtxt \
    --data_dir=VOCdevkit --year=VOC2012 --set=val \
    --output_path=pascal_val.record
    • Oxford-IIIT Pet
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # From tensorflow/models/research/
    wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
    wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz
    tar -xvf annotations.tar.gz
    tar -xvf images.tar.gz
    python object_detection/dataset_tools/create_pet_tf_record.py \
    --label_map_path=object_detection/data/pet_label_map.pbtxt \
    --data_dir=`pwd` \
    --output_dir=`pwd`
  • 如果你使用了自己的格式,可以参考TensorFlow官方文档完成格式转换;

在本方案中我们采用自己处理的方式来进行格式转换,使用文件xml_to_csv.py将所有图片的PASCAL格式文件转化为一个csv文件,然后进一步的利用TensorFLow工具generate_tfrecord.py生成TFRecord格式文件:

  1. 执行python xml_to_csv.py,该文件将在根目录的annotations的文件夹下所有的*xml文件,并生成screen_labels.csv文件;

    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
    import os
    import glob
    import pandas as pd
    import xml.etree.ElementTree as ET


    def xml_to_csv(path):
    xml_list = []
    for xml_file in glob.glob(path + '/*.xml'):
    tree = ET.parse(xml_file)
    root = tree.getroot()
    for member in root.findall('object'):
    filename = root.find('filename').text
    value = (root.find('filename').text,
    int(root.find('size')[0].text),
    int(root.find('size')[1].text),
    member[0].text,
    int(member[4][0].text),
    int(member[4][1].text),
    int(member[4][2].text),
    int(member[4][3].text)
    )
    xml_list.append(value)
    column_name = ['filename', 'width', 'height', 'class', 'xmin', 'ymin', 'xmax', 'ymax']
    xml_df = pd.DataFrame(xml_list, columns=column_name)
    return xml_df


    def main():
    image_path = os.path.join(os.getcwd(), 'Annotations') # Need Changes
    xml_df = xml_to_csv(image_path)
    xml_df.to_csv('screen_labels.csv', index=None) # Need Changes
    print('Successfully converted xml to csv.')


    main()
  1. 使用如下代码随机生成训练数据和测试数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import numpy as np
    import pandas as pd
    np.random.seed(1)
    full_labels = pd.read_csv('screen_labels.csv')
    gb = full_labels.groupby('filename')
    grouped_list = [gb.get_group(x) for x in gb.groups]
    train_index = np.random.choice(len(grouped_list), size=180, replace=False)
    test_index = np.setdiff1d(list(range(229)), train_index)
    train = pd.concat([grouped_list[i] for i in train_index])
    test = pd.concat([grouped_list[i] for i in test_index])
    train.to_csv('train_labels.csv', index=None)
    test.to_csv('test_labels.csv', index=None)
  2. 分别执行脚本将训练数据和测试数据转换为TFRecord:

    1
    2
    3
    4
    5
    6
    7
    # From tensorflow/models/research/
    protoc object_detection/protos/*.proto --python_out=.
    # Create train data:
    python generate_tfrecord.py --csv_input=data/train_labels.csv --output_path=train.record

    # Create test data:
    python generate_tfrecord.py --csv_input=data/test_labels.csv --output_path=test.record
  3. 将train.record和test.record的文件存储至根目录的data文件夹下

  4. 至此数据准备基本结束,我们创建了如下目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    .
    ├── annotations
    │   ├── 000001.xml
    ...
    │   └── 000229.xml
    ├── data
    │   ├── screen_labels.csv
    │   ├── test_labels.csv
    │   ├── test.record
    │   ├── train_labels.csv
    │   └── train.record
    ├── generate_tfrecord.py
    ├── images
    │   ├── 000001.jpg
    ...
    │   └── 000229.jpg
    ├── __init__.py
    ├── README.md
    ├── split labels.ipynb
    ├── test_generate_tfrecord.py
    ├── test_xml_to_csv.py
    └── xml_to_csv.py

2.2 模型配置/迁移学习

完全从头训练一个用于物体检测的模型即使采用大量GPU资源至少也需要数周时间,为了加速模型初期迭代过程,我们选择了一个已经在COCO数据集针对其他多种物体识别场景预训练好的模型,通过重复使用该模型的多数参数来快速生成我们的模型。更多技术内容可以参照迁移学习的技术实现。

由于没有足够的资源从头训练一个模型,我们将采用迁移学习技术,利用一个已经训练好的模型进行迁移学习及训练。

从测试角度考虑,本测试方案选择了体积最小,速度最快的用于嵌入式设备的SSD模型:ssd_mobilenet_v1_coco

下载模型包,可以得到如下文件:

1
2
3
4
5
6
7
8
9
10
11
.
├── object-detection.pbtxt
├── ssd_mobilenet_v1_pets.config
├── checkpoint
├── frozen_inference_graph.pb
├── model.ckpt.data-00000-of-00001
├── model.ckpt.index
├── model.ckpt.meta
└── saved_model
├── saved_model.pb
└── variables
  • a graph proto (graph.pbtxt)
  • a checkpoint (model.ckpt.data-00000-of-00001, model.ckpt.index, model.ckpt.meta)
  • a frozen graph proto with weights baked into the graph as constants (frozen_inference_graph.pb) to be used for out of the box inference
  • a config file (pipeline.config) which was used to generate the graph. These directly correspond to a config file in the samples/configs) directory but often with a modified score threshold.

我们下载并在根目录解压模型包ssd_mobilenet_v1_coco,同时创建一个training文件,存储如下文件:

1
2
3
training/
├── object-detection.pbtxt
└── ssd_mobilenet_v1_pets.config

Label Map

其中 object-detection.pbtxt是我们模型所有分类的标签,如下所示,如果有多个分类id从1开始递增,同时给每个标签一个唯一的名称(id为0预留给背景分类)

1
2
3
4
5
6
7
8
item {
id: 1
name: 'panel'
}
item{
id: 2
name: '其他分类'
}

配置物体识别训练流程文件

更多内容参考:https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/configuring_jobs.md

Tensorflow Object Detection API 使用 protobuf 文件来配置训练和检测的流程。通过配置Training Pipleline的参数配置可以决定训练参数的选择,我们将尽量多的利用已经训练好的参数进行训练。

一个配置文件由5部分组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
model {
(... Add model config here...)
}

train_config : {
(... Add train_config here...)
}

train_input_reader: {
(... Add train_input configuration here...)
}

eval_config: {
}

eval_input_reader: {
(... Add eval_input configuration here...)
}
  1. The model configuration. This defines what type of model will be trained (ie. meta-architecture, feature extractor).
  2. The train_config, which decides what parameters should be used to train model parameters (ie. SGD parameters, input preprocessing and feature extractor initialization values).
  3. The eval_config, which determines what set of metrics will be reported for evaluation (currently we only support the PASCAL VOC metrics).
  4. The train_input_config, which defines what dataset the model should be trained on.
  5. The eval_input_config, which defines what dataset the model will be evaluated on. Typically this should be different than the training input dataset.

ssd_mobilenet_v1_pets.config为模型配置文件,我们在样例(详见:object_detection/samples/configs 文件夹)上进行如下修改:

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
  ssd {
num_classes: 1 #修改类别为实际类别值
box_coder {
faster_rcnn_box_coder {
y_scale: 10.0
x_scale: 10.0
height_scale: 5.0
width_scale: 5.0
}
}

fine_tune_checkpoint: "ssd_mobilenet_v1_coco_2017_11_17/model.ckpt" #指向模型文件中的checkpoint文件
train_input_reader: {
tf_record_input_reader {
input_path: "data/train.record" #修改为上一个步骤生成的训练record路径
}
label_map_path: "data/object-detection.pbtxt" #修改为pbtxt文件路径,描述类别标签
}
eval_input_reader: {
tf_record_input_reader {
input_path: "data/test.record" #修改为上一个步骤生成的测试数据record路径
}
label_map_path: "data/object-detection.pbtxt" #修改为pbtxt文件路径,描述类别标签
shuffle: false
num_readers: 1
}

train_config provides two fields to specify pre-existing checkpoints: fine_tune_checkpoint and from_detection_checkpoint. fine_tune_checkpoint should provide a path to the pre-existing checkpoint (ie:”/usr/home/username/checkpoint/model.ckpt-#####”). from_detection_checkpoint is a boolean value. If false, it assumes the checkpoint was from an object classification checkpoint. Note that starting from a detection checkpoint will usually result in a faster training job than a classification checkpoint.

The list of provided checkpoints can be found here.

2.3 训练

Tensorflow Object Detection API 安装

  1. 从GitHub下载Tensorflow Object Detection API

    1
    git clone https://github.com/tensorflow/models.git
  2. 配置基本环境

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # For CPU
    pip install tensorflow
    # For GPU
    pip install tensorflow-gpu

    sudo apt-get install protobuf-compiler python-pil python-lxml python-tk
    pip install --user Cython
    pip install --user contextlib2
    pip install --user jupyter
    pip install --user matplotlib
  3. COCO API installation

    1
    2
    3
    4
    git clone https://github.com/cocodataset/cocoapi.git
    cd cocoapi/PythonAPI
    make
    cp -r pycocotools <path_to_tensorflow>/models/research/
  1. Protobuf 编译

Tensorflow通过Google的Protobufs来配置和训练模型,所以在开始使用之前需要对protobuf相关库进行编译。

1
2
# From tensorflow/models/research/
protoc object_detection/protos/*.proto --python_out=.
  1. Add Libraries to PYTHONPATH[重要]

在路径 tensorflow/models/research/ 下添加PYTHONPATH路径,实现全局引用

1
2
# From tensorflow/models/research/
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim

Note: This command needs to run from every new terminal you start. If you wish to avoid running this manually, you can add it as a new line to the end of your ~/.bashrc file.

  1. 测试安装是否成功

    1
    2
    # From tensorflow/models/research/
    python object_detection/builders/model_builder_test.py

启动训练过程

  1. 将在数据准备和模型配置阶段的文件复制到tensorflow object_detect文件夹下/models/research/object_detection,包括data/文件夹,image/文件夹,training/文件夹,ssd_mobilenet_v1_coco_2017_11_17/原始模型文件夹

  2. 执行如下代码启动训练过程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # From the tensorflow/models/research/ directory
    PIPELINE_CONFIG_PATH={path to pipeline config file}
    MODEL_DIR={path to model directory}
    NUM_TRAIN_STEPS=50000
    SAMPLE_1_OF_N_EVAL_EXAMPLES=1
    python object_detection/model_main.py \
    --pipeline_config_path=${PIPELINE_CONFIG_PATH} \
    --model_dir=${MODEL_DIR} \
    --num_train_steps=${NUM_TRAIN_STEPS} \
    --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \
    --alsologtostderr

使用TensorBoard 跟踪训练过程

训练过程会持续几个小时到十几个小时,可以通过tensorboard查看训练的情况

使用一台Azure的CPU虚拟机进行训练~4s进行一次迭代,正常模型有比较不错结果迭代次数大概在10K以上。

1
tensorboard --logdir=${MODEL_DIR}

tensorboard_loss

2.4 导出模型

模型训练过程中,会每隔一段时间生成一个checkpoint,一个checkpoint至少包括三个文件:

  • model.ckpt-${CHECKPOINT_NUMBER}.data-00000-of-00001
  • model.ckpt-${CHECKPOINT_NUMBER}.index
  • model.ckpt-${CHECKPOINT_NUMBER}.meta

可以通过如下命令从checkpoints中提取模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Example Usage:
--------------
python export_inference_graph \
--input_type image_tensor \
--pipeline_config_path path/to/ssd_inception_v2.config \
--trained_checkpoint_prefix path/to/model.ckpt \
--output_directory path/to/exported_model_directory

-----
The expected output would be in the directory
path/to/exported_model_directory (which is created if it does not exist)
with contents:
- graph.pbtxt
- model.ckpt.data-00000-of-00001
- model.ckpt.info
- model.ckpt.meta
- frozen_inference_graph.pb
+ saved_model (a directory)
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
# From tensorflow/models/research/
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
python export_inference_graph.py \
--input_type image_tensor \
--pipeline_config_path training/ssd_mobilenet_v1_pets.config \
--trained_checkpoint_prefix training/model.ckpt-10116 \
--output_directory water_meter_panel
-----
INPUT_TYPE=image_tensor
PIPELINE_CONFIG_PATH="object_detection/training/pipeline.config"
TRAINED_CKPT_PREFIX=/home/ubuntu/1.objectDetection/training/model.ckpt-14450
EXPORT_DIR=/home/ubuntu/1.objectDetection/export

$ python object_detection/export_inference_graph.py \
--input_type=${INPUT_TYPE} \
--pipeline_config_path=${PIPELINE_CONFIG_PATH} \
--trained_checkpoint_prefix=${TRAINED_CKPT_PREFIX} \
--output_directory=${EXPORT_DIR}

-----
WARNING:tensorflow:From /home/gaoc/data/test/object_detect/models/research/object_detection/exporter.py:357: get_or_create_global_step (from tensorflow.contrib.framework.python.ops.variables) is deprecated and will be removed in a future version.
Instructions for updating:
Please switch to tf.train.get_or_create_global_step
2018-01-20 06:34:08.551446: I tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA
2018-01-20 06:34:14.056881: I tensorflow/core/grappler/devices.cc:51] Number of eligible GPUs (core count >= 8): 0
Converted 199 variables to const ops.

如果Exportor.py文件报bug,请参考https://github.com/tensorflow/models/issues/2861 修复

运行以上代码,将在water_meter_panel文件夹下生成模型所需的相关文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gaoc@DataScience:~/data/test/object_detect/models/research/object_detection$ ls -l water_meter_panel/
total 44820
-rw-r--r-- 1 gaoc root 77 Jan 20 06:34 checkpoint
-rw-r--r-- 1 gaoc root 22636802 Jan 20 06:34 frozen_inference_graph.pb
-rw-r--r-- 1 gaoc root 22174240 Jan 20 06:34 model.ckpt.data-00000-of-00001
-rw-r--r-- 1 gaoc root 8873 Jan 20 06:34 model.ckpt.index
-rw-r--r-- 1 gaoc root 1058139 Jan 20 06:34 model.ckpt.meta
drwxr-xr-x 3 gaoc root 4096 Jan 20 06:34 saved_model
water_meter_panel/
├── checkpoint
├── frozen_inference_graph.pb
├── model.ckpt.data-00000-of-00001
├── model.ckpt.index
├── model.ckpt.meta
└── saved_model
├── saved_model.pb
└── variables

2.5 测试

1
2
3
4
5
6
# From the tensorflow/models/research/ directory
python object_detection/eval.py \
--logtostderr \
--pipeline_config_path=${PATH_TO_YOUR_PIPELINE_CONFIG} \
--checkpoint_dir=${PATH_TO_TRAIN_DIR} \
--eval_dir=${PATH_TO_EVAL_DIR}

物体识别领域的算法性能评价指标多数选择AP和mAP(mean average precision),多个类别物体检测中,每一个类别都可以根据recall和precision绘制一条曲线,AP就是该曲线下的面积,mAP是多个类别AP的平均值

1
2
3
4
5
6
7
8
Average Precision (AP):
AP% AP at IoU=.50:.05:.95 (primary challenge metric) APIoU=.50% AP at IoU=.50 (PASCAL VOC metric) APIoU=.75% AP at IoU=.75 (strict metric)
AP Across Scales:
APsmall% AP for small objects: area < 322 APmedium% AP for medium objects: 322 < area < 962 APlarge% AP for large objects: area > 962
Average Recall (AR):
ARmax=1% AR given 1 detection per image ARmax=10% AR given 10 detections per image ARmax=100% AR given 100 detections per image
AR Across Scales:
ARsmall% AR for small objects: area < 322 ARmedium% AR for medium objects: 322 < area < 962 ARlarge% AR for large objects: area > 962

AP_MAP

从性能测试结果来看该模型已经具备96%的检测精度了,具备投入生产环境所需的性能。

预测结果见下图,准确率达到了99%以上。

detect_demo

3. 结论

  • 对于简单业务场景的物体识别类案例,可以通过迁移学习利用已有的成熟模型快速迭代生成新的特定模型;
  • 这种迁移的另一个优势是对小数据样本具备很好的适应能力,可以解决前期数据不足和数据质量差的问题;
  • 当然采用这种方式也同时存在一定的弊端,比如由于模型预训练参数较多,模型体积较大,模型的选取需要反复测试和优化。

3.1 经验总结

  1. 虽然Tensorflow Object Detection API提供了丰富的文档介绍相关工作流程,但由于技术、平台和软件版本本身更新较快,实践中还是或多或少会遇到不少问题,静下心来多翻翻Github的issues里一般都有别人的提问及解答;建议还是先根据文档跑通demo,熟悉相关工具和流程再将框架迁移到自己的数据集之上;

  2. 建议自己识别的项目文件单独建立一个数据准备文件夹进行数据准备和相关配置脚本的准备,不要跟Github克隆的Object Detection项目混在一起,不容易管理,也不利于重复利用;

  3. TFOD API提供的Tensorflow可视化相当完备,启动训练任务之后,一定要同步启动验证脚本,可以实时跟踪训练进程;

  4. 很容易疏忽的一个步骤是关于PYTHON PATH的处理,在执行相关API之前一定要记得EXPORT相关path,可以些一个bash文件,在执行命令的Terminal中source一下;常见错误如下:

    1
    ImportError: No module named ’object_detection’
  5. 充分利用GPU和CPU进行训练:

    1. 如果使用CPU训练,控制配置参数中num_examples为一个很小的值(5-10),这样将使用验证数据中的一部分进行验证而不是全部;

    2. 配置CUDA_VISIBLE_DEVICES环境变量,选择使用哪个GPU或CPU来分配内存资源:

      1
      2
      $ export CUDA_VISIBLE_DEVICES="0"
      ...
      1
      2
      $ export CUDA_VISIBLE_DEVICES="1"
      ... another scripts

      配置为空使用CPU

  6. 尽量使用最新版的Tensorflow,在撰写本文时已经是1.7了,当时做实验用的是1.4,复现的时候发现1.4版本已经抛错了…

参考

1. The TensorFlow Object Detection API is an open source framework built on top of TensorFlow that makes it easy to construct, train and deploy object detection models.
2. See MSCOCO evaluation protocol.
  1. Image classification with a pre-trained deep neural network
  2. How to train your own Object Detector with TensorFlow’s Object Detector API
  3. https://github.com/datitran/raccoon_dataset

物体识别技术之Faster R-CNN

摘要: 2015年提出的Faster R-CNN架构在基于机器视觉的物体识别领域占据重要的地位,从R-CNN到fast R-CNN再到faster R-CNN,乃至后续的Mask-R-CNN形成了一条完整的两步识别的物体识别技术生态。

概述

物体识别技术一直是机器视觉中业务场景最丰富,关注度最高的一个类别。将花几期来分别对主流的物体识别技术如Faster RCNN,SSD,YOLO,Mask-RCNN进行整理和分析,并利用实践的方式进行强化。

在R-CNN,Fast R-CNN,Faster R-CNN中,物体识别被分为两个步骤实现(与SSD、YOLO的主要差异):候选区域选择和基于深度网络的物体识别。

传统的物体识别算法

传统的物体识别技术采用的滑动窗口+图像金字塔+分类器的算法,可以参见前述博文在基于机器视觉技术的品牌LOGO检测中做的实际测试,原理易于理解,但效率较低,很难达到实时处理的需求:

  1. 速度慢,效率低:需要利用滑动窗口遍历图像的不同位置;
  2. 受图像畸变影响严重:由于CNN的输入必须是固定大小的图像,所以限制了检测目标的长宽比例,比如这种方法不能同时检测矮胖对象和长瘦对象;
  3. 错误率高,没法识别图像的全局特征,每个窗口只能看到局部特征,所以检测精度也受到了比较大的影响。

物体识别精度的衡量指标

  • IoU(Intersection over Union)

    IoU

  • mAP(Mean Average Precision)

    • 所有分类的IoU均值;

参考论文

  1. R-CNN: Rich feature hierarchies for accurate object detection and semantic segmentation
  2. Faster R-CNN

R-CNN

论文: Rich feature hierarchies for accurate object detection and semantic segmentation,2013,Girshick

问题:解决目标检测网络

R-CNN的实现包括如下图所示的4个主要步骤:

  1. 接受输入图像;
  2. 利用Selective Search算法从图像中抽取大约2000个候选区域;
  3. 对每个候选区域利用预训练的CNN进行特征抽取(迁移学习);
  4. 对每个特征抽取区域利用线性SVM进行分类

R-CNN

论文的主要贡献:

  1. 使用Selective Search替代了特征金字塔和滑动窗口实现的兴趣区域选择,提升了效率;
  2. 利用预训练的神经网络进行特征提取替代了手工特征如HOG的特征提取方法,正是由于CNN学习的特征具备的鲁棒性大大提供了系统的泛化性能

仍然存在的问题:

  1. 识别慢,效率低;
  2. 不是一个端到端的解决方案

Selective Search算法

论文:Selective Search for Object Recognition,2012,J.R.R.Uijings

之前很多算法都是基于蛮力搜索(Exhaustive Search),对整张图片进行扫描,或者是采用动态窗口的方法,这种方法耗时严重,操作麻烦。J.R.R提出的选择性搜索的方法,在识别前期在整张图片中生成1~3K个proposal的方法,再对每个proposal进行处理。

Selective Search [4], one of the most popular methods, greedily merges superpixels based on engineered low-level features.

缺点:效率低,计算量大,使用1个CPU处理一张图片,需要2s1

Fast R-CNN

问题提出:解决端到端训练的问题,提出了Region of Interest(ROI)Pooling

跟R-CNN中使用深度CNN的方式不同,Fast R-CNN中首先将CNN应用到整个图像中进行特征提取,利用一个固定窗口在抽取特征上滑动,分别进行分类预测和回归预测。Fast R-CNN的主要处理流程包括:

  1. 输入图像和标定的识别框信息;
  2. 利用深度卷积神经网络抽取图像特征;
  3. 利用ROI pooling获取ROI特征向量;
  4. 利用两个全联通层进行分类和回归预测

Fast RCNN

端到端的训练过程源于提出了多任务损失函数,将分类问题和回归问题整合在一起,打通了梯度的更新路径,下图描述了Fast R-CNN的训练和测试过程:

fast R-CNN 训练与测试

缺点:仍然没有摆脱Selective Search算法在推理阶段进行候选区域生成。

Faster R-CNN

论文:Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks, 2015, Shaoqing Ren, Kaiming He, Ross Girshick, Jian Sun

实现:Github上作者提供的Python实现

商业实现:Pinterests2

问题提出:在基于候选区域选择的CNN(region-based CNN)物体识别网络中,候选区域选择的效率成为了整个系统的瓶颈;Faster R-CNN中提出了Region Proposal Netwrok与物体识别网络共享网络参数(替代了Fast R-CNN中的Selective Search算法),降低了候选区域选择的时间代价。

  • 基础网络(Base Network):特征抽取(迁移学习),抽取的特征将同时应用于RPN和RoIP阶段
  • RPN:候选区域选择(利用了网络的Attention机制),用于发掘图像中潜在的可能存在物体的区域
  • RoIP:兴趣区域特征提取
  • R-CNN:分类预测和候选框回归

fasterrcnn-architecture.b9035cba

使用一块GPU,性能大概在7-10 FPS

基础网络

基础网络的主要作用是利用迁移学习完成原始图像的特征抽取,在论文中使用了 在ImageNet预训练的ZFVGG来完成这一任务。当然根据物体识别任务的不同应用场景可以在模型精度和推理时间上进行折中选择 MobileNet, ResNet-152, DenseNet

文献中,Faster R-CNN的基础网络在使用VGG作为特征提取网络时,使用conv5/conv5_1层的输出特征;

目前ResNet在很多情况下已经替代了VGG16作为特征提取网络;

为了保证网络是全卷积神经网络架构,需要把全连接层剔除,保证可以输入任意维度的输入图像

base-network

Anchor Box

替代传统算法的特征金字塔或filter金字塔

一张图像中被识别目标形状大小各异,这也是在原始算法中加入特征金子塔来对原始图像进行多个维度特征变换的原因。

Anchor Box也是为了解决上述问题,我们可以不改变图像的形状,通过改变预测每个区域物体的“窗口”来框出不同大小的物体。首先在原始图像中均匀的选取一些Anchor Box中心点,然后在每个中心点上预制多个Anchor Box。

anchors-centers.141181d6

在Faster R-CNN是使用3个不同形状(1:1, 1:2,2:1)和3个不同大小(128x128,256x256,512x512,按照原图尺寸生成)进行组合共计3x3=9种不同的Anchor box。

使用VGG16做特征提取的情况下,一张输入图片总共可以刻画为512个窗口区域,生成512x(4+2)x9个输出参数。

anchors-progress.119e1e92

由于直接预测bouding box难以实现,作者将问题转变为预测预测值与真实值之间的偏移,将问题转变为四个偏移值的预测问题。

Region Proposal Network(RPN)

RPN的主要目的是对每个区域是否可能有物体进行打分,基于打分值决定是否进行下一步的分类任务。在基础网络抽取的特征图上使用一个3x3的滑动窗口(512个卷积核),每个滑动窗口的中心点位置为上述Achor Box的中心点区域,在每个滑动窗口区域,将得到两个1x1卷积网络输出,分别为2k的前景/背景预测(该区域是否存在可被预测物体,分类问题)以及4k的位置信息预测(回归问题),四个值分别是

k是Anchor Box的数目

我们将从候选区域中选择打分较高的前N个进行下一轮分析,如果物体打分足够高,下一步将进行非极大抑制和区域选择,如果打分值很低将抛弃这些区域

rpn-conv-layers.63c5bf86

目标和损失函数

The RPN does two different type of predictions: the binary classification and the bounding box regression adjustment.

For training, we take all the anchors and put them into two different categories. Those that overlap a ground-truth object with an Intersection over Union (IoU) bigger than 0.5 are considered “foreground” and those that don’t overlap any ground truth object or have less than 0.1 IoU with ground-truth objects are considered “background”.

其中分类的损失函数为:

$p_i$为第$i$个参考框是物体的预测概率值,$p_i^*$为实际值,如果anchor是物体的话该值为1,否则为0。

回归损失函数为:

其中R为smooth L1平滑方程:

$t_i$与 $ t_i^ $ 分别对应四个偏差值。$ t_i $ 是预测框与anchor之间的偏差,$ t_i^ $ 是ground truth与anchor之间的偏差

参数选择

参数选择与其说是一个技术活倒不如认为是一个经验活,是通过大量实践验证出来的最佳实践,所以有必要分析整理每篇文章对参数选择和优化的技巧。

  • 非极大抑制的IoU阈值一般使用0.6
  • 论文中关于候选区域选择了N=2000,但一般而言比这个小的数目也能取得不错的效果,比如50,100 …

Region of Interest(ROI)Pooling

ROI阶段的主要作用为使用矩阵操作(Array Slicing)从特征图中捕获N个兴趣区域,并降采样到7x7xD的尺寸,服务于接下来的全联同网络。

roi

Region-based CNN

使用两个不同的全联通网络(Fully-Connected Network,FC):

  • A fully-connected layer with N+1units where N is the total number of classes and that extra one is for the background class.
  • A fully-connected layer with 4N units. We want to have a regression prediction, thus we need $\Delta{center{x}}$, $\Delta{center{y}}$, $\Delta{width}$, $\Delta{height}$ for each of the N possible classes.

rcnn-architecture.6732b9bd

在这个步骤中同样由两个损失函数构成:Categorical cross-entropy分类损失和Smooth L1回归损失

网络训练

  • 实验表明:联合训练(使用weighted sum)优于单独训练两个网络;
    • 将每个阶段的4个损失函数(其中RPN阶段2个,R-CNN阶段2个)组合在一起,并赋不同的权重,分类损失需要获得比回归损失更多的权重;
    • 使用L2正则损失
  • 是否单独训练基础网络取悦于目标与预训练网络的差异,这个跟迁移学习类似;
  • 使用带动量的随机梯度下降(SGD with momentum),其中monmentum=0.9,初始学习率lr=0.001, 50k步之后,lr调整为0.0001

实践时间

数据集:LISA交通标志数据库

  • 下载地址:http://cvrr.ucsd.edu/LISA/lisa-traffic-sign-dataset.html
  • 47 US sign types
  • 7855 annotations on 6610 frames.
  • Sign sizes from 6x6 to 167x168 pixels.
  • Images obtained from different cameras. Image sizes vary from 640x480 to 1024x522 pixels.
  • Some images in color and some in grayscale.
  • Full version of the dataset includes videos for all annotated signs.
  • Each sign is annotated with sign type, position, size, occluded (yes/no), on side road (yes/no).
  • All annotations are save in plain text .csv-files.
  • ~7.7GB大小

参考

1. Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks, 2015, Shaoqing Ren, Kaiming He, Ross Girshick, Jian Sun
2. D. Kislyuk, Y. Liu, D. Liu, E. Tzeng, and Y. Jing, “Human curation and convnets: Powering item-to-item recommendations on pinterest,” arXiv:1511.04003, 2015.
3. Faster R-CNN: Down the rabbit hole of modern object detection

利用迁移学习实现车辆识别

摘要: 利用迁移学习技术训练识别汽车厂商和款式的模型。

@[toc]

概述

数据集: Stanford Cars Dataset

car

数据集特点:

  1. 存在明显的数据不平衡问题;
  2. 每个分类图像数目过少,无法达到准确预测分类目标的基准;

针对数据集的特点,利用迁移学习Fine-Tune来训练一个在ImageNet上预训练的模型是一个不错的方式,下面我们将从数据的准备开始一步步得完成模型的训练任务。

数据准备

在这一部分中,我们将读入原始数据,进行基本的数据处理,然后对数据进行统一存储,一般面对大规模的数据集可以选用HDF5或MXNet的.LST格式进行存储。

通过这种方式进行存储可以解决每张图片读取都要产生一次IO带来的访问时延,同时可以利用存储系统连续读的方式,直接对大规模数据集进行切片操作。

配置信息

为了配合后续处理流程方便,新建一个car.config的配置文件,用于对相关配置信息的存储:

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
from os import path

# define the base path to the cars dataset
BASE_PATH = "Path-to-car-dataset"

# based on the base path, derive the images path and meta file path
IMAGES_PATH = path.sep.join([BASE_PATH, "car_ims"])
LABELS_PATH = path.sep.join([BASE_PATH, "complete_dataset.csv"])

#define path for HDF5
TRAIN_HDF5 = path.sep.join([MX_OUTPUT, "hdf5/train.hdf5"])
VAL_HDF5 = path.sep.join([MX_OUTPUT, "hdf5/val.hdf5"])
TEST_HDF5 = path.sep.join([MX_OUTPUT, "hdf5/test.hdf5"])

#define path for storing Mean R G B data
DATASET_MEAN = path.sep.join([BASE_PATH, "output/car_mean.json"])

# define the path to the output directory used for storing plots,
# classification reports, etc.
OUTPUT_PATH = "output"
MODEL_PATH = path.sep.join([OUTPUT_PATH,"inceptionv3_stanfordcar.hdf5"])
FIG_PATH = path.sep.join([OUTPUT_PATH,"inceptionv3_stanfordcar.png"])
JSON_PATH = path.sep.join([OUTPUT_PATH,"inceptionv3_stanfordcar.json"])

# define the path to the label encoder
LABEL_ENCODER_PATH = path.sep.join([BASE_PATH, "output/le.cpickle"])

# define the percentage of validation and testing images relative
# to the number of training images
NUM_CLASSES = 164
NUM_VAL_IMAGES = 0.15
NUM_TEST_IMAGES = 0.15

# define the batch size
BATCH_SIZE = 64

配置文件中包括HDF5文件的存放位置描述,原始数据和数据描述文件路径,RGB均值存储位置,训练过程中输出的图像和日志存储位置等信息。

数据概览

  1. 我们先将数据描述文件complete_dataset.csv导入,了解下数据格式:
1
2
3
4
import pandas as pd
# loading image paths and labels
df = pd.read_csv(config.LABELS_PATH)
df.head()
id Image FileName Make Model Vechicle Typle Year
0 car_ims/000090.jpg Acura RL Sedan 2012
1 car_ims/000091.jpg Acura RL Sedan 2012
2 car_ims/000092.jpg Acura RL Sedan 2012
3 car_ims/000093.jpg Acura RL Sedan 2012
  1. 遍历文件列表,将文件地址和样本标签分别进行存储,在本实验中值使用了制造商和款式两种特征,所以构成分类总共有164个:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import os
    from sklearn.preprocessing import LabelEncoder
    trainPaths = []
    trainLabels = []

    for id,name in enumerate(df["Image Filename"]):
    trainPaths.append(os.sep.join([config.IMAGES_PATH,name]))
    trainLabels.append("{}:{}".format(df.iloc[id]["Make"], df.iloc[id]["Model"]))
    #Encoding labels to num
    le = LabelEncoder()
    trainLabels = le.fit_transform(trainLabels)
  2. 按照70%,15%,15%切分训练集、验证集和测试集:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from sklearn.model_selection import train_test_split

    numVal = int(len(trainPaths)*0.15)
    numTest = int(len(trainPaths)*0.15)

    # perform sampling from the training set to construct a a validation set
    split = train_test_split(trainPaths, trainLabels, test_size=numVal,
    stratify=trainLabels)
    (trainPaths, valPaths, trainLabels, valLabels) = split

    # perform stratified sampling from the training set to construct a testing set
    split = train_test_split(trainPaths, trainLabels, test_size=numTest,
    stratify=trainLabels)
    (trainPaths, testPaths, trainLabels, testLabels) = split
  3. 初始化相关配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # initialize the lists of RGB channel averages
    (R, G, B) = ([], [], [])

    # construct a list pairing the training, validation, and testing
    # image paths along with their corresponding labels and output list
    # files
    datasets = [
    ("train", trainPaths, trainLabels, config.TRAIN_HDF5),
    ("val", valPaths, valLabels, config.VAL_HDF5),
    ("test", testPaths, testLabels, config.TEST_HDF5)]
  4. 遍历数据集并存储至HDF5文件:

    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
    import HDF5DatasetWriter
    import AspectAwarePreprocessor
    import progressbar

    #resize images to (256,256,3)
    aap = AspectAwarePreprocessor(256,256)

    # loop over the dataset tuples
    for (dType, paths, labels, outputPath) in datasets:
    # create HDF5 writer
    print("[INFO] building {}...".format(outputPath))
    writer = HDF5DatasetWriter((len(paths), 256, 256, 3), outputPath)

    # initialize the progress bar
    widgets = ["Building Dataset: ", progressbar.Percentage(), " ",
    progressbar.Bar(), " ", progressbar.ETA()]
    pbar = progressbar.ProgressBar(maxval=len(paths),
    widgets=widgets).start()

    # loop over the image paths
    for (i, (path, label)) in enumerate(zip(paths, labels)):
    # load the image from disk
    try:
    image = cv2.imread(path)
    image = aap.preprocess(image)
    #print(image.shape)
    # if we are building the training dataset, then compute the
    # mean of each channel in the image, then update the respective lists
    if dType == "train":
    (b, g, r) = cv2.mean(image)[:3]
    R.append(r)
    G.append(g)
    B.append(b)

    # add the image and label to the HDF5 dataset
    writer.add([image], [label])
    pbar.update(i)
    except:
    print(path)
    print(label)
    break

    # close the HDF5 writer
    pbar.finish()
    writer.close()

    我们首先统一将文件调整到(256,256,3)大小,再进行存储,所以在读取文件之后进行了简单的预处理。

  5. 将RGB均值存储至单独文件

    1
    2
    3
    4
    5
    6
    7
    8
    import pandas as pd
    import json
    # construct a dictionary of averages, then serialize the means to a JSON file
    print("[INFO] serializing means...")
    D = {"R": np.mean(R), "G": np.mean(G), "B": np.mean(B)}
    f = open(config.DATASET_MEAN, "w")
    f.write(json.dumps(D))
    f.close()

至此,我们完成了将图像文件分为三个类别并分别存到了三个HDF5文件之中。

关于HDF5文件存储相关内容,详见后续推出的预处理博客~~

如果使用MXNET的list和rec来构建数据存储集合,在上述步骤3的基础上,按如下步骤:

  1. 构建数据集列表文件’.list’文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # construct a list pairing the training, validation, and testing
    # image paths along with their corresponding labels and output list
    # files
    datasets = [
    ("train", trainPaths, trainLabels, config.TRAIN_MX_LIST),
    ("val", valPaths, valLabels, config.VAL_MX_LIST),
    ("test", testPaths, testLabels, config.TEST_MX_LIST)]

    # loop over the dataset tuples
    for (dType, paths, labels, outputPath) in datasets:
    # open the output file for writing
    print("[INFO] building {}...".format(outputPath))

    f = open(outputPath, "w")

    # loop over each of the individual images + labels
    for (i, (path, label)) in enumerate(zip(paths, labels)):
    # write the image index, label, and output path to file
    row = "\t".join([str(i), str(label), path])
    f.write("{}\n".format(row))

    # close the output file
    f.close()
  2. 将Label名称序列化存储,便于后续调用:

    1
    2
    3
    f = open(config.LABEL_ENCODER_PATH, "wb")
    f.write(pickle.dumps(le))
    f.close()

    3.利用MXNet工具im2rec创建记录文件

    1
    2
    3
    4
    5
    $ /dsvm/tools/mxnet/bin/im2rec ./raid/datasets/cars/lists/train.lst "" ./raid/datasets/cars/rec/train.rec resize=256 encoding='.jpg' quality=100

    $ /dsvm/tools/mxnet/bin/im2rec ./raid/datasets/cars/lists/test.lst "" ./raid/datasets/cars/rec/test.rec resize=256 encoding='.jpg' quality=100

    $ /dsvm/tools/mxnet/bin/im2rec ./raid/datasets/cars/lists/val.lst "" ./raid/datasets/cars/rec/val.rec resize=256 encoding='.jpg' quality=100

训练迁移学习网络

在迁移学习的模型选择上我们选择了基于Keras提供的InceptionV3,可通过Keras官方文档了解更多使用说明。下表列出了在keras中各模型的表现:

模型 大小 Top1准确率 Top5准确率 参数数目 深度
Xception 88MB 0.790 0.945 22,910,480 126
VGG16 528MB 0.715 0.901 138,357,544 23
VGG19 549MB 0.727 0.910 143,667,240 26
ResNet50 99MB 0.759 0.929 25,636,712 168
InceptionV3 92MB 0.788 0.944 23,851,784 159
IncetionResNetV2 215MB 0.804 0.953 55,873,736 572
MobileNet 17MB 0.665 0.871 4,253,864 88

数据读入

输入读入过程主要包括几个关键任务:读取RGB均值文件,对原始数据进行预处理:图像扣取、数据增强、去通道均值、矩阵化等。

这块内容不是本篇重点,只能挖坑留给后续更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# construct the training image generator for data augmentation
aug = ImageDataGenerator(rotation_range=20, zoom_range=0.15,
width_shift_range=0.2, height_shift_range=0.2, shear_range=0.15,
horizontal_flip=True, fill_mode="nearest")

# load the RGB means for the training set
means = json.loads(open(config.DATASET_MEAN).read())

# initialize the image preprocessors
sp = SimplePreprocessor(224, 224)
pp = PatchPreprocessor(224, 224)
mp = MeanPreprocessor(means["R"], means["G"], means["B"])
iap = ImageToArrayPreprocessor()

# initialize the training and validation dataset generators
trainGen = HDF5DatasetGenerator(config.TRAIN_HDF5, 64, aug=aug,
preprocessors=[pp, mp, iap], classes=config.NUM_CLASSES)
valGen = HDF5DatasetGenerator(config.VAL_HDF5, 64,
preprocessors=[sp, mp, iap], classes=config.NUM_CLASSES)

模型设计

模型设计过程参考了Keras官方文档给出的演示,导入没有top的预训练InceptionV3模型,

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
from keras.applications.inception_v3 import InceptionV3
from keras.preprocessing import image
from keras.models import Model
from keras.layers import Dense, GlobalAveragePooling2D
from keras import backend as K

# load the Inception network, ensuring the head FC layer sets are left off
baseModel = InceptionV3(weights="imagenet", include_top=False,
input_tensor=Input(shape=(224, 224, 3)))
# initialize the new head of the network, a set of FC layers
# followed by a softmax classifier
x = baseModel.output
x = GlobalAveragePooling2D()(x)
x = Dense(1024, activation='relu')(x)
headModel = Dense(config.NUM_CLASSES, activation='softmax')(x)

model = Model(inputs=baseModel.input, outputs=headModel)

# loop over all layers in the base model and freeze them so they
# will *not* be updated during the training process
for layer in baseModel.layers:
layer.trainable = False

# compile our model (this needs to be done after our setting our
# layers to being non-trainable
print("[INFO] compiling model...")
opt = SGD(lr=0.005,momentum=0.9)
model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])

特别注意在迁移学习中,由于新添加增的初始权重是随机生成的,而前面大量网络参数并frozen之后不再发生变化,所以需要一个预测的过程来学习参数到一定水平,需要控制学习率在一个比较小的范围。

这个过程可能需要反复尝试试错。

训练过程优化

训练过程参考了《Deep Learning for Computer Vison with Python》作者给出的Ctrl+C训练方法,可以随时保存训练现场,调整训练率继续进行训练。

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
import argparse
import json
import os
import logging
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-c", "--checkpoints", required=True, help="path to output checkpoint directory")
ap.add_argument("-m", "--model", type=str, help="path to *specific* model checkpoint to load")
ap.add_argument("-s", "--start-epoch", type=int, default=0, help="epoch to restart training at")
args = vars(ap.parse_args())

# set the logging level and output file
logging.basicConfig(level=logging.DEBUG,
filename="training_{}.log".format(args["start_epoch"]), filemode="w")

if args["model"] is None:
# load the VGG16 network, ensuring the head FC layer sets are left off

...
## 实现上一步骤的预训练模型定义和模型预热

else:
print("[INFO] loading {}...".format(args["model"]))
model = load_model(args["model"])
# update the learning rate
print("[INFO] old learning rate: {}".format(K.get_value(model.optimizer.lr)))
K.set_value(model.optimizer.lr, 1e-3)
print("[INFO] new learning rate: {}".format(K.get_value(model.optimizer.lr)))

# construct the set of callbacks
callbacks = [
EpochCheckpoint(args["checkpoints"], every=5,
startAt=args["start_epoch"]),
TrainingMonitor(config.FIG_PATH, jsonPath=config.JSON_PATH,
startAt=args["start_epoch"])]

# train the network
print("[INFO] training network...")
model.fit_generator(
trainGen.generator(),
steps_per_epoch=trainGen.numImages // config.BATCH_SIZE,
validation_data=valGen.generator(),
validation_steps=valGen.numImages // config.BATCH_SIZE,
epochs=100,
max_queue_size=config.BATCH_SIZE * 2,
callbacks=callbacks, verbose=1)

参考

  1. deep learning for computer vision with python

基于机器视觉技术的品牌LOGO检测

利用Flickr LOGO数据集训练一个检测品牌LOGO的网络,对机器视觉的物体识别技术进行验证。

@[toc]

概述

最近在做一个利用机器视觉技术进行超市物品检点的项目调研分析,需要先寻找一个可行的技术方案验证可行性,Flickr提供的LOGO数据集是一个很好的品牌LOGO识别例子,本文记录利用Flickr LOGO数据集训练一个物体识别的深度神经网络过程。

b

数据集

Flickr LOGO数据集提供了三种不同类型的LOGO数据集集合,分别为Flickr Logos 27 datasetDatasets: FlickrLogos-32以及Datasets: FlickrLogos-47。我们先来看一下每种数据集的组成及数据结构:

  1. Flickr Logos 27 dataset

    • 训练集包含27个分类的810张标记照片,每个分类30张照片

    • 分散集包含4207张logo图片

    • 测试集有270张照片,每个分类5张照片,另外有135张分类外照片集

    • 27个分类包括:Adidas, Apple, BMW, Citroen, Coca Cola, DHL, Fedex, Ferrari, Ford, Google, Heineken, HP, McDonalds, Mini, Nbc, Nike, Pepsi, Porsche, Puma, Red Bull, Sprite, Starbucks, Intel, Texaco, Unisef, Vodafone and Yahoo.

    • 下载地址:下载

    • 数据格式:下载文件夹中提供一个txt文件用于描述每个文件中LOGO的分类和位置信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #  FileName ClassName subset   Coordinates(x1 y1 x2 y2)
      4763210295.jpg Adidas 1 91 288 125 306
      4763210295.jpg Adidas 1 182 63 229 94
      4763210295.jpg Adidas 1 192 291 225 306
      4763210295.jpg Adidas 1 285 61 317 79
      4763210295.jpg Adidas 1 285 298 324 329
      4763210295.jpg Adidas 1 377 292 421 324
      4763210295.jpg Adidas 1 383 55 416 76
      1230939811.jpg Adidas 2 129 326 257 423
      1230939811.jpg Adidas 2 137 336 243 395

      dataset1_bboxes

  2. Flickr Logos 32/47 dataset

    FlickrLogos-32 was designed for logo retrieval and multi-class logo detection and object recognition. However, the annotations for object detection were often incomplete,since only the most prominent logo instances were labelled.

    FlickrLogos-47 uses the same image corpus as FlickrLogos-32 but has been re-annotated specifically for the task of object detection and recognition.

    2.1 Flickr Logos-32

Partition Description Images #Images
P1 (training set) Hand-picked images 10 per class 320 images
P2 (validation set) Images showing at least a single logo under various views 30 per class + 3000 non-logo images 3960 images
P3 (test set = query set) Images showing at least a single logo under various views 30 per class + 3000 non-logo images 3960 images
/ / / 8240 images
2.2 FlickrLogos-47

小结

  • Flickr Logo数据集虽然类别数目众多,但具体到每个分类提供的样本数目有限,在数据预处理环节需要配合数据增强手段来扩充数据集的数目;
  • 另外也可以仿照车牌识别的方法,将扣取的LOGO图像添加到不同背景噪声的图像中,生成多种训练数据;
  • 由于LOGO图像包含图像特征有限,同时提供小样本数据,通过迁移学习的方案利用ImageNet训练好的模型进行迁移学习是一种很好的方式,本文将对这种方式进行讨论及实现;
  • 三种数据集面向不同的功能也设计需求,从图像质量上来看Flickr-47质量相对较好,同时在32分类和47分类中提供了对图像语义分割的标定数据;

在概览过任务数据集之后,我们将按照深度学习业务处理流程,逐步进行数据的预处理、模型准备、训练和验证等工作。为简化问题处理难度,我们使用Flickr Logo -27来进行本次实验。

数据准备

数据准备环节主要使用如下基本的工具和库文件:

1
2
3
4
5
6
7
import numpy as np
import pandas as pd
import os
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import cv2
import imutils

其中,

  • numpy用来做基本的矩阵处理;
  • pandas用于读取和分析数据描述文件;
  • matplotlib用于辅助显示预处理结果;
  • cv2是opencv的python封装,进行图像读取、图像分析等操作;
  • imutils是一个很好用的图像处理库,可以满足基本的图像处理需求

首先我们先查看从flickr-27上下载的文件flickr_logos_27_dataset_training_set_annotation.txt来了解基本的图像数据信息和分类信息:

1
2
3
4
df=pd.read_csv("./flickr_logos_27_dataset/flickr_logos_27_dataset_training_set_annotation.txt",sep=" ", header=None)
df.drop(df.columns[-1],axis=1, inplace=True)
df.columns=["Name","labels","subset","x1","y1","x2","y2"]
df.head()
1
2
3
4
5
6
7
output:>>>>
Name labels subset x1 y1 x2 y2
0 144503924.jpg Adidas 1 38 12 234 142
1 2451569770.jpg Adidas 1 242 208 413 331
2 390321909.jpg Adidas 1 13 5 89 60
3 4761260517.jpg Adidas 1 43 122 358 354
4 4763210295.jpg Adidas 1 83 63 130 93

在描述文件中总共提供了4536条记录,而实际提供的图像文件只有1000多张,这说明很多文件包括不止一个LOGO。

1
2
len(df)
>>>: 4536

我们可以利用pandas对文件进行一个简单的shuffle处理,便于快速切分成训练集和测试集:

1
2
# shuffle the datasets
df = df.sample(frac=1).reset_index(drop=True)

为了快速查看描述文件提供的标记信息在图像中的显示效果,我们写一个函数来查看一下LOGO标记信息的效果:

1
2
3
4
5
6
7
8
9
10
11
12
def show_image(id):
fig = plt.figure()
image = os.path.join("./flickr_logos_27_dataset/flickr_logos_27_dataset_images/",df.loc[id]["Name"])
image = cv2.imread(image)
plt.figure(8)
plt.imshow(image)
currentAxis=plt.gca()
rect=patches.Rectangle((df["x1"].iloc[id], df["y1"].iloc[id]),
df["x2"].iloc[id]-df["x1"].iloc[id],
df["y2"].iloc[id]-df["y1"].iloc[id],
linewidth=2,edgecolor='r',facecolor='none')
currentAxis.add_patch(rect)

其中用到了plt.gca()和matplotlib的patches函数用于图像的叠加显示,当然也可以直接调用cv2.rectangle函数

随机查看一个标记在图像中的显示效果

1
2
3
import random
id = random.randint(0,len(df))
show_image(id)

BMW_sample

下面需要写一个抠图程序,把所有LOGO从原始图像中扣取出来,形成训练用数据集,在保存图像之前进行图像简单的预处理和调整形状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def crop_img(id):
image = os.path.join("./flickr_logos_27_dataset/flickr_logos_27_dataset_images/",df.loc[id]["Name"])
image = cv2.imread(image)
crop_image = image[df["y1"].iloc[id]:df["y2"].iloc[id],df["x1"].iloc[id]:df["x2"].iloc[id]]
return crop_image

WIDTH = 64
HEIGHT = 64

for id, name in enumerate(df["Name"]):
cropped_image = crop_img(id)
try:
resized_image = cv2.resize(cropped_image,
(WIDTH,HEIGHT),interpolation=cv2.INTER_CUBIC)
except:
print(id)
continue
image_name = str(id)+"_"+df.iloc[id]["labels"]+".jpg"
cv2.imwrite(os.path.join("./flickr_logos_27_dataset/cropped/",image_name),resized_image)

在图像扣取过程中,有几点需要注意:

  1. 由于描述问题提供的信息本身的问题,有一些异常数据需要剔除,比如有5条记录提供的x1=x2,或y1=y2,即在原始图像上没有进行标记;
  2. 图像缩放其实不应该采用这种傻瓜的压缩方式,应该尽量控制长宽比,保证不产生明显的形变;

处理完成之后,扣取图像将在cropped文件夹中以{id}_{label}.jpg的文件名存储。

图像扣取之后,通过人工核对,我们发现仍然存在一些明显有问题的图像,比如多张puma的图像,其实存在明显的标记问题,需要从数据集中剔除:

puma_error_img

数据集切分

我们将扣取数据读入进行简单预处理和数据切分:

1
2
3
4
5
6
7
8
9
10
data = []
labels = []
for img in os.listdir("./flickr_logos_27_dataset/cropped/"):
img_file = cv2.imread(os.path.join("./flickr_logos_27_dataset/cropped/",img))
data.append(img_file)
labels.append(img.split("_")[1].split(".")[0])
data = np.stack(data)
labels = np.stack(labels)

data = data/255

将标签数据转变成OneHot矩阵:

1
2
3
from sklearn.preprocessing import LabelBinarizer
le = LabelBinarizer()
labels = le.fit_transform(labels)

切分数据集

1
X,testX,y,testy = train_test_split(data, labels,test_size=0.1,stratify=labels,random_state=42 )

数据增强

数据增强是图像处理中经常采用的一种数据处理方式,由于涉及内容较多,在本篇实战中不单独展开,仅把利用Keras数据增强工具ImageDataGenerator的方法提供一下:

1
2
3
4
5
6
7
8
from keras.preprocessing.image import ImageDataGenerator 
# construct the training image generator for data augmentation
aug = ImageDataGenerator(rotation_range=18, zoom_range=0.15,
width_shift_range=0.2, height_shift_range=0.2, shear_range=0.15,
horizontal_flip=True, fill_mode="nearest")

gen_flow=aug.flow(X, y,batch_size=64,seed=0)
validation=aug.flow(testX,testy,batch_size=32,seed=0)

模型定义

根据前文对数据的分析,我们分别采取两种方式设计网络模型:从头训练一个深度卷积神经网络和利用迁移学习Fine-Tune一个满足需求的网络模型。

从头训练一个网络模型

由于问题本质是一个物体识别任务,所以在实现上应该包括图像分类和定位的回归两个子任务,我们可以简化问题通过一个滑动窗口来对输入图像进行扫描,然后针对每个扫描窗口进行图像分类。

当然实际过程中,问题要远比这复杂,很难选择合适的滑动窗口大小适用现实图像的需求,所以在主流的物体识别模型中一般都采用多种不同大小的Anchor box来回归图像的位置。

由于LOGO每张图像包含特征有限,我们在本次实验中利用LeNet的架构,设计了一个简单的卷积网络模型如下图所示:

model

模型主体利用三个CONV => RELU => POOL结构来抽取图像特征,最后利用全联通网络+Softmax分类器来获得最终27类分类结果。

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
from keras.models import Sequential
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers.core import Activation
from keras.layers.core import Flatten
from keras.layers.core import Dense
from keras import backend as K


model = Sequential()
inputShape = (HEIGHT, WIDTH, 3)
# first set of CONV => RELU => POOL layers
model.add(Conv2D(16, (3, 3), padding="same",input_shape=inputShape))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

# second set of CONV => RELU => POOL layers
model.add(Conv2D(32, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

# third set of CONV => RELU => POOL layers
model.add(Conv2D(64, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

# first (and only) set of FC => RELU layers
model.add(Flatten())
model.add(Dense(500))
model.add(Activation("relu"))
model.add(Dropout(0.25))
# softmax classifier
model.add(Dense(len(CLASSNAME)))
model.add(Activation("softmax"))

定义目标优化函数:

1
2
3
from keras.optimizers import Adam,SGD,RMSprop
opt = RMSprop(lr=0.001, rho=0.9)
model.compile(loss="categorical_crossentropy", optimizer=opt,metrics=["accuracy"])

迭代训练100个epoch:

1
2
3
4
5
6
7
history=model.fit_generator(
gen_flow,
steps_per_epoch=len(X) // 32,
validation_data=aug.flow(testX,testy,batch_size=32,seed=0),
validation_steps=len(testX) // 32,
epochs=100,
verbose=1)

100轮之后,验证集达到了99.89%的准确率,基本满足了要求,训练过程中训练数据和验证数据的准确率及Loss变化详见下图:

accuracy_curve

loss_curve

测试

1
2
3
4
5
6
7
8
9
10
11
plt.figure(figsize = (15,40))
for i,test_img in enumerate(os.listdir("./test")):
img = cv2.imread(os.path.join("./test",test_img))
img = cv2.resize(img, (WIDTH,HEIGHT),interpolation=cv2.INTER_CUBIC)
img = np.expand_dims(img,axis=0)
result = model.predict(img)
result = le.inverse_transform(result)
plt.subplot(8,4, i+1)
img = cv2.cvtColor(img[0], cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.title('pred:' + str(result[0]))

test_result

设计滑动窗口和特征金字塔

其中滑动窗口用来遍历图像,特征金字塔用于实现图像的多尺度变换,保证多种不同大小的LOGO都可以被准确识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def sliding_window(image, step, ws):
# slide a window across the image
for y in range(0, image.shape[0] - ws[1], step):
for x in range(0, image.shape[1] - ws[0], step):
# yield the current window
yield (x, y, image[y:y + ws[1], x:x + ws[0]])

def image_pyramid(image, scale=1.5, minSize=(64, 64)):
# yield the original image
yield image

# keep looping over the image pyramid
while True:
# compute the dimensions of the next image in the pyramid
w = int(image.shape[1] / scale)
image = imutils.resize(image, width=w)

# if the resized image does not meet the supplied minimum
# size, then stop constructing the pyramid
if image.shape[0] < minSize[1] or image.shape[1] < minSize[0]:
break

# yield the next image in the pyramid
yield image

特征金字塔

为了检测不同尺度的目标,依次将原图按比例缩放并送入网络。缺点是需要多次resize图像,繁琐耗时。

我们定义了输入图像的尺寸为(150,150),滑动窗口大小与我们前面训练的分类网络的输入一致为(64,64),特征金字塔的缩小比例为1.5倍,这样将在原始图像基础上进行两次缩放;另外定义了滑动窗口的步长为16。

1
2
3
4
5
6
7
8
# initialize variables used for the object detection procedure
INPUT_SIZE = (150, 150)
PYR_SCALE = 1.5
WIN_STEP = 16
ROI_SIZE = (64, 64)

labels = {}
CLASS_NAMES = list(lb.classes_)

为简化后续分析,定义一个预测函数,用于返回图像中预测准确率超过minProb窗口及对象分类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def logo_prediction(model, batchROIs, batchLocs, labels, minProb=0.5,dims=(64, 64)):
preds = model.predict(batchROIs)
for i in range(0,len(preds)):
prob = np.max(preds[i])
if prob > 0.5:
index = np.argmax(preds[i])
label = CLASS_NAMES[int(index)]
# grab the coordinates of the sliding window for
# the prediction and construct the bounding box
(pX, pY) = batchLocs[i]
box = (pX, pY, pX + dims[0], pY + dims[1])
L = labels.get(label, [])
L.append((box,prob))
labels[label] = L
return labels

我们将遍历每个特征金字塔和每个滑动窗口,对识别结果进行预测:

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
img_file = "./test/2.jpg"
orig = cv2.imread(img_file)
# resize the input image to be a square
resized = cv2.resize(orig, INPUT_SIZE, interpolation=cv2.INTER_CUBIC)

# initialize the batch ROIs and (x, y)-coordinates
batchROIs = None
batchLocs = []
# loop over the image pyramid
for image in image_pyramid(resized, scale=PYR_SCALE,minSize=ROI_SIZE):
# loop over the sliding window locations
for (x, y, roi) in sliding_window(resized, WIN_STEP, ROI_SIZE):
# take the ROI and pre-process it so we can later classify the
# region with Keras
#roi = img_to_array(roi)
roi = roi/255
roi = np.expand_dims(roi, axis=0)
# roi = imagenet_utils.preprocess_input(roi)

# if the batch is None, initialize it
if batchROIs is None:
batchROIs = roi

# otherwise, add the ROI to the bottom of the batch
else:
batchROIs = np.vstack([batchROIs, roi])

# add the (x, y)-coordinates of the sliding window to the batch
batchLocs.append((x, y))


# classify the batch, then reset the batch ROIs and
# (x, y)-coordinates
model.predict(batchROIs)
labels = logo_prediction(model, batchROIs, batchLocs,labels, minProb=0.9)

当进行到这步骤才突然发现训练分类中缺了一个很重要的背景分类,将导致在背景上很多信息的预测会出问题,后续等整些背景图片再重新训练网络,😭

最后一步是预测结果的极大值抑制和显示:

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
from imutils.object_detection import non_max_suppression
# loop over the labels for each of detected objects in the image
for k in labels.keys():
# clone the input image so we can draw on it
clone = resized.copy()

# loop over all bounding boxes for the label and draw them on the image
for (box, prob) in labels[k]:
(xA, yA, xB, yB) = box
cv2.rectangle(clone, (xA, yA), (xB, yB), (0, 255, 0), 2)

# grab the bounding boxes and associated probabilities for each
# detection, then apply non-maxima suppression to suppress
# weaker, overlapping detections
boxes = np.array([p[0] for p in labels[k]])
proba = np.array([p[1] for p in labels[k]])
boxes = non_max_suppression(boxes, proba)

# loop over the bounding boxes again, this time only drawing the
# ones that were *not* suppressed
for (xA, yA, xB, yB) in boxes:
cv2.rectangle(clone, (xA, yA), (xB, yB), (0, 0, 255), 2)

# show the output image
print("[INFO] {}: {}".format(k, len(boxes)))
plt.imshow(clone)

极大值抑制是物体识别中很重要的一个环节,相关概念以后在慢慢整理

利用迁移学习优化一个物体识别网络模型

上述方法虽然简单容易理解,但存在很大的计算效率问题,每张图片需要进行多次特征提取和多次运算,对计算效率造成很大影响。目前主流的物体识别算法往往都可以应用于实时视频流的分析,显然使用上述方法是不合适的。我们将在后面探讨利用现有的物体识别网络通过迁移学习解决我们的目标识别问题。

由于本篇内容太多,利用迁移学习实现的方法,将单独作为一篇,此处留待插入链接

本文涉及代码详见Github

训练一个二分类网络检查货架上是否有百事可乐

参考Github实现一个物品检测原型:训练一个二分类分类器

  1. 在数据准备阶段与上述过程唯一不同是label的设置,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os, cv2
import numpy as np
data = []
labels = []
HEIGHT = 64
WIDTH = 64
for img in os.listdir("./flickr_logos_27_dataset/cropped/"):
img_file = cv2.imread(os.path.join("./flickr_logos_27_dataset/cropped/",img))
data.append(img_file)
label = img.split("_")[1].split(".")[0]
if label != "Pepsi":
label = "Nop"
labels.append(label)
data = np.stack(data)
labels = np.stack(labels)
  1. 由于是二分类问题,所以只需要最后一层使用sigmoid函数构建分类器即可,label的序列话方面使用LabelEncoder转换为0或者1即可:

    1
    2
    3
    4
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelEncoder
    lb = LabelEncoder()
    y = lb.fit_transform(labels)
  2. 数据增强与前文类似,不再赘言。在网络结构上,只需要修改最后为sigmoid函数输出,优化目标使用binary_crossentropy

    1
    2
    3
    4
    5
    ...
    model.add(Activation("sigmoid"))
    ...
    model.compile(loss="binary_crossentropy", optimizer=opt,
    metrics=["accuracy"])

    由于只有两个分类,所以模型很容易收敛,最后准确率也接近100%。

  3. 最后利用一个滑动窗口不停的扫描图像并利用cv2展示结果即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    for (x, y, window) in sliding_window(img, stepSize=32, windowSize=(winW, winH)):
    # if the window does not meet our desired window size, ignore it
    if window.shape[0] != winH or window.shape[1] != winW:
    continue

    crop_img=crop_image(sample_path,x, y, x + winW, y + winH)
    crop_img=imresize(crop_img,(64,64))
    crop_img = crop_img/255
    prediction=model.predict(crop_img.reshape(1,64,64,3))
    if prediction == 1:
    pred = 'Pepsi'
    else:
    pred=' '

    clone = img.copy()
    cv2.putText(clone, pred, (10, 30),cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
    cv2.rectangle(clone, (x, y), (x + winW, y + winH), (0, 255, 0), 2)
    clone = cv2.cvtColor(clone,cv2.COLOR_BGR2RGB)
    cv2.imshow("Window", clone)

    cv2.waitKey(1)
    time.sleep(0.5)

Logo检测的应用及分析

DeepSense.ai给出了一种Logo检测的分析方法,通过分析视频中不同品牌的logo呈现,统计了不同品牌在同一个视频中Logo出现的时间、出现的方式、呈现的效果等,最终提供给客户一个Logo Visubility Report

方案的主要流程如下图所示:

logo_detection_overview

生成的分析报告参见下图:

logo_detection_report

针对的分析视频如下:


参考

  1. Flickr Logos 27 dataset
  2. Datasets: FlickrLogos-32 / FlickrLogos-47

深度学习基础之优化算法

合适优化算法的选择有助于提升训练效率和收敛的速度,本文对常用的优化算法进行总结。在这章中将归纳SGD、RMSprop、Adam等的基础原理。优化算法的优劣来自于对相关算法的熟悉程度。


Transfer Learning Summary

本文对迁移学习在机器视觉中的实践技巧进行汇总整理 … …

@toc

Feature Extraction

在这种迁移学习模式中,预训练模型将被当作特征提取器(feature extractor),获得图像的特征表示(feature vactor)。获得特征向量之后,我们只需一个简单的分类器模型,如SVM、逻辑回归分类器、随机森林就可以完成目标分类器的设计。

VGG16倒数第二层(参数层)的输出维度为: 7x7x512 = 25,088

HDF5

抽取特征的高效存储可以选用HDF5。Hierarchical Data Format(HDF)是一种针对大量数据进行组织和存储的文件格式。经历了20多年的发展,HDF格式的最新版本是HDF5,它包含了数据模型,库,和文件格式标准。以其便捷有效,移植性强,灵活可扩展的特点受到了广泛的关注和应用。很多大型机构的数据存储格式都采用了HDF5,比如NASA的地球观测系统,MATLAB的.m文件,流体细算软件CDF,都将HDF5作为标准数据格式。HDF5本身用C实现,可以使用python的库h5py对HDF5文件进行操作。可以像操作Numpy数组一样对大型数据进行操作,比如切片,按行读取

数据在HDF5中采取分层存储方式,很像文件系统管理方式,第一级叫做组,类似于container,每个组中可以创建新的组或数据集,每一个dataset包含两部分的数据,Metadata和Data。其中Metadata包含Data相关的信息,而Data则包含数据本身。

HDF5文件存储方式

1
2
3
4
5
6
7
8
9
10
import h5py
p = "./datasets/hdf5/features.hdf5"

db = h5py.File(p)
list(db.keys())
>>> [u’features’, u’label_names’, u’labels’]
db["features"].shape
>>> (3000, 25088)
list(db["label_name"])
>>> ['cat', 'dogs', 'panda']

Fine-Tune

  • 一般而言,fine-tune在样本数据足够的情况下训练效果优于特征抽取;

    fine_tune

学习率要控制的尽量小

如何取层(Keras)

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
from keras.applications import VGG16
model = VGG16(weights="imagenet", include_top=False)
model.layers
>>>
[<keras.engine.topology.InputLayer at 0x7ff06cc3b518>,
<keras.layers.convolutional.Conv2D at 0x7ff06cc3b7f0>,
<keras.layers.convolutional.Conv2D at 0x7ff06cc3b7b8>,
<keras.layers.pooling.MaxPooling2D at 0x7ff06cc3bd30>,
<keras.layers.convolutional.Conv2D at 0x7ff06cdb3b00>,
<keras.layers.convolutional.Conv2D at 0x7ff06cdb3748>,
<keras.layers.pooling.MaxPooling2D at 0x7ff06cbbb400>,
<keras.layers.convolutional.Conv2D at 0x7ff06cbcc630>,
<keras.layers.convolutional.Conv2D at 0x7ff06cb5dd68>,
<keras.layers.convolutional.Conv2D at 0x7ff06cb6d208>,
<keras.layers.pooling.MaxPooling2D at 0x7ff06cb7fdd8>,
<keras.layers.convolutional.Conv2D at 0x7ff06cb30fd0>,
<keras.layers.convolutional.Conv2D at 0x7ff06cb30e80>,
<keras.layers.convolutional.Conv2D at 0x7ff06cb52978>,
<keras.layers.pooling.MaxPooling2D at 0x7ff06caf6470>,
<keras.layers.convolutional.Conv2D at 0x7ff06cb076a0>,
<keras.layers.convolutional.Conv2D at 0x7ff06ca98dd8>,
<keras.layers.convolutional.Conv2D at 0x7ff06caaa278>,
<keras.layers.pooling.MaxPooling2D at 0x7ff06cab8e48>]

model.output
>>>
<tf.Tensor 'block5_pool/MaxPool:0' shape=(?, ?, ?, 512) dtype=float32>

如何添加层(Keras)

1
2
3
4
5
6
7
8
9
10
11
12
13
# load the VGG16 network, ensuring the head FC layer sets are left # off
baseModel = VGG16(weights="imagenet", include_top=False, input_tensor=Input(shape=(224, 224, 3)))
# add new layers
headModel = baseModel.output
headModel = Flatten(name="flatten")(headModel)
headModel = Dense(D_num, activation="relu")(headModel)
headModel = Dropout(0.5)(headModel)
headModel = Dense(classes_num, activation="softmax")(headModel)

model = Model(inputs=baseModel.input, outputs=headModel)
#freeze baseModel layers
for layer in baseModel.layers:
layer.trainable = False
  1. Typically you’ll allow your own FC head to warmup for 10-30 epochs, depending on your dataset.使用RMSprop作为优化算法;​

    1
    2
    3
    4
    5
    6
    opt = RMSprop(lr=0.001)
    model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])
    # train the head of the network for a few epochs (all other # layers are frozen) -- this will allow the new FC layers to
    # start to become initialized with actual "learned" values # versus pure random
    print("[INFO] training head...")
    model.fit_generator(aug.flow(trainX, trainY, batch_size=32), validation_data=(testX, testY), epochs=25, steps_per_epoch=len(trainX) // 32, verbose=1)
    1. 然后可以适当往前解冻一些层,重新训练,一般会解冻最后一层CONV,使用SGD(lr=0.001)作为优化算法;

      1
      2
      3
      # now that the head FC layers have been trained/initialized, lets # unfreeze the final set of CONV layers and make them trainable 
      for layer in baseModel.layers[15:]:
      layer.trainable = True
      1
      2
      3
      4
      5
      6
      7
      # for the changes to the model to take affect we need to recompile # the model, this time using SGD with a *very* small learning rate 
      print("[INFO] re-compiling model...")
      opt = SGD(lr=0.001)
      model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])
      # train the model again, this time fine-tuning *both* the final set # of CONV layers along with our set of FC layers
      print("[INFO] fine-tuning model...")
      model.fit_generator(aug.flow(trainX, trainY, batch_size=32), validation_data=(testX, testY), epochs=100, steps_per_epoch=len(trainX) // 32, verbose=1)

迁移学习的选择

主要由样本数据量以及训练目标于原目标之间的相似程度决定。

数据规模 相似数据分布 不同数据分布
小样本数据集 特征提取:FC+分类器 特征提取:低层次的Conv+分类器
大样本数据集 Fine-Tune 从头训练新的网络模型

神经网络之感知机(Perceptron)

感知机于1957年由Rosenblatt等人提出,是神经网络和支持向量机的基础。


读书笔记:《Practical Python and OpenCV》

《Practical Python and OpenCV》读书札记。

1. Load,Display and Save an Image

1
2
3
4
5
6
7
8
9
import cv2
# Load the image and show some basic information on it
image = cv2.imread("Path/to/Image")
# Show the image and wait for a keypress
cv2.imshow("Image", image)
cv2.waitKey(0)
# Save the image -- OpenCV handles converting filetypes
# automatically
cv2.imwrite("newimage.jpg", image)

2. cv2

图像上添加线条及形状

  • cv2.line(image, start_point, stop_point, color, thickness)
  • cv2.rectangle(image,top_left, bottom_right, color, thickness)
    • thickness 为负数,填充形状
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Initialize our canvas as a 300x300 with 3 channels,
# Red, Green, and Blue, with a black background
canvas = np.zeros((300, 300, 3), dtype = "uint8")
# Draw a green line from the top-left corner of our canvas
# to the bottom-right
green = (0, 255, 0)
cv2.line(canvas, (0, 0), (300, 300), green)
# Now, draw a 3 pixel thick red line from the top-right
# corner to the bottom-left
red = (0, 0, 255)
cv2.line(canvas, (300, 0), (0, 300), red, 3)
# Draw a green 50x50 pixel square, starting at 10x10 and
# ending at 60x60
cv2.rectangle(canvas, (10, 10), (60, 60), green)
# Draw another rectangle, this time we'll make it red and
# 5 pixels thick
cv2.rectangle(canvas, (50, 200), (200, 225), red, 5)
# Let's draw one last rectangle: blue and filled in
blue = (255, 0, 0)
cv2.rectangle(canvas, (200, 50), (225, 125), blue, -1)
cv2.imshow("Canvas", canvas)
cv2.waitKey(0)

Line and Rectangle

添加圆形

  • cv2.circle(image, (centerX, centerY), r, color,thickness)
1
2
3
4
5
6
7
8
9
10
# Reset our canvas and draw a white circle at the center
# of the canvas with increasing radii - from 25 pixels to
# 150 pixels
canvas = np.zeros((300, 300, 3), dtype = "uint8")
(centerX, centerY) = (canvas.shape[1] // 2, canvas.shape[0] // 2)
white = (255, 255, 255)
for r in range(0, 175, 25):
cv2.circle(canvas, (centerX, centerY), r, white)
cv2.imshow("Canvas", canvas)
cv2.waitKey(0)

Circles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Let's go crazy and draw 25 random circles
for i in range(0, 25):
# randomly generate a radius size between 5 and 200,
# generate a random color, and then pick a random
# point on our canvas where the circle will be drawn
radius = np.random.randint(5, high = 200)
color = np.random.randint(0, high = 256, size = (3,)).tolist()
pt = np.random.randint(0, high = 300, size = (2,))

# draw our random circle
cv2.circle(canvas, tuple(pt), radius, color, -1)

# Show our masterpiece
cv2.imshow("Canvas", canvas)
cv2.waitKey(0)

RandomCircles

3. Image Processing

  1. 平移

    • cv2.warpAffine()
    • imutil
    1
    2
    3
    4
    5
    6
    7
    def translate(image, x, y):
    # Define the translation matrix and perform the translation
    M = np.float32([[1, 0, x], [0, 1, y]])
    shifted = cv2.warpAffine(image, M, (image.shape[1], image.shape[0]))

    # Return the translated image
    return shifted
  2. 旋转

    • cv2.getRotationMatrix2D
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def rotate(image, angle, center = None, scale = 1.0):
    # Grab the dimensions of the image
    (h, w) = image.shape[:2]

    # If the center is None, initialize it as the center of
    # the image
    if center is None:
    center = (w / 2, h / 2)

    # Perform the rotation
    M = cv2.getRotationMatrix2D(center, angle, scale)
    rotated = cv2.warpAffine(image, M, (w, h))

    # Return the rotated image
    return rotated
  3. 缩放

    • cv2.resize
    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
    def resize(image, width = None, height = None, inter = cv2.INTER_AREA):
    # initialize the dimensions of the image to be resized and grab the image size
    dim = None
    (h, w) = image.shape[:2]

    # if both the width and height are None, then return the original image
    if width is None and height is None:
    return image

    # check to see if the width is None
    if width is None:
    # calculate the ratio of the height and construct the dimensions
    r = height / float(h)
    dim = (int(w * r), height)

    # otherwise, the height is None
    else:
    # calculate the ratio of the width and construct the dimensions
    r = width / float(w)
    dim = (width, int(h * r))

    # resize the image
    resized = cv2.resize(image, dim, interpolation = inter)

    # return the resized image
    return resized
  4. 反转

    • cv2.flip(image, num)
    • num=1, 水平反转;num=0,垂直翻转;num为负,对角反转
  5. bitwise

    1
    2
    bitwiseAnd = cv2.bitwise_and(rectangle, circle) cv2.imshow("AND", bitwiseAnd) 
    cv2.waitKey(0)
  6. MASKING

    1
    2
    3
    4
    5
    mask = np.zeros(image.shape[:2], dtype = "uint8") 
    cv2.circle(mask, (cX, cY), 100, 255, -1)
    masked = cv2.bitwise_and(image, image, mask = mask) cv2.imshow("Mask", mask)
    cv2.imshow("Mask Applied to Image", masked)
    cv2.waitKey(0)
  7. 色彩空间变换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # Load the image and show it
    image = cv2.imread(args["image"])
    # Convert the image to grayscale
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # Convert the image to the HSV (Hue, Saturation, Value)
    # color spaces
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    # Convert the image to the L*a*b* color spaces
    lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
  8. 平滑/模糊(blurring)

    1
    2
    3
    4
    5
    6
    7
    8
    # Averaging Blurring
    cv2.blur(image, (3, 3))
    # Gaussian Blurring
    cv2.GaussianBlur(image, (3, 3), 0)
    # Median Blurring
    cv2.medianBlur(image, 3)
    # Bilateral Blurring
    cv2.bilateralFilter(image, 5, 21, 21)

Traditionally, the median blur method has been most effective when removing salt-and-pepper noise.

  1. Threshold

    1
    2
    3
    4
    (T, thresh) = cv2.threshold(blurred, 155, 255, cv2.THRESH_BINARY) 
    cv2.imshow("Threshold Binary", thresh)
    (T, threshInv) = cv2.threshold(blurred, 155, 255, cv2. THRESH_BINARY_INV)
    cv2.bitwise_and(image, image, mask = threshInv)
  2. 边缘检测

    1
    2
    3
    4
    image = cv2.imread(img)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    image = cv2.GaussianBlur(image, (5,5), 0)
    canny = cv2.Canny(image, 30 ,150)

canny

  1. 轮廓检测

    1
    2
    3
    4
    5
    6
    7
    8
    image = cv2.imread(img)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    image = cv2.GaussianBlur(image, (11,11), 0)
    canny = cv2.Canny(image, 30 ,150)
    (_, cnts, _) = cv2.findContours(canny.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    print("I count {} coins in this image".format(len(cnts)))
    coins = image.copy()
    cv2.drawContours(coins, cnts, -1, (0, 255, 255), 2)

    countours

    • 从图像中将对象扣取
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # Now, let's loop over each contour
    for (i, c) in enumerate(cnts):
    # We can compute the 'bounding box' for each contour, which is the rectangle that encloses the contour
    (x, y, w, h) = cv2.boundingRect(c)

    # Now that we have the contour, let's extract it using array slices
    print("Coin #{}".format(i + 1))
    coin = image[y:y + h, x:x + w]
    cv2.imshow("Coin", coin)

    # Just for fun, let's construct a mask for the coin by finding The minumum enclosing circle of the contour
    mask = np.zeros(image.shape[:2], dtype = "uint8")
    ((centerX, centerY), radius) = cv2.minEnclosingCircle(c)
    cv2.circle(mask, (int(centerX), int(centerY)), int(radius), 255, -1)
    mask = mask[y:y + h, x:x + w]
    cv2.imshow("Masked Coin", cv2.bitwise_and(coin, coin, mask = mask))
    cv2.waitKey(0)

Tips

  1. 为解决python 2.7和python 3中print函数不兼容的问题,可通过导入如下命令解决在python 2.7环境运行python 3中print函数不兼容的问题:

    1
    from __future__ import print_function
  2. OpenCV存储RGB信息采用的逆向存储方式,即为BGR; matplotlib.plot中为RGB

  3. np.random

Method Desription
rand(d0, d1, …, dn) Random values in a given shape.
randn(d0, d1, …, dn) Return a sample (or samples) from the “standard normal” distribution.
randint(low[, high, size, dtype]) Return random integers from low (inclusive) to high (exclusive).
random_integers(low[, high, size]) Random integers of type np.int between low and high, inclusive.
random_sample([size]) Return random floats in the half-open interval [0.0, 1.0).
random([size]) Return random floats in the half-open interval [0.0, 1.0).产生随机矩阵,如random.random([2,3])产生一个2x3维的随机数
ranf([size]) Return random floats in the half-open interval [0.0, 1.0).
sample([size]) Return random floats in the half-open interval [0.0, 1.0).
choice(a[, size, replace, p]) Generates a random sample from a given 1-D array
bytes(length) Return random bytes.

深度学习基础之正则化

明确任务目标和评价准则对于模型的设计及优化至关重要,本文将总结常用的相关方法和模型性能评价准则。为了简化,本文将主要针对回归问题和分类问题分别予以归纳,其它问题可采取类似的方法及手段。


Deep Learning for Computer Vision

本文记录深度学习书籍《Deep Learning for Computer Vision with Python》的读书笔记。

@toc

背景

  • 深度学习拥有60多年历史,虽然曾经采用过不同的名称和不同的主导技术:“deep learning” has existed since the 1940s undergoing various name changes, including cybernetics, connectionism, and the most familiar, Artificial Neural Networks (ANNs).
  • 神经网络的普适定律:Further research demonstrated that neural networks are universal approximators , capable of approximating any continuous function (but placing no guarantee on whether or not the network can actually learn the parameters required to represent a function).
  • Classic machine learning algorithms for unsupervised learning include Principle Component Analysis (PCA) and k-means clustering. Specific to neural networks, we see Autoencoders, Self-Organizing Maps (SOMs), and Adaptive Resonance Theory applied to unsupervised learning.
  • Popular choices for semisupervised learning include label spreading, label propagation, ladder networks, and co-learning/co-training.

Image and Pixels

  • Pixels are represented in two ways:
    • Grayscale: Each pixel is a scalar value between 0 and 255.(0 for “Black” and 255 for “White”),0—>255 dark —> light
    • Color: RGB color space, (R,G,B), Each Red, Green, and Blue channel can have values defined in the range [0,255] for a total of 256 “shades”, where 0 indicates no representation and 255 demonstrates full representation.
  • Given that the pixel value only needs to be in the range [0,255], we normally use 8-bit unsigned integers to represent the intensity.

Images as Numpy Arrays

  • (height, width, depth) 表示

height 排第一的主要原因是由于矩阵表示形式中,一般把行放在前面,而图像中height大小表征了行的数目。

1
2
3
4
5
6
import cv2
image = cv2.imread("example.png") print(image.shape)
cv2.imshow("Image", image)
cv2.waitKey(0)
## Access an individual pixel value
(b, g, r) = image[20, 100] # accesses pixel at x=100, y=20
  1. 取像素y在x前面,还是由于矩阵的表示形式;
  2. RGB顺序反的,这是由于OpenCV历史原因导致的表示形式差异: Because the BGR ordering was popular among camera manufacturers and other software developers at the time.

Others

aspect ratio: the ratio of the width to the height of the image.

神经网络模型一般都是固定输入,比如32×32, 64×64, 224×224, 227×227, 256×256, and 299×299. 需要对不同大小的图像进行reshape操作,For some datasets you can simply ignore the aspect ratio and squish, distort, and compress your images prior to feeding them through your network. On other datasets, it’s advantageous to preprocess them further by resizing along the shortest dimension and then cropping the center.

Image Classification

图像分类和图像理解是当今技术视觉领域最火的课题。

定义

图像分类: the task of assigning a label to an image from a predefined set ofcategories.

图像分类的过程是学习图片中的“underlying patterns”

Semantic Gap: the difference between how a human perceives the contents of an image versus how an image can be represented in a way a computer can understand the process.

挑战

Challenge for Image Classification

数据集(TODO)

  1. MNIST

    • 目标: 完成0-9手写字符的识别

    • 说明:

      • NIST代表National Institute ofStandards and TechnologyM代表Modified
      • 深度学习的Hello World
      • 包含60,000训练样本,10,000测试样本,每个样本为28x28的灰度图像
    • 目前准确度: >99%

    • 获取地址: http://yann.lecun.com/exdb/mnist/

      MNIST

  2. Fashion-MNIST

    • 目标: 完成10种不同衣服的识别

    • 说明:

      • 根据MNIST设计的新的数据集,难度比MNIST略高
      • 包含60,000训练样本,10,000测试样本,每个样本为28x28的灰度图像
    • 目前准确度: >95%

    • 获取地址: https://github.com/zalandoresearch/fashion-mnist

      Fashion-MNIST

  3. CIFAR-10

  4. Animals: Dogs,Cat, Pandas

  5. Flowers-17

  6. CALtECH-101

  7. Tiny ImageNet 200

  8. Adience

  9. ImageNet

  10. 表情识别(是否笑脸)

    • 说明:
    • 共计13165张灰度图片,每张图片大小为64x64
    • 分为笑脸和非笑脸两类,其中笑脸3690张,非笑脸9475张(数据不平衡)
    • 获取地址:https://github.com/hromi/SMILEsmileD
    • 另外fer2013提供了更多表情的训练用数据集

Smile Datasets

  1. 性别和年龄数据集
  1. Indoor CVPR

  2. Stanford Cars

神经网络基础

优化算法(TODO,整合到单独Note)

  • Chapter 8

Regularization (TODO, 整合到单独Note)

  • Chapter 9
  • chapter 10,激活函数,perception

为什么验证损失函数值有时候小于训练损失函数

这可能是有几方面原因导致的,或多方面原因综合作用的结果,主要的原因包括:

  1. 训练集和验证集分布不均,导致训练集数据难度大,验证集简单数据分布比例大;
  2. 数据放大本身形成了一种规则化,降低了训练集的训练结果;(这本身是规则化的目标,降低在训练集的表现,提升泛化性能)
  3. 训练时间或轮数不够;

关于学习率

  • keras中提供了decay参数来调节学习率的变化情况:

    1
    opt = SGD(lr=0.01, decay=0.01 / 40, momentum=0.9, nesterov=True)

    使用公式:

  • 另一种学习率为阶梯学习率:ctrl + c

    Keras提供一个类:LearningrateScheduler来配置自定义的学习率函数

    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def step_decay(epoch):
    # initialize the base initial learning rate, drop factor, and epochs to drop every
    initAlpha = 0.01
    factor = 0.25
    dropEvery = 5

    # compute learning rate for the current epoch
    alpha = initAlpha * (factor ** np.floor((1 + epoch) / dropEvery))

    # return the learning rate
    return float(alpha)

    ##定义callback
    callbacks = [LearningRateScheduler(step_decay)]

当定义了学习率之后,SGD中声明的配置信息将被忽略

网络模型

VGG

  • 所有的卷积层使用同一种卷积核:3X3
  • 堆积多个CONV=>RELU层再进行一次POOL操作

MNIST

Researchers tend to use the MNIST dataset as a benchmark to evaluate new classification algorithms. If their methods cannot obtain > 95% classification accuracy, then there is either a flaw in (1) the logic of the algorithm or (2) the implementation itself.

Case Study

使用OpenCV的Haar cascade 算法进行人脸检测,提取人脸的ROI(Region of intrest), 通过一个卷积神经网络进行表情识别;

可以结合Github开源的表情识别代码一起研究

  • 路径处理 os.path.sep: 提取路径分隔符
  • 数据不平衡的处理,可以考虑不同分类的权重,在训练时通过赋权调整平衡性,代码如下:
1
2
3
4
5
6
7
8
# Handle data imbalance
# account for skew in the labeled data
classTotals = labels.sum(axis=0)
classWeight = classTotals.max() / classTotals

## When training
H = model.fit(trainX, trainY, validation_data=(testX, testY),
class_weight=classWeight, batch_size=64, epochs=15, verbose=1)

手写字的预处理

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
def preprocess(image, width, height):
#grap the dimensions of the image, then initialize the padding values
(h, w) = image.shape[:2]
#if width greater than height, resize along the width
if w > h:
image = imutils.resize(iamge, width=width)
else:
image = imutils.resize(image, height=height)
#padding values for w and h to obtain the target dimensions
padW = int((width - image.shape[1])/2.0)
padH = int((height - image.shape[0])/2.0)

#pad the image then apply one more resizing to handle any rounding issues
image = cv2.copyMakeBorder(image, padH, padH, padW, padW, cv2.BORDER_REPLICATE)
iamge = cv2.resize(image, (width, height))

return image

image = cv2.imread(img)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.copyMakeBorder(gray, 20,20,20,20, cv2.BORDER_REPLICATE)
# threshold the image to reveal the digits
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
#find contours in the image, keeping only the four largest ones
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if imutils.is_cv2() else cnts[1]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:4]
cnts = contours.sort_contours(cnts)[0]

# initialize the output image as a "grayscale" image with 3
# channels along with the output predictions
output = cv2.merge([gray] * 3)

for c in cnts:
# compute the bounding box for the contour then extract the
# digit
(x, y, w, h) = cv2.boundingRect(c)
roi = gray[y - 5:y + h + 5, x - 5:x + w + 5]
roi = preprocess(roi, 28, 28)
roi = np.expand_dims(img_to_array(roi), axis=0) / 255.0
#pred = model.predict(roi).argmax(axis=1)[0] + 1
#predictions.append(str(pred))
# draw the prediction on the output image
cv2.rectangle(output, (x - 2, y - 2),(x + w + 4, y + h + 4), (0, 255, 0), 1)
#cv2.putText(output, str(pred), (x - 5, y - 5),cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 2)
# show the output image
#print("[INFO] captcha: {}".format("".join(predictions)))

plt.imshow(output)
#cv2.waitKey()

Useful Functions

图像预处理及加载模板

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
'''
File1: Preprocessor
'''
import cv2

class SimplePreporcessor:
def __init__(self, width, height, inter=cv2.INTER_AREA)
# store the target image width, height, and interpolation method used when resizing
self.width = width
self.height = height
self.inter = inter

def preprocess(self, image):
# resize the image to a fixed size, ignoring the aspect ratio
return cv2.resize(image, (self.width, self.height), interpolation = self.inter)

'''
Data Loader
'''
# import the necessary packages
import numpy as np
import cv2
import os

class SimpleDatasetLoader:
def __init__(self, preprocessors=None):
self.preprocessors = preprocessors
# if the preprocessors are None, initialize them as an empty list
if self.preprocessors is None:
self.preprocessors = []
def load(self, imagePaths, verbose =-1):
data = []
labels = []

for (i, imagePath) in enumerate(imagePaths):
# load the image and extract the class label assuming
# # that our path has the following format:
# # /path/to/dataset/{class}/{image}.jpg
image = cv2.imread(imagePath)
label = imagePath.split(os.path.sep)[-2]

# check to see if our preprocessors are not None
if self.preprocessors is not None:
for p in self.preprocessors:
image = p.preprocess(image)

data.append(image)
labels.append(label)

# show an update every ‘verbose‘ images
if verbose >0 and i >0 and (i+1)%verbose == 0:
print("[INFON] process {}/{}").format(i+1,len(imagePaths))
# return a tuple of the data and labels
return (np.array(data), np.array(labels))
'''
Main
'''
# import the necessary packages
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from imutils import paths
import argparse

# construct the argument parse and parse the arguments ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required=True, help="path to input dataset")
ap.add_argument("-k", "--neighbors", type=int, default=1, help="# of nearest neighbors for classification")
ap.add_argument("-j", "--jobs", type=int, default=-1, help="# of jobs for k-NN distance (-1 uses all available cores)")
args = vars(ap.parse_args())

print("[INFO] loading images ...")
imagePaths = list(paths.list_images(args["dataset"]))

# initialize the image preprocessor, load the dataset from disk,
# and reshape the data matrix
sp = SimplePreporcessor(32, 32)
sdl = SimpleDatasetLoader(preprocessors=[sp])

(data, labels) = sdl.load(imagePaths, verbose=500)
#flatten for use in KNN
data = data.reshape((data.shape[0],32*32*3))

print("[INFO] feature matrix: {:.1f}MB").format(data.nbytes/(1024*1000.0))

# encode the labels as integers
le = LabelEncoder()
labels = le.fit_transform(labels)

(trainX, testX, trainY, testY) = train_test_split(data, labels, test_size=0.25, random_state=42)

# train and evaluate a kNN classifier on raw pixel intensities
print("[INFO] evaluate kNN classifier ...")
model = KNeighborsClassifier(n_neighbors=args["neighbors"]), n_jobs=args["jobs"])
model.fit(trainX, trainY)
print(classification_report(testY, model.predict(testX),target_names==le.classes_))

sklearn.metrics.classification_report

sklearn中的classification_report函数用于显示主要分类指标的文本报告.在报告中显示每个类的精确度,召回率,F1值等信息。
主要参数:

  • y_true:1维数组,或标签指示器数组/稀疏矩阵,目标值。
  • y_pred:1维数组,或标签指示器数组/稀疏矩阵,分类器返回的估计值。
  • labels:array,shape = [n_labels],报表中包含的标签索引的可选列表。
  • target_names:字符串列表,与标签匹配的可选显示名称(相同顺序)。
  • sample_weight:类似于shape = [n_samples]的数组,可选项,样本权重。
  • digits:int,输出浮点值的位数.
1
2
3
4
5
from sklearn.metrics import classification_report
y_true = [0, 1, 2, 2, 2]
y_pred = [0, 0, 2, 2, 1]
target_names = ['class 0', 'class 1', 'class 2']
print(classification_report(y_true, y_pred, target_names=target_names))

opencv 给图像添加描述

1
2
3
import cv2
# draw the label with the highest score on the image as our # prediction
cv2.putText(orig, "Label: {}".format(labels[np.argmax(scores)]), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)

Keras中的Checkpoint机制

1
2
3
4
5
6
7
8
9
10
11
12
13
from keras.callbacks import ModelCheckpoint

# construct the callback to save only the *best* model to disk
# based on the validation loss
fname = os.path.sep.join([args["weights"],
"weights-{epoch:03d}-{val_loss:.4f}.hdf5"])
checkpoint = ModelCheckpoint(fname, monitor="val_loss", mode="min",
save_best_only=True, verbose=1)
callbacks = [checkpoint]

print("[INFO] training network...")
H = model.fit(trainX, trainY, validation_data=(testX, testY),
batch_size=64, epochs=40, callbacks=callbacks, verbose=2)

参数:

  • filename:字符串,保存模型的路径
  • monitor:需要监视的值
  • verbose:信息展示模式,0或1
  • save_best_only:当设置为True时,将只保存在验证集上性能最好的模型
  • mode:‘auto’,‘min’,‘max’之一,在save_best_only=True时决定性能最佳模型的评判准则,例如,当监测值为val_acc时,模式应为max,当检测值为val_loss时,模式应为min。在auto模式下,评价准则由被监测值的名字自动推断。
  • save_weights_only:若设置为True,则只保存模型权重,否则将保存整个模型(包括模型结构,配置信息等)
  • period:CheckPoint之间的间隔的epoch数
  1. 可以monitor loss值也可以是val_acc,train_loss, train_acc;
  2. 更多内容参见:http://keras-cn.readthedocs.io/en/latest/other/callbacks/

EarlyStopping

1
keras.callbacks.EarlyStopping(monitor='val_loss', patience=0, verbose=0, mode='auto')

当监测值不再改善时,该回调函数将中止训练

参数

  • monitor:需要监视的量
  • patience:当early stop被激活(如发现loss相比上一个epoch训练没有下降),则经过patience个epoch后停止训练。
  • verbose:信息展示模式
  • mode:‘auto’,‘min’,‘max’之一,在min模式下,如果检测值停止下降则中止训练。在max模式下,当检测值不再上升则停止训练。

基于keras callback实现训练过程监控

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
# import the necessary packages
from keras.callbacks import BaseLogger
import matplotlib.pyplot as plt
import numpy as np
import json
import os

class TrainingMonitor(BaseLogger):
def __init__(self, figPath, jsonPath=None, startAt=0):
# store the output path for the figure, the path to the JSON serialized file, and the starting epoch
super(TrainingMonitor, self).__init__()
self.figPath = figPath
self.jsonPath = jsonPath
self.startAt = startAt


def on_train_begin(self, logs={}):
# initialize the history dictionary
self.H = {}

# if the JSON history path exists, load the training history
if self.jsonPath is not None:
if os.path.exists(self.jsonPath):
self.H = json.loads(open(self.jsonPath).read())

# check to see if a starting epoch was supplied
if self.startAt > 0:
# loop over the entries in the history log and
# trim any entries that are past the starting
# epoch
for k in self.H.keys():
self.H[k] = self.H[k][:self.startAt]

def on_epoch_end(self, epoch, logs={}):
# loop over the logs and update the loss, accuracy, etc.
# for the entire training process
for (k, v) in logs.items():
l = self.H.get(k, [])
l.append(v)
self.H[k] = l
# check to see if the training history should be serialized
# to file
if self.jsonPath is not None:
f = open(self.jsonPath, "w")
f.write(json.dumps(self.H))
f.close()
# ensure at least two epochs have passed before plotting
# (epoch starts at zero)
if len(self.H["loss"]) > 1:
#plot the training loss and accuracy
N = np.arange(0, len(self.H["loss"]))
plt.style.use("ggplot")
plt.figure()
plt.plot(N, self.H["loss"], label="train_loss")
plt.plot(N, self.H["val_loss"], label="val_loss")
plt.plot(N, self.H["acc"], label="train_acc")
plt.plot(N, self.H["val_acc"], label="val_acc")
plt.title("Training Loss and Accuracy [Epoch {}]".format(len(self.H["loss"])))
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend()

#save the figure
plt.savefig(self.figPath)
plt.close()
  • figPath: The path to the output plot that we can use to visualize loss and accuracy over time.
  • jsonPath: An optional path used to serialize the loss and accuracy values as a JSON file. This path is useful if you want to use the training history to create custom plots of your own.
  • startAt: This is the starting epoch that training is resumed at when using ctrl + c training.

参考

深度学习基础之目标及评估

明确任务目标和评价准则对于模型的设计及优化至关重要,本文将总结常用的相关方法和模型性能评价准则。为了简化,本文将主要针对回归问题和分类问题分别予以归纳,其它问题可采取类似的方法及手段。


深度学习基础之模型压缩

深度学习中模型压缩关键技术总结。

深度学习基础篇将从几个不同的层面来总结在过去一段时间对于深度学习关键技术的理解,通过知识体系的归纳了解知识体系的不足,提升对核心技术点的认识。所有系列文章将在未来一段时间内容随着掌握了解的深入迭代更新。目前主要希望对如下几个领域进行归纳汇总:

  1. 问题定义
  2. 目标及评估
  3. 数据准备与预处理
  4. 激活函数的归纳及总结
  5. 优化算法的归纳及总结
  6. 正则化与泛化性能
  7. 模型压缩
  8. 数据扩充

为了增加深度学习模型的容量,往往包含了大量的待训练参数和模型规模,以VGG-16为例,参数数目为1亿3千万,占用空间500MB,如果利用VGG-16进行一次图片识别任务,需要进行309亿次的浮点运算(FLOPs)。如此大的模型体积和对算力的依赖,为深度学习模型迁移到嵌入式设备或者利用CPU进行推理带来了很大的难度,为此通过合理的模型压缩技术在保证模型性能没有明显下降的情况下降低模型体积和参数数目成为一个热点的研究问题。

同时,研究发现深度神经网络面临严峻的过参数化问题,需要注意的是,这种冗余在模型训练阶段是十分必要的。因为深度神经网络面临的是一个极其复杂的非凸优化问题,对于现有的基于梯度下降的优化算法而言,这种参数上的冗余保证了网络能够收敛到一个比较好的最优值。因而在一定程度上,网络越深,参数越多,模型越复杂,其最终的效果也往往越好。

按照压缩过程对网络结构的破坏程度,我们将模型压缩技术分为“前端压缩”与“后端压缩”两部分。所谓“前端压缩”,是指不改变原网络结构的压缩技术,主要包括知识蒸馏、紧凑的模型结构设计以及滤波器层面的剪枝等;而“后端压缩”则包括低秩近似、未加限制的剪枝、参数量化以及二值网络等,其目标在于尽可能地减少模型大小,因而会对原始网络结构造成极大程度的改造。其中,由于“前端压缩”未改变原有的网络结构,仅仅只是在原模型的基础上减少了网络的层数或者滤波器的个数,其最终的模型可完美适配现有的深度学习库。相比之下,“后端压缩”为了追求极致的压缩比,不得不对原有的网络结构进行改造,如对参数进行量化表示等,而这样的改造往往是不可逆的。同时,为了获得理想的压缩效果,必须开发相配套的运行库,甚至是专门的硬件设备,其最终的结果往往是一种压缩技术对应于一套运行库,从而带来了巨大的维护成本。

剪枝与稀疏约束

知识蒸馏

参数量化

二值网络

参考

  1. 《解析卷积神经网络—深度学习实践手册》
  2. Keras文档

深度学习基础之数据准备与预处理

由于人工智能面向的应用和场景的多样性,导致需要分析的数据无论是从维度还是格式上都存在巨大差异,数据准备阶段需要解决数据的数值化、归一化、特征工程等共性的问题。

深度学习基础篇将从几个不同的层面来总结在过去一段时间对于深度学习关键技术的理解,通过知识体系的归纳了解知识体系的不足,提升对核心技术点的认识。所有系列文章将在未来一段时间内容随着掌握了解的深入迭代更新。目前主要希望对如下几个领域进行归纳汇总:

  1. 问题定义
  2. 目标及评估
  3. 数据准备与预处理
  4. 激活函数的归纳及总结
  5. 优化算法的归纳及总结
  6. 正则化与泛化性能
  7. 模型压缩
  8. 数据扩充

由于人工智能面向的应用和场景的多样性,导致需要分析的数据无论是从维度还是格式上都存在巨大差异,数据准备阶段需要解决数据的数值化、归一化、特征工程等共性的问题。

深度学习的端到端学习能力并不意味着在实际的业务处理中把原始数据直接丢进网络模型,与传统的机器学习技术类似必要的数据预处理工作无论是对于提升模型的收敛效率还是提升模型的训练质量都具备十分重要的意义。

数值化

由于神经网络的输入限定为数值数据,所以对于字符串数据、文本数据、类别数据,在导入网络模型之前需要进行数值化处理,转换为数值数据。其中类别数据可以采用one-hot编码等方式进行编码。

数据归一化

数据归一化是属于预处理阶段经常采用的一种手段。虽然这里有一系列可行的方法,但是这一步通常是根据数据的具体情况而明确选择的。特征归一化常用的方法包含如下几种:

  • 简单缩放
  • 逐样本均值消减(也称为移除直流分量)
  • 特征标准化(使数据集中所有特征都具有零均值和单位方差)

简单缩放

在简单缩放中,我们的目的是通过对数据的每一个维度的值进行重新调节(这些维度可能是相互独立的),使得最终的数据向量落在 [0,1]或[ − 1,1] 的区间内(根据数据情况而定)。这对后续的处理十分重要,因为很多默认参数(如 PCA-白化中的 epsilon)都假定数据已被缩放到合理区间。

例子:在处理自然图像时,我们获得的像素值在 [0,255] 区间中,常用的处理是将这些像素值除以 255,使它们缩放到 [0,1] 中.

逐样本均值消减

如果你的数据是平稳的(即数据每一个维度的统计都服从相同分布),那么你可以考虑在每个样本上减去数据的统计平均值(逐样本计算)。

例子:对于图像,这种归一化可以移除图像的平均亮度值 (intensity)。很多情况下我们对图像的照度并不感兴趣,而更多地关注其内容,这时对每个数据点移除像素的均值是有意义的。注意:虽然该方法广泛地应用于图像,但在处理彩色图像时需要格外小心,具体来说,是因为不同色彩通道中的像素并不都存在平稳特性。

特征标准化

特征标准化指的是(独立地)使得数据的每一个维度具有零均值和单位方差。这是归一化中最常见的方法并被广泛地使用(例如,在使用支持向量机(SVM)时,特征标准化常被建议用作预处理的一部分)。在实际应用中,特征标准化的具体做法是:首先计算每一个维度上数据的均值(使用全体数据计算),之后在每一个维度上都减去该均值。下一步便是在数据的每一维度上除以该维度上数据的标准差。

例子:处理音频数据时,常用 Mel 倒频系数 MFCCs 来表征数据。然而MFCC特征的第一个分量(表示直流分量)数值太大,常常会掩盖其他分量。这种情况下,为了平衡各个分量的影响,通常对特征的每个分量独立地使用标准化处理。

原理: 在每个样本中减去数据的统计平均值,可以移除数据的共同部分,凸显个体差异。

注意:

数据归一化中采取的统计平均值和均方差值都来源于训练数据,由于理论上不应该从验证集和测试集中获取信息,所以对于验证集和测试集的处理也使用训练集的结果。

标准流程

在这一部分中,我们将介绍几种在一些数据集上有良好表现的预处理标准流程.

自然灰度图像

灰度图像具有平稳特性,我们通常在第一步对每个数据样本分别做均值消减(即减去直流分量),然后采用 PCA/ZCA 白化处理,其中的 epsilon 要足够大以达到低通滤波的效果。

彩色图像

对于彩色图像,色彩通道间并不存在平稳特性。因此我们通常首先对数据进行特征缩放(使像素值位于 [0,1] 区间),然后使用足够大的 epsilon 来做 PCA/ZCA。注意在进行 PCA 变换前需要对特征进行分量均值归零化。

音频 (MFCC/频谱图)

对于音频数据 (MFCC 和频谱图),每一维度的取值范围(方差)不同。例如 MFCC 的第一分量是直流分量,通常其幅度远大于其他分量,尤其当特征中包含时域导数 (temporal derivatives) 时(这是音频处理中的常用方法)更是如此。因此,对这类数据的预处理通常从简单的数据标准化开始(即使得数据的每一维度均值为零、方差为 1),然后进行 PCA/ZCA 白化(使用合适的 epsilon)。

MNIST 手写数字

MNIST 数据集的像素值在 [0,255] 区间中。我们首先将其缩放到 [0,1] 区间。实际上,进行逐样本均值消去也有助于特征学习。注:也可选择以对 MNIST 进行 PCA/ZCA 白化,但这在实践中不常用。

图像预处理

Mean Subtraction

data normalization

目的:减少不同图片受光照变化的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cv2

class MeanPreprocessor:
def __init__(self, rMean, gMean, bMean):
# store the Red, Green, and Blue channel averages across a
# training set
self.rMean = rMean
self.gMean = gMean
self.bMean = bMean

def preprocess(self, image):
# split the image into its respective Red, Green, and Blue
# channels
(B, G, R) = cv2.split(image.astype("float32"))

# subtract the means for each channel
R -= self.rMean
G -= self.gMean
B -= self.bMean

# merge the channels back together and return the image
return cv2.merge([B, G, R])

Patch Extraction

从原始图像中随机采样MxN的区域,当原始图像的稀疏度较高时可以采用该方法。

降低过拟合的概率

256x256 ===> 227x227

1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.feature_extraction.image import extract_patches_2d

class PatchPreprocessor:
def __init__(self, width, height):
# store the target width and height of the image
self.width = width
self.height = height

def preprocess(self, image):
# extract a random crop from the image with the target width
# and height
return extract_patches_2d(image, (self.height, self.width),
max_patches=1)[0]

Cropping(Over-Sampling)

使用扣取方法可以从原始图像的四个角+中心位置进行扣取,实验证明该方法可以提升1-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
import numpy as np
import cv2

class CropPreprocessor:
def __init__(self, width, height, horiz=True, inter=cv2.INTER_AREA):
# store the target image width, height, whether or not
# horizontal flips should be included, along with the
# interpolation method used when resizing
self.width = width
self.height = height
self.horiz = horiz
self.inter = inter

def preprocess(self, image):
# initialize the list of crops
crops = []

# grab the width and height of the image then use these
# dimensions to define the corners of the image based
(h, w) = image.shape[:2]
coords = [
[0, 0, self.width, self.height],
[w - self.width, 0, w, self.height],
[w - self.width, h - self.height, w, h],
[0, h - self.height, self.width, h]]

# compute the center crop of the image as well
dW = int(0.5 * (w - self.width))
dH = int(0.5 * (h - self.height))
coords.append([dW, dH, w - dW, h - dH])

# loop over the coordinates, extract each of the crops,
# and resize each of them to a fixed size
for (startX, startY, endX, endY) in coords:
crop = image[startY:endY, startX:endX]
crop = cv2.resize(crop, (self.width, self.height),
interpolation=self.inter)
crops.append(crop)

# check to see if the horizontal flips should be taken
if self.horiz:
# compute the horizontal mirror flips for each crop
mirrors = [cv2.flip(c, 1) for c in crops]
crops.extend(mirrors)

# return the set of crops
return np.array(crops)

Keras中的数据预处理功能

http://keras-cn.readthedocs.io/en/latest/preprocessing/sequence/

A. 设置随机种子

1
np.random.seed(1337)  # for reproducibility

B. 输入数据维度规格化,这里每个样本只是size为784的一维数组。

1
X_train = X_train.reshape(60000, 784)

将类别标签转换为one-hot encoding, 这一步对多分类是必须的

1
one_hot_labels  = keras.utils.np_utils.to_categorical(labels, num_classes=10)

C. 输入数据类型转换,数值归一化

1
2
X_train = X_train.astype('float32')
X_train /= 255

序列预处理

文本预处理

图片预处理

样本数据序列化为HDF5文件

目的:减少多次IO读取的延时

参考

  1. 《解析卷积神经网络—深度学习实践手册》
  2. Keras文档
  3. http://deeplearning.stanford.edu/wiki/index.php/%E6%95%B0%E6%8D%AE%E9%A2%84%E5%A4%84%E7%90%86

Tips for Keras

撰写一篇博文用于记录Keras使用过程中的一些关键用法,以方便速查。


Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×