[혼공머신] 4장. 다양한 분류 알고리즘
1. 로지스틱 회귀
이름은 회귀지만 분류 모델이다.
가중치와 계수들을 곱한 z의 값이 아주 큰 양수이면 1, 아주 큰 음수이면 0으로 바꿀 수 있도록 하는 함수는 시그모이드 함수(=로지스틱 함수)이다.
z는 0~1사이의 범위를 벗어날 수 없어서 0~1사이의 값을 0~100%까지 확률로 해석할 수 있다.
z위치마다 시그모이드 함수를 계산하며 지수 함수 계산을 np.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()
(1) 로지스틱 회귀로 이진 분류 수행하기
넘파이 배열은 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]
bream_smelt_indexes 배열은 도미와 빙어일 경우 True이고 그 외는 모두 False 값이 들어간다.
해당 데이터로 로지스틱 회귀 모델을 훈련해 보겠다. LogisticRegression 클래스는 선형모델이다.
훈련한 모델을 사용해 train_bream_smelt에 있는 처음 5개 샘플의 예측 확률을 출력하면 다음과 같다.
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
print(lr.predict_proba(train_bream_smelt[:5]))
Smelt가 양성 클래스이며 나머지는 Bream으로 예측한다.
print(lr.classes_)
로지스틱 회귀로 성공적인 이진 분류를 수행하였으며 로지스틱 회귀가 학습한 계수는 다음과 같다.
print(lr.coef_, lr.intercept_)
decision_function()메서드를 활용하여 z값을 출력할 수 있다. 처음 5개의 샘플의 z값을 출력하였다.
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
해당 z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다. expit()함수를 사용한다.
from scipy.special import expit
print(expit(decisions))
정리를 해보자면, 이진 분류일 경우 predict_proba() 메서드는 음성 클래스와 양성 클래스에 대한 확률을 출력한다. decision_function() 메서드는 양성 클래스에 대한 z 값을 계산한다. 또한, coef_ 속성과 intercept_ 속성에는 로지스틱 모델이 학습한 선형 방정식의 계수가 들어간다.
(2) 로지스틱 회귀로 다중 분류 수행하기
LogisticRegression 클래스를 사용해 7개의 생선을 분류해보자
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))
5개의 샘플에 대한 얘측 확류를 출력해 보면 다음과 같다.
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
데이터는 5개의 특성을 사용하며 coef_ 배열의 열은 5개이다. 행이 7개인 이유는 다중 분류는 클래스마다 z값을 하나씩 계산하기 때문이다. 다중 분류는 소프트맥스 함수를 사용하여 7개의 z값을 확률로 변환하여준다.
print(lr.coef_.shape, lr.intercept_.shape)
decision_fuction() 메서드로 z1~z7까지의 값을 구한 다음 소프트맥스 함수를 사용해 확률로 바꾸어 보았다. z1~z7의 값은 다음과 같다.
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))
softmax() 함수를 사용하여 확률을 확인하면 다음과 같다.
from scipy.special import softmax
proba = softmax(decision, axis=1)
print(np.round(proba, decimals=3))
2. 확률적 경사 하강법
(1) 점진적인 학습
< 확률적 경사 하강법 >
확률적 경사 하강법 : 훈련 세트에서 랜덤하게 하나의 샘플을 고르는 것
에포크 : 확률적 경사 하강법에서 훈련 세트를 한 번 모두 사용하는 과정
미니배치 경사 하강법 : 여러 개의 샘플을 사용해 경사 하강법을 수행하는 방식
배치 경사 하강법 : 극단적으로 한 번 경사로를 따라 이동하기 위해 전체 샘플을 사용할 수 있음
< 손실 함수 >
손실 함수 : 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준
< 로지스틱 손실 함수 >
로지스틱 손실 함수 : 로지스틱 회귀 모델이 만들어지면서 이진 크로스엔트로피 손실함수라고도 불린다.
크로스엔트로피 손실 함수 : 다중 분류에서 사용하는 손실 함수
(2) SGDClassifier
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
#Species 열을 제외한 나머지 5개는 입력데이터로 사용, Species 열은 타깃 데이터
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)
#SGDClassifier의 객체를 만들 때 2개의 매개변수를 지정. max_iter를 10으로 지정하여 전체 훈련 세트를 10번 반복
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))
print(sc.score(test_scaled, test_target))
(3) 에포크와 과대/과소 적합
적은 에포크 횟수 동안에 훈련한 모델은 훈련 세트와 테스트 세트에 잘 맞지 않는 과소적합된 모델이 될 가능성이 높다. 반대로 많은 에포크 횟수 동안에 훈련한 모델은 훈련 세트에 너무 잘 맞아 테스트 세트에는 오히려 점수가 나쁜 과대적합된 모델이 가능성이 높다.
partial_fit() 메서드만 사용하려면 훈련 세트에 있는 전체 클래스의 레이블을 partial_fit() 메서드에 전달해주어야한다. 이를 위해 np.unique() 함수로 train_target에 있는 7개 생선의 목록을 만든다. 또 에포크마다 훈련 세트와 테스트 세트에 대한 점수를 기록하기 위해 2개의 리스트를 준비한다.
import numpy as np
sc = SGDClassifier(loss='log_loss', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_target)
#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))
#300번의 에포크 동안 기록한 훈련 세트와 테스트 세트의 점수를 그래프로 그림
import matplotlib.pyplot as plt
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()
그림에서 백번째 에포크 이후에는 훈련 세트와 테스트 세트의 점수가 조금씩 벌어지고 있다. 확실히 에포크 초기에는 과소적합되어 훈련세트와 테스트 세트의 점수가 낮다. 이 모델의 경우 백 번째 에포크가 적절한 반복 횟수로 보여 반복 회수를 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))
print(sc.score(test_scaled, test_target))
최종 점수가 좋게 나오면서 훈련 세트와 테스트 세트에서의 정확도 점수가 비교적 높게 나옴을 확인할 수 있다. 확률적 경사 하강법을 사용한 생선 분류 문제도 성공적으로 수행하였다.