728x90
반응형
SMALL
728x90
반응형
LIST
728x90
반응형
SMALL

지난 신경망 내용에서 매끄럽게 변화하는 시그모이드 함수와 퍼셉트론에서는 갑자기 변화하는 계단 함수를 사용하는 신경망 학습의 차이에 대하여 알아보았습니다.

2023.11.03 - [Programming/Deep Learning] - [Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensor flow)

 

[Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensorflow)

지난 블로그, 신경망의 출력층에 대하여 알아보고 설계에 대하여 알아보았습니다. 2023.10.11 - [Programming/Deep Learning] - [Python/DeepLearning] #6. 출력층(output layer) 설계 [Python/DeepLearning] #6. 출력층(output lay

yuja-k.tistory.com

이번 블로그에서는 모델을 학습 때, 모델의 출력값과 예측값의 오차를 계산해 주는 함수, 손실 함수 Loss Function의 종류에 대하여 알아보도록 하겠습니다.

손실 함수 "Loss Function"

"배가 얼마나 고파요?"라는 질문에 뭐라고 대답할까요?

"배가 많이 고파요" 또는 "적당히 배가 고파요"라는 그냥 막연한 대답이 돌아와야 하는 게 보통입니다.

만약 "저는 지금 63.33 만큼 배가 고파요"라고 이야기하면 질문한 사람이 조금은 당황하겠죠?

이 사람이 개인적으로 만들어낸 이 사람만의 지표라는 것을 토대로 배고픔을 수치적으로 표현했기 때문입니다.

위 사람은 아마 일정 수치가 되면 "배가 고프다"라고 표현하고, 일정 수치가 되면 배가 고프지 않게 밥을 먹는 조금은 특이한 사람이죠.

위에서 이야기한 지표는 하나의 은유적인 비유이지만, 신경망 학습에서는 현재의 상태를 '하나의 지표'로 표현하고, 그 지표를 가장 좋게 만들어주는 가중치 매개변수의 값을 탐색하는 것입니다.

 

위에서 언급한 사람이 자신만의 '배고픔 지표'를 가지고 '최적의 배고픔'을 찾기 위해 노력하듯, 신경망도 '하나의 지표'를 기준으로 최적의 매개변수 값을 탐색합니다.

신경망 학습에서 사용하는 지표를 "손실 함수 Loss Function"라고 합니다. 이 손실 함수는 임의의 함수를 마음대로 사용할 수 있지만, 일반적으로는 평균 제곱 오차 (MSE)교차 엔트로피 오차를 사용합니다.

 

손실 함수는 신경망 성능의 '나쁨'을 나타내는 지표로써, 현재의 신경망이 훈련데이터를 얼마나 잘 처리하지 '못'하느냐를 나타냅니다.

'나쁨'을 지표로 한다는 것이 약간은 어색하지만 그냥 음수만 곱하게 되면 반대를 나타내기 때문에 크게 신경 쓰지 않아도 됩니다.

결론적으로 나쁨을 최소로 하는 것과 좋음을 최대로 하는 것은 같은 의미가 됩니다.

"'나쁨'을 지표로 삼아도 수행하는 일은 다르지 않다"라는 것입니다.

 

평균 제곱 오차 (MSE - Mean Squared Error)

가장 많이 쓰이는 손실 함수로 평균 제곱 오차(Mean Squared Error)가 있습니다. 수식은 다음과 같습니다.

앞쪽이 1/2인 이유는 미분했을 때 남는 식이 (𝑦𝑘−𝑡𝑘) 이기 때문이다.

  • 𝑦𝑘는 예측값을 의미한다. (𝑦̂ 𝑘)
  • 𝑡𝑘는 정답(타깃값)을 의미한다.
import numpy as np
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

def mean_squared_error(y, t):
  return 0.5 * np.sum((y-t) ** 2)
  
print("정답을 2로 추정했을 때의 MSE 값 : {:.3f}".format(mean_squared_error(np.array(y), np.array(t))))

이 배열들의 원소는 첫 번째 인덱스부터 순서대로 '0', '1', '2'를 의미합니다. 

  • y는 𝑠𝑜𝑓𝑡𝑚𝑎𝑥의 출력물로 가정하고, 정답이 2일 확률을 60%로 추정하고 있다.
  • t는 One-Hot Encoding 된 정답 레이블로써, 원래 정답이 2라고 정해져 있다.

소프트맥스 함수의 결과물은 확률이라고 해석할 수 있기 때문에 위 예에서 이미지가 0으로 분류될 확률은 0.1, '1'로 분류될 확률은 0.05, '2'로 분류될 확률은 0.6... 이런 식으로 해석해 보도록 합시다. 

𝑡는 정답 레이블로써, 정답을 가리키는 위치의 원소는 1로, 그 외에는 0으로 표기한 모습입니다. 숫자 '2'를 의미하는 원소 값이 1이기 때문에 정답은 '2'로 생각할 수 있습니다. 평균 제곱오차 수식을 직접 구현해 보겠습니다.

