[Week 10] Solving Nonlinear Problems: Decision Tree and Random Forest π³π²
English translation
Welcome to Week 10!
So far, we have learned Linear Regression and Logistic Regression, both linear models.
These models attempted to explain relationships in data with a single 'line' or 'plane'.
Real-world data often cannot be explained by a straight line alone,
having more complex nonlinear relationships.
Today, we will learn powerful models for nonlinear problems:
Decision Trees and their extension, Random Forests,
and implement them ourselves with scikit-learn.
π³ Module 1: Decision Trees - Splitting with Questions Like Twenty Questions
What Is a Decision Tree?
- This model repeatedly divides data into 'Yes' or 'No' groups according to a specific question (condition),
so that each final group has one correct answer (class).\ - Its principle resembles 'Twenty Questions'.
Example Data: Deciding whether to visit an amusement park
- Features: Weather (sunny/cloudy/rainy), day type (weekend/weekday)
- Target: Go (Yes)/Do not go (No)
| Weather | Weekend status | Go |
|---|---|---|
| Sunny | Weekend | Yes |
| Sunny | Weekday | Yes |
| Cloudy | Weekend | Yes |
| Rainy | Weekend | No |
| Rainy | Weekday | No |
Goal: Find the best question for splitting the data!
β‘οΈ Make groups as pure as possible (only one type per group)
Step 1: Calculate Impurity in the Current State (Root Node)
- Current data: 3 Yes, 2 No (5 total)
- Impurity: Measures how mixed the types are within a group. (Lower is better)
- Here, we use Gini Impurity.
- Gini = (C: number of classes, : proportion of the ith class)
- Root node Gini =
Step 2: Evaluate Candidate First Questions
Candidate Question 1: "What Is the Weather?"
- Sunny -> [Yes, Yes] (Gini=0, pure!)
- Cloudy -> [Yes] (Gini=0, pure!)
- Rainy -> [No, No] (Gini=0, pure!)
- Weighted average Gini after splitting =
- Information Gain: How much did impurity decrease?
- Information Gain = (Gini before splitting) - (average Gini after splitting) =
Candidate Question 2: "Is It a Weekend?"
- Weekend -> [Yes, Yes, No] (Gini β 0.44)
- Weekday -> [Yes, No] (Gini = 0.5)
- Weighted average Gini after splitting =
- Information Gain =
Step 3: Choose the Best Question
- The question "What is the weather?" has much greater Information Gain (0.48).
- The root node question is therefore "What is the weather?".
Step 4: Complete and Visualize the Tree
- Each group (sunny, cloudy, rainy) produced by the question "What is the weather?" is already pure,
so no further questions are needed.
The code below visualizes this final tree structure and explains key terms.
# Visualize the tree using graphviz
import graphviz
# Define the tree structure (DOT language)
dot_source = """
digraph WeatherTree {
node [shape=box, style="filled, rounded", fillcolor="#e8f4f8"];
edge [fontname="Helvetica"];
// λ£¨νΈ λ
Έλ
0 [label="λ μ¨λ?", fillcolor="#a1d6e2"];
// 리ν λ
Έλ
1 [label="κ°λ€ (Yes)\n(λ§μ)", fillcolor="#7dd3a2"];
2 [label="κ°λ€ (Yes)\n(νλ¦Ό)", fillcolor="#7dd3a2"];
3 [label="μκ°λ€ (No)\n(λΉ)", fillcolor="#f8a7a7"];
// κ°μ§ (Branch)
0 -> 1 [label="λ§μ"];
0 -> 2 [label="νλ¦Ό"];
0 -> 3 [label=" λΉ "];
}
"""
graph = graphviz.Source(dot_source)
graph # Display the visualization directly in Colab.<graphviz.sources.Source at 0x7e59e45aaed0>
Key Decision Tree Terms (See the Figure Above):
- Root Node: The topmost node where questioning begins ("What is the weather?").
- Decision Node: An intermediate node containing a question that splits data (none in this example).
- Leaf Node: A bottommost terminal node where the final decision (prediction) is made ("Go", "Do not go").
- Branch/Edge: Connects nodes and represents an answer to a question ('sunny', 'cloudy', 'rainy').
A Decision Tree repeatedly splits data by finding questions that maximize information gain at each step,
building its final prediction rules.
πΈ Module 2: Step-by-Step Decision Tree Modeling (Iris Data)
Now build a Decision Tree model on the Iris dataset using scikit-learn.
Use the two features petal length and petal width
to classify all three varieties (setosa, versicolor, virginica),
a Multiclass Classification problem.
Follow the 5-step Machine Learning workflow again.
# Import required libraries
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
# Import DecisionTreeClassifier.
from sklearn.tree import DecisionTreeClassifier
# Import evaluation metric functions.
from sklearn.metrics import (accuracy_score,
confusion_matrix,
classification_report)
# 1. Load data
iris = load_iris()
# Features (X): Petal length (index 2), petal width (index 3) -> Use 2 features!
X = iris.data[:, 2:4]
# Ground truth (y): Iris species (0, 1, 2)
y = iris.target
feature_names = iris.feature_names[2:]
# 'petal length (cm)', 'petal width (cm)'
target_names = iris.target_names
# 'setosa', 'versicolor', 'virginica'
# 2. Explore data (visualization)
plt.figure(figsize=(8, 6))
# Plot each species (y) with a different color/marker
for i, target_name in enumerate(target_names):
plt.scatter(X[y == i, 0], X[y == i, 1], label=target_name)
plt.title("Iris data (feature)")
plt.xlabel(feature_names[0])
plt.ylabel(feature_names[1])
plt.legend()
plt.grid(True)
plt.show()
print("κ²°κ³Ό ν΄μ:")
print("- Setosa(νλμ)λ λͺ
νν ꡬλΆλ©λλ€.")
print("- Versicolor(μ£Όν©μ)μ Virginica(λ
Ήμ)λ μΌλΆ κ²Ήμ³ λ³΄μ
λλ€.")
print("- μ§μ νλλ‘λ μΈ νμ’
μ μλ²½ν λλκΈ° μ΄λ €μ 보μ
λλ€.")<Figure size 800x600 with 1 Axes>
κ²°κ³Ό ν΄μ: - Setosa(νλμ)λ λͺ νν ꡬλΆλ©λλ€. - Versicolor(μ£Όν©μ)μ Virginica(λ Ήμ)λ μΌλΆ κ²Ήμ³ λ³΄μ λλ€. - μ§μ νλλ‘λ μΈ νμ’ μ μλ²½ν λλκΈ° μ΄λ €μ 보μ λλ€.
Next, split the data into practice problems and an actual exam.
Since this is multiclass classification, use the stratify option.
# Split data into 'practice problems' and a 'real exam' at a 7:3 ratio
# Use stratify=y for this multiclass classification problem
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"Train λ°μ΄ν° ν¬κΈ°: {X_train.shape}")
print(f"Test λ°μ΄ν° ν¬κΈ°: {X_test.shape}")Train λ°μ΄ν° ν¬κΈ°: (105, 2) Test λ°μ΄ν° ν¬κΈ°: (45, 2)
Now create a Decision Tree model and train it.
Main hyperparameters of DecisionTreeClassifier:
criterion: Impurity criterion ('gini' or 'entropy'). Default: 'gini'.max_depth: Maximum tree depth.None(the default) allows unlimited growth. (Controls Overfitting.)min_samples_split: Minimum samples required to split a node. Default: 2. (Controls overfitting)min_samples_leaf: Minimum samples required to form a leaf node. Default: 1. (Controls overfitting)random_state: Seed for reproducible results.
# 1. Create a Decision Tree model object
# random_state: Fix the random seed to obtain identical results
# max_depth: Limit maximum tree depth (None allows unlimited growth)
# -> An important hyperparameter for preventing overfitting (explained later)
model_dt = DecisionTreeClassifier(random_state=42)
# 2. Train the model on 'practice problems' (fit)
# Internally, it finds optimal questions (features/thresholds)
# and builds a tree structure that splits the data.
model_dt.fit(X_train, y_train)
print("κ²°μ νΈλ¦¬ λͺ¨λΈ νμ΅ μλ£!")κ²°μ νΈλ¦¬ λͺ¨λΈ νμ΅ μλ£!
Visualize the trained Decision Tree's internal structure
to see which questions the model uses to classify data.
(Requires the graphviz library)
import graphviz
from sklearn.tree import export_graphviz
# Visualize the trained tree structure
dot_data = export_graphviz(model_dt, out_file=None,
feature_names=feature_names,
class_names=target_names,
filled=True, rounded=True,
special_characters=True)
graph = graphviz.Source(dot_data)
graph # Display the visualization directly in Colab.<graphviz.sources.Source at 0x7e59b4f63020>
Visualize how the trained tree partitions the data space (decision boundaries).
This can reveal signs of Overfitting.
Overfitting occurs when a model becomes too specialized in practice problems
and performs poorly on the actual exam.
If a Decision Tree grows without limits, it can learn even noise in the training data,
producing very complex decision boundaries and becoming prone to Overfitting.
# Define a decision boundary visualization function (reuse the previous lecture's code)
def plot_decision_boundary(model, X, y, target_names, feature_names, title):
plt.figure()
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
np.arange(y_min, y_max, 0.02))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, cmap='coolwarm', alpha=0.8)
for i, target_name in enumerate(target_names):
plt.scatter(X[y == i, 0], X[y == i, 1], label=target_name,
edgecolors='k')
plt.title(title)
plt.xlabel(feature_names[0])
plt.ylabel(feature_names[1])
plt.legend()
plt.show()
# Inspect the trained Decision Tree's decision boundary using Train data
plot_decision_boundary(model_dt, X_train, y_train, target_names,
feature_names, "Decision Boundary (Train Data)")
print("κ²°κ³Ό ν΄μ:")
print("- μ°μ΅ λ¬Έμ (Train Data)λ₯Ό μ λΆλ₯νκΈ° μν΄")
print(" μ λ¬ν κ²°μ κ²½κ³κ° λ§λ€μ΄μ‘μ΅λλ€.")
print("- κ³Όμ ν©(Overfitting)μ κ°λ₯μ±μ΄ μμ΅λλ€.")<Figure size 640x480 with 1 Axes>
κ²°κ³Ό ν΄μ: - μ°μ΅ λ¬Έμ (Train Data)λ₯Ό μ λΆλ₯νκΈ° μν΄ μ λ¬ν κ²°μ κ²½κ³κ° λ§λ€μ΄μ‘μ΅λλ€. - κ³Όμ ν©(Overfitting)μ κ°λ₯μ±μ΄ μμ΅λλ€.
# Evaluate the Decision Tree model using Test data
y_pred_dt = model_dt.predict(X_test)
accuracy_dt = accuracy_score(y_test, y_pred_dt)
report_dt = classification_report(y_test, y_pred_dt, target_names=target_names)
print("--- κ²°μ νΈλ¦¬ μ±λ₯ νκ° (Test Data) ---")
print(f"μ νλ: {accuracy_dt:.4f}")
print("\nClassification Report:")
print(report_dt)
# Visualize the decision boundary using Test data
plot_decision_boundary(model_dt, X_test, y_test, target_names,
feature_names, "Decision Boundary (Test Data)")--- κ²°μ νΈλ¦¬ μ±λ₯ νκ° (Test Data) ---
μ νλ: 0.9333
Classification Report:
precision recall f1-score support
setosa 1.00 1.00 1.00 15
versicolor 0.88 0.93 0.90 15
virginica 0.93 0.87 0.90 15
accuracy 0.93 45
macro avg 0.93 0.93 0.93 45
weighted avg 0.93 0.93 0.93 45
<Figure size 640x480 with 1 Axes>
π² Module 3: Overfitting and Random Forest
A Limitation of Decision Trees: Overfitting (Overfitting)
- A Decision Tree tends to grow too deep to split the data perfectly.\
- This leads to 100 points on 'practice problems',
but poor performance on a slightly different 'actual exam'.
(Like a student who only memorizes old exam questions.) - The complex decision boundary shown above may be a sign of Overfitting.
Ways to Reduce Overfitting:
max_depth: Limit the maximum tree depthmin_samples_split: Set the minimum number of samples required to split a nodemin_samples_leaf: Sets the minimum number of samples in a leaf node- Use Ensemble Methods
Random Forest: The Power of Ensembles
Imagine a Decision Tree as an all-purpose doctor who diagnoses every patient alone.
This doctor gives perfect diagnoses (100 points) for previously seen patients (training data),
but may become so specialized in those experiences that new patients with slightly different symptoms (test data)
receive incorrect diagnoses (overfitting).
Random Forest addresses this problem
by gathering multiple 'slightly different specialists' instead of one all-purpose doctor
to form a Committee of Doctors.
β‘οΈ The number of doctors is the n_estimators parameter of RandomForestClassifier.
How Do We Create 'Slightly Different' Doctors? (Randomness!)
- Different Patient Experience (Data Sampling β Bagging):
- From all patient records (the original data), randomly sample patient records with replacement
and give them to each doctor. - A doctor may see a particular patient multiple times and never see some others.
- This gives each doctor experience with slightly different patient groups.
- β‘οΈ The
bootstrap=Trueparameter (the default) means this method is used.
- From all patient records (the original data), randomly sample patient records with replacement
- Limit the Symptoms Considered (Random Feature Selection):
- Whenever a doctor makes a diagnosis (asks a question),
randomly limit the types of symptoms (features) they can consider. - For example, one doctor must decide using only 'temperature' and 'cough',
while another uses only 'headache' and 'fatigue'. - This gives each doctor more expertise in particular symptom combinations.
- β‘οΈ The
max_featuresparameter controls this step.
(Default: )
- Whenever a doctor makes a diagnosis (asks a question),
Diagnosis Process (Prediction):
- When a new patient arrives, every doctor in the committee independently makes a diagnosis (prediction).
(Each judges based on their patient experience and the symptoms they were trained to focus on.) - Final diagnosis: Collect all doctors' diagnoses and take a majority vote.
The diagnosis with the most votes becomes the final result.
(Use the mean for regression.)
Why Is It More Effective?
- Individual doctors can misdiagnose patients because of limited information (different patient experience and partial symptoms).
- Combining the opinions of several doctors with diverse perspectives
allows individual errors to cancel one another out,
increasing the chance of more accurate and stable diagnoses overall. - This is why Random Forests resist overfitting and generalize better than a single Decision Tree.
Now create and train a Random Forest model
and compare its performance with the Decision Tree model.
# Import the Random Forest model
from sklearn.ensemble import RandomForestClassifier
# 1. Create and train a Random Forest model
# n_estimators: Number of trees (doctors). More is better, but increases computation time.
# random_state: Seed for reproducible results.
model_rf = RandomForestClassifier(n_estimators=15, random_state=42)
model_rf.fit(X_train, y_train)
plot_decision_boundary(model_rf, X_train, y_train, target_names,
feature_names, "Random Forest Decision Boundary (Train Data)")
# 2. Evaluate the Random Forest model using Test data
y_pred_rf = model_rf.predict(X_test)
accuracy_rf = accuracy_score(y_test, y_pred_rf)
report_rf = classification_report(y_test, y_pred_rf, target_names=target_names)
print("--- λλ€ ν¬λ μ€νΈ μ±λ₯ νκ° (Test Data) ---")
print(f"μ νλ: {accuracy_rf:.4f}")
print("\nClassification Report:")
print(report_rf)
# Visualize the decision boundary using Test data
plot_decision_boundary(model_rf, X_test, y_test, target_names,
feature_names, "Random Forest Decision Boundary (Test Data)")
print("\nλΉκ΅:")
print(f"- κ²°μ νΈλ¦¬ μ νλ: {accuracy_dt:.4f}")
print(f"- λλ€ ν¬λ μ€νΈ μ νλ: {accuracy_rf:.4f}")
print("\n=> κ³Όμ ν©μ λλμ΄ μμ§λ μμ§λ§, μ νλκ° μ½κ° ν₯μλμμ΅λλ€.")<Figure size 640x480 with 1 Axes>
--- λλ€ ν¬λ μ€νΈ μ±λ₯ νκ° (Test Data) ---
μ νλ: 0.9556
Classification Report:
precision recall f1-score support
setosa 1.00 1.00 1.00 15
versicolor 0.93 0.93 0.93 15
virginica 0.93 0.93 0.93 15
accuracy 0.96 45
macro avg 0.96 0.96 0.96 45
weighted avg 0.96 0.96 0.96 45
<Figure size 640x480 with 1 Axes>
λΉκ΅: - κ²°μ νΈλ¦¬ μ νλ: 0.9333 - λλ€ ν¬λ μ€νΈ μ νλ: 0.9556 => κ³Όμ ν©μ λλμ΄ μμ§λ μμ§λ§, μ νλκ° μ½κ° ν₯μλμμ΅λλ€.
Random Forest provides information about how much each feature contributed to model predictions:
Feature Importance.
# (Reference) Inspect feature importance
# Stored in the model_rf object's feature_importances_ attribute
importances = model_rf.feature_importances_
print("\n--- νΉμ§ μ€μλ (Feature Importances) ---")
for name, importance in zip(feature_names, importances):
print(f"{name}: {importance:.4f}")
print("\n=> μ΄ λ¬Έμ μμλ κ½μ λλΉ(petal width)κ° νμ’
λΆλ₯μ")
print(" κ½μ κΈΈμ΄(petal length)λ³΄λ€ μ½κ° λ μ€μν νΉμ§μμ μ μ μμ΅λλ€.")--- νΉμ§ μ€μλ (Feature Importances) --- petal length (cm): 0.5217 petal width (cm): 0.4783 => μ΄ λ¬Έμ μμλ κ½μ λλΉ(petal width)κ° νμ’ λΆλ₯μ κ½μ κΈΈμ΄(petal length)λ³΄λ€ μ½κ° λ μ€μν νΉμ§μμ μ μ μμ΅λλ€.
β¨ Wrap-up
Today we learned powerful tools for nonlinear problems:
Decision Trees and Random Forests.
- Decision Tree: Splits data through questions, like Twenty Questions. Intuitive, but prone to Overfitting.
- Random Forest: An Ensemble of multiple Decision Trees. Reduces Overfitting and improves performance.
Next time, we will explore a method that finds hidden structure in data without correct answers:
Unsupervised Learning, represented by K-Means Clustering.