본문 바로가기
  • 하고 싶은 일을 하자
개발

딥러닝 모델의 Overffiting을 줄이는 방법

by 박빵떡 2020. 12. 15.
반응형
이 글은 Washington University in St.Louis의 Jeff Heaton 교수님의 강의를 참고하였습니다.
05_1 reg ridge lasso
05_2 kfold
05_3 keras L1 L2
05_4 dropout
05_5 bootstrap
링크 : https://github.com/jeffheaton/t81_558_deep_learning

목차

  • Regularization(가중치 규제 : Riedge, Lasso)
  • K-Fold Cross Validation(K-겹 교차 검증)
  • Holdout
  • Dropout
  • Regularization을 활용한 벤치마킹(boosting)

Regularization(가중치 규제 : Riedge, Lasso)

Overfitting(과적합) 이란?

Linear Regression 모델 예시

Linear Regression 모델로 예시를 들어보겠습니다. 예시의 모델 중 가운데 모델이 가장 좋은 모델입니다. 왼쪽 모델은 Underfitting, 오른쪽 모델은 Overfitting 한 문제가 있습니다.

 

Overfitting 모델은 테스트 셋에서는 손실이 0에 가깝지만 학습하지 않은 새로운 데이터는 제대로 예측하지 못합니다.

Logistic Regression 모델 예시

이번에는 Logistic Regression 모델을 예시로 들겠습니다. Linear Regression 예제와 마찬가지로 왼쪽 모델은 Underfitting, 가운데 모델이 가장 좋고, 오른쪽 모델은 Overfitting 합니다.


모델을 만들 때 Overfitting을 야기시키는 두 개의 X를 무시하고 학습한다면 가운데 모델이 됩니다. 따라서 Overfitting을 없앤다는 것은 특이점의 영향을 제거하여 모델을 더 일반화시키는 것이라고 생각할 수 있습니다.


그렇다면 어떻게 Overfitting을 없앨 수 있을까요?

과적합을 해결하는 방법

1. 학습 데이터 늘리기
모델은 데이터의 양이 적을 경우, 해당 데이터의 특정 패턴이나 노이즈까지 쉽게 암기하기 되므로 과적합 현상이 발생할 확률이 늘어납니다. 그렇기 때문에 데이터의 양을 늘릴 수록 모델은 데이터의 일반적인 패턴을 학습하여 과적합을 방지할 수 있습니다.

 

2. 학습 데이터를 늘릴 수 없다면 모델을 단순화 시키기 학습 데이터의 양을 늘리는 건 어려운 일입니다.

이에 대한 대안으로 사용되는 방법이 Occam's Razor(오캄의 면도날) 방법입니다. 14세기 수도사이자 철학자인 Occam은 면도날 법칙을 만들었습니다. "같은 현상을 설명하는 두 개의 주장이 있다면 간단한 쪽을 선택하는 것이 좋다"라는 법칙입니다. 이 방법은 머신 러닝 뿐만아니라 통계에서도 흔히 사용된다고 합니다. 이 법칙을 머신러닝에 적용하면 "모델이 덜 복잡할수록 좋은 결과를 얻을 수 있다" 이라고 할 수 있습니다. 오캄의 면도날 방법을 사용하는 것이 Regularization 입니다.

L1, L2 Regularization

L1, L2 Regularization은 Overfitting을 줄이는 대표적인 방법입니다. 기존의 학습 과정에서는 손실을 최소화하는 방향으로 w들을 변화시켰습니다. Regularization에서는 모델의 복잡도를 함께 최소화 하는 방향으로 w를 변경시킵니다. 손실 함수에서 복잡도를 표현하기 위해 w에 대한 penalty를 추가합니다.

 

자동차의 여러가지 데이터로 연비를 예측하는 모델을 L1, L2 Regularization을 사용하여 Overfitting을 줄이는 예시를 설명드리겠습니다.

< 예시 : 연비 에측 모델 >

자동차에 대한 데이터로 연비를 예측하는 모델
모델 : Linear Regression
특성 : 실린더수, 마력, 무게, 가속력, 연도, 차 이름 등
라벨 : 연비 csv 파일 링크 : https://data.heatonresearch.com/data/t81-558/auto-mpg.csv

학습 후 mean square error와 가중치

L1 (Lasso) Regularization

L1 Regularization, LASSO (Least Absolute Shrinkage and Selection Operator)는 neural network에 "희박(sparsity)"를 만듭니다. weight이 0에 가까우면 해당 연결을 끊습니다. 연결을 끊기 때문에 촘촘했던 뉴럴 네트워크를 희박하게(sparse) 만듭니다.

 

우리가 직접 훈련시켜서 필요없는 feature를 제거할 수도 있지만, 입력 feature가 너무 많으면 힘듭니다. L1 정규화를 이용하면 개발자가 직접하지 않고 자동으로 해당 feature를 없앨 수 있습니다. 이를 feature selection 이라고 합니다.

 

에러 함수

위의 식을 손실함수에 더합니다. 즉, 손실을 계산할 때 w에 대한 정보를 추가한다고 생각하시면 됩니다.

손실 함수 식

위는 손실함수에 w에 대한 정보를 더한 식입니다.

w에 대한 편미분 식

C0​ : Cost function​
n : 훈련 데이터 개수
λ : Regularization 상수​(hyperparameter) (위의 식에선 알파값)

 

위는 w에 대해 편미분한 값입니다. sgn(w)는 항상 1입니다. 따라서 위의 식은 w의 값에 영향을 받지 않고 부호에 따라 λ/n​​(상수) 를 빼는 것입니다. 람다 값은 유저가 선택할 수 있습니다.

 

L1 Regularization을 자동차 연비 예측 모델에 적용해보겠습니다.
케라스에선 아래 코드를 이용하면 쉽게 L1 정규화를 사용할 수 있습니다.

# Create linear regression
regressor = Lasso(random_state=0,alpha=0.1)

# Fit/train LASSO
regressor.fit(x_train,y_train)

# Predict
pred = regressor.predict(x_test)

계수 그래프

Linear Regression을 했을 때와 비교해보면 0에 가까웠던 coef(가중치)는 더 감소한 것을 알 수 있습니다.

L2 (Ridge) Regularization

에러 함수

일반적으로 L2가 L1보다 성능이 더 좋습니다. 에러에 w^2을 사용합니다.

손실 함수 식
w에 대한 편미분 식

# Create linear regression
regressor = Ridge(alpha=1)

Regularization을 통해 w가 작아진다는 것은 local noise가 학습에 영향을 끼치지 않도록 한다는 것입니다. 또한 outlier(특이점)의 영향도 적게 받을 수 있게 된다. 일반화에 적합한 모델을 만들 수 있게 됩니다.

 

sparse model(희박한 모델)을 만들기 위해선 L2보다 L1이 적절합니다. L1은 상수 값을 빼기 때문에 작은 가중치들은 거의 0으로 수렴하여 몇개의 중요한 가중치들만 남게 됩니다. 많은 입력 중 영향도가 낮은 feature를 잘라내야할때 L1을 쓰면 좋습니다.

 

이와 달리 L2는 영향력이 낮은 feature를 유지합니다. 그리고 제곱해서 더하기 때문에 outlier(특이점)에 대하여 L1보다 더 민감합니다.

ElasticNet Regularization

ElasticNet은 L1과 L2를 합친 것입니다.

a * L1 + b * L2

# Create linear regression
regressor = ElasticNet(alpha=0.1, l1_ratio=0.1)

계수 그래프

K-Fold Cross Validation(K-겹 교차 검증)

k겹 교차 검증은 모델을 만들때 다양한 목적으로 쓰입니다.

  • Overfitting(과적합)을 없애기 위해
  • 뉴럴 네트워크에서 표본 집단으로 예측할 때
  • 좋은 횟수의 epoch로 모델을 만들때 (early stopping)
  • 하이퍼 파라미터(활성 함수, 뉴런 갯수, 레이어 갯수)를 검증할 때

데이터셋의 크기가 작을 경우 테스트셋을 어떻게 잡느냐에 따라 모델 평가에 편향이 생기게 됩니다. 이를 해결하기위해 k겹 교차 검증을 이용합니다. 모든 segment들이 training, validation set이 될 수 있는 기회를 갖도록 합니다.

각각의 fold(겹)마다 하나의 모델이 생깁니다. 새로운 데이터가 생겼을 때 model을 학습시키는 방법은 여러가지 방법이 있습니다.

  • 가장 성능이 좋은 모델을 사용하여 새로운 데이터를 학습
  • 새로운 데이터를 5가지 모델을 모두 사용하고 평균을 낸다.(이것이 ensemble learning 입니다.)
  • 새로운 데이터를 dataset에 추가하고 새로운 모델(같은 k fold cross-validation, 같은 히든 레이어 수 사용, 더 많은 epoch)을 학습시킨다.

Regression vs Classification K-Fold Cross-Validation

회귀와 분류 모델은 cross-validation에서 꽤 다릅니다. 회귀가 더 간단합니다. 데이터셋을 KFold object에 분할하고 랜덤하게 나눠주면 됩니다. 모든 fold(겹)이 같은 크기의 데이터를 가질 필요는 없습니다.(항상 같은 크기의 fold를 가지는게 불가능)

 

분류 문제에서도 KFold object를 쓸 수 있지만 KFold는 각각의 Fold가 균형을 이루지 못할 수 있습니다. 예를 들어 binary classification일 경우 각 Fold마다 50%, 50%의 데이터셋이 있어야 좋은 모델을 만들 수 있습니다. 극단적인 예로 하나의 Fold에 100% 같은 라벨이 들어가있다면 training 했을때 이상해지겠죠? 요약하자면 다음과 같습니다.

  • KFold 회귀 문제 때 사용
  • StratifiedKFold 분류 문제 때 사용

Out-of-Sample Regression Predictions with K-Fold Cross-Validation

Out-of-Sample은 모델을 만들 때 사용하지 않은 샘플 데이터를 의미합니다. 아래 예시는 5-fold cross-validation 하는 코드입니다. jh-simple-dataset은 job(직업), area, income(수입), aspect, dist_healthy(건강함), save_rate(저축율), crime(범죄율), age(나이) 등이 있습니다. 나이를 예측하는 회귀 모델을 만들어 보겠습니다.

데이터셋 : https://data.heatonresearch.com/data/t81-558/jh-simple-dataset.csv

# Cross-Validate
kf = KFold(5, shuffle=True, random_state=42) # Use for KFold classification

oos_y = []
oos_pred = []

fold = 0
for train, test in kf.split(x): # 나누어진 fold 들을 반복하면서 학습
    fold+=1
    print(f"Fold #{fold}")
        
    x_train = x[train]
    y_train = y[train]
    x_test = x[test]
    y_test = y[test]
    
    model = Sequential()
    model.add(Dense(20, input_dim=x.shape[1], activation='relu'))
    model.add(Dense(10, activation='relu'))
    model.add(Dense(1))
    model.compile(loss='mean_squared_error', optimizer='adam')
    
    model.fit(x_train,y_train,validation_data=(x_test,y_test),verbose=0,
              epochs=500)
    
    pred = model.predict(x_test)
    
    oos_y.append(y_test)
    oos_pred.append(pred)    

    # Measure this fold's RMSE
    score = np.sqrt(metrics.mean_squared_error(pred,y_test))
    print(f"Fold score (RMSE): {score}")

(중략)

Classification with Stratified K-Fold Cross-Validation

위와 동일한 csv로 product를 분류하는 모델을 만들어 보겠습니다. 분류 모델이기 때문에 StratifiedKFold를 사용합니다.

kf = StratifiedKFold(5, shuffle=True, random_state=42) 
    
oos_y = []
oos_pred = []
fold = 0