# 일부로 오답 예측을 만들어서 MSE 값을 측정하기. 예측값을 7이라고 가정한 경우로 테스트
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print("정답을 7로 추정했을 때의 MSE 값 : {:.3f}".format(mean_squared_error(np.array(y), np.array(t))))

두 가지 예를 살펴보았습니다. 첫 번째 예는 정답이 '2'이고, 신경망의 출력도 '2'에서 가장 높은 경우라고 생각하면 됩니다.

두 번째 예에서도 정답은 똑같이 '2' 지만, 출력이 '7'인 경우가 가장 높다고 가정한 것입니다.

결괏값을 확인해 보면 정답을 '2'로 예측했을 때의 손실 함수 오차가 더 작으며, 정답 레이블과의 오차도 작은 것을 알 수 있습니다.

즉, 평균 제곱 오차를 기준으로는 첫 번째 추정 결과가 오차가 더 작기 때문에 정답에 가깝다!라고 판단할 수 있을 것입니다.

교차 엔트로피 오차 (CEE - Cross Entropy Error)

또 다른 손실 함수로써 교차 엔트로피 오차(cross entropy error)도 많이 활용됩니다. 교차 엔트로피 오차 수식은 다음과 같습니다.

여기에서의 𝑙𝑜𝑔는 밑이 𝑒인 자연로그 (𝑙𝑜𝑔𝑒)입니다.

𝑦𝑘는 신경망의 출력, 𝑡𝑘는 정답 레이블입니다. 𝑡𝑘는 정답에 해당하는 인덱스의 원소만 1이고, 나머지는 0인 One-Hot-Encoding 된 형식의 데이터입니다.

그래서 위 식은 실질적으로 정답일 때의 추정(𝑡𝑘가 1일 때의 𝑦𝑘)의 자연로그를 계산하는 식이 됩니다.

정답이 아닌 나머지 모드는 𝑡𝑘가 0이므로 𝑙𝑜𝑔𝑦𝑘와 곱해도 0이 되어 결과에 영향을 주지 않습니다. 예를 들어 정답 레이블은 '2'가 정답이라 하고, 이때의 신경망 출력이 0.6이라면 교차 엔트로피 오차는 −𝑙𝑜𝑔0.6=0.51이 됩니다. 또한 같은 조건에서 신경망 출력이 0.1이라면 −𝑙𝑜𝑔0.1=2.30이 됩니다.

즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 됩니다. 교차 엔트로피를 구현해 보도록 하겠습니다.

  • One Hot Encoding 된 𝑡𝑘가 곱해지기 때문에, 정답이 아닌 타깃은 신경을 전혀 쓰지 않는다. ( 𝑙𝑜𝑔𝑦𝑘 X '0 값' )
  • 예측값 자체가 오차 출력값이 된다.
def cross_entropy_error(y, t):
  delta = 1e-7
  return -np.sum(t * np.log(y + delta))

 

위에서 아주 작은 값인 delta를 더하는 이유는 np.log 함수에 0이 대입되면 마이너스 무한대를 나타내게 됩니다. 따라서 더 이상 계산을 할 수 없기 때문에 y에 아주 작은 값 delta를 더해 절대 0이 되지 않도록, 즉 마이너스 무한대가 되지 않도록 방지한 코드입니다.

위의 함수를 이용해서 2를 예측하는 간단한 계산을 해보겠습니다.

