RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Gradient Boosting

S
# gradient_boosting.r
# Gradient Boosting Algorithm Implementation in R
# A sequential ensemble learning method that builds models iteratively
# Each new model corrects errors made by previous models
#
# Algorithm details:
# - Uses decision trees as weak learners
# - Fits each tree to the residuals (errors) of previous predictions
# - Combines predictions using weighted sum
# - Learning rate controls contribution of each tree
# - Time complexity: O(n_trees * n_samples * log(n_samples))
# - Space complexity: O(n_trees * tree_size)

library(R6)

#' Decision Tree Node
#' Simple decision tree implementation for gradient boosting
DecisionTreeNode <- R6Class(
  "DecisionTreeNode",
  
  public = list(
    feature = NULL,
    threshold = NULL,
    left = NULL,
    right = NULL,
    value = NULL,
    
    initialize = function(feature = NULL, threshold = NULL, 
                         left = NULL, right = NULL, value = NULL) {
      self$feature <- feature
      self$threshold <- threshold
      self$left <- left
      self$right <- right
      self$value <- value
    },
    
    is_leaf = function() {
      return(!is.null(self$value))
    }
  )
)

#' Regression Tree for Gradient Boosting
RegressionTree <- R6Class(
  "RegressionTree",
  
  public = list(
    max_depth = NULL,
    min_samples_split = NULL,
    root = NULL,
    
    initialize = function(max_depth = 3, min_samples_split = 2) {
      self$max_depth <- max_depth
      self$min_samples_split <- min_samples_split
      self$root <- NULL
    },
    
    fit = function(X, y) {
      "Build the decision tree"
      self$root <- private$build_tree(X, y, depth = 0)
      invisible(self)
    },
    
    predict = function(X) {
      "Predict values for input data"
      if (is.vector(X)) {
        X <- matrix(X, nrow = 1)
      }
      apply(X, 1, function(row) private$predict_sample(row, self$root))
    }
  ),
  
  private = list(
    build_tree = function(X, y, depth) {
      "Recursively build decision tree"
      n_samples <- nrow(X)
      
      # Stopping criteria
      if (depth >= self$max_depth || 
          n_samples < self$min_samples_split ||
          length(unique(y)) == 1) {
        return(DecisionTreeNode$new(value = mean(y)))
      }
      
      # Find best split
      best_split <- private$find_best_split(X, y)
      
      if (is.null(best_split)) {
        return(DecisionTreeNode$new(value = mean(y)))
      }
      
      # Split data
      left_idx <- X[, best_split$feature] <= best_split$threshold
      right_idx <- !left_idx
      
      # Build subtrees
      left_subtree <- private$build_tree(
        X[left_idx, , drop = FALSE], 
        y[left_idx], 
        depth + 1
      )
      right_subtree <- private$build_tree(
        X[right_idx, , drop = FALSE], 
        y[right_idx], 
        depth + 1
      )
      
      return(DecisionTreeNode$new(
        feature = best_split$feature,
        threshold = best_split$threshold,
        left = left_subtree,
        right = right_subtree
      ))
    },
    
    find_best_split = function(X, y) {
      "Find the best feature and threshold to split on"
      best_mse <- Inf
      best_split <- NULL
      n_features <- ncol(X)
      n_samples <- nrow(X)
      min_samples_leaf <- max(1, floor(self$min_samples_split / 2))
      
      for (feature in 1:n_features) {
        # Sort feature values and corresponding targets
        sorted_idx <- order(X[, feature])
        sorted_x <- X[sorted_idx, feature]
        sorted_y <- y[sorted_idx]
        
        # Consider only unique values as potential thresholds
        unique_vals <- unique(sorted_x)
        if (length(unique_vals) <= 1) next
        
        # Pre-compute cumulative statistics
        cum_sum <- cumsum(sorted_y)
        cum_sum_sq <- cumsum(sorted_y^2)
        
        # Evaluate splits between unique values
        for (i in 1:(length(unique_vals)-1)) {
          threshold <- (unique_vals[i] + unique_vals[i+1]) / 2
          split_idx <- which(sorted_x <= threshold)
          n_left <- length(split_idx)
          n_right <- n_samples - n_left
          
          # Skip if split doesn't meet minimum samples requirement
          if (n_left < min_samples_leaf || n_right < min_samples_leaf) next
          
          # Calculate MSE using pre-computed statistics
          left_sum <- cum_sum[n_left]
          left_sum_sq <- cum_sum_sq[n_left]
          right_sum <- cum_sum[n_samples] - left_sum
          right_sum_sq <- cum_sum_sq[n_samples] - left_sum_sq
          
          left_mse <- (left_sum_sq - (left_sum^2)/n_left)
          right_mse <- (right_sum_sq - (right_sum^2)/n_right)
          mse <- left_mse + right_mse
          
          if (mse < best_mse) {
            best_mse <- mse
            best_split <- list(feature = feature, threshold = threshold)
          }
        }
      }
      
      return(best_split)
    },
    
    predict_sample = function(x, node) {
      "Predict single sample by traversing tree"
      if (node$is_leaf()) {
        return(node$value)
      }
      
      if (x[node$feature] <= node$threshold) {
        return(private$predict_sample(x, node$left))
      } else {
        return(private$predict_sample(x, node$right))
      }
    }
  )
)

