一个提升图像识别准确率的精妙技巧

图灵汇官网

起因

今天和同学讨论了正在合作的一个项目,他特别指出了其中一段Python代码的巧妙之处。起初我没有完全理解,但经过反复思考,终于弄明白了这个问题,并为此感到兴奋。因此,我决定总结一下这个问题,并分享我的思考。

背景

像素坐标系

我们知道,计算机图像由像素组成。简单来说,图像可以看作红、绿、蓝三个颜色通道的二维矩阵组合。每个通道都是一个矩阵,这样在屏幕上,同一个像素点的三种颜色混合在一起形成一幅彩色图像。如下图所示:

像素坐标系

人类看到的是五彩斑斓的图像,而计算机看到的实际上是填充了数字的矩阵。

Bounding Box

在图像识别过程中,一个重要的任务是通过Bounding Box框出图像的大致区域。这一步不需要精确区分每个像素,只需大致框出物体的范围。如下图所示:

Bounding Box

那么,如何表示这个Bounding Box呢?

  • 需要画出边框上的每个点吗?显然不需要。
  • 最少需要几个点?可以发现四个顶点是最特殊的,但是否每个顶点都需要呢?显然也不需要。
  • 最少我们只需要两个不是共线的顶点,即互为对角线的顶点即可。
  • 习惯上,我们取左上角和右下角的顶点(从人的角度看,暂时不考虑像素坐标系的方向问题)。

图像识别算法

如上图所示,图像识别算法就是对任意输入的图像,判断其Bounding Box的位置是否准确。为了使算法能够学习,我们通常会预先通过Bounding Box标记出图像的位置,然后训练神经网络去预测可能出现的Bounding Box位置。

简而言之,我们先标记一些Bounding Box,然后通过训练算法,最终实现对未知图像的Bounding Box预测。

精度(Precision)和召回率(Recall)

这时,一个自然的问题出现了:我们如何判断预测是否准确?

通常在机器学习算法中,我们使用两个指标来衡量算法的性能:精度(Precision)和召回率(Recall)。简单地说,用疾病检测为例,假设在10000人的样本中有500个阳性病人需要被预测出来。现在设计了一个算法,预测了400个人是阳性,但实际上这400人中只有300人是真的阳性(预测正确),其余100人是阴性(预测错误)。因此,精度(Precision)是预测正确的结果占总预测结果的比例,即300 / 400 = 0.75。换句话说,精度是所有预测结果中有多少是对的。

召回率(Recall)表示预测结果中正确的部分占所有目标用户的比例。可以看到,我们的目标是预测出500个阳性病人,但预测的400人中只有300个是对的,所以召回率是300 / 500 = 0.6。换句话说,召回率是覆盖率,我们希望算法能够覆盖更多的目标人群。

极端情况下,如果你只预测一个人且这个人是真正的阳性,那么精度将是1 / 1 = 100%,但召回率却很低,仅为1 / 500 = 0.002。另一个极端情况是,你认为所有人都可能是阳性病人,那么召回率将是1。因为你把所有人都包括了,预测对的人数是500,而期望的目标人群也是500,所以召回率是500 / 500 = 1。换句话说,通过极端严格的筛查条件,虽然实现了全覆盖,但精度却很低,只有500 / 10000 = 0.05。

因此,在日常应用中,我们往往需要综合这两个指标。常见的指标有F1分数等,感兴趣的读者可以自行研究。

问题

问题的提出

我们集中讨论今天遇到的问题: - 假设对于某个图像,我的算法提出了N个预测的Bounding Box,同时知道该图像的M个正确的Bounding Box,如何快速准确地计算精度(Precision)和召回率(Recall)?

下面我给出问题的预备代码和绘图代码帮助大家理解这个问题:

```python import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as patches

创建数组数据

predict = np.array([ [1, 2, 2, 1], [4.5, 2.5, 2, 1], [6, 6, 8, 4] ], dtype=np.double)

truth = np.array([ [1, 4, 3, 3], [5, 2, 8, 1] ], dtype=np.double)

下面展示问题布局

红色代表真实值

蓝色代表预测值

fig = plt.figure() ax = fig.add_subplot(111, aspect='equal') recList = []

添加蓝色矩形框(预测)

for rect in predict: recList.append( patches.Rectangle( (rect[0], rect[3]), np.abs(rect[2] - rect[0]), np.abs(rect[3] - rect[1]), fill=False, edgecolor="blue" ) )

添加红色矩形框(真实值)

for rect in truth: recList.append( patches.Rectangle( (rect[0], rect[3]), np.abs(rect[2] - rect[0]), np.abs(rect[3] - rect[1]), fill=False, edgecolor="red" ) )

绘制图形

for p in recList: ax.add_patch(p)

plt.plot() plt.show() fig.savefig('rect.png', dpi=90, bbox_inches='tight') ```

具体图像如下:

图像示例

同时,我们做一个简化,只考虑每个矩阵中心是否在可接受的误差范围内重合。这相当于我们暂时只考虑位置而不考虑大小,因为大小通常会在后续进行精细调节。大部分算法首先预测位置,然后再逐步细化大小。

计算中心可以使用如下函数:

python def bbox2centroid(bboxes): return np.column_stack(((bboxes[:, 0] + bboxes[:, 2]) / 2, (bboxes[:, 1] + bboxes[:, 3]) / 2))

问题的思路

一个简单的思路是逐个比较。这样的话,你的算法需要编写大量的循环,非常费力。

因此,在实际工作中,我们大量使用矩阵操作,以避免循环。因为矩阵操作通常可以避免循环,并且如果你能够使用GPU(图像识别通常在GPU上运行),矩阵操作本身是经过特别优化的,特别适合GPU运行,从而提高速度。

但是如何操作呢?

可以看到,我特意给出了预测Bounding Box数量和真实Bounding Box数量不一致的情况,因此如果不小心,很容易出现矩阵维度不匹配的问题,导致所有计算失败。

因此,我们可以用上面的例子来帮助思考,首先求中心的坐标,这样原来的N x 4矩阵和M x 4矩阵就变成了N x 2矩阵和M x 2矩阵。

numpynewaxis

在这里,不得不提一个小技巧。在numpy中,None也可以作为一个新维度的占位符,称为numpy.newaxis。用官方文档的话说,这叫做numpy.newaxis

python The newaxis object can be used in all slicing operations to create an axis of length one. None can be used in place of this with the same result.

也就是说,如果你想让你的矩阵扩展出一个新的维度,比如从长度为3的向量到3 x 1或者1 x 3,你可以这样写:

```python A = np.array([1, 3, 5]) print(A) print(A.shape)

print("====1====") print(A[:, None]) print(A[:, None].shape)

print("====2====") print(A[None, :]) print(A[None, :].shape) ```

输出是:

python [1 3 5] (3,) ====1==== [[1] [3] [5]] (3, 1) ====2==== [[1 3 5]] (1, 3)

那么二维的情况,新增加一个维度,用上面的写法,我们可以看到结果是:

```python A = np.array([[1, 3, 5], [7, 8, 9]]) print(A) print(A.shape)

print("====1====") print(A[:, None]) print(A[:, None].shape)

print("====2====") print(A[None, :]) print(A[None, :].shape) ```

输出是:

```python [[1 3 5] [7 8 9]] (2, 3) ====1==== [[[1 3 5]]

[[7 8 9]]] (2, 1, 3) ====2==== [[[1 3 5] [7 8 9]]] (1, 2, 3) ```

这样,在numpy的操作中,如果需要增加一个额外的维度,比如存储两个矩阵做差的结果,就可以方便地将结果存储在新增加的这个维度,后面会进一步解释。

问题的解决代码

这里直接给出代码:

```python def computescoredetailbycentroid(predbbox, gtbbox, tolerance=(2, 2)): predc = bbox2centroid(predbbox) gtc = bbox2centroid(gtbbox) diffs = abs(predc[:, None] - gtc)

x1, x2 = np.nonzero((diffs < tolerance).all(axis=-1))

tp = len(x1)
fp = len(pred_c) - tp
fn = len(gt_c) - tp

precision = tp / (tp + fp)
recall = tp / (tp + fn)

return precision, recall

```

这段代码用于计算精度(Precision)和召回率(Recall),通过中心点的比较来确定预测的Bounding Box是否准确。

本文来源: 图灵汇 文章作者: 川蜀无人机