● Chapter 4 학습목표
1. Logistic regression, 확률적 경사 하강법과 같은 분류 알고리즘을 배웁니다.
2. 이진 분류와 다중 분류의 차이를 이해하고 클래스별 확룰을 예측합니다.
● Chapter 04-1 학습목표 : Logistic regression algorithm을 배우고 이진 분류 문제에서 클래스 확률을 예측합니다.
● 지난 Chapter에서 배운 다중 회귀 문제 풀이 scheme
1. Data 불러오기
2. Train & test set 나누기
3. Preprocessing - Feature engineering
3-1. Featue engineering
3-2. Degree 증가 시키기
4. Preprocessing - Scaling * 규제하기전, Scaling은 필수
5. 규제를 적용한 Regression model 학습
6. 최적의 alpha 값 찾기
7. Model 평가
8. 계수 찾아보기 (.coef_ 함수)
● 로지스틱 회귀 문제 풀이 scheme
1. Data 불러오기
2. Input / Target data 분리하기
3. Preprocessing - Scaling
4. Train / test set 분리하기
5. Logistic regression model 학습 시키기
6. 원하는 값의 확률 예측하기 (LinearRegression.predict_prob)
[문제 상황]
1. 백화점에서 7가지 생선이 들어 있는 럭키백 아이템을 팔것입니다. 나중에 열어보면, 그때 어떤 생선이 들어있는 줄 알수 있게요.
2. 이때, 고객 만족팀의 반대로 생선이 어떤 확률로 들어 있는지 알려주는식으로 럭키백을 판매하려고 전략을 바꿨습니다.
3. 생선의 길이, 높이, 두께, 대각선의 길이 정보를 바탕으로 어떤 생선인지 예측할 수 있는지, 7개 생선에 대한 확률까지 확인 할 수 있는 알고리즘을 확인해보겠습니다.
"KNN는 주변 이웃을 찾아주니까, 이웃의 클래스 비율을 확률이라고 출력하면 되지 않을까?"
KNN을 활용한 분류 문제 확인 및 한계점 확인
KNeighbors classifier로 임의의 데이터가 주어졌을 때,
주변 데이터를 가지고 확률을 계산하고 가장 높은 확률을 따르는 클래스로 분류 된다는 아이디어인것이다.
#1. Data 불러오기
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')
fish.head()
Input data를 불러올 때는 scikit-learn이 2차원 array를 input으로 받기 때문에, to_numpy()로 가져온다.
한가지 sample에 5가지 feature가 들어있는 형태이기 때문에, 괄호안에 또 괄호를 쳐줘야 한다.
#2. 생선 종류 데이터들의 나머지 데이터들을 numpy array input으로 만들기
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
fish_input[:5]
np.unique() : 칼럼에서 고유한 값을 추출한다.
#3. np.unique() 함수를 틍홰 어떤 종류의 fish가 있는지 보기
import numpy as np
np.unique(fish['Species']) # 분류시, target data를 만들때는 1차원으로 만들어야 한다, fish_input은 한 샘플당 feature가 여러개 있기 때문에, 괄호를 두번 치는것임.
# 주의 해야 할 점 - 괄호의 개수 (input -2개, target - 1개)
주의해야 할 점은 target data를 불러줘야 할 때이다. 똑같이 괄호를 두번치는 것이 아닌, 한번만 쳐줘야 한다. 각각의 샘플마다 Species라는 data 한개씩만 가지고 있기 때문이다.
Data load시 주의사항: Data load가 쉽다고 대충하면 안된다. 내가 classification data를 다루는지, feautre가 여러개 있는 input을 다루고 있는지 잘 확인해야 한다.
#4. 생선 종류(Species)를 target numpy array로 만들기
fish_target = fish['Species'].to_numpy()
fish_target[:5]
이제, Train/Test set로 분리한다. 이제는 좀 익숙하다. 저자분도 이러한 routine에 대해 익숙해졌으면 좋겠다고 저술하셨다.
#5. Train/Test set 나누기
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)
#6. 훈련 시키기 전, 스케일링 하기
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
# 나의 생각 - Sampling bias
전체 target 데이터 안에서 모든 데이터가 골고루 있지 않기 때문에, Train set와 test set를 나누는 과정에서 bias가 있진 않았을지 확인하였다. Stratify = fish_target 함수를 사용해 맞춰주어도 좋을것 같다.
target_list = np.unique(train_target)
for target in target_list:
print('Total',target, np.sum(fish_target == target)/fish_target.shape)
for target in target_list:
print('Train',target, np.sum(train_target == target)/train_target.shape)
for target in target_list:
print('Test',target, np.sum(test_target == target)/test_target.shape)
Total Bream [0.22012579 7. ]
Total Parkki [0.06918239 2.2 ]
Total Perch [ 0.35220126 11.2 ]
Total Pike [0.10691824 3.4 ]
Total Roach [0.12578616 4. ]
Total Smelt [0.08805031 2.8 ]
Total Whitefish [0.03773585 1.2 ]
Train Bream [0.20168067]
Train Parkki [0.07563025]
Train Perch [0.3697479]
Train Pike [0.1092437]
Train Roach [0.14285714]
Train Smelt [0.07563025]
Train Whitefish [0.02521008]
Test Bream [0.275]
Test Parkki [0.05]
Test Perch [0.3]
Test Pike [0.1]
Test Roach [0.075]
Test Smelt [0.125]
Test Whitefish [0.075]
Stratify = fish_target으로 훈련 했을 때도 조금 더 유사해진것 같지만, 사이즈가 작은 데이터라 그런지 완벽히 맞지는 않는다. 그래도 조금 개선 되었다.
# stratify = fish_target으로 훈련 했을 때.
Total Bream [0.22012579]
Total Parkki [0.06918239]
Total Perch [0.35220126]
Total Pike [0.10691824]
Total Roach [0.12578616]
Total Smelt [0.08805031]
Total Whitefish [0.03773585]
Train Bream [0.21848739]
Train Parkki [0.06722689]
Train Perch [0.35294118]
Train Pike [0.1092437]
Train Roach [0.12605042]
Train Smelt [0.08403361]
Train Whitefish [0.04201681]
Test Bream [0.225]
Test Parkki [0.075]
Test Perch [0.35]
Test Pike [0.1]
Test Roach [0.125]
Test Smelt [0.1]
Test Whitefish [0.025]
Chat GPT에게 조금 더 간결한 코드를 만들어 달라고 했다.
print(f''Total {target} : {total_ratio}:.2%') 처럼 더 보기 좋게 코드를 짜주었다.
# sampling bias 일어나지 않았는지 확인
target_list = np.unique(train_target)
for target in target_list:
total_ratio = np.sum(fish_target == target) / fish_target.shape[0]
train_ratio = np.sum(train_target == target) / train_target.shape[0]
test_ratio = np.sum(test_target == target) / test_target.shape[0]
print(f'Total {target}: {total_ratio:.2%}')
print(f'Train {target}: {train_ratio:.2%}')
print(f'Test {target}: {test_ratio:.2%}')
print() # 빈 줄로 구분
Total Bream: 22.01%
Train Bream: 21.85%
Test Bream: 22.50%
Total Parkki: 6.92%
Train Parkki: 6.72%
Test Parkki: 7.50%
Total Perch: 35.22%
Train Perch: 35.29%
Test Perch: 35.00%
Total Pike: 10.69%
Train Pike: 10.92%
Test Pike: 10.00%
Total Roach: 12.58%
Train Roach: 12.61%
Test Roach: 12.50%
Total Smelt: 8.81%
Train Smelt: 8.40%
Test Smelt: 10.00%
Total Whitefish: 3.77%
Train Whitefish: 4.20%
Test Whitefish: 2.50%
# 주의할 점 - Scaling 하기
훈련 시키기전에, scaling을 해줘야 한다. 어떤 문제든, 훈련 시키기 전에 꼭 scaling을 해줘야한다고 생각하고 분석을 진행하면 될것이다.
#7. k =3으로 KNN model에 훈련 시키기
from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier() # Default n_neighbors = 5
kn.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_ : Scikit-learn이 만든 classification model이 어떤 순서로 분류의 class를 갖고 있는지? (속성)
#8. Scikit learn을 통해 만든 model에서는 알파벳순으로 target 값의 분류 순서가 메겨져서 비교 해봄.
## .classes_ 속성으로 확인
print(np.unique(fish['Species']))
print(kn.classes_)
# ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
# ['Bream' 'Parkki' 'Perch' 'Pike' 'Roach' 'Smelt' 'Whitefish']
이제 test set의 앞 5개의 값이 어떤 분류 예측 값을 반환하는지 확인한다.
- predict_proba() : 클래스별 확률 값을 반환 (method)
#9. Test set로 예측값 확인
print(kn.predict(test_scaled[:5]))
# ['Perch' 'Smelt' 'Pike' 'Perch' 'Perch']
각각의 class에 대해서 어떤 확률을 갖기 때문에 해당 class로 분류 된것인지 확인하기 위해, 각 확률을 확인해 본다.
- np.round() : 소수점 첫째자리에서 반환
- decimals : 몇번째 자리까지 남길건지
#10. 처음 5개의 클래스별 확률값 반환하기.
## predict_proba()
## np.round() : 소수점 첫째 자리에서 반올림.
## decimals 매개변수 : 유지할 소수점 아래 자리수를 지정.
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals = 4))
print(kn.classes_) # 각 column이 무엇인지 다시 확인
# 결과 예시 4번째 행 : 주변 3개의 point에서 2개가 Perch, 1개가 Roach이다.
# array([[0. , 0. , 1. , 0. , 0. , 0. , 0. ],
# [0. , 0. , 0. , 0. , 0. , 1. , 0. ],
# [0. , 0. , 0. , 1. , 0. , 0. , 0. ],
# [0. , 0. , 0.67, 0. , 0.33, 0. , 0. ],
# [0. , 0. , 0.67, 0. , 0.33, 0. , 0. ]])
하나의 예시로 4번째 행의 최근접 이웃들을 확인해서 어떤 샘플들이 가까이 있는지 확인한다.
#11. 4번째 샘플의 최근접 이웃의 클래스를 확인
distances, indexes = kn.kneighbors(test_scaled[3:4]) # 슬라이싱 할 때, 마지막 원소는 포함되지 않습니다. # 2차원 array를 input으로 사용 하는 경우가 많기 때문에 이러한 방식으 많이 사용.
print(train_target[indexes]) # kn model에 train set로 훈련이 됐으므로,
# [['Roach' 'Perch' 'Perch']]
# 주의사항 : scikit-learn은 2차원 배열을 받기 때문에 하나의 샘플만 활용하더라도 슬라이싱을 활용
슬라이싱 할 때, 마지막 원소는 포함되지 않고 하나의 샘플만 선택하더라도 항상 2차원 배열이 만들어집니다. # 2차원 array를 input으로 사용 하는 경우가 많기 때문에 이러한 방식으로 많이 사용합니다.
가까운 3점중에 Perch가 2개 있어서 2/3 = 0.67, Roach가 1개 있어서 1/3 = 0.33을 반환한것이여서 문제는 없다.
하지만, 이 확률은 k값에 제한을 받게 되어 제한된 확률만을 표시할 수 밖에 없다.
따라서, 조금 더 합리적이고 정확한 방법으로 분류 문제와 확률을 계산해보고자 한다.
로지스틱 회귀 (Logistic regression)
선형 방정식을 학습하는 분류 알고리즘입니다. 이후, 학습된 선형 회귀 방정식은 sigmoid 함수나 softmax 함수로 압축 되어 확률로 표기 됩니다.
이번 예제에서는 아래와 같은 선형 함수를 따르게 되며,
z = a x (Weight) + b x (Length) + c x (Diagonal) + d x (Height) + e x (Width) + f
이러한 선형 함수인 z 값이 큰 음의 값을 가질 때, 0으로 수렴하고 큰 양의 값을 가질 때, 1로 수렴하는 함수를 가지면 분류 하기 좋은 함수가 될 것입니다. 이는 sigmoid 혹은 Logistic 함수를 사용하면 가능합니다.
시그모이드 함수 (sigmoid function)
선형 방정식의 출력을 0과 1 사이의 값으로 압축하며, 이진 분류를 위해 사용합니다.
점근선과 함수식은 chat gpt의 도움을 받았습니다.
- 지수 함수의 계산은 np.exp()을 사용합니다.
## sigmoid 함수 그려보기
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, c = 'k')
# 점근선 추가
plt.axhline(y=0.5, color='gray', linestyle='--') # y = 0.5 점근선
plt.axhline(y=0, color='gray', linestyle='--') # y = 0 점근선
plt.axhline(y=1, color='gray', linestyle='--') # y = 1 점근선
plt.axvline(x=0, color='gray', linestyle='--') # x = 0 점근선
plt.xlabel('z')
plt.ylabel('phi')
# 함수식
plt.text(-4.5, 0.8, r'$\frac{1}{1+e^{-x}}$', fontsize=15, bbox=dict(facecolor='white', edgecolor='black'))
plt.show()
# 참고사항 : 정확히 0.5일때, scikit-learn library는 음성 클래스로 판단하며, 이는 library마다 다릅니다.
이진분류 (Binary classification)
도미와 빙어 데이터만 골라내서 이진 분류로 먼저 logistic regression을 통한 분류 문제를 확인합니다.
- 불리언 인덱싱 (Boolean indexing) : 넘파이 배열에서 True, False의 값을 전달하여 행을 선택 할 수 있습니다.
- 아래에서 A와 C를 골라내기 위해서 A와 C index에서만 True를 할당해줍니다.
## Boolean indexing
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr)
char_arr[[True, False, True, False, False]] # array(['A', 'C'], dtype='<U1')
Boolean indexing 방법을 통해서 도미 (Bream)와 빙어 (Smelt)만 추출해냅니다. 또한, 비트 OR 연산자인 ("|") 를 사용해줍니다.
## 다중 분류 수행 전, 두개의 target만 가지고 학습 하고 수행 시켜 보기
###1. 빙어와 도미 데이터만 가져오기
bream_smelt_index = (train_target == 'Bream') | (train_target == 'Smelt')
train_bream_smelt = train_scaled[bream_smelt_index]
target_bream_smelt = train_target[bream_smelt_index]
분리된 데이터를 학습시키고 처음 5개의 값에 대한 예측값과 실제 값을 비교해봅니다.
또한, 처음 5개의 값을 학습 했을 때, 샘플 예측 확률을 출력합니다.
첫번째 열이 음성에 대한 예측값, 두번째 열이 양성에 대한 예측값입니다.
- LogisticRegression : 선형 분류 알고리즘인 로지스틱 회귀를 위한 클래스입니다.
- solver : 사용할 알고리즘을 선택할 수 있으며, 기본값은 lbfgs, scikit-learn에 추가된 sag는 확률적 평균 경사 하강법 알고리즘으로 특성과 샘플 수가 많을 때 성능이 빠르고 좋습니다. (매개변수)
- penalty : L2에 규제 (Ridge 방식)와 L1 규제 (Lasso 방식)를 선택할 수 있습니다. 기본 값은 L2를 의미하는 l2입니다. (매개변수)
- C : 규제의 강도를 제어합니다. 기본값은 1.0이며, 작을수록 규제의 강도가 강합니다. (매개변수)
- predict_proba : 음성 클래스와 양성 클래스에 대한 확률을 출력, 다중 분류에서는 모든 클래스에 대한 확률을 반환 (method)
- decision_funtcion : 모델이 학습한 선형 방정식의 출력을 반환 이진 분류의 경우 양성 class의 확률이 반환, 다중 분류의 경우 각 클래스마다 선형 방정식을 계산합니다. 가장 큰 값의 클래스가 예측 클래스가 됩니다.
###2. Logistic regression model 도미, 빙어 데이터로만 학습 시키기
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)
print(lr.classes_) # 어떤것이 음성, 양성 클래스인지 확인 : 알파벳 순, ['Bream' 'Smelt']
print(lr.predict(train_bream_smelt[:5])) # 처음 다섯개에 대한 예측 값, ['Bream' 'Smelt' 'Bream' 'Bream' 'Bream']
print(target_bream_smelt[:5]) # 실제 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]]
다음은 어떤 선형 방정식을 따르는지 model parameter들을 확인합니다.
###3. Logistic regression의 model parameter 확인
print(lr.coef_, lr.intercept_)
# [[-0.4037798 -0.57620209 -0.66280298 -1.01290277 -0.73168947]] [-2.16155132]
아래와 같은 선형의 방정식을 model이 따른다고 할 수 있습니다.
-0.4037798 x (Weight) -0.57620209 x (Length) -0.66280298 x (Diagonal) -1.01290277 x (Height) -0.73168947 x (Width) -2.16155132
그러면 여기서 z의 기울기가 분류의 확률과 어떤 관계를 갖는지 알고 싶어 고찰을 해보았다.
기울기가 양수이면 해당 feature의 값이 클수록 확률은 1이 되므로, 해당 feature의 값은 양성 클래스의 확률과 비례한다고 할 수 있다.
기울기가 음수이면 해당 feature의 값이 클수록 확률은 0이 되므로, 해당 feature의 값은 양성 클래스의 확률과 반비례한다고 할 수 있다.
즉, z선형 방정식의 기울기는 해당 feature가 class 분류에 미치는 영향을 의미한다고 할수 있겠다.
- decsion_function() : 양성 클래스의 선형 방정식의 z값을 구하는 함수.
이제 첫 5개의 샘플에 대해서 선형 방정식의 값, z 값을 구해 볼것입니다. 그 이후, scipy library에서 expit 함수를 이용하여, sigmoid 함수에 z 값을 넣어 확률을 확인합니다.
###4. z값 구하기
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions) # [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
###5. Sigmoid 함수 값을 구하기
from scipy.special import expit
print(expit(decisions))
# [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]
값을 보면, 위에서 양성에 대한 예측값과 동일한것을 알 수 있습니다.
즉, lr.predict_proba는 학습된 데이터에서 선형의 방정식 z 값을 계산한 이후, logistic regression까지의 값까지 도출 하는 함수라고 알 수 있습니다.
다중분류 (Multiple class classification)
두개 이상의 클래스가 포함된 분류 문제입니다. 로지스틱 회귀는 다중 분류를 위해 소프트맥스 함수를 사용하여 클래스를 예측합니다.
LogisticRegression 클래스를 사용해 7개의 생선을 분류해 보면서 이진 븐류와 차이점을 알아보겠습니다.
LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용합니다.
max_iter 매개변수에서 반복 횟수를 지정하며 기본값은 100 -> 1000으로 늘려 충분하게 훈련 시키겠습니다.
또한, LogisticRegression 클래스는기본적으로 Ridge 회귀와 같이 계수의 곲을 규제합니다. 이를 L2 규제로 합니다.
Ridge는 alpha 값을 조절하여 규제를 조절하지만, 여기서는 C입니다. 작을 수록 규제가 커집니다. 기본 값은 1이며, -> 20으로 늘려 규제를 완화하겠습니다.
먼저, 학습을 시킨 뒤에
#12. 다중 분류 model 만들어서 학습
## max_iter = 1000 (학습 반복 횟수)
## C = 20 (규제, 여기서는 수가 증가하면, 규제가 적어짐)
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(max_iter = 1000, C=20)
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target))
처음 5개의 값을 예측하고, 그 값이 어떻게 나왔는지 확률을 확인해보겠습니다.
이진 분류는 샘플마다 2개의 확률을 출력한다면, 다중 분류는 샘플마다 클래스의 개수만큼 확률을 출력합니다.
즉, 다중 회귀의 predict_proba의 결과값에서 행은 각 샘플들을, 열은 모든 class에 대한 z 값을 반환합니다.
#13. test set의 처음 5개의 값을 예측해보기
print(lr.predict(test_scaled[:5]))
['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']
#14. 예측 확률 반환하기, 소수점 4째자리에서 반올림
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']
그렇다면, coef_와 intercpet_도 확인해 보겠습니다.
행은 z1 ~ z7 (target 개수 만큼), 열은 각 특성에 대한 계수 혹은 절편 (특성의 개수 만큼)
#15. z값과 coefficeint와 intercept 구하기
## 각 샘플당 7개의 z 값을 반환한다.
print(lr.coef_.shape, lr.intercept_.shape) # (7, 5) (7,)
decision = lr.decision_function(test_scaled[:5])
np.round(decision, decimals = 2)
헷갈려서 글로 적어봤다.
소프트 맥스 함수 (softmax)
다중 분류에서 여러 선형 방정식의 출력 결과를 정규화하여 합이 1이 되도록 만듭니다.
시그모이드 함수는 하나의 선형 방정식의 출력 값을 0~1 사이로 압축 합니다. 이와 달리 소프트맥스 함수는 여러개의 선형 방정식의 출력 값을 0 ~ 1사이로 압축하고 전체 합이 1이 되도록 만듭니다. 이를 위해 지수 함수를 사용하기 때문에 정규화된 지수 함수라고도 부릅니다.
이는 신경망을 배울 때 또 다시 등장하기 때문에 자세히 익혀 두어야 합니다.
s1 ~ sn 까지 모두 더하면 1이 됩니다.
위에서 decision_function을 통해서 구한 z 값을 scipy library의 softmax 함수를 통해 확률을 반환합니다.
axis = 1로 지정하여 각 행 (샘플)에 대해 소프트맥스를 계산합니다. axis를 지정하지 않으면, 전게 배열에 대해 소프트맥스를 계산합니다.
#16.Softmax 함수로 확률 구하기
## lr.predict는 sigmoid, softmax의 계산법을 통해 바로 분류 문제의 값을 반환한다.
## lr.decision을 통해 z 값을 반환하고, 다시 이 책에서 softmax를 통해 확률 값을 계산하는 과정을 일일히 시켜본것이다.
from scipy.special import softmax
proba = softmax(decision, axis = 1)
np.round(proba, decimals = 3)
array([[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의 값과 정확히 일치합니다.
다중 분류 문제에서 LogisticRegression.predict_proba는 모든 샘플에 대해 각 class의 확률을 계산해준다고 알 수 있습니다.