#' Gradient Boosting Regressor
GradientBoostingRegressor <- R6Class(
  "GradientBoostingRegressor",
  
  public = list(
    n_estimators = NULL,
    learning_rate = NULL,
    max_depth = NULL,
    min_samples_split = NULL,
    trees = NULL,
    initial_prediction = NULL,
    early_stopping_rounds = NULL,
    best_iteration = NULL,
    
    initialize = function(n_estimators = 100, learning_rate = 0.1, 
                         max_depth = 3, min_samples_split = 2,
                         early_stopping_rounds = NULL) {
      "Initialize gradient boosting parameters"
      if (n_estimators <= 0 || learning_rate <= 0 || max_depth <= 0) {
        stop("Parameters must be positive")
      }
      
      self$n_estimators <- n_estimators
      self$learning_rate <- learning_rate
      self$max_depth <- max_depth
      self$min_samples_split <- min_samples_split
      self$trees <- list()
      self$initial_prediction <- NULL
    },
    
    fit = function(X, y, verbose = FALSE) {
      "Train the gradient boosting model"
      # Input validation
      if (!is.numeric(y)) {
        stop("Target variable 'y' must be numeric")
      }
      if (is.vector(X)) {
        X <- matrix(X, ncol = 1)
      }
      if (!is.matrix(X) || !is.numeric(X)) {
        stop("Input 'X' must be a numeric matrix or vector")
      }
      if (length(y) != nrow(X)) {
        stop("Number of samples in X and y must match")
      }
      if (any(is.na(X)) || any(is.na(y))) {
        stop("Input contains missing values")
      }
      
      # Initialize with mean of target
      self$initial_prediction <- mean(y)
      predictions <- rep(self$initial_prediction, length(y))
      
      # Build trees sequentially
      for (i in 1:self$n_estimators) {
        # Calculate residuals (negative gradient for MSE loss)
        residuals <- y - predictions
        
        # Fit tree to residuals
        tree <- RegressionTree$new(
          max_depth = self$max_depth,
          min_samples_split = self$min_samples_split
        )
        tree$fit(X, residuals)
        
        # Update predictions
        tree_predictions <- tree$predict(X)
        predictions <- predictions + self$learning_rate * tree_predictions
        
        # Store tree
        self$trees[[i]] <- tree
        
        # Calculate training error
        if (verbose && (i %% 10 == 0 || i == 1)) {
          mse <- mean((y - predictions)^2)
          rmse <- sqrt(mse)
          cat(sprintf("Iteration %d/%d - RMSE: %.4f\n", 
                     i, self$n_estimators, rmse))
        }
      }
      
      invisible(self)
    },
    
    predict = function(X) {
      "Make predictions using the trained model"
      if (is.null(self$initial_prediction)) {
        stop("Model has not been fitted yet")
      }
      
      if (is.vector(X)) {
        X <- matrix(X, ncol = 1)
      }
      
      # Start with initial prediction
      predictions <- rep(self$initial_prediction, nrow(X))
      
      # Add contribution from each tree
      for (tree in self$trees) {
        predictions <- predictions + self$learning_rate * tree$predict(X)
      }
      
      return(predictions)
    },
    
    score = function(X, y) {
      "Calculate R-squared score"
      predictions <- self$predict(X)
      ss_res <- sum((y - predictions)^2)
      ss_tot <- sum((y - mean(y))^2)
      r2 <- 1 - (ss_res / ss_tot)
      return(r2)
    },
    
    get_feature_importance = function() {
      "Calculate relative feature importance based on split frequency and gain"
      if (length(self$trees) == 0) {
        stop("Model has not been fitted yet")
      }
      
      # Count feature usage and gain in splits
      n_features <- ncol(X)  # Assumes X from last fit
      importance <- rep(0, n_features)
      names(importance) <- paste0("Feature_", 1:n_features)
      
      calculate_tree_importance <- function(node, depth = 0) {
        if (is.null(node) || node$is_leaf()) {
          return(NULL)
        }
        
        # Add importance score based on depth (earlier splits are more important)
        feature_idx <- node$feature
        importance[feature_idx] <<- importance[feature_idx] + 1 / (depth + 1)
        
        calculate_tree_importance(node$left, depth + 1)
        calculate_tree_importance(node$right, depth + 1)
      }
      
      # Calculate importance for each tree
      for (tree in self$trees) {
        calculate_tree_importance(tree$root)
      }
      
      # Normalize importance scores
      if (sum(importance) > 0) {
        importance <- importance / sum(importance)
      }
      
      # Sort and return as named vector
      importance <- sort(importance, decreasing = TRUE)
      return(importance)
    }
  )
)

