5 분 소요

❗ 문제 ❗

한빛 럭키백이라는 것을 만들건데 거기에 특정 물고기가 나올 확률을 구해라

💡 해결 방안 💡

한 생선을 기준으로 주변에 어떤 종류가 있는지를 구함 -> 이를 이용해서 확률을 구하자
k - 최근접 이웃 분류 사용!

1️⃣ 데이터 준비하기

  • 입력 데이터 : Weight, Length, Diagonal, Height, Width
  • 타깃 데이터 : Species
    그 이외의 많은 변수들이 나와서 변수 이름들을 정리하는 느낌으로 포스팅 할 예정. 변수는 🚦로 표현해서 정리하겠다.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

fish = pd.read_csv('https://bit.ly/fish_csv_data')
print(fish.head()) # 처음 5개 항목 출력
print(pd.unique(fish['Species'])) # Species에 해당하는 고유한 값 추출

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

train_input, test_input, train_target, test_target = train_test_split(fish_input, fish_target, random_state = 42)

# 훈련 세트와 테스트 세트를 표준화 전처리한다.
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
  • 🚦 fish : csv 파일을 읽어와서 데이터로 저장
  • 🚦 fish_input : Weight, Length, Diagonal, Height, Width 을 기준으로 해당하는 값들을 모아서 넘파이 배열로 전환
  • 🚦 fish_target : Species 의 값을을 모아서 넘파이 배열로 전환
  • 🚦 train_input, test_input : fish_input 데이터를 훈련 세트와 테스트 세트로 분리
  • 🚦 train_target, test_target : fish_target 데이터를 훈련 세트와 테스트 세트로 분리
  • 🚦 train_scaled : 표준화 전처리 과정을 거친 훈련 세트
  • 🚦 test_scaled : 표준화 전처리 과정을 거친 테스트 세트
  • ps. 타깃 데이터는 수정하지 않는다. (train_target, test_target 수정 X)

데이터프레임은 판다스에서 제공하는 2차원 표 형식의 주요 데이터 구조이다. 그래서 판다스를 임포트해준 것이다. 입력 데이터와 타깃 데이터는 새로운 데이터프레임이므로 이를 to_numpy() 를 이용하여서 넘파이 배열로 바꾼다.

  • 📍 unique() 메소드

특정 열에서의 고유한 값을 추출해준다.

2️⃣ 확률 예측하기(feat. 다중 분류)

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors = 3)
kn.fit(train_scaled, train_target)
print(kn.classes_)

>>> ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
  • classes_ 속성
    KNeighborsClassifier에서 정렬된 타깃값 출력을 위한 속성
  • 다중 분류 앞에서 도미와 빙어에 대한 이진 분류는 했었다. 근데 이때는 타깃 데이터과 1은 도미, 0은 빙어로 따로 결정을 해서 만들었었다. 하지만 다중 분류같은 경우는 이를 모두 숫자로 전환하여 타깃 데이터를 만들기가 불가능하고, 비효율적이다.



📍 다중 분류와 predict 메소드

predict 메소드를 사용하면 타깃값을 예측해서 알아서 출력해준다.

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

>>> ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']

test_scaled에 있는 값들 중에서 5개를 뽑아서 각각 예측한 데이터
test_scaled[0] 예측값 : Perch
test_scaled[1] 예측값 : Smelt
test_scaled[2] 예측값 : Pike
test_scaled[3] 예측값 : Perch
test_scaled[4] 예측값 : Perch

📍 predict_proba 메소드

클래스별 확률값을 반환한다.

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

['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish'] : 위 그래프의 0번째 행? 이라고 생각하면 predict 메소드 결과값을 유추할 수 있다.

하나의 샘플을 이용하여 샘플 주변의 3개 생선을 출력할 수도 있다.

distances, indexes = kn.kneighbors(test_scaled[3:4]) # 2차원 배열로 샘플을 추출
print(train_target[indexes])

>>> [['Roach' 'Perch' 'Perch']]
  • Kneighbors() : 이웃까지의 거리와 이웃 샘플의 인덱스를 반환하는 메소드

❓ 근데 왜 test_scaled의 4번째 샘플의 인덱스를 구했는데, 왜 train_target에서 해당 인덱스 값을 찾고 있지?
헐,,,,4번째 테스트 세트의 샘플 주변 훈련 세트의 샘플 찾기이기 때문에, train_target[indexes]로 주변 생선 종류를 출력한 것이다.

