본문 바로가기

MLOps 개발자 양성과정/ml&dl

[Day-52] 로지스틱 회귀 / 확률적 경사 하강법

Chap.04 다양한 분류 알고리즘 

럭키백의 확률을 계산하라!

1. 분류 => 농어다 아니다 (k-최근접이웃 분류)

2. 회귀 => 예측 (농어의 길이/두께를 통해 무게 예측)

3. 확률 => 럭키백에 포함된 생선의 확률 알려줘


4-1. 로지스틱 회귀

1. k- 최근접 이웃 분류기로 확률 계산

# 데이터 준비하기
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head() # 위에서부터 5개 데이터 출력
# 판다스 CSV 파일의 첫 줄을 자동으로 인식해 열 제목으로 만들어줘
# 앞쪽 숫자 행 번호

# unique 중복값을 제외한 고유한 값을 꺼내줘
# 생선의 종류
print(pd.unique(fish['Species'])) >>> ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']

# Species 열 타깃으로 나머지 5개 열 입력 데이터로 사용
# 입력 데이터 넘파이 배열로 저장
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)
print(train_input.shape) >>> (119, 5) # 75%는 train 세트로
print(test_input.shape) >>> (40, 5) # 25%는 test 세트로 나눠줌

# 전처리 표준화 작업
from sklearn.preprocessing import StandardScaler
ss = StandardScaler() # 객체 생성
ss.fit(train_input) # 훈련 데이터로fit
train_scaled = ss.transform(train_input) #transform
test_scaled = ss.transform(test_input) # transform

# k-최근접 이웃 분류기의 확률 예측
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

# 정렬된 타깃값 classes_ 속성에 저장 (알파벳 순서로 정렬)
print(kn.classes_) >>> ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']

# 예측값 확인
# 5줄에 대한 예측값
print(kn.predict(test_scaled[:5])) >>> ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
# 클래스별 확률값
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.    ]]
 
 # 최근접이웃의 클래스 확인해보기
distances, indexes = kn.kneighbors(test_scaled[3:4]) # 네 번째 줄 하나만 최근접 이웃 찾아봐
print(train_target[indexes])
# 100으로 봤을 때 Roach 하나 => 0.3333 / Perch는 두 개 => 0.6667

 

 

❓ k-최근접 이웃의 한계?

- 최근접 이웃을 사용하기 때문에 

- 그러나 최근접이웃 훈련데이터에서 떨어진 값은 다 똑갑은 값으로 인식

 


 

2. 로지스틱 회귀

- 이름은 회귀지만 사실 분류 모델

- 선형 회귀와 동일하게 선형 방정식을 학습

(z = a x (Weight) + b x (Length) + c x (Diagonal) + d x (Height) + ex (Width) + f )

- z 값은 어떤 값이라도 가능

=> 하지만, 확률이 되려면 0~1 사이의 값이 되어야 함 => 시그모이드 or 소프트 맥스 함수 사용

 

분류
이진분류 다중분류
: 둘 중에 하나 맞추기
음성 클래스와 양성클래스 각각 1과 0으로 만들 수 있어
: 타깃 클래스가 2 개 이상인 분류 문제
정답지에 두 개 이상의 답이 있을 경우
 시그모이드 함수 소프트맥스 함수

 

 

💡 1) 시그모이드 함수 

: 선형 방정식의 출력을 0과 1 사이의 값으로 압축하며 이진 분류를 위해 사용

- 아무리 큰 값이 나와도 1과 가깝게 아무리 작은 값 나와도 0

- 사이파이 라이브러리에 시그모이드 함수 from scipy.special import expit

 
# 로지스틱 회귀로 이진 분류 수행하기 => 시그모이드
# 도미와 빙어만 분류
# for문 if쓰는 거보다 비트 연산 속도  훨씬 빨라 대규모데이터에서는 비트 연산 사용
bream_smelt_indexes = (train_target == 'Bream') | (train_target == 'Smelt') # 도미와 빙어일 경우 True 나머지 False
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']
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]]
                                                     
# 첫 번째가 농어인지 빙어인지 확인해보기
print(lr.classes_) >>> ['Bream' 'Smelt'] #sklearn 타깃값 알파벳 순으로 정렬해서 사용함

# 로지스틱 회귀가 학습한 계수 확인
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 x (Diagonal) - 1.013 × (Height) - 0.732 x (Width) - 2.161

# 방정식의 결과 z 값 계산
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions) >>> [-6.02927744  3.57123907 -5.26568906 -4.24321775 -6.0607117 ]

