고해상도 이미지에서 수백 종류의 물체를 감지하는 것처럼 아주 복잡한 문제를 다뤄야 한다면
수백 개의 뉴런으로 구성된 10개 이상의 층을 수십만 개의 가중치로 연결해 훨씬 더 깊은 심층 신경망을 훈련해야 할 것입니다. 심층 신경망 훈련은 쉽지 않습니다. 훈련 중 다음과 같은 문제를 맞닥뜨릴 수 있습니다.
- 까다로운 그레이디언트 소실 또는 그레이디언트 폭주 문제에 직면할 수 있습니다. (신경망의 아래쪽으로 갈수록 그레이디언트가 점점 더 작아지거나 커지는 현상입니다.)
- 대규모 신경망을 위한 훈련 데이터가 충분하지 않거나 레이블을 만드는 작업에 비용이 많이 들 수 있습니다.
- 훈련이 극단적으로 느려질 수 있습니다.
- 모델이 너무 복잡해 과대적합될 위험이 큽니다. 특히 훈련 샘플이 충분하지 않거나 잡음이 많은 경우에 그렇습니다.
그레이디언트 소실과 폭주 문제
역전파 알고리즘은 출력층에서 입력층으로 오차 그레이디언트를 전파하면서 진행됩니다.
그런데 알고리즘이 하위층으로 진행될수록 그레이디언트가 점점 작아지는 경우가 많습니다.
경사 하강법이 하위층의 연결 가중치를 변경되지 않은 채로 둔다면 좋은 솔루션으로 수렴하기 어렵습니다.
이 문제를 그레이디언트 소실이라고 합니다.
어떤 경우엔 반대 현상이 일어나서 여러 층이 비정상적으로 큰 가중치로 갱신되면 알고리즘이 발산합니다.
이 문제를 그레이디언트 폭주라고 하고 주로 순환 신경망에서 나타납니다.
글로럿과 HE 초기화
글로럿과 벤지오는 논문에서 불안정한 그레이디언트 문제를 크게 완화하는 방법을 제안합니다.
예측을 할 때는 정방향으로, 그레이디언트를 역전파할때는 역방향으로 양방향 신호가 적절하게 흘러야 합니다.
신호가 죽거나 폭주 또는 소멸하지 않아야 합니다.
논문에서 적절한 신호가 흐르기 위해서는 각 층의 출력에 대한 분산이 입력에 대한 분산과 같아야 한다고 주장합니다.
그리고 역방향에서 층을 통과하기 전과 후의 그레이디언트 분산이 동일해야 합니다.
그런데 층의 입력과 출력 연결 개수(이 개수를 fan-in과 fan-out이라 합니다.)가 같지 않다면
위 두가지를 보장하기 힘듭니다.
저자들은 실전에서 매우 잘 작동한다고 입증된 대안을 제안했는데,
바로 각 층의 연결 가중치를 아래 식과 같이 무작위로 초기화하는 것입니다.
이 초기화 방법을 세이비어 초기화 또는 글로럿 초기화라고 합니다.
\[ \displaystyle \hbox{평균이 0이고 분산이 } \sigma^{2} = \frac{1}{fan_{avg}} \]
\[ \displaystyle \hbox{ 또는 } r = \sqrt{\frac{3}{fan_{avg}}} \hbox{일 때 -r과 +r 사이의 균등분포} \]
몇 비슷한 전략들 또한 제안되었는데, 표에서 볼 수 있듯이 거의 비슷합니다.
초기화 전략 | 활성화 함수 |
글로럿 | 활성화 함수 없음, 하이퍼볼릭 탄젠트, 로지스틱, 소프트맥스 |
He | ReLU 함수와 그 변종들 |
르쿤 | SELU |
케라스는 기본적으로 균등분포의 글로럿 초기화를 사용합니다.
다음과 같이 층을 만들 때
kernel_initializer='he_uniform' 또는 kernel_initializer='he_normal'로 설정해주면 He 초기화를 쓸 수 있습니다.
keras.layers.Dense(10, activation='relu', kernel_initializer='he_normal')
\(fain_{in}\)가 아닌 \(fain_{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)
수렴하지 않는 활성화 함수
활성화 함수를 잘못 선택하면 그레이디언트의 소실이나 폭주로 이어질 수 있습니다.
이전에는 대부분 뉴런의 방식과 비슷한 시그모이드 활성화 함수가 최선의 선택일 것이라고 생각했으나,
다른 활성화 함수가 심층 신경망에서 훨씬 더 잘 작동한다는 사실이 밝혀졌습니다.
특히 ReLU는 특정 양숫값에 수렴하지 않는다는 장점이 있습니다. (계산도 빠릅니다.)
그런데 ReLU 함수는 완벽하지 않습니다.
죽은 ReLU로 알려진 문제가 있습니다.
훈련하는 동안 일부 뉴런이 0 이외의 값을 출력하지 않는다는 의미에서 죽었다고 말합니다.
뉴런의 가중치가 바뀌어서 훈련 세트에 있는 모든 샘플에 대해 입력의 가중치 합이 음수가 되면 뉴런이 죽게 됩니다.
가중치 합이 음수이면 ReLU 함수의 그레이디언트가 0이라 경사 하강법이 더 일어나지 않습니다.
이 문제의 해결을 위해 LeakyReLU와 같은 ReLU 함수의 변종을 사용합니다.
\[ \displaystyle LeakyReLU_{\alpha}(z) = max(\alpha z, z) \]
하이퍼파라미터 \(\alpha\)가 이 함수가 새는 정도를 결정합니다.
새는 정도는 \(z < 0\)일 때 기울기이며, 일반적으로 0.01로 설정합니다.
이 작은 기울기가 LeakyReLU가 절대 죽지 않게 만듭니다.
뉴런이 혼수 상태에 있을 수는 있지만 다시 깨어날 가능성을 얻게 됩니다.
최근 한 논문에서 여러 ReLU함수의 변종을 비교해 얻은 결론 하나는
LeakyReLU가 ReLU보다 항상 성능이 좋다는 것입니다.
그리고 사실 \(\alpha = 0.2\)로 설정해 많이 통과하는 것이
\(alpha = 0.01\)로 설정해 조금 통과하는 것보다 성능이 좋은 것으로 보입니다.
해당 논문에서 \(\alpha\)를 무작위로 선택하고 테스트 시에는 평균을 사용하는 RReLU(randomized leaky ReLU)도 평가했는데, 이 함수도 꽤 잘 작동했고 규제의 역할을 하는 것으로 보였습니다.
또한 \(\alpha\)가 훈련하는 동안 학습되는 PReLU(parametric leaky ReLU)도 비교했습니다. 이 함수는 대규모 이미지 데이터셋에는 ReLU보다 성능이 크게 앞섰지만 소규모 데이터셋에는 훈련 세트에 과대적합될 위험이 있습니다.
마지막으로 툐르크-아르네 클레베르트 등의 2015년 논문은 ELU라는 새로운 활성화 함수를 제안했습니다.
이 함수는 저자들의 실험에서 다른 모든 ReLU 변종의 성능을 앞질렀습니다.
훈련 시간이 줄고 신경망의 테스트 세트 성능도 높았습니다.
ELU는 다음과 같습니다.
\[ \displaystyle ELU_{\alpha}(z) = \begin{cases} \alpha(exp(z) - 1), & z < 0 \\ z, & z \geq 0 \end{cases} \]
이 함수는 몇 가지를 제외하고는 ReLU와 매우 비슷합니다.
- \(z < 0\)일 때 음숫값이 들어오므로 활성화 함수의 평균 출력이 0에 더 가까워집니다. 이는 앞서 이야기한 그레이디언트 소실 문제를 완화해줍니다. \(\alpha\)는 \(z\)가 큰 음숫값일 때 ELU가 수렴할 값을 정의합니다. 보통 1로 설정하지만 다른 값으로도 설정 가능합니다.
- \(z < 0\)이어도 그레이디언트가 0이 아니므로 죽은 뉴런을 만들지 않습니다.
- \(\alpha = 1\)이면 이 함수는 \(z = 0\)에서 급격히 변동하지 않으므로 \(z = 0\)을 포함해 모든 구간에서 매끄러워 경사 하강법의 속도를 높여줍니다.
이 활성화 함수의 주요 단점은 지수 함수를 써서 ReLU나 다른 변종들보다 계산이 느리다는 것입니다. 훈련 동안에는 수렴 속도가 빠르기 때문에 느린 계산이 상쇄되지만 테스트 시에는 ELU를 사용한 네트워크가 다른 네트워크들보다 더 느릴 것입니다.
귄터 클람바우어 등의 논문에서는 SELU(Scaled ELU) 활성화 함수를 소개했습니다.
이름이 의미하듯 이 함수는 스케일이 조정된 ELU 함수의 변종입니다.
저자들은 완전 연결 층만 쌓아서 신경망을 만들고 모든 은닉층이 SELU 활성화 함수를 사용한다면 네트워크가 자기 정규화(self-normalize)된다는 것을 알아냈습니다.
훈련하는 동안 각 층의 출력이 평균 0과 표준편차 1을 유지하는 경향이 있습니다.
이는 그레이디언트 소실과 폭주 문제를 막아줍니다.
그 결과로 SELU 활성화 함수는 이런 종류의 네트워크(특히 아주 깊은 네트워크)에서 다른 활성화 함수보다 뛰어난 성능을 종종 내곤 합니다. 그러나 자기 정규화가 일어나기 위한 몇 가지 조건이 있습니다.
- 입력 특성이 반드시 표준화(평균 0, 표준편차 1)이 돼야 합니다.
- 모든 은닉층의 가중치는 르쿤 정규분포 초기화로 초기화돼야 합니다. 케라스에서는 kernel_initializer='lecun_normal'로 설정합니다.
- 네트워크는 일렬로 쌓은 층으로 구성돼야 합니다. 순환 신경망이나 스킵 연결과 같은 순차적이지 않은 구조에 SELU를 사용하면 정규화되는 것이 보장되지 않습니다.
LeakyReLU 활성화 함수를 사용하려면 LeakyReLU 층을 만들고 모델에서 적용하려는 층 뒤에 추가합니다.
model = keras.models.Sequential([
[...]
keras.layers.Dense(10, kernel_initializer='he_normal'),
keras.layers.LeakyReLU(alpha=0.2),
[...]
])
SELU 활성화 함수를 사용하려면 층을 만들 때 아래와 같이 하면 됩니다.
keras.layers.Dense(10, activation='selu', kernel_initializer='lecun_normal')
배치 정규화
ELU 등과 함께 HE 초기화를 사용하면 훈련 초기 단계에서 그레이디언트 소실이나 폭주 문제를 크게 감소시킬 수 있지만, 훈련 동안 발생하지 않으리란 보장이 없습니다.
세르게이 이오페와 치리슈티언 세게지의 논문에서 그레이디언트 소실과 폭주 문제를 막기 위한 배치 정규화(Batch Normalization, BN)기법을 제안했습니다.
이 기법은 각 층에서 활성화 함수를 통과하기 전이나 후에 연산을 하나 추가합니다.
이 연산은 단순히 입력을 원점에 맞추고 정규화한 다음,
각 층에서 두 개의 새로운 파라미터로 결괏값의 스케일을 조정하고 이동시킵니다.
하나는 스케일 조정에, 다른 하나는 이동에 사용합니다.
많은 경우 첫 번째 층으로 배치 정규화를 추가하면 훈련 세트를 표준화할 필요가 없습니다.
입력 데이터를 원점에 맞추고 정규화하려면 알고리즘은 평균과 표준편차를 추정해야 합니다.
이를 위해 현재 미니배치에서 입력의 평균과 표준편차를 평가합니다.
배치 정규화 알고리즘을 요약하면 아래와 같습니다.
- \( \displaystyle \mu_{B} = \frac{1}{m_{B}}\sum^{m_{B}}_{i=1}x^{(i)} \)
- \( \displaystyle \sigma_{B}^{2} = \frac{1}{m_{B}}\sum^{m_{B}}_{i=1}(x^{(i)} - \mu_{B})^{2} \)
- \( \displaystyle \hat{x}^{(i)} = \frac{x^{(i)} - \mu_{B}}{\sqrt{\sigma_{B}^{2} + \varepsilon}} \)
- \( \displaystyle z^{(i)} = \gamma \bigotimes \hat{x}^{(i)} + \beta \)
위 식들을 살펴봅시다.
- \(\mu_{B}\): 미니배치 B에 대해 평가한 입력의 평균 벡터
- \(\sigma_{B}\): 미니배치 B에 대해 평가한 입력의 표준편차 벡터
- \(m_{B}\): 미니배치에 있는 샘플 수
- \(\hat{x}^{(i)}\): 평균이 0이고 정규화된 샘플 i의 입력
- \(\gamma\): 층의 출력 스케일 파라미터 벡터 (입력마다 하나씩 있습니다.)
- \(\bigotimes\): 원소별 곱셈 (각 입력은 해당되는 출력 스케일 파라미터와 곱해집니다.)
- \(\beta\): 층의 출력 이동 파라미터 벡터 (입력마다 하나씩 있습니다.) 각 입력은 해당 파라미터만큼 이동합니다.
- \(\varepsilon\): 분모가 0이 되는 것을 막기 위한 작은 숫자(보통 \(10^{-5}\)), 안전을 위한 항이라고 합니다.
- \(z^{(i)}\): 배치 정규화 연산의 출력
훈련하는 동안 배치 정규화는 입력을 정규화한 다음 스케일을 조정하고 이동시킵니다.
테스트 시에는 간단하지는 않습니다.
샘플의 배치가 아니라 샘플 하나에 대한 예측을 만들어야 합니다.
이 경우 입력의 평균과 표준편차를 계산할 방법이 없습니다.
샘플의 배치를 사용한다 하더라도 매우 작거나 독립 동일 분포 조건을 만족하지 못할 수 있습니다.
이런 배치 샘플에서 계산한 통계는 신뢰도가 떨어집니다.
한 가지 방법은 훈련이 끝난 후 전체 훈련 세트를 신경망에 통과시켜 배치 정규화 층의 각 입력에 대한 평균과 표준편차를 계산하는 것입니다. 예측할 때 배치 입력 평균과 표준 편차로 이 최종 입력 평균과 표준편차를 대신 사용합니다.
대부분 배치 정규화 구현은 입력 평균과 표준편차의 이동 평균을 사용해 훈련하는 동안 최종 통계를 추정합니다. 케라스의 BuildNormalization은 이를 자동으로 수행합니다.
배치 정규화를 사용하면 그레이디언트 소실 문제가 크게 감소하여 하이퍼볼릭 탄젠트나 로지스틱 활성화 함수 같은 수렴성을 가진 활성화 함수를 사용할 수 있고, 가중치 네트워크가 훨씬 덜 민감해집니다. 마지막으로 배치 정규화는 규제와 같은 역할을 하여 다른 규제 기법의 필요성을 줄여줍니다.
그러나 배치 정규화는 모델의 복잡도를 키웁니다. 실행 시간 면에서도 손해입니다.
따라서 항상 배치 정규화를 쓰는 것이 아니라 장단점을 생각해서 잘 선택해야 합니다.
BarchNormalization 클래스는 조정할 하이퍼파라미터가 적습니다.
보통 기본값이 잘 작동하지만 momentum 매개변수를 조정해야 할 수도 있습니다.
BatchNormalization 층이 지수 이동 평균을 업데이트할 때 이 파라미터를 사용합니다.
새로운 값 \(v\)(현재 배치에서 계산한 새로운 입력 평균 벡터나 표준편차 벡터)가 주어지면 다음 식을 사용해 이동 평균\(\hat{v}\)를 업데이트합니다.
\[ \displaystyle \hat{v} \leftarrow \hat{v} \times momentum + v \times (1 - momentum) \]
적절한 모멘텀 값은 일반적으로 1에 가깝습니다.
예를 들어 0.9, 0.99, .... 등입니다. 데이터셋이 크고 미니배치가 작으면 끝에 9를 더넣어 1에 가깝게 합니다.
중요한 다른 파라미터는 axis입니다. 이 매개변수는 정규화할 축을 결정합니다.
기본값은 -1로, 다른 축을 계산한 평균과 표준편차를 이용해서 마지막 축을 정규화합니다.
배치 정규화 층은 훈련 도중과 훈련이 끝난 후에 수행하는 계산이 다릅니다.
훈련하는 동안 배치 통계를 사용하고 끝난 후에는 최종 통계를 사용합니다.
그레이디언트 클리핑
그레이디언트 폭주 문제를 완화하는 인기 있는 다른 방법은 역전파될 때 일정 임곗값을 넘어서지 못하게 그레이디언트를 잘라내는 것입니다. 이를 그레이디언트 클리핑이라고 합니다. 케라스에서 그레이디언트 클리핑을 구현하려면 옵티마이저를 만들 때 clipvalue와 clipnorm 매개변수를 지정하면 됩니다.
optimizer = keras.optimizers.SGD(clipvalue=1.0)
model.compile(loss='mse', optimizer=optimizer)
위와 같은 옵티마이저는 그레이디언트 벡터의 모든 원소를 -1 ~ 1 사이로 클리핑합니다.
즉 손실의 모든 편미분 값을 -1 ~ 1로 잘라냅니다.
임곗값은 하이퍼파라미터이기때문에 튜닝할 수 있습니다.
이 기능은 그레이디언트 벡터의 방향을 바꿀 수 있습니다.
예를 들어 원래 그레이디언트 벡터가 [0.9, 100.0]이라면 대부분 두 번째 축 방향을 향하는데,
클리핑을 하면 [0.9, 1.0]이 되고 두 축 사이 대각선 방향을 향합니다.
실전에서는 이 방식이 잘 작동하는데, 만약 그레이디언트 클리핑이 그레이디언트 벡터의 방향을 바꾸지 못하게 하려면
clipvalue대신 clipnorm을 지정해서 노름으로 클리핑해야합니다.
예를 들어 위와 같은 경우에서 clipnorm=1.0으로 지정한다면 벡터 [0.9, 100.0]이 [0.00899964, 0.9999595]로 클리핑되므로 방향을 그대로 유지합니다. 훈련하는동안 그레이디언트가 폭주한다면 다른 임곗값으로 값과 노름을 모두 사용하여 클리핑할 수 있습니다.
사전훈련된 층 재사용하기
일반적으로 아주 큰 규모의 DNN을 처음부터 새로 훈련하는 것은 좋은 생각이 아닙니다.
해결하려는 것과 비슷한 유형의 문제를 처리한 신경망이 이미 있는지 찾아본 다음,
그 신경망의 하위층을 재사용하는 것이 좋습니다.
이를 전이 학습(transfer learning)이라고 합니다.
보통 원본 모델의 출력층을 바꿔야 합니다.
이 층이 새로운 작업에 가장 유용하지 않는 층이고 새로운 작업에 필요한 출력 개수와 맞지 않을 수도 있습니다.
비슷하게 원본 모델의 상위 은닉층은 하위 은닉층보다 덜 유용합니다.
새로운 작업에 유용한 고수준 특성은 원본 작업에서 유용했던 특성과는 상당히 다르기 때문입니다.
재사용할 층 개수를 잘 선정하는 게 필요합니다.
먼저 재사용하는 층을 모두 동결합니다. (경사 하강법으로 가중치가 바뀌지 않도록 훈련되지 않는 가중치로 바꿉니다.)
그다음 모델을 훈련하고 성능을 평가합니다.
맨 위에 있는 한두개의 은닉층의 동결을 해제하고 역전파룰 통해 가중치를 조정해 성능이 향상되는지 확인합니다.
훈련 데이터가 많을수록 더 많은 층의 동결을 해제할 수 있습니다.
재사용 층의 동결을 해제할 때는 학습률을 줄이는 것이 좋습니다. (가중치를 세밀하게 튜닝하는 데 도움이 됩니다.)
이렇게 한 후에도 성능이 만족스럽지 않다면
상위 은닉층들을 제거하고 남은 은닉층을 다시 동결하여 성능 향상을 기대할 수 있습니다.
이런 식으로 재사용할 은닉층의 적절한 개수를 찾을 때까지 반복합니다.
케라스를 이용한 전이 학습
모델 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_B_on_A는 일부 층을 공유합니다.
model_B_on_A를 훈련하면 model_A도 영향을 받습니다.
이를 원하지 않으면 층을 재사용하기 전에 clone_model() 메소드로 모델의 구조를 복제한 후
set_weights() 메소드를 이용해 가중치를 복제해야 합니다.
model_A_clone = keras.models.clone_model(model_A)
model_A_clone.set_weights(model_A.get_weights())
이제 새로운 작업을 위해 model_B_on_A를 훈련할 수 있습니다.
그런데 새로 추가한 출력층이 랜덤하게 초기화되어있으므로 큰 오차를 만들 것입니다.
따라서 큰 오차 그레이디언트가 재사용된 부분의 가중치를 망칠 수 있습니다.
이를 해결하는 방법 중 하나는 재사용된 층들을 동결해두고 새로운 층에게 적절한 가중치를 학습하게 하는 것입니다.
이를 위해 재사용된 층들의 trainable 속성을 False로 설정하고 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'])
이제 몇 번의 에포크동안 모델을 훈련한 후 재사용된 층의 동결을 해제하고 모델을 다시 컴파일합니다.
작업 B에 맞게 모든 층을 세밀하게 튜닝하기 위한 훈련을 계속합니다.
일반적으로는 재사용된 층의 동결을 해제한 후에 학습률을 낮추는 것이 좋습니다.
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))
이렇게 쓰는 전이 학습은 작은 완전 연결 네트워크에서는 잘 동작하지 않습니다.
전이 학습은 조금 더 일반적인 특성을 감지하는 경향이 있는 심층 합성곱 신경망에서 잘 동작합니다.
비지도 사전훈련
레이블된 훈련 데이터가 많지 않은 복잡한 문제가 있을 때 더 많은 레이블된 훈련 데이터를 모으는 게 어렵다면 비지도 사전훈련을 수행할 수 있습니다. 레이블되지 않은 훈련 데이터를 많이 모을 수 있다면 이를 사용하여 오토인코더나 생성적 적대 신경망과 같은 비지도 학습 모델을 훈련할 수 있습니다. 그다음 오토인코더나 GAN 판별자의 하위층을 재사용하고 그 위에 새로운 작업에 맞는 출력층을 추가할 수 있습니다. 그다음 지도학습으로(레이블된 훈련 샘플을 이용해서) 최종 네트워크를 세밀하게 튜닝합니다.
보조 작업에서 사전훈련
레이블된 훈련 데이터가 많지 않다면 마지막 선택 사항은 레이블된 훈련 데이터를 쉽게 얻거나 생성할 수 있는 보조 작업에서 첫 번째 신경망을 훈련하는 것입니다.그리고 이 신경망의 하위 층을 실제 작업을 위해 재사용합니다. 첫 번째 신경망의 하위층은 두 번째 신경망에 재사용될 수 있는 특성 추출기를 학습하게 됩니다.
고속 옵티마이저
여태 훈련 속도를 높이는 네 가지 방법(연결 가중치에 좋은 초기화 전략 사용하기, 좋은 활성화 함수 사용하기, 배치 정규화 사용하기, 사전훈련된 네트워크의 일부 재사용하기)를 다뤘는데 또 다른 방법으로 표준적 경사 하강법 옵티마이저 대신 더 빠른 옵티마이저를 사용할 수 있습니다.
모멘텀 최적화
모멘텀 알고리즘은 다음과 같이 진행됩니다.
\[ m \leftarrow \beta m - \eta \nabla_{\theta}J(\theta) \]
\[ \theta \leftarrow \theta + m \]
모멘텀 최적화는 최적점에 도달할 때까지 점점 빠르게 내려갑니다.
배치 정규화를 사용하지 않는 심층 신경망에서 상위층은 종종 스케일이 매우 다른 입력을 받게 되는데, 모멘텀 최적화를 사용하면 이런 경우 큰 도움이 됩니다.
또한 모멘텀 최적화는 지역 최적점을 건너뛰는데도 도움이 됩니다.
케라스에서 모멘텀 최적화를 구현하는 것은 쉽습니다. 그냥 SGD 옵티마이저에 momentum 매개변수를 쓰면 됩니다.
optimizer = keras.optimizers.SGD(learning_rate=0.001, momentum=0.9)
모멘텀 최적화의 한 가지 단점은 튜닝할 하이퍼파라미터가 하나 늘어난다는 것인데, 대부분의 경우에 0.9에서 잘 작동하며 경사 하강법보다 거의 항상 더 빠릅니다.
네스테로프 가속 경사
모멘텀 최적화의 한 변종인 네스테로프 가속 경사(NAG)는 기본 모멘텀 최적화보다 거의 항상 더 빠릅니다.
NAG는 다음과 같이 진행됩니다.
\[ m \leftarrow \beta m - \eta \nabla_{\theta}J(\theta + \beta m) \]
\[ \theta \leftarrow \theta + m \]
NAG는 일반적으로 기본 모멘텀 최적화보다 훈련 속도가 빠릅니다.
사용도 간단합니다. SGD 옵티마이저에 nesterov=True를 설정해주면 됩니다.
optimizer = keras.optimizers.SGD(learning_rate=0.001, momentum=0.9, nesterov=True)
AdaGrad
한쪽이 길쭉한 그릇 문제를 생각해보면 경사 하강법 시에 전역 최적점 방향으로 바로 향하지 않고 가장 가파른 경사를 따라 빠르게 내려가기 시작해서 골짜기 아래로 느리게 이동합니다. 알고리즘이 이를 일찍 감지하고 전역 최적점 쪽으로 좀 더 정확한 방향을 잡았다면 더 좋았을 것입니다. AdaGrad 알고리즘은 가장 가파른 차원을 따라 그레이디언트 벡터의 스케일을 감소시켜서 이런 문제를 해결합니다.
\[ s \leftarrow s + \nabla_{\theta}J(\theta) \otimes \nabla_{\theta}J(\theta) \]
\[ \theta \leftarrow \theta - \eta\nabla_{\theta}J(\theta) \oslash \sqrt{s + \varepsilon} \]
이 알고리즘은 학습률을 감소시키지만 경사가 완만한 차원보다 가파른 차원에 대해 더 빠르게 감소됩니다.
이를 적응적 학습률이라고 하고, 전역 최적점 방향으로 더 곧장 가도록 하는 데 도움이 됩니다.
또한 학습률 파라미터 \(\eta\)를 덜 튜닝해도 되는 것이 장점입니다.
AdaGrad는 간단한 2차방정식 문제에 대해선 잘 작동하지만 신경망을 훈련할 때 너무 일찍 멈출 때가 있습니다.
학습률이 너무 감소되어 전역 최적점에 도달하기 전에 알고리즘이 멈추는 것입니다.
그래서 AdaGrad 옵티마이저가 있지만 심층 신경망에는 사용하지 말아야 합니다.
(간단한 작업에는 효과적일 수 있습니다.)
AdaGrad를 사용하려면 아래와 같이 optimizer를 만들어주면 됩니다.
optimizer = keras.optimizers.Adagrad(learning_rate=0.001)
RMSProp
AdaGrad는 너무 빨리 느려져서 전역 최적점에 수렴하지 못하는 위험이 있습니다.
RMSProp은 가장 최근 반복에서 비롯된 그레이디언트만 누적함으로써 이 문제를 해결했습니다.
\[ s \leftarrow \beta s + (1 - \beta)\nabla_{\theta}J(\theta) \otimes \nabla_{\theta}J(\theta) \]
\[ \theta \leftarrow \theta - \eta\nabla_{\theta}J(\theta) \oslash \sqrt{s + \varepsilon} \]
보통 감쇠율 \(\beta\)는 0.9로 설정합니다.
하이퍼파라미터가 하나 더 생긴 건 맞지만 기본값이 잘 작동하는 경우가 많으므로 튜닝할 필요는 없습니다.
RMSProp을 사용하려면 아래와 같이 optimizer를 만들어주면 됩니다.
optimizer = keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9)
Adam과 Nadam 최적화
적응형 모멘트 추정(adaptive moment estimation)을 의미하는 Adam은 모멘텀 최적화와 RMSProp의 아이디어를 합친 것입니다. 모멘텀 최적화처럼 지난 그레이디언트의 지수 감소 평균을 따르고 RMSProp처럼 지난 그레이디언트 제곱의 지수 감소된 평균을 따릅니다.
- \( \displaystyle m \leftarrow \beta_{1}m - (1 - \beta_{1})\nabla_{\theta}J(\theta) \)
- \( \displaystyle s \leftarrow \beta_{2}s + (1 - \beta_{2})\nabla_{\theta}J(\theta) \otimes \nabla_{\theta}J(\theta) \)
- \( \displaystyle \hat{m} \leftarrow \frac{m}{1 - \beta_{1}^{t}} \)
- \( \displaystyle \hat{s} \leftarrow \frac{s}{1 - \beta_{2}^{t}} \)
- \( \theta \leftarrow \theta + \eta\hat{m} \oslash \sqrt{\hat{s} + \varepsilon} \)
모멘텀 감쇠 하이퍼파라미터 \(\beta_{1}\)은 보통 0.9로 초기화하고 스케일 감쇠 하이퍼파라미터 \(\beta_{2}\)는 0.999로 초기화하는 경우가 많습니다.
keras에서 Adam 옵티마이저를 만들려면 아래와 같이 옵티마이저를 만들어주면 됩니다.
optimizer = keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
Adam이 적응적 학습률 알고리즘이기때문에 학습률 파라미터 \(\eta\)를 튜닝할 필요가 적습니다.
기본값 0.001을 일반적으로 사용하므로 경사 하강법보다 Adam이 더 사용하기 쉽습니다.
외에도 살펴볼만한 Adam의 두 가지 변종이 있습니다.
AdaMax는 어떤 작업에서 Adam이 잘 작동하지 않는다면 시도할 수 있는 옵티마이저 중 하나이고,
아래와 같이 사용할 수 있습니다.
optimizer = keras.optimizers.Adamax(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
Nadam은 Adam에 네스테로프 기법을 더한 것으로, 종종 Adam보다 조금 더 빠르게 수렴합니다.
optimizer = keras.optimizers.Nadam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
아래는 여태 소개한 모든 옵티마이저를 비교하는 표입니다.
클래스 | 수렴 속도 | 수렴 품질 |
SGD | ⭐ | ⭐⭐⭐ |
SGD(momentum=...) | ⭐⭐ | ⭐⭐⭐ |
SGD(momentum=..., nesterov=True) | ⭐⭐ | ⭐⭐⭐ |
AdaGrad | ⭐⭐⭐ | ⭐(너무 일찍 멈춤) |
RMSProp | ⭐⭐⭐ | ⭐⭐ 또는 ⭐⭐⭐ |
Adam | ⭐⭐⭐ | ⭐⭐ 또는 ⭐⭐⭐ |
Nadam | ⭐⭐⭐ | ⭐⭐ 또는 ⭐⭐⭐ |
AdaMax | ⭐⭐⭐ | ⭐⭐ 또는 ⭐⭐⭐ |
학습률 스케줄링
좋은 학습률을 찾는 것은 중요합니다.
너무 크면 훈련이 발산할 수 있고,
너무 작으면 최적점에 수렴하겠지만 시간이 매우 오래 걸릴 수 있습니다.
조금 높게 잡는다면 처음에는 매우 빠르게 진행하겠지만 최적점 근처에서 요동이 심해져 수렴하지 못할 것입니다.
컴퓨팅 자원이 한정적이라면 차선의 솔루션을 만들기 위해 완전히 수렴하기 전 훈련을 멈춰야 합니다.
학습률을 찾을 때 매우 작은 값에서 매우 큰 값까지 지수적으로 학습률을 증가시키면서
다 테스트해봐서 좋은 학습률을 찾을 수 있습니다.
그러나 일정한 학습률보다 큰 학습률로 시작하고 학습 속도가 느려질 때 학습률을 낮추면 최적의 고정 학습률보다 좋은 솔루션을 더 빨리 발견할 수 있습니다. 훈련하는 동안 학습률을 감소시키는 전략에는 여러 가지가 있습니다. 이런 전략을 학습 스케줄이라고 합니다.
다음과 같은 것들이 가장 널리 사용하는 학습 스케줄입니다.
- 거듭제곱 기반 스케줄링
\( \displaystyle \eta(t) = \eta_{0} / (1 + t/s)^{c} \)
\(\eta\): 초기 학습률
\(c\): 거듭제곱 수(일반적으로 1)
\(s\): 스텝 횟수, 하이퍼파라미터입니다. - 지수 기반 스케줄링
\( \displaystyle \eta(t) = \eta_{0}0.1^{t/s} \) - 구간별 고정 스케줄링
- 성능 기반 스케줄링
- 1사이클 스케줄링
케라스에서 거듭제곱 기반 스케줄링이 가장 구현하기 쉽습니다.
옵티마이저를 만들 때 decay 매개변수만 지정하면 됩니다.
optimizer = keras.optimizers.SGD(learning_rate=0.01, decay=1e-4)
decay는 \(s\)의 역수입니다. 케라스는 \(c\)를 1로 가정합니다.
지수 기반 스케줄링과 구간별 스케줄링도 꽤 간단합니다.
먼저 현재 에포크를 받아 학습률을 반환하는 함수를 정의해야 합니다.
예를 들어 아래와 같이 구현할 수 있습니다.
def exponential_decay_fn(epoch):
return 0.01 * 0.1**(epoch / 20)
\(\eta_{0}\)과 \(s\)를 하드코딩하고 싶지 않다면 이 변수를 설정한 클로저를 반환하는 함수를 만들 수 있습니다.
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)
이 스케줄링 함수를 전달하여 LearningRateScheduler 콜백을 만듭니다.
그리고 이 콜백을 fit() 메소드에 전달하면 됩니다.
lr_scheduler = keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train_scaled, y_train, [...], callbacks=[lr_scheduler])
LearningRateScheduler는 에포크를 시작할 때마다 옵티마이저의 learning_rate 속성을 업데이트합니다.
스케줄 함수는 두 번째 매개변수로 현재 학습률을 받아올 수 있습니다.
예를 들어 다음과 같은 스케줄 함수는 지수 감쇠 효과를 냅니다.
def exponential_decay_fn(epoch, lr):
return lr * 0.1**(1 / 20)
구간별 고정 스케줄링은 아래와 같이 구현할 수 있습니다.
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])
이렇게 만든 다음 지수 기반 스케줄링에서 했던 것처럼 LearningRateScheduler 콜백을 만들어서 쓰면 됩니다.
lr_scheduler = keras.callbacks.LearningRateScheduler(piecewise_constant_fn)
history = model.fit(X_train_scaled, y_train, [...], callbacks=[lr_scheduler])
성능 기반 스케줄링을 위해서는 ReduceLROnPlateau 콜백을 사용합니다.
예를 들어 아래와 같이 콜백을 만들어서 fit() 메소드에 전달하면 최상의 검증 손실이 다섯 번의 연속적인 에포크 동안 향상되지 않을 때마다 학습률에 0.5를 곱합니다.
lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)
tf.keras는 학습률 스케줄링을 위한 또 다른 방법을 제공합니다.
keras.optimizers.schedules에 있는 스케줄 중 하나를 사용해 학습률을 정의하고 이 학습률을 옵티마이저에 전달합니다.
이렇게 하면 에포크가 아니라 매 스텝마다 학습률을 업데이트합니다.
예를 들어 다음은 앞서 정의한 exponential_decay_fn()과 동일한 지수 기반 스케줄링을 구현하는 코드입니다.
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)
간결하고 이해하기 좋습니다. 또한 모델을 저장할 때 학습률과 현재 상태를 포함한 스케줄도 함께 저장됩니다.
그러나 이 방식은 표준 Keras API는 아니며 tf.keras 에서만 지원합니다.
1사이클 방식을 사용하기 위한 구현에 특별한 어려움은 없습니다.
매 반복마다 학습률을 조정하는 사용자 정의 콜백을 만들면 됩니다.
규제를 사용해 과대적합 피하기
심층 신경망은 보통 아주 많은 파라미터를 가지고 있습니다.
이 때문에 네트워크의 자유도가 아주 높습니다.
즉, 대규모의 복잡한 데이터셋을 학습할 수 있지만 네트워크를 훈련 세트에 쉽게 과대적합되게 합니다.
신경망에서 사용되는 여러 규제들을 알아보겠습니다.
\(l_{1}\)과 \(l_{2}\) 규제
신경망의 연결 가중치를 제한하기 위해 \(l_{2}\) 규제를 사용하거나,
희소 모델을 만들기 위해 \(l_{1}\) 규제를 사용할 수 있습니다.
다음과 같이 규제 강도를 정해서 규제를 적용할 수 있습니다.
layer = keras.layers.Dense(100, activation="elu",
kernel_initializer="he_normal",
kernel_regularizer=keras.regularizers.l2(0.01))
# or l1(0.1) for ℓ1 regularization with a factor of 0.1
# or l1_l2(0.1, 0.01) for both ℓ1 and ℓ2 regularization, with factors 0.1 and 0.01 respectively
일반적으로 네트워크의 모든 은닉층에 동일한 활성화 함수, 동일한 초기화 전략을 사용하거나 모든 층에 동일한 규제를 사용하기 때문에 동일한 매개변수 값을 반복하는 경우가 많습니다.
이는 코드를 읽기 어렵게 만들고 버그를 만들기 쉬우므로 반복문을 사용하도록 코드를 리팩터링하거나 다음과 같이 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")
])
드롭아웃
드롭아웃은 가장 인기 있는 규제 기법 중 하나입니다.
이 알고리즘은 매우 간단합니다.
각 뉴런(출력 뉴런 제외)은 임시적으로 드롭아웃될 확률 p를 가집니다.
이번 훈련 스텝에는 완전히 무시되지만 다음 스텝에는 활성화될 수 있습니다.
하이퍼파라미터 p를 드롭아웃 비율이라고 하고, 보통 10~50%로 지정합니다.
순환 신경망에서는 20~30%에 가깝고 합성곱 신경망에서는 40~50%에 가깝습니다.
훈련이 끝난 뒤에는 뉴런에 더는 드롭아웃을 적용하지 않습니다.
한 가지 고려해야 할 기술적 세부사항이 있습니다.
p=50%로 하면 테스트하는 동안 하나의 뉴런이 훈련 때보다 평균적으로 두 배 많은 입력 뉴런과 연결됩니다. 이런 점을 보상하기 위해 훈련하고 나서 각 뉴런의 연결 가중치에 0.5를 곱할 필요가 있습니다. 그렇지 않으면 각 뉴런이 훈련한 것보다 거의 두 배 많은 입력 신호를 받기 때문에 잘 동작하지 않을 것입니다.
조금 더 일반적으로 말하면 훈련이 끝난 뒤 각 입력의 연결 가중치에 보존 확률(1 - p)를 곱해야 합니다.
또는 훈련하는 동안 각 뉴런의 출력을 보존 확률로 나눌 수도 있습니다. (위에 말한 방법과 같은 건 아닙니다.)
케라스에서는 keras.layers.Dropout 층을 사용해 드롭아웃을 구현합니다.
다음은 드롭아웃 비율 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")
])
드롭아웃은 수렴을 상당히 느리게 만드는 경향이 있지만 적절히 튜닝하면 훨씬 좋은 모델을 만들 수 있습니다.
따라서 일반적으로 추가적인 시간과 노력을 기울일 가치가 있습니다.
Tip: SELU 활성화 함수를 기반으로 자기 정규화하는 네트워크를 규제하고 싶다면 알파 드롭아웃을 사용해야 합니다.
몬테 카를로 드롭아웃
훈련된 드롭아웃 모델을 재훈련하거나 전혀 수정하지 않고 성능을 크게 향상시킬 수 있는 몬테 카를로 드롭아웃이라 불리는 기법이 있습니다. 이를 사용하면 모델의 불확실성을 더 잘 측정할 수 있고 구현도 쉽습니다.
아래 코드는 훈련된 드롭아웃 모델을 재훈련하지 않고 성능을 향상시키는 완전한 MC 드롭아웃 구현입니다.
y_probas = np.stack([model(X_test_scaled, training=True)
for sample in range(100)])
y_proba = y_probas.mean(axis=0)
위 코드에서 model(X)는 넘파이 배열이 아니라 텐서를 반환한다는 것 빼고는 model.predict(X)와 비슷하고 trainig 매개변수를 지원하는데, 여기서 이를 True로 설정해주었기 때문에 Dropout층이 활성화되어 예측이 달라집니다. 그러고 첫 번째 차원을 기준으로 평균을 내면 끝입니다. 드롭아웃으로 만든 예측을 평균하면 일반적으로 드롭아웃 없이 예측한 하나의 결과보다 더 안정적입니다.
모델이 훈련하는 동안 다르게 작동하는 (BatchNormalization과 같은) 층이 있다면 앞에서처럼 훈련 모드를 강제로 설정해선 안됩니다. 대신 Dropout층을 다음과 같은 MCDropout 클래스로 바꿔줘야 합니다.
class MCDropout(keras.layers.Dropout):
def call(self, inputs):
return super().call(inputs, training=True)
# 아래와 같이 AlphaDropout 클래스를 상속하여 MCAlphaDropout도 사용가능합니다.
class MCAlphaDropout(keras.layers.AlphaDropout):
def call(self, inputs):
return super().call(inputs, training=True)
처음부터 모델을 만든다면 그냥 Dropout 대신 MCDropout을 사용하여 만ㄷ르면 되고,
이미 Dropout을 이용해 모델을 훈련했다면 Dropout층을 MCDropout으로 바꾸고 기존 모델과 동일한 새 모델을 만든 후 기존 모델의 가중치를 새 모델로 복사하면 됩니다.
맥스-노름 규제
또 다른 널리 사용되는 규제 기법은 맥스-노름 규제입니다.
이 방식은 각 뉴런에 대해 입력의 연결 가중치 \(w\)가 \( \Vert w \Vert_{2} \leq r \)이 되도록 제한합니다.
\(r\)은 맥스-노름 하이퍼파라미터이고 \( \Vert \cdot \Vert_{2} \)는 \(l_{2}\) 노름을 나타냅니다.
케라스에서 맥스-노름 규제를 사용하려면 다음과 같이 적절한 최댓값으로 지정한 max_norm()이 반환한 객체로 은닉층의 kernel_constraint 매개변수를 지정합니다.
layer = keras.layers.Dense(100, activation="selu", kernel_initializer="lecun_normal",
kernel_constraint=keras.constraints.max_norm(1.))
요약 및 실용적 가이드라인
뭐가 너무 많아서 어지러울 수 있습니다.
아래 표에 하이퍼파라미터 튜닝을 하지 않고 대부분의 경우에 잘 맞는 설정의 정리했습니다.
(고정된 규칙으로 생각해선 안됩니다.)
하이퍼파라미터 | 기본값 |
커널 초기화 | He 초기화 |
활성화 함수 | ELU |
정규화 | 얕으면 필요없음, 깊은 신경망이면 배치 정규화 |
규제 | 조기 종료 (필요하면 \(l_{2}\) 규제 추가 |
옵티마이저 | 모멘텀 최적화 (또는 RMSProp이나 Nadam) |
학습률 스케줄 | 1사이클 |
네트워크가 완전 연결 층을 쌓은 모델이라면 자기 정규화를 사용 가능합니다.
이 경우에는 아래와 같은 설정이 일반적으로 잘 맞습니다.
하이퍼파라미터 | 기본값 |
커널 초기화 | 르쿤 초기화 |
활성화 함수 | SELU |
정규화 | 없음 (자기 정규화) |
규제 | 필요하면 알파 드롭아웃 |
옵티마이저 | 모멘텀 최적화 (또는 RMSProp이나 Nadam) |
학습률 스케줄 | 1사이클 |
이 때 입력 특성을 정규화하는 것을 잊으면 안됩니다.
비슷한 문제를 해결한 모델을 찾을 수 있다면 사전훈련된 신경망의 일부를 재사용해야 하고,
레이블이 없는 데이터가 많다면 비지도 사전훈련을 수행하세요.
또는 비슷한 작업을 위한 레이블된 데이터가 많다면 보조 작업에서 사전 훈련을 수행할 수 있습니다.
이러한 가이드라인에도 아래와 같은 예외는 있습니다.
- 희소 모델이 필요하다면 \(l_{1}\) 규제를 사용할 수 있습니다. 매우 희소한 모델이 필요하면 텐서플로 모델 최적화 툴킷(TF-MOT)을 사용할 수 있습니다. 이 도구는 자기 정규화를 깨뜨리므로 이 경우 기본 DNN 설정을 사용해야 합니다.
- 빠른 응답을 하는 모델이 필요하면 층 개수를 줄이고 배치 정규화 층을 이전 층에 합치세요. 그리고 LeakyReLU나 ReLU 같은 빠른 활성화 함수를 사용하세요. 희소 모델을 만드는 것도 도움이 됩니다. 또한 부동소수점 정밀도를 32비트에서 16비트 또는 8비트로 낮출 수도 있습니다. 여기에서도 TF-MOT을 확인하세요.
- 위험에 민감하고 예측 속도가 매우 중요하지 않은 애플리케이션이면 성능을 올리고 불확실성 추정과 신뢰할 수 있는 확률 추정을 얻기 위해 MC 드롭아웃을 사용할 수 있습니다.
이번 장은 내용이 어찌나 많은지 글이 너무너무너무 기네요..ㅠㅠ
블로그 글은 제가 이동하면서 복습하면서 보는 용도로 쓰고 있는데 이거 다시 볼 생각하니까 눈앞이 컴컴하네요..
그래도 해야겠죠! 나름 재미는 있었습니다 ㅎㅎ
파이팅!
'DATA > 머신 러닝' 카테고리의 다른 글
[머신 러닝] 텐서플로를 사용한 사용자 정의 모델과 훈련 (0) | 2022.02.04 |
---|---|
[머신 러닝] 머신러닝을 위한 텐서플로 (0) | 2022.02.02 |
[머신 러닝] 신경망 하이퍼파라미터 튜닝하기 (0) | 2022.01.23 |
[머신 러닝] 텐서보드를 이용해 시각화하기 (0) | 2022.01.23 |
[머신 러닝] 콜백 사용하기 (0) | 2022.01.22 |