# 정답은 2
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 정답이 2일 확률이 가장 높다고 추정함(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print("정답을 2로 추정했을 때의 CEE값 : {:.3f}".format(cross_entropy_error(np.array(y), np.array(t))))

# 정답이 7일 확률이 가장 높다고 추정함(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print("정답을 7로 추정했을 때의 CEE값 : {:.3f}".format(cross_entropy_error(np.array(y), np.array(t))))

 

MSE와 마찬가지로 첫 번째 예에서는 오차가 약 0.51, 두 번째 예에서는 2.30 정도로 많은 차이가 나타나고 있는 것이 확인됩니다.

즉, 결과가 더 작은 첫 번째 추정이 정답일 가능성이 높다고 판단한 것으로, 앞서 평균 제곱 오차의 판단과 일치하게 됩니다.

미니 배치 학습

일전에 배치에 대해 이야기해 보았습니다.

2023.11.03 - [Programming/Deep Learning] - [Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensor flow)

 

[Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensorflow)

지난 블로그, 신경망의 출력층에 대하여 알아보고 설계에 대하여 알아보았습니다. 2023.10.11 - [Programming/Deep Learning] - [Python/DeepLearning] #6. 출력층(output layer) 설계 [Python/DeepLearning] #6. 출력층(output lay

yuja-k.tistory.com

입력한 데이터의 묶음에 대한 결과물들의 묶음이었는데요, 이 데이터들을 모두 훈련 데이터로 사용하게 된다면, 모든 데이터에 대해 손실함수를 적용시켜야 할 것입니다.

즉, 훈련데이터가 100 묶음이라면, 그로부터 계산한 100개의 손실 함숫값들의 합을 지표로 삼아야 한다는 이야기가 될 것입니다.

지금까지 데이터 하나에 대한 손실 함수만 생각해 보았으니, 이제 훈련 데이터 모두에 대한 손실 함수를 적용시킨다고 생각해 보겠습니다.

그렇다면 교차 엔트로피의 오차는 다음과 같겠네요!

𝑁 개의 배치 데이터를 활용했을 때 바뀐 CEE 공식

식이 조금 복잡해진 것 같지만, 간단합니다.

데이터가 𝑁개라면 𝑡𝑛𝑘는 𝑛번째 데이터의 𝑘번째 값을 의미합니다.

(𝑦𝑛𝑘는 신경망의 출력, 𝑡𝑛𝑘는 정답 레이블) 단순히 교차 엔트로피 함수를 𝑁개로 확장하고, 그 오차를 모두 더해서 마지막에 𝑁으로 나누어 정규화하고 있을 뿐입니다.

즉, 제일 마지막에 𝑁으로 나눔으로써 평균 손실 함수를 구하는 것이다라고 생각할 수 있겠습니다.

이렇게 평균을 구해 사용하면 훈련 데이터의 개수와 관계없이 언제든 통일된 지표를 얻을 수 있을 것 같습니다. 예를 들어 훈련 데이터가 1,000개든, 10,000개든 상관없이 평균 손실 함수를 구할 수 있다는 뜻이 됩니다.

 

많은 양의 데이터셋에 대한 처리

일전에 사용했은 MNIST 데이터셋은 훈련 데이터가 무려 60,000개였습니다.

그래서 모든 데이터를 대상으로 손실 함수의 합을 구하려면 시간이 많이 걸리게 됩니다. 더 나아가 빅데이터 수준이 되면 그 수는 수백만에서 수천만도 넘는 거대한 값이 되기도 합니다.

이 많은 데이터를 대상으로 일일이 손실 함수를 계산하는 것은 비현실적이기 때문에, 데이터 일부를 추려 '근사치'로 이용할 수 있습니다.

이 일부가 되는 데이터를 미니배치(mini-batch)라고 합니다. MNIST에서 미니배치 학습을 구현하는 코드를 작성해 보도록 하겠습니다.

# mnist 데이터셋 로딩
from tensorflow.keras import datasets
mnist = datasets.mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train.shape

y_train.shape

 

Cross Entropy는 One Hot Encoding 이 되어 있어야 한다.

  • X_train.shape -> (60000, 784)
  • y_train.shape -> (60000, 10)
# step 1 : reshape 부터 진행 -> (60000, 1)
y_train_step_1 = y_train.reshape(-1, 1)
y_train_step_1.shape

y_train_step_1[:3]

# OneHotEncoder를 이용해서 원핫 인코딩 수행
from sklearn.preprocessing import OneHotEncoder

y_train_one_hot = OneHotEncoder().fit_transform(y_train_step_1)
y_train_one_hot = y_train_one_hot.toarray()

y_train_one_hot.shape

y_train_one_hot[:3]

X_train = X_train.reshape(60000, -1)
X_train.shape

미니 배치 구현하기
# 훈련 데이터에서 무작위로 10장만 빼내오기
train_size = X_train.shape[0] # 전체 훈련 데이터셋 크기
batch_size = 10 # 미니 배치의 사이즈
batch_mask = np.random.choice(train_size, batch_size) # train_size에서 무작위로 batch_size만큼의 정수를 선택

# 랜덤으로 선택된 인덱스에 있는 데이터만 추려내기
X_batch = X_train[batch_mask] 

# 원핫 인코딩된 y_train에서 선택된 인덱스에 있는 데이터만 추려내기
y_batch = y_train_one_hot[batch_mask]

np.random.choice 함수를 이용해 지정한 범위의 수 중에서 무작위로 원하는 개수만 꺼내는 역할을 합니다.

위의 예에서는 60000 미만의 수 중에서 10개의 무작위 한 숫자를 뽑아낸 것입니다.

실제 batch_mask를 확인하면 10개의 인덱스가 무작위로 들어있는 배열을 확인할 수 있습니다.

print("무작위로 선택된 인덱스 : {}".format(batch_mask))

 

배치용 교차 엔트로피 구현하기

미니배치 같은 배치데이터를 지원하는 교차 엔트로피 오차는 다음과 같이 구현합니다.

단순히 배열 하나만 처리하던 부분에서, 여러 개의 배열을 한꺼번에 처리할 수 있도록만 바꿔주면 간단히 구현 가능 합니다.

# 배치 및 배치가 아닐 때 까지 고려
def cross_entropy_error(y, t):

  # 1차 원일 때에 대한 처리( 배치가 아닐 때의 처리 )
  if y.ndim == 1:
    # 강제로 2차원 배열화 시키는 것
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)

  batch_size = y.shape[0]
  return -np.sum(t * np.log(y)) / batch_size

