ML | Logistic Regression과 Stochastic Gradient Descent



Logistic Regression

Logistic regression은 선형 방정식을 이용한 분류 알고리즘입니다. Linear regression 처럼 변수의 상과관계를 표현하는 방정식을 만들지만, 각각의 클래스에 속할 확률을 예측하는 것이 목적입니다.

그래서 선형 방정식을 학습한다는 것은 이전에 보여드린 Linear Regression과 같지만, 결과는 0 ~ 1사이의 확률로 표현하여 합니다.

예를 들어, 아래와 같은 생선의 종류, 무게, 길이, 대각선, 높이, 두께 데이터가 있고, 생선의 종류의 확률을 예측하는 모델을 만든다고 해보겠습니다.



Species Weight Length Diagonal Height Width
Bream 242 25.4 30 11.52 4.02
Bream 290 26.3 31.2 12.48 4.3056
Smelt 19.7 14.3 15.2 2.8728 2.0672
Smelt 19.9 15 16.2 2.9322 1.8792



선형 방정식은 다음 처럼 학습할 것입니다.

z = a * Weight + b * Length + c * Diagonal + d * Height + e * Width + f

a, b, c, d, e는 가중치를 나타내고, f는 절편입니다.

  • z는 주어지는 생선 데이터에 따라 무엇이든 나올 수 있습니다.
  • 하지만 확률을 예측해야 하므로, z에 따라 확률로 나타내야합니다.
  • 이를 가능하게 해주는 것이 Sigmoid Function입니다. (Logistic Fuction이라고도 함)




Sigmoid Function


  • z값이 커질 수록 1을, 작을수록 0을 나타냅니다.
  • z에 따라 표시하면 아래과 같습니다.



scikit-learnd에서도 LogisticRegression으로 제공하고 있습니다. Binary logistic regression과 multinomial logistic regression을 할 수 있습니다.



Binary Classification

Logistic regression을 이용해 2가지 클래스에 대한 분류를 진행해보겠습니다.

1. 데이터 전처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 데이터 불러오기
fish = pd.read_csv("https://bit.ly/fish_csv_data")

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Train, Test 나누기. 0번째 column이 타겟입니다.
train_input, test_input, train_target, test_target = \
train_test_split(fish.iloc[:, 1:], fish.iloc[:, 0], random_state=42)

# Standardization
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)



2. Bream과 Smelt의 이중분류 해보기

1
2
3
4
# 데이터에서 Bream과 Smelt 데이터 분리 
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]



3. LogisticRegression 사용해보기

1
2
3
4
5
6
7
8
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt) # 학습

# train 데이터의 앞 5개 만 예측해보기
print(lr.predict(train_bream_smelt[:5])) # 예측
# ['Bream', 'Smelt', 'Bream', 'Bream', 'Bream']



학습

linear regression과 마찬가지로 주어진 데이터의 변수들에 대한 선형 방정식을 학습합니다.

lr.coef_와 lr.intercept_ 에 다음처럼 가중치 (혹은 계수)와 절편이 있습니다.

1
2
3
4
5
print(lr.coef_)
# [[-0.4037798 , -0.57620209, -0.66280298, -1.01290277, -0.73168947]]

print(lr.intercept_)
# [-2.16155132]



lr.coef_의 결과는 입력된 train데이터에 따라 Weight, Length, Diagonal, Height , Width의 가중치입니다.

따라서 방정식은 다음과 같습니다.

z = (-0.4037798) * Weight + (-0.57620209) * Length + (-0.66280298) * Diagonal + (-1.01290277) * Height + (-0.73168947) * Width + (-2.16155132)



예측

train 데이터의 5개를 predict를 하였을 때, 'Bream', 'Smelt', 'Bream', 'Bream', 'Bream'으로 나타났습니다.

예측한 확률을 predict_proba을 이용해 확인하면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
lr.predict_proba(train_bream_smelt[:5])
# array([[0.99759855, 0.00240145],
# [0.02735183, 0.97264817],
# [0.99486072, 0.00513928],
# [0.98584202, 0.01415798],
# [0.99767269, 0.00232731]])

