[텐서플로2] MNIST 데이터를 훈련 데이터로 사용한 DNN 학습

TensorFlow 2에서 손글씨로 작성해 스캔한 MNIST 데이터를 DNN 모델 학습을 통해 분류하는 코드를 정리해 봅니다.

먼저 아래처럼 텐서플로 라이브러리를 임포트 해야 합니다.

import tensorflow as tf

텐서플로와 케라스가 매우 밀접하게 통합되었고, 다양한 데이터셋이 케라스 라이브러리를 통해 활용할 수 있습니다. 아래의 코드를 통해 MNIST 데이터셋을 인터넷을 통해 가져옵니다.

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

x_train에는 총 60000개의 28×28 크기의 이미지가 담겨 있으며, y_train에는 이 x_train의 60000개에 대한 값(0~9)이 담겨 있는 레이블 데이터셋입니다. 그리고 x_train과 y_train은 각각 10000개의 이미지와 레이블 데이터셋입니다. 먼저 x_train와 y_train을 통해 모델을 학습하고 난 뒤에, x_test, y_test 를 이용해 학습된 모델의 정확도를 평가하게 됩니다. 다음 코드는 신경망 모델입니다.

model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(10, activation='softmax')
])

총 4개의 레이어로 구성된 신경망인데, 1번째 레이어는 입력 이미지의 크기가 28×28이므로 이를 1차원 텐서로 펼치는 것이고, 2번째 레이어는 1번째 레이어에서 제공되는 784 개의 값(28×28)을 입력받아 128개의 값으로 인코딩해 주는데, 활성함수로 ReLU를 사용하도록 하였습니다. 2번째 레이어의 실제 연산은 1번째 레이어에서 제공받은 784개의 값을 784×128 행렬과 곱하고 편향값을 더하여 얻은 128개의 출력값을 다시 ReLU 함수에 입력해 얻은 128개의 출력입니다. 3번째는 128개의 뉴런 중 무작위로 0.2가 의미하는 20%를 다음 레이어의 입력에서 무시합니다. 이렇게 20% 정도가 무시된 값이 4번째 레이어에 입력되어 충 10개의 값을 출력하는데, 여기서 사용되는 활성화 함수는 Softmax가 사용되었습니다. Softmax는 마지막 레이어의 결과값을 다중분류를 위한 확률값으로 해석할 수 있도록 하기 위함입니다. 10개의 값을 출력하는 이유는 입력 이미지가 0~9까지의 어떤 숫자를 의미하는지에 대한 각각의 확률을 얻고자 함입니다. 이렇게 정의된 모델을 학습하기에 앞서 다음처럼 컴파일합니다.

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

모델의 학습 중에 역전파를 통한 가중치 최적화를 위한 기울기 방향에 대한 경사하강을 위한 방법으로 Adam을 사용했으며 손실함수로 다중 분류의 Cross Entropy Error인 ‘sparse_categorical_crossentropy’를 지정하였습니다. 그리고 모델 평가를 위한 평가 지표로 ‘accuracy’를 지정하였습니다. 이제 다음처럼 모델을 학습할 수 있습니다.

model.fit(x_train, y_train, epochs=5)

학습에 사용되는 데이터넷과 학습 반복수로 5 Epoch을 지정했습니다. Epoch은 전체 데이터셋에 대해서 한번 학습할때의 단위입니다. 학습이 완료되면 다음과 같은 내용이 출력됩니다.

Train on 60000 samples
Epoch 1/5
2019-11-16 21:24:27.115767: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library cublas64_100.dll
60000/60000 [==============================] – 6s 103us/sample – loss: 0.2971 – accuracy: 0.9137
Epoch 2/5
60000/60000 [==============================] – 5s 78us/sample – loss: 0.1428 – accuracy: 0.9577
Epoch 3/5
60000/60000 [==============================] – 5s 79us/sample – loss: 0.1074 – accuracy: 0.9676
Epoch 4/5
60000/60000 [==============================] – 5s 80us/sample – loss: 0.0846 – accuracy: 0.9742
Epoch 5/5
60000/60000 [==============================] – 5s 80us/sample – loss: 0.0748 – accuracy: 0.9766

다음 코드로 모델을 평가합니다.

model.evaluate(x_test,  y_test, verbose=2)

평가를 위한 데이터셋을 지정하고, 평가가 끝나면 다음과 같이 평가 데이터셋에 대한 손실값과 정확도가 결과로 표시됩니다.

