Как стать автором
Обновить

Комментарии 34

Вот нифига не понял вот какую штуку.
1) У меня сделана свёртка для каждого ядра. После свёртки у каждого ядра стоит слой субдискретизации (pooling), за ним то что получилось отправляется на один нейрон (назову его N). Таких нейронов — по числу ядер. Они служат входами обычной полносвязной нейросети.
2) Делаем обратное распространение ошибки для полносвязной нейросети. Приходим до одного нейрона N. Дельту мы знаем со входного слоя полносвязной нейросети, так что прогоняем её на начало нейрона. Получаем дельт по размерности результата после субдискретизации. Дальше эти дельты прогоняем через слой субдискретизации (мы ведь помним, откуда брали максимальную точку). Получаем дельт по числу точек картинки, после применения свёртки.
3) А вот и проблема. А как веса ядра по ним модифицировать? То, что написано в этой статье, так это расчёт дельт на каждой точке исходного изображения (до свёртки). Это нужно, когда слоёв свёртки много. А на первом-то слое как быть? Как модифицировать веса, зная дельты для каждой точки свёртки?

Что интересно, внятную статью, что и как делать вообще не могу найти. Почти всегда либо тонна математики, либо сразу ныряние в TensorFlow. А так, чтобы по-действиям описать все этапы как алгоритм — такого нет.
У меня была такая же проблема при написании диплома много лет назад. Сейсас, к сожалению, потерял источник, но может быть вот эта статья описывает процесс немного лучше: ТЫЦ
(См. процесс обратной свертки внизу)
Так это эта же статья и есть.
Я нашёл, что
Градиент для ядра свёртки можно посчитать как свёртку матрицы входа свёрточного слоя с «перевёрнутой» матрицей ошибки для выбранного ядра.

Приложенная к этой фразе формула, правда, показывала, что градиенты получатся тоже перевёрнутые и их надо будет перевернуть для коррекции ядра.
То есть, нужно взять исходное изображение и посчитать свёртку с перевёрнутой матрицей дельт. Как раз получится матрица размером с ядро.
Я вчера так и сделал. Но вся фишка в том, что дельты эти даже в соединении ядро->один выходной нейрон (т.е. в части до полносвязной сети) очень маленькие. Ядро вообще практически не обучается. То ли я дельты считаю неправильно (а я считаю как для обычной нейросети, по тем же формулам), то ли где-то ошибка и я чего-то не понимаю.
Честно говоря, я опешил, когда посмотрел, что та ки есть )))
Пятница, пора домой значит
Дополню.
Правильный метод вычисления поправок к весам ядра такой:
Градиент для ядра свёртки можно посчитать как свёртку перевёрнутой матрицы входа свёрточного слоя с матрицей ошибки для выбранного ядра.


Возможен и вариант
Градиент для перевёрнутого ядра свёртки можно посчитать как свёртку матрицы входа свёрточного слоя с перевёрнутой матрицей ошибки для выбранного ядра.


Оба варианта дадут одинаковый результат. Этот градиент ещё стоит поделить на количество элементов в свёртке.
Всё написанное выше считать неверным. :) Нашёл на stackoverflow тот же вопрос, что у меня, а именно, почему странные градиенты получаются по вышеуказанному методу расчёта. К вопросу прилагался довольно бестолковый (и похоже, неправильный) ответ на примере, но главное, в ответе была высказана мысль, что обучение ядер ничем не отличается от обычного обратного распространения, только вместо умножений свёртки. Я так и сделал. Я свернул прямое изображение с прямой же матрицей ошибки и получил прямые поправки к ядру. И что бы вы думали? Градиенты стали абсолютно верными и ядро моментально обучалось и приходило к заданным значениям! Почему во всех статьях указано переворачивание — загадка природы. Переворачивать, как я понимаю, нужно для расчёта дельт на входе слоя, но никак не для коррекции ядра по известным дельтам.
Кстати тут в статье ошибка. Посмотрите на анимацию обучения — ошибка дельта распространяется, умножаясь на вес связи, а производная функции активации заботливо засунута в формулу модификации весов. На самом деле, эта производная должна участвовать в расчёте дельты слоёв, как написано во множестве других статей. Вот поэтому анимация в статье неверна.

