
Реализация стохастической глубины/пути падения в PyTorch
DropPath доступен в моей библиотеке компьютерного зрения очки.
Код находится здесь, интерактивную версию этой статьи можно скачать здесь.
Введение
Сегодня мы собираемся реализовать Stochastic Depth, также известную как Drop Path, в PyTorch! Стохастическая глубина, представленная Гао Хуангом и др., — это метод деактивации некоторых слоев во время обучения. Мы будем придерживаться DropPath.
Давайте взглянем на обычный блок ResNet, который использует остаточные соединения (как сейчас почти все модели). Если вы не знакомы с ResNet, у меня есть статья, показывающая, как это реализовать.
По сути, выход блока добавляется к его входу: output = block(input) + input. Это называется остаточным подключением.

Здесь мы видим четыре ResnNet-подобных блока, один за другим.

Stochastic Depth/Drop Path деактивирует часть веса блока

Выполнение
Давайте начнем с импорта нашего лучшего друга, torch
import torch from torch import nn from torch import Tensor
Мы можем определить тензор 4D (batch x channels x height x width), в нашем случае давайте просто отправим 4 изображения с одним пикселем каждое, чтобы было легче увидеть, что происходит :)
x = torch.ones((4, 1, 1, 1))
Нам нужен тензор формы batch x 1 x 1 x 1, который будет использоваться для обнуления некоторых элементов в пакете с помощью заданной задачи. Бернулли на помощь!
keep_prob: float = .5
mask: Tensor = x.new_empty(x.shape[0], 1, 1, 1).bernoulli_(keep_prob)
# mask =
tensor([[[[1.]]],
[[[1.]]],
[[[0.]]],
[[[0.]]]])
Кстати, это эквивалентно
mask: Tensor = (torch.rand(x.shape[0], 1, 1, 1) > keep_prob).float()
Мы хотим установить некоторые из элементов x равными нулю, поскольку наши маски состоят из 0s и 1s, мы можем умножить их на x. Прежде чем мы это сделаем, нам нужно разделить x на keep_prob, чтобы уменьшить активацию входа во время обучения, см. cs231n. Так
x_scaled : Tensor = x / keep_prob
Окончательно
output: Tensor = x_scaled * mask
# output =
tensor([[[[2.]]],
[[[0.]]],
[[[2.]]],
[[[2.]]]])
Посмотрите, как некоторые элементы в пакете были установлены на ноль. Мы можем объединить это в функцию
def drop_path(x: Tensor, keep_prob: float = 1.0) -> Tensor:
mask: Tensor = x.new_empty(x.shape[0], 1, 1, 1).bernoulli_(keep_prob)
x_scaled: Tensor = x / keep_prob
return x_scaled * mask
drop_path(x, keep_prob=0.5)
Мы также можем выполнять операции на месте
def drop_path(x: Tensor, keep_prob: float = 1.0) -> Tensor:
mask: Tensor = x.new_empty(x.shape[0], 1, 1, 1).bernoulli_(keep_prob)
x.div_(keep_prob)
x.mul_(mask)
return x
drop_path(x, keep_prob=0.5)
Однако мы можем захотеть использовать x в другом месте, и деление x или mask на keep_prob будет таким же. Приступаем к окончательной реализации
def drop_path(x: Tensor, keep_prob: float = 1.0, inplace: bool = False) -> Tensor:
mask: Tensor = x.new_empty(x.shape[0], 1, 1, 1).bernoulli_(keep_prob)
mask.div_(keep_prob)
if inplace:
x.mul_(mask)
else:
x = x * mask
return x
x = torch.ones((4, 1, 1, 1))
drop_path(x, keep_prob=0.5)
drop_path работает только для 2D-данных, нам нужно автоматически вычислить количество измерений из входного размера, чтобы он работал для любого времени данных.
def drop_path(x: Tensor, keep_prob: float = 1.0, inplace: bool = False) -> Tensor:
mask_shape: Tuple[int] = (x.shape[0],) + (1,) * (x.ndim - 1)
# remember tuples have the * operator -> (1,) * 3 = (1,1,1)
mask: Tensor = x.new_empty(mask_shape).bernoulli_(keep_prob)
mask.div_(keep_prob)
if inplace:
x.mul_(mask)
else:
x = x * mask
return x
Давайте создадим красивый DropPath nn.Module
class DropPath(nn.Module):
def __init__(self, p: float = 0.5, inplace: bool = False):
super().__init__()
self.p = p
self.inplace = inplace
def forward(self, x: Tensor) -> Tensor:
if self.training and self.p > 0:
x = drop_path(x, self.p, self.inplace)
return x
def __repr__(self):
return f"{self.__class__.__name__}(p={self.p})"
Использование с остаточными соединениями
У нас есть свои DropPath, круто! Как мы это используем? Нам нужен остаточный блок, мы можем использовать классический блок ResNet: старый добрый друг BottleNeckBlock
from torch import nn
class ConvBnAct(nn.Sequential):
def __init__(self, in_features: int, out_features: int, kernel_size=1):
super().__init__(
nn.Conv2d(in_features, out_features, kernel_size=kernel_size, padding=kernel_size // 2),
nn.BatchNorm2d(out_features),
nn.ReLU()
)
class BottleNeck(nn.Module):
def __init__(self, in_features: int, out_features: int, reduction: int = 4):
super().__init__()
self.block = nn.Sequential(
# wide -> narrow
ConvBnAct(in_features, out_features // reduction, kernel_size=1),
# narrow -> narrow
ConvBnAct( out_features // reduction, out_features // reduction, kernel_size=3),
# wide -> narrow
ConvBnAct( out_features // reduction, out_features, kernel_size=1),
)
# I am lazy, no shortcut etc
def forward(self, x: Tensor) -> Tensor:
res = x
x = self.block(x)
return x + res
BottleNeck(64, 64)(torch.ones((1,64, 28, 28)))
Чтобы деактивировать блок, операция x + res должна быть равна res, поэтому наше DropPath должно быть применено после блока.
class BottleNeck(nn.Module):
def __init__(self, in_features: int, out_features: int, reduction: int = 4):
super().__init__()
self.block = nn.Sequential(
# wide -> narrow
ConvBnAct(in_features, out_features // reduction, kernel_size=1),
# narrow -> narrow
ConvBnAct( out_features // reduction, out_features // reduction, kernel_size=3),
# wide -> narrow
ConvBnAct( out_features // reduction, out_features, kernel_size=1),
)
# I am lazy, no shortcut etc
self.drop_path = DropPath()
def forward(self, x: Tensor) -> Tensor:
res = x
x = self.block(x)
x = self.drop_path(x)
return x + res
BottleNeck(64, 64)(torch.ones((1,64, 28, 28)))
Тада! Теперь случайным образом наш .block будет полностью пропущен!
Заключение
В этой статье мы увидели, как реализовать DropPath и использовать его внутри остаточного блока. Надеюсь, когда вы прочтете/увидите траекторию падения/стохастическую глубину, вы поймете, как это делается.
Заботиться :)
Франческо