Линейная регрессия: графический смысл, расчёт, простой пример на чистом Python

от
Прочее    линейная регрессия, машинное обучение, ml, расчёт линейной регрессии, python

Линейная регрессия предсказывает один признак на основе другого*, известного признака, если между ними есть линейная зависимость. Например – рост на основе веса, цена квартиры на основе площади. Линейная регрессия – один из самых простых алгоритмов машинного обучения, его работу можно увидеть наглядно:
Линейная регрессия на графике
И на примере этого алгоритма можно познакомиться с кое-какими важными темами из 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) – производная от константы равна нулю. Константа – это просто число, что-то, не содержащее аргумента функции. Например, производная просто от числа 6d/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:
производная по x
При взятии частной производной все переменные кроме той, по которой её берём, рассматриваем как коэффициенты или константы. В данном случае y рассматриваем как коэффициент. Тогда, если (x^2)’ = 2x, то в ∂/∂x(yx^2) двойка умножается на коэффициент y, и получается 2yx.

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

Сумма ∑

Знак суммы – , называется сигма. Это что-то вроде цикла в программировании. Результат выражения после сигмы складывается установленное количество раз. Индекс внизу i = 1 (может принимать только целые числа) значит, что "цикл" начинаем читать с единицы.

Каждую итерацию число в i возрастает на единицу. Конечное значение n вверху значит, что складываем результат до тех пор, пока i не станет больше n.

Например:
сумма i
Вот что будет, если после знака суммы будет стоять индекс. А вот – если поставить просто число:
сумма
Оно (3 в нашем примере) сложится 5 раз. Всё равно, что 5*3.

Обучение(К содержанию ^)
А теперь – приступим к самому механизму обучения линейной регрессии.

Функция потерь (loss-функция)

С помощью функции потерь мы оцениваем "правильность" работы модели – насколько её предсказания совпадают с действительностью. Линейная регрессия работает с непрерывными значениями, т.е. предсказывает не вероятность принадлежности к определённому классу, а просто число. Поэтому функция потерь должна определять разницу между реальным и предсказанным значением – y - y_pred.

Для линейной регрессии обычно используют функцию среднеквадратической ошибки (MSE – Mean Squared Error). Так она выглядит для единичного случая ***:
MSE (среднеквадратичная ошибка) для одного значения
Мы просто возводим в квадрат разность между реальным (y) и предсказанным (y под "шапочкой", или y_pred) значением одного независимого признака.

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

Теперь нам нужно выяснить влияние на ошибку модели параметров, которые мы обучаем – сдвига (b) и веса (w). Для этого вычислим частные производные по ним.

Сдвиг (b)

Взять производную по сдвигу (b) будет слегка проще. Взгляните на нашу функцию потерь ещё раз:
MSE (среднеквадратическая ошибка) – общий случай
y под "шапочкой" на самом деле тоже функция – b + wx (вернее b +wx_i, поскольку независимых признаков x при обучении будет несколько). Поэтому MSE запишем так:
Раскрытие y_pred (формулы линейной регрессии) в MSE
От неё и возьмём производную по b:
нахождение производной по сдвигу (b)
Что тут произошло? Прежде всего, 1/n не трогаем, это коэффициент, и на знак суммы не обращаем внимания тоже. Затем, выражение в скобках – сложная функция, производную от неё находим по цепному правилу. Внешняя функция – возведение в степень. Находим производную от неё, не трогая внутреннюю функцию:
внешняя функция
Теперь находим производную внутренней функции y_i – (b + wx_i). Для удобства раскроем скобки – если перед скобками стоит минус, все знаки в скобках меняем на противоположные: y_i - (b + wx_i) = y_i - b - wx_i. И, как мы знаем, производная от суммы или разности это просто сумма/разность производных от каждого слагаемого/вычитаемого:
d_db_inner_der.jpg
Поскольку производную берём по b, остальные аргументы принимаем за константы или коэффициенты. Производная от константы равна 0, а от аргумента – 1.

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

Вес (w)

Тут всё почти так же:
нахождение производной по весу (w)
Отличие в том, как берём производную внутренней функции:
d_dw_inner_der.jpg
В данном случае y_i и b константы, а x_i – коэффициент, поскольку умножается на w, по которому берём производную.

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

А сейчас давайте раскроем один момент о сущности того, что произошло выше.

По сути, алгоритм линейной регрессии – нейрон полносвязного слоя нейросети. Во время работы в режиме предсказания мы просто получаем от него прогноз в виде непрерывного значения. Функция активации у нас линейная – (f(x) = x), а во время обучения мы используем, например, нашу loss-функцию MSE. Когда мы определяем общую ошибку модели, сначала вычисляем её предсказание, затем функцию потерь. То есть мы идём вперёд: предсказание модели (-> функция активации) -> функция потерь.

Но если нам хочется узнать, какой вклад в ошибку вносят обучаемые параметры, мы, пользуясь цепным правилом, идём в обратном направлении: функция потерь -> предсказание модели -> (функция активации ->) параметры модели (w и b). Именно это мы и сделали, вычислив частные производные. Это называется обратным распространением ошибки (backpropagation).

Градиентный спуск

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

