Post

[ML기초세션]_4주차

세션 전 학습 내용

교재: p.176 ~ p.217

[4-1] 로지스틱 회귀

핵심 키워드: 로지스틱 회귀, 다중 분류, 시그모이드 함수, 소프트맥스 함수

  • 예제: 생선의 길이, 높이, 두께, 대각선 길이, 무게 데이터를 이용하여 럭키백에 들어갈 수 있는 생선 7개에 대한 확률을 출력하라.

k-nn 회귀 알고리즘으로의 접근

  • 데이터 준비하기
1
2
3
4
import pandas as pd

fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()

'img1'

  • 데이터프레임: 판다스에서 제공하는 2차원 표 형식의 주요 데이터 구조. 넘파이 배열과 비슷하게 행과 열로 이루어져 있다. 통계와 그래프를 위한 메서드를 풍부하게 제공하고 넘파이와의 상호 변환, 사이킷럿과의 호환이 좋다.

  • unique(): Species 열에서 고유한 값 추출

1
print(pd.unique(fish['Species']))

-> [‘Bream’ ‘Roach’ ‘Whitefish’ ‘Parkki’ ‘Perch’ ‘Pike’ ‘Smelt’]

타깃 데이터: Species 설정, df의 나머지 5개 열은 입력 데이터로 사용

1
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy()

-> 입력데이터로 사용할 5개 열을 넘파이 배열로 바꾸어 fish_input에 저장

1
fish_target = fish['Species'].to_numpy()

-> 타깃데이터로 사용할 Species 열을 넘파이 배열로 바꾸어 fish_target에 저장

  • 훈련 세트와 테스트 세트로 나누기
1
2
3
4
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)

그 다음, 사이킷런의 StandardScaler 클래스를 사용해 각 데이터 세트 표준화한다. 훈련세트의 통계 값으로 테스트 세트를 변환해야 함을 잊지 말 것!

1
2
3
4
5
6
from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
  • k-nn 분류기의 확률 예측
1
2
3
4
5
6
7
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))

(우리의 목적은 클래스 확률을 배우는 것이므로 잠시 점수에 대해서는 접어두자)

  • 다중 분류: 타깃 데이터에 2개 이상의 클래스가 포함된 문제

우리의 예제에도 7개의 생선 종류가 포함되어 있기에 다중 분류에 해당한다. 하지만 위의 코드에서 볼 수 있듯, 이진 분류와 모델을 만들고 훈련하는 방식은 동일하다. 이진 분류를 사용했을 때는 양성 클래스와 음성 클래스를 각각 1과 0으로 지정하여 타깃 데이터를 만들었다. 다중 분류에서도 타깃값을 숫자로 바꾸어 입력할 수 있지만 사이킷런에서는 편리하게도 문자열로 된 타깃값을 그대로 사용할 수 있다. 이때 주의해야 할 점은, 타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 매겨진다는 것이다. 따라서 위의 pd.unique(fish[‘Species’])로 출력했던 순서와 다르다는 점을 유념하자.

1
print(kn.classes_)

-> [‘Bream’ ‘Parkki’ ‘Perch’ ‘Pike’ ‘Roach’ ‘Smelt’ ‘Whitefish’]

위의 반환값대로, ‘Bream’이 첫 번째 클래스, ‘Parkki’가 두 번째 클래스가 되는 방식이다.

  • predict(): 타깃값으로 예측을 출력
1
print(kn.predict(test_scaled[:5]))

-> [‘Perch’ ‘Smelt’ ‘Pike’ ‘Perch’ ‘Perch’]

  • predict_proba(): 사이킷런의 분류 모델 메서드, 클래스별 확률값을 반환.

  • round(): 기본적으로 소수점 첫째 자리에서 반올림을 하는데, decimals 매개변수로 유지할 소수점 아래 자릿수 지정 가능.

1
2
3
4
import numpy as np

proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))

'img2'

'img3'

  • kneighbors() 메서드의 입력은 2차원 배열이어야 한다. 이를 위해 넘파이 배열의 슬라이싱 연산자를 사용. 슬라이싱 연산자는 하나의 샘플만 선택해도 항상 2차원 배열이 만들어진다.
1
2
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])

