LAB 010 — Random Forests & Gradient Boosting

Θα εξετάσουμε το dataset BreastCancer (πακέτο mlbench) ώστε να χτίσουμε ένα μοντέλο που θα προβλέπει αν ένας όγκος είναι καλοήθης (benign) ή κακοήθης (malignant).

Το dataset έχει 699 παρατηρήσεις, με 9 features και binary classification (benign/malignant).

Θέλουμε το μοντέλο να είναι ακριβές, να προσφέρει ερμηνευσιμότητα ως προς τα χαρακτηριστικά που μετράνε και να έχει υψηλή ευαισθησία, ώστε να μην χάνονται περιπτώσεις κακοήθειας.

Setup

Φορτώνουμε τα απαραίτητα πακέτα και το dataset BreastCancer.

Στη συνέχεια καθαρίζουμε τα δεδομένα αφαιρώντας το ID, διαχειριζόμαστε τις τιμές που λείπουν και μετατρέπουμε τα χαρακτηριστικά σε αριθμητική μορφή ώστε να μπορούν να χρησιμοποιηθούν από τα μοντέλα.

# --- Φόρτωση πακέτων ---
library(tidyverse)
library(mlbench)
library(randomForest)
library(xgboost)
library(caret)
library(pROC)

set.seed(42)

# --- Φόρτωση δεδομένων ---
data("BreastCancer", package = "mlbench")
bc <- BreastCancer

# Καθαρισμός: αφαίρεση ID, χειρισμός missing, μετατροπή σε numeric
bc$Id <- NULL
bc <- na.omit(bc)
bc[, 1:9] <- lapply(bc[, 1:9], function(x) as.numeric(as.character(x)))

str(bc)
## 'data.frame':    683 obs. of  10 variables:
##  $ Cl.thickness   : num  5 5 3 6 4 8 1 2 2 4 ...
##  $ Cell.size      : num  1 4 1 8 1 10 1 1 1 2 ...
##  $ Cell.shape     : num  1 4 1 8 1 10 1 2 1 1 ...
##  $ Marg.adhesion  : num  1 5 1 1 3 8 1 1 1 1 ...
##  $ Epith.c.size   : num  2 7 2 3 2 7 2 2 2 2 ...
##  $ Bare.nuclei    : num  1 10 2 4 1 10 10 1 1 1 ...
##  $ Bl.cromatin    : num  3 3 3 3 3 9 3 3 1 2 ...
##  $ Normal.nucleoli: num  1 2 1 7 1 7 1 1 1 1 ...
##  $ Mitoses        : num  1 1 1 1 1 1 1 1 5 1 ...
##  $ Class          : Factor w/ 2 levels "benign","malignant": 1 1 1 1 1 2 1 1 1 1 ...
##  - attr(*, "na.action")= 'omit' Named int [1:16] 24 41 140 146 159 165 236 250 276 293 ...
##   ..- attr(*, "names")= chr [1:16] "24" "41" "140" "146" ...
table(bc$Class)
## 
##    benign malignant 
##       444       239

Μέρος Α - Baseline με Random Forest

Εκπαίδευση μοντέλου

Διαχωρίζουμε τα δεδομένα σε train και test σύνολο (70/30) με stratified sampling, ώστε να διατηρείται η αναλογία των κλάσεων.

# todo 1
trainIndex <- createDataPartition(bc$Class, p = 0.7, list = FALSE)

trainData <- bc[trainIndex, ]
testData  <- bc[-trainIndex, ]

Εκπαιδεύουμε ένα μοντέλο Random Forest με 500 δέντρα για την πρόβλεψη της μεταβλητής Class.

# todo 2
rf_model <- randomForest(Class ~ ., 
                         data = trainData, 
                         ntree = 500, 
                         importance = TRUE)

