Решаване на просто уравнение на линейна регресия

Статията обсъжда няколко начина за определяне на математическото уравнение на проста (двойка) регресионна линия.

Всички методи за решаване на разглежданото тук уравнение се основават на метода на най-малките квадрати. Ние обозначаваме методите, както следва:

  • Аналитично решение
  • градиентно спускане
  • Стохастичен градиентен спускане

За всеки от начините за решаване на уравнението на права линия статията представя различни функции, които са разделени главно на тези, които са написани без използване на библиотеката numpy и тези, които се използват за изчисления numpy. Смята се, че умелото използване numpy ще намали цената на изчисленията.

Целият код в тази статия е написан на python 2.7 с Джупиър Бележник. Изходният код и примерният файл с данни са достъпни на Github

Статията е по-фокусирана както върху начинаещите, така и върху тези, които вече постепенно са започнали да овладяват изучаването на много обширен раздел в изкуствения интелект - машинното обучение.

Нека използваме един много прост пример, за да илюстрираме материала.

Примерни условия

Имаме пет ценности, които характеризират пристрастяването Y от X (Маса 1):

Таблица № 1 "Условия на примера"

Решаване на просто уравнение на линейна регресия

Ще приемем, че стойностите Решаване на просто уравнение на линейна регресия е месецът от годината и Решаване на просто уравнение на линейна регресия - печалба този месец. С други думи, приходите зависят от месеца в годината и Решаване на просто уравнение на линейна регресия - единственият знак, от който зависят приходите.

Примерът е така, както по отношение на условната зависимост на приходите от месеца на годината, така и по отношение на броя на стойностите - има много малко от тях. Подобно опростяване обаче ще позволи, както се казва на пръстите, да обясни, не винаги с лекота, материала, усвоен от начинаещи. Освен това простотата на числата ще позволи на тези, които желаят да решат примера на „хартия“ без значителни разходи за труд.

Да предположим, че зависимостта, дадена в примера, може да бъде апроксимирана доста добре чрез математическото уравнение на линията на проста (двойка) регресия на формата:

Решаване на просто уравнение на линейна регресия

където Решаване на просто уравнение на линейна регресия е месецът, през който са получени приходите, Решаване на просто уравнение на линейна регресия - приходи, съответстващи на месеца, Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия са регресионните коефициенти на оценената линия.

Имайте предвид, че коефициентът Решаване на просто уравнение на линейна регресия често се нарича наклон или градиент на прогнозната линия; е сумата, с която Решаване на просто уравнение на линейна регресия когато се промени Решаване на просто уравнение на линейна регресия.

Очевидно нашата задача в примера е да изберем такива коефициенти в уравнението Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия, при което отклоненията на прогнозните ни стойности на приходите по месеци от верните отговори, т.е. стойностите, представени в извадката, ще бъдат минимални.

Метод на най-малките квадрати

Според метода на най-малките квадрати отклонението трябва да се изчисли чрез повдигане на квадрат. Такава техника позволява да се избегне взаимното изплащане на отклонения, ако те имат противоположни знаци. Например, ако в един случай, отклонението е +5 (плюс пет), а в другата -5 (минус пет), тогава сумата на отклоненията ще се анулира взаимно и ще бъде 0 (нула). Възможно е отклонението да не се квадратизира, а да се използва свойството модул и тогава всички отклонения да са положителни и да се натрупват. Няма да се спираме подробно на тази точка, а просто посочваме, че за удобство на изчисленията е обичайно отклонението да се квадратира.

Ето как изглежда формулата, с помощта на която ще определим най-малката сума на квадратите на отклонения (грешки):

Решаване на просто уравнение на линейна регресия

където Решаване на просто уравнение на линейна регресия е функция на приближаване на верните отговори (т.е. приходите, изчислени от нас),

Решаване на просто уравнение на линейна регресия са верните отговори (приходите, предоставени в извадката),

Решаване на просто уравнение на линейна регресия е индексът на извадката (номерът на месеца, в който е определено отклонението)

Нека диференцираме функцията, дефинираме частичните диференциални уравнения и сме готови да преминем към аналитично решение. Но първо, нека направим кратко отклонение относно това какво е диференциране и да си припомним геометричното значение на производната.

Диференциация

Диференцирането е операция за намиране на производната на функция.

За какво е производната? Производната на функция характеризира скоростта на изменение на функцията и ни показва нейната посока. Ако производната в дадена точка е положителна, тогава функцията е нарастваща; в противен случай функцията е намаляваща. И колкото по-голяма е стойността на производната по модул, толкова по-висока е скоростта на промяна на стойностите на функцията, както и толкова по-стръмен е наклонът на графиката на функцията.

Например при условията на декартова координатна система стойността на производната в точка M(0,0) е равна на +25 означава, че в дадена точка, когато стойността е изместена Решаване на просто уравнение на линейна регресия вдясно с условна единица, стойност Решаване на просто уравнение на линейна регресия се увеличава с 25 условни единици. На графиката изглежда като доста стръмен ъгъл на покачване на стойностите Решаване на просто уравнение на линейна регресия от дадена точка.

Друг пример. Стойността на производната е -0,1 означава, че при превключване Решаване на просто уравнение на линейна регресия за една условна единица, стойността Решаване на просто уравнение на линейна регресия намалява само с 0,1 условна единица. В същото време на графиката на функцията можем да наблюдаваме едва забележим наклон надолу. Правейки аналогия с планина, все едно много бавно се спускаме по лек склон от планина, за разлика от предишния пример, където трябваше да изкачваме много стръмни върхове :)