-> [[‘Roach’ ‘Perch’ ‘Perch’]]

이 샘플의 이웃은 다섯 번째 클래스인 ‘Roach’가 1개이고 세 번째 클래스인 ‘Perch’가 2개이다. 따라서 세 번째 클래스에 대한 확률은 1/3, 다섯 번째 클래스에 대한 확률은 2/3이다.

이렇게 k-nn 회귀 모델을 이용하여 클래스 확률을 예측할 수 있지만, 3개의 최근접 이웃을 사용하기에 가능한 확률은 0, 1/3, 2/3, 1 이 전부이다. 제대로 된 확률을 구할 수 있는 다른 방법을 찾아보자!

로지스틱 회귀 (Logistic Regression)

  • 로지스틱 회귀는 이름은 회귀이지만 분류 모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다.

'img4'

여기에서 a, b, c, d, e는 가중치 혹은 계수다. 특성은 늘어났지만 3장에서 다룬 다중 회귀를 위한 선형 방정식과 같다. z는 어떤 값도 가능하다.

  • 시그모이드 함수(로지스틱 함수): 어떤 값이든 가능한 z 값을 0~1 사이 값이 되도록 만드는 함수. z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 1이 되도록 한다.

'img5'

numpy를 이용해 이 그래프를 그려보자. 먼저 -5와 5 사이에 0.1 간격으로 배열 z를 만들고, z 위치마다 시그모이드 함수를 계산한다. 지수 함수 계산은 np.exp() 함수를 사용한다.

1
2
3
4
5
6
7
8
9
10
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()

'img6'

사이킷런에는 로지스틱 회귀 모델인 LogisticRegression 클래스가 준비되어 있다. 이진 분류일 경우 시그모이드 함수의 출력이 0.5보다 크면 양성, 작으면 음성 클래스로 판단한다.

정확히 0.5일 경우는 라이브러리마다 다를 수 있는데, 사이킷런에서는 음성 클래스로 판단한다.

로지스틱 회귀로 이진 분류 수행하기

넘파이 배열은 True, False 값을 전달하여 행을 선택할 수 있는데, 이를 불리언 인덱싱(Bolean indexing)이라고 한다.

1
2
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])

-> [‘A’ ‘C’]

이를 이용해 도미와 빙어의 행만 골라내보자.

  • 도미와 빙어에 대한 비교 결과를 비트 OR 연산자를 사용해 합치면 도미와 빙어에 대한 행만 골라낼 수 있다.
1
2
3
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]

이제 이 데이터로 로지스틱 회귀 모델을 훈련해보자.

1
2
3
4
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)

훈련한 모델을 사용해 train_bream_smelt에 있는 처음 5개 샘플을 예측해보자.

1
print(lr.predict(train_bream_smelt[:5]))

-> [‘Bream’ ‘Smelt’ ‘Bream’ ‘Bream’ ‘Bream’]

이번엔 train_bream_smelt에서 처음 5개 샘플의 예측 확률을 출력해보자.

1
print(lr.predict_proba(train_bream_smelt[:5]))

'img7'

샘플마다 2개의 확률이 출력되었다. 첫 번째 열이 음성 클래스(0)에 대한 확률이고, 두 번째 열이 양성 클래스(1)에 대한 확률이다. 그럼 Bream과 Smelt 중 어느 것이 양성일까? 앞서 보았듯, 사이킷런은 타깃값을 알파벳순으로 정렬하여 사용한다. 때문에 앞에 오는 Bream이 음성, 뒤에 오는 Smelt가 양성이다.

  • 만약 bream을 양성 클래스로 사용하고 싶다면 bream인 타깃값을 1로 만들고 나머지 타깃값은 0으로 만들어 사용하면 된다.

로지스틱 회귀가 학습한 계수를 확인해 보자.

1
print(lr.coef_, lr.intercept_)

-> [[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]

따라서 이 로지스틱 회귀 모델이 학습한 방정식은 다음과 같다.

'img8'

LogisticRegression 모델로 z값을 계산해보는 것도 가능하다. decision_function() 메서드로 z값을 출력할 수 있다. train_bream_smelt의 처음 5개 샘플의 z값을 출력해보자.

1
2
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)