print(rf_model)
## 
## Call:
##  randomForest(formula = Class ~ ., data = trainData, ntree = 500,      importance = TRUE) 
##                Type of random forest: classification
##                      Number of trees: 500
## No. of variables tried at each split: 3
## 
##         OOB estimate of  error rate: 3.34%
## Confusion matrix:
##           benign malignant class.error
## benign       301        10  0.03215434
## malignant      6       162  0.03571429

Αξιολόγηση

Αξιολογούμε το μοντέλο στο test set υπολογίζοντας τις τιμές Accuracy, Sensitivity και AUC.

Το confusion matrix μας επιτρέπει να δούμε αναλυτικά τα σωστά και λάθος classifications.

# todo 3
# Predictions
rf_pred <- predict(rf_model, testData)
rf_prob <- predict(rf_model, testData, type = "prob")[,2]

# Confusion Matrix
cm <- confusionMatrix(rf_pred, testData$Class)
cm
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  benign malignant
##   benign       131         2
##   malignant      2        69
##                                           
##                Accuracy : 0.9804          
##                  95% CI : (0.9506, 0.9946)
##     No Information Rate : 0.652           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9568          
##                                           
##  Mcnemar's Test P-Value : 1               
##                                           
##             Sensitivity : 0.9850          
##             Specificity : 0.9718          
##          Pos Pred Value : 0.9850          
##          Neg Pred Value : 0.9718          
##              Prevalence : 0.6520          
##          Detection Rate : 0.6422          
##    Detection Prevalence : 0.6520          
##       Balanced Accuracy : 0.9784          
##                                           
##        'Positive' Class : benign          
## 
# Metrics
accuracy_rf <- cm$overall["Accuracy"]
sensitivity_rf <- cm$byClass["Sensitivity"]

# ROC & AUC
roc_rf <- roc(testData$Class, rf_prob, levels = rev(levels(testData$Class)))
## Setting direction: controls > cases
auc_rf <- auc(roc_rf)

accuracy_rf
##  Accuracy 
## 0.9803922
sensitivity_rf
## Sensitivity 
##   0.9849624
auc_rf
## Area under the curve: 0.9982

Εξετάζουμε τη σημασία των χαρακτηριστικών (feature importance) για να κατανοήσουμε ποιες μεταβλητές επηρεάζουν περισσότερο την πρόβλεψη.

# todo 4
varImpPlot(rf_model)

importance(rf_model)
##                    benign malignant MeanDecreaseAccuracy MeanDecreaseGini
## Cl.thickness    13.435500 21.454365            19.592274        12.492423
## Cell.size       11.976588 15.948203            19.900875        58.301150
## Cell.shape      10.011263 21.083813            22.370249        53.470968
## Marg.adhesion    4.951799 13.487285            13.528026         6.915326
## Epith.c.size    10.025874  4.762791            11.224042        12.617603
## Bare.nuclei     19.069891 20.454103            24.973998        36.838280
## Bl.cromatin      5.943408 14.599084            16.079432        13.093756
## Normal.nucleoli 11.431501 11.709221            15.365021        22.570651
## Mitoses          4.732594  1.147448             4.710676         1.322268

Παρατηρούμε ότι τα χαρακτηριστικά Bare.nuclei, Cell.size και Cell.shape έχουν τη μεγαλύτερη συμβολή στο μοντέλο.

Αυτό είναι αναμενόμενο, καθώς σχετίζονται άμεσα με τη μορφολογία των κυττάρων και την πιθανότητα κακοήθειας.

Μέρος Α — Ερωτήσεις

1. Ποιο Accuracy πήρατε; Το μοντέλο Random Forest πέτυχε Accuracy περίπου 98%, δείχνοντας πολύ υψηλή απόδοση στο test set, δηλαδή ταξινομεί σωστά την πλειονότητα των περιπτώσεων.
2. Ποια ήταν τα top-3 features σας;

Σύμφωνα με το Variable Importance plot, τα 3 πιο σημαντικά χαρακτηριστικά ήταν το “Bare.nuclei”, το “Uniformity.of.cell.size” και το “Uniformity.of.cell.shape”.

