[혼공머신] 로지스틱 회귀와 시그모이드·소프트맥스 함수

강민우·2022년 1월 14일
0
post-thumbnail

[혼자 공부하는 머신러닝+딥러닝] 책에 기반한 정리글입니다.
전체 소스코드는 아래 Github 링크에서 확인할 수 있습니다.

Github 링크

0. 개요

로지스틱 회귀
선형 방정식을 사용한 분류 알고리즘이다.
시그모이드 함수나 소프트맥스 알고리즘을 사용하여 클래스 확률을 출력할 수 있다.

이 책에서는, 구성품을 모른채 먼저 구매할 수 있는 럭키백이 있다고 가정하고 럭키백을 열어봐야 구성품을 알 수 있다고 한다.
럭키백에 들어간 생선의 크기, 무게 등 특성이 주어졌을 때, 어떤 생선인지에 대한 확률을 출력해야 한다.

이를 확인할 수 있는 로지스틱 회귀를 알아보고, 이진분류에 필요한 시그모이드 함수와 다중 분류에 필요한 소프트맥스 함수를 알아본다.

1. 데이터 준비하기

csv파일을 pandas로 읽어와 타깃 데이터, 입력 데이터로 나눈다.

import pandas as pd
fish = pd.read_csv("http://bit.ly/fish_csv_data")

print(pd.unique(fish['Species'])) # 열에서 고유한 값 출력

#fish의 종류를 타깃 데이터, 나머지 특성을 입력 데이터
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
fish_target = fish['Species'].to_numpy()

훈련 세트와 테스트세트로 나눈 후 표준점수로 전처리한다.

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

#훈련세트와 테스트세트로 나눠주기
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)

2. k-최근접 이웃 분류기로 확률 예측

사이킷런의 KneighborsClassifier 클래스로 모델을 훈련한다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled, train_target)
print(kn.classes_)

출력 ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
사이킷런에서는 문자열로 된 타깃값을 그대로 사용할 수 있지만, 순서가 자동으로 알파벳 순서로 매겨진다.

훈련된 모델로 테스트 세트의 5개 샘플의 종류를 예측한다.

print(kn.predict(test_scaled[:5]))

출력 ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']

pridict_proba() 메서드는 클래스별 확률값을 반환한다.
Numpy의 round()는 반올림 함수이며 decimals 매개변수는 유지할 소수점 아래 자리를 지정할 수 있다.

import numpy as np
# 클래스별 확률값 반환
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4)) #소숫점 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. ]]

4번째 샘플의 경우 Perch일 확률이 2/3, Roach일 확률이 1/3이다.

distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])

출력 [['Roach' 'Perch' 'Perch']]
4번째 샘플의 이웃은 Perch가 2개, Roach가 1개로, 구한 확률이 맞음을 보여준다.

단, 이 방법은 3개의 최근접 이웃만을 사용하기에 확률은 0, 1/3, 2/3, 1 뿐이라는 한계가 있다.

3. 로지스틱 회귀

로지스틱 회귀는 이름은 회귀이지만 분류 모델이다.
선형 회귀와 동일하게 선형 방정식을 학습한다.
z = a x Weight + b x length + ··· + f
a, b, c, d, e는 계수이며 z는 어떤 값도 될 수 있다.

하지만 확률로 표현하려면 0~1 사이의 값이 되어야 하기 때문에
z가 아주 큰 음수일때 0이 되고, 아주 큰 양수일 때 1이 되도록 바꾼다.
이는 시그모이드 함수를 사용하면 가능하다.

3-1. 시그모이드 함수

시그모이드 함수 : ϕ=11+ez\phi = \frac{1}{1+e^{-z}}

넘파이를 이용해서 간단하게 그려본다.

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()

이진 분류를 먼저 수행해 볼 것이다.
이진 분류에서 시그모이드 출력이 0.5보다 크면 양성클래스, 작으면 음성클래스로 판단한다.

3-2. 로지스틱 회귀로 이진분류 수행하기

불리언 인덱싱으로 도미와 빙어 데이터를 골라낸다.

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]

이 데이터로 로지스틱 회귀 모델을 훈련한다.

from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)

훈련한 모델로, 5개의 테스트 샘플을 예측해본다.

print(lr.predict(train_bream_smelt[:5]))

출력 ['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']

각각의 샘플 확률을 예측해본다.

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.coef_, lr.intercept_)

출력 [[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]][-2.16155132]
계수들과 절편을 볼 수 있다.

따라서 이 로지스틱 회귀 모델이 학습한 방정식은 다음과 같다.
z = -0.404 x 무게 + -0.576 x 길이 + ··· + -2.161

z값과 시그노이드 함수의 값 또한 볼 수 있다.

decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)

from scipy.special import expit
print(expit(decisions))

출력
[-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ] [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]

3-3. 로지스틱 회귀로 다중 분류 수행하기

LogisticRegression 클래스는 반복적인 알고리즘을 사용하며, max_iter 매개변수에서 반복값을 지정하며 기본값은 100 이다.

또한 릿지 회귀와 같이 계수의 제곱을 규제하며, L2 규제라고도 불린다.

릿지회귀에서 alpha로 규제의 양을 조절한 것과 달리, C 매개변수로 조절한다. C의 기본값은 1이며 작을수록 규제가 커진다.

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))

print(lr.predict(test_scaled[:5])) # 샘플 5개의 종류 예측

출력
0.9327731092436975
0.925
['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
과대적합이나 과소적합이 되지 않았다.

5개 샘플에 대한 예측 샘플도 볼 수 있다.

print(lr.classes_)
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
출력
['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
[[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]]

클래스 정보와 클래스 예측 확률을 볼 수 있다.

다중 분류에서의 예측 확률은 소프트맥스 함수를 사용하여 7개의 z값을 확률로 변환한다.

3-4. 소프트맥스 함수

여러 개의 선형 방정식의 출력값을 0~1로 압축하고, 전체 합이 1이 되도록 만든다.

7개의 z값을 z1, z2, ··· , z7 라고 가정하고, 지수함수로 ez1e^{z1}, ez2e^{z2} ... 로 만들어 더해준다.

e_sum = $$e^{z1}$$ + ... + $$e^{z7}$$

그 다음 ez1e^{z1}, ez2e^{z2} ... 를 각각 e_sum 으로 나눠준다.
s1 = ez1esum\frac{e^{z1}}{e sum}, s2 = ez1esum\frac{e^{z1}}{e sum} ...

s1에서 s7까지 모두 더하면 분자와 분모가 같아져 1이 된다.
7개 생선에 대한 확률의 합은 1이므로 잘 맞다.

이를 softmax() 함수로 나타낼 수 있다.

from scipy.special import softmax

decision = lr.decision_function(test_scaled[:5]) #샘플의 z값 계산
proba = softmax(decision, axis=1)
print(np.round(proba, decimals=3)) #각 행의 확률의 합은 1
출력
[[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]]
profile
어제보다 성장한 오늘

0개의 댓글