주제
1. 선형 SVM
2. 비선형 SVM
3. SVR
4. Decision Tree
5. SVM과 Decision Tree를 이용한 실습
선형 SVM
- 두 데이터를 나누는 직선(고차원의 경우 초평면)을 찾는 것
- 이때 직선과 평행한 두 개의 직선을 만들 수 있고, 이 두 직선 사이의 거리를 마진(margin)이라고 함
- 서포트 벡터(support vector) : 마진을 구성하는 데이터(마진 선에 걸친 데이터들)

- SVM은 마진을 최대화 하는 최적 직선(최대 마진 초평면)을 만드는 것이 목적

- w : 최대 마진 초평면의 법선 벡터
- b : 편향값
1.1 서포트 벡터(support vector)
- 평행한 두 직선 위에 서포트 벡터가 존재함
- 서포트 벡터는 아래의 식을 만족함

- w^T + b ≥ 1 인 영역에서는 y = 1을, w^T + b ≤ 1인 영역에서는 y = -1을 만족한다
- 마진은 점과 직선 사이의 거리 공식을 이용하여 다음과 같이 표현이 가능하다


- 최적화 목표 : 마진을 키우면서 모든 데이터를 알맞게 분류

1.2 하드 마진 SVM & 소프트 마진 SVM
- 하드 마진 SVM : 어떠한 오분류도 허용하지 않고 완벽한 선형 모델로 분리가 가능한 것
- 소프트 마진 SVM : 어느 정도의 오분류를 허용하면서 오차 발생에 따른 패널티를 비용 함수에 부과하여 일반화 성능을 올릴 수 있는 것
1.2.1 슬랙 변수(Slack variable)
- 소프트 마진 SVM에서 사용하는 개념으로, 각 데이터 포인트(i) 당 하나의 슬랙 변수 ξ(크사이)가 할당된다
- 슬랙 변수 : 해당 데이터 포인트가 마진을 얼마나 위반하는지 수치적으로 나타내는 변수
- 마진을 위반하지 않은 데이터 : ξi = 0
- 서포트 벡터, 서포트 벡터보다 멀리 있는 데이터
- 마진을 위반한 데이터 :
- 마진 경계 ~ 결정 경계 : 0 < ξi ≤ 1
- 결정 경계 이후 : 1 < ξi (올바르지 못한 클래스로 분류됨)

1.3 소프트 마진 SVM의 최적화 함수
- 소프트 마진 SVM은 하드 마진 SVM 최적화 과정에 규제 패널티(ξi)를 도입하여 일반화한 식을 사용
- 식 아래는 조건이다

- 목적 : 마진의 크기를 최대화 & 마진 위반을 최소화
- C : 일반화를 위한 하이퍼파라미터로 마진 크기와 규제 사이의 중요도 변수. 마진 크기와 규제 사이를 밸런싱
- 1 - ξi : 규제가 적용될 데이터 포인트에 대해, 결정 경계에서 ξi의 거리만틈 벗어날 수 있음을 허용하는 과정
- ξi ≥ 0 : ξi이 음수를 가질 수 없음을 조건으로 제시
실습
- 선형 SVM을 위한 데이터 생성
# 데이터 생성
X1, y1 = make_classification(n_samples=100, # 생성할 데이터 수
n_features=2, # 사용할 특성의 수
n_redundant=0, # 중복 특성(다른 특성으로부터 파생된 특성) 수, 상관관계
n_clusters_per_class=1, # 클래스 당 클러스터의 수
flip_y=0, # 클래스 레이블이 뒤바뀔 확률, 노이즈에 해당
class_sep=2, # 클래스 간 분리도를 조절, 높을수록 분리가 잘 됨을 의미
random_state=5)

