Networks/Hands-On 머신러닝 정리

Hands-On Machine Learning 정리 - 딥러닝(Chapter 11: 심층 신경망 훈련하기)

코딩하는 Español되기 2024. 11. 3. 12:30

10장에서는 인공 신경망 소개와 첫 번째 심층 신경망을 훈련했습니다(몇 개의 은닉층만으로 이루어진 얕은 네트워크)

고해상도 이미지에서 수백 종류의 물체를 감지하는 것처럼 복잡한 문제를 다뤄야 한다면 어떻게 해야할까?

[훈련 중 마주칠 수 있는 문제들]

    ● 까다로운 그레디언트 소실 or 그레디언트 폭주 문제에 직면 가능

        - 두 현상 모두 하위층을 훈련하기 어렵게 함

    ● 대규모 신경망을 위한 훈련 데이터가 충분하지 않거나 레이블을 만드는 작업에 비용이 많이 들 수 있음

    ● 훈련이 극단적으로 느려질 수 있음

    ● 수백만 개의 파라미터를 가진 모델은 훈련 세트에 과대적합될 위험이 高

       (특히, 훈련 샘플 부족이나 잡음이 많은 경우 심함)

아래의 목차와 같이 이와 관련한 내용을 정리할 예정입니다.


Chap 11.

 

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

    - 글로럿과 He 초기화

    - 수렴하지 않는 활성화 함수

    - 배치 정규화

    - 그레디언트 클리핑

 

2. 사전 훈련된 층 재사용하기

    - 케라스를 사용한 전이 학습

    - 비지도& 보조 작업에서 사전 훈련

 

3. 고속 옵티마이저

    - 모멘텀 최적화

    - 네스테로프 가속 경사

    - AdaGrad

    - RMSProp

    - Adam과 Nadam 최적화

    - 학습률 스케줄링

 

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

    - L1, L2 규제

    - 드롭아웃

    - 몬테 카를로 드롭아웃

    - 맥스-노름 규제

 

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


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

○ 역전파 알고리즘은 출력층에서 입력층으로 오차 그레디언트를 전파하면서 진행

    ● 알고리즘이 신경망의 모든 파라미터에 대한 오차 함수의 그레디언트를 계산

        → 경사 하강법 단계에서 이 그레디언트를 사용해서 각 파라미터 수정 

○ 알고리즘이 하위층으로 진행될수록 그레디언트가 점점 작아지는 경우가 多

그레디언트 소실(Vanishing Gradient)

    ● 경사 하강법이 하위층의 연결 가중치를 변경되지 않은 채로 두어 훈련이 좋은 솔루션으로 수렴하지 않는 문제

그레디언트 폭주(Exploding Gradient) 

    ● 그레디언트가 점점 커져 여러 층이 비정상적으로 큰 가중치로 갱신되어 알고리즘이 발산하는 문제

    ● 주로 순환 신경망에서 주로 나타남

○ 심층 신경망을 훈련할 때 그레디언트를 불안정하게 만드는 원인

    ● 로지스틱 시그모이드 활성화 함수와 가중치 초기화 방법의 조합

        - 이 활성화 함수와 초기화 방식을 사용했을 때 각 층에서 출력의 분산 > 입력의 분산

        - 신경망의 위쪽으로 갈수록 층을 지날 때마다 분산이 계속 커져 가장 높은 층에서는 활성화 함수가 0 | 1로 수렴

○ 로지스틱 활성화 함수

    ● 입력이 커지면 0 | 1로 수렴해서 기울기가 0에 매우 가까워짐

    ● So, 역전파가 될 때 신경망으로 전파할 그레디언트가 거의 없음

    ● 그레디언트는 최상위층에서부터 역전파가 진행되면서 점차 약해짐

    ● 실제로 아래쪽 층에는 아무것도 도달하지 않게 됨

※ 로지스틱 함수의 도함수는 σ(1- σ) 이므로 함수의 값이 0이나 1에 가까우면 도함수의 결과가 매우 작아지고

    층이 거듭될수록 그 값은 더 작아짐

 

[시각화 코드]

더보기
def logit(z):
    return 1 / (1 + np.exp(-z))

z = np.linspace(-5, 5, 200)

plt.plot([-5, 5], [0, 0], 'k-')
plt.plot([-5, 5], [1, 1], 'k--')
plt.plot([0, 0], [-0.2, 1.2], 'k-')
plt.plot([-5, 5], [-3/4, 7/4], 'g--')
plt.plot(z, logit(z), "b-", linewidth=2)
props = dict(facecolor='black', shrink=0.1)
plt.annotate('Saturating', xytext=(3.5, 0.7), xy=(5, 1), arrowprops=props, fontsize=14, ha="center")
plt.annotate('Saturating', xytext=(-3.5, 0.3), xy=(-5, 0), arrowprops=props, fontsize=14, ha="center")
plt.annotate('Linear', xytext=(2, 0.2), xy=(0, 0.5), arrowprops=props, fontsize=14, ha="center")
plt.grid(True)
plt.title("Sigmoid activation function", fontsize=14)
plt.axis([-5, 5, -0.2, 1.2])

plt.show()

1-1 글로럿과 he 초기화

○ 글로럿과 벤지오가 논문에서 불안정한 그레디언트 문제를 크게 완화하는 방법을 제안

    ● 예측할 때는 정방향으로, 그레디언트 역전파할 때는 역방향으로 양방향 신호가 적절하게 흘러야 함

    ● 신호가 죽거나 폭주 또는 소멸하지 않아야 함

    ● 적절한 신호가 흐르기 위해서는"각 층의 출력에 대한 분산 =  입력에 대한 분산" 한다고 주장

    ● 역방향에서 층을 통과하기 전과 후의 그레디언트 분산이 동일해야 함

    ● 사실 층의 입력과 출력 개수가 같지 않다면 두 가지를 보장 불가

    ● 각 층의 연결 가중치를 글로럿 초기화식처럼 무작위로 초기화하는 대안을 제시

○ 글로럿 초기화를 사용하면 훈련 속도를 높일 수 있음

○ 분산의 스케일링이나 fan_avg, fan_in을 쓰는 것만 다름

$$ \textup{※ 균등 분포의 경우 단순히} r = \sqrt{3\sigma^{2}} $$

He 초기화: ReLU 활성화 함수에 대한 초기화 전략

    ● 르쿤 초기화를 사용해야 함

초기화 전략 활성화 함수 σ^2 (정규 분포)
글로럿 활성화 함수 X, 하이퍼볼릭 탄젠트, 로지스틱, 소프트 맥스 1 / fan_{avg}
He ReLU 함수와 그 변종들 2 / fan_{in}
르쿤 SELU 1 / fan_{in}

[글로럿 초기화(=세이비어 초기화) 로지스틱 활성화 함수를 사용할 때]

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

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

[르쿤 초기화]

$$ \textup{※} fan_{\textup{avg}} \textup{를} fan_{in} \textup{로 바꾸면 얀 르쿤이 제안한 초기화 전략이 됨} $$ 

$$ \textup{※} fan_{\textup{in}} = fan_{\textup{out}} \textup{이면 르쿤 초기화는 글로럿 초기화와 동일} $$

 

1-2 수렴하지 않는 활성화 함수

○ ReLU함수 장점: 특정 양숫값에 수렴하지 않는다(+ 계산 빠름)

○ 하지만 완벽하지는 않음 → 죽은 ReLU 문제(= 훈련하는 동안 일부 뉴런이 0 이외의 값을 출력하지 않는다)

    ● 특히, 큰 학습률을 사용하면 신경망의 뉴런 절반이 죽어있기도 함

    ● 뉴런의 가중치가 바뀌어 훈련 세트에 있는 모든 샘플에 대해 입력의 가중치 합이 음수가 되면 뉴런이 죽음

    ● IF SUM(가중치) < 0 THEN ReLU함수의 그레디언트 = 0 So, 경사 하강법이 더는 작동 X

    ● LeakyReLU : 문제 해결을 위해 사용하는 ReLU 함수의 변종

$$ \textup{LeakyReLU}_{\alpha} = \textup{max}(\alpha z, z ) $$

하이퍼파라미터 α가 이 함수가 '새는(leaky)' 정도를 결정

    ● 새는 정도: z < 0 일때 함수의 기울기(일반적으로 0.01)

※ 최근 논문에서 LeakyReLU가 ReLU보다 항상 성능이 높다는 결론을 얻음