# Must specify y StratifiedKFold for
for train, test in kf.split(x,df['product']):  
    fold+=1
    print(f"Fold #{fold}")
        
    x_train = x[train]
    y_train = y[train]
    x_test = x[test]
    y_test = y[test]
    
    model = Sequential()
    model.add(Dense(50, input_dim=x.shape[1], activation='relu')) # Hidden 1
    model.add(Dense(25, activation='relu')) # Hidden 2
    model.add(Dense(y.shape[1],activation='softmax')) # Output
    model.compile(loss='categorical_crossentropy', optimizer='adam')

    model.fit(x_train,y_train,validation_data=(x_test,y_test),verbose=0,epochs=500)
    
    pred = model.predict(x_test)
    
    oos_y.append(y_test)
    # raw probabilities to chosen class (highest probability)
    pred = np.argmax(pred,axis=1) 
    oos_pred.append(pred)  

    # Measure this fold's accuracy
    y_compare = np.argmax(y_test,axis=1) # For accuracy calculation
    score = metrics.accuracy_score(y_compare, pred)
    print(f"Fold score (accuracy): {score}")

Training with both a Cross-Validation and a Holdout Set

모델 선택에 같은 테스트 세트를 반복해서 재사용하면 이는 훈련 세트의 일부가 되는 셈이고 모델이 과적합되는데 원인이 됩니다.

따라서 데이터 양이 충분하다면 holdout을 사용하여 Overfitting을 줄이는 것이 좋습니다. holdout이란 데이터셋에서 의도적으로 학습하지 않는 부분을 의미합니다. 이 holdout은 학습이 끝난 후 모델을 테스트 할 때 사용합니다.

holdout과 kfold cross-validation을 동시에 사용할 수 있습니다.

# Keep a 10% holdout
x_main, x_holdout, y_main, y_holdout = train_test_split(x, y, test_size=0.10)

# Cross-validate
kf = KFold(5)
    
oos_y = []
oos_pred = []
fold = 0
for train, test in kf.split(x_main):        
    fold+=1
    print(f"Fold #{fold}")
        
    x_train = x_main[train]
    y_train = y_main[train]
    x_test = x_main[test]
    y_test = y_main[test]
    
    model = Sequential()
    model.add(Dense(20, input_dim=x.shape[1], activation='relu'))
    model.add(Dense(5, activation='relu'))
    model.add(Dense(1))
    model.compile(loss='mean_squared_error', optimizer='adam')
    
    model.fit(x_train,y_train,validation_data=(x_test,y_test),
              verbose=0,epochs=500)
    
    pred = model.predict(x_test)
    
    oos_y.append(y_test)
    oos_pred.append(pred) 

    # Measure accuracy
    score = np.sqrt(metrics.mean_squared_error(pred,y_test))
    print(f"Fold score (RMSE): {score}")
(중략)

Dropout to decrease overfitting

Hinton, Srivastava, Krizhevsky, Sutskever, & Salakhutdinov (2012) 이 dropout 정규화 알고리즘을 소개하였습니다. [Cite:srivastava2014dropout] Dropout은 L1, L2와 다른 방식이지만 overfitting을 예방합니다. Dropout은 뉴런이나 연결을 지우는 방식입니다. (L1이나 L2처럼 weight penalty를 주지 않습니다.)

 

대부분의 뉴럴 네트워크에서 droput은 분리된 레이어로 구현합니다. 뉴럴 네트워크에 하나의 layer로 추가하면 됩니다. 학습 중에 dropout 레이어는 주기적으로 뉴런의 일부를 삭제 시킵니다. 다만 영구적으로 삭제하진 않습니다. 뉴런의 갯수는 학습을 하더라도 변하지 않습니다. 단순히 뉴런의 일부를 일시적으로 막습니다(mask).