10000/1 – 1s – loss: 0.0409 – accuracy: 0.9778

이진분류(Binary Classification)에 대한 Cross Entropy Error

현재 딥러닝에서 분류에 대해 가장 흔히 사용되는 손실함수는 Cross Entropy Error(CEE)입니다. CEE를 비롯하여 다른 손실함수를 간단이 정리하는 글은 아래와 같습니다.

손실함수(Loss Function)

다시, CEE에 대한 공식을 언급하면 다음과 같습니다.

    $$L=-\displaystyle\sum_{i=1}^{n} {t_{i}\log{y_i}}$$

t는 정답 값이고, y는 추론 값입니다. 정답의 개수와 추론의 개수는 당연이 같구요. 이 개수가 2개이면 이진분류이고 2개보다 많으면 다중분류입니다. y값은 신경망 여러 개를 거쳐 산출된 값이 최종 마지막 어떤 특별한 활성함수의 입력값이 되어 산출된 결과값입니다. 이진분류로 가장 많이 사용되는 활성함수는 Sigmoid이고 다중분류로 가장 많이 사용되는 것이 Softmax입니다. 이러한 활성화 함수에 대한 글은 아래와 같습니다.

활성화 함수(Activation Function)

CEE는 추론값과 그에 대한 정답값들에 대한 연산의 합입니다. 즉, 추론값과 정답값 사이의 괴리(손실)을 합한것입니다. 이진분류는 추론값과 정답값이 2개로, 하나는 참이고 두번째는 거짓입니다. 참은 1이고 거짓은 0값입니다. 이 이진분류를 CEE로 나타내면 다음과 같습니다.

    $$L=-\displaystyle\-(t\log(y) + (1-t)\log(1-y))$$

진관적으로 이해할 수 없다면, 각각의 경우로 구체화하여 이해할 수 있는데, 다행이 구체화할 경우의 수가 2가지입니다. 즉, 참일때(t=1) L은 -logy가 되고, 거짓일때(t=0) L은 -log(1-y)가 됩니다. 이것은 무엇을 의미할까요? log 그래프를 보면 다음과 같습니다.

y는 Sigmoid와 Softmax의 결과이므로 값의 범위는 0~1사이입니다. 0은 무한대이고, 1은 0이입니다. 즉 참일때 y가 1(참)에 가까울 수록 L(손실, 오류)은 0에 가까워지고 y가 0(거짓)에 가까울 수록 L은 무한대에 가까워집니다. 또한 거짓일때 y가 1(참)에 가까울 수록 L(손실, 오류)은 무한대에 가까워지고 y가 0(거짓)에 가까울 수록 L은 0 가까워집니다. 손실함수의 기반과 정확이 일치한다는 것일 알 수 있습니다.

이미지 분류 모델의 구성 레이어에 대한 결과값 시각화

이미지에 대한 Classification 및 Detection, Segmentation에 대한 신경망 모델을 구성하는 레이어 중 Convolution 관련 레이어의 결과값에 대한 시각화에 대한 내용입니다. 딥러닝 라이브러리 중 PyTorch로 예제를 작성했으며, CNN 모델 중 가장 이해하기 쉬운 VGG를 대상으로 하였습니다.

먼저 필요한 패키지와 미리 학습된 VGG 모델을 불러와 그 레이어 구성을 출력해 봅니다.

import matplotlib.pyplot as plt
from torchvision import transforms
from torchvision import models
from PIL import Image

vgg = models.vgg16(pretrained=True).cuda()
print(vgg)

결과는 다음과 같습니다.

VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)

.
.

(생략)

위의 특징(Feature)를 추출하는 레이어 중 0번째 레이어의 출력결과를 시각화 합니다. PyTorch는 특정 레이어의 입력 데이터와 그 연산의 결과를 특정 함수로, 연산이 완료되면 전달해 호출해 줍니다. 아래는 이에 대한 클래스입니다.

class LayerResult:
    def __init__(self, payers, layer_index):
        self.hook = payers[layer_index].register_forward_hook(self.hook_fn)
    
    def hook_fn(self, module, input, output):
        self.features = output.cpu().data.numpy()
    
    def unregister_forward_hook(self):
        self.hook.remove()