lr.classes_
# array(['Bream', 'Smelt'], dtype=object)

  • predict_proba의 결과로 5개의 데이터셋에 대한 확률을 2개씩 내었는데, 주어진 class의 순서에 따라 확률을 표시합니다.
  • class 의 순서는 lr.classes_로 보면 첫 번째는 Bream, 2번 째는 Smelt입니다.
  • 안에서 일어나는 일은, 위에 학습된 선형 방정식에 따른 z값을 얻고, sigmoid 함수를 통해 0~1 사이의 값으로 변환이 가능합니다.



z 값 계산

1
2
3
decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)
# [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]



Sigmoid를 이용한 확률 계산

1
2
3
from scipy.special import expit
print(expit(decisions))
# [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]



실습을 위해 선형방정식을 이용해 직접 계산해도 같은 값이 나옵니다.

1
2
3
4
5
6
7
8
9
10
11
12
def make_decision(dataset):
z_list = []
for data in dataset:
z = float(sum((lr.coef_ * data)[0]) + lr.intercept_)
z_list.append(z)
return np.array(z_list)

decisions = make_decision(train_bream_smelt[:5])
print(decisions)
# [-6.02927744 3.57123907 -5.26568906 -4.24321775 -6.0607117 ]
print(expit(decisions))
# [0.00240145 0.97264817 0.00513928 0.01415798 0.00232731]



다중 분류

scikit learn의 LogisticRegression은 기본적으로 규제와 최대 반복수가 정해져 있습니다. 파라미터 기본값은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class sklearn.linear_model.LogisticRegression(
penalty='l2',
*,
dual=False,
tol=0.0001,
C=1.0,
fit_intercept=True,
intercept_scaling=1,
class_weight=None,
random_state=None,
solver='lbfgs',
max_iter=100,
multi_class='auto',
verbose=0,
warm_start=False,
n_jobs=None,
l1_ratio=None
)

여기서 CInverse of regularization strength 으로 릿지 회귀에서는 alpha가 커질 수록 규제가 강해졌지만, 여기서는 반대로 (inverse) 값이 작을 수록 규제가 커집니다.

max_iterMaximum number of iterations taken for the solvers to converge 으로, 훈련을 하는 반복수입니다. 반복이 부족하다면 학습이 제대로 되지 않을 수 있고, 경고가 발생합니다.

자세한 설명은 공식문서에 있습니다.



실습

이번엔 여러개 클래스에 대해 예측을 해보겠습니다.

생선의 종류는 ‘Bream’ ‘Pike’ ‘Smelt’ ‘Perch’ ‘Parkki’ ‘Roach’ 'Whitefish’으로 7가지입니다.


Species Weight Length Diagonal Height Width
Bream 242 25.4 30 11.52 4.02
Bream 290 26.3 31.2 12.48 4.3056
Smelt 19.7 14.3 15.2 2.8728 2.0672
Smelt 19.9 15 16.2 2.9322 1.8792



이번에는 규제를 완화하여 20으로 하고, max_iter는 기본값 100보다 충분히 주기 위해 1000으로 지정하겠습니다.

1
2
3
4
5
6
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

이번엔 데이터셋에 주어진 생선의 종류를 모두 사용하여 학습하였습니다.
훈련 데이터셋과 테스트 데이터셋의 성능이 overfitting이나 underfitting없이 나타나는 것 같습니다.


이번에는 테스트셋 5개의 예측결과, 예측된 확률을 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
print(lr.predict(test_scaled[:5]))
# ['Perch' 'Smelt' 'Pike' 'Roach' 'Perch']

