분류 알고리즘들

신민제·2023년 7월 15일
0

머신러닝, 딥러닝

목록 보기
3/6

로지스틱 회귀

로지스틱 회귀란, 선형 방정식을 사용한 분류 알고리즘 이다. 선형 회귀와 달리 '시그모이드 함수''소프트맥스 함수'를 사용해 클래스 확률을 추출할 수 있다는 특징이 있다. 그렇다면 '시그모이드 함수'와 '소프트맥스 함수'가 무엇일까?

시그모이드 함수


(출처: https://taewanmerepo.github.io/2017/09/sigmoid/post.jpg)

우선 위의 그림이 시그모이드 함수의 그래프이다.

시그모이드 함수의 특징

  • 시그모이드 함수는 0에서 1사이의 함수이며, 값이 들어왔을 때 0~1사이의 값을 반환한다.
  • 연속형 데이터이기 때문에 계단함수가 끊기지 않고 매끄러운 형태를 띈다.
  • 분류가 0과 1로 나뉘며 출력값이 어느 값에 가까운지를 통해 어느 분류에 속하는지 쉽게 알 수 있다.

소프트맥스 함수

소프트맥스 함수란, 세 개 이상으로 분류하는 다중 클래스 분류에서 사용되는 활성화 함수다. 분류될 클래스가 n개라고 할 때, n차원의 벡터를 입력받아 각 클래스에 속할 확률을 추정한다. 소프트맥스 함수는 시그모이드 함수로부터 유도된 것인데 쉽게 사진으로 설명하겠다.

(출처: https://miro.medium.com/v2/resize:fit:720/0*tGSrq3hfKFBgKntB)

소프트맥스 함수의 특징

  • 시그모이드 함수와 달리 다중 분류에서 사용된다.
  • 확률의 총합이 1이기 때문에 어떤 분류에 속할 확률이 가장 높을지를 쉽게 인지할 수 있다.
  • 다중 분류에서 여러 선형 방정식의 출력 결과를 정규화하여 합이 1이 되도록 만든다.

다중분류란, 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 말한다.

그럼 이제 본격적으로 로지스틱 회귀에 대해서 배워보자.

이번에 예제로 다루어볼 문제는 생선의 크기, 무게 등의 데이터가 주어졌을 때 각 생선들에 대한 확률을 출력해주는 것을 다루어 볼 것이다. 이것을 앞서 배웠던 k-최근접 이웃을 이용해서 해결해보자. 우선 데이터를 준비해보자.

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

Species Weight Length Diagonal Height Width
0 Bream 242.0 25.4 30.0 11.5200 4.0200
1 Bream 290.0 26.3 31.2 12.4800 4.3056
2 Bream 340.0 26.5 31.1 12.3778 4.6961
3 Bream 363.0 29.0 33.5 12.7300 4.4555
4 Bream 430.0 29.0 34.0 12.4440 5.1340

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

['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']

여기서 7가지의 생선 종류가 있는 것을 확인할 수 있다. 그럼 이 데이터프레임에서 Species 열을 타깃으로 만들고, 나머지 5개의 열은 입력 데이터로 사용하겠다. 우선 Species열을 제외한 5개 열을 선택해 보겠다.

#넘파이 메서드로 넘파이 배열로 바꿔 fish_input에 저장하기
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
print(fish_input[:5])

[[242. 25.4 30. 11.52 4.02 ][290. 26.3 31.2 12.48 4.3056]
[340. 26.5 31.1 12.3778 4.6961][363. 29. 33.5 12.73 4.4555]
[430. 29. 34. 12.444 5.134 ]]

fish_input에 Species를 제외한 열이 잘 저장되었음을 알 수 있다. 그럼 동일한 방식으로 타깃 데이터를 만들고 훈련시켜보자.

#타깃 데이터 만들기
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)

그럼 이제 사이킷런의 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))

0.8907563025210085
0.85

