본문 바로가기
Python/ML

[머신러닝] 로지스틱 회귀(생선 확률 예측)

by JooRi 2023. 2. 10.
728x90
SMALL

 

교재: 혼자 공부하는 머신러닝+딥러닝 (hanbit.co.kr)

 

* 문제: 로지스틱 회귀로 확률을 예측한다.

k-최근접 이웃 분류기의 한계를 경험해 보고 이를 해결하기 위해 로지스틱 회귀를 사용해 생선 종류의 확률을 예측한다.

 

* 문제 해결 과정

1. 데이터 준비하기(pandas)

2. 훈련 세트와 테스트 세트 만들기

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

4. 로지스틱 회귀

  1) 로지스틱 회귀로 이진 분류

  2) 로지스틱 회귀로 다중 분류

 

 

1. 데이터 준비하기(pandas)

판다스로 인터넷에서 데이터를 바로 다운로드하자.

 

import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()  # 처음 5개 행 출력

 

출력

 

read_csv()는 csv 파일(판다스 데이터프레임을 만들기 위해 사용하는 파일)을 인터넷에서 읽어 판다스 데이터프레임으로 변환하는 함수이다. read_csv() 함수에 주소를 넣으면 판다스에서 파일을 읽을 수 있다.

 

 

어떤 종류의 생선이 있는지 판다스의 unique() 함수로 Species열에서 값을 출력해 보자.

 

print(pd.unique(fish['Species']))
# 출력: ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']

 

총 7종류의 생선이 있다.

 

 

위의 데이터프레임에서 Species열을 타깃 데이터, 나머지 5개 열을 입력 데이터로 사용해 보자.

 

# 입력 데이터 만들기
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()

 

데이터프레임에서 열을 선택할 때는 원하는 열을 리스트로 나열하면 된다.

Species열을 뺀 나머지 5개 열을 선택한 후 to_numpy() 메서드로 넘파이 배열로 변환하여 fish_input에 저장하였다.

 

 

fish_input에 5개의 특성이 잘 저장되었는지 처음 5개 행을 출력해 보자.

 

print(fish_input[:5])  # 입력 데이터의 처음 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 ]]

 

 

위의 입력 데이터와 동일한 방식으로 Species열을 타깃 데이터로 만들어보자.

 

# 타깃 데이터 만들기
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)

 

사이킷런의 train_test_split() 함수를 임포트 한 후 fish_input과 fish_target을 훈련 세트와 테스트 세트로 나누었고, 랜덤 시드(radom_state)는 42로 지정하였다.

train_test_split() 함수는 전달되는 리스트나 배열을 비율에 맞게 훈련 세트와 테스트 세트로 나눠주며, 자체적으로 랜덤 시드를 지정할 수 있는 random_state 매개변수가 있다.

 

 

훈련 세트와 테스트 세트를 표준화 전처리해 보자.

 

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()  # 객체 생성
ss.fit(train_input)  # 훈련
train_scaled = ss.transform(train_input)  # 훈련 세트 변환
test_scaled = ss.transform(test_input)  # 테스트 세트 변환

 

평균과 표준편차를 직접 구해 특성을 표준점수로 바꾸지 않고 사이킷런 변환기 중 하나인  StandardScaler 클래스를 사용하였다.

StandardScaler 클래스의 객체 ss를 초기화한 후 PolynomialFeatures 클래스로 만든 train_input을 사용해 ss 객체를 훈련하였고, 변환기로 훈련 세트와 테스트 세트를 변환하였다.

이제 데이터 준비가 끝났다.

 

 

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

먼저 모델을 훈련해 보자.

 

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors=3)  # 객체 생성
kn.fit(train_scaled, train_target)  # 훈련
print(kn.score(train_scaled, train_target))  # 훈련 세트 점수 출력: 0.8907563025210085
print(kn.score(test_scaled, test_target))  # 테스트 세트 점수 출력: 0.85

 

사이킷런 k-최근접 이웃 분류 모델을 만드는 클래스인 KNeighborsClassifier를 임포트 하고 KNeighborsClassifier 클래스의 kn 객체를 만든 후 최근접 이웃 개수를 3으로 지정하여 모델을 훈련하였다.

 

 

타깃 데이터를 만들 때 fish['Species']를 사용했기 때문에 훈련 세트와 테스트 세트의 타깃 데이터에도 7개의 생선 종류가 들어가 있다. 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류라고 한다.

이진 분류를 사용했을 때 클래스를 각각 1과 0으로 지정하여 타깃 데이터로 만든 것처럼 다중 분류도 타깃값을 숫자로 바꿀 수 있지만 사이킷런에서는 문자열로 된 타깃값을 그대로 사용 가능하다.

타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 매겨진다. 따라서 pd.unique(fish['Species'])로 출력한 순서와 다르다.

KNeighborsClassifier에서 정렬된 타깃값을 출력해 보자.

 