버려지는 뉴런과 연결들은 점선으로 표시되어 있습니다. dropout 레이어에서는 50%의 뉴런이 삭제되어 있습니다.(몇% 삭제할지는 코드에서 직접 지정할 수 있습니다.) 이 뉴런들은 계산하지도 훈련되지도 않습니다. 하지만 마지막 output 레이어에서는 지워진 뉴런도 모두 사용합니다. 이 뉴런들은 일시적으로 mask 한 것이기 때문입니다.

 

다음번 학습 iteration때 dropout 될 뉴런은 바뀝니다. 여기서 50%는 6개 중 3개를 dropout 한다는 것이 아니라 각 뉴런마다 50%의 확률로 dropout 된다는 의미입니다. bias 뉴런은 dropout 하지 않습니다.

 

학습 알고리즘이 어떻게 구현되있는가에 따라 뉴런을 dropout 하는 방법이 달라집니다. 몇몇 뉴럴 네트워크 프레임워크에서는 dropout을 매 iteration(epoch)마다 할 수도 있고, batch마다 할 수도 있습니다. dropout 간격을 설정할 수 있는 프레임워크도 있습니다.

 

dropout이 overfitting을 줄이는 이유는 두 뉴런간의 상호의존성을 줄일 수 있기 때문입니다. 상호 의존이 생긴 뉴런 중 하나의 뉴런을 dropout하면 남은 뉴런은 효과적으로 동작할 수 없습니다. 그 결과, 뉴럴 네트워크는 모든 뉴런에 의존하여 학습할 수 없게 됩니다. 그 결과 현재의 정보를 기억하는 능력을 줄이게되며 일반화 시킬 수 있습니다.

Bootstrapping

Bootstrapping 을 이용하여 dropout을 할 수도 있습니다. Bootstrapping은 매우 대중적인 ensemble 기술입니다. 여러개의 모델을 합쳐 좋은 결과를 내는 방법입니다. 앙상블은 음악에서의 앙상블(여러 악기가 함께 연주 하는것)에서 유래된 말이라고 합니다.

 

Bootstrapping은 많은 수의 뉴럴 네트워크를 이용해 학습합니다. 각각의 뉴럴 네트워크는 weight 초기 값이 다르고, 학습 방법에 따라 학습이 달라지기 때문에 다른 성능을 냅니다. 뉴럴 네트워크들의 앙상블은 이 학습들의 평균이 됩니다. 이 방법 또한 Regularization 되기 때문에 overfitting을 줄일 수 있습니다.

 

Dropout은 bootstrapping과 비슷합니다. Bootstrapping에서의 각각의 뉴럴 네트워크를 dropout 되어 달라진 뉴런들로 생각할 수 있습니다. 반면 Dropout은 bootstrapping과 같은 양의 processing을 요구하지 않습니다. 새로운 뉴럴 네트워크는 일시적으로 생기고 한번의 iteration에만 존재합니다. 또한 앙상블처럼 평균을 하지도 않습니다.

 

Dropout 설명 Animation dropout works

 

Deep Learning Projects

Deep learning experiments by Yusuke Sugomori (@yusugomori).

yusugomori.com

# Generate dummies for job
df = pd.concat([df,pd.get_dummies(df['job'],prefix="job")],axis=1)
df.drop('job', axis=1, inplace=True)

# Generate dummies for area
df = pd.concat([df,pd.get_dummies(df['area'],prefix="area")],axis=1)
df.drop('area', axis=1, inplace=True)

(중략)

Benchmarking by using regularization techniques

Overfitting을 줄이는 방법은 아니지만 정규화 방법을 이용하여 벤치마킹을 할 수 있습니다. 벤치마킹이란 우리가 어떤 hyperparameter를 사용해야 더 좋은 모델을 만들 수 있는지 알아내는 방법입니다.

 

지금까지 hyperparameter 들을 소개했습니다. hyperparameter를 어떻게 설정하느냐에 따라 모델의 accuracy가 달라집니다. hyperparameter는 다음이 있습니다.

  • 뉴럴 네트워크의 레이어 수
  • 각 레이어에 있는 뉴런의 수
  • 각 레이어에서 쓰는 활성 함수
  • 각 레이어에서 Dropout 비율
  • 각 레이어에서 L1, L2 값