※ 사실 α = 0.2(많이 통과)로 하는 것이 α = 0.01(조금 통과)보다 더 나은 성능을 내는 것으로 보임

※ 주어진 범위에서 α를 무작위로 선택하고 테스트시에는 평균을 사용하는 RReLU도 평가함

RReLU: 잘 작동(훈련 세트의 과대적합 위험을 줄이는) 규제의 역할도 하는 것으로 나옴

ELU(Exponential Linear Unit): 새로운 활성화 함수로 다른 ReLU 변종의 성능을 앞지름

 

○ ELU 함수 특징

    ● 몇 가지를 제외하고는 ReLU와 매우 비슷

    ● z < 0 일때, 음숫값이 들어오므로 활성화 함수의 평균 출력이 0에 더 가까워짐(그레디언트 소실 문제 완화)

    ● 하이퍼파라미터 α: z가 큰 음숫값일 때 ELU가 수렴할 값(보통 1로 설정하지만 변경 가능)

    ● α = 1 이면 이 함수는 z = 0에서 급격히 변동하지 않으므로 z = 0을 포함해 모든 구간에서 매끄러워짐

        → 경사하강법의 속도를 높여줌

※ ELU 함수의 도함수는 z < 0 일때 α(exp(z))이고, z ≥ 0일 때 1이됨 So, α ≠ 1일 경우 이 도함수는 z = 0에서 불연속적

    ● 단점

        - 지수함수를 사용하므로 계산이 느림

        - 훈련하는 동안에는 수렴 속도가 빨라서 느린 계산이 상쇄 But 테스트 시에는 느림

[ELU 활성화 함수]
$$ \textup{ELU}_\alpha (z) = \begin{cases}\alpha (\textup{exp}(z)-1) & z<0 \\ z & z \geq 0\end{cases} $$

 

○ SELU(Scaled ELU) 활성화 함수

    ● 완전 연결 층만 쌓아서 신경망을 만들고 모든 은닉층이 SELU 활성화 함수를 사용한다면

       네트워크가 자기 정규화 된다는 것을 보임

    ● 훈련하는 동안 각 층의 출력이 평균 0과 표준편차 1을 유지하는 경향이 존재(그레디언트 소실과 폭주 문제 방지)

    ● 좋은 성능을 내지만 자기 정규화가 일어나기 위한 조건이 있음

    ● 조건

        - 입력 특성이 반드시 표준화(평균 0, 표준편차 1)되어야 함

        - 모든 은닉층의 가중치 = 르쿤 정규분포 초기화로 초기화

        - 네트워크는 일렬로 쌓은 층으로 구성되어야 함(순환 신경망이나 스킵 연결)과 같은 순차적이지 않은 구조에서

          사용하면 자기 정규화되는 것을 보장되지 않음

○ 일반적으로 심층신경망의 은닉층에서 사용하는 활성화 함수
    ● SELU > ELU > LeakyReLU > ReLU > tanh > 로지스틱 순
○ 네트워크가 자기 정규화 되지 못하는 구조라면 ELU가 SELU보다 나을 수 있음
    ● SELU는 z = 0에서 연속적이지 못하기 때문
○ 실행 속도가 중요하다면 LeakyReLU 선택 가능
○ 신경망이 과대 적합되었다면 RReLU, 훈련 세트가 아주 크다면 RPeLU를 포함시키면 좋음

 

[시각화 코드]

더보기

- Leaky ReLU 훈련

(X_train_full, y_train_full), (X_test, y_test) = keras.datasets.fashion_mnist.load_data()
X_train_full = X_train_full / 255.0
X_test = X_test / 255.0
X_valid, X_train = X_train_full[:5000], X_train_full[5000:]
y_valid, y_train = y_train_full[:5000], y_train_full[5000:]
tf.random.set_seed(42)
np.random.seed(42)

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, kernel_initializer="he_normal"),
    keras.layers.LeakyReLU(),
    keras.layers.Dense(100, kernel_initializer="he_normal"),
    keras.layers.LeakyReLU(),
    keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy",
              optimizer=keras.optimizers.SGD(learning_rate=1e-3),
              metrics=["accuracy"])
              
history = model.fit(X_train, y_train, epochs=10, validation_data=(X_valid, y_valid))

 

- PReLU 훈련

tf.random.set_seed(42)
np.random.seed(42)

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, kernel_initializer="he_normal"),
    keras.layers.PReLU(),
    keras.layers.Dense(100, kernel_initializer="he_normal"),
    keras.layers.PReLU(),
    keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy",
              optimizer=keras.optimizers.SGD(learning_rate=1e-3),
              metrics=["accuracy"])

history = model.fit(X_train, y_train, epochs=10,validation_data=(X_valid, y_valid))

 

- Leaky ReLU 시각화

def leaky_relu(z, alpha=0.01):
    return np.maximum(alpha*z, z)
    
plt.plot(z, leaky_relu(z, 0.05), "b-", linewidth=2)
plt.plot([-5, 5], [0, 0], 'k-')
plt.plot([0, 0], [-0.5, 4.2], 'k-')
plt.grid(True)
props = dict(facecolor='black', shrink=0.1)
plt.annotate('Leak', xytext=(-3.5, 0.5), xy=(-5, -0.2), arrowprops=props, fontsize=14, ha="center")
plt.title("Leaky ReLU activation function", fontsize=14)
plt.axis([-5, 5, -0.5, 4.2])

plt.show()

 

- ELU 시각화

def elu(z, alpha=1):
    return np.where(z < 0, alpha * (np.exp(z) - 1), z)
    
    
plt.plot(z, elu(z), "b-", linewidth=2)
plt.plot([-5, 5], [0, 0], 'k-')
plt.plot([-5, 5], [-1, -1], 'k--')
plt.plot([0, 0], [-2.2, 3.2], 'k-')
plt.grid(True)
plt.title(r"ELU activation function ($\alpha=1$)", fontsize=14)
plt.axis([-5, 5, -2.2, 3.2])

plt.show()

1-3 배치 정규화

○ ELU와 함께 He 초기화를 사용하면 훈련 초기 단계에서 그레디언트 소실이나 폭주 문제를 크게 감소시킬 수 있음

    ● 훈련하는 동안 다시 발생하지 않으리란 보장 X

배치 정규화(BN;Batch Normalization)기법

    ● 각 층에서 활성화 함수를 통과하기 전이나 후에 모델에 연산 하나 추가

    ● 연산

        - 단순하게 입력을 원점에 맞추고 정규화

        → 각 층에서 두 개의 새로운 파라미터로 결괏값의 스케일을 조정하고 이동

        - 파라미터 하나는 스케일 조정, 다른 하나는 이동에 사용

    ● 신경망의 첫 번째 층으로 BN를 추가하면 훈련 세트를 표준화할 필요 X(BN층이 역할을 대신함)

※ 한 번에 하나의 배치만 처리 So, 근사적 & 입력 특성마다 스케일을 조정하고 이동 가능

    ● 입력 데이터를 원점에 맞추고 정규화하려면 알고리즘은 평균과 표준편차를 추정해야함

        - 이를 위해 현재 미니배치에서 입력의 평균과 표준편차를 평가

    ● 규제와 같은 역할을하여 다른 규제 기법의 필요성을 줄여줌

    ● But 모델의 복잡도를 키우고 실행 시간도늘림

    ● 훈련이 끝난 후에 이전 층과 배치 정규화층을 합쳐 실행 속도 저하를 피할 수 있음

[배치 정규화 알고리즘]

$$ \textup{1단계 }  \mu_B = \frac{1}{m_B}\sum_{i=1}^{m_B}x^{(i)} $$

$$ \textup{2단계 } \sigma_{B}^{2} = \frac{1}{m_B}\sum_{i=1}^{m_B}(x^{i}-\mu_{B})^2  $$

$$ \textup{3단계 } \hat{x}^{(i)} = \frac{x^{(i)}-\mu_{B}}{\sqrt{\alpha_{B}^{2}+\varepsilon}} $$

$$ \textup{4단계 } z^{(i)}=\gamma \otimes\hat{x}^{(i)}+ \beta $$

