책에서는 시나리오 예를 들어 잘 설명을 했는데 결론은
책 초반에는 데이터가 확정되어 있는 상태에서는 학습하고 결론을 끌어냈는데 이번 챕터에서는 훈련할 데이터가 계속 증가된다면 어떻게 처리할까이다.
앞에서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련하는걸 점진적 학습이라고 부른다.
이런 점진적 학습 알고리즘에 대표가 확률적 경사 하강법이라는 것이다.
개념적으로 접근해보자면 확률적 경사 하강법이라는 문장에 여러 힌트가 있다.
일단 경사 하강법이라는 문장은 경사를 따라 내려가는 방법 (목표점을 향한 기울기정도로 이해)
어떤 목표에 도달하기 위해 경사를 급하게 주면 목표를 지나칠수도 있기 때문에 천천히 조금씩 내려와야 한다. 하지만 샘플을 하나하나 학습해서 순차적으로 경사를 구하는게 아닌 확률적으로 랜덤하게 샘플을 골라서 학습하는게 바로 확률적 경사 하강법이라는 것이다.
산을 내려가는 방법을 목표을 달성하는 것이라고 한다면 모든 샘플을 사용해도 산을 내려오지 못했으면?
다시 처음부터 시작한다. 훈련 세트에 모든 샘플을 다시 채워놓고 랜덤하게 샘플을 이어서 경사를 내려간다. 이렇게 목표에 도달할때까지 계속 내려가면 된다.
확률적 경사 하강법에서 훈련 세트를 한 번 모두 사용하는 과정을 에포크라고 부른다. 일반적으로 경사 하강법은 수십, 수백번 이상 에포크를 수행한다.
이 때 1개씩 말고 무작위로 몇 개의 샘플을 선택해서 경사를 따라 내려가는 기법을 미니 배치 경사 하강법이라고 한다.
경사로를 한개씩이나 몇개씩 말고 샘플 몽땅 쓰는것을 배치 경사 하강법이라고 한다.
그런데 위에서 말한 산을 내려간다는게 정확히 어떤걸 말하는거냐 하면 바로 손실 함수를 말하는 것이다.
손실함수
손실함수는 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준이다. 따라서 손실함수의 값이 작을 수록 좋다. 하지만 어떤 값이 최솟값인지는 알지 못한다.
다행히 우리가 다루는 많은 문제에 필요한 손실 함수는 이미 정의되어 있다. 그럼 생선을 분류하기 위해서는 어떤 손실함수를 사용하는지 알아보자.
분류에서 손실은 아주 확실하다. 정답을 못 맞히는 거다.
예를 들어 도미와 빙어를 구분하는 이진 분류 문제를 보자.
도미는 양성클래스(1), 빙어는 음성 클래스(0)이라고 가정해보자. 아래처럼 예측과 정답이 있다고 상상해보자
예측 | 정답 | |
1 | 같음 | 1 |
0 | 같지않음 | 1 |
0 | 같음 | 0 |
1 | 같지않음 | 0 |
정확도는 4개의 예측 중에 2개만 맞았으므로 정확도는 1/2 = 0.5 이다. 정확도를 손실함수로 사용해도 될까?
하지만 정확도에는 치명적인 단점이 있다. 예를 들어 앞의 예제와 같이 4개의 샘플만 잇다고 가정하면 정확도는 0, 0.25, 0.5, 0.75, 1 다섯까지 뿐이다. 정확도가 이렇게 듬성듬성하면 경사 하강법을 이용해서 조금씩 움직일 수 없다. 산의 경사면은 연속적이어야 한다고 한다.
(기술적으로 손실 함수는 미분 가능해야 한다고 함)
로지스틱 손실 함수 또는 이진 크로스엔트로피 손실 함수
그럼 어떻게 연속적인 손실 함수를 만들 수 있을까? 예측은 0 또는 1이지만 확률은 0~1 사이의 어떤 값도 될 수 있다. 즉 연속적이다.
가령 위의 샘플 4개의 예측 확률을 각각 0.9, 0.3, 0.2, 0.8 이라고 가정하면
양성 클래스의 타깃인 1과 곱한 다음 음수로 바꿨을때 1에 가까울수록 좋은 모델이다.
세번째, 네번째 타깃은 음성클래스가 0인데 이걸 그대로 곱하면 0이 나오므로 양성 클래스에 대한 예측으로 바꿔야 함
즉 1 - 0.2 = 0.8로 사용해야 함.
여기에서 예측 확률에 로그 함수를 적용하면 더 좋다. 예측 확률의 범위는 0~1사이인데 로그 함수는 이 사이에서 음수가 되므로 최종 손실 값은 양수가 된다. 손실이 양수가 되면 이해하기 더 쉽다.
타깃 | 예측 | 정답 | 손실율 |
1 | 0.9 | 1 | -0.9 (낮은 손실율) |
1 | 0.3 | 1 | -0.3 (높은 손실율) |
0 | 02 -> 0.8 | 1 | -0.8 (낮은 손실율) |
0 | 0.8 -> 0.2 | 1 | -0.2 (높은 손실율) |
정리하면 양성 클래스(타깃 = 1)일때 손실은 -log(예측 확률)로 계산한다. 확률이 1에서 멀어질수록 손실은 아주 큰 양수가 된다.
음성 클래스(타깃 = 0)일때 손실은 -log(1 - 예측 확률)로 계산한다. 이 예측 확률이 0에서 멀어질수록 손실은 아주 큰 양수가 된다.
이렇게 손실 함수를 정의했고 이 손실함수를 로지스틱 손실 함수 또는 이진 크로스엔트로피 손실 함수라고 부른다.
손실함수를 직접 계산하는 일은 드물다 머신러닝 라이브러리가 처리해주니까
하지만 손실 함수가 무엇이고 어떻게 작동하는지 왜 정의를 해야 하는지 이해를 위해 코드를 살펴보자
SGDClassifier
데이터를 준비하고, 훈련세트와 테스트 세트로 나누고 표준화까지 한번에 보자
이제 사이킷런에서 제공하는 확률적 경사 하강법을 적용해서 학습하고 점수를 보자.
SGDClassifier라는 분류용 클래스를 사용했는데 매개변수로 loss는 손실함수의 종류를 지정하는데 여기서는 log로 지정하여 로지스틱 손실 함수를 지정했다. max_iter는 수행할 에포크 횟수를 지정한다. 10으로 지정해서 전체 훈련세트를 10회 반복했다.
그 다음 나온 점수는 0.773, 0775 정도로 다른 알고리즘에 비해 점수가 낮다.
하지만 확률적 경사 하강법은 점진적 학습이 가능하다. SGDClassifier 객체를 다시 만들지 않고 훈련한 모델 sc를 추가로 더 훈련해보자.
아직도 점수가 낮지만 에포크를 한 번 더 실행하니 정확도가 향상되었다. 이 모델을 여러 에포크서 더 훈련해야 겠는데 얼마나 더 훈련해야 할까?
에포크와 과대/과소 적합
확률적 경사 하강법을 사용한 모델은 에포크 횟수에 따라 과소적합이나 과대적합이 될 수 있다. 에포크 횟수가 적으면 모델이 훈련 세트를 덜 학습하고 목표를 달성하기 전에 훈련이 끝날수 있다. 그러면 에포크를 충분히 늘려서 훈련을 해야 하는데 훈련세트에 너무 최적화 된 모델이 될수 있다. 과대 적합이 시작하기 전에 훈련을 멈추는것을 조기 종료라고 한다.
7종류의 생선 분류에 확률적 경사 하강법으로 300번의 에포크 동안 훈련 반복하여 진행하자. 그리고 각 에포크별로 점수를 기록하자.
그걸 그래프를 그려서 확인해보자.
대략 백 번째 에포크 이후에는 훈련 세트와 테스트 세트의 점수가 조금씩 벌어지고 있다. 이 모델의 경우 백 번째 에포크가 적절한 반복 횟수로 보인다.
그럼 SGDClassifier의 반복 횟수를 100에 맞추고 모델을 다시 훈련해 보자.
SGDClassifier는 일정 에포크 동안 성능이 향상되지 않으면 더 훈련하지 않고 자동으로 멈춘다. tol 매개변수에서 향상될 최솟값을 지정하는데 여기선 None으로 지정하여 자동으로 멈추지 않고 max_iter=100만큼 무조건 반복하도록 하였다.
최종점수가 좋게 나왔다. 미션 클리어다.
챕터를 마무리하기 전에 SGDClassifier의 loss 매개 변수로 원래 기본값은 hinge 힌지 손실이라는 서포트 벡터 머신이라 불리는 또다른 손실 함수인데 이 책에서는 다루지 않는다.
최종코드
# 확률적 경사 하강법
## SGDClassifier
### 4-1장의 데이터 준비과정 복습
### 데이터 준비하기
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)
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))
print(sc.score(test_scaled, test_target))
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))
### 과대/과소 적합
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()
sc = SGDClassifier(loss='log', max_iter=100, random_state=42, tol=None)
sc.fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))