Комментарии 34
1) У меня сделана свёртка для каждого ядра. После свёртки у каждого ядра стоит слой субдискретизации (pooling), за ним то что получилось отправляется на один нейрон (назову его N). Таких нейронов — по числу ядер. Они служат входами обычной полносвязной нейросети.
2) Делаем обратное распространение ошибки для полносвязной нейросети. Приходим до одного нейрона N. Дельту мы знаем со входного слоя полносвязной нейросети, так что прогоняем её на начало нейрона. Получаем дельт по размерности результата после субдискретизации. Дальше эти дельты прогоняем через слой субдискретизации (мы ведь помним, откуда брали максимальную точку). Получаем дельт по числу точек картинки, после применения свёртки.
3) А вот и проблема. А как веса ядра по ним модифицировать? То, что написано в этой статье, так это расчёт дельт на каждой точке исходного изображения (до свёртки). Это нужно, когда слоёв свёртки много. А на первом-то слое как быть? Как модифицировать веса, зная дельты для каждой точки свёртки?
Что интересно, внятную статью, что и как делать вообще не могу найти. Почти всегда либо тонна математики, либо сразу ныряние в TensorFlow. А так, чтобы по-действиям описать все этапы как алгоритм — такого нет.
Я нашёл, что
Градиент для ядра свёртки можно посчитать как свёртку матрицы входа свёрточного слоя с «перевёрнутой» матрицей ошибки для выбранного ядра.
Приложенная к этой фразе формула, правда, показывала, что градиенты получатся тоже перевёрнутые и их надо будет перевернуть для коррекции ядра.
То есть, нужно взять исходное изображение и посчитать свёртку с перевёрнутой матрицей дельт. Как раз получится матрица размером с ядро.
Я вчера так и сделал. Но вся фишка в том, что дельты эти даже в соединении ядро->один выходной нейрон (т.е. в части до полносвязной сети) очень маленькие. Ядро вообще практически не обучается. То ли я дельты считаю неправильно (а я считаю как для обычной нейросети, по тем же формулам), то ли где-то ошибка и я чего-то не понимаю.
Пятница, пора домой значит
Правильный метод вычисления поправок к весам ядра такой:
Градиент для ядра свёртки можно посчитать как свёртку перевёрнутой матрицы входа свёрточного слоя с матрицей ошибки для выбранного ядра.
Возможен и вариант
Градиент для перевёрнутого ядра свёртки можно посчитать как свёртку матрицы входа свёрточного слоя с перевёрнутой матрицей ошибки для выбранного ядра.
Оба варианта дадут одинаковый результат. Этот градиент ещё стоит поделить на количество элементов в свёртке.
P.S. Мне так и не удалось до сих пор заставить внятно работать CNN. Например, с той же ReLu ядра легко влетают в область, где обучение отсутствует (производная 0). Инициализация He, увы, не очень помогает. Непонятно, сколько вообще нужно ядер (часть из них вообще не обучится с ReLu). У меня в сети три разных слоя: ядра->субдискретизация->нейрон по одному на ядро->полносвязная сеть. Вот этот нейрон, который один на ядро, он смвотрит на огромную картинку (224x224 после свёртки и субдискретизации раза 2x2). И вот ошибка этого нейрона очень плохо переносится в дельту ядер. Пришлось даже отказаться от деления изменения весов ядра на размер изображения после свёртки.
Возможно пришло время осилить батчнорм
У вас есть куча автодифф фреймворков на выбор (torch, chainer, mxnet, etc). Дробите свою сеть на части, сверяйте производные которые взяли руками с тем, что выдает фреймворк, правьте реализации, если результаты не совпадают.
Фреймворки проектируются так, чтобы ими можно было пользоваться имя минимальные навыки программирования
lua-torch мертв, по сути, все используют pytorch. Строго говоря, все популярные фреймворки ориентируются на python, в первую очередь. Если знаете C++, то необходимая база питона набирается за пару недель
Градиент для ядра свёртки можно посчитать как свёртку матрицы входа свёрточного слоя с «перевёрнутой» матрицей ошибки для выбранного ядра.
Только есть нюанс. Вот представим, что у меня есть только свёртка (без нейронов и без функции нейрона) и я желаю настроить её ядро. Зададим очень простую задачу — сделать выход свёртки всегда равным нулю. Логично, что ядро просто должно обнулиться. Считаем свёртку и задаём дельту. Так как нелинейностей у нас нет, то дельта (как я понимаю) должна быть равна разности нуля (мы же его хотим получить) и результата свёртки (а это полученный результат). Так вот, если посчитать изменения весов ядра, то оно будет огромным, что в корне неверно. Потому что вот эта вот дельта, вообще говоря, большая. Отсюда следует, что дельту я считаю явно неправильно. Как же тогда правильно в этом случае считать дельту?
Так вот, если посчитать изменения весов ядра, то оно будет огромным, что в корне неверно
Почему?
Так же, надо понимать, что обычно ядра инициализируют весьма специфичным образом. При правильной инициализации, не должно быть больших проблем с масштабом и зависимостью от размера ядра/числа нейронов
Также иногда используют какие-то случайные алгоритмы для подстройки весом между несколькими итерациями спуска, чтобы получить случайные значения и выйти из точки локального экстремума, если застряли.
Для одного слоя дельта определяется как
Тогда частная производная функции оценки (W — матрица весов омега) по изменению веса будет
Входы у нас 1,2,3, веса 3,2,1, функция нейрона линейная 1:1 (т.е. производная 1). А ошибка дельта 10. Получаем значения производных 10,20 и 30 для весов 3,2,1.
В данном случае такие шаги огромные. Коррекция выполняется как
Скорость ограничивает альфа. Но вот вопрос — про альфу известно, что она меньше 1. Но насколько? У меня она 0.25.И интересно, почему при скорости 1 сеть не делает приращение в один шаг. Ведь это вполне логично. Образ обучающий один. Функция линейная. Вся математика точная.
А для CNN я нашёл аж три разных варианта коррекции весов ядра. Какой правильный? А фиг знает. То переворачивают изображение и умножают на дельты, то ещё и применяют функцию нейрона к изображению и перевернув умножают на дельту, то дельту переворачивают и умножают на изображение (в этом случае ещё два варианта видел — в одном ядро получится прямое, а в другом указано, что перевёрнутое — кому верить непонятно).
про альфу известно, что она меньше 1. Но насколько? У меня она 0.25
Этот коэффициент часто подбирается эмпирически. Тут нет четких правил какое число выбрать, точно также как и нет правил по выбору топологии сети или начальных значений весов.
Обычно в начале обучения коэффициент высокий (0.25 вроде как популярное значение для персептрона), но со временем его уменьшают дабы не переобучить сеть.
Но вот вопрос — про альфу известно, что она меньше 1
На самом деле не обязательно) Бывают случаи, когда выгодно брать шаг больше 1, для методов первого порядка. Для методов второго да, 1 максимальный разумный шаг
При обратном распространении ошибку получает именно этот элемент свёртки, остальные в блоке получают ноль
Именно так работает производная от максимума
А можно чуть подробней про задачу?
Какую задачу? Я просто обучаю сеть распознавать лица двух человек.
Для проверки обучения ядра я сделал изображение, задал ядро в 1 и посчитал от него свёртку. Пропустил свёртку через субдискретизацию и получил что мне требовать от ядер. Теперь сбрасываем ядра в случайное значение, зная ответ свёртки считаем поправки к ядрам и смотрим, приходят ли ядра к 1. Так вот, без субдискретизации приходят. С 2x2 приходят уже существенно медленнее, а с 4x4 и выше я вообще не дождался результата.
Я не уверен, но похоже на локальный минимум
Рассмотрим одномерный случай — "картинку" размера 4, которая подается на вход свертке с ядром 3, а потом макспулингу размера 2, после чего мы имеем на выходе одно число
Пусть картинка имеет вид [10, 3, 5, -4]
. Свертка состоящая из [1, 1, 1]
, без bias, даст на выходе картинку [18, 4]
, пулинг даст на выходе просто 18
. Т.е. 18
и будет нашим целевым значением. Теперь мы стартуем со случайных весов, и пусть так получилось, что веса имеют вид [0, 3, 0]
. Свертка даст на выходе вектор [9, 15]
, пулинг даст, соответственно, 15
. Теперь, при оптимизации свертка будет ориентироваться только на правую часть картинки, т.к. макспулинг пустит их только в одну сторону. Мне немного лень считать конечный результат, но скорее всего оптимизация будет немного увеличивать все три значения ядра, до сходимости.
При этом ошибка будет нулевая, и итоговое ядро не будет совпадать с изначальным.
Но, т.к. вы, вероятно, пропускаете через свертку большую картинку, то у вас требуется чтобы свертка удовлетворяла сразу множеству выходов, и тут уже не все локальные минимумы позволяют получить нулевой лосс.
При этом, чем больше размер пуллинга, тем больше шансов получить другой локальный минимум, что согласуется с вашим экспериментом. По идее, должно быть меньше проблем, если поставить свертку после пуллинга
UPD — немного поправил пример, оптимизация не привела бы к моему решению, из указанного мной стартового ядра
Моя реализация свёрточной нейросети на Си++ для linux.
Сама сеть находится в ccudacnn.cu.h.
Сверточная нейронная сеть, часть 2: обучение алгоритмом обратного распространения ошибки