fast.ai 深度学习笔记(四)(3)

简介: fast.ai 深度学习笔记(四)

fast.ai 深度学习笔记(四)(2)https://developer.aliyun.com/article/1482714

测试[51:58]

x,y = next(iter(md.val_dl))
x,y = V(x),V(y)
learn.model.eval()
batch = learn.model(x)
b_clas,b_bb = batch
b_clas.size(),b_bb.size()
'''
(torch.Size([64, 16, 21]), torch.Size([64, 16, 4]))
'''

确保这些形状是合理的。现在让我们看看地面真实y[53:24]:

idx=7
b_clasi = b_clas[idx]
b_bboxi = b_bb[idx]
ima=md.val_ds.ds.denorm(to_np(x))[idx]
bbox,clas = get_y(y[0][idx], y[1][idx])
bbox,clas
'''
(Variable containing:
  0.6786  0.4866  0.9911  0.6250
  0.7098  0.0848  0.9911  0.5491
  0.5134  0.8304  0.6696  0.9063
 [torch.cuda.FloatTensor of size 3x4 (GPU 0)], 
 Variable containing:
   8
  10
  17
 [torch.cuda.LongTensor of size 3 (GPU 0)])
'''

请注意,边界框坐标已缩放到 0 和 1 之间 - 基本上我们将图像视为 1x1,因此它们是相对于图像大小的。

我们已经有了show_ground_truth函数。这个torch_gt(gt:地面真相)函数简单地将张量转换为 numpy 数组。

def torch_gt(ax, ima, bbox, clas, prs=None, thresh=0.4):
    return show_ground_truth(
        ax, ima, 
        to_np((bbox*224).long()),
        to_np(clas), 
        to_np(prs) 
        if prs is not None 
        else None, thresh
    )
fig, ax = plt.subplots(figsize=(7,7))
torch_gt(ax, ima, bbox, clas)


以上是一个地面真相。这是我们最终卷积层的4x4网格单元[54:44]:

fig, ax = plt.subplots(figsize=(7,7))
torch_gt(ax, ima, anchor_cnr, b_clasi.max(1)[1])


每个正方形框,不同的论文称其为不同的东西。您将听到的三个术语是:锚框、先验框或默认框。我们将坚持使用术语锚框。

对于这个损失函数,我们将通过一个匹配问题,看看这 16 个框中的每一个与给定正方形中的这三个地面真实对象哪一个有最高的重叠量。为了做到这一点,我们必须有一种衡量重叠量的方法,这种标准函数称为 Jaccard 指数(IoU)。


我们将逐个查看这三个对象与每个 16 个锚框的 Jaccard 重叠[57:11]。这将给我们一个3x16矩阵。

这是我们所有锚框(中心、高度、宽度)的坐标

anchors
'''
Variable containing:
 0.1250  0.1250  0.2500  0.2500
 0.1250  0.3750  0.2500  0.2500
 0.1250  0.6250  0.2500  0.2500
 0.1250  0.8750  0.2500  0.2500
 0.3750  0.1250  0.2500  0.2500
 0.3750  0.3750  0.2500  0.2500
 0.3750  0.6250  0.2500  0.2500
 0.3750  0.8750  0.2500  0.2500
 0.6250  0.1250  0.2500  0.2500
 0.6250  0.3750  0.2500  0.2500
 0.6250  0.6250  0.2500  0.2500
 0.6250  0.8750  0.2500  0.2500
 0.8750  0.1250  0.2500  0.2500
 0.8750  0.3750  0.2500  0.2500
 0.8750  0.6250  0.2500  0.2500
 0.8750  0.8750  0.2500  0.2500
[torch.cuda.FloatTensor of size 16x4 (GPU 0)]
'''

这是 3 个地面真实对象和 16 个锚框之间的重叠量:

overlaps = jaccard(bbox.data, anchor_cnr.data)
overlaps
'''
Columns 0 to 7   
0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000    0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000    0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000 Columns 8 to 15   
0.0000  0.0091 0.0922  0.0000  0.0000  0.0315  0.3985  0.0000  0.0356  0.0549 0.0103  0.0000  0.2598  0.4538  0.0653  0.0000  0.0000  0.0000 0.0000  0.1897  0.0000  0.0000  0.0000  0.0000 [torch.cuda.FloatTensor of size 3x16 (GPU 0)]
'''

现在我们可以取维度 1(按行)的最大值,这将告诉我们每个地面真实对象的最大重叠量以及索引:

overlaps.max(1)
'''
(
  0.3985
  0.4538
  0.1897
 [torch.cuda.FloatTensor of size 3 (GPU 0)], 
  14
  13
  11
 [torch.cuda.LongTensor of size 3 (GPU 0)])
'''

我们还将查看维度 0(按列)的最大值,这将告诉我们每个网格单元与所有地面真实对象之间的最大重叠量是多少:

overlaps.max(0)
'''
(
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0356
  0.0549
  0.0922
  0.1897
  0.2598
  0.4538
  0.3985
  0.0000
 [torch.cuda.FloatTensor of size 16 (GPU 0)], 
  0
  0
  0
  0
  0
  0
  0
  0
  1
  1
  0
  2
  1
  1
  0
  0
 [torch.cuda.LongTensor of size 16 (GPU 0)])
'''

这里特别有趣的是,它告诉我们每个网格单元与之重叠最多的地面真实对象的索引是什么。零在这里有点过载 - 零可能意味着重叠量为零,也可能意味着它与对象索引零的重叠最大。这将被证明并不重要,但只是供参考。

有一个名为map_to_ground_truth的函数,我们现在不用担心。这是非常简单的代码,但稍微难以理解。基本上它的作用是以 SSD 论文中描述的方式将这两组重叠组合起来,将每个锚框分配给一个地面真实对象。它的分配方式是每个三个(按行最大)都被分配为是。对于其余的锚框,它们被分配给它们与至少 0.5 重叠的任何东西(按列)。如果两者都不适用,则被视为包含背景的单元格。

gt_overlap,gt_idx = map_to_ground_truth(overlaps)
gt_overlap,gt_idx
'''
(
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0000
  0.0356
  0.0549
  0.0922
  1.9900
  0.2598
  1.9900
  1.9900
  0.0000
 [torch.cuda.FloatTensor of size 16 (GPU 0)], 
  0
  0
  0
  0
  0
  0
  0
  0
  1
  1
  0
  2
  1
  1
  0
  0
 [torch.cuda.LongTensor of size 16 (GPU 0)])
'''

现在您可以看到所有分配的列表。任何gt_overlap < 0.5的地方都被分配为背景。三行最大锚框具有较高的数字以强制分配。现在我们可以将这些值组合到类别中:

gt_clas = clas[gt_idx]; gt_clas
'''
Variable containing:
  8
  8
  8
  8
  8
  8
  8
  8
 10
 10
  8
 17
 10
 10
  8
  8
[torch.cuda.LongTensor of size 16 (GPU 0)]
'''

然后添加一个阈值,最后得出正在预测的三个类:

thresh = 0.5
pos = gt_overlap > thresh
pos_idx = torch.nonzero(pos)[:,0]
neg_idx = torch.nonzero(1-pos)[:,0]
pos_idx 
'''
 11
 13
 14
[torch.cuda.LongTensor of size 3 (GPU 0)]
'''

这里是每个锚框预测的含义:

gt_clas[1-pos] = len(id2cat)
[id2cat[o] if o<len(id2cat) else 'bg' for o in gt_clas.data]
'''
['bg',
 'bg',
 'bg',
 'bg',
 'bg',
 'bg',
 'bg',
 'bg',
 'bg',
 'bg',
 'bg',
 'sofa',
 'bg',
 'diningtable',
 'chair',
 'bg']
'''

