[AIB 18기] Section 3 - Sprint 1 - Note 4 - Hyperparameters

ysk1230·2023년 4월 21일
0

하이퍼파라미터 튜닝으로 성능 올리기


개념

딥러닝도 머신러닝의 한 분야이기 때문에 교차 검증(Cross-Validation)을 사용했던 것처럼 신경망도 교차 검증을 사용하여 일반화 성능을 평가할 수 있다.

항상 주의해야 하는 건 '내가 풀고자 하는 문제가 어디에 속하는지?' 를 먼저 생각해야 한다.


교차검증 사용예제

1. 예제 사용

#Keras에서 제공하는 Boston 집값 예측 예제를 수행해보겠습니다.
from tensorflow.keras.datasets import boston_housing
(x_train, y_train), (x_test, y_test) = boston_housing.load_data()

2. 신경망에 교차검증 적용하기

라이브러리 import

from sklearn.model_selection import KFold, StratifiedKfold
import numpy as np
import pandas as pd
import tensorflow as tf
import os

3. KFold를 통해 학습 데이터셋을 몇 개로 나눌지 결정

k = 5로 설정

kf = KFold(n_splits=5)
skf = StratifiedKFold(n_splits = 5, random_state = 42, shuffle = True)
x_train.shape

(404, 13)

y_train[:5]

array([15.2, 42.3, 50. , 21.1, 17.7])

질문

KFoldStratifiedKFold의 차이는 무엇일지 다시 떠올려봅시다.
어떤 경우에 KFold가 아닌 StratifiedKFold를 써주어야 할까요?

정답

KFold와 StratifiedKFold 모두 교차 검증(cross-validation)을 위한 방법입니다.
KFold는 각 폴드에서의 클래스 비율을 고려하지 않으므로, 클래스 간 분포가 불균형한 데이터셋에서는 모델의 성능을 제대로 평가할 수 없을 수 있습니다. 따라서, 이러한 경우에는 StratifiedKFold를 사용하는 것이 더 적합합니다.
StratifiedKFold는 KFold와는 달리 각 폴드에서의 클래스 비율을 고려하여 데이터를 분할합니다. 따라서, 클래스 간 분포가 불균형한 데이터셋에서도 각 폴드에서의 클래스 비율이 전체 데이터셋의 클래스 비율과 유사하도록 분할할 수 있습니다.
즉, 클래스 분포가 균등하다면 KFold를 사용해도 되지만, 클래스 분포가 불균형하거나 작은 샘플 데이터셋이라면 StratifiedKFold를 사용하는 것이 더 적합합니다.

4. 데이터 셋 나누기

training_data = x_train.iloc[train_index]
validation_data = x_train.iloc[val_index]
# 	for train_index, val_index in kf.split(np.zeros(x_train.shape[0]),y_train):
#   training_data = x_train.iloc[train_index]
#   validation_data = x_train.iloc[val_index]

질문

위 코드 실행 시 에러가 발생하는데 그 이유는?
AttributeError Traceback (most recent call last)
in
----> 1 training_data = x_train.iloc[train_index]
2 validation_data = x_train.iloc[val_index]
3
4 # for train_index, val_index in kf.split(np.zeros(x_train.shape[0]),y_train):
5 # training_data = x_train.iloc[train_index]
AttributeError: 'numpy.ndarray' object has no attribute 'iloc'

답변

.iloc은 pandas DataFrame에서 사용되는 인덱싱 방법 중 하나입니다. pandas DataFrame은 열(column)과 행(row)으로 구성되어 있으며, .iloc을 사용하여 행(row)을 선택할 수 있습니다.
반면에, NumPy 배열은 행렬(matrix)의 개념으로 이루어져 있으며, pandas DataFrame과는 다른 데이터 타입이므로, .iloc을 사용할 수 없습니다. NumPy 배열에서는 인덱싱을 위해 다른 방법을 사용해야 합니다.
예를 들어, NumPy 배열에서는 정수 인덱싱(integer indexing)과 슬라이싱(slicing)을 사용하여 데이터를 선택할 수 있습니다. 또한, 부울 인덱싱(boolean indexing)을 사용하여 조건에 맞는 데이터를 선택할 수도 있습니다.
따라서, .iloc은 pandas DataFrame에서만 사용되므로, NumPy 배열에서는 사용할 수 없는 것입니다.

올바른 코드

x_train = pd.DataFrame(x_train)
y_train = pd.DataFrame(y_train)
# .split()은 데이터의 인덱스를 받는 메서드입니다.
# x_train.shape[0](정수)를 그대로 사용할 수 없기 때문에 np.zeros()를 통해 영행렬를 생성 후 인덱스를 받습니다.
for train_index, val_index in kf.split(np.zeros(x_train.shape[0]), y_train):
    training_data = x_train.iloc[train_index]
    validation_data = x_train.iloc[val_index]
    training_y = y_train.iloc[train_index]
    validation_y = y_train.iloc[val_index]

5. 신경망 모델 작성

#Sequential API를 Import합니다.
from tensorflow.keras.models import Sequential
model = Sequential()
# tf.keras.layers에서 dense layer를 호출합니다.
from tensorflow.keras.layers import Dense
model = Sequential()
model.add(Dense(64, activation='relu'))
model.add(Dense(64, activation='relu'))
model.add(Dense(1))
# 손실 함수로 평균 제곱 로그 에러를 사용합니다. 해당 손실 함수를 사용했을 때 모델이 학습하는 과정을 잘 살펴보세요.
model.compile(loss='mean_squared_logarithmic_error',
              optimizer='adam',
              metrics=['accuracy'])  ```
model.fit(training_data, training_y, epochs=2)

6. 신경망 모델에 교차검증 적용

x_train = pd.DataFrame(x_train)
y_train = pd.DataFrame(y_train)
for train_index, val_index in kf.split(np.zeros(x_train.shape[0]),y_train):
  training_data = x_train.iloc[train_index, :]
  training_data_label = y_train.iloc[train_index]
  validation_data = x_train.iloc[val_index, :]
  validation_data_label = y_train.iloc[val_index]
model.fit(training_data, training_data_label, 
        epochs=10, batch_size=64, 
        validation_data=(validation_data, validation_data_label),
        )

모델 적용

model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(training_data, training_data_label,
        epochs=10,
        batch_size=32,
        )

회귀 문제 이므로 MSE를 쓴다

7. 신경망을 테스트

x_train = pd.DataFrame(x_train)
y_train = pd.DataFrame(y_train)

for train_index, val_index in kf.split(np.zeros(x_train.shape[0])):
   training_data = x_train.iloc[train_index, :]
   training_data_label = y_train.iloc[train_index]
   validation_data = x_train.iloc[val_index, :]
   validation_data_label = y_train.iloc[val_index]
   model.compile(loss='mean_squared_error', optimizer='adam')
   model.fit(training_data, training_data_label,
             epochs=10,
             batch_size=32,
             validation_data = (validation_data, validation_data_label),
             )    
   results = model.evaluate(x_test, y_test, batch_size=32)
   print("test loss, test mse:", results)

교차 검증을 통해 신경망 모델 구현 완료


하이퍼 파라미터 튜닝

1. 종류

1). 수작업
100% 수작업(Manual)으로 파라미터를 수정하는 방법입니다.
학계에서 논문을 출간할 수 있을 정도로 놀라운 정확도를 보여주는 하이퍼파라미터의 수치를 찾아내기 위해 쓰는 방법이죠.

이를 위해서 실험자의 경험이나 도메인 지식이 필요하기도 합니다.

2). Grid Search
1번 방식을 자동화한 방법이 바로 "Grid Search"입니다.
이 방법에서는 하이퍼파라미터마다 탐색할 지점을 정해주면 모든 지점에 해당하는 조합을 알아서 수행합니다.

Grid Search는 학습을 실행한 뒤 한참 놀다오면 되는 매우 편한 방법이지만 장점만 있는 것은 아닙니다. 범위를 너무 많이 설정하면 '좀 놀다 오면 끝나는' 수준을 넘어 '수료하고 취직을 하고 나서도 끝나지 않을 수도' 있는데요.
만약 5개의 파라미터에 대해 각각 5개의 지점을 지정해주면 Grid Search는 총 55=3,1255^5=3,125 번의 모델 학습을 진행하게 됩니다.
여기에 5번의 교차 검증까지 진행한다면 모델은 3,125×5=15,6253,125 \times 5 = 15,625 번이나 학습을 수행합니다.
모델 한 번 학습에 10분만 걸린다고 쳐도 3달 반이 걸리는 무시무시한 작업입니다. 실제로 이런 일은 없어야겠죠?

그렇기 때문에 Grid Search 로 너무 많은 하이퍼파라미터 조합을 찾으려고 하지 않는 것이 좋습니다.
1개, 혹은 최대 2개 정도의 파라미터 최적값을 찾는 용도로 적합합니다.
굳이 많은 하이퍼파라미터 조합을 시도할 필요는 없습니다.
모델 성능에 보다 직접적인 영향을 주는 하이퍼파라미터가 따로 있기 때문인데요.

이러한 파라미터만 제대로 튜닝해서 최적값을 찾은 후 나머지 하이퍼파라미터도 조정해나가면 못해도 90% 이상의 성능을 확보할 수 있습니다. 이런 식으로 하나씩 접근하다 보면 적어도 무한루프가 발생하는 위험은 줄일 수 있습니다.

3). Random Search
"Random Search" 는 무한 루프라는 Grid Search의 단점을 해결하기 위해 나온 방법입니다.
Random Search 는 지정된 범위 내에서 무작위로 모델을 돌려본 후 최고 성능의 모델을 반환합니다.
시도 횟수를 정해줄 수 있기 때문에 Grid Search 에 비해서 훨씬 적은 횟수로도 끝마칠 수 있겠죠?

Grid Search 에서는 파라미터의 중요도가 모두 동등하다고 가정합니다.
하지만 위에서 알아본 것처럼 실제로 더 중요한 하이퍼파라미터가 있는데요.
Random Search 는 상대적으로 중요한 하이퍼파라미터에 대해서는 탐색을 더 하고, 덜 중요한 하이퍼파라미터에 대해서는 실험을 덜 하도록 합니다.

Random Search 는 절대적으로 완벽한 하이퍼파라미터를 찾아주지는 않는다는 단점을 가지고 있는데요.
하지만 Grid Search와 비교했을 때, 학습에 걸리는 시간이 훨씬 더 적다는 점으로도 Random Search의 의의를 찾을 수 있습니다.

4). Bayesian Methods
"Baby sitting" 이나 "Grid Search" 등의 방식에서는 탐색 결과를 보고, 결과 정보를 다시 새로운 탐색에 반영하면 성능을 더 높일 수 있었습니다.
베이지안 방식(Bayesian Method) 은 이렇게 이전 탐색 결과 정보를 새로운 탐색에 활용하는 방법입니다.
그렇기 때문에 베이지안 방법을 사용하면 하이퍼파라미터 탐색 효율을 높일 수 있습니다.
bayes_opthyperopt와 같은 패키지를 사용하면 베이지안 방식을 적용할 수 있습니다.

2. 튜닝 가능한 파라미터

  • 배치 크기(batch_size)
  • 에포크(epochs)
  • 옵티마이저(optimizers)
  • 학습률(learning rate)
  • 활성화 함수(activation)
  • Regularization(weight decay, Dropout 등)
  • 은닉층(Hidden layer)의 노드(Node) 수

3. GridSearch를 사용한 최적의 배치 사이즈 탐색

1. 필요한 패키지를 import 합니다.

!pip install scikeras
인공 신경망 모델을 Scikit-learn에서 사용하기 위해 wrapping을 해주어야합니다.
wrapping하는 방법으로 scikeras를 사용해보겠습니다.
scikeras는 Colab 내에 내장되어 있는 패키지가 아니기 때문에 따로 설치하고 import 하겠습니다.

import numpy
import pandas as pd
from sklearn.model_selection import GridSearchCV
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from scikeras.wrappers import KerasClassifier # scikeras.wrappers 안의 KerasClassifier를 호출합니다.

2. 재현성을 위해 랜덤시드를 고정합니다

numpy.random.seed(42)

3. 데이터셋을 불러온 후에 Feature 와 Label로 분리합니다.

url ="https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv"
dataset = pd.read_csv(url, header=None).values
X = dataset[:,0:8]
Y = dataset[:,8]

4. 모델을 제작합니다.

추후 KerasClassifier 로 Wrapping 하기 위하여 신경망 모델을 함수 형태로 정의합니다.
❗최초 노드의 개수를 정해주어야 정상 작동합니다.

  def create_model(nodes=8):
  '''
  KerasClassifier를 사용하기 위해 모델을 함수의 형태로 선업합니다.
  model 변수를 Sequential로 선언 후 앞선 노트에서 사용한 방법으로 층을 추가하여 신경망을 구축 후 compile합니다.
  이후 model을 반환하도록 작성합니다.
  '''
  model = Sequential()
  model.add(Dense(nodes, input_dim=8, activation='relu'))
  model.add(Dense(nodes, activation='relu'))
  model.add(Dense(1, activation='sigmoid'))
  model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
  return model

5. KerasClassifier 로 wrapping 하여줍니다.

# KerasClssifier를 통해 신경망을 wrapping합니다.
# model 인자로 앞서 함수로 선언한 신경망을 설정하고 batch_size도 설정해줄 수 있습니다.
model = KerasClassifier(model=create_model, batch_size=8)

6. 하이퍼파라미터 탐색을 위한 탐색 범위를 설정한 후 GridSearchCV 를 지정하여 학습합니다.

# 튜닝하고 싶은 하이퍼파라미터의 범위를 list의 형태로 선언합니다.
# 선언한 리스트를 이용해 dictionary를 만듭니다. key는 모델 내 인자와 동일하게 선언합니다.
nodes = [16, 32, 64]
batch_size = [16, 32, 64]
param_grid = dict(model__nodes=nodes, batch_size=batch_size) 

# GridSearchCV를 통해 서치를 수행합니다.
# estimator : wrapping한 모델을 넣어줍니다.
# param_grid : dictionary로 만든 튜닝할 하이퍼파라미터와 범위를 넣어줍니다.
# n_jobs : 병렬 작업의 개수를 설정합니다. -1로 설정할 경우 모든 프로세스를 사용합니다.
# cv : 교차 검증을 실행을 위해 나눌 개수를 설정합니다.
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=1, cv=3)
grid_result = grid.fit(X, Y)

7. 최적의 결과를 낸 하이퍼파라미터와 각각의 결과를 출력

# 아래와 같이 튜닝을 검증할 다양한 지표를 출력해볼 수 있습니다.
# .best_score_ : 최고 성능의 점수를 나타냅니다. 서치하는 하이퍼파라미터 단일 조합 교차 검증의 평균을 나타냅니다.
# .best_params_ : 최고 성능의 하이퍼파라미터의 조합을 나타냅니다.
# f-string은 출력문에 변수를 사용하고 싶을 때 사용합니다. 문자열 맨 앞에 f를 작성하고 문자열 내부에 사용하고자 하는 변수를 {} 안에 작성하면 됩니다.

print(f"Best: {grid_result.best_score_} using {grid_result.best_params_}")
means = grid_result.cv_results_['mean_test_score'] # .cv_results_['mean_test_score'] : 테스트 데이터의 가중 평균
stds = grid_result.cv_results_['std_test_score'] # .cv_results_['std_test_score'] : 테스트 데이터에 대한 분산 점수
params = grid_result.cv_results_['params'] # .cv_results_['params'] : 하아퍼파라미터 조합을 출력합니다.
for mean, stdev, param in zip(means, stds, params):
 
 print(f"Means: {mean}, Stdev: {stdev} with: {param}") 

라이브러리를 사용한 하이퍼파라미터 튜닝

Keras Tuner를 사용하여 하이퍼파라미터 탐색

1. 필요한 패키지를 import

from tensorflow import keras
from tensorflow.keras.layers import Dense, Flatten
import tensorflow as tf
import IPython

2. Keras Tuner를 설치한 후 import 합니다.

!pip install -U keras-tuner
import keras_tuner as kt

3. 데이터셋을 불러온 후에 정규화(Normalizing) 해줍니다.

(X_train, y_train), (X_test, y_test) = keras.datasets.fashion_mnist.load_data()
X_train = X_train.astype('float32') / 255.0
X_test = X_test.astype('float32') / 255.0

4. Model을 제작합니다.

  • 탐색할 하이퍼파라미터와 범위
    - 은닉층의 노드 수 : 32 부터 512 까지 32개씩 증가시키며 탐색
    - 학습률(Learning rate) : 0.01, 0.001, 0.0001 의 3개 지점을 탐색
    def model_builder(hp):
    '''
    Tuner를 지정하기 전에 앞선 Grid Search와 마찬가지로 함수의 형태로 신경망 모델을 구축 후 선언합니다.
    마찬가지로 model을 Sequential()로 선언 후 층을 추가해 신경망을 구축한 뒤 model을 반환합니다.
    함수 내 변수 hp를 통해 다양한 하이퍼파라미터 범위를 설정합니다.
    '''
    model = keras.Sequential()
    model.add(Flatten(input_shape=(28, 28)))
    # hp.Int() : 아래 dense layer 내 units에 들어올 integer의 형태로 하이퍼파라미터의 범위를 생성합니다. 32 ~ 512 범위에서 32단계마다 범위를 설정합니다.
    hp_units = hp.Int('units', min_value = 32, max_value = 512, step = 32)
     model.add(Dense(units = hp_units, activation = 'relu'))
    model.add(Dense(10, activation='softmax'))
    #hp.Choice() : 아래 optimizer 내 learning_rate 인자에 list values 중 하나를 선택하는 하이퍼파라미터의 범위를 생성합니다.
    hp_learning_rate = hp.Choice('learning_rate', values = [1e-2, 1e-3, 1e-4]) 
    model.compile(optimizer = keras.optimizers.Adam(learning_rate = hp_learning_rate),
                  loss = keras.losses.SparseCategoricalCrossentropy(), 
                  metrics = ['accuracy'])
    return model

5. 하이퍼파라미터 튜닝을 수행할 튜너(Tuner)를 지정합니다.

Keras Tuner 에서는 Random Search, Bayesian Optimization, Hyperband 등의 최적화 방법을 수행할 수 있습니다.
아래에서는 Hyperband를 통해서 튜닝을 수행해보도록 하겠습니다.

Hyperband 사용 시 Model builder function(model_builder), 훈련할 최대 epochs 수(max_epochs) 등을 지정해주어야 합니다.
Hyperband 는 리소스를 알아서 조절하고 조기 종료(Early-stopping) 기능을 사용하여 높은 성능을 보이는 조합을 신속하게 통합한다는 장점을 가지고 있습니다.

tuner = kt.Hyperband(model_builder,
                   objective = 'val_accuracy', # 학습 진행 지표를 문자열로 받습니다.
                   max_epochs = 10, # 학습을 진행할 최대 epoch 수입니다.
                   factor = 3, # iteration, epoch 등을 감소시킬 계수이며 default=3입니다.
                   directory = 'my_dir', # 튜닝 결과를 저장할 directory를 설정합니다.
                   project_name = 'intro_to_kt') # 저장하는 튜닝 결과의 이름입니다.

6. Callback 함수를 지정합니다.

# 하이퍼파라미터를 튜닝하는 과정이 모두 출력되면 내용이 매우 길어지기 때문에 이를 지워주는 코드입니다.
# 아래 내용은 간단히 보고 넘어가셔도 좋습니다.
class ClearTrainingOutput(tf.keras.callbacks.Callback):
def on_train_end(*args, **kwargs):
  IPython.display.clear_output(wait = True)

7.하이퍼파라미터 탐색을 수행합니다.

❗️ 아래 코드를 통해 하이퍼파라미터 탐색을 수행하려면 약 20분의 시간이 필요합니다.
충분한 시간 여유를 가지고 수행해주세요.

tuner.search(X_train, y_train, epochs = 10, validation_data = (X_test, y_test), callbacks = [ClearTrainingOutput()])
best_hps = tuner.get_best_hyperparameters(num_trials = 1)[0] # 반환할 최고의 조합을 선언합니다. num_trials = 1은 개체 수를 의미합니다.
print(f"""
하이퍼 파라미터 검색이 완료되었습니다. 
최적화된 첫 번째 Dense 노드 수는 {best_hps.get('units')} 입니다.
최적의 학습 속도는 {best_hps.get('learning_rate')} 입니다.
""")
model = tuner.hypermodel.build(best_hps) # 최고 성능의 조합으로 모델을 다시 구축합니다.
model.summary()

model.fit(X_train, y_train, epochs = 10, validation_data = (X_test, y_test))

0개의 댓글