촉촉한초코칩

[혼공머신] Ch04 본문

Study/PBL

[혼공머신] Ch04

햄친구베이컨 2024. 7. 18. 11:00

https://colab.research.google.com/drive/1sfIIY16vBHCjrX5VPn4HrCZR5UYLzgpy?authuser=0#scrollTo=SomJpD0y3yyq

 

Ch04. 다양한 분류 알고리즘

럭키백의 확률 계산하기
- 로지스틱 회귀, 확률적 경사 하강법과 같은 분류 알고리즘을 배운다.
- 이진 분류와 다중 분류의 차이를 이해하고 클래스별 확률을 예측한다. 

 

04-1 로지스틱 회귀

로지스틱 회귀 알고리즘을 배우고 이진 분류 문제에서 클래스 확률을 예측한다. 

 

문제 : 럭키백에 포함된 생선의 확률 알려주기 

럭키백의 확률 

  • 럭키백에 들어갈 수 있는 생선 : 7개 
  • 생선 특성(길이, 높이, 두께, 대각선 길이, 무게)이 주어졌을 때 7개 생선에 대한 확률 출력 
  • → k-최근접 이웃 분류기 사용 

 

데이터 준비하기 

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

#Species 열에서 고유한 값 추출
#Species 열을 타깃으로 만들고 나머지 5개 열은 입력 데이터로 사용 
print(pd.unique(fish['Species']))

#Species 열 빼고 나머지 5개 열 선택 
#데이터프레임에서 여러 열을 선택하면 새로운 데이터프레임이 반환된다. 
#to_numpy() : 넘파이 배열로 변환 
fish_input = fish[['Weight', 'Length', 'Diagonal','Height','Width']].to_numpy()
#print(fish_input[:5])
fish_target = fish['Species'].to_numpy()

#훈련/테스트 세트로 나누기 
#데이터 세트 2개 
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)

 

k-최근접 이웃 분류기의 확률 예측

#KNeighborsClassifier 클래스 객체 만들고 훈련 세트로 모델 훈련한 다음 
#훈련 세트와 테스트 세트의 점수 확인 
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

 

다중 분류 (Multi-class classification)

  • 타깃 데이터에 2개 이상의 클래스가 포함된 문제 (위 문제에서는 7개의 생선 종류가 들어가 있다.)
  • 이진 분류 : 양성 클래스와 음성 클래스를 각각 1과 0으로 지정하여 타깃 데이터 만듦 샘플마다 2개의 확률 출력
  • 다중 분류 : 타깃값을 숫자로 바꾸어 입력할 수 있지만 사이킷런에서는 문자열로 된 타깃값도 사용 가능 샘플마다 클래스 개수만큼 확률 출력 
    → 이때 알파벳 순서로 지정됨 
print(kn.classes_)

#테스트 세트에 있는 처음 5개 샘플 타깃값 예측 
#사이킷런의 분류 모델 : predict_proba() 메서드로 클래스별 확률값 반환 
print(kn.predict(test_scaled[:5]))

import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4)) #소수점 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.    ]]

4번째 샘플의 최근접 이웃 클래스 확인해보기 

distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes]) #[['Roach' 'Perch' 'Perch']]

다섯번째 클래스인 Roach가 1개이고 세번째 클래스인 Perch가 2개이다. 
Roach : 1/3 = 0.3333, Perch = 2/3 = 0.6667이므로 출력되 클래스 확률과 같다. 

문제점 : 3개의 최근접 이웃을 사용하기 때문에 가능한 확률 수가 너무 적음 (0/3, 1/3, 2/3, 3/3)

 

로지스틱 회귀 (Logistic Regression)

  • 분류 모델 
  • 선형 회귀와 동일하게 선형 방정식 학습 
z = a * (Weight) + b * (Length) + c * (Diagonal) + d * (Height) + e * (Width) + f 

문제점 : z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 1이 되도록 바꾸려고 함 → 시그모이드 함수 사용 

 

시그모이드 함수 (Sigmoid function, 로지스틱 함수)

  • 선형 방정식의 출력 z의 음수를 사용해 자연 사우 e를 거듭제곱하고 1을 더한 값의 역수를 취한다. 
  • z가 무한하게 큰 음수일 경우 함수는 0에 가까워지고, z가 무한하게 큰 양수가 될 때는 1에 가까워진다. 

 

