# 글 작성에 앞서..
본 글은 이번 2023 여름 방학 가운데 공부했던 머신러닝에 대한 일종의 회고(?)글로, 기존 네이버 블로그에 있던 글을 다시 들여다보고자 가져왔다. (복습이라고 하지만 그대로 냅뒀다간 들여다보지도 않을까 염려가 되기도 해서.. ㅎㅎ;;) 개념에 대한 개인적인 정리와 함께 교재 내 핵심 개념들을 임의로 정한 것이라 다소 난잡할 수 있다.. 참고하시길~~
# 럭키백의 확률
- 럭키백에 들어갈 수 있는 생선은 총 7마리. 럭키백에 들어간 생선의 크기, 무게 등이 주어졌을 때, 7개 생선에 대한 확률을 출력하는 문제가 되겠다. 이번에는 길이, 높이, 두께 외에 대각선 길이와 무게도 사용 가능하다.
# K - 최근접 이웃 분류기의 확률 예측
1) 데이터 불러오기 / 전처리
fish = pd.read_csv("https://bit.ly/fish_csv")
print(pd.unique(fish['Species']))
# > ['Bream' 'Roach' 'Whitefish' 'Parkki' 'Perch' 'Pike' 'Smelt']
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
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()
fish_target[:5]
# > array([['Bream'],
# ['Bream'],
# ['Bream'],
# ['Bream'],
# ['Bream']], dtype=object)
2) 앞선 주차에서 다루었던 변환기를 통해 훈련 세트와 테스트 세트를 각각 transform 한다!
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.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors = 3)
kn.fit(train_scaled, train_target)
print(kn.score(train_scaled, train_target))
print(kn.score(test_scaled, test_target))
# > 0.8907563025210085
# > 0.85
> 타깃 데이터를 생성할 때, fish['Species']를 사용해 만들었다. 그렇다면, 훈련 세트와 테스트 세트 모두 7개의 생선 종류가 들어있으며, 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 '다중 분류'라고 한다.
print(kn.classes_) # fish_target에 사용된 클래스 명들의 입력된 순서
# > ['Bream', 'Parkki', 'Perch', 'Pike', 'Roach', 'Smelt', 'Whitefish']
print(kn.predict(test_scaled[:5]))
# > ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
print(kn.predict(test_scaled[:5])) # test_scaled의 4번째 인덱스까지 예측함.
# > ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
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. ]]
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
# > [['Roach', 'Perch', 'Perch']]
> 이 샘플의 이웃은 다섯 번째 클래스인 Roach가 1개이고, Perch가 두개다. 따라서 세 번째 클래스에 대한 확률은 2/3, 다섯 번째 클래스에 대한 확률은 1/3이다. (책에 오류가 있는 듯)..하지만, 3개의 최근접 이웃을 사용하면서 예측할 수 있는 확률은 0, 1/3, 2/3, 3/3 이 전부다. 조금 더 확률의 분포를 늘릴 수 있는 방법은 없을까?
# 로지스틱 회귀
> 이름은 회귀이지만, 실제로는 분류 모델이다. 선형 회귀와 동일하게 선형 방정식을 학습하는데, 다음과 같은 식이 있다.
z = a * (Weight) + b * (Length) + c * (Diagonal) + d * (Height) + e * (Width) + f
여기서 a, b, c, d, e, f 는 가중치 혹은 계수이다. 특성은 늘어났지만, 다중 회귀를 위한 선형 방정식과 같다. z는 어떤 값도 가능하지만, 확률이 되기 위해선
0~1 사이 값이 되어야 한다. z가 무한정 커질 수는 없기 때문에, 아주 큰 음수일 때는 0으로 수렴하고, 아주 큰 양수일 때는 1로 수렴하는 방법이 바로 시그모이드 함수(로지스틱 함수)이다.

