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

Все мы знаем, что sklearn может подбирать модели для нас. Но знаем ли мы, что он на самом деле делает, когда мы вызываем .fit(). Продолжайте читать, чтобы узнать.

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

Обязательный фон

Подгонка = поиск смещения модели и коэффициентов, которые минимизируют ошибку.

Ошибка = среднеквадратическая ошибка (MSE). Среднее значение квадратов разностей между фактическими и прогнозируемыми значениями в наборе данных.

Простая линейная регрессия = модель, основанная на уравнении прямой «y = mx + b». Он принимает в качестве входных данных одну характеристику, применяет смещение и коэффициент и прогнозирует y.

Кроме того, вместе коэффициент и смещение иногда называют просто «весами».

Подготовьте набор данных

Загрузите набор данных California Housing из kaggle и загрузите его во фреймворк.

import pandas as pd
df = pd.read_csv('california-housing-dataset.csv')
df.head()

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

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

Подсчитайте примеры в наборе данных.

len(df)
#=> 20640

Это много данных. Уменьшим размер на 75%.

df = df.sample(frac=0.25)
len(df)
#=> 5160

Это более управляемо.

Соберите наши входные функции и метки.

X = df['median_income'].tolist()
y = df['median_house_value'].tolist()

Подгонка линейной регрессии к градиентному спуску

Согласно Википедии,

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

Мой перевод:

Градиентный спуск использует градиент функции ошибок, чтобы предсказать, в каком направлении следует обновить коэффициент m и смещение b, чтобы уменьшить ошибку в данном наборе данных.

Уклон каждого груза m и b рассчитывается отдельно. Градиент рассчитывается путем усреднения частной производной веса по всем примерам.

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

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

Реализация

Вы можете найти частные производные функции MSE (как показано ниже) в Интернете, поэтому мы не будем выводить ее здесь.

Мы реализуем описанное выше следующим образом, выполняя итерацию по набору данных.

m_gradient += -(2/N) * x * (y - y_hat)
b_gradient += -(2/N) * (y - y_hat)

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

def bias_coef_update(m, b, X, Y, learning_rate):
    m_gradient = 0
    b_gradient = 0
    
    N = len(Y)
    
    # iterate over examples
    for idx in range(len(Y)):
        x = X[idx]
        y = Y[idx]
                      
        # predict y with current bias and coefficient
        y_hat = (m * x) + b
        m_gradient += -(2/N) * x * (y - y_hat)
        b_gradient += -(2/N) * (y - y_hat)
        
    # use gradient with learning_rate to nudge bias and coefficient
    new_coef = m - (m_gradient * learning_rate)
    new_bias = b - (b_gradient * learning_rate)
    
    return new_coef, new_bias

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

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

def run(epoch_count=1000):
    # store output to plot later
    epochs = []
    costs = []
        
    m = 0 
    b = 0 
    learning_rate = 0.01
    for i in range(epoch_count):
        m, b = bias_coef_update(m, b, X, y, learning_rate)
        print(m,b)
        
        C = cost(b, m, x_y_pairs)
        
        epochs.append(i)
        costs.append(C)
    
    return epochs, costs, m, b
epochs, costs, m, b = run()

Выведем окончательную стоимость и вес подобранной модели.

print(m)
print(b)
print(costs[-1])
# I've rounded these myself so they're nicer to look at
#=> 46,804
#=> 19,963
#=> 7,261,908,362

И проследите, как оно улучшалось за эпохи.

import matplotlib.pyplot as plt
plt.xlabel('Epoch')
plt.ylabel('Error')
plt.suptitle('Cost by epoch')
plt.plot(epochs,costs, linewidth=1)

Прохладный. Мы видим, что большая часть его прогресса пришлась на первые 100 эпох.

Подгонка линейной регрессии с помощью Sklearn

Теперь мы проверим те же значения в модели, оснащенной sklearn.

Измените особенности.

import numpy as np
X_array = np.array(X).reshape(5160,1)

Подходит модель.

from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X_array,y)

Проверьте вес и погрешность.

from sklearn.metrics import mean_squared_error
m = model.coef_[0]
b = model.intercept_
mse = mean_squared_error(y_test, y_pred, sample_weight=None, multioutput='uniform_average')
print(m)
print(b)
print(mse)
# rounded
#=> 42,324
#=> 41,356
#=> 7,134,555,443

Как мы сделали?

Настройка Sklearn немного превзошла нашу, 7,134,555,443 VS 7,261,908,362, но мы подошли довольно близко.

Наша предвзятость также сильно отличается от предвзятости, обнаруженной sklearn, 41,356 VS 19,963.

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