Линейная регрессия: графический смысл, расчёт, простой пример на чистом Python
от M_N
Линейная регрессия предсказывает один признак на основе другого*, известного признака, если между ними есть линейная зависимость. Например – рост на основе веса, цена квартиры на основе площади. Линейная регрессия – один из самых простых алгоритмов машинного обучения, его работу можно увидеть наглядно:

И на примере этого алгоритма можно познакомиться с кое-какими важными темами из ML. Что ж, давайте разберёмся, как это всё работает...
Содержание> Графический смысл
> Визуальный пример
> Расчёт: предсказание
> Обучение: нужная математика
> Расчёт: обучение
> Линейная регрессия на чистом Python
Графический смысл
И ещё раз, так выглядит линейная регрессия на графике:

Что мы видим? У нас есть две оси – X и Y.
> X – независимый признак. На основе чего предсказываем.
> Y – зависимый признак. Что предсказываем.
Зелёные квадратики на сетке – данные, на которых мы обучаем модель. Какое-то количество примеров пар значений, допустим, стоимости дома и его удалённости от центра города.
Оранжевая линия – то, как модель предсказывает значения:

Возьмём какое-либо значение независимого признака и отметим его на оси X. Проведём от неё перпендикулярную линию вверх до точки пересечения с оранжевой линией. От этой точки сделаем ещё одну линию к оси Y, перпендикулярную ей. Значение точке пересечения оси Y и будет предсказанное значение.
А что значит – обучить модель линейной регрессии? Мы подбираем наклон и высоту оранжевой линии (линии регрессии) правильным образом.

За наклон и высоту отвечают два числа:
> b (Bias) – сдвиг. Двигает линию относительно оси Y с зависимым признаком.
> w (Weight) – вес. Определяет угол наклона линии (но не является им) относительно оси X с независимым признаком.
С помощью них мы приводим линию регрессии в положение, когда модель предсказывает всё максимально близко к правильному. Это происходит так:
Впрочем, компьютер понимает числа. Линии и фигурки ему безразличны. Как же расчитать линейную регрессию?
Расчёт линейной регрессииПредсказание
(К содержанию ^)
Так выглядит формула линейной регрессии:

> y под "шапочкой" – предсказанное значение. Будем его также обозначать как y_pred.
> b и w – сдвиг (Bias) и вес (Weight) соответственно.
> x – независимый признак. На основании чего мы предсказываем интересующее нас значение.
То есть для предсказания нам всего лишь надо умножить независимый признак на вес и прибавить к этому произведению сдвиг. Происходит что-то вроде:

X подтягивается до искомого значения Y умножаясь на вес w и прибавляя к себе смещение b. В итоге получается предсказанное значение y_pred, которое в реальности всегда будет немного не совпадать с соответствующим Y, потому что часто идеально линейная зависимость это условность.
Для ясности стоит сказать, случай на картинке выше – если сдвиг положительный и вес больше 1. Если, например, вес будет от 0 до 1, он, подтягивая X к Y, будет наоборот уменьшать независимый признак:

Вот и готово! Всё элементарно, нужно только найти числа b и w. Хм… Как же это сделать?
Обучение: что нужно знать из математики(К содержанию ^)
Нам придётся погрузиться в математику чуть глубже, чем просто умножение и сложение. Давайте познакомимся с тем, что нам понадобится:
Функция
Функция** – правило, превращающее одно число в другое.
**На самом деле функция ничего не превращает, а сопоставляет элементы одного множества с элементами другого множества по заданному правилу, но такие детали могут сейчас запутать.
Допустим, функция f(x) = 2*x умножает аргумент, то есть входное значение, на два, и если мы сообщим на вход этой функции число 6, мы получим 12, т.к. 2*6=12.
Производная функции
Производная – скорость изменения функции (в заданной точке).
Для нахождения производных существуют правила. Вот – те из них, которые будут нам полезны:
> d/dx(const) = 0 (или const’ = 0) – производная от константы равна нулю. Константа – это просто число, что-то, не содержащее аргумента функции. Например, производная просто от числа 6 – d/dx(6) = 0.
Можно записать производную, используя оператор d/dx, либо используя знак ‘.
> d/dx(x) = 1 – производная от аргумента равна единице. Если аргумент умножен на какое-то число, т.е. коэффициент, он сохраняется – d/dx(2x) = 2 * 1 = 2.
> d/dx(x^2) = 2x – производная от x^2 равна 2x.
Либо общий случай, производная от степени – d/dx(x^n) = nx^(n-1).
> (f +/- g)’ = f’ +/- g’ – производная от суммы/разности равна сумме/разности производных от каждого слагаемого (или вычитаемого).
Цепное правило
Цепное правило позволяет найти производную от сложной функции.

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

