编者按:环信开源国内首本免费深度学习理论和实战专著《深度学习理论与实战:提高篇 》,内容涵盖听觉,视觉,语言和强化学习,七百多页详尽的理论和代码分析。本文节选自《深度学习理论与实战:提高篇 》一书,原文链接http://fancyerii.github.io/2019/03/14/dl-book/ 。作者李理,环信人工智能研发中心vp,有十多年自然语言处理和人工智能研发经验,主持研发过多款智能硬件的问答和对话系统,负责环信中文语义分析开放平台和环信智能机器人的设计与研发。

深度学习.jpg

目录

  • 安装

  • demo.ipynb

    • 运行

    • 关键代码

  • train_shapes.ipynb

    • 配置

    • Dataset

    • 创建模型

    • 训练

    • 检测

    • 测试

  • inspect_data.ipynb

    • 选择数据集

    • 加载Dataset

    • 显示样本

    • Bounding Box

    • Mini Masks

    • Anchor

    • 训练数据生成器

Facebook(Mask R-CNN的作者He Kaiming等人目前在Facebook)的实现在这里。但是这是用Caffe2实现的,本书没有介绍这个框架,因此我们介绍Tensorflow和Keras的版本实现的版本。但是建议有兴趣的读者也可以尝试一下Facebook提供的代码。

安装

git clone https://github.com/matterport/Mask_RCNN.git
# 或者使用作者fork的版本
git clone https://github.com/fancyerii/Mask_RCNN.git

#建议创建一个virtualenv
pip install -r requirements.txt

# 还需要安装pycocotools
# 否则会出现ImportError: No module named 'pycocotools'
# 参考 https://github.com/matterport/Mask_RCNN/issues/6

pip install "git+https://github.com/philferriere/cocoapi.git#egg=pycocotools&subdirectory=PythonAPI"

demo.ipynb

1、运行

jupyter notebook
打开文件samples/demo.ipynb,运行所有的Cell

2、关键代码

这里是使用预训练的模型,会自动上网下载,所以第一次运行会比较慢。这是下载模型参数的代码:

COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")
# Download COCO trained weights from Releases if needed
if not os.path.exists(COCO_MODEL_PATH):
utils.download_trained_weights(COCO_MODEL_PATH)

创建模型和加载参数:

# 创建MaskRCNN对象,模式是inferencemodel = modellib.MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=config)

# 加载模型参数 model.load_weights(COCO_MODEL_PATH, by_name=True)

读取图片并且进行分割:

# 随机加载一张图片
file_names = next(os.walk(IMAGE_DIR))[2]
image = skimage.io.imread(os.path.join(IMAGE_DIR, random.choice(file_names)))

# 进行目标检测和分割
results = model.detect([image], verbose=1)

# 显示结果
r = results[0]
visualize.display_instances(image, r['rois'], r['masks'], r['class_ids'],
class_names, r['scores'])

检测结果r包括rois(RoI)、masks(对应RoI的每个像素是否属于目标物体)、scores(得分)和class_ids(类别)。

下图是运行的效果,我们可以看到它检测出来4个目标物体,并且精确到像素级的分割处理物体和背景。

潮科技行业入门指南 | 深度学习理论与实战:提高篇(14)——Mask R-CNN代码简介

图:Mask RCNN检测效果

train_shapes.ipynb

除了可以使用训练好的模型,我们也可以用自己的数据进行训练,为了演示,这里使用了一个很小的shape数据集。这个数据集是on-the-fly的用代码生成的一些三角形、正方形、圆形,因此不需要下载数据。

1、配置

代码提供了基础的类Config,我们只需要继承并稍作修改:

class ShapesConfig(Config):
"""用于训练shape数据集的配置
继承子基本的Config类,然后override了一些配置项。
"""
# 起个好记的名字
NAME = "shapes"

# 使用一个GPU训练,每个GPU上8个图片。因此batch大小是8 (GPUs * images/GPU).
GPU_COUNT = 1
IMAGES_PER_GPU = 8

# 分类数(需要包括背景类)
NUM_CLASSES = 1 + 3 # background + 3 shapes

# 图片为固定的128x128
IMAGE_MIN_DIM = 128
IMAGE_MAX_DIM = 128