- 하드 마진 SVM
- 커널은 선형(linear), C는 1000으로 지정하여 학습
svm_hard_margin = SVC(kernel='linear', C=1000)
svm_hard_margin.fit(X1, y1)
- 결과 시각화
plt.scatter(X1[:, 0], X1[:, 1], c=y1, cmap=plt.cm.Paired)
plt.xlabel('feature1')
plt.ylabel('featuer2')
plt.margins(0.2)
# 현재 그래프의 x축과 y축 범위를 가져옴
xlim = plt.gca().get_xlim()
ylim = plt.gca().get_ylim()
# 그래프의 x & y축 범위를 바탕으로 모든 x,y 조합의 좌표 그리드를 생성
xx, yy = np.meshgrid(np.linspace(xlim[0], xlim[1], 50),
np.linspace(ylim[0], ylim[1], 50))
# 그리드 포인트를 이용해 각 포인트에서의 Desicion 결과값을 출력
Z = svm_hard_margin.decision_function(
np.column_stack(
(xx.ravel(), # xx matrix를 1차원 행렬로 flatten
yy.ravel()) # yy matrix를 1차원 행렬로 flatten
) # 1 차원 행렬을 열 방향으로 묶어줌
) # 입력된 각 (x, y) 포인트에 대해 결정 경계로부터의 거리를 계산
Z = Z.reshape(xx.shape)
# 결정 경계와 마진 직선을 그리는 함수
plt.contour(xx, yy, # xx와 yy 공간 안에
Z, # Z를 그릴건데
levels=[-1, 0, 1], # Z가 -1, 0, 1 인 부분만 그릴것!
colors=['orange', 'blue', 'orange'],
alpha=0.5,
linestyles=['--', '-', '--'] # -1, 0, 1 의 직선 개형을 표시
)

- 소프트 마진 SVM
- 노이즈가 섞인 데이터 생성
# 데이터 생성
X2, y2 = make_classification(n_samples=100, # 생성할 데이터 수
n_features=2, # 사용할 특성의 수
n_redundant=0, # 중복 특성(다른 특성으로부터 파생된 특성) 수
n_clusters_per_class=1, # 클래스 당 클러스터의 수
flip_y=0.06, # 클래스 레이블이 뒤바뀔 확률, 노이즈에 해당
class_sep=1.5, # 클래스 간 분리도를 조절, 높을수록 분리가 잘 됨을 의미
random_state=5)

- 커널은 선형으로 동일, 단 c의 값을 0.1로 부여
svm_soft_margin = SVC(kernel='linear', C=0.1)
svm_soft_margin.fit(X2, y2)
- 결과를 위한 시각화 진행
plt.scatter(X2[:, 0], X2[:, 1], c=y2, cmap=plt.cm.Paired)
plt.xlabel('feature1')
plt.ylabel('featuer2')
plt.margins(0.2)
# 현재 그래프의 x축과 y축 범위를 가져옴
xlim = plt.gca().get_xlim()
ylim = plt.gca().get_ylim()
# 그래프의 x & y축 범위를 바탕으로 모든 x,y 조합의 좌표 그리드를 생성
xx, yy = np.meshgrid(np.linspace(xlim[0], xlim[1], 50),
np.linspace(ylim[0], ylim[1], 50))
# 그리드 포인트를 이용해 각 포인트에서의 Desicion 결과값을 출력
Z = svm_soft_margin.decision_function(
np.column_stack(
(xx.ravel(), # xx matrix를 1차원 행렬로 flatten
yy.ravel()) # yy matrix를 1차원 행렬로 flatten
) # 1 차원 행렬을 열 방향으로 묶어줌
) # 입력된 각 (x, y) 포인트에 대해 결정 경계로부터의 거리를 계산
Z = Z.reshape(xx.shape)
# 결정 경계와 마진 직선을 그리는 함수
plt.contour(xx, yy, # xx와 yy 공간 안에
Z, # Z를 그릴건데
levels=[-1, 0, 1], # Z가 -1, 0, 1 인 부분만 그릴것!
colors=['orange', 'blue', 'orange'],
alpha=0.5,
linestyles=['--', '-', '--'] # -1, 0, 1 의 직선 개형을 표시
)

- 다양한 c값에 따른 결과 확인
- C값이 높을수록 더 적절한 직선을 찾는 것을 확인할 수 있다

