Линейнейный слой (linear layer) или полносвясный (full connected layer) производит линейное преобразование над входными данными
Веса и баес инициализируются согласно инициализации Xavier.
Рассмотрим случай батча размера
class Linear(Module):
"""Classic linear layer - y=wx+b."""
def __init__(self, dim_in, dim_out, bias=True):
super().__init__()
self._bias = bias
# Xavier initialization
stdv = 1/np.sqrt(dim_in)
self.W = np.random.uniform(-stdv, stdv, size=(dim_in, dim_out))
if self._bias:
self.b = np.random.uniform(-stdv, stdv, size=dim_out)
def forward(self, input):
self.output = np.dot(input, self.W)
self.output += self.b if self._bias else 0
return self.output
def backward(self, input, grad_output):
self.grad_W = np.dot(input.T, grad_output)
grad_input = np.dot(grad_output, self.W.T)
if self._bias:
self.grad_b = np.mean(grad_output, axis=0)
return grad_input
def parameters(self):
return [self.W, self.b] if self._bias else [self.W]
def grad_parameters(self):
return [self.grad_W, self.grad_b] if self._bias else [self.grad_W]
Нормирует вход слоя сети по каждому обучающиму mini-batch(то есть m > 1).
Эффекты слоя:
- решает проблему Internal Covariate Shift*, что сильно ускоряет сходимость;
- так же действует в качестве регулиризатора, что позволяет убрать или снизить влияние Dropout;
- позволяет использовать saturated nonlinearties (например Sigmoid);
- позволяет использовать высокий learning rate без риска несходимости и более небрежную иницилизацию весов.
По поводу того, где ставить ведутся дискуссии, но анализируя мнение в интернете люди ставят после функции активации Но нельзя не отметить, что авторы статьи ставят перед функцией активации(хотя далее сообщеет разработчик Keras, что автор статьи сейчас ставит после функции активации) Обсуждение на stackoverflow также рассматривается и вопрос куда ставить Dropout.
Сходимость нейросети ускоряется, если на вход сети подаются нормализованные данные. То же самое верно и для "подсетей" нейросети (1 и более слоёв).
Для преобразования данных внутри слоёв сети данный слой используется два упрощения позволяющих решить задачу нормализации слоёв на всей статистики обучения:
- Нормализировать каждую scalar input features независимо (var=1, mean=0);
- Каждый мини-батч выдаёт оценку дисперсии и мат. ожидания для входа.
Covariate Shift представляет из себя проблему при которой изменяется распределения входа нейросети, которое приводит к тому, что нужно подстраиваться под новое распределение, что замедляет обучение.
Internal Covariate Shift является той же проблемой только в масштабах "подсети" нейросети, то есть проблемой для одного или нескольких слоёв. Так после очередного gradient otimizer step распределение на выходе одного из слоёв может изменится, что заставит последующие подстраиваться.
- Принимаем на вход мини-батч выхода предыдущего слоя размера m>1.
- Нормализируем все фичи отдельно для выхода предыдущего слоя с
$d$ измерениями$x=(x^{(1)}, x^{(2)}, ..., x^{(d)})$ .$$\large\hat{x}^{(k)} = \frac {x^{(k)} − E[x^{(k)}]}{\sqrt{Var[x(k)]}}$$ - Scale and shift the normalize values, используя trainable параметры
$\gamma^{(k)}, \beta^{(k)}$ , которые "учатся" восстановливать representation power of the model.$$\large{\hat{y}^{(k)} = \gamma^{(k)}\hat{x}^{(k)} + \beta^{(k)}}$$
Note. Нормализация входа слоя может приводить к изменению того что может представлять слой. Например, так сигмоиду можно перевести в линейный режим (значения близкие к нулю). Эту проблему и призвана решить линейная трансформация, представленная выше, которая способна репрезентовать идентичную.
Алгоритм:
Рассмотрим мини-батч
The distributions of values of any
$\hat{x}$ has the expected value of 0 and the variance of 1, as long as the elements of each mini-batch are sampled from the same distribution,and if we neglect$\epsilon$ .
Производные для backprop. Для infernce мы также собираем экспоненциальное скользящие среднее мат. ожидания и дисперсии, представляющие из себя:
Фиксируем параметры
class BatchNorm(Module):
def __init__(self, num_features, eps=1e-8):
super().__init__()
self.eps = eps
self.gamma = np.ones((1, num_features))
self.beta = np.zeros((1, num_features))
self.sigma_mean = 1
self.mu_mean = 0
def forward(self, input):
if self._train:
assert input.shape[0] > 1, "Batch size need to be >1"
self._mu = np.mean(input, axis=0)
self._sigma = np.var(input, axis=0)
self.mu_mean = self.mu_mean*.9 + self._mu*.1
self.sigma_mean = self.sigma_mean*.9 + self._sigma*.1
self._input_norm = self._normalize(input, self._mu, self._sigma)
self.output = self.gamma*self._input_norm + self.beta
else:
self._input_norm = self._normalize(input, self.mu_mean, self.sigma_mean)
self.output = self.gamma*self._input_norm + self.beta
return self.output
def backward(self, input, grad_output):
if self._train:
m = input.shape[0]
input_minus_mu = (input - self._mu)
dinput_norm = grad_output * self.gamma
dsigma = np.sum(dinput_norm*input_minus_mu*(-.5)*self.std_inv**3, axis=0)
dmu = np.sum(dinput_norm * (-self.std_inv), axis=0) \
+ dsigma * np.mean(-2 * input_minus_mu, axis=0)
self.grad_gamma = np.sum(grad_output * self._input_norm, axis=0)
self.grad_beta = np.sum(grad_output, axis=0)
grad_input = dinput_norm*self.std_inv + dsigma*input_minus_mu/m + dmu/m
else:
self.grad_gamma = np.sum(grad_output * self._input_norm, axis=0)
self.grad_beta = np.sum(grad_output, axis=0)
grad_input = grad_output * self.gamma * self.std_inv
return grad_input
def parameters(self):
return [self.gamma, self.beta]
def grad_parameters(self):
return [self.grad_gamma, self.grad_beta]
def _normalize(self, input, mu, sigma):
self.std_inv = 1/np.sqrt(sigma + self.eps)
return (input - mu)*self.std_inv
Нормализируем также по каждому значенею матрицы feature maps, но обучаем
P.S. нейроны - можно читать как units или выходы предыдущего слоя.
Метод регуляризации главная идея которого заключается в отключение части нейронов с некоторой вероятностью
Обычно оптимальное значение
По сути, вместо одной большой сети мы обучаем
Также не могу не отметить, то как применяется dropout слой в нейронной сети. Пусть dropout является
P.S. Мои пометки довольно условны, но надеюсь понятны.
Мы зануляем часть входа предыдущего слоя тем самым "отключая" некоторые нейроны. (В случае, если мы не хотим во время инференса домножать на
Мы сохраняем маску (если домножали на
class Dropout(Module):
def __init__(self, p=0.5):
super().__init__()
self.p = p
self.mask = None
def forward(self, input):
if self._train:
p_save = 1 - self.p
self.mask = np.random.binomial(
1, p=p_save, size=input.shape)/p_save
self.output = self.mask*input
else:
self.output = input
return self.output
def backward(self, input, grad_output):
if self._train:
grad_input = self.mask*grad_output
else:
grad_input = grad_output
return grad_input
Выбор гиперпараметра
Настройска размер сети. Пусть в каком-то скрытом слое нашей сети
Авторы статьи отмечают, что dropout нейросети особенно хорошо работают в купе с:
- высоким momentum. Авторы указывают, что 0.95-0.99 работает достаточно хорошо, также можно использовать обычный SGD с learning rate в 10-100 раз больше чем обычный. Это позволяет значительно ускорить обучение.
- Max-norm Regularization. Высокий learning rate/momentum приводит к сильному увелечению значения весов. Для предотвращение этого можно использовать max-norm регулиризацию. Оптимальное значение
$c$ в промежутке от 3 до 4. - большим learning decay.