-> [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]

이 z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다. 파이썬의 사이파이 라이브러리에서 시그모이드 함수를 expit()으로 호출할 수 있다.

1
2
3
from scipy.special import expit

print(expit(decisions))

-> [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]

출력된 값을 보면, predict_proba() 메서드 출력의 두 번째 열과 동일하다. 즉 decision_function() 메서드는 양성 클래스에 대한 z값을 반환한다.

로지스틱 회귀로 다중 분류 수행하기

LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다. max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100이다. 또, 릿지 회귀와 같이 L2 규제를 사용하여 계수의 제곱을 규제한다. 릿지 회귀에서는 alpha 매개변수로 규제의 양을 조절했지만, LogisticRegression에서는 C 매개변수로 규제를 제어한다. 하지만 C는 alpha와 다르게, 작을수록 규제가 커지고 기본값은 1이다.

1
2
3
4
5
lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)

print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target))

->0.9327731092436975
0.925

과대적합이나 과소적합으로 치우친 것 같진 않으니 처음 5개 샘플에 대한 예측을 출력해보자.

1
print(lr.predict(test_scaled[:5]))

-> [‘Perch’ ‘Smelt’ ‘Pike’ ‘Roach’ ‘Perch’]

이번엔 테스트 세트의 처음 5개 샘플에 대한 예측 확률을 출력해보자

1
2
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))

'img9'

5개 샘플에 대한 예측이므로 5개의 행이 출력되었다. 또 7개 생선에 대한 확률이므로 7개의 열이 출력되었다.

첫 번째 샘플을 보면 세 번째 열의 확률이 가장 높다.

1
print(lr.classes_)

-> [‘Bream’ ‘Parkki’ ‘Perch’ ‘Pike’ ‘Roach’ ‘Smelt’ ‘Whitefish’]

세 번째 열이 농어(Perch)에 대한 확률이므로 첫 번째 샘플은 농어로 예측하리라는 것을 알 수 있다.

다 중 분류의 선형 방정식은 어떤 모습일까?

1
print(lr.coef_.shape, lr.intercept_.shape)

-> (7, 5) (7,)

이 데이터는 5개의 특성을 사용하므로 coef_배열의 열은 5개다.

다중 분류는 클래스마다 z값을 하나씩 계산하기에 intercept_, 행, z의 개수가 모두 7개이다.

이진 분류에서는 시그모이드 함수를 사용해 z를 확률값으로 변환한 반면, 다중 분류는 소프트맥스(softmax) 함수를 사용하여 7개의 z 값을 확률로 변환한다.

소프트맥스 함수는 여러 개의 선형 방정식의 출력값을 0~1 사이로 압축하고 전체 합이 1이 되도록 만든다. 이를 위해 지수 함수를 사용하기 때문에 정규화된 지수함수라고도 부른다.

  • 소프트맥스 함수의 계산 방식
  1. 7개의 z값의 이름을 z1 ~ z7 라고 하자.
  2. z값들을 활용해 e_sum을 정의한다. 'img12'
  3. e^z1 ~ e^z7 를 각각 e_sum으로 나누어 s1 ~ s7을 구한다. 'img13'
  4. s1~s7의 합을 구하면 1이 되므로 올바르게 구한 것을 알 수 있다.

시그모이드 함수와 소프트맥스 함수는 나중에 신경망을 배울 때 또다시 등장하므로 잘 알아두는 것이 좋다.

이제 decision_function() 메서드로 z1~z7까지의 값을 구한 다음 소프트맥스 함수를 사용해 확률로 바꾸어보자.

1
2
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))

'img10'

사이파이는 소프트맥스 함수도 제공한다.

1
2
3
4
from scipy.special import softmax

proba = softmax(decision, axis=1)
print(np.round(proba, decimals=3))

'img11'

앞서 구한 decision배열을 softmax()에 전달했다. softmax()의 axis 매개변수는 소프트맥스를 계산할 축을 지정한다. 여기선 axis=1으로 지정하여 각 행(샘플)에 대해 소프트맥스를 계산한다.

출력 결과를 앞서 구한 proba 배열과 비교해보니 동일함을 알 수 있다.

핵심 패키지와 함수

