В интернете есть много статей с описанием алгоритма градиентного спуска. Здесь будет еще одна. 8 июля 1958 года The New York Times писала: «Психолог показывает эмбрион компьютера, разработанного, чтобы читать и становиться мудрее. Разработанный ВМФ… стоивший 2 миллиона долларов компьютер "704", обучился различать левое и правое после пятидесяти попыток… По утверждению ВМФ, они используют этот принцип, чтобы построить первую мыслящую машину класса "Перцептрон", которая сможет читать и писать; разработку планируется завершить через год, с общей стоимостью $100 000… Ученые предсказывают, что позже Перцептроны смогут распознавать людей и называть их по имени, мгновенно переводить устную и письменную речь с одного языка на другой. Мистер Розенблатт сказал, что в принципе возможно построить "мозги", которые смогут воспроизводить самих себя на конвейере и которые будут осознавать свое собственное существование» (цитата и перевод из книги С. Николенко, «Глубокое обучение, погружение в мир нейронный сетей»). Ах уж эти журналисты, умеют заинтриговать. Очень интересно разобраться, что на самом деле представляет из себя мыслящая машина класса «Перцептрон». Вот наш искусственный нейрон, он делит объекты на два класса (выполняет двоичную классификацию объектов):  Итак, у нас есть: Классификация — потому, что нейрон назначает объекту класс, бинарная (двоичная) — потому, что возможных классов всего два.  [игрек с крышкой] — будем обозначать предсказанное (вычисленное) значение класса для объекта 
 [обычный игрек без крышки] — истинные (известные) значения класса для объекта  из обучающей выборки. Значения  (здесь и дальше  и  — это не единичные значения, а векторы) меняются от объекта к объекту, весовые коэффициенты  (будучи выбраны один раз) остаются неизменны. Для обучающей выборки для каждого объекта  известна метка класса . На этапе обучения нужно подобрать весовые коэффициенты  так, чтобы модель выдавала правильное значение  (совпадающие с ) для максимального количества объектов обучающей выборки. Предположение о полезности обученного таким образом нейрона базируется на надежде на то, что с подобранными коэффициентами он будет выдавать правильные значение  для новых объектов , истинное значение класса  для которых заранее не известно. Интуитивный смысл взвешенной суммы входов нейрона заключается в том, что все признаки объекта (каждый из признаков — это один из входов нейрона) оказывают влияние на результат классификации объекта, но не все признаки — в одинаковой степени. В какой именно степени — определяют веса; обнуление какого-то весового коэффициента обнуляет вклад соответствующего признака в общую сумму, т.е. это равносильно удалению у объекта признака. Нейрон ADALINE (adaptive linear neuron) — это обычный искусственный нейрон вот с такой функцией активации:     Здесь и дальше верхний индекс  в скобках будет обозначать -й элемент обучающей выборки  или истинное значение класса  или предсказанное значение класса  для него же. Можно сказать, что у такого нейрона функции активации просто нет и на вход квантизатора (порога) подается значение взвешенной суммы входов. Но для единообразия будет удобнее считать, что в качестве активации берется значение взвешенной суммы. Порог (квантизатор) — предсказывает метку класса:   Если значение активации больше некоторого значения порога θ [тета], то квантизатор назначает объекту метку "1", если значение активации меньше порога θ, то объект получает метку "-1". Здесь мы можем сформулировать задачу в первом приближении: нам нужно подобрать параметры нейрона так, чтобы значения классов , которые нейрон назначает объектам обучающей выборки, совпадали с истинными значениями классов  для этих же элементов (или, по крайней мере, давали правильное значение для большинства). Немного преобразуем пороговую функцию, возьмем случай для класса  и перенесем порог в левую часть неравенства:   обозначим  и    Как видим, нам удалось избавиться от отдельного параметра θ, внеся его под видом нового весового коэффициента  под знак суммы, добавив при этом к описанию объекта новый фиктивный единичный признак . Скорректируем формулировку задачи с учетом новых обозначений. Задача': подобрать параметры нейрона — весовые коэффициенты ,
 (признак-константа) — фиктивный нейрон (нейрон смещения) Начиная с этого места нумеруем признаки и весовые коэффициенты c 0, а не с 1. Про вектор  будем говорить, как про (m+1)-мерный, а не m-мерный. Вектор  в зависимости от контекста можем считать (m+1)-мерным (по большей части в формулах), но при этом помнить, что на самом деле он m-мерный. Почему нейрон (в нашем случае, правда, это не нейрон, а признак объекта или просто вход, но в случае многослойной сети он превращается в нейрон и обычно его именно так называют) фиктивный — понятно уже сейчас. Почему он еще и смещения станет понятно позднее. Активация с суммой будут выглядеть теперь так:   Порог теперь всегда 0 (ноль) (реальное значение переехало в параметр ):   Еще раз сформулируем задачу другими словами (геометрический смысл задачи) Если мы внимательно посмотрим на формулу функции активации, то мы увидим, что она представляет собой параметрическую гиперплоскость в (m+1)-мерном пространстве, при этом в первых m измерениях она сосуществует вместе с точками элементов выборки, а (m+1)-е измерение — пространство значений функции, отдельное от элементов. Теперь, если мы приравняем значение активации к нулю (значение порога), то это тоже будет гиперплоскость, только уже в m-мерном пространстве, т.е. полностью в пространстве значений элементов . Эта гиперплоскость будет разделять элементы  на две непересекающиеся группы. Обычно в этом месте говорят, что наша задача — подобрать значения параметров , т.е. построить m-мерную гиперплоскость в пространстве элементов так, чтобы элементы обучающей выборки с истинным значением класса "1" оказались по одну сторону плоскости, а элементы с истинным классом "-1" — по другую. Для тех, кто не совсем понял, что здесь написано, читайте дальше — сейчас мы все увидим, это во-первых. Во-вторых, мы так же увидим, что такая постановка задачи хотя и справедлива, но не совсем полна. Здесь начнет появляться код. Все графики строим с обычной библиотекой Matplotlib, но здесь я еще использую библиотеку Seaborn в одной строчке для настройки области графика, т.к. мне нравится, как у нее это получается, но в принципе можно обойтись и без нее. Берем множество 1-мерных точек и ответов к ним: Здесь у нас каждый i-й элемент массива X1 — это i-й элемент (i-я точка) обучающей выборки (еще точнее — его 1-й и единственный признак): ,  Каждый i-й элемент массива y — правильный ответ, истинная метка, соответствующая i-му элементу обучающей выборки с единственным признаком X1[i]. Возьмем всего 5 точек, первые две отнесем к классу "-1", остальные три — к классу "1". Нарисуем эти точки на линии: Теперь посмотрим на функцию активации:   Как видим, это обычная параметрическая прямая на плоскости (в 2-мерном, т.е. (m+1)-мерном пространстве): Вспомним еще, что после небольшого преобразования у нас порог активации обратился в ноль. Таким образом, если проекция i-го элемента на линию активации оказывается ниже нуля, мы назначаем элементу класс "-1" (), если выше нуля, назначаем элементу класс "1" (). Фиолетовая точка — пересечение линии активации с осью , разделяющая элементы из разных классов, — это и есть та самая разделяющая гиперплоскость (для 1-мерного пространства точка — это гиперплоскость), построенная в 1-мерном (т.е. m-мерном) пространстве признаков. Как видим, для того, чтобы разделить элементы на группы, её достаточно, но для того, чтобы назначить группам классы — уже не достаточно. Для того, чтобы назначить элементам классы, нам нужна прямая (2-мерная гиперплоскость) активации, построенная в 2-д (т.е. в (m+1)-д) пространстве «признаки+активация»: направление отклонения активации от вертикальной оси будет определять класс для групп элементов, т.к. от этого зависит, окажутся ли проекции элементов на активацию выше или ниже нуля. Меняя параметры  и  мы будем получать разные активационные линии. Нам нужно построить такую линию активации, т.е. найти такую комбинацию параметров , при которой проекция первых двух точек обучающей выборки на линию активации окажется ниже нуля (для них значение ), а проекция оставшихся 3-х точек окажется выше нуля (для них ). Совершенно очевидно, что в нашем конкретном случае нет ничего сложного в том, чтобы построить такую линию, более того, таких линий вообще можно построить бесконечное количество. Но мы постараемся ее построить так, чтобы удовлетворялся некий критерий оптимальности (может влиять на качество будущих предсказаний), плюс должна быть возможность распространить алгоритм на многомерный случай. Здесь так же отметим, что мы специально выбрали исходное множество точек так, чтобы его можно было такой линией разделить (для 1-д: все элементы первой группы меньше, все элементы второй группы больше некоторого фиксированного значения), т.е. множество обучающих точек линейно разделимо. Добавим на график еще две горизонтальные линии, соответствующие классам {1, -1}, и спроецируем на них элементы. Точки с классом "-1" проецируем на нижнюю линию , точки с классом "1" проецируем на верхнюю линию . Обратим здесь внимание на еще один небольшой нюанс. По вертикальной оси мы строим значения активации, пространство значений активации непрерывно. Но результат работы классификатора (функция активации, пропущенная через порог) — дискретное множество из двух элементов {-1, 1}, а не непрерывная шкала. Здесь мы берем дискретное множество классов  и накладываем его на непрерывную шкалу активации  так, что дискретные значения классов становятся обычными точками на шкале активации — частными случаями значений активации, которые она может непосредственно принимать или приближаться к ним достаточно близко. Строго говоря, мы могли бы изначально в качестве классов взять не числовые значения, а строковые метки "класс-1" и "класс-2", в таком случае мы бы должны были поставить в соответствие строковым меткам числовые значения на шкале активации. Поэтому и в нашем случае значения классов "-1" и "1" следует воспринимать скорее не как метки классов как есть, а как отображение помеченных классов на шкалу активации. Естественно принять, что чем ближе окажется значение активации для выбранного элемента к значению класса для этого же элемента, тем лучше активация предсказывает для этого элемента его класс. Таким образом, за ошибку для выбранного элемента можно принять расстояние между точками — вертикальной проекцией элемента на линию активации и проекцией элемента на горизонтальную линию его известного (истинного) класса. На графике: ошибки — вертикальные оранжевые линии. У нас есть метрика ошибки для каждого отдельного элемента. Мы можем получить из нее метрику качества для всей линии активации. Вполне естественно принять, например, что чем меньше сумма ошибок всех элементов обучающей выборки, тем лучше у нас построена линия активации. Для каждого отдельного элемента ошибка будет не минимальной, но для всей обучающей выборки в целом можно получить некий компромисс. Но можно взять не простую сумму ошибок, а сумму ошибок, возведенных в квадрат (сумма квадратичных ошибок, sum of squared errors, SSE). Вполне очевидно, что, как и в случае с суммой обычных ошибок, чем ближе линия активации находится к точкам с истинными классами элементов, тем меньше будет и сумма квадратичных ошибок, но в случае с квадратичной ошибкой наиболее удаленные элементы будут получать более серьезный штраф. На самом деле нас здесь больше интересует не размер штрафа за дальние элементы, а тот факт, что квадратичная функция имеет минимум и при этом везде дифференцируема (у обычной суммы будет минимум, но в этом минимуме она не будет дифференцируема), зачем это нужно, увидим чуть потом. Итак: Возьмем в качестве функции стоимости  SSE, в общем случае и для линейного нейрона она будет выглядеть так:   ( перед SSE, во-первых, не мешает, во-вторых, для удобства — она дальше красиво сократится) Здесь  — номер элемента, а  — количество элементов в обучающей выборке. Напомню, что  — истинный класс -го элемента обучающей выборки, т.е. заранее известный правильный ответ. Как мы помним, положение линии активации определяют параметры — весовые коэффициенты , поэтому вектор  выступает в качестве параметра функции потерь.   Значения  и  заранее известны (это обучающая выборка), поэтому фиксированы. Мы подбираем параметры , т.е.  и  так, чтобы значение  получилось минимальным. Попробуем построить график, на котором будет видно, как значение  зависит от параметров  и  В общем, здесь уже видно, что у функции потерь есть минимум, и где он примерно находится. Но давайте сделаем еще один фокус и построим тот же график, только с логарифмической вертикальной шкалой. Не знаю, как вы, а лично я, когда увидел этот график в первый раз, испытал просветление. Эта натуральная впадина — не просто образная визуализация многомерных холмов из популярной статьи о нейронных сетях, это реальный график. Наша задача — подобрать такие значения  и , чтобы попасть на дно этой ямы. Получим значения весовых коэффициентов — получим обученный нейрон. Раз уж мы все равно построили график и воочию наблюдаем его минимум, никто нам не запретит найти его координаты простым перебором на сетке "вручную": Это значения:  и , сумма квадратичных ошибок SSE при этом 0.69, функция стоимости  (более точно: 0.3456478371758288). Посмотрим, как с этими параметрами выглядит активация: Как по мне, вполне норм. Точка пересечения активации с нулевым порогом разделяет элементы из разных классов, а сама активация назначает им правильные значения. При этом активация, похоже, находится в некотором оптимальном положении. Перед тем, как двигаться дальше, еще раз полюбуемся на график на сетке пошире: Похоже, что никаких других минимумов поблизости нет, кто бы мог подумать. Итак, мы получили весовые коэффициенты — координаты минимального значения ошибки. Это будет оптимальное значение весовых коэффициентов на обучающей выборке. Вообще говоря, именно это нам и нужно, можно сказать, что нейрон обучен. Может на этом можно и завершить? Значит, если мы захотим обучить один-единственный нейрон (даже не нейросетку) на изображении 28x28=784 пикселей методом поиска минимума прямым перебором на сетке в 100 точек на каждое измерение, нам нужно перебрать 10^1570 комбинаций. Это довольно много и для хранения, и для перебора (в видимой части Вселенной всего 10^80 атомов, Вселенная существует примерно 4*10^17 секунд = 4*10^26 наносекунд). Попробуем найти вариант побыстрее. Посмотрим на график функции потерь  на плоскости: фиксируем , меняем  Это обычная парабола (точнее, семейство парабол — они будут немного отличаться в зависимости от того, какое именно значение зафиксировали на ). Чтобы найти минимум параболы, не обязательно перебирать все точки. Мы можем выбрать произвольную точку на горизонтальной оси и двигаться в сторону минимума с некоторым шагом. Рассмотрим вариант с постоянным шагом Замечание: когда я рассказывал про такой вариант спуска на лекции, один студент спросил, зачем нужно двигаться по шагам, если можно сразу найти минимум параболы по формуле? Я сначала ответил что-то в духе, что нам сейчас интересно рассмотреть вариант с итерациями, чтобы потом иметь возможность применить его не только с параболой, но и в других ситуациях. Плюс к этому, на самом деле нам не нужен минимум параболы конкретно на этом срезе — мы будем двигаться к минимуму не по одному измерению, а по всем измерениям поочередно так, что на каждой новой итерации новый шаг будет проходить не по этой параболе, а на параболе с нового среза со сдвинутым значением . Но подумав позднее, я подумал, что в принципе нет ничего плохого, если мы на каждом срезе будем двигаться не шагами, а скатываться сразу к минимуму текущего среза. Так раз за разом измерение за измерением мы должны все равно скатиться к глобальному минимуму, и, похоже, быстрее, чем по шагам. Для единственного нейрона должно сработать, и не только с параболой. Но я пока не стал тратить время на проверку этой теории, поэтому здесь просто двинемся дальше — я обещал рассказать про градиентный спуск. В общем так, спускаемся по шагам, но делаем это более умно. Используем для подбора шага производную кривой стоимости (здесь не кривая стоимость, а кривая стоимости). Это всё хорошо, но причем здесь производная? Сейчас разберемся. Для меня производная долгое время оставалась набором специальных формул и правил для ее вычисления, плюс что-то про возрастание, убывание и экстремумы. Здесь будет уместно вспомнить или выяснить, что из себя представляет производная на самом деле. Производная функции  в данной точке  — это предел отношения приращения функции  к приращению аргумента  при приращении аргумента , стремящемся к нулю:   На картинке точка  — точка, в которой мы хотим определить производную. Точка  — точка, получаемая приращением аргумента . Прямая  — секущая, проходящая через эти две точки. Точка  — пересечение секущей  с горизонтальной осью . Рассмотрим два прямоугольных треугольника: треугольник  с участком секущей  в качестве гипотенузы и треугольник  с продолжением секущей до оси  — отрезком  в качестве гипотенузы. Из графика и школьного курса геометрии очевидно, что углы  и  равны, а значит равны и их тангенсы:   Добавим на картинку:  — касательная к исходной кривой в точке , пересекает ось  в точке . Треугольник  — прямоугольный треугольник с гипотенузой — участком касетельной, отрезком . Устремляем приращение  к нулю: Точка  съезжает в точку  по функции, точка  ползет в точку  по оси , секущая  превращается в касательную  с точкой касания . Исходный треугольник  с катетами  и  сжимается в точку, но подобный ему треугольник  превращается в треугольник , сохраняя не только макроскопические габариты, но и равенство углов  и . Как приращение , бесконечно приближаясь к нулю, никогда не достигнет нуля, так и точка  никогда не встанет на точное место , точка  не достигнет точки , треугольник  не превратится в . К счастью, мы можем зафиксировать точный пункт назначения для всех этих движений, используя волшебный математический оператор «предел» . Предел превращения треугольника  — в точности треугольник , получаем:   Короче:   Вспоминая определение производной, как мы ее ввели, получаем:   И видим, что производная функции в данной точке равна тангенсу угла между касательной к исходной кривой в данной точке и горизонтальной осью координат . В этом и заключается её геометрический смысл. Строго говоря, не совсем корректно говорить, что секущая стремится занять положение некоторой заранее известной касательной, которую мы, кстати сказать, добавили на картинку без предварительного определения. Более корректно говорить, что предельное положение секущей при приращении аргумента, стремящемся к нулю, мы будем называть касательной, т.е. мы понятие касательной выводим из определения производной (если вы посмотрите на универсальную формулу касательной, то увидите, что она задается через производную). Но сути дела это не меняет: производная численно равна тангенсу, тангенс характеризует наклон (кстати, в английском языке касательная — это tangent line, может быть интересно разобраться, кто от кого происходит — касательная от тангенса или тангенс от касательной). В итоге отметим интересные нам свойства производной: Вот, например, наш случай с параболой: Оранжевая линия — исходная функция, красная линия — касательная к исходной функции в точке , синяя линия — это производная исходной функции в каждой из точек. Две зеленые линии — катеты прямоугольного треугольника с гипотенузой — касательной. Отношение вертикальной зеленой линии к горизонтальной — тангенс острого угла между касательной и горизонтальной осью , оно же — абсолютное значение производной в этой точке. Как видим, чем ближе минимум исходной функции, тем более пологий спуск, тем меньше вертикальная зеленая линия по сравнению с горизонтальной, тем меньше абсолютное значение производной. В правой части графика (спуск справа налево, функция возрастает) производная везде больше нуля (геометрически: угол между касательной и осью  острый, тангенс острого угла больше нуля). Спуск слева направо (исходная функция убывает): все то же самое, только со знаком минус (геометрически: угол между касательной и осью  тупой, тангенс тупого угла меньше нуля). Значит, подытожим: у нас есть универсальный вычислимый индикатор (метрика), который показывает как «крутизну»/«пологость» спуска, так и его направление в каждой из точек. Этот индикатор — производная функции в каждой точке. И, раз у нас есть такой индикатор, почему бы нам не использовать его для вычисления динамического шага при спуске к минимуму данной функции? Ровно это мы сейчас и сделаем. Возвращаемся к нашей функции ошибки . Мы уже видели, что, хотя она у нас является многомерной, в каждом из измерений ее можно порезать на параболы.     Кого испугали эти четыре строчки формул, не бойтесь: здесь производная суммы равна сумме производных, производная степенной функции, производная сложной функции, производная константы (равна нулю) и другие правила взятия производной из школьной программы. Все элементы, рядом с которыми нет множителя  (обратите внимание, он может прятаться внутри общей суммы), берутся за константы. И еще, кстати, обратите внимание, наша  перед SSE сократилась именно здесь. Получили в общем виде:   И вот наш градиент — вектор из частных производных (обозначается символом  [набла], перевернутым треугольником, т.е. перевернутой [дельтой]):   Определяем правило шага:   Оно же для -го измерения:   Здесь обратим внимание на: Те из вас, кто не пропустил часть с геометрическим смыслом производной, скорее всего уже поняли, зачем они здесь нужны. В любом случае, и то и другое подробно разберем ниже.   Частные производные (элементы градиента):     Шаги по каждому из измерений:     Между прочим, у нас уже все готово для спуска. Давайте построим графики и спустимся по одному из измерений. Фиксируем , спускаемся к минимуму  Сумма квадратичных ошибок для элементов обучающей выборки  с правильными ответами (истинными классами)  при выбранных коэффициентах  и  (будем дальше ссылаться на эту функцию): Посчитаем ошибки на отрезке  от -1.5 до 1.5. Построим график функции стоимости и заодно отметим первую точку, из которой будем спускаться (вообще говоря, она может быть любая, в том числе случайная): Градиент, а точнее, частная производная  — элемент градиента: Шаги спуска  (да, здесь размер шага  зависит от текущего значения , т.е. шаг зависит от того, из какой точки мы шагаем): Все вместе Здесь видим Обратите внимание на масштаб по вертикальным осям на двух нижних графиках: экранные размеры у них одинаковые, но абсолютные значения отличаются в 1000 раз. Ну давайте уже, наконец, спускаться Исходные веса  — обычно это какие-то случайные не очень большие значения или какие-то константы. Мы договорились взять , а исходную точку спуска . Здесь же выберем коэффициент обучения  и количество итераций (количество шагов, количество эпох обучения) пусть будет 12: Посчитаем значения шагов и значения ошибок на каждом новом шаге: Обозначим точки спуска с длинами шагов по  на графике : И те же точки спуска с длинами шагов на графике  Да, это он, спуск по градиенту (по одному из его измерений), градиентный спуск. В этом месте можно тоже испытать просветление, не такое сильное, как первое, конечно. Процедура спуска: для текущей точки на верхнем графике смотрим длину и направление шага на среднем графике, на верхнем графике откладываем шаг из текущей точки, движемся по линии шага на верхнем графике, «падаем» с нее на график, место падения — новая отправная точка, повторяем процедуру. В общем, именно в этом и заключается метод градиентного спуска. С таким уточнением, что здесь мы спустились только по одной координате и попали в локальный минимум на одном срезе. Чтобы попасть в глобальный минимум многомерной функции, мы будем на каждой итерации делать по одному шагу по каждой из координат (или один шаг, но многомерный). Коэффициент обучения  нужно подобрать таким образом, чтобы он обеспечил подходящий масштаб сразу для всех срезов. Замечание: возможно, можно представить модификацию алгоритма с разными коэффициентами обучения для каждого из измерений, не скажу сейчас, есть ли в этом реальный практический смысл. В принципе, на этом можно было бы и закончить, но давайте разберем и разжуем все совсем до жидкой кашицы, раз уж все равно до сюда добрались. Подытожим Возьмем значение  Здесь еще раз обратите внимание на диапазон значений по вертикальной оси на среднем графике и диапазон на горизонтальной оси наверху. Вертикальные отрезки на среднем графике по длине равны соответствующим горизонтальным отрезкам шагов на верхнем графике, хотя при экранном масштабе это не бросается в глаза. Здесь начинаем движение из точки 3-й справа, дальше перепрыгиваем через минимум в 3-ю слева, потом опять через минимум направо, налево и т.п. Каждый раз забираемся все выше по параболе, т.е. с каждым шагом ошибка возрастает. Это значит, что алгоритм расходится, нужно выбрать [эту] поменьше. Для выбора коэффициента  удобно построить график сходимости  при разных  по эпохам Так сходимся: ошибка падает на каждой эпохе, причем в первые эпохи довольно быстро, дальше падение замедляется. Кстати, здесь уже спускаемся по всем координатам сразу — это реальный многомерный алгоритм, градиентный спуск, уже совсем без оговорок. Эпох можно взять чуть побольше с запасом: А вот так расходимся: Ошибка растет. Алгоритм должен успеть спуститься вниз при заданном темпе обучения . Идеальный минимум получить не можем, но можем приблизиться к нему настолько близко, насколько захотим. Здесь темп ок, эпох маловато. На практике количество эпох удобнее выбрать одним из двух способов: Кстати, ответ (конкретное значение ошибки) будет зависеть от исходных значений весов , в том числе от того, с какой стороны минимума начнется спуск. Если, например, первоначальные веса задаются случайно, мы будем каждый раз получать разные значения ошибки через одно и то же количество эпох. Нас интересует не точное значение ошибки, а то, насколько близко к минимуму оно приблизилось. Вернемся к графику функции потерь, понаблюдаем спуск воочию. Начальная позиция, начинаем спуск: Первый шаг — движемся в сторону воронки: Через 12 эпох — глубоко внутри воронки, но не на самом дне: Через 50 эпох: Через 1767 эпох — получаем первый раз ошибку меньше, чем поиск по сетке: Это фактически и было дно, через 62000 эпох результат лучше не сильно: По шагам: Шарик неотвратимо скатывается на дно воронки. Кажется, что в этом нет ничего удивительного: вот мы видим шарик, вот видим воронку, совершенно логично шарику катиться по стенкам вниз. Однако, здесь стоит вспомнить, что шарик здесь скатывается по стенке не потому, что нам так захотелось его нарисовать, а потому, что такие шаги подбирает для него наш алгоритм, основанный на математике градиентного спуска. Нам теперь понятно, почему так происходит, но в этом все-таки заключается небольшое математическое волшебство. И еще, возможно, кто-то сейчас заметил, что на этом графике в процессе скатывания есть что-то не то: на верхней пологой части склона шарик катится очень быстро и достигает воронку за несколько шагов, после чего внутри самой воронки с почти отвесными стенками замедляется на несколько порядков и дно достигает вообще через полторы тысячи шагов, хотя, по идее, на пологой части движение должно быть медленнее, а на крутых склонах — максимально быстрое. Понятно, что у нас здесь никакая не гравитация, но, если мы вспомним геометрический смысл производной и принцип, по которому мы подбирали шаг, то все равно, на крутых склонах шаг должен быть большим, на пологих спусках — все меньше и меньше. В чем же дело? А просто вспомним, что на этом графике вертикальная шкала у нас логарифмическая. На самом деле поверхность спуска выглядит так: Короче, поверхность спуска у нас не воронка, а гамак (или подвешенное за концы провисшее одеяло). Для интуитивного понимания геометрического смысла спуска по производной лучше смотреть на этот график: чем круче наклон функции, тем быстрее спуск. Хотя воронка, конечно, живописнее. Посмотрим еще на функцию активации. С нее начинали, к ней вернулись. Начальная позиция. То, что линии ошибок неплохо укоротить, видно невооруженным глазом. Для всех синих точек активация предсказывает неправильный класс, для красных — правильный. Первый шаг — ошибки потихоньку начинают ужиматься: К 11-му шагу активация заметно уехала наверх и сменила направление наклона: двум синим точкам теперь назначает правильный класс, но для одной синей точки класс все еще неправильный; правильный класс для красных точек сохранился: 12-й шаг: классы для всех точек назначены правильно, но есть ощущение, что можно лучше: 50-я попытка: лучше, чем 12-я Шаг 1766: вот линия активации лежит уже довольно красиво. Функция ошибки на этой эпохе  — это все еще больше, чем ошибка, найденная вручную по сетке (там была : разницу видно только в 6-м знаке, причем, если до него не округлять) Шаг 1767: ошибка  — это лучше, чем ручной результат (разница в 6-м знаке, можно с округлением). Искомые коэффициенты ,  ( заметно отличается от посеточных вариантов в пределах 2-х знаков: , ) Шаг 62000: ошибка  — получше, но совсем не радикально (в 2-х знаках после запятой разницы не видно). По внешнему виду графики почти не отличить: в динамике: Как стержень на резиночках. Не удивлюсь, если реальная не слишком тяжелая, но жесткая палочка займет такое же положение, если привязать ее к таким же гвоздикам специальными короткими резиночками или пружинками, которые тянутся только по вертикали. Подытожим Результат обучения — нашли коэффициенты (при условии, что остановились на эпохе 1767): , . Возьмем объект  с заранее неизвестным классом (будем считать, что  — это объекты тестовой выборки). Но, т.к. он находится между двумя красными точками из обучающей выборки, будем ожидать, что обученный нейрон назначит ему класс , т.е. отнесет его к группе красных точек слева. взвешенная сумма   активация   квантизатор    Как видим, наши ожидания оправдались. Попробуем еще один вариант:    Это точка находилась между двумя синими точками справа и нейрон назначил ей класс , т.е. отнес ее к группе синих точек справа. Наше мнение и мнение нейрона совпало и в этот раз. Между прочим, мы с вами только что научили искусственный нейрон (так же известный как мыслящая машина класса «Перцептрон») отличать красные точки слева от синих точек справа и нам потребовалось на это всего 12 попыток. Как по мне, отличный результат! В общем, градиентный спуск, как алгоритм, мы разобрали. Посмотрим еще на вариант с двумерными объектами. Конечно, уже не так подробно, здесь просто есть еще несколько интересных картинок. Элементы выборки — точки на плоскости (две координаты). Каждый элемент принадлежит к одному из 2-х классов. Здесь могли быть длина и ширина чашелистика ириса, но это просто несколько удобных точек.   Видим, что у нас активация — это параметрическая плоскость в трёхмерном пространстве, значит мы не можем нарисовать ее на плоскости, как для 1-мерного случая, нужно переходить в 3-д: Плоскость активации с проекциями элементов: Плоскости классов с проекциями элементов: Ошибка для каждого элемента — расстояние от проекции элемента на плоскость активации до проекции этого же элемента на плоскость истинного класса: Обратите внимание на линию пересечения плоскости активации (сиреневая) и нулевой плоскости  (матово-белая). Вот она же на двумерном графике с точками элементов: В статьях, описывающих градиентный спуск или рассказывающих про нейросети, обычно рисуют такую линию и говорят, что вот ее нам нужно построить, конечно, если множество еще линейно разделимо (оно линейно разделимо, если группы элементов можно разделить вот такой линией). Это правда, но это не вся правда. На самом деле, как мы только что увидели, нам нужно построить не линию в пространстве m=2, а плоскость в пространстве (m+1)=3: пересечение плоскости активации с нулевой координатой активации и даст нам эту линию, хотя все равно одной линии не достаточно — она разделит элементы на две части, но значения классов назначает не линия, а плоскость — в зависимости от того, выше или ниже нуля оказывается проекция элемента на ней (см квантизатор).   Красивую воронку функции потерь (стоимости) мы здесь нарисовать уже не можем, т.к., как видим, у нас исходных параметра 3 + значение функции — получается 4 измерения. Кому интересно, попробуйте сделать отдельные 2-д или 3-д срезы или придумать что-нибудь с цветовым кодированием нескольких поверхностей на одном 3д-графике, добавить динамику, или воспользоваться какими-нибудь другим приемом изображения 4-мерных поверхностей в 3-д, если вы такие знаете и если они есть. И это уже для простейшего случая для элементов с 2-мя признаками. Для элементов с большим количеством признаков у впадины будет еще больше измерений, но в нее все равно получится спуститься по градиенту, как в 1-д и как в 2-д. Градиент   Частные производные (элементы градиента):       Шаги по каждому из измерений:       Спускаемся одновременно по 3-м параметрам (градиент из 3-х измерений), коэффициент обучения , начальные значения небольшие фиксированные , , . Начальная позиция — плоскость активации, зацепившись уголком за вертикальную ось на краю видимой области, свисает далеко вниз: Уже на первом шаге стремительно подлетает вверх: Крышка захлопнулась: К 3-му шагу линия пересечения активации с нулевой плоскостью элементов уже проходит плюс-минус сквозь группы элементов: На 4-м шаге она назначает неправильный класс только одному красному элементу: На 60-м шаге положение еще не поменялось — плоскость активации постепенно доворачивается, но с неправильным классом остался все тот же один красный элемент: Примерно к 70-му шагу он, наконец, тоже попал в свою группу: Шаг 200-й — все элементы по своим группам с некоторым запасом: Шаг 400-й — радикальных изменений уже нет: В динамике: Здесь интересно наблюдать, как плоскость активации стремительно подлетает наверх по всем параметрам одновременно, а потом начинает всё медленнее доворачиваться почти только по . Код Библиотека matplotlib (её расширение mpl_toolkits.mplot3d.axis3d) не умеет в пересекающиеся поверхности (все приведенные выше графики можно построить, но они будут выглядеть, как плохо читабельная каша в антураже 3д). Поэтому для построения красивых трёхмерных графиков выше мы воспользовались замечательной библиотекой Mayavi. Тот факт, что Mayavi является глючной инопланетянской поделкой, нисколько не отменяет заложенный в неё потенциал нереальной мощщи. Часы и нервы, потраченные во время её первичной установки, а потом освоения, в конечном итоге стоят того. Mayavi, в отличие от Matplotlib/axes3d, использует на 3д-сцене полноценный OpenGL. Однако, он будет работать нормально (рисовать пересекающиеся плоскости) только в том случае, если использовать с ним бэкэнд для рендера на основе Qt. По умолчанию этот бэкэнд вместе с mayavi не устанавливается и не включается. Я установил через pip пакеты PyQt5 и python-qt (возможно, еще какие-то пакеты, содержащие в имени строку 'qt'). Честно, не знаю, какой из них сработал, но в конечном итоге у меня получилось успешно запустить скрипт, корректно рисующий пересекающиеся плоскости, вот так: 12 эпох: 70 эпох: Здесь отметим, что из этих графиков уже не очень понятно, в какой момент нужно останавливаться: ошибка радикально падает уже к 6-12-й эпохе, а полностью корректная классификация всех точек обучающей выборки наступает только в районе 70-го шага — почему стоит остановиться именно на 70-м шаге, а не на 30-м, 40-м или 200-м, этот график, по крайней мере, в представленном виде, ответ не дает. Архитектура ADALINE (adaptive linear neuron — адаптивный линейный нейрон) — искусственный нейрон с линейной функцией активации на текущий момент считается устаревшей. Разработчики библиотеки scikit-learn не хотят включать алгоритм ADALINE (вариант в веб-архиве для тех, у кого роскомнадзор) в свою библиотеку, ссылаясь на то, что они предпочитают не тратить время и силы на поддержку ретро-алгоритмов «из 80-х» (ADALINE из 60-х), которые для решения современных практических задач больше никому не нужны. Автор замечательной книги «Python и машинное обучение» Себастьян Рашка (он же начал приведенное выше обсуждение в рассылке разработчиков scikit-learn) приводит свою реализацию алгоритма в бумажной книге, онлайн-версии книги и на гитхабе в образовательных целях и в качестве музейного экспоната. Но мы с вами провели всё это время с нейроном ADALINE не зря. Во-первых, линейная активация — отличная подопытная функция для изучения алгоритма градиентного спуска, который используется и с другими вариантами активаций: устарела конкретная комбинация нейрона и линейной активации, но сам нейрон и метод его обучения градиентным спуском, тем более, сам алгоритм градиентного спуска никак не устарел. Во-вторых, если мы отрежем от нашего нейрона порог (квантизатор) так, что на выходе будет не двоичная метка класса, а значение активации на непрерывной шкале, мы вместо нейрона получим алгоритм построения линейной регрессии в чистом виде (значение активации будет значением целевой переменной, да, в качестве истинных значений  тоже вместо меток взять непрерывные значения) — это вполне себе актуальный метод машинного обучения, реализация которого присутствует в том числе в библиотеке scikit-learn. P.S. У меня так сходу не получилось проследить, чем отличается нейрон ADALINE от перцептрона Розенблатта с линейной функцией активации и отличаются ли они вообще. Перцептрон Розенблатта по времени появился раньше, архитектурно, судя по описанию, они идентичны, но в статьях про ADALINE почему-то не ссылаются на Розенблатта, а ссылаются на других людей. Поэтому здесь я осторожно предполагаю, что ADALINE можно считать специальным названием для единичного перцептрона Розенблатта с линейной функцией активации. Возможно, кто-то в комментариях сможет этот момент прояснить. разнорабочий