[혼자공부하는 머신러닝 + 딥러닝] 8_로지스틱 회귀
7. 로지스틱 회귀
1. 들어가며
- 이전까지 분류와 회귀에 대해 다뤄보았다.
- 이번에는 생선들의 특성을 이용해 럭키백에 넣을 생선들의 확률을 출력할 필요가 있다.
- 다양한 특성을 사용하되 이번에는 길이, 높이, 두께, 대각선 길이, 무게 등을 사용할 수 있다.
- 먼저 데이터를 준비한다.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from scipy.special import expit
fish = pd.read_csv('https://bit.ly/fish_csv_data')
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 |
- Speices 열의 고유값을 출력해 보도록 한다.
fish.Species.unique()
array(['Bream', 'Roach', 'Whitefish', 'Parkki', 'Perch', 'Pike', 'Smelt'],
dtype=object)
- 종류의 고유값을 확인해보면 다음과 같다. 7개의 고유값을 가진다.
- Species를 타깃으로 사용하고 나머지 데이터를 입력데이터로 사용한다.
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']]
print(fish_input[:5])
Weight Length Diagonal Height Width
0 242.0 25.4 30.0 11.5200 4.0200
1 290.0 26.3 31.2 12.4800 4.3056
2 340.0 26.5 31.1 12.3778 4.6961
3 363.0 29.0 33.5 12.7300 4.4555
4 430.0 29.0 34.0 12.4440 5.1340
fish_target = fish['Species'].to_numpy()
fish_target[:5]
array(['Bream', 'Bream', 'Bream', 'Bream', 'Bream'], dtype=object)
train_input, test_input, train_target, test_target = train_test_split(fish_input, fish_target, random_state=42)
- 데이터를 훈련세트와 테스트세트로 분류한다. 이를 표준화를 통해 전처리를 실시한다.
- 여기서 주의해야 하는것은 훈련세트의 통계값으로 테스트 세트를 변환해야 한다는 것이다.
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
- 표준화를 한 데이터를 가지고 k-최근접이웃 분류를 이용해 확률을 예측해 보도록 한다.
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개의 생선이 있었다. 타깃 데이터를 만들 때 fish.Species를 이용해 만들었기 때문에 훈련, 테스트 세트에도 각각 7개의 생선종류가 들어있다.
- 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류라고 한다.
- 이진분류 모델은 양성 클래스와 음성 클래스를 각각 1과 0으로 지정하여 타깃 데이터를 만들었다. 다중 분류에서도 타깃값을 숫자로 바꾸어 입력할 수 있지만 사이킷런을 이용하면 문자열로 된 타깃값을 그대로 사용이 가능하다.
- 다만 타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 정렬된다. 따라서 pd.unique(fish[‘Species’])로 출력했던 순서와 다르다.
print(kn.classes_)
['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
- ‘Bream’이 첫 번째 클래스, ‘Parkki’가 두 번째 클래스가 된다. predict() 메서드는 친절하게도 타깃값으로 예측을 출력한다.
print(kn.predict(test_scaled[:5]))
['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
- 5개의 샘플에 대한 예측은 어떤 확률을 통해 만들어 졌는가?
- 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환한다.
- 테스트 세트에 있는 처음 5개의 샘플에 대한 확률을 출력하면 다음과 같다.
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4)) # decimals 매개변수를 이용해 소수점 네번째 자리까지 자릿수를 지정한다.
[[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. ]]
- predict_proba() 메서드의 출력 순서는 앞서 보았던 classes_ 속성과 같다.
- 이 모델이 계산한 확률이 가장 가까운 이웃의 비율을 확인해보아야 한다. 네 번째 샘플의 최근접 이웃의 클래스를 확인해보겠습니다.
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
[['Roach' 'Perch' 'Perch']]
- 이를 살펴보면 ‘Roach’가 1개 ‘Perch’가 2개로 각각 0.3333, 0.6667의 확률을 갖는다.
- 앞서 출력한 네번째 샘플의 클래스 확률과 같다.
- 하지만 3개의 최근접 이웃만 사용하다 보니 확률은 0/3, 1/3, 2/3 3/3이 전부이다. 이것을 확률적이라고 말하기에 부족하다 따라서 다른 방법을 찾아보아야 한다.
2. 로지스틱 회귀
- 로지스틱 회귀는 이름은 회귀이지만 사실상 분류모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다.
- \[z = a * (Weight) + b * (Length) + c * (Diagonal) + d * (Height) + e * (Width) + f\]
- a,b,c,d는 가중치 혹은 계수를 말한다. z는 어떤 값도 가능하다. 하지만 확률이 되기 위해서는 0~1(또는 0~100%)사이 값이 되어야 한다. z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 1이 되도록 하기 위해서 시그모이드 함수(로지스틱 함수)를 사용하면 가능하다.
- 시그모이드 함수는 선형 방정식의 출력 z의 음수를 사용해 자연상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다.
- z가 무한하게 큰 음수일 경우 이 함수는 0에 가까워지고, z가 무한하게 큰 양수가 될 때는 1에 가까워진다.
- z가 0이 될 때는 0.5가 된다. z가 어떤 값이 되더라도 공집합은 절대로 0에서 1사이의 범위를 벗어날 수 없다.
- 이를 통해 0~100%까지의 확률로 해석할 수 있다.
- -5와 5사이에 0.1간격으로 배열 x를 만든 다음 z 위치마다 시그모이드 함수를 계산한다.
- 지수함수 계산은 np.exp()함수를 사용한다.
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()
- 이를 통해 시그모이드 함수의 출력은 0에서 1까지 변하는 것을 알 수 있다.
- 로지스틱 회귀모델을 이용해 훈련해보도록 하자.
- 훈련하기 전에 간단한 이진분류를 통해 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']
- 이와 같은 방식을 사용해 훈련 세트에서 도미와 빙어의 행만 골라내보자.(도미와 빙어에 대한 비교 결과를 비트 OR 연산자를 사용해 합치면 도미와 빙어에 대한 행만 골라내는 것이 가능하다.)
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배열에 불리언 인덱싱을 적용하면 손쉽게 도미와 빙어의 데이터만 골라낼 수 있다.
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
LogisticRegression()
print(lr.predict(train_bream_smelt[:5]))
['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
- 두 번째 샘플을 제외하고는 모두 도미로 예측했다. KNeighborsClassifier와 마찬가지로 예측 확률은 Predict_proba() 메서드에서 제공합니다. train_bream_smelt에서 처음 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]]
print(lr.classes_)
['Bream' 'Smelt']
- 샘플마다 2개의 확률이 출력되었다. 첫번째가 음성클래스, 두번째가 양성클래스에 대한 확률이다.
- 아래를 살펴보면 도미가 음성, 빙어가 양성인것을 알 수 있다.
- 이로써 완벽하게 이진분류를 수행했다. 그렇다면 로지스틱 회귀가 학습한 계수를 확인해보자.
print(lr.coef_, lr.intercept_)
[[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]
- 이처럼 로지스틱 회귀는 선형 회귀와 매우 비슷하다다. logisticRegression모델로 z값을 계산해 볼 수 있다. decision_function()메서드로 z값을 출력할 수 있다.
decision = lr.decision_function(train_bream_smelt[:5])
print(decision)
[-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
- 위의 z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다.
- 이를 가능하게 해주는 함수가 존재한다. expit()이 그것이다. 이를 이용해 decisions 배열의 값을 확률로 변환해 보자.
print(expit(decision))
[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
- 출력된 값을 보면 predict_proba() 메서드의 두번째 열의 값과 동일하다. 즉 decision_function() 메서드는 양성 클래스에 대한 z값을 반환한다.
- 그렇다면 최종적으로 7개의 생선을 분류하는 다중분류 문제로 넘어가 보자.
3. 로지스틱 회귀를 이용한 다중 분류 수행
- LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다. max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100이다.
- 충분히 훈련하기 위해 반복횟수를 1000회로 늘리도록 한다.
- 로지스틱 회귀는 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제한다. 이런 규제를 L2 규재라고 한다. 릿지 회귀에서는 alpha 매개변수로 규제의 양을 조절했다. alpha가 커지면 규제도 커진다. 로지스틱회귀에서 규제를 제어하는 매개변수는 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
print(lr.predict(test_scaled[:5]))
['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
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]]
- 첫번째 샘플을 확인해보면 세번째 열의 확률이 가장 높다. 세번쨰 열은 농어(Perch)에 대한 확률이다.
- 두번째 샘플은 여섯번째 열인 Smelt 즉, 빙어로 예측했다.
- 이렇듯 다중 분류도 비슷하게 작동한다. 샘플마다 클래스 개수만큼 확률을 출력한다. 이 중 가장 높은 확률이 예측 클래스가 된다.
print(lr.coef_.shape, lr.intercept_.shape)
(7, 5) (7,)
- 다중분류는 클래스마다 z값을 하나씩 계산한다. 가장 높은 z값을 가진 클래스가 예측 클래스가 된다. 확률은 소프트맥스 함수를 사용해 z값을 확률로 변환한다.
- 시그모이드 함수는 하나의 선형 방정식의 출력값을 0과1 사이로 압축한다. 이와 달리 소프트맥스함수는 여러 개의 선형 방정식의 출력값을 0과1사이로 압축하고 전체 합이 1이 되도록 만든다.
- 이를 위해 지수 함수를 사용하기 때문에 정규화된 지수 함수라고도 부른다.
- 이진분류에서 처럼 decision_function()메서드를 이용해 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]]
- 계산한 값을 그대로 출력하는 것이 아닌 로지스틱 회귀는 이 값을 0과 1사이로 압축한다. 우리는 이 값을 확률로 이해할 수있다.
- 로지스틱 회귀는 이진 분류에서는 하나의 선형방정식을 훈련하고 이 방정식의 출력값을 시그모이드 함수에 통과시켜 0과 1사이의 값을 만든다. 이는 양성클래스에 대한 확률을 말하며 음성클래스의 확률은 1에서 양성클래스의 확률을 빼면 된다.
- 다중분류일 경우에는 클래스 개수만큼 방정식을 훈련하고 각 방정식의 출력값을 소프트맥스 함수를 통과시켜 전체 클래스에 대한 합이 항상 1이 되도록 만든다. 이 값을 각 클래스에 대한 확률로 이해할 수 있다.
4. 마치며
- 이번 시간에는 로지스틱회귀에 대한 전반적인 내용을 배울 수 있었다. 로지스틱 회귀에서의 이진분류와 다중분류, 그리고 이진분류에서의 시그모이드 함수, 다중분류에서의 소프트맥스 함수 등을 배워보며 실제 데이터를 가지고 실습해보았다. 하지만 한번에 이해하기 힘들어 다시 읽어보며 차근차근 되짚어 봐야겠다.
Leave a comment