기존 훈련 데이터에 새로운 데이터를 추가하여 모델 매일매일 다시 훈련해보자! → 근데 시간이 지날수록 데이터가 늘어난다. 몇 달이 지나면 모델을 훈련하기 위해 서버를 늘려야한다. 지속 가능한 방법은 아님!
또 다른 방법은 새로운 데이터 추가할 때 이전 데이터 버려서 훈련 데이터의 크기를 일정하게 유지하는 방법 → 데이터 버릴 때 다른 데이터에는 없는 중요한 생선 데이터가 같이 버려지면 큰일!
위의 두 방법 말고 새로운 데이터만 조금씩 더 훈련 시켜보자! → 점진적 학습 또는 온라인 학습이라고 부른다.
확률적 경사 하강법(Stochastic Gradient Descent)이 대표적인 점진적 학습 알고리즘.
확률적 경사 하강법에서 확률적이란 말은 '무작위하게' 혹은 '랜덤하게'의 기술적인 표현이다. 다음 '경사'는 기울기를 말함. 다시 말해 경사 하강법은 경사를 따라 내려가는 방법을 말한다.
경사 하강법이 바로 가장 가파른 경사를 따라 원하는 지점에 도달하는 것이 목표이다. 가장 가파른 길을 찾아 내려오지만 조금씩 내려오는 것이 중요하다. 이렇게 내려오는 과정이 바로 경사 하강법 모델을 훈련하는 것.
확률적이란 말은 경사 하강법으로 내려올 때 가장 가파른 길을 찾는 방법은? → 훈련 세트 사용해 모델 훈련하기 때문에 경사 하강법도 당연히 훈련 세트를 사용해 가장 가파른 길 찾을 것. 그런데 전체 샘플 사용하지 않고 딱 하나의 샘플을 훈련 세트에서 랜덤하게 골라 가장 가파른 길 찾는다! 이처럼 랜덤하게 하나의 샘플을 고르는 것이 바로 확률적 경사 하강법이다.
조금 더 자세히 설명하자면 확률적 경사 하강법은 훈련 세트에서 랜덤하게 하나의 샘플을 선택하여 가파른 경사르 조금 내려간다. 그 다음 훈련 세트에서 랜덤하게 또 다른 샘플을 하나 선택하여 경사를 조금 내려간다. 이런 식으로 전체 샘플 사용할 때까지 계속한다.
모든 샘플을 다 사용했는데도 못 내려왔다면???? 다시 처음부터 시작!!!
훈련 세트에 모든 샘플 다시 채워 넣음. 그 다음 다시 랜덤하게 하나의 샘플 선택해 이어서 경사를 내려간다. 이렇게 만족할만한 위치에 도달할 때까지 계속 내려간다.
에포크(epoch) : 확률적 경사 하강법에서 훈련 세트를 한 번 모두 사용하는 과정. 일반적으로 경사 하강법은 수십, 수백 번 이 상 에포크 수행한다.
무작위로 샘플 선택해서 산 내려가면 너무 무책임 하니 아주 조금씩 내려가야 한다.
미니배치 경사 하강법(minibatch gradient descent) : 여러 개의 샘플 사용해 경사 하강법 수행하는 방식.
배치 경사 하강법(batch gradient descent) : 극단적으로 한 번 경사로를 따라 이동하기 위해 전체 샘플 사용하는 방식. 전체 데이터 사용하기 때문에 가장 안정적인 방법될 수 있지만 그만큼 컴퓨터 자원 많이 사용하게 되어 전체 데이터 모두 읽을 수 없을지도 모른다.
+확률적 경사 하강법과 신경망 알고리즘
신경망 알고리즘은 확률적 경사 하강법을 꼭 사용해야 한다. 신경망은 일반적으로 ㅁ낳은 데이터를 사용하기 때문에 한 번에 모든 데이터 사용하기 어렵다. 또 모델이 매우 복잡하기 땜누에 수학적인 방법으로 해답 얻기 어렵다. 신경망 모델이 확률적 경사 하강법이나 미니배치 경사 하강법을 사용한다는 점 꼭 기억!
손실함수(loss function)는 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지 측정하는 기준! 손실함수의 값이 작을수록 좋지만 어떤 값이 최솟값인지 알지는 못한다. 가능한 많이 찾아보고 만족핢나한 수준이라면 산을 다 내려왔다고 인정해야 한다. 이 값 찾아 조금씩 이동하려면 경사 하강법이 잘 맞는다!
분류에서 손실은 아주 확실. 정답 못 맞히는 것!
예를 들어, 도미는 양성 클래스(1), 빙어는 음성 클래스(0)라고 가정.
[예측 정답(타깃)
1 = 1
0 ≠ 1
0 = 0
1 ≠ 0] 이라고 가정해보자.
4개의 예측 중에 2개만 맞았으므로 정확도는 1/2 = 0.5이다. 하지만 정확도가 듬성듬성하다면 경사 하강법을 이용해 조금씩 움직일 수 없다.
+ 기술적으로 말하면 손실함수는 미분이 가능해야한다.
첫 번째 샘플의 예측은 0.9이므로 양성 클래스의 타깃인 1과 곱한 다음 음수로 바꿀 수 있다. 이 경우 예측이 1에 가까울수록 좋은 모델이다. 예측이 1에 가까울수록 예측과 타깃의 곱의 음수는 점점 작아진다.
두 번째 샘플의 예측은 0.3이다. 타깃이 양성 클래스(1)인데 거리가 멀다. 위에서와 마찬가지로 예측과 타깃을 곱해 음수로 바꿔보면 -0.3이 되기 때문에 확실히 첫 번째 샘플보다 높은 손실이 된다!
세 번째 샘플의 타깃은 음성 클래스라 0이다. 이 값을 예측 확률인 0.2와 그대로 곱해서는 무조건 0이 되기 때문에 곤란하다. 이럴 때 방법은 타깃을 마치 양성 클래스처럼 바꾸어 1로 만드는 것! 대신 예측값도 양성 클래스에 대한 예측으로 바꾼다. 즉, 1 - 0.2 = 0.8로 사용하고 곱하고 음수로 바꾸는 것은 위와 동일하다.
세 번째 샘플은 음성 클래스인 타깃을 맞추었으므로 손실이 낮아야 한다. -0.8은 꽤 낮은 손실이다. 네 번째 샘플도 음성 클래스. 하지만 정답을 맞추진 못했다. 타깃을 1로 바꾸고 예측 확률을 1에서 뺸 다음 곱해서 음수로 바꿔보자!
네 번째 샘플의 손실이 높다. 예측 확률을 사용해 이런 방식으로 계산하면 연속적인 손실 함수를 얻을 수 있을 것이다. 여기서 예측 확률에 로그 함수를 적용하면 좋다. 예측 확률의 범위는 0~1 사이인데 로그 함수는 이 사이에서 음수가 되므로 최종 손실 값은 양수가 된다. 손실이 양수가 되면 이해하기 더 쉽다! 또 로그 함수는 0에 가까울수록 아주 큰 음수가 되기 때문에 손실을 아주 크게 만들어 모델에 큰 영향 미칠 수 있다!
정리하면 아래 그림과 같다. 양성 클래스(타깃 = 1)일 때 손실은 -log(예측확률)고 계산. 확률이 1에서 멀어질수록 손실이 아주 큰 양수가 된다. 음성 클래스(타깃 = 0)일 때 손실은 -log(1-예측확률)로 계산. 이 예측확률이 0에서 멀어질수록 손실은 아주 큰 양수가 된다.
이 손실 함수를 로지스틱 손실 함수(logistic loss function) 또는 이진 크로스엔트로피 손실 함수(binary cross-entropy loss function)라고 부른다.
다중 분류에서 사용하는 손실 함수는 크로스엔트로피 손실 함수(cross_entropy loss function)라고 부른다.
이 손실 함수를 우리가 직접 만드는 일은 없다! 이미 문제에 잘 맞는 손실 함수가 개발되어 있기 때문!
+회귀에는 어떤 손실 함수를 사용???
평균 절댓값 오차 사용할 수 있다. 타깃에서 예측을 뺸 절댓값을 모든 샘플에 평균한 값. 또는 평균 제곱 오차(mean squared error)를 많이 사용. 타깃에서 예측을 뺸 값을 제곱한 다음 도느 샘플에 평균한 값. 이 값이 작을수록 좋은 모델!
이번에도 fish_csv_data 파일에서 판다스 데이터프레임 만들어보자.
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
# Species 열 제외한 나머지 5개는 입력 데이터로 사용. Species 열은 타깃 데이터
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
fish_target = fish['Species'].to_numpy()
# 사이킷런의 train_test_split() 함수 사용하여 데이터를 훈련 세트와 테스트 세트로 나눈다.
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. sklearn.linear_model 패키지 아래에서 임포트.
from sklearn.linear_model import SGDClassifier
SGDClassifier의 객체를 만들 때 2개의 매개변수 지정. loss는 손실 함수의 종류를 지정. 여기서는 loss='log'로 지정하여 로지스틱 손실 함수를 지정. max_iter는 수행할 에포크 횟수를 지정. 10으로 지정하여 전체 훈련 세트를 10번 반복. 그 다음 훈련 세트와 테스트 세트에서 정확도 점수 출력한다.
+다중 분류일 경우 SGDClassifier에 loss='log'로 지정하면 클래스마다 이진 분류 모델을 만든다. 즉 도미는 양성 클래스로 두고 나머지를 모두 음성 클래스로 두는 방식. 이런 방식을 OvR(One versis Rest)이라고 부른다.
sc = SGDClassifier(loss='log_loss', 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
출력된 훈련 세트와 테스트 세트 정확도가 낮다. 아마도 지정한 반복 횟수 10번이 부족한 듯 ㅠㅠ
확률적 경사 하강법은 점진적 학습이 가능하므로 SGDClassifier 객체를 다시 만들지 않고 훈련한 모델 sc를 추가로 더 훈련해보자. 모델을 이어서 훈련할 때는 partial_fit() 메서드 사용.
이 메서드는 fit() 메서드와 사용법 같지만 호출할 때마다 1 에포크씩 이어서 훈련할 수 있다. partial_fit() 메서드를 호출하고 다시 훈련 세트와 테스트 세트의 점수를 확인해보자.
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
아직 점수 낮지만 에포크 한 번 더 실행하니 정확도 향상! 이 모델을 여러 에포크에서 더 훈련해볼 수 있는데, 얼마나 더 훈련해야할지 기준이 필요하다.
+이런 배치 경사 하강법 아닌가요?
tarin_scaled와 train_target을 한꺼번에 모두 사용했으니 확률적 경사 하강법이 아닌 배치 경사 하강법 아닌가 하겠지만 아니다. SGDClassifier 객체에 한 번에 훈련 세트 전체를 전달했지만 이 알고리즘은 전달한 훈련 세트에서 1개씩 샘플을 꺼내어 경사 하강법 단계를 수행한다. 아쉽지만 SGDClassifier는 미니배치 경사 하강법이나 배치 하강법을 제공하지 않는다.
확률적 경사 하강법 사용한 모델은 에포크 횟수에 따라 과소적합이나 과대적합이 될 수 있다. 에포크 횟수가 적으면 모델이 훈련 세트를 덜 학습한다. 에포크 횟수가 충분히 많으면 훈련 세트를 완전히 학습할 것. 훈련 세트에 아주 잘 맞는 모델이 만들어진다.
바꾸어 말하면 적은 에포크 횟수 동안에 훈련한 모델은 훈련 세트와 테스트 세트에 잘 맞지 않는 과소적합된 모델일 가능성이 높다. 반대로 많은 에포크 횟수 동안에 훈련한 모델은 훈련 세트에 너무 자 맞아 테스트 세트에는 오히려 점수가 나쁜 과대적합된 모델인 가능성이 높다.
이 그래프는 에포크가 진행됨에 따라 모델의 정확도를 나타낸 것. 훈련 세트 점수는 에포크가 진행되루록 꾸준히 증가하지만 테스트 세트 점수는 어느 순간 감소한다. 바로 이 지점이 과대적합되기 시작하는 곳. 과대적합이 시작하기 전에 훈련을 멈추는 것을 조기 종료(early stopping)라고 한다.
이 예제에서는 fit() 메서드 사용하지 않고 partial_fit() 메서드만 사용. 이 메서드만 사용하려면 훈련 세트에 있는 전체 클래스의 레이블을 partial_fit() 메서드에 전달해 주어야 한다. 이를 위해 np.unique() 함수로 train_target에 있는 7개 생선의 목록을 만든다. 또 에포크마다 훈련 세트와 테스트 세트에 대한 점수를 기록하기 위해 2개의 리스트 준비.
import numpy as np
sc = SGDClassifier(loss='log_loss', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_target)
300번의 에포크 동안 훈련 반복하여 진행. 반복마다 훈련 세트와 테스트 세트의 점수르 계산하여 train_score, test_score 리스트에 추가.
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()
데이터가 작기 때문에 아주 잘 드러나지는 않지만, 백 번째 에포크 이후에느 훈련 세트와 테스트 세트의 점수가 조금씩 벌어지고 있음. 또 확실히 에포크 초기에는 과소적합되어 훈련 세트와 테스트 세트의 점수가 낮다. 이 모델의 경우 백 번째 에포크가 적절한 반복 횟수로 보인다.
그럼 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는 일정 에포크 동안 성능이 향상되지 않으면 더 훈련하지 않고 자동으로 멈춘다. to; 매개변수에서 향상될 최솟값을 지정한다. 앞의 코드에서는 tol 매개변수를 None으로 지정하여 자동으로 멈추지 않고 max_iter=100 만큼 무조건 반복하도록 했다.
최종 점수가 좋다! 훈련 세트와 테스트 세트에서의 정확도 점수가 비교적 높게 나왔다. 확률적 경사 하강법 사용한 생선 분류 문제도 성공적으로 수행!!
# 힌지 손실 예
sc = SGDClassifier(loss='hinge', 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.9495798319327731
0.925
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_loss', 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_loss', 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_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))
scikit-learn