위 코드에서 y는 신경망의 출력, t는 정답 레이블을 의미합니다.

y가 1차원이라면, 즉 데이터 하나당 교차 엔트로피 오차를 구하는 경우는 reshape 함수로 데이터의 형상을 바꿔줍니다. 그리고 배치의 크기로 나눠 정규화시키고 이미지 1장당 평균의 교차 엔트로피 오차를 계산합니다.

정답 레이블이 원 - 핫 인코딩이 아니라 '2'나 '7' 등의 숫자 레이블로 주어졌을 때의 교차 엔트로피 오차는 다음과 같이 구현 가능합니다.

# 원-핫 인코딩이 되어있지 않은 경우 대응
def cross_entropy_error(y, t):
  if y.ndim == 1:
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)

  batch_size = y.shape[0]
  return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

위 구현에서는 원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이기 때문에 그 계산은 무시해도 좋다는 점이 핵심입니다.

다시 말해, 정답에 해당하는 신경망의 출력만으로 교차 엔트로피 오차를 계산할 수 있다는 이야기가 됩니다.

그래서 원-핫 인코딩 시 t * np.log(y)였던 부분을 레이블 표현일 때는 np.log(y [np.arange(batch_size), t]) 로 구현합니다.

 

np.log(y[np.arange(batch_size), t]를 간단히 설명하면, np.arange(batch_size)는 0부터 batch_size - 1까지의 배열을 생성합니다.

즉, batch_size가 5라면 np.arange(batch_size)는 [0, 1, 2, 3, 4] 같은 Numpy 배열을 생성하게 되고, t에는 레이블이 [2, 7, 0, 9 ,8]와 같이 저장되어 있으므로 y [np.arange(batch_size), t]는 각 데이터의 정답 레이블에 해당하는 신경망의 출력을 추출합니다.

위 예에서의 출력은 [y [0, 2], y [1, 7], y [2, 0], y [3, 9], y [4, 8]]인 Numpy 배열이 만들어지는 것을 알 수 있습니다.

마지막 신경망 학습을 위한 경사법을 위한 미분은 다음 시간에 다뤄보도록 하겠습니다!

 

728x90
반응형
LIST
728x90
반응형
SMALL

이번 블로그는 선형 모델에 대하여 알아보도록 합시다.

선형 모델은 매우 오래전 개발된 모델입니다. 선형 모델은 입력 특성에 대한 선형 함수를 만들어 예측을 수행합니다. 먼저 회귀의 선형 모델부터 알아보겠습니다

회귀의 선형 모델을 위한 일반화된 예측 함수는 다음과 같습니다.

𝑦̂ =𝑤[0]∗𝑥[0]+𝑤[1]∗𝑥[1]+...+𝑤[𝑝]∗𝑥[𝑝]+𝑏

위의 식에서 𝑥[0]부터 𝑥[𝑝]까지는 하나의 데이터 포인트에 대한 특성을 나타내고, (특성의 수는 𝑝+1 )와 𝑏는 모델이 학습해야 할 파라미터입니다. 𝑦̂ 는 모델이 만들어낸 예측값입니다.

그렇다면 특성이 한 개인 예측 함수는 어떻게 될까요? 다음과 같습니다.

𝑦̂ =𝑤[0]∗𝑥[0]+𝑏

위의 함수를 보면 예전 수학시간에 배운 직선의 방정식이 떠오릅니다. 𝑤[0]은 직선의 기울기( 계수(cofficient)라고도 합니다 )가 되고, 𝑏는 y 축과 만나는 절편( offset 또는 intercept )이 됩니다.

특성이 많아질수록 𝑤는 각 특성에 해당하는 기울기를 모두 가지게 됩니다. 즉 예측값은 입력특성에 𝑤의 가중치를 모두 더한 가중치의 합으로 볼 수 있겠습니다.

머신러닝에서 알고리즘이 주어진 데이터로부터 학습하는 파라미터라서 𝑤와 𝑏를 모델 파라미터라고 합니다.

나중에 살펴보겠지만 모델이 학습할 수 없어 사람이 직접 설정해 줘야 하는 파라미터를 하이퍼 파라미터(Hyper Parameter)라고 합니다.


wave 데이터셋을 이용해 눈으로 확인해 보겠습니다.

 

#필요 라이브러리 임포트
from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
plt.rcParams['axes.unicode_minus'] = False

import platform
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry!')




# mglearn 샘플 그래프 보기
mglearn.plots.plot_linear_regression_wave()

 

그려진 그래프를 보면 직선의 방정식이 생각나는데요, 기울기는 대략 0.4, 절편은 0에 가깝게 나타나고 있는 것이 확인됩니다.

회귀 모델을 위한 선형 모델은 특성이 하나인 경우에는 직선, 두 개면 평면이 되며, 세 개 이상( 더 높은 차원 )이 되면 초평면(Hyperplane)이 되는 회귀 모델의 특징을 가지고 있습니다.

위의 그래프와 이전에 했었던 KNeighborsRegressor를 사용하여 만든 그래프를 비교해 보면 어떤가요? 직선을 지나는 점이 얼마 없는 것으로 보아 데이터의 상세정보를 대부분 잃어버린 것처럼 보이지 않나요?

사실 wave 같은 저 차원 데이터 셋에서는 타깃의 𝑦의 특성이 선형 조합이라고 생각하는 것은 매우 비현실 적인 가정입니다. 직선을 따라 데이터가 쭉 연결되지는 않으니까요.

하지만 1차원 데이터셋만 놓고 봐서 생긴 우리의 편견 일 수도 있습니다. 특성이 많은 데이터 셋이라면 선형 모델은 매우 훌륭한 성능을 발휘하며, 훈련 데이터보다 특성이 더 많은 경우엔 어떤 타깃 𝑦도 완벽하게 훈련 세트에 대해서 선형 함수로 모델링할 수 있습니다.

 

회귀를 위한 선형 모델은 다양합니다.

인기 있는 몇 가지 선형 모델을 살펴보려고 합니다. 이 모델들은 훈련 데이터로부터 모델 파라미터 𝑤와 𝑏학습하는 방법모델의 복잡도를 제어하는 방법에서 차이가 납니다.

선형 회귀 Linear Regression (최소제곱법)

선형 회귀(Linear Regression) 또는 최소 제곱법(OLS, Ordinary Least Squares)은 가장 간단하고 오래된 회귀용 선형 알고리즘이라고 할 수 있습니다.

선형 회귀는 훈련 세트에 있는 타깃 y 사이에 평균 제곱 오차(MSE - Mean Squared Error)를 최소화하는 모델 파라미터 𝑤와 𝑏를 찾습니다. 참고로 MSE는 다음과 같습니다.

 

위의 수식의 𝑛은 샘플의 개수입니다. 이를 𝐿2 norm을 적용했다라고 이야기 합니다.

위의 수식이 조금 불편 하긴 하지만 사실 MSE는 모델이 예측한 예측값( 𝑦𝑖^ )과 훈련 세트로 인해 훈련된 값 타깃값( 𝑦𝑖 )의 차이를 제곱하여 더한 후에 평균을 구한 것이라고 보면 됩니다. ( 샘플의 개수로 나눈 것 )


from sklearn.model_selection import train_test_split # 일반화 성능 평가를 위해 기존 데이터를 훈련 세트와 테스트 세트로 나눔

from sklearn.linear_model import LinearRegression
X, y = mglearn.datasets.make_wave(n_samples=60) # 60개의 샘플 데이터 준비
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) # 훈련, 테스트 데이터셋 준비

