아래 내용은 "혼자 공부하는 머신러닝+딥러닝" 을 공부하며 간략하게 정리한 내용입니다. 쉬운 사례로 어려운 수식 없이 설명하므로 입문자에게 강추하는 책입니다!
1. 시작하기 전에
한빛마켓 마케팅 팀은 다가오는 명절 이벤트로 럭키백을 준비하고 있습니다. 상품은 생선으로 한정해서 럭키백에 포함된 생선의 확률을 알려줄 것입니다. (생선 럭키백이라니.....)
김팀장은 담당자를 다시 불러 럭키백에 들어갈 생선 7개에 대한 확률을 뽑으라고 합니다.....
2. 데이터 준비하기
이번에는 생선의 길이, 높이, 두께외에도 대각선의 길이와 무게도 포함되어 있습니다.
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()
어떤 종류의 생선이 있는지 Species 열에서 고유한 값을 추출해보겠습니다.
print(pd.unique(fish['Species']))
→ ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
학습셋과 타겟셋을 구분하고 표준화 전처리 작업까지 하겠습니다.
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
fish_target = fish[['Species']]
# 학습셋, 테스트셋 구분
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)
3. k-최근접 이웃 분류기의 확률 예측
앞서 배운 k-최근접 알고리즘을 사용하여 모델을 학습해보겠습니다. 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)이라고 부릅니다.
사이킷런에서는 편리하게 클래스가 문자열인 경우 그대로 사용할 수 있는 장점이 있으나 알파벳순으로 자동으로 순서가 매겨지기 때문에 유의해야합니다. 정렬된 타겟값은 classes_ 속성에서 저장되어 있습니다.
print(kn.classes_)
→ ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
predict() 메서드를 이용하여 처음 5개 샘플의 타겟값을 예측해보겠습니다.
print(kn.predict(test_scaled[:5]))
→ ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
이 5개의 샘플에 대해 예측은 어떤 확률로 만들어졌을까요? 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환합니다.
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))
→ [[0. 0. 1. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 1. 0. ]
[0. 0. 0. 1. 0. 0. 0. ]
[0. 0. 0.6667 0. 0.3333 0. 0. ]
[0. 0. 0.6667 0. 0.3333 0. 0. ]]
predict_proba() 메서드의 출력 순서는 앞서 보았던 classes_속성과 같습니다. 즉 첫 번째 열이 'Bream'에 대한 확률, 두 번째 열이 'Parkki'에 대한 확률입니다. 그런데 이웃 개수를 3개로한 모델이기 때문에 가능한 확률이 0/3, 1/3, 2/3, 3/3이 전부가 될 것입니다. 뭔가 어색한데 더 좋은 방법을 찾아봐야할 것 같습니다!
4. 로지스틱 회귀
로지스틱회귀는(logistic regression)는 이름은 회귀이지만 분류모델입니다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습하지만 분류 모델이기 때문에 타겟값이 확률을 갖으려면 0~1의 사이의 값이어야 하기 때문에 시그모이드함수(sigmoid function)를 사용합니다.
타겟값이 무한하게 큰 음수인 경우 이 함수는 0에 가까워지고, 무한하게 큰 양수가 될 때는 1에 가까워집니다. 그리고 0인 경우는 0.5가 되죠. 그래서 어떤 값이 되더라도 절대로 0~1사이의 범위를 벗어날 수 없습니다. 그리면 0~100%까지 확률로 해석할 수 있을 것입니다!
그럼 로지스틱 회귀로 다중 분류를 수행해보겠습니다.
먼저 사이킷런 LogisticRegression 클래스를 사용하여 로지스틱 회귀를 이용할 수 있는데 기본적으로 반복적인 알고리즘을 사용하기 때문에 max_iter 매개변수로 반복 횟수를 지정할 수 있습니다. 앞장의 라쏘(lasso) 모델처럼 반복 횟수가 부족하면 경고가 발생하기 때문에 충분하게 훈련하기 위해 1,000으로 늘리겠습니다.
또 로지스틱회귀는 릿지 회귀와 같이 계수의 제곱을 규제합니다. 이런 규제를 L2규제라고 부르고 규제의 양을 조절하는 alpha가 LogisticRegression에서는 C로 사용됩니다. alpha와 반대로 C는 작을수록 규제가 커지게 되며 기본값은 1입니다. 여기에서는 규제를 완하하기 위해 20으로 늘리겠습니다.
from sklearn.linear_model import LogisticRegression
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]))
→ ['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
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]]
5개 샘플에 대한 예측이므로 5개 행이 출력되고 타겟 클래스가 7개이므로 7개의 열이 출력되었습니다. 그럼 다중 분류일 경우 선형 방정식의 coef_와 intercept_의 크기를 출력해 보겠습니다.
print(lr.coef_.shape, lr.intercept_.shape)
→ (7, 5) (7,)
이 데이터는 5개의 특성을 사용하기 때문에 coef_의 열은 5개 입니다. 그런데 행이 7개라는 건 각 클래스별로 확률값을 하나씩 계산하는 것입니다. 이진 분류의 경우 시그모이드 함수를 사용하여 0~1 사이의 값으로 변환했는데 다중 분류의 경우 소프트맥스(softmax) 함수를 이용하여 7개의 타겟값을 확률로 변환합니다.
소프트맥스의 계산 방식을 살펴보겠습니다. 7개의 타겟값 이름을 z1~z7이라고 할 때 지수함수 $e^{z1}$ ~ $e^{z7}$을 계산해 모두 더합니다. 이를 $e_{-sum}$이라고 하겠습니다.
$$e_{-sum} = e^{z1} + e^{z2} + e^{z3} + e^{z4} + e^{z5} + e^{z6} + e^{z7} $$
그 다음 $e^{z1}~e^{z7}$을 각각 $e_{-sum}$으로 나누어 주면 됩니다.
$$s1 = \frac{e^{z1}}{e_{-sum}}, s2 = \frac{e^{z2}}{e_{-sum}}, ..., s3 = \frac{e^{z3}}{e_{-sum}}$$
그래서 $s1$에서 $s7$까지 모두 더하면 분자와 분모가 같아지므로 1이 됩니다.
5. 확률적 경사하강법
학습시 데이터가 충분하지 않거나 계속적으로 데이터가 추가되는 경우 매일 모델을 다시 훈련하게 된다면 시간이 지날수록 데이터는 늘어나 점차 모델을 훈련하기 위한 서버가 늘어나야할 것 입니다. 또는 데이터셋의 크기가 너무 커지지 않게 이전 데이터를 지운다면 중요한 데이터가 손실될 가능성도 있습니다.
앞서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련하면 데이터를 모두 유지할 필요도 없고 앞서 학습한 중요한 데이터도 까먹을 일도 없을 것입니다. 이런 식의 훈련 방식을 점진적 학습 또는 온라인 학습이라고 부릅니다. 대표적인 점진적 학습 알고리즘은 확률적 경사 하강법(Stochastic Gradient Descent)입니다.
경사 하강법은 가장 가파른 경사를 따라 원하는 지점에 도달하는 것이 목표로 만약 내려오는 폭이 크다면 내려가지 못하고 올라갈 수도 있습니다. 가장 가파른 길을 찾아 내려오지만 조금씩 내려오는 것이 중요한데 이렇게 내려오는 과정이 바로 경사 하강법 모델을 훈련하는 것입니다.
확률적 경사 하강법이란 훈련셋에서 랜덤하게 하나의 샘플을 골라 가파른 경사를 조금 내려오고 그다음 훈련셋에서 랜덤하게 또 다른 샘플을 하나 선택하여 경사를 조금 내려오는 것으로 전체 샘플을 모두 사용할 때까지 계속 반복합니다. 만약 모든 샘플을 사용하여도 원하는 지점까지 도달하지 못한다면 처음부터 다시 시작합니다. 이렇게 훈련셋을 모두 사용하는 과정을 에포크(epoch)라고 부릅니다.
경사하강법이 잘 작동하기는 하지만 1개씩 말고 무작위로 몇 개의 샘플을 선택해서 경사를 내려가는 방식도 있습니다. 이를 미니배치 경사하강법(minibatch gradient descent)라고 합니다.
또는 전체 샘플을 다 사용할 수도 있습니다. 이를 배치경사하강법(batch gradient descent)라고 합니다. 전체 데이터를 다 사용하면 안정적인 방법이 될 수 있지만 컴퓨터 자원을 그만큼 많이 사용하게 됩니다.
6. 손실함수
손실함수(loss function)는 어떤 문제에서 머신러닝 알고리즘이 잘못 된 학습을 하고 있는지 측정하는 기준입니다. 손실함수는 작을수록 좋지만 어떤 값이 최솟값인지는 알지 못합니다. 분류에서 손실은 아주 확실합니다. 정답을 못 맞히는거죠!
예를들어 샘플 4개의 이진 분류 예측 확률을 각각 0.9, 0.3, 0.2, 0.8이라고 가정해보겠습니다.
첫 번째 샘플의 예측은 0.9이므로 양성 클래스의 타깃은 1과 곱한 다음 음수로 바꾸면 -0.9가 됩니다. 예측이 1에 가까울수록 예측과 타겟의 곱의 음수는 점점 작아지게 됩니다. 세 번째 샘플의 타겟은 0입니다. 이 값을 그대로 곱하면 무조건 0이 될 것이기 때문에 양성 클래스(1)처럼 바꾸어 사용해보겠습니다. 세 번째 샘플은 음성 클래스(0) 타겟을 맞추었으므로 손실이 낮게 됩니다.
이런 식으로 계산하면 연속적인 손실 함수를 얻을 수 있을 것 같은데 예측 확률에 로그 함수를 적용하면 더 좋습니다. 예측 확률의 범위는 0~1 사이인데 로그 함수는 이 사이에서 음수가 되므로 최종 손실 값은 양수가 되기 때문입니다. 음성 클래스(0)일 때 손실은 $-log(1-예측확률)$로 계산되어 0에서 멀어질수록 손실은 아주 큰 양수가 됩니다.
이 손실 함수를 로지스틱 손실 함수(logistic loss function) 또는 이진 크로스엔트로피 손실 함수(binary cross-entropy loss function)라고도 부릅니다.
7. SGDClassifier
확률적 경사 하강법을 사용한 분류 모델을 만들어보겠습니다. 먼저 데이터를 준비해보겠습니다.
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data.csv')
# input/output 구분
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개의 매개변수를 지정합니다. 손실함수 종류를 지정하는 loss와 수행할 에포크 횟수를 지정합니다.
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss = 'log', 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에 loss='log'로 지정하면 클래스마다 이진 분류 모델을 만듭니다. 출력된 훈련셋과 테스트셋의 정확도가 낮은데 아마 지정한 반복 횟수 10번이 부족한 것으로 보이네요. 앞서 이야기 한 점진적 학습이 가능한데 SGDClassifier객체를 다시 만들지 않고, 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.825
partial_fit() 메서드는 fit()메서드와 사용법이 같지만 호출할 때마다 1에포크씩 이어서 훈련할 수 있습니다. 아직 점수가 낮지만 에포크를 한 번 더 실행하니 정확도가 향상되었습니다!!
8. 에포크와 과대/과소 적합
확률적 경사 하강법을 사용한 모델을 에포크 횟수에 따라 과소적합이나 과대적합이 될 수 있습니다.
왜냐하면 횟수가 적으면 훈련 셋을 덜 학습할 것이고, 횟수가 많으면 훈련셋을 완전히 학습할 것이기 때문에 과대적합된 모델일 가능성이 높습니다.
위의 그래프는 에포크가 진행됨에 따라 모델의 정확도를 나타낸 것인데 훈련셋은 꾸준히 증가하지만 테스트셋 점수는 어느 순간 감소하기 시작합니다. 바로 이 지점이 모델이 과대적합되기 시작하는 곳입니다. 그래서 우리는 과대적합이 시작하기 전에 훈련을 멈추는 조기종료(early stopping)을 합니다. 여기에서는 임의로 300번의 에포크 동안 훈련을 반복하면서 훈련셋과 테스트셋의 점수를 계산하여 적절한 에포크 수를 확인해보겠습니다.
import numpy as np
sc = SGDClassifier(loss='log', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_target)
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.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()
데이터가 작기 때문에 아주 잘 드러나지는 않지만 백 번째 에포크 이후에는 훈련셋과 테스트셋의 점수가 조금씩 벌어지고 있습니다. 이 모델의 경우 백 번째 에포크가 적절한 반복 횟수로 보입니다.
지금까지 회귀와 분류에 널리 사용되는 알고리즘을 배웠는데 신경망 알고리즘을 제외하고 머신러닝에서 가장 뛰어난 성능을 내는 알고리즘들을 배워보겠습니다. (다음장에서 계속...)
감사합니다 :)
참고자료
혼자 공부하는 머신러닝+딥러닝
'머신러닝&딥러닝 > 책요약및리뷰' 카테고리의 다른 글
[혼자공부하는머신러닝+딥러닝] 5. 트리 알고리즘 (0) | 2022.06.22 |
---|---|
[혼자공부하는머신러닝+딥러닝] 3. 회귀 알고리즘과 모델 규제 (0) | 2022.06.15 |
[혼자공부하는머신러닝+딥러닝] 2. 데이터 다루기 (0) | 2022.06.13 |
[혼자공부하는머신러닝+딥러닝] 1. 마켓과 머신러닝 (0) | 2022.06.10 |
[XAI 설명가능한 인공지능] 3. 모델 튜닝하기-Xgboost (0) | 2022.05.17 |