Така след обособяване на функцията Решаване на просто уравнение на линейна регресия по коефициенти Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия, дефинираме уравненията на частни производни от 1-ви ред. След като дефинираме уравненията, ще получим система от две уравнения, решавайки които можем да изберем такива стойности на коефициентите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия, при което стойностите на съответните производни в дадени точки се променят с много, много малка сума, а в случай на аналитично решение не се променят изобщо. С други думи, функцията на грешката при намерените коефициенти ще достигне минимум, тъй като стойностите на частичните производни в тези точки ще бъдат равни на нула.

И така, съгласно правилата за диференциране, уравнението на частната производна от 1-ви ред по отношение на коефициента Решаване на просто уравнение на линейна регресия ще приеме формата:

Решаване на просто уравнение на линейна регресия

Уравнение с частна производна от 1-ви ред по отношение на Решаване на просто уравнение на линейна регресия ще приеме формата:

Решаване на просто уравнение на линейна регресия

В резултат на това получихме система от уравнения, която има доста просто аналитично решение:

начало{уравнение*}
начало{случаи}
na + bsumlimits_{i=1}^nx_i - sumlimits_{i=1}^ny_i = 0

sumlimits_{i=1}^nx_i(a +bsumlimits_{i=1}^nx_i - sumlimits_{i=1}^ny_i) = 0
край {случаи}
край{уравнение*}

Преди да решим уравнението, нека предварително заредим, проверим коректността на зареждането и форматираме данните.

Зареждане и форматиране на данни

Трябва да се отбележи, че поради факта, че за аналитичното решение и в бъдеще за градиентно и стохастично градиентно спускане, ще използваме кода в два варианта: използване на библиотеката numpy и без да го използваме, тогава се нуждаем от подходящо форматиране на данните (вижте кода).

Код за зареждане и обработка на данни

# импортируем все нужные нам библиотеки
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import math
import pylab as pl
import random

# графики отобразим в Jupyter
%matplotlib inline

# укажем размер графиков
from pylab import rcParams
rcParams['figure.figsize'] = 12, 6

# отключим предупреждения Anaconda
import warnings
warnings.simplefilter('ignore')

# загрузим значения
table_zero = pd.read_csv('data_example.txt', header=0, sep='t')

# посмотрим информацию о таблице и на саму таблицу
print table_zero.info()
print '********************************************'
print table_zero
print '********************************************'

# подготовим данные без использования NumPy

x_us = []
[x_us.append(float(i)) for i in table_zero['x']]
print x_us
print type(x_us)
print '********************************************'

y_us = []
[y_us.append(float(i)) for i in table_zero['y']]
print y_us
print type(y_us)
print '********************************************'

# подготовим данные с использованием NumPy

x_np = table_zero[['x']].values
print x_np
print type(x_np)
print x_np.shape
print '********************************************'

y_np = table_zero[['y']].values
print y_np
print type(y_np)
print y_np.shape
print '********************************************'

Визуализация

Сега, след като, първо, заредихме данните, второ, проверихме коректността на зареждането и накрая форматирахме данните, ще извършим първата визуализация. Често се използва този метод парцел библиотека Морскородено. В нашия пример, поради ограничения брой, няма смисъл да се използва библиотеката Морскородено. Ще използваме обичайната библиотека Матплотлиб и гледайте само диаграмата на разсейване.

Код на точечната диаграма

print 'График №1 "Зависимость выручки от месяца года"'

plt.plot(x_us,y_us,'o',color='green',markersize=16)
plt.xlabel('$Months$', size=16)
plt.ylabel('$Sales$', size=16)
plt.show()

Графика № 1 "Зависимост на приходите от месеца на годината"

Решаване на просто уравнение на линейна регресия

Аналитично решение

Използваме най-често срещаните инструменти в питон и реши системата от уравнения:

начало{уравнение*}
начало{случаи}
na + bsumlimits_{i=1}^nx_i - sumlimits_{i=1}^ny_i = 0

sumlimits_{i=1}^nx_i(a +bsumlimits_{i=1}^nx_i - sumlimits_{i=1}^ny_i) = 0
край {случаи}
край{уравнение*}

Според правилото на Креймър намерете обща детерминанта, както и детерминанти по Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия, след което, разделяйки детерминантата на Решаване на просто уравнение на линейна регресия към обща детерминанта - намерете коефициента Решаване на просто уравнение на линейна регресия, по подобен начин намираме коефициента Решаване на просто уравнение на линейна регресия.

Код на аналитично решение

# определим функцию для расчета коэффициентов a и b по правилу Крамера
def Kramer_method (x,y):
        # сумма значений (все месяца)
    sx = sum(x)
        # сумма истинных ответов (выручка за весь период)
    sy = sum(y)
        # сумма произведения значений на истинные ответы
    list_xy = []
    [list_xy.append(x[i]*y[i]) for i in range(len(x))]
    sxy = sum(list_xy)
        # сумма квадратов значений
    list_x_sq = []
    [list_x_sq.append(x[i]**2) for i in range(len(x))]
    sx_sq = sum(list_x_sq)
        # количество значений
    n = len(x)
        # общий определитель
    det = sx_sq*n - sx*sx
        # определитель по a
    det_a = sx_sq*sy - sx*sxy
        # искомый параметр a
    a = (det_a / det)
        # определитель по b
    det_b = sxy*n - sy*sx
        # искомый параметр b
    b = (det_b / det)
        # контрольные значения (прооверка)
    check1 = (n*b + a*sx - sy)
    check2 = (b*sx + a*sx_sq - sxy)
    return [round(a,4), round(b,4)]