lr = LinearRegression().fit(X_train, y_train) # 선형 회귀 모델에 훈련!

모델이 데이터를 입력받고 훈련을 통해 알아내야 할 𝑤( 가중치 - weight 또는 계수 - cofficient )와 𝑏( 편향 - offset 또는 절편 - intercept ) 은 각각 coef와 intercept 변수에 들어 있습니다.

print("lr의 계수(weight 또는 cofficient) : {}".format(lr.coef_))
print("lr의 편향(offset 또는 intercept) : {}".format(lr.intercept_))

위의 결과에서 알 수 있는 사실 중 하나는 coef값은 NumPy 배열 형태이지만 intercept 값은 언제나 실수(float) 값 하나라는 사실입니다.

즉 coef 값은 각 특성 부여되는 가중치들의 배열이라는 사실을 알 수 있고, intercept 속성은 그 가중치에 대한 조정값이라는 점입니다.

wave 데이터 셋은 입력 특성이 하나밖에 없기 때문에 배열의 길이도 1이라는 사실을 알 수 있습니다.

print("훈련 세트 점수 : {}".format(lr.score(X_train,y_train)))
print("테스트 세트 점수 : {}".format(lr.score(X_test, y_test)))

 

단순히 선형 회귀(LinearRegression) 모델을 훈련시킨 후 점수를 확인해 보니 𝑅2 값이 66% 정도로 별로 좋지 않습니다. 또한 훈련 세트의 점수와 테스트 세트의 점수가 거의 비슷한 것을 확인할 수 있는데, 이는 과소적합인 상태를 의미합니다. 많이 생각할 필요 없이 우리가 테스트 용도로 사용한 wave 데이터 셋은 특성이 하나뿐인 1차원 데이터 셋이기 때문에 과대적합을 걱정하지 않아도 됩니다. 하지만 지금부터 사용해 볼 고차원 데이터 셋인 보스턴 주택 가격 데이터셋에 대해서는 선형 모델의 성능이 매우 높아지기 때문에 과대적합이 될 가능성이 있습니다!

X, y = mglearn.datasets.load_extended_boston()

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 0)
lr = LinearRegression().fit(X_train, y_train)

print("훈련 세트 점수 : {:.2f}".format(lr.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lr.score(X_test, y_test)))

 

