Сверточная нейронная сеть, часть 2: обучение алгоритмом обратного распространения ошибки

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

    Обучение сверточной нейронной сети


    На начальном этапе нейронная сеть является необученной (ненастроенной). В общем смысле под обучением понимают последовательное предъявление образа на вход нейросети, из обучающего набора, затем полученный ответ сравнивается с желаемым выходом, в нашем случае это 1 – образ представляет лицо, минус 1 – образ представляет фон (не лицо), полученная разница между ожидаемым ответом и полученным является результат функции ошибки (дельта ошибки). Затем эту дельту ошибки необходимо распространить на все связанные нейроны сети.

    Таким образом обучение нейронной сети сводится к минимизации функции ошибки, путем корректировки весовых коэффициентов синаптических связей между нейронами. Под функцией ошибки понимается разность между полученным ответом и желаемым. Например, на вход был подан образ лица, предположим, что выход нейросети был 0.73, а желаемый результат 1 (т.к. образ лица), получим, что ошибка сети является разницей, то есть 0.27. Затем веса выходного слоя нейронов корректируются в соответствии с ошибкой. Для нейронов выходного слоя известны их фактические и желаемые значения выходов. Поэтому настройка весов связей для таких нейронов является относительно простой. Однако для нейронов предыдущих слоев настройка не столь очевидна. Долгое время не было известно алгоритма распространения ошибки по скрытым слоям.

    Алгоритм обратного распространения ошибки


    Для обучения описанной нейронной сети был использован алгоритм обратного распространения ошибки (backpropagation). Этот метод обучения многослойной нейронной сети называется обобщенным дельта-правилом. Метод был предложен в 1986 г. Румельхартом, Макклеландом и Вильямсом. Это ознаменовало возрождение интереса к нейронным сетям, который стал угасать в начале 70-х годов. Данный алгоритм является первым и основным практически применимым для обучения многослойных нейронных сетей.

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

    Основные соотношения метода обратного распространения ошибки получены при следующих обозначениях:



    Величина ошибки определяется по формуле 2.8 среднеквадратичная ошибка:



    Неактивированное состояние каждого нейрона j для образа p записывается в виде взвешенной суммы по формуле 2.9:



    Выход каждого нейрона j является значением активационной функции

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



    В качестве метода минимизации ошибки используется метод градиентного спуска, суть этого метода сводится к поиску минимума (или максимума) функции за счет движения вдоль вектора градиента. Для поиска минимума движение должно быть осуществляться в направлении антиградиента. Метод градиентного спуска в соответствии с рисунком 2.7.



    Градиент функции потери представляет из себя вектор частных производных, вычисляющийся по формуле 2.11:



    Производную функции ошибки по конкретному образу можно записать по правилу цепочки, формула 2.12:



    Ошибка нейрона обычно записывается в виде символа δ (дельта). Для выходного слоя ошибка определена в явном виде, если взять производную от формулы 2.8, то получим t минус y, то есть разницу между желаемым и полученным выходом. Но как рассчитать ошибку для скрытых слоев? Для решения этой задачи, как раз и был придуман алгоритм обратного распространения ошибки. Суть его заключается в последовательном вычислении ошибок скрытых слоев с помощью значений ошибки выходного слоя, т.е. значения ошибки распространяются по сети в обратном направлении от выхода к входу.

    Ошибка δ для скрытого слоя рассчитывается по формуле 2.13:


    Алгоритм распространения ошибки сводится к следующим этапам:
    • прямое распространение сигнала по сети, вычисления состояния нейронов;
    • вычисление значения ошибки δ для выходного слоя;
    • обратное распространение: последовательно от конца к началу для всех скрытых слоев вычисляем δ по формуле 2.13;
    • обновление весов сети на вычисленную ранее δ ошибки.

    Алгоритм обратного распространения ошибки в многослойном персептроне продемонстрирован ниже:

    До этого момента были рассмотрены случаи распространения ошибки по слоям персептрона, то есть по выходному и скрытому, но помимо них, в сверточной нейросети имеются подвыборочный и сверточный.

    Расчет ошибки на подвыборочном слое


    Расчет ошибки на подвыборочном слое представляется в нескольких вариантах. Первый случай, когда подвыборочный слой находится перед полносвязным, тогда он имеет нейроны и связи такого же типа, как в полносвязном слое, соответственно вычисление δ ошибки ничем не отличается от вычисления δ скрытого слоя. Второй случай, когда подвыборочный слой находится перед сверточным, вычисление δ происходит путем обратной свертки. Для понимания обратно свертки, необходимо сперва понять обычную свертку и то, что скользящее окно по карте признаков (во время прямого распространения сигнала) можно интерпретировать, как обычный скрытый слой со связями между нейронами, но главное отличие — это то, что эти связи разделяемы, то есть одна связь с конкретным значением веса может быть у нескольких пар нейронов, а не только одной. Интерпретация операции свертки в привычном многослойном виде в соответствии с рисунком 2.8.


    Рисунок 2.8 — Интерпретация операции свертки в многослойный вид, где связи с одинаковым цветом имеют один и тот же вес. Синим цветом обозначена подвыборочная карта, разноцветным – синаптическое ядро, оранжевым – получившаяся свертка

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

    Рисунок 2.9 — Вычисление δ подвыборочного слоя за счет δ сверточного слоя и ядра

    Обратная свертка – это тот же самый способ вычисления дельт, только немного хитрым способом, заключающийся в повороте ядра на 180 градусов и скользящем процессе сканирования сверточной карты дельт с измененными краевыми эффектами. Простыми словами, нам необходимо взять ядро сверточной карты (следующего за подвыборочным слоем) повернуть его на 180 градусов и сделать обычную свертку по вычисленным ранее дельтам сверточной карты, но так чтобы окно сканирования выходило за пределы карты. Результат операции обратной свертки в соответствии с рисунком 2.10, цикл прохода обратной свертки в соответствии с рисунком 2.11.

    Рисунок 2.10 — Результат операции обратной свертки


    Рисунок 2.11 — Повернутое ядро на 180 градусов сканирует сверточную карту

    Расчет ошибки на сверточном слое


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

    Заключение


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

    Источники


    Алгоритм обратного распространения ошибки для сверточной нейронной сети

    Обратное распространение ошибки в сверточных слоях
    раз и два

    Обратное распространение ошибки в персептроне

    Еще можно почитать в РГБ диссертацию Макаренко: АЛГОРИТМЫ И ПРОГРАММНАЯ СИСТЕМА КЛАССИФИКАЦИИ
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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


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


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

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

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

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


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

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

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


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

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


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

                    Для проверки обучения ядра я сделал изображение, задал ядро в 1 и посчитал от него свёртку. Пропустил свёртку через субдискретизацию и получил что мне требовать от ядер. Теперь сбрасываем ядра в случайное значение, зная ответ свёртки считаем поправки к ядрам и смотрим, приходят ли ядра к 1. Так вот, без субдискретизации приходят. С 2x2 приходят уже существенно медленнее, а с 4x4 и выше я вообще не дождался результата.
                      0

                      Я не уверен, но похоже на локальный минимум
                      Рассмотрим одномерный случай — "картинку" размера 4, которая подается на вход свертке с ядром 3, а потом макспулингу размера 2, после чего мы имеем на выходе одно число


                      Пусть картинка имеет вид [10, 3, 5, -4]. Свертка состоящая из [1, 1, 1], без bias, даст на выходе картинку [18, 4], пулинг даст на выходе просто 18. Т.е. 18 и будет нашим целевым значением. Теперь мы стартуем со случайных весов, и пусть так получилось, что веса имеют вид [0, 3, 0]. Свертка даст на выходе вектор [9, 15], пулинг даст, соответственно, 15. Теперь, при оптимизации свертка будет ориентироваться только на правую часть картинки, т.к. макспулинг пустит их только в одну сторону. Мне немного лень считать конечный результат, но скорее всего оптимизация будет немного увеличивать все три значения ядра, до сходимости.
                      При этом ошибка будет нулевая, и итоговое ядро не будет совпадать с изначальным.
                      Но, т.к. вы, вероятно, пропускаете через свертку большую картинку, то у вас требуется чтобы свертка удовлетворяла сразу множеству выходов, и тут уже не все локальные минимумы позволяют получить нулевой лосс.


                      При этом, чем больше размер пуллинга, тем больше шансов получить другой локальный минимум, что согласуется с вашим экспериментом. По идее, должно быть меньше проблем, если поставить свертку после пуллинга


                      UPD — немного поправил пример, оптимизация не привела бы к моему решению, из указанного мной стартового ядра

                        0
                        Что ж, может быть, это действительно локальный минимум. Поэкспериментирую. :)

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое