본문 바로가기

데이터/머신러닝

[혼공] ch 4. 다양한 분류 알고리즘

1. 로지스틱 회귀 

 

1) 럭키백의 확률 

 

- 럭키백에 들어갈 수 있는 생선은 7개

- 럭키백에 들어간 생선의 크기, 무게 등이 주어졌을 때 7개 생선에 대한 확률을 출력 

 

- K-최근점 이웃은 주변 이웃을 찾아주니까 이웃의 클래스 비율을 확률이라고 출력 

 

 

- X 주위에 가장 가까운 이웃 샘플 10개를 표시

- 사각형이 3개, 삼각형이 5개, 원이 2개

- 이웃한 샘플의 클래스를 확률로 삼는다면 샘플 X가 사각형일 확률은 30%, 삼각형일 확률은 50%, 원일 확률은 20%이다. 

 

데이터 준비 

 

import pandas as pd

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

 

 

- 어떤 종류의 생선이 있는지 Species 열에서 고유한 값 추출 -> 판다스의 unique() 함수를 사용 

 

print(pd.unique(fish['Species']))

 

 

- Species 열을 타깃으로 만들고 나머지 5개의 열은 입력 데이터로 사용 

- 데이터프레임에서 원하는 열을 리스트로 나열하여 열을 선택 

- 데이터프레임에서 여러 열을 선택하면 새로운 데이터프레임이 반환됨, 이를 to_numpy() 메서드로 넘파이 배열로 바꾸어 fish_input에 저장 

 

fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
print(fish_input[:5])

 

- 데이터를 훈련 세트와 테스트 세트로 나누기 

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- 최근접 이웃 분류기의 확률 예측

 

- 사이킷런의 KNeighborsClassifier 클래스 객체를 만들고 훈련 세트로 모델을 훈련한 다음 훈련 세트와 테스트 세트의 점수를 확인 

 

from sklearn.neighbors import KNeighborsClassifier

kn = KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled, train_target)

print(kn.score(train_scaled, train_target))
print(kn.score(test_scaled, test_target))

 

 

- 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류(multi-class classification)이라고 함

- 이진 분류를 사용했을 때는 양성 클래스와 음성 클래스를 각각 1과 0으로 지정하여 타깃데이터를 만든다. 

- 다중 분류에서도 타깃값을 숫자로 바꾸어 입력할 수 있지만 사이킷런에서는 편리하게도 문자열로 된 타깃값을 그대로 사용할 수 있다. 

 

- KNeighborsClassifier에서 정렬된 타깃값은 classes_속성에 저장되어 있다. 

print(kn.classes_)

 

- 테스트 세트에 있는 처음 5개 샘플의 타깃값을 예측 

 

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

 

 

- 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환

 

import numpy as np

proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))

 

 

- 이 모델이 계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인 

- 네 번째 샘플의 최근접 이웃의 클래스를 확인 

 

distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])

 

 

2) 로지스틱 회귀 

 

- 이름은 회귀이지만 분류 모델

- 선형 회귀와 동일하게 선형 방정식을 학습 

 

z = a *(Weight) + b*(Length) + c*(Diagonal) + d*(Height) + e*(Width) +f

 

- a,b,c,d,e는 가중치 혹은 계수

- z는 어떤 값도 가능하지만 확률이 되려면 0~1 사이의 값이 되어야 한다. 

- 시그모이드함수(sigmoid function)를 사용하여 z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 1이 되도록 바꾼다. 

- z가 0이 될때는 0.5가 된다. 

- 선형 방정식의 출력 z의 음수를 사용해 자연 상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다. 

 

 

 

- -5~5 사이에 0.1 간격으로 배열 z를 만든 다음 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()

 

 

 

 

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

 

- 넘파이 배열은 True, False 값을 전달하여 행을 선택할 수 있다. 이를 불리언 인덱싱(boolean indexing)이라고 한다.

- 훈련 세트에서 도미(Bream)와 빙어(Smelt)의 행만 골라낸다. 

 

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 값이 들어있음

- 이 배열을 사용해 train_scaled와 train_target 배열에 불리언 인덱싱을 적욯하면 손쉽게 도미와 빙어 데이터만 골라낼 수 있다. 

 