( = 로지스틱 함수)
> z가 어떤 값이 되더라도 세타는 절대로 0~1사이의 범위를 벗어나지 못하기에, 0 ~ 1사이의 값을 0 ~ 100%의 확률로 해석할 수 있다.
# 그래프 시각화
z = np.arange(-5, 5, 0.1)
phi = 1 / (1 + np.exp(-z))
plt.plot(z, phi)
plt.show()


부연 설명. 출처) https://brunch.co.kr/@parkkyunga/85
# 이진 분류
> 이진 분류의 경우, 시그모이드 함수의 출력이 0.5 보다 크면 양성 클래스, 0.5보다 작으면 음성 클래스로 판단한다. 도미와 빙어를 우선 분류해보자.
# + 불리언 인덱싱(boolean indexing)
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_target[bream_smelt_indexes]
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']
print(lr.predict_proba(train_bream_smelt[:5])) # 각 생선('Bream', 'Smelt')에 해당될 확률
# > [[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
z가 나오기 위한 각 특성들의 가중치이다. 이처럼 로지스틱 회귀는 선형회귀와 굉장히 유사함을 알 수 있다. 그럼 이제 로지스틱 회귀 모델로 z값을 계산해보자.
decisions = lr.decision_function(train_bream_smelt[:5])
# > [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
> 이제 이 z값들을 시그모이드 함수에 통과시키면 우리는 최종적으로 그 확률을 알 수 있다.
from scipy.special import expit # 파이썬의 사이파이(scipy) 라이브러리에 들어있는 시그모이드 함수
print(expit(decisions))
# >[0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
> predict_proba() 메소드 출력의 두 번째 열과 동일한 값들이 출력됐다. 이는 즉, decision_function 메소드는 양성 클래스에 대한 z값을 반환함을 알 수 있다.
# 다중 분류
> 이제 로지스틱 회귀로 다중 분류를 수행해보자.
# max_iter: 반복횟수 릿지와 라쏘에서는 하이퍼 파라미터를 alpha라고 했지만, 로지스틱 회귀 모델에
# 서는 규제의 매개변수를 C라고 한다. 그리고 규제의 강도 또한 alpha와는 반대로 작을 수록 규제의 정도가 커진다.
lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target))
# > 0.9327731092436975
# > 0.925
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]]
> 5행 7열의 다중 분류된 예측 확률이다. 각각의 행마다 가장 높은 확률이 바로 classes_속성에서 확인할 수 있는 물고기의 종류이다.
> 따라서 첫 번째 행은 [2]번째 물고기인 'Perch'에 84%의 확률로 해당한다고 볼 수 있다.
# 선형 방정식
print(lr.coef_.shape, lr.intercept_.shape) # 가중치(계수)와 절편의 shape이 특이하다.
# > (7, 5) (7,)
> 5개의 특성을 이용했기 때문에, coef_ 배열의 열 또한 5개이다. 하지만, 행은 왜 7개일까? intercept, 절편도 7개다. 이는 다중 분류의 특성에서 비롯된 것인데, z값을 클래스마다 하나씩 계산한다. 그 중에서 가장 높은 z값을 출력하는 클래스가 예측 클래스가 되는 구조다. 그렇다면 확률은 어떻게 계산하는가?
다중분류는 소프트맥스 함수를 사용하여 7개의 z값을 확률로 변환한다.
여기서 잠깐!
# 소프트맥스 함수
> 시그모이드 함수는 하나의 선형방정식에서 도출된 z값을 0~1 사이의 확률로 압축한다. 반면 소프트맥스 함수는 여러 개의 선형방정식의 출력값을 0~1 사이로 압축하고, 전체의 합이 1이 되도록 만든다. 이를 위해 지수 함수를 사용하는데, '정규화된 지수 함수'라고도 부른다.

e_sum은 e의 z^i들의 합이다.

s1, s2, s3... s7을 다 더하면 1이 된다.
따라서, 7개의 생선에 대한 확률의 합은 1이 된다.
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) # 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]]
# 다중 분류 keyword
> 로지스틱 회귀 : 선형 방정식을 사용한 분류 알고리즘이다. '시그모이드 함수'(이진 분류일 때,)나, '소프트맥스 함수'(다중 분류일 때,)를 사용해 클래스 확률을 계산할 수 있다.
> 다중 분류: 타깃의 클래스가 2개 이상인 분류 문제다. 로지스틱 회귀는 다중 분류를 위해 소프트맥스 함수를 사용해 클래스를 예측한다.
> 시그모이드 함수 / 소프트맥스 함수 : 각각 로지스틱 회귀의 이진 분류와 다중 분류를 위해 사용하는 함수이다. 실제 z값이 도출되는 과정이 이 함수들을 통해 이루어지며, 0과 1 사이의 확률로 값을 제공하기 때문에, 예측에 용이한 함수이다.
# 확률적 경사 하강법
- 손실함수
- 에포크
기존의 훈련 데이터를 통해 훈련을 마친 모델에 다른 데이터를 넣을 수 있는 방법은 없을까? 기존의 데이터에 새로운 데이터를 계속해서 더한다면? 서버 용량에는 한계가 있다. 때문에 아무리 천천히 넣는다고 하더라도 언젠가 그 용량은 다할 것이다. 그렇다면 기존의 데이터를 조금씩 버리면서 새로운 데이터를 채운다면? 크기나 용량에 대한 기준은 충족한다 할지라도, 기존의 데이터 가운데 중요한 데이터가 있으면 어떻게 할 것인가. 위험부담이 큰 선택이다.
> 그래서 나온 것이 '점진적 학습'이다. 대표적인 점진적 학습 알고리즘이 바로 '확률적 경사 하강법'인데, 보다 기술적인 용도와 사용에 앞서 그 정의에 대해 알아보자.
이 모델은 예시) 경사진 산을 '가장' 효율적이고 안전하게 내려오기 위해 산의 '가장' 경사진 구간을 무작위적으로 찾는다. 그 경사진 구간을 찾기 위해 훈련세트를 랜덤하게 선택하고, 산을 내려간다. 반복해서 랜덤하게 훈련 세트를 고르고, 그 세트를 이용해 산을 내려간다. 만약 내려가는 도중 훈련 세트를 모두 사용하면? 다시 처음부터 시작한다. 훈련 세트에 모든 샘플을 다시 채워넣고, 다시 랜덤하게 고르기를 이어간다.
> 확률적 경사 하강법에서 훈련 세트를 한 번 사용(돌리는)하는 과정을 '에포크'라고 한다. 일반적으로 경사 하강법은 많은 과정에 거쳐 에포크를 수행한다.
> 경사 하강법에도 종류가 있다. 샘플을 꺼내고 사용하는 방식에 따라 1) 미니배치 경사 하강법 2) 배치 경사 하강법 3) 확률적 경사 하강법으로 부르는데, 다음은 각각의 경사 하강법 모델이 적용되는 예시를 그림으로 표현한 것이다.