Сначала находим производную от (x^2 - 1)^2, не трогая то, что в скобках. Получается 2(x^2 - 1), согласно правилам из темы выше. Затем находим производную от того, что в скобках – x^2 - 1. Получится 2x - 0, т.к. 1 – константа, и в итоге выйдет просто 2x. Умножаем получившиеся производные, и получаем 4x(x^2 – 1), что можно свести до 4x^3 – 4x.
Частные производные
Теперь давайте разберёмся, что можно делать с функциями, принимающими два аргумента или более. Допустим, у нас есть функция f(x, y) = yx^2. В неё входят два аргумента (переменные) – x и y. От такой функции можно взять частную производную по x или по y. Давайте возьмём по x:

При взятии частной производной все переменные кроме той, по которой её берём, рассматриваем как коэффициенты или константы. В данном случае y рассматриваем как коэффициент. Тогда, если (x^2)’ = 2x, то в ∂/∂x(yx^2) двойка умножается на коэффициент y, и получается 2yx.
Теперь возьмём производную по y:

В качестве коэффициента теперь рассматривается x^2, а производная от y будет равняться 1, поэтому ∂/∂y(yx^2) = 1 * x^2 = x^2.
Сумма ∑
Знак суммы – ∑, называется сигма. Это что-то вроде цикла в программировании. Результат выражения после сигмы складывается установленное количество раз. Индекс внизу i = 1 (может принимать только целые числа) значит, что "цикл" начинаем читать с единицы.
Каждую итерацию число в i возрастает на единицу. Конечное значение n вверху значит, что складываем результат до тех пор, пока i не станет больше n.
Например:

Вот что будет, если после знака суммы будет стоять индекс. А вот – если поставить просто число:

Оно (3 в нашем примере) сложится 5 раз. Всё равно, что 5*3.
Обучение(К содержанию ^)
А теперь – приступим к самому механизму обучения линейной регрессии.
Функция потерь (loss-функция)
С помощью функции потерь мы оцениваем "правильность" работы модели – насколько её предсказания совпадают с действительностью. Линейная регрессия работает с непрерывными значениями, т.е. предсказывает не вероятность принадлежности к определённому классу, а просто число. Поэтому функция потерь должна определять разницу между реальным и предсказанным значением – y - y_pred.
Для линейной регрессии обычно используют функцию среднеквадратической ошибки (MSE – Mean Squared Error). Так она выглядит для единичного случая ***:

Мы просто возводим в квадрат разность между реальным (y) и предсказанным (y под "шапочкой", или y_pred) значением одного независимого признака.
При обучении нам нужно измерять ошибку для нескольких значений. Поэтому нам нужен общий случай:

Эта запись говорит о том, что сумму квадратов разностей между реальными и предсказанными значениями для каждого независимого признака (n) нужно поделить на количество этих признаков. Всё равно, что записать так:

Ошибки по сдвигу и весу
Теперь нам нужно выяснить влияние на ошибку модели параметров, которые мы обучаем – сдвига (b) и веса (w). Для этого вычислим частные производные по ним.
Сдвиг (b)
Взять производную по сдвигу (b) будет слегка проще. Взгляните на нашу функцию потерь ещё раз:

