Почему нельзя инициализировать веса нейронной сети нулями?

Ответ

Инициализация весов нулями (или одинаковыми константами) приводит к проблеме симметрии, которая блокирует обучение сети.

Механизм проблемы:

  1. Прямой проход: Если все веса в слое равны нулю, каждый нейрон получит одинаковый взвешенный вход (ноль). После применения функции активации (например, ReLU) все нейроны выдадут одинаковый выход (например, ноль для ReLU).
  2. Обратное распространение (Backpropagation): Поскольку выходы нейронов одинаковы, градиенты ошибки по весам, пришедшие к ним, также будут идентичными.
  3. Обновление весов: Все веса в слое обновятся на одну и ту же величину. После первой же итерации они перестанут быть нулями, но останутся равными друг другу.

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

Код, иллюстрирующий проблему:

import numpy as np

# ПЛОХО: Инициализация нулями
def zero_init(size):
    return np.zeros(size)

# ХОРОШО: Случайная инициализация (например, He для ReLU)
def he_init(size):
    # size = (n_neurons_current, n_neurons_previous)
    fan_in = size[1]
    return np.random.randn(*size) * np.sqrt(2.0 / fan_in)

# Сравнение
layer_size = (5, 10)  # 5 нейронов, 10 входов
weights_zero = zero_init(layer_size)
weights_he = he_init(layer_size)

print("Zero init (первые два нейрона идентичны):n", weights_zero[0], "n", weights_zero[1])
print("nHe init (нейроны различны):n", weights_he[0][:3], "...n", weights_he[1][:3], "...")

Решение: Использовать случайную инициализацию, которая нарушает симметрию:

  • Xavier/Glorot: W = np.random.randn(n_in, n_out) * np.sqrt(1 / n_in) — хорошо для сигмоидных/гиперболических активаций.
  • He: W = np.random.randn(n_in, n_out) * np.sqrt(2 / n_in) — стандарт для слоев с ReLU и его вариациями.

Ответ 18+ 🔞

Да ты посмотри, какая классика подъехала! Прямо как в том анекдоте про двух одинаковых близнецов — нихуя не разберёшь, кто есть кто. Так и тут: если все веса в слое занулить или сделать одинаковыми, получается ёперный театр.

Смотри, как это работает, на пальцах. Допустим, ты такой умный, взял и всем нейронам в слое выставил веса в ноль. Что происходит?

  1. Прямой проход: Каждому нейрону на вход прилетает сумма (входы * веса). А если все веса — нули, то и сумма у каждого — ноль, блядь, один в один. Кидаешь это дело в ReLU — на выходе у всех опять ноль. Получается хор из пяти одинаковых мудаков, которые орут одну и ту же ноту.
  2. Обратка (Backpropagation): А когда ошибку назад распространяешь, градиенты, которые приходят к этим весам, тоже, блять, получаются идентичными. Потому что вышли-то они из одного места!
  3. Обновление: И вот ты обновляешь веса. Они все сдвинутся на одну и ту же величину. Да, они перестанут быть нулями, но станут как братья-близнецы — не отличишь. После каждой эпохи они будут дружно шагать строем, как солдаты.

И что в итоге? А в итоге весь твой слой из, условно, ста нейронов работает как один-единственный, самый тупой нейрон. Вся мощь сети накрывается медным тазом. Она не может выучить нихуя разнообразного, только одну, блять, простейшую фичу. Пиздец, а не модель.

Вот, глянь код, чтобы совсем понятно стало. Видишь разницу?

import numpy as np

# ПЛОХО, НЕ ДЕЛАЙ ТАК: Сделал всех одинаковыми
def zero_init(size):
    return np.zeros(size)

# НОРМ, ДЕЛАЙ ТАК: Разбросал как попало (He для ReLU)
def he_init(size):
    # size = (n_neurons_current, n_neurons_previous)
    fan_in = size[1]
    return np.random.randn(*size) * np.sqrt(2.0 / fan_in)

# Сравниваем, ёпта
layer_size = (5, 10)  # 5 нейронов, 10 входов
weights_zero = zero_init(layer_size)
weights_he = he_init(layer_size)

print("Zero init (первые два нейрона — клоны):n", weights_zero[0], "n", weights_zero[1])
print("nHe init (нейроны — разные личности):n", weights_he[0][:3], "...n", weights_he[1][:3], "...")

Так что же делать, спросишь ты? Да хуй с горы, всё просто! Надо эту ебаную симметрию сломать в самом начале. Берёшь и инициализируешь веса случайно, чтобы они все были разные, как снежинки, блядь.

Есть проверенные рецепты, ядрёна вошь:

  • Xavier/Glorot: W = np.random.randn(n_in, n_out) * np.sqrt(1 / n_in) — старый добрый способ, хорошо для всяких сигмоид и гиперболических тангенсов.
  • He: W = np.random.randn(n_in, n_out) * np.sqrt(2 / n_in) — вот это сейчас царь и бог для всех, кто использует ReLU и его родственников. Бери — не прогадаешь.

Главное — не делай из своих нейронов мартышлюшек, повторяющих одно и то же. Дай им свободу в начале пути, а там уж они разберутся, кто на что способен.