scikit-learn

  • LogisticRegression은 선형 분류 알고리즘인 로지스틱 회귀를 위한 클래스다.
  • solver 매개변수에서 사용할 알고리즘을 선택할 수 있는데 기본값은 ‘lbfgs’다. 사이킷런 0.17에 추가된 ‘sag’는 확률적 평균 경사 하강법 알고리즘으로 특성과 샘플 수가 많을 때 성능은 빠르고 좋다. 사이킷런 0.19에서는 개선 버전인 ‘saga’가 추가되었다.
  • penalty 매개변수에서 L2 규제와 L1 규제를 선택할 수 있다. 기본값은 L2를 의미하는 ‘l2’이다.
  • C 매개변수에서 규제의 강도를 제어한다. 기본값은 1.0이며, 값이 작을수록 규제가 강해진다.
  • predict_proba(): 예측 확률을 반환한다. 이진 분류의 경우에는 샘플마다 음성 클래스와 양성 클래스에 대한 확률을 반한하고, 다중 분류의 경우에는 샘플마다 모든 클래스에 대한 확률을 반환한다.

[4-2] 확률적 경사 하강법

핵심 키워드: 확률적 경사 하강법, 손실 함수, 에포크

점진적인 학습

교재 속 예시에서의 문제점은 훈련 데이터가 한 번에 준비되는 것이 아니라 조금씩 전달된다는 것이다. 여러 방법이 있겠지만, 앞서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련하는 방법을 사용한다면 훈련에 사용한 데이터를 모두 유지할 필요도 없고 앞서 학습한 내용을 잊을 일도 없다. 이를 점진적인 학습 또는 온라인 학습이라고 부른다. 대표적인 점진적 학습 알고리즘은 확률적 경사 하강법이다.

확률적 경사 하강법: ‘확률적’이라는 말은 ‘무작위하게’ 혹은 ‘랜덤하게’의 의미를 갖는다. 또, 경사는 ‘기울기’를, 하강법은 ‘내려가는 방법’을 말한다. 다시 말해 확률적 경사 하강법은 랜덤하게 하나의 샘플을 골라 가장 가파른 길을 찾는 방법이다.

  • 에포크: 확률적 경사 하강법에서 훈련세트를 모두 한 번씩 사용하는 과정, 일반적으로 경사 하강법은 수십, 수백 번 이상 에포크를 수행한다.

  • 미니배치 경사 하강법: 1개씩이 아니라 무작위로 몇 개의 샘플을 선택해서 경사를 따라 내려가는 방법

  • 배치 경사 하강법: 극단적으로, 한 번 경사로를 따라 이동하기 위해 전체 샘플을 사용하는 방법. 전체 데이터를 사용하기에 의외로 가장 안정적인 방법이 될 수 있으나 그만큼 컴퓨터 자원을 많이 사용하게 된다.

'img14'

확률적 경사 하강법은 훈련 세트를 사용해 산 아래에 있는 최적의 장소로 조금씩 이동하는 알고리즘이다. 이 때문에 훈련 데이터가 모두 준비되어 있지 않고 매일매일 업데이트 되어도 학습을 계속 이어나갈 수 있다.

확률적 경사 하강법과 미니배치 경사 하강법은 신경망 알고리즘에서 사용된다.

손실 함수

손실 함수(loss function)는 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준이다. 때문에 손실 함수의 값이 작을수록 좋다. 하지만 어떤 값이 최솟값인지는 모르기에 가능한 많이 찾아보고 만족할만한 수준이라면 멈춰야한다.

비용 함수(cost function)는 손실 함수의 다른 말이다. 엄밀히 말하면, 손실 함수는 샘플 하나에 대한 손실을 정의하고 비용 함수는 훈련 세트에 있는 모든 샘플에 대한 손실 함수의 합을 말하지만 보통 이 둘을 엄격히 구분하지는 않는다.

분류에서의 손실은 정답을 못 맞히는 것이다. 도미와 빙어를 구분하는 이진 분류 문제를 생각해보자. 도미는 양성 클래스(1), 빙어는 음성 클래스(0)이라 하자.

'img15'

