2주차도 감사하게 우수혼공적으로 선정되어 토네이노를 받게 되었네요
공부하면서 맛있는 간식까지 서비스로 다들 혼공학습단 해보세요 개꿀이에요 ^^
그리고 혼공족장님이 칭찬도 해주셨어요 ^^
그러니 이번에도 파이팅하고 달려봅시다
한 생선가게가 생선 랜덤박스를 준비했습니다
거기에는 7개의 종류 생선이 랜덤으로 들어갔습니다
하지만 완전 랜덤으로 알려주기에는 조금 그래서
들어간 생선의 무게, 크기 등이 주어졌을 때 7개 생선에 대한 확률을 알려주기로 했습니다
그러면 확률을 어떻게 알려주는게 좋을까요?
생선가게는 "k-최근접 이웃은 주변 이웃을 찾아주니까
이웃의 클래스 비율을 확률이라고 출력하면 되지 않을까?" 라고 생각을 했습니다
마침 사이킷선의 k-최근접 이웃 분류기도 이와 동일한 방식으로
클래스 확률을 계산하여 제공하는데 그러면 바로 해보도록 하겠습니다!!
import pandas as pd
fish = pd.read_csv("https://bit.ly/fish_csv_data")
fish.head()
판다스의 read_csv()
함수로 CSV 파일을 데이터프레임으로 변환한 다음
head()
메서드로 처음 5개 행을 출력했다
데이터프레임은 판다스에서 제공하는 2차원 표 형식의 주요 데이터 구조로
넘파이 배열과 비슷하게 열과 행으로 이루어져 있다데이터프레임은 통계와 그래프를 위한 메서드를 풍부하게 제공하고,
또 데이터프레임은 넘파이로 상호 변환이 쉽고 사이킷런과도 잘 호환된다
그러면 이 데이터에서 어떤 종류의 생선이 있는지 Species를 찾아보자
판다스의 unique()
함수를 사용하면 된다
print(pd.unique(fish['Species'])) # ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
우리는 이 데이터에서 Species 열을 타깃으로 만들고
나머지 5개 열은 입력 데이터로 사용할 것이다
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()
데이터프레임에서 여러 열을 선택하면 새로운 데이터프레임이 반환된다
이를 to_numpy()
메서드로 배열로 바꾸어 fish_input에 저장을 했다
print(fish_input[:5])
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)
이와 같은 방법으로 타깃 데이터도 만들고 train_test_split을 사용해서
훈련 데이터와 테스트 데이터도 나눴다 그리고
StandardScaler를 사용해서 표준화 역시도 진행해줬다
이제 준비가 끝났으니 본격적으로 k-최근접 이웃 분류기로 테스트 데이터를 예측해보자
from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled,train_target)
print(kn.score(train_scaled, train_target)) # 0.8907563025210085
print(kn.score(test_scaled,test_target)) # 0.85
전에 했던 것처럼 KNeighborsClassifier 클래스 객체를 만들고
훈련 데이터로 훈련한 다음 훈련 데이터의 점수와 테스트 데이터의 점수를 확인해봤다
앞서 우리가 fish 데이터를 확인했을 때 총 7개의 종이 있었다
이처럼 타깃 데이터에 2개 이상의 클래스가 포함된 문제를
다중 분류(multi-class classification)라고 한다
우리가 전에 이중 분류를 했던 방법과 크게 다르지는 않다
다중 분류에서도 타깃값을 숫자로 바꾸어 입력할 수 있지만
사이킷런에서는 편리하게도 문자열로 된 타깃값을 쓸 수 있다
이 때 주의할 점이 있다 타깃값을 그대로 사이킷런 모델에 전달하면
순서가 자동으로 알파벳 순으로 매겨진다
따라서 pd.unique(fish['Species'])
로 출력했던 순서와 다르다
KNeighborsClassifier에서 정렬된 타깃값은 classes_
속성에 저장되어 있다
print(kn.classes_) # ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
'Bream'이 첫 번째 클래스, 'Parkki'가 두 번째 클래스가 되는 식이다
하지만 predict()
메서드는 친절하게 타깃값으로 예측을 출력해준다
print(kn.predict(test_scaled[:5])) # ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
그렇다면 이 5개 샘플에 대한 예측은 어떤 확률로 만들어질까?
사이킷런의 분류 모델은 predict_proba()
메서드로 클래스별 확률값을 반환하는데
이걸 사용해보자
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))
넘파이의 round()
함수는 기본으로 소수점 첫째 짜리에서 반올림을 하는데
decimals
매개변수로 유지할 소수점 아래 자릿수를 지정할 수 있다
predict_proba()
메서드의 출력 순서는 앞서 보았던 classed_ 속성과 같다
즉 첫 번째 열 'Bream'에 대한 확률, 두 번째 열이 'Parkki'에 대한 확률이다
그러면 이 모델이 계산할 확률이 가장 가까운 이웃의 비율이 맞는지 확인을 해보자
이를 위해 4번째 샘플의 최근접 이웃의 클래스를 확인해보자
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes]) # [['Roach' 'Perch' 'Perch']]
kneighbors()
메서드의 입력은 2차원 배열이어야 한다
그래서 넘파이 배열의 슬라이싱 연산자를 사용했다
슬라이싱 연산자는 하나의 샘플만 선택해도 항상 2차원 배열이 만들어진다
결과를 확인해보면 이 샘플의 이웃은 Roach 1개이고 세 번째 클래스인 Perch가 2개이다
그렇기 각각 0.3333..., 0.6666 이렇게 확률이 나오는 것 같다
그런데 생각을 해보면 확률이 너무 제한적이다
왜냐하면 주변의 3개의 클래스만 확인을 하니
가능한 확률은 0/3, 1/3, 2/3, 3/3 뿐이다
너무 제한적이지 않는가!!!!
로지스틱 회귀(logistic regression)는 이름은 회귀이지만 분류 모델이다
이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다
z = a x (Weight) + b x (Length) + c x (Diagonal) + d x (Height) + e x (Width) + f
여기에서 a,b,c,d,e는 가중치 혹은 계수이다
특성은 늘어났지만 다중 회귀를 위한 선형 방정식과 같다
z는 어떤 값도 가능하지만 확률이 되려면 0~1(또는 0~100%) 사이 값이 되어야 한다
그래서 z가 아주 큰 음수일 때에는 0으로 아주 큰 양수일 때 1이 되도록 바꾸는 방법이 있다
그게 바로 시그모이드 함수 (Sigmoid function) = 로지스틱 함수 (logistic function)이다
다음의 식이 시그모이드 함수인데 선형 방정식의 출력 z의 음수를 사용해
자연 상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다
이렇게 하면 z가 무한하게 큰 음수일 경우 이 함수는 0에 가까워지고,
z가 무한하게 큰 양수가 될 때는 1에 가까워진다
또 z가 0이 될 때에는 0.5가 된다
결국 z는 어떤 값이 되더라도 0~1 사이의 범위를 벗어날 수 없게 된다
그래서 우리는 0~1 사이의 값을 0~100%까지 확률로 해석을 할 수 있다
import numpy as np
import matplotlib.pyplot as plt
z = np.arange(-5.5,0.1)
phi = 1 / (1+np.exp(-z))
plt.plot(z, phi)
plt.xlabel('z')
plt.ylabel('phi')
plt.show()
-5와 5 사이에 0.1 간격으로 배열 z를 만든 다음 z 위치마다 시그모이드 함수를 계산했다
보면 알 수 있듯이 시그모이드 함수의 출력은 0에서 1까지 변하는 걸 확인할 수 있었다
그러면 이제 로지스틱 회귀 모델을 훈련해볼 것인데
사이킷런에서는 로지스틱 회귀 모델인 LogisticRegression라는 클래스를 제공한다
본격적으로 훈련하기 전에 간단한 이진 분류를 먼저 해보자
이진 분류일 경우 시그모이드 함수의 출력이 0.5보다 크면 양성 클래스
0.5보다 작으면 음성 클래스로 판단을 한다
정확히 0.5이면?
라이브러리마다 다를 수 있지만 사이킷런에서는 음성 클래스로 판단한다
넘파이 배열은 True, False 값을 전달하여 행을 선택할 수 있다
이를 불리언 인덱싱이라고 한다
예를 바로 들어보자
char_arr = np.array(['A','B','C','D','E'])
print(char_arr[[True,False,True,False,False]]) # ['A' 'C']
다음과 같이 5개로 이루어진 배열이 있다고 해보자
여기서 A와 C만을 골라내려면 첫 번째와 세 번째 원소만 True이고
나머지 원소는 모두 False인 배열을 전달하면 된다
이와 같은 방법을 사용해서 훈련 데이터에서 도미와 빙어의 행만 골라내보자
비교 연산자를 사용하면 도미와 빙어의 행을 모두 True로 바꿀 수 있다
예를 들어 도미인 행을 골라내려면 train_target == 'Bream'
과 같이 쓴다
이 비교식을 쓰게 된다면 train_target 배열에서 Bream인 것은 True이고
그 외는 모두 False인 배열을 반환하게 된다
도미와 빙어 대한 비교 결과를 비트 OR 연산자를 사용해 합쳐보도록 하자
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]
bream_smelt_indexes 배열은 도미와 빙어일 경우 True이고
그 외에는 모두 False 값이 들어간다 따라서 이 배열을 사용해 train_scaled와
train_target 배열에 불리언 인덱싱을 적용하면 손쉽게 도미와 빙어 데이터만 골라낼 수 있다
그러면 이제 본격적으로 로지스틱 회귀 모델을 학습시켜보자
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
print(lr.predict(train_bream_smelt[:5])) # ['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
LogisticRegression 클래스는 선형모델이므로 sklearn.linear_model 패키지 아래에 있다
훈련한 모델을 가지고 처음에 있는 5개의 샘플을 예측해봤다
두번째 샘플을 제외하고 모두 도미로 예측했다
KNeighborsClassifier과 마찬가지로 에측 확률은 predict_proba()
메서드에서 제공한다
print(lr.predict_proba(train_bream_smelt[:5]))
샘플마다 2개의 확률이 보여지고 있고 첫 번째 열이 음성 클래스 (0)에 해당하는 확률이고,
두 번째 열이 양성 클래스 (1)에 대한 확률입니다
그러면 Bream과 Smelt 중 어떤 것이 양성일까요?
앞서 k-최근접 이웃 분류기에서 보았듯이 사이킷런은 타깃값을 알파벳순으로 정렬하여 사용한다
print(lr.classes_) # ['Bream' 'Smelt']
빙어(Smelt)가 양성 클래스이다, predict_proba()
메서드가 반환한 배열 값을 보면
두 번째 샘플만 양성 클래스인 빙어의 확률이 높다
나머지는 모두 도미(Bream)로 예측 하고 있다
그러면 이제 선형 회귀에서처럼 로지스틱 회귀가 학습한 계수를 확인해보자
print(lr.coef_,lr.intercept_) # [[-0.40451732 -0.57582787 -0.66248158 -1.01329614 -0.73123131]] [-2.16172774]
이렇게 보면 로지스틱 회귀는 선형 회귀와 매우 비슷하다고 볼 수 있다
또 LogisticRegression 모델로 z값을 계산해볼 수도 있다
바로 decision_function()
메서드로 z값을 출력할 수 있다
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions) # [-6.02991358 3.57043428 -5.26630496 -4.24382314 -6.06135688]
이 z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있게 된다
다행히 파이썬의 사이파이(scipy) 라이브러리에도 시그모이드 함수가 있다
바로 expit()
이다 np.exp()
함수를 사용해서 분수 계산을 하는 것보다 훨씬 편리하고 안전하다
from scipy.special import expit
print(expit(decisions)) # [0.00239993 0.97262675 0.00513614 0.01414953 0.00232581]
출력된 값을 보면 predict_proba()
메서드 출력의 두 번째 열의 값과 동일하다
즉, decision_function()
메서드는 양성 클래스에 대한 z값을 반환한다
자, 정리를 하면 우리는 이진 분류를 위해 2개의 생선 샘플을 골라냈고,
이를 이용하여 로지스틱 회귀 모델을 훈련시켰다 그래서 이진 분류일 경우
predict_proba()
메서드는 음성 클래스와 양성 클래스에 대한 확률을 출력하고
decision_function()
메서드는 양성클래스에 대한 z값을 계산한다
또 coef_
속성과 intercept_
속성에는 로지스틱 모델이 학습한 선형 방정식의 계수가 들어있다
이번에는 LogisticRegression 클래스를 사용해서 7개의 생선을 분류해볼 것이다
먼저 기존의 이진 분류와의 차이점을 알아보자
LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용하는데
max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100이다
하지만 이번에 우리가 준비한 데이터셋으로 모델을 훈련하면
반복 횟수가 부족하다는 경고가 뜰 것이다
그래서 충분하게 훈련시키기 위해 반복횟수를 1,000으로 늘릴 것이다
또 LogisticRegression은 기본적으로 릿지 회귀와 같이
계수의 제곱을 규제를 하는데 우리는 이런 규제를 L2 규제라고도 부른다
릿지 회귀에서는 값이 커지면 규제도 더 크게 작용하는 alpha 매개변수로
규제의 양을 조절하는데 LogisticRegression에서는 규제를 제어하는 변수가 바로 C다
C는 기본값이 1로 alpha와 반대로 값이 작을수록 규제를 커지게 하는데
이번에는 규제를 완화하기 위해 20으로 늘렸다
lr= LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target)) # 0.9327731092436975
print(lr.score(test_scaled, test_target)) # 0.925
print(lr.predict(test_scaled[:5])) # ['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
훈련 데이터와 테스트 데이터의 값을 보면 점수가 높고,
과대적합이나 과소적합으로 치우친 것 같지 않다
이번에는 테스트 데이터 처음 5개 샘플에 대한 예측 확률을 출력해보자
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
출력을 간소화 하기 위해서 소수점 네 번째 자리에서 반올림을 진행했다
자세히 확인을 해보면
첫 번째 샘플에서는 세 번째 열의 확률이 가장 높은 것을 확인할 수 있다
우리가 예측을 했을 때 첫 번째 샘플의 결과값은 'Perch'였다
그렇다면 실제로 'Perch'가 맞는지 확인을 해보자
print(lr.classes_) # ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
확인을 해보면 세 번째 열에 Perch가 있는 것을 확인할 수 있다
그렇다면 다중 분류일 때 선형방정식은 어떻게 표현이 될까?
바로 coef_
와 intercept_
의 크기를 출력해보자
print(lr.coef_.shape, lr.intercept_.shape) # (7, 5) (7,)
이 데이터는 5개의 특성을 사용하므로 coef_
배열의 열은 5개이다
그런데 행이 7개이고 intercept_
도 7개이다
이 말은 이진 분류에서 보았던 z를 7개나 계산하다는 의미인데
다중 분류는 클래스마다 z값을 하나씩 계산하게 된다
당연히 가장 높은 z값을 출력하는 클래스가 예측 클래스가 된다
각각의 확률은 이진 분류에서는 시그모이드 함수를 사용해서
z를 o과 1 사이의 값으로 변환하고 다중 분류는 이와 달리
소프트맥스(softmax) 함수를 사용하여 7개의 z값을 확률로 반환한다
시그모이드 함수는 하나의 선형 방정식의 출력값을 0~1 사이로 압축한다
이와 달리 소프트맥스 함수는 여러 개의 선형 방정식의 출력값을 0~1 사이로
압축하고 전체 합이 1이 되도록 한다 이를 위해 지수함수를 이용하기 때문에
정규화된 지수 함수라고도 부른다
그러면 이진 분류에서처럼 decision_function()
메서드로
z1~z7까지의 값을 구한 다음 소프트맥스 함수를 사용해서 확률로 바꿔보자
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))
사파이어에서 소프트맥스 함수를 제공해준다
scipy.special 아래에 softmax()
함수를 임포트 !!
from scipy.special import softmax
proba = softmax(decision, axis=1)
print(np.round(proba,decimals=3))
앞서 구한 decision 배열을 softmax()
함수에 전달을 했다
softmax()
의 axis 매개변수는 소프트맥스를 계산할 축을 지정한다
여기에서는 axis=1
로 지정을 해서 각 행의 샘플에 대해서 소프트맥스를 계산했다
만약에 axis를 지정하지 않으면 배열 전체에 대해 소프트맥스를 계산한다
결과값을 앞과 비교해보면 잘 구한 것을 확인할 수 있다
분류 모델은 예측뿐만 아니라 예측의 근거가 되는 확률을 출력할 수 있다
이 확률은 분류 모델이 얼마나 예측을 확신하는지를 나타낸다
확률이 높을수록 강하게 예측하는 셈이다
k-최근접 이웃 모델도 확률을 출력을 할 수 있지만
이웃한 샘플의 클래스 비율이므로 항상 정해진 확률만 출력을 하게 된다
그래서 우리는 가장 대표적인 분류 알고리즘 중 하나인 로지스틱 회귀를 사용했다
로지스틱 회귀는 회귀 모델이 아닌 분류 모델이다
해당 모델은 선형 회귀처럼 선형 방정식을 사용하지만
선형 회귀처럼 계산한 값을 그대로 출력하는 것이 아니라
로지스틱 회귀는 값을 0~1 사이로 압축을 한다
그래서 우리는 이 값을 0~100% 사이의 확률로 이해할 수 있다
로지스틱 회귀는 이진 분류에서는 하나의 선형 방정식을 훈련한다
이 방정식의 출력값의 시그모이드 함수에 통과시켜 0~1 사이의 값을 만든다
이 값이 양성 클래스에 대한 확률이다
음성 클래스의 확률은 1에서 양성 클래스의 확률은 빼면 된다
다중 분류일 경우에는 클래스 개수만큼 방정식을 훈련시킨다
그 다음 각 방정식의 출력값을 소프트맥스 함수를 통과시켜
전체 클래스에 대한 합이 항상 1이 되도록 만든다
우리는 이 값을 각 클래스에 대한 확률로 이해할 수 있다
기존에 있던 모델에서 계속해서 새로운 데이터가 쌓이고 있다
하지만 훈련 데이터가 한 번에 준비되는 것이 아니라 조금씩 전달되고 있다
하지만 도착하는데로 서비스를 운영해야 하므로
데이터가 쌓일 때까지 무작정 기다릴 수 없는 상황이다!!
그렇다면 기존의 훈련 데이터에 새로운 데이터를 추가하여
모델을 매일매일 다시 훈련하면 어떨까?
하지만 단점이 있다
시간이 지날수록 데이터가 늘어난다는 점이다
몇 달이 지나면 모델을 훈련하기 위해서 서버를 늘려야한다
.....
그러면 또 다른 방법으로는 새로운 데이터를 추가할 때
이전 데이터를 버림으로 훈련 데이터를 일정하게 유지하는 것이다
이렇게 하면 데이터셋의 크기가 너무 커지지 않을 수 있다
하지만 데이터를 버릴 때 다른 데이터에 없는 중요한 데이터가 포함되어 있다면?
위에 말한 방법은 이전에 훈련된 모델을 버리고
다시 새로운 모델을 훈련하는 방식이다
그러면 앞서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만
조금씩 더 훈련할 수는 없을까?
만약에 그렇게 할 수 있다면 훈련에 사용한 데이터를 모두 유지할 필요도 없고
앞서 학습한 생선을 까먹을 일도 없다
이런식의 훈련 방식을 점진적 학습 또는 온라인 학습이라고 한다
대표적인 점진적 학습 알고리즘으로는
확률적 경사 하강법(Stochastic Gradient Descent)이 있다
확률적이란 말은 '무작위하게' 혹은 '랜덤하게'의 기술적 표현이다
이 말의 의미는 나중에 설명을 해볼 것이다
그 다음 '경사'는 '기울기'를 말한다 '하강법'은 '내려가는 방법'이다
정리하면 경사 하강법은 기울기를 따라 내려가는 방법을 말한다
자 이제 예를 들어보자
우리가 산을 내려오는데 빠르게 내려오기 위해서는
경사가 가장 가파른 길을 선택해야 한다
하지만 실제로는 천천히 조금씩 내려와야 한다
왜냐하면 장애물이 있을 수도 있기 때문이다
그러니 가파른 길을 찾아 내려오지만 안전하게 조금씩 내려오는 것이 중요하다
이렇게 내려오는 과정이 바로 경사 하강법 모델을 훈련하는 것이다
그러면 이제 확률적이라는 말을 이해해보자
경사 하강법으로 내려올 때 가장 가파른 길을 찾는 방법은 뭘까?
훈련 데이터를 사용해 모델을 훈련하기 때문에
경사 하강법도 당연히 훈련 데이터를 사용하여 가장 가파른 길을 찾는다
그런데 전체 샘플을 사용하지 않고 딱 하나의 샘플을 훈련 데이터에서
랜덤하게 골라 가장 가파른 길을 찾는다
이처럼 훈련 데이터에서 랜덤하게 샘플을 고르는 것이
바로 확률적 경사 하강법이다
정리를 해보면
확률적 경사 하강법은 훈련 데이터에서 랜덤하게 하나의 샘플을 선택하여
가파른 경사를 조금씩 내려간다 그 다음 훈련 데이터에서 랜덤하게
또 다른 샘플을 하나 선택하여 경사를 조금 내려간다이런 식으로 전체 샘플을 모두 사용할 때까지 계속하게 된다
만약에 모든 샘플을 다 사용했는데 산을 내려오지 못했다면
다시 처음부터 훈련 데이터에 모든 샘플을 다시 채워 넣는다
그 다음 랜덤하게 하나의 샘플을 선택해서 이어서 경사를 내려간다
이렇게 만족할만한 위치에 도달할 때까지 계속 내려가면 된다
확률적 경사 하강법에서 훈련 데이터를 한 번 모두 사용하는 과정을 에포크라고 부른다
일반적으로 경사 하강법은 수십, 수백 번 이상 에포크를 수행한다
그런데 이런 생각이 들 수 있다
무작위로 샘플을 선택해서 산에서 내려가다니 너무 무책임한 거 아니야?
맞는 말이다
그래서 우리는 아주 조금씩 내려가야 한다
그렇지 않으면 돌이킬 수 없는 길로 들어설지 모른다
하지만 생각보다 확률적 경사 하강법은 꽤 잘 작동을 한다
만약 걱정이 된다면 1개씩 말고 무작위로 몇 개의 샘플을 선택해서
경사를 따라 내려가는 방법도 있다 이렇게 여러 개의 샘플을 사용해
경사 하강법을 수행하는 방식을 미니배치 경사 하강법이라고 한다
극단적으로 한 번 경사로를 따라 이동하기 위해 전체 샘플을 사용할 수도 있다
이를 배치 경사 하강법이라고 부른다
사실 전체 데이터를 사용하기 때문에 가장 안정적인 방법일 수는 있지만
전체 데이터를 사용하면 그만큼 컴퓨터 자원을 많이 사용하게 된다
어떤 경우에는 데이터가 너무 많아서
한 번에 전체 데이터를 모두 읽을 수도 없을 수도 있다
다시 정리를 해보면
확률적 경사 하강법은 훈련 데이터를 사용해 산 아래에 있는
최적의 장소로 조금씩 이동하는 알고리즘이다이 때문에 훈련 데이터가 모두 준비되어 있지 않고
매일매일 업데이트되어도 학습을 계속 이어나갈 수 있다
즉, 다시 산 꼭대기에서부터 시작할 필요가 없다
그런데 어디서 내려가야 할까?
다시 말해 가장 빠른 길을 찾아 내려가려고 하는 이 산은 무엇일까?
이 산은 바로 손실 함수라고 부른다
손실 함수(loss function)는 어떤 문제에서 머신러닝 알고리즘이
얼마나 엉터리인지를 측정하는 기준이다
그렇기에 손실 함수의 값이 작을수록 좋다
하지만 어떤 값이 최솟값인지는 알지 못한다
가능한 많이 찾아보고 만족할만한 수준이면 산을 다 내려왔다고 인정해야 한다
손실 함수와 비용 함수
비용 함수(cost function)는 손실 함수의 다른 말이다
정확하게 말하면 손실 함수는 샘플 하나에 대한 손실을 정의하고
비용 함수는 훈련 데이터에 있는 모든 샘플에 대한 손실 함수의 합을 말한다하지만 이 둘을 엄격히 구분하지 않고 섞어서 많이 사용한다
분류에서 손실은 아주 정확하다
바로 정답을 못 맞추는 것이다
그렇다면 손실 함수로 정확도를 사용할 수 있을까?
정확도에는 치명적인 단점이 있다
예를 들어 4개의 샘플이 있다고 한다면
가능한 정확도는 0, 0.25, 0.5, 0.75, 1 다섯 가지뿐이다
경사하강법은 아주 조금씩 내려오는데 정확도가 이렇게 듬성 듬성하면
경사 하강법을 이용해 조금씩 움직일 수 없다
산의 경사면은 확실히 연속적이어야 한다
기술적으로 말하면 손실 함수는 미분 가능해야 한다
그러면 어떻게 연속적인 손실 함수를 만들 수 있을까요?
우리는 전에 로지스틱 회귀를 배웠었다
예측은 0과 1이지만 확률은 0~1 사이의 어떤 값도 될 수 있었다
즉, 연속적이였다
샘플 4개가 있고, 각각 모델이 예측한 양성 클래스(1)에 대한 확률은 다음과 같다
0.9, 0.3, 0.2, 0.8
우리는 아주 단순하게 예측값 x 타깃을 하고
그것을 음수로 바꾸는 방식으로 손실을 정의할 것이다
왜냐하면 타깃이 1이면 예측값 x 타깃 = 예측값
자체가 되기 때문이다
예측값이 크면 모델이 정답(양성)을 잘 맞췄다는 뜻이다
그러므로 손실은 작아져야 한다
그래서 음수로 바꾸면, 예측값이 클수록 손실이 작아져 좋은 모델이 된다
예측값 | 타깃 | 예측값 × 타깃 | 음수로 바꾼 값 = 손실 |
---|---|---|---|
0.9 | 1 | 0.9 | -0.9 |
0.3 | 1 | 0.3 | -0.3 |
0.2 | 1 | 0.2 | -0.2 |
0.8 | 1 | 0.8 | -0.8 |
반대로 손실값이 0에 가까우면 나쁜 예측이라는 것이다
그래서 더 높은 손실을 일으킨다
예측 확률을 이용해 이런 방식으로 계산하면 연속적인 손실 함수를 얻을 수 있다
그런데 여기에서 예측 확률에 로그함수를 적용하면 더 좋다
예측 확률의 범위는 0~1 사이인데 로그 함수는
이 사이에서 음수가 되므로 최종 손실 값은 양수가 되게 된다
손실이 양수가 되면 더 이해하기가 쉽다
또 로그 함수는 0에 가까울수록 아주 큰 음수가 되기 때문에
손실을 아주 크게 만들어 모델에 큰 영향을 미칠 수도 있다
정리를 하면 양성 클래스 (타깃 = 1) 일 때
손실은 -log (예측확률) 로 계산한다
확률이 1에서 멀어질수록 손실은 아주 큰 양수가 된다
음성 클래스 (타킷 =0) 일 때
손실은 -log (1-예측 확률) 로 계산한다
이 예측 확률이 0에 멀어질수록 손실은 아주 큰 양수가 될 것이다
정리하면
타깃이 1이면-log(p)
->p
가 1일수록 손실 ↓
타깃이 0이면-log(1-p)
->p
가 0일수록 손실 ↓
우리는 이 손실 함수를 로지스틱 손실 함수 (logistic loss function)
또는 이진 크로스엔트로피 손실 함수 (binary cross-entropy loss function)이라고 부른다
여기에서는 이진 분류로 예시를 들었지만 다중 분류도 매우 비슷한 손실 함수를 사용한다
그리고 그 이름을 크로스엔트로피 손실 함수라고 부른다
사실 우리가 직접 손실 함수를 만드는 일은 없다
왜냐하면 이미 문제 잘 맞는 손실 함수가 개발되어 있기 때문이다
이진 분류는 로지스틱 손실 함수를 사용하고
다중 분류는 크로스 엔트로피 손실 함수를 사용한다
손실 함수를 직접 계산하는 일도 사실은 거의 없다
왜냐하면 라이브러리에서 다 처리를 해준다
그러면 바로 만나보자
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)
fish_csv_data
파일에서 판다스 데이터프레임을 만든 후에
Species 열을 제외한 나머지 5개는 입력 데이터로 사용한다
Species는 타깃 데이터이다
그리고 훈련 데이터와 테스트 데이터로 나눠준다
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
sc = SGDClassifier(loss='log_loss',max_iter=10, random_state=42)
sc.fit(train_scaled,train_target)
print(sc.score(train_scaled, train_target)) # 0.773109243697479
print(sc.score(test_scaled, test_target)) # 0.775
SGDClassifier의 객체를 만들 때는 2개의 매개변수를 지정한다
loss
는 손실 함수의 종류를 지정한다
여기에서는 loss='log_loss'
를 지정하여 로지스틱 손실 함수를 지정했다
max_iter
은 수행할 에포크 횟수를 지정한다
10으로 지정해서 전체 훈련 세트를 10회 반복했다
그런데 훈련 데이터와 테스트 데이터에 대해 정확도가 낮다
아마 지정한 반복횟수가 낮았던 것 같다
앞서 이야기한 것처럼 확률적 경사 하강법은 점진적 학습이 가능하다
SGDClassifier 객체를 다시 만들지 않고
훈련한 모델 sc를 추가로 더 훈련을 시켜보겠다
모델을 이어서 훈련할 때는 partial_fit()
메서드를 사용한다
이 메서드는 fit()
메서드와 사용법이 같지만 호출할 때마다
1 에포크씩 이어서 훈련할 수 있다
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target)) # 0.7983193277310925
print(sc.score(test_scaled,test_target)) # 0.775
아직 점수는 낮지만 에포크를 한 번 더 실행하니 정확도가 높아졌다
이 모델을 여러 에포크에서 더 훈련해 볼 필요가 있어 보인다
그런데 얼마나 더 훈련해야 할까?
확률적 경사 하강법을 사용한 모델은
에포크 횟수에 따라 과소적합이나 과대적합이 될 수 있다
에포크 횟수가 적으면 모델이 훈련 데이터의 학습을 덜하고
에포크 횟수가 많으면 모델이 훈련 데이터를 완전히 학습해서
훈련 데이터에 아주 잘 맞는 모델이 만들어 질 것이다
바꾸어 말하면 적은 에포크 횟수 동안에 훈련된 모델은 훈련 데이터와 테스트 데이터
모두 잘 맞지 않는 과소적합된 모델일 가능성이 높다
반대로 많은 에포크 횟수 동안에 훈련한 모델은 훈련 데이터에 너무 잘 맞아
테스트 데이터는 오히려 점수가 나쁜 과대적합된 모델일 수도 있다
보통 에포크가 진행됨에 따라 훈련 데이터 점수는 올라가지만
테스트 데이터는 어느 순간 감소하기 시작한다
바로 이 지점이 과대적합되기 시작하는 곳이다
그래서 우리는 과대적합이 시작하기 전에 훈련을 멈춰야 하는데
이것을 조기종료(early stoppint)이라고 한다
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))
한 번 이를 알아보기 위해 그래프를 그려볼 것이다
partial_fit()
메서드만을 사용하기 위해서
훈련 데이터에 있는 전체 클래스의 레이블을 전달해줘야 한다
그래서 np.unique()
함수로 train_target에 있는 7개 생선의 목록을 만들었다
SGDClassifier에서
partial_fit()
을 사용할 때는
전체 클래스 목록을 처음 호출 시에 꼭 지정해줘야 해줘야 한다왜냐하면
partial_fit()
은 전체 데이터를 한 번에 보지 않기 때문에,
처음부터 "클래스가 총 몇 개인지" 알고 있어야 내부를 설정할 수 있다
그리고 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)) # 0.957983193277311
print(sc.score(test_scaled,test_target)) # 0.925
SGDClassifier는 일정 에포크 동안 성능이 향상되지 않으면 더 훈련하지 않고
자동으로 멈춘다 tol
매개변수에서 향상될 최솟값을 지정할 수 있다
방금 코드에서는 tol
매개변수를 None으로 지정하여 자동으로 멈추지 않고
max_iter = 100 만큼 무조건 반복하도록 하였다
마지막으로 SGDClassifier의 loss 매개변수를 알아보자
사실 loss 매개변수의 기본값은 'hinge'이다
힌지 손실(hinge)은 서포트 벡터 머신이라고
불리는 또 다른 머신러닝 알고리즘을 위한 손실 함수이다
지금은 이에 대해서 깊게 다루지는 않을것이다
그저 서포트 벡터 머신이 널리 사용되는 머신러닝 알고리즘이고
SGDClassifier가 여러 종류의 손실 함수를 loss 매개변수에 지정하여
다양한 머신러닝 알고리즘을 지원한다는 것만 기억하자
Q. 로지스틱 회귀가 이진 분류에서 확률을 출력하기 위해 사용하는 함수는?
정답은 시그모이드 함수
시그모이드 함수는 선형 방정식의 결과를 0~1 사이로 압축하여 확률로 해석할 수 있게 해준다
소프트맥스 함수는 다중 분류에서 확률을 출력할 때 사용한다
근데 이건 위에 했다