您正在使用IE低版浏览器,为了您的FUTUREAI账号安全和更好的产品体验,强烈建议使用更快更安全的浏览器
FUTUREAI 业界
发私信给FUTUREAI
发送

怎样制止那8个常睹的深度进修/计较机视觉毛病?

本文作者:FUTUREAI 2019-10-28 16:01
导语:人是没有完善的,我们常常正在法式中出错误。偶然那些毛病很简单发明:您的代码底子不克不及事情,您的使用法式解体 等等。可是有些bug是躲藏的,那使得它们愈加伤害。 正在处

人是没有完善的,我们常常正在法式中出错误。偶然那些毛病很简单发明:您的代码底子不克不及事情,您的使用法式解体 等等。可是有些bug是躲藏的,那使得它们愈加伤害。

正在处理深度进修成绩时,因为一些没有肯定性,很简单呈现那品种型的bug:很简单看到web使用端面路由恳求能否准确,而没有简单查抄您的梯度降落步调能否准确。但是,正在DL从业者死涯中有许多毛病是能够制止的。

我念分享一些我的经历,闭于我正在已往两年的计较机视觉事情中看到或造制的毛病。我正在集会上道到过那个话题,许多人正在会后报告我:“是的,伴计,我也有许多那样的毛病。”我期望我的文章能够帮忙您最少制止此中的一些成绩。

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

看起去仿佛是准确的,嗯,让我们把成果可视化一下:

mage = 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

out:

IndexError                                
Traceback (most recent call last)
<ipython-input-5-997162463eae> in <module>
     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),  # 左小指
   (20, 200),  # 左小指
   ...
   ]

那意味着加强实践上改动了语义:左酿成左,左酿成左,但我们没有交流数组中的枢纽面索引。它会给锻炼带去年夜量的乐音战更蹩脚的襟怀。

我们该当汲取经验:

正在使用加强或其他特征之前,要理解战思索数据构造战语义;

连结您的尝试本子性:增加一个小的变革(比方一个新的变更),假如分数曾经进步,查抄它怎样停止战兼并。

3.编码自界说丧失函数

熟习语义朋分成绩的人能够晓得IoU襟怀。没有幸的是,我们不克不及间接用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该当靠近0。可是成果取我希冀的有不同 ,那里呈现毛病了呢?

上里的函数是襟怀的一个很好的远似。襟怀没有是一种丧失:它凡是(包罗那种状况)越下越好。当我们利用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)

那些成绩能够从两个圆里去肯定:

编写一个单位测试去查抄丧失的标的目的

运转健齐性查抄

4.当我们逢到Pytorch的时分

假定有一个预先锻炼好的模子。编写基于ceevee API的Predictor 类。

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

那个代码准确吗?或许!关于某些模子去道的确是准确的。比方,当模子出有dropout或norm 层,如torch.nn.BatchNorm2d。

可是关于年夜大都计较机视觉使用去道,代码疏忽了一些主要的工具:转换到评价形式。

假如试图将静态PyTorch图转换为静态PyTorch图,那个成绩很简单认识到。torch.jit模块用于那种转换。

In [3]: model = nn.Sequential(
  ...:     nn.Linear(10, 10),
  ...:     nn.Dropout(.5)
  ...: )
  ...:
  ...: traced_model = torch.jit.trace(model, torch.rand(10))
/Users/Arseny/.pyenv/versions/3.6.6/lib/python3.6/site-packages/torch/jit/__init__.py:914: TracerWarning: Trace had nondeterministic nodes. Did you forget call .eval() on your model? Nodes:
   %12 : Float(10) = aten::dropout(%input, %10, %11), scope: Sequential/Dropout[1] # /Users/Arseny/.pyenv/versions/3.6.6/lib/python3.6/site-packages/torch/nn/functional.py:806:0
This may cause errors in trace checking. To disable trace checking, pass check_trace=False to torch.jit.trace()
 check_tolerance, _force_outplace, True, _module_class)
/Users/Arseny/.pyenv/versions/3.6.6/lib/python3.6/site-packages/torch/jit/__init__.py:914: TracerWarning: Output nr 1. of the traced function does not match the corresponding output of the Python function. Detailed error:
Not within tolerance rtol=1e-05 atol=1e-05 at input[5] (0.0 vs. 0.5454154014587402) and 5 other locations (60.00%)
check_tolerance, _force_outplace, True, _module_class)

