본문 바로가기

AI/기계학습(Machine Learning)

[기계학습/ML]8. 회귀 알고리즘(3) - 로지스틱 회귀

728x90
반응형

이제 Pupbani는 회귀 문제를 다룰 수 있고 특성값을 전처리하거나 특성을 조합해 새로운 특성을 만들 수 있게 되었다.

어느날 마케팅 팀에서 Pupbani에게 다음과 같은 요청을 했다.

  • "7개의 생선이 랜덤하게 들어 있는 럭키백 이벤트를 진행할 것인데 이 럭키백에 있는 생선들이 나올 확률을 구해주세요!"
  • "생선의 무게 ,길이, 높이, 두께, 대각선 길이 데이터도 같이 드릴게요!"

Pupbani는 새로운 과제에 대해 생각에 잠겼다.

갑자기 번뜩이는 아이디어가 떠올랐다.

K-최근접 이웃은 주변 이웃을 찾아주니까 이웃의 클래스 비율을 확률이라고 출력하면 되지 않을까?

  • 사각형이 나올 확률 30%
  • 원이 나올 확률 20%
  • 삼각형이 나올 확룰 50%
  • 사이킷런의 K-최근접 이웃 분류기로 하면 될 것 같다.

데이터 준비하기

import pandas as pd

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

  • pandas 라이브러리의 read_csv() 함수를 사용해서 생선 데이터 csv 파일을 불러왔다.
  • head() 메서드로 데이터프레임의 앞의 5개의 데이터를 살펴본다. 
  • 데이터 프레임에 어떤 생선이 있는지 확인해 본다.
print(pd.unique(fish['Species']))

  • fish에서 Species열을 추출하여 unique() 함수를 통해 중복값을 제거한 고유한 값만 추출한다.
  • 총 7개의 종류의 생선이 존재한다.
  • 이제 훈련 데이터와 타겟 데이터를 나눠보겠다,
    • 훈련 : Weight, Length, Diagonal, Height, Width
    • 타겟 : Species
  • 데이터프레임에서 여러 열을 선택하면 그 열로 이뤄진 새로운 데이터 프레임이 반환 된다.
  • sklearn에서 사용하기 위해 넘파이 배열로 변환(to_numpy())한다.
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
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)
  • 훈련 세트와 테스트 세트에 대해 표준화 처리를 한다.
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(f'훈련세트 정확도: {kn.score(train_scaled, train_target)}%')
print(f'테스트 세트 정확도: {kn.score(test_scaled, test_target)}%')

  • 정확도가 나쁘지만 확률을 배우는 목적이므로 점수는 잠시 잊도록 한다.
  • 이 모델의 클래스를 확인해보자.
    • classes_ 속성을 사용해 알 수 있다.
print(kn.classes_)

  • 타깃 데이터의 고유한 값들이 순서는 틀리지만 전부 들어 있다.
    • 타깃 값을 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 정렬되어 classes_ 속성에 저장된다.
  • 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류(Multi-Class Classification)라고 부른다.

다중분류(Multi-Class Classification)

  • 사이킷런의 predict() 메서드는 친절하게 타깃값으로 예측을 출력해 준다.
  • 테스트 세트의 상위 5개의 샘플 데이터의 타깃값을 예측해 보겠다.
print(f"예측 결과 : {kn.predict(test_scaled[:5])}")

  • 예측된 결과들은 어떻게 만들어질까?
  • 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률 값을 반환한다.
  • 소수점 자리가 너무 클 수 있으므로 numpy의 round() 함수를 사용한다.
    • decimals 매개변수로 유지할 소수점 아래 자리수를 정한다. 
import numpy as np

