[혼공머신] 4-2장 확률적 경사 하강법

Changh2·2024년 11월 5일
0

[혼자 공부하는 머신러닝+딥러닝] 교재 4장을 기반으로 작성되었습니다.


럭키백 이벤트가 많은 인기를 얻어 새로운 생선을 많이 추가해야하는데 훈련데이터를 얻는데 시간이 오래 걸린다. 기다리기만 할 수는 없어서, 학습 방법을 생각해봤다.
1. 기존의 훈련 데이터에 새로운 데이터를 추가하여 모델을 매일 다시 훈련
--> 초반엔 괜찮지만 데이터가 쌓여갈수록 서버의 용량이 늘어난다.
2. 새로운 데이터를 추가할 때 이전 데이터를 버려 데이터의 크기를 일정하게 유지
--> 아주 중요한 데이터가 버려질 수 있음
3. 이전에 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련
--> Good

점진적인 학습

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


확률적 경사 하강법

= SGD: Stochastic Gradient Descent

우선, 경사 하강법이란 말그대로 '경사를 따라 내려가는 방법'을 말하는데, 자세히는
손실함수의 그래프의 가장 낮은 지점을 향해 내려가는 방법이다. (가장 빠르게!) == (가장 가파른 길로!)

경사 하강법은 훈련 세트를 사용하여 가장 가파른 길을 찾는데,
딱 하나의 샘플을 훈련 세트에서 랜덤하게 골라 가장 가파른 길을 찾는다.
여기서 '랜덤하게' 하나의 샘플을 고르는 것이 바로 '확률적' 경사 하강법이다

모든 샘플을 다 사용해도 최하점을 찾지 못 했으면 어떻게 될까?
다시 처음부터 시작한다!
여기서 훈련 세트를 한번 모두 사용하는 과정에포크(epoch)라고 한다.
실제 경사하강법은 수십, 수백번의 에포크를 수행한다.

훈련 세트에서 여러 개의 샘플을 꺼내서 경사를 내려가면 미니배치 경사 하강법,
훈련 세트의 모든 샘플을 꺼내서 경사를 내려가면 배치 경사 하강법 이라고 한다.


손실 함수

손실 함수란, 모델이 예측한 값과 실제 값 사이의 차이를 측정하는 함수 이다.
이를 통해 알고리즘이 얼마나 엉터리인지 측정할 수 있다.

기본적으로 손실 함수는 미분 가능해야한다.
즉, 값이 듬성듬성 있지 않고 연속적이어야 한다.

이렇게 연속적인 손실 함수를 만들기 위해서 확률값을 사용한다.
(확률은 0과 1사이의 연속적인 값을 갖는다)


로지스틱 손실 함수

예측	    정답(타깃)
 1	  =	   1
 0	 !=	   1
 0	  =	   0
 1	 !=	   0
 

로지스틱 회귀의 모델이 확률을 출력했던걸 다시 생각하고,
위 샘플 4개의 예측 확률을 각각 0.9, 0.3, 0.2, 0.8 이라고 가정해보자.

위와 같이 예측값과 타깃값을 곱한 뒤 음수로 바꿔 적절한 손실 함수값을 구할 수 있다.
3번째, 4번째 샘플은 타깃이 0이므로 그냥 곱해주면 안 되고,
타깃을 1로 설정하고 예측값도 그에 맞게 1 - (0에대한 예측값)으로 바꿔줘야 한다.

이렇게 간단한 손실 함수가 얻어졌는데, 이보다 좋은 방법은 예측값에 로그함수를 씌우는 방법이다. 로그함수를 씌우면 모델에 끼치는 영향을 크게 만들 수 있다. (로그 함수는 0에 가까울수록 아주 큰 음수가 된다)

이렇게 정의된 이진 분류에서의 손실 함수는 로지스틱 손실함수 혹은
이진크로스엔트로피 손실 함수(binary cross-entropy) 라고 부른다.

다중 분류에서의 손실 함수는 크로스엔트로피 손실 함수(cross-entropy)라고 부른다.

회귀 모델에서는 평균 제곱 오차(MSE:Mean Squared Error)를 많이 사용한다.


SGDClassifier

이제 확률적 경사 하강법을 사용한 분류 모델을 만들어 보자.
먼저, 데이터를 준비하고 전처리를 해주자.

import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
							  			  # Species 열을 제외하고 입력 데이터로 사용
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
fish_target = fish['Species'].to_numpy()  # Species 열은 타깃 데이터
from sklearn.model_selection import train_test_split
  # 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의 매개변수

SGDClassifier의 객체를 만들 때 여러 매개변수를 지정한다.
loss는 손실 함수의 종류를 지정하고 (기본값 = hinge 손실 함수를 위한 'hinge'),
max_iter는 수행할 에포크 횟수를 지정한다.(기본값 = 1000)
추가적으로,
penalty는 규제의 종류를 지정할 수 있고 (기본값 = L2 규제를 위한 'l2'),
tol은 반복을 멈출 조건을 명시할 수 있는데, (기본값 = 0.001)
n_iter_no_chage에서 지정한 에포크 동안 손실이 tol 만큼 줄어들지 않으면 알고리즘이 중단된다. (기본값 = 5)

from sklearn.linear_model import SGDClassifier

sc = SGDClassifier(loss='log_loss', max_iter=10, random_state=42)
				# 로지스틱 손실 함수 사용, 훈련 세트 10회 반복
sc.fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))
>>> 0.773109243697479
	0.775
    
# 정확도가 낮은걸 보니 반복 횟수 10번으론 부족한 것 같다.

글의 초반에 얘기한 것처럼 확률적 경사 하강법은 점진적 학습이 가능하다.
즉, SGDClassifier 객체를 다시 만들지 않고 이어서 데이터를 추가해 모델을 훈련시킬 수 있다.
이렇게 이어서 훈련할 때는 partial_fit() 메서드를 사용한다.

partial_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

결과를 보니 더 훈련해야될거 같은데, 얼마나 더 훈련해야 되는걸까? 기준이 필요할 것 같다.


에포크와 과대/과소적합

확률적 경사 하강법을 사용한 모델은 에포크 횟수에 따라 과소적합이나 과대적합이 될 수 있다.
이를 나타내는 그래프는 아래와 같다.

그래프를 보면 테스트 세트 점수가 증가하다가 감소하는 순간이 있는데, 이 지점부터 과대적합된다.과대적합이 시작되기 전에 훈련을 멈추는 것을 조기 종료(early stopping)라고 한다.

적절한 반복횟수, 즉, 적절한 에포크를 찾기위해
partial_fit() 메서드를 사용해여 훈련을 반복 진행해보자.

# 점수 리스트, 생선 목록 준비
import numpy as np
sc = SGDClassifier(loss='log_loss', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_target)  # train_target에 있는 7개 생선의 목록을 준비 

# 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 에포크가 적절한 반복 횟수로 보여진다!
SGDClassifier의 반복 횟수를 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))
>>> 0.957983193277311
	0.925

성공적인 결과가 나왔다!

추가 +)
SGDClassifier은 확률적 경사 하강법을 사용한 분류 모델이고
SGDRegressor로 확률적 경사 하강법을 사용한 회귀 모델이 제공된다.

profile
Shoot for the moon!

0개의 댓글