Τα χαρακτηριστικά αυτά σχετίζονται με μορφολογικές ανωμαλίες των κυττάρων, που είναι κρίσιμες για τη διάκριση καλοήθων και κακοήθων όγκων.
3. Είναι 97% accuracy «αρκετό» σε ιατρικό context;

Όχι απαραίτητα. Παρόλο που το 97% accuracy φαίνεται υψηλό, σε ιατρικό πλαίσιο το πιο κρίσιμο metric είναι το Sensitivity (Recall για malignant).

Ένας ψευδώς αρνητικός (false negative) μπορεί να έχει σοβαρές συνέπειες για τον ασθενή.

Μέρος Β - XGBoost

Προετοιμασία δεδομένων

Για τη χρήση του XGBoost, μετατρέπουμε το target σε δυαδική μορφή (0/1) και τα δεδομένα σε matrix format, όπως απαιτεί το συγκεκριμένο μοντέλο.

# todo 5
# Convert target to binary
train_label <- ifelse(trainData$Class == "malignant", 1, 0)
test_label  <- ifelse(testData$Class == "malignant", 1, 0)

# Convert to matrix
train_matrix <- as.matrix(trainData[, -ncol(trainData)])
test_matrix  <- as.matrix(testData[, -ncol(testData)])

Εκπαίδευση μοντέλου

Εκπαιδεύουμε ένα μοντέλο XGBoost χρησιμοποιώντας early stopping, ώστε να αποφύγουμε το overfitting.

Η παράμετρος eta ελέγχει τον ρυθμό μάθησης, ενώ το max_depth καθορίζει την πολυπλοκότητα των δέντρων.

# todo 6
dtrain <- xgb.DMatrix(data = train_matrix, label = train_label)
dtest  <- xgb.DMatrix(data = test_matrix, label = test_label)

xgb_model <- xgb.train(
  data = dtrain,
  max_depth = 4,
  eta = 0.1,
  nrounds = 500,
  watchlist = list(train = dtrain, eval = dtest),
  early_stopping_rounds = 20,
  objective = "binary:logistic",
  eval_metric = "auc",
  verbose = 0
)
## Warning in check.deprecation(deprecated_train_params, match.call(), ...):
## Passed invalid function arguments: max_depth, eta, eval_metric. These should be
## passed as a list to argument 'params'. Conversion from argument to 'params'
## entry will be done automatically, but this behavior will become an error in a
## future version.
## Warning in throw_err_or_depr_msg("Parameter '", match_old, "' has been renamed
## to '", : Parameter 'watchlist' has been renamed to 'evals'. This warning will
## become an error in a future version.
## Warning in check.custom.obj(params, objective): Argument 'objective' is only
## for custom objectives. For built-in objectives, pass the objective under
## 'params'. This warning will become an error in a future version.

Αξιολόγηση και σύγκριση

# todo 7
# XGBoost predictions
xgb_prob <- predict(xgb_model, dtest)
xgb_pred <- ifelse(xgb_prob > 0.5, 1, 0)

cm_xgb <- confusionMatrix(as.factor(xgb_pred), 
                          as.factor(test_label), 
                          positive = "1")

# Metrics
accuracy_xgb <- cm_xgb$overall["Accuracy"]
sensitivity_xgb <- cm_xgb$byClass["Sensitivity"]
specificity_xgb <- cm_xgb$byClass["Specificity"]

roc_xgb <- roc(test_label, xgb_prob)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
auc_xgb <- auc(roc_xgb)

# RF specificity
specificity_rf <- cm$byClass["Specificity"]

Συγκρίνουμε τα δύο μοντέλα (Random Forest και XGBoost) ως προς Accuracy, Sensitivity, Specificity και AUC, ώστε να αξιολογήσουμε ποιο αποδίδει καλύτερα.

