计算机视觉领域还有那些坑,深度学习/计算机视觉常见的8个错误总结及避坑指南...

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

在处理深度学习问题时,由于某些不确定性,很容易产生此类错误:很容易看到 web 应用的端点路由请求是否正确,但却不容易检查梯度下降步骤是否正确。然而,在深度学习实践例程中有很多 bug 是可以避免的。

4e0c7e900e6cb5141c0e8d44c35fc9d7.png

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

1.翻转图像和关键点

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

让我们对这些数据编进行基本的增强:

def flip_img_and_keypoints(img: np.ndarray, kpts:

Sequence[Sequence[int]]):

img = np.fliplr(img)

h, w, *_ = img.shape

kpts = [(y, w - x) for y, x in kpts]

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()

for y, x in kpts:

img1[y, x] = 0

img2 = image_flipped.copy()

for y, x in kpts_flipped:

img2[y, x] = 0

_ = plt.imshow(np.hstack((img1, img2)))

f514e7695a1cc906d384d8cb80ee76c6.png

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

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()

for y, x in kpts:

img1[y, x] = 0

img2 = image_flipped.copy()

for y, x in kpts_flipped:

img2[y, x] = 0

-------------------------------------------------------------------- -------

IndexError

Traceback (most recent call last)

in

8 img2 = image_flipped.copy()

9 for y, x in kpts_flipped:

---> 10 img2[y, x] = 0

IndexError: index 10 is out of bounds for axis 1 with size 10

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

def flip_img_and_keypoints(img: np.ndarray, kpts: Sequence[Sequence[int]]):

img = np.fliplr(img)

h, w, *_ = img.shape

kpts = [(y, w - x - 1) for y, x in kpts]

return img, kpts

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

2.继续谈谈关键点

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

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

1dece00fc3726a54cf25306a9c5a25c9.png

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

kpts = [

(20, 20), # left pinky

(20, 200), # right pinky

...

]

cb57857030149720b78dc0ccd33849fa.png

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

这里应该吸取教训:

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

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

3.自定义损失函数

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

def iou_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 把损失降到最低,我们真的应该采用用相反的方法:

v> 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 模块是用于这种转换的。

3afb2e4f2e0c66d6f7f8a80ca145d626.png

一个简单的修复:

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.复制粘贴问题

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

v> 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')

67679561307d4a0c1937f9ac84dea3c3.png

图像已经改变了。这是我们期望的吗?嗯,也许改变太多了。

这里有一个危险的操作:将 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)

14a3e1c73fc744661e334033d05c7bc6.png

看起来好多了,是吧?

顺便说一句,还有一个方法可以避免这个问题:不要重新发明轮子,可以在前人的基础上,修改代码。例如:albumentations.augmentations.transforms.GaussNoise 。

我又产生了同样来源的 bug。

59de9dcc890ed9d32e6f72f9dc0b82b0.png

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

baf7b5e0ef5e1cb9c40f845d6aba2c64.png

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

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)

8ada0eb96c9a921921a29be13d6faf60.png

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

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)

a36cef0f90b457cf020b821e526ad442.png

曲率看起来很好,但交叉熵的损失值预计不会是 -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)

6af1adc82459e95b49986b92ce0902fc.png

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

总而言之:

测试很重要;

运行断言可以用于训练管道;

可视化是一种不错的手段;

抄袭是一种诅咒;

没有什么是灵丹妙药,机器学习工程师必须时刻小心。

雷锋网雷锋网雷锋网

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/298533.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL从原理到实践,一篇从头到尾讲清楚

前两天&#xff0c;我跟一个面试官聊天&#xff0c;发现一个普遍现象&#xff0c;不少候选人&#xff0c;对数据库的认知&#xff0c;还处在比较基础的阶段&#xff0c;以为会写“增删改查”、做表关联就足够了&#xff0c;那些工作中经常出现的问题&#xff0c;却支支吾吾答不…

库卡机器人是s7编程_「西门子1200PLC教程」19.S7-1200入门实例