一个简朴的处理举措 :

In [4]: model = nn.Sequential(
  ...:     nn.Linear(10, 10),
  ...:     nn.Dropout(.5)
  ...: )
  ...:
  ...: traced_model = torch.jit.trace(model.eval(), torch.rand(10))
  # 出有正告!

torch.jit.trace运转模子几回并比力 成果。
但是torch.jit.trace其实不 是全能的,您该当理解并记着。

5.复造粘揭成绩

许多工具皆是成对存正在的:锻炼战考证、宽度战下度、纬度战经度……假如您认真浏览,您会很简单发明一个bug是由某一个成员中复造粘揭到别的一个成员中惹起的:

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

不由 是我犯了愚笨的毛病,比方。盛行的albumentations库中也有相似的成绩。

# https://github.com/albu/albumentations/blob/0.3.0/albumentations/augmentations/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。

我已经犯过另外一个一样的毛病。

raw_mask = cv2.imread('mask_small.png')
mask = raw_mask.astype('float32') / 255
mask = cv2.resize(mask, (64, 64), interpolation=cv2.INTER_LINEAR)
mask = cv2.resize(mask, (128, 128), interpolation=cv2.INTER_CUBIC)
mask = (mask * 255).astype('uint8')
_ = plt.imshow(np.hstack((raw_mask, mask)))

那里出了甚么成绩?尾先,用三次样条插值调解mask的巨细是一个坏主张。取转换float32到uint8的成绩是一样的:三次样条插值的输出值会年夜于输进值,会招致溢出。

我正在做可视化的时分发明了那个成绩。正在您的锻炼轮回中四处利用断行也是一个好主张。

7. 拼写毛病发作

假定需求对齐卷积收集(如语义朋分成绩)战一个宏大的图象停止推理。该图象是云云宏大,出有时机把它放正在您的GPU上 -比方,它能够是一个医疗或卫星图象。

正在那种状况下,能够将图象朋分成网格,自力天对每块停止推理,最初兼并。别的,一些猜测穿插能够有助于光滑边沿的真影

让我们编码真现吧!

from tqdm import tqdm
class GridPredictor:
   """
   您有GPU内存限定时,此类可用于猜测年夜图象的朋分掩码
   """
   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)

AssertionError                            Traceback (most recent call last)
<ipython-input-24-a72034c717e9> in <module>
     9 grid_pred = grid_predictor(img)
    10
---> 11 np.testing.assert_allclose(simple_pred, grid_pred, atol=.001)
~/.pyenv/versions/3.6.6/lib/python3.6/site-packages/numpy/testing/_private/utils.py in assert_allclose(actual, desired, rtol, atol, equal_nan, err_msg, verbose)
  1513     header = 'Not equal to tolerance rtol=%g, atol=%g' % (rtol, atol)
  1514     assert_array_compare(compare, actual, desired, err_msg=str(err_msg),
-> 1515                          verbose=verbose, header=header, equal_nan=equal_nan)
  1516
  1517
~/.pyenv/versions/3.6.6/lib/python3.6/site-packages/numpy/testing/_private/utils.py in assert_array_compare(comparison, x, y, err_msg, verbose, header, precision, equal_nan, equal_inf)
   839                                 verbose=verbose, header=header,
   840                                 names=('x', 'y'), precision=precision)
--> 841             raise AssertionError(msg)
   842     except ValueError:
   843         import traceback
AssertionError:
Not equal to tolerance rtol=1e-07, atol=0.001
Mismatch: 99.6%
Max absolute difference: 765.
Max relative difference: 0.75000001
x: array([[[215.333333],
       [192.666667],
       [250.      ],...
y: array([[[ 215.33333],
       [ 192.66667],
       [ 250.     ],...

call办法的准确版本以下:

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

假如您仍旧没有晓得成绩是甚么,留意止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)能够很快天检测到成绩。一样,也能够是单位测试。


本文由进驻维科号的做者撰写,不雅面仅代表做者自己,没有代表景智AI坐场。若有侵权或其他成绩,请联络告发。

声明:景智AI网尊重行业规范,任何转载稿件皆标注作者和来源;景智AI网的原创文章,请转载时务必注明文章作者和"来源:景智AI网", 不尊重原创的行为将受到景智AI网的追责;转载稿件或作者投稿可能会经编辑修改或者补充,有异议可投诉至:mailto:813501038@qq.com

分享:
相关文章
最新文章