어떤 타깃에 속하는지에 대한 확률을 구하는 방법?
확률은 회귀 문제? 분류?
샘플 X 주위에 가장 가까운 이웃 샘플 10개를 표시.
사각형🟦이 3개, 삼각형🔺이 5개, 원🟢이 2개.
이웃한 샘플의 클래스를 확률로 삼는다면 샘플 X가 사각형🟦일 확률 30%, 삼각형🔺일 확률 50%, 원🟢일 확률 20%
사이킷런의 k-최근접 이웃 분류기도 이와 동일한 방식으로 클래스 확률을 계산하여 제공!
# 데이터 준비하기
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv')
fish.head()
# Species 열에서 고유한 값 추출하기
print(pd.unique(fish['Species']))
# ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
# 데이터프레임에서 Species 열을 타깃으로 설정
# 나머지 5개 열은 입력 데이터로 사용
# to_numpy()로 넘파이 배열로 바꾸어 fish_input에 저장
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
)
# StandardScaler 클래스를 사용하여 훈련 세트와 테스트 세트를 표준화 전처리
# 훈련 세트의 통계 값으로 테스트 세트를 변환해야함
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
# k-최근접 이웃 분류기의 확률 예측
from sklearn.neighbors import KNeighborsClassifier
# 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
# 다중 분류 multi-class classification
# 타깃값을 사이킷런 모델에 전달하면 알파벳순으로 정렬됨
print(kn.classes_)
# 테스트 세트에 있는 처음 5개 샘플의 타깃값 예측
print(kn.predict(test_scaled[:5]))
['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
# predict_proba() : 클래스별 확률값을 반환
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
# 테스트 세트에 있는 처음 5개의 샘플에 대한 확률 출력
print(np.round(proba, decimals=4))
# predict_proba()의 출력 순서는 classes_ 속성과 동일
# ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
# 4번째 샘플
# 모델이 계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인
# 4번째 샘플의 최근접 이웃의 클래스 확인
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
# [['Roach' 'Perch' 'Perch']]
# 3개의 최근접 이웃을 사용하기 때문에 가능한 확률은 0/3, 1/3, 2/3, 3/3이 전부
💡 k-최근접 이웃 분류기가 표시한 확률은 확률이라고 말하기 어색..
확률은 0부터 1까지의 모든 실수들이 나올 수 있어야 하는데 k-최근접 이웃 분류기의 경우 특정 실수만 나오기 때문이다!
다시 말해, k-최근접 이웃 모델이 확률은 출력할 수 있지만 이웃한 샘플의 클래스 비율이므로 항상 정해진 확률만 출력한다는 뜻!
Logistic Regression은 이름은 회귀이지만 분류 모델로, 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다.
z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 1이 되도록?
시그모이드 함수는 오른쪽과 같은 그래프를 만들기 위해서, 선형 방정식의 출력 z의 음수를 사용하여 자연 상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다.
# 로지스틱 회귀 모델을 훈련
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
# train_bream_smelt에 있는 처음 5개 샘플 예측
print(lr.predict(train_bream_smelt[:5]))
# ['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
# predict_proba()를 이용하여 예측 확률 확인
print(lr.predict_proba(train_bream_smelt[:5]))
# [[음성 클래스(0)에 대한 확률, 양성 클래스(1)에 대한 확률]]
# 클래스 확인
print(lr.classes_)
# ['Bream' 'Smelt']
# 음성 클래스 : Bream
# 양성 클래스 : Smelt
# LogisticRegression 모델로 z값 계산하기
# decision_function()
decisions = lr.decision_function(train_bream_smelt[:5])
# 처음 5개 샘플의 z값 출력
print(decisions)
[-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
# z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있음
# 파이썬의 사이파이 라이브러리의 시그모이드 함수
from scipy.special import expit
print(expit(decisions))
decision_function()는 양성 클래스에 대한 z값을 반환한다!!!
이진 분류일 경우, predict_proba()는 음성 클래스와 양성 클래스에 대한 확률을 출력
decision_function()는 양성 클래스에 대한 z값을 계산
coef_ 속성과 intercept_ 속성에는 로지스틱 모델이 학습한 선형 방정식의 계수 표시
# 로지스틱 회귀로 다중 분류 수행
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))
로지스틱 회귀 모델과 이진 분류의 차이점
다중 분류는 클래스마다 z값을 하나씩 계산!
가장 높은 z값을 출력하는 클래스가 예측 클래스가 되며, 각각의 z값을 확률로 변환하는 함수.
# 소프트맥스 함수를 사용
# 테스트 세트의 처음 5개 샘플에 대한 z1~z7의 값 구하기
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]]
# 사이파이가 제공하는 소프트맥스 함수
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]]
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]]
💡시그모이드 함수는 하나의 선형 방정식의 출력값을 0~1 사이로 압축!
소프트맥스 함수는 여러 개의 선형 방정식의 출력값을 0~1 사이로 압축하고 전체 합이 1이 되도록!
이전에 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련하는 점직전 학습을 진행하는 알고리즘 중 대표적인 알고리즘.
경사를 따라 내려가는 방법.
가장 가파른 경사를 따라 원하는 지점에 도달하는 것이 목표.
가장 가파른 길을 찾아 내려오지만 조금씩 내려와야함.
훈련 세트를 사용하여 모델을 훈련하기에 경사 하강법도 훈련 세트를 사용하여 가장 가파른 길을 찾음.
전체 샘플을 사용하지 않고 딱 하나의 샘플을 훈련 세트에서 랜덤하게 골라 가장 가파른 길을 찾는다.
훈련 세트에서 랜덤하게 하나의 샘플을 고르는 것!
확률적 경사 하강법은 훈련 세트에서 랜덤하게 하나의 샘플을 선택하여 가파른 경사를 조금 내려간다. 그 다음 훈련 세트에서 랜덤하게 또 다른 샘플을 하나 선택하여 경사를 조금 내려간다. 이와 같은 방식으로 전체 샘플을 모두 사용할 때까지 계속 진행한다.
확률적 경사 하강법에서 훈련 세트를 한 번 모두 사용하는 과정
여러 개의 샘플을 사용해 경사 하강법을 수행하는 방식
한 번 경사로를 따라 이동하기 위해 전체 샘플을 사용하는 방식
어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준.
어떤 값이 최솟값인지 알지는 못하지만 손실 함수의 값이 작을수록 좋음.
손실함수를 이용하고 경사 하강법을 이용할 때에는 연속적이어야 한다!
이진 분류의 경우, 타깃이 0일 때의 손실을 음성 클래스일 때의 손실이라고 하며 타깃이 1일 때의 손실을 양성 클래스일 때의 손실이라고 한다.
위의 그림처럼 예측 확률이 0에서 멀어질수록 손실은 아주 큰 양수가 된다.
다중 분류에서 사용하는 손실 함수.
사이킷런에서 확률적 경사 하강법을 제공하는 대표적인 분류용 클래스. sklearn.linear_model 패키지 아래에서 import할 수 있다.
# 확률적 경사 하강법
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss='log_loss', max_iter=10, random_state=42)
sc.fit(train_scaled, train_target)
# 훈련 세트 정확도 점수
print(sc.score(train_scaled, train_target))
# 0.773109243697479
# 테스트 세트 정확도 점수
print(sc.score(test_scaled, test_target))
# 0.775
SGDClassifier의 객체를 만들 때 2개의 매개변수를 지정.
loss는 손실 함수의 종류를 지정하고 max_iter는 수행할 에포크 횟수를 지정.
~~ConvergenceWarning
사이킷런이 모델이 충분히 수렴하지 않았다는 내용으로 보내는 경고. 이 경고를 보면 max_iter의 값을 늘려주면 된다.
~~
이미 훈련한 모델을 이어서 조금씩 훈련할 때 사용하는 partial_fit()
# 기존의 모델으 추가로 더 훈련
# 점진적 학습
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
# 0.8151260504201681
print(sc.score(test_scaled, test_target))
# 0.85
에포크를 한 번 더 실행하여 정확도를 향상시켰다. 모델을 여러 에포크에서 더 훈련할 필요가 있지만, 훈련을 몇 번 더 진행하는 것이 좋을지에 대한 기준은 무엇일까?
적은 에포크 횟수 동안에 훈련한 모델은 훈련 세트와 테스트 세트에 잘 맞지 않는 과소적합된 모델일 가능성이 높고, 많은 에포크 횟수 동안에 훈련한 모델은 훈련 세트에 너무 잘 맞아 테스트 세트에는 오히려 점수가 나쁜 과대적합된 모델일 가능성이 높다.
에포크가 진행됨에 따라 모델의 정확도를 나타낸 사진.
훈련 세트 점수는 에포크가 진행될수록 꾸준히 증가하지만 테스트 세트 점수는 어느 순간 감소하기 시작한다.
과대적합이 시작하기 전에 훈련을 멈추는 것
import numpy as np
sc = SGDClassifier(loss='log_loss', random_state=42)
# 훈련 세트에 대한 점수 기록용 리스트
train_score = []
# 테스트 세트에 대한 점수 기록용 리스트
test_score = []
# train_target에 있는 7개 생선의 목록 생성
classes = np.unique(train_target)
print(classes)
# array(['Bream', 'Parkki', 'Perch', 'Pike', 'Roach', 'Smelt', 'Whitefish'],
# dtype=object)
# 300번의 에포크 훈련 반복 진행
# 반복마다 훈련 세트와 테스트 세트의 점수 계산 후 리스트에 추가
for _ in range(0, 300):
sc.partial_fit(train_scaled, train_target, classes=classes)
train_score.append(sc.score(train_scaled, train_target))
test_score.append(sc.score(test_scaled, test_target))
# 훈련 세트와 테스트 세트의 점수를 그래프로 그리기
# 적절한 반복 횟수 확인
import matplotlib.pyplot as plt
plt.plot(train_score)
plt.plot(test_score)
plt.show()
100번째 에포크 이후에는 훈련 세트와 테스트 세트의 점수가 조금씩 벌어진다. 확실히 에포크 초기에는 과소적합되어 훈련 세트와 테스트 세트의 점수가 낮다. 즉, 이 모델의 경우 100번째 에포크가 적절한 에포크가 적절한 반복 횟수!
# 반복 횟수를 100에 맞추고 모델 다시 훈련
sc = SGDClassifier(loss='log_loss', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)
# 최종 훈련/테스트 세트에서 점수를 출력
print(sc.score(train_scaled, train_target))
# 0.957983193277311
print(sc.score(test_scaled, test_target))
# 0.925
💡 SGDClassifier는 일정 에포크 동안 성능이 향상되지 않으면 더 훈련하지 않고 자동으로 멈춘다. tol에서 향상될 최솟값을 지정한다. tol=None으로 지정하여 자동으로 멈추지 않고 max_iter=100만큼 무조건 반복됨!
loss 매개변수의 기본값은 hinge!
힌지 손실(hinge loss)은 서포트 벡터 머신(support vector machine)이라고 불리는 다른 머신러닝 알고리즘을 위한 손실 함수이다.