在上一节中,我们先构建了一个分类网络,用于图片中最大目标的类别划分;然后构建了一个用于输出目标坐标的网络。我们尚未将两个网络联系起来。但事实上,两个网络的架构十分相似(都是基于resnet34
)。那么能否去除这种冗余,使用一个网络同时实现目标分类与定位呢?本部分将按照:准备数据—构建网络—定义优化目标这一分解步骤,来展示针对应用场景进行建模的通用流程。
数据分为自变量和因变量两部分,自变量自然就是图片了。无论是分类还是定位,在构建网络时针对图片所做的操作,均可通用,因此,这一部分不用考虑。而对于因变量,需要将目标类别和定位坐标结合在一起。但两者一个是连续型的,一个是离散型的;因此,在生成数据文件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类。
在分类网络与回归网络的共有部分的基础上,再添加附加层以输出分类和定位所需的数值:共需要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
后不起作用。
将定位网络的L1
范数误差和分类网络的交叉熵加权求和,即得到所需的损失函数。其中要点如下:
input
和target
参数、返回一个数值的函数。其中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
)使用。将目标的坐标数据整理为多类别分类网络所需的CSV
文件格式。
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
辆摩托车,是目标数目最大的图片)。
按照SSD
方法,构建附加层。由于需要预测的信息略多,可添加微网络以增强模型的描述功能。最终网络输出两组预测值:一组的尺寸为batch_sizex16x(1+c)
,用于目标类别的判定;一组尺寸为batch_sizex16x4
,用于目标定位。
考虑卷积结果的接触域。(卷积结果中的一个元素实际上是由原图像中的部分元素的值决定的,这些原图像中的元素的分布区域即为卷积结果中对应元素的接触域。)所得输出为4x4x(4+c+1)
,即可认为将原图像分为了4x4
部分,网络输出结果中的每条特征(4+c+1
维的向量),是对原图像中的某一块的描述。
首先考虑分类。如何确定原图像中的某一块属于哪一类呢?定义图像的某块与目标的重叠率为重叠区域面积与二者面积之和的比。
比如原图像中有3
个目标,计算其与图像的16-划分块的重叠率,可得维度为3x16
的数组。对该矩阵沿行求最大值的索引,可得图像中的3
个目标的主体位于哪一小块里;沿列求最大值的索引,可得原图像的每一小块分属哪个目标;而在实际应用中,一般是设定一个阈值,当一小块与某目标的重叠率超过该阈值时,将该小块判定为该目标。
在知晓图像中的各个小块与目标的关系后,就可按照单目标分类与定位的损失函数,对每一小块计算损失,然后求平均。这里需要注意的有如下几点:
每一条特征生成的定位框的坐标,限定在其所对应的格点的附近范围内。这样就需要对图像网格化进行多样化处理,以提供更强的描述能力。之所以采用这种方法的考虑如下:由于不确定格点对应的图像网格与目标的关联程度,若使用某格点在全图范围内预测整个目标的定位框,可能需要引入重叠框的加权问题。
在求分类问题的损失函数时,采用的是二值熵函数,同时去除了背景项。考虑如下:各个网格构成了一个小的样本,但这里有个问题就是,这些样本中大部分可能都是背景,也就是说这是个不均衡的样本。去除背景类后,使用二值熵去判定各个网格是不是某类,更合适。
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 FL(pt)=−αt(1−pt)γlogpt
其中 p t p_t pt为二值熵函数。(具体为啥能改进,后面搞懂了再说吧~~)
若两个框框分属同类,又有很高的重叠度,则将两个框融合。
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
中获取文件名。
Faster R-CNN
论文。YOLO
论文。SSD
论文。Focal
论文。