우리는 성능 좋은 모델을 만들 수 있는 hyperparameter를 찾아야 합니다. 그래서 여러가지 hyperparameter 들을 테스트하게 됩니다. 하지만 뉴럴 네트워크에서 initial weight가 랜덤이기 때문에 어떤 hyperparameter set가 더 좋은 성능을 내는지 비교하기가 어렵습니다. Bootstrapping은 두개의 hyperparameter set을 비교할 때(벤치마킹 할 때) 유용합니다.

 

Bootstrapping은 cross-validation(교차검증)과 비슷합니다. Bootstrapping은 cycle 횟수 만큼, cross-validation은 folds 갯수만큼 학습합니다. Bootstrapping은 무한정 cycle을 돌릴 수 있는게 다른 점입니다. 또한 Bootstrapping은 cross-validation과 다르게 중복을 허용하여 똑같은 row를 학습 혹은 검증할 수 있습니다.

 

여기서는 bootstrapping을 초매개변수 벤치마킹에 사용합니다. SPLITS 라는 상수 값만큼 뉴럴 네트워크를 학습시킵니다. 두 셋트의 초매개변수로 각각 100번 학습시킨다고 가정해봅시다. 100번의 학습을 끝내면 평균 정확도를 구할 수 있습니다. 두 셋트의 평균 정확도를 비교하면 어떤 초매개변수가 더 적합한지 알 수 있습니다. 추가적으로 early stopping을 사용하면 epoch 평균값을 내어 학습에 적절한 epoch 수도 구할 수 있습니다.

 

참고로 부트스트래핑은 랜덤 샘플링을 통해 학습 데이터를 늘리는 방법으로 쓰입니다. overfitting을 줄일 수 있습니다. 각각의 모델이 overfitting 되어있어도 평균을 내면 서로 상쇄되어 general 한 모델이 되기 때문입니다.

Bootstrapping for Regression

회귀 부트스트래핑은 ShuffleSplit 객체를 사용합니다. 마찬가지로 위에서 사용한 데이터셋을 이용해 나이를 예상하는 모델을 만듭니다.

SPLITS = 50

# Bootstrap
boot = ShuffleSplit(n_splits=SPLITS, test_size=0.1, random_state=42)

# Track progress
mean_benchmark = []
epochs_needed = []
num = 0

# Loop through samples
for train, test in boot.split(x):
    start_time = time.time()
    num+=1

    # Split train and test
    x_train = x[train]
    y_train = y[train]
    x_test = x[test]
    y_test = y[test]

    # Construct neural network
    model = Sequential()
    model.add(Dense(20, input_dim=x_train.shape[1], activation='relu'))
    model.add(Dense(10, activation='relu'))
    model.add(Dense(1))
    model.compile(loss='mean_squared_error', optimizer='adam')
    
    monitor = EarlyStopping(monitor='val_loss', min_delta=1e-3, 
        patience=5, verbose=0, mode='auto', restore_best_weights=True)

    # Train on the bootstrap sample
    model.fit(x_train,y_train,validation_data=(x_test,y_test),
              callbacks=[monitor],verbose=0,epochs=1000)
    epochs = monitor.stopped_epoch
    epochs_needed.append(epochs)
    
    # Predict on the out of boot (validation)
    pred = model.predict(x_test)
  
    # Measure this bootstrap's log loss
    score = np.sqrt(metrics.mean_squared_error(pred,y_test))
    mean_benchmark.append(score)
    m1 = statistics.mean(mean_benchmark)
    m2 = statistics.mean(epochs_needed)
    mdev = statistics.pstdev(mean_benchmark)

Bootstrapping for Classification

분류 문제에서는 StratifiedShuffleSplit 객체를 사용합니다. product 를 예측하는 모델을 만듭니다.

boot = StratifiedShuffleSplit(n_splits=SPLITS, test_size=0.1, random_state=42)
반응형

댓글