# запустим функцию и запишем правильные ответы
ab_us = Kramer_method(x_us,y_us)
a_us = ab_us[0]
b_us = ab_us[1]
print ' 33[1m' + ' 33[4m' + "Оптимальные значения коэффициентов a и b:"  + ' 33[0m' 
print 'a =', a_us
print 'b =', b_us
print

# определим функцию для подсчета суммы квадратов ошибок
def errors_sq_Kramer_method(answers,x,y):
    list_errors_sq = []
    for i in range(len(x)):
        err = (answers[0] + answers[1]*x[i] - y[i])**2
        list_errors_sq.append(err)
    return sum(list_errors_sq)

# запустим функцию и запишем значение ошибки
error_sq = errors_sq_Kramer_method(ab_us,x_us,y_us)
print ' 33[1m' + ' 33[4m' + "Сумма квадратов отклонений" + ' 33[0m'
print error_sq
print

# замерим время расчета
# print ' 33[1m' + ' 33[4m' + "Время выполнения расчета суммы квадратов отклонений:" + ' 33[0m'
# % timeit error_sq = errors_sq_Kramer_method(ab,x_us,y_us)

Ето какво получихме:

Решаване на просто уравнение на линейна регресия

Така се намират стойностите на коефициентите, задава се сумата от квадратните отклонения. Нека начертаем права линия върху хистограмата на разсейване в съответствие с намерените коефициенти.

Код на регресионна линия

# определим функцию для формирования массива рассчетных значений выручки
def sales_count(ab,x,y):
    line_answers = []
    [line_answers.append(ab[0]+ab[1]*x[i]) for i in range(len(x))]
    return line_answers

# построим графики
print 'Грфик№2 "Правильные и расчетные ответы"'
plt.plot(x_us,y_us,'o',color='green',markersize=16, label = '$True$ $answers$')
plt.plot(x_us, sales_count(ab_us,x_us,y_us), color='red',lw=4,
         label='$Function: a + bx,$ $where$ $a='+str(round(ab_us[0],2))+',$ $b='+str(round(ab_us[1],2))+'$')
plt.xlabel('$Months$', size=16)
plt.ylabel('$Sales$', size=16)
plt.legend(loc=1, prop={'size': 16})
plt.show()

Таблица № 2 "Верни и изчислени отговори"

Решаване на просто уравнение на линейна регресия

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

Код на диаграмата на отклонението

# определим функцию для формирования массива отклонений в процентах
def error_per_month(ab,x,y):
    sales_c = sales_count(ab,x,y)
    errors_percent = []
    for i in range(len(x)):
        errors_percent.append(100*(sales_c[i]-y[i])/y[i])
    return errors_percent

# построим график
print 'График№3 "Отклонения по-месячно, %"'
plt.gca().bar(x_us, error_per_month(ab_us,x_us,y_us), color='brown')
plt.xlabel('Months', size=16)
plt.ylabel('Calculation error, %', size=16)
plt.show()

Графика № 3 "Отклонения,%"

Решаване на просто уравнение на линейна регресия

Не е перфектно, но ние си свършихме работата.

Нека напишем функция, която да определи коефициентите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия използва библиотеката numpy, по-точно, ще напишем две функции: едната използваща псевдообратна матрица (не се препоръчва на практика, тъй като процесът е изчислително сложен и нестабилен), другата използваща матрично уравнение.

Код на аналитично решение (NumPy)

# для начала добавим столбец с не изменяющимся значением в 1. 
# Данный столбец нужен для того, чтобы не обрабатывать отдельно коэффицент a
vector_1 = np.ones((x_np.shape[0],1))
x_np = table_zero[['x']].values # на всякий случай приведем в первичный формат вектор x_np
x_np = np.hstack((vector_1,x_np))

# проверим то, что все сделали правильно
print vector_1[0:3]
print x_np[0:3]
print '***************************************'
print

# напишем функцию, которая определяет значения коэффициентов a и b с использованием псевдообратной матрицы
def pseudoinverse_matrix(X, y):
    # задаем явный формат матрицы признаков
    X = np.matrix(X)
    # определяем транспонированную матрицу
    XT = X.T
    # определяем квадратную матрицу
    XTX = XT*X
    # определяем псевдообратную матрицу
    inv = np.linalg.pinv(XTX)
    # задаем явный формат матрицы ответов
    y = np.matrix(y)
    # находим вектор весов
    return (inv*XT)*y

# запустим функцию
ab_np = pseudoinverse_matrix(x_np, y_np)
print ab_np
print '***************************************'
print

# напишем функцию, которая использует для решения матричное уравнение
def matrix_equation(X,y):
    a = np.dot(X.T, X)
    b = np.dot(X.T, y)
    return np.linalg.solve(a, b)

# запустим функцию
ab_np = matrix_equation(x_np,y_np)
print ab_np

Сравнете времето, необходимо за определяне на коефициентите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия, според представените 3 метода.

Код за изчисляване на времето

print ' 33[1m' + ' 33[4m' + "Время выполнения расчета коэффициентов без использования библиотеки NumPy:" + ' 33[0m'
% timeit ab_us = Kramer_method(x_us,y_us)
print '***************************************'
print
print ' 33[1m' + ' 33[4m' + "Время выполнения расчета коэффициентов с использованием псевдообратной матрицы:" + ' 33[0m'
%timeit ab_np = pseudoinverse_matrix(x_np, y_np)
print '***************************************'
print
print ' 33[1m' + ' 33[4m' + "Время выполнения расчета коэффициентов с использованием матричного уравнения:" + ' 33[0m'
%timeit ab_np = matrix_equation(x_np, y_np)