○ BN 알고리즘 계산식

    ● µ_B: 미니배치 B에 대해 평가한 입력의 평균 벡터(입력마다 하나의 평균을 가짐)

    ● α_B: 미니배치 B에 대해 평가한 입력의 표준편차 벡터

    ● m_B: 미니배치에 있는 샘플 수

    ● hat{x}^{(i)}: 평균이 0이고 정규화된 샘플 i의 입력

    ● γ: 층의 출력 스케일 파라미터 벡터(입력마다 하나의 스케일 파라미터 존재)

    ● ⊗: 원소별 곱셈(각 입력은 해당되는 출력 스케일 파라미터와 곱해짐)

    ● β: 층의 출력 이동(오프셋) 파라미터 벡터, 각 입력은 해당 파라미터만큼 이동

    ● ε: 분모가 0이 되는 것을 막기 위한 작은 숫자(안전을 위한 항이라고 함)

    ● z^{(i)}: 배치 정규화 연산의 출력(즉, 입력 스케일을 조정하고 이동시킨 것)

 

1-3-1 케라스로 배치 정규화 구현

○ 은닉층의 활성화 함수 전이나 후에 BatchNormalization 층을 추가하여 BN층 구현 가능    ● BN층은 입력마다 네 개의 파라미터 µ, α, γ, β를 추가        e.g. 첫 번째 배치 정규화 층은 4 * 784 = 3136개    ● 마지막 두 개의 파라미터 µ, σ는 이동 평균        - 이 파라미터는 역전파로 학습되지 않기 때문에 케라스는 Non-trainable 파라미터로 분류        e.g. 배치 정규화 파라미터의 전체 개수: 3136 + 1200 + 400               을 2로 나누면 이 모델에서 훈련되지 않는 전체 파라미터 수 = 2368

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.BatchNormalization(),
    keras.layers.Dense(300, activation="relu"),
    keras.layers.BatchNormalization(),
    keras.layers.Dense(100, activation="relu"),
    keras.layers.BatchNormalization(),
    keras.layers.Dense(10, activation="softmax")
])
model.summary()

 

1-4 그레디언트 클리핑

: 그레디언트 폭주 문제 완화를 위한 방법으로 일정 임곗값을 넘어서지 못하게 그레디언트를 자르는 방법

○ 케라스에서 이를 구현하려면 옵티마이저를 만들 때 clipvalue와 clipnorm 매개변수를 지정하면 됨

    ● 그레디언트 벡터의 모든 원소를 -1.0 ~ 1.0 사이로 클리핑

    ● 즉, 훈련되는 각 파라미터에 대한 손실의 모든 편미분 값을 -1.0 ~ 1.0으로 잘라냄

※ clipnrom과 clipvalue 매개변수가 모두 지정될 경우 clipnorm이 먼저 적용

optimizer = keras.optimizers.SGD(clipvalue=1.0)
optimizer = keras.optimizers.SGD(clipnorm=1.0)

 

2. 사전훈련된 층 재사용하기

전이 학습: 비슷한 유형의 문제를 처리한 신경망이 이미 있는 지 찾아본 후 그 신경망의 하위층을 재사용 하는 방법

   e.g. 동물, 식물, 자동차, 생활용품을 포함해 카테고리 100개로 구분된 이미지 분류를 훈련한 DNN

    - 구체적인 자동차 종류를 분류하는 DNN 훈련하고자 함

    - 보통 원본 모델의 출력층을 바꿔야함

       why? 가장 유용하지 않은 층 & 새로운 작업에 필요한 출력 개수와 맞지 않을 수 있음

    - 원본 모델의 상위 은닉층은 하위 은닉층보다 덜 유용

       why? 새로운 작업에 유용한 고수준 특성 = 원복 작업에서 유용했던 특성과는 상당히 다름

       So, 재사용할 층 개수를 잘 선정하는 것이 중요

    [순서]

    - 재사용하는 층을 동결(즉, 경사 하강법으로 가중치가 바뀌지 않도록 훈련되지 않는 가중치로 만듬)

    - 모델 훈련 → 성능 평가 → 맨 위에 한 두개의 은닉층의 동결 해제

    - 역전파를 통해 가중치 조정하여 성능이 향상되는지 확인 → 적절한 개수를 찾을 때까지 반복

2-1 케라스를 사용한 전이 학습

○ 예시

    ● 데이터셋: 8개 클래스만 담겨 있는 패션 MNIST 데이터셋

    ● A: 데이터 클래스를 분류하는 작업한 모델

        - 모델 정확도 > 90%

    ● B: 샌들과 셔츠 이미지를 구분하는 작업(이진분류기 훈련)한 모델

        - 양성 = 셔츠, 음성 = 샌들

        - 레이블된 이미지: 200

        - 모델 정확도 = 97.2%

    ● 더 좋은 성능을 내고 싶어 전이 학습을 활용하고자 함(A모델의 층을 기반으로 model_B_on_A를 생성)

    ● 출력층만 제외하고 모든 층을 재사용

    ● model_B_on_A를 훈련할 때 model_A도 영향을 받음

       ※ 원치 않을 경우 층을 재사용하기 전에 model_A를 클론

    ● clone_model() 메서드는 가중치를 복제하지 않음 

    ● 새로운 출력층이 랜덤하게 초기화되어 큰 오차를 생성 → 그레디언트가 재사용된 가중치를 망칠 수 있음

    ● So, 처음 몇 번의 에포크동안 재사용된 층을 동결하고 새로운 층에게 적절한 가중치를 훈련할 시간을 부여

       (코드에서 trainable 속성을 False로 지정하고 모델 컴파일)

※ compile() 메서드가 모델에서 훈련될 가중치를 모으기 때문에 동결 or 해제 후 반드시 다시 컴파일해야함

    ● 테스트 정확도: 99.25% & 오차율이 2.8% → 약 0.7%로 4배까지 낮춤

    ● But, 속임수가 존재

        - 전이 학습은 작은 완전 연결 네트워크에서는 잘 동작하지 않음

        - 작은 네트워크는 패턴 수를 적게 학습하고 완전 연결 네트워크는 특정 패턴을 학습하기 때문

[코드]

더보기
def split_dataset(X, y):
    y_5_or_6 = (y == 5) | (y == 6) # sandals or shirts
    y_A = y[~y_5_or_6]
    y_A[y_A > 6] -= 2 # class indices 7, 8, 9 should be moved to 5, 6, 7
    y_B = (y[y_5_or_6] == 6).astype(np.float32) # binary classification task: is it a shirt (class 6)?
    return ((X[~y_5_or_6], y_A),
            (X[y_5_or_6], y_B))

(X_train_A, y_train_A), (X_train_B, y_train_B) = split_dataset(X_train, y_train)
(X_valid_A, y_valid_A), (X_valid_B, y_valid_B) = split_dataset(X_valid, y_valid)
(X_test_A, y_test_A), (X_test_B, y_test_B) = split_dataset(X_test, y_test)
X_train_B = X_train_B[:200]
y_train_B = y_train_B[:200]


tf.random.set_seed(42)
np.random.seed(42)

 

- model A 훈련 및 저장

model_A = keras.models.Sequential()
model_A.add(keras.layers.Flatten(input_shape=[28, 28]))
for n_hidden in (300, 100, 50, 50, 50):
    model_A.add(keras.layers.Dense(n_hidden, activation="selu"))
model_A.add(keras.layers.Dense(8, activation="softmax"))
model_A.compile(loss="sparse_categorical_crossentropy",
                optimizer=keras.optimizers.SGD(learning_rate=1e-3),
                metrics=["accuracy"])
history = model_A.fit(X_train_A, y_train_A, epochs=20, validation_data=(X_valid_A, y_valid_A))
model_A.save("my_model_A.h5")

 

- model B 훈련

model_B = keras.models.Sequential()
model_B.add(keras.layers.Flatten(input_shape=[28, 28]))
for n_hidden in (300, 100, 50, 50, 50):
    model_B.add(keras.layers.Dense(n_hidden, activation="selu"))
model_B.add(keras.layers.Dense(1, activation="sigmoid"))
model_B.compile(loss="binary_crossentropy",
                optimizer=keras.optimizers.SGD(learning_rate=1e-3),
                metrics=["accuracy"])
history = model_B.fit(X_train_B, y_train_B, epochs=20, validation_data=(X_valid_B, y_valid_B))

 

- model A 기반의 model_B_on_A 생성

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_A_clone = keras.models.clone_model(model_A)
model_A_clone.set_weights(model_A.get_weights())
model_B_on_A = keras.models.Sequential(model_A_clone.layers[:-1])
model_B_on_A.add(keras.layers.Dense(1, activation="sigmoid"))
for layer in model_B_on_A.layers[:-1]:
    layer.trainable = False

model_B_on_A.compile(loss="binary_crossentropy",
                     optimizer=keras.optimizers.SGD(learning_rate=1e-3),
                     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

model_B_on_A.compile(loss="binary_crossentropy",
                     optimizer=keras.optimizers.SGD(learning_rate=1e-3),
                     metrics=["accuracy"])
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=16,
                           validation_data=(X_valid_B, y_valid_B))

2-2 비지도 & 보조 학습에서 사전훈련

2-2-1 비지도 사전훈련(Unsupervised Pretraining)

    ● 풀어야 할 문제가 복잡하고 재사용할 수 있는 비슷한 모델이 없고

    ● 레이블된 훈련 데이터가 적고 그렇지 않은 데이터가 많을 때 좋은 선택

    ● 레이블이 없는 데이터(또는 전체 데이터)로 모델을 훈련

     → 지도 학습 기법을 사용해 레이블된 데이터에서 최종 학습을 위해 세밀하게 튜닝

 

2-2-2 보조 작업에서 사전 훈련

○ 레이블된 훈련 데이터가 많지 않다면?

    ● 레이블된 훈련 데이터를 쉽게 얻거나 생성할 수 있는 보조작업에서 첫 번째 신경망을 훈련

    ● 이 신경망의 하위층을 실제 작업을 위해 재사용

    ● 첫 번째 신경망의 하위층: 두 번째 신경망에 재사용될 수 있는 특성 추출기 학습

○ 예시

    ● 얼굴을 인식하는 시스템을 만들고자 함 But 개인별 이미지가 얼마 없다면 훈련하기에 충분하지 X

    ● 인터넷에서 무작위로 많은 인물의 이미지를 수집 → 두 개의 다른 이미지가 같은 사람의 것인지 감지

       ▷ 첫 번째 신경망 훈련

    ● 첫 번째 신경망: 얼굴의 특성을 잘 감지하도록 학습

    ● 신경망의 하위층을 재사용해 적은 양의 훈련 데이터에서 얼굴을 잘 구분하는 분류기 학습 가능

자연어처리 (NLP; Natural Language Processing) 애플리케이션

    ● 텍스트 문서로 이루어진 코퍼스를 다운 → 이 데이터에서 레이블된 데이터 자동 생성 가능

    ● e.g. 일부 단어를 랜덤하게 지우고 누락된 단어를 예측하는 모델 훈련 가능

자기지도학습(self-supervised learning)
: 데이터에서 스스로 레이블을 생성하고 지도 학습 기법으로 레이블된 데이터셋에서 모델을 훈련하는 방법
  이 방식은 사람이 레이블을 부여할 필요가 없음 So, 비지도 학습의 형태로 분류

 

3. 고속 옵티마이저

○ 큰 심층 신경망은 훈련 속도가 심각하게 느릴 수 있음. 훈련 속도를 높이는 방법

    ● 연결 가중치에 좋은 초기화 전략 사용

    ● 좋은 활성화 함수 사용

    ● 배치 정규화 사용

    ● 사전 훈련된 네트워크의 일부 재사용

    ● 표준 경사 하강법 옵티마이저 대신 더 빠른 옵티마이저 사용

        - e.g. 모멘텀 최적화, 네스테로프 가속 경사, AdaGrad, RMSProp, Adam, Nadam

 

3-1 모멘텀 최적화(Momentum Optimization)

○ 볼링공이 완만한 경사를 따라간다고 생각했을 때 처음에는 느리게 가다가 종단속도에 도달할 때까지 빠르게 가속

※ 종단속도: 가속되는 물체가 저항때문에 더는 가속되지 않고 등속도 운동을 하게 될때의 속도

○ 위와 같은 원리가 모멘텀 최적화의 간단한 원리

    ● 반대로 표준적인 경사하강법은 경사면을 따라 일정한 스텝으로 조금씩 내려감 → 맨 아래 도착하는 데 시간 長

○ 경사하강법의 경우

    ● 가중치에 대한 비용함수[J(θ)]에 학습률 η를 곱한 것을 바로 차감해 가중치 θ를 갱신

    ● 공식: θ ← θ - η▽_{θ}J(θ)

    ● 위의 공식은 이전 그레디언트가 얼마였는지 고려하지 않음(만약 그레디언트가 아주 작으면 매우 느려질 것)

○ 모멘텀 최적화는 이전 그레디언트가 얼마였는지를 중요하게 생각

    ● 매 반복에서 현재 그레디언트(학습률 η를 곱한 후)를 모멘텀 벡터 m에 더하고 이 값을 빼는 방식으로 가중치 갱신

    ● 그레디언트를 속도가 아닌 가속도로 사용

    ● 하이퍼파라미터 일종의 마찰저항을 표현하고 모멘텀이 너무 커지는 것을 막기위해 사용

        → 값의 범위: 0(높은 마찰 저항) ~ 1(마찰저항 없음)[일반적으로 모멘텀 값 = 0.9] 

 

[모멘텀 알고리즘]

$$ \textup{1.} m \leftarrow \beta m - \eta \bigtriangledown_{\theta}J(\theta) $$

$$ \textup{2.} \theta \leftarrow \theta + m $$

※ 1번과 2번 식의 덧셈, 뺄셈 부호를 반대로 쓰는 경우도 있음.

※ m은 누적되는 값이기 때문에 음수를 누적해 θ에 더하나 양수를 누적해 θ에서 빼나 똑같기 때문

※ 학습률 η를 2번 식으로 옮겨 m을 모두 누적한 후 학습률을 곱하도록 표현하는 경우도 있음

 

○ 그레디언트가 일정하다면 종단속도(= 가중치를 갱신하는 최대 크기)는 학습률 η를 곱한 그레디언트에 1/ 1- β를 곱한 것과 같음을 확인 가능

※ 종단속도 = 등속도 운동 So, 모멘텀 알고리즘의 1번에서 좌우변을 같게 놓고 정리하면 됨

 

○ 모멘텀 최적화 구현 및 단점

    ● SGD 옵티마이저를 사용하고 momentum 매개변수 지정

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

    ● 단점: 튜닝할 하이퍼파라미터가 하나 증가 

 

3-2 네스테로프 가속 경사

○ 기본 모멘텀 최적화보다 거의 항상 더 빠름

○ NAG(Nesterov Accelerated Gradient; 네스트로프 가속경사)는 모멘텀 방향으로 조금 앞선 θ + βm에서 비용함수의 그레디언트를 계산

[네스테로프 가속 경사 알고리즘]

$$ \textup{1.} m \leftarrow \beta m - \eta \bigtriangledown_{\theta} J (\theta + \beta m) $$

$$ \textup{2.} \theta \leftarrow \theta + m $$

○ 네스테로프 업데이트가 최적값에 조금 더 가까움

    ● 시간이 지나면 작은 개선이 쌓여서 NAG가 기본 모멘텀 최적화보다 확연하게 빨라짐

    ● ▽_{1} = 시작점 θ에서 측정한 비용함수의 그레디언트

    ● ▽_{2} = θ + βm에서 측정한 그레디언트

    ● 모멘텀이 골짜기를 가로지르도록 가중치에 힘을 가할 때

        - ▽_{1}는 골짜기를 더 가로지르도록 독려

        - ▽_{2}는 계곡의 아래쪽으로 잡아당김

      So, 진동을 감소시키고 수렴을 빠르게 만들어줌

○ NAG 사용

    ● SGD 옵티마이저를 만들 때 use_nesterov=True로 설정

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

 

3-3 AdaGrad

○ 한쪽이 길쭉한 문제의 경우 경사 하강법은 전역 최적점 방향으로 곧장 향하지 않고 가장 가파른 경사를 따라 빠르게 내려가기 시작해서 골짜기 아래로 느리게 이동 

    ● AdaGrad에서는 가장 가파른 차원을 따라 그레디언트 벡터의 스케일을 감소시켜서 해결

[AdaGrad 알고리즘]

$$ \textup{1.} s \leftarrow s + \triangledown_{\theta}J(\theta) \otimes \triangledown_{\theta}J(\theta) $$

$$ \textup{2.} \theta \leftarrow \theta - \eta \triangledown_{\theta} J(\theta) \oslash \sqrt{s + \varepsilon } $$

○ 단계별 설명

    ● 그레디언트 제곱을 벡터 s에 누적(⊗: 원소별 곱셈)

    ● 벡터화된 식 = 벡터의 각 원소 s_i마다 아래와 같이 계산하는 것과 동일

$$ s_{i} \leftarrow s_{i}+ (\partial J(\theta)/\partial \theta_{i})^2 $$

