[Week 8] Starting Classification: Building a Logistic Regression Model ๐ฏ
English translation
Welcome to Week 8!
Last time, we addressed a regression problem that predicts continuous numerical values
using a Linear Regression model.
Today, we explore another major Machine Learning task: Classification.
The goal is to assign given data to one of the specified categories.
Implement and evaluate Logistic Regression, a basic but important classification model,
using scikit-learn.
๐ค Module 1: Why Is Linear Regression Insufficient? (Classification)
What is classification? - Assigning data to one of a predefined set of categories.
- Examples: Is an email spam? (Binary classification.) Is the animal in a photo a dog, cat, or bird? (Multiclass classification.)
Limitations of Linear Regression:
- Linear Regression is designed to predict continuous numerical values. ()\
- Predictions may fall within .\
- Applied to classification (e.g., 0 or 1), it can produce meaningless values outside the categories,
such as 0.5, 1.7, or -0.2.
Classification therefore requires a new model that restricts outputs to a particular range (e.g., between 0 and 1).
# Import libraries
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Generate synthetic classification data (pass/fail based on study hours)
# Study hours (feature X)
study_hours = np.array(
[[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
)
# Pass status (ground truth y, 0: fail, 1: pass)
pass_fail = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1, 1])
# Try training a Linear Regression model
linear_model = LinearRegression()
linear_model.fit(study_hours, pass_fail)
# Visualize the results
plt.scatter(study_hours, pass_fail,
c=pass_fail, cmap='coolwarm', s=100,
label='Actual Data (0:Fail, 1:Pass)')
# Prediction line from the Linear Regression model
plt.plot(study_hours, linear_model.predict(study_hours),
color='black', label='Linear Regression Prediction')
plt.title("Classification by regression")
plt.xlabel("Study time")
plt.ylabel("Pass or not (0 or 1)")
plt.yticks([0, 1]) # Fix the y-axis ticks at 0 and 1
plt.legend()
plt.grid(True)
plt.show()
print("๊ฒฐ๊ณผ ํด์:")
print("- ์์ธก ์ง์ ์ด y=0๊ณผ y=1 ๋ฒ์๋ฅผ ํฌ๊ฒ ๋ฒ์ด๋ฉ๋๋ค.")
print("- ์๋ฅผ ๋ค์ด ๊ณต๋ถ ์๊ฐ์ด 1์๊ฐ์ผ ๋ ์์ธก๊ฐ์ 0๋ณด๋ค ์๊ณ ,")
print(" 10์๊ฐ์ผ ๋๋ 1๋ณด๋ค ํฐ ๊ฐ์ด ๋์ต๋๋ค.")
print("=> ์ฆ, ๋ถ๋ฅ ๋ฌธ์ ์ ์ง์ ์ ์ฉํ๊ธฐ ๋ถ์ ํฉํฉ๋๋ค.")<Figure size 640x480 with 1 Axes>
๊ฒฐ๊ณผ ํด์: - ์์ธก ์ง์ ์ด y=0๊ณผ y=1 ๋ฒ์๋ฅผ ํฌ๊ฒ ๋ฒ์ด๋ฉ๋๋ค. - ์๋ฅผ ๋ค์ด ๊ณต๋ถ ์๊ฐ์ด 1์๊ฐ์ผ ๋ ์์ธก๊ฐ์ 0๋ณด๋ค ์๊ณ , 10์๊ฐ์ผ ๋๋ 1๋ณด๋ค ํฐ ๊ฐ์ด ๋์ต๋๋ค. => ์ฆ, ๋ถ๋ฅ ๋ฌธ์ ์ ์ง์ ์ ์ฉํ๊ธฐ ๋ถ์ ํฉํฉ๋๋ค.
Module 2: Logistic Regression and the Sigmoid () Function
Logistic Regression (Logistic Regression):
- Despite 'Regression' in its name, this model is actually used for classification.\
- Pass the Linear Regression prediction through a Sigmoid function
to convert the output into a probability between 0 and 1.
Sigmoid Function:
- (where )\
- is Euler's number (approximately 2.718), and approaches 0 as increases,
and approaches infinity as decreases.\ - Because of this property, when becomes very large, the denominator becomes , giving ,
and when becomes very small, the denominator becomes , giving .\ - This is why the sigmoid function always forms an S-shaped curve between 0 and 1.
# Define the sigmoid function
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# Generate z values over the range (-10 ~ 10)
z = np.linspace(-10, 10, 100)
# Calculate the sigmoid function for each z value
sigma_z = sigmoid(z)
# Plot the sigmoid function
plt.plot(z, sigma_z)
plt.title("Sigmoid Function")
plt.xlabel("z (Linear Regression Output: Wx + b)")
plt.ylabel("Sigmoid Output (Probability)")
plt.grid(True)
# Mark the point where probability is 0.5 (z=0) with lines
plt.axhline(y=0.5, color='red', linestyle='--',
label='Decision Boundary (Prob=0.5)')
plt.axvline(x=0, color='gray', linestyle=':')
plt.legend()
plt.show()
print("๊ฒฐ๊ณผ ํด์:")
print("- ์ ํ ํ๊ท์ ๊ฒฐ๊ณผ(z)๊ฐ ์๋ฌด๋ฆฌ ์ปค์ ธ๋ ํ๋ฅ ์ 1์ ์๋ ดํฉ๋๋ค.")
print("- ๊ฒฐ๊ณผ(z)๊ฐ ์๋ฌด๋ฆฌ ์์์ ธ๋ ํ๋ฅ ์ 0์ ์๋ ดํฉ๋๋ค.")
print("- ๊ฒฐ๊ณผ(z)๊ฐ 0์ผ ๋, ํ๋ฅ ์ ์ ํํ 0.5๊ฐ ๋ฉ๋๋ค.")<Figure size 640x480 with 1 Axes>
๊ฒฐ๊ณผ ํด์: - ์ ํ ํ๊ท์ ๊ฒฐ๊ณผ(z)๊ฐ ์๋ฌด๋ฆฌ ์ปค์ ธ๋ ํ๋ฅ ์ 1์ ์๋ ดํฉ๋๋ค. - ๊ฒฐ๊ณผ(z)๊ฐ ์๋ฌด๋ฆฌ ์์์ ธ๋ ํ๋ฅ ์ 0์ ์๋ ดํฉ๋๋ค. - ๊ฒฐ๊ณผ(z)๊ฐ 0์ผ ๋, ํ๋ฅ ์ ์ ํํ 0.5๊ฐ ๋ฉ๋๋ค.
Decision Boundary:
- Typically, probabilities of 0.5 or higher are classified as 1 and those below 0.5 as 0.\
- The point corresponding to this criterion (probability=0.5, or ) is called the decision boundary.
Cost Function:
- Linear Regression used MSE (mean squared error),
whereas Logistic Regression uses the Cross-Entropy cost function, which is better suited to probability prediction.\ - Cross-Entropy measures how far the model's predicted probabilities differ from the ground truth (0 or 1),
and guides training by imposing a larger penalty for incorrect predictions.
(The formula is omitted because it is beyond this course's scope.)
๐ฉบ Module 3: Logistic Regression Modeling Step by Step (Breast Cancer Diagnosis)
Now solve a real classification problem using scikit-learn.
Using the breast cancer diagnosis dataset, build a model that classifies tumors as malignant or benign from their features.
Follow the 5-step Machine Learning workflow again.
[Supplement] NumPy Indexing [:, 0:1] Explained
Before feeding data into the scikit-learn model,
let's take another detailed look at the X = cancer.data[:, 0:1] code we use.
1. Shape of cancer.data - The breast cancer dataset contains multiple samples (tumors),
each with multiple features (radius, texture, etc.).
- Thus,
cancer.datahas Shape (number of samples, number of features) and is a 2-dimensional array (Matrix).- Example: (569, 30) -> 569 tumor samples, each with 30 feature values
2. [:, 0] vs [:, 0:1]
[:, 0]: "For every row, select just one column, at Index 0."- Result:
[value1, value2, ..., value569]-> 1-dimensional array (Vector), Shape:(569,) - When the result is a single column, NumPy reduces its dimensionality and returns a 1-dimensional array.
- Result:
[:, 0:1]: "For every row, select columns starting at Index 0 and ending before Index 1."- Result:
[[value1], [value2], ..., [value569]]-> 2-dimensional array (Matrix), Shape:(569, 1) - With slicing (
:), NumPy preserves the original 2-dimensional structure even when selecting just one column.
- Result:
Conclusion: For scikit-learn models, input X must always be a 2-dimensional array with shape (sample count, feature count).
Even when selecting one feature, use slicing such as [:, 0:1] to preserve dimensions.
# Import additional required libraries
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression # Change the model!
from sklearn.model_selection import train_test_split
# The metrics module contains performance metrics.
from sklearn.metrics import (accuracy_score, precision_score,
recall_score, f1_score,
confusion_matrix, classification_report)
# 1. Prepare data (breast cancer dataset)
cancer = load_breast_cancer()
# Use only 'mean radius' as a 2D array from the available features
X = cancer.data[:, 0:1]
y = cancer.target # 0: Malignant, 1: Benign
# Explore the data (omitted here, but important in actual analysis)
print(f"๋ฐ์ดํฐ์
ํฌ๊ธฐ (์ํ ์, ํน์ง ์): {X.shape}")
print(f"ํ๊ฒ ์ข
๋ฅ ๋ฐ ๊ฐ์: {np.unique(y, return_counts=True)}")๋ฐ์ดํฐ์ ํฌ๊ธฐ (์ํ ์, ํน์ง ์): (569, 1) ํ๊ฒ ์ข ๋ฅ ๋ฐ ๊ฐ์: (array([0, 1]), array([212, 357]))
# 2. Split data into 'practice problems' and a 'real exam' at a 7:3 ratio
# stratify=y : Preserve the original class proportions (the ratio of 0 and 1) when splitting
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 ๋ฐ์ดํฐ ํฌ๊ธฐ: (398, 1) Test ๋ฐ์ดํฐ ํฌ๊ธฐ: (171, 1)
# 3. Create a Logistic Regression model object
model = LogisticRegression()
# Train the model on 'practice problems' (fit)
# Internally, it calculates probabilities using the sigmoid function,
# then compares them with the ground truth (0 or 1) to find the optimal W, b.
model.fit(X_train, y_train)
print("๋ก์ง์คํฑ ํ๊ท ๋ชจ๋ธ ํ์ต ์๋ฃ!")๋ก์ง์คํฑ ํ๊ท ๋ชจ๋ธ ํ์ต ์๋ฃ!
โ Module 4: Evaluating Classification Models with Confusion Matrices
Step 4: Evaluate Performance
To understand classification performance, go beyond 'how many were correct (Accuracy)'
and examine 'how' predictions were right or wrong.
Confusion Matrix (Confusion Matrix):
- A table comparing the model's predictions with the actual ground truth.\
- Binary classification (0/1) is represented by a 2x2 matrix.
Let's understand this through the example of a doctor's cancer diagnosis.
- Actual: Whether the patient actually has cancer
- Predicted: Whether a doctor diagnosed cancer based on test results
TN (True Negative): A doctor correctly diagnoses a healthy (Negative) person as healthy (Negative)
FP (False Positive): The doctor incorrectly diagnoses someone who is actually healthy (Negative) as having cancer (Positive)
(Telling a healthy person they have cancer - Type I Error)FN (False Negative): For someone who actually has cancer (Positive), the doctor incorrectly diagnoses them as healthy (Negative)
(Missing a cancer patient - Type II Error)TP (True Positive): A doctor correctly diagnoses an actual cancer patient (Positive) as having cancer (Positive)
Key Performance Metrics:
- Precision (Precision): Among people diagnosed by the doctor as having cancer (TP + FP), the fraction who actually have cancer (TP)\
- \
- How precise was the doctor's diagnosis? (FP must be reduced.)
- Recall (Sensitivity): Among actual cancer patients (TP + FN), the proportion correctly diagnosed with cancer (TP)\
- \
- How completely did the doctor identify cancer patients? (Reduce FN)
- Accuracy: The fraction of all predictions that are correct (TP + TN)\
- \
- The most intuitive metric, but it can distort performance on imbalanced data (e.g., 99% normal, 1% defective).
- F1 Score: The harmonic mean of Precision and Recall. Used to consider both metrics in balance.\
- \
- F1 Score is high when both Precision and Recall are high.
# Make predictions on the 'real exam' data
y_pred = model.predict(X_test)
# 1. Calculate and display the Confusion Matrix
# TN FP
# FN TP
conf_matrix = confusion_matrix(y_test, y_pred)
print("--- Confusion Matrix ---")
print(conf_matrix)
tn, fp, fn, tp = conf_matrix.ravel() # Assign each value to a separate variable--- Confusion Matrix --- [[51 13] [11 96]]
# Visualize a heatmap using seaborn
import seaborn as sns
plt.figure(figsize=(6, 5))
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues',
xticklabels=['Predicted Malignant(0)', 'Predicted Benign(1)'],
yticklabels=['Actual Malignant(0)', 'Actual Benign(1)'])
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix Visualization')
plt.show()<Figure size 600x500 with 2 Axes>
# 2. Calculate the main performance metrics
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
print("\n--- ์ฃผ์ ์ฑ๋ฅ ์งํ ---")
print(f"์ ํ๋ (Accuracy): {accuracy:.4f}") # (TP + TN) / Total
print(f"์ ๋ฐ๋ (Precision): {precision:.4f}") # TP / (TP + FP)
print(f"์ฌํ์จ (Recall): {recall:.4f}") # TP / (TP + FN)
print(f"F1 ์ ์ (F1 Score): {f1:.4f}") # Harmonic mean of Precision and Recall--- ์ฃผ์ ์ฑ๋ฅ ์งํ --- ์ ํ๋ (Accuracy): 0.8596 ์ ๋ฐ๋ (Precision): 0.8807 ์ฌํ์จ (Recall): 0.8972 F1 ์ ์ (F1 Score): 0.8889
# 3. Use classification_report() (summarize everything at once)
# support: Actual sample count for each class
print("\n--- Classification Report ---")
print(classification_report(y_test, y_pred,
target_names=['Malignant(0)', 'Benign(1)']))
print("\nํด์:")
print("- Malignant(0) Precision = 0.82: ์(์
์ฑ) ์ง๋จํ ๊ฒฝ์ฐ, 82%๋ ์ค์ ์์ด์์ต๋๋ค.")
print("- Malignant(0) Recall = 0.80: ์ค์ ์(์
์ฑ) ํ์ ์ค 80%๋ฅผ ์ฐพ์๋์ต๋๋ค. (20%๋ ๋์นจ)")
print("- Benign(1) Precision = 0.88: ์ ์(์์ฑ) ์ง๋จํ ๊ฒฝ์ฐ, 88%๋ ์ค์ ์ ์์ด์์ต๋๋ค.")
print("- Benign(1) Recall = 0.90: ์ค์ ์ ์(์์ฑ) ํ์ ์ค 90%๋ฅผ ์ฐพ์๋์ต๋๋ค. (10%๋ ๋์นจ)")
--- Classification Report ---
precision recall f1-score support
Malignant(0) 0.82 0.80 0.81 64
Benign(1) 0.88 0.90 0.89 107
accuracy 0.86 171
macro avg 0.85 0.85 0.85 171
weighted avg 0.86 0.86 0.86 171
ํด์:
- Malignant(0) Precision = 0.82: ์(์
์ฑ) ์ง๋จํ ๊ฒฝ์ฐ, 82%๋ ์ค์ ์์ด์์ต๋๋ค.
- Malignant(0) Recall = 0.80: ์ค์ ์(์
์ฑ) ํ์ ์ค 80%๋ฅผ ์ฐพ์๋์ต๋๋ค. (20%๋ ๋์นจ)
- Benign(1) Precision = 0.88: ์ ์(์์ฑ) ์ง๋จํ ๊ฒฝ์ฐ, 88%๋ ์ค์ ์ ์์ด์์ต๋๋ค.
- Benign(1) Recall = 0.90: ์ค์ ์ ์(์์ฑ) ํ์ ์ค 90%๋ฅผ ์ฐพ์๋์ต๋๋ค. (10%๋ ๋์นจ)
# Visualize results (including the decision boundary)
# Scatter plot of Test data (colored by ground truth)
plt.scatter(X_test, y_test, c=y_test, cmap='coolwarm',
s=50, marker='o', alpha=0.1, label='Actual Class')
# Add small points colored by model predictions to identify incorrect predictions
plt.scatter(X_test, y_pred, c=y_pred, cmap='coolwarm',
s=10, marker='x', label='Predicted Class')
# Calculate and plot the decision boundary (probability=0.5)
# Find where z = Wx + b = 0 -> x = -b / W
decision_boundary = -model.intercept_ / model.coef_[0]
plt.axvline(x=decision_boundary, color='black', linestyle='--',
label='Decision Boundary')
plt.title("Test Data")
plt.xlabel("Mean Radius")
plt.ylabel("Class (0:Malignant, 1:Benign)")
plt.yticks([0, 1])
plt.legend()
plt.grid(True)
plt.show()
print(f"\n๊ฒฐ์ ๊ฒฝ๊ณ: ํ๊ท ๋ฐ์ง๋ฆ ์ฝ {decision_boundary[0]:.2f}")
print("=> ์ด ๋ชจ๋ธ์ ํ๊ท ๋ฐ์ง๋ฆ์ด ์ด ๊ฐ๋ณด๋ค ํฌ๋ฉด 0(์
์ฑ), ์์ผ๋ฉด 1(์์ฑ)์ผ๋ก ์์ธกํฉ๋๋ค.")<Figure size 640x480 with 1 Axes>
๊ฒฐ์ ๊ฒฝ๊ณ: ํ๊ท ๋ฐ์ง๋ฆ ์ฝ 14.71 => ์ด ๋ชจ๋ธ์ ํ๊ท ๋ฐ์ง๋ฆ์ด ์ด ๊ฐ๋ณด๋ค ํฌ๋ฉด 0(์ ์ฑ), ์์ผ๋ฉด 1(์์ฑ)์ผ๋ก ์์ธกํฉ๋๋ค.
Step 5: Inspect Predicted Probabilities and Make Predictions
Logistic Regression calculates probabilities internally.
predict() returns the final classification (0 or 1),
whereas predict_proba() lets you inspect the probability of belonging to each class (0 and 1).
Use this to perform Step 5: prediction.
# Inspect predicted probabilities for the first 5 Test samples
# Results have the form [probability of 0, probability of 1].
probabilities = model.predict_proba(X_test[:5])
print("--- ์ฒซ 5๊ฐ Test ๋ฐ์ดํฐ์ ์์ธก ํ๋ฅ ---")
print("[ 0(์
์ฑ)์ผ ํ๋ฅ , 1(์์ฑ)์ผ ํ๋ฅ ]")
print(np.round(probabilities, 3)) # Round to 3 decimal places
# Inspect final predictions for the first 5 Test samples
predictions = model.predict(X_test[:5])
print("\n--- ์ฒซ 5๊ฐ Test ๋ฐ์ดํฐ์ ์ต์ข
์์ธก ---")
print(predictions)
# Predict on new tumor data
# A tumor with mean radius 10
new_tumor_small = np.array([[10]])
pred_small_prob = model.predict_proba(new_tumor_small)
pred_small = model.predict(new_tumor_small)
print(f"\n--- ํ๊ท ๋ฐ์ง๋ฆ 10์ธ ์ข
์ ---")
print(f" ์์ธก ํ๋ฅ [์
์ฑ, ์์ฑ]: {np.round(pred_small_prob[0], 3)}")
print(f" ์ต์ข
์์ธก: {'์์ฑ(Benign)' if pred_small[0] == 1 else '์
์ฑ(Malignant)'}")
# A tumor with mean radius 20
new_tumor_large = np.array([[20]])
pred_large_prob = model.predict_proba(new_tumor_large)
pred_large = model.predict(new_tumor_large)
print(f"\n--- ํ๊ท ๋ฐ์ง๋ฆ 20์ธ ์ข
์ ---")
print(f" ์์ธก ํ๋ฅ [์
์ฑ, ์์ฑ]: {np.round(pred_large_prob[0], 3)}")
print(f" ์ต์ข
์์ธก: {'์์ฑ(Benign)' if pred_large[0] == 1 else '์
์ฑ(Malignant)'}")--- ์ฒซ 5๊ฐ Test ๋ฐ์ดํฐ์ ์์ธก ํ๋ฅ --- [ 0(์ ์ฑ)์ผ ํ๋ฅ , 1(์์ฑ)์ผ ํ๋ฅ ] [[0.267 0.733] [0.147 0.853] [0.052 0.948] [0.989 0.011] [0.44 0.56 ]] --- ์ฒซ 5๊ฐ Test ๋ฐ์ดํฐ์ ์ต์ข ์์ธก --- [1 1 1 0 1] --- ํ๊ท ๋ฐ์ง๋ฆ 10์ธ ์ข ์ --- ์์ธก ํ๋ฅ [์ ์ฑ, ์์ฑ]: [0.008 0.992] ์ต์ข ์์ธก: ์์ฑ(Benign) --- ํ๊ท ๋ฐ์ง๋ฆ 20์ธ ์ข ์ --- ์์ธก ํ๋ฅ [์ ์ฑ, ์์ฑ]: [0.996 0.004] ์ต์ข ์์ธก: ์ ์ฑ(Malignant)