y под "шапочкой" на самом деле тоже функция – b + wx (вернее b +wx_i, поскольку независимых признаков x при обучении будет несколько). Поэтому MSE запишем так:

От неё и возьмём производную по b:

Что тут произошло? Прежде всего, 1/n не трогаем, это коэффициент, и на знак суммы не обращаем внимания тоже. Затем, выражение в скобках – сложная функция, производную от неё находим по цепному правилу. Внешняя функция – возведение в степень. Находим производную от неё, не трогая внутреннюю функцию:

Теперь находим производную внутренней функции y_i – (b + wx_i). Для удобства раскроем скобки – если перед скобками стоит минус, все знаки в скобках меняем на противоположные: y_i - (b + wx_i) = y_i - b - wx_i. И, как мы знаем, производная от суммы или разности это просто сумма/разность производных от каждого слагаемого/вычитаемого:

Поскольку производную берём по b, остальные аргументы принимаем за константы или коэффициенты. Производная от константы равна 0, а от аргумента – 1.
Умножаем производную от внешней функции на производную от внутренней, после чего b + wx_i снова превращаем в y под "шапочкой" для удобства, а коэффициенты 2 и -1 умножаем на коэффициент 1/n: -1 * 2 * 1/n = -2/n.
Вес (w)
Тут всё почти так же:

Отличие в том, как берём производную внутренней функции:

В данном случае y_i и b константы, а x_i – коэффициент, поскольку умножается на w, по которому берём производную.
Обратное распространение ошибки
А сейчас давайте раскроем один момент о сущности того, что произошло выше.
По сути, алгоритм линейной регрессии – нейрон полносвязного слоя нейросети. Во время работы в режиме предсказания мы просто получаем от него прогноз в виде непрерывного значения. Функция активации у нас линейная – (f(x) = x), а во время обучения мы используем, например, нашу loss-функцию MSE. Когда мы определяем общую ошибку модели, сначала вычисляем её предсказание, затем функцию потерь. То есть мы идём вперёд: предсказание модели (-> функция активации) -> функция потерь.
Но если нам хочется узнать, какой вклад в ошибку вносят обучаемые параметры, мы, пользуясь цепным правилом, идём в обратном направлении: функция потерь -> предсказание модели -> (функция активации ->) параметры модели (w и b). Именно это мы и сделали, вычислив частные производные. Это называется обратным распространением ошибки (backpropagation).
Градиентный спуск
Итак, вот наши производные:

Они указывают величину и направление возрастания ошибки модели по сдвигу и весу. Суть градиентного спуска в том, что мы идём против направления этой ошибки – просто вычитаем из b и w ∂/∂b и ∂/∂w. Вычисление ошибки и её коррекция обычно делается несколько раз, столько, чтобы ошибка между действительными и предсказанными значениями стала минимальной:

Одно обновление параметров модели для всех обучающих данных называется эпохой.
Производные также умножают на коэффициент η, learning rate – число от 0 до 1, которое корректирует шаг обучения, делая его меньше. Таким образом, в процессе обучения предсказанные значения приближаются к реальным более аккуратно, и не перепрыгнут минимум.
Хм… А где тут градиент и что он вообще такое?
Градиент – это вектор из частных производных по параметрам, которые мы обучаем. Вектор – объект из линейной алгебры. Представьте его как просто последовательность (список) чисел, например [1, 2]. Для нашей модели линейной регрессии градиент будет выглядеть так:
∇L = [∂L/∂b, ∂L/∂w]
Он указывает направление наискорейшего возрастания функции.
Линейная регрессия на чистом Python(К содержанию ^)
Что ж! Зная, как работает линейная регрессия, сделаем что-то практичное. Напишем простую реализацию линейной регрессии на Python. Да, всегда можно просто вызвать метод .fit() модели из, скажем, scikit-learn, и это будет очень правильно – в таких библиотеках всё сделано на низком уровне и работает гораздо быстрее, чем чистый Python. Но если мы хотим рассмотреть работу алгоритма детально, на простом наглядном примере, давайте пройдём чуть дальше метода .fit().
Посмотрим сразу на весь код:
Сначала делаем тестовые данные в виде списков – x_data и соответствующие y_data.
Затем объявляем нужные переменные. В w и b значения веса и сдвига, которые мы будем обучать. Сделаем им начальные значения 0.0. Используем float тип, при вычислениях будут возникать дробные числа.
n – количество отдельных значений, на которых будет обучаться модель. То самое конечное значение n сверху над сигмой (знаком суммы). С помощью len() мы получаем количество элементов в списке, целое число, и присваиваем его нашему конечному значению.
learning_rate и epochs – нужные нам гиперпараметры. Как было написано выше, learning_rate – число от 0 до 1, которое уменьшает шаг обучения. epochs – количество эпох, одного шага корректировки параметров по всем данным.
Далее обрабатываем сам алгоритм. Нам понадобится вложенный цикл, где внешний цикл for _ in range(epochs) будет чередовать эпохи, а внутренний for i in range(n) вычислять и складывать то, что написано после знака суммы. Во внешнем цикле нижнее подчеркивание _ – что-то вроде заглушки. Переменная на месте _ нам не понадобится, но удобнее будет пользоваться циклом for.
Внутри внешнего цикла объявляем переменные dw и db, которые будут нам нужны только в пределах одной итерации и куда будут записываться значения производных по w и b.
Во внутреннем цикле достаём нужные значения x_i и y_i для текущей его итерации из созданных нами данных. Затем предсказываем значение с текущими параметрами и записываем его в y_pred (то есть y под "шапочкой"), используя формулу линейной регрессии – b + w * x_i.
В dw и db вычисляем соответствующие частные производные – только ту часть, которая после знака суммы – складываем с предыдущим их значением (+= вместо присваивания =) и записываем их.
Далее, снова во внешнем цикле, умножаем полученную сумму в dw и db на коэффициент -2/n.
И последний шаг – делаем градиентный спуск. Отнимаем от w и b значения полученных частных производных (поскольку надо идти в противоположную сторону возрастания ошибки), умноженных на learning_rate.
После того, как весь алгоритм отработал, можно посмотреть значения веса и сдвига, и предсказать моделью какое-то значение:
Итоги
Линейная регрессия – довольно простая штука. Скажем, не то, чтобы для понимания её работы нужно знать какую-то сложную математику, хотя ей и пугают тех, кто хочет разобраться в машинном обучении. Для того, чтобы вычислить предсказание модели нужны знания, ну, где-то класса до 3-го. Умножить и прибавить. Чтобы обучить линейную регрессию, нужно знать ещё несколько фокусов с производными.
Эта же простота – преимущество. Линейная регрессия не требует много вычислительных ресурсов и её результаты легко интерпретировать. Поэтому она – один из самых популярных алгоритмов в ML.
Всем спасибо за внимание!
* В реальности независимых признаков бывает несколько. Значения независимых признаков (x) для каждого зависимого (w) и соответствующие веса объединяются в векторы, и при предсказании одного зависимого признака мы делаем скалярное произведение вектора x с вектором w, складывая его со смещением: x_1 * w_1 + x_2 + w_2 +x_3 * w_3 ... + b.
*** Иногда коэффициент функции потерь умножают на 1/2, чтобы после взятия производных двойки в числителе и значенателе сократились, и получилось просто 1/n, а не 2/n. Делается это просто ради более эстетического вида.

И на примере этого алгоритма можно познакомиться с кое-какими важными темами из ML. Что ж, давайте разберёмся, как это всё работает...
Содержание> Графический смысл
> Визуальный пример
> Расчёт: предсказание
> Обучение: нужная математика
> Расчёт: обучение
> Линейная регрессия на чистом Python
Графический смысл
И ещё раз, так выглядит линейная регрессия на графике:

Что мы видим? У нас есть две оси – X и Y.
> X – независимый признак. На основе чего предсказываем.
> Y – зависимый признак. Что предсказываем.
Зелёные квадратики на сетке – данные, на которых мы обучаем модель. Какое-то количество примеров пар значений, допустим, стоимости дома и его удалённости от центра города.
Оранжевая линия – то, как модель предсказывает значения:

Возьмём какое-либо значение независимого признака и отметим его на оси X. Проведём от неё перпендикулярную линию вверх до точки пересечения с оранжевой линией. От этой точки сделаем ещё одну линию к оси Y, перпендикулярную ей. Значение точке пересечения оси Y и будет предсказанное значение.
А что значит – обучить модель линейной регрессии? Мы подбираем наклон и высоту оранжевой линии (линии регрессии) правильным образом.

За наклон и высоту отвечают два числа:
> b (Bias) – сдвиг. Двигает линию относительно оси Y с зависимым признаком.
> w (Weight) – вес. Определяет угол наклона линии (но не является им) относительно оси X с независимым признаком.
С помощью них мы приводим линию регрессии в положение, когда модель предсказывает всё максимально близко к правильному. Это происходит так:
Впрочем, компьютер понимает числа. Линии и фигурки ему безразличны. Как же расчитать линейную регрессию?
Расчёт линейной регрессииПредсказание
(К содержанию ^)
Так выглядит формула линейной регрессии:

> y под "шапочкой" – предсказанное значение. Будем его также обозначать как y_pred.
> b и w – сдвиг (Bias) и вес (Weight) соответственно.
> x – независимый признак. На основании чего мы предсказываем интересующее нас значение.
То есть для предсказания нам всего лишь надо умножить независимый признак на вес и прибавить к этому произведению сдвиг. Происходит что-то вроде:

X подтягивается до искомого значения Y умножаясь на вес w и прибавляя к себе смещение b. В итоге получается предсказанное значение y_pred, которое в реальности всегда будет немного не совпадать с соответствующим Y, потому что часто идеально линейная зависимость это условность.
Для ясности стоит сказать, случай на картинке выше – если сдвиг положительный и вес больше 1. Если, например, вес будет от 0 до 1, он, подтягивая X к Y, будет наоборот уменьшать независимый признак:

Вот и готово! Всё элементарно, нужно только найти числа b и w. Хм… Как же это сделать?
Обучение: что нужно знать из математики(К содержанию ^)
Нам придётся погрузиться в математику чуть глубже, чем просто умножение и сложение. Давайте познакомимся с тем, что нам понадобится:
Функция
Функция** – правило, превращающее одно число в другое.
**На самом деле функция ничего не превращает, а сопоставляет элементы одного множества с элементами другого множества по заданному правилу, но такие детали могут сейчас запутать.
Допустим, функция f(x) = 2*x умножает аргумент, то есть входное значение, на два, и если мы сообщим на вход этой функции число 6, мы получим 12, т.к. 2*6=12.
Производная функции
Производная – скорость изменения функции (в заданной точке).
Для нахождения производных существуют правила. Вот – те из них, которые будут нам полезны:
> d/dx(const) = 0 (или const’ = 0) – производная от константы равна нулю. Константа – это просто число, что-то, не содержащее аргумента функции. Например, производная просто от числа 6 – d/dx(6) = 0.
Можно записать производную, используя оператор d/dx, либо используя знак ‘.
> d/dx(x) = 1 – производная от аргумента равна единице. Если аргумент умножен на какое-то число, т.е. коэффициент, он сохраняется – d/dx(2x) = 2 * 1 = 2.
> d/dx(x^2) = 2x – производная от x^2 равна 2x.
Либо общий случай, производная от степени – d/dx(x^n) = nx^(n-1).
> (f +/- g)’ = f’ +/- g’ – производная от суммы/разности равна сумме/разности производных от каждого слагаемого (или вычитаемого).
Цепное правило
Цепное правило позволяет найти производную от сложной функции.

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