LayerResult은 레이어의 연산 결과를 검사할 레이어를 특정하는 인자를 생성자의 인자값으로 갖습니다. 해당 레이어의 register_forward_hook 함수를 호출하여 그 결과를 얻어올 함수를 등록합니다. 등록된 함수에서 연산 결과를 시각화하기 위한 데이터 구조로 변환하게 됩니다. 이 클래스를 사용하는 코드는 다음과 같습니다.

result = LayerResult(vgg.features, 0)

img = Image.open('./images/cat.jpg')
img = transforms.ToTensor()(img).unsqueeze(0)
vgg(img.cuda())

activations = result.features

위의 코드의 마지막 라인에서 언급된 activations에 특정 레이어의 결과값이 담겨 있습니다. 이제 이 결과를 출력하는 코드는 다음과 같습니다.

fig, axes = plt.subplots(8,8)
for row in range(8):
    for column in range(8):
        axis = axes[row][column]
        axis.get_xaxis().set_ticks([])
        axis.get_yaxis().set_ticks([])
        axis.imshow(activations[0][row*8+column])

plt.show()

결과 이미지가 총 64인데, 이는 앞서 VGG의 구성 레이어를 살펴보면, 첫번째 레이어의 출력 채널수가 64개이기 때문입니다. 결과는 다음과 같습니다.

추가로 특정 레이어의 가중치값 역시 시각화가 가능합니다. 아래의 코드가 그 예입니다.

import matplotlib.pyplot as plt
from torchvision import transforms
from torchvision import models
from PIL import Image

vgg = models.vgg16(pretrained=True).cuda()

print(vgg.state_dict().keys())
weights = vgg.state_dict()['features.0.weight'].cpu()

fig, axes = plt.subplots(8,8)
for row in range(8):
    for column in range(8):
        axis = axes[row][column]
        axis.get_xaxis().set_ticks([])
        axis.get_yaxis().set_ticks([])
        axis.imshow(weights[row*8+column])

plt.show()

9번 코드에서 가중치를 가지는 레이어의 ID를 출력해 주는데, 그 결과는 다음과 같습니다.

odict_keys([‘features.0.weight’, ‘features.0.bias’, ‘features.2.weight’, ‘features.2.bias’, ‘features.5.weight’, ‘features.5.bias’, ‘features.7.weight’, ‘features.7.bias’, ‘features.10.weight’, ‘features.10.bias’, ‘features.12.weight’, ‘features.12.bias’, ‘features.14.weight’, ‘features.14.bias’, ‘features.17.weight’, ‘features.17.bias’, ‘features.19.weight’, ‘features.19.bias’, ‘features.21.weight’, ‘features.21.bias’, ‘features.24.weight’, ‘features.24.bias’, ‘features.26.weight’, ‘features.26.bias’, ‘features.28.weight’, ‘features.28.bias’, ‘classifier.0.weight’, ‘classifier.0.bias’, ‘classifier.3.weight’, ‘classifier.3.bias’, ‘classifier.6.weight’, ‘classifier.6.bias’])

위의 레이어 ID로 가중치값을 가져올 레이어를 특정할 수 있는데요. 최종적으로 위의 코드는 다음과 같이 가중치를 시각화해 줍니다.

파이썬의 NLTK 라이브러리를 이용한 WordNet 활용

WordNet은 프린스턴 대학교에서 구축한 유의어 DB인데, 유의어 사이의 관계를 그래프로 정의하고 있는 방대한 데이터입니다. 이 WordNet을 이용하여 유사한 단어를 파악할 수 있고, 각 단어의 유사도를 계산할 수 있습니다. 이를 파이썬의 NLTK를 이용해 활용할 수 있는데, 이에 대한 코드를 정리해 봅니다.

먼저 NLTK 패키지의 설치가 필요합니다. 제 환경에서는 conda install nltk와 같은 식으로 설치하였습니다.

NLTK 패키지의 설치가 끝나면 다음과 같은 코드를 통해 WordNet 데이터를 PC에 다운로드 받을 수 있습니다.

import nltk

nltk.download('wordnet')

가장먼저 ‘man’이라는 단어의 유의어를 검색하기 위해 다음 코드가 사용됩니다.

from nltk.corpus import wordnet

