티스토리 뷰
chapter4. 다양한 분류 알고리즘¶
04-1 로지스틱 회귀¶
- 럭키백의 확률¶
이번 예제는 7가지 생선 중 럭키백에 들어가 있는 생선의 확률을 구하는 것이다. 럭키백에 들어간 생선의 크기, 무게 등이 주어졌을 때 7개 생선에 대한 확률을 출력해 볼 것이다.
- 데이터 준비하기¶
판다스의 read_csv() 함수로 csv 파일을 데이터프레임으로 변환한 다음 head() 메서드로 처음 5개의 행을 출력해보자. 데이터 출처 : https://github.com/rickiepark/hg-mldl
import pandas as pd
fish = pd.read_csv('/home/jaeyoon89/hg-mldl/fish.csv')
fish.head()
Species | Weight | Length | Diagonal | Height | Width | |
---|---|---|---|---|---|---|
0 | Bream | 242.0 | 25.4 | 30.0 | 11.5200 | 4.0200 |
1 | Bream | 290.0 | 26.3 | 31.2 | 12.4800 | 4.3056 |
2 | Bream | 340.0 | 26.5 | 31.1 | 12.3778 | 4.6961 |
3 | Bream | 363.0 | 29.0 | 33.5 | 12.7300 | 4.4555 |
4 | Bream | 430.0 | 29.0 | 34.0 | 12.4440 | 5.1340 |
어떤 종류의 생선이 있는지 Species열에서 고유한 값을 출력해 보자. 판다스의 unique() 함수를 사용하면 간단하다.
print(pd.unique(fish['Species']))
['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
이 데이터프레임에서 Species 열을 타깃으로 만들고 나머지 5개 열은 입력 데이터로 사용하자. 데이터프레임에서 열을 선택하는 방법은 간단하다. 데이터프레임에서 원하는 열을 리스트로 나열하면 된다. Species 열을 빼고 나머지 5개 열을 선택해 보자.
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
데이터프레임에서 여러 열을 선택하면 새로운 데이터프레임이 반환된다. 이를 to_numpy() 메서드로 넘파이 배열로 바꾸어 fish_input에 저장했다. fish_input에 처음 5개의 행을 출력해보자.
print(fish_input[:5])
[[242. 25.4 30. 11.52 4.02 ] [290. 26.3 31.2 12.48 4.3056] [340. 26.5 31.1 12.3778 4.6961] [363. 29. 33.5 12.73 4.4555] [430. 29. 34. 12.444 5.134 ]]
이제 동일한 방식으로 타깃 데이터를 만들자.
fish_target = fish['Species'].to_numpy()
앞서 배웠듯이 머신러닝에서는 기본으로 데이터 세트가 2개 필요하다.
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-최근접 이웃 분류기의 확률 예측¶
앞서 배웠듯이 사이킷런의 KNeighborsClassfier 클래스 객체를 만들고 훈련 세트로 모델을 훈련한 다음 훈련 세트와 테스트 세트의 점수를 확인해 보자. 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))
0.8907563025210085 0.85
여기에선 클래스 확률을 배우는 것이 목적이라 위 점수에 대해서는 잊도록 하자.
앞서 fish 데이터프레임에서 7개의 생선이 있었다. 타깃 데이터가 Species이기 때문에 훈련 세트와 테스트 세트의 타깃 데이터에도 7개의 생선 종류가 들어가 있다. 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류라고 한다. 앞서 이진 분류를 배웠듯이 다중 분류에서도 타깃값을 숫자로 바꾸어 입력할 수 있지만 사이킷런에서는 편리하게 문자열로 된 타깃값을 그대로 사용할 수 있다.
하지만 주의할 점은 타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순서가 된다. 따라서 pd.unique로 출력했던 순서와 다르다. KNeighborsClassifier에서 정렬된 타깃값은 classes_속성에 저장되어 있다.
print(kn.classes_)
['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
이번엔 predict() 메서드를 이용해 타깃값으로 예측을 출력해보자. 테스트 세트에 처음 5개 샘플의 타깃값을 예측해보자.
print(kn.predict(test_scaled[:5]))
['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
위 5개의 대한 예측은 어떤 확률로 만들어 졌을까? 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환한다. 테스트 세트에 있는 처음 5개의 샘플에 대한 확률을 출력해 보자. 넘파이 round() 함수는 기본으로 소수점 첫째 자리에서 반올림 하는데, decimals 매개변수로 유지할 소수점 아래 자릿수를 지정할 수 있다.
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))
[[0. 0. 1. 0. 0. 0. 0. ] [0. 0. 0. 0. 0. 1. 0. ] [0. 0. 0. 1. 0. 0. 0. ] [0. 0. 0.6667 0. 0.3333 0. 0. ] [0. 0. 0.6667 0. 0.3333 0. 0. ]]
predictproba() 메서드의 출력 순서는 앞서 보았던 classes 속성과 같다.
이 모델이 계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인해 보자. 네 번째 샘플의 최근접 이웃의 클래스를 확인해 보자.
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
[['Roach' 'Perch' 'Perch']]
이 샘플의 이웃은 5번째 클래스인 Roach가 1개이고 3번째 클래스인 Perch가 2개이다. 따라서 5번째 클래스의 대한 확률은 1/3 = 0.3333이고 3번째 클래스의 대한 확률은 2/3 = 0.6667 이 된다.
- 로지스틱 회귀¶
로지스틱 회귀는 이름은 회귀이지만 분류 모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다. (로지스틱함수의 대한 설명은 책에 자세히 나와있다.)
로지스틱 함수는 넘파이를 이용하면 간단히 그릴 수 있다. -5 와 5 사이에 0.1 간격으로 배열z를 만든 다음 z 위치마다 로지스틱함수 계산한다. 지수 함수 계산은 np.exp() 함수를 사용한다.
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보다 작으면 음성 클래스로 판단한다. 먼저 도미와 빙어 2개를 사용해서 이진 분류를 수행해 보자.
- 로지스틱 회귀로 이진 분류 수행하기¶
넘파이 배열을 True,False 값을 전달하여 행을 선택할 수 있다. 이를 불린 인덱싱이라고 한다. 다음 예를 따라해보자.
char_arr = np.array(['A','B','C','D','E'])
print(char_arr[[True,False,True,False,False]])
['A' 'C']
이와 같은 방식으로 사용해 훈련 세트에서 도미와 빙어의 행만 골라내자. 아래와 같이 비교 연산자를 사용하면 도미와 빙어의 행을 모두 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]
bream_smelt_indexes 배열은 도미와 빙어일 경우 True이고 그 이외는 모두 False 값이 들어가 있다. 따라서 이 배열을 사용해 train_scaled와 train_target 배열에 불린 인덱싱을 적용하면 손쉽게 도미와 빙어 데이터만 골라낼 수 있다.
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
LogisticRegression()
훈련한 모델을 사용해 train_bream_smelt에 있는 처음 5개 샘플을 예측하자.
print(lr.predict(train_bream_smelt[:5]))
['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
두 번째 샘플을 제외하고는 모두 도미로 예측했다. predict_proba() 메서드로 처음 5개 샘플의 예측 확률을 출력해보자.
print(lr.predict_proba(train_bream_smelt[:5]))
[[0.99759855 0.00240145] [0.02735183 0.97264817] [0.99486072 0.00513928] [0.98584202 0.01415798] [0.99767269 0.00232731]]
샘플마다 2개의 확률이 출력되었다. Bream과 Smelt중 어떤것이 양성 클래스일까? classes_ 속성으로 확인해보자.
print(lr.classes_)
['Bream' 'Smelt']
빙어가 양성 클래스이다. 앞에 확률을 보았듯이 두 번째 샘플만 빙어의 확률이 높다.
이제 로지스틱 회귀가 학습한 계수를 확인해 보자.
print(lr.coef_, lr.intercept_)
[[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]
이제 LogisticRegression 모델로 z 값을 계산해보자. train_bream_smelt의 처음 5개 샘플의 z값을 출력해 보자.
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
[-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
이 z값을 로지스틱 함수에 통과시키면 확률을 얻을 수 있다. 다행히 파이썬의 scipy 라이브러리에도 로지스틱 함수가 있다. 바로 expit()이다. np.exp()함수를 사용해 계산하는 것보다 훨씬 편리하고 안전하다. decisions 배열의 값을 확률로 변환해 보자.
from scipy.special import expit
print(expit(decisions))
[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
출력된 값을 보면 predict_proba() 메서드 출력의 두 번째 열의 값과 동일하다. 즉 decision_fucntion() 메서드는 양성 클래스에 대한 z값을 반환한다.
- 로지스틱 회귀로 다중 분류 수행하기¶
이제 LogisticRegression 클래스를 사용해 7개의 생선을 분류해 보면서 이진 분류와 비교해보자. LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다. max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100이다. 여기에 준비한 데이터셋을 사용해 모델을 훈련하면 반복 횟수가 부족하다는 경고가 발생한다. 충분하게 훈련시키기 위해 반복 횟수를 1000으로 늘리자. 또 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제한다. 이런 규제를 L2라고 하는데 릿지 회귀에서는 alpha 매개변수로 규제의 양을 조절했다. 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))
0.9327731092436975 0.925
테스트 세트의 처음 5개 샘플에 대한 예측을 출력해보자.
print(lr.predict(test_scaled[:5]))
['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
이번엔 테스트 세트의 처음 5개 샘플에 대한 예측확률을 출력하자.
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
[[0. 0.014 0.841 0. 0.136 0.007 0.003] [0. 0.003 0.044 0. 0.007 0.946 0. ] [0. 0. 0.034 0.935 0.015 0.016 0. ] [0.011 0.034 0.306 0.007 0.567 0. 0.076] [0. 0. 0.904 0.002 0.089 0.002 0.001]]
첫 번째 샘플을 보면 3번째 열의 확률이 가장 높다. 3번째 열이 농어(perch)에 대한 확률일까? classes_ 속성에서 클래스 정보를 확인해보자.
print(lr.classes_)
['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
샘플중에서 가장 높은 확률이 예측 클래스가 된다. 그럼 다중 분류일 경우 선형 방정식은 어떤 모습일까? coef_와 intercept_의 크기를 출력해 보자.
print(lr.coef_.shape, lr.intercept_.shape)
(7, 5) (7,)
이 데이터는 5개의 특성을 사용하므로 coef_ 배열의 열은 5개이다. 그런데 행은 7개이다. intercept_도 7개나 있다. 이 말은 이진 분류에서 보았던 z를 7개나 계산한다는 의미이다. 다중 분류는 클래스마다 z 값을 하나씩 계산한다. 당연히 가장 높은 z값을 출력하는 클래스가 예측 클래스가 된다. 그럼 확률은 어떻게 계산한 것일까? 이진 분류에서는 로지스틱 함수를 사용해 z값을 0고 1 사이의 값으로 변환했다. 다중 분류는 이와 달리 소프트맥스 함수를 사용하여 7개의 z값을 확률로 변환한다.
그럼 이진 분류에서 처럼 decision_fucntion() 메서드로 z1 ~ z7까지의 값을 구한 다음 소프트맥스 함수를 사용해 확률로 바꾸어 보자.
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))
[[ -6.5 1.03 5.16 -2.73 3.34 0.33 -0.63] [-10.86 1.93 4.77 -2.4 2.98 7.84 -4.26] [ -4.34 -6.23 3.17 6.49 2.36 2.42 -3.87] [ -0.68 0.45 2.65 -1.19 3.26 -5.75 1.26] [ -6.4 -1.99 5.82 -0.11 3.5 -0.11 -0.71]]
역시 scipy는 소프트 맥스 함수를 제공한다. 아래 코드처럼 softmax()함수를 임포트하자.
from scipy.special import softmax
proba = softmax(decision, axis=1)
print(np.round(proba, decimals=3))
[[0. 0.014 0.841 0. 0.136 0.007 0.003] [0. 0.003 0.044 0. 0.007 0.946 0. ] [0. 0. 0.034 0.935 0.015 0.016 0. ] [0.011 0.034 0.306 0.007 0.567 0. 0.076] [0. 0. 0.904 0.002 0.089 0.002 0.001]]
앞서 구한 proba 배열과 정확히 일치한다.
출처 : 혼자 공부하는 머신러닝 + 딥러닝
'혼자공부하는 머신러닝+딥러닝' 카테고리의 다른 글
혼자 공부하는 머신러닝+딥러닝(ch5-1 결정 트리) (0) | 2021.05.02 |
---|---|
혼자 공부하는 머신러닝+딥러닝(ch4-2 확률적 경사 하강법) (0) | 2021.05.01 |
혼자 공부하는 머신러닝+딥러닝(ch3-3 특성 공학과 규제) (1) | 2021.04.29 |
혼자 공부하는 머신러닝+딥러닝(ch3-2 선형회귀) (0) | 2021.04.28 |
혼자 공부하는 머신러닝+딥러닝(ch.3-1 k-최근접 이웃 회귀) (0) | 2021.04.27 |