# 시그모이드 함수를 이용해 확률 계산
from scipy.special import expit
print(expit(decisions)) >>> [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
# predict_proba() 출력의 두 번째 열과 동일

# predict_proba() 메서드 음성 클래스와 양성 클래스에 대한 확률을 출력
# decision_function() 양성 클래스에 대한 z값을 계산
# expit 함수 사용해 z값을 확률로

💡 2) 소프트맥스 

: 다중 분류에서 여러 선형 방정식의 출력 결과를 정규화하여 합이 1이 되도록 만듦

- 여러 개의 선형 방식의 출력값을 0~1 사이로 압축하고 전체 합이 1이 되도록

- 기본적으로 반복적인 알고리즘을 사용 (한 번에 학습하지 않고 여러번 반복해서 학습함)

- 사이파이 라이브러리에 시그모이드 함수 from scipy.special import  softmax

 

1. 반복 횟수를 설정 ( max_iter 매개변수 )
- 기본값100으로 실행
- 경고가 뜨면 1000으로 늘림

2. 규제 조절( C 매개변수)
- 로지스틱회귀 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제 : L2 규제
- C는 (기본값1) 규제 강화시키려면 숫자 줄여
(↔ 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']
# 예측 확률 출력
proba = lr.predict_proba(test_scaled[:5])
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]]

# 클래스 정보 확인
print(lr.classes_) >>> ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']

# 다중 분류의 선형 방정식 모습 확인
print(lr.coef_.shape, lr.intercept_.shape) >>> (7, 5) (7,)
# 다중 분류 행 여러 개(클래스 개수만큼)

# 5개 샘플에 대한 z1~z7의 값
decision = lr.decision_function(test_scaled[:5]) # z값을 반환 시켜주는 함수
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]]
# predict_proba() 배열에서 구한 값과 일치
 

 

🔎 predict() & predict_proba()

ㆍpredict()

: 타깃값으로 예측을 출력

- 결과 문자열이 나와(이 생선일 거다)

- 클래스(명칭)을 내보내

 

ㆍpredict_proba()

: 클래스별 확률값을 반환

- 확률값을 찍어줘(7개 생선 중에 이거일 확률이 몇 퍼센트다)

- 자동으로 시그모이드 / 소프트 맥스 구분해 확률 구함

 

ㆍdecision_function()

: 모델이 학습한 선형 방정식의 결과를 출력(z값)

- 이진 분류의 경우 양성 클래스의 확률이 반환 (0보다 크면 양성, 작거나 같으면 음성 클래스로 예측)

- 다중 분류 각 클래스마다 선형 방정식을 계산(가장 큰 값의 클래스가 예측 클래스가 됨)

 


4-2. 확률적 경사 하강법

💡 확률적 경사 하강법 

: 훈련 세트에서 샘플 하나씩 꺼내 손실 함수의 경사를 따라 최적의 모델을 찾는 알고리즘

=> 대용량 데이터가 있을 때 효율적

=> 신규 데이터가 불특정 시간에 들어올 때

- 점진적 학습(온라인 학습) 알고리즘

- sklearn.linear_model 패키지의 SGDClassifier

 

* 경사하강법의 의미 ❓

- 경사(기울기)를 따라 내려가는 방법

- 가장 빠른 길 경사가 가장 가파른 길

- 0을 맞추기 위해서 계속 아래로 점점 내려가

- 조금씩 내려가야지 미분계수 값 그대로 쓰면 너무 내려가 오히려 올라갈 수가 있어

- 신경망(CNN) 알고리즘에서 경사 하강법 필수로 사용(미니배치 주로 사용)

 

* 확률적의 의미 ❓

- 훈련 세트를 사용해 가장 가파른 길을 찾을 때

- 전체 샘플을 사용하지 않고 딱 하나의 샘플을 훈련 세트에서 랜덤하게 고르는 것

 

* 확률적 경사 하강법 세 가지 방법

ㆍ임의 샘플 하나만 => 확률적 경사 하강법

ㆍ임의의 샘플 여러개 => 미니배치

ㆍ통째로 다 사용 => 배치

 

🔎 에포크(ephoch)

: 확률적 경사 하강법에서 전체 샘플을 모두 사용하는 한 번 반복 (훈련 세트를 한 번 모두 사용하는 과정)

- 훈련 세트에서 랜덤하게 하나의 샘플을 선택하여 경사를 내려가는 과정을 전체 샘플을 모두 사용할 때까지 계속 반복

- 전체 샘플 모두 사용했는데 0까지 내려오지 않았다면 다시 훈련 세트에 모든 샘플을 채워넣어 반복

- 처음 학습하면 손실 들쭉날쭉 일반적으로 수십에서 수백 번의 에포크를 반복

 

* 미분?
- 잘게 쪼개
- 미분계수 기울기값
- 미분계수의 부호는 x가 어느 방향으로 움직여야 하는지를 
( 미분계수의 부호 - 는  x가 + 방향으로 / +는 x가 - 방향으로)

