addinedu

iris with decision tree

붓꽃 데이터셋을 활용한 Decision Tree 분류 모델 구현 및 결정 경계 시각화. 데이터 탐색, 모델 학습, 과적합 분석, 오분류 시각화를 포함한 머신러닝 파이프라인 실습

Machine Learning
Python
Scikit-learn
In [181]:
# 붓꽃 데이터 로드

from sklearn.datasets import load_iris
import pandas as pd

iris = load_iris()
iris_pd = pd.DataFrame(iris.data, columns=iris.feature_names)
iris_pd.head()
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
Plain text view
   sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)
0                5.1               3.5                1.4               0.2
1                4.9               3.0                1.4               0.2
2                4.7               3.2                1.3               0.2
3                4.6               3.1                1.5               0.2
4                5.0               3.6                1.4               0.2
In [182]:
# 정답 컬럼 추가

iris_pd["species"] = iris.target
iris_pd.head()
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) species
0 5.1 3.5 1.4 0.2 0
1 4.9 3.0 1.4 0.2 0
2 4.7 3.2 1.3 0.2 0
3 4.6 3.1 1.5 0.2 0
4 5.0 3.6 1.4 0.2 0
Plain text view
   sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)  \
0                5.1               3.5                1.4               0.2   
1                4.9               3.0                1.4               0.2   
2                4.7               3.2                1.3               0.2   
3                4.6               3.1                1.5               0.2   
4                5.0               3.6                1.4               0.2   

   species  
0        0  
1        0  
2        0  
3        0  
4        0  

데이터 시각화

첫번째로 꽃받침 (sepal)의 길이와 너비를 살펴본다. 우리는 boxplot 을 이용해 데이터를 시각화한다.

Boxplot을 사용하면 다음과 같은 장점이 있다.

  • 데이터 분포 확인: 중앙값, 사분위수, 최솟값/최댓값 파악
  • 이상치(outlier) 탐지
  • 품종별 비교
In [183]:
# 꽃받침 길이 (sepal length)

import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
sns.boxplot(
    x="sepal length (cm)",
    y="species",
    hue="species", # 품종을 기준으로 색깔 구분
    data=iris_pd,
    orient="h"
)
plt.tight_layout()
plt.show()
Notebook output

꽃받침 길이로는 Setosa(0)가 짧아 다른 품종과 구분됨을 알 수 있다. 다만 Versicolor(1)와 Verginica(2)는 꽃받침 길이만으로는 완벽한 분류가 불가능하다.

In [184]:
# 꽃받침 폭 (sepal width)

import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(12,6))

sns.boxplot(
    x="sepal width (cm)",
    y="species",
    hue="species",
    data=iris_pd,
    orient="h"
)
plt.tight_layout()
plt.show()
Notebook output

꽃받침 너비로는 품종간 구분이 어렵다. 다음으로 꽃잎(petal) 길이와 너비를 살펴보자. 이번에는 scatter plot 을 사용해본다.

scatter plot 을 사용하면 다음과 같은 것들이 가능하다.

  • 상관관계 파악
  • 데이터 군집 확인
  • 분류 가능성 탐색
In [185]:
# 데이터 시각화 scatter plot
plt.figure(figsize=(12, 6))

sns.scatterplot(
    x="petal length (cm)",
    y="petal width (cm)",
    hue="species",
    data=iris_pd,
)
plt.show()
Notebook output

꽃잎길이(petal length)를 사용하면 3가지 종을 나눌 수 있는 가능성을 발견했다. 다음으로 petal length 와 Decision boundary 를 사용하여 각각을 분류해보자.

In [186]:
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

X = iris_pd[["petal length (cm)", "petal width (cm)"]].values
y = iris_pd["species"].values

# 데이터를 훈련 세트와 테스트 세트로 분할
# X: 특성 데이터 (petal length, petal width)
# y: 타겟 데이터 (species)
# test_size=0.2: 전체 데이터의 20%를 테스트 세트로 사용 (80%는 훈련 세트)
# random_state=42: 랜덤 시드 고정으로 재현 가능한 결과 보장
# stratify=y: 클래스 비율을 유지하며 분할 (각 클래스가 훈련/테스트 세트에 균등하게 분배됨)
X_train, X_test, y_train, y_test = train_test_split(
    X, 
    y, 
    test_size=0.2, 
    random_state=42, 
    stratify=y
)

clf = DecisionTreeClassifier(max_depth=2, random_state=42)
clf.fit(X_train, y_train)