위와 같은 정답과 예측이 있다고 상상해보자. 이 경우 정확도는 0.5이다. 이 정확도를 손실 함수로 사용하기에 괜찮아 보이기도 한다.

그러나 정확도에는 치명적인 단점이 있다. 그림과 같이 4개의 샘플만 있다면 가능한 정확도는 0, 0.25, 0.5, 0.75, 1로 5가지 뿐이다. 정확도가 이렇게 듬성듬성하면 경사 하강법을 이용해 조금씩 움직일 수 없다. 다시 말해 손실 함수는 연속적이고, 미분 가능해야 한다.

'img16'

로지스틱 손실 함수

1절 ‘로지스틱 회귀’의 4개 샘플의 예측 확률을 각각 0.9, 0.3, 0.2, 0.8 이라고 가정하자.

  1. 첫 번째 샘플의 예측은 0.9이므로 양성 클래스의 타깃인 1과 곱한 다음 음수로 바꿀 수 있다. 이 경우 예측이 1에 가까울수록 좋은 모델이다. 예측이 1에 가까울수록 예측과 타깃의 곱의 음수는 점점 작아진다.
  2. 두 번째 샘플의 예측은 0.3이다. 같은 과정을 거치면 -0.3의 값을 구할 수 있다.
  3. 세 번째 샘플의 타깃은 음성 클래스라 0이다. 이 경우에는 여집합의 개념으로 양성 클래스에 대한 예측으로 바꾸어 생각해보자. 그러면 -0.8의 값을 얻을 수 있다.
  4. 네 번째 샘플도 마찬가지로 계산하면 -0.2의 값을 얻을 수 있다.

'img17'

여기서 로그 함수를 적용하면 더 좋다. 예측 확률의 범위는 0~1인데, 로그 함수는 이 사이에서 음수가 되므로 최종 손실 값은 양수가 되기 때문이다. 또한 로그 함수는 0에 가까울수록 급격하게 작아지기 때문에 손실을 아주 크게 만들어 모델에 큰 영향을 미칠 수 있다.

'img18'

양성 클래스(타깃=1)일 때 손실은 -log(예측 확률)로 계산한다. 확률이 1에서 멀어질수록 손실은 아주 큰 양수가 된다. 음성 클래스(타깃=0)일 때 손실은 -log(1-예측 확률)로 계산한다. 이 예측 확률이 0에서 멀어질수록 손실은 아주 큰 양수가 된다.

이 손실 함수를 로지스틱 손실 함수(logistic loss function)라고 부릅니다. 또는 이진 크로스엔트로피 손실 함수(binary cross-entropy loss function)라고도 부릅니다.

로지스틱 손실 함수란 이름에서 알 수 있다시피 이 손실 함수를 사용하면 로지스틱 회귀 모델이 만들어진다. 다른 손실 함수도 존재함!

  • 크로스엔트로피 손실 함수(cross-entropy loss function): 다중 분류에서 사용하는 손실 함수

이미 문제에 잘 맞는 손실 함수가 개발되어 있기에 우리 직접 손실 함수를 만드는 일은 거의 없다. 이진 분류는 로지스틱 손실 함수를 사용하고 다중 분류는 크로스엔트로피 손실 함수를 사용한다.

회귀의 손실 함수로는 3장에서 소개한 평균 절댓값 오차를 사용할 수 있다. 타깃에서 예측을 뺀 모든 샘플에 평균한 값. 또는 평균 제곱 오차(mean squared error)를 많이 사용한다. 타깃에서 예측을 뺀 값을 제곱한 다음 모든 샘플에 평균한 값이다. 확실히 이 값이 작을수록 좋은 모델이다.

SGDClassifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#이번에도 fish_csv_data 파일에서 판다스 df를 만들어보자.
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()

#test set, training set 나누기
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다.

1
from sklearn.linear_model import SGDClassifier

SGDClassifier의 객체를 만들 때 2개의 매개변수를 지정한다. loss는 손실 함수의 종류를 지정한다. 여기서는 loss= ‘log’로 지정하여 로지스틱 손실 함수를 지정했다. max_iter는 수행할 에포크 횟수를 지정하는데 10으로 설정했다. 그다음 훈련 세트와 테스트 세트에서 정확도 점수를 출력한다.

