[ML] 혼공머닝 3편 로지스틱 회귀와 확률적 경사 하강법(Stochastic Gradient Descent
카테고리: ML
💡 ‘혼자 공부하는 머신러닝 딥러닝(박해선 저)’ 책을 읽고 공부한 내용을 요약한 페이지입니다.
책에 나오는 코드를 그대로 쓰지 않고, 코드와 파라미터를 변형하고 조정해가며 다른 결과를 출력하며 공부했습니다.
04 다양한 분류 알고리즘
04-1 로지스틱 회귀
- k-최근접 이웃 분류기: k-최근접 이웃을 이용하여 인접 샘플의 클래스 비율을 확률로 나타내는 방식
#데이터 준비
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head() #처음 5개 행 출력
print(pd.unique(fish['Species']))
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.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled, train_target)
print(kn.score(train_scaled, train_target))
print(kn.score(test_scaled, test_target))
import numpy as np
proba = kn.predict_proba(test_scaled[:5]) #테스트세트 첫 5개 샘플의 확률
print(np.round(proba, decimals=4)) #소수점 네번째 자리까지 표기, 다섯번째 자리에서 반올림
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
그러나 k-최근접 이웃 방식은 3개의 최근접 이웃만을 활용하기에 가능한 확률은 0/3, 1/3, 2/3, 3/3이 전부이다. 더 디테일한 확률이 필요하다. 로지스틱 회귀는 이것을 가능하게 해준다.
- 로지스틱 회귀(logistic regression): 선형 방정식을 활용한 분류 모델로서, 시그모이드 함수나 소프트맥스 함수를 활용하여 클래스의 확률을 출력한다. z값이 아주 큰 음수이면 확률이 0이 되고, 아주 큰 양수이면 1이 되도록 한다.
왼쪽은 시그모이드 그래프, 오른쪽은 시그모이드 함수. x값이 로지스틱 방정식이다.
#시그모이드 함수 그리기
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()
로지스틱 회귀로 간단한 이진 분류를 수행해보자. 이진분류의 경우 시그모이드의 출력이 0.5보다 크면 양성, 작으면 음성으로 판단한다.
#불리언 인덱싱을 활용하여 도미(Bream)와 빙어(Smelt)만 뽑아냄
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]
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
print(lr.predict(train_bream_smelt[:5])) #처음 5개 샘플 예측
print(lr.predict_proba(train_bream_smelt[:5])) #처음 5개 샘플의 확률 출력
#결과값
['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
[[0.99759855 0.00240145]
[0.02735183 0.97264817]
[0.99486072 0.00513928]
[0.98584202 0.01415798]
[0.99767269 0.00232731]]
#decision_function(): z값을 계산하는 메서드
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
#결과값
[-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
#이 z값을 시그모이드 함수에 넣으면 확률을 얻을 수 있음
from scipy.special import expit
print(expit(decisions))
#결과값
[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
#predict_proba()메서드 출력의 두번째 열과 동일!
이제 로지스틱 회귀로 다중 분류를 수행해보자.
lr = LogisticRegression(C=20, max_iter=1000)
#max_iter매개변수는 반복 횟수를 지정
#C는 릿지의 alpha처럼 규제의 정도를 위한 매개변수. 작을수록 규제가 커짐.
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target)) #0.9327
print(lr.score(test_scaled, test_target)) #0.925
print(lr.predict(test_scaled[:5]))
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
다중분류는 클래스마다 z값을 하나씩 계산한다. 이중 분류의 경우 z값을 시그모이드 함수를 사용하여 확률로 변환했지만, 다중 분류의 경우 소프트맥스 함수를 사용하여 z값을 확률로 변환한다.
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2)) #z값 출력.
from scipy.special import softmax
proba = softmax(decision, axis=1) #axis매개변수는 계산할 축을 지정
print(np.round(proba, decimals=3))
#결과값
[[0. 0.014 0.841 0. 0.136 0.007 0.003]
[0. 0.003 0.044 0. 0.007 0.946 0. ]
[0. 0. 0.034 0.935 0.015 0.016 0. ]
[0.011 0.034 0.306 0.007 0.567 0. 0.076]
[0. 0. 0.904 0.002 0.089 0.002 0.001]]
#앞서 구한 proba 배열과 결과가 일치함!
04-1 핵심 키워드
- 로지스틱 회귀: 선형 방정식을 사용한 분류 알고리즘
- 다중 분류: 타깃 클래스가 2개 이상인 분류 문제
- 시그모이드 함수: 이중 분류에서 쓰이며, 선형 방정식의 출력(z)을 0과 1 사이의 확률값으로 출력
- 소프트맥스 함수: 다중 분류에서 쓰이며, 여러 선형 방정식의 출력 결과를 정규화하여 합이 1이 되도록 만듦.
04-1 핵심 패키지와 함수
- scikit-learn
LogisticRegression
: 로지스틱 회귀를 위한 클래스. C매개변수에서 규제의 강도를 제어한다. 작을수록 규제가 강해진다.predict_proba()
: 예측 확률을 반환하는 메서드decision_function()
: 모델이 학습한 선형 방정식의 값(z)을 반환하는 메서드.
04-2 확률적 경사 하강법
앞서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 훈련할 수 없을까? ⇒이러한 방식이 점진적 학습이다. 대표적인 점진적 학습 알고리즘은 확률적 경사 하강법(Stochastic Gradient Descent) 이다.
훈련세트에서 랜덤하게 하나의 샘플을 선택하여 가파른 경사를 조금 내려간다. 그 다음 훈련 세트에서 랜덤하게 또 다른 샘플을 하나 선택하여 경사를 조금 내려간다. 이런 식으로 전체 샘플을 모두 사용할 때까지 계속한다. 만약 모든 샘플을 사용했다면, 훈련세트에 모든 샘플을 다시 채워넣고 앞선 과정을 반복한다. 이 때 훈련세트를 한 번 모두 사용하는 텀을 에포크(epoch) 라고 부른다.
샘플을 1개씩 꺼내는 것이 확률적 경사 하강법, 샘플을 여러개씩 꺼내는 것이 미니배치 경사 하강법, 샘플을 전부 꺼내는 것이 배치 경사 하강법이다.
- 손실함수(loss function): 머신러닝 알고리즘이 얼마나 엉터리인지 측정하는 함수. 로지스틱 손실 함수, 크로스엔트로피 손실 함수 등이 있다.
#데이터 준비 및 전처리
import pandas as pd
fish = pd.read_csv('http://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)
# 확률적 경사 하강법을 제공하는 분류용 클래스 SGDClassifier
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss='log', max_iter=10, random_state=42)
#loss는 손실함수를 지정하는 매개변수. log는 로지스틱 손실 함수
#max_iter는 수행할 에포트 횟수
sc.fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target)) #0.7731
print(sc.score(test_scaled, test_target)) #0.775
#출력된 훈련세트와 테스트세트의 정확도가 낮은 걸 보아하니 반복 횟수 10번이 부족한 것으로 보임.
sc.partial_fit(train_scaled, train_target)
#partial_fit메서드는 호출할 때마다 1 에포크씩 이어서 훈련한다.
print(sc.score(train_scaled, train_target)) #0.8151
print(sc.score(test_scaled, test_target)) #0.825
#아직 점수가 낮지만 그래도 나아졌다.
그렇다면 에포크를 어느 정도로 두어야 가장 뛰어난 점수가 나올까.
import numpy as np
sc = SGDClassifier(loss='log', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_target)
for _ in range(300): #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()
주황색은 테스트 세트, 파란색은 훈련 세트이다.
50번째 에포크부터는 점수가 거의 향상하지 않고, 100번째 에포크부터는 두 세트 간의 격차가 벌어지고 있다. 따라서 백 번째 에포크가 적절한 반복 횟수로 보인다.
sc = SGDClassifier(loss='log', max_iter=100, tol=None, random_state=42)
#SGDClassifier는 일정 에포크동안 성능이 향상되지 않으면 자동으로 멈춘다
#그러나 tol매개변수를 None으로 지정하면 자동으로 멈추지 않고 끝까지 간다.
sc.fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target)) #0.9579
print(sc.score(test_scaled, test_target)) #0.925
#최종 결과가 좋게 나옴!
04-2 핵심 키워드
- 확률적 경사 하강법(stochastic gradient descent): 훈련 세트에서 샘플 하나씩 꺼내 손실 함수의 경사를 따라 최적의 모델을 찾는 알고리즘.
- 손실함수: 확률적 경사 하강법이 최적화할 대상으로, 이중 분류에는 로지스틱 회귀 손실함수를 사용하고, 다중 분류에는 크로스엔트로피 손실함수를 사용한다.
- 에포크(epoch): 확률적 경사 하강법에서 전체 샘플을 모두 사용하는 한 번 반복을 의미한다.
04-2 핵심 패키지와 함수
- scikit-learn
SGDClassifier
: 확률적 경사 하강법을 사용한 분류 모델을 만드는 클래스.loss
매개변수는 최적화할 손실 함수를 택한다.penalty
매개변수는 규제의 종류를 지정한다.max_iter
매개변수는 에포크 횟수를 지정한다.tol
매개변수는 반복을 멈출 조건이다.
SGDRegressor
: 확률적 경사 하강법을 사용한 회귀 모델을 만드는 클래스