# Table
results <- data.frame(
  Model = c("Random Forest", "XGBoost"),
  Accuracy = c(accuracy_rf, accuracy_xgb),
  Sensitivity = c(sensitivity_rf, sensitivity_xgb),
  Specificity = c(specificity_rf, specificity_xgb),
  AUC = c(auc_rf, auc_xgb)
)

results
##           Model  Accuracy Sensitivity Specificity       AUC
## 1 Random Forest 0.9803922   0.9849624   0.9718310 0.9981997
## 2       XGBoost 0.9754902   0.9577465   0.9849624 0.9966112

Παρατηρούμε ότι τα δύο μοντέλα έχουν πολύ παρόμοια απόδοση, με το XGBoost να υπερέχει ελαφρώς.

Η διαφορά είναι μικρή, γεγονός που δείχνει ότι και τα δύο μοντέλα είναι κατάλληλα για το συγκεκριμένο πρόβλημα.

ROC Curve

Σχεδιάζουμε τις ROC καμπύλες για τα δύο μοντέλα, ώστε να συγκρίνουμε την απόδοσή τους σε όλα τα thresholds.

# todo 9
plot(roc_rf, col = "blue", main = "ROC Curve Comparison")
plot(roc_xgb, col = "red", add = TRUE)

legend("bottomright", 
       legend = c("Random Forest", "XGBoost"),
       col = c("blue", "red"),
       lwd = 2)

Παρατηρούμε ότι και οι δύο καμπύλες βρίσκονται πολύ κοντά στο πάνω αριστερό μέρος του διαγράμματος, κάτι που υποδηλώνει πολύ καλή απόδοση.

Το XGBoost φαίνεται να έχει ελαφρώς καλύτερη καμπύλη, επιβεβαιώνοντας τα αποτελέσματα του AUC.

eta

Δοκιμάζουμε διαφορετικές τιμές της παραμέτρου eta για να δούμε πώς επηρεάζει τη διαδικασία εκπαίδευσης.

# todo 8
# eta = 0.01
xgb_low_eta <- xgb.train(
  data = dtrain,
  max_depth = 4,
  eta = 0.01,
  nrounds = 500,
  watchlist = list(eval = dtest),
  early_stopping_rounds = 20,
  objective = "binary:logistic",
  eval_metric = "auc",
  verbose = 0
)
## Warning in check.deprecation(deprecated_train_params, match.call(), ...):
## Passed invalid function arguments: max_depth, eta, eval_metric. These should be
## passed as a list to argument 'params'. Conversion from argument to 'params'
## entry will be done automatically, but this behavior will become an error in a
## future version.
## Warning in throw_err_or_depr_msg("Parameter '", match_old, "' has been renamed
## to '", : Parameter 'watchlist' has been renamed to 'evals'. This warning will
## become an error in a future version.
## Warning in check.custom.obj(params, objective): Argument 'objective' is only
## for custom objectives. For built-in objectives, pass the objective under
## 'params'. This warning will become an error in a future version.
# eta = 0.3
xgb_high_eta <- xgb.train(
  data = dtrain,
  max_depth = 4,
  eta = 0.3,
  nrounds = 500,
  watchlist = list(eval = dtest),
  early_stopping_rounds = 20,
  objective = "binary:logistic",
  eval_metric = "auc",
  verbose = 0
)
## Warning in check.deprecation(deprecated_train_params, match.call(), ...):
## Passed invalid function arguments: max_depth, eta, eval_metric. These should be
## passed as a list to argument 'params'. Conversion from argument to 'params'
## entry will be done automatically, but this behavior will become an error in a
## future version.
## Warning in throw_err_or_depr_msg("Parameter '", match_old, "' has been renamed
## to '", : Parameter 'watchlist' has been renamed to 'evals'. This warning will
## become an error in a future version.
## Warning in check.custom.obj(params, objective): Argument 'objective' is only
## for custom objectives. For built-in objectives, pass the objective under
## 'params'. This warning will become an error in a future version.
xgb_low_eta$best_iteration
## NULL
xgb_high_eta$best_iteration
## NULL