#-5와 5 사이에 0.1 간격으로 배열 z를 만든 다음 z 위치마다 시그모이드 함수 계산 
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보다 크면 양성 클래스 / 0.5보다 작으면 음성 클래스로 판단 

불리언 인덱싱 (Boolean Indexting) : True, False 값을 전달하여 행 선택하기 
→ 훈련 세트에서 도미(Break)와 빙어(Smelt) 행만 골라낸다. 

#불리언 인덱싱 
char_arr = np.array(['A','B','C','D','E'])
print(char_arr[[True,False,True,False,False]]) #['A' 'C']

#bream_smelt_indexes : 도미와 빙어일 경우 True, 그 외는 False 값이 들어간다. 
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)

#5개 샘플 예측
print(lr.predict(train_bream_smelt[:5])) #['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
#5개 샘플 예측 확률 
print(lr.predict_proba(train_bream_smelt[:5]))
# 음성 클래스(0) 확률   양성 클래스(1) 확률 
#[[0.99759855 0.00240145]
#  [0.02735183 0.97264817]
#  [0.99486072 0.00513928]
#  [0.98584202 0.01415798]
#  [0.99767269 0.00232731]] -> 모두 도미로 예측 

print(lr.classes_) #['Bream' 'Smelt'] 빙어가 양성 클래스 

#로지스틱 회귀가 학습한 계수 확인
print(lr.coef_, lr.intercept_) #[[-0.4037798  -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]

 

로지스틱 회귀 모델이 학습한 방정식 

z = -0.404 * (Weight) - 0.576 * (Length) - 0.663 * (Diagonal) - 1.013 * (Height) - 0.732 * (Width) - 2.161

LogisticRegression의 decision_function() 사용해서 z 값 계산

#train_bream_smelt의 처음 5개 샘플의 z 값 출력
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions) #[-6.02927744  3.57123907 -5.26568906 -4.24321775 -6.0607117 ]

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

#np.exp() 함수 사용해서 decisions 배열 값을 확률로 변환하기 
from scipy.special import expit
print(expit(decisions)) #[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]

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

 

정리 

  • 이진 분류를 위해 2개의 생선 샘플을 골라 로지스틱 회귀 모델 훈련
  • 이진 분류일 경우 predict_proba() : 음성/양성 클래스에 대한 확률 출력
  • decision_function() : 양성 클래스에 댛나 z 값 계산 
  • coef_, intercept_ 속성 : 로지스틱 모델이 학습한 선형 방정식의 계수 

→ 7개의 생선을 분류하는 다중 분류 문제 풀기 

 

로지스틱 회귀로 다중 분류 수행하기 
LogisticRegression 클래스를 사용해 7개 생선을 분류해 보면서 이진 분류와 차이점을 알아본다. 

LogisticRegression 클래스

  • 반복적인 알고리즘 사용 → max_iter 매개변수로 반복횟수 지정
  • 릿지 회귀와 같이 계수의 제곱 규제 (L2 규제) → C 사용 (C는 alpha와 반대로 작을수록 규제가 커진다.) 
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

#테스트 세트의 5개 샘플 예측 출력
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))
#각 열마다 높은 확률로 예측한 게 무엇인지 알아본다. (lr.classes_ 사용해서 어느 생선(클래스)인지 확인) 
#이중 가장 높은 확률이 예측 클래스가 된다. 
# [[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]]
print(lr.classes_) #['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']

#선형방정식의 매개변수 크기 출력
#5개의 특성 사용하므로 coef_ 열은 5개 
#다중 분류는 클래스마다 z값을 하나씩 계산(클래스 개수만큼) -> 가장 높은 z 값을 출력하는 클래스가 예측 클래스가 됨 
print(lr.coef_.shape, lr.intercept_.shape) #(7, 5) (7,)

소프트맥스 함수를 사용하여 7개의 z 값을 확률로 변환

#decision_function() 사용해서 z1~z7까지의 값을 구한 다음 소프트맥스 함수 사용해서 확률로 변환 

#1. 테스트 세트의 처음 5개 샘플에 대한 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]]

#proba 배열과 비교해서 결과가 일치하면 성공 
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]]

 