prob = lr.predict_proba(test_scaled[:5])
print(np.round(prob, 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']
  • predict_proba의 결과를 보면 예측할 클래스 7개에 대한 확률을 각각의 데이터에 대해 보여줍니다.
  • 그리고 binary logistic regression에서 말씀드린 것 처럼, class의 순서에 따라 확률을 표시합니다.
  • 예측된 방정식을 보면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print(lr.coef_.shape)
print(lr.coef_)
# (7, 5)
# [[-1.49001912 -1.02912113 2.59344919 7.70357975 -1.20070316]
# [ 0.19618178 -2.01069031 -3.77976124 6.50491703 -1.99482274]
# [ 3.56279889 6.34356697 -8.48970852 -5.75757441 3.79307052]
# [-0.10458149 3.60319752 3.93067857 -3.61737459 -1.75069665]
# [-1.4006158 -6.07503214 5.25969469 -0.87219731 1.86043717]
# [-1.38526207 1.49214216 1.39225873 -5.67734316 -4.40097705]
# [ 0.62149782 -2.32406307 -0.90661142 1.7159927 3.69369191]]

print(lr.intercept_.shape)
print(lr.intercept_)
# (7,)
# [-0.09205174 -0.2629083 3.25101298 -0.14742661 2.65498449 -6.78783562
# 1.38422481]

7개의 class에 대한 5가지 feature 이므로 방정식의 크기 또한 다릅니다. 절편 또한 7개로 되어있습니다. multinomial logistic regression에서는 class마다 z값을 하나씩 계산합니다.

그러면 7개의 z값이 존재합니다. binary regression에서는 하나의 z값으로 두개를 분류했는데, 여기서는 어떻게 분류할까요?

scikit learn의 predict_proba의 설명을 보면 다음과 같습니다.

The returned estimates for all classes are ordered by the label of classes.

For a multi_class problem, if multi_class is set to be “multinomial” the softmax function is used to find the predicted probability of each class. Else use a one-vs-rest approach, i.e calculate the probability of each class assuming it to be positive using the logistic function. and normalize these values across all the classes.

  • multinomical의 경우에는 softmax 함수를 사용합니다.
  • softmax는 7개의 z값을 모두 합친 값이 1이 되도록 하여 확률로 변환합니다. 각각의 값에 모두 합친 값을 나누어 줍니다.

이번에도 구해진 z값을 softmax를 이용해 계산해서 같은 값인지 확인해보겠습니다.

5개 데이터의 대한 7개 z값은 다음과 같습니다.

1
2
3
4
5
6
7
decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, 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]]

scipy의 softmax함수를 이용해 계산했을 때, predict_proba의 결과와 같음을 볼 수 있습니다.

1
2
3
4
5
6
7
from scipy.special import softmax
print(np.round(softmax(decision, axis=1), 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]]



여기서 중간점검을 한번 해보겠습니다.

Logistic regression에서 이진 분류의 확률을 출력하기 위해 사용하는 확률은 무엇일까요?



답은 Sigmoid함수입니다. 이진 분류에서는 선형 방정식을 통해 계산된 z값을 두가지 클래스 중에 확정해야 하는데,

값이 작을수록 0을 나타내고 값이 클 수록 1을 나타내는 sigmoid 함수를 이용하면 클래스의 확률을 0, 1에 가깝게 예측할 수 있습니다. 0.5를 기준으로 높으면 1, 낮으면 0 으로 분류가 가능하므로 두가지 클래스에서 하나를 선택할 수 있는 확률이 구해집니다.



Stochastic Gradient Descent (SGD)

지금까지의 방법은 모델을 선언하고, 학습시키고, 예측하는 방법이었습니다. 만약 학습할 데이터가 나중에도 계속해서 추가된다면 어떻게 할까요? 기존처럼 한다면 학습시킨 모델을 버리고 처음부터 학습시키게 되는데, 훈련 데이터가 어마어마하게 크다면 리소스가 많이 소모될 것입니다. 이럴 때는 점진적으로 학습하는 알고리즘을 사용할 수 있습니다. 대표적으로 사용되는 것은 SGD입니다.

  • Stochastic이라는 말은 학습을 시작할 데이터를 랜덤하게 선택한다는 의미입니다.
  • Gradient라는 기울기를 통해 손실을 계산하는 것을 말합니다.
  • Gradient descent라는 것은 손실을 계산해서 낮은 쪽을 따라 내려가는 것을 의미입니다.

정리하면 훈련 데이터에서 하나씩 데이터를 꺼내서 학습할 때 손실을 계산하여 낮은 쪽을 따라 학습하는 알고리즘 입니다.

SGD는 데이터를 하나씩 사용해서 손실를 줄이는 방향으로 학습합니다. 이를 반복하여 전체 데이터셋을 학습하는 것을 하나의 에포크 (epoch) 라고 합니다. 보통은 한번의 에포크가 아닌 수백번의 에포크를 거쳐야합니다. 왜냐하면 아주 조금씩 학습이 되기 때문입니다.



