Hands-On Machine Learning 정리 - 딥러닝(Chapter 11: 심층 신경망 훈련하기)
10장에서는 인공 신경망 소개와 첫 번째 심층 신경망을 훈련했습니다(몇 개의 은닉층만으로 이루어진 얕은 네트워크)
고해상도 이미지에서 수백 종류의 물체를 감지하는 것처럼 복잡한 문제를 다뤄야 한다면 어떻게 해야할까?
[훈련 중 마주칠 수 있는 문제들]
● 까다로운 그레디언트 소실 or 그레디언트 폭주 문제에 직면 가능
- 두 현상 모두 하위층을 훈련하기 어렵게 함
● 대규모 신경망을 위한 훈련 데이터가 충분하지 않거나 레이블을 만드는 작업에 비용이 많이 들 수 있음
● 훈련이 극단적으로 느려질 수 있음
● 수백만 개의 파라미터를 가진 모델은 훈련 세트에 과대적합될 위험이 高
(특히, 훈련 샘플 부족이나 잡음이 많은 경우 심함)
아래의 목차와 같이 이와 관련한 내용을 정리할 예정입니다.
Chap 11.
- 배치 정규화
3. 고속 옵티마이저
- 모멘텀 최적화
- AdaGrad
- RMSProp
- 학습률 스케줄링
- 드롭아웃
- 맥스-노름 규제
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을 사용할 수 있음