개요
1. 문제 상황
2. 원인 파악 및 대처
3. 결과
4. 더 나아가서
1. 문제 상황
Numpy로 Adam optimizer를 구현하여 MLP 학습에 사용하려고 한다. 식을 구현하는 데에 있어서 토대가 된 Adam optimizer의 수학식은 아래와 같다.
초기에 m과 v값이 너무 작아서 결과적으로 파라미터의 값을 업데이트 할 때 적응형 lr이 너무 커지게 된다. 이에 따라, 초기 이동이 너무 먼 곳으로 이동하게 될 수 있다. 즉, 초기 학습에서 편향이 생길 수 있는 가능성이 존재하게 된다.
그래서 이를 방지하기 위해, 구한 m과 v값에 1-Beta값을 나눠주는 식을 추가해주었다. 이러한 식을 추가해줌으로써, 맨 처음 m과 v값이 0일 때에도 즉, m과 v값이 작을 때에도 적당한 값의 파라미터 업데이트가 이뤄지도록 하였다.
이와 같은 식을 코드로 구현한 것은 아래와 같았다.
for i in range(1, len(grads.keys()) // 2 + 1):
eps = 1e-8
self.velocity['W' + str(i)] = (self.beta1 * self.velocity['W' + str(i)]) + (grads['W' + str(i)] * (1 - self.beta1))
self.velocity['b' + str(i)] = (self.beta1 * self.velocity['b' + str(i)]) + (grads['b' + str(i)] * (1 - self.beta1))
self.da['W' + str(i)] = self.beta2 * self.da['W' + str(i)] + ((1 - self.beta2) * (grads['W' + str(i)] * grads['W' + str(i)]))
self.da['b' + str(i)] = self.beta2 * self.da['b' + str(i)] + ((1 - self.beta2) * (grads['b' + str(i)] * grads['b' + str(i)]))
# 초기 편향 문제 해소를 위한 식
self.velocity['W' + str(i)] = self.velocity['W' + str(i)] / (1 - self.beta1)
self.velocity['b' + str(i)] = self.velocity['b' + str(i)] / (1 - self.beta1)
self.da['W' + str(i)] = self.da['W' + str(i)] / (1 - self.beta2)
self.da['b' + str(i)] = self.da['b' + str(i)] / (1 - self.beta2)
# 파라미터 업데이트
self.params['W' + str(i)] -= self.lr * (self.velocity['W' + str(i)] / (np.sqrt(self.da['W' + str(i)]) + eps))
self.params['b' + str(i)] -= self.lr * (self.velocity['b' + str(i)] / (np.sqrt(self.da['b' + str(i)]) + eps))
그러나 이와 같은 코드로 이뤄진 Adam optimizer를 사용하여 학습을 진행하니 정상적인 학습이 이뤄지지 않았다.
Test dataset에 대한 acc가 10%를 웃돌았다. Hyperparameter로 설정해둔 MLP의 layer 개수나 layer의 사이즈, lr을 바꿨지만 그럼에도 학습은 정상적으로 이뤄지지 않았다.
이러한 문제가 발생하였고, 문제 발생의 원인을 분석하고 해결하기 위해 아래와 같은 과정을 거쳤다.
2. 원인 파악 및 대처
(1) Clipping
문제 발생 원인을 파악하기 위해 1차원적으로 epoch당 모델 parameter값과 parameter를 L2 norm 처리 한 값을 확인해보았다.
이 값들을 확인해본 결과로 0 epoch부터 nan값이 발생하고 있음을 확인할 수 있었다.
이에 따라, 학습이 진행되지 않는 원인을 parameter 업데이트에서 발생하는 nan값의 발생이라고 보았다. 즉, nan 값을 해결해주면 학습이 정상적으로 이뤄질 것이라고 판단했다. 이 nan 값을 해결해주기 위한 방법으로는 gradient clipping을 사용했다. Hyperparameter 값을 작게 해줌으로써 parameter 업데이트에서 발생하는 값의 크기를 줄여줄 수도 있다. 하지만, gradient clipping을 사용하면 학습 parameter 값은 그대로 가져가면서도 너무 큰 값들은 clipping을 해주기 때문에, 학습 성능을 그대로 가져가면서도 현재의 문제를 해결해줄 수 있는 좋은 방법이라고 생각했다.
그래서, nan 값을 clipping을 통해 바꿔주기로 결정했다. nan 값을 교체해줄 값은 3개로 선정했다. 1, 0.1, 0.01이다. 1은 transformer 모델 학습 시, clipping에서 가장 좋은 성능을 보이는 hyperparameter 값이라고 알고 있어서 선정했다. 0.01은 나의 MLP 모델에서 정상적인 학습이 진행이 될 시에 확인됐던 parameter 값들이 0.01 대였어서 선정했다. 0.1은 그것의 중간값이기에 선정했다. 동작 방식은 기존의 gradient clipping 동작 방식과 동일하다. Gradient를 l2 norm으로 처리한 값이 사용자가 설정한 max_norm 값을 넘게 되면 1, 0.1, 0.01등의 값으로 바뀌도록 하였다.
적용 결과 acc가 0.1이 올라 0.2를 웃돌았다. 사실상 적용을 하였음에도 변화가 없으며 학습은 진행되지 않았다고 말할 수 있다.
(2) 식 개선
이번에는 근본으로 들어가서 adam optimizer의 식을 분석해보기로 했다. 그리고 인터넷에 공개되어 있는 adam optimizer 구현 코드를 살펴보았다. 그 결과 adam optimizer의 학습 초기 편향 문제를 해결하기 위해 적용된 식이 코드 내에서는 쓰이지 않는 것을 발견할 수 있었다.
이러한 발견을 계기로 하여 초기 편향 문제를 개선해주는 식이 학습에 어떤 영향을 미치는지 확인해보기로 했다.
식을 분석하여 확인해본 결과, 초기 편향 문제 해결을 위한 식을 적용했을 때 m과 v값이 비정상적으로 커지게 되는 현상이 발생하게 됨을 알 수 있었다. 정리하면, 초기 편향 문제 해결을 위한 식을 적용하는 순간 2가지 문제가 발생하게 된다. 그 문제들은 아래와 같다.
(1) 1 - beta값에 의해 decaying 되지 않은 raw한 gradient 값이 m 혹은 v에 더해진다.
(2) beta를 0.999로 설정했을 때, gradient가 적용이 된 m 혹은 v의 값에서 1000이 곱해지게 된다.
위의 사진을 통해 (1)번 문제를 눈으로 확인할 수 있다. raw한 gradient가 더해짐을 눈으로 쉽게 확인하기 위해 약분을 하였다.
만약 약분을 하지 않는다면, 위의 최종식에서 1000을 곱한 값에다가 1000을 나누는 식이 나타나게 된다. 즉, 문제 (2)를 확인할 수 있게 된다.
따라서, nan값이 발생했던 원인은 초기 편향 문제 개선을 위한 추가식이라고 판단했다.
이러한 판단의 신뢰성을 높이기 위해, step당 m과 v값을 확인해보았다. 확인 결과 이 값들이 inf가 되는 것을 발견할 수 있었다. 그리고 beta의 값을 0.999 -> 0.99 -> 0.9순으로 적용해본 결과 inf값의 발생 타이밍이 늦어지는 것을 발견할 수 있었다. 즉, 문제 (1), (2)번이 분명히 parameter값을 업데이트하는 데에 있어서 영향을 주고 있음을 확인할 수 있었다.
더불어, 코드의 문제로 인한 학습 불가 현상이 나타났는지도 확인해보기 위해 beta값에 모두 0을 집어넣어 학습을 진행해보기도 했고 그 결과 acc는 96%를 달성했다.
정리하자면, 초기 편향 문제 개선을 위한 추가식이 문제라고 판단되어 코드에서 그 식을 지웠다. 그리고 결과적으로 1 layer, lr=1e-1의 상황에서 약 97%의 acc를 달성했다. 학습에 성공한 것이다.
3. 결과
Adam optimizer를 구현하는 데에 쓰이는 식을 개선하여 코드로 구현함으로써 학습을 성공으로 이끌 수 있었다.
Adam optimizer의 개선식이 언제나 좋은 역할을 해주지는 않는다는 것을 알 수 있었다. 개선식의 적용이 오히려 gradient exploding 문제를 발생시켰기 때문이었다. 따라서, 이러한 유사한 문제가 발생할 시, 식 분석을 하여 식을 개선하고 코드를 개선해보는 시도를 해볼 수 있음을 배웠다.
4. 더 나아가서
문제가 발생한 원인을 분석하고 인지한 뒤에 해결 방법을 찾는 것이 정말 중요하다는 것을 깨달을 수 있었다. 이러한 경험이 앞으로의 실험에 있어서 도움이 되었으면 한다. 문제가 발생하면 느낌으로 무언가를 해결하는 것이 아니라, 어떤 것이 원인인지 분석하고 그에 따른 적절한 해결책은 무엇일지 고민할줄 아는 그런 사람이 되어야겠다.
Reference
'자연어 처리 과정' 카테고리의 다른 글
CNN을 이용한 sequential data processing (0) | 2023.05.06 |
---|---|
CBOW와 skip-gram (0) | 2023.05.05 |
Sigmoid function의 forward, backward 과정 (0) | 2023.03.18 |
해석학적 미분과 수치 미분 (0) | 2023.03.06 |
Training with SAM optimizer (0) | 2023.02.10 |