> 그렇다면, 경사진 곳을 가장 빨리 내려가게 만드는 '산'은 무엇일까? 그건 바로 '손실 함수'이다. 분류에서 '손실'의 정의는 간단하다. 정답을 못 맞히는 샘플이다. 예측한 값과 정답이 동일하면 확률이 늘어나고, 동일하지 않으면 늘어나지 않는 간단한 구조다. 책에선 이진 분류의 예를 들었다.
# 손실 함수
> 알고리즘의 정확도를 측정하는 함수라고도 할 수 있다. 낮을수록 알고리즘이 정확하다고 판단한다. 분류의 경우, 손실의 의미는 정답을 못 맞히는 것이다. 그렇다면, 손실이 낮을수록 정확한 알고리즘이라고 볼 수 있겠다. 하지만, 여기서 중요한 건 분류의 정확도는 손실함수로 사용할 수 없다는 점이다. 밑의 예시를 보면,
도미 (1)
빙어 (0)
예측 ---> 정답(타깃)
1 == 1
0 != 1
0 == 0
1 != 0
예측과 정답이 동일한 경우: 4가지 중 2가지 --> 2/4 = 1/2(0.5)
정확도는 0.5다.
- 0(다 틀림)
- 0.25 (1개 맞음)
- 0.5 (2개 맞음)
- 0.75 (3개 맞음)
- 1 (4개 맞음)
이를 그래프로 나타냈을 때, 그래프는 연속적이지 않기 때문에(불연속 -> 미분 불가능), 경사 하강법을 사용할 수 없다.
# 로지스틱 손실 함수(이진 크로스 엔트로피 손실 함수)
> 샘플 4개의 예측 확률을 각각 0.9, 0.3, 0.2, 0.8로 가정하고 손실 함수를 만들 수 있다고 가정해보자. 예측 확률이 1에 가까울수록 좋은 모델이다. 우리는 손실 함수의 값을 도출하기 위해 예측확률과 각각의 클래스 타깃(1, 0)을 곱할 것이다.
ex)
예측 --> 정답(타깃)
0.9 x 1 --> - (0.9) 낮은 손실
0.3 x 1 --> - (0.3) 높은 손실
0.2 -> 0.8 x 0 -> 1 --> - (0.8) 낮은 손실
0.8 -> 0.2 x 0 -> 1 --> - (0.2) 높은 손실
> 여기서 로그 함수를 적용하면, 보다 가시적이다.

> 양성 클래스(타깃 = 1)일 때, 손실은 -log (예측 확률)로 계산한다. 때문에, 확률이 1에서 멀어질 수록, 손실은 아주 큰 양수가 된다. 음성 클래스(타깃 = 0)일 때, 손실은 -log (1- 예측 확률)로 계산한다. 이 예측 확률이 0에서 멀어질수록 손실은 아주 큰 양수가 된다.
# 확률적 경사 하강법 Keyword
- 경사하강법: 훈련 세트에서 샘플을 꺼내 손실 함수의 경사를 따라 최적의 모델을 찾는 방법.
- 손실함수: 확률적 경사 하강법이 최적화해야 할 대상. 이진 분류 -> 로지스틱 회귀(이진 크로스 엔트로피) / 다중 분류 -> 크로스 엔트로피 손실 함수
- 에포크: 확률적 경사 하강법에서 전체 샘플을 모두 사용하는 한 번의 반복 단위 수(?). 일반적으로 경사 하강법을 사용하는 알고리즘에서는 에포크를 수백 번 이상 반복한다.
내용이 갈 수록 난해하다... 손실함수 파트부터는 정말 손을 놔버리고 싶었다 ㅠㅠㅠ 하지만 이 정도면 약과다.. 신경망 파트에서 놓치지 않고 따라가려면 부지런히 공부해야 된다. 나도 로지스틱 회귀 모델처럼,, 수 백번의 에포크를 거쳐 산을 내려가는 샘플들처럼 죽ㅇ라 반복해야겠다. 반복이 생명이다.. 포기하지 말자!!
이미지 출처)
"https://www.flaticon.com/kr/free-icons/" 아이콘 제작자: Nuricon - Flaticon
'머신러닝(ML), 딥러닝(DL)' 카테고리의 다른 글
인공신경망(ANN)과 활성화 함수 [딥러닝] (0) | 2024.03.13 |
---|---|
퍼셉트론(Perceptron) [딥러닝] (0) | 2024.03.12 |
비지도 학습 [머신러닝] (1) | 2024.01.13 |
트리 알고리즘 [머신러닝] (1) | 2023.12.26 |
회귀 알고리즘과 모델 규제 [머신러닝] (0) | 2023.12.21 |