테스트 결과 훈련 세트의 𝑅2는 95%지만, 테스트 세트의 점수는 61%입니다. 이는 모델이 과대적합되었다는 확실한 신호입니다. 따라서 우리는 모델의 일반화를 위해 복잡도를 제어할 수 있어야 하겠습니다. (각 특성별 가중치를 조절합니다.) 기본 선형 회귀 방식인 LinearRegression 모델로는 파라미터를 통해 복잡도를 제어 할 수 없으니, 여러 가지 회귀 모델을 알아보도록 하겠습니다.


릿지 Ridge 회귀

릿지(Ridge)도 회귀를 위한 선형 모델이므로 최소 제곱법 같은 예측 함수를 사용합니다. 릿지 회귀에서의 가중치(𝑤) 선택은 훈련 데이터를 잘 예측하기 위해서 뿐만 아니라 추가 제약 조건을 만족시키기 위한 목적도 있습니다. 즉 𝑤의 모든 원소가 0에 가깝게 만들어 가중치의 절댓값을 가능한 한 작게 한다는 뜻입니다. 결론적으로는 모든 특성이 예측(출력)에 주는 영향을 최소화하겠다는 뜻이 됩니다. 즉 𝑤에 의한 그래프의 기울기가 작아집니다. 이러한 제약 사향들을 규제(regularzation)이라고 합니다. 규제란 과대적합이 되지 않도록 모델을 강제로 제한한다는 뜻이 됩니다. 먼저 아무런 규제의 페널티가 없는 상황부터 살펴보겠습니다.

from sklearn.linear_model import Ridge

ridge = Ridge().fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(ridge.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(ridge.score(X_test, y_test)))

 

결과를 확인해 보니 훈련 세트에 대한 점수는 낮지만 테스트 세트에 대한 점수는 더 높네요. 선형 회귀(LinearRegression)는 보스턴 데이트 셋에 대해 과대적합 되지만 Ridge 회귀 방식은 더 자유로운 ( 특성에 제한이 걸리는 ) 모델이기 때문에 과대적합이 적어진다고 볼 수 있습니다.

따라서 모델의 적합도가 낮아졌다고 볼 수 있는데, 이렇게 모델의 복잡도가 낮아지면 훈련 세트에서의 성능은 나빠지지만 더욱 일반화된 모델이 됩니다.

우리는 테스트 세트에 대한 성능이기 때문에 보스턴 데이터셋에는 일반 선형 회귀보다는 Ridge 회귀가 어울리는 것 같네요


페널티

여기서 페널티에 대해 잠깐 이야기 하자면, 패널티란 예측 결과물에 대한 오차 범위 허용이라고 볼 수 있습니다. 이정도 오차는 괜찮아~ 라고 말 하는 것과 비슷합니다. 간단한 예로 택시의 예상 도착 시간을 예측 한다고 했을 때 예상 한 것 보다 1~2분 늦는 것은 별로가 문제가 되지 않으나, 10 ~ 20분 늦는 것은 예측이 매우 실패 했다 라고 판단 할 수 있습니다. 우리가 위에서 사용한 MSE(Mean Squad Error) 같은 공식이 예측값의 패널티에 대한 공식 같은 것이라고 판단하면 됩니다. MSE의 공식을 자세히 보면 오차 제곱( 타깃값 - 예측값의 제곱 )을 구하고 있는데, 이는 오차가 커지면 커질수록 더욱더 많은 페널티를 부여한다고 보면 됩니다. 보통 사용되는 다른 공식으로써 MAE(Mean Absolute Error)라는 것이 있습니다.

MSE와 다른 점은 MAE는 오차에 대한 페널티가 단순히 오차의 절대 값인 것을 확인할 수 있습니다. 이를 𝐿1 norm이라고 합니다. 즉 MSE는 오차가 커질수록 페널티가 커지고, MAE는 오차가 커질 수록 패널티가 일정 하다 라고 생각 해 볼 수 있겠습니다. 패널티를 계산하는 공식에는 RMSE, RMAE 등등의 여러가지 공식들이 있습니다.

Ridge 회귀에서의 패널티

Ridge는 모델을 단순하게 ( 계수 - 𝑤를 0에 가깝게 ) 해주고 훈련 세트와 테스트 세트 사이의 선능 사이를 절출 할 수 있는 방법을 제공합니다. 우리는 Ridge 회귀의 alpha 매개변수를 조절하여 모델의 성능을 단순화할 수 있습니다. 매개변수를 아무것도 넣지 않으면 alpha는 1.0을 기본적으로 사용하고 있었습니다. 참고로 alpha 값을 높이면 계수를 0으로 점점 가깝게 설정해 페널티의 효과가 커져 가중치가 차지하는 값이 0에 가까워져 모델이 단순해지고, alpha 값을 낮추면 점점 가중치가 높아져 모델이 복잡해집니다. Ridge 회귀를 사용할 때 최적화된 alpha 값은 사용하는 데이터셋에 따라 달라집니다. 바로 예를 들어 한번 보겠습니다. alpha 값을 10으로 조절한 경우입니다.