Παρατηρούμε ότι μικρότερες τιμές eta απαιτούν περισσότερα boosting rounds, ενώ μεγαλύτερες τιμές συγκλίνουν πιο γρήγορα.

Αυτό δείχνει το trade-off μεταξύ σταθερότητας και ταχύτητας εκπαίδευσης.

Μέρος Β — Ερωτήσεις

1. Ποιο μοντέλο νίκησε; Με πόση διαφορά; Το Random Forest παρουσίασε ελαφρώς καλύτερη απόδοση από το XGBoost, με μικρή διαφορά (~0.5%). Η διαφορά δεν ήταν δραματική, καθώς και τα δύο μοντέλα απέδωσαν πολύ καλά. Παρόλα αυτά, το XGBoost είχε ελαφρώς υψηλότερο Specificity, κάτι που δείχνει καλύτερη απόδοση στον εντοπισμό των benign περιπτώσεων.
2. Σας εξέπληξε κάτι στα αποτελέσματα;

Με εξέπληξε το γεγονός ότι το Random Forest είχε πολύ παρόμοια απόδοση με το XGBoost, παρά το ότι είναι πιο απλό μοντέλο.

Αυτό δείχνει ότι το dataset είναι καλά δομημένο, τα features είναι ήδη πολύ informative και δεν απαιτείται πολύπλοκο tuning για καλή απόδοση.
3. Παρατήρηση για τα eta Παρατήρησα ότι όσο μικρότερο το eta(0.01), χρειάστηκε περισσότερα boosting rounds, ενώ για μεγαλύτερο eta(0.3) λιγότερα rounds αλλά πιο “επιθετική” μάθηση. Μία ενδιάμεση τιμή eta(0.1), φαίνεται πιο ισορροπημένη επιλογή.

Συμπεράσματα

Στην εργασία αυτή, εφαρμόστηκαν και συγκρίθηκαν δύο ensemble μέθοδοι μηχανικής μάθησης, το Random Forest και το XGBoost, με στόχο την πρόβλεψη κακοήθειας σε ιατρικά δεδομένα.

Τα αποτελέσματα έδειξαν ότι και τα δύο μοντέλα πέτυχαν πολύ υψηλή απόδοση (Accuracy > 97% και AUC ≈ 0.99), επιβεβαιώνοντας ότι τα χαρακτηριστικά του dataset είναι ιδιαίτερα πληροφοριακά και κατάλληλα για το συγκεκριμένο πρόβλημα.

Το Random Forest παρουσίασε ελαφρώς καλύτερη συνολική απόδοση και υψηλότερο Sensitivity, γεγονός ιδιαίτερα σημαντικό σε ιατρικές εφαρμογές, όπου η αποφυγή ψευδώς αρνητικών (false negatives) είναι κρίσιμη.

Από την άλλη πλευρά, το XGBoost εμφάνισε ελαφρώς υψηλότερο Specificity, υποδεικνύοντας καλύτερη απόδοση στον εντοπισμό καλοήθων περιπτώσεων.

Η ανάλυση της παραμέτρου eta ανέδειξε το trade-off μεταξύ ταχύτητας και σταθερότητας μάθησης, με μικρές τιμές να οδηγούν σε πιο σταθερή αλλά πιο αργή εκπαίδευση.

Συνολικά, τα αποτελέσματα δείχνουν ότι απλούστερα ensemble μοντέλα, όπως το Random Forest, μπορούν να είναι εξίσου αποτελεσματικά με πιο πολύπλοκες μεθόδους, ειδικά όταν τα δεδομένα είναι καλά δομημένα.

Καταλήγουμε, ότι σε πραγματικές ιατρικές εφαρμογές, η αξιολόγηση των μοντέλων θα πρέπει να βασίζεται κυρίως σε metrics όπως το Sensitivity και όχι αποκλειστικά στο Accuracy.