본 포스팅은 밑바닥부터 시작하는 딥러닝 3 책의 내용을 공부한 후 개인적으로 중요하다고 생각되는 내용들을 정리하는 것이 목적입니다.
해당 책은 큰 맥락을 '고지'로 나누고 있으며 총 5 고지가 있으며 각 고지의 세부 내용을 'step'으로 분리해서 학습합니다.
1개의 고지를 학습하고 포스팅 할 경우 내용이 너무 길어져 가독성 측면을 고려해 각 고지의 절반에 해당하는 내용들을 하나의 포스팅에서 다룰 예정입니다. 혹시 틀린 내용이 있다면 피드백은 항상 환영입니다🤗
이번 포스팅에서는 그동안 만들어온 나만의 딥러닝 프레임워크를 더 큰 도전을 향해 나아가도록 발전시켜 보도록 하겠습니다.
학습시킨 모델의 파라미터를 로드하거나 저장하는 기능뿐만 아니라 Overfitting을 방지하기 위한 Dropout과 같은 기능을 추가해 보도록 하겠습니다.
1. 모델의 학습된 파라미터(Weight)를 저장 및 로드하는 기능
유명한 오픈소스 딥러닝 프레임워크(파이토치, 텐서플로우 등)에는 학습시킨 모델의 파라미터를 저장하거나 로드하는 기능이 추가되어 있다. 이러한 기능을 통해 대량의 데이터로 학습시킨 Pre-trained 모델의 가중치를 다운로드하고 이를 통해 어느 정도의 성능을 내는 모델을 쉽게 사용할 수 있게 되었다. 이렇게 학습시킨 모델의 파라미터를 저장하거나 로드하는 기능을 우리의 딥러닝 프레임워크에도 추가시켜 보도록 하자.
해당 기능을 구현하기 위해서 Numpy를 사용 할 예정이며 구체적으로는 Numpy의 save와 load라는 함수를 사용하는 방법에 대해 먼저 알아보자.
import numpy as np
x = np.array([1, 2, 3])
np.save('./params.npy', x)
param = np.load('./params.npy')
print(param)
위와 같이 간단한 넘파이 배열을 np.save 메서드를 통해 .npy 확장자 파일로 저장한 후 np.load 메서드를 통해 저장된 .npy 확장자 파일을 불러올 수 있다. 그런데 save 메서드 말고도 savez라는 메서드도 존재하는데, 현재 우리의 상황에서는 savez 메서드를 사용하는 것이 더 적절할 수 있다. 바로 파라미터의 이름을 명시해 줄 수 있기 때문인데 아래의 예시 코드와 함께 살펴보도록 하자.
import numpy as np
x1 = np.array([1, 2, 3])
x2 = np.array([4, 5, 6])
np.savez('./params.npz', param1=x1, param2=x2)
params = np.load('./params.npz')
print('x1:', params['param1'])
print('x2:', params['param2'])
이렇듯 savez 메서드는 이전과 다르게 확장자명을 .npz로 명시하면 된다. 그리고 savez 메서드로 넘파이 배열을 지정해 줄 때 특정 배열에 대한 이름을 argument로 명시해 주어 저장할 수 있다. 이렇게 하면 저장된 넘파이 배열을 로드할 때도 argument로 넣어준 이름을 key값으로 넣어주어 가져올 수 있다.
현재 우리의 딥러닝 프레임워크는 중첩된 계층의 구조를 표현하도록 되어 있다.
그래서 현재 우리의 프레임워크로 만든 모델들의 파라미터도 중첩되어 있는 상태이다.
그래서 파라미터들을 1차원 상태로 평탄화(Flatten) 시켜주어야 할 필요가 있다. 이를 위해 아래처럼 Layer 클래스에 특정 메서드를 추가해 보도록 하자.
import weakref
import numpy as np
from dezero import Parameter
from dezero import functions as F
class Layer():
def __init__(self):
self._params = set()
...생략
def _flatten_params(self, param_dict, parent_key=''):
for name in self._params:
obj = self.__dict__[name]
key = parent_key + '/' + name if parent_key else name
if isinstance(obj, Layer):
obj._flatten_params(param_dict, parent_key=key)
else:
param_dict[key] = obj
def save_weights(self, path):
param_dict = {}
self._flatten_params(param_dict)
array_dict = {key: param.data for key, param in param_dict.items() if param is not None}
try:
np.savez_compressed(path, **array_dict)
except (Exeption, KeyboardInterrupt) as e:
if os.path.exists(path):
os.remove(path)
raise
def load_weights(self, path):
npz = np.load(path)
param_dict = {}
self._flatten_params(param_dict)
for key, param in param_dict.items():
param.data = npz[key]
파라미터를 저장하는 save_weights 함수를 봤을 때 로직은 이해하기 쉽지만 try, except 구문을 넣은것이 특이하다.
except 구문에 KeyboardInterrupt 예외처리가 있는데, 이는 사용자가 파라미터를 저장하는 액션을 갑작스럽게 취소할 때 처리하도록 하는 구문이다. 즉, 갑작스레 취소했을 때 불완전한 파일이 이미 일부 디렉토리에 저장되어 있을 텐데 그 불완전한 파일을 삭제시키는 구문이다.
다음으로 저장한 파라미터를 로드하는 load_weights 메서드를 살펴보자. 주목할 부분은 마지막 for loop 이다.
해당 구문이 가능한 이유는 다음과 같다. 우리가 로드할 파라미터를 갖고 있는 모델(A)의 구조와 로드한 파라미터를 사용하려는 모델(B)이 있다고 하자. 즉 A와 B 모델의 구조가 동일해야 적용이 가능하다. 만약 구조가 조금이라도 틀리면 어떤 모델에는 존재하는 파라미터의 이름이 다른 모델에는 존재하지 않아 KeyError가 발생할 것이다.
2. Dropout 기능 추가하기
딥러닝 모델을 학습시킬 때 주로 과대적합(Overfitting)이 자주 발생한다. 이러한 과대적합이 발생하는 원인으로는 크게 2가지가 있다.
첫 번째는 학습 데이터가 충분하지 않은 문제, 두 번째는 모델의 표현력이 지나치게 높은 문제 때문에 과적합이 주로 발생하게 된다.
위 2가지 원인에 대응하는 해결책은 다음과 같다. 학습 데이터 자체가 부족한 문제는 데이터를 더 많이 수집하거나, 데이터 증강(Augmentation)을 활용하여 해결할 수 있다. 두 번째 원인에 대한 대응으로는 가중치 감소(Weight Decay), 드롭아웃(Drop out), 정규화(Regularization), 배치 정규화(Batch Normalization) 등을 사용하여 해결할 수 있다.
본 포스팅에서는 드롭아웃을 구현하는 것으로 Overfitting을 방지하는 방법을 다루어 보도록 하겠다.
드롭아웃의 핵심은 모델 학습 과정에서 데이터를 흘려보낼 때마다 각 층에서 삭제할 뉴런을 무작위로 선택하는 방법이다.
드롭아웃을 구현하는 방법으로는 크게 2가지가 있는데 먼저 가장 일반적인(Directed) 드롭아웃을 먼저 살펴보자.
import numpy as np
dropout_ratio = 0.5
x = np.ones(10)
mask = np.random.rand(10) > dropout_ratio
y = x * mask
print(y)
위처럼 마스킹이라는 불리언 인덱스를 생성하여 dropout_ratio 보다 낮은 값을 가지는 인덱스에 해당하는 값을 0으로 만들어준다.
드롭아웃을 바라보는 또 하나의 관점은 앙상블(Ensemble)의 관점으로 바라보는 것이다.
드롭아웃을 수행한다는 것은 앙상블 학습을 하는 것이라고 볼 수 있다. 앙상블 학습의 개념은 여러 모델을 개별적으로 학습시킨 후 추론할 때 개별 모델들의 출력값을 평균 내게 된다. 이러한 관점에서 드롭아웃도 비슷하다고 볼 수 있다.
드롭아웃은 매번 학습 시마다 삭제하는 뉴런이 달라질 것이고 이는 곧 매번 학습 모델의 구조가 달라짐을 의미한다.
결국 학습 시에 매번 다른 모델로 학습한다는 점에서 앙상블과 비슷하다고 볼 수 있다.
그래서 앙상블 학습이 추론 시에는 '평균'을 내는 것처럼 드롭아웃도 추론 시에는 '평균'을 내는 것과 같이 개별 모델이 내놓은 결과 값들을 '약화'시키는 과정이 필요하다. 이러한 '약화' 시키는 과정은 보통 학습 시에 적용한 드롭아웃 비율을 1에서 뺀 값을 출력값에 곱해준다.
이를 코드로 나타내면 아래와 같다.
import numpy as np
dropout_ratio = 0.6
x = np.ones(10)
# train
mask = np.random.rand(10) > dropout_ratio
y = x * mask
# test
scale = 1 - dropout_ratio
y = x * scale
다른 드롭아웃의 유형으로는 역(Inverted) 드롭아웃이 있다. 역 드롭아웃은 스케일 맞추기를 학습할 때 수행한다.
즉 위의 일반적인 드롭아웃에서 추론(test) 시 정의한 scale이라는 값을 학습 과정에 포함시킨다는 것이다.
그러고 나서 추론 시에는 출력값에 어떠한 연산도 수행하지 않는다. 코드로 구현하면 아래와 같다.
import numpy as np
dropout_ratio = 0.6
x = np.ones(10)
# train
scale = 1 - dropout_ratio
mask = np.random.rand(*x.shape) > dropout_ratio
y = x * mask / scale # 학습 시에 scale을 수행
# test
y = x
역 드롭아웃이 갖는 장점에 대해 살펴보도록 하자. 우선 테스트시에 어떠한 연산도 하지 않기 때문에 일반적인 드롭아웃에 비해 추론 시에 추론 속도가 살짝 향상된다. 두 번째로는 학습할 때의 드롭아웃 비율(dropout_ratio)을 동적으로 변경할 수 있다.
일반적인 드롭아웃 같은 경우는 학습 시에 지정한 드롭아웃 비율을 추론 시에도 동일하게 사용해야 하기 때문에 학습 시 중간에 드롭아웃 비율을 다른 값으로 바꾸면 결과가 이상해질 우려가 있다. 하지만 역 드롭아웃 비율은 애초에 모든 연산 과정이 학습 내에서 이루어지기 때문에 동적으로 드롭아웃 비율 조정이 가능하다.
이러한 이유 때문에 실제로 많은 딥러닝 프레임워크에서 역 드롭아웃 방식을 채택해 사용하고 있다.
다음 포스팅에서는 여태껏 구현한 우리의 프레임워크를 통해 더욱 복잡한 계산인 CNN과 RNN 연산을 수행하는 과정을 살펴보도록 하겠습니다.
'AI Development > PyTorch' 카테고리의 다른 글
밑바닥부터 시작하는 딥러닝3 - Dezero의 도전(3) (0) | 2024.02.23 |
---|---|
밑바닥부터 시작하는 딥러닝3 - Dezero의 도전(2) (1) | 2024.02.23 |
밑바닥부터 시작하는 딥러닝 3 - 자연스러운 코드로(1) (0) | 2024.02.03 |
밑바닥부터 시작하는 딥러닝 3 - 미분 자동 계산(2) (0) | 2024.02.01 |
밑바닥부터 시작하는 딥러닝3 - 미분 자동 계산(1) (0) | 2024.01.30 |