머신러닝(AI학습 31)

이유진·2024년 7월 8일

--13.LogisticRegression.ipynb--

데이터 준비

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

base_path = r'/content/drive/MyDrive/dataset'

file_path = os.path.join(base_path, "fish.csv")
fish_df = pd.read_csv(file_path)
fish_df

fish_df.Species.unique()

Species 를 target으로 만들고

나머지 5개 컬럼(feature)를 입력 데이터로 사용하여 Species를 예측

입력 데이터 만들기

fish_input = fish_df[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
fish_input[:5]

target 데이터 만들기

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

fish_input.shape, fish_target.shape

train / test 세트 분리

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_input.shape, test_input.shape

전처리

from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input) # 반드시 train 세트의 통계값으로 test 세트를 변환해야한다.

KNN Classfier로 확률 예측

"""
사이킷런의 KNeighborsClassifier 클래스 객체를 만들고 train 세트로 모델을 훈련한 다음
train 세트와 test 세트의 점수를 확인해 보겠습니다.
"""
None

KNN의 '이웃개수 k'를 3으로 지정해서 사용

from sklearn.neighbors import KNeighborsClassifier

kn = KNeighborsClassifier(n_neighbors=3) # 이웃 개수
kn.fit(train_scaled, train_target)

kn.score(train_scaled, train_target)

kn.score(test_scaled, test_target)

다중 분류 (multi-class classification)

target 데이터에 2개 이상의 클래스가 포함된 분류 문제

※ vs. '이진분류 (binary classification)'

"""
기본적으로 다중분류 문제도 이진분류때와 '모델을 만들고' '훈련하는 방식' 은 동일합니다.

이진분류때에는 각 클래스를 1, 0 으로 지정하여 target 데이터를 만들었었습니다.

다중분류에서도 target 값을 숫자로 바꾸어 입력할수 있지만,
★사이킷런에서는 편리하게도 '문자열로 된 target 값'을 그대로 사용할수 있답니다.

이때 주의할 점 하나!
target 값을 문자열 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳순으로 매겨짐.

↓ 아래와 같이 확인해볼수 있다
"""
None

pd.unique(fish_df["Species"])

kn.classes_ # KNN에서 정렬된 target 값 확인

kn.predict(test_scaled[:5])

test_target[:5]

사이킷런의 분류모델에서 클래스별 확률 값 확인 가능

--> predict_proba()

test 세트의 첫 5개 샘플에 대한 확률값 확인

proba = kn.predict_proba(test_scaled[:5])
proba

proba.shape

print(kn.classes_)

print(np.round(proba, decimals = 4)) # 소수점 4자리 밑에서 반올림

"""
[0. 0. 0.6667 0. 0.3333 0. 0. ] <-- 네번째 샘플에 대한 확률
"""
None

test_scaled[3:4] # 네번째 샘플

네번째 샘플의 이웃들

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

"""
[['Roach' 'Perch' 'Perch']]

↑ 이 샘플의 이웃은 다섯번째 클래스인 'Roach' 가 1개. 세번째 클래스인 'Perch' 가 2개
따라서!
다섯번째 클래스일 확률은 1/3 = 0.3333
세번째 클래스일 확률은 2/3 = 0.6667

따라서 앞서 출력했던 아래 결과와 같다
[0. 0. 0.6667 0. 0.3333 0. 0. ]

"""
None

"""
딱 3개의 최근접 이웃?
0/3, 1/3, 2/3, 3/3 <-- 가능한 확률은 딱 4가지 밖에 없다??
이건 굉장히 불안정한것임.

KNN이 다중분류에서의 약점!!
"""
None

로지스틱 회귀(Rogistic Regression)

  • 이름은 회귀이지만 분류모델 이다
  • 선형회귀(Linear Regression)과 동일하게 선형 방정식을 학습

"""
z = a x (Weight) + b x (Length) + c x (Diagonal) + d x (Height) + e x (Width) + f

a, b, c, d, e는 가중치 혹은 계수
z 는 어떤 값도 가능. 하지만! 확률이 되려면 0.0 ~ 1.0(0% ~ 100%) 사이 값이 되어주어야 한다.

z 가 아주 큰 음수일때는 0 이 되고 (매핑?),  아주 큰 양수일때는 1 로 바꾸어 주는 방법이 필요하다

=> 바로 시그모이드 함수 (sigmoid function) 또는 로지스틱 함수 (logistic function) 을 사용하면 가능하다!

"""
None

시그모이드 함수 (sigmoid function)

선형방정식의 출력 z 의 음수를 사용해 자연상수 e 를 거듭제곱 하고 1을 더한 값의 역수

바로 위와 같은 그래프를 만들기 위한 함수다!

z 가 무한하게 큰 음수일 경우 이 함수는 0에 가까워지고

z 가 무한하게 큰 양수가 될때 이 함수는 1에 가까워진다

z 가 0 이 될때는 0.5 가 된다.

z 가 어떤 값이 되더라도 Φ(phi) 는 절대로 0 ~ 1 사이의 범위를 벗어날수 없습니다.

시그모이드 함수를 시각화 해보자

-5 ~ +5 사이에 0.1 간격으로 배열 z 생성후 시그모이드 함수 계산

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보다 크면 양성클래스, 0.5 보다 작으면 음성클래스로 판단.

도미와 빙어 2개를 사용하여 이진분류 수행

boolean indexing

