220706_TIL / (신경망에서의) Hyperparameter Tuning

신두다·2022년 7월 6일
0

TIL

목록 보기
53/82

Key words

하이퍼 파라미터, 교차검증, Grid Search, Keras Tuner

NOTE
오늘 자주 나온 개념인 교차검증 / GridSearchCV 등은 섹션2 머신러닝 배울 때 자세히 다루었던 것이기 때문에 여기에 굳이 내용을 반복하여 옮겨두지는 않을 것이다. 필요시 아래 두 글을 참고할 것.

오늘은 신경망에서 최적의 하이퍼 파라미터를 어떻게 효율적으로 찾을 수 있는지 그 방법 및 구현에 대해서 주로 배웠는데, Keras를 통해 어떻게 다양한 하이퍼 파라미터 튜닝을 할 수 있는지 코드 위주로 기록해두려고 한다.

1. 딥러닝에서의 하이퍼 파라미터 튜닝

  • 딥러닝도 머신러닝의 범주에 속하기 때문에 하이퍼 파라미터 세팅이 매우 매우 중요하다.
  • 머신러닝은 딥러닝에 비해 상대적으로 세팅해야할 하이퍼 파라미터가 적지만, 신경망은 layer를 여러 개 만들수록, 각각의 layer마다 조정할 수 있는 하이퍼 파라미터가 엄청나게 쌓이게 되기 때문에 좋은 파라미터를 찾기가 상대적으로 더 어려울 수 밖에 없다.
  • 튜닝 가능한 파라미터는 다음과 같다.
    • 배치 크기(batch_size)
    • 에포크(epochs)
    • 옵티마이저(optimizers)
    • 학습률(learning rate)
    • 활성화 함수(activation)
    • Regularization(weight decay, Dropout 등)
    • 은닉층(Hidden layer)의 노드(Node) 수
      물론 이 밖에도 더 많지만 우선 이 정도만 기억해도 된다고 함.

참고) 파라미터 vs 하이퍼 파라미터

  • 하이퍼 파라미터란 연구자가 지정하는 파라미터를 말한다고 알고는 있었는데, 오늘 막상 그럼 파라미터는 뭔가요?라는 질문을 받았을 때 뭔가 턱 막혔다. 그래서 들은 내용을 다시 정리해둔다.
  • 간단하게 설명하는 버전
    • 파라미터라고 하면 일반적으로 우리가 학습(가중치와 편향 업데이트)을 진행을 하면서 값이 변하는 것들을 파라미터라고 부른다.
    • 하이퍼 파라미터는 학습 전에 세팅을 딱 해두고! 너 학습할 때는 값이 변하지 마! 라고 하는 것들.
    • 대표적인 하이퍼 파라미터는 학습률이 있다. (학습률을 초기에 세팅하니까.)
      • (여기서 잠시 내가 헷갈렸던 것 어제 했던 학습률 감소와 계획법인데, 그건 학습 중간에 바뀌는 건 아니지! 여러 번 돌리면서 바뀌는 거지.)
  • 조금 더 딥한 버전
    • Adam을 딥하게 공부하다보면 탄성/탄력성/관성 같은 단어를 볼 수 있을 거다. 지난 번에 옵티마이저 뭘 쓸지 잘 모르겠으면 Adam을 쓰라고 했었는데, 아담의 대표적인 특징은 초기에 학습률을 정해주면 그 값이 처음부터 쭉 가지가 않고 조금씩 변하는 것이라고 한다. 이러한 성질을 가지고 있기 때문에 탄성/관성 등의 단어가 언급되는 것이라고 함.
    • 어라 그러면 이건 하이퍼 파라미터가 아닌거 아니야?
    • 그래서 ‘일반적으로'라는 말을 깔아두는 거다. 일반적으로 학습률은 변하지 않는게 맞다.
    • 아담은 나중에 공부할 기회가 있을지는 모르겠다.

자 이제 오늘 본 코드로 바로 넘어가보자~


2. 실습한 것 (중요!!!!!!)

참고로 오늘 실습은 전처리를 제외하고는 거의 구현이 되어있는 걸 직접 돌려보며 코드의 구조를 이해한다는 목적이 강했다. 나중에 잘 가져다가 쓰기 위해 기록해둔다는 의미로 생각하자!

