Go together

If you want to go fast, go alone. If you want to go far, go together.

핸즈온 머신러닝 2판

11장 심층 신경망 훈련하기

NowChan 2021. 12. 30. 20:21

고해상도 이미지에서 수백 종류의 물채를 감지하는 복잡한 문제는 수백 개의 뉴런으로 구성된 10개 층 이상의 층을 가진 신경망으로 훈련해야할 것입니다. 심층 신경망을 훈련하는 것은 쉬운 일이 아닙니다. 훈련 중에 다음과 같은 문제를 마주할 수 있습니다.

  • 그레디언트 소실, 그레디언트 폭주: 심층 신경망 아래쪽으로 갈 수록 그레디언트가 점점 더 작아지거나 커지는 현상입니다.
  • 훈련 데이터가 충분하지 않거나, 레이블을 만드는 비용이 많이 들 수 있습니다.
  • 훈련이 극단적으로 느려질 수 있습니다.
  • 수백만개의 파라미터를 가진 모델은 훈련 세트에 과적합될 위험이 큽니다. 훈련 샘플이 적거나 잡음이 많은 경우 그렇습니다.

 

그레디언트 소실과 폭주 문제

심층 신경망을 훈련할 때 아래쪽으로 갈 수록 그레디언트가 점점 더 작아지거나 커지는 현상입니다. 그레디언트가 작아서 경사 하강법이 하위층의 연결 가중치를 변경되지 않은 채로 두거나 너무 크게 학습시켜 발산하게 된다면, 훈련은 좋은 솔루션으로 수렴하지 못하게 됩니다.

 

2010년 전까지는 로지스틱 함수와 가중치 초기화 방법(평균 0, 표준편차 1인 정규분포)의 조합 때문이었습니다. 이 활성화 함수와 초기화 방식을 사용했을 때, 각 층에서 출력의 분산의 입력의 분산보다 크다는 것을 밝혔습니다. 신경망의 위쪽으로 갈 수록 분산이 계속 커져 가장 높은 층에서는 활성화 함수가 0이나 1로 수렴합니다. 이는 로지스틱 함수가 항상 양수를 출력해(평균이 0.5) 출력의 가중치 합이 입력보다 커질 가능성이 높기 때문입니다.(편향 이동)

로지스틱 함수

입력이 (양수나 음수로) 커지면 로지스틱 활성화 함수값이 0이나 1로 수렴하며 기울기가 0에 매우 가까워지는 것을 알 수 있습니다. 그래서 역전파가 될 때 신경망으로 전파할 그레디언트가 거의 없고 그마저도 최상위층에서부터 역전파가 진행되면서 점점 약해져서 아래층에는 아무 것도 도달하지 않게 됩니다.

 

 

글로럿과 He 초기화

 

예측할 때 정방향으로, 그레디언트를 역전파할 때 역방향으로 양방향 신호가 적절히 흘러야하고 신호가 죽거나 폭주, 소멸하지 않아야합니다. 이는 각 층의 출력에 대한 분산이 입력에 대한 분산과 같아야 합니다. 그리고 역방향에서 층을 통과하기 전과 후의 그레디언트 분산이 동일해야합니다. 사실 층의 입력과 출력 연결 개수(fan-in, fan-out)가 같지 않다면, 이 두 가지를 보장할 수 없습니다. 

 

하지만, 실전에서 매우 잘 작동하는 대안이 있습니다. 각 층의 연결 가중치를 아래의 식으로 무작위로 초기화하는 것입니다. 이는 Xavier initialization 또는 Glorot initialization이라고 합니다.

$$평균이\ 0이고\ 분산이\ δ^2=\frac{1}{fan_{avg}}인\ 정규\ 분포\\$$

$$fan_{avg}=\frac{\left(fan_{in}+fan_{out}\right)}{2}$$

$$또는\ r=\sqrt{\frac{3}{fan_{avg}}}일\ 때\ -r과\ +r\ 사이의\ 균등\ 분포$$

 

르쿤 초기화는 fan_in = fan_out인 glorot initialization과 동일합니다. initialization은 가중치의 초깃값을 설정하는 전략입니다.

 

다른 활성화 함수에 대해서도 비슷한 전략들을 사용하는데, 이 전략들은 분산의 스케일링, fan_avg, fan_in을 쓰는 것만 다릅니다.

초기화 전략 활성화 함수 δ²(정규 분포)
Glorot None, tanh, logistic, softmax 1/fan_avg
He ReLU와 그 변종들 2/fan_in
르쿤 SELU 1/fan_in

 

케라스는 기본적으로 균등분포의 Glorot initialization을 사용합니다. 층을 만들 때 kernel_initializer='he_uniform'이나 'he_normal'로 바꾸어 He 초기화를 사용할 수 있습니다.

keras.layers.Dense(10, activation='relu', kernel_initializer='he_normal')

fan_in 대신 fan_avg 기반의 균등분포 He 초기화를 사용하고 싶다면 VarianceScaling을 사용할 수 있습니다.

he_avg_init = keras.initializers.VarianceScaling(scale=2, mode='fan_avg',
						distribution='uniform')
keras.layers.Dense(10, activation='sigmoid', kernel_initializer=he_avg_init)
더보기

VarianceScaling(scale=1.0, mode='fan_in', distribution='truncated_normal'), distribution='truncated_normal'(절단 정규분포)인 경우 δ²=1.3xscale/mode로 계산됩니다. 

distribution='untruncated_normal')(정규분포)인 경우 δ²=scale/mode로 계산됩니다.

 

kernel_initializer 매개변수 비교

glorot_uniform = VarianceScaling(scale=1.0, mode='fan_avg', distribution='uniform')

he_normal = VarianceScaling(scale=2., mode='fan_in', distribution='truncated_normal')

lecun_normal = VarianceScaling()

 

 

수렴하지 않는 활성화 함수

 

활성화 함수를 잘못 선택하면 그레디언트 소실이나 폭주로 이어질 수 있습니다. 과거에는 생물학적 뉴런의 방식과 비슷한 시그모이드 활성화 함수가 최선일 것이라 생각했습니다. 하지만, 다른 활성화 함수가 심층 신경망에서 더 잘 작동한다는 사실이 밝혀졌습니다. 특히 ReLU는 특정 양숫값에 수렴하지 않는다는 큰 장점이 있습니다.(계산도 빠릅니다.)

 

ReLU는 dying ReLU라는 문제가 알려졌습니다. 훈련하는 동안 일부 뉴런이 0 이외의 값을 출력하지 않는다는 의미에서 죽었다고 말합니다. 훈련 세트의 모든 샘플에 대해 입력의 가중치 합이 음수가 되면 뉴런이 죽게 됩니다. 가중치 합이 음수면 ReLU의 그레디언트가 0이 되어 경사 하강법이 작동하지 않습니다.

더보기

첫 번째 은닉층이 아니라면 이따금 죽은 뉴런이 되살아날 수 있습니다. 경사 하강법이 이전 층의 뉴런을 바꾸어 죽은 뉴런의 입력에 대한 가중치 합이 다시 양수가 될 수 있습니다.

이 문제를 해결하기 위해 LeakyReLU를 사용합니다. α값은 ReLU의 음수 부분의 기울기로 사용됩니다. 이 작은 기울기가 뉴런이 절대 죽지 않게 만들어 주고, 항상 ReLU보다 좋은 성능을 냅니다. 보통 α=0.01을 사용합니다. α=0.2에서 더 나은 성능을 내는 것으로 보입니다. RReLU, PReLU 등의 함수도 있습니다.

LeakyReLU

ELU(exponential linear unit)라는 새로운 활성화 함수는 다른 ReLU 변종의 성능을 앞질렀습니다. 훈련 시간도 줄고 신경망 테스트 세트 성능도 더 높았습니다.

 $$ELU_{\alpha }\left(z\right)=\begin{cases}\alpha \cdot \left(e^z-1\right),\ \ \ z<0\\z,\ \ \ z\ge 0\end{cases}$$

ELU

z<0일 때 음수값이 들어와 평균 출력이 0에 더 가까워져서 그레디언트 소실 문제를 완화시켜줍니다. 또한 z<0인 부분의 그레디언트가 0이 아니므로 죽은 뉴런을 만들지 않습니다. α=1이면 z=0에서 그레디언트가 급변하지 않아 경사하강법의 속도를 높여줍니다. (파라미터로 설정 가능)α=1이 아니면 z=0에서 불연속적이 됩니다. 

 

ELU의 단점은 지수 함수를 사용해 계산이 느리다는 것입니다. 훈련 시에는 수렴 속도가 빨라 느린 계산이 상쇄되지만, 테스트 시에는 ELU를 사용한 네트워크가 ReLU를 사용한 네트워크보다 느릴 것입니다.

 

SELU(Scaled ELU)

SELU는 스케일이 조정된 ELU 활성화 함수의 변종입니다. 저자들은 완전 연결층만 쌓고 모든 은닉층이 SELU 활성화 함수를 사용한다면 네트워크가 자기 정규화(self-normalize)된다는 것을 보였습니다. 훈련하는 동안 각 층의 출력이 평균 0, 표준 편차 1을 유지하는 경향이 있습니다. 이는 그레디언트 소실, 폭주를 막아줍니다.

 