정리 [로지스틱 회귀로 확률 예측]

  • 문제 : 럭키백에 담긴 생선이 어떤 생선인지 확률 예측하기 
  • 분류 모델 : 예측 + 예측 확률 출력 확률이 높을수록 강하게 예측
  • k-최근접 이웃 모델 : 모델 확률을 출력할 수 있지만 샘플의 클래스 비율이므로 항상 정해진 확률만 출력
  • 로지스틱 회귀 모델
    - 분류모델로, 선형 방정식 사용 값을 그대로 출력하는 것이 아니라 0~1사이로 압축 
    - 하나의 선형 방정식 훈련 → 방정식의 출력값을 시그모이드 함수에 통과시켜 0~1 사이의 값을 만든다. (양성클래스에 대한 확률, 음성 클래스의 확률은 1에서 양성 클래스 확률 빼면 된다.)
  • 다중 분류 : 클래스 개수만큼 방정식 훈련 그다음 각 방정식의 출력값을 소프트맥스 함수를 통과시켜 전체 클래스에 대한 합이 항상 1이 되도록 함  → 이 값이 각 클래스에 대한 확률이 도니다. 

 

04-2 확률적 경사 하강법

경사 하강법 알고리즘을 이해하고 대량의 데이터에서 분류 모델을 훈련하는 방법 배우기

 

점진적인 학습 

  • 문제 : 훈련 데이터가 조금씩 전달됨 → 앞서 훈련된 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련하기 
  • 대표적인 점진적 학습 알고리즘 : 확률적 경사 하강법 (Stochastic Gradient Descent) 

 

확률적 경사 하강법 

  • 가장 가파른 경사를 따라 원하는 지점에 조금씩 도달하는 것 
  • 가장 가파른 경사를 찾는 방법 : 훈련 세트에서 랜덤하게 하나의 샘플을 고른다. → 조금씩 내려간다.  (이 과정 반복) 
  • 모든 샘플을 다 사용했는데 아직 내려오지 못했다면 : 훈련 세트에 모든 샘플을 다시 채워넣고 랜덤하게 하나의 샘플을 선택해 경사를 내려간다. (만족할 만한 위치에 도달할 때까지) 
  • 에포크 (Epoch) : 훈련 세트를 한 번 모두 사용하는 과정 
  • 미니배치 경사 하강법 (Minibatch Gradient Descent) : 무작위로 여러 개의 샘플을 선택해 경사 하강법 수행 
  • 배치 경사 하강법 (Batch Gradient Descent) : 극단적으로 한 번 경사로를 따라 이동하기 위해 전체 샘플을 사용하는 것 (오히려 가장 안정적인 방법일 수 있으나 전체 데이터를 사용하면 그만큼 컴퓨터 자원을 많이 사용하게 된다.) 

 

손실 함수 (Loss function) 

  • 어떤 문제에서 알고리즘이 얼마나 손실됐는지 측정하는 기준 → 정답을 맞추지 못하는 것 
  • 손실 함수는 정확도로 쓸 수 없다. 

 

로지스틱 손실 함수 

  • 양성 클래스의 타깃과 곱한 다음 음수로 바꾸기 예측이 1에 가까울수록 좋은 모델
  • 예측 확률을 사용해 연속적인 손실 함수를 얻고 예측 확률에 로그 함수를 적용한다. 
  • 예측 확률의 범위는 0~1 사이로, 로그 함수는 이 사이에서 음수가 되므로 최종 손실 값은 양수가 된다.
  • 로그 함수는 0에 가까울수록 아주 큰 음수가 되기 때문에 손실을 아주 크게 만들어 모델에 큰 영향을 미칠 수 있다. 
도미 양성 클래스(1)
빙어 음성 클래스(0)
예측 정답 (타깃) 예측 확률 손실 함수 
1 1 0.9 0.9 * 1 = 0.9 = -0.9 낮은 손실 
0 1 0.3 0.3 * 1 = 0.3 = -0.3 높은 손실 
0 0 0.2 1 - 0.2 = 0.8 = -0.8 낮은 손실 
1 0 0.8 1 - 0.8 = 0.2 = -0.2 높은 손실  

  • 양성 클래스 (타깃 = 1)일 때 손실 : -log(예측 확률) → 확률이 1에서 멀어져 0에 가까워질수록 손실은 아주 큰 양수가 됨 
  • 음성 클래스 (타깃 = 0)일 때 손실 : -log(1 - 예측 확률) → 확률이 0에서 멀어져 1에 가까워질수록 손실은 아주 큰 양수가 됨 

 

* 다양한 손실 함수 
이진 분류 → 로지스틱 손실 함수 
다중 분류 → 크로스엔트로피 손실 함수 

 

SGDClassifier
사이킷런에서 확률적 경사 하강법을 제공하는 대표적인 분류용 클래스 

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()

#훈련/테스트 세트로 나누기
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.linear_model import SGDClassifier
#매개변수
#loss : 손실 함수의 종류 지정 
#max_iter : 수행할 에포크 횟수 지정 
sc = SGDClassifier(loss='log', 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 -> 반복 횟수가 적어서 낮게 나오는 듯 

#확률적 경사 하강법 점진적 학습 -> 객체를 다시 만들지 않고 훈련한 모델 sc를 추가로 더 훈련한다. 
#partial_fit() : 모델 이어서 훈련할 때 사용, 호출할 때마다 1 에포크씩 이어서 훈련한다. 
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target)) #0.8151260504201681
print(sc.score(test_scaled, test_target)) #0.85

 

에포크와 과대/과소적합 

  • 확률적 경사 하강법을 사용한 모델은 에포크 횟수에 따라 과소적합이나 과대적합이 될 수 있다. 
  • 이유 
    - 에포크 횟수가 적음: 모델 훈련 세트를 덜 학습함 → 훈련 세트와 테스트 세트에 잘 맞지 않는 과소적합된 모델일 가능성이 높다.  
    - 에포크 횟수가 많음 : 훈련 세트 완전히 학습  → 오히려 너무 잘 맞아 테스트 세트에는 점수가 나쁜 과대적합된 모델일 수 있음 

에포크가 진행됨에 따라 모델 정확도 나타냄

  • 훈련 세트 점수 : 에포크가 진행될수록 꾸준히 증가하지만 테스트 세트 점수는 감소 → 해당 지점이 과대적합되기 시작하는 곳 
  • 조기 종료 (Early Stopping) : 과대적합이 시작하기 전에 훈련을 멈추는 것 
#partial_fit() 메서드만 사용 : 훈련 세트에 있는 전체 클래스의 레이블을 partial_fit() 메서드에 전달 
#1. 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)

#에포크 한번 반복할 때마다 훈련/테스트 세트의 점수를 계산하여 리스트에 저장한다.
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)) 
   
#그래프로 그려보기 
import matplotlib.pyplot as plt
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

  • 백번째 에포크 이후부터 훈련/테스트 세트의 점수가 벌어지고 있음 
  • 에포크 초기에는 과소적합되어 점수가 낮다. 

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

#tol : 향상될 최솟값 None으로 설정하면 자동으로 멈추지 않고 max_iter=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)) #0.957983193277311
print(sc.score(test_scaled, test_target)) #0.925

SGDClassifier는 일정 에포크 동안 성능이 향상되지 않으면 더 훈련되지 않고 자동으로 멈춘다. 

힌지 손실 사용한 모델
* 힌지 손실 (Hinge 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)) #0.9495798319327731
print(sc.score(test_scaled, test_target)) #0.925

 

정리 [점진적 학습을 위한 확률적 경사 하강법]

  • 확률적 경사 하강법을 사용해 점진적으로 학습하는 로지스틱 회귀 모델 훈련
  • 확률적 경사 하강법
    - 손실 함수를 정의하고 경사를 따라 조금씩 내려온다.
    - 충분히 학습하여 훈련 세트에서 높은 점수를 얻는 모델을 만들 수 있다. 
    - 훈련을 반복할수록 모델이 훈련 세트에 더 잘 맞게 되어 어느 순간 괴대적합되고 테스트 세트의 정확도가 줄어든다. 

'Study > PBL' 카테고리의 다른 글

[혼공머신] Ch06  (1) 2024.07.24
[혼공머신] Ch05  (0) 2024.07.23
[혼공머신] Ch03  (0) 2024.07.17
[혼공머신] Ch01, 2  (1) 2024.07.12
딥러닝 모델 경량화  (0) 2024.07.11