锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

CV新手避坑指南:计算机视觉常见的8个错误

时间:2023-07-18 17:07:00 d40传感器

点击上方“小白学视觉”,选择加"星标"或“置顶

重磅干货,第一时间送达  

本文转自:机器学习实验室

人类并不完美。我们在编写软件时经常犯错误。有时很容易找到这些错误:您的代码根本不工作,您的应用程序将崩溃。 bug 它是隐藏的,很难发现,这使它们更危险。

在处理深度学习问题时,由于一些不确定性,很容易出现这样的错误:很容易看到 web 应用程序的端点路由要求是否正确,但不容易检查梯度下降步骤是否正确。然而,在深度学习实践中有许多例子 bug 可以避免。

我想和你们分享一些我在过去两年的计算机视觉工作中发现或产生的错误经验。我在会议上谈到了这个话题,很多人在会后告诉我:「是的,兄弟,我也有很多这样的东西 bug。」我希望我的文章能帮助你避免一些问题。

1.翻转图像和关键点

假设有人在研究关键点检测问题。它们的数据看起来像一对图像和一系列关键点组,比如 [0,1),(2,2)] x 和 y 坐标。

让我们基本增强这些数据:

defflip_img_and_keypoints(img:np.ndarray,kpts:Sequence[Sequence[int]]): img=np.fliplr(img) h,w,*_=img.shape kpts=[(y,w-x)fory,xinkpts] return img, kpts 

上面的代码看起来很对,不是吗?接下来,让我们可视化它:

image=np.ones((10,10),dtype=np.float32) kpts=[(0,1),(2,2)] image_flipped, kpts_flipped = flip_img_and_keypoints(image, kpts)   img1 = image.copy()   fory,xinkpts: img1[y,x]=0  img2=image_flipped.copy() fory,xinkpts_flipped: img2[y,x]=0 _ = plt.imshow(np.hstack((img1, img2))) 

这张图不对称,看起来很奇怪!如果我们检查极值呢?

image=np.ones((10,10),dtype=np.float32) kpts=[(0,0),(1,1)] image_flipped, kpts_flipped = flip_img_and_keypoints(image, kpts)   img1=image.copy() fory,xinkpts:        img1[y, x] = 0   img2=image_flipped.copy() fory,xinkpts_flipped:        img2[y, x] = 0   -------------------------------------------------------------------- -------   IndexError Traceback(mostrecentcalllast) in 8img2=image_flipped.copy() 9fory,xinkpts_flipped: --->10img2[y,x]=0 IndexError: index 10 is out of bounds for axis 1 with size 10 

不好!这是典型的错误。正确的代码如下:

defflip_img_and_keypoints(img:np.ndarray,kpts:Sequence[Sequence[int]]): img=np.fliplr(img) h,w,*_=img.shape kpts=[(y,w-x-1)fory,xinkpts] return img, kpts 

我们已经通过可视化检测到了这个问题,但是,使用它 x=0 单元测试也有帮助。一个有趣的事实是,我们团队的三个人(包括我自己)独立犯了几乎相同的错误。

2.继续谈重点

即使上述函数已经修复,也有危险。接下来更多的是关于语义,而不仅仅是代码。

假设一个人需要用两只手掌来增强图像。看起来很安全——左右翻转后手还是手。

但等等!我们对关键点的语义一无所知。假如关键点真的是这样的意思:

