와! 어렵다! 흑흑
수학적 원리 이해보다 사용법을 먼저 익히고,
그다음 원리 이해를 하는 전략으로 가야겠다.
기본 알고리즘(4). Logistic Regression
Linear Regression에서 보았듯, Regression은 회귀 문제를 푸는 알고리즘이다.
그런데 Logistic Regression(로지스틱회귀)는 분류 문제를 푸는 회귀 모델이라는 독특한 모델이다.
이게 왜 그런가 하니...
위 그림처럼 분류 문제에서 이렇게 선형 경계가 나타날 때가 있다.
선 위면 파란색, 선 아래면 빨간색이다.
그런데 잘 보면, 경계에 가까운 점들은 선 위인데 파란색일 때도 있고 그 반대도 있다.
이런 경우, 우리는 저 경계 부근의 값을 두 가지로 처리할 수 있다.
- 경계 주변 일정 범위에 있는 값은 이상치로 다 날려 버린다.
- 경계에서 멀어질수록 100%에 가깝게, 경계에 가까워질수록 50%에 가깝게 표현한다.
1은 이후에 배울 Support Vector Machine의 방식이고,
2가 바로 Logistic Regression의 방식이다.
'경계쪽이면 위에 있어도 파랑 아닐 수도 있음~' 하는거지.
따라서 Logistic Regression에서는 데이터에 대해 두가지 처리를 한다.
Step 1.
Linear Regression과 마찬가지로 y = w0 + w1x의 편향(w0)과 가중치(w1)를 구함으로써
적절한 분류 기준을 회귀선의 형태로 나타낸다.
Step 2.
자료를 Sigmoid / Softmax 함수에 통과시켜 1과 0 사이의 확률값으로 만든다.
1단계야 지난 포스팅에 설명되어 있고,
2단계를 살펴보자.
위에서 Logistic Regression은 경계선에서 멀수록 확실한 분류,
가까울수록 불확실한 분류로 본다고 했다.
그런데 기본적으로 회귀선은 직선이고, 직선의 범위는 [-inf : inf]이다.(무한하다)
따라서 우리는 그 직선을 [0:1]로 바꾸어 주어야 한다.
이때 쓰이는 것이 Sigmoid 함수(Softmax는 뒤에서 살펴보자.)이다.
시그모이드 함수를 통과한 데이터는 직선이 아닌 s자 곡선 주변에 분포하게 되는데,
임계값 0.5를 기준으로 더 높으면 1일 확률이 높으니 1,
더 낮으면 0일 확률이 높으니 0으로 분류한다.
이것만 보면 그냥 회귀선 위아래로 끊는 것처럼 보일 수도 있지만,
시그모이드 함수를 통해 0~1사이의 값이 되었기에 확률 기반 해석이 가능해진다는 장점이 있다.
따라서 코드상으로도 각 row의 분류 결과뿐 아니라,
그 확률도 볼 수 있다.
# 확률값 확인
p = model.predict_proba(x_test)
print(p[:10].round(3)) # 왼쪽은 0일 확률, 오른쪽은 1일 확률
이를테면 위 확률표의 8번째 행은, 0.578 : 0.422의 확률로 0으로 판결되었다.
이 행의 대상은 회귀선에 가깝다는 뜻이다.
이러한 특징으로 인해, 분석자의 판단에 따라 임계점을 조절해 recall을 올리거나 내릴 수도 있다.
# 새로운 예측값 : 임계값 조정 가능 - 임계값을 낮추면 1을 더 적극적으로 판별
y_pred2 = [1 if x > 0.4 else 0 for x in p1] # if 컴프리헨션 : 0.4로 임계값 조절
print('첫번째 예측: ',[x for x in y_pred[:10]]) # y_pred2와 포맷을 맞추기 위한 처리
print('두번째 예측: ', y_pred2[:10])
print('첫번째 예측의 recall:', recall_score(y_test, y_pred))
print('두번째 예측의 recall:', recall_score(y_test, y_pred2))
이 자료는 당뇨병 예측 모델이었는데, 진료 모델의 특성상 recall이 중요하므로
0.4로 낮추는게 적절한 조치였던 것으로 보인다.
Softmax의 경우, n차원으로 바뀌기에 그림으로 나타낼 수는 없지만
마찬가지로 각 범주에 속할 확률로 변환해 주는 함수다.
softmax 함수를 통과한 범주 3개짜리 자료의 확률표.
범주끼리 합하면 1이 된다.
기본 알고리즘(5). SVM(Supprort Vector Machine)
SVM은 자료가 위 그림처럼 나뉠 때, 최선의 결정 경계선을 찾는 알고리즘이다.
이때 각각의 점들이 Vector,
Vector 중에서 결정 경계선을 만드는 결정적 역할을 한 점을 Support Vector라고 한다.
따라서 Support Vector는 결정 경계선이 평행이동할 수 있는 한계점이 되며,
이때 경계선이 평행이동할 수 있는 범위를 Margin이라고 한다.
그렇다면 이 과정에서 Margin 안에 들어온 점이 있다면,
즉 Support Vector 보다 더 결정 경계선에 가까운 점이 있다면
이것들은 이상치로 취급하게 된다.
<Margin 안의 점들을 이상치로 처리하는 이유>
Margin을 좁게 잡는 것,
즉 결정 경계선에 가까운 = 각 범주에 모호하게 속한 점들을 Support Vector로 삼는 것을
'비용(C / Cost)을 높인다'라고 표현한다.
비용이 높아질수록 모델이 데이터에 예민하게 반응한다는 것이고,
KNN에서도 봤지만 이것은 모델의 복잡도가 높다(= 과적합의 위험성이 높다)는 것을 의미한다.
<참고사항>
SVM은 분류 모델인 SVC와 회귀 모델인 SVR로 나뉘는데,
우리가 일반적으로 접하게 되는 것은 SVC이다.
헌데, 현실 세계의 데이터란 일반적으로 비선형적인 경우가 더 많다.
이런 비선형 데이터는 데이터의 차원을 높여서 분류가능한데,
실제로 데이터를 옮기는 것은 계산량이 지나치게 많아 사용하기가 힘들다.
따라서 SVC에서는 파라미터를 통한 커널 트릭을 지원한다.
<커널 트릭>
데이터의 차원을 실제로 높이지는 않으면서 높인 것과 같은 효과를 주는 방법.
2차원 데이터를 3차원에서 보면 결정 경계선을 매우 빠르게 찾을 수 있다.
위 이미지에 커널 트릭을 적용시켜 3차원에서 본 모습.
결정 경계선이 쉽게 보인다.
따라서 SVC는 비선형 자료에서도 결정 경계선을 생성할 수 있다.
커널 트릭을 사용하지 않았을 때
# 모델링
model = SVC(kernel='linear')
model.fit(x, y)
# 시각화
svc_visualize(x, y, model)
커널 함수 1. poly
# 모델링
model = SVC(kernel='poly', C=1)
model.fit(x, y)
# 시각화
svc_visualize(x, y, model)
커널 함수 2. rbf
# 모델링
model = SVC(kernel='rbf', C=1)
model.fit(x, y)
# 시각화
svc_visualize(x, y, model)
이때, 회귀 곡선의 성능에 영향을 주는 지표가 둘 있다.
하나는 앞서 본 C(cost) 하나는 gamma이다.
위에서 말했듯 cost는 회귀선이 margin을 얼마나 빡세게(?) 만드느냐,
즉 자신에게 가까운 점들에 얼마나 민감하게 반응하느냐를 나타낸 것이다.
높을수록 복잡도가 커져 과적합의 위험이 있다.
gamma는 각 점들이 곡선에 얼마나 큰 영향을 미치느냐를 나타낸 값이다.
마찬가지로 점들의 영향이 커진다 = 모델이 민감해진다 = 복잡도가 증가한다. = 과적합의 위험이 커진다.
K-Fold Cross Validation
사실 실전에서는, test 데이터란 미래의 데이터이기 때문에
성능 평가를 위해 데이터 일부를 검증용 데이터(Validation data)로 빼 둬야 한다.
하지만 검증용 데이터의 대표성은 100% 보장될 수 없다.
따라서 이런 대표성 문제를 해결하기 위해서
모든 학습용 데이터가 한 번씩은 검증 데이터로 쓰여야 한다.
이를 달성하기 위한 방법론이 K-Fold Cross Validation(K-분할 교차 검증)이다.
위 그림은 분할 교차 K=5일 때의 데이터이다.
학습용 데이터의 1/K만큼을 검증용 데이터로 선정하고,
검증용 데이터를 바꿔 가며 K번의 학습 및 평가를 거치는 것이다.
이렇게 하면 모든 데이터는 1번의 평가, K-1번의 학습에 이용된다.
다시말해, 데이터의 편향 없이 균등한 학습이 이루어진다.
따라서 이를 통해 평가된 모델 성능은 신뢰도가 있다고 볼 수 있다.
이 방법도 sklean에서 지원한다.
sklearn의 model_selection에 속한 cross_val_score을 사용하면 된다.
# 불러오기
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
# 선언하기
model = DecisionTreeClassifier(max_depth=5, random_state=1)
# 검증하기
cv_score = cross_val_score(model, x_train, y_train, cv=10)
# 확인
print(cv_score)
print('평균:', cv_score.mean())
k값을 입력하는 파라미터가 cv이다.
cross_val_score은 배열을 반환하고, 그 평균이 모델의 성능이 된다.
이 과정은 model.fit()이전에 실행된다.
즉, cross_val_scored()는 모델 선언 단계에서 모델의 성능을 우선 확인하기 위한 것이다.
실전에서 사용할 모델을 정하기에 앞서, 모델 간의 예측 성능을 비교할 수 있을 테니까.
따라서 매 모델 선언 단계에서 cv_score을 수집해서 시각화하면,
예상되는 모델의 성능을 비교할 수 있다.
코드로 보자.
# KNN
from sklearn.neighbors import KNeighborsClassifier
model = KNeighborsClassifier()
# 검증 및 수집
cv_score = cross_val_score(model, x_train_s, y_train, cv=10)
cvs = {}
cvs['KNeighborsClassifier'] = cv_score.mean()
# DecisionTree
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier()
# 검증 및 수집
cv_score = cross_val_score(model, x_train, y_train, cv=10)
cvs['DecisionTreeClassifier'] = cv_score.mean()
# LogisticRegression
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
# 검증 및 수집
cv_score = cross_val_score(model, x_train, y_train, cv=10)
cvs['LogisticRegression'] = cv_score.mean()
이렇게 빈 딕셔너리 cvs를 선언한 후,
매 모델 선언마다 해당 알고리즘을 key로, cv_score의 평균을 value로 갖는 요소로 채운다.
이후에 그걸 시각화하면
plt.barh(cvs.keys(), cvs.values())
plt.show()
이런 성능 비교 그래프를 얻을 수 있다.
Learning Curve
한편, 적절한 학습 데이터가 몇 개인지 시각적으로 알아 보는 방법도 있다.
머신러닝은 일반적으로 특정 데이터 수까지는 성능이 꾸준히 증가하다가
어느 순간부터 성능 변화가 거의 없게 된다.
이런 현상을 포화점(Saturation Point)라고 한다.
따라서 포화점까지 몇 개의 데이터가 필요한 지를 볼 필요가 있는데,
Learning Curve를 통해 간단하게 시각화할 수 있다.
# 모듈 불러오기
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import learning_curve
# 모델선언
model = DecisionTreeClassifier(max_depth=3)
# Learning Curve 수행
tr_size, tr_scores, val_scores = learning_curve(model,
x,
y,
train_sizes=range(10, 7900, 20),
shuffle=True,
cv=5)
learning curve는 모델과 train 데이터셋, train 사이즈와 cv를 받아서
학습 크기(점점 증가)와
학습 크기 증가에 따른 학습 데이터 성능과 검증 데이터 성능을 반환한다.
그렇다면 학습 크기를 시계열로 삼아
종합적인 데이터 성능의 변화를 추적할 수 있을 것이다.
# CV 결과 --> 평균
val_scores_3_mean = val_scores.mean(axis=1)
# 시각화
plt.figure(figsize=(8, 5))
plt.plot(tr_size, val_scores_3_mean)
plt.title('Learning Curve', size=20, pad=15)
plt.ylabel('Score')
plt.xlabel('Training Size')
plt.show()
하이퍼파라미터 튜닝
모델 성능 최적화를 위한 다양한 매개변수들.
모델의 성능에 영향을 미칠 수 있는 파라미터들을 하이퍼파라미터라고 한다.
- KNN
하이퍼파라미터 | 의미/해석 | 복잡성과의 관계 |
n_neibors | 비교할 이웃의 수 | 반비례 |
metrics | 거리측정방식 Euclidean / Manhanttan |
변수가 단순하고 독립적 : Euclidean 변수가 복잡하고 의존적 : Manhanttan |
- Decision Tree
하이퍼파라미터 | 의미/해석 | 복잡성과의 관계 |
max_depth | 트리가 탐색을 진행하는 최대 깊이 | 비례 |
min_samples_leaf | leaf의 최소 요구 샘플 수 leaf 생성의 제약조건이 된다. |
반비례 |
min_samples_split | 노드 분할의 최소 요구 샘플 수 노드 분할의 제약조건이 된다. |
반비례 |
이런 하이퍼파라미터를 어떻게 설정하는지에 따라 모델의 성능이 달라지는데,
그래서 우리는 적합한 하이퍼파라미터를 찾기 위해 몇가지 방법을 사용한다.
그것이 바로 sklearn.model_selection의 Grid Search와 Random Search이다.
- Grid Search
전달받은 파라미터 범위 전체를 돌면서 최선의 파라미터를 찾고,
자동으로 그 파라미터 기반 학습을 진행한다.
- Random Search
전달받은 파라미터 범위를 전달받은 횟수만큼 랜덤하게 돌면서 진행한다.
그래서 Random Search는 횟수를 전달하는 파라미터가 하나 더 있는데...
웬만하면 Grid Search를 쓰도록 한다.
기본적으로 사용자가 지정한 파라미터 범위 안에서 각 파라미터의 성능을 측정한다.
따라서 어떤 파라미터를 얼마 범위 안에서 탐색할지는
사용자가 딕셔너리 형태로 전달할 필요가 있다.
# 예시
param - {'n_neibors' : range(1, 101),
'metric' : ['euclidean', 'manhattan']}
이후의 사용법이 조금 독특한데,
GridSearch를 import한 이후에
GridSearch를 모델로 선언하고, 그 매개변수로 내가 원하는 알고리즘을 넣는다.
이외에는 params, cv를 입력해 주면 된다.
사용례
# 불러오기
from sklearn.model_selection import GridSearchCV
# Grid Search 선언
model = GridSearchCV(DecisionTreeRegressor(), param, cv=5)
# 학습하기
model.fit(x_train, y_train)
# 중요 정보 확인
print('최적파라미터:', model.best_params_)
print('최고성능:', model.best_score_)
이렇게 결과값까지 확인하고 나면 모델링이 사실 끝이다.
아직 알고리즘을 안 돌렸지 않냐고?
강사님의 표현을 빌리자면,
이 시점에서 나의 모델은 최적의 파라미터로 무장한 상태다.
이 시점에서 나의 모델은 어떤 상황이냐면
model | GridSearchCV(), 하지만 model의 함수(predict 등)을 정상적으로 수행 가능하다. |
model.best_estimator_ | 파라미터가 최적화된 나의 알고리즘 위 예시의 경우 DecisionTreeRegressor() |
따라서 변수 중요도를 시각화하려면
model.best_estimator_로 만들어 줘야 한다.
# 변수 중요도
plt.figure(figsize=(5, 5))
plt.barh(y=list(x), width=model.best_estimator_.feature_importances_)
plt.show()