ML 세션 - Week4.다양한 분류 알고리즘

» AI

데이터 준비

import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
print(pd.unique(fish['Species'])) #species 열에서 교유한 값 추출
# ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
fish_input = fish[['Weight','Length','Diagonal','Height','Width']].to_numpy() #5개 열 입력데이터로 저장
fish_target = fish['Species'].to_numpy() #species 열을 타깃으로

# 훈련 세트와 테스트 세트로 나누기
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-최근접 이웃 분류기로 확률 예측

from sklearn.neighbors import KNeighborsClassifier

kn = KNeighborsClassifier(n_neighbors=3) # k=3
kn.fit(train_scaled, train_target)
  • 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중분류라고 함
    • 사이킷런에서는 0과 1이 아닌 문자열로 된 타깃값 그대로 사용 가능(순서 알파벳 순으로 매겨짐) -> 정렬된 타깃값 확인해야함
    •   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]) # predict_proba() 메서드로 클래스별 확률값 반환
        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.    ]]
        """
      
    • 첫번째 열이 bream 일 확률, 두번째 열이 parkki일 확률….
    • alt text
    • 실제 네 번째 샘플 최근접 이웃 확인해보기
    •   distances, indexes = kn.kneighbors(test_scaled[3:4])
        print(train_target[indexes]) # [['Roach' 'Perch' 'Perch']]
        # 세 번째 클래스인 perch가 2개, 다섯 번째 클래스인 roach가 1개 -> 세 번째 클래스에 대한 확률 0.6667, 다섯 번째 클래스에 대한 확률 0.3333
      

로지스틱 회귀

  • 이름은 회귀이지만 분류모델
  • 선형 회귀와 동일하게 선형 방정식 학습
  • 결과값이 확률이 되려면 0~1 또는 0~100% 사이 값이 돼야 함 -> 시그모이드 함수(Sigmoid functioon) 로지스틱 함수(Logistic function) 사용
    • alt text
    • 시그모이드 사용 시 결과값 z가 무한하게 큰 음수일 경우 0에 가까워지고 무한하게 큰 양수일 경우 1에 가까워짐
  • 도미와 빙어 이진 분류 수행해보기
    • 이진 분류의 경우 시그모이드 함수의 출력이 0.5보다 크면 양성 클래스, 작으면 음성 클래스로 판단
    •   bream_smelt_indexes = (train_target == 'Bream') | (train_target == 'Smelt') #도미 또는 빙어면 true
        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)
      
        print(lr.classes_) 
        # ['Bream','Smelt'] -> Bream(도미)이 음성 클래스, Smelt(빙어)가 양성 클래스
      
        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.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 * (widtb) -2.161
        decisions = lr.decision_function(train_bream_smelt[:5]) # decision_function() 메서드로 z값 계산
        print(decisions) # [-6.02927744  3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
      
        from scipy.special import expit #시그모이드 함수
      
        print(expit(decisions)) #시그모이드 함수에 z값 통과해서 확률값 변환 
        # [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
        # predict_proba() 메서드 출력의 두 번째 열과 동일 -> decision_function() 메서드는 양성 클래스에 대한 z값 반환
      
  • 다중 분류 수행
    • LogisticRegression 클래스는 기본적으로 반복적 알고리즘 사용
      • max_iter 매개변수로 반복 횟수 설정(기본값 100)
    • LogisticRegression 클래스는 기본적으로 릿지 회귀와 같이 계수의 제곱 규제
      • 이 규제를 L2 규제라고 부름
      • 규제를 제어하는 매개변수 C, C는 alpha와 반대로 작을수록 규제 커짐(기본값 1)
    •   lr = LogisticRegression(C=20, max_iter=1000) #C=20으로 설정해 규제 완화, 반복횟수 1000으로 늘리기
        lr.fit(train_scaled, train_target)
      
        print(lr.predict(test_scaled[:5])) # 예측값 출력
        # ['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
      
        print(lr.classes_)
        # ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
      
        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]]
        '''
        # 첫번째 행: 세번째 열일 확률 높음 -> perch
        # 두번째 행: 여섯번째 열일 확률 높음 -> smelt 
      
    •   print(lr.coef_.shape, lr.intercept_.shape) # (7, 5) (7,)
        # 5개의 특성 사용하므로 coef_ 배열의 열 5개, 근데 행과 intercept_가 7개 -> z 7개 계산한다는 뜻
      
    • 다중분류는 클래스마다 z값을 하나씩 계산 -> 가장 높은 z 값을 출력하는 클래스가 예측 클래스
    • 다중분류는 이진 분류와 달리 시그모이드 함수가 아닌 소프트맥스 함수 사용
      • 소프트맥스 함수는 여러 개의 선형방정식 출력값을 0~1 사이로 압축하고 전체 합이 1이 되도록 함
      • 정규화된 지수 함수라고도 부름
      • 7개의 z 값을 z1 ~ z7이라고 하면
      \[\text{e\_sum}= e^{z1} + e^{z2} + e^{z3} + e^{z4} + e^{z5} + e^{z6} + e^{z7}\] \[s1 = \frac{e^{z1}}{e\_sum}, s2 = \frac{e^{z2}}{e\_sum}, \dots, s7 = \frac{e^{z7}}{e\_sum}\]
      • 7개 생선에 대한 확률의 합은 1
    •   decision = lr.decision_function(test_scaled[:5]) # decision_function()메서드로 z1~z7의 값 구하기
        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) # 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]]
        ''' #앞서 한 proba와 결과 동일
      

점진적 학습

  • 앞서 훈련한 모델을 버리지 않고 새로운 데이터에 대해서만 조금씩 더 훈련하는 방식
  • 온라인 학습이라고도 함
  • 대표적 알고리즘 확률적 경사 하강법
    • 확률적 = 무작위 = 랜덤 -> 훈련세트에서 하나의 샘플 랜덤하게 고르기
    • 경사하강법 = 경사를 따라 내려가는 방법 -> 가장 빠른 길 선택 -> 가장 가파른 경사를 따라 원하는 지점에 도달 but 조금씩 내려와야 함
    • 훈련세트에서 하나의 샘플 랜덤하게 골라 가장 가파른 길 찾기, 그다음 훈련세트에서 랜덤하게 샘플 선택해 경사 내려오기 이걸 전체 샘플 다 쓸 때까지
    • 모든 샘플 다 사용해도 산을 다 못 내려왔다면 다시 처음부터 시작 에포크 = 훈련세트를 한 번 모두 사용하는 과정
    • 여러 개의 샘플 골라서 경사 하강하기 -> 미니배치 경사 하강법
    • 전체 샘플 사용해서 경사 하강하기 -> 배치 경사 하강법
    • alt text
    • 여기서 산이란 손실함수
      • 머신러닝 알고리즘이 얼마나 엉터리인지 측정하는 기준
      • 비용함수라고도 함
      • 작을수록 좋음
      • 연속적이어야 함

로지스틱 손실 함수

  • 이진분류는 로지스틱 손실 함수(이진 크로스엔트로피 손실함수) 사용
  • 양성클래스(타깃=1)일 때 손실은 -log(예측확률)로 계산
  • 확률이 1에서 멀어질수록 손실은 커짐
  • 음성클래스(타깃=0)일 때 손실은 -log(1-예측확률)로 계산
  • 예측확률이 0에서 멀어질수록 손실 커짐
  • 다중분류는 크로스엔트로피 손실함수 사용

  •   from sklearn.linear_model import SGDClassifier #확률적 경사 하강법 클래스 제공
      sc = SGDClassifier(loss='log_loss', max_iter=10, random_state=42) #SGDClassifier의 객체 만들 때 2개의 매개변수 지정
      #loss는 손실함수 종류 -> 로그로 지정
      # max_iter는 에포크 횟수 -> 10회 반복
    
      sc.fit(train_scaled, train_target)
      # ConvergenceWarning: Maximum number of iteration reached before convergence. Consider increasing max_iter to improve the fit. 경고가 뜸 max-iter 값 늘려주기
    
      sc.partial_fit(train_scaled, train_target)
      # 훈련한 모델 sc를 추가로 더 훈련해보기 -> partial_fit() 메서드 사용( fit()과 비슷하지만 1에포크씩 이어서 훈련 )
    
  • 에포크 횟수가 너무 적으면 과소적합 가능성 o
  • 반대로 많으면 과대적합 가능성 o
    • alt text
    • 과대적합 시작하기 전 훈련을 멈추는 것을 조기종료라고 함
  •   import numpy as np
      sc = SGDClassifier(loss='log_loss', random_state=42)
      # 훈련세트 테스트 세트 점수 기록용 리스트
      train_score = []
      test_score = []
      classes = np.unique(train_target)
    
      for _ in range(0, 300): # 에포크 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()
    
  • alt text
  • 에포크 초기는 과소적합 형태
  • 100번째 에포크 이후로 세트 간 점수차 벌어짐 -> 에포크 100으로 설정

  •   sc = SGDClassifier(loss='log_loss', max_iter=100, tol=None, random_state=42) 
      # SGDClassifier는 일정 에포크 동안 성능이 향상되지 않으면 자동으로 멈춤 -> tol=None으로 무조건 100번 반복하게 함
      sc.fit(train_scaled, train_target)
      print(sc.score(train_scaled, train_target)) # 0.957983193277311
      print(sc.score(test_scaled, test_target)) # 0.925