혼자 공부하는 머신러닝 + 딥러닝 04-1 로지스틱 회귀

손지호·2023년 7월 22일
0

럭키백의 확률

럭키백에 들어갈 수 있는 생선은 7마리!
"k-최근접 이웃은 주변 이웃을 찾아주니까 이웃의 클래스 비율을 확률이라고 출력하면 되지 않을까?"

샘플 X 주위 가장 가까운 이웃 샘플 10개 표시. 사각형 3개, 삼각형 5개, 원 2개. 즉, 사각형일 확률 30%, 삼각형일 확률 50%, 원일 경우 20%
이를 사이킷런의 k-최근접 이웃 분류기에서도 동일한 방식으로 클래스 확률을 계산하여 제공한다!


데이터 준비

import pandas as pd

fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()

+ 데이터프레임이란??
판다스에서 제공하는 2차원 표 형식의 주요 데이터 구조. 넘파이 배열과 비슷하게 열과 행으로 이루어져 있음. 통계와 그래프 위한 메서드 풍부하게 제공. 넘파이로 상호 변환 쉽고 호환 잘 됨!

판다스의 unique() 함수 사용하여 Species 열의 고유한 값 추출 가능!

print(pd.unique(fish['Species']))
>>> ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']

Species 열을 타깃으로 만들고 나머지 5개 열을 입력 데이터로 사용. to_numpy() 메서드 사용하여 넘파이 배열로 바꾸어 fish_input에 저장.

fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()

# 처음 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()

훈련 세트와 테스트 세트로 나누기!

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)

사이킷런의 StandardScler 클래스 사용해 훈련 세트와 테스트 세트를 표준화 전처리해보자. 여기서도 훈련 세트의 통계 값으로 테스트 세트 변환해야 한다!!

from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

k-최근접 이웃 분류기의 확률 예측

2장에서 했던 것처럼 사이킷런의 KNeighborsClassifer 클래스 객체 만들고 훈련 세트로 모델 훈련 후 훈련 세트와 테스트 세트의 점수 확인. 이웃 개수인 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['Species'] 사용해 만들었기 때문에 훈련 세트와 테스트 세트의 타깃 데이터에도 7개의 생선 종류가 들어가 있음.
이렇게 타깃 데이터에 2개 이상 클래스 포함된 문제를 다중 분류(multiclass classifiction) 이라 부른다.

2장에서 이진 분류는 양성 클래스와 음성 클래스를 각각 1과 0으로 지정하여 타깃 데이터 만들었다면, 사이킷런에서는 편리하게 문자열로 된 타깃값 그대로 사용 가능!!
다만, 타깃값 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 매겨진다. 따라서 pd.unique(fish['Species'])로 출력한 순서와 다르다.

# 정렬된 타깃값이 저장되어 있는 속성.
print(kn.classes_)
>>> ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
# predict() : 타깃값으로 예측을 출력.
print(kn.predict(test_scaled[:5]))
>>> ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']

사이킷런 분류 모델은 predict_proba() 매서드로 클래스별 확률값 반환. 넘파이 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.    ]]

네 번째 샘플 [0. 0. 0.6667 0. 0.3333 0. 0. ]
첫 번째 클래스(Bream)에 대한 확률
두 번째 클래스(Parkki)에 대한 확률

가장 가까운 이웃의 비율이 맞는지 확인. 네 번째 새믈의 최근접 이웃의 클래스 확인해보자.

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

이 샘플의 이웃은 다섯 번째 클래스인 'Roach'가 1개, 세 번째 클래스인 'Perch'가 2개. 따라서 다섯 번째 클래스에 대한 확률은 1/3 = 0.3333이고 세 번째 클래스에 대한 확률은 2/3 = 0.6667이 된다. 출력한 네 번째 샘플의 클래스와 같다!!
번거로운 계산은 사이킷런이 수행해 주므로 predict_proba() 매서드 호출하면 된다!!!

하지만 3개의 최근접 이웃만을 사용해서 가능한 확률은 0/3, 1/3, 2/3, 3/3이 전부이다. 더 좋은 방법을 찾아보자!!


로지스틱 회귀

로지스틱 회귀(logistic regression) : 이름은 회귀지만 분류 모델. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다.
예를 들어 z = a (Weight) + b (Length) + c (Diagonal) + d (Height) + e * (Width) + f
여기에서 a, b, c, d, e는 가중치 혹은 계수. z는 어떤 값도 가능하지만 확률이 되려면 0~1(or 0~100%) 사이값 되어야 한다. z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 떄 1이 되도록 바꿔주는 시그모이드 함수(sigmoid function)(또는 로지스틱 함수(logistic function)) 사용하면 된다!!