2. 비선형 SVM
- 데이터의 복잡성이 증가하면 선형 결정 경계로 데이터를 분류할 수 없는 경우가 있음(데이터가 휘어진 상태)
- 따라서 비선형 SVM을 사용

2.1 고차원 데이터
- 선형적으로 분리할 수 없는 데이터를 고차원으로 변형하면 고차원의 초평면으로 분리할 수 있는 형태가 나옴
- 어떻게?
- 차원이 증가하면 데이터 포인트 간의 상대 거리 증가
- 차원에서 데이터끼리 차지하는 공간 확징
- 비슷한 특성을 공유하는 데이터들은 특정한 축 혹은 방향으로 군집될 가능성 증가
- 고차원의 데이터일수록 선형으로 분류할 수 있는 가능성이 높다
- 저차원의 데이터를 고차원의 데이터로 옮기는 과정의 함수 : Mapping function(ф)
- 단점 :
- 고차원의 데이터는 계산량이 늘어남
- 확장한 고차원이 원본 데이터의 차원보다 훨씬 크다면 모델의 복잡도 ↑, 효율성 ↓
- 이를 해결하기 위해 커널 트릭이 제시
2.1.1 커널 트릭(Kernel Trick)
- 데이터를 선형 분류하기 위해서는 내적 계산이 필요 → 고차원에서도 필요
- 내적 : 두 임의의 vector 사이의 유사도를 측정 & 선형 경계를 생성하는데 사용
- 고차원에서의 내적 계산식

- 하지만, 계산량이 너무 많다는 단점이 있음
- 커널 트릭 : 고차원의 내적 연산의 결과와 똑같은 결과를 보여주는 저차원 vector끼리의 연산 함수

- 고차원으로 변형하지 않고, 저차원 데이터만으로 고차원 데이터의 내적 연산의 효과를 가짐
- 커널의 종류 :
- 다항 커널(Polynomial Kernel) : 다양한 차수 설정으로 여러 식을 근사할 수 있음. 과적합의 위험 존재
- RBF 커널(Radial Basis Function Kernel) 혹은 가우시안 커널(Gaussian Kernel) : 다양한 데이터에 적용하면서도 유연성이 높아 범용성이 높음
- 시그모이드 커널(Sigmoid Kernel) : 이진 분류에 최적화되며 RBF 커널에 비해 성능이 떨어짐



2.2 비선형 SVM의 최적화
- 소프트 마진과 비슷하지만 저차원이 아니라 고차원의 데이터 분류가 가능하도록 하는 조건이 들어감

- 목적 : 마진의 크기를 최대화 & 마진 위반을 최소화
- 아래 조건은 고차원 데이터에서 선형 분류가 가능함을 표시하는 조건

실습
- make_moons를 이용한 비선형 데이터 생성
from sklearn.datasets import make_moons
# 데이터 생성
X_moons, y_moons = make_moons(n_samples=100, noise=0.15, random_state=0)
# 시각화
plt.figure(figsize=(6, 5))
plt.scatter(X_moons[:, 0], X_moons[:, 1], c=y_moons, cmap=plt.cm.Paired)
plt.title("Moon Shaped Data")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()

- 비선형 SVM으로 분류하고 다양한 커널을 시도
# 비선형 SVM 으로 분류
# 선형 커널
Linear_SVM = SVC(kernel='linear')
# 다항 커널
Poly_SVM = SVC(kernel='poly', degree=3)
# RBF 커널
RBF_SVM = SVC(kernel='rbf')
# Sigmoid 커널
sigmoid_SVM = SVC(kernel='sigmoid')

- RBF 커널이 가장 잘 분류한 것을 확인할 수 있다
3. SVR(Support Vector Regression)
- 회귀 문제에 적용되는 SVM : SVR
- 주어진 데이터에서 가능한 많은 데이터 포인트를 포함하는 마진 구역을 설정
- 마진 구역은 사용자가 선언한 허용 오차(ε) 내부의 구역
- 그 구연 안에서 회귀선(초평면)을 찾는 것이 목표

3.1 SVR의 최적화
- 커널 함수를 적용한 SVR의 최적화 함수는 다음과 같다