SELU는 아주 깊은 네트워크에서 종종 다른 활성화 함수보다 뛰어난 성능을 내지만, 자기 정규화가 일어나기 위한 몇가지 조건이 있습니다.

  • 입력 특성이 반드시 표준화(평균 0, 표준편차 1)되어야 합니다.
  • 모든 은닉층 가중치는 르쿤 정규분포로 초기화 되어야 합니다. kernel_initializer='lecun_normal'
  • 네트워크가 순환 신경망, 스킵 연결(skip connection)과 같은 순차적이지 않은 구조에서는 자기 정규화를 보장할 수 없습니다.

 

이 논문은 모두 완전 연결층일 때만 자기 정규화가 보장된다고 했습니다. 합성곱 신경망에서도 SELU 활성화 함수가 성능을 향상시킬 수 있다고 말합니다.

더보기

심층 신경망은 은닉층에서 자주 쓰이는 활성화 함수는 SELU > ELU > LeakyReLU(그리고 변종들) > ReLU > tanh > logistic 순입니다. 

 

네트워크가 자기 정규화가 되지 못하는 구조라면 ELU가 SELU보다 성능이 나을 수 있습니다.(SELU가 z=0에서 연속적이지 않기 때문)

 

실행 속도가 중요하다면 LeakyReLU를 선택할 수 있습니다. 하이퍼파라미터를 추가하고 싶지 않다면 케라스에서 사용하는 기본값 α=0.3를 사용합니다.

 

신경망이 과적합되었다면 RReLU, 훈련 세트가 아주 크다면 PReLU를 다른 활성화 함수가 비교해 성능이 좋은 활성화 함수를 선택해볼 수 있습니다.

 

ReLU가 가장 널리 사용되는 활성화 함수이므로 많은 라이브러리, 하드웨어 가속기들은 ReLU에 특화, 최적화되어 있습니다. 속도가 중요하다면 ReLU가 가장 좋은 선택입니다.

 

LeakyReLU 활성화 함수를 사용하려면 LeakyReLU 층을 만들고 모델에서 적용하려는 층 뒤에 추가합니다.

model = keras.models.Sequential([
 ...
 keras.layers.Dense(10, kernel_initializer='he_normal'),
 keras.layers.LeakyReLU(alpha=0.2),
 ...
])

PReLU를 사용하려면 LeakyReLU(alpha=0.2)를 PReLU()로 바꿉니다. RReLU는 아직 케라스에 공식적인 구현이 없습니다.

 

SELU 활성화 함수를 사용하려면 층을 만들 때 activation='selu'와 kernel_initializer='lecun_normal'로 지정합니다.

layer = keras.layers.Dense(10, activation='selu',
		kernel_initializer='lecun_normal')

 

배치 정규화

ELU와 He 초기화를 사용하면 그레디언트 소실, 폭주 문제를 크게 감소시킬 수 있지만, 훈련하는 동안 다시 발생하지 않으리란 보장은 없습니다. 이 때 배치 정규화(BN) 기법을 사용하면 그레디언트 소실, 폭주 문제를 해결할 수 있습니다.

 

이 기법은 각 층에서 활성화 함수를 통과하기 전이나 후에 모델에 연산을 하나 추가합니다. 각 층에서 두 개의 새로운 파라미터로 입력 데이터를 원점에 맞추고 정규화시킵니다. 파라미터 중 하나는 스케일 조정에, 다른 하나는 이동에 사용합니다.

 

입력 데이터를 원점에 맞추고 정규화하려면 평균과 표준편차를 추정해야 합니다. 이를 위해 현재 미니배치에서 입력의 평균과 표준편차를 평가합니다.

$$\mu _B=\frac{1}{m_B}\sum _{i=1}^{m_B}X^{\left(i\right)}$$

$$δ_B^2=\frac{1}{m_B}\sum _{i=1}^{m_B}\left(X^{\left(i\right)}-\mu _B\right)^2$$

$$\hat{X}^{\left(i\right)}=\frac{X^{\left(i\right)}-\mu _B}{\sqrt{δ_B^2+ε}}$$

$$Z^{\left(i\right)}=\gamma \otimes \hat{X}^{\left(i\right)}+\beta $$

 

  • μ_B는 미니배치 B에 대해 평가한 입력의 평균 벡터(입력마다 하나의 평균을 가집니다.)
  • δ_B도 미니배치 B에 대해 평가한 입력의 표준 편차 벡터입니다.
  • γ는 층의 출력 스케일 파라미터 벡터입니다.(입력마다 즉, 층의 뉴런마다 하나의 γ를 가집니다.)
  • β는 층의 출력 이동(오프셋) 파라미터 벡터입니다.(입력마다 즉, 층의 뉴런마다 하나의 β를 가집니다.) 각 입력은 해당 파라미터만큼 이동합니다.
  • ε는 분모가 0이 되는 걸 막기 위한 smoothing term입니다.

 

훈련하는 동안 배치 정규화는 입력을 정규화한 다음 스케일을 조정하고 이동시킵니다. 테스트 시에는 샘플의 배치가 아니라 샘플 하나에 대한 예측을 만들어야합니다. 이 경우 입력의 평균, 표준편차를 계산할 방법이 없습니다. 테스트 데이터가 비교적 적기 때문에,  샘플 배치를 사용한다 하더라도 매우 작거나 independent identically distributed(IID) 조건을 만족하지 못할 수 있습니다. 이런 경우 신뢰도가 떨어집니다.

 

한 가지 방법은 훈련이 끝난 후 '최종' 입력 평균과 표준편차를 대신 사용하는 것입니다. 예측 시에 배치 입력 평균과 표준편차로 이 '최종' 입력 평균과 표준편차를 사용하는 것입니다.

 

케라스의 BatchNormalization 층은 배치 정규화를 자동으로 수행합니다. 배치 정규화 구현은 층의 입력과 표준 편차에 이동 평균을 적용해 훈련하는 동안 최종 통계를 추정합니다. 정리하자면 배치 정규화 층마다 4개의 파라미터 벡터가 학습됩니다. γ(출력 스케일 벡터)와 β(출력 이동 벡터)는 일반적인 역전파를 통해 학습됩니다. μ(최종 입력 평균 벡터)와 δ(최종 입력 표준편차 벡터)는 지수 이동 평균을 사용하여 추정됩니다. 

 

배치 정규화의 장점:

  1. 그레디언트 소실 문제가 크게 감소해 tanh, logistic 같은 수렴성을 가진 활성화 함수 사용 가능
  2. 가중치 초기화에 네트워크가 덜 민감
  3. 큰 학습률을 사용해 학습 과정의 속도를 높일 수 있음
  4. 규제와 같은 역할을 한다.
더보기

배치 정규화는 전체 데이터셋이 아니고 미니배치마다 평균과 표준편차를 계산하므로 훈련 데이터에 일종의 잡음을 넣는다고 볼 수 있습니다. 이런 잡음은 훈련 세트에 과대적합되는 것을 방지하는 규제 효과를 가지며, 미니배치의 크기가 클 수록 효과는 줄어듭니다. 하지만, 배치 정규화로 인한 규제는 크지 않으므로 드롭아웃과 함께 사용하는게 좋습니다.

 

배치 정규화의 단점:

  1. 모델의 복잡도를 키운다. 층마다 추가되는 계산이 신경망 예측을 느리게 한다.

 

배치 정규화의 단점은 훈련이 끝난 후 이전 층과 배치 정규화 층을 합쳐 실행 속도 저하를 피할 수 있습니다. 즉, 아래와 같은 식으로 배치 정규화 전 층의 W, b를 수정하면 배치 정규화층을 제거할 수 있습니다.

더보기

배치 정규화를 사용하면 에포크마다 더 많은 시간이 걸리지만, 수렴이 훨씬 빨라져서 보통 상쇄됩니다. 오히려 실제 걸리는 시간은 보통 더 짧습니다.

$$XW+b\ \overrightarrow{배치\ 정규화}\ \gamma \otimes \frac{\left(XW+b-\mu \right)}{δ}+\beta $$

$$XW+b\ \overrightarrow{배치\ 정규화}\ XW'+b'$$

$$W'=\gamma \otimes \frac{W}{δ},\ b'=\gamma \otimes \frac{b-\mu }{δ}+\beta $$

 

 

케라스로 배치 정규화 구현하기

model = keras.models.Sequential([
      keras.layers.Flatten(input_shape=[28, 28]),
      keras.layers.BatchNormalization(),
      keras.layers.Dense(300, activation='elu', kernel_initializer='he_normal'),
      keras.layers.BatchNormalization(),
      keras.layers.Dense(100, activation='elu', kernel_initializer='he_normal'),
      keras.layers.BatchNormalization(),
      keras.layers.Dense(10, activation='softmax')
])
model.summary()
'''
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten (Flatten)           (None, 784)               0         
                                                                 
 batch_normalization (BatchN  (None, 784)              3136      
 ormalization)                                                   
                                                                 
 dense (Dense)               (None, 300)               235500    
                                                                 
 batch_normalization_1 (Batc  (None, 300)              1200      
 hNormalization)                                                 
                                                                 
 dense_1 (Dense)             (None, 100)               30100     
                                                                 
 batch_normalization_2 (Batc  (None, 100)              400       
 hNormalization)                                                 
                                                                 
 dense_2 (Dense)             (None, 10)                1010      
                                                                 
=================================================================
Total params: 271,346
Trainable params: 268,978
Non-trainable params: 2,368
_________________________________________________________________
'''

배치 정규화 층은 γ, β, μ, δ 총 4개의 파라미터가 있어서 예를 들어, batch_1의 경우 입력 300*4 = 1,200개의 파라미터가 생성됩니다. μ, δ 는 이동 평균으로 구한 값들입니다. 이는 역전파로 학습되지 않기 때문에 non-trainable 파라미터로 분류합니다.

더보기

 μ, δ 는 훈련하는 동안 훈련 데이터를 기반으로 추정되므로 학습된다고 볼 수도 있습니다. 케라스에서 'Non-trainable'은 역전파로 업데이트 되지 않는다는 의미입니다.

[(var.name, var.trainable) for var in model.layers[1].variables]
'''
[('batch_normalization/gamma:0', True),
 ('batch_normalization/beta:0', True),
 ('batch_normalization/moving_mean:0', False),
 ('batch_normalization/moving_variance:0', False)]
'''

두 개는 역전파로 훈련되고 두 개는 훈련되지 않습니다.

 

케라스에서 배치 정규화 층을 만들 때, 훈련하는 동안 매 반복마다 케라스에서 호출될 두 개의 연산이 함께 생성됩니다.  이 연산들이 이동 평균을 업데이트 합니다. 텐서플로 백엔드를 사용하므로 이 연산은 텐서플로 연산입니다.

 

배치 정규화층을 활성화 함수 이전, 이후에 추가한 후 어떤 것이 주어진 데이터셋에 잘 맞는지 확인하는 것이 좋습니다. 활성화 함수 이전에 배치 정규화층을 사용하려면 은닉층의 activation을 지정하지 않습니다. 또한 배치 정규화층은 층마다 이동 파라미터를 포함하기 때문에 편향을 뺄 수 있습니다.(use_bias=False)

model=keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.BatchNormalization(),
    keras.layers.Dense(300, kernel_initializer='he_normal', use_bias=False),
    keras.layers.BatchNormalization(),
    keras.layers.Activation('elu'),
    keras.layers.Dense(100, kernel_initializer='he_normal', use_bias=False),
    keras.layers.BatchNormalization(),
    keras.layers.Activation('elu'),
    keras.layers.Dense(10, activation='softmax')
])

 

BatchNormalization 클래스는 기본값이 잘 동작하지만, momentum, axis의 하이퍼파라미터 변수를 튜닝할 수 있습니다.

BatchNormalization 층은 지수 이동 평균을 업데이트할 때 momentum을 사용합니다. 새로운 V(현재 배치에서 계산한 새로운 입력 평균 벡터나 표준 편차 벡터)가 주어지면 다음 식으로 이동 평균 v_hat을 업데이트합니다.

$$\hat{V}\gets \hat{V}\cdot momentum+V\cdot \left(1-momentum\right)$$

적절한 momentum값은 1에 가까우며, 데이터셋이 크고 미니배치가 작으면 소수점 뒤에 9를 더 넣어 1에 가깝게 합니다.

 

axis는 정규화할 축을 결정합니다. default는 -1(다른 축을 따라 계산한 평균과 표준편차를 사용하여) 마지막 축을 정규화합니다.

입력 배치가 2D(배치 크기가 [샘플 개수, 특성 개수])이면 각 특성 마다 평균, 표준 편차를 모든 샘플에 대해 구하고, 이를 기반으로 정규화합니다.

 

가령 Flatten으로 1D화된 입력에 대해 첫 번째 배치 정규화층은 784개의 특성마다 독립적으로 정규화(그리고 스케일 조정과 이동)될 것입니다.

 

배치 정규화 층은 훈련 도중에는 배치 통계를 사용하고 훈련이 끝난 후에는 '최종' 통계를 사용합니다.(즉, 이동 평균의 마지막값) 이를 처리하는 클래스 코드를 봅시다.

class BatchNormalization(keras.layers.Layer):
  ...
  def call(self, inputs, training=None):
    ...

call() 메서드에서 실제 계산을 수행하는데, training의 default는 None입니다. fit() 메서드는 훈련하는 동안 이를 1로 설정합니다. 사용자 정의 층(12장)에서 training 매개변수를 조작해 훈련/테스트에서 다르게 동작하는 BatchNormalization을 만들 것입니다.

 

BatchNormalization은 심층 신경망에서 매우 널리 사용해 보통 모든 층 뒤에 배치 정규화가 있다고 가정하므로 종종 신경망 그림에서 빠져있습니다.

 

하지만, 최근 Fixup 가중치 초기화 기법을 사용해 매우 깊은 심층 신경망을 훈련한 사례가 있습니다. 이는 매우 최근 연구라서 이를 뒷받침할만한 추가 연구가 더 나온 후 사용하는 것이 좋을 것입니다.

 

그레디언트 클리핑

그레디언트 폭주를 완화하는 다른 방법에는 역전파될 때 일정 임곗값을 넘어서지 못하게 그레디언트를 잘라내는 것입니다. 순환 신경망은 배치 정규화를 적용하기 어려워 gradient cliping을 많이 사용합니다.

optimizer = tf.keras.optimizers.SGD(clipvalue=1.0)
model.compile(loss='mse', optimizer=optimizer)

gradient cliping을 구현하려면 clipvalue, clipnorm 매개변수를 지정하면 됩니다. clipvalue는 gradient의 모든 원소를 ±지정값으로 잘라냅니다. 가령 clipvalue=1.0이고, [0.9, 100.0]이 gradient vector라면, [0.9, 1.0]으로 잘라냅니다.

 

clipnorm은 gradient의 원소 중 ι₂노름보다 큰 값이 있다면 gradient 전체를 클리핑합니다. 가령, clipnorm=1.0이고, [0.9, 100.0]이 [0.00899964, 0.9999595]로 클리핑되므로 방향을 그대로 유지합니다.

 

훈련하는 동안 gradient가 폭주한다면 clipvalue, clipnorm을 모두 사용하여 클리핑할 수 있습니다. 두 가지 모두 지정될 경우 clipnorm이 먼저 적용됩니다.

 

 

사전 훈련된 층 재사용하기

아주 큰 규모의 DNN을 처음부터 새로 훈련하면 매우 오랜 시간이 걸립니다. 해결하려는 것과 비슷한 유형의 문제를 처리한 신경망이 있는지 찾아본 다음, 그 신경망의 하위층을 재사용하는 것이 좋습니다. 이를 전이 학습(transfer learning)이라고 합니다. 이 방법은 훈련 속도를 크게 높이고 필요한 훈련 데이터도 크게 줄여줍니다.

 

동물, 식물, 자동차, 생활용품을 포함한 카테고리 100개로 구분된 이미지를 분류하도록 훈련한 DNN이 있고, 구체적인 자동차의 종류를 분류하는 DNN을 훈련하려 할 때, 두 DNN은 작업에 비슷한 점도 많고 일부 겹치기도 하므로 첫 번째 신경망의 일부를 재사용해봐야 합니다.

더보기

만약 원래 문제에서 사용한 것과 크기가 다른 이미지를 입력으로 사용한다면 원본 모델에 맞는 크기로 변경하는 전처리 단계를 추가해야 합니다. 일반적으로 전이 학습은 저수준 특성이 비슷한 입력에서 잘 작동합니다.

원본 모델의 상위 은닉층은 하위 은닉층보다 덜 유용합니다. 작업이 비슷할 수록 낮은 층부터 더 많은 층을 재사용합니다. 아주 비슷한 작업이라면 출력층만 교체합니다.

 

먼저 재사용하는 층을 모두 동결합니다.(경사하강법으로 훈련되지 않는 가중치로 만듭니다.) 그 다음 맨 위에 있는 한 두개의 은닉층의 동결을 해제하고 역전파를 통해 가중치를 조정하여 성능이 향상되는지 확인합니다. 훈련 데이터가 많을 수록 많은 층의 동결을 해제할 수 있고, 재사용 층의 동결을 해제할 때는 학습률을 줄이는 것이 좋습니다. 동결 해제한 층의 가중치를 세밀하게 튜닝할 수 있기 때문입니다.

 

케라스를 사용한 전이 학습

model_A = keras.models.load_model('my_model_A.h5')
model_B_on_A = keras.models.Sequential(model_A.layers[:-1])
model_B_on_A.add(keras.layers.Dense(1, activation='sigmoid'))

model_A와 model_B_on_A는 일부 층을 공유하므로 model_B_on_A를 훈련할 때 model_A도 영향을 받습니다. 이를 원하지 않는다면 model_A를 클론하세요.(clone_model() 메서드는 가중치를 복제하지 않습니다.)

model_A_clone = keras.models.clone_model(model_A)
model_A_clone.set_weights(model_A.get_weights())

이제 작업 B를 위해 model_B_on_A를 훈련할 수 있는데, 새로운 출력층의 가중치가 랜덤하게 초기화되어 있어 처음 몇 번의 에포크 동안 큰 오차를 일으킬 것인데, 이 때 처음 몇 번의 에포크 동안 재사용된 층을 동결하고, 새로운 층에게 적절한 가중치를 학습할 시간을 주는 것입니다. 동결 및 동결 해제한 후 다시 컴파일해야 합니다. compile() 메서드가 모델에서 훈련될 가중치를 모으기 때문입니다.

for layer in model_B_on_A.layers[:-1]:
  layer.trainable=False

model_B_on_A.compile(loss='binary_crossentropy', optimizer='sgd',
                     metrics=['accuracy'])
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=4,
                           validation_data=(X_valid_B, y_valid_B))

for layer in model_B_on_A.layers[:-1]:
  layer.trainable = True

optimizer = tf.keras.optimizers.SGD(lr=1e-4) # 기본 학습률은 1e-2
model_B_on_A.compile(loss='binary_crossentropy', optimizer=optimizer,
                     metrics=['accuracy'])
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=16,
                           validation_data = (X_valid_B, y_valid_B))
'''
Epoch 1/4
7/7 [==============================] - 1s 48ms/step - loss: 0.6049 - accuracy: 0.6950 - val_loss: 0.3978 - val_accuracy: 0.8256
Epoch 2/4
7/7 [==============================] - 0s 17ms/step - loss: 0.3199 - accuracy: 0.8850 - val_loss: 0.2487 - val_accuracy: 0.9402
Epoch 3/4
7/7 [==============================] - 0s 16ms/step - loss: 0.2054 - accuracy: 0.9550 - val_loss: 0.1831 - val_accuracy: 0.9706
Epoch 4/4
7/7 [==============================] - 0s 16ms/step - loss: 0.1517 - accuracy: 0.9750 - val_loss: 0.1460 - val_accuracy: 0.9797
Epoch 1/16
/usr/local/lib/python3.7/dist-packages/keras/optimizer_v2/gradient_descent.py:102: UserWarning: The `lr` argument is deprecated, use `learning_rate` instead.
  super(SGD, self).__init__(name, **kwargs)
7/7 [==============================] - 1s 46ms/step - loss: 0.1285 - accuracy: 0.9850 - val_loss: 0.1441 - val_accuracy: 0.9797
Epoch 2/16
7/7 [==============================] - 0s 18ms/step - loss: 0.1265 - accuracy: 0.9850 - val_loss: 0.1421 - val_accuracy: 0.9807
...
Epoch 16/16
7/7 [==============================] - 0s 20ms/step - loss: 0.1040 - accuracy: 0.9900 - val_loss: 0.1206 - val_accuracy: 0.9838
'''
model_B_on_A.evaluate(X_test_B, y_test_B)
'''
[0.11980337649583817, 0.9894999861717224]
'''

모델 A와 비슷한 모델B를 처음부터 만들었을 때는 테스트 정확도가 97.2% 였는데, 전이 학습을 통해 98.9%까지 올릴 수 있었습니다. 사실, 타깃 클래스나 랜덤 초깃값을 바꾸면 성능이 떨어집니다. 논문의 결과가 너무 긍정적이라면, 의심해보아야 합니다. 논문에서 이런 속임수를 쓴 이유는 전이 학습은 작은 완전 연결 네트워크에서는 잘 동작하지 않기 때문입니다. 작은 네트워크는 패턴 수를 적게 학습하고 완전 연결 네트워크는 특정 패턴을 학습하기 때문일 것입니다. 전이 학습은 조금 더 일반적인 특성(특히 아래쪽 층에서) 감지하는 경향이 있는 심층 합성곱 신경망에서 잘 동작합니다. 전이 학습은 14장(CNN)에서 다시 살펴볼 것입니다. 이때는 정말 속임수가 없다고 합니다!

 

비지도 사전훈련

 

데이터가 적고 복잡한 문제일 경우 전이 학습을 이용할 수 있는데, 비슷한 작업에 대해 훈련된 모델을 찾기 어렵다면 비지도 사전훈련(unsupervised pretraining)을 사용해 볼 수 있습니다. 레이블되지 않은 훈련 샘플을 많이 모을 수 있다면 비지도 사전훈련을 사용해 autoencoder나 GAN 판별자 하위층을 재사용하고 그 위에 작업에 맞는 출력층을 추가할 수 있습니다. 그 다음 지도 학습으로( 즉, 레이블된 훈련 샘플로) 최종 네트워크를 세밀하게 튜닝합니다.

 

딥러닝 초기에는 greedy layer-wise pretrainig 기법을 사용했습니다.

탐욕적 층 단위 사전훈련

먼저 하나의 층을 가진 비지도 학습 모델을 훈련하고 그 다음 이 층을 동결한 채로 다시 그 위에 다른 층을 추가한 다음 모델을 다시 훈련하기를 반복합니다. 적절히 반복한 후 출력층을 추가하고 지도 학습 기법으로 훈련해 최종 네트워크를 튜닝합니다.

 

보조 작업에서 사전훈련

 

훈련 데이터를 쉽게 얻거나 생성할 수 있는 보조 작업에서 첫 번째 신경망을 훈련하고, 이 첫 번째 신경망의 하위 층을 가져다가 쓰는 것입니다. 이 첫 번째 층의 하위 층은 특성 추출기로서 학습합니다. 가령 얼굴을 인식하는 시스템을 만들 때, 각 사람의 사진을 수백만 장 모으기는 어려울 것입니다. 따라서 인터넷에서 무작위로 많은 인물의 이미지를 수집해 두 개의 다른 이미지가 동일 인물인지 감지하는 첫 번째 신경망을 만들고, 이 하위층을 사람의 얼굴 특성을 잘 감지하는 특성 추출기로 사용하는 것입니다.

 

 

고속 옵티마이저

아주 큰 심층 신경망의 훈련 속도는 심각하게 느릴 수 있습니다. 지금까지 훈련 속도를 높이는 방법 4가지를 살펴보았습니다.

  1. 연결 가중치에 좋은 초기화 전략 사용
  2. 좋은 활성화 함수 사용
  3. 배치 정규화 사용
  4. 사전 훈련된 네트워크 일부 재사용하기(보조 작업, 비지도 학습)

 

훈련 속도를 크게 높일 수 있는 다른 방법으로 표준 경사 하강법 옵티마이저 대신 다른 optimizer를 사용하는 것입니다. 이 절에서 가장 인기 있는 옵티마이저인 momentum optimization, Nesterov accelerated gradient, AdaGrad, RMSProp, Adam, Nadam를 소개합니다.

 

momentum optimization

경사 하강법은 그레디언트에 학습률 η을 곱한 것을 차감하여 가중치 Θ를 갱신합니다. 이 때, 이전 그레디언트가 얼마였는지 고려하지 않습니다. 국부적으로 그레디언트가 아주 작으면 가중치 갱신이 매우 느려질 것입니다.

 

$$\theta \gets \theta -\eta \nabla _{\theta }J\left(\theta \right)$$

[경사 하강법]

 

모멘텀 최적화는 momentum vector m에 현재 그레디언트를 더해서 m을 갱신하고 가중치에 m을 빼서 가중치를 갱신합니다. 모멘텀 최적화는 볼링공이 경사를 따라 굴러간다고 했을 때를 생각하면 이해가 쉽습니다. 종단속도(물체가 저항 때문에 더 이상 가속되지 않고 등속도 운동을 하게 될 때의 속도)까지 빠르게 가속될 것입니다.

$$m\ \gets \beta m+\eta \nabla _{\theta }J\left(\theta \right)$$

$$\theta \gets \theta -m$$

[모멘텀 알고리즘]

 

β는 momentum으로 마찰저항과 같은 역할을 합니다. 0(높은 마찰 저항) ~ 1(마찰 저항 없음) 사이의 값을 갖고, 보통 0.9의 값을 갖습니다. 그레디언트가 일정하다면, 종단속도(가중치를 갱신하는 최대 크기)는 학습률 η에 1/(1-β)를 곱한 것과 같음을 알 수 있습니다. β=0.9이면, 종단 속도는 10을 그레디언트에 곱한 것과 같습니다. 경사 하강법보다 10배 빠르게 진행됩니다. 이는 모멘텀 최적화가 더 빠르게 평편한 지역을 탈출할 수 있게 도와줍니다.

 

입력값의 스케일이 매우 다르면 비용 함수는 오른쪽처럼 길쭉한 모양이 되는데, 경사 하강법은 좁고 길쭉한 골자기에서 학습에 오랜 시간이 걸립니다. 반면 모멘텀 최적화는 골자기를 따라 바닥(최적점)까지 점점 더 빠르게 내려갑니다. 배치 정규화를 사용하지 않는 심층 신경망에서 상위층은 종종 스케일이 매우 다른 입력을 받게 되는데, 모멘텀 최적화가 이런 경우 큰 도움을 줍니다.

 

또, 모멘텀 최적화는 지역 최적점(local optima)를 건너뛰도록 하는데 도움이 됩니다. 이는 볼링 공이 얕은 골짜기를 넘어 가는 걸 상상하면 쉽습니다. 이는 모멘텀 때문에 optimizer가 최적값에 안정되기 전까지 왔다 갔다하며 진동하는데, 시스템에 마찰 저항이 조금 있는 것이 좋은 이유입니다.

 

케라스에서 모멘텀 최적화 구현은 SGD optimizer에 momentum 매개 변수를 지정하면 됩니다.

optimizer = tf.keras.optimizers.SGD(lr=0.001, momentum=0.9)

모멘텀 최적화의 단점은 튜닝할 파라미터가 하나 늘어난다는 점입니다. 그러나 실제로 momentum=0.9에서 보통 잘 동작하며 경사 하강법보다 거의 항상 더 빠릅니다.

 

 

네스테로프 가속 경사

 

모멘텀 최적화의 변종으로 네스테로프 가속 경사(NAG 혹은 Nesterov momentum optimizaion)는 현재 위치가 θ가 아니라 모멘텀의 방향으로 조금 앞선 θm에서 비용 함수의 그레디언트를 계산하는 것입니다.

$$m\ \gets \beta m+\eta \nabla _{\theta }J\left(\theta +\beta m\right)$$

$$\theta \gets \theta -m$$

일반적으로 모멘텀 벡터가 올바른 방향을 가리킬 것이므로 이런 변경이 가능합니다. 현재 위치에서 그레디언트를 사용하는 것보다 그 방향으로 조금 더 나아가서 측정한 그레디언트를 사용하는 것이 약간 더 정확할 것입니다.

&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;nabla;₁: momentum, &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;nabla;₂: Nesterov

그림에서 보면 NAG가 기본 momentum보다 최적값에 조금 더 가깝습니다. NAG의 경우 골짜기를 가로지르기 보다 계곡 아래쪽()으로 잡아 당겨집니다. 이는 진동을 감소시키고 수렴이 빨라지게 만들어줍니다.

 

케라스에서는 nesterov=True로 설정하면 됩니다. momentum은 default가 0이므로 설정해주어야 합니다.

optimizer = tf.keras.optimizers.SGD(lr=0.001, momentum=0.9, nesterov=True)

 

AdaGrad

 

경사 하강법은 전역 최적점 방향으로 곧장 향하지 않고 가장 가파른 경사를 따라 빠르게 내려가기 시작해서 골짜기 아래로 느리게 이동합니다. 알고리즘이 이를 일찍 감지하고 전역 최적점 쪽으로 좀 더 정확한 방향을 잡았다면 좋았을 것입니다. AdaGrad 알고리즘은 가장 가파른 차원을 따라 그레디언트의 스케일을 감소시켜 이 문제를 해결합니다.

 

$$s\gets s+\nabla _{\theta }J\left(\theta \right)\otimes \nabla _{\theta }J\left(\theta \right)$$

$$\theta \gets \theta -\eta \nabla _{\theta }J\left(\theta \right)\oslash \sqrt{s+\epsilon }$$

 

⊘: 원소별 나눗셈, ⊗: 원소별 곱셈

 

$$s_i\gets s_i+\left(\frac{\partial J\left(\theta \right)}{\partial \theta _i}\right)^2$$

위 벡터 식을 벡터 s의 각 원소 s_i의 업데이트 식으로 표현하면 위와 같습니다. 비용함수가 i 번째 차원을 따라 가파르다면 s_i는 반복이 진행됨에 따라 점점 커질 것입니다.

 

$$\theta _i\gets \theta _i-\eta \frac{\frac{\partial J\left(\theta \right)}{\partial \theta _i}}{\sqrt{s_i+\epsilon }}$$

위 벡터식은 모든 파라미터 θ_i에 대해 위와 같은 계산을 적용한 것과 동일합니다.

 

요약하자면 이 알고리즘은 훈련이 반복됨에 따라 학습률을 감소시키는 단점이 있습니다. 하지만 경사가 완만한 차원보다 가파른 차원에 대해 더 빠르게 감소합니다. 이를 adaptive learning rate이라 부릅니다. 학습률 η을 덜 튜닝해도 되는 장점이 있습니다.

더보기

sqrt(s_i+ε)로 나눠지기 때문에 차원 별로 학습률이 다르게 감소됩니다. 그레디언트가 클 수록 s_i도 커져 가파른 차원에 학습률이 빠르게 감소됩니다.

 

AdaGrad는 신경망 훈련 시 너무 일찍 멈추는 경우가 종종 있습니다. 학습률이 너무 감소되어 전역 최적점에 도달하기 전에 알고리즘이 완전히 멈춥니다. 그래서 Adagrad optimizer는 심층 신경망에는 사용하지 말아야 합니다. (선형 회귀 같은 간단한 작업에는 효과적일 수 있습니다.)

 

 

RMSProp

AdaGrad는 너무 빨리 느려저 전역 최적점에 수렴하지 못하는 위험이 있습니다. RMSProp 알고리즘은 가장 최근 반복에서 비롯된 그레디언트만 누적함으로써 이 문제를 해결했습니다. 이렇게 하기 위해 알고리즘 첫 번째 단계에서 지수 감소를 사용합니다.

$$s\gets \beta s+\left(1-\beta \right)\nabla _{\theta }J\left(\theta \right)\otimes \nabla _{\theta }J\left(\theta \right)$$

$$\theta \gets \theta -\eta \nabla _{\theta }J\left(\theta \right)\oslash \sqrt{s+\epsilon }$$

 

보통 감쇠율 β=0.9로 설정합니다. 기본값에서 잘 동작하므로 β는 튜닝할 필요가 전혀 없습니다.

 

케라스에는 RMSprop optimizer가 있습니다. rho는 감쇠율이며, 거의 언제나 AdaGrad보다 성능이 좋습니다.

optimizer = tf.keras.optimizers.RMSprop(lr=0.001, rho=0.9)

 

Adam과 Nadam 최적화

 

adaptive moment estimation(적응적 모멘트 추정)을 의미하는 Adam은 모멘텀 최적화 + RMSProp을 합친 것입니다. 모멘텀 최적화처럼 이전 그레디언트의 지수 감소 평균을 따르고, RMSProp처럼 이전 그레디언트 제곱의 지수 감소된 평균을 따릅니다.

 

$$m\gets \beta _1m-\left(1-\beta _1\right)\nabla _{\theta }J\left(\theta \right)\tag{1}$$

$$s\gets \beta _2s-\left(1-\beta _2\right)\nabla _{\theta }J\left(\theta \right)\otimes \nabla _{\theta }J\left(\theta \right)\tag{2}$$

$$\hat{m}=\frac{m}{1-\beta _1^t}\tag{3}$$

$$\hat{s}=\frac{s}{1-\beta _2^t}\tag{4}$$

$$\theta \gets \theta +\eta \hat{m}\oslash \sqrt{\hat{s}+\epsilon }\tag{5}$$

 

t는 반복 횟수를 나타냅니다. Adam은 지수 감소 합 대신 지수 감소 평균을 계산합니다. (지수 감소 평균은 지수 감소 합의 $1-β_1$배입니다.)

 

3, 4번 식은 m, s가 0으로 초기화되기 때문에 훈련 초기에 0으로 치우치게 될 것입니다. 3, 4번은 훈련 초기에 m, s의 값을 증폭시키는 데 도움을 줍니다. 보통 $β_1$=0.9, $β_2$=0.999로 초기화합니다.

더보기

m, s가 0부터 시작하므로 $β_1m$과 $β_2s$는 반복 초기에 크게 기여를 못합니다. 이를 보상해주기 위해 반복 초기에 m, s를 증폭시켜주면 반복이 많이 진행되면 단계 3, 4는 분모가 1에 가까워져 거의 증폭되지 않습니다.

케라스에서 Adam optimizer를 만드는 방법은 아래와 같습니다.

optimizer = keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999)

 

Adam은 AdaGrad, RMSProp처럼 적응적 학습률 알고리즘이기 때문에, 학습률 η를 튜닝할 필요가 적습니다. 다른 Adam 의 변종들 AdaMax, Nadam을 알아봅시다.

 

AdaMax

 

Adam은 s에 그레디언트 제곱을 누적합니다.(최근 그레디언트에 더 큰 가중치를 부여합니다.) 즉, $l_2$노름으로 파라미터 업데이트 스케일을 낮춥니다. AdaMax는 $l_2$노름을 $l_\infty$로 바꿉니다. 즉, Adam의 2번 식을 $s\gets \max _{\ }^{ }\left(\beta _2s,\ \nabla _{\theta }J\left(\theta \right)\right)$로 바꾸고 4번 식을 삭제하면 됩니다. 실제로 데이터셋에 따라 다르지만 일반적으로 Adam의 성능이 더 낫습니다. 어떤 작업에서 Adam이 잘 동작하지 않는다면, 시도해볼 수 있는 optimizer입니다.

 

$$\max _{\ }^{ }\left(a,\ b\right)=\frac{a+b}{2}+\frac{\left|a-b\right|}{2}$$

 

Nadam

 

Adam에 네스테로프 기법을 더한 것으로 종종 Adam보다 조금 더 빠르게 솔루션에 수렴합니다. 일반적으로 Nadam이 Adam보다 성능이 좋았지만 이따금 RMSProp이 나을 때도 있었습니다.

더보기

(RMSProp, Adam, Nadam 최적화를 포함하여) 적응적 최적화 방법이 좋은 솔루션으로 빠르게 수렴합니다. 하지만, 이 방식이 일부 데이터셋에서 나쁜 결과를 낸다는 것을 보였습니다. 모델의 성능이 만족스럽지 못할 때 네스테로프 가속 경사를 이용해보세요.

 

지금까지 논의한 optimizer 기법은 모두 1차 편미분(Jacobian 야코비안)에만 의존합니다. 최적화 이론에는 2차 편미분(Hessain, 야코비안의 편미분)을 기반으로 한 뛰어난 알고리즘들이 있는데, 불행하게도 이런 알고리즘은 심층 신경망에 적용하기 매우 어렵습니다. n개의 파라미터를 가진 신경망을 1차 편미분이아니라 2차 편미분을 하면 $n^2$개의 2차 편미분을 계산해야 하기 때문입니다. DNN은 전형적으로 수 만개의 파라미터를 가지고 있어 메모리 용량을 넘어서는 경우가 많고 계산이 매우 느립니다.

 

 

희소 모델 훈련

 

모든 최적화 알고리즘은 대부분 파라미터가 0이 아닌 밀집 모델을 만드는데, 엄청난 속도나 메모리를 적게 차지하는 모델이 필요할 경우 희소 모델을 만들 수 있습니다.

 

간단한 방법은 보통 처럼 훈련한 후 작은 값의 가중치를 0으로 만드는 것입니다. 일반적으로 이 방법은 많이 희소한 모델을 만들지 못하고 모델의 성능을 낮춥니다.

 

