YOLO原理,看这一篇就够了!

目标检测是计算机视觉中的经典问题之一:

识别给定图像中的对象是什么以及它们在图像中的位置。

检测是一个比分类更复杂的问题,分类也可以识别对象,但不能准确告诉您对象在图像中的位置,而且它不适用于包含多个对象的图像。

分类与目标检测

YOLO是一个聪明的神经网络,用于实时进行对象检测。

在这篇博文中,我将描述如何使用 Metal Performance Shaders 在 iOS 上运行“微型”版本的 YOLOv2。

在继续之前,请务必观看精彩的 YOLOv2 预告片。 😎

YOLO 的工作原理

您可以使用VGGNetInception等分类器,通过在图像上滑动一个小窗口,将其转变为对象检测器。在每一步中,您都运行分类器来预测当前窗口内的对象类型。使用滑动窗口可以对该图像提供数百或数千个预测,但您只保留分类器最确定的预测。

这种方法有效,但显然会非常慢,因为您需要多次运行分类器。稍微更有效的方法是首先预测图像的哪些部分包含有趣的信息(所谓的区域建议),然后仅在这些区域上运行分类器。与滑动窗口相比,分类器要做的工作更少,但仍然会运行很多次。

YOLO 采用了完全不同的方法。它不是一个被重新设计为目标检测器的传统分类器。 YOLO 实际上只查看图像一次(因此得名:You Only Look Once),但方式很巧妙。

YOLO 将图像划分为 13 x 13 个单元格的网格:

13x13 网格

每个单元负责预测 5 个边界框。边界框描述了包围对象的矩形。

YOLO 还输出一个置信度分数,告诉我们预测的边界框实际上包含某个对象的确定性。这个分数并没有说明盒子里有什么类型的物体,只是说明盒子的形状是否良好。

预测的边界框可能如下所示(置信度分数越高,框画得越粗):

网格单元预测的边界框

对于每个边界框,单元格还预测一个类别。这就像分类器一样:它给出所有可能类别的概率分布。我们使用的 YOLO 版本是在PASCAL VOC 数据集上进行训练的,它可以检测 20 个不同的类别,例如:

  • 自行车
  • 等等…

边界框的置信度分数和类别预测合并为一个最终分数,该分数告诉我们该边界框包含特定类型对象的概率。例如,左边的大黄框有 85% 的把握包含对象“狗”:

边界框及其班级分数

由于有 13×13 = 169 个网格单元,每个单元预测 5 个边界框,因此我们最终得到总共 845 个边界框。事实证明,大多数这些框的置信度分数非常低,因此我们只保留最终分数为 30% 或更高的框(您可以根据您希望检测器的准确度更改此阈值)。

那么最终的预测是:

最终预测

在总共 845 个边界框中,我们只保留这三个,因为它们给出了最好的结果。但请注意,尽管有 845 个单独的预测,但它们都是同时做出的——神经网络只运行一次。这就是 YOLO 如此强大和快速的原因。

(以上图片来自pjreddie.com。)

神经网络

YOLO的架构很简单,就是一个卷积神经网络:

Layer         kernel  stride  output shape
---------------------------------------------
Input                          (416, 416, 3)
Convolution    3×3      1      (416, 416, 16)
MaxPooling     2×2      2      (208, 208, 16)
Convolution    3×3      1      (208, 208, 32)
MaxPooling     2×2      2      (104, 104, 32)
Convolution    3×3      1      (104, 104, 64)
MaxPooling     2×2      2      (52, 52, 64)
Convolution    3×3      1      (52, 52, 128)
MaxPooling     2×2      2      (26, 26, 128)
Convolution    3×3      1      (26, 26, 256)
MaxPooling     2×2      2      (13, 13, 256)
Convolution    3×3      1      (13, 13, 512)
MaxPooling     2×2      1      (13, 13, 512)
Convolution    3×3      1      (13, 13, 1024)
Convolution    3×3      1      (13, 13, 1024)
Convolution    1×1      1      (13, 13, 125)
---------------------------------------------

该神经网络仅使用标准层类型:具有 3×3 内核的卷积和具有 2×2 内核的最大池化。没有花哨的东西。 YOLOv2中没有全连接层。

注意:我们将使用的 YOLO 的“微型”版本只有这 9 个卷积层和 6 个池化层。完整的 YOLOv2 模型使用了三倍的层数,并且形状稍微复杂一些,但它仍然只是一个常规的卷积网络。