ridge10 = Ridge(alpha=10).fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(ridge10.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(ridge10.score(X_test, y_test)))

alpha값을 10으로 높였더니 페널티의 효과가 높아져 가중치가 감소 한 것이 확인 됩니다. 즉 모델이 점점 단순화 되어 과소 적합 되어 가고 있다고 판단 할 수 있습니다.

이번엔 반대로 alpha 계수를 낮춰서 패널티의 효과를 줄이고 가중치를 증가시켜 보겠습니다.

ridge01 = Ridge(alpha=0.1).fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(ridge01.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(ridge01.score(X_test, y_test)))

테스트 세트의 점수가 꽤나 높아졌습니다. 보스턴 주택가 데이터셋을 Ridge 회귀로 분석했을 때 alpha=0.1이 꽤나 좋은 성능을 내는 것 같네요.

이번엔 alpha 값에 따라서 각 모델의 coef_ ( 가중치 값 )이 어떻게 달라지는지 시각화해서 살펴보겠습니다.

예상을 해보자면 높은 alpha 값은 제약이 적은 모델이기 때문에 ( 가중치가 0에 점점 가까워지기 때문에 ) coef_의 절댓값의 크기가 작을 것 같네요. 확인해 보겠습니다.

plt.plot(ridge10.coef_, '^', label="Ridge alpha 10")
plt.plot(ridge.coef_, 's', label="Ridge alpha 1.0")
plt.plot(ridge01.coef_, 'v', label="Ridge alpha 0.1")

plt.plot(lr.coef_, 'o', label="LinearRegression")
plt.xlabel("계수 목록")
plt.ylabel("계수 크기")
plt.hlines(0, 0, len(lr.coef_))
plt.ylim(-25, 25)
plt.legend()

뭔가 굉장히 복잡해 보이네요, 먼저 X축은 coef_의 원소를 위치대로 나열한 것입니다. 즉 x=0은 첫 번째 특성, 약간 100을 넘는 값은 마지막 특성이라고 보면 될 것 같습니다.

y축은 계수의 수치를 나타내는데, alpha=10 일 때 ( 파란색 세모 ) 대부분의 계수는 -3부터 3 사이에 위치하는 것을 볼 수 있습니다. alpha=1 ( 주황색 네모 )의 계수의 크기는 조금 더 커져 가중치가 증가한 것이 확인됩니다. alpha=0.1 ( 초록색 세모 ) 같은 경우는 계수의 크기가 더 커져 가중치가 1일 때 보다 더 커진 것을 확인할 수 있고

alpha 값이 0인 ( 규제가 전혀 없는 ) LinearRegression 같은 경우는 그래프를 벗어나는 특성도 존재하는 것을 확인해 볼 수 있습니다.

 

Ridge에서 alpha 값은 고정되고, 데이터의 개수를 조절한다면?

규제의 효과를 확인해 보기 위해서 alpha를 1로 고정한 채로 훈련 데이터의 크기만 변화를 시켜 데이터 세트의 크기에 대해 훈련 효과를 확인해 보겠습니다.

보스턴 주택 가격 데이터세트에서 여러 가지 크기로 훈련시켜 LinearRegression과 Ridge(alpha=1)을 적용한 그래프를 확인 해 보겠습니다.

mglearn.plots.plot_ridge_n_samples()

릿지와 선형 회귀 모두 훈련 세트의 점수 ( 점선 )가 테스트 세트의 점수 ( 실선 ) 보다 높은 것을 확인할 수 있습니다.

여기서 살펴볼 수 있는 사실은 릿지 회귀에는 규제가 적용되기 때문에 훈련 데이터의 점수가 대체적으로 선형 회귀보다 낮은 것을 확인할 수 있습니다. 이는 데이터 세트의 개수가 적으면 적을수록 더 확연히 확인해 볼 수 있습니다.

데이터 크기가 400 미만에서는 선형 회귀는 무엇도 학습하지 못하고 있는 것이 확인됩니다.

즉 두 모델의 성능은 ( 테스트 데이터세트의 결과 ) 데이터가 많으면 많을수록 좋아지며, 마지막에는 선형 회귀가 릿지 회귀의 테스트 점수를 따라잡는 것이 확인됩니다.

정리하자면 데이터를 충분히 주게 되면 규제 자체는 덜 중요해져 릿지 회귀와 선형 회귀의 성능이 같아질 것이라는 것을 예상해 볼 수 있다는 점입니다.

또 하나 이상한 점은 주황색 점선 ( 훈련 데이터셋 점수 )가 점점 내려가는 것을 확인할 수 있는데, 이는 선형 회귀의 훈련 데이터셋에 대한 성능이 점점 감소하는 것을 의미합니다.

이는 데이터가 많아지면 많아질수록 모델이 데이터를 기억하거나 과대적합 하기 어려워진다는 것을 의미합니다.


라쏘 Lasso 회귀