print(kn.classes_)
# 출력: ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']

 

'Bream'이 첫 번째 클래스, 'Parkki'가 두 번째 클래스이다.

Species열에서 출력한 결과 ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']와 다르다.

 

 

테스트 세트에 있는 처음 5개 샘플의 타깃값을 예측해 보자.

 

print(kn.predict(test_scaled[:5]))  
# 출력: ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']

 

 

위의 출력된 예측이 어떤 확률로 만들어졌는지 테스트 세트의 처음 5개 샘플의 확률을 출력해 보자.

 

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

 

사이킷런 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환한다.

넘파이 round() 함수는 소수점 첫째 자리에서 반올림하는데, decimals 매개변수로 유지할 소수점 아래 자릿수를 지정한다.

소수점 네 번째 자리까지 출력(다섯 번째 자리에서 반올림)하도록 decimals=4로 지정하였다.

출력 결과 첫 번째 열이 'Bream'에 대한 확률, 두 번째 열이 'Parkki'에 대한 확률이다.

 

 

계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인해 보자. 

 

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

 

네 번째 샘플의 최근접 이웃의 클래스를 출력하였더니 다섯 번째 클래스인 'Roach'가 1개이고 세 번째 클래스인 'Perch'가 2개이다. 따라서 세 번째 클래스에 대한 확률은 2/3 = 0.6667, 다섯 번째 클래스에 대한 확률은 1/3 = 0.3333이다.

위에서 출력한 네 번째 클래스의 확률 0.6667과 같으므로 확률 예측에 성공하였지만 3개의 최근접 이웃을 사용했기 때문에 가능한 확률은 0/3, 1/3, 2/3, 3/3뿐이다.

 

 

4. 로지스틱 회귀

로지스틱 회귀는 이름은 회귀이지만 분류 모델이고, 선형 회귀와 동일하게 선형 방정식을 학습한다.

ex) z = a x 무게 + b x 길이 + c x 대각선 길이 + d x 높이 + e x 너비

위의 선형 방정식에서 확률 z는 0~1(또는 0~100%) 사이 값이어야 한다. 따라서 시그모이드 함수를 사용해 z가 음수일 때 0, 양수일 때 1이 되도록 해야 한다.

시그모이드 함수는 로지스틱 함수라고도 하며, 음수 z를 사용해 자연 상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다.

 

import numpy as np
import matplotlib.pyplot as plt
z = np.arange(-5, 5, 0.1)  #  -5와 5 사이의 간격을 0.1로 하여 배열 z 생성
phi = 1 / (1 + np.exp(-z))  # 시그모이드 함수 계산
plt.plot(z, phi)
plt.xlabel('z')
plt.ylabel('phi')
plt.show()

 

시그모이드 함수

 

-5와 5 사이의 간격을 0.1로 하여 배열 z를 만들었고, z의 위치마다 시그모이드 함수를 계산하였다.

지수 함수 계산은 np.exp() 함수를 사용하였다.

 

 

1) 로지스틱 회귀로 이진 분류

이진 분류는 시그모이드 함수의 출력이 0.5보다 크면 양성 클래스, 0.5보다 작으면 음성 클래스로 판단한다.

먼저 도미와 빙어로 이진 분류를 수행해 보자.

 

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]

 

넘파이 배열은 True와 False 값을 전달하여 행을 선택할 수 있는데, 이를 불리언 인덱싱이라고 한다.

train_target == 'Bream'은 train_target 배열에서 'Bream'인 것은 True, 나머지는 False인 배열을 반환하여 도미인 행을 골라낸다. 비트 OR 연산자(|)로 도미와 빙어에 대한 결과를 합쳤다.

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)  # 훈련
print(lr.predict(train_bream_smelt[:5]))  # 출력: ['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']

 

LogisticRegression 클래스로 다중 분류 모델을 훈련하였다.

train_bream_smelt에 있는 처음 5개 샘플을 예측한 결과 두 번째 샘플을 제외하고 모두 도미로 예측하였다.

 

 

처음 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)에 대한 확률이다.

 

 

도미와 빙어 중 무엇이 양성 클래스인지 확인해 보자.

 

print(lr.classes_)  # 출력: ['Bream' 'Smelt']

 

빙어가 양성 클래스이다.

로지스틱 회귀로 성공적인 이진 분류를 수행하였다!

 

 

로지스틱 회귀가 학습한 계수를 확인해 보자.

 

print(lr.coef_, lr.intercept_)
# 출력: [[-0.4037798  -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]

 

 

coef_ 속성과 intercept_ 속성에는 로지스틱 모델이 학습한 선형 방정식의 계수가 들어있다.

출력 결과 로지스틱 회귀 모델이 학습한 방정식은 아래 식과 같다.

z = -0.404 x 무게 + -0.576 x 길이 + -0.663 x 대각선 길이 + -1.013 x 높이 + -0.732 x 너비 - 2.161

 

 

