앞의 데이터와는 달리 만약 훈련 데이터들이 준비되어 있지 않고,

시간에 걸쳐 조금씩 추가가된다면 어떻게 학습을 시켜야할까?

 

기존에 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 훈련시킬 수는 없을까?

점진적 학습을 통해 이를 해결할 수 있다.

대표적인 점진적 학습법은 확률적 경사 하강법이라고 한다. (stochastic Gradient Descent)

 

확률적 경사 하강법에서 확률적이다 라는 말은 '무작위하게' 혹은 '랜덤하게'의 기술적인 표현이다.

경사 = 기울기

하강법 = 내려가는 방법

 

경사하강법은 기본적으로 가장 빠르게 내려가는 방법을 고수한다. (산에서 내려오는것을 생각)

하지만 만약 발걸음이 너무 크다면 경사를 따라 내려가는게 아니라 오히려 올라갈 수 도있다.

 경사하강법은 훈련 세트를 사용해 훈련할 때, 전체 샘플을 사용하지 않고, 딱 하나의 샘플을 훈련 세트에서 랜덤하게 골라 가장 가파른 길을 찾는다. 이처럼 훈련 세트에서 랜덤하게 하나의 샘플을 고르는것이 바로 률적 경사 하강법이라고 한다.

 < 훈련세트에서 랜덤하게 하나의 샘플을 택해 조금 내려간 후 다음 샘플 선택해서 내려가고, 다음,..다음...이런방식 >

 이렇게 모든 샘플을 사용할때 까지 반복한다.

 

하지만 만약 그래도 산을 다 내려오지 못한다면? 

다시 처음부터! 훈련세트에 모든 샘플을 다시 채워넣은 후 다시 랜덤하게 하나의 샘플을 선택해 이어서 경사를 내려간다.

이렇게 만족할 때 까지 계속 내려가면 된다.

확률적 경사 하강법에서 훈련 세트를 한 번 모두 사용하는 과정을 에포크라고 부른다.

일반적으로 경사하강법은 수십,수 백번의 에포크를 수행한다고 한다..

 

경사하강법은 무작위 샘플을 선택해서 산에서 내려가는건데 너무 무책임한거 아니야??라고 생각할 수 있다.

원래는 걱정하는것과 달리 이 알고리즘은 매우 정상적으로 작동하지만, 그래도 걱정이 된다면

1개씩 말고 몇 개씩 샘플을 선택해서 따라 내려갈 수 있다. 이를

미니배치 경사 하강법이라고 부른다. 이 방법또한 실전에서 많이 쓰인다.

 

그 밖에도 배치 경사 하강법이라는 방법도 있는데, 이는 한 번 경사를 따라 이동하는데에 모든 샘플을 사용한다.

이 방법이 가장 안전,정확하지만 그만큼 전체데이터를 사용하기 때문에 시간,자원이 많이 소모된다.

 

이 확률적 경사 하강법을 통해 매일매일 데이터가 업데이트 되어도 학습을 계속 이어나갈 수 있을것이다!

 

그런데 문제가 있다. 어디서 내려가야하는걸까? + 산은 도대체 무엇인걸까?? 이 산은 손실함수라고 부른다.

 

손실 함수 ( loss function )

손실함수란 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준이다.

그렇다면 손실함수의 값이 작을수록 좋다. 다행히도 우리가 다루는 많은 문제에 필요한 손실함수는 이미 정의가 되어있다.

 

이제 더 자세히 알아보도록 하자.

분류에서 손실은 확실하다. 못맞추는것 !

 

그럼 이를 어떻게 표현할 수 있지 않을까?

만약 4번의 예측 중에 두 번만 맞췄다면 정확도는 1/2 ( 0.5 ) 이다.

정확도를 손실함수로 사용한다면? 음수를 취해 -1이 가장 낮고, -0이 가장 높다. 이렇게 정확도를 손실함수로 사용할 수 있다.

하지만 정확도에는 치명적 단점이 있는데, 앞의 4개의 샘플만 있다면 가능한 정확도는 0, 0.25, 0.5 , 0.75, 1 다섯가지 뿐이다. 이는 경사하강법에서 연속적이지 않고 듬성듬성하게 내려오면 부적절할 수 있다.

 