z가 무한하게 큰 음수일 경우 이 함수는 0에 가까워지고, z가 무한하게 큰 양수가 될 때는 1에 가까워진다. z가 0이면 0.5가 된다. z가 어떤 값이던지 무조건 0-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()


로지스틱 회귀로 이진 분류 수행하기

불리언 인덱싱(boolean indexing) : True, False 값 전달하여 행 선택하는 넘파이 배열

# 'A', 'C'만 골라내고 싶으면 이 두 원소에만 True 값 지정.
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])
>>> ['A' 'C']

이를 이용해 도미(Bream)와 빙어(Smelt)의 행만 골라내자. 도미인 행 골라내기 위해선 train_target == 'Bream'과 같이 작성. 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_indexs 배열은 도미와 비어일 경우 True이고 그 외는 모두 False 값이 들어가 있다. 따라서 이 배열을 사용해 train_scaled와 train_target 배열에 불리언 인덱싱 적용하면 손쉽게 도미와 빙어 데이터만 골라낼 수 있다!!

# LogisticRegression 클래스는 선형 모델이므로 sklearn.linear_model 패키지 아래 있음
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
>>> LogisticRegression
	LogisticRegression()

# 훈련한 모델 사용해 train_bream_smelt에 있는 처음 5개 샘플 예측해보자.
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]]

샘플마다 2개의 확률이 출력되었음. 첫 번째 열이 음성 클래스(0)에 대한 확률, 두 번째 열이 양성 클래스(1)에 대한 확률. classes_ 속성을 통해 Bream과 Smelt 중 어떤 것이 양성 클래스인지 확인.

print(lr.classes_)
>>> ['Bream' 'Smelt']

빙어(Smelt)가 양성 클래스. predict_proba() 메서드가 반환한 배열 값 보면 두 번째 샘플만 양성 클래스인 빙어의 확률이 높다. 나머지는 모두 도미(Bream)로 예측.

# 로지스틱 회귀가 학습한 계수 확인
print(lr.coef_, lr.intercept_)
>>> [[-0.4037798  -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]

따라서 이 로지스틱 회귀 모델이 학습한 방정식은
z = -0.404 (Weight) - 0.576 (Length) - 0.663 (Diagonal) - 1.013 (Height) - 0.732 * (Width) - 2.161

확실히 로지스틱 회귀와 선형 회귀는 매우 비슷. LogisticRegression 클래스는 decision_function() 메서드로 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() 함수를 사용해 분수 계산 하는 것보다 훨씬 편리하고 안전!

# dicisions 배열의 값을 확률로 변환
from scipy.special import expit

print(expit(decisions))
>>> [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]

출력된 값 보면 predict_proba() 메서드 출력의 두 번째 열의 값과 동일. 즉 dicision_function() 메서드는 양성 클래스에 대한 z 값을 반환.



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

LogisticRegression 클래스 사용해 7개의 생선 분류해 보면서 이진 분류와의 차이점 알아보자!

LogisticRegression 클래스는 기본적으로 반복적인 알고리즘 사용. max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100. 여기에 준비한 데이터셋을 사용해 모델을 훈련하면 반복 횟수가 부족하다는 경고 발생. 충분히 훈련시키기 위해 반복 횟수를 1,000으로 늘려보자.
또 LogisticRegression 클래스는 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제한다. 이런 규제를 L2 규제라고도 부른다. 릿지 회귀에서는 alpha 매개변수로 귲의 양을 조절했다. alpha가 커지면 규제도 커진다. LogisticRegression에서 규제를 제어하는 매개변수는 C. 이 C는 alpha와 반대로 작을수록 규제가 커진다.

# LogisticRegression 클래스로 다중 분류 모델 훈련하는 코드.
# 7개의 생선 데이터가 모두 들어 있는 train_scaled와 train_target을 사용한 점 눈여겨보기.
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]]

첫 번째 샘플의 세 번째 열의 확률이 84.1%로 가장 높음.

# 세 번째 열이 농어(Perch)에 대한 확률. classes_ 속성에서 클래스 정보 확인.
print(lr.classes_)
>>> ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']

첫 번째 샘플은 Perch를 가장 높은 확률로 예측. 두 번째 샘플은 여섯 번째 열인 Smelt를 94.6%로 가장 높은 확률로 예측.

#coef_와 intercept_의 크기 출력 대중 분류일 경우 선형 방정식의 모형 확인.
print(lr.coef_.shape, lr.intercept_.shape)
>>> (7, 5) (7,)

