Я придерживаюсь мнения, что если хочешь в чем-то разобраться, то реализуй этой сам. Когда я только начинал заниматься датасаенсом, я разобрался, как считать градиенты на бумажке, перескочил этап реализации сеток на numpy и сразу стал их обучать. Однако, когда спустя долгое я всё-таки решил это сделать, то столкнулся с тем, что не могу это сделать, потому что у меня не сходятся размерности.
Перебрав множество материалов, я остановился на книге Deep Learning from Scratch. Теперь я разобрался, и хочу сделать свой туториал.
На вопрос "Зачем очередной туториал с сеткой на numpy" я отвечу:
В туториале сделаны акценты в неочевидных местах, где могут не сходиться размерности;
В коде нет абстракций (классы слоёв), чтобы не отвлекать от сути.
Код доступен тут. Также можно посмотреть это видео с курса deep learning на пальцах, чтобы посчитать градиенты на бумажке.
Для того, чтобы обучить нейросеть, нам нужно понимать chain rule (дифференцирование сложной функции). Данное правило описывает, как брать производную композиций функций. Если у нас есть выражение y = f(g(x)), то производная y по x.
Так как нейронная сеть является большой композицией функций (слоёв), то для того, чтобы посчитать производную (градиент) ошибки мы перемножим производные всех слоёв.
Обучать будем двухслойный персептрон (два полносвязных слоя) с сигмоидальной функции активацией между слоями для задачи регрессии (предсказания цены дома) на датасете цен на дома в калифорнии.
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))
Нейронную сеть можно представить в виде следующего вычислительного графа:
Это как раз композиция, и нам нужно знать производную функции ошибки для всех весов. Они будут такие:
Посчитаем все промежуточные производные:
В следующей производной могут возникнуть проблемы с размерностями. Единица здесь - это единичный вектор с размерностью как у С np.ones_like(C)
.
Аналогично np.ones_like(b2)
.
Тут тоже надо быть аккуратным. Так как производная по D, которая стоит слева, то нам нужно транспонировать w2 и ставить его справа при перемножении с другой матрицей np.dot(prev_grad, w2.T)
.
Аналогично нам нужно транспонировать D и ставить его слева при перемножении с другой матрицей np.dot(D.T, prev_grad)
У сигмоиды классная производная.
Тут np.ones_like(b1)
.
Тут np.ones_like(b1)
.
Тут нужно транспонировать D и ставить его справа при перемножении с другой матрицей np.dot(prev_grad, 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
Как говорится "Охапку дров и перцептрон готов". Надеюсь, этот туториал поможет тем, кто столкнулся с той же проблемой, что и я.
А еще у меня есть телеграм канал, где я рассказываю про сетки с упором в инференс.
Комментарии (22)
NickDoom
21.01.2023 22:44+1Можно вопрос не совсем по теме?
Вот тут вот мы видим, как сеть сжимает изображение в одной ей ведомый формат, разжимает, получается что-то похожее (но вместо домика — совершенно другой домик).
А что у нас со сжатием в детерминированный формат? Допустим, какие-то вейвлеты в таком общем их виде, что «просто взять и разложить» не получится. Или векторизация в контуры с заполнением в виде каких-то гармоник. Или фрактальное сжатие. Или всё вместе — фрактальная иерархия контуров, ограничивающих вейвлеты, чтобы окончательно сойти с ума (шутка).
В общем, любой формат, который легко декомпрессится без всяких нейросетей, но сжать — адский ад и чуть ли не метод перебора. Какие-то успехи сейчас были на этом фронте? В теории — там хотя бы можно будет доверять результату.
encyclopedist
21.01.2023 23:50Фрактальное кодирование - пример такого алгоритма.
NickDoom
22.01.2023 00:01Ну да, я так и сказал о_О «Или фрактальное сжатие». Пробовал его кто-то сделать на нейросетке?
Я нагуглил только «We propose use of two different neural network models to implement fractal image compression and decompression», что вызвало у меня ступор — зачем decompression делать тоже на нейросетке, мне решительно непонятно.
Я это вижу как элементарную сеть (одну), у которой на входе растр, на выходе — что-то типа .FIF, а потом декомпрессор разжимает, а обучалка даёт сети по башке соразмерно ошибке.
Плюс куча нюансов, которые я могу излить в песочницу — для комментария это будет слишком «войнаимир».
encyclopedist
22.01.2023 00:05Ну да, я так и сказал о_О «Или фрактальное сжатие».
Я каким-то образом упустил эту фразу в вашем комментарии.
Пробовал его кто-то сделать на нейросетке?
Мне такого не попадалось.
PrinceKorwin
22.01.2023 07:16Пробовал его кто-то сделать на нейросетке?
Будучи студентом я делал фрактальную компрессию и декомпрессию. Удавалось с помощью нее сжимать файлы которые уже были пожаты (jpg, zip, gzip). Проблема была в двух моментах:
Очень медленно сжимает. Прям хотелось тогда аппаратного ускорения (привет GPU)
Патенты. Это уже было, как оказалось, закрыто патентами
peacemakerv
22.01.2023 13:06А вот объясните как быть, если все нейронки на вход хотят квадратную картинку, скажем до 50К пикселей, а фото объектов в датасете (объект уже обнаружен предыдущей нейронкой) - длинная картинка 700 х 150 пикселов, с мелкими артефактами (не буквами, не символами...) ?
Т.е. тупо масштабировать картинку вниз, теряя артефакты - глупо. И как быть ?lgorSL
23.01.2023 03:18Можно вместо уменьшения наоборот увеличить до 700x700 и заполнить края чем-нибудь нейтральным.
В некоторые виды нейросеток типа свёрточных можно подавать входные изображения разного размера, но к сожалению это сильно зависит используемых от слоёв в сети и ограничений библиотек.peacemakerv
23.01.2023 07:45Так задача вместится в малый размер квадрата без потери деталей. Я так понимаю, остается только в паззл играть, кромсая картинку на 9 квадратов и собирая из нее нужный квадрат, без масштабирования.
jobber_man
23.01.2023 09:45+1Если есть возможность, то делайте свою сетку под 700x150 и обучайте/дообучайте. Веса для сверхточных слоев можно взять из готовой сетки, а остальное дообучить. Практически любая популярная библиотека позволяет работать с любой размерностью изображения. Даже фильтры не обязательно использовать квадратные, можно попробовать, например, прямоугольные, если они подойдут к вашей задаче.
Если же возможности использовать свою сетку нет, а только использовать готовую, то да, остаются только три варианта: растягивать картинку, заполнять поля до квадрата и нарезать на квадратные куски и потом собирать результат.
DistortNeo
23.01.2023 11:01А это смотря какую задачу нейронная сеть решает и какая у неё архитектура. Надо понимать, что нейронная сеть обучена на работу на определённых масштабах и не способна делать обобщения на другие масштабы в общем случае. Поэтому вам придётся плясать от масштаба. Либо делать ресайз или экстраполяцию, либо обрабатывать потайлово.
yet_another_mle Автор
23.01.2023 11:10+1Есть разные варианты. Например, такие:
- Дополнить картинку до 700х700
- Дообучить на 700х150
- Резать картинку на куски по 150х150
gybson_63
22.01.2023 19:39По этой теме порекомендую вот такую статью
Let’s code a Neural Network in plain NumPy | by Piotr Skalski | Towards Data Science
defecator
Осталось только выкинуть готовые библиотеки и попробовать на элементарном уровне рассказать про то, как строятся нейронные сети. А то начинается херня, как везде, где питон - берём такую библиотеку, берём сяку библиотеку, и вуаля - смотрите, чего я наделал !
А что там под капотом - покрыто мраком
zzzzzzerg
@sim0nsaysделал хороший курс - https://www.youtube.com/playlist?list=PL5FkQ0AF9O_o2Eb5Qn8pwCDg7TniyV1Wb
lair
numpy
- это библиотека, которая в этом коде считает линейную алгебру. Зачем самостоятельно реализовывать операции над векторами, которые в формуле одним знаком пишутся?phoaw
а что там под капотом у numpy? лапша из кода на древнем фортране, Си и яве. Когда пишешь свой код, да еще применяешь принципы DRY, никакие библиотеки не нужны.
lair
А какая разница?
Не верю я, что в numpy используется Java. Пруфы есть?
На C#, скажем, невозможно писать "без библиотек" (технически, наверное, возможно, но требует очень много лишней работы по настройке всего). И это логично, потому что зачем тратить свои усилия на уже решенные задачи (вроде той же математики над векторами), если можно их тратить на те, которые требуют решения?
thevlad
Естественно там нативный, максимально оптимизированный код на C. Это как раз позволяет, писать на NumPy быстрый код даже на таком медленном языке как Python. Особенно если вы будете оперировать предоставленными абстракциями, а не спускаться до ручного итерирования элементов.
Dynasaur
И питон тоже выкинуть. Настоящие пацаны пишут на ассемблере! А то начинается херня, как везде, и не понятно что там под капотом.
NickDoom
Смешно, но я такие сетки запускал :)
Их мне учили всякими матлабами, а я потом для скорости скармливал полученный массив оптимизированному низкоуровневому коду.
vya
Сеть строится постепенно. Сперва образуются устойчивые пути прохождения сигналов. Потом они обрастают изолирующим слоем. А если сеть счастлива, то может выдавать то ли гениальные вещи, то ли бред. Мне бы кармы и двух соавторов...