Machine learning / Regularization overfitting and optimization
Regularization Techniques for Robust Machine Learning Models
Contents
- L1 Regularization - The Lasso
- L2 Regularization - Ridge Regression
- Elastic Net - Combining L1 and L2
- Dropout Regularization
- Early Stopping Implementation
- Weight Decay Regularization
- Cross-Validation with Regularization
- Batch Normalization
- Data Augmentation as Regularization
- Real-world Application - Housing Price Prediction
- Real-world Application - Credit Card Fraud Detection
- Results Analysis - Regularization Impact
- Advanced Regularization Techniques - Maximum Margin
- Additional Resources
L1 Regularization - The Lasso
L1 regularization adds the absolute value of weights to the loss function, promoting sparsity by driving some coefficients to exactly zero. This selective feature elimination makes models more interpretable while preventing overfitting through parameter shrinkage and automatic variable selection.
import numpy as np
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_regression
# Generate synthetic data
X, y = make_regression(n_samples=100, n_features=20, noise=0.1)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Initialize and train Lasso model
lasso = Lasso(alpha=0.1)
lasso.fit(X_scaled, y)
# Examine coefficient sparsity
nonzero_coef = np.sum(lasso.coef_ != 0)
print(f"Number of non-zero coefficients: {nonzero_coef}")
print(f"Total coefficients: {len(lasso.coef_)}")
L2 Regularization - Ridge Regression
Ridge regression adds the squared magnitude of coefficients to the loss function, effectively shrinking all parameters toward zero without eliminating them completely. This technique helps stabilize learning and reduces model variance while maintaining sensitivity to all features.
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_split
# Prepare data
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)
# Train Ridge model
ridge = Ridge(alpha=1.0)
ridge.fit(X_train, y_train)
# Compare coefficient distributions
print("Ridge coefficients distribution:")
print(np.percentile(np.abs(ridge.coef_), [0, 25, 50, 75, 100]))
Elastic Net - Combining L1 and L2
Elastic Net combines the strengths of both L1 and L2 regularization, providing a hybrid approach that simultaneously performs feature selection and coefficient shrinkage. This method excels when dealing with correlated predictors and helps prevent the limitations of using either regularization alone.
from sklearn.linear_model import ElasticNet
# Initialize and train Elastic Net
elastic = ElasticNet(alpha=0.1, l1_ratio=0.5)
elastic.fit(X_train, y_train)
# Compare models' performance
models = {'Lasso': lasso, 'Ridge': ridge, 'ElasticNet': elastic}
for name, model in models.items():
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
print(f"{name} - Train R²: {train_score:.3f}, Test R²: {test_score:.3f}")
Dropout Regularization
Dropout randomly deactivates neurons during training, forcing the network to learn redundant representations and preventing co-adaptation of features. This technique significantly reduces overfitting in neural networks by creating an implicit ensemble of subnetworks.
import tensorflow as tf
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.models import Sequential
def create_dropout_model(input_dim, dropout_rate=0.3):
model = Sequential([
Dense(128, activation='relu', input_shape=(input_dim,)),
Dropout(dropout_rate),
Dense(64, activation='relu'),
Dropout(dropout_rate),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
return model
# Train model with dropout
model = create_dropout_model(X_train.shape[1])
history = model.fit(X_train, y_train, validation_split=0.2,
epochs=100, batch_size=32, verbose=0)
Early Stopping Implementation
Early stopping monitors validation performance during training and halts when improvement stops, preventing overfitting by finding the optimal point between underfitting and overfitting. This method effectively reduces training time while ensuring optimal model generalization.
from tensorflow.keras.callbacks import EarlyStopping
# Configure early stopping
early_stopping = EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True,
min_delta=0.001
)
# Train with early stopping
model_es = create_dropout_model(X_train.shape[1])
history_es = model_es.fit(
X_train, y_train,
validation_split=0.2,
epochs=1000,
callbacks=[early_stopping],
verbose=0
)
print(f"Training stopped at epoch {len(history_es.history['loss'])}")
Weight Decay Regularization
Weight decay, also known as L2 regularization in neural networks, progressively reduces weight magnitudes during training by adding a penalty term to the loss function. This technique prevents weights from growing excessively large and helps maintain a simpler, more generalizable model.
import tensorflow as tf
from tensorflow.keras.regularizers import l2
def create_weight_decay_model(input_dim, weight_decay=0.01):
model = Sequential([
Dense(128, activation='relu', kernel_regularizer=l2(weight_decay),
input_shape=(input_dim,)),
Dense(64, activation='relu', kernel_regularizer=l2(weight_decay)),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
return model
# Compare models with different weight decay values
weight_decays = [0.001, 0.01, 0.1]
for wd in weight_decays:
model_wd = create_weight_decay_model(X_train.shape[1], wd)
history = model_wd.fit(X_train, y_train, validation_split=0.2,
epochs=100, verbose=0)
print(f"Weight decay {wd}: Val Loss = {history.history['val_loss'][-1]:.4f}")
Cross-Validation with Regularization
Cross-validation with regularization provides robust model evaluation by testing different regularization strengths across multiple data splits. This comprehensive approach helps identify optimal hyperparameters while ensuring consistent performance across different subsets of data.
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import ElasticNet
# Define parameter grid
param_grid = {
'alpha': [0.001, 0.01, 0.1, 1.0],
'l1_ratio': [0.1, 0.3, 0.5, 0.7, 0.9]
}
# Perform grid search with cross-validation
elastic_cv = ElasticNet()
grid_search = GridSearchCV(
elastic_cv, param_grid,
cv=5, scoring='neg_mean_squared_error',
n_jobs=-1
)
grid_search.fit(X_scaled, y)
print(f"Best parameters: {grid_search.best_params_}")
print(f"Best CV score: {-grid_search.best_score_:.4f}")
Batch Normalization
Batch normalization normalizes layer inputs during training, stabilizing and accelerating the learning process while acting as a regularizer. This technique reduces internal covariate shift and allows higher learning rates, leading to faster convergence and better generalization.
from tensorflow.keras.layers import BatchNormalization
def create_batchnorm_model(input_dim):
model = Sequential([
Dense(128, input_shape=(input_dim,)),
BatchNormalization(),
tf.keras.layers.Activation('relu'),
Dense(64),
BatchNormalization(),
tf.keras.layers.Activation('relu'),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
return model
# Train model with batch normalization
model_bn = create_batchnorm_model(X_train.shape[1])
history_bn = model_bn.fit(
X_train, y_train,
validation_split=0.2,
epochs=100,
batch_size=32,
verbose=0
)
Data Augmentation as Regularization
Data augmentation serves as an implicit regularization technique by increasing training data variety through controlled transformations. This approach improves model robustness and generalization by exposing the model to diverse variations of the input data.
import numpy as np
def augment_regression_data(X, y, noise_factor=0.05):
X_aug = np.array(X)
y_aug = np.array(y)
# Add Gaussian noise
noise = np.random.normal(0, noise_factor, X.shape)
X_aug_noise = X + noise
# Combine original and augmented data
X_combined = np.vstack([X, X_aug_noise])
y_combined = np.hstack([y, y_aug])
return X_combined, y_combined
# Apply augmentation
X_aug, y_aug = augment_regression_data(X_train, y_train)
print(f"Original shape: {X_train.shape}, Augmented shape: {X_aug.shape}")
# Train model with augmented data
model_aug = create_dropout_model(X_train.shape[1])
history_aug = model_aug.fit(X_aug, y_aug, validation_split=0.2,
epochs=100, batch_size=32, verbose=0)
Real-world Application - Housing Price Prediction
This implementation demonstrates regularization techniques applied to the California Housing dataset, showcasing how different regularization methods affect model performance and feature importance in a real-world regression problem.
from sklearn.datasets import fetch_california_housing
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
# Load and prepare data
housing = fetch_california_housing()
X, y = housing.data, housing.target
X_scaled = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)
# Compare different regularization methods
models = {
'Lasso': Lasso(alpha=0.01),
'Ridge': Ridge(alpha=1.0),
'ElasticNet': ElasticNet(alpha=0.01, l1_ratio=0.5)
}
results = {}
for name, model in models.items():
model.fit(X_train, y_train)
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
results[name] = {'train': train_score, 'test': test_score}
print(f"{name} - Train R²: {train_score:.3f}, Test R²: {test_score:.3f}")
Real-world Application - Credit Card Fraud Detection
Regularization becomes crucial in fraud detection where class imbalance and high-dimensional feature spaces require robust model generalization. This implementation showcases how regularization prevents overfitting while maintaining high detection accuracy.
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report
from sklearn.linear_model import LogisticRegression
# Simulating imbalanced fraud dataset
np.random.seed(42)
n_legitimate = 10000
n_fraudulent = 100
# Generate synthetic fraud data
legitimate = np.random.normal(0, 1, (n_legitimate, 30))
fraudulent = np.random.normal(1.5, 2, (n_fraudulent, 30))
X = np.vstack([legitimate, fraudulent])
y = np.hstack([np.zeros(n_legitimate), np.ones(n_fraudulent)])
# Train models with different regularization strengths
C_values = [0.001, 0.01, 0.1, 1.0]
for C in C_values:
clf = LogisticRegression(C=C, class_weight='balanced')
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(f"\nRegularization strength C={C}")
print(classification_report(y_test, y_pred))
Results Analysis - Regularization Impact
A comprehensive analysis of how different regularization techniques affect model performance across various metrics. This quantitative comparison helps in selecting the most appropriate regularization strategy for specific use cases.
import matplotlib.pyplot as plt
import seaborn as sns
def plot_regularization_comparison(results_dict, metric='test'):
techniques = list(results_dict.keys())
scores = [results_dict[t][metric] for t in techniques]
plt.figure(figsize=(10, 6))
sns.barplot(x=techniques, y=scores)
plt.title(f'Regularization Techniques Comparison ({metric} scores)')
plt.ylabel('R² Score')
plt.xticks(rotation=45)
# Convert plot to text representation for presentation
for i, score in enumerate(scores):
print(f"{techniques[i]}: {score:.4f}")
# Analyze coefficient sparsity
def analyze_sparsity(models_dict, feature_names):
for name, model in models_dict.items():
nonzero = np.sum(model.coef_ != 0)
print(f"\n{name} Sparsity Analysis:")
print(f"Non-zero coefficients: {nonzero}")
print(f"Sparsity ratio: {1 - nonzero/len(model.coef_):.2f}")
# Example usage
plot_regularization_comparison(results)
Advanced Regularization Techniques - Maximum Margin
Maximum margin regularization enforces larger decision boundaries between classes, crucial for robust classification. This implementation demonstrates how to achieve optimal margin separation while maintaining model generalization capabilities.
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
def create_max_margin_classifier(X, y, C=1.0):
# Create pipeline with scaling and SVM
pipeline = Pipeline([
('scaler', StandardScaler()),
('svm', LinearSVC(C=C, dual=False))
])
# Train with different margin strengths
margins = {}
for C in [0.1, 1.0, 10.0]:
pipeline.set_params(svm__C=C)
pipeline.fit(X_train, y_train)
# Calculate margin width
w_norm = np.linalg.norm(pipeline.named_steps['svm'].coef_)
margin = 2 / w_norm if w_norm != 0 else float('inf')
margins[C] = {
'margin': margin,
'train_score': pipeline.score(X_train, y_train),
'test_score': pipeline.score(X_test, y_test)
}
return margins
# Analyze margin impact
margin_results = create_max_margin_classifier(X, y)
for C, metrics in margin_results.items():
print(f"\nC={C}:")
print(f"Margin width: {metrics['margin']:.4f}")
print(f"Train accuracy: {metrics['train_score']:.4f}")
print(f"Test accuracy: {metrics['test_score']:.4f}")
Additional Resources
- arxiv.org/abs/1711.05101 - "A Disciplined Approach to Neural Network Hyper-Parameters"
- arxiv.org/abs/1505.05424 - "Batch Normalization: Accelerating Deep Network Training"
- arxiv.org/abs/1706.02677 - "When Does Label Smoothing Help?"
- arxiv.org/abs/1810.12281 - "Rethinking the Usage of Batch Normalization and Dropout"
- arxiv.org/abs/2002.11022 - "Regularization: A Short Survey"