Сначала находим производную от (x^2 - 1)^2, не трогая то, что в скобках. Получается 2(x^2 - 1), согласно правилам из темы выше. Затем находим производную от того, что в скобках – x^2 - 1. Получится 2x - 0, т.к. 1 – константа, и в итоге выйдет просто 2x. Умножаем получившиеся производные, и получаем 4x(x^2 – 1), что можно свести до 4x^3 – 4x.
Частные производные
Теперь давайте разберёмся, что можно делать с функциями, принимающими два аргумента или более. Допустим, у нас есть функция f(x, y) = yx^2. В неё входят два аргумента (переменные) – x и y. От такой функции можно взять частную производную по x или по y. Давайте возьмём по x:

При взятии частной производной все переменные кроме той, по которой её берём, рассматриваем как коэффициенты или константы. В данном случае y рассматриваем как коэффициент. Тогда, если (x^2)’ = 2x, то в ∂/∂x(yx^2) двойка умножается на коэффициент y, и получается 2yx.
Теперь возьмём производную по y:

В качестве коэффициента теперь рассматривается x^2, а производная от y будет равняться 1, поэтому ∂/∂y(yx^2) = 1 * x^2 = x^2.
Сумма ∑
Знак суммы – ∑, называется сигма. Это что-то вроде цикла в программировании. Результат выражения после сигмы складывается установленное количество раз. Индекс внизу i = 1 (может принимать только целые числа) значит, что "цикл" начинаем читать с единицы.
Каждую итерацию число в i возрастает на единицу. Конечное значение n вверху значит, что складываем результат до тех пор, пока i не станет больше n.
Например:

Вот что будет, если после знака суммы будет стоять индекс. А вот – если поставить просто число:

Оно (3 в нашем примере) сложится 5 раз. Всё равно, что 5*3.
Обучение(К содержанию ^)
А теперь – приступим к самому механизму обучения линейной регрессии.
Функция потерь (loss-функция)
С помощью функции потерь мы оцениваем "правильность" работы модели – насколько её предсказания совпадают с действительностью. Линейная регрессия работает с непрерывными значениями, т.е. предсказывает не вероятность принадлежности к определённому классу, а просто число. Поэтому функция потерь должна определять разницу между реальным и предсказанным значением – y - y_pred.
Для линейной регрессии обычно используют функцию среднеквадратической ошибки (MSE – Mean Squared Error). Так она выглядит для единичного случая ***:

Мы просто возводим в квадрат разность между реальным (y) и предсказанным (y под "шапочкой", или y_pred) значением одного независимого признака.
При обучении нам нужно измерять ошибку для нескольких значений. Поэтому нам нужен общий случай:

Эта запись говорит о том, что сумму квадратов разностей между реальными и предсказанными значениями для каждого независимого признака (n) нужно поделить на количество этих признаков. Всё равно, что записать так:

Ошибки по сдвигу и весу
Теперь нам нужно выяснить влияние на ошибку модели параметров, которые мы обучаем – сдвига (b) и веса (w). Для этого вычислим частные производные по ним.
Сдвиг (b)
Взять производную по сдвигу (b) будет слегка проще. Взгляните на нашу функцию потерь ещё раз:

y под "шапочкой" на самом деле тоже функция – b + wx (вернее b +wx_i, поскольку независимых признаков x при обучении будет несколько). Поэтому MSE запишем так:

От неё и возьмём производную по b:

Что тут произошло? Прежде всего, 1/n не трогаем, это коэффициент, и на знак суммы не обращаем внимания тоже. Затем, выражение в скобках – сложная функция, производную от неё находим по цепному правилу. Внешняя функция – возведение в степень. Находим производную от неё, не трогая внутреннюю функцию:

Теперь находим производную внутренней функции y_i – (b + wx_i). Для удобства раскроем скобки – если перед скобками стоит минус, все знаки в скобках меняем на противоположные: y_i - (b + wx_i) = y_i - b - wx_i. И, как мы знаем, производная от суммы или разности это просто сумма/разность производных от каждого слагаемого/вычитаемого:

Поскольку производную берём по b, остальные аргументы принимаем за константы или коэффициенты. Производная от константы равна 0, а от аргумента – 1.
Умножаем производную от внешней функции на производную от внутренней, после чего b + wx_i снова превращаем в y под "шапочкой" для удобства, а коэффициенты 2 и -1 умножаем на коэффициент 1/n: -1 * 2 * 1/n = -2/n.
Вес (w)
Тут всё почти так же:

Отличие в том, как берём производную внутренней функции:

В данном случае y_i и b константы, а x_i – коэффициент, поскольку умножается на w, по которому берём производную.
Обратное распространение ошибки
А сейчас давайте раскроем один момент о сущности того, что произошло выше.
По сути, алгоритм линейной регрессии – нейрон полносвязного слоя нейросети. Во время работы в режиме предсказания мы просто получаем от него прогноз в виде непрерывного значения. Функция активации у нас линейная – (f(x) = x), а во время обучения мы используем, например, нашу loss-функцию MSE. Когда мы определяем общую ошибку модели, сначала вычисляем её предсказание, затем функцию потерь. То есть мы идём вперёд: предсказание модели (-> функция активации) -> функция потерь.
Но если нам хочется узнать, какой вклад в ошибку вносят обучаемые параметры, мы, пользуясь цепным правилом, идём в обратном направлении: функция потерь -> предсказание модели -> (функция активации ->) параметры модели (w и b). Именно это мы и сделали, вычислив частные производные. Это называется обратным распространением ошибки (backpropagation).
Градиентный спуск
Итак, вот наши производные:

Они указывают величину и направление возрастания ошибки модели по сдвигу и весу. Суть градиентного спуска в том, что мы идём против направления этой ошибки – просто вычитаем из b и w ∂/∂b и ∂/∂w. Вычисление ошибки и её коррекция обычно делается несколько раз, столько, чтобы ошибка между действительными и предсказанными значениями стала минимальной:

