이 포스팅은 혼자 공부하는 머신러닝 + 딥러닝 책을 공부하고 정리한 것입니다
이번에는 럭키백 이벤트를 예로 들어서 머신러닝 모델을 만들어볼 것이다. 럭키백은 구성품을 모른 채 먼저 구매하고 배송받은 다음에야 비로소 구성품을 알 수 있는 상품이다. 럭키백에 어떤 생선 확률이 높은지 표시를 할 것이다. 머신러닝으로 럭키백의 생선이 어떤 타깃에 속하는지 확률을 구할 수 있을까?
럭키백에 들어갈 수 있는 생선은 7개다. 이 이벤트를 잘 마치려면 럭키백에 들어간 생선의 크기, 무게 등이 주어졌을 때 7개 생선에 대한 확률을 출력할 것이다. 이번에는 길이, 높이, 두께 이외도 대각선 길이와 무게도 사용할 수 있다.
이 문제는 회귀일까 분류일까? k-최근접 이웃은 주변 이웃을 찾아줌으로 이웃의 클래스 비율을 확률이라고 출력하게 만들면 된다. 사이킷런의 k-최근접 이웃 분류기도 이와 동일한 방식으로 클래스 확률을 계산하여 제공한다.
데이터 준비하기
import pandas as pd
fish=pd.read_csv('https://bit.ly/fish_csv')
fish.head()
print(pd.unique(fish['Species']))
이 데이터 프레임에서 Species 열을 타깃으로 만들고 나머지 5개 열은 입력 데이터로 사용할 것이다.
fish_input=fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
fish_target=fish['Species'].to_numpy()
print(fish_input[:5])
from sklearn.model_selection import train_test_split
train_input,test_input,train_target,test_target=train_test_split(fish_input,fish_target,random_state=42)
사이킷런의 StandardScaler 클래스를 사용해 훈련 세트와 테스트 세트를 표준화 전처리 한다. 여기에서도 훈련 세트의 통계 값으로 테스트 세트를 변환해야 한다는 점을 잊으면 안된다.
from sklearn.preprocessing import StandardScaler
ss=StandardScaler()
ss.fit(train_input)
train_scaled=ss.transform(train_input)
test_scaled=ss.transform(test_input)
k-최근접 이웃 분류기의 확률 예측
앞에서 했던 것처럼 사이킷런의 KNeighborsClassifer 클래스 객체를 만들고 훈련 세트로 모델을 훈련한 다음 훈련 세트와 테스트 세트의 점수를 확인해 본다. 최근접 이웃 개수인 k를 3으로 지정하여 사용한다.
from sklearn.neighbors import KNeighborsClassifier
kn=KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled,train_target)
print(kn.score(train_scaled,train_target))
print(kn.score(test_scaled,test_target))
여기에서는 클래스 확률을 배우는 것이 목적이므로 훈련 세트와 테스트 세트 점수에 대해서는 잠시 잊는다. 앞서 타깃 데이트를 만들 때 fish['Species']를 사용해 만들었기 때문에 훈련 세트와 테스트 세트의 타깃 데이터에도 7개의 생선 종류가 들어가 있다. 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류(multi-class classification) 라고 부른다.
이름은 다르지만 조금 전 코드에서 보듯이 이전에 만들었던 이진 분류와모델을 만들고 훈련하는 방식은 동일하다. 이진 분류를 사용했을 때는 양성 클래스와 음성 클래스를 각각 1과 0으로 지정하여 타깃 데이터를 만들었는데 다중 분류에서도 타깃값을 숫자로 바꾸어 입력할 수 있지만 사이킷런에서는 편리하게도 문자열로 된 타깃값을 그대로 사용할 수 있다.
이때 주의할 점이 하나 있는데 타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 매겨진다. pd.unique(fish['Species']로 출력했던 순서와 다르다. KNeighborsClassifier에서 정렬된 타깃값은 classes_ 속성에 저장되어 있다.
print(kn.classes_)
predict() 메서드는 타깃값으로 예측을 출력한다.
print(kn.predict(test_scaled[:5]))
이 5개의 샘플에 대한 예측은 predict_proba() 메서드로 클래스별 확률값을 반환한다.
proba=kn.predict_proba(test_scaled[:5])
print(np.round(proba*100,decimals=4)) # 소수점 네번째 자리까지 표기한다
predict_proba() 메서드의 출력 순서는 앞서 보았던 classes_ 속성과 같다. 즉 첫 번째 열이 'Bream' 두번째 열이 'Parkki'에 대한 확률이다.
이 모델이 계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인해 보자. 네 번째 샘플의 최근접 이웃의 클래스를 확인해 보면
distances,indexes=kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
이 샘플의 이웃은 다섯 번째 클래스인 'Roach'가 1개이고 세 번째 클래스인 'Perch'가 2개이다. 따라서 세 번째 클래스에 대한 확률은 0.3333 이고 다섯 번째 클래스에 대한 확률은 0.6667이다.
하지만 3개의 최근접 이웃을 사용하기 때문에 가능한 확률은 0/3,1/3,2/3,3/3 이 전부이다. 만약 럭키백의 확률을 이렇게만 표시한다면 불만족스러울 것이다. 확률이라고 말하기가 어색하다. 더 좋은 방법이 필요할것 같다.
로지스틱 회귀
로지스틱 회귀(logistic regression)은 이름은 회귀이지만 분류 모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다. 예를 들면 다음과 같다
여기에서 a,b,c,d는 가중치 혹은 계수이다. 특성은 늘어났지만 앞에서 다룬 다중회귀를 위한 선형 방정식과 같다. z는 어떤값도 가능하지만 확률이 되려면 0~1(또는 0~100%) 사이 값이 되어야 한다. z가 아주 큰 음수일때 0이 되고 아주 큰 양수일때 1이 되도록 하는 방법은 바로 시그모이드 함수(sigmoid function) ( 또는 로지스틱 함수) 를 사용하면 가능하다.
import numpy as np
import matplotlib.pyplot as plt
z=np.arange(-5,5,0.1)
phi=1/(1+np.exp(-z))
plt.plot(z,phi)
plt.show()
사이킷런에서는 로지스틱 회귀 모델인 LogisticRegression 클래스가 준비되어 있다. 훈련하기 전에 간단한 이진 분류를 수행해 보겠다. 이진 분류일 경우 시그모이드 함수의 출력이 0.5보다 크면 양성 클래스, 0.5보다 작으면 음성 클래스로 판단한다. 그럼 먼저 도미와 빙어 2개를 사용해서 이진 분류를 수행해 보겠다.
로지스틱 회귀로 이진 분류 수행하기
먼저 도미(Bream)과 빙어(Smelt) 행만 골라낸다. 비교 연산자를 사용하면 도미와 빙어 행을 모두 True로 만들 수 있다.
bream_smelt_indexes=(train_target=='Bream') | (train_target=='Smelt')
train_bream_smelt=train_scaled[bream_smelt_indexes]
target_bream_smelt=train_target[bream_smelt_indexes]
이제 이 데이터로 로지스틱 회귀 모델을 훈련해 보겠다. LogisticRegression 클래스는 선형 모델이므로 sklearn_learn_model 패키지 아래 있다.
from sklearn.linear_model import LogisticRegression
lr=LogisticRegression()
lr.fit(train_bream_smelt,target_bream_smelt)
print(lr.predict(train_bream_smelt[:5]))
두 번째 샘플을 제외하고는 모두 도미로 예측했다. KNeighborsClassifier와 마찬가지로 예측 확률은 predict_porba() 메서드에서 제공한다. train_bream_smelt 에서 처음 5개 샘플의 예측 확률을 출력해 보겠다.
print(lr.predict_proba(train_bream_smelt[:5]))
사이킷런은 타깃값을 알파벳순으로 정렬한다.
print(lr.classes_)
Smelt가 양성 클래스 이다.
로지스특 회귀로 성공적인 이진 분류를 수행했다. 그럼 선형 회귀에서처럼 로지스틱 회귀가 학습한 계수를 확인해 보자.
print(lr.coef_,lr.intercept_)
따라서 이 로지스틱 회귀 모델이 학습한 방정식은 다음과 같다.
LogisticRegression 클래스는 decision_function() 메서드로 z값을 출력할 수 있다. train_bream_smelt의 처음 5개 샘플의 z값을 출력해보면
decisions=lr.decision_function(train_bream_smelt[:5])
print(decisions)
이 z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다. 다행이 파이썬의 사이파이(scipy) 라이브러리에도 시그모이드 함수가 있다. 바로 expit()이다.
from scipy.special import expit
print(expit(decisions))
출력된 값을 보면 predict_porba() 메서드 출력의 두 번째 열의 값과 동일하다. 즉 decision_function() 메서드는 양성 클래스에 대한 z값을 반환한다.
로지스틱 회귀로 다중 분류 수행하기
다중 분류도 크게 다르지 않다. LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다. max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100이다. 여기에 준비한 데이터셋을 사용해 모델을 훈련하면 반복횟수가 부족하다는 경고가 발생한다. 충분하게 훈련시키기 위해 반복 횟수를 1000으로 늘리겠다.
또 LogisticRegression은 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제한다. 이런 규제를 L2규제라고 한다. LogisticRegression 에서 규제를 제어하는 매개변수는 C다. 하지만 C는 alpha와 반대로 작을수록 규제가 커진다. C의 기본값은 1이다. 여기에서는 규제를 조금 완화하기 위해 20으로 늘린다.
lr=LogisticRegression(C=20,max_iter=1000)
lr.fit(train_scaled,train_target)
print(lr.score(train_scaled,train_target))
print(lr.score(test_scaled,test_target))
테스트 세트의 처음 5개 샘플에 대한 예측을 출력해보면
print(lr.predict(test_scaled[:5]))
이번에는 테스트 세트의 처음 5개 샘플에 대한 예측 확률을 출력해 보자.
proba=lr.predict_proba(test_scaled[:5])
print(np.round(proba,decimals=3))
5개 샘플에 대한 예측이므로 5개의 행이 출력되었다. 또 7개 생선에 대한 확률을 계산했으므로 7개의 열이 출력 되었다.
print(lr.classes_)
다중 분류의 선형 방정식은 어떤 모습일까?
print(lr.coef_.shape,lr.intercept_.shape)
이 데이터는 5개의 특성을 사용하므로 coef_ 배열의 열은 5개이다. 그런데 행이 7개이다. 이 말은 이진 분류에서 보았던 z를 7개나 계산한다는 의미이다. 다중 분류는 클래스마다 z값을 하나씩 계산한다. 당연히 가장 높은 z값을 출력하는 클래스가 예측 클래스가 된다. 이진분류 에서는 시그모이드 함수를 이용해 z를 0과 1 사이의 값으로 변환했는데 다중 분류는 이와 달리 소프트맥스(softmax) 함수를 사용하여 7개의 z값을 확률로 변환한다
소프트 맥스 함수는 먼저 7개의 z값의 이름을 z1에서 z7이라고 붙이면 지수함수를 계산해 모두 더해 e_sum을 만든다.
그 다음 각각 e_sum으로 나누어 주면 된다.
그럼 이진 분려에서 처럼 decision_function() 메서드로 z1~z7까지의 값을 구한 다음 소프트맥스 함수를 사용해 확률을 바꾸어 보겠다.
decision=lr.decision_function(test_scaled[:5])
print(np.round(decision,decimals=2))
proba=softmax(decision,axis=1)
print(np.round(proba,decimals=3))
softmax()의 axis 매개변수는 소프트맥스를 계산할 축을 지정한다. axis=1로 지정하여 각 행, 즉 각 샘플에 대해 소프트맥스를 계산한다. 만약 axis 매개변수를 지정하지 않으면 배열 전체에 대해 소프트맥스 계산을 한다.
결론
분류 모델은 예측뿐만 아니라 예측의 근거가 되는 확률을 출력할 수 있다.
k-최근접 이웃 모델이 확률을 출력할 수 있지만 이웃한 샘플의 클래스 비율이므로 항상 정해진 확률만 출력한다.
가장 대표적인 분류 알고리즘 중 하나인 로지스틱회귀는 회귀모델이 아닌 분류모델인데 선현 회귀처럼 선형 방정식을 사용한다. 하지만 선형 회귀처럼 계산한 값을 그대로 출력하는 것이 아니라 로지스틱 회귀는 이 값을 0~1 사이로 압축한다. 우리는 이 값을 마치 0~100% 사이의 확률로 이해할 수 있다.
로지스틱 회귀는 이진 분류에서는 하나의 선형방정식을 훈련한다. 이 방정식의 출력값을 시그모이드 함수에 통과시켜 0~1 사이의 값을 만든다. 이 값이 양성 클래스에 대한 확률이다. 음성 클래스의 확률은 1에서 양성 클래스의 확률을 빼면 된다.
다중 분류일 경우에는 클래스 개수만큼 방정식을 훈련한다. 그다음 각 방정식의 출력값을 소프트맥스 함수로 통과시켜 전체 클래스에 대한 합이 항상 1이 되도록 만든다. 이 값을 각 클래스에 대한 확률로 이해할 수 있다.
'Machine Learning > Basic' 카테고리의 다른 글
[5-1] 결정 트리 (0) | 2021.04.07 |
---|---|
[4-2] 확률적 경사 하강법 (0) | 2021.04.07 |
[3-3] 특성 공학과 규제 (0) | 2021.04.06 |
[3-2] 선형 회귀 (0) | 2021.04.05 |
[3-1] k-최근접 이웃 회귀 (0) | 2021.04.05 |