이진 분류를 사용했을 때는 양성 클래스와 음성 클래스를 각각 0과 1로 지정하여 타깃 데이터를 만들었다. 다중 분류에서도 타깃값을 숫자로 바꿔 입력할 수 있지만, 사이킷런에서는 문자열로 된 타깃값을 그대로 사용할 수 있다.

print(kn.classes_)

['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']

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

['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']

이 5개에 대한 예측은 predict_proba()메서드로 클래스별 확률값을 반환한다. 테스트 세트에 있는 처음 5개의 샘플에 대한 확률을 출력해보자. 넘파이 round() 함수는 기본적으로 소수점 첫째 자리에서 반올림을 하는데, decimals매개변수로 유지할 소수점 아래 자릿수를 지정할 수 있다.

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

여기서 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. ]]

결과값은 다음과 같은데, 이것을 표로 보아서 첫번째 열이 'Bream'에 대한 확률, 두번째 열이 'Parkki'에 대한 확률이라고 볼 수 있다. 나머지 세번째, 네번째는 앞서 출력해본 classes들의 순서대로 나아간다.

그럼 이 모델이 계산한 확률이 가장 가까운 이웃의 비율이 맞는지를 확인해보자. 확인을 위해 네번째 샘플의 최근접 이웃의 클래스를 확인해 보겠다.

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

[['Roach' 'Perch' 'Perch']]

이해가 가지 않는 부분이 있어 이부분에 대한 설명은 추후에 덧붙이겠다.

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

그럼 이제 로지스틱 회귀를 이용해서 이진 분류를 해보자.
넘파이 배열은 True, False값을 전달해 행을 선택할 수 있다. 이를 불리언 언덱싱이라고 한다. 이 방식을 사용해 훈련 세트에서 도미(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]

#로지스틱 회귀 모델로 훈련해보기
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']

두번째 샘플을 제외하고 모두 도미로 예측한 것을 볼 수 있다. KNeighborsClassifier와 마찬가지로 예측 확률은 predict_proba() 메서드에서 제공한다. 그럼 이것의 예측 확률을 출력해보자.

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

[[0.99759855 0.00240145]

[0.02735183 0.97264817]

[0.99486072 0.00513928]

[0.98584202 0.01415798]

[0.99767269 0.00232731]]

각 샘플마다 2개의 확률이 출력되었는데 첫 번째 열이 음성 클래스(0)에 대한 확률이고, 두번째 열이 양성 클래스(1)에 대한 확률이다. 그럼 Bream과 Smelt중 어느 것이 음성이고 양성일까?

앞서 k-최근접 이웃 분류기에서 봤듯이, 사이킷런은 타깃값을 알파벳순으로 정렬하여 사용한다. Classes_의 속성에서 확인해보자

print(lr.classes_)

['Bream' 'Smelt']

빙어(Bream)이 양성 클래스인 것을 알 수 있다. predict_proba() 메서드가 반환한 배열 값을 보면 두 번째 샘플만 양성 클래스 (빙어)일 확률이 높다. 그럼 나머지는 모두 도미(Bream)으로 예측할 것이라는 것을 알 수 있다.

그럼 선형 회귀처럼 로지스틱 회귀가 학습한 계수를 확인해보자.

print(lr.coef_, lr.intercept_)

[[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]][-2.16155132]

다음과 같은 계수를 학습한 것을 알 수 있고, 그렇다면 이 로지스틱 회귀 모델이 학습한 방정식은 다음과 같음을 알 수 있다. 이를 통해서 로지스틱 회귀는 선형 회귀와 비슷하다는 것 또한 알 수 있다.

z = -0.404 ⨉ (weight) - 0.576 ⨉ (length) - 0.663 ⨉ (Diagonal) - 1.013 ⨉(Height) - 0.732 ⨉ (Width) - 2.161

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

앞서 이진 분류를 위해 로지스틱 회귀 모델을 훈련시켜 보았다. LogisticRegression 클래스를 사용해 7개의 생선을 분류해 보며 이진 분류와의 차이점을 알아보겠다.