* 미분계수 값 그대로 쓰면 너무 내려갈 수 있어

alpha값 런닝메이트

 

 

 

 

정답과 내가 학습한 데이터

오차를 점점 줄이기 위해서

 

 

 🔎 손실함수(loss function)

: 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준

(손실 = 정답을 못 맞추는 것)

- 확률적 경사 하강법이 최적화할 대상

- 값이 작을 수록 좋음

- 하지만 0이랑 똑같다던지 어떤 값이 최솟값인지는 모름

- 가능한 많이 찾아보고 만족할만한 수준이라면 종료

- 경사하강법 이용하기 위해서는 정확도 연속적인 형태여야 함.

=> 손실함수 미분 가능해야 해

- 손실함수도 머신러닝 라이브러리가 알아서 계산해줘

- 이진이 들어오면 자동으로 로지스틱 손실 함수, 그 이상이 들어오면 크로스엔트로피 선택해

이진 분류 => 로지스틱 손실 함수 (or 이진 크로스엔트로피 손실 함수)

다중 분류 => 크로스엔트로피 손실 함수

(- 회귀문제에는 평균 제곱 오차 손실 함수 사용)


SGDClassifier

* 런타임 GPU 로 변경하기

# 판다스 데이터프레임 만들기
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
type(fish) >>> pandas.core.frame.DataFrame

# 넘파이 배열로 변경하기 위해 정답 따로 뽑아놓기 
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 #'log' 로지스틱 손실 함수로 지정
# 수행할 에포크 횟수 지정 max_iter # 10번 반복
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
# 정확도 너무 낮아 훈련이 부족해 # 과소적합

# partial_fit 으로 다시 학습
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
# 현재까지 20번 학습했어 10번 학습했을 때보다 정확도 올라감

####################조기종료 시점 구하기######################
# 모델 새롭게 만들기 만들기
# partial_fit()메서드만 사용하려면 훈련 세트에 있는 전체 클래스의 레이블을 partial_fit()에 전달해줘야 해
# 에포크가 진행됨에 따라 모델의 정확도 저장하기 위한 빈리스트
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))
  
print(len(train_score)) >>> 300
print(len(test_score)) >>> 300

####################시각화######################
import matplotlib.pyplot as plt
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

# 적절한 에포크 값 찾아내기 => 100
############################################################

# 반복횟수 100에 맞추고 다시 모델 훈련
sc = SGDClassifier(loss='log', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)

# 일정 에포크 동안 성능이 향상되지 않으면 모델이 스스로 판단해서 멈춰
# 자동으로 멈추지 않고 무조건 내가 지정한 횟수만큼 반복 => tol=None으로 지정해야 해

print(sc.score(train_scaled, train_target)) >>> 0.957983193277311
print(sc.score(test_scaled, test_target)) >>> 0.925

 

※ 모델을 만들 때 한 번에 완성되는 모델은 없음.

= > 최성능의 모델을 훈련 반복 횟수, 손실을 바꿔가며 직접 찾아가야 함. 

fit()
: 항상 새 객체처럼 매개변수를 초기화하고 fit()메소드에 전달된 데이터 세트로 모델을 훈련함
- 하나의 인스턴스()에서 수행하려고 시도

partial_fit()
: 초기화 매개변수 위에서 작동하고 전달된 새 데이터 세트로 기존 가중치를 개선하려고 시도
- 데이터의 일부에 적합하도록 함
- 데이터의 일부를 사용하기 때문에 전체 데이터의 클래스를 따로 전달해줘야 함. 
 sc.partial_fit(train_scaled, train_target, classes=classes)​

 

모델은 내부에 패턴을 기억하고 있어

모델 객체 내부에 훈련한 내용, 가중치, 절편 기억하고 있어

(하이퍼파라미터는 기억 못해)


시각화를 통해 조기종료 시점 찾기

- 에포크 값 반복 어느 정도 할지는 내가 정해

- 일단 계속 평가자료를 만들어 시각화 하기

- 조기종료(과대적합이 시작하기 전에 훈련을 멈추는 것)의 시점을 찾아야 함

 

- 앞부분 초기상태 과소적합

- 어느정도 안정화 되는 위치 100

- 100보다 더 하면 간격 다시 멀어져

 

# 일정 에포크 동안 성능이 향상되지 않으면 모델이 스스로 판단해서 멈춰

 
# 무조건 내가 지정한 횟수만큼 반복시키기 위해서
tol=None으로 지정해야 함
sc = SGDClassifier(loss='log', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)

 


📍 hinge loss

SGDClassifier의 loss 매개변수

기본값 hinge 힌지 손실( =서포트 벡터 머신)

 

# 힌지 손실 ( 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