proba = kn.predict_proba(test_scaled[:5])*100
df = pd.DataFrame(np.round(proba,decimals = 2), columns=kn.classes_)
print(df)

  • 이 모델의 계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인해보자.
  • 네 번째 샘플의 최근접 이웃의 클래스를 확인한다.
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])

  • 이 샘플의 이웃은 'Roach'가 1개, 'Pearch'가 2개이다.
  • 클래스에 대한 확률을 계산한면
    • 'Roach'는 1/3 = 0.3333 = 33.33%
    • 'Pearch'는  2/3 = 0.6667 = 66.67%
  • 샘플의 클래스 확률값과 같다.

하지만 3개의 이웃을 사용하기 때문에 가능한 확률이 0/3, 1/3, 2/3, 3/3이 전부다.

다른 방법을 사용해야 할 것 같다.

 

로지스틱 회귀(Logistic Regression)

  • 이름은 회귀이지만 분류 모델이다.
  • 이 알고리즘은 선형 방정식을 학습한다.
  • 예를 들면 다음과 같다.

  • z는 어떤 값도 가능하지만 확률이 되려면 0~1(또는 0~100%)가 되야한다.
  • z가 아주 큰 음수일 때 0, z가 아주 큰 양수일 때 1이 되도록 바꾸는 방법을 찾아야한다.
  • 시그모이드 함수(Sigmoid Function 또는 로지스틱 함수; Logistic Function)를 사용하면 가능하다.선형 방정식의 출력 z의 음수를 사용해 자연 상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다.

시그모이드 함수를 이해하기 위해 파이썬을 통해 그려보자.

  • 조건들
    • z가 무한하게 큰 음수일 경우 함수는 0에 가까워 지고 무한하게 큰 양수일 경우 1에 가까워진다.
    • z가 0일때는 0.5가 된다.
    • ⏀는 절대로 0~1 사이의 범위를 벗어날 수 없다. -> 0~100%까지 확률로 해석할 수 있다.
  • Numpy의 arange()를 통해 -5와 5사이에 0.1 간격으로 배열 z를 만든다.
  • Numpy의 exp() 함수를 통해 지수함수를 계산한다.
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()

Logistic Regression 모델을 사용해보자 - 이진분류

  • Numpy 배열은 True, False 값을 전달하여 행을 선택할 수 있다.
    • 이를 불리언 인덱싱(Boolean Indexing)이라고 부른다.
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])

  • 불리언 인덱싱을 이용해 훈련 세트에서 도미와 빙어의 행만 골라낸다.
    • 도미와 빙어의 행을 모두 True로 만든다.
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]
  • 이렇게 데이터가 준비되었고 모델을 훈련해 보자.
  • LogisticRegression 클래스는 sklearn.linear_model 패키지 아래에 있다.
print("로지스틱 회귀 모델로 예측한 상위 5개의 생선 샘플 예측 결과\n")
prcnt = lr.predict_proba(train_bream_smelt[:5]).round(4) * 100
a = lr.predict(train_bream_smelt[:5]).reshape(5,-1)
con = np.column_stack((prcnt,a))
col = lr.classes_.tolist()+["예측 결과"]
df = pd.DataFrame(con, columns =col)
print(df)

  • prodict_proba() 함수가 반환한 배열값에 첫번째 열이 음성 클래스(0), 두번째 열이 양성 클래스(1)에 대한 확률이다.
    • Bream - 음성
    • Smelt - 양성
  • 예측 결과를 보면 두번째 샘플만 양성 클래스인 빙어의 확률이 높고 나머진 도미가 높다.
  • 학습한 로지스틱 회귀 모델의 계수들을 통해 z 값을 구해보자.
for i,j in zip(['a','b','c','d'],lr.coef_[0]):
    print(f"{i} = {j}")
print(f"f = {lr.intercept_[0]}")

로지스틱 회귀 모델이 학습한 방정식

  • LogisticRegression 클래스는 decision_function() 메서드로 z값을 쉽게 구할 수 있다.