# Demonstration and testing
demonstrate_gradient_boosting <- function() {
  cat("=== Gradient Boosting Algorithm Demo ===\n\n")
  
  # Generate synthetic dataset
  set.seed(42)
  n_samples <- 200
  
  cat("Example 1: Non-linear regression problem\n")
  cat("Generating synthetic data...\n\n")
  
  # Create non-linear relationship
  X <- matrix(runif(n_samples, -3, 3), ncol = 1)
  y <- sin(X[, 1]) + 0.3 * X[, 1]^2 + rnorm(n_samples, 0, 0.1)
  
  # Split into train and test
  train_idx <- sample(1:n_samples, size = 0.8 * n_samples)
  test_idx <- setdiff(1:n_samples, train_idx)
  
  X_train <- matrix(X[train_idx, ], ncol = 1)
  y_train <- y[train_idx]
  X_test <- matrix(X[test_idx, ], ncol = 1)
  y_test <- y[test_idx]
  
  # Train model
  cat("Training Gradient Boosting model...\n")
  model <- GradientBoostingRegressor$new(
    n_estimators = 50,
    learning_rate = 0.1,
    max_depth = 3
  )
  model$fit(X_train, y_train, verbose = TRUE)
  
  # Make predictions
  cat("\nEvaluating model...\n")
  train_pred <- model$predict(X_train)
  test_pred <- model$predict(X_test)
  
  # Calculate metrics
  train_rmse <- sqrt(mean((y_train - train_pred)^2))
  test_rmse <- sqrt(mean((y_test - test_pred)^2))
  train_r2 <- model$score(X_train, y_train)
  test_r2 <- model$score(X_test, y_test)
  
  cat(sprintf("\nResults:\n"))
  cat(sprintf("Train RMSE: %.4f | Train R²: %.4f\n", train_rmse, train_r2))
  cat(sprintf("Test RMSE:  %.4f | Test R²:  %.4f\n\n", test_rmse, test_r2))
  
  # Example 2: Multi-feature problem
  cat("Example 2: Multi-feature regression\n")
  cat("Generating multi-dimensional data...\n")
  
  X_multi <- matrix(rnorm(n_samples * 3), ncol = 3)
  y_multi <- 2 * X_multi[, 1] - 3 * X_multi[, 2] + 
             0.5 * X_multi[, 3]^2 + rnorm(n_samples, 0, 0.5)
  
  # Train-test split
  X_train_multi <- X_multi[train_idx, ]
  y_train_multi <- y_multi[train_idx]
  X_test_multi <- X_multi[test_idx, ]
  y_test_multi <- y_multi[test_idx]
  
  # Train model
  model2 <- GradientBoostingRegressor$new(
    n_estimators = 50,
    learning_rate = 0.1,
    max_depth = 4
  )
  model2$fit(X_train_multi, y_train_multi)
  
  # Evaluate
  test_rmse2 <- sqrt(mean((y_test_multi - model2$predict(X_test_multi))^2))
  test_r2_2 <- model2$score(X_test_multi, y_test_multi)
  
  cat(sprintf("Test RMSE: %.4f | Test R²: %.4f\n\n", test_rmse2, test_r2_2))
  
  # Example 3: Hyperparameter comparison
  cat("Example 3: Impact of learning rate\n")
  learning_rates <- c(0.01, 0.1, 0.5)
  
  for (lr in learning_rates) {
    model_lr <- GradientBoostingRegressor$new(
      n_estimators = 30,
      learning_rate = lr,
      max_depth = 3
    )
    model_lr$fit(X_train, y_train, verbose = FALSE)
    test_rmse_lr <- sqrt(mean((y_test - model_lr$predict(X_test))^2))
    cat(sprintf("Learning rate %.2f - Test RMSE: %.4f\n", lr, test_rmse_lr))
  }
  
  cat("\n=== Demo Complete ===\n")
}

# Run demonstration
if (!interactive()) {
  demonstrate_gradient_boosting()
}