AI深度学习入门与实战19 语义分割:打造简单高效的人像分割模型
上一讲,我向你介绍了语义分割的原理。在理解上一课时中 U-Net 语义分割网络的基础上,这一讲,让我们来实际构建一个人像分割模型吧。
语义分割的评估
我们先简单回顾一下语义分割的目的:把一张图中的每一个像素进行预测,并将它们分成对应的类别。如下图所示:
图 1:图像分割举例
你可能注意到,在图中有一个新的概念Ground Truth(GT),它在目标检测与语义分割项目中十分常见。其实 GT 实际的表现形式就是我们下面会讲到的 mask。
GT 也就是真实的类别、真实的结果,在语义分割中指每个像素真实的类别,这些类别是我们人工标记好的。所以在语义分割任务中,我们希望模型的输出(Prediction),能够尽可能高精度地与 GT 重合。
那如何衡量这种重合程度呢?
在图像分割中,我们会采用一个新的指标来衡量模型分割的好坏,我们把它叫作 IoU(Intersection over Union,交并比),用来衡量重合程度的高低。
关于 IoU 的计算过程也很简单,我们来看一个例子:
# 为了易于理解,我们先定义几个方法
def count(area):"""返回给定区域元素的个数"""return area 的元素个数
def intersection(area1, area2):"""返回 area1 与 area2 的交集"""return area1 与 area2 的交集
def union(area1, area2):"""返回 area1 与 area2 的并集"""return area1 与 area2 的并集
IoU = intersection(GT, Prediction) / union(GT, Prediction)
案例中 IoU 的取值范围是 0~1,其中 0 代表预测部分完全没有覆盖到真实区域,1 代表我们的预测完美地覆盖到了真实区域。
利用下面的例子,我们来可视化地了解一下 IoU 是如何计算。下图左边是 GT,右边是我们模型的预测结果。
图 2:GT 与 Prediction
分子是 GT 与预测结果的交集(下图左边的图片),分母是 GT 与预测结果的并集(下面右边的图片),用分子除以分母就是我们需要的 IoU,即 Intersection / Union。
图 3: GT 与 Prediction的交集与并集
我们可以使用 NumPy 编写一段 IoU 的计算程序:
intersection = np.logical_and(target, prediction)
union = np.logical_or(target, prediction)
iou_score = np.sum(intersection) / np.sum(union)
在评价过程中,每个类别都会有一个 IoU。IoU 是语义分割中经常采用的一种评价方式,无论是论文还是各种比赛里都会看到。也许你会好奇,为什么不使用像素的精确度来衡量语义分割模型呢?
其实也不是不可以,只不过在类别严重不平衡的时候,像素的精确度会严重误导我们,无法客观有效地评价模型。
假设有如下场景:
一张图片有 100 个像素;
只有背景与类别 A,且类别 A 的 GT 的像素个数为 5;
模型将整张图片都预测为背景。
具体如下图所示:
图 4:整张图片都被预测为背景
在这个假设的场景下,模型将整张图片都预测为背景,也就是在这张图中,模型有 5 个像素预测错了,那此时模型像素的精确度为:
accuracy = 95 / 100 = 0.95
显然,在这个类别严重不平衡的场景下,用 95%的精准度来衡量我们的模型是不合理的。
我们希望模型能更好地分割出类别 A。但在这个场景中,我们的模型没有做到这一点,同时还有一个很高的精确度,这个精确度并不能反映出模型的实际情况。
为了避免这种情况,有时候我们会使用所有类别的 IoU 的均值来综合衡量当前模型,我们把这个均值称为mean IoU,简称mIoU。mIoU 会通过所有类别的 IoU 的均值来综合衡量当前模型,这样得到的数据也能更准确地反映模型的实际情况。
我们来看类别 A 的 IoU 以及模型的 mIoU 分别是多少。
类别 A 的 IoU:
IoU_A = intersection(GT_A, Prediction_A) / union(GT_A, Prediction_A)
= 0 / (5 + 0 - 0) = 0
背景的 IoU:
IoU_BG = intersection(GT_BG, Prediction_BG) / union(GT_BG, Prediction_BG)
= 95 / (95 + 100 - 95) = 0.95
mIoU = (IoU_A + IoU_BG) / 2 = (0 + 0.95) = 0.475
通过计算可以发现,类别 A 的 IoU 是 0%,模型的 mIoU 是 47.5%。
虽然模型的性能依然很差,无法很好地分割类别 A,但它的评估指标也不是很高。相比 95%的精确度,mIoU 的 47.5% 显然更加客观、合理。你可以根据 mIoU 反映的情况查看是哪里出了问题,然后进一步提升模型的性能。
从这一简单的场景中能很明显地看到,在数据量严重不平衡的时候,像素的精确度是无法衡量我们的模型的。模型如果将所有的像素都预测为背景,模型的精确度再高都没有用。所以,mIoU 是我们在语义分割中经常使用的一个指标。
语义分割网络的训练
在介绍“如何训练语义分割网络”前,我要引入一个概念mask 图片。mask 图片一般是一张单通道的图片,里面的数值标记对应图片在相同位置的像素类别。
以人像分割举例,我们的目的是把一张图片中的人像与背景分割出来。那么人像的像素在 mask 中对应的位置就是 1,背景就是 0。
1 代表人像这个类别,0 代表背景这个类别。
当然,根据项目的不同,数值是可以任意取的。
刚才讲到的 GT 与语义分割模型的最终的输出都是一个 mask。不过对于 GT 的 mask,我们经常会省略 mask,直接叫 GT。
在训练语义分割模型的时候,训练数据一般包含 2 个文件夹。
JPEGImages文件夹:存放原图。
SementationClass文件夹:存放 GT 的 mask 图片。
以上 2 个文件夹的名字并没有硬性要求,举例的只是较为常用的名字。
以下 2 张图片分别是原图与 mask 图片:
图 5:原图和 mask 图片
为什么需要 mask 图片呢?
机器学习或者说深度学习的目标就是让机器像人一样思考,但机器怎么像人一样思考呢?答案是人来教机器如何思考。
在《16 | 图像分类:技术背景与常用模型解析》中,我们学习了图像分类模型的训练方式。图像分类模型最初所有的参数都是随机初始化的,所以我们要把训练数据中的图片提前分好类,然后在训练时,通过不断地前向/后向传播来降低 loss(也就是让我们预测的类别无限的接近真实的类别),从而更新网络参数。这个真实的类别需要我们事先人工分好。
例如我们要训练一个模型来分类猫和狗,那么在训练前,我们要把狗的图片放到 dog 文件夹里,猫的图片放到 cat 文件夹里。在训练的时候,模型会读取对应类别文件夹的数据进行训练。
图 6:cat 与 dog 文件夹
那么在语义分割中,应该如何训练呢?
语义分割的目的是把每一个像素都分成相应的类别,但是我们又怎么把图像中的像素,像图片那样提前分好类别呢?
在语义分割中我们首先要定义模型的目标,即 GT,告诉模型每张图片每个像素所属的类别。在训练过程中,模型会不断更新参数来减小 loss。这样,我们就可以获得一个语义分割的模型了。
图片标记工具
在这里我要向你介绍一下如何标记语义分割的数据(图片),生成 GT。
好的数据集是获得一个高精度模型的第一步,尤其是在我们的语义分割任务中。现在有很多公开的数据集可以用,例如 COCO Person、VOC Person、Supervisely。但是这些公开数据中,有些数据可能并不适合我们的场景,并且标记的质量不是很高。
我们可以下载开源的人像数据集:Supervisely Dataset(需要自己注册账号)与 AISegment。你可以分别点击链接获取。
Supervisely Dataset:一共有 5711 张图片,图片标记质量高,包含多人的图片。
AISegment:目前最大的公开数据集,包含 34427 张高质量的标注图片。用这部分数据训练的模型已经投入商用。
为了提高模型的健壮性与精度,我们经常需要收集一些符合我们应用场景的图片,然后将其标记。
我们标记的工具叫作Labelme,安装地址为https://github.com/wkentaro/labelme。
标记共分为 6 个步骤。
(1)将需要标记的图片放入一个文件夹。
(2)准备一个 labels.txt 文件,内容是需要标记类别的名字。
因为是人像分割任务,所以 labels.txt 的内容是_background_与 person 就可以了,每一类占用一行,如下所示:
_background_
person
(3)在命令行中输入以下代码:
labelme --labels labels.txt --nodata
然后会出现一个界面:
图 7:标记界面
(4)点击 open dir,选择第一步中的文件夹,将需要标记的图片导入进来。
图 8:需要标记的原图
(5)点击 Create Polygons,把需要标记的每一类都框起来。
在人像分割任务中,你需要把图中的人物沿着人物的轮廓标记出来。需要注意的是,标记的时候起点和终点必须是同一个地方。在标记完成之后,从弹出的对话框中选择对应的类别。如图所示:
图 9:标记图片
每编辑完一张图,点击 save,就会自动生成一张同名的 json 文件。
(6)在命令行中执行下面的命令,就可以将标记好的图片转换为一张 mask 图片。
labelme_json_to_dataset json 文件名
执行完上述的命令之后,会生成下面的文件,其中 label.png 就是我们所需要的 GT 的 mask。
图 10:标记完成
到这里,咱们的标记就算完成了。
视频虚拟背景:人像分割
这篇文章写于 2020 年,这一年对所有人来说都是不平凡的一年。突如其来的疫情席卷了全球,在家办公成了常态。在家使用视频会议时,你肯定接触过虚拟背景这个功能。
所谓人像分割模型,就是把一张图片作为输入,有人的部分作为前景输出。一般,人的部分作为类别 1 输出,而背景则作为类别 0 输出。
接下来,我们就要具体来实现一个 U-Net,用它来训练一个人像分割模型了。我会从以下 4 个方面进行介绍:数据加载、网络结构定义、层的定义、损失函数。
为什么主要介绍这 4 个方面呢?因为机器学习任务离不开数据加载、计算 loss、更新权重这三大块。其中,权重就是网络结构中的参数,网络结构中的参数又体现于 U-Net 的网络结构与层的定义。
数据加载
在我们的人像分割项目中,我们要把原图(JPEGImages)的数据读取到一个 NumPy 的数组中,GT(SegementationClass)的数据读取到另一个 NumPy 的数组中,这一过程就是数据加载。
我们先创建一个工作目录,叫 u-net-dev。在 u-net-dev 下面创建一个 data_set 文件夹,用来存放我们的数据。
我从 VOC 数据集中把和人有关的图片都挑选出来了,把数据加载成 NumPy 的数组,提供给模型训练。
图 11:界面展示
JPEGImages 文件夹用于存放原图,SegmentationClass 文件夹用于存放对应的 GT。
在工作目录下面创建一个 utils 文件夹,用来存放与本项目有关的一切工具代码,包括 loader.py 和 dataset.py。
loader.py:所有和数据相关的代码都会写在 loader.py 文件中。
dataset.py:定义一个 DataSet 类别,用来抽象我们的数据。
完成上面 2 个 py 文件之后,我们只需要在训练时通过下面的代码即可导入训练数据:
from util import loader as data_loader
def load_dataset(train_rate):"""按比例将数据分割为训练数据与评估数据
Args:
train_rate: 训练数据所占的比例
Returns:
训练集、测试集
“”"
loader = data_loader.Loader(dir_original=“data_set/JPEGImages”,
dir_segmented=“data_set/SegmentationClass”)
return loader.load_train_test(train_rate=train_rate, shuffle=False)
train, test = load_dataset(train_rate=parser.trainrate)
具体的 loader.py 与 dataset.py 的定义,因为代码过长,我将其放在了 GitHub 中,你可以点击相应的链接,我已经写好了注释。
U-Net 网络结构
U-Net 网络结构是我们的重中之重,有了它模型可以知道以何种方式进行前向传播,分割图片,推断出图片中的前景(人像)与背景。我在《18|语义分割:技术背景与算法剖析》向你介绍了 U-Net 的运作原理,这里我们就来看 U-Net 的网络结构。
我把 U-Net 的网络定义命名为 unet.py,也放在 util 文件夹中。
这里我定义的是 1 个输入输出大小均为 256x256 的 U-Net,网络结构和参数与我在“18 课时”中介绍一样。
有关 unet.py 的定义,你可以点击链接查看,我已经写好注释了。
层的定义
完成 U-Net 的网络定义之后,我们就算是搭建好了一个模型的主要框架。这个框架中的细节该如何实现呢?方法就是层的定义。接下来我们看一下 U-Net 使用到层的具体实现。
我定义了 U-Net 网络中使用到的一些层与操作。
conv:卷积层。
pooling:池化层。
up_conv:上采样层。
copy_and_crop: 连接操作。
我们把这些层与操作写在 layers.py 文件中,也把它放在 util 中。
有关 layers.py 的定义,我同样放在了 GitHub 中,你可以点击链接查看,我已经写好注释了。
损失函数
我们的人像分割问题也是分类问题,所以可以使用常见的交叉熵损失函数,一般有 Sigmoid 交叉熵损失函数与 Softmax 交叉熵损失函数:
Sigmoid 函数可以把一个数值映射到(0, 1)的区间内,所以经常用在二分类任务中;
Softmax 函数可以把一组数中的每个数映射到(0, 1)的区间内,并且和是 1,所以 Softmax 函数经常用于多分类。
我们的人像分割模型是一个二分类任务,可以使用上述交叉熵损失函数的任意一种。
接下来,我们就看看该如何定义我们的损失函数以及优化方法吧。我选择的是 Softmax 交叉熵损失函数。当然,你也可以换成 Sigmoid 交叉熵损失。具体代码如下:
# 加载数据,刚才已经讲过了
def load_dataset(train_rate):loader = ld.Loader(dir_original="data_set/VOCdevkit/person/JPEGImages",dir_segmented="data_set/VOCdevkit/person/SegmentationClass")return loader.load_train_test(train_rate=train_rate)
def train():# 加载数据train, test = load_dataset(train_rate=parser.trainrate)# 创建模型model = U_Net()# 使用 Softmax 交叉熵损失函数以及 Adam 优化方法cross_entropy = tf.reduce_mean(tf.nn.Softmax_cross_entropy_with_logits(labels=model.labels, logits=model.predict))update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)with tf.control_dependencies(update_ops):train_step = tf.train.AdamOptimizer(0.001).minimize(cross_entropy)
训练中最主要的几个环节我已经讲完了,我们来回顾一下训练流程,把它们串起来。
加载数据,生成训练集与验证集(参考 loader.py)。
设定 batch_size 与 epoch 数, 以 batch_size 为单位循环读取训练集。
利用每个 batch 的数据进行前向传播与反向传播,计算 loss,更新权重。
根据我们写好的 u-net 的网络结构进行前向传播,这里会用到 layers.py 中使用到的层。
loss 我们也已经定义好了。
反向传播与更新权重是由我们使用的框架(TensorFlow)来帮助我们完成。
通过在《15 | TensorBoard:实验统计分析助手》中学习的 TensorBoard 观察每个 batch 或者每个 epoch 的 loss,检验我们模型训练是否有问题。
保存模型。
评估模型,计算 mIoU。
如果 mIoU 的结果越接近 1,那么你建立的人像分割模型就越好。
结语
恭喜你,你已经可以自己建立一个人像分割模型了。训练模型是一个需要根据结果优化的过程,要想直接建立一个性能非常好的人像分割模型是很难的。
那么,对于人像分割模型,你觉得从哪些方面着手,可以提高模型的精度呢?欢迎在留言区以及在交流群中分享你的问题和回答。
下一讲,我将带你了解“文本分类”,这是我们要实现的最后一个应用场景了。
精选评论
AI深度学习入门与实战19 语义分割:打造简单高效的人像分割模型相关推荐
- AI深度学习入门与实战21 文本分类:用 Bert 做出一个优秀的文本分类模型
在上一讲,我们一同了解了文本分类(NLP)问题中的词向量表示,以及简单的基于 CNN 的文本分类算法 TextCNN.结合之前咱们学习的 TensorFlow 或者其他框架,相信你已经可以构建出一个属 ...
- AI深度学习入门与实战15 TensorBoard:实验统计分析助手
在 13 和 14 讲中,我们一同了解了 TensorFlow.通过 TensorFlow,我们可以将设计好的理论模型变成实际可用的真正的模型.这一讲,我们将学习一个高效的实验分析助手:TensorB ...
- PyTorch深度学习入门与实战(案例视频精讲)
作者:孙玉林,余本国 著 出版社:中国水利水电出版社 品牌:智博尚书 出版时间:2020-07-01 PyTorch深度学习入门与实战(案例视频精讲)
- Tensorflow2.0深度学习入门与实战(日月光华)(学习总结1)
Tensorflow2.0深度学习入门与实战(学习总结1) 我是刚学的,网易云课堂跟着日月光华老师,现在对每节课的学习课程做一下记录,总结,仅仅作为总结. 1.使用快捷键 shift+enter执行代 ...
- 【深度学习】用于小目标检测的一个简单高效的网络
导读 以Tiny YOLOV3的速度达到YOLOV3的效果. 1. 介绍 本文提出一种专门用于检测小目标的框架,框架结构如下图: 我们探索了可以提高小目标检测能力的3个方面:Dilated模块,特征融 ...
- Pytorch深度学习入门与实战(笔记)
目录 第二章 1.张量数据类型 (1)查看张量数据类型 (2)修改张量数据类型
- pytorch深度学习入门与实战——今天我们来对一张图像进行卷积、池化,以及激活层的使用展示
import numpy as np import torch import torch.nn as nn import matplotlib.pyplot as plt from PIL impor ...
- Pytorch深度学习入门与实战一--全连接神经网络
全连接神经网络在分类和回归问题中都非常有效,本节对全连接神经网及神经元结构进行介绍,并使用Pytorch针对分类和回归问题分别介绍全连接神经网络的搭建,训练及可视化相关方面的程序实现. 1.全连接神经 ...
- 深度学习入门系列6项目实战:声纳回声识别
大家好,我技术人Howzit,这是深度学习入门系列第六篇,欢迎大家一起交流! 深度学习入门系列1:多层感知器概述 深度学习入门系列2:用TensorFlow构建你的第一个神经网络 深度学习入门系列3: ...
最新文章
- Python爬虫应用实战案例-xpath正则表达式使用方法,爬取精美壁纸
- PHP中的addslashes()
- Jquery调用ajax参数说明
- 通过NHibernate ORM和CodeDom在任何数据库中自动生成表以生成代码
- Java动态绑定与多态
- matlab 深度学习环境配置
- 电脑桌面云便签怎么新建分类文件夹标签?
- 自动驾驶基础——惯性测量单元(IMU)
- C语言实验——圆周率 sdut oj
- html页面中文本框的光标控制
- 手机电脑传文件?教你三种最简单的方法!
- 浏览器打开任意可执行exe文件方法
- 大工19春计算机文化基础 在线测试,大工19春《计算机文化基础》在线测试123满分答案...
- 数字电子技术设计--彩灯控制电路的设计与制作
- 求n位水仙花数(C语言实现)
- mysql函数之日期函数
- java实验八 内部类,匿名类
- 7个好用的运维监控工具,你在用哪个?
- 淘宝sku API接口实例代码
- gperftools安装和使用