- LogisticRegression 클래스로 로지스틱 회귀 모델 훈련하고 train_bream_smelt에 있는 처음 5개 샘플을 예측 

 

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
print(lr.predict(train_bream_smelt[:5]))

 

 

- train_bream_smelt에서 처음 5개 샘플의 예측 확률 출력 

print(lr.predict_proba(train_bream_smelt[:5]))

 

 

- 첫 번째 열이 음성 클래스(0)에 대한 확률이고 두 번째 열이 양성 클래스(1)에 대한 확률이다. 

 

print(lr.classes_)

 

- 빙어(Smelt)가 양성 클래스 

 

- 로지스틱 회귀가 학습한 계수 확인

- coef_ 속성과 intercept_ 속성에는 로지스틱 모델이 학습한 선형 방정식의 계수가 들어있다. 

print(lr.coef_, lr.intercept_)

 

 

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

z = -0.404 * (Weight) - 0.576*(Length) - 0.663*(Diagonal) - 1.013*(Height) - 0.732*(Width) - 2.161

 

- LogisticRegression 클래스는 decision_function() 메서드로 z값을 출력할 수 있다. 

 

decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)

 

 

- z 값을 시그모이드 함수에 통과시키면 확률을 알 수 있다. 

- 파이썬의 사이파이(scipy) 라이브러리의 시그모이드 함수(expit())를 사용

 

from scipy.special import expit

print(expit(decisions))

 

 

- decision_function() 메서드는 양성 클래스에 대한 z값을 반환한다. 

 

 

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

 

 

- LogisticRegression 클래스를 사용해 7개의 생선을 분류

- LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용 

- max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100

- LogisticRegression은 릿지 회귀와 같이 계수의 제곱을 규제 -> L2규제 

- 규제를 제어하는 매개변수는 C이고 작을수록 규제가 커진다. 

 

- LogisticRegression 클래스로 다중 분류 모델을 훈련하고 처음 5개 샘플에 대한 에측 출력

 

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

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

 

 

- 테스트 세트의 처음 5개 샘플에 대한 예측 확률을 출력

 

proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))

 

 

- 5개 샘플에 대한 예측이므로 5개의 행이 출력되고, 7개 생선에 대한 확률을 계산했으므로 7개의 열이 출력

 

- classes_ 속성에서 클래스 정보를 확인 

 

print(lr.classes_)

 

 

- 이진 분류는 샘플마다 2개의 확률을 출력하고 다중 분류는 샘플마다 클래스 개수만큼 확률을 출력

 

- coef_와 intercept_ 의 크기 출력

 

print(lr.coef_.shape, lr.intercept_.shape)

 

- 다중 분류는 클래스마다 z값을 하나씩 계산

- 가장 높은 z값을 출력하는 클래스가 예측 클래스가 된다 

- 다중 분류는 소프트맥스(softmax)함수를 사용하여 7개의 z값을 확률로 변환 

 

소프트맥스 함수

- 여러 개의 선형 방정식의 출력값을 0~1 사이로 압축하고 전체 합이 1이 되도록 만든다. 

 

- decision_function() 메서드로 z1 ~ z7까지의 값을 구한 다음 소프트맥스 함수를 사용해 확률로 변환 

decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))

 

 

- scipy.special 아래에 softmax() 함수를 임포트해서 사용 

- 앞서 구한 decision 배열을 softmax() 함수에 전달

- softmax()의 axis 매개변수는 소프트맥스를 계산할 축을 지정

- axis=1로 지정하여 각 행, 즉 각 샘플에 대해 소프트맥스를 계산

- axis 매개변수를 지정하지 않으면 배열 전체에 대해 소프트맥스를 계산

 

from scipy.special import softmax

proba = softmax(decision, axis=1)
print(np.round(proba, decimals=3))

 

 

 

2. 확률적 경사 하강법

 

1) 점진적인 학습 

 

- 이전에 훈련한 모델을 버리고 다시 새로운 모델을 학습하는 방식

- 앞서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련 

 

확률적 경사 하강법(Stochastic Gradient Descent)

 

- 훈련세트에서 랜덤하게 하나의 샘플을 구하는 것

