Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Archives
Today
Total
관리 메뉴

cb

[혼공머신] Chapter 04 - 다양한 분류 알고리즘 본문

ai - study

[혼공머신] Chapter 04 - 다양한 분류 알고리즘

10011001101 2024. 2. 10. 21:29

본 게시물은 <혼자서 공부하는 머신러닝+딥러닝>의 Chapter 04: 다양한 분류 알고리즘를 보고 정리한 글입니다. 원본 코드는 책의 저자인 박해선님의 깃허브 코드를 참고하시길 바랍니다.

 

GitHub - rickiepark/hg-mldl: <혼자 공부하는 머신러닝+딥러닝>의 코드 저장소입니다.

<혼자 공부하는 머신러닝+딥러닝>의 코드 저장소입니다. Contribute to rickiepark/hg-mldl development by creating an account on GitHub.

github.com


다중 분류

이전 챕터까지는 생선 데이터가 단순히 도미와 빙어로만 구성되어 있었다. 하지만 데이터세트는 항상 두 가지의 타겟값으로만 구성되지 않는다. 이처럼 2개 이상의 클래스가 타겟 데이터에 포함된 문제를 다중 분류(multi-class classification)라고 부른다.

 

이진 분류를 사용했을 때에는 각 클래스를 임의로 0, 1로 지정하여 사용하였다. 다중 분류에서도 이처럼 숫자값을 통해 클래스를 구분할 수 있지만, 사이킷런에서는 문자열로 된 타겟값을 그대로 사용할 수 있다는 장점이 있다. (다만, 알파벳순으로 타겟값이 매겨진다는 점을 주의하자.)


로지스틱 회귀

로지스틱 회귀(logistic regression)는 선형회귀와 동일하게 선형 방적실을 학습하는 분류 모델이다. 이름은 회귀이지만, 본 역할은 분류를 한다는 것에 주의하자. 로지스틱 회귀가 학습하는 선형 방정식은 아래와 같다.

 

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

 

여기서 a,b,c,d,e는 가중치 혹은 계수를 의미하고, z는 확률일 때 0~1사이의 값을 가진다.

 

이는 시그모이드 함수(sigmoid function)을 사용해 0과 1 사이의 값으로 바꾸어 줄 수 있다.

시그모이드 함수(로지스틱 함수)는 z가 무한하게 큰 음수일 경우 함수는 0에 가까워지고, z가 무한하게 큰 양수일 때는 1에 가까운 값을 가진다. 0~1사이의 값을 0~100%까지 확률로 해석한다면 더 이해하기 쉬울 것이다.

 

로지스틱 회귀 모델 또한 사이킷런에 구현된 클래스를 통해 사용해 볼 수 있다. 먼저, 아래의 코드를 사용하여 데이터를 준비해 주자.

# 데이터 준비
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')

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)

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)

 

아래의 코드를 사용하면 train_bream_smelt에서 처음 5개 샘플의 예측 확률을 출력할 수 있다.

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

 

위 코드를 실행시켜 보면 아래와 같은 출력이 나올 것이다. 이는 샘플마다 2개의 확률이 출력된 것으로, 첫 번째 열은 클래스 0에 대한 확률이고 두 번째 열은 클래스 1에 대한 확률을 나타낸다. 사이킷런은 타겟값을 알파벳순으로 정렬하여 사용한다고 하였으므로, bream인 도미가 클래스 0에, smelt인 빙어가 클래스 1에 속하는 것을 알 수 있다.

 

이제, 아래의 코드를 사용하여 선형 회귀에서 로지스틱 회귀가 학습한 계수를 확인해 보자.

print(lr.coef_, lr.intercept_)

 

coef, intercept로 위의 값을 가진다는 것은, 아래의 방정식을 학습한 것과 같다고 이해할 수 있다.

 

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

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

다중 분류도 이진 분류와 다르지 않다. Logistic Regression 클래스는 기본적으로 반복적인 알고리즘을 사용한다. max_iter라는 매개변수를 통해 반복 횟수를 지정할 수 있다. 여기서 기본값은 100으로, 반복 횟수가 부족하다고 판단되면 아래의 코드처럼 늘려 주면 된다.

 