P.S. Мне так и не удалось до сих пор заставить внятно работать CNN. Например, с той же ReLu ядра легко влетают в область, где обучение отсутствует (производная 0). Инициализация He, увы, не очень помогает. Непонятно, сколько вообще нужно ядер (часть из них вообще не обучится с ReLu). У меня в сети три разных слоя: ядра->субдискретизация->нейрон по одному на ядро->полносвязная сеть. Вот этот нейрон, который один на ядро, он смвотрит на огромную картинку (224x224 после свёртки и субдискретизации раза 2x2). И вот ошибка этого нейрона очень плохо переносится в дельту ядер. Пришлось даже отказаться от деления изменения весов ядра на размер изображения после свёртки.

Возможно пришло время осилить батчнорм

Я попробую понять, что это. :) Ещё бы убедиться, что вся математика работает верно. Так-то обучается, но оно обучается и с неправильной математикой (к примеру, без производных в дельтах тоже будет работать, но часто будет улетать в бесконечность, хотя в других случаях будет отлично сходиться), только иным образом и не всегда.

У вас есть куча автодифф фреймворков на выбор (torch, chainer, mxnet, etc). Дробите свою сеть на части, сверяйте производные которые взяли руками с тем, что выдает фреймворк, правьте реализации, если результаты не совпадают.

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

Фреймворки проектируются так, чтобы ими можно было пользоваться имя минимальные навыки программирования

Так это только внешний интерфейс. Нутрянка-то не предназначена для простого понимания. Вот этот torch использует lua. Но я-то lua не знаю. TensorFlow использует python. Но я его не использую. Я Си++ использую. Ну и т.д. и т.п.

lua-torch мертв, по сути, все используют pytorch. Строго говоря, все популярные фреймворки ориентируются на python, в первую очередь. Если знаете C++, то необходимая база питона набирается за пару недель

Если знаете C++, то необходимая база питона набирается за пару недель


Завидую. :) Но мне новый язык так просто не дастся.
Вроде как действительно
Градиент для ядра свёртки можно посчитать как свёртку матрицы входа свёрточного слоя с «перевёрнутой» матрицей ошибки для выбранного ядра.


Только есть нюанс. Вот представим, что у меня есть только свёртка (без нейронов и без функции нейрона) и я желаю настроить её ядро. Зададим очень простую задачу — сделать выход свёртки всегда равным нулю. Логично, что ядро просто должно обнулиться. Считаем свёртку и задаём дельту. Так как нелинейностей у нас нет, то дельта (как я понимаю) должна быть равна разности нуля (мы же его хотим получить) и результата свёртки (а это полученный результат). Так вот, если посчитать изменения весов ядра, то оно будет огромным, что в корне неверно. Потому что вот эта вот дельта, вообще говоря, большая. Отсюда следует, что дельту я считаю явно неправильно. Как же тогда правильно в этом случае считать дельту?
Так вот, если посчитать изменения весов ядра, то оно будет огромным, что в корне неверно

Почему?
Почему неверно или почему огромным? Первое приводит к разносу, а второе получается, так как точек исходного изображения много (и все они положительные — это ведь цвет точки) и дельта на первом шаге тоже велика. У меня такая подстройка улетала за десяток шагов в бесконечность.
Первое. Это нормальное поведение алгоритма, при неправильно выбранном шаге.
Но тогда этот шаг будет сильно зависеть от размеров ядра и изображения. Как в таком случае его избавить от этой зависимости?
Будет зависеть, но не обязательно в чистом виде. Есть куча вариантов. Из инвариантных к задаче — методы второго порядка, и методы нормирующие градиенты на их среднюю длину (Adam). Из вариантов зависящих от задачи — делить лосс на размер картинки, если выход сетки это картинка, еще можно использовать BatchNorm.
Так же, надо понимать, что обычно ядра инициализируют весьма специфичным образом. При правильной инициализации, не должно быть больших проблем с масштабом и зависимостью от размера ядра/числа нейронов
А вот ещё веселье. Берём обычную сеть из трёх входных нейронов и одного выходного. Забиваем на функцию нейрона — пусть будет линейной. Производная — число. Даём входы как 1,2,3, а веса как 3,2, 1. Хотим получить ноль на выходе. Первый раз получается 10. Дельта выхода, соответственно, тоже 10. Считаем эту простейшую сеть обратно и получаем на первой итерации коррекцию весов -10,-20,-30. Офигеваем. Направление верное, веса нужно уменьшать к нулю, но вот масштаб… И что интересно, с каждой итерацией всё это стремительно летит в бесконечность. При этом всё это работает, если веса задать разнородными (больше и меньше нуля) и маленькими. Что-то я вообще в обратном распространении перестал понимать, как так получается.
Это одна из проблем обучения — можно перескочить точку экстремума при слишком большом шаге, поэтому есть настраиваемый параметр обучаемости (насколько я помню из университетского курса). Грубо говоря это дробный коэффициент на который умножают дельты, чтобы обучаться медленнее, но увереннее. На википедии (ТЫЦ) называют этот коэффициент «скоростью движения»
Также иногда используют какие-то случайные алгоритмы для подстройки весом между несколькими итерациями спуска, чтобы получить случайные значения и выйти из точки локального экстремума, если застряли.
Всё это так, но формально-то получается частная производная функции оценки по изменению веса (вот есть статья).
Для одного слоя дельта определяется как
image
Тогда частная производная функции оценки (W — матрица весов омега) по изменению веса будет
image