이 데이터는 5개의 특성을 사용하므로 coef_ 배열의 열은 5개. 그런데 행은 7개. 이 말은 이진 분류에서 보았던 z를 7개나 계산한다는 의미. 다중 분류 클래스는 z값을 하나씩 계산. 당연히 가장 높은 z 값을 출력하는 클래스가 예측 클래스가 된다. 다중 분류는 소프트맥스(softmax) 함수 사용하여 7개의 z값 확률로 변환.

먼저 7개의 z 값의 이름을 z1~z7로 붙이고 이 값을 사용해 지수 함수 e^z1 ~ e^z7을 계산해 모두 더한다.
e_sum = e^z1 + e^z2 + e^z3 + e^z4 + e^z5 + e^z6 + e^z7

그 다음 e^z1~e^z7을 각각 esum으로 나누어주기!
_s1 = e^z1/e_sum, s2 = e^z2/e_sum, ..., s7 = e^z7/e_sum

s1에서 s7까지 모두 더하면 분자, 분모 같아지므로 1이 된다. 7개 생선에 대한 확률의 합은 1이어야 하니 잘 맞는다!

# 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]]

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

앞서 구한 decision 배열은 softmax() 함수에 전달. softmax()의 axis 매개변수는 소프트맥스를 계산할 축을 지정. 여기서 axis=1로 지정하여 각 행, 즉 각 샘플에 대해 소프트맥스를 계산. 만약 axis 매개변수를 지정하지 않으면 배열 전체에 대해 소프트맥스를 계산한다.
출력 결과를 앞서 구한 proba 배열과 비교해보자. 겨로가가 정확히 일치!!




전체 코드 (출처 : https://bit.ly/hg-04-1)

import pandas as pd

fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()

print(pd.unique(fish['Species']))

fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()

print(fish_input[:5])

fish_target = fish['Species'].to_numpy()

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)

print(kn.score(train_scaled, train_target))
print(kn.score(test_scaled, test_target))

print(kn.classes_)

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

import numpy as np

proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))

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

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

char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, 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)

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)

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

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

print(lr.classes_)

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

from scipy.special import expit

print(expit(decisions))

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

proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))

print(lr.classes_)

print(lr.coef_.shape, lr.intercept_.shape)

decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))

from scipy.special import softmax

proba = softmax(decision, axis=1)
print(np.round(proba, decimals=3))

정리

  • 로지스틱 회귀는 선형 방정식을 사용한 분류 알고리즘이다. 선형 회귀와 달리 시그모이드 함수나 소프트맥스 함수를 사용하여 클래스확률을 출력할 수 있다.
  • 다중 분류는 타깃 클래스가 2개 이상인 분류 문제. 로지스틱 회귀는 다중 분류를 위해 소프트맥스 함수를 사용하여 클래스 예측.
  • 시그모이드 함수는 선형 방정ㅎ식의 출력을 0과 1 사이의 값으로 압축하며 이진 분류를 위해 사용.
  • 소프트맥수 함수는 다중 분류에서 여러 선형 방정식의 출력 결과를 정규화하여 합이 1이 되도록 만듦.

핵심 패키지와 함수

scikit-learn

  • LogisticRegression : 선형 분류 알고리즘인 로지스틱 회귀를 위한 클래스.
    solver 매개변수에서 사용할 알고리즘 선택 가능. 기본값은 'lbfgs'. 사이킷런 0.17 버전에 추가된 'sag' 는 확률적 평균 경사 하강법 알고리즘으로 특성과 샘플 수가 많을 때 성능 빠르고 좋음. 사이킷런 0.19 버전에는 'sag'의 개선 버전인 'saga'가 추가됨.
    penalty 매개변수에서 L2 규제(릿지 방식)와 L1 규제(라쏘 방식)를 선택 가능. 기본값은 L2 규제 의미하는 'l2'
    C 매개변수에서 규제의 강도 제어. 기본값은 1.0이며 값이 작을수록 규제가 강해짐.
  • predict_proba : 예측 확률 반환.
    이진 분류 경우 샘플마다 음성 클래스와 양성 클래스에 대한 확률 반환.
    다중 분류의 경우에는 샘플마다 모든 클래스에 대한 확률 반환.
  • decision_function : 모델이 학습한 선형 방정식의 출력을 반환.
    이진 분류 경우 양성 클래스의 확률 반환. 이 갑시 0보다 크면 양성 클래스, 작거나 같으면 음성 클래스로 예측.
    다중 분류의 경우 각 클래스마다 선형 방정식 계산. 가장 큰 값의 클래스가 예측 클래스가 됨.
profile
초보 중의 초보. 열심히 하고자 하는 햄스터!

1개의 댓글

comment-user-thumbnail
2023년 7월 22일

좋은 글 감사합니다. 자주 올게요 :)

답글 달기