또, 기본적으로 릿지 회귀를 통해 계수의 제곱을 규제한다. alpha 값이 커지면 규제도 커지는데, 이는 C 매개변수를 통해 조정해 줄 수 있다. C의 기본값은 1로, C값이 작을수록 규제 강도가 커진다.

 

아래 코드를 활용하여 규제, 반복 횟수를 지정해 주고 로지스틱 회귀를 사용한 다중 분류 모델을 훈련시켜 보자.

lr = LogisticRegression(C=20, max_iter=1000) # C, max_iter 조정
lr.fit(train_scaled, train_target) # 모델 훈련

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

 

score 점수까지 확인했다면, 테스트 세트의 처음 5개 샘플을 활용하여 예측 확률을 출력해 보자.

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

 

이 또한 좀 전의 이진 분류와 같이 첫 번째 열은 클래스 0번, 두 번째 열은 클래스 1번, ..., 등으로 해석할 수 있다. 

 

첫 번째 샘플을 보면 세 번째 열의 확률이 가장 높은데, print(lr.classes)를 사용하여 클래스들을 확인해 보면 세 번째 열은 농어(Perch) 데이터에 해당함을 알 수 있다.

 

그럼, 이중 분류와의 차이점을 알아보기 위해 다중 분류의 선형 방정식을 한번 확인해 보자. 먼저, coef와 intercept의 크기를 출력해 보자.

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

이 데이터는 5개의 특성을 사용하였으므로 coef는 총 5개의 열을 가진다. 그런데 7개의 행, 7개의 Intercept를 가진다는 사실도 확인할 수 있다.

 

다중 분류는 클래스마다 z 값을 하나씩 계산하여 가장 높은 z 값을 출력하는 클래스를 예측 클래스로 가진다. 여기서 다중 분류는 이진 분류와 다르게 소프트맥스(softmax) 함수를 사용하여 7개의 z값을 확률로 변환한다.

$$ e_sum = e^{z1} + e^{z2} + e^{z3} + e^{z4} + e^{z5} + e^{z6} + e^(z7) $$

먼저 7개의 z값을 z1, z2, ..., z7이라고 했을 때, 지수함수를 통해 e_sum 값을 계산한다. 그 후 각각의 e들을 e_sum으로 나누어 주면 s1~s7의 합이 1이 되기 때문에 전체 확률의 합이 1이 될 수 있다.

 

이진 분류에서처럼 decision_function() 메서드로 z1~z7의 값을 구하고, 소프트맥스 함수를 사용해 확률로 바꿔보자. 코드는 아래와 같다.

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

 

이를 실행하면 테스트세트의 처음 5개 샘플에 대한 z1~z7의 값을 구할 수 있다.

 

이후 아래의 코드를 실행하면 앞서 구한 decision 배열을 softmax() 함수로 전달하여 각 샘플에 대한 소프트맥스 계산 값을 반환해 준다.

from scipy.special import softmax

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


확률적 경사 하강법

앞서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련하는 방식을 점진적 학습이라고 한다. 대표적인 점진적 학습 방법에는 확률적 경사 하강법(stochastic gradient descent)이 있다.

 

확률적 경사 하강법의 '확률적'은 '무작위하게', 혹은 '랜덤하게'를 기술적으로 표현한 것이다. '경사'는 우리가 흔히 알고 있는 기울기를 의미하고, '하강법'은 '내려가는 방법'을 의미한다. 다시 말해, 경사 하강법은 경사를 따라 내려가는 방법을 말한다.

 

더 구체적으로 설명하자면, 확률적 경사 하강법은 훈련 세트에서 랜덤하게 하난의 샘플을 선택하여 가파른 경사를 조금 내려간다. 그 다음 훈련 세트에서 랜덤하게 또 다른 샘플을 하나 선택하여 경사를 내려간다. 이런 식으로 전체 샘플을 모두 사용할 떄까지 반복한다. 만약 이 과정이 모두 끝났는데, 경사를 다 내려오지 못 했다면 처음부터 다시 전 과정을 진행한다.

 