bream_smelt_indexes = (train_target == 'Bream') | (train_target == 'Smelt')
bream_smelt_indexes

np.sum(bream_smelt_indexes)

train_bream_smelt = train_scaled[bream_smelt_indexes]
train_bream_smelt.shape

target_bream_smelt = train_target[bream_smelt_indexes]
target_bream_smelt.shape

from sklearn.linear_model import LogisticRegression

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

lr.predict(train_bream_smelt[:5])

target_bream_smelt[:5]

LogisticRegression가 뽑은 첫 5개 샘플의 예측 확률을 확인해보자

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

선형회귀에서처럼 로지스틱회귀가 학습한 계수를 확인해보자

print(lr.coef, lr.intercept)

"""
로지스틱회귀가 학습한 방정식은.
z = -0.404 x (Weight) - 0.576 x (Length)
- 0.663 x (Diagonal) - 1.013 x (Height) - 0.732 x (Width) - 2.161

이 모델로 z 값을 계산해보자! --> decision_function()
"""
None

첫 5개 샘플에 대한 z 값

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

"""
위 z 값을 시그모이드 함수에 통과시키면 '확률'을 얻을 수 있다.

scipy 라이브러리에 있는 expit() 시그모이드 함수 사용
"""
None

from scipy.special import expit

expit(decisions)

"""
↑ 출력된 값을 보면 아까의 predict_proba() 출력의 두번째 열의 값과 동일하다! (왜지?)
즉, decision_function() 메소드는 양성 클래스에 대한 z 값을 리턴하는 겁니다

[[0.99759855 0.00240145] <- 도미의 확률이 높다.
[0.02735183 0.97264817] <- 두번째 샘플만 빙어의 확률이 높다.
[0.99486072 0.00513928][0.98584202 0.01415798]
[0.99767269 0.00232731]]
"""
None

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

max_lter, L2 규제, C

"""
LogisticReression 은 기본적으로 '반복' 알고리즘 사용.
max_iter 매개변수에서 '최대 반복횟수' 지정. (default = 100)

LogisticRegression 은 기본적으로 릿지회귀와 같이 계수의 제곱을 규제합니다.
이런 규제를 'L2 규제' 라고 함
규제를 제어하는 매개변수는 C. (릿지회귀 에서는 alpha였다)

릿지회귀의 alpha와는 달리 C는 작을수록 규제의 강도가 커진다. C의 default는 1.
"""
None

lr = LogisticRegression(C=20, max_iter = 1000)
lr.fit(train_scaled, train_target) # 7가지 생선 데이터 사용

lr.score(train_scaled, train_target)

lr.score(test_scaled, test_target)

test 세트의 첫 5개 샘플 예측

lr.predict(test_scaled[:5])

test_target[:5]

위 5개 샘플에 대한 예측 확률

proba = lr.predict_proba(test_scaled[:5])

np.round(proba, decimals=3) # 소숫점 네번째 자리에서 반올림

"""
5개 샘플 x 7개 생선에 대한 예측확률

[0. , 0.014, 0.841, 0. , 0.136, 0.007, 0.003]

               ↑ 세번째 생선일 확률이 가장 높다!

"""
None

lr.classes_

학습된 선형 방정식 확인

lr.coef_

7 x 5 ???

z를 7개 계산. (7클래스 x 5개의 특성)

다중 분류는 생선의 class 별로 계산. 5개의 특성

lr.intercept_

소프트맥스 (softmax)

확률 계산은?

"""
그럼 확률은 어떻게 계산한 걸까요?
이진 분류에서는 시그모이드 함수를 사용해 z 를 0 과 1 사이의 값으로 변환했습니다.
다중 분류는 이와 달리 소프트맥스 (softmax) 함수를 사용하여 7개의 z 값을 확률로 변환합니다

소프트맥스 함수란?
시그모이드 함수는 하나의 선형방정식의 출력값을 0 ~ 1 사이로 압축합니다.
이와 달리 소프트맥스 함수는 여러개의 선형방정식의 출력값을 0 ~ 1 사이로 압축하고,
전체 합이 1이 되도록 만듭니다. 이를 위해 지수함수를 사용하기 때문에
'정규화된 지수함수' 라고도 부릅니다.

"""
None

"""
소프트맥스의 계산 방식도 어렵지 않습니다

7개의 z 값의 이름을 z1 ~ z7 이라고 하자.
z1 ~ z7 까지 값을 사용해 지수함수 e^z1 ~ e^z7 을 계산해 모두 더합니다
이를 e_sum 이라 하겠습니다

e_sum =  e^z1 + e^z2 + e^z3 + e^z4 + e^z5 + e^z6 + e^z7

그 다음 e^z1 ~ e^z7 을 각각 e_sum 으로 나누어 주면 됩니다

       e^z1           e^z2                  e^z7
s1 =  ──────  , s2 =  ──────  , ... , s7 =  ──────
       e_sum          e_sum                 e_sum


s1 에서 s7 까지 모두 더하면 1이 됩니다.  7개 생선에 대한 확률의 합은 1이 되는 겁니다. =

"""
None

z1 ~ z7의 값들

decision = lr.decision_function(test_scaled[:5])

np.round(decision, decimals = 2)

from scipy.special import softmax # scipy에서 소프트맥스 함수 제공

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

"""
↑ 이 출력결과와 앞서 구한 proba 배열과 비교해보자 => 정확히 일치!
"""
None

profile
독해지자

0개의 댓글