深度学习/计算机视觉常见的8个错误总结及避坑指南
ztj100 2024-11-11 15:14 13 浏览 0 评论
人类并不是完美的,我们经常在编写软件的时候犯错误。有时这些错误很容易找到:你的代码根本不工作,你的应用程序会崩溃。但有些 bug 是隐藏的,很难发现,这使它们更加危险。
在处理深度学习问题时,由于某些不确定性,很容易产生此类错误:很容易看到 web 应用的端点路由请求是否正确,但却不容易检查梯度下降步骤是否正确。然而,在深度学习实践例程中有很多 bug 是可以避免的。
我想和大家分享一下我在过去两年的计算机视觉工作中所发现或产生的错误的一些经验。我在会议上谈到过这个话题,很多人在会后告诉我:「是的,老兄,我也有很多这样的 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)))
这个图是不对称的,看起来很奇怪!如果我们检查极值呢?
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)
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.继续谈谈关键点
即使上述函数已修复,也存在危险。接下来更多的是关于语义,而不仅仅是一段代码。
假设一个人需要用两只手掌来增强图像。看起来很安全——手在左右翻转后会还是手。
但是等等!我们对关键点语义一无所知。如果关键点真的是这样的意思呢:
kpts = [
(20, 20), # left pinky
(20, 200), # right pinky
...
]
这意味着增强实际上改变了语义: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 模块是用于这种转换的。
一个简单的修复:
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')
图像已经改变了。这是我们期望的吗?嗯,也许改变太多了。
这里有一个危险的操作:将 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
总而言之:
测试很重要;
运行断言可以用于训练管道;
可视化是一种不错的手段;
抄袭是一种诅咒;
没有什么是灵丹妙药,机器学习工程师必须时刻小心。
雷锋网雷锋网雷锋网
相关推荐
- Vue 技术栈(全家桶)(vue technology)
-
Vue技术栈(全家桶)尚硅谷前端研究院第1章:Vue核心Vue简介官网英文官网:https://vuejs.org/中文官网:https://cn.vuejs.org/...
- vue 基础- nextTick 的使用场景(vue的nexttick这个方法有什么用)
-
前言《vue基础》系列是再次回炉vue记的笔记,除了官网那部分知识点外,还会加入自己的一些理解。(里面会有部分和官网相同的文案,有经验的同学择感兴趣的阅读)在开发时,是不是遇到过这样的场景,响应...
- vue3 组件初始化流程(vue组件初始化顺序)
-
学习完成响应式系统后,咋们来看看vue3组件的初始化流程既然是看vue组件的初始化流程,咋们先来创建基本的代码,跑跑流程(在app.vue中写入以下内容,来跑流程)...
- vue3优雅的设置element-plus的table自动滚动到底部
-
场景我是需要在table最后添加一行数据,然后把滚动条滚动到最后。查网上的解决方案都是读取html结构,暴力的去获取,虽能解决问题,但是不喜欢这种打补丁的解决方案,我想着官方应该有相关的定义,于是就去...
- Vue3为什么推荐使用ref而不是reactive
-
为什么推荐使用ref而不是reactivereactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代optionapi的data的替...
- 9、echarts 在 vue 中怎么引用?(必会)
-
首先我们初始化一个vue项目,执行vueinitwebpackechart,接着我们进入初始化的项目下。安装echarts,npminstallecharts-S//或...
- 无所不能,将 Vue 渲染到嵌入式液晶屏
-
该文章转载自公众号@前端时刻,https://mp.weixin.qq.com/s/WDHW36zhfNFVFVv4jO2vrA前言...
- vue-element-admin 增删改查(五)(vue-element-admin怎么用)
-
此篇幅比较长,涉及到的小知识点也比较多,一定要耐心看完,记住学东西没有耐心可不行!!!一、添加和修改注:添加和编辑用到了同一个组件,也就是此篇文章你能学会如何封装组件及引用组件;第二能学会async和...
- 最全的 Vue 面试题+详解答案(vue面试题知识点大全)
-
前言本文整理了...
- 基于 vue3.0 桌面端朋友圈/登录验证+60s倒计时
-
今天给大家分享的是Vue3聊天实例中的朋友圈的实现及登录验证和倒计时操作。先上效果图这个是最新开发的vue3.x网页端聊天项目中的朋友圈模块。用到了ElementPlus...
- 不来看看这些 VUE 的生命周期钩子函数?| 原力计划
-
作者|huangfuyk责编|王晓曼出品|CSDN博客VUE的生命周期钩子函数:就是指在一个组件从创建到销毁的过程自动执行的函数,包含组件的变化。可以分为:创建、挂载、更新、销毁四个模块...
- Vue3.5正式上线,父传子props用法更丝滑简洁
-
前言Vue3.5在2024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性...
- Vue 3 生命周期完整指南(vue生命周期及使用)
-
Vue2和Vue3中的生命周期钩子的工作方式非常相似,我们仍然可以访问相同的钩子,也希望将它们能用于相同的场景。...
- 救命!这 10 个 Vue3 技巧藏太深了!性能翻倍 + 摸鱼神器全揭秘
-
前端打工人集合!是不是经常遇到这些崩溃瞬间:Vue3项目越写越卡,组件通信像走迷宫,复杂逻辑写得脑壳疼?别慌!作为在一线摸爬滚打多年的老前端,今天直接甩出10个超实用的Vue3实战技巧,手把...
- 怎么在 vue 中使用 form 清除校验状态?
-
在Vue中使用表单验证时,经常需要清除表单的校验状态。下面我将介绍一些方法来清除表单的校验状态。1.使用this.$refs...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Vue 技术栈(全家桶)(vue technology)
- vue 基础- nextTick 的使用场景(vue的nexttick这个方法有什么用)
- vue3 组件初始化流程(vue组件初始化顺序)
- vue3优雅的设置element-plus的table自动滚动到底部
- Vue3为什么推荐使用ref而不是reactive
- 9、echarts 在 vue 中怎么引用?(必会)
- 无所不能,将 Vue 渲染到嵌入式液晶屏
- vue-element-admin 增删改查(五)(vue-element-admin怎么用)
- 最全的 Vue 面试题+详解答案(vue面试题知识点大全)
- 基于 vue3.0 桌面端朋友圈/登录验证+60s倒计时
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)