头条号私信回复1&#xff0c;可免费获取海量资源下载链接本文任务&#xff1a;电动机启保停控制练习按下瞬时启动按钮I0.6&#xff0c;电动机Q0.0启动&#xff1b;按下瞬时停止按钮I0.7&#xff0c;电动机Q0.0停止。目录1.组态设备2.编写程序3.下载项目4.监视运行情况1.组态设备…

戴尔新版bios设置中文_戴尔电脑装机过程

戴尔电脑装机过程U盘制作过程&#xff1a;一&#xff1a;启动盘安装 win 101、准备工具&#xff1a;一块空的 8G 以上的 U 盘、一套官网下载的 win 10 操作系统 2、制作启动盘&#xff1a;将下载好的操作系统直接解压到U盘里面就完成了 二&#xff1a;pe 盘安装 win 101、准备工…

云计算

一 云计算 1 定义1&#xff09;新兴的互联网服务&#xff0c;该服务即是由成千上万的超级计算机构成的超强的计算机处理能力。2&#xff09;用户可以通过购买这种服务来满足本地对计算机运算能力需求。3&#xff09;互联网服务商一般会通过与某些需要计算机运算能力的软件的集成…

一组超炫酷的动图,感受那让人窒息的数学之美!

全世界只有3.14 % 的人关注了爆炸吧知识在蒋迅博客上看到的一组图片&#xff0c;转自imgur。本文来源&#xff1a;蒋迅的博客原文连接&#xff1a;http://blog.sciencenet.cn/blog-420554-923731.html

《飞机大战》安卓游戏开发源码(三)

为什么80%的码农都做不了架构师&#xff1f;>>> 本文章属于原创性文章&#xff0c;珍惜他人劳动成果&#xff0c;转载请注明出处&#xff1a;http://www.pm-road.com/index.php/2014/11/06/161/ 前言&#xff1a;最 近闲来无事&#xff0c;而且也是因为刚接触安卓不…

抽屉远离在计算机的应用,抽屉原理的应用及其推广优秀毕业论文

抽屉原理的应用及其推广优秀毕业论文 抽屉原理的应用及其推广 数学与计算机科学学院 数学与应用数学 指导老师&#xff1a; 王美能 摘要&#xff1a;抽屉原理也叫鸽巢原理&#xff0c;是研究如何将元素分类的一个原理&#xff0c;也是组合数学里最简单、最基本的原理。本文简述…

马斯克脑机接口_马斯克的脑机接口,让我倍感担忧

前段时间&#xff0c;特斯拉创始人马斯克展示了一项研究成果——把芯片装在人脑袋里。按照他的说法&#xff0c;植入的芯片不仅不会对人脑造成任何伤害&#xff0c;相反的&#xff0c;可以解决很多神经系统问题&#xff0c;比如注意力不集中、上瘾、焦虑等。不仅如此&#xff0…

kivy中文手册python_K-Meleon

K-Meleon K-Meleon - 一手掌控你的瀏覽體驗 K-Meleon 是一個速度超快、高度自訂、輕量化的網路瀏覽器&#xff0c;採用 Mozilla 開發、用於 Firefox 的 Gecko 佈局引擎。K-Meleon 完全免費、使用 GNU General Public License 授權的開放源碼軟體&#xff0c;專為 Microsoft Win…

男孩子也是要护肤的!!!

1 如果让一个黑人在黑暗中穿一件白色的衣服会是什么样的画面&#xff1f;2 自己做蛋糕翻车现场别人的▼你的▼3 单生狗的你4 在家健身翻车现场5 小朋友为了吃有多拼6 男孩子也是要护肤的你点的每个赞&#xff0c;我都认真当成了喜欢

JS对数据分页的封装方法

为什么80%的码农都做不了架构师&#xff1f;>>> 该文章属于原创&#xff1a;更多详细说明查看&#xff1a;http://www.pm-road.com/index.php/2014/07/24/26/ 因为web端经常会展示很多数据&#xff0c;如果一页下来&#xff0c;肯定会看的眼花缭乱&#xff0c;所以…