y_pred_train = clf.predict(X_train)
y_pred_test = clf.predict(X_test)


train_accuracy = accuracy_score(y_train, y_pred_train)
test_accuracy = accuracy_score(y_test, y_pred_test)
print(f"Train Accuracy: {train_accuracy:.2f}")
print(f"Test Accuracy: {test_accuracy:.2f}")
Train Accuracy: 0.97
Test Accuracy: 0.93

결정 경계를 시각화 해보자

In [187]:
# 결정 경계 시각화를 위한 x축, y축 범위 설정
# X[:, 0]: 첫 번째 특성(petal length)의 최솟값과 최댓값
# X[:, 1]: 두 번째 특성(petal width)의 최솟값과 최댓값
# 각각 -1, +1을 해서 여백을 추가하여 경계가 잘리지 않도록 함
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1

# 결정 경계를 그리기 위해 전체 영역에 대한 그리드 생성
# meshgrid: x축과 y축 범위에 대해 0.01 간격으로 격자 좌표 생성
# xx, yy는 각각 2D 배열로, 모든 (x, y) 좌표 조합을 포함
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01),
                     np.arange(y_min, y_max, 0.01))

# 각 그리드 포인트에 대해 모델의 예측 수행
# ravel(): 2D 배열을 1D로 평탄화 (예: [[1,2],[3,4]] -> [1,2,3,4])
# np.c_: 두 1D 배열을 열(column)로 결합하여 (N, 2) 형태의 배열 생성
# predict: 각 (x, y) 좌표에 대해 어떤 클래스로 분류되는지 예측
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])

# 예측 결과를 다시 원래 그리드 형태(xx.shape)로 변환하여 contourf로 시각화 가능하게 함
Z = Z.reshape(xx.shape)

# 그래프 크기 설정 (가로 12, 세로 6)
plt.figure(figsize=(12, 6))

# contourf: 결정 경계 영역을 채워서 그리기
# xx, yy: 그리드 좌표, Z: 각 좌표의 예측 클래스, alpha: 투명도 (0.3 = 30% 불투명)
plt.contourf(xx, yy, Z, alpha=0.3)

# scatter: 실제 데이터 포인트를 산점도로 표시
# X[:, 0], X[:, 1]: petal length와 petal width 값
# c=y: 색상은 실제 클래스(y)에 따라 구분, edgecolor='k': 점 테두리는 검은색
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, edgecolor='k')
plt.title("Decision Tree Boundary")
plt.xlabel("Petal Length (cm)")
plt.ylabel("Petal Width (cm)")
plt.show()
Notebook output

Decision tree 의 깊이를 2로 했기 때문에 2줄이 그어졌다. 만약 test / train set 으로 나누지 않고 decision tree depth 를 None 으로 한다면 다음과 같은 과적합(overfitting) 모델이 만들어진다.

In [188]:
# 과적합 예시

clf_overfit = DecisionTreeClassifier(max_depth=None, random_state=42)
clf_overfit.fit(X, y)

Z = clf_overfit.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

plt.figure(figsize=(12, 6))
plt.contourf(xx, yy, Z, alpha=0.3)
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, edgecolor='k')
plt.title("Decision Tree Boundary (Overfit)")
plt.xlabel("Petal Length (cm)")
plt.ylabel("Petal Width (cm)")
plt.show()
Notebook output

모델이 과적합이 되면 해당 모델은 일반화 할 수 있는 가능성을 스스로 낮추게 된다. 따라서 과적합은 항상 지양하여야 한다.

이제 오분류 데이터를 시각화해보자.

In [189]:
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
wrong_pred = y_pred_test != y_test

# 결정 경계 (배경)
plt.figure(figsize=(12, 6))
plt.contourf(xx, yy, Z, alpha=0.25, cmap="Set2")

# train 데이터
plt.scatter(
    X_train[:, 0], 
    X_train[:, 1], 
    c=y_train,
    alpha=1,
    cmap="Set2",
    label="Train",
)
# test 데이터
plt.scatter(
    X_test[:, 0], 
    X_test[:, 1], 
    c=y_test,
    marker='^',
    cmap="Set2",
    s=80,
    edgecolor='k',
    label="Test"
)
# missclassified
plt.scatter(
    X_test[wrong_pred, 0],
    X_test[wrong_pred, 1],
    facecolors="none",
    edgecolors="red",
    s=160,
    linewidth=2,
    label="Missclassified",
)

plt.title("Decision Tree Boundary + Missclassified")
plt.legend()
plt.show()
Notebook output