print(wordnet.synsets('man'))
''' Output:
[
    Synset('man.n.01'), 
    Synset('man.n.03'), 
    Synset('man.n.05'), 
    Synset('man.n.06'),     
    Synset('man.n.08'), 
    Synset('man.n.09'), 
    Synset('man.n.10'), 
    Synset('man.v.01'), 
    Synset('man.v.02')],
    Synset('serviceman.n.01'), 
    Synset('homo.n.02'), 
    Synset('valet.n.01'),     
    Synset('world.n.08')
]     
'''

위의 검색에서 man.n.01의 경우 n은 명사를 의미합니다. 참고로 v는 동사이구요. 01은 man에 대한 인덱스값입니다. 이 man.n.01에 대한 사전적 의미를 얻기 위한 코드는 다음과 같습니다.

man = wordnet.synset('man.n.01')
print(man.definition()) # an adult person who is male (as opposed to a woman)

버그인지는 모르겠으나, man.n.02는 검색되지 않았음에도 아래의 코드는 유효합니다.

man2 = wordnet.synset('man.n.02')
print(man2.definition()) # someone who serves in the armed forces; a member of a military force

man.n.01에 대한 단어의 관계 그래프는 다음 코드를 통해 얻을 수 있습니다.

print(man.hypernym_paths())
''' Output:
[
    [Synset('entity.n.01'), Synset('physical_entity.n.01'), Synset('causal_agent.n.01'), Synset('person.n.01'), Synset('adult.n.01'), Synset('man.n.01')], 
    [Synset('entity.n.01'), Synset('physical_entity.n.01'), Synset('object.n.01'), Synset('whole.n.02'), Synset('living_thing.n.01'), Synset('organism.n.01'), Synset('person.n.01'), Synset('adult.n.01'), Synset('man.n.01')], 
    [Synset('entity.n.01'), Synset('physical_entity.n.01'), Synset('causal_agent.n.01'), Synset('person.n.01'), Synset('male.n.02'), Synset('man.n.01')], 
    [Synset('entity.n.01'), Synset('physical_entity.n.01'), Synset('object.n.01'), Synset('whole.n.02'), Synset('living_thing.n.01'), Synset('organism.n.01'), Synset('person.n.01'), Synset('male.n.02'), Synset('man.n.01')]
]
'''

위의 관계 그래프 정보는 총 4개의 list로 구성된 list인데, 이를 시각화하면 다음과 같습니다.

관계 그래프의 경로 역시 4개라는 것을 알 수 있고, 경로의 흐름에 따라 단어의 의미가 점점 구체화됩니다. 이 관계 그래프를 통해 서로 다른 단어에 대한 유사도를 계산할 수 있는데, 아래의 코드가 그 예입니다.

boy = wordnet.synset('boy.n.01')
guy = wordnet.synset('guy.n.01')
girl = wordnet.synset('girl.n.01')
woman = wordnet.synset('woman.n.01')

print(man.path_similarity(man))     # 1.0
print(man.path_similarity(boy))     # 0.3333333333333333
print(man.path_similarity(guy))     # 0.5
print(man.path_similarity(girl))    # 0.25
print(man.path_similarity(woman))   # 0.3333333333333333

단어 간의 유사도는 0부터 1까지의 값으로, 1에 가까울수록 두 단어의 의미가 더욱더 유사하다는 의미이고, 0에 가까울수록 그 의미의 관계가 거의 없다는 것을 의미합니다.

딥러닝 학습 향상을 위한 고려 사항들

1. 가중치 감소(Weight Decay)를 통한 가중치 정형화(Weight Regularization)

손실함수에 어떤 제약 조건을 적용해 오버피팅을 최소화하는 방법으로 L1 정형화와 L2 정형화가 있습니다. 오버피팅은 특정 가중치값이 커질수록 발생할 가능성이 높아지므로 이를 해소하기 위해 특정값을 손실함수에 더해주는 것이 정형화 중 가중치 감소(Weight Decay)이며, 더해주는 특정값을 결정하는 것이 L1 정형화와 L2 정형화입니다. 파이토치에서 이 Weight Decay는 다음 코드처럼 적용할 수 있습니다.

optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay=0.1)

결과적으로 weight_decay의 값이 커질수록 가중치 값이 작어지게 되고, 오버피팅 현상을 해소할 수 있지만, weight_decay 값을 너무 크게 하면 언더피팅 현상이 발생하므로 적당한 값을 사용해야 합니다.

2. 데이터 증강(Data Augmentation)

