Полиномиальная регрессия может идентифицировать нелинейную связь между независимой переменной и зависимой переменной.

Фон

Эта статья является третьей в серии статей о регрессии, градиентном спуске и MSE. Предыдущие статьи посвящены Простой линейной регрессии, Нормальному уравнению регрессии и Множественной линейной регрессии.

Полиномиальная регрессия

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

Обратите внимание, что X₀ – это столбец с единицами смещения; это позволяет использовать обобщенную формулу, обсуждавшуюся в первой статье. Используя приведенное выше уравнение, каждую «независимую» переменную можно рассматривать как возведенную в степень версию X₁.

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

Обобщенные функции для модели, градиентного спуска и MSE можно использовать из предыдущих статей:

# line of best fit
def model(w, X):
  """
    Inputs:
      w: array of weights | (num features, 1)
      X: array of inputs  | (n samples, num features)

    Output:
      returns the output of X@w | (n samples, 1)
  """

  return torch.matmul(X, w)
# mean squared error (MSE)
def MSE(Yhat, Y):
  """
    Inputs:
      Yhat: array of predictions | (n samples, 1)
      Y: array of expected outputs | (n samples, 1)
    Output:
      returns the loss of the model, which is a scalar
  """
  return torch.mean((Yhat-Y)**2) # mean((error)^2)
# optimizer
def gradient_descent(w):
  """
    Inputs:
      w: array of weights | (num features, 1)

    Global Variables / Constants:
      X: array of inputs  | (n samples, num features)
      Y: array of expected outputs | (n samples, 1)
      lr: learning rate to scale the gradient

    Output:
      returns the updated weights
  """ 

  n = X.shape[0]

  return w - (lr * 2/n) * (torch.matmul(-Y.T, X) + torch.matmul(torch.matmul(w.T, X.T), X)).reshape(w.shape)

Создание данных

Теперь все, что требуется, — это некоторые данные для обучения модели. Можно использовать функцию «чертежа» и добавить случайность. Здесь используется тот же подход, что и в предыдущих статьях. Схему можно увидеть ниже:

Можно создать набор поездов размером (800, 4) и тестовый набор размером (200, 4). Обратите внимание, что каждая функция, кроме смещения, является экспоненциальной версией первой.

import torch

torch.manual_seed(5)
torch.set_printoptions(precision=2)

# features
X0 = torch.ones((1000,1))
X1 = (100*(torch.rand(1000) - 0.5)).reshape(-1,1) # generates 1000 random numbers from -50 to 50
X2, X3 = X1**2, X1**3
X = torch.hstack((X0,X1,X2,X3))

# normal distribution with a mean of 0 and std of 8
normal = torch.distributions.Normal(loc=0, scale=8)

# targets
Y = (3*X[:,3] + 2*X[:,2] + 1*X[:,1] + 5 + normal.sample(torch.ones(1000).shape)).reshape(-1,1)

# train, test
Xtrain, Xtest = X[:800], X[800:]
Ytrain, Ytest = Y[:800], Y[800:]

После определения начальных весов данные могут быть нанесены на график с линией наилучшего соответствия.

torch.manual_seed(5)
w = torch.rand(size=(4, 1))
w
tensor([[0.83],
        [0.13],
        [0.91],
        [0.82]])
import matplotlib.pyplot as plt

def plot_lbf():
  """
    Output:
      prints the line of best fit in comparison to the train and test data
  """

  # plot the train and test sets
  plt.scatter(Xtrain[:,1],Ytrain,label="train")
  plt.scatter(Xtest[:,1],Ytest,label="test")

  # plot the line of best fit
  X1_plot = torch.arange(-50, 50.1,.1).reshape(-1,1) 
  X2_plot, X3_plot = X1_plot**2, X1_plot**3
  X0_plot = torch.ones(X1_plot.shape)
  X_plot = torch.hstack((X0_plot,X1_plot,X2_plot,X3_plot))

  plt.plot(X1_plot.flatten(), model(w, X_plot).flatten(), color="red", zorder=4)

  plt.xlim(-50, 50)
  plt.xlabel("$X$")
  plt.ylabel("$Y$")
  plt.legend()
  plt.show()

plot_lbf()

Обучение модели

Чтобы частично минимизировать функцию стоимости, можно использовать скорость обучения 5e-11 и 500 000 эпох с градиентным спуском.

lr = 5e-11
epochs = 500000

# update the weights 1000 times
for i in range(0, epochs):
  # update the weights
  w = gradient_descent(w)

  # print the new values every 10 iterations
  if (i+1) % 100000 == 0:
    print("epoch:", i+1)
    print("weights:", w)
    print("Train MSE:", MSE(model(w,Xtrain), Ytrain))
    print("Test MSE:", MSE(model(w,Xtest), Ytest))
    print("="*10)

plot_lbf()
epoch: 100000
weights: tensor([[0.83],
        [0.13],
        [2.00],
        [3.00]])
Train MSE: tensor(163.87)
Test MSE: tensor(162.55)
==========
epoch: 200000
weights: tensor([[0.83],
        [0.13],
        [2.00],
        [3.00]])
Train MSE: tensor(163.52)
Test MSE: tensor(162.22)
==========
epoch: 300000
weights: tensor([[0.83],
        [0.13],
        [2.00],
        [3.00]])
Train MSE: tensor(163.19)
Test MSE: tensor(161.89)
==========
epoch: 400000
weights: tensor([[0.83],
        [0.13],
        [2.00],
        [3.00]])
Train MSE: tensor(162.85)
Test MSE: tensor(161.57)
==========
epoch: 500000
weights: tensor([[0.83],
        [0.13],
        [2.00],
        [3.00]])
Train MSE: tensor(162.51)
Test MSE: tensor(161.24)
==========

Даже с 500 000 эпох и чрезвычайно малой скоростью обучения модель не может определить первые два веса. Хотя текущее решение является очень точным с MSE 161,24, для его полной минимизации, вероятно, потребуются миллионы эпох. Это одно из ограничений градиентного спуска для полиномиальной регрессии.

Нормальное уравнение

В качестве альтернативы можно использовать нормальное уравнение из второй статьи для прямого вычисления оптимизированных весов:

def NormalEquation(X, Y):
  """
    Inputs:
      X: array of input values | (n samples, num features)
      Y: array of expected outputs | (n samples, 1)
      
    Output:
      returns the optimized weights | (num features, 1)
  """
  
  return torch.inverse(X.T @ X) @ X.T @ Y

w = NormalEquation(Xtrain, Ytrain)
w
tensor([[4.57],
        [0.98],
        [2.00],
        [3.00]])

Нормальное уравнение способно сразу определить правильные значения для каждого веса, а MSE для каждого набора примерно на 100 пунктов ниже, чем при градиентном спуске:

MSE(model(w,Xtrain), Ytrain), MSE(model(w,Xtest), Ytest)
(tensor(60.64), tensor(63.84))

Заключение

При реализации простой линейной, множественной линейной и полиномиальной регрессии следующие две статьи будут посвящены регрессии Лассо и Риджа. Эти типы регрессии вводят два важных понятия в машинном обучении: переоснащение и регуляризация.

Пожалуйста, не забудьте поставить лайк и подписаться! :)