xftp6设置默认打开文件的程序_修改文件默认打开方式,不改变原图标

由于经常需要看pdf文件&#xff0c;一直以来都是用的Microsoft Edge阅读pdf文件&#xff0c;但是Microsoft Edge打开pdf文件字体的清晰度不高&#xff0c;而且使用ctrlf进行查找时&#xff0c;高亮部分不够明显&#xff0c;所以决定用Chrome来打开pdf文件&#xff0c;于是就将p…

名片管理系统python详解_名的解释|名的意思|汉典“名”字的基本解释

【丑集上】【口】 名康熙筆画&#xff1a;6 部外筆画&#xff1a;3【唐韻】武幷切【集韻】【韻會】彌幷切【正韻】眉兵切&#xff0c; &#x20024;音詺。【說文】自命也。从口从夕。夕者&#xff0c;冥也。冥不相見&#xff0c;故以口自名。【玉篇】號也。【廣韻】名字也。【…

简单三分钟,本地搭建k8s

使用 minikube 在本地搭建 k8s 已经比以前要简单很多了。本文&#xff0c;我们通过简短的三分钟来重现一下在本地搭建 k8s 实验环境的步骤。下载 Minikube 首先&#xff0c;你可能会考虑从官网下载 minikube 然后进行安装&#xff0c;但是这样实际上可以预知的是&#xff0c;在…

作为史上最疯狂的科学家,特斯拉提出过哪些惊世骇俗的设想

全世界只有3.14 % 的人关注了爆炸吧知识尼古拉特斯拉是有史以来最疯狂的科学家之一&#xff0c;不管你认为他是天才还是疯子&#xff0c;都无法否认他的才华和想象力。特斯拉在他的时代提出了许多天才的发明和想法&#xff0c;其中一些直到今天都被认为是不切实际或极其危险的。…

新电脑怎么测试软件,鲁大师如何测试新电脑 鲁大师测新机方法

核心提示&#xff1a;鲁大师如何测试新电脑呢&#xff1f;快来看看吧。1、首先需要先打开鲁大师这款软件&#xff0c;进入到主界面&#xff0c;在看新机之前有需要注意的几点&#xff0c;如果电脑的拥有者之前本来就是通过二手的渠道购买的电脑&#xff0c;那电脑本身就是二手的…

机器人等级考试一级教具_全国青少年机器人技术等级考试(一级):基本结构...

​机器人技术&#xff0c;是衡量一个国家科技创新和高端制造业水平的重要标志。大力推动机器人技术创新与行业发展&#xff0c;关键在于人才的培养。少年强&#xff0c;则国强。机器人作为信息化教育的新载体&#xff0c;不仅要让更多中国青少年了解机器人的发展&#xff0c;掌…

react dispatch_React测试的那些事(三) React Hook 测试实例

useReducer测试 useReducer 首先需要在组件中用 actions 和 reducers &#xff0c;代码如下。Reducerimport * as ACTIONS from ./actionsexport const initialState {stateprop1: false, }export const Reducer1 (state initialState, action) > {switch(action.type) {…

Blazor 组件库开发指南

翻译自 Waqas Anwar 2021年5月21日的文章 《A Developer’s Guide To Blazor Component Libraries》 [1]Blazor 的核心是组件&#xff0c;我们创建不同类型的组件并在整个项目中重用它们。没有人想重复造轮子&#xff0c;因此创建一个可重用的 Blazor 组件库始终是一个好主意&a…

今年大学毕业照画风太清奇.....我忍不住笑出了声!

全世界只有3.14 % 的人关注了爆炸吧知识转眼又到毕业季&#xff0c;今年毕业季因疫情有些不一样。PS云毕业照、云答辩、云面试&#xff0c;不少毕业生调侃自己是“云毕业”。最近不少大学生在网上哀嚎&#xff0c;原因是学校要他们上交照片&#xff0c;以便制作毕业照&#xff…