훈련 데이터를 알고리즘에 의해 그 수를 늘리는 방법으로, 오버피팅 해소는 물론 훈련 및 테스트 데이터에 대한 정확도를 높일 수 있는 방법입니다. 다음 코드의 예처럼 데이터셋에 대한 transform 인자에 지정하여 적용할 수 있습니다.

mnist_train = dset.MNIST(
    './', 
    train=True, 
    transform = transforms.Compose([
        transforms.Resize(34),                        # 원래 28x28인 이미지를 34x34로 늘립니다.
        transforms.CenterCrop(28),                    # 중앙 28x28를 뽑아냅니다.
        transforms.RandomHorizontalFlip(),            # 랜덤하게 좌우반전 합니다.
        transforms.Lambda(lambda x: x.rotate(90)),    # 람다함수를 이용해 90도 회전해줍니다.
        transforms.ToTensor(),                        # 이미지를 텐서로 변형합니다.
    ]),
    target_transform=None,
    download=True
)

3. 가중치 초기화(Weight Initialization)

신경망이 깊어질 수록 각 신경망의 가중치값들의 분포가 한쪽으로 쏠리거나, 특정 값 부분으로 심하게 모일 수 있는 현상이 발생합니다. 이런 현상이 발생하면 기울기 소실(Gradient Vanishing)이 발생할 수 있고, 신경망의 표현력에 제한이 생겨 신경망을 깊게 구성하는 의미가 사라집니다. 이를 위해 학습하기 전에 가중치를 적당하게 초기하는 것이 필요합니다. 아래의 코드는 파이토치에서 흔히하게 가중치를 초기하는 코드의 예입니다.

class aDNN(nn.Module):
    def __init__(self):
        super(aDNN,self).__init__()
        self.layer = nn.Sequential(
            # ...
        )             
        
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                '''
                m.weight.data.normal_(0.0, 0.1)     # 가중치를 평균 0, 편차 0.1로 초기화
                m.bias.data.fill_(0)                # 편차를 0으로 초기화
                '''

                '''
                init.xavier_normal(m.weight.data)   # Xavier Initialization
                m.bias.data.fill_(0)                # 편차를 0으로 초기화 
                '''

                '''                
                init.kaiming_normal_(m.weight.data) # Kaming He Initialization
                m.bias.data.fill_(0)                # 편차를 0으로 초기화 
                '''
            elif isinstance(m, nn.Linear):
                # 위의 코드와 유사함

Xavier 초기화는 활성화함수가 Sigmoid나 Tanh일때 적당하며, Kaming He 초기화는 활성화 함수가 ReLU일때 적당합니다.

4. 학습률 스케쥴러(Learning Rate Scheduler)

학습이 진행되면서 학습률을 그 상황에 맞게 가변적으로 적당하게 변경될 수 있다면 더 낮은 손실값을 얻을 수 있습니다. 이를 위해 학습률을 스케쥴이 필요합니다. 이와 관련된 코드는 다음과 같습니다.

# 학습률 스케줄러는 옵티마이져를 통해 적용된다.
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 지정한 스텝 단위로 학습률에 감마를 곱해 학습률을 감소시키는 방식
scheduler = lr_scheduler.StepLR(optimizer, step_size=1, gamma= 0.99)       

# 지정한 스텝 지점(예시에서는 10,20,40)마다 학습률에 감마를 곱해줘서 감소시키는 방식
scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[10,20,40], gamma= 0.1)  

# 매 epoch마다 학습률에 감마를 곱해줌으로써 감소시키는 방식
scheduler = lr_scheduler.ExponentialLR(optimizer, gamma= 0.99)                             

# 원하는 에폭마다, 이전 학습률 대비 변경폭에 따라 학습률을 감소시켜주는 방식
scheduler = lr_scheduler.ReduceLROnPlateau(optimizer,threshold=1,patience=1,mode='min')

위의 스케줄러를 학습에 적용할때 사용방법은 나뉘는데, StepLR, MultiStepLR, ExponentialLR의 경우는 아래 코드를 참고하기 바랍니다.

for i in range(num_epoch):
    scheduler.step() # !!

    optimizer.zero_grad()
    output = model.forward( .. )
    loss = loss( .. )
    loss.backward()
    optimizer.step()

ReduceLRONPlateau의 경우는 아래 코드를 참고하기 바랍니다.

