第9篇 Fast AI深度学习课程——多目标识别与定位

一、一个模型同时实现单目标识别与定位

在上一节中,我们先构建了一个分类网络,用于图片中最大目标的类别划分;然后构建了一个用于输出目标坐标的网络。我们尚未将两个网络联系起来。但事实上,两个网络的架构十分相似(都是基于resnet34)。那么能否去除这种冗余,使用一个网络同时实现目标分类与定位呢?本部分将按照:准备数据—构建网络—定义优化目标这一分解步骤,来展示针对应用场景进行建模的通用流程。

1. 准备数据

数据分为自变量和因变量两部分,自变量自然就是图片了。无论是分类还是定位,在构建网络时针对图片所做的操作,均可通用,因此,这一部分不用考虑。而对于因变量,需要将目标类别和定位坐标结合在一起。但两者一个是连续型的,一个是离散型的;因此,在生成数据文件CSV文件时进行合并,无益于后续处理。课程中给出的方法是:针对角点坐标和类别标签,生成两个数据集,然后将两个数据集的dataset拼接起来(dataset实际上即为存储数据的地方)。拼接方法是:在获取数据时(即调用__getitem__()函数时),同时返回角点坐标和类别标签。

md = ImageClassifierData.from_csv(PATH, JPEGS, BB_CSV, tfms=tfms, 
                   continuous=True, val_idxs=val_idxs)
md2 = ImageClassifierData.from_csv(PATH, JPEGS, CSV,
                   tfms=tfms_from_model(f_model, sz))
class ConcatLblDataset(Dataset):
    def __init__(self, ds, y2): self.ds,self.y2 = ds,y2
    def __len__(self): return len(self.ds)
    
    def __getitem__(self, i):
        x,y = self.ds[i]
        return (x, (y,self.y2[i]))

    md.trn_dl.dataset = ConcatLblDataset(md.trn_ds, md2.trn_y)
    md.val_dl.dataset = ConcatLblDataset(md.val_ds, md2.val_y)

事实上,ConcatLblDataset()的第二个参数为一个可迭代对象,且和md的数据的文件索引相对应即可。

后面可考虑继承DataSet类。

2. 网络架构

在分类网络与回归网络的共有部分的基础上,再添加附加层以输出分类和定位所需的数值:共需要4+c个输出,其中c为类别数目。

head_reg4 = nn.Sequential(
    Flatten(),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(25088,256),
    nn.ReLU(),
    nn.BatchNorm1d(256),
    nn.Dropout(0.5),
    nn.Linear(256,4+len(cats)),
)
models = ConvnetBuilder(f_model, 0, 0, 0, custom_head=head_reg4)

其中ConvenetBuilder中取值为0的3个参数分别表示:全连接层的节点数目、是否为多分类、是否为回归问题,但在设定custom_head后不起作用。

3. 损失函数

将定位网络的L1范数误差和分类网络的交叉熵加权求和,即得到所需的损失函数。其中要点如下:

  • 损失函数为接受inputtarget参数、返回一个数值的函数。其中input即每个数据块经过网络的前向传播后所得的结果,target即为每个数据块的y值(角点值,类别标签)。
  • 损失函数中的数据大部分为torch.Variable类型,以用于梯度计算。
  • 通过设置学习器的crit域来设置损失函数,其接受一个函数。通过设置学习器的metrics来显示训练过程中的指标,其接受一个函数列表。

后续训练过程就没啥新鲜东西了。

二、多目标的识别与定位

我们已经得到了能够同时进行目标分类和定位的网络,考虑将之扩展为多目标分类与定位。思路是输出固定目标数(课程中设定的是16)的信息:16x(4+c)。有两种方式:

  • 修改单目标网络的输出层,使之直接输出16x(4+c)的数值。这一方法最初由YOLO(You Only Look Once)网络使用。
  • resnet34后接一个跨立度为2的卷积层,使其输出为4x4x(4+c)(resnet34的最后一层输出为7x7x512)。这一方法最初由SSD(Single Shot Detector)使用。
图 1. 实现16类分类与定位的两种网络结构
本节使用第二种方法。
1. 数据准备

将目标的坐标数据整理为多类别分类网络所需的CSV文件格式。

图 2. 目标的坐标数据文件
然后在生成数据模型时注意声明其为连续型数据。

    md = ImageClassifierData.from_csv(PATH, JPEGS, MBB_CSV, tfms=tfms, bs=bs, continuous=True, num_workers=4) 