Одно обновление параметров модели для всех обучающих данных называется эпохой.
Производные также умножают на коэффициент η, learning rate – число от 0 до 1, которое корректирует шаг обучения, делая его меньше. Таким образом, в процессе обучения предсказанные значения приближаются к реальным более аккуратно, и не перепрыгнут минимум.
Хм… А где тут градиент и что он вообще такое?
Градиент – это вектор из частных производных по параметрам, которые мы обучаем. Вектор – объект из линейной алгебры. Представьте его как просто последовательность (список) чисел, например [1, 2]. Для нашей модели линейной регрессии градиент будет выглядеть так:
∇L = [∂L/∂b, ∂L/∂w]
Он указывает направление наискорейшего возрастания функции.
Линейная регрессия на чистом Python(К содержанию ^)
Что ж! Зная, как работает линейная регрессия, сделаем что-то практичное. Напишем простую реализацию линейной регрессии на Python. Да, всегда можно просто вызвать метод .fit() модели из, скажем, scikit-learn, и это будет очень правильно – в таких библиотеках всё сделано на низком уровне и работает гораздо быстрее, чем чистый Python. Но если мы хотим рассмотреть работу алгоритма детально, на простом наглядном примере, давайте пройдём чуть дальше метода .fit().
Посмотрим сразу на весь код:
- x_data = [1,2,3,4,5]
- y_data = [2,4,6,4,5]
- w = 0.0
- b = 0.0
- n = len(x_data)
- learning_rate = 0.01
- epochs = 1000
- for _ in range(epochs):
- dw = 0.0
- db = 0.0
- for i in range(n):
- x_i = x_data[i]
- y_i = y_data[i]
- y_pred = b + w * x_i
- dw += x_i * (y_i - y_pred)
- db += y_i - y_pred
- dw = (-2/n) * dw
- db = (-2/n) * db
- w -= dw * learning_rate
- b -= db * learning_rate
- #Смотрим результат
- print(f'Weight: {w:.6}')
- print(f'Bias: {b:.6}')
- print(f'Predicting: for X 3.5 Y is {b + 3.5 * w:.6}')
Сначала делаем тестовые данные в виде списков – x_data и соответствующие y_data.
- x_data = [1,2,3,4,5]
- y_data = [2,4,6,4,5]
Затем объявляем нужные переменные. В w и b значения веса и сдвига, которые мы будем обучать. Сделаем им начальные значения 0.0. Используем float тип, при вычислениях будут возникать дробные числа.
n – количество отдельных значений, на которых будет обучаться модель. То самое конечное значение n сверху над сигмой (знаком суммы). С помощью len() мы получаем количество элементов в списке, целое число, и присваиваем его нашему конечному значению.
- w = 0.0
- b = 0.0
- n = len(x_data)
learning_rate и epochs – нужные нам гиперпараметры. Как было написано выше, learning_rate – число от 0 до 1, которое уменьшает шаг обучения. epochs – количество эпох, одного шага корректировки параметров по всем данным.
- learning_rate = 0.01
- epochs = 1000
Далее обрабатываем сам алгоритм. Нам понадобится вложенный цикл, где внешний цикл for _ in range(epochs) будет чередовать эпохи, а внутренний for i in range(n) вычислять и складывать то, что написано после знака суммы. Во внешнем цикле нижнее подчеркивание _ – что-то вроде заглушки. Переменная на месте _ нам не понадобится, но удобнее будет пользоваться циклом for.
Внутри внешнего цикла объявляем переменные dw и db, которые будут нам нужны только в пределах одной итерации и куда будут записываться значения производных по w и b.
- dw = 0.0
- db = 0.0
Во внутреннем цикле достаём нужные значения x_i и y_i для текущей его итерации из созданных нами данных. Затем предсказываем значение с текущими параметрами и записываем его в y_pred (то есть y под "шапочкой"), используя формулу линейной регрессии – b + w * x_i.
- x_i = x_data[i]
- y_i = y_data[i]
- y_pred = b + w * x_i
В dw и db вычисляем соответствующие частные производные – только ту часть, которая после знака суммы – складываем с предыдущим их значением (+= вместо присваивания =) и записываем их.
- dw += x_i * (y_i - y_pred)
- db += y_i - y_pred
Далее, снова во внешнем цикле, умножаем полученную сумму в dw и db на коэффициент -2/n.
- dw = (-2/n) * dw
- db = (-2/n) * db
И последний шаг – делаем градиентный спуск. Отнимаем от w и b значения полученных частных производных (поскольку надо идти в противоположную сторону возрастания ошибки), умноженных на learning_rate.
- w -= dw * learning_rate
- b -= db * learning_rate
После того, как весь алгоритм отработал, можно посмотреть значения веса и сдвига, и предсказать моделью какое-то значение:
- print(f'Weight: {w:.6}')
- print(f'Bias: {b:.6}')
- print(f'Predicting: for X 3.5 Y is {b + 3.5 * w:.6}')
Итоги
Линейная регрессия – довольно простая штука. Скажем, не то, чтобы для понимания её работы нужно знать какую-то сложную математику, хотя ей и пугают тех, кто хочет разобраться в машинном обучении. Для того, чтобы вычислить предсказание модели нужны знания, ну, где-то класса до 3-го. Умножить и прибавить. Чтобы обучить линейную регрессию, нужно знать ещё несколько фокусов с производными.
Эта же простота – преимущество. Линейная регрессия не требует много вычислительных ресурсов и её результаты легко интерпретировать. Поэтому она – один из самых популярных алгоритмов в ML.
Всем спасибо за внимание!
* В реальности независимых признаков бывает несколько. Значения независимых признаков (x) для каждого зависимого (w) и соответствующие веса объединяются в векторы, и при предсказании одного зависимого признака мы делаем скалярное произведение вектора x с вектором w, складывая его со смещением: x_1 * w_1 + x_2 + w_2 +x_3 * w_3 ... + b.
*** Иногда коэффициент функции потерь умножают на 1/2, чтобы после взятия производных двойки в числителе и значенателе сократились, и получилось просто 1/n, а не 2/n. Делается это просто ради более эстетического вида.

