Files
FYS-STK4155/doc/Programs/Regression/lasso.py
T
2025-09-03 15:56:58 +02:00

101 lines
4.5 KiB
Python

import numpy as np
import matplotlib.pyplot as plt
class LassoGD:
def __init__(self, lr=0.01, l1_penalty=1.0, tol=1e-6, max_iter=1000):
"""Initialize LASSO regressor with given hyperparameters."""
self.lr = lr # learning rate (step size)
self.l1_penalty = l1_penalty # L1 regularization strength (λ)
self.tol = tol # convergence tolerance for change in cost
self.max_iter = max_iter # maximum iterations to run
self.weights = None # model weights (including intercept as w[0])
self.feature_means = None # to store feature means for normalization
self.feature_stds = None # to store feature std devs for normalization
self.cost_history = None # to record cost at each iteration
def fit(self, X, y, normalize=True):
"""Train the LASSO model on data X (shape m x n) and targets y (length m)."""
X = np.array(X, dtype=float)
y = np.array(y, dtype=float)
m, n = X.shape
# 1. Feature normalization (zero mean, unit variance)
if normalize:
self.feature_means = X.mean(axis=0)
self.feature_stds = X.std(axis=0)
self.feature_stds[self.feature_stds == 0] = 1.0 # avoid division by zero
X = (X - self.feature_means) / self.feature_stds
else:
# If not normalizing, set means=0 and stds=1 for consistency
self.feature_means = np.zeros(n)
self.feature_stds = np.ones(n)
# Add bias term (intercept) as an extra column of ones in X
X_bias = np.hstack([np.ones((m, 1)), X])
# Initialize weights (n features + 1 intercept) to zero
self.weights = np.zeros(n + 1)
self.cost_history = []
prev_cost = float('inf')
# Gradient Descent Loop
for it in range(self.max_iter):
# 2. Predictions for current weights
y_pred = X_bias.dot(self.weights)
error = y_pred - y
# 3. Compute cost = MSE + L1 penalty (do not penalize intercept w[0])
mse_cost = (error ** 2).mean() / 2.0
l1_cost = self.l1_penalty * np.sum(np.abs(self.weights[1:]))
cost = mse_cost + l1_cost
self.cost_history.append(cost)
# Check convergence: stop if change in cost is below tolerance
if abs(prev_cost - cost) < self.tol:
break
prev_cost = cost
# 4. Compute gradient of MSE part
grad_mse = (X_bias.T.dot(error)) / m # gradient of 1/(2m)*RSS is X^T(error)/m
# 5. Perform gradient descent update with L1 penalty via soft-thresholding
# Take a gradient step for MSE
w_temp = self.weights - self.lr * grad_mse
# Soft-thresholding for L1: shrink weights toward 0 by lr*λ
thresh = self.lr * self.l1_penalty
w0 = w_temp[0] # intercept (no regularization)
w_rest = w_temp[1:]
# Apply soft threshold to each weight in w_rest
w_rest_updated = np.sign(w_rest) * np.maximum(np.abs(w_rest) - thresh, 0.0)
# Update weights (combine intercept and rest)
self.weights = np.concatenate(([w0], w_rest_updated))
# End of gradient descent loop
def predict(self, X):
"""Make predictions using the trained model on new data X."""
X = np.array(X, dtype=float)
# Normalize using the training mean and std
X_norm = (X - self.feature_means) / self.feature_stds
# Add bias term
X_bias = np.hstack([np.ones((X_norm.shape[0], 1)), X_norm])
return X_bias.dot(self.weights)
# --- Example usage on a synthetic dataset ---
np.random.seed(0)
# Create synthetic data: m samples, n features
m, n = 100, 5
X = np.random.randn(m, n)
# True underlying weights for features (some are zero to illustrate feature selection)
true_w = np.array([0, 0, 5, 0, -3], dtype=float)
true_intercept = 10.0
# Generate targets with a linear combination of X and noise
y = true_intercept + X.dot(true_w) + 0.5 * np.random.randn(m)
# Train LASSO regression model
model = LassoGD(lr=0.05, l1_penalty=0.5, tol=1e-6, max_iter=1000)
model.fit(X, y)
print("Learned weights (intercept + coefficients):", model.weights)
# Plot the cost function history over iterations
plt.figure(figsize=(6,4))
plt.plot(model.cost_history, label="Cost")
plt.title("Cost Function Value vs Iterations")
plt.xlabel("Iteration")
plt.ylabel("Cost (MSE + L1 penalty)")
plt.legend()
plt.grid(True)
plt.show()