❗ 문제점 ❗
3개의 최근접 이웃을 사용했기 때문에 가능한 확률은 0/3, 1/3, 2/3, 3/3 뿐이다. (확률이 너무 적다.)

💡 해결책 💡
로지스틱 회귀 알고리즘 사용, 목적 : KNN을 대신해서 확률을 더 좋게 표현하기 위해서!

3️⃣ 로지스틱 회귀

로지스틱 회귀는 이름은 회귀지만 분류 모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다. 확률이 되려면 0~1의 값을 가져야 된다. 이를 위해서 시그모이드 함수(로지스틱 함수)를 사용한다.
❓ 이 함수의 값이 어떻게 확률이 된다는 거지?
지금 우리는 로지스틱 회귀를 이용하여서 나온 값을 다시 시그모이드 함수를 이용해서 확률로 바꿀 것이다. 즉 로지스틱의 회귀의 타깃값(z)를 다시 시그모이드 함수의 입력 부분에 넣어서 나온 타깃값을 사용한 것이다. 시그모이드 함수는 로지스틱 회귀 외에도 다른 모듈에서 사용을 해주는데 여기서는 로지스틱 회귀를 이용해서 함수를 그렸다.

  • 사이킷런
    • 모델 클래스
      • 분류 모델
        • KNeighborsClassifier
        • KNeighborsRegressor
        • LogisticRegression
      • 회귀 모델
        • LinearRegression
    • 변환기 클래스
      • PolynomialFeatures
      • StandardScaler

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

넘파이 배열은 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)
print(lr.predict(train_bream_smelt[:5]))

>>> ['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
  • 🚦 bream_smelt_indexes : train_target이 Bream이거나 Smelt이면 True, 아니면 False를 리턴.

    print(bream_smelt_indexes)
    
    >>>
    [ True False  True False False False False  True False False False  True False False False  True  True False False  True False  True False False False  True False False  True False False False False  True False False
    True  True False False False False False  True False False False False False  True False  True False False  True False False False  True False False False False False False  True False  True False False False False False False False False False  True False  True False False  True  True False False False  True False False False False False  True False False False  True False  True False False  True  True False False False False False False False False  True  True False False  True False False]
    
  • 🚦 train_bream_smelt : train_scaled에서 인덱스가 True인 값만 뽑아서 배열 형태로 전환
  • 🚦 target_bream_smelt : train_target에서 인덱스가 True인 값만 뽑아서 배열 형태로 전환

로지스틱 회귀 학습를 위한 데이터들을 모아주었으면(도미와 빙어인 값들만 불리언 인덱싱을 위해서 모아주었으면) 로지스틱 회귀로 이진 분류를 수행해보자! 위 과정은 다양한 생선들 중에 도미와 빙어를 추리기 위한 과정이다.

print(lr.coef_, lr.intercept_) # [[-0.4037798  -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions) # [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]

from scipy.special import expit
print(expit(decisions))

>>>
[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
  • decision_function을 통해서 로지스틱 회귀 모델이 학습한 방정식의 결과값을 출력해줍니다.

z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다. 사이파이 라이브러리에 시그모이드 함수인 expit()을 이용하여 확률을 구해준다.

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

LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용하여 max_iter 을 통해서 반복 횟수를 지정한다. 그리고 릿지 회귀에서 규제를 제어하기 위한 매개변수 alpha처럼 로지스틱 회귀에서는 C를 통해서 규제를 제어한다. 하지만 alpha와 반대로 작을수록 규제가 커진다.

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

과대 적합이나 과소적합으로 치우친 것 같진 않음!

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

>>> ['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']

로지스틱 회귀를 위한 다중 분류는 이중 분류를 반복적으로 계산해서 각 계산값을 포함하고 있다. 이중 분류에서는 decision_funtion을 통해서 0~1 사이의 값으로 변환했지만 다중 분류는 소프트맥스 함수를 사용하여 7개의 z값을 확률로 변환한다. 소프트맥스 함수는 시그모이드 함수와는 다르지만 결국 0부터 1까지의 값을 확률로 본다라는 점에서 유사하다.

from scipy.special import softmax
decision = lr.decision_function(test_scaled[:5])
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]]

axis = 1을 통해서 샘플들의 출현 확률을 구한다.

댓글남기기