Решаване на просто уравнение на линейна регресия

При малко количество данни се появява „самонаписана“ функция, която намира коефициентите с помощта на метода на Cramer.

Сега можете да преминете към други начини за намиране на коефициентите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия.

градиентно спускане

Първо, нека дефинираме какво е градиент. По прост начин, градиентът е сегмент, който показва посоката на максимален растеж на функцията. По аналогия с изкачването нагоре, където изглежда наклонът, има най-стръмното изкачване до върха на планината. Развивайки планинския пример, не забравяйте, че всъщност се нуждаем от най-стръмното спускане, за да достигнем възможно най-бързо ниското, тоест минимума - мястото, където функцията не се увеличава и не намалява. В този момент производната ще бъде нула. Следователно не се нуждаем от градиент, а от антиградиент. За да намерите антиградиента, просто трябва да умножите градиента по -1 (минус едно).

Нека обърнем внимание на факта, че една функция може да има няколко минимума и след като се спуснем в един от тях според предложения по-долу алгоритъм, няма да можем да намерим друг минимум, който може да е по-нисък от намерения. Спокойно, не сме в опасност! В нашия случай имаме работа с един минимум, тъй като нашата функция Решаване на просто уравнение на линейна регресия на графиката е обикновена парабола. И както всички трябва да знаем много добре от училищния курс по математика, параболата има само един минимум.

След като разбрахме защо имаме нужда от градиент и също, че градиентът е сегмент, тоест вектор с дадени координати, които са същите коефициенти Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия можем да приложим градиентно спускане.

Преди да започнете, предлагам да прочетете само няколко изречения за алгоритъма за спускане:

  • Определяме псевдослучайно координатите на коефициентите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия. В нашия пример ще дефинираме коефициенти близо до нула. Това е обичайна практика, но всеки случай може да има своя собствена практика.
  • От координата Решаване на просто уравнение на линейна регресия извадете стойността на частната производна от 1-ви ред в точката Решаване на просто уравнение на линейна регресия. Така че, ако производната е положителна, тогава функцията нараства. Следователно, като извадим стойността на производната, ще се движим в посока, обратна на растежа, тоест в посока на спускане. Ако производната е отрицателна, тогава функцията в този момент намалява и изваждайки стойността на производната, се движим към спускане.
  • Извършваме подобна операция с координатата Решаване на просто уравнение на линейна регресия: извадете стойността на частната производна в точката Решаване на просто уравнение на линейна регресия.
  • За да не прескочите минимума и да не отлетите в дълбокия космос, е необходимо да зададете размера на стъпката в посоката на спускане. Като цяло може да се напише цяла статия за това как да настроите правилно стъпката и как да я промените по време на спускане, за да намалите разходите за изчисления. Но сега имаме малко по-различна задача и ще установим размера на стъпката чрез научния метод на "мушкане" или, както се казва в обикновените хора, емпирично.
  • След като излезем от зададените координати Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия изваждаме стойностите на производните, получаваме нови координати Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия. Правим следващата стъпка (изваждане), вече от изчислените координати. И така цикълът започва отново и отново, докато се достигне необходимата конвергенция.

Всичко! Сега сме готови да тръгнем в търсене на най-дълбокото дефиле на Марианската падина. Да започваме.

Код за градиентно спускане

# напишем функцию градиентного спуска без использования библиотеки NumPy. 
# Функция на вход принимает диапазоны значений x,y, длину шага (по умолчанию=0,1), допустимую погрешность(tolerance)
def gradient_descent_usual(x_us,y_us,l=0.1,tolerance=0.000000000001):
    # сумма значений (все месяца)
    sx = sum(x_us)
    # сумма истинных ответов (выручка за весь период)
    sy = sum(y_us)
    # сумма произведения значений на истинные ответы
    list_xy = []
    [list_xy.append(x_us[i]*y_us[i]) for i in range(len(x_us))]
    sxy = sum(list_xy)
    # сумма квадратов значений
    list_x_sq = []
    [list_x_sq.append(x_us[i]**2) for i in range(len(x_us))]
    sx_sq = sum(list_x_sq)
    # количество значений
    num = len(x_us)
    # начальные значения коэффициентов, определенные псевдослучайным образом
    a = float(random.uniform(-0.5, 0.5))
    b = float(random.uniform(-0.5, 0.5))
    # создаем массив с ошибками, для старта используем значения 1 и 0
    # после завершения спуска стартовые значения удалим
    errors = [1,0]
    # запускаем цикл спуска
    # цикл работает до тех пор, пока отклонение последней ошибки суммы квадратов от предыдущей, не будет меньше tolerance
    while abs(errors[-1]-errors[-2]) > tolerance:
        a_step = a - l*(num*a + b*sx - sy)/num
        b_step = b - l*(a*sx + b*sx_sq - sxy)/num
        a = a_step
        b = b_step
        ab = [a,b]
        errors.append(errors_sq_Kramer_method(ab,x_us,y_us))
    return (ab),(errors[2:])

# запишем массив значений 
list_parametres_gradient_descence = gradient_descent_usual(x_us,y_us,l=0.1,tolerance=0.000000000001)


