Files
FYS-STK4155/doc/Programs/EnsembleMethods/ensemblemethods.py
T
Morten Hjorth-Jensen abf8c01dce new updates
2025-05-29 17:56:20 +02:00

484 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
class DecisionTreeClassifier:
def __init__(self, criterion='gini', max_depth=None, min_samples_split=2, max_features=None):
self.criterion = criterion
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.max_features = max_features
self.tree = None
class Node:
def __init__(self, feature=None, threshold=None, left=None, right=None, *, value=None):
self.feature = feature
self.threshold = threshold
self.left = left
self.right = right
self.value = value # Leaf class label
def fit(self, X, y):
X, y = np.array(X), np.array(y)
self.n_features_ = X.shape[1]
self.tree = self._build_tree(X, y, depth=0)
return self
def _build_tree(self, X, y, depth):
num_samples, _ = X.shape
# Stop if conditions met
if num_samples < self.min_samples_split or (self.max_depth is not None and depth >= self.max_depth) or len(np.unique(y)) == 1:
leaf_val = self._majority_class(y)
return DecisionTreeClassifier.Node(value=leaf_val)
# Find best split
feat_idx, thr = self._best_split(X, y)
if feat_idx is None:
leaf_val = self._majority_class(y)
return DecisionTreeClassifier.Node(value=leaf_val)
# Split data
left_mask = X[:, feat_idx] <= thr
left_node = self._build_tree(X[left_mask], y[left_mask], depth+1)
right_node = self._build_tree(X[~left_mask], y[~left_mask], depth+1)
return DecisionTreeClassifier.Node(feature=feat_idx, threshold=thr, left=left_node, right=right_node)
def _best_split(self, X, y):
best_gain = 0
best_feat, best_thr = None, None
if self.criterion == 'gini':
base_impurity = self._gini(y)
else:
base_impurity = self._entropy(y)
n_features = X.shape[1]
features = range(n_features)
# Possibly sample subset of features (for Random Forest use)
if self.max_features is not None:
if isinstance(self.max_features, int) and self.max_features < n_features:
features = np.random.choice(n_features, self.max_features, replace=False)
elif isinstance(self.max_features, float):
k = int(n_features * self.max_features)
features = np.random.choice(n_features, k, replace=False)
for feat in features:
X_col = X[:, feat]
unique_vals = np.unique(X_col)
if len(unique_vals) <= 1:
continue
# Try midpoints between sorted unique values
thresholds = (unique_vals[:-1] + unique_vals[1:]) / 2.0
for thr in thresholds:
left_mask = X_col <= thr
y_left, y_right = y[left_mask], y[~left_mask]
if len(y_left) == 0 or len(y_right) == 0:
continue
# Compute impurity of the split
if self.criterion == 'gini':
imp_left = self._gini(y_left)
imp_right = self._gini(y_right)
else:
imp_left = self._entropy(y_left)
imp_right = self._entropy(y_right)
p = float(len(y_left)) / len(y)
gain = base_impurity - (p * imp_left + (1 - p) * imp_right)
if gain > best_gain:
best_gain, best_feat, best_thr = gain, feat, thr
return best_feat, best_thr
def _gini(self, y):
_, counts = np.unique(y, return_counts=True)
p = counts / counts.sum()
return 1.0 - np.sum(p**2)
def _entropy(self, y):
_, counts = np.unique(y, return_counts=True)
p = counts / counts.sum()
p = p[p > 0]
return -np.sum(p * np.log2(p))
def _majority_class(self, y):
unique, counts = np.unique(y, return_counts=True)
return unique[np.argmax(counts)]
def predict(self, X):
X = np.array(X)
return np.array([self._predict_input(x, self.tree) for x in X])
def _predict_input(self, x, node):
if node.value is not None:
return node.value
if x[node.feature] <= node.threshold:
return self._predict_input(x, node.left)
else:
return self._predict_input(x, node.right)
class DecisionTreeRegressor:
def __init__(self, max_depth=None, min_samples_split=2, max_features=None):
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.max_features = max_features
self.tree = None
class Node:
def __init__(self, feature=None, threshold=None, left=None, right=None, *, value=None):
self.feature = feature
self.threshold = threshold
self.left = left
self.right = right
self.value = value # Leaf output value
def fit(self, X, y):
X, y = np.array(X), np.array(y, dtype=float)
self.n_features_ = X.shape[1]
self.tree = self._build_tree(X, y, depth=0)
return self
def _build_tree(self, X, y, depth):
num_samples, _ = X.shape
if num_samples < self.min_samples_split or (self.max_depth is not None and depth >= self.max_depth) or np.var(y) == 0:
leaf_val = np.mean(y)
return DecisionTreeRegressor.Node(value=leaf_val)
best_sse = float('inf')
best_feat, best_thr = None, None
n_features = X.shape[1]
features = range(n_features)
if self.max_features is not None:
if isinstance(self.max_features, int) and self.max_features < n_features:
features = np.random.choice(n_features, self.max_features, replace=False)
elif isinstance(self.max_features, float):
k = int(n_features * self.max_features)
features = np.random.choice(n_features, k, replace=False)
for feat in features:
X_col = X[:, feat]
unique_vals = np.unique(X_col)
if len(unique_vals) <= 1:
continue
thresholds = (unique_vals[:-1] + unique_vals[1:]) / 2.0
for thr in thresholds:
left_mask = X_col <= thr
y_left, y_right = y[left_mask], y[~left_mask]
if len(y_left) == 0 or len(y_right) == 0:
continue
# Compute sum of squared errors (SSE)
left_mean, right_mean = np.mean(y_left), np.mean(y_right)
sse_left = np.sum((y_left - left_mean) ** 2)
sse_right = np.sum((y_right - right_mean) ** 2)
sse = sse_left + sse_right
if sse < best_sse:
best_sse, best_feat, best_thr = sse, feat, thr
if best_feat is None:
leaf_val = np.mean(y)
return DecisionTreeRegressor.Node(value=leaf_val)
left_mask = X[:, best_feat] <= best_thr
left_node = self._build_tree(X[left_mask], y[left_mask], depth+1)
right_node = self._build_tree(X[~left_mask], y[~left_mask], depth+1)
return DecisionTreeRegressor.Node(feature=best_feat, threshold=best_thr, left=left_node, right=right_node)
def predict(self, X):
X = np.array(X)
return np.array([self._predict_input(x, self.tree) for x in X])
def _predict_input(self, x, node):
if node.value is not None:
return node.value
if x[node.feature] <= node.threshold:
return self._predict_input(x, node.left)
else:
return self._predict_input(x, node.right)
# Random Forests (Classification and Regression)
"""
Random forests train an ensemble of decision trees on bootstrapped data subsets and average their outputs . For classification, the final class is the majority vote of all trees; for regression, the output is the average prediction . Key points:
Bootstrap sampling: Each tree is trained on a random sample (with replacement) of the data.
Feature randomness: When splitting, each node may consider only a random subset of features (parameter max_features).
Aggregation: Classification uses mode of tree predictions; regression uses mean. This reduces overfitting compared to a single tree .
"""
import numpy as np
from collections import Counter
class RandomForestClassifier:
def __init__(self, n_estimators=100, max_depth=None, min_samples_split=2, max_features='sqrt'):
self.n_estimators = n_estimators
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.max_features = max_features
self.trees = []
def fit(self, X, y):
X, y = np.array(X), np.array(y)
n_samples, n_features = X.shape
# Determine how many features to try at each split
if self.max_features == 'sqrt':
max_feats = int(np.sqrt(n_features))
elif self.max_features == 'log2':
max_feats = int(np.log2(n_features))
elif isinstance(self.max_features, int):
max_feats = self.max_features
elif isinstance(self.max_features, float):
max_feats = int(n_features * self.max_features)
else:
max_feats = n_features
# Build trees
for _ in range(self.n_estimators):
indices = np.random.choice(n_samples, n_samples, replace=True)
X_sample, y_sample = X[indices], y[indices]
tree = DecisionTreeClassifier(criterion='gini',
max_depth=self.max_depth,
min_samples_split=self.min_samples_split,
max_features=max_feats)
tree.fit(X_sample, y_sample)
self.trees.append(tree)
return self
def predict(self, X):
X = np.array(X)
# Collect predictions from all trees
tree_preds = np.array([tree.predict(X) for tree in self.trees]).T # shape (n_samples, n_trees)
y_pred = []
for preds in tree_preds:
vote = Counter(preds).most_common(1)[0][0]
y_pred.append(vote)
return np.array(y_pred)
class RandomForestRegressor:
def __init__(self, n_estimators=100, max_depth=None, min_samples_split=2, max_features=None):
self.n_estimators = n_estimators
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.max_features = max_features
self.trees = []
def fit(self, X, y):
X, y = np.array(X), np.array(y, dtype=float)
n_samples, n_features = X.shape
if self.max_features == 'sqrt':
max_feats = int(np.sqrt(n_features))
elif self.max_features == 'log2':
max_feats = int(np.log2(n_features))
elif isinstance(self.max_features, int):
max_feats = self.max_features
elif isinstance(self.max_features, float):
max_feats = int(n_features * self.max_features)
else:
max_feats = n_features
for _ in range(self.n_estimators):
indices = np.random.choice(n_samples, n_samples, replace=True)
X_sample, y_sample = X[indices], y[indices]
tree = DecisionTreeRegressor(max_depth=self.max_depth,
min_samples_split=self.min_samples_split,
max_features=max_feats)
tree.fit(X_sample, y_sample)
self.trees.append(tree)
return self
def predict(self, X):
X = np.array(X)
# Average predictions from all trees
tree_preds = np.array([tree.predict(X) for tree in self.trees])
return np.mean(tree_preds, axis=0)
# Gradient Boosting (Classification and Regression)
"""
Gradient boosting builds an additive ensemble of trees by fitting each new tree on the residuals (errors) of the existing model, effectively performing gradient descent on a loss function . At each stage m:
Compute the pseudo-residuals r_{im} = -\partial L(y_i, F(x_i)) / \partial F(x_i) (the negative gradient) .
Fit a tree h_m(x) to these residuals.
Update the model: F_m(x) = F_{m-1}(x) + \gamma_m \, h_m(x) (with line-search multiplier \gamma_m or simply a learning rate).
For binary classification, we use the logistic loss: initialize F_0 = \log(p/(1-p)) (log-odds of positive class) and repeatedly fit trees to y - \sigma(F). For multiclass, we fit one-vs-rest models (one boosting ensemble per class) and predict the class with highest score. Key concepts:
Additive updates: Each trees prediction is scaled by a learning rate and added to the ensemble output.
Regression loss: Typically squared-error (L2) for regression, logistic (cross-entropy) for classification.
Weak learners: Trees are often shallow (small max_depth).
"""
import numpy as np
from math import log, exp
class GradientBoostingRegressor:
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.trees = []
self.gammas = []
self.initial_prediction = None
def fit(self, X, y):
X, y = np.array(X), np.array(y, dtype=float)
# Initialize prediction with mean
self.initial_prediction = np.mean(y)
F = np.full_like(y, fill_value=self.initial_prediction, dtype=float)
for _ in range(self.n_estimators):
residual = y - F
tree = DecisionTreeRegressor(max_depth=self.max_depth)
tree.fit(X, residual)
pred = tree.predict(X)
# Line search for optimal multiplier gamma
gamma = np.dot(residual, pred) / (np.dot(pred, pred) + 1e-8)
F = F + self.learning_rate * gamma * pred
self.trees.append(tree)
self.gammas.append(gamma)
return self
def predict(self, X):
X = np.array(X)
F = np.full(X.shape[0], fill_value=self.initial_prediction, dtype=float)
for tree, gamma in zip(self.trees, self.gammas):
F += self.learning_rate * gamma * tree.predict(X)
return F
class GradientBoostingClassifier:
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.models = [] # For multiclass, one model per class
def fit(self, X, y):
X, y = np.array(X), np.array(y)
self.classes_ = np.unique(y)
if len(self.classes_) <= 2:
# Binary classification (labels may be 0/1 or not)
# Map labels to 0/1
if set(self.classes_) != {0, 1}:
class0, class1 = self.classes_[0], self.classes_[1]
y_bin = np.array([0 if yi==class0 else 1 for yi in y])
self.class_map = {0: class0, 1: class1}
else:
y_bin = y
self.class_map = None
# Initialize log-odds
p = np.clip(np.mean(y_bin), 1e-6, 1-1e-6)
F = np.full(y_bin.shape, fill_value=log(p/(1-p)), dtype=float)
self.initial_F = F[0]
self.trees = []
for _ in range(self.n_estimators):
P = 1 / (1 + np.exp(-F))
residual = y_bin - P
tree = DecisionTreeRegressor(max_depth=self.max_depth)
tree.fit(X, residual)
pred = tree.predict(X)
F = F + self.learning_rate * pred
self.trees.append(tree)
else:
# Multiclass one-vs-rest
for cls in self.classes_:
y_binary = (y == cls).astype(int)
model = GradientBoostingClassifier(n_estimators=self.n_estimators,
learning_rate=self.learning_rate,
max_depth=self.max_depth)
model.fit(X, y_binary)
self.models.append(model)
return self
def predict(self, X):
X = np.array(X)
if len(self.classes_) <= 2:
# Binary
F = np.full(X.shape[0], fill_value=self.initial_F, dtype=float)
for tree in self.trees:
F += self.learning_rate * tree.predict(X)
P = 1 / (1 + np.exp(-F))
y_pred = (P >= 0.5).astype(int)
if self.class_map:
inv_map = {v:k for k,v in self.class_map.items()}
y_pred = np.array([inv_map[val] for val in y_pred])
return y_pred
else:
# Multiclass: compute score for each class
scores = []
for model in self.models:
F_cls = np.full(X.shape[0], fill_value=model.initial_F, dtype=float)
for tree in model.trees:
F_cls += self.learning_rate * tree.predict(X)
scores.append(F_cls)
scores = np.vstack(scores).T # shape (n_samples, n_classes)
class_idx = np.argmax(scores, axis=1)
return np.array([self.classes_[i] for i in class_idx])
# Classification Example: Generate a 2-class dataset, train models, and evaluate accuracy.
import numpy as np
import csv
# Synthetic binary classification data
np.random.seed(0)
N = 100
# Class 0 centered at (-2, -2), Class 1 at (2, 2)
X0 = np.random.randn(N, 2) - 2
X1 = np.random.randn(N, 2) + 2
X_clf = np.vstack([X0, X1])
y_clf = np.array([0]*N + [1]*N)
# Shuffle data
perm = np.random.permutation(len(y_clf))
X_clf, y_clf = X_clf[perm], y_clf[perm]
# Train models
dt_clf = DecisionTreeClassifier(max_depth=3)
dt_clf.fit(X_clf, y_clf)
rf_clf = RandomForestClassifier(n_estimators=10, max_depth=3)
rf_clf.fit(X_clf, y_clf)
gb_clf = GradientBoostingClassifier(n_estimators=20, learning_rate=0.1, max_depth=2)
gb_clf.fit(X_clf, y_clf)
# Predictions
pred_dt = dt_clf.predict(X_clf)
pred_rf = rf_clf.predict(X_clf)
pred_gb = gb_clf.predict(X_clf)
# Accuracy evaluation
acc_dt = np.mean(pred_dt == y_clf)
acc_rf = np.mean(pred_rf == y_clf)
acc_gb = np.mean(pred_gb == y_clf)
print(f"Decision Tree Accuracy: {acc_dt:.2f}")
print(f"Random Forest Accuracy: {acc_rf:.2f}")
print(f"Gradient Boosting Accuracy: {acc_gb:.2f}")
# Export to CSV
with open('classification_results.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['GroundTruth', 'DT_Pred', 'RF_Pred', 'GB_Pred'])
for true, d, r, g in zip(y_clf, pred_dt, pred_rf, pred_gb):
writer.writerow([true, d, r, g])
# Regression Example: Generate a simple regression dataset, train models, and compute MSE.
import numpy as np
import csv
# Synthetic regression data: y = 3*x1 - 2*x2 + noise
np.random.seed(1)
N = 200
X_reg = np.random.randn(N, 2)
y_reg = 3 * X_reg[:,0] - 2 * X_reg[:,1] + np.random.randn(N) * 0.5
# Train models
dt_reg = DecisionTreeRegressor(max_depth=4)
dt_reg.fit(X_reg, y_reg)
rf_reg = RandomForestRegressor(n_estimators=10, max_depth=4)
rf_reg.fit(X_reg, y_reg)
gb_reg = GradientBoostingRegressor(n_estimators=50, learning_rate=0.1, max_depth=2)
gb_reg.fit(X_reg, y_reg)
# Predictions
pred_dt_r = dt_reg.predict(X_reg)
pred_rf_r = rf_reg.predict(X_reg)
pred_gb_r = gb_reg.predict(X_reg)
# MSE evaluation
mse_dt = np.mean((pred_dt_r - y_reg)**2)
mse_rf = np.mean((pred_rf_r - y_reg)**2)
mse_gb = np.mean((pred_gb_r - y_reg)**2)
print(f"Decision Tree MSE: {mse_dt:.3f}")
print(f"Random Forest MSE: {mse_rf:.3f}")
print(f"Gradient Boosting MSE: {mse_gb:.3f}")
# Export to CSV
with open('regression_results.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['GroundTruth', 'DT_Pred', 'RF_Pred', 'GB_Pred'])
for true, d, r, g in zip(y_reg, pred_dt_r, pred_rf_r, pred_gb_r):
writer.writerow([true, d, r, g])