성질들을 이용하여 특정 타깃일 확률을 구하는 머신러닝을 구현해보자.
특정 타깃일 확률이란 모든 class에 대한 확률을 의미한다. 이 중 가장 높은 것이 유력할 것이다.
feature는 길이, 높이, 두께, 대각선의 길이, 무게를 사용한다.
class 7개를 사용할 것이며 2개 이상의 class 분류를 다중 분류(multi-class classification)
K-최근접 이웃 알고리즘을 사용하여 주변 이웃의 클래스 비율을 확률이라 출력하면 어떨까?
import pandas as pd fish = pd.read_csv('https://bit.ly/fish_csv_data') fish.head() # 상위 5개 행 출력
이번엔 class를 출력해보자.
pd.unique(fish['Species']) # ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
이제 feature 셋을 만들어주자.
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy() fish_input = fish_input.to_numpy()
데이터 프레임의 리스트를 이용한 인덱싱은 2차원 배열을 반환한다.
fish_target = fish['Species'].to_numpy()
훈련, 테스트 셋을 나눠준다.
import 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)
scaling 한다.
from sklearn.preprocessing import StandardScaler ss = StandardScaler() ss.fit(train_input) train_scaled = ss.transform(train_input) test_scaled = ss.transform(test_input)
from sklearn.neighbors import KNeighborsClassifier kn = KNeighborsClassifier(n_neighbors=3) kn.fit(train_scaled, train_target) # target => 문자를 저장하는 배열 사용 가능. kn.score(train_scaled, train_target) # 0.8907563025210085 kn.score(test_scaled, test_target) # 0.85
참고로 모델 객체에 저장된 target 배열은
classes_
필드에 존재하며 알파벳 순으로 정렬되어 있다.
kn.predict(test_scaled[:5]) # ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
이렇게 예측된 확률을 알아보기 위해 predict_proba()
매서드를 사용한다.
이를 위해 numpy의
round()
함수를 사용한다.
이 함수는 기본적으로 소수점 첫째 자리에서 반올림을 하는데, decimals 매개변수로 소수점 자릿수를 지정할 수 있다.
import numpy as np proba = kn.predict_proba(test_scaled[:5]) np.round((proba, decimals=4)) # 소수점 네 번째 자리까지
classes_ 필드의 정렬 순서와 동일하다.
여기서 네 번째 샘플의 확률을 보자.
distances, indexes = kn.kneighbors(test_scaled[3:4]) test_taget[indexes] # [['Roach', 'Perch', 'Perch']]
예측을 위한 이웃들의 class는 두 개 뿐이다.
앞서 본 네 번째 샘플의 확률이 납득이 간다.
Perch가 2/3, 즉 0.6667이다. Roach가 1/3 즉 0.3333이다.
그런데 당연하게도 이웃을 세 개만 뽑으니 모든 class에 대한 확률을 보일 수 없다.
그렇다고 이웃의 수를 늘려도 보장되지 않고, 너무 많이 늘렸다가 샘플이 더 많은 class로 분류할 가능성이 높아진다.
로지스틱 회귀
는 이름은 회귀이지만 사실 분류 모델이다.
이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다. 예를 들면 다음과 같다.
z = a x (Weight) + b x (Length) + c x (Diagonal) + d x (Height) + e x (Width) + f
여기에서 a, b, c, d는 가중치 혹은 계수이다. 특성은 늘어났지만, 3장에서 다룬 다중 회귀를 위한 선형 방정식과 같다. z는 어떤 값도 가능하다. 하지만 확률이 되려면 [0, 1]사이 값이 되어야 한다.
z가 아주 큰 음수일 때 0이 되고, 아주 큰 양수일 때 1이 되도록 방법이 무엇일까?
시그모이드 함수(sigmoid function)
또는 로지스틱 함수(logistic function)
를 사용하면 가능하다.
z가 무한하게 큰 음수일 경우 이 함수는 0에 가까워지고 z가 무한하게 큰 양수가 될 때에는 1에 가까워 진다.
z가 0이 될 때, 0.5가 된다. z가 어떤 값이 되더라도 함수값은 절대로 [0, 1] 사이를 벗어날 수 없다.
이 값을 0~100%까지의 확률로 해석한다.
그래프를 한 번 그려보자
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.xlabel('z') plt.ylabel('phi') plt.show()
위와 같은 그래프가 나온다.
이제 사이킷런의 LogisticRegression
클래스로 로지스틱 회귀를 학습 시켜보자.
일단 이진분류인 경우 함수값이 0.5보다 클 경우 양성 클래스, 0.5보다 작거나 같을 경우 음성 클래스로 분류한다.
도미와 빙어에 대해서 학습한다.
bream_smelt_indexes = (train_test == 'Bream') | (train_target == 'Smelt') train_bream_smelt = train_scaled[bream_smelt_indexes] target_bream_smelt = train_target[bream_smelt_indexes]
from sklearn.linear_model import LogisticRegression lr = LogisticRegression() lr.fit(train_bream_smelt, target_bream_smelt) lr.predict(train_bream_smelt[:5]) # ['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
확률 확인
lr.predict_proba(train_bream_smelt[:5])
index 0이 음성 클래스이고, 1이 양성 클래스이다.
lr.clasess_ # ['Bream', 'Smelt']
빙어가 양성 클래스이다.
로지스틱 회귀로 성공적인 이진 분류를 수행했다. 그럼 선형 회귀에서의 로지스틱 회귀가 학습한 계수를 확인해보자.
lr.coef_ # [[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]] lr.intercept_ # [-2.16155132]
따라서 이 로지스틱 회귀 모델이 학습한 방정식은 다음과 같다.
z = -0.404 x (Weight) - 0.576 x (Length) - 0.663 x (Diagonal) - 1.013 x (Height) - 0.732 x (Width) - 2.161
z값을 출력해보자.(처음 5개 샘플의 z값)
decisions = lr.decision_function(train_bream_smelt[:5]) decisions # [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
이 z값을 이용하여 사그모이드 함수에 대입하여 확률을 얻을 수 있다. 물론 파이썬의 scipy 라이브러리에도 시그모이드 함수가 존재한다. 바로
expit()
함수이다. np.exp() 함수를 사용해 분수를 계산하는 것보다 훨씬 편리하고 안전하다.
from scipy.special import expit expit(decisions) # [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
출력된 값을 보면 predict_proba() 매서드 출력의 두 번째 열의 값과 동일하다. 즉 decision_function() 매서드는 양성 클래스에 대한 z 값을 반환한다.
class 두 개를 분류하는 이중 분류를 수행하였고, 이제 7개의 class를 분류하는 다중 분류를 알아보자.
LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다.
max_iter
매개변수에서 반복 횟수를 지정하며 기본값은 100이다. 여기에 준비한 데이터 셋을 사용해 모델을 훈련하면 반복 횟수가 부족하다는 경고가 발생한다. 충분히 훈련시키기 위해 반복횟수를 1000으로 늘린다.
또 LogisticRegression은 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제한다. 이런 규제를 L2 규제
라고도 부른다. 릿지 회귀에서는 alpha 매개변수로 규제의 양을 조절한다. alpha가 커지면 규제도 커진다.
LogisticRegression에서 규제를 제어하는 매개변수는 C
이다. 하지만 C는 alpha와 반대로 작을수록 규제가 커진다.
C의 기본값은 1이다. 여기에서는 규제를 조금 완화하기 위해 20으로 늘린다.
다음 코드는 LogisticRegression 클래스로 다중 분류 모델을 훈련하는 코드이다. 7개의 생선 데이터가 모두 들어있는 train_scaled와 train_target을 사용한 점을 눈여겨보자.
lr = LogisticRegression(C=20, max_iter=1000) lr.fit(train_scaled, train_target) lr.score(train_scaled, train_target) # 0.9327731092436975 lr.score(test_scaled, test_target) # 0.925
훈련 셋과 테스트 셋에 대한 점수가 높고 과대적합이나 과소적합으로 치우지지 않았다.
테스트 셋 처음 5개의 대한 예측을 확인하자.
lr.predict(test_scaled[:5]) # ['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
이번에는 테스트 셋의 처음 5개 샘플에 대한 예측 확률을 출력해보자.
proba = lr.predict_proba(test_scaled[:5]) np.round(proba, decimals=3)
(.classes_를 확인하여 어느 class에 대한 예측 확률인지 알아봐라.)
첫 번째 sample은 Perch, 세 번째 열을 가장 높은 확률로 예측했다. 두 번째는 여섯 번째 열인 Smelt를 가장 높은 확률로 예측했다.
다중 분류의 선형 방정식을 알아보자.
lr.coef_ # (7, 5) lr.intercept_ # (7,)
계수와 편향은 클래스의 개수만큼 도출한다. 게다가 계수는 특성마다 존재한다.
그렇다면 z를 클래스의 개수만큼 계산할 수 있고 이를 통해 클래스 개수만큼의 예측 확률을 구할 수 있다.
이 확률 중 가장 높은 확률을 통해 클래스를 분류한다.
다중 분류는 이진 분류와 달리, 시그모이드 함수가 아닌 소프트맥스(softmax)
함수를 사용하여 7개의 z값을 확률로 변환한다.
소프트맥스 함수를 알아보자. 먼저 7개의 z 값의 이름을 z1에서 z7이라고 부르겠다.
z1~z7까지 값을 사용해 지수 함수 e^z1~e^z7을 계산해 모두 더한다. 이를 e_sum이라고 한다.
e_sum = e^z1 + e^z2 + e^z3 + e^z4 + e^z5 + e^z6 + e^z7
그 다음 e^z1 ~ e^z7을 각각 e_sum으로 나누어 주면 된다.
s1 = e^z1 / e_sum
s2= e^z2 / e_sum ...
s7 = e^z7 / e_sum
s1에서 s7까지 모두 더하면 분자와 분모가 같아지므로 1이 된다. 7개 생선에 대한 확률의 합은 1이 되어야 하므로 잘 맞는다.
그럼 이진 분류에서 처럼 .decision_function()
에 z1~z2값이 존재한다. 이를 사용하여 확률을 구해보자.
(역시 사이파이는 소프트맥스 함수도 제공한다. scipy.special 아래에 softmax() 함수를 임포트 해 사용한다.)
from scipy.special import softmax proba = softmax(decision, axis=1) np.round(proba, decimals=3)
proba 배열과 비교해보면 결과가 정확히 일치한다. 성공이다.