decisions = lr.decision_function(train_bream_smelt[:5])
for idx, z in enumerate(decisions):
    print(f"{idx}번째 샘플의 z값 = {z}")

  • 이 z값을 시그모이드 함수에 통과 시키면 확률값을 얻을 수 있다.
  • 파이썬의 싸이파이(Scipy) 라이브러리에 시그모이드 함수가 있다.
    • expit() : 시그모이드 함수
from scipy.special import expit
for idx, z in enumerate(expit(decisions)):
    print(f"{idx}번째 샘플의 확률값 = {z*100:.2f}%")

 

Logistic Regression 모델을 사용해보자 - 다중분류

  • LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다.
    • max_iter 매개변수에서 반복 회수를 결정한다. (기본값 = 100)
  • 그리고 기본적으로 규제(L2 Norm)를 한다.
    • 릿지 처럼 계수의 제곱을 규제한다.
    • C 매개변수의 값에 따라 규제가 커지거나 작아진다. (기본값 = 1)  
  • 학습을 진행해보자.
lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)

print(f'훈련세트 정확도: {lr.score(train_scaled, train_target)*100:.2f}%')
print(f'테스트 세트 정확도: {lr.score(test_scaled, test_target)*100:.2f}%')

  • 과대적합, 과소적합 없이 적절히 잘나온 것 같다.
  • 처음 5개 샘플에 대한 예측과 예측확률을 출력해보자
print("로지스틱 회귀 모델로 예측한 상위 5개의 생선 샘플 예측 결과\n")
prcnt = lr.predict_proba(test_scaled[:5]).round(4) * 100
a = lr.predict(test_scaled[:5]).reshape(5,-1)
con = np.column_stack((prcnt,a))
col = lr.classes_.tolist()+["예측 결과"]
df = pd.DataFrame(con, columns =col)
print(df)

  • 선형 방정식 확인을 위해 coef_와 intercept_의 크기를 출력해봤다.
print(lr.coef_.shape, lr.intercept_.shape)

  • 이 데이터는 5개의 특성을 사용한다.
    • coef는 5개
    • intercept는 7개 - z를 7번 계산한다.
  • 다중 분류는 z값을 하나씩 계산한다.
  • 그렇다면 z로 확률은 어떻게 계산한 것일까?
  • 이진 분류에서는 시그모이드 함수를 사용했지만 다중 분류는 소프트맥스 함수(Softmax Function)을 사용하여 7개의 z 값을 확률 값으로 변환한다.
    • 7개의 z값이 있고 z1 ~ z7라고 이름을 가정
    • z1~z7을 사용해 e^z1 ~ e^z7을 계산하여 모두 더한 값을 e_sum이라고 한다.
    • e^z1 ~ e^z7을 각각 e_sum으로 나누어준 것들이 각각의 확률이다.
    • 이렇게 나온 확률들은 모두 더하면 1이 된다.

  • 먼저 decision_function() 으로 z1 ~ z7의 값을 구한다.
decision = lr.decision_function(test_scaled[:5])
print("5개 샘플에 대한 z1 ~ z7의 값\n")
for idx,z in enumerate(decision):
    print(f"{idx}번째 샘플의 z값\n{z}\n")

  • 그 다음은 Scipy 라이브러리의 소프트맥스 함수를 사용해 확률값을 구한다.
    • scipy.special 아래의 softmax() : axis는 계산할 축을 설정(0: 열, 1: 행)
from scipy.special import softmax
print("소프트맥스 함수로 예측 확률 구하기\n")
class_ = lr.classes_.tolist() + ["예측 결과"]
prd = lr.predict(test_scaled[:5]).reshape(5,-1)
sm = softmax(decision, axis=1).round(2) * 100
con = np.column_stack((sm,prd))
print(pd.DataFrame(con,columns=class_))

  • 앞에서 구한 proba 배열과 일치한다.

 

Pupbani는 이제 7개의 생선에 대한 확률을 예측하는 모델을 훈련했다.

마케팅 팀이 만족하겠네요.

728x90
반응형