※ 즉, s_{i} = 파라미터 θ_{i}에 대한 비용 함수의 편미분을 제곱하여 누적

※ 비용함수가 i번째 차원을 따라 가파르다면 s_{i}는 반복이 진행됨에 따라 점점 커질 것

    ● 두 번째 단계(경사 하강법과 거의 동일)

        - 차이: 그레디언트 벡터를 \sqrt{s + ε}로 나누어 스케일을 조정

        - ⊘: 원소별 나눗셈

        - ε: 0으로 나누는 것을 막기 위한 값(일반적으로 10 ^{-10})

        - 벡터화된 식 = 모든 파라미터에 θ_{i}에 대해 동시에 아래를 계산하는 것과 동일

     $$ \theta_{i} \leftarrow \theta_{i} - \eta \partial J(\theta)/ \partial \theta_{i} / \sqrt{s_{i}+ \varepsilon } $$

    ● 요약

        - 학습률을 감소시키지만 경사가 완만한 차원보다 가파른 차원에 대해 더 빠르게 감소

        - 이를 적응적 학습률(Adaptive Learning Rate) ← 전역 최적점 방향으로 더 곧장 가도록 갱신되는 데 도움

        - 학습률 하이퍼파라미터 η를 덜 튜닝해도 되는 점이 장점

이유: \sqrt{s_{i} + ε} 으로 나눠지기 때문에 차원별 학습률이 다르게 감소

그레디언트가 클수록 s_{i}도 커져 가파른 차원에서 학습률이 빠르게 감소되어 속도가 느려지고 완만한 차원에서 진행이 빨라지는 효과

○ 2차방정식 문제에 대해서는 잘 작동하지만 신경망을 훈련할 때 너무 일찍 멈추는 경우가 있음

    ● 학습률이 너무 감소되어 전역 최적점에 도착 전에 알고리즘이 완전히 멈춤

    ● So, Adagrad 옵티마이저가(케라스에) 있지만 심층 신경망에는 사용하지 말아야함

optimizer = keras.optimizers.Adagrad(learning_rate=0.001)

 

3-4 RMSProp

○ AdaGrad는 너무 빨리 느려져서 전역 최적점에 수렴하지 못하는 위험이 존재

○ RMSProp는 가장 최근 반복에서 비롯된 그레디언트만 누적함으로써 이 문제를 해결

    ● 이렇게 하기위해 알고리즘의 첫 번째 단계에서 지수 감소를 사용

[RMSProp 알고리즘]

$$ \textup{1.} s \leftarrow \beta s + (1 - \beta)\triangledown_{\theta}J(\theta)\otimes \triangledown_{\theta}J(\theta) $$

$$ \textup{2.} \theta \leftarrow \theta - \eta \triangledown_{\theta}J(\theta)\oslash \sqrt{s + \varepsilon} $$

○ 보통 감쇠율 β는 0.9로 설정

    ● 하이퍼파라미터가 한 개 증가했지만 기본값으로 잘 작동하는 경우가 多 So, 튜닝할 필요는 X

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

 

3-5 Adam과 Nadam 최적화

○ Adam(Adaptive Moment Estimation; 적응적 모멘트 추정)

    ● 모멘텀 최적화 + RMSProp

    ● 모멘텀 최적화처럼 지난 그레디언트의 지수 감소 평균을 따르고

    ● RMSProp처럼 지난 그레디언트 제곱의 지수 감소된 평균을 따름

[Adam 알고리즘]

※ 그레디언트의 평균과 분산에 대한 예측

※ 평균 = 첫 번째 모멘트, 분산 = 두 번째 모멘트

※ t = (1부터 시작하는) 반복 횟수

$$ \textup{1.} m \leftarrow \beta_{1}m - (1 - \beta_{1}) \triangledown_{\theta} J(\theta) $$

$$ \textup{2.} s \leftarrow \beta_{2}s + (1 - \beta_{1}) \triangledown_{\theta} J(\theta) \otimes \triangledown-{\theta}J(\theta) $$

$$ \textup{3.} \hat{m} \leftarrow \frac{m}{1-\beta_{1}^{t}} $$

$$ \textup{4.} \hat{s} \leftarrow \frac{s}{1-\beta_{2}^{t}} $$

$$ \textup{5.} \theta \leftarrow \theta + \eta \hat{m} \oslash \sqrt{\hat{s} + \varepsilon} $$

단계 1, 2, 5는 모멘텀 최적화, RMSProp와 아주 비슷하다는 것을 알 수 있음

    ● 차이: 단계 1에서 지수 감소 합 대신 지수 감소 평균을 계산(상수 배인 것을 제외하면 동일)

$$ \textup{※ 지수 감소 평균의 지수 감소 합의} 1 - \beta_{1} \textup{배} $$ 

$$ \textup{즉, Adam식의} \beta = \frac{\beta_{1}}{1 - \beta{1}} $$

○ 단계 3,4

    ● m과 s가 0으로 초기화되기 때문에 훈련 초기에 0쪽으로 치우치게됨

    ● So, 3/4 단계가 훈련 초기에 m과 s의 값을 증폭시키는 데 도움을 줌

※ m과 s가 0부터 시작하므로 β_{1}m과 β_{2}S가 반복 초기에 크게 기여를 못함

※ So, 3, 4단계에서 반복 초기에 m과 s를 증폭시켜줌

※ But, 반복이 많이 진행되면 단계 3, 4의 분모는1에 가까워져 거의 증폭 X

○ Adam이(AdaGrad나 RMSprop처럼) 적응적 학습률 알고리즘이기 때문에 학습률 하이퍼파라미터(η)를 튜닝할 필요가 少

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

 

3-5-1 AdaMax

○ Adam 알고리즘에 단계 2에서 Adam은 s에 그레디언트의 제곱을 누적(= 최근 그레디언트에 더 큰 가중치 부여)

○ 단계 5에서 ε과 단계 3,4 를 무시하면 Adam은 s의 제곱근으로 파라미터 업데이트의 스케일을 낮춤

    ● Adam은 시간에 따라 감쇠된 그레디언트의 L2노름으로 파라미터 업데이트의 스케일을 낮춤

※ L2 노름 = 제곱 합의 제곱근

○ AdaMax에서는 L2노름을 L∞ 로 변경(그래서 이름이 Max)

    ● 단계2를 아래와 같이 바꾸고 단계 4를 삭제

$$ s \leftarrow \textup{max} (\beta_{2}s, \triangledown_\theta J(\theta)) $$

    ● 단계5에서 s에 비례해 그레디언트 업데이트의 스케일을 낮춤

        - 시간에 따라 감쇠된 그레디언트의 최댓값

3-5-2 Nadam

○ Adam 옵티마이저 + NAG(종종 Adam보다 조금 더 빠르게 수렴)

optimizer = keras.optimizers.Nadam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)

○ 지금까지 살펴본 모든 최적화 기법은 1차 편미분(야코비얀)에만 의존

○ 최적화 이론에는 2차 편미분(헤시안, 야코비안의 편미분)을 기반으로 한 뛰어난 알고리즘들이 존재

    ● But 이런 알고리즘은 심층 신경망에 적용하기가 매우 어렵

    ● ∵ 하나의 출력마다 n개의 1차 편미분이 아니라 n^2개의 2차 편미분을 계산해야하기 때문(n = 파라미터 수)

○ DNN은 전형적으로 수만 개의 파라미터를 가지므로 2차 편미분 최적화 알고리즘은 메모리 용량을 넘어서는 경우가 多

○ 가능하다고 해도 헤시안 계산은 매우 느림

희소 모델 훈련
○ 모든 최적화 알고리즘은 대부분 파라미터가 0이 아닌 밀집 모델을 생성
○ 엄청 빠르게 실행할 모델이 필요하거나 메모리를 적게 차지하는 모델이 필요하다면 희소 모델을 만들 수 있음
방법
    ● 보통 때처럼 모델을 훈련하고 작은 값의 가중치를 제거(= 0으로 만들기)
    ● 일반적으로 많이 희소한 모델을 만들지 못하고 모델의 성능을 낮출 수 있음
방법2
    ● 훈련하는 동안 L1규제를 강하게 적용
    ● 옵티마이저가 가능한 한 많은 가중치를 9으로 만들도록 강제
○ 방법3
    ● 2가지 방법이 잘 맞지 않는다면 텐서플로 모델 최적화 툴킷(TF-MOT)을 확인
    ● 훈련하는 동안 반복적으로 연결 가중치를 크기에 맞춰 제거하는 가지치기 API를 제공