Входы у нас 1,2,3, веса 3,2,1, функция нейрона линейная 1:1 (т.е. производная 1). А ошибка дельта 10. Получаем значения производных 10,20 и 30 для весов 3,2,1.
image
В данном случае такие шаги огромные. Коррекция выполняется как
image
Скорость ограничивает альфа. Но вот вопрос — про альфу известно, что она меньше 1. Но насколько? У меня она 0.25.И интересно, почему при скорости 1 сеть не делает приращение в один шаг. Ведь это вполне логично. Образ обучающий один. Функция линейная. Вся математика точная.

А для CNN я нашёл аж три разных варианта коррекции весов ядра. Какой правильный? А фиг знает. То переворачивают изображение и умножают на дельты, то ещё и применяют функцию нейрона к изображению и перевернув умножают на дельту, то дельту переворачивают и умножают на изображение (в этом случае ещё два варианта видел — в одном ядро получится прямое, а в другом указано, что перевёрнутое — кому верить непонятно).
про альфу известно, что она меньше 1. Но насколько? У меня она 0.25

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

0.25 вроде как популярное значение для персептрона


Вот ещё есть нюанс. Персептрон — двоичная игрушка. А сеть с одним нейроном — просто сеть с одним нейроном.
Возможно вам стоит немного почитать про метод градиентного спуска. Если не хочется решать проблему с шагом эмпирически, то можно использовать методы line search, или методы оптимизации второго порядка (Ньютон, квазиньютон)

Но вот вопрос — про альфу известно, что она меньше 1

На самом деле не обязательно) Бывают случаи, когда выгодно брать шаг больше 1, для методов первого порядка. Для методов второго да, 1 максимальный разумный шаг
Возможно вам стоит немного почитать про метод градиентного спуска.


Так вроде ж он описан в статье, на которую я ссылался (где формулы).
Интересно, всё вроде бы работает, но есть нюанс. Обучаю отдельно ядро с субдискретизацией. Задаю желаемую свёртку, вычисленную при известном ядре и оно к ней приходит. Но только без субдискретизации. Как только её включаю, тут же эффективность обучения падает на порядки. Субдисретизация у меня по максимальному элементу в блоке. При обратном распространении ошибку получает именно этот элемент свёртки, остальные в блоке получают ноль. С субдискретизацией 2x2 ядро ещё обучается, но с 4x4 я уже результата не вижу. Почему бы такая печаль?
А можно чуть подробней про задачу?
При обратном распространении ошибку получает именно этот элемент свёртки, остальные в блоке получают ноль

Именно так работает производная от максимума
А можно чуть подробней про задачу?


Какую задачу? Я просто обучаю сеть распознавать лица двух человек.

Для проверки обучения ядра я сделал изображение, задал ядро в 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 — немного поправил пример, оптимизация не привела бы к моему решению, из указанного мной стартового ядра

Что ж, может быть, это действительно локальный минимум. Поэкспериментирую. :)
Сделал-таки рабочую свёрточную сеть на Си++ с CUDA. Вот по этой статье. Тут все нюансы описаны (я, например, думал, что каждая свёртка первого слоя должна быть свёрнута с каждым ядром второго — а это совсем не так).

Моя реализация свёрточной нейросети на Си++ для linux.

Сама сеть находится в ccudacnn.cu.h.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории