이전 포스트에서 다룬 MLP 모델을 향상시켜보겠다. DeepLearning101의 3단원에 해당하는 부분들이다. 3-1부터 3-5장까지가 이에 해당하는 부분들이며 3-6은 다른 데이터셋, Fashion MNIST로 훈련을 진행해본다. 이는 아마 다른 포스트로 올라가지 않을까 싶다.
지금부터 다룰 부분들은 이전 포스트에서 다 다뤘기 때문에 달라지는 부분들만 언급하겠다. 코드를 먼저 살펴보면 다음과 같다.
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(28 * 28, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 10)
self.dropout_prob = 0.5
def forward(self, x):
x = x.view(-1, 28 * 28)
x = self.fc1(x)
x = F.sigmoid(x)
x = F.dropout(x, training = self.training, p = self.dropout_prob)
x = self.fc2(x)
x = F.sigmoid(x)
x = F.dropout(x, training = self.training, p = self.dropout_prob)
x = self.fc3(x)
x = F.log_softmax(x, dim = 1)
return x
보면, __init__()
에서 self.dropout_prob
가 생겼고 forward(x)
에서는 F.dropout()
라는 새로운 layer가 총 2번 더해진 것을 알 수 있다. 이 Dropout layer은 특정 확률로 계층별 뉴런을 비활성화하는 역할을 수행하는데, 여기서는 50퍼센트의 확률로 이를 진행하는 것을 뜻한다. Dropout을 쓸 때와 쓰지 않았을 때의 차이를 아래 그래프를 통해 한번 살펴보자.
아래 내용을 살펴보면, dropout을 사용하지 않은 모델의 경우 train dataset에서는 과적합이 일어나고 test data 오류가 증가하고 정확도도 향상하지 못하는 것을 알 수 있다. 하지만, dropout을 사용할 경우 비록 train dataset에서의 Loss 값은 안 했을 때보다 좋진 않지만, 과적합은 일어나지 않고 test dataset에 대한 accuracy도 더 좋은 것을 확인할 수 있다.
Dropout의 영향을 정리해보면 다음과 같다.
그래서 dropout을 사용할 경우 accuracy를 이전 모델과 비교해보면 다음과 같다. 아쉽게도, dropout을 사용했을 때 더 accuracy가 낮다. 그것은 아마 모델은 잘 정규화되어 있는데, 충분한 훈련이 부족했기에 그런 결과가 나왔을 확률이 높다. w/ dropout 그래프의 끝 부분을 보면, 그래도 accuracy가 상승하는 듯한 경향이 있긴 때문이다.
위에서 언급한 과적합 문제 말고도 다음과 같은 문제들이 발생할 수 있다.
이러한 문제를 해결하기 위한 방법으로는 다음과 같은 방법들이 있다. 추가로, 출처가 자꾸 티스토리인 것은 나도 아쉽지만, 글이 깔끔하고 보기 좋게 정리되어 있어 많이 참고하게 되었다.
이 장에서는 활성화 함수를 바꾸는 solution에 대해 다뤄볼 것이다. 아래 코드를 보면, 모델에 ReLU 활성화함수를 sigmoid 대신에 넣어줬다.
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(28 * 28, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 10)
self.dropout_prob = 0.5
def forward(self, x):
x = x.view(-1, 28 * 28)
x = self.fc1(x)
x = F.relu(x)
x = F.dropout(x, training = self.training, p = self.dropout_prob)
x = self.fc2(x)
x = F.relu(x)
x = F.dropout(x, training = self.training, p = self.dropout_prob)
x = self.fc3(x)
x = F.log_softmax(x, dim = 1)
return x
보면, 모델의 설계에서 F.sigmoid(x)
대신에 F.relu(x)
가 쓰이는 것을 확인할 수 있다.
ReLU 함수는 다음과 같은 꼴과 모양을 지니고 있다.
0보다 작은 값은 0을, 0보다 큰 값은 그 값 그대로 출력해주는 함수이다. 이 함수를 은닉층, hidden layer에 넣어주게 되면 기울기 소실(Gradient Vanishing) 문제가 발생하고 활성화 함수 속도가 매우 빠르다는 장점이 존재한다.
장점만 있을 것 같은 이 ReLU도 단점이 존재한다고 한다.
이에 대한 보충으로 다양한 ReLU variations들이 존재하는데 이는 생략하도록 하겠다.
이 장에서는 Batch Nomalization layer를 더해준다.nn.BatchNorm1d()
를 쓰고 batch_norm1과 batch_norm2로 표현함으로써 더해주는데 각각 1 dimension으로 512, 256이 있다..? 이 숫자에 대해서는 차차 알아봐보도록 하고 일단, 코드는 다음과 같다.
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(28 * 28, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 10)
self.dropout_prob = 0.5
self.batch_norm1 = nn.BatchNorm1d(512)
self.batch_norm2 = nn.BatchNorm1d(256)
def forward(self, x):
x = x.view(-1, 28 * 28)
x = self.fc1(x)
x = self.batch_norm1(x)
x = F.relu(x)
x = F.dropout(x, training = self.training, p = self.dropout_prob)
x = self.fc2(x)
x = self.batch_norm2(x)
x = F.relu(x)
x = F.dropout(x, training = self.training, p = self.dropout_prob)
x = self.fc3(x)
x = F.log_softmax(x, dim = 1)
return x
batch nomalization은 왜 하는지 알아야 한다. 위에서 언급한 gradient를 살릴려는 의도도 있지만, Internal Covariate Shift 문제를 해결하기 위해서 쓰인다. 모델을 학습시키는 게 training dataset을 잘 학습하는 것도 있지만 그것보다 더 궁극적인 목적은 validation dataset에서 얼마나 잘 분류해내느냐에 있다. 만약 모델이 training dataset에만 치우쳐져 있으면 training dataset에서 학습한 2처럼 보이는 3을 3으로 학습한 결과 validation dataset에서 나온 2도 3으로 분류할 수 있다는 것이다. 이거는 그냥 만들어 낸 예시니까, 결론은 training dataset에 치우져진 가중치값들의 분포를 완화시켜주면서도 validation dataset에서 분류를 잘 해내도록 손을 봐줘야 하는 것이다. 그 역할을 하는 것이 바로 batch normalization인 것이다.
공변량은 우리가 아는 독립변수, 종속변수와 같은 변수 개념이다. 보통 독립변수가 종속변수에 얼마만큼의 영향을 주는지 구하는데, 이때 대개 잡음이 섞여 독립변수와 종속변수 간의 관계를 명확하게 밝히지 못하는 경우가 많다. 이렇게 종속변수에 대해 독립변수와 기타 잡음인자들이 공유하는 변량이 바로 공변량인 것이다.
Covariate shift는 machine learning에서 아래 그림과 같이 training data와 test data의 data distribution이 다른 현상을 의미한다. 그런 의미에서 모델을 학습시킬 때, 입력 값(독립변수, training data)에 대해 분류(종속변수, validation data)를 얼마나 잘하는지를 보고 싶은데, 이러한 관계를 분석하는 방해를 하는 요소들(covariates)의 분포가 어떤 현상에 의해 변하기 때문에 이러한 이름을 갖게 된 것이다.
모델이 위의 사진에 나와있는 training data를 학습한 후, test data를 분류하려고 할때 가중치 분포가 training data와 test data의 관점에서 동일해야 분류가 잘 될 것이다. 이게 다를 경우 overfitting이 나고 분류가 잘 이뤄지지 않게 되는 것이다.
학습 과정에서 계층 별로 입력의 데이터 분포가 달라지는 현상을 의미한다. 각 계층에서 입력으로 feature를 받게 되고 그 feature는 convolution이나 fully connected 연산을 거친 뒤 activation function을 적용 받는데, 연산 전/후에 데이터 간 분포가 달라질 수 있다. Batch 단위로 학습을 시킬 경우, Batch 단위간에 데이터 분포의 차이가 존재할 수 있다. 이러한 Batch 간의 데이터 분포가 서로 다른 게 Internal Covariate Shift이다.
이러한 문제는 Batch Normalization으로 해결이 가능하다. Batch normalization은 각 배치 단위 별로 데이터가 다양한 분포를 가지더라도 각 배치별로 평균과 분산을 이용해 정규화하는 것을 뜻한다. 아래 그림을 보면 batch 단위나 layer에 따라서 입력 값의 분포가 모두 다르지마 정규화를 통해 zero mean gaussian 형태로 만들어준다. 이러한 형태가 되면 평균은 0, 표준편차는 1로 데이터의 분포를 조정할 수 있다.
여기서 중요한 건 Batch Normalization이 학습 단계와 추론 단계에서 조금씩 다르게 적용되어야 하는 것이다.
먼저 학습 단계에서는 위의 사진과 같이 정규화를 하게 된다. γ 는 스케일링 역할을 하고 β는 bias이고 이 두 값 모두 backpropagation을 통해 학습을 하게 된다.
위의 사진을 보면, 평균과 분산 모두 B로 나뉘는 것을 알 수 있다. 각 배치들의 평균과 분산이 표준 정규 분포를 따르기 위함이다. 이렇게 Feature를 정규화하면 생기는 장점이 있는데, Feature들이 모두 동일한 Scale이 되어 learning rate 결정에 유리해진다. Scale이 다르면, gradient descent를 할 때, gradient가 다르게 되고 learning rate에 대해 weight마다 반응하는 정도가 달라지게 된다. gradient 간의 편차가 크면, gradient가 큰 weight은 gradient exploding, 작은 weight은 gradient vanishing 문제가 발생할 수 있다. 정규화를 통해 weight의 반응을 같게 만들어 학습에 더 유리하게 해준다. 추가적으로 normalize 단계에서 입실론이 더해지는 데, 이는 0으로 나뉘는 것을 방지하기 위해 쓰이는 것이다.
다음은 γ,β의 역할을 알아보겠다. BN은 activation function 앞에서 적용되는데, BN이 된 후 weight의 평균은 0, 분산은 1이 된다. 이 상태에서 ReLU 활성화 함수를 돌려버리면, 전체 분포에서 절반이 음수이므로 0이 되어 버린다. 기껏 정규화를 했는데 다 사라져 버리면 의미가 없어져 버리게 된다. 이러한 불상사를 방지하기 위해 γ,β가 정규화 값에 곱해지고 더해지는 것이다. 이 값들은 학습을 통해서 최적화된다.
추론 과정에서는 BN에 적용할 평균과 분산에 고정값을 사용한다. 학습 단계에서는 데이터가 배치 단위로 들어와 배치의 평균/분산 값을 구하는 것이 가능하지만, 테스트 단계에서는 배치 단위로 평균/분산을 구하기가 어려워 고정된 평균/분산을 사용한다.
고정된 평균과 분산은 학습 과정에서 이동 평균(moving average) 또는 지수 평균(exponential average)을 통해 계산한 값이다. 이동 평균을 하게 되면 최근 N개 이전의 평균과 분산은 미반영되지만, 지수 평균을 이용하면 전체 데이터가 반영된다. 그리고 이때 사용되는 γ,β는 학습 과정에서 학습한 파라미터이다.
참고로 내가 참고한 사이트에 따르면 이 부분을 강조했는데, 학습 과정과 추론 과정의 알고리즘이 다르므로 framework에서 사용할 때, 학습 과정인지, 추론 과정인지에 따라 다르게 동작하도록 관리하는 것이 중요하다고 했다. 즉, 추론 과정에서는 framework의 옵션을 지정해서 평균과 분산을 moving average/variance를 사용하도록 하는 것이 중요하다고 했습니다.
Batch의 크기가 너무 작으면 잘 동작하지 않는다. 극단적인 예시로 Batch size가 1이라면 평균은 샘플 값이고 표준편차는 0이 되므로 정상적으로 normalization이 잘 되지 않을 것이다. 그리고 너무 작으면, 큰수의 법칙과 중심 극한 이론을 만족하지 못해 평균과 표준 편차가 데이터 전체 분포를 잘 표현하지 못할 수도 있다.
반대로 Batch의 크기가 너무 커도 잘 동작하지 않는다. 너무 큰 경우에는 multi modal 형태의 gaussian mixture 모델 형태가 나타날 수 있기 때문이다 (정규 분포의 봉우리가 여러개 나타나는 형태). 또한 크기가 너무 커서 병렬 연산이 비효율적일 수 있고 gradient를 계산하는 시점이 Batch 단위인데, gradient를 한번에 너무 많이 하게 되어 학습에 악영향이 있을 수 있다. 이 한계를 개선하기 위해 Weight Normalization이나 Layer Normalization을 사용할 수 있는데, 오늘은 여기까지만 하려고 한다.
torch.nn.BatchNorm1d
나 torch.nn.BatchNorm2d
가 대표적인 방법이다. 이 두가지 방법 모두 아래 식을 따른다. 특히 γ와 β는 학습되는 파라미터 이며 기본값은 γ=1,β=0을 초기값으로 학습을 시작한다.
이때의 차이점은 BatchNorm1d
는 Input과 Output이 (N,C) 또는 (N,C,L)의 형태를 가지고 BatchNorm2d
는 (N,C,W,H)의 형태를 갖는다는 것이다. N은 Batch의 크기, C는 Channel, L은 Length를, H와 W는 각각 Height와 Width를 의미한다. BatchNorm
에서 반드시 입력으로 들어가야 하는 인자는 C, 즉 Channel의 수이다. 코드 예시를 살펴보면 다음과 같다.
# With Learnable Parameters
m = nn.BatchNorm1d(100)
input = torch.randn(20, 100)
output = m(input) # print(output.size()) => torch.Size([20, 100])
# With Learnable Parameters
m = nn.BatchNorm2d(100)
input = torch.randn(20, 100, 35, 45)
output = m(input) # print(output.size()) => torch.Size([20, 100, 35, 45])
Batch Normalization이 Channel을 기준으로 연산되기 때문에 Channel의 수만 맞춰주면 된다. nn.BatchNorm1d
의 경우를 살펴보면 다음과 같다. 평균은 0에 가깝고 표준편차도 또한 1에 가까워진 것을 알 수 있다.
지금까지 훈련한 모델의 test accuracy는 다음과 같다. Activation function을 ReLU로 그리고 Batch Normalization을 사용한 모델의 정확도가 가장 높은 것을 알 수 있다.
모델을 훈련시킬 때, 목적함수를 최적화하기 위해 역전파를 이용해서 가중치(weight)를 조금 변화시킨다. 지금까지 가중치 값들을 변화시키는 것도 중요하지만, 가중치의 초기값도 중요하다. 초기값에 따라서 학습의 진행방향이 달라지기 때문이다. torch.nn.init.uniform_()
, torch.nn.init.normal_()
, torch.nn.init.xavier_uniform_()
등 다양한 초기값 function들이 존재하는데 그 중 해당 torch.nn.init.kaiming_uniform_()
을 사용하고자 한다. 해당 function을 자세히 살펴보면 다음과 같은 parameter들이 존재한다. torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
하나하나 살펴보면
'leaky-relu'
)'fan-in'
or 'fan-out'
. Choosing 'fan-in'
preserves the magnitude of the variance of the weights in the forward pass. 'fan-out'
preserves the magnitudes in the backwards pass.'relu'
or 'leaky-relu'
.해당 초기화 방식은 He initialization이라고도 불리며, 무작위 초기화가 아닌 레이어의 특성을 고려한 초기화라서 Xavier Initialization과 같이 최근 자주 쓰인다.
이 부분은 Optimizer에 대한 부분인데.. 내용이 복잡하다. 그래서 사진 몇 장과 움직이는 짤로 대체를 해보도록 하겠다.
위의 사진들과 짤에서 볼 수 있듯이 SGD는 매우 느리고 다음으로는 Momentum RMSProp 등이 있다. 여기서 장점들만 가져온게 Adam이고 이 기법을 잘 쓰면 되는 것이다.
Adam 기법을 사용했을 때의 정확도 결과를 보여주겠다.
3-5가 Adam 기법을 사용했을 때인데, 처음부터 정확도가 꽤 높으며 훈련이 다 되었을 때에도 성능이 좋은 걸 확인할 수 있다.
https://github.com/Justin-A/DeepLearning101
https://wandb.ai/wandb_fc/korean/reports/---VmlldzoxNDI4NzEy
https://gooopy.tistory.com/55
https://wegonnamakeit.tistory.com/47
https://89douner.tistory.com/44
https://gaussian37.github.io/dl-concept-batchnorm/
https://onevision.tistory.com/entry/Optimizer-%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-%ED%8A%B9%EC%84%B1-Momentum-RMSProp-Adam