2023. 8. 15. 08:07ㆍ2023-2 KHUDA/ML 기초세션
이 게시물은 한빛미디어의 <혼자 공부하는 머신러닝 + 딥러닝>를 정리한 글입니다.
혼자 공부하는 머신러닝+딥러닝
혼자 공부하는 머신러닝 딥러닝, 무료 동영상 강의, 머신러닝+딥러닝 용어집을 다운로드 하세요. 포기하지 마세요! 독학으로 충분히 하실 수 있습니다. ‘때론 혼자, 때론 같이’ 하며 힘이 되겠
hongong.hanbit.co.kr
머신러닝으로 럭키백의 생선이 어떤 타깃(총 7가지)에 속하는지의 확률을 구해보자
혼공머신은 "k-최근접 이웃은 주변 이웃을 찾아주니까 이웃의 클래스 비율을 확률이라고 출력하면 되지 않을까?"라고 생각한다.
데이터 준비
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv')
fish.head()

어떤 종류의 생선이 있는지 알기 위해 Species 열에서 값을 추출한다.
print(pd.unique(fish['Species']))

이 데이터프레임에서 Species 열을 타깃으로, 나머지 5개 열은 입력 데이터로 만든다.
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
fish_target = fish['Species'].to_numpy()
데이터를 훈련 세트와 테스트 세트로 나누고, StandardScaler 클래스를 사용해 표준화 전처리한다.
(훈련 세트의 통계 값으로 테스트 세트를 변환해야 한다는 점을 잊지 말자)
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)
k-최근접 이웃 분류기의 확률 예측
Chapter2에서 했던 것처럼 KNeighborsClassifier로 학습을 진행한다. 이때 최근접 이웃 개수인 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))
# -> 0.8907563025210085
print(kn.score(test_scaled, test_target))
# -> 0.85
클래스 확률을 배우는 것이 목적이기 때문에 점수에 대해서는 잠시 고려하지 않는다.
현재 타깃 데이터에는 7개의 생선 종류가 들어가 있다. 이렇게 2개 이상의 클래스가 포함된 문제를 다중 분류(multi-class classification)라고 부른다. 다중 분류에서는 타깃값을 숫자로 바꿀 수 있지만 사이킷런에서는 문자열로 된 타깃값을 그대로 사용할 수 있다.
※주의!※
타깃값을 문자열 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 매겨진다. 따라서 pd.unique(fish['Species'])로 출력했던 순서와는 다르다.
kn에서 정렬된 타깃값은 classes_속성에 저장되어 있다. 테스트 세트에 있는 처음 5개 샘플의 타깃값을 예측해본다.
print(kn.classes_)
print(kn.predict(test_scaled[:5]))

이 5개 샘플에 대한 예측은 어떻게 만들어졌을까? 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환한다. (round() 함수는 기본으로 소수점 첫째 자리에서 반올림을 한다. decimals 매개변수로 유지할 소수점 아래 자릿수를 지정할 수 있다. ex) decimals = 4이면 소수점 다섯 번째 자리에서 반올림해 네 번째 자리까지 표기한다.)
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))

predict_proba() 메서드의 출력 순서는 classes_ 속성과 같다.
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])

이 샘플의 이웃은 다섯 번째 클래스인 'Roach'가 1개이고 세 번째 클래스인 'Perch'가 2개이다. 따라서 세 번째 클래스에 대한 확률은 1/3이고 다섯 번째 클래스에 대한 확률은 2/3이다. 앞서 출력한 네 번째 샘플의 클래스 확률과 같다.
그런데 3개의 최근접 이웃을 사용하기 때문에 가능한 확률은 0/3, 1/3, 2/3, 3/3이 전부다. 확률이라 말하기 좀 어색하다는 문제가 발생한다.
로지스틱 회귀
로지스틱 회귀(logistic regression)는 이름은 회귀이지만 분류 모델이다. 이 알고리즘은 다음과 같이 선형 회귀와 동일하게 선형 방정식을 학습한다.
z = a * weight + b * length + c * diagonal + d * height + e * width + f
특성은 늘어났지만 chapter3에서 다룬 다중 회귀를 위한 선형 방정식과 같다. z는 어떠한 값도 가능하지만 확률이 되려면 0~1 사이 값이 되어야 한다. z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 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()
이진 분류를 수행할 경우 시그모이드 함수의 출력이 0.5보다 크면 양성 클래스, 0.5보다 작으면 음성 클래스로 판단한다. 이때 정확이 0.5가 된다면 라이브러리마다 다를 순 있지만 사이킷런은 음성 클래스를 반환한다.
로지스틱 회귀로 이진 분류 수행하기
넘파이 배열은 True, False 값을 전달하여 행을 선택할 수 있다. 이것을 불리언 인덱싱(boolean indexing)이라고 한다.
예를 들어 A에서 E까지 5개의 원소로 이루어진 배열이 있는데, A와 C만 골라내려면 그 원소에 해당하는 인덱스에 True를, 나머지에는 False를 전달하면 된다.
import numpy as np
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])
# -> ['A' 'C']
이 방식을 사용해서 훈련 세트에서 도미(bream)와 빙어(smelt)의 행만 골라낸다. 비교 연산자를 사용해 도미와 빙어의 행을 모두 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 클래스를 불러와 로지스틱 회귀 모델을 훈련한다.
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
print(lr.classes_)
print(lr.predict(train_bream_smelt[:5]))
print(lr.predict_proba(train_bream_smelt[:5]))

train_bream_smelt에서 처음 5개 샘플을 예측했다. 두 번째 샘플만 양성 클래스인 빙어이고, 나머지는 모두 도미로 예측하였다.
로지스틱 회귀가 학습한 계수와 train_bream_smelt의 처음 5개 샘플의 z값 그리고 이 z값을 시그모이드 함수에 통과시켜 확률을 순서대로 얻는다.
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)
# -> [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
from scipy.special import expit
print(expit(decisions))
# -> [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
출력된 값을 보면 predict_proba() 메서드 출력의 두 번째 열의 값과 동일하다. 즉 decision_function()메서드는 양성 클래스에 대한 z값을 반환한다.
로지스틱 회귀로 다중 분류 수행하기
다중 분류도 앞서 훈련한 이진 분류와 크게 다르지 않다. 여기서도 LogisticRegression 클래스를 사용해 이진 분류와 다중 분류의 차이점에 대해 알아보겠다.
LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다. max_iter 매개변수에서 반복 횟수를 지정하며, 기본값은 100이다. 여기에 준비한 데이터셋을 사용해 모델을 훈련하면 반복 횟수가 부족하다는 경고가 뜬다. 충분히 훈련시키기 위해 max_iter = 1000으로 설정한다.
또 LogisticRegression은 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제한다. L2 규제라고도 하는데, LogisticRegression에서 규제를 제어하는 매개변수는 C이다. 하지만 C는 릿지 회귀에서 사용한 alpha와 반대로 작을수록 규제가 커진다. C의 기본값은 1이다. 여기서는 규제를 조금 완화하기 위해 20으로 설정한다.
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
훈련 세트와 테스트 세트의 점수가 높고 과대적합이나 과소적합이 발생하지 않았다.
테스트 세트의 처음 5개 샘플에 대한 예측이다.
print(lr.predict(test_scaled[:5]))
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
print(lr.coef_.shape, lr.intercept_.shape)

이진 분류는 샘플마다 2개의 확률을 출력하고 다중 분류는 샘플마다 클래스 개수만큼 확률을 출력한다. 이 중에서 가장 높은 확률을 가진 클래스가 예측 클래스가 된다.
이 다중 분류에서는 5개의 특성을 사용하므로 coef_ 배열의 열은 5개이다. 또 intercept_가 7로, z를 7개나 계산한다.
다중 분류는 클래스마다 z값을 하나씩 계산한다. 당연히 갖아 높은 z값을 출력하는 클래스가 예측 클래스가 된다. 이진 분류에서는 시그모이드 함수를 사용해 z를 0과 1 사이의 값으로 변환했다. 다중 분류는 소프트맥스(softmax) 함수를 사용하여 7개의 z값을 확률로 변환한다.
여기서 소프트맥스 함수는 여러 개의 선형 방정식의 출력값을 0~1 사이로 압축하고 전체 합이 1이 되도록 만든다. 이를 위해 지수 함수를 사용하기 때문에 정규화된 지수 함수라고도 부른다.
계산 방식은 다음과 같다.


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

softmax()의 axis 매개변수는 소프트맥스를 계산할 축을 지정한다. 여기서는 1로 지정하여 각 행, 즉 각 샘플에 대해 소프트맥스를 계산한다. 만약 axis 매개변수를 지정하지 않으면 배열 전체에 대해 소프트맥스를 계산한다.
앞서 구한 proba 배열과 정확히 일치한다. -> 성공!!
'2023-2 KHUDA > ML 기초세션' 카테고리의 다른 글
| [혼자 공부하는 머신러닝 + 딥러닝] Chapter 05-1 결정 트리 (1) | 2023.08.22 |
|---|---|
| [혼자 공부하는 머신러닝 + 딥러닝] Chapter 04-2 확률적 경사 하강법 (0) | 2023.08.15 |
| [혼자 공부하는 머신러닝 + 딥러닝] Chapter 03-3 특성 공학과 규제 (0) | 2023.08.08 |
| [혼자 공부하는 머신러닝 + 딥러닝] Chapter 03-2 선형회귀 (0) | 2023.08.08 |
| [혼자 공부하는 머신러닝 + 딥러닝] Chapter 03-1 k-최근접 이웃 회귀 (0) | 2023.08.08 |