Производные также умножают на коэффициент η, learning rate – число от 0 до 1, которое корректирует шаг обучения, делая его меньше. Таким образом, в процессе обучения предсказанные значения приближаются к реальным более аккуратно, и не перепрыгнут минимум.

Хм… А где тут градиент и что он вообще такое?

Градиент – это вектор из частных производных по параметрам, которые мы обучаем. Вектор – объект из линейной алгебры. Представьте его как просто последовательность (список) чисел, например [1, 2]. Для нашей модели линейной регрессии градиент будет выглядеть так:

∇L = [∂L/∂b, ∂L/∂w]

Он указывает направление наискорейшего возрастания функции.

Линейная регрессия на чистом Python(К содержанию ^)
Что ж! Зная, как работает линейная регрессия, сделаем что-то практичное. Напишем простую реализацию линейной регрессии на Python. Да, всегда можно просто вызвать метод .fit() модели из, скажем, scikit-learn, и это будет очень правильно – в таких библиотеках всё сделано на низком уровне и работает гораздо быстрее, чем чистый Python. Но если мы хотим рассмотреть работу алгоритма детально, на простом наглядном примере, давайте пройдём чуть дальше метода .fit().

Посмотрим сразу на весь код:

  1. x_data = [1,2,3,4,5]
  2. y_data = [2,4,6,4,5]
  3.  
  4. w = 0.0
  5. b = 0.0
  6. n = len(x_data)
  7.  
  8. learning_rate = 0.01
  9. epochs = 1000
  10.  
  11. for _ in range(epochs):
  12.     dw = 0.0
  13.     db = 0.0
  14.  
  15.     for i in range(n):
  16.         x_i = x_data[i]
  17.         y_i = y_data[i]
  18.         y_pred = b + w * x_i
  19.  
  20.         dw += x_i * (y_i - y_pred)
  21.         db += y_i - y_pred
  22.  
  23.     dw = (-2/n) * dw
  24.     db = (-2/n) * db
  25.  
  26.     w -= dw * learning_rate
  27.     b -= db * learning_rate
  28.  
  29. #Смотрим результат
  30.  
  31. print(f'Weight: {w:.6}')
  32. print(f'Bias: {b:.6}')
  33. print(f'Predicting: for X 3.5 Y is {b + 3.5 * w:.6}')

Сначала делаем тестовые данные в виде списков – x_data и соответствующие y_data.

  1. x_data = [1,2,3,4,5]
  2. y_data = [2,4,6,4,5]

Затем объявляем нужные переменные. В w и b значения веса и сдвига, которые мы будем обучать. Сделаем им начальные значения 0.0. Используем float тип, при вычислениях будут возникать дробные числа.

n – количество отдельных значений, на которых будет обучаться модель. То самое конечное значение n сверху над сигмой (знаком суммы). С помощью len() мы получаем количество элементов в списке, целое число, и присваиваем его нашему конечному значению.

  1. w = 0.0
  2. b = 0.0
  3. n = len(x_data)

learning_rate и epochs – нужные нам гиперпараметры. Как было написано выше, learning_rate – число от 0 до 1, которое уменьшает шаг обучения. epochs – количество эпох, одного шага корректировки параметров по всем данным.

  1. learning_rate = 0.01
  2. epochs = 1000

Далее обрабатываем сам алгоритм. Нам понадобится вложенный цикл, где внешний цикл for _ in range(epochs) будет чередовать эпохи, а внутренний for i in range(n) вычислять и складывать то, что написано после знака суммы. Во внешнем цикле нижнее подчеркивание _ – что-то вроде заглушки. Переменная на месте _ нам не понадобится, но удобнее будет пользоваться циклом for.

Внутри внешнего цикла объявляем переменные dw и db, которые будут нам нужны только в пределах одной итерации и куда будут записываться значения производных по w и b.

  1. dw = 0.0
  2. db = 0.0

Во внутреннем цикле достаём нужные значения x_i и y_i для текущей его итерации из созданных нами данных. Затем предсказываем значение с текущими параметрами и записываем его в y_pred (то есть y под "шапочкой"), используя формулу линейной регрессии – b + w * x_i.

  1. x_i = x_data[i]
  2. y_i = y_data[i]
  3. y_pred = b + w * x_i

В dw и db вычисляем соответствующие частные производные – только ту часть, которая после знака суммы – складываем с предыдущим их значением (+= вместо присваивания =) и записываем их.

  1. dw += x_i * (y_i - y_pred)
  2. db += y_i - y_pred

Далее, снова во внешнем цикле, умножаем полученную сумму в dw и db на коэффициент -2/n.

  1. dw = (-2/n) * dw
  2. db = (-2/n) * db

И последний шаг – делаем градиентный спуск. Отнимаем от w и b значения полученных частных производных (поскольку надо идти в противоположную сторону возрастания ошибки), умноженных на learning_rate.

  1. w -= dw * learning_rate
  2. b -= db * learning_rate

После того, как весь алгоритм отработал, можно посмотреть значения веса и сдвига, и предсказать моделью какое-то значение:

  1. print(f'Weight: {w:.6}')
  2. print(f'Bias: {b:.6}')
  3. 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. Делается это просто ради более эстетического вида.
  • +1
  • views 33