Done is Better Than Perfect

[딥러닝] 5. 딥러닝 모델 학습의 문제점 pt.2 : 기울기 소실, 가중치 초기화 방법 본문

공부/딥러닝

[딥러닝] 5. 딥러닝 모델 학습의 문제점 pt.2 : 기울기 소실, 가중치 초기화 방법

jimingee 2024. 6. 12. 17:45

딥러닝 모델 학습의 문제점으로 아래의 4가지가 있다.

1. 학습 속도 문제와 최적화 알고리즘

2. 기울기 소실 문제

3. 초기값 설정 문제

4. 과적합 문제

 

이번 장에서는 2. 기울기 소실 문제, 3. 가중치 초기화 설정 문제와 이를 해결하기 위한 기법에 대해 자세히 알아보도록 하겠다.


 

2. 기울기 소실 문제와 방지 기법

 

기울기 소실 (Vanishing Gradient) 

  • 발생 원인 : 기울기가 0인 값을 전달하며 중간 전달값이 사라지는 문제
    • 기울기가 소실되는 문제가 반복되며 학습이 잘 이루어지지 않음 
    • 깊은 층의 모델에서 역전파 시에 전달되는 손실 함수(loss function)의 gradient 값에 활성화 함수인 sigmoid 함수의 0에 가까운 기울기 값이 계속해서 곱해지면서 결국 가중치 업데이트가 잘 안되는 문제
  • 해결 방법 :
    • ReLU : 기존에 활성화 함수로 사용하던 sigmoid 함수 대신 ReLU 함수를 사용하여 해결
    • Tanh : 내부 hidden layer에는 ReLU를 적용하고, output layer에서만 Tanh 적용
  • ReLU가 sigmoid 보다 기울기 소실 문제에 강한 이유
    • sigmoid 함수는 입력값이 매우 크거나 작다면 기울기도 0에 가까워짐
    • ReLU 함수는 양수 입력 값에 대해 일정한 기울기를 갖고 있으므로 기울기 소실 문제를 방지할 수 있음

 

 

 

[ hidden layer의 activation function이 sigmoid인 모델 VS relu인 모델  비교]

import tensorflow as tf
import logging, os
logging.disable(logging.WARNING)

''' 1. hidden layer의 활성화 함수가 `relu`인 10층 이상의 모델 '''
def make_model_relu():
    
    model_relu = tf.keras.models.Sequential([
        tf.keras.layers.Flatten(input_shape=(28, 28)),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(32, activation='relu'),
        tf.keras.layers.Dense(10, activation='softmax')
    ])
    
    return model_relu
    
''' 2. hidden layer의 활성화 함수가 `sigmoid`인 10층 이상의 모델 '''
def make_model_sig():
    
    model_sig = tf.keras.models.Sequential([
        tf.keras.layers.Flatten(input_shape=(28, 28)),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(32, activation='sigmoid'),
        tf.keras.layers.Dense(10, activation='softmax')
    ])
    
    return model_sig

''' 3. 두 모델을 불러온 후 학습시키고 테스트 데이터에 대해 평가 '''
def main():
   
    # MNIST 데이터를 불러오고 전처리
    mnist = tf.keras.datasets.mnist
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train, x_test = x_train / 255.0, x_test / 255.0
    
    model_relu = make_model_relu()  # hidden layer들의 활성화 함수로 relu를 쓰는 모델
    model_sig = make_model_sig()   # hidden layer들의 활성화 함수로 sigmoid를 쓰는 모델
    
    # 모델 최적화
    model_relu.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model_sig.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    
    model_relu.summary()
    model_sig.summary()
    
    # 모델 학습
    model_relu_history = model_relu.fit(x_train, y_train, epochs=5, verbose=0)
    print('\n')
    model_sig_history = model_sig.fit(x_train, y_train, epochs=5, verbose=0)
    
    # 모델 평가
    scores_relu = model_relu.evaluate(x_test, y_test)
    scores_sig = model_sig.evaluate(x_test, y_test)
    
    print('\naccuracy_relu: ', scores_relu[-1])
    print('accuracy_sig: ', scores_sig[-1])
    
    return model_relu_history, model_sig_history

if __name__ == "__main__":
    main()

 