다른 방법으로는 훈련하는 동안 $l_1$규제를 강하게 적용하는 것입니다. 라쏘 회귀처럼 optimizer가 가능한 많은 가중치를 0으로 만들도록 강제합니다.

 

다른 방법으로는 TF-MOT(텐서플로 모델 최적화 툴킷)을 사용할 수 있습니다. 이는 훈련하는 동안 반복적으로 연결 가중치를 크기에 맞춰 제거하는 가지치기(pruning) API를 제공합니다.

 

 지금까지 소개한 optimizer 비교

클래스 수렴 속도 수렴 품질
SGD * ***
SGD(momentum=..) ** ***
SGD(momentum=.., nesterov=True) ** ***
Adagrad *** *(너무 일찍 멈춤)
RMSrop *** ** or ***
Adam *** ** or ***
Nadam *** ** or *** 
AdaMax *** ** or ***

*=나쁨, **=보통, ***=좋음

 

 

학습률 스케줄링

좋은 학습률을 찾는 것은 매우 중요합니다. 학습률을 너무 작게 잡으면 최적점에 가기까지의 시간이 오래 걸립니다. 학습률을 높게 잡으면 처음에는 빠르게 학습하다가 최적점 근처에서 요동이 심해져 수렴하지 못할 것입니다. 컴퓨팅 자원이 한정적이라면 차선의 솔루션을 만들기 위해 완전히 수렴하기 전에 멈추어야합니다.

학습률을 작은 값에서 큰 값까지 지수적으로 증가시키면서 모델 훈련을 수백 번 반복하여 좋은 학습률을 찾아낼 수 있습니다. 그 다음 학습 곡선을 보면서 다시 상승하는 곡선보다 조금 더 작은 학습률을 선택합니다. 그 다음 모델을 다시 초기화하고 이 학습률로 훈련합니다.

 

일정한 학습률을 사용하는 것보다 큰 학습률로 시작해 학습 속도가 느려질 때 학습률을 낮추면 최적의 고정 학습률보다 좋은 솔루션을 더 빨리 발견할 수 있습니다. 학습률을 감소시키는 방법은 여러가지가 있는데, 이 방법들을 학습 스케줄(learning schedule)이라 합니다.

 

  • 거듭제곱 기반 스케줄링 power scheduling
    • 학습률을 반복 횟수 t에 대한 함수 $\eta \left(t\right)=\frac{\eta _0}{\left(1+\frac{t}{s}\right)^c}$로 지정합니다. 초기 학습률 $\eta _0$, 거듭 제곱 수 c(일반적으로 1), 스텝 횟수 s는 하이퍼파라미터입니다. s번 스탭 뒤에 $\frac{\eta _0}{2}$만큼 줄어듭니다. $\eta _0$, s를 튜닝합니다.

 

  • 지수 기반 스케줄링 exponential scheduling
    • 학습률을 $\eta \left(t\right)=\eta _0\cdot \left(0.1\right)^{\frac{t}{s}}$로 설정합니다. 학습률이 s 스텝마다 10배씩 줄어듭니다.

 

  • 구간별 고정 스케줄링 piecewise constant scheduling
    • 일정 횟수의 에포크 동안 일정한 학습률(5 에포크 동안 $\eta _0$=0.1)을 사용하고, 그 다음 또 다른 횟수의 에포크 동안 작은 학습률을 사용하는(50 에포크 동안 $\eta _1$=0.001) 식입니다. 적절한 학습률과 에포크 횟수의 조합을 찾으려면 이리저리 바꿔봐야 합니다.

 

  • 성능 기반 스케줄링 performance scheduling
    • 매 N 스텝마다 (조기 종료처럼) 검증 오차를 측정하고, 오차가 줄어들지 않으면 λ배 만큼 학습률을 감소시킵니다.

 

  • 1사이클 스케줄링 1cycle scheduling
    • 훈련 절반 동안 초기 학습률$\eta _0$를 선형적으로 $\eta _1$까지 증가시킵니다. 그 다음 나머지 절반 동안 선형적으로 학습률을 $\eta _0$까지 다시 줄입니다. 마지막 몇 번 에포크는 학습률을 소수점 몇 째 자리까지 줄입니다.(여전히 선형적으로) 최대 학습률$\eta _1$은 최적의 학습률을 찾을 때와 같은 방식을 사용해 선택하고 초기 학습률 $\eta _0$은 대략 10배 정도 낮은 값을 선택합니다.
    • 모멘텀을 사용할 때는 처음에 높은 모멘텀으로 시작해서 (예를 들면, 0.95) 훈련의 처음 절반 동안 낮은 모멘텀으로 줄어듭니다.(선형적으로 0.85). 그 다음 다시 나머지 훈련 절반 동안 최댓값(0.95)까지 되돌립니다. 마지막 몇번의 에포크는 최댓값으로 진행합니다.
    • 스미스 학자는 이 방법이 많은 실험을 통해 훈련 속도를 크게 늘려주고 더 높은 성능을 낸다는 것을 보였습니다.

 

성능 기반 스케줄링, 지수 기반 스케줄링 둘 다 잘 작동하지만, 튜닝, 수렴 속도면에서 지수 기반 스케줄링이 더 선호됩니다. 하지만 1사이클 방식이 조금 더 좋은 성능을 냅니다.

 

케라스에서 거듭제곱 기반 스케줄링 구현

optimizer=tf.keras.optimizers.SGD(lr=0.01, decay=1e-4)

decay는 s의 역수입니다. 케라스는 c=1로 가정합니다.

 

지수 기반 스케줄링 구현

def exponential_decay_fn(epoch):
	return 0.01*0.1**(epoch/20)

$\eta _0$와 s를 직접 입력하고 싶지 않다면, 이 변수를 설정한 closure를 반환하는 함수를 만들 수 있습니다.

def exponential_decay(lr0, s):
	def exponential_decay_fn(epoch):
    	return lr0*0.1**(epoch/s)
    return exponential_decay_fn

exponential_decay_fn = exponential_decay(lr0=0.01, s=20)
더보기

exponential_decay_fn 함수가 반환된 이후에도 exponential_decay 함수의 지역변수 lr0와 s를 계속 참조할 수 있습니다. 이런 함수를 클로저라고 부릅니다.

그 다음 이 스케줄링 함수를 전달하여 LearningRateScheduler 콜백을 만듭니다. 그리고 이 콜백을 fit() 메서드에 전달합니다.

lr_scheduler = keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train_scaled, y_train, [..], callbacks=[lr_scheduler])

LearningRateScheduler는 에포크를 시작할 때마다 optimizer의 learning_rate 속성을 업데이트합니다. 에포크마다 스텝이 많다면 스텝마다 학습률을 업데이트하는 것이 좋습니다. keras.optimizers.schedules를 사용할 수 있습니다.

 

스케줄 함수는 두 번째 매개변수로 현재 학습률을 받을 수 있습니다. 이 스케줄 함수는 이전 학습률에 $0.1^{\frac{1}{20}}$을 곱하여 동일한 지수 감쇠 효과를 나타냅니다.

def exponential_decay_fn(epoch, lr):
	return lr*0.1**(1/20)

이 구현은 (이전 구현과 달리)옵티마이저의 초기 학습률에만 의존하므로 이를 적절히 설정해야 합니다.

 

모델을 저장할 때 옵티마이저와 학습률이 함께 저장됩니다. 새로운 스케줄 함수를 사용할 때도 저장된 모델의 훈련이 중지된 지점부터 훈련을 계속 진행할 수 있습니다.

 

하지만, 에포크는 저장되지 않는데 스케줄 함수가 epoch 매개변수를 사용하면, fit() 메서드를 호출할 때마다 0으로 초기화되어서 모델의 가중치를 망가뜨립니다. 한가지 방법은 fit() 메서드의 initial_epoch 매개변수에 시작 epoch값을 입력할 수 있습니다.

 

구간별 고정 스케줄링

def piecewise_constant_fn(epoch):
	if epoch < 5:
    	return 0.01
    elif epoch < 15:
    	return 0.005
    else:
    	return 0.001

지수 기반 스케줄링에서 했던 것처럼 이 함수로 LearningRateScheduler 콜백을 만들어 fit() 메서드에 전달합니다.

 

성능 기반 스케줄링

lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)

성능 기반 스케줄링에서는 ReduceLROnPlateau 콜백을 사용합니다. 이 콜벡을 fit() 메서드에 전달하면 최상의 검증 손실이 5번 연속적인 에포크동안 향상되지 않을 때 학습률에 0.5를 곱합니다.

 

매 스텝마다 학습률을 업데이트하는 방법

 

tf.keras.optimizers.schedules에 있는 스케줄을 사용해 이 학습률을 옵티마이저에 전달합니다.

s=20*len(X_train)//32 # 20번 에포크에 담긴 전체 스텝 수(배치크기=32)
learning_rate = keras.optimizers.schedules.ExponentialDecay(0.01, s, 0.1)
optimizer = tf.keras.optimizers.SGD(learning_rate)

모델을 저장할 때 학습률과 (현재 상태를 포함한) 스케줄도 함께 저장됩니다.

 

1사이클 방식 스케줄링

매 반복마다 학습률을 조정하는 사용자 정의 콜백을 만들면됩니다.(self.model.optimizer.lr을 바꾸어 옵티마이저의 학습률을 업데이트할 수 있습니다.)

 