1
2
3
4
5
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번이 부족해보인다.

ConvergenceWarning: 모델이 충분히 수렴하지 않았다는 경고로, max_iter 매개변수의 값을 늘려주는 것이 좋다.

앞서 이야기한 것처럼 확률적 경사 하강법은 점진적 학습이 가능하다. SGDCalssifier 객체를 다시 만들지 않고 훈련한 모델 sc를 추가로 더 훈련해보자. 모델을 이어서 훈련할 때는 partial_fit() 메서드를 사용한다. 이 메서드는 fit() 메서드와 사용법이 같지만 호출할 때마다 1 에포크씩 이어서 훈련할 수 있다.

1
2
3
4
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

아직도 여전히 점수가 낮지만 에포크를 한 번 더 실행하니 정확도가 향상된 것을 볼 수 있다. 이 모델을 여러 에포크에서 더 훈련해 볼 필요가 있다.

train_scaled와 train_target을 한꺼번에 모두 사용했으니 배치 경사 하강법 아닌가요? -> No! SGDClassifier 객체에 한 번에 훈련 세트 전체를 전달했지만 이 알고리즘은 전달한 훈련 세트에서 1개씩 샘플을 꺼내어 경사 하강법 단계를 수행한다. SGDClassifier는 미니배치 경사 하강법이나 배치 하강법을 제공하지 않는다.

에포크와 과대/과소적합

확률적 경사 하강법을 사용한 모델은 에포크 횟수에 따라 과소적합이나 과대적합이 될 수 있다.

'img19'

이 그래프는 에포크가 진행됨에 따라 모델의 정확도를 나타낸 것이다. 훈련 세트 점수는 에포크가 진행될수록 꾸준히 증가하지만 테스트 세트 점수는 어느 순간 감소하기 시작한다. 바로 이 지점이 모델이 과대적합되기 시작하는 곳이다. 과대적합이 시작하기 전에 훈련을 멈추는 것을 조기 종료(early stopping)라고 한다.

이 예제에서는 partial_fit() 메서드만 사용하겠다. 이를 위해 훈련 세트에 있는 전체 클래스의 레이블을 partial_fit() 메서드에 전달해 주어야 한다. np.unique() 함수로 train_target에 있는 7개 생선의 목록을 만들고 에포크마다 훈련 세트와 테스트 세트에 대한 점수를 기록하기 위해 2개의 리스트를 준비한다.

1
2
3
4
5
6
7
8
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 리스트에 추가한다.

1
2
3
4
5
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))

파이썬의 _는 특별한 변수다. 나중에 사용하지 않고 그냥 버리는 값을 넣어두는 용도로 사용. 여기서는 0~299까지 반복 횟수를 임시 저장하기 위한 용도로 사용했다.

300번의 에포크 동안 기록한 훈련 세트와 테스트 세트의 점수를 그래프로 그려보자.

1
2
3
4
5
6
7
import matplotlib.pyplot as plt

plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

'img20'

데이터가 작기 때문에 아주 잘 드러나지는 않지만, 백 번째 에포크 이후에는 훈련 세트와 테스트 세트의 점수가 조금씩 벌어지고 있다. 또 확실히 에포크 초기에는 과소적합되어 훈련 세트와 테스트 세트의 점수가 낮다. 이 모델의 경우 백 번째 에포크가 적절한 반복 횟수로 보인다.

다시 SGDClassifier의 반복 횟수를 100에 맞추고 모델을 다시 훈련해 보자.

1
2
3
4
5
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는 일정 에포크 동안 성능이 향상되지 않으면 더 훈련하지 않고 자동으로 멈춘다. tol 매개변수에서 향상될 최솟값을 지정한다. 앞의 코드에서는 tol 매개변수를 None으로 지정하여 자동으로 멈추지 않고 max_iter=100 만큼 무조건 반복하도록 하였다.

확률적 경사 하강법을 사용한 회귀 모델도 존재한다. SGDRegressor가 이를 제공한다. 사용법은 SGDClassifier와 동일하다.