print ' 33[1m' + ' 33[4m' + "Значения коэффициентов a и b:" + ' 33[0m'
print 'a =', round(list_parametres_gradient_descence[0][0],3)
print 'b =', round(list_parametres_gradient_descence[0][1],3)
print


print ' 33[1m' + ' 33[4m' + "Сумма квадратов отклонений:" + ' 33[0m'
print round(list_parametres_gradient_descence[1][-1],3)
print



print ' 33[1m' + ' 33[4m' + "Количество итераций в градиентном спуске:" + ' 33[0m'
print len(list_parametres_gradient_descence[1])
print

Решаване на просто уравнение на линейна регресия

Гмурнахме се до самото дъно на Марианската падина и там открихме всички същите стойности на коефициентите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресиякоето всъщност може да се очаква.

Нека направим още едно гмуркане, само че този път пълнежът на нашето дълбоководно превозно средство ще бъде други технологии, а именно библиотеката numpy.

Код за градиентно спускане (NumPy)

# перед тем определить функцию для градиентного спуска с использованием библиотеки NumPy, 
# напишем функцию определения суммы квадратов отклонений также с использованием NumPy
def error_square_numpy(ab,x_np,y_np):
    y_pred = np.dot(x_np,ab)
    error = y_pred - y_np
    return sum((error)**2)

# напишем функцию градиентного спуска с использованием библиотеки NumPy. 
# Функция на вход принимает диапазоны значений x,y, длину шага (по умолчанию=0,1), допустимую погрешность(tolerance)
def gradient_descent_numpy(x_np,y_np,l=0.1,tolerance=0.000000000001):
    # сумма значений (все месяца)
    sx = float(sum(x_np[:,1]))
    # сумма истинных ответов (выручка за весь период)
    sy = float(sum(y_np))
    # сумма произведения значений на истинные ответы
    sxy = x_np*y_np
    sxy = float(sum(sxy[:,1]))
    # сумма квадратов значений
    sx_sq = float(sum(x_np[:,1]**2))
    # количество значений
    num = float(x_np.shape[0])
    # начальные значения коэффициентов, определенные псевдослучайным образом
    a = float(random.uniform(-0.5, 0.5))
    b = float(random.uniform(-0.5, 0.5))
    # создаем массив с ошибками, для старта используем значения 1 и 0
    # после завершения спуска стартовые значения удалим
    errors = [1,0]
    # запускаем цикл спуска
    # цикл работает до тех пор, пока отклонение последней ошибки суммы квадратов от предыдущей, не будет меньше tolerance
    while abs(errors[-1]-errors[-2]) > tolerance:
        a_step = a - l*(num*a + b*sx - sy)/num
        b_step = b - l*(a*sx + b*sx_sq - sxy)/num
        a = a_step
        b = b_step
        ab = np.array([[a],[b]])
        errors.append(error_square_numpy(ab,x_np,y_np))
    return (ab),(errors[2:])

# запишем массив значений 
list_parametres_gradient_descence = gradient_descent_numpy(x_np,y_np,l=0.1,tolerance=0.000000000001)

print ' 33[1m' + ' 33[4m' + "Значения коэффициентов a и b:" + ' 33[0m'
print 'a =', round(list_parametres_gradient_descence[0][0],3)
print 'b =', round(list_parametres_gradient_descence[0][1],3)
print


print ' 33[1m' + ' 33[4m' + "Сумма квадратов отклонений:" + ' 33[0m'
print round(list_parametres_gradient_descence[1][-1],3)
print

print ' 33[1m' + ' 33[4m' + "Количество итераций в градиентном спуске:" + ' 33[0m'
print len(list_parametres_gradient_descence[1])
print

Решаване на просто уравнение на линейна регресия
Стойности на коефициента Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия са непроменени.

Нека да разгледаме как се променя грешката по време на градиентно спускане, т.е. как сумата от квадратите на отклоненията се променя с всяка стъпка.

Код за диаграма на отклонение на сума на квадрат

print 'График№4 "Сумма квадратов отклонений по-шагово"'
plt.plot(range(len(list_parametres_gradient_descence[1])), list_parametres_gradient_descence[1], color='red', lw=3)
plt.xlabel('Steps (Iteration)', size=16)
plt.ylabel('Sum of squared deviations', size=16)
plt.show()

Диаграма #4 "Сума от квадратни отклонения при градиентно спускане"

Решаване на просто уравнение на линейна регресия

На графиката виждаме, че грешката намалява с всяка стъпка и след определен брой итерации наблюдаваме почти хоризонтална линия.

И накрая, нека да оценим разликата във времето за изпълнение на кода:

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

print ' 33[1m' + ' 33[4m' + "Время выполнения градиентного спуска без использования библиотеки NumPy:" + ' 33[0m'
%timeit list_parametres_gradient_descence = gradient_descent_usual(x_us,y_us,l=0.1,tolerance=0.000000000001)
print '***************************************'
print

print ' 33[1m' + ' 33[4m' + "Время выполнения градиентного спуска с использованием библиотеки NumPy:" + ' 33[0m'
%timeit list_parametres_gradient_descence = gradient_descent_numpy(x_np,y_np,l=0.1,tolerance=0.000000000001)

Решаване на просто уравнение на линейна регресия

Може би правим нещо нередно, но отново проста „самонаписана“ функция, която не използва библиотеката numpy преди изчислението на функцията с помощта на библиотеката numpy.

Но ние не стоим на едно място, а се движим към изучаването на друг вълнуващ начин за решаване на простото уравнение на линейна регресия. Среща!

Стохастичен градиентен спускане