[협곡과 말안장점에서 옵티마이저 경로]

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

 

고속 옵티마이저

이 내용은 핸즈온 머신러닝 2판 책을 보고 정리한 것 입니다.

hwk0702.github.io

 

3-6 학습률 스케줄링

○ 학습률을 너무 크게 잡으면 훈련이 발산할 수 있고, 작게 잡으면 최적점에 수렴하지만 너무 오래 걸릴 수 있음

학습 스케줄(Learning Schedule)

    ● 큰 학습률로 시작 → 학습 속도가 느려질 때 학습률 낮추기

    ● 최적의 고정 학습률 보다 좋은 솔루션을 더 빨리 발견 가능

3-6-1 학습 스케줄 종류

○ 거듭제곱 기반 스케줄링(Power scheduling)

    ● 학습률을 반복 횟수(t)에 대한 함수로 지정

$$ \eta (t) = \eta_{0} / (1 + t/s)^{c} $$  

    ● 하이퍼파라미터: 초기 학습률 η_{0}, 거듭 제곱 수 c(일반적으로 1로 지정), 스텝 횟수 s

    ● η는 각 스텝마다 감소

    ● s번 스텝 뒤에 학습률은 η_{0} / 2 로 감소

    ● s번 더 스텝이 진행된 후 학습률 = η_{0} / 3 .... η_{0} / 4 

    ● 처음에 빠르게 감소하다가 점점 더 느리게 감소

○ 지수 기반 스케줄링(Exponential Scheduling)

    ● 학습률을 아래와 같이 설정

$$ \eta (t) = \eta_{0} 0.1^{t/s} $$

    ● η는 s 스텝마다 10배씩 점차 감소

    ● 거듭제곱 기반 스케줄링은 학습률이 갈수록 천천히 감소시키는 반면 s번 스텝마다 계속 10배씩 감소

○ 구간별 고정 스케줄링(Piecewise Constant Scheduling)

    ● 일정 횟수의 에포크 동안 일정한 학습률 사용

    ● 다음 에포크 동안 작은 학습률을 사용하는 방법

    ● 5에포크 동안 η_{0} = 0.1 → 50에포크 동안 η_{1} = 0.001 과 같은 식으로 변경

○ 성능 기반 스케줄링(Performance Scheduling)

    ● 매 N 스텝마다(like 조기 종료) 검증 오차를 측정하고 오차가 줄어들지 않으면 λ배 만큼 η를 감소

1 사이클 스케줄링(1Cycle Scheduling)

    ● 1 사이클은 훈련 절반 동안 초기 학습률 η_{0}을 선형적으로 η_{1}까지 증가시킴

    ● 나머지 절반 동안 선형적으로 학습률을 η_{0}까지 다시 줄임

    ● 마지막 몇 번의 에포크는 η를 소수점 몇 째 자리까지 줄임

    ● 최대 학습률(η_{1})은 최적의 학습률을 찾을 때와 같은 방식을 사용해 선택

    ● 초기 학습률(η_{0})은 대략 10배 정도 낮은 값을 선택

    ● 모멘텀을 사용할 때는 처음에 높은 모멘텀으로 시작해서 훈련의 처음 절반 동안 낮은 모멘텀으로 줄임

    ● 다시 나머지 훈련 절반 동안 최댓값으로 되돌림

    ● 마지막 몇 번의 에포크는 최댓값으로 진행

더보기

- 거듭 제곱 스케줄링

optimizer = keras.optimizers.SGD(learning_rate=0.01, decay=1e-4)
model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"])
n_epochs = 25
history = model.fit(X_train_scaled, y_train, epochs=n_epochs, validation_data=(X_valid_scaled, y_valid))
import math

learning_rate = 0.01
decay = 1e-4
batch_size = 32
n_steps_per_epoch = math.ceil(len(X_train) / batch_size)
epochs = np.arange(n_epochs)
lrs = learning_rate / (1 + decay * epochs * n_steps_per_epoch)

plt.plot(epochs, lrs,  "o-")
plt.axis([0, n_epochs - 1, 0, 0.01])
plt.xlabel("Epoch")
plt.ylabel("Learning Rate")
plt.title("Power Scheduling", fontsize=14)
plt.grid(True)
plt.show()

 

- 지수 기반 스케줄링

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


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)
model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam", metrics=["accuracy"])
n_epochs = 25
lr_scheduler = keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train_scaled, y_train, epochs=n_epochs,
                    validation_data=(X_valid_scaled, y_valid),
                    callbacks=[lr_scheduler])
plt.plot(history.epoch, history.history["lr"], "o-")
plt.axis([0, n_epochs - 1, 0, 0.011])
plt.xlabel("Epoch")
plt.ylabel("Learning Rate")
plt.title("Exponential Scheduling", fontsize=14)
plt.grid(True)
plt.show()

 

-기간별 고정 스케줄링

def piecewise_constant_fn(epoch):
    if epoch < 5:
        return 0.01
    elif epoch < 15:
        return 0.005
    else:
        return 0.001
def piecewise_constant(boundaries, values):
    boundaries = np.array([0] + boundaries)
    values = np.array(values)
    def piecewise_constant_fn(epoch):
        return values[np.argmax(boundaries > epoch) - 1]
    return piecewise_constant_fn

piecewise_constant_fn = piecewise_constant([5, 15], [0.01, 0.005, 0.001])
lr_scheduler = keras.callbacks.LearningRateScheduler(piecewise_constant_fn)

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam", metrics=["accuracy"])
n_epochs = 25
history = model.fit(X_train_scaled, y_train, epochs=n_epochs,
                    validation_data=(X_valid_scaled, y_valid),
                    callbacks=[lr_scheduler])
plt.plot(history.epoch, [piecewise_constant_fn(epoch) for epoch in history.epoch], "o-")
plt.axis([0, n_epochs - 1, 0, 0.011])
plt.xlabel("Epoch")
plt.ylabel("Learning Rate")
plt.title("Piecewise Constant Scheduling", fontsize=14)
plt.grid(True)
plt.show()

 

- 성능 기반 스케줄링

tf.random.set_seed(42)
np.random.seed(42)
lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(10, activation="softmax")
])
optimizer = keras.optimizers.SGD(learning_rate=0.02, momentum=0.9)
model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"])
n_epochs = 25
history = model.fit(X_train_scaled, y_train, epochs=n_epochs,
                    validation_data=(X_valid_scaled, y_valid),
                    callbacks=[lr_scheduler])
plt.plot(history.epoch, history.history["lr"], "bo-")
plt.xlabel("Epoch")
plt.ylabel("Learning Rate", color='b')
plt.tick_params('y', colors='b')
plt.gca().set_xlim(0, n_epochs - 1)
plt.grid(True)

ax2 = plt.gca().twinx()
ax2.plot(history.epoch, history.history["val_loss"], "r^-")
ax2.set_ylabel('Validation Loss', color='r')
ax2.tick_params('y', colors='r')

plt.title("Reduce LR on Plateau", fontsize=14)
plt.show()

 

- tf.keras 스케줄링

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(10, activation="softmax")
])
s = 20 * len(X_train) // 32 # number of steps in 20 epochs (batch size = 32)
learning_rate = keras.optimizers.schedules.ExponentialDecay(0.01, s, 0.1)
optimizer = keras.optimizers.SGD(learning_rate)
model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"])
n_epochs = 25
history = model.fit(X_train_scaled, y_train, epochs=n_epochs,
                    validation_data=(X_valid_scaled, y_valid))

※ 구간별 고정 스케줄링의 경우 아래 코드 사용

learning_rate = keras.optimizers.schedules.PiecewiseConstantDecay(
    boundaries=[5. * n_steps_per_epoch, 15. * n_steps_per_epoch],
    values=[0.01, 0.005, 0.001])

 

- 1사이클 스케줄링

K = keras.backend

class ExponentialLearningRate(keras.callbacks.Callback):
    def __init__(self, factor):
        self.factor = factor
        self.rates = []
        self.losses = []
    def on_batch_end(self, batch, logs):
        self.rates.append(K.get_value(self.model.optimizer.lr))
        self.losses.append(logs["loss"])
        K.set_value(self.model.optimizer.lr, self.model.optimizer.lr * self.factor)