for i in range(num_epoch):
    optimizer.zero_grad()
    output = model.forward( .. )
    loss = loss( .. )
    loss.backward()
    optimizer.step()
    
    scheduler.step(loss) # !!  

5. 학습 데이터의 정규화(Data Normalization)

입력되는 데이터에 대해서 공간상 분포를 정규화시켜주게 되면, 더 높은 정확도를 얻을 수 있게 됩니다. 정규화의 방법은 전체 데이터에 대한 평균과 표준편차를 이용하는데, 데이터에 대해서 평균을 빼고 표준편차로 나눠줍니다. 이를 위한 코드의 예는 아래와 같습니다.

mnist_train = dset.MNIST(
    "./", 
    train=True, 
    transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize(mean=(0.1307,), std=(0.3081,))
    ]),
    download=True
)

mnist_test = dset.MNIST(
    "./", 
    train=False, 
    transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize(mean=(0.1307,), std=(0.3081,))
    ]),
    download=True)

데이터에 대한 평균 및 표준편차를 얻는 것은 아래의 글을 참고하기 바랍니다.

이미지 Dataset에 대한 평균과 표준편차 구하기

6. 다양한 경사하강법(Gradient Descent Variants)

최소의 손실값을 찾기 위해 손실함수의 미분으로 구한 기울기를 따라 이동하게 되는데, 이동하는 방식에 대한 선택에 대한 것입니다. SGD 방식, Adam 방식 등이 존재하는데 그중 Adam에 대한 파이토치의 코드는 다음과 같습니다.

optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

SGD와 Adam 이외에도 Momentum 방식과 AdaGrad 방식의 있으며, Adam이 Momentum 방식과 AdaGrad 방식의 장점을 혼합한 것입니다.

7. 배치 정규화(Batch Normalization)

각 신경망의 활성화 값 분포가 적당히 퍼지도록 개선하여 원할한 학습이 진행되도록 돕는 기법입니다. 학습속도가 빨라지고, 앞서 언급한 가중치 초기값 설정에 신경쓸 필요가 없게 됩니다. 또하는 오버피팅을 억제하게 되어 가중치 감소 기법이나 드롭아웃 기법의 필요성이 감소합니다. 이 배치 정규화는 배치 단위로 입력되는 데이터의 연산 결과를 다시 결과의 평균과 분선이 1이 되도록 재가공합니다. 그리고 또 다시 특정 값을 곱하고 또 다른 특정값을 더하는 간단한 Scaling 및 Shifting 처리가 이루어집니다. Scaling 및 Shifting 처리를 위한 2개의 값은 학습을 통해 산출됩니다.

아래의 코드는 배치 정규화에 대한 예제 코드입니다.

class CNN(nn.Module):
    def __init__(self):
        super(CNN,self).__init__()
        self.layer = nn.Sequential(
            nn.Conv2d(1, 16, 3, padding=1),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.Conv2d(16, 32, 3,padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),           
            nn.Conv2d(32, 64, 3,padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)        
        )
        self.fc_layer = nn.Sequential(
            nn.Linear(64*7*7, 128),
            nn.BatchNorm1d(128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(),            
            nn.Linear(64, 10),
        )       

    def forward(self,x):
        out = self.layer(x)
        out = out.view(batch_size, -1)
        out = self.fc_layer(out)
        return out

학습이 끝나고 평가 시에는 반드시 아래의 코드 호출이 선행되어야 합니다.

model.eval()

8. 드롭아웃(Drop Out)

이 기법이 적용되면 훈련 데이터에 대한 정확도가 떨어지지만 오버피팅을 억제하기 위한 기법입니다. 즉, 특정 확률로 신경망의 뉴런을 비활성화시켜 연산에서 제외시키는 방식입니다. 드롭아웃에 대한 코드 예는 다음과 같습니다.

layer = nn.Sequential(
     nn.Conv2d(1,16,3,padding=1),
     nn.ReLU(),
    nn.Dropout2d(0.2),
    nn.Conv2d(16,32,3,padding=1),
    nn.ReLU(),
    nn.Dropout2d(0.2),
    nn.MaxPool2d(2,2),
    nn.Conv2d(32,64,3,padding=1),
    nn.ReLU(),
    nn.Dropout2d(0.2),
    nn.MaxPool2d(2,2)
)

학습이 끝나고 평가 시에는 반드시 아래의 코드 호출이 선행되어야 합니다.

model.eval()