- 정답과 예측값 사이의 차이가 ε 이라는 변수 안에서 허용될 수 있음을 내포함
- 양쪽 방향의 슬랙 변수를 도입해 많은 데이터 포인트를 포괄하도록 강제
실습
- 실습을 위한 데이터 생성
# 데이터 생성 변수
w0 = 2.3
w1 = 3.5
num_data = 100
noise = np.random.normal(0, 6, num_data)
# 데이터 생성
x = np.linspace(0, 10, num_data)
y = w0 + w1 * x + noise

- epsilon을 5로 지정한 SVR 분류 실행(시각화 코드는 생략)
from sklearn.svm import SVR
epsilon = 5
# SVR 객체 생성
svr_linear = SVR(kernel='linear', epsilon=epsilon)
svr_poly = SVR(kernel='poly', degree=3, epsilon=epsilon)
svr_rbf = SVR(kernel='rbf', epsilon=epsilon)
svr_sigmoid = SVR(kernel='sigmoid', epsilon=epsilon)
# SVR 모델 학습
svr_linear.fit(x.reshape(-1, 1), y)
svr_poly.fit(x.reshape(-1, 1), y)
svr_rbf.fit(x.reshape(-1, 1), y)
svr_sigmoid.fit(x.reshape(-1, 1), y)

- 생성된 데이터가 선형 관계였기 때문에 선형 SVR이 가장 적합한 모델임을 알 수 있다.
4. Decision Tree
- 데이터를 분할하는 기준을 결정하는데 사용되는 방법론
- 트리의 각 단계에서 최적의 분할을 찾기 위해 사용
- 트리의 깊이와 복잡성을 관리할 수 있음
- 좋은 결정 기준은 트리를 간결하고 효율적으로 만들며 과적합을 방지하고 일반화 성능을 향상시킴
- 용어 :
- 노드(Node) : 데이터에 대한 특정 질문이나 조건. 데이터를 분류하는 과정에서 사용
- 엣지(Edge) : 노드와 노드를 연결하는 선. 상위 노드의 특정 질문에 대한 가능한 답변
- 루트 노드(Root node) : 트리의 가장 상단에 위치한 노드. 분류 또는 예측을 시작한는 지점
- 분할 노드(=결정 노드) : 데이터를 더 작은 하위 집합으로 나누는 데 사용되는 중간 노드
- 리프 노드(=터미널 노드) : 트리의 말단에 위치한 노드. 더 이상의 분기가 없고 자식 노드를 갖지 않음

- 분류에서 사용되는 결정 기준 :
- 정보 이득
- 지니 불순도
- 회귀에서 사용되는 결정 기준 :
- MSE 최소화
4.1 엔트로피(Entropy)
- 엔트로피 : 어떤 상황이나 현상이 품고 있는 불확실성을 의미, 포함하는 정보의 양과 반비례
- 엔트로피 ↑, 불확실성 ↑, 정보량 ↓
- 엔트로피의 수식은 다음과 같다

- P(xi) : 각 사건의 발생 확률. 불확실성이 높은 상황일수록 다양한 사건의 발생 확률이 균일
- log2P(xi) : 이진 로그를 사용해 비트 단위의 정보량을 표현
- -∑ : 발생할 각 사건을 모두 더한 뒤, 로그값을 양수로 만들기 위해 -를 취함
4.2 정보 이득(Information Gain)
- 부모 노드와 자식 노드들의 엔트로피를 계산해 엔트로피가 낮아지는 방향으로 결정 경계를 선정하는 것
- 정보 이득을 최대화하는 방향 = 엔트로피가 낮아지는 방향

- 각 노드에 포함되는 데이터의 순도에 따라 엔트로피가 계산됨(엔트로피가 클수록 고르게 분포, 순도가 낮음)
- 노드 안에 서로 다른 클래스의 데이터가 많이 섞여 있으면 순도 ↓
- 같은 클래스의 데이터가 모여있다면 순도 ↑
4.3 지니 불순도(Gini Impurity)
- 데이터 안에 존재하는 클래스 분포의 불균형을 평가하는 방법(집합의 순도를 측정)