За да разберете бързо как работи стохастичното градиентно спускане, е по-добре да дефинирате разликите му от конвенционалното градиентно спускане. Ние, в случай на градиентно спускане, в уравненията на производните на Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия използва сумата от стойностите на всички функции и верни отговори, налични в извадката (т.е. сумата от всички Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия). При стохастично градиентно спускане няма да използваме всички стойности в извадката, но вместо това ще изберем псевдослучайно така наречения примерен индекс и ще използваме неговите стойности.

Например, ако индексът е определен като номер 3 (три), тогава ние вземаме стойностите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия, след това заместваме стойностите в уравненията на производните и определяме нови координати. След това, след като определихме координатите, ние отново определяме примерния индекс по псевдослучаен начин, заместваме стойностите, съответстващи на индекса, в частичните диференциални уравнения и определяме координатите по нов начин Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия и т.н. до зелена конвергенция. На пръв поглед може да изглежда, че изобщо може да работи, но работи. Вярно е, че трябва да се отбележи, че грешката не намалява с всяка стъпка, но със сигурност има тенденция.

Какви са предимствата на стохастичното градиентно спускане пред конвенционалното градиентно спускане? Ако размерът на нашата извадка е много голям и се измерва в десетки хиляди стойности, тогава е много по-лесно да обработим, да речем, произволни хиляда от тях, отколкото цялата извадка. Това е мястото, където стохастичното градиентно спускане започва. В нашия случай, разбира се, няма да забележим голяма разлика.

Нека да разгледаме кода.

Код за стохастичен градиентен спускане

# определим функцию стох.град.шага
def stoch_grad_step_usual(vector_init, x_us, ind, y_us, l):
#     выбираем значение икс, которое соответствует случайному значению параметра ind 
# (см.ф-цию stoch_grad_descent_usual)
    x = x_us[ind]
#     рассчитывыаем значение y (выручку), которая соответствует выбранному значению x
    y_pred = vector_init[0] + vector_init[1]*x_us[ind]
#     вычисляем ошибку расчетной выручки относительно представленной в выборке
    error = y_pred - y_us[ind]
#     определяем первую координату градиента ab
    grad_a = error
#     определяем вторую координату ab
    grad_b = x_us[ind]*error
#     вычисляем новый вектор коэффициентов
    vector_new = [vector_init[0]-l*grad_a, vector_init[1]-l*grad_b]
    return vector_new


# определим функцию стох.град.спуска
def stoch_grad_descent_usual(x_us, y_us, l=0.1, steps = 800):
#     для самого начала работы функции зададим начальные значения коэффициентов
    vector_init = [float(random.uniform(-0.5, 0.5)), float(random.uniform(-0.5, 0.5))]
    errors = []
#     запустим цикл спуска
# цикл расчитан на определенное количество шагов (steps)
    for i in range(steps):
        ind = random.choice(range(len(x_us)))
        new_vector = stoch_grad_step_usual(vector_init, x_us, ind, y_us, l)
        vector_init = new_vector
        errors.append(errors_sq_Kramer_method(vector_init,x_us,y_us))
    return (vector_init),(errors)


# запишем массив значений 
list_parametres_stoch_gradient_descence = stoch_grad_descent_usual(x_us, y_us, l=0.1, steps = 800)

print ' 33[1m' + ' 33[4m' + "Значения коэффициентов a и b:" + ' 33[0m'
print 'a =', round(list_parametres_stoch_gradient_descence[0][0],3)
print 'b =', round(list_parametres_stoch_gradient_descence[0][1],3)
print


print ' 33[1m' + ' 33[4m' + "Сумма квадратов отклонений:" + ' 33[0m'
print round(list_parametres_stoch_gradient_descence[1][-1],3)
print

print ' 33[1m' + ' 33[4m' + "Количество итераций в стохастическом градиентном спуске:" + ' 33[0m'
print len(list_parametres_stoch_gradient_descence[1])

Решаване на просто уравнение на линейна регресия

Гледаме внимателно коефициентите и се хващаме на въпроса "Как така?". Имаме други стойности на коефициентите Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия. Може би стохастичното градиентно спускане е намерило по-оптимални параметри за уравнението? За съжаление не. Достатъчно е да погледнете сумата на квадратите на отклоненията и да видите, че с нови стойности на коефициентите грешката е по-голяма. Не бързаме да се отчайваме. Нека изградим графика на промяната на грешката.

Код за начертаване на сумата от квадратни отклонения при стохастичен градиентен спад

print 'График №5 "Сумма квадратов отклонений по-шагово"'
plt.plot(range(len(list_parametres_stoch_gradient_descence[1])), list_parametres_stoch_gradient_descence[1], color='red', lw=2)
plt.xlabel('Steps (Iteration)', size=16)
plt.ylabel('Sum of squared deviations', size=16)
plt.show()

Диаграма #5 „Сума от квадратни отклонения при стохастичен градиентен спад“

Решаване на просто уравнение на линейна регресия

След като гледам графика, всичко си идва на мястото и сега ще оправим всичко.

И какво стана? Случи се следното. Когато произволно избираме месец, именно за избрания месец нашият алгоритъм се стреми да намали грешката в изчисляването на приходите. След това избираме друг месец и повтаряме изчислението, но намаляваме грешката за втория избран месец. А сега нека си припомним, че първите два месеца значително се отклоняват от линията на простото уравнение на линейната регресия. Това означава, че когато бъде избран някой от тези два месеца, чрез намаляване на грешката на всеки от тях, нашият алгоритъм сериозно увеличава грешката върху цялата извадка. И така, какво да правя? Отговорът е прост: трябва да намалите стъпката на спускане. Всъщност, чрез намаляване на стъпката на спускане, грешката също ще спре да „скача“ нагоре или надолу. Или по-скоро грешката „скачане“ няма да спре, но няма да го направи толкова бързо :) Нека проверим.