기본적으로 LogisticRegression은 반복적인 알고리즘을 사용한다. max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100이다. 이것을 그대로 사용하게 된다면, 반복 횟수가 부족하다는 경고가 발생한다. 충분히 학습되지 않을 수도 있다는 것이다. 그럼 충분히 훈련시키기 위해 반복횟수를 1000으로 늘려서 학습시켜 보자. 또한 LogisticRegression은 릿지 회귀와 같이 계수의 제곱을 규제한다.(L2규제라고 부름) 릿지 회귀에서는 alpha 매개변수로 규제의 양을 조절했다면, LogisticRegression에서는 매개변수 C를 통해 규제를 한다. 기본값은 1이며, 규제를 조금 완화하기 위해 20으로 늘려보겠다.

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

0.9327731092436975
0.925

훈련세트와 테스트 세트에 대한 점수가 과대적합이나 과소적합에 해당되지 않는 것 같다. 그럼 테스트 세트의 처음 5개 샘플에 대한 예측을 해보자.

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

['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']

이번에는 각각의 예측 확률을 출력해보자.

#predict_proba 메서드로 클래스별 확률값 반환
proba = lr.predict_proba(test_scaled[:5])

#decimals로 소수점 아래 자릿수 지정해주기
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개의 열이 출력된 것을 볼 수 있다. 여기서 이진분류와의 차이는, 이진 분류에서는 2개의 열만 존재했었다.

그럼 다중 분류일 경우의 선형 방정식은 어떤 형태를 띄고 있을까?

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

여기서 shape를 사용한 이유는 단지 계수들의 형태를 파악하기 위함이다.

(7, 5) (7,)

다중 분류일 경우 각 계수들은 다음과 같은 형태를 띄고 있다. 이 데이터는 5개의 특성을 사용하기 때문에 coef 배열의 열은 5개이다. 하지만 행과, intercept도 7개 임을 볼 수 있다. 즉 이진 분류에서 보았던 z를 7개 계산한다는 말이다. 여기서 알 수 있는 다중 분류의 특징은 각 클래스마다 z값을 하나씩 계산한다는 것이다.

그럼 각각의 확률은 어떻게 계산할까?

이진 분류에서는 시그모이드 함수를 사용해 z를 0과 1 사이의 값으로 변환했다. 다중 분류는 소프트맥스 함수를 사용해 7개의 z값을 확률을 변환한다. 소프트맥스 함수에 대해서는 앞서 설명했기 때문에 넘어가겠다.

이제 이진 분류에서처럼 decision_function() 메서드로 z1~z7까지의 값을 구한 다음 소프트맥스 함수를 통해 확률로 바꾸어 보겠다.

#테스트 세트의 처음 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]]

그럼 scipy.special 아래에 softmax() 함수를 임포트해 사용해 보자.

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

앞서 구한 decision 배열을 softmax() 함수에 전달했고, softmax() 의 axis 매개변수는 소프트맥스를 계산할 축을 지정한다. axis = 1 로 지정하면 각 행, 즉 각 샘플에 대해 소프트맥스를 계산한다. 만약 axis를 지정하지 않으면 배열 전제체 대해 소프트맥스 함수를 계산하게 된다.

확률적 경사 하강법

확률적 경사 하강법은 점진적 학습(온라인 학습)의 한 부류이다.


(출처: https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGeQb%2Fbtrv2EFsBoD%2FeDZnLKx46IGTzTQkHzMVVK%2Fimg.png)

그렇다면 점진적 학습이란 무엇일까?

점진적 학습: 훈련한 모델을 버리지 않고, 새로운 데이터에 대해 조금씩 더 훈련하는 학습

  • 데이터 양이 너무 많아 배치 학습 알고리즘을 사용하기 어려운 경우에 적용한다.
  • 데이터를 순차적으로 한 개씩 혹은 미니배치(mini-batch)라 부르는 작은 묶음 단위로 주입해 시스템을 훈련시킨다.
  • 매 학습 단계가 빠르고 비용이 적게 들어 시스템은 데이터가 도착하는 대로 즉시 학습할 수 있다.
  • 연속적으로 데이터를 받고 빠른 변화에 스스로 적응해야 하는 시스템에 적합하다.