z 값을 계산해 보자.

 

decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
# 출력: [-6.02927744  3.57123907 -5.26568906 -4.24321775 -6.0607117 ]

 

선형 분류 알고리즘인 로지스틱 회귀를 위한 LogisticRegression 클래스의 decision_function() 메서드로 train_bream_smelt의 처음 5개 샘플의 z값을 출력하였다. 

 

 

마지막으로 z값을 시그모이드 함수에 통과시켜 확률을 출력해 보자.

 

from scipy.special import expit
print(expit(decisions))
# 출력: [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]

 

파이썬 사이파이 라이브러리의 expit() 시그모이드 함수로 decisions 배열의 값을 확률로 변환하였다. expit() 함수는 분수 계산을 하는 np.exp() 함수보다 계산이 훨씬 편리하다.

출력 결과 처음 5개 샘플의 예측 확률의 두 번째 열과 값이 같다. 즉 decision_function() 메서드는 양성 클래스에 대한 z값을 반환한다.

로지스틱 회귀로 성공적인 이진 분류를 수행하였다!

 

 

2) 로지스틱 회귀로 다중 분류

다중 분류 모델을 훈련해 보자.

 

lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target))  # 훈련 세트 점수 출력: 0.9327731092436975
print(lr.score(test_scaled, test_target))  # 테스트 세트 점수 출력: 0.925

 

LogisticRegression 클래스로 다중 분류 모델을 훈련하였다.

LogisticRegression 클래스는 max_iter 매개변수에 반복 횟수를 지정하며 기본값은 100이다. 충분하게 훈련시키기 위해 반복 횟수를 1000으로 지정하였다.

LogisticRegression은 릿지와 같이 계수의 제곱을(R^2) 규제하는데, 이런 규제를 L2 규제라고 한다. 릿지 회귀에서는 alpha 매개 변수로 규제의 양을 조절하고, alpha가 커지면 규제도 커졌다. LogisticRegression에서 규제를 제어하는 매개변수는 C이며 기본값은 1이고, alpha와 달리 C가 작을수록 규제가 커진다. 따라서 규제를 완화하기 위해 C값을 20으로 늘렸다.

출력 결과 훈련 세트와 테스트 세트의 점수 모두 높고 과대적합이나 과소적합으로 치우 지지 않았다.

 

 

테스트 세트의 처음 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]]

 

이진 분류의 경우 2개의 열만 출력된 것과 달리 5개 샘플을 예측하였으므로 5개의 행, 7개 생선에 대한 확률을 예측하였으므로 7개의 열이 출력되었다.

첫 번째 샘플의 경우 세 번째 열의 확률이 0.841(84%)로 가장 높다.

 

 

세 번째 열이 어떤 생선인지 확인해 보자.

 

print(lr.classes_)
# 출력: ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']

 

첫 번째 샘플의 가장 높은 확률인 세 번째 열은 'Perch'이다.

 

 

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

 

선형 분류 알고리즘인 로지스틱 회귀를 위한 LogisticRegression 클래스의 decision_function() 메서드로 테스트 세트의 처음 5개 샘플에 대한 z1부터 z7까지의 값을 출력하였다.

 

 

마지막으로 z값을 확률로 변환해 보자.

 

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

 

소프트맥스 함수를 사용해 확률로 변환하였다.

이진 분류에서 시그모이드 함수를 사용해 z값을 0~1 사이로 변환한 것과 달리 다중 분류는 소프트맥스 함수를 사용한다.

소프트 맥스 함수는 여러 선형 방정식의 결과를 0~1 사이로 변환하고 전체 합이 1이 되도록 한다.

decision 배열을 softmax() 함수에 전달하였다.

softmax() 함수의 axis 매개변수로 소프트맥스를 계산할 축을 1로 지정하여 각 샘플에 대해 소프트맥스를 계산하였다. 만약 axis 매개변수를 지정하지 않으면 배열 전체를 계산한다.

출력 결과 proba 배열의 결과와 동일하다. 로지스틱 회귀로 성공적인 다중 분류를 수행하였다!

 

 

* 용어 정리

1, 다중 분류: 타깃 클래스가 2개 이상인 분류 문제

2. 로지스틱 회귀: 선형 방정식을 사용한 분류 알고리즘

3. LogisticRegression: 선형 분류 알고리즘인 로지스틱 회귀를 위한 클래스

4. 시그모이드 함수: 이진 분류에서 선형 방정식의 출력을 0~1 사이의 값으로 변환하는 함수

5. 소프트맥스 함수: 다중 분류에서 여러 선형 방정식의 결과를 0~1 사이로 변화하고 전체 합이 1이 되도록 하는 함수

6. predict_proba(): 예측 확률을 반환하는 함수

7. decision_function(): 모델이 학습한 선형 방정식의 출력을 반환하는 함수

 

 

728x90
LIST

댓글