Machine learning / Advanced ML topics
Exploring the Mathematics of Machine Learning Cost Functions
Contents
- Understanding the Cost Function in Machine Learning
- Why Square the Errors?
- The Role of 2m in the Cost Function
- Mathematical Derivation vs. Intuition
- Alternative Error Measures
- Pros and Cons of Squared Errors
- Real-Life Example: Image Compression
- Real-Life Example: Weather Prediction
- Gradient Descent and the Cost Function
- The Importance of Normalization
- Regularization and the Cost Function
- Cross-Validation and the Cost Function
- Hyperparameter Tuning with Grid Search
- Learning Curves and Overfitting
- Additional Resources
Understanding the Cost Function in Machine Learning
The cost function is a crucial component in machine learning, measuring how well a model's predictions align with actual values. Let's explore the mathematical reasoning behind its formulation and why certain choices were made.
import matplotlib.pyplot as plt
# Generate sample data
y_true = np.array([1, 2, 3, 4, 5])
y_pred = np.array([1.2, 2.3, 2.7, 4.1, 5.2])
# Calculate squared errors
squared_errors = (y_pred - y_true) ** 2
# Plot the results
plt.figure(figsize=(10, 6))
plt.scatter(y_true, y_pred, color='blue', label='Predictions')
plt.plot([0, 6], [0, 6], color='red', linestyle='--', label='Perfect predictions')
plt.xlabel('True values')
plt.ylabel('Predicted values')
plt.title('Predictions vs True Values')
plt.legend()
plt.show()
print(f"Squared errors: {squared_errors}")
Why Square the Errors?
Squaring errors addresses several key issues:
- It eliminates the problem of positive and negative errors canceling out.
- Unlike absolute values, squared errors are differentiable everywhere, facilitating optimization.
- It penalizes larger errors more heavily, encouraging the model to minimize outliers.
import matplotlib.pyplot as plt
# Generate sample errors
errors = np.linspace(-5, 5, 100)
# Calculate different error measures
absolute_errors = np.abs(errors)
squared_errors = errors ** 2
# Plot the results
plt.figure(figsize=(10, 6))
plt.plot(errors, absolute_errors, label='Absolute Error')
plt.plot(errors, squared_errors, label='Squared Error')
plt.xlabel('Error')
plt.ylabel('Error Measure')
plt.title('Comparison of Error Measures')
plt.legend()
plt.grid(True)
plt.show()
The Role of 2m in the Cost Function
Dividing by 2m in the cost function \frac{1}{2m}\sum_{i=1}^m(y'_i-y_i)^2 serves two purposes:
- It averages the errors across all data points, providing a consistent measure regardless of dataset size.
- The factor of 1/2 simplifies the derivative calculation during optimization.
def cost_function(y_true, y_pred):
m = len(y_true)
return (1 / (2 * m)) * np.sum((y_pred - y_true) ** 2)
# Example usage
y_true = np.array([1, 2, 3, 4, 5])
y_pred = np.array([1.2, 2.3, 2.7, 4.1, 5.2])
cost = cost_function(y_true, y_pred)
print(f"Cost: {cost}")
Mathematical Derivation vs. Intuition
The cost function's formulation combines mathematical reasoning with practical considerations:
- Mathematical: Differentiability for optimization, handling of positive/negative errors.
- Intuitive: Simplicity, interpretability, and effectiveness in practice.
import matplotlib.pyplot as plt
# Generate sample data
x = np.linspace(0, 10, 100)
y_true = 2 * x + 1 + np.random.normal(0, 1, 100)
# Define different cost functions
def mse(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
def mae(y_true, y_pred):
return np.mean(np.abs(y_true - y_pred))
def huber(y_true, y_pred, delta=1.0):
error = y_true - y_pred
return np.mean(np.where(np.abs(error) <= delta,
0.5 * error ** 2,
delta * (np.abs(error) - 0.5 * delta)))
# Calculate costs for different slopes
slopes = np.linspace(0, 4, 100)
mse_costs = [mse(y_true, slope * x) for slope in slopes]
mae_costs = [mae(y_true, slope * x) for slope in slopes]
huber_costs = [huber(y_true, slope * x) for slope in slopes]
# Plot the results
plt.figure(figsize=(12, 6))
plt.plot(slopes, mse_costs, label='MSE')
plt.plot(slopes, mae_costs, label='MAE')
plt.plot(slopes, huber_costs, label='Huber')
plt.xlabel('Slope')
plt.ylabel('Cost')
plt.title('Comparison of Different Cost Functions')
plt.legend()
plt.grid(True)
plt.show()
Alternative Error Measures
While squared errors are common, other error measures exist:
- Absolute Error: |y' - y|
- Cubic Error: (y' - y)^3
- Fourth Power Error: (y' - y)^4
Each has unique properties and use cases.
import matplotlib.pyplot as plt
# Generate sample errors
errors = np.linspace(-2, 2, 100)
# Calculate different error measures
absolute_errors = np.abs(errors)
squared_errors = errors ** 2
cubic_errors = errors ** 3
fourth_power_errors = errors ** 4
# Plot the results
plt.figure(figsize=(12, 6))
plt.plot(errors, absolute_errors, label='Absolute Error')
plt.plot(errors, squared_errors, label='Squared Error')
plt.plot(errors, cubic_errors, label='Cubic Error')
plt.plot(errors, fourth_power_errors, label='Fourth Power Error')
plt.xlabel('Error')
plt.ylabel('Error Measure')
plt.title('Comparison of Different Error Measures')
plt.legend()
plt.grid(True)
plt.show()
Pros and Cons of Squared Errors
Advantages:
- Differentiable everywhere
- Penalizes large errors more heavily
- Mathematically convenient for optimization
Disadvantages:
- Sensitive to outliers
- May not always reflect real-world error importance
import matplotlib.pyplot as plt
# Generate sample data with an outlier
np.random.seed(42)
x = np.linspace(0, 10, 20)
y = 2 * x + 1 + np.random.normal(0, 1, 20)
y[-1] += 10 # Add an outlier
# Fit models with different error measures
def fit_line(x, y, error_func):
best_m, best_b = 0, 0
min_error = float('inf')
for m in np.linspace(0, 5, 100):
for b in np.linspace(-5, 5, 100):
y_pred = m * x + b
error = error_func(y, y_pred)
if error < min_error:
min_error = error
best_m, best_b = m, b
return best_m, best_b
m_mse, b_mse = fit_line(x, y, lambda y, y_pred: np.mean((y - y_pred) ** 2))
m_mae, b_mae = fit_line(x, y, lambda y, y_pred: np.mean(np.abs(y - y_pred)))
# Plot the results
plt.figure(figsize=(12, 6))
plt.scatter(x, y, color='blue', label='Data points')
plt.plot(x, m_mse * x + b_mse, color='red', label='MSE fit')
plt.plot(x, m_mae * x + b_mae, color='green', label='MAE fit')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Comparison of MSE and MAE fits with an outlier')
plt.legend()
plt.grid(True)
plt.show()
Real-Life Example: Image Compression
In image compression, mean squared error (MSE) is often used to measure the quality of compressed images. Let's see how it works in practice.
import matplotlib.pyplot as plt
from skimage import data, img_as_float
from skimage.metrics import mean_squared_error
# Load a sample image
image = img_as_float(data.camera())
# Add some noise to simulate compression artifacts
noisy_image = image + 0.1 * np.random.randn(*image.shape)
noisy_image = np.clip(noisy_image, 0, 1)
# Calculate MSE
mse = mean_squared_error(image, noisy_image)
# Display the results
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
ax1.imshow(image, cmap='gray')
ax1.set_title('Original Image')
ax2.imshow(noisy_image, cmap='gray')
ax2.set_title(f'Noisy Image (MSE: {mse:.4f})')
plt.tight_layout()
plt.show()
Real-Life Example: Weather Prediction
In weather forecasting, mean squared error is used to evaluate the accuracy of temperature predictions. Let's simulate this scenario.
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
# Generate simulated weather data
days = np.arange(30)
actual_temps = 20 + 5 * np.sin(days / 7) + np.random.normal(0, 2, 30)
predicted_temps = 20 + 5 * np.sin(days / 7) + np.random.normal(0, 1, 30)
# Calculate MSE
mse = mean_squared_error(actual_temps, predicted_temps)
# Plot the results
plt.figure(figsize=(12, 6))
plt.plot(days, actual_temps, label='Actual Temperatures')
plt.plot(days, predicted_temps, label='Predicted Temperatures')
plt.xlabel('Day')
plt.ylabel('Temperature (°C)')
plt.title(f'Weather Prediction Accuracy (MSE: {mse:.2f})')
plt.legend()
plt.grid(True)
plt.show()
Gradient Descent and the Cost Function
The cost function's form is crucial for gradient descent optimization. Let's visualize how gradient descent works with our squared error cost function.
import matplotlib.pyplot as plt
# Define the cost function
def cost_function(m, b, x, y):
return np.mean((y - (m * x + b)) ** 2)
# Generate sample data
np.random.seed(42)
x = np.linspace(0, 10, 100)
y = 2 * x + 1 + np.random.normal(0, 1, 100)
# Gradient descent
m, b = 0, 0
learning_rate = 0.01
iterations = 1000
costs = []
for _ in range(iterations):
y_pred = m * x + b
error = y_pred - y
m -= learning_rate * np.mean(error * x)
b -= learning_rate * np.mean(error)
costs.append(cost_function(m, b, x, y))
# Plot the results
plt.figure(figsize=(12, 6))
plt.plot(range(iterations), costs)
plt.xlabel('Iteration')
plt.ylabel('Cost')
plt.title('Gradient Descent Optimization')
plt.yscale('log')
plt.grid(True)
plt.show()
print(f"Final parameters: m = {m:.2f}, b = {b:.2f}")
The Importance of Normalization
When using squared errors, it's crucial to normalize your data to prevent features with larger scales from dominating the cost function. Let's see the effect of normalization.
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
# Generate sample data
np.random.seed(42)
x1 = np.random.normal(0, 1, 100)
x2 = np.random.normal(0, 1000, 100)
y = 2 * x1 + 0.001 * x2 + np.random.normal(0, 0.1, 100)
# Calculate costs without normalization
costs_unnormalized = ((2 * x1 + 0.001 * x2) - y) ** 2
# Normalize the data
scaler = StandardScaler()
X_normalized = scaler.fit_transform(np.column_stack((x1, x2)))
costs_normalized = ((2 * X_normalized[:, 0] + 0.001 * X_normalized[:, 1]) - y) ** 2
# Plot the results
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
ax1.scatter(x1, costs_unnormalized, alpha=0.5, label='x1')
ax1.scatter(x2, costs_unnormalized, alpha=0.5, label='x2')
ax1.set_title('Costs without Normalization')
ax1.set_xlabel('Feature Value')
ax1.set_ylabel('Cost')
ax1.legend()
ax2.scatter(X_normalized[:, 0], costs_normalized, alpha=0.5, label='x1 (normalized)')
ax2.scatter(X_normalized[:, 1], costs_normalized, alpha=0.5, label='x2 (normalized)')
ax2.set_title('Costs with Normalization')
ax2.set_xlabel('Normalized Feature Value')
ax2.set_ylabel('Cost')
ax2.legend()
plt.tight_layout()
plt.show()
Regularization and the Cost Function
Regularization is often added to the cost function to prevent overfitting. Let's explore how L2 regularization (Ridge regression) affects the cost function.
import matplotlib.pyplot as plt
from sklearn.linear_model import Ridge
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
# Generate sample data
np.random.seed(42)
X = np.sort(np.random.rand(40, 1), axis=0)
y = np.cos(1.5 * np.pi * X).ravel() + np.random.randn(40) * 0.1
# Create models with different regularization strengths
alphas = [0, 0.001, 0.01, 0.1]
degrees = [1, 4, 15]
plt.figure(figsize=(14, 10))
for i, degree in enumerate(degrees):
ax = plt.subplot(3, 1, i + 1)
for alpha in alphas:
model = make_pipeline(PolynomialFeatures(degree), Ridge(alpha=alpha))
model.fit(X, y)
y_pred = model.predict(X)
plt.plot(X, y_pred, label=f'alpha = {alpha}')
plt.plot(X, y, 'r.', label='data', markersize=10)
plt.xlabel('x')
plt.ylabel('y')
plt.title(f'Degree {degree} Polynomial')
plt.legend(loc='lower left')
plt.tight_layout()
plt.show()
Cross-Validation and the Cost Function
Cross-validation helps us choose the best model by evaluating the cost function on different subsets of the data. Let's implement k-fold cross-validation.
from sklearn.model_selection import KFold
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
# Generate sample data
np.random.seed(42)
X = np.random.rand(100, 1)
y = 2 * X + 1 + np.random.randn(100, 1) * 0.1
# Perform k-fold cross-validation
kf = KFold(n_splits=5, shuffle=True, random_state=42)
mse_scores = []
for train_index, test_index in kf.split(X):
X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
mse_scores.append(mse)
print(f"Cross-validation MSE scores: {mse_scores}")
print(f"Average MSE: {np.mean(mse_scores):.4f}")
Hyperparameter Tuning with Grid Search
Grid search is a technique to find the best hyperparameters for a model by systematically working through multiple combinations of parameter tunes, cross-validating as it goes to determine which tune gives the best performance.
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import Ridge
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
# Generate sample data
np.random.seed(42)
X = np.random.rand(100, 1)
y = np.sin(2 * np.pi * X).ravel() + np.random.randn(100) * 0.1
# Create the pipeline
model = make_pipeline(PolynomialFeatures(), Ridge())
# Define the parameter grid
param_grid = {
'polynomialfeatures__degree': [1, 2, 3, 4],
'ridge__alpha': [0.01, 0.1, 1, 10]
}
# Perform grid search
grid_search = GridSearchCV(model, param_grid, cv=5, scoring='neg_mean_squared_error')
grid_search.fit(X, y)
print("Best parameters:", grid_search.best_params_)
print("Best MSE:", -grid_search.best_score_)
Learning Curves and Overfitting
Learning curves help us diagnose bias and variance problems in our models by showing how the training and validation errors change as we increase the amount of training data.
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
from sklearn.linear_model import Ridge
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
# Generate sample data
np.random.seed(42)
X = np.random.rand(200, 1)
y = np.sin(2 * np.pi * X).ravel() + np.random.randn(200) * 0.1
# Create models with different complexities
degrees = [1, 4, 15]
train_sizes = np.linspace(0.1, 1.0, 10)
plt.figure(figsize=(14, 5))
for i, degree in enumerate(degrees):
model = make_pipeline(PolynomialFeatures(degree), Ridge())
train_sizes, train_scores, valid_scores = learning_curve(
model, X, y, train_sizes=train_sizes, cv=5, scoring='neg_mean_squared_error'
)
train_scores_mean = -np.mean(train_scores, axis=1)
valid_scores_mean = -np.mean(valid_scores, axis=1)
plt.subplot(1, 3, i+1)
plt.plot(train_sizes, train_scores_mean, label='Training error')
plt.plot(train_sizes, valid_scores_mean, label='Validation error')
plt.title(f'Degree {degree} Polynomial')
plt.xlabel('Training set size')
plt.ylabel('Mean Squared Error')
plt.legend()
plt.tight_layout()
plt.show()
Additional Resources
For those interested in diving deeper into the mathematics behind machine learning cost functions and optimization techniques, here are some valuable resources:
- "Understanding Machine Learning: From Theory to Algorithms" by Shalev-Shwartz and Ben-David. Available at: https://arxiv.org/abs/1406.0923
- "Optimization Methods for Large-Scale Machine Learning" by Bottou, Curtis, and Nocedal. Available at: https://arxiv.org/abs/1606.04838
These papers provide in-depth discussions on the theoretical foundations and practical applications of cost functions in machine learning.