그렇다면 손실함수를 연속적으로 만들려면 앞의 로지스틱과 연관지어보면된다.

 

로지스틱 손실 함수

우선 예시로 샘플 4개의 예측 확률을 각각 0.9, 0.3, 0.2, 0.8 이라고 가정해보자.

그리고 첫번째 두번째의 타겟은 1,   세 번째, 네 번째의 타겟은 0 이다. (불리안)

첫 번째 샘플의 예측은 0.9이므로 양성 클래스의 타깃인 1과 곱한다음 음수로 바꿀 수 있다. 이 경우 예측이 1에 가까울 수록 좋은 모델이라고 할 수 있다. 반대로 예측이 1에 가까울수록 음수는 점점 작아진다. 이 값을 손실함수로 사용해보자.

 

두 번째 샘플의 예측은 0.3이다. 거리가 멀다. 첫 번째 샘플처럼  - (예측 x 타겟)을 해보자. -0.3이다. 첫 번째보다 높은 손실.

 

세 번째 샘플에서 타겟 0을 곱하면 무조건 0이되기 때문에 문제가 생긴다.

★★★따라서 타겟을 양성클래스와 같이 1로 바꿔준다. 대신에 예측값도 양성클래스에 대한 예측으로 바꿔준다!!! ★★★

그렇다면 1-0.2 = - (0.8 x 1) = -0.8이다. 꽤 낮은 손실이다.

 

네 번째 샘플또한 세번째처럼 양성처럼 바꿔준다. -0.2이다. 높은 손실이다.

1,3번째 샘플 = 낮은 손실, 2,4번째 샘플  = 높은 손실

 

 

예측 확률을 사용해 이런 방식으로 계산한다면 연속적인 손실 함수를 얻을 수 있을 것 같다. 여기에서 예측 확률에 로그함수를 적용한다면 더욱 보기 좋을 수 있다.

로그함수의 특성 ( 0에 가까울 수록 큰 음수가 되어 손실을 크게 만들어줆.)

위와 같은  손실 함수를 로지스틱 손실 함수 또는 <이진or다중>크로스엔트로피 손실 함수라고 부른다.

 

이제 손실함수까지 알아봤으니, 경사 하강법을 이용한 분류 모델을 보자.

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)

 

SGD Classifier

★SGD Classifier클래스는 사이킷런에서 확률적 경사 하강법을 제공하는 대표적인 분류용 클래스이다. !!!!★

from sklern.linear_model import SGDClassifier

SGDClassifier는 두개의 매개변수가 필요하다. loss= 와 max_iter= 이다.

loss=는 손실함수의 종류, max_iter = 은 에포크 횟수

여기서는 loss='log', max_iter = 10으로 해보자.

sc = SGDClassifier(loss='log', max_iter=10 , random_state=42)
sc.fit(train_scaled,train_target)

sc.score(train_scaled, train_target)
sc.score(test_scaled, test_target)

0.773109243697479
0.775

0.7로 정확도가 그리 높지 않은것을 볼 수 있다.

아마도 에포크가 부족한 것일 수도 있다.

 

SGDClassifier의 장점?특성에 따라 다시 만들 필요없이 추가 훈련을 해보자.

모델 추가 훈련할 때에는 partial_fit() 메소드를 사용하면 된다.

 

이 메소드는 fit()과 같지만 호출할 때마다 에포크를 1씩 이어서 훈련할 수 있다는 특징이 있다.

partial_fit() 메소드를 호출하고 다시 훈련세트와 테스트세트의 점수를 확인해보자.

 

sc.partial_fit(train_scaled, train_target)
sc.score(train_scaled, train_target)
sc.score(test_scaled, test_target)

0.8151260504201681
0.85

오 더 올라감. 그런데 얼마나 올려야할까????? 기준이 필요하다.

 

에포크의 과대/과소 적합

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

에포크 횟수가 적으면 모델이 훈련 세트를 덜 학습한다. 마치 산을 다 내려오지 못하고 훈련이 끝난 상황.

에포크 횟수가 많으면 훈련 세트를 완전히 학습할 것이다.

바꿔 말하면 적은 에포크 횟수 동안 훈련한 모델은 훈련/테스트 세트에 잘 맞지 않는 과소적합된 모델일 것이고,  많은 에포크 횟수 동안 훈련한 모델은 훈련세트에 잘맞아 테스트세트에는 점수가 나쁜 과대적합된 모델일 가능성이 높다.

이를 그림으로 표현하면

 

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

과대적합이 시작하기전에 훈련을 멈추는것을 조기종료라고 한다.

 

우리가 가진 데이터으로 위와 같은 그래프를 만들어보자.

이제 fit()이 아닌 partial_fit()만 사용할건데, 이를 위해서는 훈련세트에 있는 전체 클래스의 레이블을 partial_fit()메소드에 전달해줘야 한다. 이를 위해 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)		# unique() = 오름차순으로 고유값 정렬시킴(중복제거)

300번의 에포크를 해보자.

for _ in range(0,300):
	sc.partial_fit(train_scaled, train_target, classes=classes)
    train_score.append(sc.score(train_scaled,train_target))
    train_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()

약 백 번째 에포크 이후에는 훈련 세트의 테스트 세트가 점점 벌어진다!!

 

이제 반복 횟수를 100에 맞춰볼까?

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만큼 무조건 반복하도록 하였다.

 

판다스를 이용하여 csv파일을 읽어보자!

import pandas as pd
fish = pd.read_csv("https://bit.ly/fish_csv_data")
fish.head()

species에는 어떠한 종류의 생선들이 있는지 확인하기 위해 unique() 함수를 사용해보자.

pd.unique(fish["Species"])

도미,빙어를 비롯해 총 7가지 종류의 생선이 있다.

이제 Species 열을 타겟으로 만들고 나머지 5개 열을 입력 데이터로 사용해보자.

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

잘 입력되었는지 처음 5개 행을 출력해보자!

print(fish_input[:5])

array([[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 ]])

이제 타겟 데이터를 준비하자. ( 종류 ) 

fish_target = fish["Species"].to_numpy()

이제 훈련,테스트 세트로 나눠보자.

from sklearn.model_selection import train_test_split()
train_input, train_target, test_input, test_target = train_test_split(fish_input,fish_target,random_state=42)

 

이제 데이터 전처리 (표준화)를 하고 훈련시켜보자 !

from sklearn.preprocessing import StandardScaler
ss = StandardSclaer()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

K-최근접 이웃 분류기로 확률 예측

from sklearn.neighbors import KNeighborsClassifier
Kn = KNeighborsClassifier()
Kn.fit(train_scaled,train_target)
print(Kn.score(train_scaled,train_target))
print(kn.score(test_scaled,test_target))

0.8907563025210085
0.85

이제 타겟값을 어떻게 예측하는지 알아보자.

print(kn.classes_)

array(['Bream', 'Parkki', 'Perch', 'Pike', 'Roach', 'Smelt', 'Whitefish'],
      dtype=object)

'Bream'이 첫 번째 클라스, 'Parkki'가 두 번째 클라스.. 방식 predict()메소드를 이용해서 타깃값으로 예측을 출력해보자.

Kn.predict(test_scaled[:5])

array(['Perch', 'Smelt', 'Pike', 'Perch', 'Perch'], dtype=object)

predit_proba()메소드클래스별 확률값을 반환한다.

import numpy as np
proba = Kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4)     #np.round() = 반올림 , decimals = n >>소수 n번째 자리까지 표시

해석 => 첫 번째 열 : 'Bream'일 확률, 두 번째 열 : 'Parkki'일 확률 ,,,,,,,이다.

확률을 봤으니 직접 샘플의 최근접 이웃 (3개)가 무엇인지 살펴보자.

distances, indexes = Kn.kneighbors(test_scaled[3:4])
print(train_scaled[indexes])

[['Roach' 'Perch' 'Perch']]

1/3 = 0.33333333      2/3 = 0.66666667 

기준 샘플 수가 3개이다 보니 확률이 0, 0.3, 0.6, 1 네 개의 숫자 밖에 나오지 않는다...  더 좋은 방법은 없을까?

 

로지스틱 회귀

로지스틱 회귀 (logistic regression)는 이름은 회귀이지만 분류 모델이다.

또한 로지스틱 회귀는 선형 회귀와 마찬가지로 선형 방정식을 학습한다.

 

변수 a,b,c,d,e는 가중치/계수.

z의 값은 0~1로 나오면 확률로 표현할 수 있다. 이를 위해 시그모이드 함수를 사용하면 된다!

import numpy as np
import matplot.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 ~ 1로 출력된다!

 

# 로지스틱 회귀로 이진 분류 수행해보자

이진 분류이므로 불리언 인덱싱을 통해 해결 ( T / F)

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

['A' 'C']

위와 같은 방식을 통해 훈련 세트에서 도미,빙어의 행만 골라 내자

bream_smelt_indexes = (train_target == "Bream" ) | (train_target == "Smelt")
train_bream_smelt = train_scaled[bream_smelt_indexes]
target_bream_smelt = train_scaled[bream_smelt_indexes]

# | = or 과 같음

위 코딩 => 도미,빙어에는 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']

두 번째 샘플을 빼고 모두 도미로 예측했다.! 

KNeighborsClassifier()과 마찬가지로 예측확률은 preidct_proba() 메소드에서 제공한다.

lr.predict_proba(train_bream_smelt[:5])

array([[0.99759855, 0.00240145],
       [0.02735183, 0.97264817],
       [0.99486072, 0.00513928],
       [0.98584202, 0.01415798],
       [0.99767269, 0.00232731]])

샘플마다 두개의 확률이 출력되었다. !! 첫 번째 열 = 음성 클래스(0), 두 번째 열 = 양성 클래스(1)

무엇이 0, 1인지는 classes_ 를 통해 알아보자.

lr.classes_

array(['Bream', 'Smelt'], dtype=object)

빙어가 양성이다 ! 

 

이제 계수까지 확인해보자.

print( lr.coef_, lr.intercept_ )

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

이 계수를 통해 보면 방정식은 다음과 같다.

위 방정식에서 z값을 구해보고싶다!!

= decision_function()메소드로 출력할 수 있다.

lr.decision_function(train_bream_smelt[:5])

array([-6.02927744,  3.57123907, -5.26568906, -4.24321775, -6.0607117 ])

이 z값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다!!

파이썬의 사이파이 라이브러리를 통해 시그모이드함수(expit()) == np.exp()함수를 이용할 수 있다.

 

from scipy.special import expit
expit(decision)

[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]

predict_proba()메소드를 사용했을 때의 두 번째 열과 동일하다. 

즉, decision_function() 메소드는 양성 클래스(빙어)에 대한 z값을 반환함을 알 수 있다.

 

이제 다중 분류 문제로 넘어가보자.

 

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

 

lr = LogisticRegression(C=20, max_iter = 1000)
lr.fit(train_scaled, train_target)

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

0.9327731092436975
0.925

처음 5개 샘플에 대한 예측을 출력해보자.

lr.predict(test_scaled[:5])

array(['Perch', 'Smelt', 'Pike', 'Roach', 'Perch'], dtype=object)

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

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

array([[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]])

5개 샘플에 대한 예측이므로 5개의 행이 출력, 7개의 생선에 대한 확률을 계산했으므로 7개의 열이 출력되었다.

 

첫 번째 샘플을 보면  세 번째 열이 가장 높다. (Perch:농어)

lr.classes_

array(['Bream', 'Parkki', 'Perch', 'Pike', 'Roach', 'Smelt', 'Whitefish'],
      dtype=object)

3번째 열이 농어임을 확인!!!

 

 

이제 다중 분류일 경우의 선형방정식을 살펴보자.

소프트맥스함수

시그모이드함수는 하나의 선형방정식의 출력값을 0~1로 반환하는 반면,

소프트맥스함수는 여러개의 방정식의 출력값을 0~1로 압축하고 전체합이 1이 되게한다.

소프트맥스함수를 정규화된 지수함수라고 부르기도 한다.

 

 

먼저 decision_function()메소드를 통해 z1 ~ z7까지의 값을 구하고,

소프트맥스함수를 이용해 확률로 바꿔보자.

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]]

 

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]]

앞서 구한 decision()배열을 softmax함수에 전달했다.

axis= 의 매개변수는 소프트맥스를 계산할 축을 지정한다.

여기서는 각 행(샘플)에 대해 소프트맥스를 계산한다. !

+ Recent posts