출처: https://www.jeremyjordan.me/nn-learning-rate/



데이터를 하나씩 꺼내서 학습시키고 다음 경사를 따라 갑니다. 조금씩 학습하는 이유는 여기에 있습니다. 조금씩 오차를 계산할 때마다 점점 낮아지므로 다음 단계도 낮을거라는 기대를 가지고 학습하기 때문에, 학습률이 높다면 도로 올라갈 수도 있습니다.



출처: https://www.mltut.com/wp-content/uploads/2020/04/Untitled-document-3.png



또한 Stochastic (확률적)이라는 것은 랜덤하게 데이터를 뽑는 것이라고 말씀드렸는데, 이는 그림에서 나타나는 문제를 보완하기 위함입니다. 랜덤으로 하지 않는다면, 우리가 이상적으로 얻고자하는 Global Minimum이 아닌 처음 찾은 Local minimum이 진짜라고 착각하고 끝내게 될 수도 있습니다.

그리고 성능을 예측하려면 지표가 필요한데, 손실함수Loss Function가 손실이 어느정도인지 계산을 해줍니다.

  • Regression에서는 Mean Squeared Error (MSE)나 Mean Absolute Error (MAE)를 사용할 수 있습니다.
  • Classification에서는 cross entropy loss function (logistic loss function)을 사용할 수 있습니다.

SGD에 대해 더 궁금하시다면 다음 영상도 참고해주세요.





실습

  • 이번에도 classification 을 해보겠습니다.
  • scikit learn에서 SGDClassifier를 제공해줍니다.

데이터 불러오기 및 전처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 데이터 불러오기
fish = pd.read_csv("https://bit.ly/fish_csv_data")

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Train, Test 나누기. 0번째 column이 타겟입니다.
train_input, test_input, train_target, test_target = \
train_test_split(fish.iloc[:, 1:], fish.iloc[:, 0], random_state=42)

# Standardization
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)



SGDClassifier 모델 사용해 예측점수 확인

1
2
3
4
5
6
7
8
9
from sklearn.linear_model import SGDClassifier

sc = SGDClassifier(loss = "log_loss", 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

train 데이터와 test 데이터의 점수가 낮습니다. 지금은 에포크를 10번으로 지정하여 출력한 결과입니다. 또한 아래의 경고메세지도 같이 나옵니다. 이는 모델이 충분히 수렴하지 않았다는 의미이고, 이럴 때에는 max_iter를 높여서 진행하는 것이 좋습니다.

ConvergenceWarning:
Maximum number of iteration reached before convergence. Consider increasing max_iter to improve the fit.mber of iteration reached before convergence. Consider increasing max_iter to improve the fit.

SGD는 점진적 학습을 하는 알고리즘이라 말씀드렸는데, partial_fit으로 모델을 이어서 훈련할 수 있습니다.

1
2
3
4
5
6
# 1 에포크 추가하여 학습
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
# 0.8151260504201681
print(sc.score(test_scaled, test_target))
# 0.8



모델의 성능이 더 좋아지는 것을 확인할 수 있습니다. 그러면 많이 학습시키면 계속해서 좋아질까요?


약 300 에포크 정도를 학습시켜보고, 훈련 데이터셋 점수와 테스트 데이터셋 점수를 비교해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
sc = SGDClassifier(loss = "log_loss", random_state = 42)
# sc.fit(train_scaled, train_target)

train_score = []
test_score = []
classes = np.unique(train_target)

for _ in range(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))



데이터셋이 작아서 크게 차이는 없지만 아래 그림에서 확인해보면, 100 에포크 이후로 훈련 데이터셋과 테스트 데이터셋의 성능에 차이가 나타나는 것을 볼 수 있습니다. Overfitting이 나타나는 지점으로 보입니다.

따라서, 여기서는 100 에포크까지만 학습한 모델을 예측에 사용하는 것이 좋습니다

오늘은 Logistic Regression과 Stochastic Gradient Descent에 대해 알아보았습니다. 다음에는 트리 알고리즘, 교차검증, 앙상블에 대해 정리해보겠습니다.

읽어주셔서 감사합니다.👋