Код за изпълнение на SGD с по-малка стъпка

# запустим функцию, уменьшив шаг в 100 раз и увеличив количество шагов соответсвующе 
list_parametres_stoch_gradient_descence = stoch_grad_descent_usual(x_us, y_us, l=0.001, steps = 80000)

print ' 33[1m' + ' 33[4m' + "Значения коэффициентов a и b:" + ' 33[0m'
print 'a =', round(list_parametres_stoch_gradient_descence[0][0],3)
print 'b =', round(list_parametres_stoch_gradient_descence[0][1],3)
print


print ' 33[1m' + ' 33[4m' + "Сумма квадратов отклонений:" + ' 33[0m'
print round(list_parametres_stoch_gradient_descence[1][-1],3)
print



print ' 33[1m' + ' 33[4m' + "Количество итераций в стохастическом градиентном спуске:" + ' 33[0m'
print len(list_parametres_stoch_gradient_descence[1])

print 'График №6 "Сумма квадратов отклонений по-шагово"'
plt.plot(range(len(list_parametres_stoch_gradient_descence[1])), list_parametres_stoch_gradient_descence[1], color='red', lw=2)
plt.xlabel('Steps (Iteration)', size=16)
plt.ylabel('Sum of squared deviations', size=16)
plt.show()

Решаване на просто уравнение на линейна регресия

Диаграма #6 „Сума от квадратни отклонения за стохастичен градиент надолу (80k стъпки)“

Решаване на просто уравнение на линейна регресия

Коефициентите се подобриха, но все още не са идеални. Хипотетично това може да се коригира по този начин. Избираме, например, на последните 1000 итерации стойностите на коефициентите, с които е направена минималната грешка. Вярно е, че за това ще трябва да запишем стойностите на самите коефициенти. Няма да правим това, а по-скоро ще обърнем внимание на графика. Изглежда гладко и грешката изглежда намалява равномерно. Всъщност не е така. Нека да разгледаме първите 1000 итерации и да ги сравним с последните.

Код за SGD диаграма (първите 1000 стъпки)

print 'График №7 "Сумма квадратов отклонений по-шагово. Первые 1000 итераций"'
plt.plot(range(len(list_parametres_stoch_gradient_descence[1][:1000])), 
         list_parametres_stoch_gradient_descence[1][:1000], color='red', lw=2)
plt.xlabel('Steps (Iteration)', size=16)
plt.ylabel('Sum of squared deviations', size=16)
plt.show()

print 'График №7 "Сумма квадратов отклонений по-шагово. Последние 1000 итераций"'
plt.plot(range(len(list_parametres_stoch_gradient_descence[1][-1000:])), 
         list_parametres_stoch_gradient_descence[1][-1000:], color='red', lw=2)
plt.xlabel('Steps (Iteration)', size=16)
plt.ylabel('Sum of squared deviations', size=16)
plt.show()

Диаграма № 7 "Сума от квадратни отклонения на SGD (първите 1000 стъпки)"

Решаване на просто уравнение на линейна регресия

Диаграма #8 "Сума от квадратни отклонения на SGD (последните 1000 стъпки)"

Решаване на просто уравнение на линейна регресия

В самото начало на спускането наблюдаваме доста равномерно и рязко намаляване на грешката. При последните итерации виждаме, че грешката се движи около и около стойността от 1,475 и в някои моменти дори се равнява на тази оптимална стойност, но след това все още се повишава ... Повтарям, можете да запишете стойностите на коефициенти Решаване на просто уравнение на линейна регресия и Решаване на просто уравнение на линейна регресия, и след това изберете тези, за които грешката е минимална. Имахме обаче по-голям проблем: трябваше да направим 80 хиляди стъпки (вижте кода), за да получим стойности, близки до оптималните. И това вече противоречи на идеята за спестяване на изчислително време при стохастичен градиентен спускане по отношение на градиентния спускане. Какво може да се коригира и подобри? Не е трудно да се види, че в първите итерации ние постоянно вървим надолу и следователно трябва да оставим голяма стъпка в първите итерации и да намаляваме стъпката, докато се движим напред. В тази статия няма да правим това - вече се проточи. Желаещите могат да се сетят сами как да го направят, не е трудно 🙂

Сега нека извършим стохастичен градиентен спускане с помощта на библиотеката numpy (и нека не се спъваме в скалите, които идентифицирахме по-рано)

Код за стохастичен градиентен спускане (NumPy)

# для начала напишем функцию градиентного шага
def stoch_grad_step_numpy(vector_init, X, ind, y, l):
    x = X[ind]
    y_pred = np.dot(x,vector_init)
    err = y_pred - y[ind]
    grad_a = err
    grad_b = x[1]*err
    return vector_init - l*np.array([grad_a, grad_b])

# определим функцию стохастического градиентного спуска
def stoch_grad_descent_numpy(X, y, l=0.1, steps = 800):
    vector_init = np.array([[np.random.randint(X.shape[0])], [np.random.randint(X.shape[0])]])
    errors = []
    for i in range(steps):
        ind = np.random.randint(X.shape[0])
        new_vector = stoch_grad_step_numpy(vector_init, X, ind, y, l)
        vector_init = new_vector
        errors.append(error_square_numpy(vector_init,X,y))
    return (vector_init), (errors)

