machinelearningmastery.ru

Машинное обучение, нейронные сети, искусственный интеллект
Header decor

Home

8 ошибок глубокого обучения / компьютерного зрения и как я мог бы их избежать

Дата публикации Oct 3, 2019

Люди не идеальны, мы часто делаем ошибки в нашем программном обеспечении. Иногда эти ошибки легко найти: ваш код просто не работает, ваше приложение вылетает и так далее. Но некоторые ошибки скрыты, и это делает их еще более опасными.

Работая над проблемами глубокого обучения, можно легко сделать несколько ошибок такого типа из-за некоторой неопределенности: легко увидеть, правильно ли запрашивает маршрут конечная точка веб-приложения, и не так просто проверить, был ли ваш шаг градиентного спуска правильным. Тем не менее, есть много ошибок в процедурах практикующего 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

Выглядит правильно, а? Хорошо, давайте представим это.

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)
<ipython-input-5-997162463eae> in <module>
8 img2 = image_flipped.copy()
9 for y, x in kpts_flipped:
---> 10 img2[y, x] = 0IndexError: 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
...
]

Это означает, что расширение фактически меняет семантику: левый становится правым, правый становится левым, но мы не меняем индексы ключевых точек в массиве. Это принесет огромное количество шума на тренировку и гораздо худшие показатели.

Урок должен быть усвоен:

  • знать и думать о структуре данных и семантике, прежде чем применять дополнения или другие необычные функции;
  • сделайте ваши эксперименты атомарными: добавьте небольшое изменение (например, новое преобразование), проверьте, как оно идет, объедините, если оценка была улучшена.

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должно быть близко к нулю. В чем дело?

Ну, функция выше является хорошим приближениемметрический, Метрика - это не потеря: обычно (включая этот случай) чем выше, тем лучше. Как мыминимизирующийпотеря с 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

Предположим, у кого-то есть предварительно обученная модель, и это время вывода. Кодирование некоторыхPredictorкласс на основеceeveeAPI,

from ceevee.base import AbstractPredictorclass 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, Или когда модель требует использовать фактическую статистику нормы для каждого изображения (например, множество архитектур на основе pix2pix)требовать этого).

Но для большинства приложений компьютерного зрения код пропустил что-то важное:переход в режим оценки,

Эту проблему легко выявить, если попытаться преобразовать динамический граф PyTorch в статический. Eсть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)) # No more warnings!

Под капотом,torch.jit.traceзапускает модель несколько раз и сравнивает результаты. Разница здесь подозрительна.

тем не мениеtorch.jit.traceздесь не панацея Это своего рода нюанс PyTorch, который нужно знать и помнить.

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

Это не только я делаю глупые ошибки. Например. был похожий в популярном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)))

Что здесь не так? Прежде всего, это плохая идея изменить размер маски с помощью кубической интерполяции. А также та же проблема с кастингомfloat32вuint8: кубическая интерполяция может выводить значения больше, чем ввод, и это приводит к переполнению.

Я нашел эту проблему, делая визуализацию. Это также хорошая идея, чтобы иметь утверждения здесь и там в вашем цикле обучения.

7. Опечатки случаются

Предположим, нужно выполнить вывод для полностью сверточной сети (например, проблема семантической сегментации) и огромного изображения. Изображение настолько велико, что его невозможно вставить в графический процессор - например, это может быть медицинское или спутниковое изображение.

В таких случаях можно нарезать изображение на сетку, выполнить вывод для каждого фрагмента независимо и, наконец, объединить. Кроме того, некоторые предсказания пересечения могут быть полезны для сглаживания артефактов вблизи границ.

Давайте закодируем это!

from tqdm import tqdmclass 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)---------------------------------------------------------------------------
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 tracebackAssertionError:
Not equal to tolerance rtol=1e-07, atol=0.001Mismatch: 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 Normalizenorm = 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) обнаружит проблему довольно быстро. Опять же, это может быть и модульный тест.

TL; DR byКапитан Очевидность

  • тесты имеют значение;
  • утверждения времени выполнения хороши для обучающего конвейера;
  • визуализация - это благословение;
  • копипаста это проклятие;
  • ничто не является серебряной пулей, инженер по машинному обучению должен быть всегда осторожен (или просто страдать).

Я благодарю людей изСообщество ODS.aiза их полезные отзывы на эту тему.

Оригинальная статья

Footer decor

© machinelearningmastery.ru | Ссылки на оригиналы и авторов сохранены. | map