[ 코드 실행 결과 ]

  • hidden layer의 활성화 함수로 ReLU를 선택한 모델의 정확도가 더 높음.
  • 따라서, 기울기 소실 문제에서 ReLU 함수를 활성화 함수로 선택하는 것이 더욱 정확도가 높음
### output ###
accuracy_relu:  0.9632
accuracy_sig:  0.7123

 

 

 

 

3. 초기값 설정 문제와 방지 기법

 

가중치 초기화 (weight initialization)

  • 활성화 함수의 입력 값이 너무 커지거나 작아지지 않게 만들어주는 것이 핵심 (기울기 소실 방지)
  • 초기화 설정 문제 해결 방법 :
    • 표준 정규분포를 이용한 초기화 (분산을 줄이기 위해 표준편차를 0.01로 하는 정규분포로 초기화)
    • Xavier 초기화 방법 + sigmoid 함수 : 표준 정규 분포를 입력 개수의 제곱근으로 나누어 줌. sigmoid와 같은 S자 함수의 경우 출력 값들이 정규 분포 형태를 가져야 안정적으로 학습 가능
    • Xavier 초기화 방법 + ReLU 함수 : ReLU 함수에는 Xavier 초기화가 부적합. 레이어를 거쳐갈수록 값이 0에 수렴
    • He 초기화 방법 : 정규분포를 입력 개수 절반의 제곱근으로 나누어. 10층 레이어에서도 평균과 표준편차가 0으로 수렴하지 않음.
  • 적절한 가중치 초기화 방법 :
    • Sigmoid, tanh의 경우, Xavier 초기화 방법이 효율적.
    • ReLU계의 활성화 함수 사용 시,  He 초기화 방법이 효율적
    • 최근 대부분의 모델에서는 He 초기화를 주로 선택

 

[ 1. 표준 정규 분포를 이용한 가중치 초기화 VS 표준편차를 0.01로 하는 정규분포로 가중치 초기화 ]

import numpy as np
from visual import *
np.random.seed(100)

def sigmoid(x):
    result = 1 / (1 + np.exp(-x))
    return result

def main():
    # 100개의 노드를 가진 모델에 들어갈 1000개의 입력 데이터
    x_1 = np.random.randn(1000,100) 
    x_2 = np.random.randn(1000,100) 
    
    node_num = 100
    hidden_layer_size = 5
    
    activations_1 = {}
    activations_2 = {}
    
    for i in range(hidden_layer_size):
        if i != 0:
            x_1 = activations_1[i-1]
            x_2 = activations_2[i-1]
        
        # 가중치 정의
        w_1 = np.random.randn(100,100)*1 + 0 # 표준 정규 분포 - N(0,1)
        w_2 = np.random.randn(100,100)*0.01 + 0 # 표준편차가 0.01인 정규분포 - N(0,0.01)
        
        a_1 = np.dot(x_1, w_1)
        a_2 = np.dot(x_2, w_2)
        
        ## sigmoid 통과
        z_1 = sigmoid(a_1)
        z_2 = sigmoid(a_2)
        
        activations_1[i] = z_1
        activations_2[i] = z_2
        
    Visual(activations_1,activations_2)
    
    return activations_1, activations_2

if __name__ == "__main__":
    main()

 

[ 코드 실행 결과 - activation 결과의 분포도 ]

  • 표준 정규 분포로 가중치를 초기화한 모델 -> activation 결과값이 0, 1 값으로 몰림
  • 표준편차 0.01인 정규분포로 가중치를 초기화한 모델 -> activation 결과값이 0.5 주변으로 몰림