最后一个卷积层具有 1×1 内核,旨在将数据减少到 13×13×125 的形状。这个 13×13 应该看起来很熟悉:这是图像被划分成的网格的大小。

因此,最终每个网格单元有 125 个通道。这 125 个数字包含边界框和类别预测的数据。为什么是125?那么,每个网格单元预测 5 个边界框,一个边界框由 25 个数据元素描述:

  • 边界框矩形的 x、y、宽度、高度
  • 置信度得分
  • 20 个类别的概率分布

使用 YOLO 很简单:你给它一个输入图像(大小调整为 416×416 像素),它单次通过卷积网络,并从另一端出来一个 13×13×125 张量,描述边界框网格单元。然后您需要做的就是计算边界框的最终分数,并丢弃分数低于 30% 的分数。

提示:要了解有关 YOLO 工作原理及其训练方式的更多信息,请查看其发明者之一的精彩演讲。该视频实际上描述了 YOLOv1,这是一个较旧版本的网络,其架构略有不同,但主要思想仍然相同。值得一看!

转换为金属

我刚刚描述的架构适用于 Tiny YOLO,这是我们将在 iOS 应用程序中使用的版本。完整的 YOLOv2 网络的层数是其三倍,而且有点太大,无法在当前的 iPhone 上运行得足够快。由于 Tiny YOLO 使用的层数较少,因此它比它的老大哥更快……但准确性也稍差一些。

我很确定那不是狗...

YOLO是用Darknet编写的,Darknet是YOLO作者定制的深度学习框架。可下载的权重仅以暗网格式提供。尽管Darknet 的源代码是可用的,但我并不真正期待花费大量时间来弄清楚它是如何工作的。

对我来说幸运的是,其他人已经付出了努力,并将 Darknet 模型转换为 Keras,这是我选择的深度学习工具。因此,我所要做的就是运行这个“YAD2K”脚本将 Darknet 权重转换为 Keras 格式,然后编写我自己的脚本将 Keras 权重转换为 Metal。

然而,有一个小问题……YOLO在其卷积层之后使用了一种称为批量归一化的正则化技术。

“批量归一化”背后的想法是,当数据干净时,神经网络层的工作效果最好。理想情况下,层的输入的平均值为 0 并且方差不会太大。对于任何做过机器学习的人来说,这听起来应该很熟悉,因为我们经常在输入数据上使用一种称为“特征缩放”或“白化”的技术来实现这一点。

批量标准化对层之间的数据进行类似的特征缩放。这项技术确实有助于神经网络表现得更好,因为它可以阻止数据流经网络时恶化。

为了让您了解批归一化的效果,下面是没有和有批归一化的第一个卷积层输出的直方图:

使用和不使用批量归一化的第一层输出的直方图

批量归一化在训练深度网络时很重要,但事实证明我们可以在推理时摆脱它。这是一件好事,因为不必进行批量标准化计算将使我们的应用程序更快。无论如何,金属没有MPSCNNBatchNormalization层。

批量归一化通常发生在卷积层之后但应用激活函数之前(在 YOLO 的情况下称为“泄漏”ReLU)。由于卷积和批量标准化都执行数据的线性变换,因此我们可以将批量标准化层的参数与卷积的权重结合起来。这称为将批归一化层“折叠”到卷积层中。

长话短说,通过一些数学运算,我们可以摆脱批量标准化层,但这意味着我们必须改变前面的卷积层的权重。

快速回顾一下卷积层的计算内容:如果x是输入图像中的像素,w是该层的权重,则卷积基本上为每个输出像素计算以下内容:

out[j] = x[i]*w[0] + x[i+1]*w[1] + x[i+2]*w[2] + ... + x[i+k]*w[k] + b

这是输入像素与卷积核权重加上偏置值 的点积b

这是批量归一化对该卷积输出执行的计算:

        gamma * (out[j] - mean)
bn[j] = ---------------------- + beta
            sqrt(variance)

它从输出像素中减去平均值,除以方差,乘以缩放因子 gamma,并添加偏移 beta。这四个参数 - meanvariancegammabeta- 是批量归一化层在网络训练时学习的内容。

为了摆脱批量归一化,我们可以稍微打乱这两个方程,以计算卷积层的新权重和偏差项:

           gamma * w
