Just for the sake of practice, I've decided to write a code for polynomial regression with Gradient Descent
Code:
import numpy as np
from matplotlib import pyplot as plt
from scipy.optimize import approx_fprime as gradient
class polynomial_regression():
def __init__(self,degrees):
self.degree = degrees
self.weights = np.random.randn(degrees+1)
self.training_loss = []
self.loss_type = None
def predict(self, x: float):
output = 0
for i in range(len(self.weights)-1):
output += (x**(i+1) * self.weights[i])
return output + self.weights[-1]
def fit(self,X: '1 dim array',y: '1 dim array',epochs,lr,loss,ridge=False,reg_rate=0.1):
training_loss = []
if loss == 'MSE':
loss = MSE
self.loss_type = 'MSE'
elif loss == 'RMSE':
loss = RMSE
self.loss_type = 'RMSE'
else:
raise Exception('Undefined loss function')
for epoch in range(epochs):
self.weights -= lr*gradient(self.weights,
MSE,
[np.sqrt(np.finfo(float).eps)/100]*len(self.weights),
X,
y,
ridge,
reg_rate
)
training_loss.append(MSE(self.weights,X,y))
self.training_loss = training_loss
def MSE(w,X,y,ridge=False,reg_rate=0.1):
total = 0
weights = w[:-1]
bias = w[-1]
for i in range(len(X)):
total += (np.dot(weights,[X[i]**(p+1) for p in range(len(weights))]) + bias - y[i])**2
if ridge == False:
return (1/len(X)) * total
else:
return (1/len(X)) * total + reg_rate*((w**2).sum())
def RMSE(w,X,y,ridge=False, reg_rate = 0.1):
total = 0
weights = w[:-1]
bias = w[-1]
for i in range(len(X)):
total += (np.dot(weights,[X[i]**(p+1) for p in range(len(weights))]) + bias - y[i])**2
if ridge == False:
return np.sqrt((1/len(X)) * total)
else:
return np.sqrt((1/len(X)) * total) + reg_rate*((w**2).sum())
def build_graph(X,y,model):
plt.figure(figsize=(20,8))
#Scatter plot of the dataset and the plot of the model's predictions
plt.subplot(1,2,1)
plt.scatter(X,y)
X.sort()
plt.plot(X,model.predict(X),c='red')
plt.title('Model',size=20)
#Curve of the training loss
plt.subplot(1,2,2)
plt.plot(np.arange(len(model.training_loss)),model.training_loss,label=f'{model.loss_type} loss')
plt.legend(prop={'size': 20})
plt.title('Training loss',size=20)
Several tests
rng = np.random.RandomState( 1)
x = (np.linspace(1,5,100))
y = 3*x + 10 + rng.rand(100)
x = x/10
y = y/10
degree = 1
epochs = 120
learning_rate = 0.9
model = polynomial_regression(degree)
model.fit(x,
y,
epochs,
learning_rate,
loss='MSE',
ridge=False,)
build_graph(x,y,model)
Output
And now with more complex dataset
rng = np.random.RandomState( 1)
x = (np.linspace(1,5,100))
y = (10*np.cos(x) + rng.rand(100))
x = x/10
y = y/10
degree = 3
epochs = 8*10**3
learning_rate = 0.9
model = polynomial_regression(degree)
model.fit(x,
y,
epochs,
learning_rate,
loss='MSE',
ridge=False,)
build_graph(x,y,model)
Output
Notes:
You might wonder why I moved functions for MSE and RMSE out of the class. The main reason is because
approx_fprime
(I renamed it asgradient
, for clarity) requires loss function to place an array of variables for which we calculate the gradient as the first argument (see the documentation). If I am to moveMSE
andRMSE
into the class, the first argument, of course, will beself
.Admittedly, Gradient Descent is not the best choice for optimizing polynomial functions. However, I would still prefer to use it here, just for the sake of solidifying my understanding of how GD works.
For more complex dataset (when we'd need to use higher degrees of polynomial), the model converges very slowly (see the training loss for the second dataset). If possible, I would like you to elaborate a bit on what might be the reason.
What can be improved?
Any suggestions will be welcome: algorithm efficiency/code style/naming conventions, or anything else you can come up with. Thanks!