# запишем массив значений 
list_parametres_stoch_gradient_descence = stoch_grad_descent_numpy(x_np, y_np, l=0.001, steps = 80000)

print ' 33[1m' + ' 33[4m' + "Значения коэффициентов a и b:" + ' 33[0m'
print 'a =', round(list_parametres_stoch_gradient_descence[0][0],3)
print 'b =', round(list_parametres_stoch_gradient_descence[0][1],3)
print


print ' 33[1m' + ' 33[4m' + "Сумма квадратов отклонений:" + ' 33[0m'
print round(list_parametres_stoch_gradient_descence[1][-1],3)
print



print ' 33[1m' + ' 33[4m' + "Количество итераций в стохастическом градиентном спуске:" + ' 33[0m'
print len(list_parametres_stoch_gradient_descence[1])
print

Решаване на просто уравнение на линейна регресия

Стойностите се оказаха почти същите като при спускане без използване numpy. Това обаче е логично.

Нека разберем колко време са ни отнели стохастичните градиентни спускания.

Код за определяне на времето за изчисляване на SGD (80 хиляди стъпки)

print ' 33[1m' + ' 33[4m' +
"Время выполнения стохастического градиентного спуска без использования библиотеки NumPy:"
+ ' 33[0m'
%timeit list_parametres_stoch_gradient_descence = stoch_grad_descent_usual(x_us, y_us, l=0.001, steps = 80000)
print '***************************************'
print

print ' 33[1m' + ' 33[4m' +
"Время выполнения стохастического градиентного спуска с использованием библиотеки NumPy:"
+ ' 33[0m'
%timeit list_parametres_stoch_gradient_descence = stoch_grad_descent_numpy(x_np, y_np, l=0.001, steps = 80000)

Решаване на просто уравнение на линейна регресия

Колкото по-навътре в гората, толкова по-тъмни са облаците: отново „написаната от себе си“ формула показва най-добър резултат. Всичко това предполага, че трябва да има още по-фини начини за използване на библиотеката. numpy, които наистина ускоряват изчислителните операции. В тази статия няма да научим за тях. Нещо, за което да помислите в свободното си време :)

Обобщаваме

Преди да обобщя, бих искал да отговоря на въпрос, който най-вероятно възникна от нашия скъп читател. Защо всъщност такива „мъчения“ със спускания, защо трябва да ходим нагоре и надолу по планината (най-вече надолу), за да намерим заветната низина, ако имаме толкова мощно и просто устройство в ръцете си, под формата на аналитично решение, което моментално ни телепортира на правилното място?

Отговорът на този въпрос е на повърхността. Сега анализирахме един много прост пример, в който е верният отговор Решаване на просто уравнение на линейна регресия зависи от една особеност Решаване на просто уравнение на линейна регресия. Не виждате това често в живота, така че нека си представим, че имаме знаци от 2, 30, 50 или повече. Добавете към това хиляди и дори десетки хиляди стойности за всяка функция. В този случай аналитичното решение може да не премине теста и да се провали. На свой ред градиентното спускане и неговите вариации бавно, но сигурно ще ни доближат до целта – минимума на функцията. И не се притеснявайте за скоростта - вероятно тепърва ще анализираме начините, които ще ни позволят да зададем и регулираме дължината на стъпката (т.е. скоростта).

А сега накратко.

Първо, надявам се, че материалът, представен в статията, ще помогне на начинаещите "изследователи на данни" да разберат как да решават прости (и не само) уравнения на линейна регресия.

Второ, разгледахме няколко начина за решаване на уравнението. Сега, в зависимост от ситуацията, можем да изберем този, който е най-подходящ за поставената задача.

Трето, видяхме силата на допълнителните настройки, а именно дължината на стъпката на градиентно спускане. Този параметър не трябва да се пренебрегва. Както беше отбелязано по-горе, за да се намалят разходите за извършване на изчисления, дължината на стъпката трябва да се промени по време на спускане.

Четвърто, в нашия случай "самостоятелно написаните" функции показаха най-добрия времеви резултат от изчисленията. Това вероятно се дължи на не най-професионалното използване на възможностите на библиотеката. numpy. Но както и да е, заключението се налага следното. От една страна, понякога си струва да поставите под въпрос установените мнения, а от друга страна, не винаги си струва да усложнявате всичко - напротив, понякога по-простият начин за решаване на проблем е по-ефективен. И тъй като целта ни беше да анализираме три подхода за решаване на просто уравнение на линейна регресия, използването на „самоизписани“ функции беше достатъчно за нас.

Литература (или нещо подобно)

1. Линейна регресия

http://statistica.ru/theory/osnovy-lineynoy-regressii/

2. Метод на най-малките квадрати

mathprofi.ru/method_naimenshih_kvadratov.html

3. Производна

www.mathprofi.ru/chastnye_proizvodnye_primery.html

4. градиент

mathprofi.ru/proizvodnaja_po_napravleniju_i_gradient.html

5. Градиентно спускане

habr.com/en/post/471458

habr.com/en/post/307312

artemarakcheev.com//2017-12-31/линейна_регресия

6. Библиотека NumPy

docs.scipy.org/doc/numpy-1.10.1/reference/generated/numpy.linalg.solve.html

docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.linalg.pinv.html

pythonworld.com/numpy/2.html

Източник: www.habr.com

Добавяне на нов коментар