Files
FYSSTK-Project1/src/pyoptim/optimizers.py
T

422 lines
12 KiB
Python

import numpy as np
def Ridge_parameters(X: np.ndarray, y: np.ndarray, lam: float) -> np.ndarray:
"""Computes the Ridge regression parameters.
Args:
X: The input data matrix of shape (n_samples, n_features).
y: The target vector of shape (n_samples,).
lam: The regularization parameter (lambda).
Returns:
The Ridge regression parameters of shape (n_features,).
"""
# Assumes X is scaled and has no intercept column
return np.linalg.inv(X.T @ X + lam * np.eye(X.shape[1])) @ X.T @ y
def OLS_parameters(X: np.ndarray, y: np.ndarray) -> np.ndarray:
"""Computes the Ordinary Least Squares (OLS) regression parameters.
Args:
X: The input data matrix of shape (n_samples, n_features).
y: The target vector of shape (n_samples,).
Returns:
The OLS regression parameters of shape (n_features,).
"""
return Ridge_parameters(X, y, lam=0.0) # OLS is Ridge with lambda=0
class GradientDescent:
def __init__(
self, *args, learning_rate: float = 0.1, num_iterations: int = 1000, **kwargs
):
"""Gradient Descent Class
Args:
learning_rate (float, optional): Learning rate used for step updates. Defaults to 0.1.
num_iterations (int, optional): Number of iterations for gradient descent. Defaults to 1000.
"""
self._cost_history = np.zeros(num_iterations)
self.learning_rate = learning_rate
self.num_iterations = num_iterations
@property
def cost_history(self) -> np.ndarray:
"""Returns the cost history of the optimization process.
Returns:
np.ndarray: Array of cost values for each iteration.
"""
return self._cost_history
def get_epochs(self) -> np.ndarray:
"""Returns an array of epoch numbers from 0 to num_iterations - 1.
Returns:
np.ndarray: Array of epoch numbers.
"""
return np.arange(self.num_iterations)
def fit(self, X: np.ndarray, y: np.ndarray) -> np.ndarray:
"""Returns the optimal solution for an optimization problem using the gradient descent
Args:
X (np.ndarray): X values
y (np.ndarray): y values
Returns:
np.ndarray: Optimal parameters
"""
self.X = X
self.y = y
self.theta = np.zeros(X.shape[1])
self._precomp()
for t in range(self.num_iterations):
self._comp_step()
self._cost_history[t] = self._compute_cost()
self._update_theta()
return self.theta
def _precomp(self):
pass
def _comp_step(self):
pass
def _compute_cost(self):
pass
def _update_theta(self):
pass
class OLSGradientDescent(GradientDescent):
def _precomp(self):
self.XTX = self.X.T @ self.X
self.XTy = self.X.T @ self.y
self.n = len(self.y)
def _comp_step(self):
self.err = self.X @ self.theta - self.y
def _compute_cost(self) -> float:
return 0.5 * np.mean(np.square(self.err))
def _compute_grad(self) -> np.ndarray:
return (self.XTX @ self.theta - self.XTy) / self.n
def _update_theta(self):
self.theta -= self.learning_rate * self._compute_grad()
class RidgeGradientDescent(OLSGradientDescent):
def __init__(self, *args, lam: float = 0.1, **kwargs):
super().__init__(*args, **kwargs)
self.lam = lam
def _compute_cost(self) -> float:
return super()._compute_cost() + 0.5 * self.lam * np.sum(np.square(self.theta))
def _compute_grad(self) -> np.ndarray:
return super()._compute_grad() + self.lam * self.theta
class OLSMomentum(OLSGradientDescent):
def __init__(self, *args, delta: float = 1.0, **kwargs):
super().__init__(*args, **kwargs)
self.delta = delta
def _precomp(self):
self.last_theta = np.zeros_like(self.theta)
return super()._precomp()
def _update_theta(self):
v = self.delta * (
self.theta - self.last_theta - self.learning_rate * self._compute_grad()
)
self.last_theta = self.theta.copy()
self.theta += v
class RidgeMomentum(OLSMomentum, RidgeGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSAdaGrad(OLSGradientDescent):
def __init__(self, *args, learning_rate=0.1, num_iterations=1000, **kwargs):
super().__init__(
*args, learning_rate=learning_rate, num_iterations=num_iterations, **kwargs
)
def _precomp(self):
self.quad_sum = np.zeros_like(self.theta)
return super()._precomp()
def _update_theta(self):
grad = self._compute_grad()
self.quad_sum += np.square(grad)
self.theta -= (
self.learning_rate * grad / (np.sqrt(self.quad_sum) + 1e-10)
) # to avoid division by zero
class RidgeAdaGrad(OLSAdaGrad, RidgeGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSRMSProp(OLSGradientDescent):
def __init__(
self, *args, learning_rate=0.1, num_iterations=1000, gamma=0.9, **kwargs
):
super().__init__(
*args, learning_rate=learning_rate, num_iterations=num_iterations, **kwargs
)
self.gamma = gamma
def _precomp(self):
self.prev_v = np.zeros_like(self.theta)
return super()._precomp()
def _update_theta(self):
grad = self._compute_grad()
v = self.gamma * self.prev_v + (1 - self.gamma) * np.square(grad)
self.theta -= (
self.learning_rate * grad / (np.sqrt(v) + 1e-10)
) # to avoid division by zero
self.prev_v = v
class RidgeRMSProp(OLSRMSProp, RidgeGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSAdam(OLSGradientDescent):
def __init__(
self,
*args,
learning_rate=0.1,
num_iterations=1000,
beta1=0.9,
beta2=0.999,
**kwargs,
):
super().__init__(
*args, learning_rate=learning_rate, num_iterations=num_iterations, **kwargs
)
self.beta1 = beta1
self.beta2 = beta2
def _precomp(self):
self.m = np.zeros_like(self.theta)
self.v = np.zeros_like(self.theta)
self.current_iteration = 0
return super()._precomp()
def _update_theta(self):
self.current_iteration += 1
grad = self._compute_grad()
self.m = self.beta1 * self.m + (1 - self.beta1) * grad
self.v = self.beta2 * self.v + (1 - self.beta2) * np.square(grad)
m_hat = self.m / (1 - self.beta1 ** (self.current_iteration))
v_hat = self.v / (1 - self.beta2 ** (self.current_iteration))
self.theta -= (
self.learning_rate * m_hat / (np.sqrt(v_hat) + 1e-10)
) # to avoid division by zero
class RidgeAdam(OLSAdam, RidgeGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSOGradientDescent(OLSGradientDescent):
def __init__(self, *args, lam: float = 0.1, **kwargs):
super().__init__(*args, **kwargs)
self.lam = lam
def _compute_cost(self) -> float:
return super()._compute_cost() + self.lam * np.sum(np.abs(self.theta))
def _compute_grad(self) -> np.ndarray:
return super()._compute_grad() + self.lam * np.sign(self.theta)
class LASSOMomentum(OLSMomentum, LASSOGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSOAdaGrad(OLSAdaGrad, LASSOGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSORMSProp(OLSRMSProp, LASSOGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSOAdam(OLSAdam, LASSOGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSStochasticGradientDescent(OLSGradientDescent):
def __init__(
self, *args, batch_size: int = 100, batches_per_epoch: int = 1, **kwargs
):
# print(self.__class__.__name__) # If you see this: debugging yaaaay, the programmer that wrote this line is stupid...
super().__init__(*args, **kwargs)
self.batch_size = batch_size
self.batches_per_epoch = batches_per_epoch
def _precomp(self):
self.N = len(self.y)
self.indices = np.arange(self.N)
self.n = self.batch_size
np.random.shuffle(self.indices)
self.X = self.X[self.indices]
self.y = self.y[self.indices]
def _comp_step(self):
index = np.random.randint(0, self.N)
batch_indices = slice(index, index + self.batch_size)
if index + self.batch_size > self.N:
batch_indices = slice(index, self.N)
X_batch = self.X[batch_indices]
y_batch = self.y[batch_indices]
self.err = X_batch @ self.theta - y_batch
self.XTX = X_batch.T @ X_batch
self.XTy = X_batch.T @ y_batch
def get_epochs(self):
return np.arange(self.num_iterations // self.batches_per_epoch)
@property
def cost_history(self) -> np.ndarray:
"""Returns the cost history of the optimization process.
Returns:
np.ndarray: Array of cost values for each epoch.
"""
return np.mean(self._cost_history.reshape(-1, self.batches_per_epoch), axis=1)
class RidgeStochasticGradientDescent(
OLSStochasticGradientDescent, RidgeGradientDescent
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSOStochasticGradientDescent(
OLSStochasticGradientDescent, LASSOGradientDescent
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSMomentumSGD(OLSMomentum, OLSStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class RidgeMomentumSGD(RidgeMomentum, RidgeStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSOMomentumSGD(LASSOMomentum, LASSOStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSAdaGradSGD(OLSAdaGrad, OLSStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class RidgeAdaGradSGD(RidgeAdaGrad, RidgeStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSOAdaGradSGD(LASSOAdaGrad, LASSOStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSRMSPropSGD(OLSRMSProp, OLSStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class RidgeRMSPropSGD(RidgeRMSProp, RidgeStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSORMSPropSGD(LASSORMSProp, LASSOStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OLSAdamSGD(OLSAdam, OLSStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class RidgeAdamSGD(RidgeAdam, RidgeStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LASSOAdamSGD(LASSOAdam, LASSOStochasticGradientDescent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
OLS_GD_OPTIMIZERS = [OLSGradientDescent, OLSMomentum, OLSAdaGrad, OLSRMSProp, OLSAdam]
RIDGE_GD_OPTIMIZERS = [
RidgeGradientDescent,
RidgeMomentum,
RidgeAdaGrad,
RidgeRMSProp,
RidgeAdam,
]
LASSO_GD_OPTIMIZERS = [
LASSOGradientDescent,
LASSOMomentum,
LASSOAdaGrad,
LASSORMSProp,
LASSOAdam,
]
OLS_SGD_OPTIMIZERS = [
OLSStochasticGradientDescent,
OLSMomentumSGD,
OLSAdaGradSGD,
OLSRMSPropSGD,
OLSAdamSGD,
]
RIDGE_SGD_OPTIMIZERS = [
RidgeStochasticGradientDescent,
RidgeMomentumSGD,
RidgeAdaGradSGD,
RidgeRMSPropSGD,
RidgeAdamSGD,
]
LASSO_SGD_OPTIMIZERS = [
LASSOStochasticGradientDescent,
LASSOMomentumSGD,
LASSOAdaGradSGD,
LASSORMSPropSGD,
LASSOAdamSGD,
]