继而将md.trn_dl.dataset以及md.val_dl.dataset与类别数据进行拼接。需要注意的是:不同文件中所含目标个数不同,md采取的策略是按同批次的图像中目标个数的最大值进行补齐(这意味着不同批次的向量长度会有所变化。pascal VOC数据中,007953.jpg包含19辆摩托车,是目标数目最大的图片)。

2. 构建网络

按照SSD方法,构建附加层。由于需要预测的信息略多,可添加微网络以增强模型的描述功能。最终网络输出两组预测值:一组的尺寸为batch_sizex16x(1+c),用于目标类别的判定;一组尺寸为batch_sizex16x4,用于目标定位。

3. 损失函数

考虑卷积结果的接触域。(卷积结果中的一个元素实际上是由原图像中的部分元素的值决定的,这些原图像中的元素的分布区域即为卷积结果中对应元素的接触域。)所得输出为4x4x(4+c+1),即可认为将原图像分为了4x4部分,网络输出结果中的每条特征(4+c+1维的向量),是对原图像中的某一块的描述。
首先考虑分类。如何确定原图像中的某一块属于哪一类呢?定义图像的某块与目标的重叠率为重叠区域面积与二者面积之和的比。

图 3. 重叠率
比如原图像中有`3`个目标,计算其与图像的16-划分块的重叠率,可得维度为`3x16`的数组。对该矩阵沿行求最大值的索引,可得图像中的`3`个目标的主体位于哪一小块里;沿列求最大值的索引,可得原图像的每一小块分属哪个目标;而在实际应用中,一般是设定一个阈值,当一小块与某目标的重叠率超过该阈值时,将该小块判定为该目标。

在知晓图像中的各个小块与目标的关系后,就可按照单目标分类与定位的损失函数,对每一小块计算损失,然后求平均。这里需要注意的有如下几点:

  • 每一条特征生成的定位框的坐标,限定在其所对应的格点的附近范围内。这样就需要对图像网格化进行多样化处理,以提供更强的描述能力。之所以采用这种方法的考虑如下:由于不确定格点对应的图像网格与目标的关联程度,若使用某格点在全图范围内预测整个目标的定位框,可能需要引入重叠框的加权问题。

  • 在求分类问题的损失函数时,采用的是二值熵函数,同时去除了背景项。考虑如下:各个网格构成了一个小的样本,但这里有个问题就是,这些样本中大部分可能都是背景,也就是说这是个不均衡的样本。去除背景类后,使用二值熵去判定各个网格是不是某类,更合适。

三、多目标的识别与定位的优化

1. 提供多样化的网格
  • 继续进行跨立度为2的卷积,提供不同尺寸的图像网格;
  • 对网格进行缩放;
  • 对网格进行拉伸。
2. 改进损失函数

如前所述,由于单一图片的样本不均衡性,导致在不确定某一小块究竟是啥时,将之判定为背景总是最安全的。这会导致目标区域在图片中较小时,网络认为图中无目标。如下图中的中间两幅图所示。一个解决方法是使用Focal Loss

F L ( p t ) = α t ( 1 p t ) γ log p t FL(p_t)=-\alpha_t(1-p_t)^\gamma\log p_t

其中 p t p_t 为二值熵函数。(具体为啥能改进,后面搞懂了再说吧~~)

图 4. 被网络忽视目标的示例图片
3. 去除重叠窗

若两个框框分属同类,又有很高的重叠度,则将两个框融合。

附注

1. 查找Dataloader返回数据的文件名

生成模型所需数据md后,可通过next(iter(md.val_dl))获取一组数据。其返回值为图像数组和图像标签。怎么找到这些数据对应的文件名呢?

先去看md,其是ImageClassfierData生成的,找到其定义处(在fastai/dataset.py文件中),发现其继承自ImageData,爷爷是ModelData,但找完了它们的变量,没有发现和图像文件名相关的。那就继续找val_dl,其是DataLoader类,并发现val_ds是由val_dl.dataset返回的。而获取一组数据时,调用的是DataLoader__iter__()方法,该方法中显示了从数据集生成Batch时,调用了self.get_batch()方法,该方法使用了抽样器self.batch_sampler。而所抽取的样本都存储在DataLoader.dataset变量中。使用val_dl.dataset.__dict__查看其变量,发现其有fnames字段,存储的应该是文件名。然后查看val_dl.batch_sampler,发现其是一个迭代器,由其产生索引,则可从fnames中获取文件名。

2. 近年来目标识别的发展历程
图 5. 目标识别方法演变脉络

一些有用的链接

猜你喜欢

转载自blog.csdn.net/suredied/article/details/84845027