규제를 사용해 과대적합 피하기

 

DNN은 수만, 수백만개의 파라미터를 가지고 있습니다. 이 때문에 네트워크의 자유도가 매우 높습니다. 즉, 대규모의 복잡한 데이터셋을 학습할 수 있다는 뜻인데, 이런 높은 자유도는 훈련 세트에 과적합되기 쉽게 만듭니다. 규제가 필요합니다.

 

10장에서 최상의 규제 방법인 조기 종료를 구현했고 배치 정규화도 꽤 괜찮은 규제 방법입니다. 신경망에 널리 사용되는 규제 방법인 $l_1$과 $l_2$규제, dropout, max-norm 규제에 대해 알아보겠습니다.

 

$l_1$과 $l_2$규제

 

다음은 케라스 층의 연결 가중치에 규제 강도 0.01의 $l_2$규제를 적용하는 방법입니다.

layer = keras.layers.Dense(100, activation='elu',
			kernel_initializer='he_normal',
           	        kernel_regularizer=keras.regularizers.l2(0.01))

l2() 함수는 훈련 동안 규제 손실을 계산하기 위해 각 스텝에서 호출되는 규제 객체를 반환합니다. 이 손실은 최종 손실($θ^2$을 말하는 듯)에 합산됩니다.?

 

keras.regularizers.l1()을 사용하면 $l_1$규제를 사용할 수 있고, 둘 다 필요하면 keras.regularizers.l1_l2()를 사용할 수 있습니다.

 

일반적으로 네트워크의 모든 은닉층에 동일한 활성화 함수, 동일한 초기화 전략을 사용하거나 모든 층에 동일한 규제를 적용하기 때문에 동일한 매개변수 값을 반복하는 경우가 많습니다. 때문에 오타가 자주 발생하는데, 파이썬의 functools.partial() 함수를 사용해 기본 매개변수 값을 사용하여 함수 호출을 감싸서 오타를 줄일 수 있습니다.

from functools import partial

RegularizedDense = partial(keras.layers.Dense,
	activation='elu',
        kernel_initializer='he_normal',
        kernel_regularizer=keras.regularizers.l2(0.01))
    
model=keras.models.Sequential([
	keras.layers.Flatten(input_shape=[28, 28]),
        RegularizedDense(300),
        RegularizedDense(100),
        RegularizedDense(10, activation='softmax',
    	    kernel_initializer='glorot_uniform')
])

 

드롭 아웃

 

이 알고리즘은 최고 성능을 내는 신경망조차도 드롭아웃을 적용해서 정확도를 1~2% 높였습니다. 이 알고리즘은 매우 간단한데, 매 훈련 스텝에서 각 뉴런(입력 뉴런 포함, 출력 뉴런 제외)은 임시적으로 드롭 아웃될 확률 p를 가집니다. 즉, 이번 훈련 스텝에서는 완전히 무시되지만, 다음 스텝에서는 활성화될 수 있습니다. p를 dropout rate라 합니다. 보통 0.1~0.5의 값을 갖습니다. 순환 신경망은 0.2~0.3, 합성곱 신경망은 0.4~0.5의 값을 갖습니다. 훈련을 끝내면 뉴런에 더는 드롭아웃을 적용하지 않습니다.

 

드롭아웃으로 훈련된 뉴런들은 몇 개의 입력 뉴런에 지나치게 의존하지 않고, 모든 입력 뉴런에 주의를 기울입니다. 그러므로 입력값의 작은 변화에 덜 민감해지고 더 안정적인 네트워크가 되어 일반화 성능이 좋아집니다.

 

드롭아웃을 적용한 신경망은 개개의 뉴런이 있을 수도 없을 수도 있기 때문에 $2^N$개의 네트워크가 가능합니다.(N은 드롭아웃이 가능한 뉴런 수). 이는 아주 큰 값이어서 같은 네트워크가 두 번 선택될 가능성이 사실상 없습니다. 10,000번의 훈련 스텝을 진행하면 10,000개의 다른 신경망( 각각 하나의 훈련 샘플을 사용해서) 훈련하게 됩니다. 이 신경망은 대부분의 가중치를 공유하고 있기 때문에 아주 독립적이지는 않습니다. 하지만, 그럼에도 모두 다르기에 결과적으로 만들어진 신경망은 이 모든 신경망을 평균한 앙상블로 볼 수 있습니다.

 

일반적으로 맨 위의 층부터 세 번째 층까지 있는 뉴런에만 드롭아웃을 적용합니다.

 

p=0.5로 설정하면 테스트하는 동안 하나의 뉴런이 훈련 때보다 평균적으로 두 배더 많은 입력 뉴런과 연결됩니다. 이런 점을 보상하기 위해 훈련하고 나서 각 뉴런의 연결 가중치에 0.5를 곱할 필요가 있습니다. 그렇지 않으면 훈련한 것보다 거의 두 배 많은 입력 신호를 받기 때문에 잘 동작하지 않을 것입니다.

 

조금 더 일반적으로 말하면, 훈련이 끝난 뒤 입력의 연결 가중치에 보존 확률(keep probability)(1-p)를 곱해야 합니다. 또는 각 뉴런의 출력을 보존 확률로 나눌 수 있습니다.

 

케라스에서는 keras.layers.Dropout 층을 사용해 드롭아웃을 구현합니다. 이 층은 훈련하는 동안 일부 입력을 랜덤하게 버립니다.(0으로 설정) 그 다음 남은 입력을 보존 확률로 나눕니다. 훈련이 끝난 후에는 어떤 작업도 하지 않고, 입력을 다음 층으로 그냥 전달합니다.

model = keras.models.Sequential([
   keras.layers.Flatten(input_shape=[28, 28]),
   keras.layers.Dropout(rate=0.2),
   keras.layers.Dense(300, activation='elu', kernel_initializer='he_normal'),
   keras.layers.Dropout(rate=0.2),
   keras.layers.Dense(100, activation='elu', kernel_initializer='he_normal'),
   keras.layers.Dropout(rate=0.2),
   keras.layers.Dense(10, activation='softmax')
])
더보기

드롭아웃은 훈련하는 동안에만 활성화되므로 훈련 손실과 검증 손실을 비교하면 오해를 일으키기 쉽습니다. 특히 비슷한 훈련 손실과 검증 손실을 얻었더라도 모델이 훈련 세트에 과대적합될 수 있습니다., 따라서 훈련이 끝난 후 훈련 손실을 평가해야 합니다.

 

모델이 과대적합 되었다면 드롭아웃 비율을 늘릴 수 있습니다. 반대로 훈련 세트에 과소적합되면 드롭아웃 비율을 낮춰야 합니다. 층이 클 때는 드롭아웃 비율을 늘리고 작은 층에는 드롭아웃 비율을 낮춰야합니다. 많은 최신 신경망 구조는 마지막 은닉층 뒤에만 드롭아웃을 사용합니다.

 

드롭아웃은 수렴이 느려지는 단점이 있지만, 적절히 튜닝하면 좋은 모델을 만들 수 있습니다.

더보기

SELU 활성화 함수를 기반으로 자기 정규화하는 네트워크를 규제하고 싶다면 alpha 드롭아웃을 사용해야 합니다. 이 방법은 입력의 평균과 표준편차를 유지하는 드롭아웃의 한 변종입니다. 일반 드롭아웃이 자기 정규화 기능을 망가뜨릴 수 있기 때문입니다.

 

 

몬테 카를로 드롭아웃

 

훈련된 드롭아웃 모델을 재훈련하거나 수정하지 않고 성능을 크게 향상시킬 수 있는 Monte Carlo dropout(MC 드롭아웃)이라 불리는 강력한 기법이 있습니다. 모델의 불확실성을 더 잘 측정할 수 있고 구현도 아주 쉽습니다.

 

y_probas = np.stack([model(X_test_scaled, training=True)
                     for sample in range(100)])
y_proba = y_probas.mean(axis=0)
y_std = y_probas.std(axis=0)

model(X)는 넘파이 베열이 아니라 텐서를 반환한다는 것을 빼면 model.predict(X)와 비슷하고 training 매개변수를 지원합니다. training=True로 지정하여 Dropout 층이 활성화되기 때문에 예측이 달라집니다. 테스트 세트에서 100개의 예측을 만들어 쌓았습니다. 패션 MNIST는 10개의 카테고리가 있고 샘플 10,000개를 100개 쌓았기 때문에 y_probas는 [100, 10000, 10] 크기의 행렬입니다. 드롭아웃을 한 채로 만든 예측을 평균하면 일반적으로 드롭아웃 없이 예측한 하나의 결과보다 더 안정적입니다.

 

# 테스트에서 dropout 없이 모델이 예측한 값
np.round(model.predict(X_test_scaled[:1]), 2)
'''
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]], dtype=float32)
'''
# 테스트시 dropout을 활성화한 모델의 예측값
np.round(y_probas[:, :1], 2)
'''
array([[[0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.07, 0.  , 0.93]],

       [[0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.07, 0.  , 0.93]],
       
       [[0.  , 0.  , 0.  , 0.  , 0.  , 0.01, 0.  , 0.04, 0.  , 0.95]],
  	   [...]
'''

dropout을 활성화하면 모델이 더 이상 확신하지 않습니다. 여전히 클레스 9(앵클 부츠)를 선호하지만 5(샌들)나 7(스니커즈)로도 생각합니다. 이들은 모두 신발이니 공통점이 있습니다. 평균을 낸 값은 아래와 같습니다. 100% 확신하는 것보다 훨씬 납득할만한 추정입니다.