확률적 경사 하강법은 대표적인 점진적 학습 알고리즘이다.

확률적 경사 하강법: 전체 데이터 중 단 하나의 데이터를 이용하여 경사 하강법을 1회 진행(배치 크기가 1)하는 방법이다.
전체 학습 데이터 중 랜덤하게 선택된 하나의 데이터로 학습을 하기 때문에 확률적 이라 부른다.

  • 오로지 샘플 데이터셋에 대해서만 경사를 계산하므로 매 반복마다 다뤄야 할 데이터가 줄어들었고, 학습 속도가 빠르다.
  • 같은 이유로 메모리 소모량이 매우 낮으며, 큰 데이터 셋이라 할지라도 학습이 가능하다.
  • 학습 중간 과정에서 진폭이 크고 배치 경사 하강법보다 불안정하게 움직인다.
  • 데이터를 하나씩 처리하기 때문에 오차율이 크고, GPU의 성능을 전부 활용할 수 없다.
  • 손실 함수가 최솟값에 가는 과정이 불안정하다 보니 최적해(global minimum)에 정확히 도달하지 못할 가능성이 있다.
  • 배치 경사 하강법과 반대로 local minimum에 빠지더라도 쉽게 빠져나올 수 있습니다. 또한, global minimum을 찾을 가능성이 SGD가 더 크다.


