- PVSM.RU - https://www.pvsm.ru -

Перцептрон на numpy

Я придерживаюсь мнения, что если хочешь в чем-то разобраться, то реализуй этой сам. Когда я только начинал заниматься датасаенсом, я разобрался, как считать градиенты на бумажке, перескочил этап реализации сеток на numpy и сразу стал их обучать. Однако, когда спустя долгое я всё-таки решил это сделать, то столкнулся с тем, что не могу это сделать, потому что у меня не сходятся размерности.

Перебрав множество материалов, я остановился на книге Deep Learning from Scratch [1]. Теперь я разобрался, и хочу сделать свой туториал.

На вопрос "Зачем очередной туториал с сеткой на numpy" я отвечу:

  • В туториале сделаны акценты в неочевидных местах, где могут не сходиться размерности;

  • В коде нет абстракций (классы слоёв), чтобы не отвлекать от сути.

Код доступен тут [2]. Также можно посмотреть это видео [3] с курса deep learning на пальцах [4], чтобы посчитать градиенты на бумажке.

Для того, чтобы обучить нейросеть, нам нужно понимать chain rule (дифференцирование сложной функции [5]). Данное правило описывает, как брать производную композиций функций. Если у нас есть выражение y = f(g(x)), то производная y по x.

frac{dy}{dx}=frac{dy}{du} . frac{du}{dx}, u=g(x)

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

Обучать будем двухслойный персептрон (два полносвязных слоя) с сигмоидальной функции активацией между слоями для задачи регрессии (предсказания цены дома) на датасете [6] цен на дома в калифорнии.

w1 = np.random.randn(in_dim, hidden_dim)
b1 = np.zeros((1, hidden_dim))
w2 = np.random.randn(hidden_dim, out_dim)
b2 = np.zeros((1, out_dim))

Нейронную сеть можно представить в виде следующего вычислительного графа:

Вычислительный граф сети
Вычислительный граф сети

y=B(C(D(E(F(X, w1), b1)), w2), b2)

Это как раз композиция, и нам нужно знать производную функции ошибки для всех весов. Они будут такие:

frac{dL}{db_2}=frac{dL}{dA} . frac{dA}{dB} . frac{dB}{db_2}frac{dL}{dw_2}=frac{dL}{dA} . frac{dA}{dB} . frac{dB}{dC} . frac{dC}{dw_2}frac{dL}{db_2}=frac{dL}{dA} . frac{dA}{dB} . frac{dB}{dC} . frac{dC}{dD} . frac{dD}{dE} . frac{dE}{db_2}frac{dL}{dw_1}=frac{dL}{dA} . frac{dA}{dB} . frac{dB}{dC} . frac{dC}{dD} . frac{dD}{dE} . frac{dE}{dF} . frac{dF}{dw_1}

Посчитаем все промежуточные производные:

L=A^2; frac{dL}{dА}=2aA=Y_{true} - Y_{pred}; frac{dA}{dB}=-1

В следующей производной могут возникнуть проблемы с размерностями. Единица здесь - это единичный вектор с размерностью как у С np.ones_like(C).

B=C + b_2; frac{dB}{dC}=1

Аналогично np.ones_like(b2).

B=C + b_2; frac{dB}{db2}=1

Тут тоже надо быть аккуратным. Так как производная по D, которая стоит слева, то нам нужно транспонировать w2 и ставить его справа при перемножении с другой матрицей np.dot(prev_grad, w2.T).

C=D @ w2; frac{dC}{dD}=w2^T

Аналогично нам нужно транспонировать D и ставить его слева при перемножении с другой матрицей np.dot(D.T, prev_grad)

C=D @ w2; frac{dC}{dw2}=D^T

У сигмоиды классная производная.

D=sigmoid(E); frac{dD}{dE}=sigmoid(E)*(1 - sigmoid(E)

Тут np.ones_like(b1).

E=F + b_1; frac{dE}{dF}=1

Тут np.ones_like(b1).

E=F + b_1; frac{dE}{db1}=1

Тут нужно транспонировать D и ставить его справа при перемножении с другой матрицей np.dot(prev_grad, X.T)

E=X @ w_1; frac{dF}{dw1}=X^T

В коде это выглядит так:

dLdA = 2 * A  # (bs, out_dim)
dAdB = -1  # (bs, out_dim)
dBdC = np.ones_like(C)  # (bs, out_dim)
dBdb2 = np.ones_like(self.B2)  # (bs, out_dim)
dCdD = self.W2.T  # (out_dim, hidden_dim)
dCdw2 = D.T  # (hidden_dim, bs)
dDdE = D * (1 - D)  # (bs, hidden_dim)
dEdF = np.ones_like(F)  # (bs, hidden_dim)
dEdb1 = np.ones_like(self.B1)  # (bs, hidden_dim)
dFdw1 = X.T  # (in_dim, bs)

dLdb2 = np.mean(dLdA * dAdB * dBdb2, axis=0, keepdims=True)  # (1, out_dim)
dLdw2 = np.dot(dCdw2, dLdA * dAdB * dBdC)  # (bs, out_dim)
dLdb1 = np.mean(
  np.dot(dLdA * dAdB * dBdC, dCdD) * dDdE * dEdb1, axis=0, keepdims=True
)  # (1, hidden_dim)
dLdw1 = np.dot(
  dFdw1, np.dot(dLdA * dAdB * dBdC, dCdD) * dDdE * dEdF
)  # (bs, in_dim)

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

b2 -= self.lr * dLdb2
w2 -= self.lr * dLdw2
b1 -= self.lr * dLdb1
w1 -= self.lr * dLdw1

Как говорится "Охапку дров и перцептрон готов". Надеюсь, этот туториал поможет тем, кто столкнулся с той же проблемой, что и я.

А еще у меня есть телеграм канал [7], где я рассказываю про сетки с упором в инференс.

Автор:
yet_another_mle

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/382232

Ссылки в тексте:

[1] Deep Learning from Scratch: https://www.amazon.com/Deep-Learning-Scratch-Building-Principles/dp/1492041416

[2] тут: https://github.com/EgShes/dl_from_scratch/blob/master/dl_from_scratch/examples/bare_perceptron.py

[3] это видео: https://youtu.be/kWTC1NvL894

[4] deep learning на пальцах: https://dlcourse.ai/

[5] дифференцирование сложной функции: https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D1%84%D1%84%D0%B5%D1%80%D0%B5%D0%BD%D1%86%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D0%B9_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8

[6] датасете: https://inria.github.io/scikit-learn-mooc/python_scripts/datasets_california_housing.html

[7] телеграм канал: https://t.me/yet_another_mle

[8] Источник: https://habr.com/ru/post/711998/?utm_source=habrahabr&utm_medium=rss&utm_campaign=711998