정규분포의 sigmoid 결과 분포도 (왼쪽- 표준 정규 분포 / 오른쪽- 표준편차가 0.01인 정규 분포

  • activation 값이 양극단(0또는 1)으로 몰리는 현상은 좋지 않음
    • 활성화 함수(예, sigmoid 함수)의 기울기가 0에 수렴 -> 학습이 잘 이루어 지지 않음

 

 

[ 2. Xavier 방법을 이용한 가중치 초기화 - 활성화 함수(sigmoid & relu)와 결합했을 때 비교]

  • Xavier 초기화 방법은 앞 레이어의 노드가 n개일 때 표준 편차가 $ \frac{1}{ \sqrt{n}}$ 인 분포를 사용하는 것
  • Xavier 초기화 방법을 사용하면 앞 레이어의 노드가 많을수록 다음 레이어의 노드의 초깃값으로 사용하는 가중치가 좁게 퍼짐.
import numpy as np
from visual import *
np.random.seed(100)

def sigmoid(x):
    result = 1 / (1 + np.exp(-x))
    return result

def relu(x):
    result = np.maximum(0,x)
    return result


def main():
    # 100개의 노드를 가진 모델에 들어갈 1000개의 입력 데이터
    x_sig = np.random.randn(1000,100)
    x_relu = np.random.randn(1000,100)
    
    node_num = 100
    hidden_layer_size = 5
    
    activations_sig = {}
    activations_relu = {}
    
    for i in range(hidden_layer_size):
        if i != 0:
            x_sig = activations_sig[i-1]
            x_relu = activations_relu[i-1]
        
        # Xavier 가중치 초기화 - 표준 편차가 1/root(n)인 정규분포
        w_sig = np.random.randn(100,100)*(1/np.sqrt(node_num))+0 
        w_relu = np.random.randn(100,100)*(1/np.sqrt(node_num))+0 
        
        a_sig = np.dot(x_sig, w_sig)
        a_relu = np.dot(x_relu, w_relu)
        
        z_sig = sigmoid(a_sig) # sigmoid 활성화 함수 이용
        z_relu = relu(a_relu) # relu 활성화 함수 이용
        
        activations_sig[i] = z_sig
        activations_relu[i] = z_relu
        
    Visual(activations_sig, activations_relu)
    
    return activations_sig, activations_relu

if __name__ == "__main__":
    main()

 

 

[ 코드 실행 결과 - activation 결과의 분포도 ]

  • (왼쪽 : sigmoid + Xavier 초기화) activation 결과 값이 어느 한쪽으로 몰리지 않고 고르게 분포됨을 확인할 수 있음
  • (오른쪽 : ReLU + Xavier 초기화) activation 결과 값이 한쪽(0)으로 몰림 -> ReLU 함수에는 Xavier 초기화가 부적합

Xavier 가중치 초기화 (왼쪽 - sigmoid 활성화 함수 / 오른쪽 - relu 활성화 함수)

 

 

 

[ 3. He 방법을 이용한 가중치 초기화 ]

  • He 초기화 방법 : 활성화 함수로 ReLU를 쓸 때 활성화 결괏값들이 한쪽으로 치우치는 문제를 해결하기 위해 나온 방법
  • 앞 레이어의 노드가 n개일 때 표준 편차가 $ \frac{\sqrt{2}}{\sqrt{n}}$인 분포를 사용하는 것
  • Xavier 초기화 방법은 표준 편차가 $ \frac{1}{\sqrt{n}}$.
    • ReLU는 음의 영역에 대한 함숫값이 0이라서 더 넓게 분포시키기 위해 $ \sqrt{2} $배의 계수가 필요하다고 이해할 수 있음.
import numpy as np
from visual import *
np.random.seed(100)
    
def relu(x):
    result = np.maximum(0,x)
    return result

def main():
    # 100개의 노드를 가진 모델에 들어갈 1000개의 입력 데이터 - 표준정규분포 따름
    x_relu = np.random.randn(1000,100)
    
    node_num = 100
    hidden_layer_size = 5
    
    activations_relu = {}
    
    for i in range(hidden_layer_size):
        if i != 0:
            x_relu = activations_relu[i-1]
            
        # He 가중치 초기화 - 표준 편차가 root(2)/root(n)인 정규분포
        w_relu = np.random.randn(100,100)*np.sqrt(2/node_num)+0
        
        a_relu = np.dot(x_relu,w_relu)
        
        z_relu = relu(a_relu)
        
        activations_relu[i] = z_relu
        
    Visual(activations_relu)
    
    return activations_relu    

if __name__ == "__main__":
    main()

 

 

 

[ 코드 실행 결과 - activation 결과의 분포도 ]

  • 앞선 'ReLU + Xavier가중치 초기화 방법'보다 'ReLU + He 가중치 초기화 방법'이 activation 결과가 고르게 분포되어 있음

왼쪽 - ReLU+Xavier 가중치 초기화 방법 / 오른쪽 - ReLU+He  가중치 초기화 방법

 

Comments