def find_learning_rate(model, X, y, epochs=1, batch_size=32, min_rate=10**-5, max_rate=10):
    init_weights = model.get_weights()
    iterations = math.ceil(len(X) / batch_size) * epochs
    factor = np.exp(np.log(max_rate / min_rate) / iterations)
    init_lr = K.get_value(model.optimizer.lr)
    K.set_value(model.optimizer.lr, min_rate)
    exp_lr = ExponentialLearningRate(factor)
    history = model.fit(X, y, epochs=epochs, batch_size=batch_size,
                        callbacks=[exp_lr])
    K.set_value(model.optimizer.lr, init_lr)
    model.set_weights(init_weights)
    return exp_lr.rates, exp_lr.losses

def plot_lr_vs_loss(rates, losses):
    plt.plot(rates, losses)
    plt.gca().set_xscale('log')
    plt.hlines(min(losses), min(rates), max(rates))
    plt.axis([min(rates), max(rates), min(losses), (losses[0] + min(losses)) / 2])
    plt.xlabel("Learning rate")
    plt.ylabel("Loss")

※ 텐서플로 2.2 이상이라면 ExponentialLearningRate 클래스를 아래의 코드로 바꾸어 사용

    ● 이유: on_batch_end() 메서드에서 logs["loss"]로 배치 손실을 모으지만 텐서플로 2.2.0에서 (에포크의) 평균 손실로 변경

class ExponentialLearningRate(keras.callbacks.Callback):
    def __init__(self, factor):
        self.factor = factor
        self.rates = []
        self.losses = []
    def on_epoch_begin(self, epoch, logs=None):
        self.prev_loss = 0
    def on_batch_end(self, batch, logs=None):
        batch_loss = logs["loss"] * (batch + 1) - self.prev_loss * batch
        self.prev_loss = logs["loss"]
        self.rates.append(K.get_value(self.model.optimizer.lr))
        self.losses.append(batch_loss)
        K.set_value(self.model.optimizer.lr, self.model.optimizer.lr * self.factor)

 

- 1사이클 스케줄링 모델 훈련

tf.random.set_seed(42)
np.random.seed(42)

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal"),
    keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy",
              optimizer=keras.optimizers.SGD(learning_rate=1e-3),
              metrics=["accuracy"])
batch_size = 128
rates, losses = find_learning_rate(model, X_train_scaled, y_train, epochs=1, batch_size=batch_size)
plot_lr_vs_loss(rates, losses)
class OneCycleScheduler(keras.callbacks.Callback):
    def __init__(self, iterations, max_rate, start_rate=None,
                 last_iterations=None, last_rate=None):
        self.iterations = iterations
        self.max_rate = max_rate
        self.start_rate = start_rate or max_rate / 10
        self.last_iterations = last_iterations or iterations // 10 + 1
        self.half_iteration = (iterations - self.last_iterations) // 2
        self.last_rate = last_rate or self.start_rate / 1000
        self.iteration = 0
    def _interpolate(self, iter1, iter2, rate1, rate2):
        return ((rate2 - rate1) * (self.iteration - iter1)
                / (iter2 - iter1) + rate1)
    def on_batch_begin(self, batch, logs):
        if self.iteration < self.half_iteration:
            rate = self._interpolate(0, self.half_iteration, self.start_rate, self.max_rate)
        elif self.iteration < 2 * self.half_iteration:
            rate = self._interpolate(self.half_iteration, 2 * self.half_iteration,
                                     self.max_rate, self.start_rate)
        else:
            rate = self._interpolate(2 * self.half_iteration, self.iterations,
                                     self.start_rate, self.last_rate)
        self.iteration += 1
        K.set_value(self.model.optimizer.lr, rate)
n_epochs = 25
onecycle = OneCycleScheduler(math.ceil(len(X_train) / batch_size) * n_epochs, max_rate=0.05)
history = model.fit(X_train_scaled, y_train, epochs=n_epochs, batch_size=batch_size,
                    validation_data=(X_valid_scaled, y_valid),
                    callbacks=[onecycle])

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

10장에서 이미 규제방법 中 조기 종료를 구현해보았습니다.

○ 배치 정규화는 불안정한 그레디언트 문제를 해결하기 위해 고안되었지만 꽤 괜찮은 규제 방법으로도 활용이 가능

○ 해당 부분에서는 신경망에서 널리 사용되는 L1 & L2 규제, 드롭아웃, 맥스-노름 규제에 대해 알아봅니다.

 

4-1 L1과 L2 규제

4장에서 간단한 선형 모델에 했던 것처럼 신경망의 연결 가중치를 제안하기 위해 사용하는 방법

    ● L2규제 사용 or 많은 가중치가 0인 희소 모델을 만들기 위해 L1 규제를 사용할 수 있었음

○ 아래 코드는 케라층의 연결 가중치에 규제 강도를 0.01을 사용해 L2 규제를 적용하는 방법

    ● l2() 함수는 훈련 동안 규제 손실을 계산하기 위해 각 스텝에서 호출되는 규제 객체를 반환

    ● IF L1 규제가 필요 THEN keras.regularizers.l1() 사용

    ● IF L1, L2 규제 모두 필요 THEN keras.regularizers.l1_l2() 사용(두 개의 규제 강도 모두를 지정해야함)

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

○ 일반적으로 동일한 매개변수 값을 반복하는 경우가 多

∵ 네트워크의 모든 은닉층에 동일한 활성화 함수/초기화 전략, 모든 층에 동일한 규제를 적용하기 때문

    ● 코드 가시성이 떨어지고 버그를 만들기 쉬움

    ● 해결을 위해 반복문을 사용하도록 리팩터링 할 수 있음

    ● 다른 방법: functools.partial() 함수 사용 → 기본 매개변수 값을 사용해 함수 호출 감싸기

[코드]

더보기
model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="elu",
                       kernel_initializer="he_normal",
                       kernel_regularizer=keras.regularizers.l2(0.01)),
    keras.layers.Dense(100, activation="elu",
                       kernel_initializer="he_normal",
                       kernel_regularizer=keras.regularizers.l2(0.01)),
    keras.layers.Dense(10, activation="softmax",
                       kernel_regularizer=keras.regularizers.l2(0.01))
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam", metrics=["accuracy"])
n_epochs = 2
history = model.fit(X_train_scaled, y_train, epochs=n_epochs,
                    validation_data=(X_valid_scaled, y_valid))
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")
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam", metrics=["accuracy"])
n_epochs = 2
history = model.fit(X_train_scaled, y_train, epochs=n_epochs,
                    validation_data=(X_valid_scaled, y_valid))

4-2 드롭아웃(dropout)

○ 심층 신경망에서 가장 인기 있는 규제 기법 中 1

○ 매 훈련 스텝에서 각 뉴런 (입력 뉴런은 포함 & 출력 뉴런은 제외)은 임시적으로 드롭아웃될 확률 p를 가짐

    ● 즉, 이번 훈련 스텝에는 완전히 무시되지만 다음 스텝에는 활성화될 수 있음

    ● 하이퍼파라미터 p : 드롭아웃 비율(dropout rate)이라 하며 보통 10% ~ 50% 사이를 지정

        - 순환 신경망에서는 20 ~ 30%에 근접

        - 합성곱 신경망에서는 40 ~ 50%에 근접

    ● 훈련이 끝난 후에는 뉴런에 더는 드롭아웃을 적용하지 않음

○ 드롭아웃으로 훈련된 뉴런은 이웃한 뉴런에 맞추어 적응될 수 없음

    ● 즉, 가능한 한 자기 자신이 유용해져야 함

    ● 또한 이런 뉴런들은 몇 개의 입력 뉴런에만 지나치게 의존할 수 없음(모든 뉴런에 주의를 기울여야 함)

    ● So, 입력값의 작은 변화에 덜 민감해짐 → 더 안정적인 네트워크가 되어 일반화 성능이 좋아짐

○ 드롭아웃 이해하는 방법(각 훈련 스텝에서 고유한 네트워크가 생성된다고 생각)

    ● 개개의 뉴런이 있을 수도, 없을 수도 있기 때문에 2^{N}개의 네트워크가 가능

    ● 매우 큰 값이어서 같은 네트워크가 두 번 선택될 가능성은 사실상 없음

    ● 이 신경망은 대부분의 가중치를 공유하고 있기 때문에 아주 독립적이지 않음(But 모두 다름)

    ● 결과적으로 만들어진 신경망은 모든 신경망을 평균한 앙상블로 볼 수 있음

※ N = 드롭아웃이 가능한 뉴런 수

※ 일반적으로 (출력층을 제외한) 맨 위의 층 ~ 3 번째 층까지 있는 뉴런에만 드롭아웃을 적용