(출처: https://velog.velcdn.com/images%2Fcha-suyeon%2Fpost%2F24b74f77-6a76-49ce-aefb-a2eb5aac356c%2Fimage.png)

에포크란, 확률적 경사 하강법에서 훈련 세트를 한번 모두 사용하는 과정을 말한다.
일반적으로 경사 하강법은 수십, 수백 번 이상의 에포크를 수행한다.

미니배치 경사 하강법: 배치의 크기를 사용자가 정하여 사용하는 방법이다. 정해진 배치의 크기만큼씩 데이터를 여러 묶음으로 나누어 각 묶음에 대하여 경사하강법을 적용한다.

  • 전체 데이터셋을 대상으로 한 SGD 보다 parameter 공간에서 shooting이 줄어든다.(미니배치의 손실 값 평균에 대해 경사 하강을 진행하기 때문에)
  • BGD에 비해 Local Minima를 어느정도 회피할 수 있다.
    최적해에 더 가까이 도달할 수 있으나 local optima 현상이 발생할 수 있다.
  • local optima의 문제는 무수히 많은 임의의 parameter로부터 시작하면 해결된다. → 학습량을 늘리면 해결됨
  • 배치 크기는 총 학습 데이터 셋의 크기를 배치 크기로 나눴을 때 딱 떨어지는 크기로 하는 것이 좋다.


(출처: https://velog.velcdn.com/images%2Fcha-suyeon%2Fpost%2F09b1801c-7819-4887-9c07-64d86da26420%2Fimage.png)

배치 경사 하강법: 전체 학습 데이터를 하나의 배치로(배치 크기가 n) 묶어 학습시키는 경사 하강법이다.

  • BGD은 한 스텝에 전체 데이터를 이용하기 때문에 연산 횟수가 적다.(1 epoch 당 1회 update)
  • 최적해에 대한 수렴이 안정적으로 진행된다.
    하지만 local optimal 상태가 되면 빠져나오기 힘들다.
  • parameter 업데이트를 위해 모든 학습 데이터에 대해 저장해야 하므로 데이터가 큰 경우 전체 데이터를 못 읽거나 많은 메모리가 필요하다.


(출처: https://velog.velcdn.com/images%2Fcha-suyeon%2Fpost%2F6fc384b9-1193-499b-8c43-e780b2951e56%2Fimage.png)

경사 하강법을 산에 비유하자면 산을 내려가는 길에 대한 것이다. 여기서 이 산이 손실 함수라고 할 수 있다. 그렇다면 손실 함수란 무엇일까?

손실함수란, 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준이다.

손실함수는 어떤 값이 최솟값인지 모르기 때문에 가능한 많이 찾아보고 만족할 만한 수준이면 그것이 최솟값임을 인정해야 한다. 다행히도 우리가 다루는 많은 문제에 필요한 손실 함수는 이미 정의되어 있다. 그럼 앞서 다루었던 생선을 분류하기 위해서는 어떤 손실함수를 사용해야 할까?

분류에서의 손실은 정답을 맞추지 못하는 것으로 비교적 확실한 편이다. 즉 정확도를 손실함수로 사용한다는 것이다. 하지만 정확도를 손실함수로 사용하게 되면 연속적이 아닌 이산적이게 되기에 경사 하강법을 통해 조금씩 움직일 수가 없다는 단점이 있다.

연속적인 손실 함수를 만들기 위해서는 로지스틱 회귀 모델을 이용하는 것이다. 이것을 이용한 손실 함수가 로지스틱 손실 함수(다중 분류에서는 크로스엔트로피 손실 함수라고도 부름)이다.

이제 확률적 경사 하강법을 사용한 분류 모델을 만들어 보자.

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)

사이킷런에서 확률적 경사 하강법을 제공하는 대표적인 분류용 클래스는 SGDClassifier이다.

from sklearn.linear_model import SGDClassifier

SGDClassifier의 객체를 만들 때 2개의 매개변수를 지정한다. loss는 손실 함수의 종류를 지정하는데 여기서 loss = 'log'로 지정해 로지스틱 손실 함수를 지정한다. max_iter는 수행할 에포크의 횟수를 지정한다. 10으로 지정해 전체 훈련 세트를 10회 반복해보겠다. 반복횟수가 적기에 정확도가 낮을 것으로 예측된다.

sc = SGDClassifier(loss='log', 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))

0.773109243697479
0.775

역시 반복횟수가 작기에 정확도가 낮은 것을 볼 수 있다. 확률적 경사 하강법은 아서 말했듯이 점진적 학습이 가능하다. SGDClssifier객체를 다시 만들지 않고, 훈련한 모델 sc를 추가로 더 훈련할 수 있다. 모델을 이어서 훈련하기 위해서는 partial_fit() 메서드를 사용한다.
이 메서드는 fit() 메서드와 사용법은 같지만 호출할 때마다 1 에포크씩 이어서 훈련할 수 있다.

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

0.8151260504201681
0.85

아직까지는 점수가 낮지만 에포크를 한번 더 실행하니 정확도가 높아졌다. 이 모델을 여러 에포크에서 더 훈련해 볼 필요가 있다. 하지만 여기서 문제는 얼마나 더 훈련해야 할까이다. 어떤 특정한 기준이 필요하다는 말이다. 이것도 3장에서 배웠던 과대적합, 과소적합 문제를 해결했듯이 그래프를 그려서 해결해 볼 수 있다.

import numpy as np
sc = SGDClassifier(loss='log', 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))
  
import matplotlib.pyplot as plt
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

데이터가 작기에 명확하게 드러나지는 않지만, 100번째 에포크 이후에는 훈련 세트와 테스트 세트의 점수가 조금씩 벌어지고 있다. 그래프를 통해 100번째 에포크가 적절한 반복 횟수인 것을 알 수 있다.

SGDClassifier의 반복 횟수를 100에 맞추고 모델을 다시 훈련해 보자.

sc = SGDClassifier(loss='log',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))

0.957983193277311
0.925

SGDClassifier은 일정 에포크 동안 성능이 향상되지 않으면 더 훈련하지 않고 자동적으로 멈춘다. tol이 매개변수에서 향상될 최솟값을 지정한다. 앞의 코드에서는 tol 매개변수를 None으로 지정해 자동적으로 멈추지 않고 max_iter=100만큼 무조건 반복하도록 했다.

오늘은 여기서 마치겠다.

0개의 댓글