np.round(y_proba[:1], 2)
'''
array([[0.  , 0.  , 0.  , 0.  , 0.  , 0.01, 0.  , 0.09, 0.  , 0.9 ]],
      dtype=float32)
'''

이 확률 추정의 표본 분포도 확인해볼 수 있습니다.

np.round(y_std[:1], 2)
'''
array([[0.  , 0.  , 0.  , 0.  , 0.  , 0.03, 0.  , 0.16, 0.  , 0.17]],
      dtype=float32)
'''

이 추정에는 많은 분산이 있습니다. 위험에 민감한 시스템(의료, 금융)을 만든다면 불확실한 예측을 매우 주의 깊게 다루어야 합니다.

model.evaluate(X_test_scaled, y_test)
'''
313/313 [==============================] - 1s 2ms/step - loss: 0.4926 - accuracy: 0.8568
[0.4926319718360901, 0.8568000197410583]
'''

y_pred = np.argmax(y_proba, axis=1)
accuracy = np.sum(y_pred == y_test)/len(y_pred)
print(accuracy)
'''
0.8609
'''

정확도가 0.856에서 0.86으로 조금 향상됐습니다. 몬테 카를로의 샘플 숫자(예제에선 100)는 튜닝할 수 있는 파라미터입니다. 이 값이 높을 수록 예측과 불확실성에 대한 추정이 더 정확해질 것입니다. 하지만, 예측 시간도 그에 따라 늘어납니다. 일정 샘플 수를 넘으면 성능이 크게 향상되지 않습니다.

 

모델이 훈련하는 동안 다르게 작동하는 (BatchNormalization 층과 같은) 층을 가지고 있다면 앞에서와 같이 훈련 모드를 강제로 설정해서는 안되며, 이 경우 Dropout 층을 아래의 MCDropout 클래스로 바꿔주어야 합니다.

class MCDropout(keras.layers.Dropout):
	def call(self, inputs):
    	return super().call(inputs, training=True)
더보기

 이 MCDropout 클래스는 Sequential API를 포함한 모든 케라스 API에서 사용할 수 있습니다. 함수형 API나 서브클래싱 API는 MCDropout 클래스를 만들 필요가 없고 보통의 Dropout 층을 만들고 training=True를 사용하여 호출할 수 있습니다. Sequential API는 모든 층의 training 매개변수를 강제로 바꾸기 때문입니다.

Dropout 층을 상속하고 call() 메서드를 override하여 training 매개변수를 Trure로 강제 설정합니다.(12장 참조) 비슷하게 AlphaDropout 클래스를 상속하여 MCAlphaDropout 클래스를 정의할 수 있습니다.

더보기

처음부터 모델을 만든다면 그냥 Dropout 대신 MCDropout을 사용하면 됩니다. 하지만 이미 Dropout을 사용하여 모델을 훈련했다면 Dropout 층을 MCDropout으로 바꾸고 기존 모델과 동일한 모델을 새로 만듭니다. 그 다음 기존 모델의 가중치를 새로운 모델로 복사합니다.

 

MC 드롭아웃은 모델의 정확성, 더 정확한 불확실성 추정을 제공하는 기술입니다. 물론 훈련하는 동안은 일반적은 드롭아웃처럼 수행하기 때문에 규제처럼 동작합니다.

 

 

멕스-노름 규제

 

이 방식은 널리 사용되는 기법으로 각각의 뉴런에 대해 입력 연결 가중치 w가 $\left|\left|w\right|\right|_2\le r$이 되도록 제한합니다. r은 max-norm 하이퍼 파라미터이고 $\left|\left|·\right|\right|_2$는 $l_{2\ }$노름을 나타냅니다.

 

맥스-노름 규제는 전체 손실함수에 규제 손실함수를 추가하지 않고, 매 훈련 스텝이 끝나고 $\left|\left|w\right|\right|_2$을 계산하고 필요하면 w의 스케일링을 조정합니다. $(w\gets w\frac{r}{\left|\left|w\right|\right|_2\ })$ 

 

r을 줄이면 규제의 양이 증가하여 과대적합을 감소시키는 데 도움이 됩니다. max-norm은 (배치 정규화를 사용하지 않았을 때) 불안정한 그레디언트 문제를 완화하는 데 도움을 줄 수 있습니다.

 

케라스에서 max-norm 규제를 구현하려면 적절한 최댓값으로 지정한 max_norm()이 반환한 객체로 은닉층의 kernel_constraint 매개변수를 지정합니다.

keras.layers.Dense(100, activation='elu', kernel_initializer='he_normal',
               kernel_constraint=keras.constraints.max_norm(1.))

매 훈련 반복이 끝난 후 fit() 메서드가 층의 가중치와 함께 max_norm()이 반환한 객체를 호출해 스케일이 조정된 가중치를 반환받습니다. 이 값을 사용하여 층의 가중치를 바꿉니다. bias_constraint 매개변수에 max_norm()을 지정하여 편향을 규제할 수 있습니다.

 

max_norm() 함수는 기본값이 0인 axis 매개변수가 있습니다. Dense 층은 [샘플 수, 뉴런 수] 크기의 가중치를 가지는데, axis=0을 사용하면 각 뉴런의 가중치 벡터에 규제가 독립적으로 적용됩니다. 

더보기

이후 합성곱(14장) 층에 max-norm을 적용하려면 axis 매개변수를 적절히 지정해야합니다. 일반적으로 axis=[0, 1, 2]를 사용합니다. 합성곱 층은 일반적으로 [샘플 수, 높이, 너비, 채널 개수] 크기의 가중치를 가집니다. 따라서 axis=[0, 1, 2]로 지정하면 채널 축에 독립적으로 적용됩니다.?

 

요약 및 실용적인 가이드라인

 

이 장에서 여러 가지 기법을 다루었는데, 작업마다 사용시 좋은 기법읁 다르며 선택에 명확한 기준은 없지만 하이퍼파라미터를 크게 튜닝하지 않고 대부분의 경우에 잘 맞는 설정을 정리했습니다.

 

기본 DNN 설정

하이퍼파라미터 기본값
커널 초기화 He 초기화
활성화 함수 ELU
정규화 얕은 신경일 경우 없음, 깊은 신경망이라면 배치 정규화
규제 조기 종료 (필요하면 $l_2$규제 추가)
옵티마이저 모멘텀 최적화(또는 RMSProp이나 Nadam)
학습률 스케줄 1사이클

 

자기 정규화를 위한 DNN 설정

 

네트워크가 완전 연결 층을 쌓은 단순한 모델이라면 자기 정규화를 사용할 수 있습니다.

하이퍼파라미터 기본값
커널 초기화 르쿤 초기화
활성화 함수 SELU
정규화 없음(자기 정규화)
규제 필요하다면 알파 드롭아웃
옵티마이저 모멘텀 최적화(또는 RMSProp이나 Nadam)
학습률 스케줄 1사이클

 

입력 특성을 정규화 해야하는 것을 잊으면 안됩니다! 비슷한 문제를 해결한 모델을 찾을 수 있다면 사전 훈련된 신경망의 일부를 재사용해봐야 하고, 레이블이 없는 데이터가 많다면 비지도 사전 훈련을 사용하세요. 비슷한 작업을 위한 레이블된 데이터가 많다면 보조 작업에서 사전 훈련을 수행할 수 있습니다.

 

 

예외적인 경우

 

희소 모델이 필요하다면 $l_1$규제를 사용할 수 있습니다.(훈련된 후 작은 가중치를 0으로 만듦). 매우 희소한 모델이 필요한 경우 TF-MOT를 사용할 수 있습니다. TF-MOT를 사용할 시 자기 정규화를 깨뜨리므로 기본 DNN 설정을 사용해야합니다.

 

빠른 응답을 하는 모델(번개처럼 빨리 예측하는 모델)이 필요하면 층 개수를 줄이고 배치 정규화 층을 이전 층에 합칩니다. LeakyReLU나 ReLU와 같이 빠른 활성화 함수를 사용합니다. 희소 모델을 만드는 것도 도움이 되고 부동소수점 정밀도를 낮출 수도 있습니다.(19장에 상세히 다룸)

 

가이드라인을 따라 이제 매우 깊은 신경망을 훈련할 준비가 되어스빈다. 케라스만 사용해도 많은 작업을 할 수 있지만, 언젠가 더 많은 제어가 필요한 경우 사용자 정의 손실 함수를 만들거나 훈련 알고리즘을 조정해야할 수 있습니다. 이럴 땐 다음 장에서 소개할 텐서플로 저수준 API를 사용해야 합니다.

 

 

출처: 핸즈온 머신러닝 2판

사진 출처:

https://ichi.pro/ko/model-gwajeog-hab-dropout-eul-sayonghasibsio-179358118490064

https://hwk0702.github.io/ml/dl/deep%20learning/2020/08/28/learning_rate_scheduling/

https://hwk0702.github.io/ml/dl/deep%20learning/2020/07/15/optimizer/

https://yganalyst.github.io/ml/ML_chap3-2/

https://blog.naver.com/PostView.naver?blogId=okcho1138&logNo=222434376766&parentCategoryNo=&categoryNo=&viewDate=&isShowPopularPosts=false&from=postView 

https://velog.io/@kangtae/EECS-498-007-598-005-101-Training-Neural-Networks

https://paperswithcode.com/method/leaky-relu

https://codedragon.tistory.com/m/9428