w_new = --------------
        sqrt(variance)

        gamma*(b - mean)
b_new = ---------------- + beta
         sqrt(variance)

在输入上使用这些新的权重和偏差项执行卷积x将得到与原始卷积加上批量归一化相同的结果。

现在我们可以删除这个批量归一化层,只使用卷积层,但使用这些调整后的权重和偏差项w_newb_new。我们对网络中的所有卷积层重复此过程。

注意: YOLO 中的卷积层实际上并不使用偏差,因此b上式中的偏差为零。但请注意,在折叠批量归一化参数后,卷积层确实获得了偏差项。

一旦我们将所有批量标准化层折叠到其前面的卷积层中,我们就可以将权重转换为金属。这是一个简单的问题,即转置数组(Keras 以与 Metal 不同的顺序存储它们)并将它们写入 32 位浮点数的二进制文件。

如果您好奇,请查看转换脚本yolo2metal.py了解更多详细信息。为了测试折叠是否有效,脚本创建了一个没有批量标准化但具有调整后的权重的新模型,并将其与原始模型的预测进行比较。

iOS 应用程序

当然,我使用Forge来构建 iOS 应用程序。 😂 你可以在YOLO文件夹中找到代码。要尝试一下:下载或克隆 Forge,在 Xcode 8.3 或更高版本中打开Forge.xcworkspace ,然后在 iPhone 6 或更高版本上运行YOLO目标。

测试该应用程序的最简单方法是将您的 iPhone 指向一些YouTube 视频

示例应用程序

有趣的代码位于YOLO.swift中。首先设置卷积网络:

let leaky = MPSCNNNeuronReLU(device: device, a: 0.1)

let input = Input()

let output = input
         --> Resize(width: 416, height: 416)
         --> Convolution(kernel: (3, 3), channels: 16, padding: true, activation: leaky, name: "conv1")
         --> MaxPooling(kernel: (2, 2), stride: (2, 2))
         --> Convolution(kernel: (3, 3), channels: 32, padding: true, activation: leaky, name: "conv2")
         --> MaxPooling(kernel: (2, 2), stride: (2, 2))
         --> ...and so on...

来自相机的输入被重新缩放为 416×416 像素,然后进入卷积层和最大池层。这与任何其他卷积网络的运行方式非常相似。

有趣的是输出会发生什么。回想一下,卷积网络的输出是一个 13×13×125 张量:覆盖在图像上的网格中的每个单元都有 125 个数据通道。这 125 个数字包含边界框和类别预测,我们需要以某种方式对它们进行排序。这发生在函数中fetchResult()

注意:中的代码fetchResult()在 CPU 上运行,而不是在 GPU 上运行。这种方式实施起来更简单。也就是说,嵌套循环可能会受益于 GPU 的并行性。也许我将来会回来写一个 GPU 版本。

工作原理如下fetchResult()