라쏘 회귀 방식도 릿지와 비슷하게 계수에 규제를 걸어 계수를 0에 가깝게 만들기 위한 노력을 합니다. 릿지 회귀와의 차이점은 릿지 회귀는 L2 규제 방식을 사용하는데 비해 라쏘 회귀는 L1 규제 방식을 사용하며, 실제 어떤 계수는 실제로 0이 되기도 한다는 점입니다.

계수가 0이 되면 해당 특성은 전혀 상관이 없이 완전히 제외된다는 뜻이 됩니다.

어떻게 보면 라쏘 회귀는 특성 선택( feature selection )이 자동으로 이루어진다고도 볼 수 있습니다.

일부 계수를 0으로 만들면 모델을 이해하기가 쉬워지고 이 모델의 가장 중요한 특성이 무엇인지가 드러납니다.

보스턴 주택가격 데이터셋에 라쏘를 적용시켜 보겠습니다.

from sklearn.linear_model import Lasso

lasso = Lasso().fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(lasso.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lasso.score(X_test, y_test)))
print("사용한 특성의 수 : {}".format(np.sum(lasso.coef_ != 0)))

확인 결과 라쏘는 훈련 세트와 테스트 세트의 점수가 전부 다 좋지 않습니다. 사용한 특성도 4개 정도밖에 안 되는 것으로 보아 과소적합이라고 생각할 수 있겠습니다.

릿지와 비슷하게 라쏘도 계수를 얼마나 강하게 보낼지를 조절하는 alpha 매개변수가 있습니다. 앞서 본 릿지에서는 기본적으로 alpha=1.0을 사용했었습니다.

과소적합을 줄이기 위해 alpha 값을 줄여보겠습니다. 하지만 이때 최대 반복 횟수를 의미하는 max_iter 값을 늘려 줘야 합니다.

max_iter는 내부적으로 Lasso의 학습 과정이 진행되는 최대 횟수를 의미합니다. 한 특성 씩 좌표축을 따라 최적화되는 좌표 하강법 방식을 사용하며 학습 과정이 max_iter에 지정된 횟수만큼 반복 횟수가 설정 되게 됩니다. lasso.niter를 이용해 반복 횟수를 확인할 수 있습니다.

lasso001 = Lasso(alpha=0.01, max_iter=100000).fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(lasso001.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lasso001.score(X_test, y_test)))
print("사용한 특성의 수 : {}".format(np.sum(lasso001.coef_ != 0)))

alpha 값을 줄여 봤더니 모델의 복잡도가 증가하여 훈련 세트와 테스트 세트의 점수가 모두 좋아졌습니다. 성능 자체가 릿지보다 조금 더 낫네요!

사용된 특성 자체가 105개 중 33개뿐이어서 모델을 분석하기가 쉬워진 것 같습니다.

하지만 alpha 값을 너무 낮추면 규제가 그만큼 효과가 없어지기 때문에 과대적합이 되므로 LinearRegression의 결과와 비슷해집니다.

lasso0001 = Lasso(alpha=0.0001, max_iter=1000000).fit(X_train, y_train)

print("훈련 세트 점수 : {:.2f}".format(lasso0001.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lasso0001.score(X_test, y_test)))
print("사용한 특성의 수 : {}".format(np.sum(lasso0001.coef_ != 0)))

마찬가지로 alpha 값에 따른 다른 모델들의 계수를 그래프로 그려 보겠습니다.

plt.plot(lasso.coef_, 's', label="Lasso alpha=1")
plt.plot(lasso001.coef_, '^', label="Lasso alpha = 0.01")
plt.plot(lasso0001.coef_, 'v', label="Lasso alpha = 0.0001")

plt.plot(ridge01.coef_, 'o', label="Ridge alpha = 0.1")
plt.legend(ncol=2, loc=(0, 1.05))
plt.ylim(-25, 25)
plt.xlabel("계수 목록")
plt.ylabel("계수 크기")

alpha가 1일 때( 파란색 )는 계수의 대부분이 0인 것을 알 수 있고, 나머지 계수들도 크기가 작다는 것이 확인됩니다. alpha를 0.01로 줄이면 ( 주황색 세모 ) alpha가 1일 때보다는 적지만 마찬가지로 계수 대부분이 1인 것이 확인 됩니다.

하지만 alpha 값이 0.0001이 되면 ( 초록색 세모 ) 계수 대부분이 0이 아니게 되고 그 값이 커지는 것이 확인 됩니다.

alpha 값이 0.1인 릿지 모델은 Lasso 모델과 비교해 성능은 비슷하지만 어떠한 계수도 0이 되지 않는 것이 확인 됩니다.

실제로 Lasso와 Ridge 중에서 릿지 회귀를 선호합니다.

하지만 입력 특성 자체가 많고 그중 일부분만 중요한 특성일 경우에는 Lasso가 더 좋은 선택이 될 수가 좋습니다. 또한 분석하기 쉬운 모델을 원한다면 Lasso가 일부 모델만 사용하므로 쉽게 해석할 수 있는 모델을 만들어 주게 될 수도 있습니다.

 

728x90
반응형
LIST

+ Recent posts