진행 방식 참고
1. 데이터를 다운로드 받고 읽어옴(load)
2. 데이터 클리닝을 진행 (필수는 아니지만 추천)
3. Keras MLP model을 만들고, 학습 진행
4. Hyperparameter 튜닝 진행:

  • batch_size
  • training epochs
  • optimizer
  • learning rate (optimizer에 따라서 해당되면)
  • momentum (optimizer에 따라서 해당되면)
  • activation functions
  • network weight initialization
  • dropout regularization
  • number of neurons in the hidden layer

데이터 불러오기 과정은 생략. 통신사 고객 이탈 예측 문제였다.


[전처리]

  • 아 오늘 오랜만에 전처리 하니까 헷갈리는게 너무 많았다ㅠㅠ

  • 코랩에서 셀 쪼개서 한 것들도 모두 한 코드 묶음으로 모아두겠음.

# 결측치 확인
df.isna().sum() # => 결측치 없음.sum()함수는 기본적으로 True의 개수를 알려줌.

# 이탈을 예측하는데 중요한 'TotalCharges' 칼럼의 타입이 숫자가 아니라 문자형이었다. 
# 데이터 타입 확인
df.info() # => target 'TotalCharges' Dtype == object.

# TotalCharges 숫자형으로 변환

'''
df['TotalCharges'] = pd.to_numeric(df['TotalCharges']) 와 같이 그냥 일괄 처리를 시도하면 'Unable to parse string " " at position 488' 에러가 나는데,
그래서 df['TotalCharges'][488]을 보면 " " 으로 공백으로 구성된 값도 있다는 걸 알 수 있다. 이것도 포함하여 적절한 처리가 필요하다. (참고로 astype()도 에러가 난다.)
'''

# apply 함수로 적용하기
df['TotalCharges'] = df['TotalCharges'].apply(pd.to_numeric, errors = 'coerce') #errors 파라미터 : 숫자 형태의 문자가 아닌 경우 NaN으로 변경함. 이외에 ignore(무시하고 지나감), raise(오류 발생)가 있음. 
  
df.info() # => 결과를 보면 우리 레이블의 Non-null Count가 다른 칼럼에 비해 7032로 11개 빠진 걸 알 수 있는데 이걸로 결측치가 몇 개구나 추측도 바로 가능함.

# TotalCharges 결측치 확인
print('1) NaN 갯수 = ', df['TotalCharges'].isnull().sum()) # 잘 기억해두기.

# 결측치 드랍
df = df.dropna(subset = ['TotalCharges'])

# 결측치 재확인
print('2) NaN 갯수 = ', df['TotalCharges'].isnull().sum())

----

# customerID 드랍
df = df.drop(columns='customerID')

# 'No internet service' 로 표현되어 있는 것을 'No'로 변경
no_internet_feats = [ 'TechSupport','OnlineBackup', 'DeviceProtection','StreamingTV',
                 'OnlineSecurity','StreamingMovies']

for i in no_internet_feats:
    df[i] = df[i].replace({'No internet service':'No'})

# 'No phone service' 로 표현되어 있는 것을 'No'로 변경
df['MultipleLines']=df['MultipleLines'].replace({'No phone service':'No'})

# 0으로 표기된 것을 'No'로, 1로 표기된 것을 'Yes'로 변경
df['SeniorCitizen']=df['SeniorCitizen'].replace({0:'No',
                                                 1:'Yes'})

## => 인코딩 제대로 해주기 위해 한 과정으로 이해함.

# 오디널 인코딩
from category_encoders import OrdinalEncoder

encoder = OrdinalEncoder()
df_encoded = encoder.fit_transform(df)

# 타겟을 0과 1로 바꿔주기
df_encoded['Churn'] = df_encoded['Churn'].replace({1: 0, 2: 1})
df_encoded['Churn'].value_counts()

----

# 훈련, 테스트 셋을 나누기
from sklearn.model_selection import train_test_split

train, test = train_test_split(df_encoded, test_size = 0.25, random_state = 1) # => df가 아니라 df_encoded인 것 유의.

train.shape, test.shape # => ((5274, 20), (1758, 20))

# features 와 target 을 분리
target = 'Churn'
features = df_encoded.drop(columns=[target]).columns

X_train = train[features]
X_test = test[features]

y_train = train[target]
y_test = test[target]

# 정규화
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

[기본 모델 만들기]

#필요한 라이브러리 import

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten
import keras
import tensorflow as tf
import IPython
from tensorflow.keras.wrappers.scikit_learn import KerasClassifier
from sklearn.model_selection import GridSearchCV

#!pip install -U keras-tuner
import kerastuner as kt

----
# Class의 개수 확인
np.unique(y_train) # => array([0, 1])

----

# 간단한 모델 만들어서 성능을 보기 !
tf.random.set_seed(7)

model2 = Sequential()
model2.add(Dense(64, activation='relu'))
model2.add(Dense(64, activation='relu'))
model2.add(Dense(1, activation='sigmoid')) # 바이너리 분류이므로 노드수 1, 활성화 함수로는 시그모이드(sigmoid)

model2.compile(optimizer='adam', 
              loss='binary_crossentropy', 
              metrics=['accuracy'])

results = model2.fit(X_train_scaled, y_train, epochs=10, validation_data=(X_test_scaled,y_test))


# 테스트셋 사용해서 결과 보기
model2.evaluate(X_test_scaled,  y_test, verbose=2) 

[GridSearchCV 사용]

# 모델 만들기
tf.random.set_seed(7)

def model_builder(nodes=16, activation='relu'):

  model = Sequential()
  model.add(Dense(nodes, activation=activation))
  model.add(Dense(nodes, activation=activation))
  model.add(Dense(1, activation='sigmoid')) # 이진분류니까 노드수 1, 활성함수로는 시그모이드

  model.compile(optimizer='adam', 
                loss='binary_crossentropy', 
                metrics=['accuracy'])

  return model

# keras.wrapper를 활용하여 분류기를 만듭니다
model = KerasClassifier(build_fn=model_builder, verbose=0)

# GridSearch
batch_size = [64, 128, 256]
epochs = [10, 20, 30]
nodes = [64, 128, 256]
activation = ['relu', 'sigmoid']
param_grid = dict(batch_size=batch_size, epochs=epochs, nodes=nodes, activation=activation)


# GridSearch CV를 만들기
grid = GridSearchCV(estimator=model, param_grid=param_grid, cv=3, verbose=1, n_jobs=-1)
grid_result = grid.fit(X_train_scaled, y_train)

----

# 최적의 결과값을 낸 파라미터를 출력합니다
print(f"Best: {grid_result.best_score_} using {grid_result.best_params_}")
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print(f"Means: {mean}, Stdev: {stdev} with: {param}")


[Keras Tuner 사용]

  • Keras Tuner는 케라스 프레임워크에서 하이퍼 파라미터를 튜닝하는데 도움이 되는 라이브러리이다.
  • 아래 tuner 지정할 때 hyperband를 사용했는데, 이건 쉽게 말해 초기에 몇 번 탐색을 해서 좋은 방향이 나오면 그쪽으로 튜닝을 계속 해주는 방법이다.
    • Hyperband 사용 시 Model builder function(model_builder), 훈련할 최대 epochs 수(max_epochs) 등을 지정해주어야 한다.
    • Hyperband 는 리소스를 알아서 조절하고 조기 종료(Early-stopping) 기능을 사용하여 높은 성능을 보이는 조합을 신속하게 통합한다는 장점을 가지고 있다.
    • hyperband 외에 Random Search, Bayesian Optimizaion 등을 사용할 수 있다.
# 모델 만들기

def model_builder(hp):

  model = Sequential()

  # Dense layer에서 노드 수를 조정(32-512)
  hp_units = hp.Int('units', min_value = 32, max_value = 512, step = 32) # 32부터 512까지 32개씩 증가시키며 탐색한다는 의미임.

  model.add(Dense(units = hp_units, activation='relu'))
  model.add(Dense(units = hp_units, activation='relu'))

  model.add(Dense(1, activation='sigmoid')) # 이진분류니까 노드수 1, 활성함수로는 시그모이드

  # Optimizer의 학습률(learning rate)을 조정[0.01, 0.001, 0.0001]합니다. 
  hp_learning_rate = hp.Choice('learning_rate', values = [1e-2, 1e-3, 1e-4])

  # 컴파일 단계, 옵티마이저와 손실함수, 측정지표를 연결해서 계산 그래프를 구성함
  model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate = hp_learning_rate), 
                loss=keras.losses.BinaryCrossentropy(from_logits = True), 
                metrics=['accuracy'])

  return model
  
 ----
 
 # 튜너를 인스턴스화하고 하이퍼 튜닝을 수행

tuner = kt.Hyperband(model_builder,
                     objective = 'val_accuracy', 
                     max_epochs = 30, 
                     factor = 3,
                     directory = 'my_dir',
                     project_name = 'intro_to_kt')

'''
AttributeError: module 'keras.optimizers' has no attribute 'Adam'
=> keras.optimizers.Adam > tf.keras.optimizers.Adam로 수정.
'''

# callback 정의 : 하이퍼파라미터 탐색을 실행하기 전에 학습이 끝날 때마다 이전 출력이 지워지도록 하는 걸 말한다. 코드를 직접 돌려보면 무슨 말인지 쉽게 알 수 있다.

class ClearTrainingOutput(tf.keras.callbacks.Callback):
  def on_train_end(*args, **kwargs):
    IPython.display.clear_output(wait = True)
    
 tuner.search(X_train_scaled, y_train, epochs = 30, batch_size=50, validation_data = (X_test_scaled,y_test), callbacks = [ClearTrainingOutput()])

best_hps = tuner.get_best_hyperparameters(num_trials = 1)[0]

print(f"""
최적화된 Dense 노드 수 : {best_hps.get('units')} 
최적화된 Learning Rate : {best_hps.get('learning_rate')} 
""")

----

from tensorflow.keras import regularizers

tf.random.set_seed(1442)
initializer = tf.keras.initializers.HeNormal()

model = Sequential()

model.add(Dense(best_hps.get('units'), 
                activation='relu', kernel_initializer=initializer,          
                kernel_regularizer=regularizers.l2(0.01),    # L2 norm regularization
                activity_regularizer=regularizers.l1(0.01))) # L1 norm regularization))
model.add(Dense(best_hps.get('units'),
                activation='relu', kernel_initializer=initializer,            
                kernel_regularizer=regularizers.l2(0.01),    # L2 norm regularization
                activity_regularizer=regularizers.l1(0.01)))
model.add(Dense(1, activation='sigmoid')) # 이진분류니까 노드수 1, 활성함수로는 시그모이드

model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate = best_hps.get('learning_rate')), 
              loss='binary_crossentropy', 
              metrics=['accuracy'])

results = model.fit(X_train_scaled, y_train, epochs=2, batch_size=50, validation_data=(X_test_scaled,y_test))

# 테스트셋 사용해서 결과 보기
model.evaluate(X_test_scaled,  y_test, verbose=2) #기본모델보다 오히려 떨어졌네?!
  • 여기서 헷갈릴만한게 있는데 마지막 부분에 최고성능을 보이는 하이퍼 파라미터 조합으로 다시 학습을 진행해 그 예측 결과를 확인해본 것이라는 것이라는 점 참고할 것. 그 위의 best_hps를 통해서는 최적의 하이퍼 파라미터만 확인할 수 있는거다.
  • 오늘 노트에서는 아래와 같이 간단히 하기도 했었다.
model = tuner.hypermodel.build(best_hps)

#학습 진행
model.fit(X_train, y_train, epochs = 10, validation_data = (img_test, label_test))

3. 그 외


Feeling


  • 오늘은 이론보다는 주어진 코드를 이해하는데 더 많은 시간을 썼다. 예전에 배운 개념들이 다시 많이 등장해 개념적으로 이해하는 것이 많이 어렵진 않았다. (기억 안나는 부분이 보일 때마다 다시 내용을 찾아보며 조금 당황스러울 뿐이었다ㅎ)
  • 오늘 이해한 코드를 내가 '체득'하기 위해서는 많이 써보는 수 밖에 없겠구나 하는 생각이 든다. 이 모든 코드를 내가 지금 당장 다 외울 수도 없고 무조건 그럴 필요도 있나 싶기도 하고. 익숙해지려고 노력하자.
  • 벌써 딥러닝 4일차가 지났고 내일은 스챌이다. 시간 빠르다. 더 열심히 해야지..!
profile
B2B SaaS 회사에서 Data Analyst로 일하고 있습니다.

0개의 댓글