- Pi : 데이터 집합 안에 존재하는 i 번째 클래스가 나타나는 확률
- 0 이상 1 미만의 값을 가짐
- 0 : 모든 데이터가 하나의 클래스에 속함. 제일 순도가 높은 상태
- 1에 가까운 값 : 모든 클래스의 데이터가 고루 섞인 상태로 불순도가 제일 높음
4.4 회귀 문제를 위한 Decision Tree
4.4.1 MSE 최소화 방식
- 각 노드에서 실제 정답과 예측값 사이의 평균 제곱 오차의 평균을 계산하고 이 값을 최소화하는 노드를 찾아가는 방식
4.5 Tree 사용의 주의사항
- 트리 구조의 장점 :
- 해성 용이성 & 사용 편리
- 적당한 성능
- 스케일링에 둔감함
- 주의점 :
- 트리 기반 모델은 축에 수직인 방향으로 데이터가 분할됨
- 경계면이 회전된 데이터는 경계면이 구불해짐
- 일반화가 어려워짐
- 필요 시 주성분 분석을 사용
- 데이터 노이즈에 굉장히 민감
- 특정 데이터의 추가가 전체 모델 결과에 큰 변화를 줄 수 있음
- Depth가 깊다면 강한 overfit의 위험이 큼
- 트리 기반 모델은 축에 수직인 방향으로 데이터가 분할됨
5. SVM과 Decision Tree를 이용한 실습
- 어제 사용한 비행 경헙 만족도 데이터를 그대로 사용
- 전처리는 동일하게 진행
SVM 학습
- 가장 좋은 성능을 보였던 RBF 커널을 이용해 학습
- SVM 학습의 학습 시간은 약 30분 정도 소요
from sklearn.svm import SVC
svm = SVC(kernel='rbf', C=0.1)
svm.fit(X_train, y_train)
# 예측 수행
y_train_pred_svm = svm.predict(X_train)
y_test_pred_svm = svm.predict(X_test)
# 평가 지표 계산: 정확도 (맞은수/전체)
acc_train = accuracy_score(y_train, y_train_pred_svm)
acc_test = accuracy_score(y_test, y_test_pred_svm)
print(f'학습 데이터를 이용한 SVM Acc 값 : {acc_train*100:.1f}%')
print(f'평가 데이터를 이용한 SVM Acc 값 : {acc_test*100:.1f}%')

- 어제 실행한 로지스틱 회귀를 이용한 정확도는 각각 82.8%와 82.9%로 SVM이 더 높은 정확도를 보인다.
- 분류 평가 척도 :
| 정밀도(precision) | 재현율(recall) | F1 점수 |
| 예측한 양성 결과가 실제로 얼마나 진짜 양성인지를 계산 | 실제 양성 중 얼마나 양성을 잘 찾아냈는지를 계산 | 정밀도와 재현율의 조화 평균 |
| 모델이 양성 결과를 잘 찾아내야 하는 상황에서 중요 | 정답을 잘 찾아내는 과정에서 중요 | 조화 평균을 사용해 낮은 점수에 대한 패널티를 늘림. 정밀도와 재현율이 전반적으로 좋아야 좋은 F1 값을 가질 수 있음 |
# 정밀도, 재현율, F1 값 비교
from sklearn.metrics import precision_score, recall_score, f1_score
logistic_precision = precision_score(y_test, y_test_pred_logis)
logistic_recall = recall_score(y_test, y_test_pred_logis)
logistic_f1 = f1_score(y_test, y_test_pred_logis)
print(f'Logistic의 P,R,F1 : {logistic_precision:.2f} / {logistic_recall:.2f} / {logistic_f1:.2f}')
svm_precision = precision_score(y_test, y_test_pred_svm)
svm_recall = recall_score(y_test, y_test_pred_svm)
svm_f1 = f1_score(y_test, y_test_pred_svm)
print(f'SVM의 P,R,F1 : {svm_precision:.2f} / {svm_recall:.2f} / {svm_f1:.2f}')

