('혼자 공부하는 머신러닝+딥러닝' 176~198 페이지의 내용을 정리한 것입니다.)
통계학에서 '회귀'란, 여러 독립 변수와 한 개의 종속 변수 간의 상관관계를 모델링하는 기법을 통칭한다.
- 예를 들어, 아파트 방 개수, 크기, 주변 학군 등 여러 개의 독립 변수에 따라 아파트 가격이라는 종속 변수가 어떤 관계를 나타내는지.
(https://john-analyst.medium.com/%ED%9A%8C%EA%B7%80-regression-%EB%9E%80-398c548e1560)
그렇다면 로지스틱 회귀란?
- 회귀를 사용해서 데이터가 어떤 범주에 속할 확률을 0에서 1사이의 값으로 '예측'하고, 그 확률에 따라 가능성이 더 높은 범주에 속하는 것으로 분류해주는 '지도 학습' 알고리즘이다.
(https://hleecaster.com/ml-logistic-regression-concept/)
로지스틱 회귀의 내용에 들어가기 전에, 어쨌거나 분류 알고리즘에 속하니까 같은 분류 알고리즘인 k-최근접 이웃 알고리즘을 통해 이웃한 샘플들의 비율을 확률로 출력한 것과 다를까에 대해 알아보고자 한다.
럭키백 안에 든 무작위의 생선을 예로 들어서, 길이, 높이, 두께, 대각선 길이, 무게를 변수로 사용해서 7가지의 생선에 대한 확률을 출력한다면, k-최근접 이웃 알고리즘으로 주변 이웃 샘플이 분포한 확률을 출력하면 되지 않을까
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()
어떤 종류의 생선이 있는지 Species열에서 고유한 값을 추출한다.(pandas의 unique() 함수 사용) -> 어떤 생선 종이 있는지 출력.
print(pd.unique(fish['Species']))
결과
['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
이제 뽑아낸 7개의 종을 target으로 만들고 데이터프레임에서 나머지 5개의 열은 입력 데이터로 사용한다.
#입력 데이터로 쓸 열 선택, 새로운 데이터 프레임으로 반환된다.
#to_numpy() 메서드로 numpy 배열로 바꿔 저장.
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
#target data 생성
fish_target = fish['Species'].to_numpy()
fish[['Species']] 같이 괄호 두 개 쓰지 말 것. 두 개 쓰면 2차원 배열이 된다.
# train set, test set 분리
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)
# train set, test set 표준화
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
k-최근점 이웃 분류기의 확률을 예측해보면,
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
하지만 클래스 확률을 배우는 것이 목적이니까 점수가 중요한 것이 아니다.
target 데이터를 만들 때 fish['Species']를 사용해 만들었기 때문에 훈련 세트와 테스트 세트의 target 데이터에도 7개의 생선 종류가 들어가 있다. 이처럼 target 데이터에 2개 이상의 클래스가 포함된 문제를 '다중 분류'라고 한다.
이진 분류와 모델을 만들고 훈련하는 방식은 동일한데, 이진 분류를 사용했을 땐 양성 클래스와 음성 클래스를 각각 1과 0으로 지정해 target 데이터를 만들었고, 다중 분류에서도 target값을 숫자로 바꿔 입력할 수 있지만 사이킷런에서는 문자열로 된 target값을 그대로 사용할 수 있다.(target값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 된다. -> pd.unique(fish['Species'])로 출력했던 것과 다름.)
print(kn.classes_)
결과
['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
이런 식으로 알파벳 순으로 나옴.
그리고 predict 메서드를 사용해 테스트 세트에 있는 처음 5개 샘플의 target값을 예측해 보면,
print(kn.predict(test_scaled[:5]))
결과
['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
그리고, 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스 별 확률 값을 반환한다.
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4)) #decimals=4는 소수점 네번째 자리까지 표기하라는 뜻(5번째에서 반올림)
결과
[[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. ]]
출력 순서는 역시 알파벳 순이다.
z = a x 무게 + b x 길이 + c x 대각선 + d x 높이 + e x 두께 + f
(a, b, c, d, e는 가중치 혹은 계수)
위 식에서 z는 어떤 값도 가능하지만, 확률이 되려면 0~1(또는 0~100%)사이 값이 되어야 한다.
그렇다면 z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 1이 되도록 바꾸는 방법은?
시그모이드 함수(또는 로지스틱 함수, 왼쪽)
시그모이드 그래프
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()
numpy를 사용해 -5와 5 사이에 0.1 간격으로 배열 z를 만들고 해당 위치마다 시그모이드 함수를 계산한다.(지수함수 계산은 np.exp() 사용)
0에서 1까지만 변하는 것을 확인할 수 있음.
사이킷런의 LogisticRegression 클래스를 사용하는데 그 전에 이진 분류를 수행해 보면, 이진 분류의 경우 시그모이드 함수의 출력이 0.5보다 크면 양성 클래스, 작으면 음성 클래스로 판단한다.(단 정확히 0.5일 때는 라이브러리마다 다를 수 있고, 사이킷런은 음성 클래스로 판단함)
이진 분류 수행하기 전 알아야 할 것
- boolean indexing
- numpy 배열은 True, False 값을 전달해 행을 선택할 수 있다.
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])
결과['A' 'C']
boolean indexing을 활용해 훈련 세트에서 도미와 빙어의 행만 골라낼 수 있다.
#행 골라내기. 도미와 빙어만 True로 반환하고 나머지는 전부 False
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)
훈련한 모델을 사용해 train_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]]
샘플마다 2개의 확률이 출력됐는데, 첫 번째 열이 음성 클래스(0)에 대한 확률이고, 두 번째 열이 양성 클래스(1)에 대한 확률임. 그럼 빙어와 도미 중 어떤 것이 양성 클래스인지를 알아보면,
print(lr.classes_)
결과
['Bream' 'Smelt'] #빙어가 양성 클래스
predict_proba() 메서드가 반환한 확률값을 보면 두번째 샘플만 양성 클래스, 즉 빙어일 확률이 높다. 나머지는 도미로 예측할 것임.
그리고 이 모델이 학습한 계수를 확인해보자.
print(lr.coef_, lr.intercept_)
결과
[[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]
따라서 이 모델이 학습한 방정식은
z = -0.404 x wight -0.576 x length - 0.663 x diagonal - 1.013 x height - 2.161
근데 이렇게 직접 z값을 계산하기는 귀찮으니까 LogisticRegression 모델로 계산 한다.
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
결과
[-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
나온 z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다.(scipy라이브러리에 있는 시그모이드 함수 expit()을 사용.)
from scipy.special import expit
print(expit(decisions))
결과
[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
위 코드는 양성 클래스(빙어)에 대한 z값을 반환한다.(predict_proba 메서드 출력의 두 번째 열의 값과 동일)
#규제를 완화하기 위해 c=20
#max_iter=1000인 이유는 반복횟수가 부족하다는 경고를 해결하기 위해.(데이터 양이 적어서?)
lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)
#7개 생선 데이터가 전부 들어있는 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']
확률까지 출력해보자
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]]
#5개 샘플에 대한 예측이라서 5개 행 출력됨
#생선 종류가 7개니까 7개의 열
각 열이 어떤 생선에 해당하는지 알아봐야 할 듯.
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개인 것은 맞는데, coef와 intercept 모두 행이 7임.(z를 7개 계산한다, 그래프가 7개?) 즉, 다중 분류는 클래스마다 z를 계산한다.
7개의 중 가장 높은 z값을 출력하는 클래스가 예측 클래스가 된다.
중요한 점은 확률을 구하는 것에서 이진분류와 다중분류의 차이가 있다.
시그모이드 함수를 이용해 z값을 0~1사이의 값으로 변환해 확률을 구하는 이중 분류와 달리
다중 분류는 이 7개의 z값을 소프트맥스 함수를 사용해 확률로 변환한다.
- 세 개 이상으로 분류하는 다중 클래스 분류에서 사용하는 활성화 함수다.
- 소프트맥스 함수는 분류될 클래스가 n개라 할 때, n차원의 벡터를 입력받아서 각 클래스에 속할 확률을 추정한다.
- 출력의 합은 1이 된다.
이제 decision_function()메서드로 z1~z7의 값을 구한 뒤 소프트맥스 함수를 써서 확률로 바꿔보면,
#z 값 구하기
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]]
#5개 샘플에 대한 7개의 z값
#소프트맥스
from scipy.special import softmax
#바로 앞에 구한 decision 배열(z값)을 소프트맥스 함수에 전달
#axis 매개변수로 계산할 축 지정 -> 지정하지 않으면 전체에 대한 소프트맥스를 계산함
#axis=1로 지정해 각 행(각 샘플)에 대해 계산을 수행
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]]
둘의 공통점은 활성화 함수라는 것임.
활성화 함수란?
딥러닝은 다양한 레이어에서 데이터를 연산하고, 다음 레이어로 값을 전달하는 구조인데, 이 과정에서 데이터를 다음 레이어로 바로 전달하지 않고 비선형 함수를 통과시켜서 데이터를 정리?? 해준 다음 전달한다. 이 때 데이터를 정리해주는 비선형 함수를 활성화 함수라고 함.
각각의 특징을 찾아 클래스별로 분류하는 모델을 가지고 분류를 수행할 때, 레이어의 연산과정에서 필요없는 데이터가 생기거나 애매한 데이터가 있을 수 있음. 그럴 때 해당 데이터값을 무시해야하는 상황이거나 수치가 너무 커서 다른 연산값에도 부정적인 영향을 미치거나 하는 상황을 미연에 방지하기 위해 활성화 함수를 써서 데이터의 폭을 어느정도 정의한다.
https://needjarvis.tistory.com/564
출처:https://blog.naver.com/th9231/221989350922
참고