那就是匹配阶段。对于 L1 损失,我们可以:

  1. 取匹配的激活(pos_idx = [11, 13, 14]
  2. 从中减去地面真实边界框
  3. 取差的绝对值
  4. 取平均值。

对于分类,我们可以做一个交叉熵

gt_bbox = bbox[gt_idx]
loc_loss = ((a_ic[pos_idx] - gt_bbox[pos_idx]).abs()).mean()
clas_loss  = F.cross_entropy(b_clasi, gt_clas)
loc_loss,clas_loss
'''
(Variable containing:
 1.00000e-02 
   6.5887
 [torch.cuda.FloatTensor of size 1 (GPU 0)], 
 Variable containing:
  1.0331
 [torch.cuda.FloatTensor of size 1 (GPU 0)])
'''

最终我们将得到 16 个预测的边界框,其中大多数将是背景。如果您想知道它在背景边界框方面的预测是什么,答案是它完全忽略了它。

fig, axes = plt.subplots(3, 4, figsize=(16, 12))
for idx,ax in enumerate(axes.flat):
    ima=md.val_ds.ds.denorm(to_np(x))[idx]
    bbox,clas = get_y(y[0][idx], y[1][idx])
    ima=md.val_ds.ds.denorm(to_np(x))[idx]
    bbox,clas = get_y(bbox,clas); bbox,clas
    a_ic = actn_to_bb(b_bb[idx], anchors)
    torch_gt(
        ax, ima, a_ic, 
        b_clas[idx].max(1)[1], 
        b_clas[idx].max(1)[0].sigmoid(), 
        0.01
    )
plt.tight_layout()


微调 1.我们如何解释激活?

我们解释激活的方式在这里定义:

def actn_to_bb(actn, anchors):
    actn_bbs = torch.tanh(actn)
    actn_centers = (actn_bbs[:,:2]/2 * grid_sizes) + anchors[:,:2]
    actn_hw = (actn_bbs[:,2:]/2+1) * anchors[:,2:]
    return hw2corners(actn_centers, actn_hw)

我们抓取激活,将它们通过tanh(记住tanh与 sigmoid 形状相同,只是缩放到-1 和 1 之间)强制使其在该范围内。然后我们抓取锚框的实际位置,并根据激活值除以二(actn_bbs[:,:2]/2)将它们移动。换句话说,每个预测的边界框可以从其默认位置最多移动一个网格大小的 50%。高度和宽度也是如此 - 它可以是默认大小的两倍大或一半小。

微调 2.我们实际上使用二元交叉熵损失而不是交叉熵

class BCE_Loss(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.num_classes = num_classes
    def forward(self, pred, targ):
        t = one_hot_embedding(targ, self.num_classes+1)
        t = V(t[:,:-1].contiguous())*#.cpu()*
        x = pred[:,:-1]
        w = self.get_weight(x,t)
        return F.binary_cross_entropy_with_logits(
            x, t, w, 
            size_average=False
        ) / self.num_classes
    def get_weight(self,x,t): 
        return None

二元交叉熵是我们通常用于多标签分类的。就像在行星卫星竞赛中,每个卫星图像可能有多个物体。如果它有多个物体,你不能使用 softmax,因为 softmax 真的鼓励只有一个物体有高的数字。在我们的情况下,每个锚框只能与一个物体相关联,所以我们避免使用 softmax 并不是因为这个原因。还有其他原因——即一个锚框可能没有任何与之相关联的物体。处理这种“背景”的想法有两种方法;一种是说背景只是一个类,所以让我们使用 softmax,将背景视为 softmax 可以预测的类之一。很多人都是这样做的。但这是一个非常困难的事情要求神经网络做[1:06:52] — 基本上是在问这个网格单元是否没有我感兴趣的 20 个物体中的任何一个,Jaccard 重叠大于 0.5。这是一个非常难以放入单个计算中的事情。另一方面,如果我们只问每个类;“这是摩托车吗?”“这是公共汽车吗?”“这是一个人吗?”等等,如果所有的答案都是否定的,那就认为是背景。这就是我们在这里做的方式。并不是我们可以有多个真实标签,而是我们可以有零个。

forward中:

  1. 首先我们获取目标的 one hot 编码(在这个阶段,我们已经有了背景的概念)
  2. 然后我们移除背景列(最后一列),结果是一个全为零或全为一的向量。
  3. 使用二元交叉熵预测。

这是一个小的调整,但这是 Jeremy 希望你考虑和理解的小调整,因为它对你的训练有很大的影响,当有一些对以前论文的增量时,会是这样的[1:08:25]。重要的是要理解这是在做什么,更重要的是为什么。

现在我们有[1:09:39]:

  • 一个自定义损失函数
  • 计算 Jaccard 指数的方法
  • 将激活转换为边界框的方法
  • 将锚框映射到地面真实的方法

现在剩下的就是 SSD 损失函数。

SSD 损失函数[1:09:55]

def ssd_1_loss(b_c,b_bb,bbox,clas,print_it=False):
    bbox,clas = get_y(bbox,clas)
    a_ic = actn_to_bb(b_bb, anchors)
    overlaps = jaccard(bbox.data, anchor_cnr.data)
    gt_overlap,gt_idx = map_to_ground_truth(overlaps,print_it)
    gt_clas = clas[gt_idx]
    pos = gt_overlap > 0.4
    pos_idx = torch.nonzero(pos)[:,0]
    gt_clas[1-pos] = len(id2cat)
    gt_bbox = bbox[gt_idx]
    loc_loss = ((a_ic[pos_idx] - gt_bbox[pos_idx]).abs()).mean()
    clas_loss  = loss_f(b_c, gt_clas)
    return loc_loss, clas_loss
def ssd_loss(pred,targ,print_it=False):
    lcs,lls = 0.,0.
    for b_c,b_bb,bbox,clas in zip(*pred,*targ):
        loc_loss,clas_loss = ssd_1_loss(b_c,b_bb,bbox,clas,print_it)
        lls += loc_loss
        lcs += clas_loss
    if print_it: 
        print(f'loc: {lls.data[0]}, clas: {lcs.data[0]}')
    return lls+lcs

ssd_loss函数是我们设置的标准,它循环遍历每个小批量中的图像,并调用ssd_1_loss函数(即一个图像的 SSD 损失)。

ssd_1_loss是所有操作发生的地方。它从bboxclas开始解构。让我们更仔细地看一下get_y[1:10:38]:

def get_y(bbox,clas):
    bbox = bbox.view(-1,4)/sz
    bb_keep = ((bbox[:,2]-bbox[:,0])>0).nonzero()[:,0]
    return bbox[bb_keep],clas[bb_keep]

你在互联网上找到的很多代码都不能用于小批量。它一次只能做一件事,而我们不想要这样。在这种情况下,所有这些函数(get_yactn_to_bbmap_to_ground_truth)都是在一次处理,不完全是一个小批量,而是一次处理一堆地面真实对象。数据加载器每次被馈送一个小批量以执行卷积层。因为我们可以在每个图像中有不同数量的地面真实对象,但张量必须是严格的矩形形状,fastai 会自动用零填充它(任何较短的目标值)[1:11:08]。这是最近添加的一个功能,非常方便,但这意味着你必须确保去掉这些零。因此,get_y会去掉任何只是填充的边界框。

  1. 去掉填充
  2. 将激活转换为边界框
  3. 计算 Jaccard 指数
  4. 进行地面真实的映射
  5. 检查是否有大约 0.4~0.5 的重叠(不同的论文使用不同的值)
  6. 找到匹配的索引
  7. 为那些不匹配的分配背景类
  8. 然后最终得到定位的 L1 损失,分类的二元交叉熵损失,并将它们返回,加入ssd_loss

训练

learn.crit = ssd_loss
lr = 3e-3
lrs = np.array([lr/100,lr/10,lr])
learn.lr_find(lrs/1000,1.)
learn.sched.plot(1)
'''
epoch      trn_loss   val_loss                            
    0      44.232681  21476.816406
'''


learn.lr_find(lrs/1000,1.)
learn.sched.plot(1)
'''
epoch      trn_loss   val_loss                            
    0      86.852668  32587.789062
'''


learn.fit(lr, 1, cycle_len=5, use_clr=(20,10))
'''
epoch      trn_loss   val_loss                            
    0      45.570843  37.099854 
    1      37.165911  32.165031                           
    2      33.27844   30.990122                           
    3      31.12054   29.804482                           
    4      29.305789  28.943184
[28.943184]
'''
learn.fit(lr, 1, cycle_len=5, use_clr=(20,10))
'''
epoch      trn_loss   val_loss                            
    0      43.726979  33.803085 
    1      34.771754  29.012939                           
    2      30.591864  27.132868                           
    3      27.896905  26.151638                           
    4      25.907382  25.739273
[25.739273]
'''
learn.save('0')
learn.load('0')

结果


在实践中,我们希望去除背景,并为概率添加一些阈值,但这是正确的方向。盆栽植物图像,结果并不令人惊讶,因为我们所有的锚盒都很小(4x4 网格)。要从这里走向更准确的东西,我们要做的就是创建更多的锚盒。

问题:对于多标签分类,为什么我们不像以前那样将分类损失乘以一个常数?很好的问题。因为后来会发现我们不需要这样做。

更多的锚点!

有 3 种方法可以做到这一点:

  1. 创建不同尺寸的锚盒(缩放):




从左边(1x1、2x2、4x4 的锚盒网格)。注意一些锚盒比原始图像大。

  1. 创建不同长宽比的锚盒:




  1. 使用更多卷积层作为锚盒的来源(盒子被随机抖动,以便我们可以看到重叠的盒子):


结合这些方法,你可以创建很多锚盒(Jeremy 说他不会打印出来,但这里有):


anc_grids = [4, 2, 1]
anc_zooms = [0.75, 1., 1.3]
anc_ratios = [(1., 1.), (1., 0.5), (0.5, 1.)]
anchor_scales = [
    (anz*i,anz*j) 
    for anz in anc_zooms 
    for (i,j) in anc_ratios
]
k = len(anchor_scales)
anc_offsets = [1/(o*2) for o in anc_grids]
anc_x = np.concatenate([
    np.repeat(np.linspace(ao, 1-ao, ag), ag)
    for ao,ag in zip(anc_offsets,anc_grids)
])
anc_y = np.concatenate([
    np.tile(np.linspace(ao, 1-ao, ag), ag)
    for ao,ag in zip(anc_offsets,anc_grids)
])
anc_ctrs = np.repeat(np.stack([anc_x,anc_y], axis=1), k, axis=0)
anc_sizes = np.concatenate([
    np.array([
        [o/ag,p/ag] 
        for i in range(ag*ag) 
        for o,p in anchor_scales
    ])
    for ag in anc_grids
])
grid_sizes = V(np.concatenate([
    np.array([ 
        1/ag 
        for i in range(ag*ag) 
        for o,p in anchor_scales
    ])
    for ag in anc_grids
]), requires_grad=False).unsqueeze(1)
anchors = V(
    np.concatenate([anc_ctrs, anc_sizes], axis=1), 
    requires_grad=False
).float()
anchor_cnr = hw2corners(anchors[:,:2], anchors[:,2:])

锚点:中间和高度,宽度

anchor_cnr:左上角和右下角

关键概念回顾


  • 我们有一个地面真相的向量(一组 4 个边界框坐标和一个类)
  • 我们有一个神经网络,它接受一些输入并输出一些输出激活
  • 比较激活和地面真相,计算损失,找到该导数的导数,并根据导数乘以学习率调整权重。
  • 我们需要一个损失函数,可以接受地面真相和激活,并输出一个数字,表示这些激活有多好。为了做到这一点,我们需要考虑每一个m个地面真相对象,并决定哪组(4+c)激活负责该对象 — 我们应该比较哪一个来决定类是否正确,边界框是否接近(匹配问题)。
  • 由于我们使用 SSD 方法,所以我们匹配的对象并不是任意的。我们希望匹配的是接收域密度最大的激活集,从真实对象所在的地方。
  • 损失函数需要是一些一致的任务。如果在第一幅图像中,左上角的对象对应于前 4+c 个激活,并且在第二幅图像中,我们把事物扔来扔去,突然它现在与最后的 4+c 个激活一起,神经网络就不知道要学习什么。
  • 一旦匹配问题解决了,其余的就和单个对象检测一样。

架构:

  • YOLO — 最后一层是全连接的(没有几何概念)
  • SSD — 最后一层是卷积

k(缩放 x 比率)

对于每个可能具有不同大小的网格单元,我们可以有不同的方向和缩放,代表不同的锚框,这些锚框就像是每个锚框都与我们模型中的一个4+c激活集相关联的概念性想法。因此,无论我们有多少个锚框,我们都需要有那么多次(4+c)激活。这并不意味着每个卷积层都需要那么多激活。因为 4x4 卷积层已经有 16 组激活,2x2 层有 4 组激活,最后 1x1 层有一组激活。所以我们基本上可以免费获得 1 + 4 + 16。因此,我们只需要知道k,其中k是缩放数乘以宽高比数。而网格,我们将通过我们的架构免费获得。

模型架构

drop=0.4
class SSD_MultiHead(nn.Module):
    def __init__(self, k, bias):
        super().__init__()
        self.drop = nn.Dropout(drop)
        self.sconv0 = StdConv(512,256, stride=1, drop=drop)
        self.sconv1 = StdConv(256,256, drop=drop)
        self.sconv2 = StdConv(256,256, drop=drop)
        self.sconv3 = StdConv(256,256, drop=drop)
        self.out1 = OutConv(k, 256, bias)
        self.out2 = OutConv(k, 256, bias)
        self.out3 = OutConv(k, 256, bias)
    def forward(self, x):
        x = self.drop(F.relu(x))
        x = self.sconv0(x)
        x = self.sconv1(x)
        o1c,o1l = self.out1(x)
        x = self.sconv2(x)
        o2c,o2l = self.out2(x)
        x = self.sconv3(x)
        o3c,o3l = self.out3(x)
        return [
            torch.cat([o1c,o2c,o3c], dim=1),
            torch.cat([o1l,o2l,o3l], dim=1)
        ]
head_reg4 = SSD_MultiHead(k, -4.)
models = ConvnetBuilder(f_model, 0, 0, 0, custom_head=head_reg4)
learn = ConvLearner(md, models)
learn.opt_fn = optim.Adam

模型几乎与之前的模型相同。但我们有许多步长为 2 的卷积,这将带我们到 4x4、2x2 和 1x1(每个步长为 2 的卷积都会将我们的网格大小在两个方向上减半)。

  • 在我们进行第一次卷积以达到 4x4 后,我们将从中获取一组输出,因为我们想要保存 4x4 的锚点。
  • 一旦我们到达 2x2,我们再抓取一组 2x2 的锚点
  • 最后我们到达 1x1
  • 然后我们将它们全部连接在一起,这给我们正确数量的激活(每个锚框一个激活)。

训练

learn.crit = ssd_loss
lr = 1e-2
lrs = np.array([lr/100,lr/10,lr])
learn.lr_find(lrs/1000,1.)
learn.sched.plot(n_skip_end=2)


learn.fit(lrs, 1, cycle_len=4, use_clr=(20,8))
'''
epoch      trn_loss   val_loss                            
    0      15.124349  15.015433 
    1      13.091956  10.39855                            
    2      11.643629  9.4289                              
    3      10.532467  8.822998
[8.822998]
'''
learn.save('tmp')
learn.freeze_to(-2)
learn.fit(lrs/2, 1, cycle_len=4, use_clr=(20,8))
'''
epoch      trn_loss   val_loss                            
    0      9.821056   10.335152 
    1      9.419633   11.834093                           
    2      8.78818    7.907762                            
    3      8.219976   7.456364
[7.4563637]
'''
x,y = next(iter(md.val_dl))
y = V(y)
batch = learn.model(V(x))
b_clas,b_bb = batch
x = to_np(x)
fig, axes = plt.subplots(3, 4, figsize=(16, 12))
for idx,ax in enumerate(axes.flat):
    ima=md.val_ds.ds.denorm(x)[idx]
    bbox,clas = get_y(y[0][idx], y[1][idx])
    a_ic = actn_to_bb(b_bb[idx], anchors)
    torch_gt(
        ax, ima, a_ic, 
        b_clas[idx].max(1)[1], 
        b_clas[idx].max(1)[0].sigmoid(), 
        0.2
    )
plt.tight_layout()

在这里,我们打印出那些至少概率为0.2的检测结果。有些看起来很有希望,但有些则不太好。


目标检测的历史


使用深度神经网络的可扩展目标检测

  • 当人们提到多框法时,他们指的是这篇论文。
  • 这篇论文提出了一个损失函数的想法,该函数具有匹配过程,然后可以用来进行目标检测。因此,自那时以来,一切都在尝试找出如何使其更好。

实时目标检测与区域提议网络

  • 同时,Ross Girshick 正在走一条完全不同的方向。他有这两个阶段的过程,第一阶段使用经典的计算机视觉方法来找到边缘和梯度变化,猜测图像的哪些部分可能代表不同的对象。然后将每个对象放入一个卷积神经网络中,这个网络基本上是设计用来确定我们感兴趣的对象的类型。
  • R-CNN 和 Fast R-CNN 是传统计算机视觉和深度学习的混合体。
  • Ross 和他的团队接着做的是,他们采用了多框法的思想,用卷积网络替换了他们两阶段过程中传统的非深度学习计算机视觉部分。现在他们有两个卷积网络:一个用于区域提议(可能是对象的所有东西),第二部分与他之前的工作相同。

统一、实时目标检测

单次多框检测器(SSD)

  • 在同一时间,这些论文出现了。这两篇论文做了一些非常酷的事情,就是他们实现了与 Faster R-CNN 相似的性能,但只用了 1 个阶段。
  • 他们采用了多框法,并试图找出如何处理混乱的输出。基本思想是使用,例如,硬负样本挖掘,他们会遍历所有看起来不太好的匹配项并将其丢弃,使用非常棘手和复杂的数据增强方法,以及各种技巧。但他们让它们运行得相当不错。

密集目标检测的焦点损失(RetinaNet)

  • 然后去年年底发生了一件非常酷的事情,那就是焦点损失。
  • 他们实际上意识到为什么这个混乱的东西不起作用。当我们查看图像时,有 3 种不同的卷积网格粒度(4x4、2x2、1x1)。1x1 很可能与某个对象有合理的重叠,因为大多数照片都有某种主题。另一方面,在 4x4 网格单元中,大多数 16 个锚框不会与任何东西有太多重叠。因此,如果有人对你说“20 美元赌注,你认为这个小片段是什么?”而你不确定,你会说“背景”,因为大多数时候,它是背景。

问题:我理解为什么我们在图像中有一个 4x4 网格的感受野,每个都有一个锚框来粗略定位对象。但我觉得我不明白的是为什么我们需要不同尺寸的多个感受野。第一个版本已经包括了 16 个感受野,每个都有一个关联的单个锚框。通过添加,现在有更多的锚框要考虑。这是因为您限制了感受野可以从其原始大小移动或缩放的程度吗?还是有其他原因?这有点反向。Jeremy 做约束的原因是因为他知道他以后会添加更多的框。但实际上,原因是 4x4 网格单元之一与占据图像大部分的单个对象的图像之间的 Jaccard 重叠永远不会达到 0.5。交集远小于并集,因为对象太大。因此,为了使这个一般想法起作用,我们说你负责的东西与之有 50%以上的重叠,我们需要锚框,这些锚框将定期具有 50%或更高的重叠,这意味着我们需要具有各种大小、形状和比例的锚框。所有这些都发生在损失函数中。所有目标检测中大部分有趣的东西都在损失函数中。

焦点损失


关键是这张第一张图片。蓝线是二元交叉熵损失。如果答案不是摩托车,我说“我认为这不是摩托车,我有 60%的把握”用蓝线,损失仍然约为 0.5,这相当糟糕。所以,如果我们想降低损失,那么对于所有这些实际上是背景的东西,我们必须说“我确定那是背景”,“我确定这不是摩托车,公共汽车或人” — 因为如果我不说我们确定它不是这些东西中的任何一个,那么我们仍然会有损失。

这就是为什么摩托车的例子不起作用。因为即使它到达右下角并想说“我认为这是一辆摩托车”,也没有回报。如果错了,就会被淘汰。而且大多数时候,它是背景。即使不是背景,仅仅说“这不是背景”是不够的 — 你必须说它是 20 件事物中的哪一个。

所以诀窍是尝试找到一个更像紫线的不同损失函数。焦点损失实际上只是一个缩放的交叉熵损失。现在如果我们说“我有 60%的把握这不是摩托车”,那么损失函数会说“干得好!没问题”。

这篇论文的实际贡献是在方程的开头添加(1 − pt)^γ,听起来像无关紧要的事情,但实际上人们多年来一直在努力解决这个问题。当你遇到这样一个改变游戏规则的论文时,不要假设你将不得不编写成千上万行的代码。很多时候只是一行代码,或者改变一个常数,或者在一个地方添加对数。

关于这篇论文的一些了不起的事情[1:46:08]:

  • 方程式以简单的方式编写
  • 他们“重构”

实现焦点损失[1:49:27]:


记住,-log(pt)是交叉熵损失,焦点损失只是一个缩放版本。当我们定义二项式交叉熵损失时,您可能已经注意到默认情况下没有权重:

class BCE_Loss(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.num_classes = num_classes
    def forward(self, pred, targ):
        t = one_hot_embedding(targ, self.num_classes+1)
        t = V(t[:,:-1].contiguous()) #.cpu()
        x = pred[:,:-1]
        w = self.get_weight(x,t)
        return F.binary_cross_entropy_with_logits(
            x, t, w, 
            size_average=False
        ) / self.num_classes
    def get_weight(self,x,t): return None

当您调用F.binary_cross_entropy_with_logits时,可以传入权重。由于我们只想将交叉熵乘以某个值,我们可以定义get_weight。这是焦点损失的全部内容[1:50:23]:

class FocalLoss(BCE_Loss):
    def get_weight(self,x,t):
        alpha,gamma = 0.25,2.
        p = x.sigmoid()
        pt = p*t + (1-p)*(1-t)
        w = alpha*t + (1-alpha)*(1-t)
        return w * (1-pt).pow(gamma)

如果您想知道为什么 alpha 和 gamma 是 0.25 和 2,这篇论文的另一个优点是,因为他们尝试了许多不同的值,并发现这些值效果很好:


训练[1:51:25]

learn.lr_find(lrs/1000,1.)
learn.sched.plot(n_skip_end=2)


learn.fit(lrs, 1, cycle_len=10, use_clr=(20,10))
'''
epoch      trn_loss   val_loss                            
    0      24.263046  28.975235 
    1      20.459562  16.362392                           
    2      17.880827  14.884829                           
    3      15.956896  13.676485                           
    4      14.521345  13.134197                           
    5      13.460941  12.594139                           
    6      12.651842  12.069849                           
    7      11.944972  11.956457                           
    8      11.385798  11.561226                           
    9      10.988802  11.362164
[11.362164]
'''
learn.save('fl0')
learn.load('fl0')
learn.freeze_to(-2)
learn.fit(lrs/4, 1, cycle_len=10, use_clr=(20,10))
'''
epoch      trn_loss   val_loss                            
    0      10.871668  11.615532 
    1      10.908461  11.604334                           
    2      10.549796  11.486127                           
    3      10.130961  11.088478                           
    4      9.70691    10.72144                            
    5      9.319202   10.600481                           
    6      8.916653   10.358334                           
    7      8.579452   10.624706                           
    8      8.274838   10.163422                           
    9      7.994316   10.108068
[10.108068]
'''
learn.save('drop4')
learn.load('drop4')
plot_results(0.75)


这次情况看起来好多了。因此,我们现在的最后一步是基本上弄清楚如何只提取感兴趣的部分。

非极大值抑制[1:52:15]

我们要做的就是遍历每对这些边界框,如果它们重叠超过一定数量,比如 0.5,使用 Jaccard 并且它们都预测相同的类别,我们将假设它们是相同的东西,并且我们将选择具有更高p值的那个。

这是非常无聊的代码,Jeremy 自己没有写,而是复制了别人的。没有特别的原因要去研究它。

def nms(boxes, scores, overlap=0.5, top_k=100):
    keep = scores.new(scores.size(0)).zero_().long()
    if boxes.numel() == 0: 
        return keep
    x1 = boxes[:, 0]
    y1 = boxes[:, 1]
    x2 = boxes[:, 2]
    y2 = boxes[:, 3]
    area = torch.mul(x2 - x1, y2 - y1)
    v, idx = scores.sort(0)  # sort in ascending order
    idx = idx[-top_k:]  # indices of the top-k largest vals
    xx1 = boxes.new()
    yy1 = boxes.new()
    xx2 = boxes.new()
    yy2 = boxes.new()
    w = boxes.new()
    h = boxes.new()
    count = 0
    while idx.numel() > 0:
        i = idx[-1]  # index of current largest val
        keep[count] = i
        count += 1
        if idx.size(0) == 1: break
        idx = idx[:-1]  # remove kept element from view
        # load bboxes of next highest vals
        torch.index_select(x1, 0, idx, out=xx1)
        torch.index_select(y1, 0, idx, out=yy1)
        torch.index_select(x2, 0, idx, out=xx2)
        torch.index_select(y2, 0, idx, out=yy2)
        # store element-wise max with next highest score
        xx1 = torch.clamp(xx1, min=x1[i])
        yy1 = torch.clamp(yy1, min=y1[i])
        xx2 = torch.clamp(xx2, max=x2[i])
        yy2 = torch.clamp(yy2, max=y2[i])
        w.resize_as_(xx2)
        h.resize_as_(yy2)
        w = xx2 - xx1
        h = yy2 - yy1
        # check sizes of xx1 and xx2.. after each iteration
        w = torch.clamp(w, min=0.0)
        h = torch.clamp(h, min=0.0)
        inter = w*h
        # IoU = i / (area(a) + area(b) - i)
        rem_areas = torch.index_select(area, 0, idx)  
        # load remaining areas)
        union = (rem_areas - inter) + area[i]
        IoU = inter/union  # store result in iou
        # keep only elements with an IoU <= overlap
        idx = idx[IoU.le(overlap)]
    return keep, count
def show_nmf(idx):
    ima=md.val_ds.ds.denorm(x)[idx]
    bbox,clas = get_y(y[0][idx], y[1][idx])
    a_ic = actn_to_bb(b_bb[idx], anchors)
    clas_pr, clas_ids = b_clas[idx].max(1)
    clas_pr = clas_pr.sigmoid()
    conf_scores = b_clas[idx].sigmoid().t().data
    out1,out2,cc = [],[],[]
    for cl in range(0, len(conf_scores)-1):
        c_mask = conf_scores[cl] > 0.25
        if c_mask.sum() == 0: 
            continue
        scores = conf_scores[cl][c_mask]
        l_mask = c_mask.unsqueeze(1).expand_as(a_ic)
        boxes = a_ic[l_mask].view(-1, 4)
        ids, count = nms(boxes.data, scores, 0.4, 50)
        ids = ids[:count]
        out1.append(scores[ids])
        out2.append(boxes.data[ids])
        cc.append([cl]*count)
    cc = T(np.concatenate(cc))
    out1 = torch.cat(out1)
    out2 = torch.cat(out2)
    fig, ax = plt.subplots(figsize=(8,8))
    torch_gt(ax, ima, out2, cc, out1, 0.1)
    for i in range(12): 
        show_nmf(i)





这里还有一些需要修复的地方[1:53:43]。技巧将是使用称为特征金字塔的东西。这就是我们将在第 14 课中做的事情。

更多关于 SSD 论文的讨论[1:54:03]

当这篇论文出来时,Jeremy 很兴奋,因为这和 YOLO 是第一种单次通过的高质量目标检测方法。在深度学习世界中存在这种连续的历史重复,即涉及多次通过多个不同部分的事物,特别是当它们涉及一些非深度学习部分(如 R-CNN)时,随着时间的推移,它们总是被转化为单一的端到端深度学习模型。因此,我倾向于忽略它们,直到发生这种情况,因为那是人们已经找到如何将其展示为深度学习模型的时候,一旦他们这样做,它们通常会变得更快更准确。因此,SSD 和 YOLO 非常重要。

这个模型有 4 段。论文非常简洁,这意味着您需要非常仔细地阅读它们。但部分原因是,您需要知道哪些部分需要仔细阅读。当他们说“在这里我们将证明该模型的误差界限”时,您可以忽略,因为您不关心证明误差界限。但是当他们说这就是模型时,您需要仔细阅读。

Jeremy 阅读了一个部分2.1 模型[1:56:37]

如果您直接阅读这样的论文,这 4 段可能毫无意义。但是现在我们已经阅读过了,您阅读这些内容时,希望会想到“哦,这就是 Jeremy 说的,只是他们比 Jeremy 说得更好,用词更少[2:00:37]。如果您开始阅读一篇论文并说“到底是什么”,那么技巧就是开始回顾引文。

Jeremy 阅读匹配策略训练目标(也称为损失函数)[2:01:44]

一些论文提示[2:02:34]

使用深度神经网络的可扩展目标检测

  • “训练目标”是损失函数
  • 双条和两个 2,像这样表示均方误差

  • log©和 log(1-c),以及 x 和(1-x)它们都是二元交叉熵的组成部分。

这周,浏览代码和论文,看看发生了什么。记住 Jeremy 为了让你更容易理解,他将损失函数复制到一个单元格中,并将其拆分,使每个部分都在单独的单元格中。然后在每次出售后,他打印或绘制该值。希望这是一个好的起点。

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
15天前
|
机器学习/深度学习 自然语言处理 PyTorch
fast.ai 深度学习笔记(三)(4)
fast.ai 深度学习笔记(三)(4)
22 0
|
15天前
|
机器学习/深度学习 算法 PyTorch
fast.ai 深度学习笔记(三)(3)
fast.ai 深度学习笔记(三)(3)
30 0
|
15天前
|
机器学习/深度学习 编解码 自然语言处理
fast.ai 深度学习笔记(三)(2)
fast.ai 深度学习笔记(三)(2)
32 0
|
15天前
|
机器学习/深度学习 PyTorch 算法框架/工具
fast.ai 深度学习笔记(三)(1)
fast.ai 深度学习笔记(三)(1)
35 0
|
15天前
|
机器学习/深度学习 固态存储 Python
fast.ai 深度学习笔记(四)(2)
fast.ai 深度学习笔记(四)
50 3
fast.ai 深度学习笔记(四)(2)
|
15天前
|
API 机器学习/深度学习 Python
fast.ai 深度学习笔记(四)(1)
fast.ai 深度学习笔记(四)
62 3
fast.ai 深度学习笔记(四)(1)
|
15天前
|
机器学习/深度学习 算法框架/工具 PyTorch
fast.ai 深度学习笔记(五)(4)
fast.ai 深度学习笔记(五)
72 3
fast.ai 深度学习笔记(五)(4)
|
机器学习/深度学习 自然语言处理 Web App开发
fast.ai 深度学习笔记(五)(3)
fast.ai 深度学习笔记(五)
121 2
fast.ai 深度学习笔记(五)(3)
|
15天前
|
机器学习/深度学习 自然语言处理 Python
fast.ai 深度学习笔记(五)(2)
fast.ai 深度学习笔记(五)
106 4
fast.ai 深度学习笔记(五)(2)
|
机器学习/深度学习 人工智能 自然语言处理
搜狗翻宝Pro机再次开挂,智能翻译硬件成中国人工智能的新风口
第五届世界互联网大会正在如火如荼的举行。
搜狗翻宝Pro机再次开挂,智能翻译硬件成中国人工智能的新风口