확률적 경사 하강법은 대표적인 점진적 학습 알고리즘이다.
확률적 경사 하강법은 마치 산 꼭대기에서 경사를 따라 내려가는 방법이다. 모든 방법들 중 가장 빠른 길이 경사가 가장 가파른 길이다.
경사를 내려올 때 가장 가파른 길을 찾아 내려오는 것도 중요하지만, 조금씩 내려오는 것도 중요하다. 이렇게 조금씩 내려오는 과정이 바로 경사 하강법 모델을 훈련하는 것이다.
학습용 데이터셋으로 학습시킨 모델에서 가장 가파른 길을 찾는 방법은 무엇일까? 당연히 학습용 데이터셋 중 하나가 가장 가파른 길일 것이다.
하지만 전체 데이터셋을 사용하지 않고, 하나의 샘플을 랜덤하게 골라 가장 가파른 길을 찾는다.
학습용 데이터셋에서 하나의 샘플 선택 후 경사를 조금 하강, 다음은 또 학습용 데이터셋에서 다른 하나의 샘플 선택 후 경사를 조금 하강. 이것을 모든 셈플을 사용할 때까지 반복한다. 하지만 그래도 산을 다 내려오지 못했더라면, 다시 처음부터 학습용 데이터셋에서 샘플을 선택하여 산을 전부 내려올 때까지 반복한다.
확률적 경사 하강법에서 학습용 데이터셋을 모두 사용하는 과정을 에포크(epoch) 라고 부른다. 일반적으로 경사 하강법은 수십, 수백번 이상의 에포크를 수행한다.
하나의 샘플이 아닌, 무작위로 몇개의 샘플을 선택하여 내려가는 방법은 미니배치 경사 하강법(Minibatch Gradient Descent라 한다.
극단적으로 한 번의 경사를 내려가는 데에 전체 샘플을 사용할 수도 있다. 이를 배치 경사 하강법(Batch Gradient descent) 이 방법은 전체 데이터를 사용하기 때문에 가장 안정적인 방법이 될 수 있다. 하지만 전체 데이터를 사용하는 만큼 CPU를 많이 사용하게 된다. 데이터가 많을 경우에는 아예 읽지 못하는 경우도 생긴다.
우리가 내려가야 하는 산은 대체 무엇일까? 이 산을 바로 손실 함수(loss function) 이라 한다.
손실 함수
- 손실 함수는 머신러닝 알고리즘이 얼마나 엉터리인지 측정하는 기준이다. 당연히 손실 함수는 값이 작을 수록 좋다.
- 하지만 손실 함수 값이 작다는 절대값 기준이 없어, 어떤 값이 최솟값인지 모른다.
- 분류에서 손실은 아주 확실하다. 정답을 맞추지 못한 것이다. 예를들어 도미(1)이란 클래스의 피처를 보고 빙어(0)으로 분류를 한다면 정답을 맞추지 못한 것이다.
만약 샘플 4개의 예측을 [1, 0, 0, 1]이라 하였고 정답(타깃)은 [1, 1, 0, 0] 이며 예측 확률은 각각 0.9, 0.3, 0.2, 0.8이라 가정한다. 이 샘플에 대한 로지스틱 손실 함수를 만들어보자.
첫 번째 샘플의 예측 확률은 0.9이다. 양성 클래스의 타깃인 1과 곱한 다음 음수로 바꾸어준다.
두 번째 샘플의 예측 확률은 0.3이다 타깃은 양성 클래스인 1과 곱한 다음 음수로 바꾸어준다.
세 번째 샘플의 예측 확률은 0.2이다. 하지만 타깃이 음성 클래스 0이므로, 곱한 값은 무조건 0이 나온다.
이 때는 타깃을 양성클래스(1)로 바꾸어준다. 그대신 이 값을 예측하는 확률은 반대 확률이 되므로, 1-0.2 = 0.8이 된다. 후에 곱한 값을 음수로 바꾸어주자.
네 번째 샘플의 예측 확률은 0.8아며, 타깃 클래스는 0이다.
손실 함수의 값은 모두 [-0.9, -0.3, -0.8, -0.2] 가 나왔다. 첫 번째, 세 번째 샘플는 낮은 손실이며, 두 번째, 네 번째 샘플은 높은 손실을 보인다.
여기에서 더 좋은 방법은 예측 확률에 로그 함수를 적용하는 것이다. 예측 확률의 범위는 0~1이라 로그 함수를 취햐면 해당 값은 음수가 되기 때문에 최종적으로 나오는 손실 값은 양수가 된다.
이진 분류는 로지스틱 손실 함수를 사용하고, 다중 분류에서는 크로스엔트로피 손실 함수(Cross-entropy loss function) 을 사용한다.
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish_X = fish[fish.columns.difference(['Species'])].to_numpy()
fish_y = fish['Species'].to_numpy()
#데이터셋 나누기
from sklearn.model_selection import train_test_split
train_X, test_X, train_y, test_y = train_test_split(fish_X, fish_y, random_state=42)
# 데이터 스케일링: 표준화(Standard)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(train_X)
train_scaled = scaler.transform(train_X)
test_scaled = scaler.transform(test_X)
loss=log: 손실 함수의 종류는 로지스틱 손실 함수로 정한다.
max_iter: 반복 횟수(에포크(epoch))
확률적 하강 경사법(SDG)
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss='log', max_iter=10, random_state=42)
sc.fit(train_scaled, train_y)
print(sc.score(train_scaled, train_y))
print(sc.score(test_scaled, test_y))
#출력값: 0.7647058823529411
#출력값: 0.775
학습용 데이터셋, 테스트용 데이터셋의 점수(결정계수)가 낮다. 아마 에포크 횟수가 부족한 것으로 보인다.
아까 확률적 경사 하강법은 점진적 학습이 가능하다. SGD 모델을 다시 만들지 말고, partial_fit() 메서드를 이용하여 점진적으로 학습시키자.
해당 메서드는 호출할 때마다 1 에포크씩 이어서 훈련이 가능하다.
sc.partial_fit(train_scaled, train_y)
print(sc.score(train_scaled, train_y))
print(sc.score(test_scaled, test_y))
#출력값: 0.7983193277310925
#출력값: 0.8
아직 점수가 낮지만, 에포크를 한 번 더 실행하니 점수가 높아진다. 무작정 많이 에포크를 많이 반복할 수 없으므로, 어떤 기준을 정해야 함.
왜냐하면 반복 횟수가 많아질 수록 과대적합, 반복 횟수가 적을수록 과소적합 현상이 나타나기 때문이다.
학습용 데이터셋의 점수는 에포크가 진행될수록 꾸준히 증가한다. 하지만 테스트용 데이터셋의 점수는 높아지다가, 어느 순간 감소하기 시작한다.
바로 이 시점이 해당 모델이 과대적합되기 시작하는 곳이다. 이 때 훈련을 멈추는 것을 조기 종료라 한다.
에포크를 300번동안 늘려가며 변하는 점수를 확인해보자.
모델 구현
import numpy as np
sc = SGDClassifier(loss='log', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_y)
300번동안 에포크 반복하면서, 그 점수를 리스트에다 저장.
for i in range(300):
sc.partial_fit(train_scaled, train_y, classes=classes)
train_score.append(sc.score(train_scaled, train_y))
test_score.append(sc.score(test_scaled, test_y))
그래프 그리기
import matplotlib.pyplot as plt
plt.rc('font', family='AppleGothic')
plt.plot(train_score, label='학습용 데이터셋 점수')
plt.plot(test_score, label='테스트용 데이터셋 점수')
plt.xlabel('epoch')
plt.ylabel('score')
plt.legend()
plt.show()
데이터가 작아 잘 드러나지 않는다. 하지만 epoch가 100일 때를 기준으로 점수가 증가 후 일정하므로 epoch가 100일때가 적절한 반복 횟수로 보인다.
epoch를 100으로 설정하고, 다시 학습한다.
SGDClassfier은 일정 에포크동안 성능이 향상되지 않으면 더 학습하지 않고 자동으로 종료한다.
이것을 방지하기 위해 tol=None이라는 매개변수 추가.
sc = SGDClassifier(loss='log', tol=None, max_iter=100, random_state=42)
sc.fit(train_scaled, train_y)
print(sc.score(train_scaled, train_y))
print(sc.score(test_scaled, test_y))
#출력값: 0.957983193277311
#출력값: 0.925