# 因为图片比较小,所以RPN anchor也是比较小的
RPN_ANCHOR_SCALES = (8, 16, 32, 64, 128) # anchor side in pixels

# 每张图片建议的RoI数量,对于这个小图片的例子可以取比较小的值。
TRAIN_ROIS_PER_IMAGE = 32

# 每个epoch的数据量
STEPS_PER_EPOCH = 100

# 每5步验证一下。
VALIDATION_STEPS = 5

config = ShapesConfig()
config.display()

2、Dataset

对于我们自己的数据集,我们需要继承utils.Dataset类,并且重写如下方法:

  • load_image

  • load_mask

  • image_reference

在重写这3个方法之前我们首先来看load_shapes,这个函数on-the-fly的生成数据。

class ShapesDataset(utils.Dataset):
"""随机生成shape数据。包括三角形,正方形和圆形,以及它的位置。
这是on-the-fly的生成数据,因此不需要访问文件。
"""

def load_shapes(self, count, height, width):
"""生成图片
count: 返回的图片数量
height, width: 生成图片的height和width
"""
# 类别
self.add_class("shapes", 1, "square")
self.add_class("shapes", 2, "circle")
self.add_class("shapes", 3, "triangle")

# 注意:这里只是生成图片的specifications(说明书),
# 具体包括性质、颜色、大小和位置等信息。
# 真正的图片是在load_image()函数里根据这些specifications
# 来on-the-fly的生成。
for i in range(count):
bg_color, shapes = self.random_image(height, width)
self.add_image("shapes", image_id=i, path=None,
width=width, height=height,
bg_color=bg_color, shapes=shapes)

其中add_image是在基类中定义:

def add_image(self, source, image_id, path, **kwargs):
image_info = {
"id": image_id,
"source": source,
"path": path,
}
image_info.update(kwargs)
self.image_info.append(image_info)

它有3个命名参数source、image_id和path。source是标识图片的来源,我们这里都是固定的字符串”shapes”;image_id是图片的id,我们这里用生成的序号i,而path一般标识图片的路径,我们这里是None。其余的参数就原封不动的保存下来。

random_image函数随机的生成图片的位置,请读者仔细阅读代码注释。

def random_image(self, height, width):
"""随机的生成一个specifications
它包括图片的背景演示和一些(最多4个)不同的shape的specifications。
"""
# 随机选择背景颜色
bg_color = np.array([random.randint(0, 255) for _ in range(3)])
# 随机生成一些(最多4个)shape
shapes = []
boxes = []
N = random.randint(1, 4)
for _ in range(N):
# random_shape函数随机产生一个shape(比如圆形),它的颜色和位置
shape, color, dims = self.random_shape(height, width)
shapes.append((shape, color, dims))
# 位置是中心点和大小(正方形,圆形和等边三角形只需要一个值表示大小)
x, y, s = dims
# 根据中心点和大小计算bounding box
boxes.append([y-s, x-s, y+s, x+s])
# 使用non-max suppression去掉重叠很严重的图片
keep_ixs = utils.non_max_suppression(np.array(boxes), np.arange(N), 0.3)
shapes = [s for i, s in enumerate(shapes) if i in keep_ixs]
return bg_color, shapes

随机生成一个shape的函数是random_shape:

def random_shape(self, height, width):
"""随机生成一个shape的specifications,
要求这个shape在height和width的范围内。
返回一个3-tuple:
* shape名字 (square, circle, ...)
* shape的颜色:代表RGB的3-tuple
* shape的大小,一个数值
"""
# 随机选择shape的名字
shape = random.choice(["square", "circle", "triangle"])
# 随机选择颜色
color = tuple([random.randint(0, 255) for _ in range(3)])
# 随机选择中心点位置,在范围[buffer, height/widht - buffer -1]内随机选择
buffer = 20
y = random.randint(buffer, height - buffer - 1)
x = random.randint(buffer, width - buffer - 1)
# 随机的大小size
s = random.randint(buffer, height//4)
return shape, color, (x, y, s)

上面的函数是我们为了生成(或者读取磁盘的图片)而写的代码。接下来我们需要重写上面的三个函数,我们首先来看load_image:

def load_image(self, image_id):
"""根据specs生成实际的图片
如果是实际的数据集,通常是从一个文件读取。
"""
info = self.image_info[image_id]
bg_color = np.array(info['bg_color']).reshape([1, 1, 3])
# 首先填充背景色
image = np.ones([info['height'], info['width'], 3], dtype=np.uint8)
image = image * bg_color.astype(np.uint8)
# 分别绘制每一个shape
for shape, color, dims in info['shapes']:
image = self.draw_shape(image, shape, dims, color)
return image

上面的函数会调用draw_shape来绘制一个shape:

def draw_shape(self, image, shape, dims, color):
"""根据specs绘制shape"""
# 获取中心点x, y和size s
x, y, s = dims
if shape == 'square':
cv2.rectangle(image, (x-s, y-s), (x+s, y+s), color, -1)
elif shape == "circle":
cv2.circle(image, (x, y), s, color, -1)
elif shape == "triangle":
points = np.array([[(x, y-s),
(x-s/math.sin(math.radians(60)), y+s),
(x+s/math.sin(math.radians(60)), y+s),
]], dtype=np.int32)
cv2.fillPoly(image, points, color)
return image

这个函数很直白,使用opencv的函数在image上绘图,正方形和圆形都很简单,就是等边三角形根据中心点和size(中心点到顶点的距离)求3个顶点的坐标需要一些平面几何的知识。

接下来是load_mask函数,这个函数需要返回图片中的目标物体的mask。这里需要稍作说明。通常的实例分隔数据集同时提供Bounding box和Mask(Bounding的某个像素是否属于目标物体)。为了更加通用,这里假设我们值提供Mask(也就是物体包含的像素),而Bounding box就是包含这些Mask的最小的长方形框,因此不需要提供。

对于我们随机生成的性质,只要知道哪种shape以及中心点和size,我们可以计算出这个物体(shape)到底包含哪些像素。对于真实的数据集,这通常是人工标注出来的。

def load_mask(self, image_id):
"""生成给定图片的mask
"""
info = self.image_info[image_id]
shapes = info['shapes']
count = len(shapes)
# 每个物体都有一个mask矩阵,大小是height x width
mask = np.zeros([info['height'], info['width'], count], dtype=np.uint8)
for i, (shape, _, dims) in enumerate(info['shapes']):
# 绘图函数draw_shape已经把mask绘制出来了。我们只需要传入特殊颜色值1。
mask[:, :, i:i+1] = self.draw_shape(mask[:, :, i:i+1].copy(),
shape, dims, 1)
# 处理遮挡(occlusions)
occlusion = np.logical_not(mask[:, :, -1]).astype(np.uint8)
for i in range(count-2, -1, -1):
mask[:, :, i] = mask[:, :, i] * occlusion
occlusion = np.logical_and(occlusion, np.logical_not(mask[:, :, i]))
# 类名到id
class_ids = np.array([self.class_names.index(s[0]) for s in shapes])
return mask.astype(np.bool), class_ids.astype(np.int32)

处理遮挡的代码可能有些tricky,不过这都不重要,因为通常的训练数据都是人工标注的,我们只需要从文件读取就行。这里我们值需要知道返回值的shape和含义就足够了。最后是image_reference函数,它的输入是image_id,输出是正确的分类。

def image_reference(self, image_id):
info = self.image_info[image_id]
if info["source"] == "shapes":
return info["shapes"]
else:
super(self.__class__).image_reference(self, image_id)

上面的代码还判断了一些info[“source”],如果是”shapes”,说明是我们生成的图片,直接返回shape的名字,否则调用基类的image_reference。下面我们来生成一些图片看看。

# 训练集500个图片
dataset_train = ShapesDataset()
dataset_train.load_shapes(500, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1])
dataset_train.prepare()

# 验证集50个图片
dataset_val = ShapesDataset()
dataset_val.load_shapes(50, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1])
dataset_val.prepare()


image_ids = np.random.choice(dataset_train.image_ids, 4)
for image_id in image_ids:
image = dataset_train.load_image(image_id)
mask, class_ids = dataset_train.load_mask(image_id)
visualize.display_top_masks(image, mask, class_ids, dataset_train.class_names)

随机生成的图片如下图所示,注意,因为每次都是随机生成,因此读者得到的结果可能是不同的。左图是生成的图片,右边是mask。

潮科技行业入门指南 | 深度学习理论与实战:提高篇(14)——Mask R-CNN代码简介

图:随机生成的Shape图片