본 포스팅은 저를 포함한 책을 구입한 분들의 학습 정리를 위해 쓰여졌습니다.
04-1 로지스틱 회귀
이번 장의 문제는 7개의 생선이 들어 있는 랜덤박스에 담긴 생선 종류의 확률을 알려주는 문제다. 이번에 다룰 수 있는 특성은 길이, 높이, 두께, 대각선 길이, 무게 총 5가지이다.
먼저 우리의 k-최근접 이웃 분류 알고리즘으로 클래스 확률을 계산해본다. 논리구조는 샘플 X 주위에 가장 가까운 이웃 샘플 10개 중 bream이 3개 roach가 5개 perch가 2개이면 bream 30%, roach 50%, perch 20%로 확률을 출력하는 것이다.
일단 데이터를 불러온다. pandas의 head() 메서드로 처음 5개 행을 출력해보자
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()
데이터프레임dataframe으로 변환하여 데이터를 읽어 들였다. 넘파이 배열과 비슷하게 열과 행으로 이루어져 있으며 통계와 그래프를 위한 메서드를 풍부하게 제공한다. 넘파이로 상호 변환이 쉽고 사이킷런과도 호환성이 높다. 여기서 맨 왼쪽의 0, 1, 2 ... 와 같은 숫자는 행 번호(pandas의 index), 맨 위에 쓰여진 Species, Weight, Length, Diagonal, Height, Width는 열 제목이다. pandas는 CSV 파일의 첫 줄을 자동으로 인식해 열 제목으로 만들어 준다. 데이터프레임은 pandas에서 제공하는 2차원 표 형식의 주요 데이터 구조이다.
Species 열에서 고유한 값을 추출하기 위해 unique() 메서드를 사용한다.
print(pd.unique(fish['Species']))
#결과값: ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
고유 값은 7개로 나와있다. 이 Species 열을 타깃으로 두고 나머지 5개 열은 입력 데이터로 이용하려 한다. 데이터프레임에서 열을 선택하는 방법은 데이터프레임에서 원하는 열을 리스트로 나열하면 된다. Species 열을 빼고 나머지 5개 열을 선택해 보면
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
리스트를 나열하면서 to_numpy() 메서드를 이용해 넘파이 배열로 바꾸어 객체에 저장했다. 객체에 저장된 5개의 특성을 출력해보자
print(fish_input[:5])
"""
결과값:
[[242. 25.4 30. 11.52 4.02 ]
[290. 26.3 31.2 12.48 4.3056]
[340. 26.5 31.1 12.3778 4.6961]
[363. 29. 33.5 12.73 4.4555]
[430. 29. 34. 12.444 5.134 ]]
"""
입력 데이터는 준비가 잘 된 것 같다. 동일한 방식으로 타깃 데이터도 따로 뽑는다. 참고로 타깃 값은 1차원 배열로 두어야 한다. 참고로 사이킷런에서 데이터는 대문자 X로 표시하고 레이블은 소문자 y로 표기한다. 이는 수학에서 함수의 입력을 x, 출력을 y로 나타내는 표준 공식 f(x)=y에서 유래된 것인데, 수학의 표기 방식을 따르되 입력 데이터는 2차원 배열(행렬)이므로 대문자 X를, 타깃은 1차원 배열(벡터)이므로 소문자 y를 사용한다.
fish_target = fish['Species'].to_numpy()
fish_target[:5]
#결과값: array(['Bream', 'Bream', 'Bream', 'Bream', 'Bream'], dtype=object)
이제는? 훈련세트와 테스트세트 분리 가야지
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)
다음은 표준점수 처리다. 2장에서 말했듯 분류 모델은 특성 스케일링이 필수다. 사이킷런의 StandardScaler로 쉽게 하는 방법을 배웠으니 활용하자
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))
"""
결과값:
0.8907563025210085
0.85
"""
어차피 여기서는 클래스 확률을 익히는 것이 목적이기 때문에 점수는 생각하지 말고 이걸 다중 분류multiclass classification 문제로 풀어보자. 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류라고 하는데 이진 분류와 모델을 만들고 훈련하는 방식은 그냥 동일하다. 주의할 점은 타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 매겨진다는 것을 기억해야 한다. 정렬된 타깃값은 classes_ 속성에 저장되어 있다.
#타겟데이터에 2개 이상의 클래스가 포함된 문제를 다중분류(multiclass classification)라고 함
#사이킷런에서는 문자열로 된 타깃값을 그대로 사용 가능한데, 타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 지정
print(kn.classes_)
#결과값: ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
알파벳 순으로 등장한다. predict() 메서드는 타깃값으로 예측을 출력해준다. 테스트 세트에 있는 처음 5개의 샘플의 타깃값을 예측해보면
#predict(): 타깃값으로 예측을 출력
print(kn.predict(test_scaled[:5]))
#결과값: ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
예측 확률도 확인하자. 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환한다. 테스트 세트에 있는 처음 5개의 샘플에 대한 확률을 출력한다. 넘파이의 round() 함수를 이용해 소수점 첫째 자리에서 반올림을 하는데 decimal 매개변수로 유지할 소수점 아래 자릿수를 지정할 수 있다. 여기선 소수점 4번째 자리까지 지정해줬다.
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))
"""
결과값:
[[0. 0. 1. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 1. 0. ]
[0. 0. 0. 1. 0. 0. 0. ]
[0. 0. 0.6667 0. 0.3333 0. 0. ]
[0. 0. 0.6667 0. 0.3333 0. 0. ]]
"""
predict_proba() 메서드의 출력 순서는 앞서 보았던 classes_ 속성과 같다. 즉, 첫 번째 열이 Bream에 대한 확율, 두 번째 열이 Parkki에 대한 확률이다. 이 모델이 계산한 확률이 가장 가까운 이웃의 비율과 동일한지 확인해 본다. 참고로 kneighbors() 메서드의 입력은 2차원 배열이어야 한다. 그래서 굳이 넘파이 배열의 슬라이싱 연산자를 선택했다. 슬라이싱 연산자는 하나의 샘플만 선택해도 항상 2차원의 배열이 만들어진다. 여기서는 4 번째 샘플을 하나 선택했다.
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
#결과값: [['Roach' 'Perch' 'Perch']]
이 샘플의 이웃은 Roach 1개 Perch 2개로 Perch의 확률이 0.6667이라는게 확인 됐다. 앞으로 번거로운 계산은 필요 없이 predict_proba()메서드만 호출하면 될 것 같다. 그런데 3개의 최근접 이웃만을 사용하기 때문에 확률이 0/3, 1/3, 2/3, 3/3으로 정해져 있다. 나쁘진 않은데 다른 방법도 찾아보는게 좋겠다.
로지스틱 회귀logistic regression는 말만 회귀지 사실 분류 모델이다. 이 모델은 선형 방정식을 가지고 학습을 하는데
z = a * (Weight) + b * (Legth) + c * (Diagonal) + d * (Height) + e * (Width) + f
a, b, c, d, e는 선형회귀모델을 배울 때와 마찬가지로 가중치나 계수로 불린다. 특성이 늘어났어도 다중 회귀 선형 방정식 적용 방법은 같다. 여기서 z는 어떤 값도 가능한데 확률은 기본적으로 0~1 사이가 되어야 한다. z가 아주 큰 음수면 0이 되고 아주 큰 양수가 되면 1이 되는게 좋겠다. 그렇게 만들기 위해 시그모이드 함수sigmoid function 또는 로지스틱 함수logistic function를 쓴다.
선형방정식의 출력 z를 음수를 사용해 자연 상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다. 이렇게 되면 z가 무한히 큰 음수 일 경우 이 함수는 0에 가까워지고 z가 무한하게 큰 양수가 될 때는 1에 가까워진다. 그리고 z가 0일 때는 0.5가 된다. 이 함수를 쓰면 절대로 0과 1 사이의 범위를 벗어날 수 없다. 그렇다면 이걸 확률로 쓰면 되겠다.
넘파이를 쓰면 그래프를 간단하게 그릴 수 있는데, -5와 5 사이에 0.1 간격으로 배열 z를 만든 다음 z 위치마다 시그모이드 함수를 계산해본다. 밑이 자연상수인 지수 함수를 계산하려면 np.exp() 함수를 사용한다.
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()
위에서 봤던 시그모이드 함수의 그래프가 그려졌다. 이제 이걸 활용한 로지스틱 회귀모델인 LogisticRegression 클래스를 활용해보자. 이진분류로 먼저 연습한다. 이진 분류에서는 시그모이드 함수의 출력이 0.5보다 크면 양성클래스 0.5보다 작으면 음성클래스로 판단한다. 참고로 사이킷런에서는 정확히 0.5가 나오면 음성 클래스로 판단한다.
넘파이 배열은 불리언 인덱싱Boolean indexing이라고 하여 True, False 값을 전달하여 행을 선택할 수 있다. 예시를 보면 쉽게 이해가 가능하다.
#넘파이 배열은 True, False값을 전달하여 행 선택 가능
#이를 불리어 배열(Boolean Indexing)이라고 함
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])
#결과값: ['A' 'C']
첫 번째와 세 번째 원소만 True기 때문에 A와 C만 골라냈다. 이 방식으로 Bream과 Smelt의 행을 골라내본다. 비교 연산자를 이용해 Bream과 Smelt의 행을 모두 True로 만들 수 있다. Bream인 행을 골라내려면 비교 연산자를 이용해 train_target == 'Bream'과 같이 쓴다. 이 비교식은 train_target에서 Bream은 True이고 나머지는 False로 배열을 반환한다. 여기서 비교결과를 OR 연산자 '|'를 사용해 합치면 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]
beam_smelt_indexes 배열은 bream과 smelt의 경우 True이고 그 외에는 False값이 들어가있다. 이 배열을 이용해서 train_scaled와 train_target 배열에 불리언 인덱싱을 적용하면 쉽게 도미와 빙어 데이터만 골라낼 수 있다. bream과 smelt만 있는 배열을 가지고 훈련을 해보고 샘플을 예측해본다.
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']
2번 빼고는 전부 Bream이다. predict_proba()메서드로 처음 5개의 예측 확률을 구해보면
print(lr.predict_proba(train_bream_smelt[:5]))
"""
결과값:
[[0.99759855 0.00240145]
[0.02735183 0.97264817]
[0.99486072 0.00513928]
[0.98584202 0.01415798]
[0.99767269 0.00232731]]
"
첫 번째 열은 음성 클래스에 대한 확률, 두 번째 열은 양성 클래스에 대한 확률이다. 여기서 우리는 Bream이 음성, 빙어가 양성 클래스라는 걸 알 수 있다. 사이킷런은 타깃값을 알파벳순으로 정렬해서 사용하기 때문에 ['Bream', 'Smelt'] 순으로 정렬이 되는데 클래스 구분도 [0, 1] 순이기 때문이다. 여기서 만약 Bream을 양성 클래스로 사용하려면 Bream의 타깃값을 1로 만들고 나머지 타깃값은 0으로 만들어 사용하면 된다.
선형 회귀에서처럼 로지스틱 회귀가 학습한 계수를 확인해보자.
print(lr.coef_, lr.intercept_)
#결과값: [[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]
이걸 정리해보면
z = -0.404 * (Weight) - 0.576 * (Legth) - 0.663 * (Diagonal) - 1.013 * (Height) - 0.732 * (Width) - 2.161
인 것과 마찬가지다. LogisticRegression 클래스는 decision_function() 메서드로 z 값을 출력할 수도 있다.
#LogisticRegression 모델로 z값 계산 -> decision_function()
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
#계산값: [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
여기서 나온 z값을 시그모이드 함수에 넣으면 확률을 구할 수 있다. 이번엔 파이썬의 사이파이scipy 라이브러리에 있는 시그모이드 함수 메서드인 expit()을 이용해 np.exp()을 대신하여 decisions 배열의 값을 확률로 변환해보자.
#이 z값을 시그모이드 함수에 통과시키면 확률 도출 가능
#파이썬의 scipy 라이브러리에 있는 시그모이드 함수 사용
#expit()
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이다. 여기선 충분한 훈련을 위해 반복횟수를 1,000으로 늘린다.
또한 LogisticRegression은 릿지회귀와 같이 계수의 제곱을 규제 가능하며 이런 규제를 L2 규제L2 regression 라고도 한다. 릿지와 마찬가지로 alpha 매개변수로 규제의 양을 조절한다. 하지만 LogisticRegression의 매개변수는 C이며 alpha와 반대로 작을수록 규제가 커진다. 기본 값은 1이다. 규제를 20으로 완화해서 다중 분류 모델을 훈련해보자.
#LogisticRegression은 기본적으로 반복적인 알고리즘을 사용(max_iter), 릿지 회귀와 같이 계수의 제곱을 규제(L2 Regression)
#L2 규제에서 규제를 제어하는 매개변수는 C, 작을수록 규제가 커짐, 기본값은 1
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개 예측과 예측 확률을 또 불러와본다.
print(lr.predict(test_scaled[:5]))
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
"""
결과값:
['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
[[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]]
"""
다중문제도 어렵지않게 풀어냈다! 선형방정식의 모양은 어떨지 확인해보기 위해 coef_와 intercept_의 크기를 출력해본다.
print(lr.coef_.shape, lr.intercept_.shape)
#결과값: (7, 5) (7,)
해당 데이터는 5개 특성을 이용하므로 배열의 열은 5이다. 그러면 행이 7개인 이유는? 분류 대상이 7가지이기 때문에 z를 7개씩 계산한다는 것이다. 여기서 가장 높은 z 값을 출력하는 클래스가 예측 클래스가 되는 것이다. 이진분류에서는 시그모이드 함수를 이용해 z를 0과 1 사이의 값으로 변환했지만, 다중 분류에서는 소프트맥스softmax 함수를 사용해 7개의 z 값을 확률로 변환한다. 소프트맥스 함수는 여러 개의 선형 방정식의 출력값을 0~1 사이로 압축하고 전체 합이 1이 되도록 만든다. 이를 위해 지수 함수를 사용하는데, 이를 정규화된 지수함수라고도 부른다.
소프트맥스의 계산방법은 먼저 n개의 z의 값의 이름을 z1~zn으로 붙여주고 이를 사용해 지수 함수 e^z1~e^zn을 계산해 모두 더한다. 이를 식으로 풀어보면
e_sum = e^z1 + e^z2 + e^z3 + e^z4 + .... e^z(n-1) + e^n
이런 방식으로 표현 가능하다. 그리고 각각의 항을 e_sum으로 나누어주면 최종 합이 1이 되는 식을 만들어 줄 수 있다. 그렇게 해서 각 항의 확률은 e^zn/e_sum인 것이다.
decision_function() 메서드로 z^1~z^n까지의 값을 구한 뒤 사이파이 라이브러리의 special 모듈 안에 있는 softmax() 소프트맥스 함수를 이용해 확률로 바꿔보자.
#소프트맥스 함수: 정규화된 지수함수, 여러 개의 선형 방정식의 출력값을 0~1사이로 압축하고 전체 합이 1이 되도록 만듦
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))
"""
결과값:
[[ -6.5 1.03 5.16 -2.73 3.34 0.33 -0.63]
[-10.86 1.93 4.77 -2.4 2.98 7.84 -4.26]
[ -4.34 -6.23 3.17 6.49 2.36 2.42 -3.87]
[ -0.68 0.45 2.65 -1.19 3.26 -5.75 1.26]
[ -6.4 -1.99 5.82 -0.11 3.5 -0.11 -0.71]]
"""
각 행마다 생선 별로 z값이 출력되었다. 이걸 소프트맥스 함수를 이용해 확률 계산을 하면
#앞서 구한 decision 배열을 softmax() 함수에 전달
from scipy.special import softmax
proba = softmax(decision, axis=1)
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]]
"""
다음과 같이 확률들이 등장한다. softmax() 함수에서 axis 매개변수는 소프트맥스를 계산할 축을 지정하는 데 여기서 axis=1로 지정하여 각 행, 각 샘플에 대해 소프트맥스를 계산한다. axis 매개변수를 지정하지 않으면 배열 전체에 대해 소프트맥스를 계산한다. 즉, z값의 위치가 열로 바뀐다.
결과값이 일치하게 나왔다.
04-2 확률적 경사 하강법
이제 얼추 머신러닝 지도학습의 기본이 끝났다. 그런데 모델링을 생성하는 과정은 꽤 간편하긴 한데, 모든 소프트웨어나 프로그램은 유지, 보수가 필수다. 학습 모델도 주기적으로 유지 보수 해줘야 하는데... 새로운 데이터를 추가할 때마다 만약 훈련을 다시한다면 너무 비효율적이지 않을까?
나중에 추가되는 데이터에 대해서만 조금씩 더 훈련하는 방식을 바로 점진적 학습 또는 온라인 학습이라 한다. 이 점진적 학습의 대표적인 알고리즘이 바로 확률적 경사 하강법Stochastic Gradient Descent이다.
일단 '확률적 = 무작위', '경사 = 기울기', ' 하강법 = 내려가는 방법'. 즉 경사하강법은 무작위로 기울기를 따라 내려가는 방법을 의미한다. 즉, 확률적 경사하강법은 훈련세트에서 랜덤하게 하나의 샘플을 골라 가장 가파른 길을 찾는 것이다.
만약 훈련세트의 모든 샘플을 다 사용했는데도 원하는 결과물(원하는 위치까지 하강)을 얻지 못했다면? 훈련세트에 모든 샘플을 다시 채워넣고 다시 랜덤하게 하나의 샘플을 선택해 이어서 경사를 내려간다. 이렇게 만족할만한 위치에 도달할 때까지 계속 내려가면 된다. 확률적 경사 하강법에서 룬련 세트를 한번 모두 사용 하는 과정을 에포크epoch라고 부른다. 일반적으로 경사하강법은 수십, 수백번 이상 에포크를 수행한다.
무작위로 샘플을 선택하다보니 문제가 생길 수 있다. 그래서 아주 조금씩 내려가야 원하는 위치에 도달할 확률이 높다. 물론 하나가 아닌 몇 개의 샘플을 한꺼번에 선택하여 경사를 따라 내려가는 방법도 있는데 이 방식을 미니배치 경사하강법minibatch gradient descent라고 한다.
극단적으로는 한번에 경사로를 따라 이동하기 위해 전체 샘플을 사용할 수도 있다. 이를 배치 경사 하강법batch graident descent라 한다. 가장 안정적이지만, 이 방법은 컴퓨터 자원을 너무 많이 사용하게 되는 문제를 발생시킨다.
결론적으로 확률적 경사 하강법은 훈련세트를 이용해 최적의 결과로 조금씩 이동하는 알고리즘이며 훈련 데이터가 모두 준비되어 있지 않고 매일 업데이트가 되어도 학습을 계속 이어나갈 수 있다. 이 이점을 이용해 데이터를 매우 많이 사용하는 신경망 알고리즘은 반드시 확률적 경사하강법을 이용한다.
그런데 대체 내려와야할 산이란? 바로 손실 함수loss function이다.
손실함수는 머신러닝 알고리즘을 측정하는 기준으로 값이 작을수록 손실이 적어 좋다고 판단한다. 하지만 어떠한 값이 최솟값인지는 알지 못하기에 가능한 많이 찾아보고 만족할만한 수준이면 산을 다 내려왔다고 인정하고 끝낸다. 확률적 경사하강법을 이용해 이 값을 찾아 조금씩 이동하는 것이다. 뱀발로 얘기하자면 비용함수cost function라는 말이 있는데 비용함수란 하나의 샘플에 대한 손실함수가 아닌 훈련세트 전체 샘플에 대한 손실 함수의 합을 말한다. 하지만 보통 손실함수와 크게 구분하지는 않는다.
손실이란 오답이다. 그러나 아무 오답이나 손실이 될 수는 없다. 경사하강법은 말 그대로 연속적인 경사를 내려오는 것이기 때문에 '연속적인 범위'를 지닌 오답만 가능하다. 즉, 손실 함수는 '미분 가능'해야한다는 것이다. 예측은 1 또는 0으로 범주categorical이지만 예측 확률은 0~1사이의 연속적인 수이다. 결국 확률 값을 손실함수의 오답으로 사용해주면 된다.
로지스틱 손실함수가 계산되는 방법을 잠깐 확인하고 넘어가보자. 만약 샘플 4개의 예측이 1, 0, 0, 1이고 정답이 1, 1, 0, 0 인데
예측 정답(타깃)
1 = 1
0 ≠ 1
0 = 0
1 ≠ 0
예측 확률은 0.9, 0.3, 0.2, 0.8이라 했을 때, 이 확률을 손실의 개념으로 바꾸어주어야한다. 간단히 말하면 높은 확률값은 낮은 손실값으로, 낮은 확률값은 높은 손실값으로 바꾸어주는 일이다.
첫 번째 샘플은 0.9이므로 타깃 값인 1과 곱한 다음 -1을 곱해 음수로 만들어주면 낮은 손실 형태로 바꿔지게 된다. 즉, 손실함수는 -1~0사이에서의 수의 크기를 비교해 0과 가까워지면 손실이 큰 (숫자가 더 크니까) 것으로 판별하도록 하는 것이다.
sample1: 예측(0.9) * 타깃(1) * -1 = -0.9
두 번째 샘플은 0.3이다. 타깃은 양성클래스이지만 못맞췄다.. 아까와 마찬가지로 타깃 값을 곱하고 음수로 변환해주면 -0.3이 되면 손실 값이 된다.
sample2: 예측(0.3) * 타깃(1) * -1 = -0.3
앞선 두 샘플은 양성클래스(1)를 맞추거나 못 맞춘 경우이다. 이 경우는 '예측 * 타깃 * -1'의 형식으로 손실함수를 계산했다. 그런데 음성클래스(0)를 맞추거나 못맞춘 것은 조금 다른 계산을 한다.
세 번째 샘플은 0.2이다. 여기서 타깃 값인 0과 곱하면 무조건 0이 된다. 이 상황을 방지하기 위해 타깃을 1(양성클래스)로 바꿔준다. 그 대신 예측 값을 양성 클래스에 대한 예측으로 반전시켜준다 (1 - 0.2 = 0.8) 이걸 타깃값과 곱해 음수로 바꿔주면 손실 값이 나온다. 음성클래스를 맞춘 샘플이기에 손실이 낮다!
sample3: (1 - 예측(0.2)) * 타깃(0 → 1) * -1 = -0.8
네 번째 샘플은 0.8로 예측은 했지만 음성클래스(0)를 못 맞춘 경우이다. 아까와 마찬가지로 타깃을 1로 바꾸고 예측 확률을 1에서 뺀 다음 음수로 바꾼다.
sample4: (1 - 예측(0.8)) * 타깃(0 → 1) * -1 = -0.2
그런데, 이 손실 함수가 음수로 되어 있는게 약간 마음에 걸린다. 이 문제로 해결하기 위해 여기서 예측 확률에 로그함수를 적용하면 좋다. 로그 함수는 예측 확률의 범위와 같은 0~1 범위의 값이 음수로 출력되기 때문에 최종 손실 값이 양수로 변환된다. 또 로그 함수는 0에 가까울수록 아주 큰 음수가 되기 때문에 손실을 아주 크게 만들어 모델에 큰 영향을 미칠 수도 있다.
이 식을 총 정리하자면
양성클래스(타깃 = 1) 일 때 손실함수 → -log(예측확률)
음성클래스(타깃 = 0) 일 때 손실함수 → -log(1-예측확률)
로 정의할 수 있다.
이 손실 함수는 로지스틱 손실 함수logistic loss function 또는 이진 크로스엔트로피 손실 함수binary cross-entrophy loss function라고 부른다. 다중 분류 또한 비슷한 형태의 손실함수를 사용하는데 그건 크로스엔트로피 손실 함수cross-entrophy loss function라 부른다.
회귀의 손실 함수로 평균 절댓값 오차mean absolute error를 사용할 수도 있다. 타깃에서 예측을 뺀 절대값에 모든 샘플을 평균한 값이다. 물론 절댓값이 아닌 제곱한 값을 사용한 다음 모든 샘플에 평균한 값을 써도 좋다. 이는 평균 제곱 오차mean squared error라고 한다. 모두 값이 작을 수록 좋은 모델이라 평가한다.
사실 손실 함수를 직접 계산하는 일은 없지만, 정의는 알고 가야 나중에 편하다. 이제 본격적으로 확률적 경사 하강법을 사용한 분류 모델을 만들어 보자.
1. 데이터를 불러온다 #pandas #read_csv()
2. 입력 데이터와 타깃 데이터를 나누어 넘파이 배열로 바꿔 준다. #pandas #to_numpy()
3. 훈련세트와 테스트 세트를 나눈다 #sklearn #model_selection #train_test_split()
4. 훈련세트에서 학습한 통계값으로 표준화 전처리를 한다. #sklearn #preprocessing #StandardScaler
1~4 과정은 모두 같다. 한번에 코드화 시켜보자
#1. 데이터 불러오기
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
# 2. 입력 데이터와 타깃 데이터 나누기
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
fish_target = fish['Species'].to_numpy()
# 3. 훈련세트와 테스트 세트 나누기
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)
# 4. 표준화 하기
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
앞선 과정이 모두 끝났다면
5. 확률적 경사 하강법 모델 이용해 학습하고 점수 산출하기 #sklearn #SGDClassifier #fit() #score()
이 5번 째 작업을 해주면 된다. 사이킷런에서 확률적 경사 하강법을 제공하는 대표적인 분류용 클래스는 SGDClassifier이다. 이 클래스는 linear_model 모듈 안에 있다. SGDClassifier의 객체를 만들 땐 2개의 매개변수를 지정한다. loss는 손실 함수의 종류를 지정하고, max_iter는 수행할 에포크의 횟수를 지정한다. 여기서는 loss='log'로 지정하여 로지스틱 손실함수를 사용하고 max_iter='10'으로 지정해 전체 훈련세트를 10회 반복한다.
#확률적 경사 하강법을 제공하는 분류용 클래스 SGDClassifier
from sklearn.linear_model import SGDClassifier
#SGDClassifier의 객체를 만들 때 2개의 매개변수 지정
#loss는 손실함수의 종류를 지정
#loss에 log를 지정하면 클래스마다 이진 분류모델을 만듦. 즉 타겟데이터는 양성으로 두고 나머지를 모두 음성으로 둠. OvR(One versus Rest)
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))
"""
결과값:
0.773109243697479
0.775
"""
모델이 충분히 수렴하지 않았다는 ConvergenceWarning 경고를 보냈다. 이럴땐 max_iter로 매개변수 값을 늘려주어야 한다. 그러나 오류까지는 아니므로 일단 넘어간다. 사실 일부러 max_iter를 적게 돌렸다. 우리는 기존 훈련 모델에 점진적 학습을 추가로 해주는게 목표니까.
다시 객체를 지정해주지 않고 훈련한 모델인 sc를 추가로 훈련해본다. 모델을 이어서 훈련할 때는 fit() 메서드가 아닌 partial_fit() 메서드를 이용해 1 에포크씩 이어서 훈련할 수 있다.
#에포크: 훈련 샘플을 활용한 학습 횟 수
#partial_fit(): 모델을 이어서 훈련, 호출할 때마다 1 에포크씩 이어서 훈련 가능
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))
"""
결과값:
0.8151260504201681
0.825
"""
에포크를 한번 더 실행하니 정확도가 향상되었다. 이 모델을 여러 에포크로 더 훈련을 해볼 수는 있겠지만.... 기준이 없으니 불안하다. 과대적합과 과소적합을 방지하는 기준이 하나 필요하다.
확률적 경사 하강법은 에포크의 횟 수에 따라 과소적합이나 과대적합이 될 수 있다. 에포크가 적으면 훈련세트를 덜 학습하고 너무 많으면 훈련세트에 아주 잘맞는 모델이 되어 과대적합을 발생시킨다. 이런 문제를 방지하기 위해 과대적합이 일어나기 전에 훈련을 멈추는 것을 조기 종료Early stopping라고 한다.
이제부터는 partitial_fit() 메서드만 사용해 훈련세트에 있는 전체 클래스의 레이블을 전달해 주어야한다. 이를 위해 np.unique()함수로 train_target에 있는 7개의 생선 목록을 만들고 에포크마다 훈련세트와 테스트 세트에 대한 점수를 기록하기 위해 2개의 리스트를 준비한다.
#조기종료(Early stopping): 과대적합이 일어나기 전에 훈련을 멈추는 것
#np.unique()로 train_target에 있는 7개의 생선 목록을 만들고 점수 기록을 위한 2개의 리스트 준비
import numpy as np
sc = SGDClassifier(loss='log', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_target)
#300번의 에포크 진행, 반복마다 훈련세트와 테스트세트의 점수를 계산하여 train_score, test_score리스트에 추가
#파이썬의 _는 특별한 변수, 나중에 사용하지 않고 그냥 버리는 값을 넣어두는 용도, 여기서는 0에서 299까지 반복 횟수를 임시 저장하기 위한 용도로 사용
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번의 에포크를 훈련을 반복해서 진행한다. 반복마다 훈련 세트와 테스트 세트의 점수를 계산하여 train_score, test_score 리스트에 추가한다. 참고로 for 문에 담긴 '_'는 파이썬에서 사용하는 특별한 변수로 나중에 사용하지 않고 그냥 버리는 값을 넣어두는 용도로 사용한다. 여기서는 0~299까지 반복 횟수를 임시로 저장하는 용도로 쓰였다. partial_fit()에 classes를 굳이 정의해주는 이유는 train_target 안에 빠진 class가 있을 수 있기 때문에 정해주는 것이다.
이제 300번의 에포크 동안 기록한 훈련 세트와 테스트 세트의 점수를 그래프로 그려보자
import matplotlib.pyplot as plt
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()
train_score와 test_score에 있는 x축은 자동으로 만들어진 데이터의 순서이고 y축은 저장된 점수이다. 데이터가 작기 때문에 아주 잘 드러나지는 않지만 백 번째 에포크 이후에는 훈련세트와 테스트세트의 점수가 조금씩 벌어지고 있다. 이 모델은 100의 반복이 적절한 것으로 보이므로 100에 반복 횟수를 맞추어 다시 훈련해본다.
#SGDClassifier는 일정 에포크 동안 성능이 향상되지 않으면 자동으로 멈춤
#따라서 tol 매개변수에 향상될 최솟값을 지정해줌
sc = SGDClassifier(loss='log', 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와 같다.
loss 매개변수에 대해 조금 더 깊이 확인한 다음 이 장을 마무리하자. loss 매개변수의 기본값은 'hinge'다. 힌지 손실hinge loss은 서포트 벡터 머신support vector machine이라고도 불리는데, 이는 또 다른 머신러닝 알고리즘을 위한 손실 함수다. 이 기본값을 가지고 훈련해보면
#loss 매개변수의 기본값은 hinge
#힌지 손실(hinge loss)은 서포트 벡터머신(support vector machine)이라 불리는 또다른 머신러닝 알고리즘을 위한 손실 함수
#SGDClassifier는 여러 손실함수를 loss 매개변수에 지정하여 다양한 머신러닝 알고리즘을 지원
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
"""
비슷하지만 조금 다른결과가 나왔다. 이 훈련 모델도 나쁘지 않다,
결론
분류모델은 예측 뿐만 아니라 예측의 근거가 되는 확률을 출력할 수 있다. k-최근접 이웃 모델은 확률을 출력할 수는 있지만 이웃한 샘플의 클래스 비율이므로 항상 정해진 확률만 출력한다. 로지스틱회귀 모델은 분류 모델로서 선형 회귀처럼 선형 방정식을 사용해 분류해낸다. 그러나 로지스틱 회귀는 계산한 z값을 이진분류에서는 시그모이드 함수, 다중 분류에서는 소프트맥스 함수를 이용해 z 값을 0~1 사이의 값으로 압축해 확률로 바꾸어 준다. 이진 분류에서는 하나의 선형방정식을 훈련해 1에 가까우면 양성 0에 가까우면 음성 클래스로 판별하며 다중 분류에서는 클래스 개수만큼 방정식을 훈련 한 뒤 각 방정식의 출력 값을 소프트맥스 함수를 통과시켜 전체 클래스에 대한 합이 항상 1이 되도록 한다.
확률적 경사하강법을 통해 점진적 학습을 하는 로지스틱 회귀 모델에 대해서도 알아봤다. 확률적 경사하강법은 손실함수라는 산을 정의하고 가장 가파른 경사(낮은 손실값)를 따라 조금씩 내려오는 알고리즘이다. 그러나 훈련을 반복할수록 모델이 훈련세트에 점점 더 잘 맞게 되어 과대적합이 발생하기도 한다. 이를 방지하기 위해 테스트세트와 훈련세트의 차이가 벌어지면 조기 종료를 지정해준다.
Key Word
로지스틱 회귀(logistic regression): 선형 방정식을 사용한 분류 알고리즘. 선형 회귀와 달리 시그모이드 함수나 소프트맥스 함수를 사용하여 클래스 확률을 출력할 수 있다.
다중 분류(multiclass classification): 타깃 클래스가 2개 이상인 분류 문제로, 로지스틱 회귀는 다중 분류를 위해 소프트맥스 함수를 사용하여 클래스를 예측한다.
시그모이드 함수(sigmoid function): 선형 방정식의 출력을 0과 1 사이의 값으로 압축하며 이진 분류를 위해 사용한다.
소프트맥스 함수(softmax function): 다중 분류에서 여러 선형 방정식의 출력 결과를 정규화하여 합이 1이 되도록 만든다. 소프트맥스 함수는 지수 함수를 사용하는데, 이를 정규화된 지수함수라고도 부른다.
불리언 인덱싱(Boolean indexing): 넘파이에서 제공하는 기능으로 배열을 불리언 형태로 인덱싱하여 추출할 수 있다. 즉, True, False 값을 전달하여 원하는 데이터(True)를 선택한다.
데이터프레임(dataframe): 판다스에서 제공하는 2차원 데이터 구조. 넘파이 배열과 비슷하게 열과 행으로 이루어져 있으며 통계와 그래프를 위한 메서드를 풍부하게 제공한다. 넘파이로 상호 변환이 쉽고 사이킷런과도 호환성이 높다.
확률적 경사 하강법(stochastic gradient descent): 훈련세트에서 샘플 하나씩 꺼내 손실 함수의 경사를 따라 최적의 모델을 찾는 알고리즘이다. 샘플을 하나씩 사용하지 않고 여러 개를 사용하면 미니배치 경사하강법(minibatch gradient descent)이 된다. 한 번에 전체 샘플을 사용하면 배치 경사 하강법(batch gradient descent)이 된다.
손실(loss): 손실이란 오답이다. 그러나 아무 오답이나 손실이 될 수는 없다. 경사하강법은 말 그대로 연속적인 경사를 내려오는 것이기 때문에 '연속적인 범위'를 지닌 오답만 가능하다. 즉, 손실 함수는 '미분 가능'해야한다는 것이다. 예측은 1 또는 0으로 범주categorical이지만 예측 확률은 0~1사이의 연속적인 수이다. 결국 확률 값을 손실함수의 오답으로 사용해주면 된다. loss 매개변수의 기본값은 'hinge'다. 힌지 손실hinge loss은 서포트 벡터 머신support vector machine이라고도 불리는데, 이는 또 다른 머신러닝 알고리즘을 위한 손실 함수다. 이 기본값을 가지고 훈련해보면
손실 함수(loss function): 확률적 경사 하강법이 최적화할 대상이다. 대부분의 문제 잘 맞는 손실함수가 이미 정의되어 있다. 이진 분류에서는 로지스틱 회귀(logistic regression loss function a.k.a. 이진 크로스엔트로피 손실 함수(binary cross-entrophy loss function)를 사용하고 다중 분류에는 크로스 엔트로피 손실 함수(cross-entrophy loss function)를 사용한다. 회귀 문제에서는 평균 제곱 오차 손실 함수(mean squared loss function)나 평균 절대값 손실함수(mean absolute loss function)를 사용한다.
비용함수(cost function): 비용함수란 하나의 샘플에 대한 손실함수가 아닌 훈련세트 전체 샘플에 대한 손실 함수의 합을 말한다. 하지만 보통 손실함수와 크게 구분하지는 않는다.
조기 종료(early stopping): 과대적합이 일어나기 전에 훈련을 멈추는 것이다. SGDClassifier에선 n_iter_no_change 매개변수와 tol 매개변수를 이용해 훈련을 멈추는 기준을 지정해준다.
에포크(epoch): 확률적 경사 하강법에서 전체 샘플을 모두 사용하는 한번 반복을 의미한다. 일반적으로 경사 하강법 알고리즘은 수십에서 수백 번의 에포크를 반복한다.
패키지와 함수
[넘파이 라이브러리numpy]
np.exp(): 밑이 자연상수(e)인 지수함수를 사용하기 위한 메서드
round(): 반올림을 하기 위한 메서드
- decimal=: 소수점 아래의 자릿수를 정하는 매개변수
[판다스pandas]
head(): 데이터프레임의 첫 5행을 출력하는 메서드. 맨 아래의 5행을 불러내는 메서드로 tail()이 있다.
unique(): 데이터프레임 내 존재하는 데이터의 유니크 값을 반환해주는 메서드
to_numpy(): 데이터프레임에서 넘파이로 배열 구조를 바꾸어 주는 메서드
[사이킷런 라이브러리scikit-learn]
LogisticRegression: 선형 분류 알고리즘인 로지스틱 회귀를 위한 클래스.
- solver=: 사용할 알고리즘 선택. 기본값은 '1bfgs'이다. 'saga'는 확률적 평균 경사 하강법 알고리즘으로 특성과 샘플 수가 많을 때 성능이 빠르고 좋음
- penalty=: 규제 방식 선택 가능. L2규제(릿지), L1규제(라쏘) 중 방식을 선택할 수 있다.
- C=: 규제의 강도를 제어한다. 기본값은 1.0이며 값이 작을수록 규제가 강해진다.
- max_iter=: 반복 학습 횟수를 설정한다. 기본값은 100이다.
SGDClassifier: 확률적 경사 하강법을 사용한 분류모델을 만든다.
- loss=: 최적화할 손실 함수를 지정한다. 기본값은 서포트 벡터머신을 위한 'hinge' 손실 함수이다. 로지스틱 회귀를 위해서는 log로 지정한다.
- penalty=: 규제의 종류를 지정한다. 기본값은 L2 규제(릿지)를 위한 'l2'이다. L1규제를 적용하려면 'l1'으로 지정한다.
- alpha=: 규제의 강도를 지정하며 기본값은 0.0001이다.
- n_iter_no_change=: 에포크의 손실에 변화가 없을 시 반복을 멈추어야 할 횟수를 지정한다. 기본값은 5이다.
- tol=: 반복을 멈출 조건을 지정한다. n_iter_no_change 매개변수에서 지정한 에포크 동안 손실이 tol만큼 줄어들지 않으면 알고리즘이 중단된다. tol 매개변수의 기본값은 0.001이다.
predict(): 예측 확률을 반환한다. 이진 분류는 샘플마다 음성클래스와 양성 클래스에 대한 확률을 반환하고 다중 분류는 샘플마다 모든 클래스에 대한 확률을 반환한다.
decision_function(): 모델이 학습한 선형방정식의 출력을 반환한다. 이진 분류의 경우 양성클래스의 확률이 반환된다. z 값이 0보다 크면 양성 클래스, 작거나 같으면 음성 클래스로 예측한다. 다중 분류는 각 클래스마다 선형 방정식을 계산하고 가장 큰 값의 클래스가 예측 클래스가 된다.
partial_fit(): 부분 학습을 위한 메서드이다. 신규 데이터에 빠진 타깃 클래스가 있을 수 있기 때문에 모든 타깃 클래스를 담은 객체를 classes 매개변수를 이용해 담아준다.
- classes: 타깃값의 분류 클래스 데이터를 지정해준다.
[사이파이 라이브러리scifi]
softmax(): special 모듈 안에 있는 소프트맥스 함수 확률 계산 메서드. 사이킷런의 decision_function() 메서드로 다중분류 문제의 z값을 구한 뒤 주로 사용한다.
sigmoid(): special 모듈 안에 있는 시그모이드 함수 확률 계산 메서드. 사이킷런의 decision_function() 메서드로 이진분류 문제의 z값을 구한 뒤 주로 사용한다.
'데이터 분석 학습' 카테고리의 다른 글
[혼자공부하는머신러닝+딥러닝] Ch.03 회귀 알고리즘과 모델 규제 / K-최근접이웃회귀, 선형회귀, 특성 공학과 규제 (0) | 2022.04.19 |
---|---|
[혼자공부하는머신러닝+딥러닝] Ch.02 데이터 다루기 / 훈련-테스트 세트 분리, 전처리 (0) | 2022.04.15 |
[혼자공부하는머신러닝+딥러닝] Ch.01 나의 첫 머신러닝 리뷰 / K-최근접 이웃 모델 (0) | 2022.04.05 |
[모두의 데이터분석 With 파이썬] Unit.08 리뷰 / 항아리모양 그래프 그리기 (0) | 2022.03.25 |
[모두의 데이터분석 With 파이썬] Unit.07 리뷰 / 데이터에 맞는 시각화 (0) | 2022.03.24 |