확률적 경사 하강법에서 훈련 세트를 한 번 모두 사용한 과정을 에포크(epoch)라고 부른다. 확률적 경사 하강법에서 샘플 1개씩이 아닌, 무작위로 여러 개의 샘플을 사용해 경사 하강법을 수행하는 방법은 미니배치 경사 하강법(minibatch gradient descent)이라 한다. 전체 샘플을 한 번에 사용하는 방법은 배치 경사 하강법(batch gradient descent)이라 부르는데, 이는 전체 데이터를 사용하므로 안정적일 수 있지만, 그만큼 컴퓨터 자원을 많이 사용한다는 단점이 존재한다.


손실 함수

손실 함수(loss function)는 샘플 하나에 대한 손실을 정의한다. 즉, 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지 측정하는 기준이 된다. 샘플에 대한 손실 정도를 나타내기 떄문에 값이 작을수록 좋은 것임을 의미한다.

 

우리는 손실 함수에 확률적 경사 하강법을 적용할 수 있다. 가장 작은 값은 곧 가장 좋은 값을 의미하므로, 경사 하강법을 사용한다면 손실 함수의 최저점을 쉽게 찾아나갈 수 있을 것이다.


로지스틱 손실 함수

만약 타깃이 1일 때에는 -log(예측 확률)을, 타깃이 0일 때는 -log(1-예측확률)을 손실함수로 가진다면 이는 로지스틱 손실 함수(logistic loss function)라고 부른다. 혹은, 이진 크로스엔트로피 손실 함수(binary cross entropy loss function)라고도 한다. 이진 분류뿐만이 아니라 다중 분류에서도 비슷한 손실 함수를 사용하는데, 다중 분류에서 사용하는 손실 함수를 크로스엔트로피 손실 함수(cross entropy loss function)라고 부른다.


SGDClassifier

손실 함수를 직접 계산하는 일은 힘들기 때문에, 주로 머신러닝 라이브러리를 사용한다. 먼저, 아래와 같이 데이터를 준비해 보자.

import pandas as pd

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

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)

 

이후, 사이킷런에서 확률적 경사 하강법을 제공하는 대표적 분류용 클래스인 SGDClassfier를 사용하여 학습을 진행해 보자.

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

 

앞서 얘기한 것처럼 확률적 경사 하강법은 점진적 학습이 가능하므로 partial_fit() 메서드를 사용하여 모델을 이어서 훈련해 줄 수 있다. 이는 호출할 때마다 1 에포크씩 이어서 훈련이 가능하다.

sc.partial_fit(train_scaled, train_target)

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

 

위의 코드까지 실행하고 나면, 에포크가 증가할수록 정확도도 향상되는 경향을 확인할 수 있다.


에포크와 과대/과소적합

그러나, 에포크는 증가할수록 오히려 과대적합이 될 수 있다는 문제점도 존재한다. 그렇기 때문에 과대 적합이 시작되기 전에 훈련을 멈추는 조기종료(early stopping)을 사용하기도 한다.

 

아래의 코드를 통해 예시를 확인해 주자. 먼저, SGDClassifier 객체를 불러와 주고, np.unique() 함수로 train_target에 있는 7개의 생선 목록을 만들어 준다.

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번째 에포크 이후 훈련 세트와 세트르 세트의 점수가 조금씩 벌어지고 있는 것을 확인할 수 있다. 이 전에는 너무 낮은 점수를 보이므로 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))
print(sc.score(test_scaled, test_target))

최종적으로 훈련 세트와 테스트 세트에서의 정확도가 비교적 높게 나왔다는 사실을 확인할 수 있다.

 

 

+)

추가적으로, SGDClassifier의 loss 매개변수에 대해 알아보자. loss 매개변수의 기본값은 'hinge'로, 힌지 손실(hinge loss)서포트 벡터 머신(support vector machine)이라는 또 다른 머신러닝 알고리즘을 위한 손실 함수이다.


마무리

이번 챕터에서는 로지스틱 회귀와 점진적 학습을 위한 확률적 경사 하강법에 대해 알아 보았다. 경사 하강법에서 쓰이는 손실 함수는 크로스 엔트로피 이외에도 다양한 것들이 존재하므로, 궁금한 사람은 다른 손실 함수들을 찾아봐도 좋을 것 같다.