- 훈련세트에서 랜덤하게 하나의 샘플을 선택하여 가파른 경사를 조금씩 내려간다. 그 다음 훈련 세트에서 랜덤하게 또 다른 샘플을 하나 선택하여 경사를 조금 내려간다. 이런식으로 전체 샘플을 모두 사용할 때까지 계속한다. 

 

에포크(epoch)

- 확률적 경사 하강법에서 훈련 세트를 한 번 모두 사용하는 과정 

 

 

미니배치 경사 하강법(minibatch gradient descent)

- 여러 개의 샘플을 사용해 경사 하강법을 수행하는 방식

 

배치 경사 하강법(batch gradient descent)

- 경사로를 따라 이동하기 위해 전체 샘플을 사용 

 

 

 

손실 함수(loss function)

- 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준

 

로지스틱 손실 함수 

 

- 양성 클래스(타깃 = 1)일 때 손실은 -log(예측 확률)로 계산

- 확률이 1에서 멀어져 0에 가까워질수록 손실은 아주 큰 양수가 된다. 

- 음성 클래스(타깃 = 0)일 때 손실은 -log(1-예측 확률)로 계산

 

 

이진 분류 -> binaray cross-entropy loss function

다중 분류 ->cross-entropy loss function 

 

 

2) SGDClassifier

 

 

- fish_csv_data 파일에서 판다스 데이터프레임 만들기 

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

 

- train_test_split() 함수를 사용해 이 데이터를 훈련 세트와 테스트 세트로 나눔 

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이다. 

- SGDClassifier의 객체를 만들 때 2개의 매개변수를 지정

- loss손실 함수의 종류를 지정

- loss의 기본값은 'hinge'

  -> 힌지 손실(hinge loss)는 서포트 벡터 머신(support vector machine)을 위한 손실 함수 

- loss = 'log_loss'로 지정하여 로지스틱 손실 함수를 지정 

- 훈련 세트와 테스트 세트에서 정확도 점수를 출력 

 

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

 

 

- 출력된 훈련 세트와 테스트 세트 정확도가 낮다.

 

- SGDClassfier 객체를 다시 만들지 않고 훈련한 모델 sc를 추가로 더 훈련

- 모델을 이어서 훈련할 때는 partial_fit() 메서드를 사용 

- 호출할 때마다 1 에포크씩 이어서 훈련 가능 

sc.partial_fit(train_scaled, train_target)

print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

 

 

- 아직 점수가 낮지만 에포크를 한 번 더 실행하니 정확도가 향상됨 

 

 

3) 에포크와 과대/과소 적합 

 

- 적은 에포크 횟수 동안에 훈련한 모델은 훈련 세트와 테스트 세트에 잘 맞지 않는 과소적합된 모델일 가능성이 높다. 

- 많은 에포크 횟수 동안에 훈련한 모델은 훈련 세트에 너무 잘맞아 테스트 세트에는 오히려 점수가 나쁜 과대적합될 모델일 가능성이 높다. 

 

 

- 훈련 세트 점수는 에포크가 진행될수록 꾸준히 증가하지만 테스트 세트 점수는 어느 순간 감소하기 시작한다. (과대적합이 시작되는 지점)

- 과대적합이 시작하기 전에 훈련을 멈추는 것을 조기 종료(early stopping)라고 한다.

- partical_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번의 에포크 동안 훈련을 반복하여 진행 

- 반복마다 훈련 세트와 테스트 세트의 점수를 계산하여 train_score, test_score 리스트에 추가

 

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번의 에포크 동안 기록한 훈련 세트와 테스트 점수를 그래프로 그리기

 ->100번째 에포크 이후에는 훈련 세트와 테스트 세트의 점수가 조금씩 벌어지므로 100번째 에포크가 적절한 반복횟수 

 

import matplotlib.pyplot as plt

plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

 

 

- SDGClassfier의 반복 횟수를 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))

 

 

 

- SGDClassfier는 일정 에포크 동안 성능이 향상되지 않으면 더 훈련하지 않고 자동으로 멈춘다. 

- tol 매개변수에서 향상될 최솟값을 지정한다. 

- tol 매개변수를 None으로 지정하여 자동으로 멈추지 않고 max_iter=100만큼 무조건 반복하도록 하였다.