○ 사소하지만 중요한 기술적 세부사항

    ● p = 50%로 하면 테스트 동안에는 하나의 뉴런이 훈련 때보다 두 배 많은 입력 뉴런과 연결됨

    ● 이런 점을 보상하기 위해 훈련 후 각 뉴런의 연결 가중치에 0.5를 곱할 필요가 있음

    ● 즉, 훈련이 끝난 뒤 각 입력의 연결 가중치에 보존 확률(1 - p)를 곱해야 함

    ● 또는 훈련하는 동안 각 뉴런의 출력을 보존 확률로 나눌 수도 있음

○ In Keras:

    ● keras.layers.Dropout 층을 사용해 드롭아웃을 구현 → 이 층은 훈련하는 동안 일부 입력을 랜덤하게 버림

    ● 남은 입력을 보존 확률로 나눔 → 훈련이 끝난 후에는 어떤 작업도 하지 않음(단순 입력 전달) 

※ 각 훈련 반복에서 (출력층을 제외하고) 하나 이상의 층에 있는 모든 뉴런의 일부를 제거

※ 이런 뉴런은 이 반복에서 0을 출력(파선으로 표시되어 있음)

○ 드롭아웃 비율 0.2를 사용한 드롭아웃 규제를 모든 Dense 층 이전에 적용하는 코드

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")
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam", metrics=["accuracy"])
n_epochs = 2
history = model.fit(X_train_scaled, y_train, epochs=n_epochs,
                    validation_data=(X_valid_scaled, y_valid))
드롭아웃
○ 훈련하는 동안에만 활성화 So, 훈련 손실과 검증 손실을 비교하면 오해를 일으키기 쉬움
○ 비슷한 훈련 손실과 검증 손실을 얻었더라도 모델이 훈련 세트에 과대적합될 수 있음
○ So, (e.g. 훈련이 끝난 후) 드롭아웃을 빼고 훈련 손실을 평가해야 함

○ If 모델이 과대적합 THEN 드롭아웃 비율을 늘릴 수 있음

   Else 모델이 과소적합 THEN 드롭아웃 비율 낮추기

○ 많은 최신의 신경망 구조 = 마지막 은닉층 뒤에만 드롭아웃을 사용

○ 드롭아웃은 수렴을 상당히 느려지게 만드는 경향이 있지만 적절하게 튜닝하면 훨씬 만듦

    ● So, 일반적으로 추가적인 시간과 노력을 기울일 가치가 있음

만약 SELU 활성화 함수를 기반으로 자기 정규화하는 네트워크를 규제하고 싶다?
○ 알파 드롭아웃을 사용해야함. → 입력의 평균과 표준편차를 유지하는 드롭아웃의 한 변종

 

4-3 몬테 카를로 드롭아웃

○ 야린 갤과 주빈 가라마니의 2016년 논문에서 드롭아웃을 사용해야할 이유를 소개

    ● 드롭아웃을 수학적으로 정의하여 드롭아웃 네트워크(모든 가중치 층 이전에 Dropout층을 포함한 신경망)와
        근사 베이즈 추록 사이의 깊은 관련성을 추론

    ● 드롭아웃 모델을 재훈련하거나 수정하지 않고 성능을 크게 향상시킬 수 있는
       몬테 카를로 드롭아웃(MC 드롭아웃)이라는 강력한 기법 소개

○ 과정

    ● training=True로 지정 → Dropout 층을 활성화 & 테스트 세트에서 100번의 예측을 생성하여 쌓기

※ 드롭아웃이 활성화 So, 예측이 모두 달라짐

    ● predict() 메서드는 {샘플 = 행, 클래스 = 열}인 행렬을 반환

 

[코드]

더보기
tf.random.set_seed(42)
np.random.seed(42)

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)

np.round(model.predict(X_test_scaled[:1]), 2)

np.round(y_probas[:, :1], 2)
np.round(y_proba[:1], 2)
y_std = y_probas.std(axis=0)
np.round(y_std[:1], 2)
y_pred = np.argmax(y_proba, axis=1)

accuracy = np.sum(y_pred == y_test) / len(y_test)
accuracy # 0.8267
class MCDropout(keras.layers.Dropout):
    def call(self, inputs):
        return super().call(inputs, training=True)

class MCAlphaDropout(keras.layers.AlphaDropout):
    def call(self, inputs):
        return super().call(inputs, training=True)
tf.random.set_seed(42)
np.random.seed(42)

mc_model = keras.models.Sequential([
    MCAlphaDropout(layer.rate) if isinstance(layer, keras.layers.AlphaDropout) else layer
    for layer in model.layers
])

mc_model.summary()
optimizer = keras.optimizers.SGD(learning_rate=0.01, momentum=0.9, nesterov=True)
mc_model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"])

mc_model.set_weights(model.get_weights())
np.round(np.mean([mc_model.predict(X_test_scaled[:1]) for sample in range(100)], axis=0), 2)
MCDropout 클래스는 시퀀셜 API를 포함해 모든 케라스 API를 사용 가능
○ 함수형 API나 서브클래싱 API를 사용할 때는 MCDropout 클래스를 사용할 필요가 없음
○ 보통 Dropout층을 만들고 training=True를 사용해 호출 가능

 

4-4 맥스- 노름 규제(Max-Rem Regularization)

○ 각각 뉴런에 대해 입력의 연결 가중치(w)가 ||w|| _{2} ≤ r 이 되도록 제한

    ● r = 맥스-노름 하이퍼파라미터

    ● ||.||_{2} = L2노름

○ 맥스-노름 규제는 전체 손실 함수에 규제 손실 항을 추가하지 않음

    ● But, 일반적으로 매 훈련 스텝이 끝나고, ||w||_{2}를 계산 → IF need THEN w's 스케일을 조정

$$ w \leftarrow w \frac{r} { \left\| w \right\|_{2}} $$

    IF  r을 줄이면 THEN 규제의 양 증가 So, 과대적합을 감소시키는데 도움

    ● 맥스-노름 규제 = (배치정규화를 사용하지 않았을 때)불안정한 그레디언트 문제를 완화하는데 도움

layer = keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal",
                           kernel_constraint=keras.constraints.max_norm(1.))

○ 매 훈련 반복이 끝난 후 모델의 fit() 메서드가 층의 가중치와 함께 max_norm()이 반환하는 객체 반환받음

    ● 반환한 객체 호출 → 스케일이 조정된 가중치를 반환받음 → 이 값을 사용하여 층의 가중치를 변경

    ● 사용자 정의 규제 함수를 정의하고 kernel_constraint 매개변수에 이를 지정 가능

    ● bias_constraint 매개변수에 지정하여 편향 규제도 가능

○ axis = 0을 사용하면 맥스-노름 규제는 각 뉴런의 가중치 벡터에 독립적으로 적용

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

○ 작업마다 사용하면 좋은 기법이 다름 & 선택에 명확한 기준 X

○ 아래 표에 하이퍼파라미터 튜닝을 크게 하지 않고 대부분의 경우에 잘 맞는 설정을 정리

[기본 DNN 설정]

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

    ● 네트워크가 완전 연결 층을 쌓은 단순한 모델이라면 자기 정규화 사용 가능

[자기 정규화를 위한 DNN 설정]

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

※ 입력 특성을 정규화해야함

※ 비슷한 문제를 해결한 모델을 찾을 수 있다면 사전훈련된 신경망의 일부를 재사용해보아야 함

※ IF 레이블이 없는 데이터가 多 THEN 비지도 사전훈련 사용

○ 예외 1)

    ● IF 희소모델이 필요 THEN L1규제 사용 가능

    ● IF 매우 희소한 모델 필요 THEN 텐서플로 모델 최적화 툴깃 사용 가능

    ● 자기 정규화를 깨뜨리므로 이 경우 기본 DNN 설정을 사용해야함

○ 예외 2)

    ● IF 빠른 응답 모델 필요 THEN 층 개수 줄이고 배치 정규화 층을 이전 층에 합치기

    ● LeakyReLU | ReLU와 같이 빠른 활성화 함수 이용

    ● 희소 모델 생성도 도움이 될 것

    ● 부동 소수점 정밀도를 32 → 8 비트로 낮추는 것도 도움이 될 것

○ 예외 3)

    ● IF 위험에 민감 & 예측 속도가 중요하지 않음

    ● 불확실성 추정 & 신뢰할 수 있는 확률 추정을 얻기 위해 MC Dropout을 사용할 수 있음