박스안에 있는 것이 7개의 물고기중에 어떤것일까? 라는 문제 즉 각 생선별로 확률이 있고 전체 확률이 100%가 되어야 하는 문제 일종의 다중분류 문제이다.
물고기의 5개의 특성으로 7개의 생선으로 분류하는 과정을 다시 암기로... ㅎㅎ
1. 데이터 준비 및 데이터 패턴 파악하기 ( 주로 그래프들을 그려가며 대략적인 추세 파악 )
2. 만약 스케일이 안맞는 데이터들이라면 스케일 맞추기
3. 훈련세트와 테스트세트 준비하기
4. 특정 알고리즘 (여기선 k-최근접 이웃 알고리즘 ) 으로 훈련하기
5. 과소적합, 과대적합 파악해서 파라미터 조정하기
6. 평가
7. 예측 및 사용하기
역시 시작은 만만한 k-최근접 이웃 모델로...
1. 데이터 준비 및 데이터 패턴 파악하기 ( 주로 그래프들을 그려가며 대략적인 추세 파악 )
fish_input, fish_target으로 입력데이터와 정답데이터를 준비했고
2. 만약 스케일이 안맞는 데이터들이라면 스케일 맞추기
여기선 3번의 훈련세트와 테스트세트로 나눈다음에 진행되는데 사이킷런의 StandardScaler 클래스를 이용한다.
3. 훈련세트와 테스트세트 준비하기
위의 2번작업을 한 화면에
4. 특정 알고리즘 (여기선 k-최근접 이웃 알고리즘 ) 으로 훈련하기
챕터 2에서 사용했던 KNeighborsClassifire 클래스로 분류해보기
2장에서는 0, 1로 타깃데이터를 만들었지만 사이킷런에서는 문자열로 된 타깃값을 그대로 사용할 수 있다고 함. 하지만 타깃값을 그대로 사이킷런에 전달하면 순서가 자동으로 알파벳순으로 매겨짐. 그래서 위에서 물고기 종류를 보려고 출력했던 pd.unique(fish['Species']) 하고 순서가 틀림.
KNeighborsClassifire 에서 정렬된 타깃값은 classes_속성에 저장되어 있다고 함
5. 과소적합, 과대적합 파악해서 파라미터 조정하기
이건 일단 이번 챕터에서는 패스
6. 평가
평가 점수는 위의 4번에서 이미 출력을 했는데 그리 높아보이지는 않지만 사실 이번 챕터에서는 k-최근접 이웃 알고리즘은 맛보기이고 다른 알고리즘을 쓸 것이다.
7. 예측 및 사용하기
아무튼 이렇게 훈련된 모델을 가지고 실제 사용을 하려고 했는데 ...
확율값을 보려면 조금 힘든데 위에서 4번째 행의 값을 보면
[0. 0. 0.6667 0. 0.3333 0. 0. ]
이렇게 7개의 배열로 구성되어 있는데
이 배열은 위에서 봤던 classes_의
['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish'] 와 같이 나열해서 보면 보기 쉽다.
Bream | Parkki | Perch | Pike | Roach | Smelt | Whitefish |
0. | 0. | 0.6667 | 0. | 0.3333 | 0. | 0. |
즉 test_scaled[3] 의 생선은 Perch가 66.67%로 젤 확률이 높다.
이 모델이 맞는지 이웃들도 확인해보면
이 샘플의 이웃은 다섯번째 클래스인 'Roach'가 1개 세번째 클래스인 'Perch' 가 2개 따라서 다섯 번째 클래스에 대한 확률은 1/3 = 0.3333 이고 세번째 클래스에 대한 확률은 2/3=0.6667라서 앞서 출력한 샘플의 클래스 확률과 같다.
즉 클래스 확률 예측은 성공이다.
하지만 3개의 최근접 이웃을 사용하기 때문에 나올수 있는 확률이 0/3, 1/3, 2/3, 3/3이 전부이다.
그래서 이번 챕터에서 로지스틱 회귀라는 좀 더 나은 분류알고리즘을 사용해본다.
로지스틱 회귀
이름은 회귀이지만 분류 모델이라고 함. 지금 계산중인 특성을 수식으로 표현하면 아래와 같은데
z = a * (Weight) + b * (Length) + c * (Diagonal) + d * (Height) + e * (Width) + f
여기에서 a,b,c,d,e는 가중치 혹은 계수이다. 특성은 늘어났지만 3장에서 다룬 회귀를 위한 선형방정식과 같다. 확률이 되려면 0~1 사이 값이 되어야 한다. z가 아주 큰 음수일때는 0이 되고, 아주 큰 양수일때 1이 되도록 하는것이 시그모이드 함수 또는 로지스틱 함수가 있다.
넘파이로 -5와 5사이에 0.1 간격으로 시그모이드 함수 그려보기는 다음과 같이 쉽게 처리할수 있다.
이 로지스틱 회귀 모델로 훈련하도록 하자. 그 전에 로지스틱 회귀로 이진 분류를 수행하려면 특별한 기법이 하나 들어가는데 넘파이 배열에서 블리언 인덱싱이라는 기법을 이용하는 것이다. 예를 들면 아래와 같다.
위 기능을 사용해 훈련세트에서 도미와 빙어 행만 골라내보자. 그리고 상위 10개만 출력
이렇게 비트연산자를 쓰면 도미와 빙어에 대한 행만 골라 낼수 있다.
자 이제 이 데이터를 로지스틱 회귀를 이용해 분류해보자. 훈련된 모델을 가지고 훈련세트에서 상위 5개를 예측해보자.
두번째만 빼고 모두 도미로 예측
이렇게 예측한 확률을 출력해보자
타깃값도 출력해보자
샘플마다 2개의 확률이 출력되었고 첫번째 열이 음성클래스에 대한 확률이고 두번째 열이 양성 클래스에 대한 확률이다. 위의 데이터와 비교해보면 음성이 도미, 양성이 빙어이다.
로지스틱 회귀로 성공적으로 이진분류를 수행했다. 학습한 계수를 확인해보자
각각 a, b, c, d, e, f를 의미한다.
z = a * (Weight) + b * (Length) + c * (Diagonal) + d * (Height) + e * (Width) + f
이 계수를 가지고 z값을 수동으로 계산할 수 도 있다. train_bream_smelt의 처음 5개의 z값을 출력해보자
이 값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있는데 파이썬의 사이파이 라이브러리에 시그모이드 함수가 있다. decisions 배열의 값을 확률로 변환해 보면
위에서 예측한 2번째 열의 확률값과 동일하다. 즉 decision_function 메서드는 양성 클래스에 대한 z값을 반환한다.
이진 분류 예제를 통해 2개의 생선 샘플로 로지스틱 회귀 훈련을 했고 원래 하려는 7개의 다중분류를 어떻게 적용하는지 살펴보자
사실 사용법은 동일하다. 먼저 코드부터 보면서 얘기하자. 훈련세트와 테스트 세트에 대한 점수가 높고 과대적합이나 과소적합으로 치우진거 같진 않다.
다른 점은 하이퍼 파라메터에 있는데 반복회수 max_iter값을 1000정도 주었다. 그리고 LogisticRegression은 릿지 회귀와 같이 계수의 제곱을 규제한다. 이런 규제를 L2규제라고 부르는데 매개변수 C이다. 작을수록 규제가 커지는데 여기서는 규제를 조금 완화하기 위해 20으로 세팅한다.
이제 테스트 세트 처음 10개의 샘플에 대한 예측을 해보자. 실제 값도 출력해보자
뭐 비교적 높은 정확도로 예측한 걸 볼수 있다. (1개 틀렸다.)
그럼 테스트세트의 처음 5개의 예측 확률을 출력해보자
5개 샘플에 대한 예측이므로 5개의 행이 출력되었고 7개의 생선에 대한 확률을 계산했기 때문에 7개의 열이 출력되었다. 위에서 2진분류일때 2개의 열만 출력되었다.
첫번째 샘플을 보면 세번째 열의 확률이 가장 높다. 0.841 즉 84.1% 위의 예측 타깃값을 보면 Perch로 나오는데 클래스 정보를 확인해보면
세번째가 Perch 이기때문에 맞았다.
다중 분류일때는 선형 방정식은 어떤 모습일까?
기울기 계수 집합이 7행 5열로 나온다. 절편이 7개 즉 물고기 7마리에 대해 각각 5개의 계수값과 5개의 절편값이 계산된다는 뜻이다.
그럼 확률은 어떻게 계산한 것일까? 이진 분류에서는 시그모이드 함수를 이용해서 z를 0과 1사이로 변환했는데
다중분류에서는 소프트맥스 함수를 사용해서 7개의 z값을 확률로 변환한다. 계산식은 생략...
이진 분류처럼 decision_function() 메서드 (양성메세드에 대한 z값을 반환)로 z1~z7 까지의 값을 구한 다음 소프트맥스 함수를 사용해 확률로 바꾸어 보자 먼저 테스트 세트의 처음 5개 샘플에 대한 z1 ~ z7의 값을 구하면
위 값을 사이파이의 소프트맥스 함수를 이용해서 계산하면
계산 결과는 앞에서 구한 proba배열과 동일함을 알 수 있다.
즉 첫번째 물고기는
['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish'] 순서에 따르면 3번째의 0.841 다시말하면 Perch가 될 확율이 84%가 된다는 뜻이다.
이로써 로지스틱 회귀를 사용해 7개의 생선에 대한 확률을 예측하는 모델을 훈련했다.
확인문제
2. 로지스틱 회귀가 이진분류에서 확률을 출력하기 위해 사용하는 함수는 무엇인가요?
- 시그모이드
- 소프트맥스
- 로그 함수
- 지수 함수
풀이 : 답은 1
1번 시그모이드는 방정식의 결과를 0과 1사이로 압축하려 확률로 해석할 수 있음.
2번 소프트맥스 함수는 다중 분류에서 확률을 출력하기 위해 사용함.
3, 4번은 일반 수학 함수임.
최종코드
# 로지스틱 회귀
## 럭키백의 확률
### 데이터 준비하기
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()
print(pd.unique(fish['Species']))
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
print(fish_input[:5])
fish_target = fish['Species'].to_numpy()
print(fish_target)
### 훈련세트와 테스트세트 준비
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)
kn.fit(train_scaled, train_target)
print(kn.score(train_scaled, train_target))
print(kn.score(test_scaled, test_target))
print(kn.classes_)
### 7종의 생선 확률 예측
print(kn.predict(test_scaled[:5]))
import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4)) # 소수 4번째 자리까지 표기, 다섯째자리에서 반올림
### 이 모델이 맞는지 이웃들을 확인하기
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
## 로지스틱 회귀
### 넘파이로 시그모이드 함수 그래프 그려보기
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()
### 블리언 인덱싱 테스트
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, 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]
print(target_bream_smelt[:10])
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
print(lr.predict(train_bream_smelt[:5]))
print(lr.predict_proba(train_bream_smelt[:5]))
print(lr.classes_)
print(lr.coef_, lr.intercept_)
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
from scipy.special import expit
print(expit(decisions))
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))
print(lr.predict(test_scaled[:10]))
print(test_target[:10])
proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))
print(lr.classes_)
print(lr.coef_.shape, lr.intercept_.shape)
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))
from scipy.special import softmax
proba = softmax(decision, axis=1)
print(np.round(proba, decimals=3))