이 섹션을 마무리하기 전에 SGDClassifier의 loss 매개변수를 알아보자. loss 매개변수의 기본값은 ‘hinge’다. 힌지 손실(hinge loss)은 서포트 벡터 머신(support vetor machine)이라 불리는 또 다른 머신러닝 알고리즘을 위한 손실 함수다. 여기서는 SVM에 대해 더 자세히 다루지 않지만 SVM이 널리 사용하는 ML알고리즘 중 하나라는 점과 SGDClassifier가 여러 종류의 손실 함수를 loss 매개변수에 지정하여 다양한 ML 알고리즘을 지원한다는 사실을 기억하자.

간단한 예시로 힌지 손실을 사용해 같은 반복 횟수 동안 모델을 훈련해보자.

1
2
3
4
5
 = 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

핵심 패키지와 함수

scikit-learn

  • SGDClassifier: 확률적 경사 하강법을 사용한 분류 모델을 만든다.
  • loss 매개변수: 확률적 경사 하강법으로 최적화할 손실 함수를 지정. 기본값은 SVM을 위한 ‘hinge’ 손실 함수다. 로지스틱 회귀를 위해서는 ‘log’로 지정해야 한다.
  • penalty 매개변수: 규제의 종류를 지정. 기본값은 ‘l2’이다. L1 규제를 적용하려면 ‘l1’으로 지정해야 한다. 규제 강도는 alpha 매개변수에서 지정한다. 기본값은 0.0001.
  • max_iter 매개변수: 에포크 횟수 지정. 기본값은 1000.
  • tol 매개변수: 반복을 멈출 조건. n_iter_no_change 매개변수에서 지정한 에포크 동안 손실이 tol 만큼 줄어들지 않으면 알고리즘이 중단도니다. tol 매개변수의 기본값은 0.001이고 n_iter_no_change 매개변수의 기본값은 5다.
  • SGDRegressor: 확률적 경사 하강법을 사용한 회귀 모델을 만든다.
  • loss 매개변수: 손실 함수 지정. 기본값은 제곱 오차를 나타내는 ‘squared_loss’다.
  • 앞의 SGDClassifier에서 설명한 매개변수는 모두 SGDRegressor에서 동일하게 사용 가능

발표 내용 복습

SGDClassifier란 SGD를 이용한, 정규화된 선형 분류 모델이다.

매개변수 모음

  • alpha : 값이 클수록 강력한 규제 설정 (default = 0.0001)
  • loss : 손실함수 (default = ‘hinge’)
  • epsilon : 손실 함수에서 현재 예측과 올바른 레이블 간의 차이가 임계값보다 작으면 무시 (default = 0.1)
  • penalty : 규제 종류 선택 (default = ‘l2’, ‘l1’, ‘elasticnet’(l1,l2 섞은거))
  • l1_ratio : L1 규제의 비율 (Elastic-net에서만 사용 , default = 0.15)
  • max_iter : 계산에 사용할 작업 수

손실함수

'img21'

'img22'

'img23'

'img24'

+팁: 데이터 가져올 시, 출처 명시할 것 & 로컬에서의 데이터 위치는 밝히지 않는게 좋다.

Q&A 복습

Q. 비용함수를 목적함수로 설정하고 각각 L1 규제와 L2규제에 대한 제약식을 설정하여 최적해를 찾아나가는 방식으로 회귀계수를 업데이트 한다. 과정에서 릿지는 제약식이 원 형태이기에 0에 근접한 최적해가 도출되고 라쏘는 마름모 형태이므로 꼭짓점(회귀계수가 0인 점)에서 최적해가 도출된다. 이러한 차이가 발생하는 이유는 무엇인가?

A. L1과 L2 규제의 손실함수의 최적해 위치는 그림과 같이 표현이 가능하다. 아래 그림에서 도형(마름모, 원 등)은 규제영역이며, L1은 절대값이기에 마름모꼴로 표현되고 L2는 제곱이기에 원형으로 표현 된다.. L1과 L2 규제를 추가한 Loss Function은 규제영역에서 가장 최적값과 가까운지점(녹색점)이라고 할 수 있다. 또한 L1에서는 θ1의 값이 0이기에(녹색점), L1을 적용하면 특정 변수를 삭제할 수 있다. (Feature Selection)

'img25'

This post is licensed under CC BY 4.0 by the author.