public func fetchResult(inflightIndex: Int) -> NeuralNetworkResult<Prediction> {
  let featuresImage = model.outputImage(inflightIndex: inflightIndex)
  let features = featuresImage.toFloatArray()

卷积网络的输出采用 的形式MPSImage。我们首先将其转换为一个Float名为 的值数组features,以使其更容易使用。

主体fetchResult()是一个巨大的嵌套循环。它查看所有网格单元以及每个单元的五个预测:

  for cy in 0..<13 {
    for cx in 0..<13 {
      for b in 0..<5 {
         . . .
      }
    }
  }

b在这个循环中,我们计算网格单元的边界框(cy, cx)

首先,我们从数组中读取边界框的 x、y、宽度和高度features,以及置信度得分:

let channel = b*(numClasses + 5)
let tx = features[offset(channel, cx, cy)]
let ty = features[offset(channel + 1, cx, cy)]
let tw = features[offset(channel + 2, cx, cy)]
let th = features[offset(channel + 3, cx, cy)]
let tc = features[offset(channel + 4, cx, cy)]

辅助函数offset()用于在数组中找到要读取的正确位置。 Metal 将其数据一次以 4 个通道为一组存储在纹理切片中,这意味着 125 个通道不是连续存储的,而是分散在各处。 (有关详细说明,请参阅代码。)

tx我们仍然需要对这五个数字, tytw,进行一些处理,th因为tc它们的格式有点奇怪。如果您想知道这些公式从何而来,论文中给出了它们(这是网络训练方式的副作用)。

let x = (Float(cx) + Math.sigmoid(tx)) * 32
let y = (Float(cy) + Math.sigmoid(ty)) * 32

let w = exp(tw) * anchors[2*b    ] * 32
let h = exp(th) * anchors[2*b + 1] * 32

let confidence = Math.sigmoid(tc)

现在xy表示我们用作神经网络输入的 416×416 图像中边界框的中心;wh是同一图像空间中盒子的宽度和高度。边界框的置信度值由下式给出tc,我们使用逻辑 sigmoid 将其转换为百分比。

现在我们有了边界框,并且知道 YOLO 对这个框实际上包含一个对象的信心有多大。接下来,我们看一下类预测,看看 YOLO 认为盒子里有什么样的物体:

var classes = [Float](repeating: 0, count: numClasses)
for c in 0..<numClasses {
  classes[c] = features[offset(channel + 5 + c, cx, cy)]
}
classes = Math.softmax(classes)

let (detectedClass, bestClassScore) = classes.argmax()

回想一下,数组中的 20 个通道features包含此边界框的类预测。我们将它们读入一个新数组classes。与分类器一样,我们采用 softmax 将数组转换为概率分布。然后我们选出得分最高的班级作为获胜者。

现在我们可以计算这个边界框的最终分数 - 例如,“我 85% 确定这个边界框包含一只狗”。由于总共有 845 个边界框,我们只想保留组合得分超过某个阈值的边界框。

let confidenceInClass = bestClassScore * confidence
if confidenceInClass > 0.3 {
  let rect = CGRect(x: CGFloat(x - w/2), y: CGFloat(y - h/2),
                    width: CGFloat(w), height: CGFloat(h))

  let prediction = Prediction(classIndex: detectedClass,
                              score: confidenceInClass,
                              rect: rect)
  predictions.append(prediction)
}

对网格中的所有单元格重复上述代码。当循环结束时,我们会得到一个predictions数组,其中通常包含 10 到 20 个预测。

我们已经过滤掉了得分非常低的所有边界框,但仍然可能存在与其他框重叠太多的框。因此,我们要做的最后一件事是一种称为非极大值抑制的fetchResult()技术来修剪那些重复的边界框。

  var result = NeuralNetworkResult<Prediction>()
  result.predictions = nonMaxSuppression(boxes: predictions,
                                         limit: 10, threshold: 0.5)
  return result
}

该函数使用的算法nonMaxSuppression()非常简单:

  1. 从得分最高的边界框开始。
  2. 删除重叠超过给定阈值(即超过 50%)的所有剩余边界框。
  3. 转到步骤 1,直到不再有边界框为止。

这会删除与其他得分较高的框重叠过多的任何边界框。它只保留最好的。

这几乎就是它的全部内容:一个常规的卷积网络和随后对结果进行一些后处理。

效果如何?

YOLO 网站声称Tiny YOLO 每秒可以处理高达 200 帧的数据。但当然这是在胖桌面 GPU 上,而不是在移动设备上。那么它在 iPhone 上的运行速度有多快呢?

在我的 iPhone 6s 上,处理一张图像大约需要0.15 秒。这只有 6 FPS,勉强可以称为实时。如果您将手机对准一辆驶过的汽车,您可以看到边界框稍微拖在汽车后面。尽管如此,我仍然对这项技术的效果印象深刻。 😁

注意:正如我上面所解释的,边界框的处理在 CPU 上运行,而不是在 GPU 上运行。如果YOLO完全在GPU上运行,会跑得更快吗?也许吧,但 CPU 代码只需要大约 0.03 秒,占运行时间的 20%。至少可以在 GPU 上完成部分工作,但考虑到转换层仍然占用 80% 的时间,我不确定这是否值得。

我认为主要的减速是由具有 512 和 1024 个输出通道的卷积层引起的。从我的实验来看MPSCNNConvolution,具有多个通道的小图像似乎比具有较少通道的大图像遇到更多问题。

我感兴趣的一件事是采用不同的网络架构,例如 SqueezeNet,并重新训练该网络以预测其最后一层的边界框。换句话说,采用 YOLO 的想法并将其置于更小、更快的卷积网络之上。速度的提高值得损失准确性吗?

  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值