촉촉한초코칩
[혼공머신] Ch04 본문
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 |