Decision Tree 학습
- 엔트로피 결정 경계를 사용하는 최대 깊이 5의 트리를 생성
- Decision Tree는 SVM에 비해 학습 시간이 짧음
from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(criterion='entropy',
max_depth=5,
min_samples_split=5)
dt.fit(X_train, y_train)
- 트리가 잘 생성이 되었는지 시각화로 확인
from sklearn.tree import plot_tree
plt.figure(figsize=(100, 20))
plot_tree(dt, filled=True,
feature_names=ext_ordinal_columns + ext_category_columns,
class_names=['satisfied', 'unsatisfied'])
plt.title("Plot of Decision Tree")
plt.show()

최고의 모델 찾기
- 머신 러닝 모델은 크기가 커지고 복잡도가 증가할수록 성능은 올라간다
- 하지만 과적합 문제가 발생하면 성능이 하락한다
- 따라서 평가 데이터에 대한 성능이 낮아지기 시작하는 지점의 세팅을 이용해 최적의 모델을 선택해야 한다
- max depth에 따른 성능 비교(시각화 부분은 편의상 생략)
# max depth에 따른 학습 결과 경향성 파악
max_depths = range(3, 30)
train_accuracies = []
test_accuracies = []
for depth in max_depths:
model = DecisionTreeClassifier(criterion='entropy',
max_depth=depth,
min_samples_split=5)
model.fit(X_train, y_train)
# 학습 데이터에 대한 정확도
y_train_pred = model.predict(X_train)
train_acc = accuracy_score(y_train, y_train_pred)
train_accuracies.append(train_acc)
# 평가 데이터에 대한 정확도
y_test_pred = model.predict(X_test)
test_acc = accuracy_score(y_test, y_test_pred)
test_accuracies.append(test_acc)

- depth가 커질수록 테스트 성능은 떨어지는 것을 확인할 수 있다
- 더 높은 정확도의 모델을 만들기 위해서는 최대 정확도를 만드는 depth를 이용하면 된다
# 최대 정확도를 달성하는 max_depth를 찾고 해당 depth로 최적 모델 학습
max_acc = max(test_accuracies)
best_depth = max_depths[test_accuracies.index(max_acc)]
print('최대 정확도의 depth :', best_depth)
dt = DecisionTreeClassifier(criterion='entropy',
max_depth=best_depth,
min_samples_split=5)
- 이 데이터의 경우에는 최적의 depth는 16이므로 max_depth를 16으로 바꿔서 다시 학습해보면 정확도는 다음과 같이 상승한다
# 예측 수행
y_train_pred_dt = dt.predict(X_train)
y_test_pred_dt = dt.predict(X_test)
# 평가 지표 계산: 정확도 (맞은수/전체)
acc_train = accuracy_score(y_train, y_train_pred_dt)
acc_test = accuracy_score(y_test, y_test_pred_dt)
print(f'학습 데이터를 이용한 DT Acc 값 : {acc_train*100:.1f}%')
print(f'평가 데이터를 이용한 DT Acc 값 : {acc_test*100:.1f}%')

- 다른 평가 척도도 확인

- 이에 따르면 최적의 depth로 수정한 Decision Tree 모델의 성능이 무려 93.0%까지 상승한 것을 볼 수있다.
- 마찬가지로 정밀도, 재현율, F1 점수도 전부 올랐다.
- 이런 과정을 통하여 최고의 모델을 찾는 것이다.
'Data Science > TIL (Today I Learned)' 카테고리의 다른 글
| 프로그래머스 데이터분석 데브코스 1기 - 50일차 (0) | 2024.02.02 |
|---|---|
| 프로그래머스 데이터분석 데브코스 1기 - 49일차 (1) | 2024.02.01 |
| 프로그래머스 데이터분석 데브코스 1기 - 47일차 (1) | 2024.01.30 |
| 프로그래머스 데이터분석 데브코스 1기 - 46일차 (1) | 2024.01.29 |
| 프로그래머스 데이터분석 데브코스 1기 - 45일차 (1) | 2024.01.26 |