kpts=[ (20,20),#leftpinky (20,200),#rightpinky ... ] 

这意味着增强实际上改变了语义:left 变为 right,right 变为 left,但是我们不交换数组中的数组 keypoints 索引。它会给训练带来巨大的噪音和更糟糕的指标。

这里要吸取教训:

  • 在应用程序增强或其他特性之前,理解和考虑数据结构和语义;

  • 保持实验的独立性:如果分数提高,请添加一个小的变化(例如,一个新的转换)来检查它是如何进行的。

3.定制损失函数

熟悉语义分割的人可能知道 IoU (interp over union)测量。不幸的是,我们不能直接使用它 SGD 优化它,所以一个常见的技巧是用微损失函数接近它。让我们编写相关代码!

defiou_continuous_loss(y_pred,y_true):       eps = 1e-6
       def _sum(x):
             return x.sum(-1).sum(-1)
       numerator = (_sum(y_true * y_pred) + eps)
       denominator = (_sum(y_true ** 2) + _sum(y_pred ** 2) -_sum(y_true * y_pred) + eps)
return (numerator / denominator).mean()

     看起来很不错,让我们做一个小小的检查:

In [3]: ones = np.ones((1, 3, 10, 10))
       ...: x1 = iou_continuous_loss(ones * 0.01, ones)
...: x2 = iou_continuous_loss(ones * 0.99, ones)


In [4]: x1, x2
Out[4]: (0.010099999897990103, 0.9998990001020204)

     在 x1 中,我们计算了与标准答案完全不同的损失,x2 是非常接近标准答案的函数的结果。我们预计 x1 会很大,因为预测结果并不好,x2 应该接近于零。这其中发生了什么?

     上面的函数是度量的一个很好的近似。度量不是损失:它通常越高越好。因为我们要用 SGD 把损失降到最低,我们真的应该采用用相反的方法:

def iou_continuous(y_pred, y_true):
       eps = 1e-6
       def _sum(x):
return x.sum(-1).sum(-1)
       numerator = (_sum(y_true * y_pred) + eps)
       denominator = (_sum(y_true ** 2) + _sum(y_pred ** 2)- _sum(y_true * y_pred) + eps)
return (numerator / denominator).mean()


def iou_continuous_loss(y_pred, y_true):
return 1 - iou_continuous(y_pred, y_true)

这些问题可以通过两种方式确定:

  • 编写一个单元测试来检查损失的方向:形式化地表示一个期望,即更接近实际的东西应该输出更低的损失;

  • 做一个全面的检查,尝试过拟合你的模型的 batch。

4.使用 Pytorch

    假设一个人有一个预先训练好的模型,并且是一个时序模型。我们基于 ceevee api 编写预测类。

from ceevee.base import AbstractPredictor
class MySuperPredictor(AbstractPredictor):
       def __init__(self, weights_path: str, ):
              super().__init__()
              self.model = self._load_model(weights_path=weights_path)
       def process(self, x, *kw):
              with torch.no_grad():
                     res = self.model(x)
return res


       @staticmethod
       def _load_model(weights_path):
              model = ModelClass()
              weights = torch.load(weights_path, map_location='cpu')
              model.load_state_dict(weights)
return model

     这个代码正确吗?也许吧!对某些模型来说确实是正确的。例如,当模型没有规范层时,例如 torch.nn.BatchNorm2d;或者当模型需要为每个图像使用实际的 norm 统计信息时(例如,许多基于 pix2pix 的架构需要它)。

     但是对于大多数计算机视觉应用程序来说,代码遗漏了一些重要的东西:切换到评估模式。

     如果试图将动态 pytorch 图转换为静态 pytorch 图,则很容易识别此问题。有一个 torch.jit 模块是用于这种转换的。

     一个简单的修复:

In [4]: model = nn.Sequential(
        ...: nn.Linear(10, 10),
        ...: nn.Dropout(.5)
        ...: )
        ...:
        ...: traced_model = torch.jit.trace(model.eval(), torch.rand(10))
# No more warnings!

     此时,torch.jit.trace 多次运行模型并比较结果。这里看起来似乎没有区别。然而,这里的 torch.jit.trace 不是万能的。这是一种应该知道并记住的细微差别。

5.复制粘贴问题

     很多东西都是成对存在的:训练和验证、宽度和高度、纬度和经度……如果仔细阅读,你可以很容易地发现由一对成员之间的复制粘贴引起的错误:

def make_dataloaders(train_cfg, val_cfg, batch_size):
       train = Dataset.from_config(train_cfg)
       val = Dataset.from_config(val_cfg)
       shared_params = {'batch_size': batch_size, 'shuffle': True,'num_workers': cpu_count()}
       train = DataLoader(train, **shared_params)
       val = DataLoader(train, **shared_params)
return train, val

     不仅仅是我犯了愚蠢的错误。在流行库中也有类似的错误。

#https://github.com/albu/albumentations/blob/0.3.0/albumentations/aug mentations/transforms.py
def apply_to_keypoint(self, keypoint, crop_height=0, crop_width=0, h_start=0, w_start=  0, rows=0, cols=0, **params):
        keypoint = F.keypoint_random_crop(keypoint, crop_height, crop_width, h_start, w_start, rows, cols)
        scale_x = self.width / crop_height
        scale_y = self.height / crop_height
        keypoint = F.keypoint_scale(keypoint, scale_x, scale_y) 
return keypoint

     别担心,这个错误已经修复了。如何避免?不要复制粘贴代码,尽量以不要以复制粘贴的方式进行编码。

datasets = []
data_a = get_dataset(MyDataset(config['dataset_a']), config['shared_param'], param_a) datasets.append(data_a)
data_b = get_dataset(MyDataset(config['dataset_b']), config['shared_param'], param_b) datasets.append(data_b)


datasets = []
for name, param in zip(('dataset_a', 'dataset_b'), (param_a, param_b), ):
        datasets.append(get_dataset(MyDataset(config[name]), config['shared_param'], param))

6.合适的数据类型

     让我们再做一个增强:

def add_noise(img: np.ndarray) -> np.ndarray:
        mask = np.random.rand(*img.shape) + .5
        img = img.astype('float32') * mask
return img.astype('uint8')

     图像已经改变了。这是我们期望的吗?嗯,也许改变太多了。这里有一个危险的操作:将 float32 转到 uint8。这可能导致溢出:

def add_noise(img: np.ndarray) -> np.ndarray:
       mask = np.random.rand(*img.shape) + .5
       img = img.astype('float32') * mask
return np.clip(img, 0, 255).astype('uint8')


img = add_noise(cv2.imread('two_hands.jpg')[:, :, ::-1]) _ = plt.imshow(img)

    看起来好多了,是吧?顺便说一句,还有一个方法可以避免这个问题:不要重新发明轮子,可以在前人的基础上,修改代码。例如:albumentations.augmentations.transforms.GaussNoise 。我又产生了同样来源的 bug。

     这里出了什么问题?首先,使用三次插值调整 mask 的大小是个坏主意。将 float32 转换为 uint8 也存在同样的问题:三次插值可以输出大于输入的值,并导致溢出。

     我发现了这个问题。在你的循环里面有断言也是一个好主意。

7.打字错误

     假设需要对全卷积网络(如语义分割问题)和一幅巨大的图像进行处理。图像太大了,你没有机会把它放进你的 gpu 中——例如,它可以是一个医学或卫星图像。

     在这种情况下,可以将图像分割成一个网格,独立地对每一块进行推理,最后合并。另外,一些预测交集可以用来平滑边界附近的伪影。

     我们来编码吧!

from tqdm import tqdm
class GridPredictor:
""" This class can be used to predict a segmentation mask for the big image when you have GPU memory limitation """
       def __init__(self, predictor: AbstractPredictor, size: int, stride: Optional[int] = None):               
              self.predictor = predictor
              self.size = size
              self.stride = stride if stride is not None else size // 2


       def __call__(self, x: np.ndarray):
              h, w, _ = x.shape
              mask = np.zeros((h, w, 1), dtype='float32')
              weights = mask.copy()
              
              for i in tqdm(range(0, h - 1, self.stride)):
                     for j in range(0, w - 1, self.stride):
                            a, b, c, d = i, min(h, i + self.size), j, min(w, j + self.size)
                            patch = x[a:b, c:d, :]
                            mask[a:b, c:d, :] += np.expand_dims(self.predictor(patch), -1) weights[a:b, c:d, :] = 1
return mask / weights

     有一个符号输入错误,代码片段足够大,因此可以很容易地找到它。我怀疑仅仅通过代码就可以快速识别它,很容易检查代码是否正确:

class Model(nn.Module):
        def forward(self, x):
              return x.mean(axis=-1)
              
model = Model()
grid_predictor = GridPredictor(model, size=128, stride=64)


simple_pred = np.expand_dims(model(img), -1)
grid_pred = grid_predictor(img)
np.testing.assert_allclose(simple_pred, grid_pred, atol=.001)

     调用方法的正确版本如下:

def __call__(self, x: np.ndarray):
       h, w, _ = x.shape
       mask = np.zeros((h, w, 1), dtype='float32')
       weights = mask.copy()


       for i in tqdm(range(0, h - 1, self.stride)):
              for j in range(0, w - 1, self.stride): a, b, c, d = i, min(h, i + self.size), j, min(w, j + self.size)
                     patch = x[a:b, c:d, :]
                     mask[a:b, c:d, :] += np.expand_dims(self.predictor(patch), -1)
                     weights[a:b, c:d, :] += 1
return mask / weights

     如果你仍然没有看出问题所在,请注意线宽 [a:b,c:d,:]+=1。

8.ImageNet 规范化

     当一个人需要进行迁移学习时,通常最好像训练 ImageNet 时那样对图像进行标准化。

     让我们使用我们已经熟悉的 albumentations 库。

from albumentations import Normalize
norm = Normalize()
img = cv2.imread('img_small.jpg')
mask = cv2.imread('mask_small.png', cv2.IMREAD_GRAYSCALE)
mask = np.expand_dims(mask, -1) # shape (64, 64) -> shape (64, 64, 1)


normed = norm(image=img, mask=mask)
img, mask = [normed[x] for x in ['image', 'mask']]


def img_to_batch(x):
       x = np.transpose(x, (2, 0, 1)).astype('float32')
return torch.from_numpy(np.expand_dims(x, 0))


img, mask = map(img_to_batch, (img, mask))
criterion = F.binary_cross_entropy

     现在是时候训练一个网络并使其过拟合某一张图像了——正如我所提到的,这是一种很好的调试技术:

model_a = UNet(3, 1)
optimizer = torch.optim.Adam(model_a.parameters(), lr=1e-3)
losses = []


for t in tqdm(range(20)):
       loss = criterion(model_a(img), mask)
       losses.append(loss.item())
       optimizer.zero_grad()
       loss.backward()
       optimizer.step()
       
_ = plt.plot(losses)

     曲率看起来很好,但交叉熵的损失值预计不会是 -300。这是怎么了?图像的标准化效果很好,需要手动将其缩放到 [0,1]。

model_b = UNet(3, 1)
optimizer = torch.optim.Adam(model_b.parameters(), lr=1e-3)
losses = []


for t in tqdm(range(20)):
       loss = criterion(model_b(img), mask / 255.)
       losses.append(loss.item())
       optimizer.zero_grad()
       loss.backward()
       optimizer.step()
       
_ = plt.plot(losses)

     训练循环中一个简单的断言(例如 assert mask.max()<=1)会很快检测到问题。同样,单元测试也可以检测到问题。

  原文链接:

https://medium.com/@arseny_info/8-deep-learning-computer-vision-bugs-and-how-i-could-have-avoided-them-d40b0e4b1da

下载1:OpenCV-Contrib扩展模块中文版教程

在「小白学视觉」公众号后台回复:扩展模块中文教程即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。

下载2:Python视觉实战项目52讲

在「小白学视觉」公众号后台回复:Python视觉实战项目即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。

下载3:OpenCV实战项目20讲

在「小白学视觉」公众号后台回复:OpenCV实战项目20讲即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。

交流群

欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章