L2 Regularization

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

저번 시간에 optimizer를 만들며 기본적인 딥 러닝 함수들은 모두 만들었습니다.

저 함수들만 사용하더라도 꽤나 좋은 인공지능을 제작할 수 있겠죠.

 

하지만, 이번 시간에는 여기서 조금 더 나아가서 인공지능의 정확도를 더욱 높일 수 있는 방법들에 대해 설명하고, 구현해 보겠습니다.

 

 

가장 먼저 구현할 것은, 가중치의 초기화입니다.

지금까지 보신 분들은 아시겠지만, 지금까지는 가중치 초기화를 단순히 랜덤값으로 선언했었습니다.

하지만, 이렇게 단순 랜덤으로 가중치를 초기화하면 Vanishing Gradient가 발생할 가능성이 매우 커집니다.

Vanishing Gradient란, 딥 러닝에서 많은 레이어들을 거치며 가중치에 값을 곱하고 활성화 함수를 거치는 과정에서, 다수의 Gradient들이 0으로 수렴하게 되는 현상을 의미합니다.

그 뿐만 아니라, 가중치를 너무 크게 초기화하면 레이어들을 거치며 Gradient가 무한으로 발산하는 경우도 발생할 수 있습니다.

 

또한, Sigmoid함수의 경우는 Saturation이라는 문제도 발생합니다.

이는 Gradient의 값의 범위가 너무 클 때 생기는 문제인데, Sigmoid의 경우 값이 너무 작으면 0에 수렴하고, 값이 너무 크면 1에 수렴하는 특징을 가지고 있습니다.

하지만 만약 가중치의 값을 -1 ~ 1로 초기화를 해 버리면, 들어오는 데이터에 따라 w*x의 값이 매우 커질수도 있습니다.

만약 이미지의 크기가 10*10이고, 모든 이미지 픽셀이 1이고 가중치도 1이라면, w*x는 최대 100이라는 수치가 나옵니다.

이러한 Gradient는 어떻게 해도 1에 수렴하게 되기 때문에, 학습에 아무런 도움도 주지 않습니다.

반대로, 가중치가 -1이었다면 w*x는 최소 -100이라는 수치가 나오게 되고, 그러면 마찬가지로 Gradient가 0으로 수렴하게 됩니다.

 

Xavier Initialization은 이러한 Saturation의 문제점을 해결할 수 있는 Initialization 방법 중 하나입니다.

이 방식은 w*x의 값의 범위를 -1~1로 제한해줄 뿐만 아니라, 앞 레이어와 뒤 레이어의 분산까지 맞춰줌으로써 학습이 더욱 원활하게 되게 해 줍니다.

 

 

이론적인 이야기는 여기까지만 하고, 바로 구현에 들어가 봅시다.

 

    def addlayer(self, layer, activation=False, input_size=None, name=None, initialization=None):
        if name is None:
            name = str(self.num)

        self.keys.append(name)
        self.num += 1
        self.layers[name] = layer

        if not activation:
            if isinstance(layer, AddLayer):
                self.params[name] = np.zeros(input_size)
            elif initialization is 'xavier':
                self.params[name] = xavier_initialization(input_size)
            else:
                self.params[name] = np.random.uniform(-1, 1, input_size)
            self.layers[name].param = self.params[name]

Model.py의 addlayer 부분입니다.

initialization이 'xavier'로 선언되었을 경우에 xavier_initialization으로 가중치 값을 초기화 하게 됩니다.

또한, AddLayer, 즉 bias의 경우는 처음부터 그냥 0으로 초기화해 주었습니다. (원래 bias는 random 초기화를 하지 않습니다)

 

 

def xavier_initialization(input_size):
    fan_in = input_size[0]
    fan_out = input_size[1]
    n = np.sqrt(6 / (fan_in + fan_out))
    w = np.random.uniform(-n, n, input_size)
    return w

다음은 위에서 사용한 xavier_initialization 함수 내부입니다.

uniform xavier 공식을 그대로 사용했습니다. (공식 유도까지 하면 너무 오래 걸리는 관계로 생략하겠습니다.)

 

또한, 위에서 언급한 Xavier initialization은 보통 activation function, 즉 활성화 함수가 sigmoid함수일 때 자주 사용합니다.

활성화 함수가 ReLU일 때는, He Initialization을 사용하는데, 크게 달라지는 점은 없으니 그냥 바로 코드로 넘어가겠습니다.

 

        if not activation:
            if isinstance(layer, AddLayer):
                self.params[name] = np.zeros(input_size)
            elif initialization is 'xavier':
                self.params[name] = xavier_initialization(input_size)
            elif initialization is 'he':
                self.params[name] = he_initialization(input_size)
            else:
                self.params[name] = np.random.uniform(-1, 1, input_size)

            self.layers[name].param = self.params[name]

...그냥 아까 윗부분에서 elif ... he: 부분만 추가했고,

 

def he_initialization(input_size):
    fan_in = input_size[0]
    n = np.sqrt(6 / fan_in)
    w = np.random.uniform(-n, n, input_size)
    return w

xavier_initialization 아래 부분에 그냥 하나 새로 만들었습니다.

 

이제, 이렇게 함으로써 다층 Neural Network를 구현할 때에도 Vanishing Gradient나 Saturating 현상이 덜 일어나게 될 것입니다.

 

 

 

다음으로, 정규화 (Regularization)을 구현해 보겠습니다.

 

Regularization이란, 인공지능의 Overfitting을 막기 위해 사용되는 기법으로, 너무 특정 데이터들에만 강력하게 학습되는 것을 막기 위한 장치입니다.

Regularization에는 여러 가지 종류 (L1, L2 ...) 등이 있는데, 이번 포스팅에서는 L2 Regularization에 대해서만 다루겠습니다.

(다른 Regularization은 직접 구현해 보세요!)

 

overfitting 예시

 

Regularization은 Loss값에 W(가중치)에 비례한 값을 더해서 overfitting을 막는 기법입니다. (L2의 경우, W^2에 비례합니다.)

즉, W값이 크다면 그만큼 Loss값은 커지고, W값이 작다면 그만큼 Loss값이 줄어드는 것이죠.

그런데 어째서 W값에 비례한 값을 Loss에 더해주는 걸까요?

이를 직관적으로 한번 이해해 봅시다.

 

 

우리의 인공지능이 특정 데이터에만 치중되어 학습된다는 것은 무엇을 의미할까요?

지금까지 해 왔던 MNIST 데이터셋으로 예시를 들어 보겠습니다.

 

만약 우리가 숫자 "2"를 쓴 이미지를 인공지능에 넣었다고 합시다.

그러면, 인공지능은 이 이미지에서 2와 관련된 픽셀값의 특징을 확인해 가며, "2"에 해당하는 답으로 가중치를 바꿀 것입니다.

그런데 만약 우리가 지금 막 넣어준 "2"의 사진에, 어떤 특정 사람의 필체로 인해 아주 구석탱이가 검은 색으로 칠해져 있다고 생각해 봅시다.

그러면, 인공지능은 구석탱이에 칠해진 검은색 하나만을 보고도 이 사진이 2임을 추측할 수 있습니다.

이런 경우엔, 해당 구석탱이의 검은색에는 "2"라는 라벨이 붙는 가중치가 엄청 커지게 되고, 그 이후부터는 구석의 검은색만 보고도 그냥 "2"라고 해석하게 되는 것입니다.

이렇게 되어버리면, 당연히 문제가 발생하게 되겠죠.

 

이를 방지하기 위해, 특정 가중치 값이 너무 커지는 것을 방지하게 됩니다.

특히 L2의 경우, 전체적으로 가중치가 큰 것보다는 어떤 특정 가중치가 무식하게 커지는 것을 막는 것을 확인할 수 있습니다. (W^2에 비례하는 걸 보면 알 수 있습니다.)

 

 

L2 Regularization의 식은 간단합니다 - 간단하게는, $\lambda W^2$가 됩니다.

그리고, 이 값을 Loss값에 더해주면 됩니다.

 

 

설명은 이렇게 간단하게(?)만 하고, 코드를 봅시다.

 

 

class Model:
    def __init__(self):
        self.params = {}
        self.grads = {}
        self.keys = []
        self.layers = {}
        self.num = 0
        self.l2 = {}
        self.weight_decay_lambda = {}
        self.loss = None
        self.pred = None

    def addlayer(self, layer, activation=False, input_size=None, name=None, initialization=None, reg=0):
        if name is None:
            name = str(self.num)

        self.keys.append(name)
        self.num += 1
        self.layers[name] = layer
        self.weight_decay_lambda[name] = reg

        if not activation:
            if isinstance(layer, AddLayer):
                self.params[name] = np.zeros(input_size)
            elif initialization is 'xavier':
                self.params[name] = xavier_initialization(input_size)
            elif initialization is 'he':
                self.params[name] = he_initialization(input_size)
            else:
                self.params[name] = np.random.uniform(-1, 1, input_size)

            self.layers[name].param = self.params[name]

    def predict(self, x, y):
        for i in range(len(self.keys) - 1):
            key = self.keys[i]
            x = self.layers[key].forward(x)
            if key in self.l2:
                self.l2[key] = np.sum(np.square(self.params[key])) * self.weight_decay_lambda[key]
        self.loss = self.layers[self.keys[-1]].forward(x, y)
        self.loss += sum(self.l2.values())/2
        self.pred = softmax(x)

이번엔 아예 Model Class 자체를 조금 바꿔야 합니다.

 

우선, L2 regularization 값들이 들어갈 self.l2를 선언해 주고 나서,

addlayer()에는 reg값을 추가했습니다. 이 reg값이 바로 위에서 언급했던 $\lambda$값입니다.

이 $\lambda$값들을 저장하는 변수 self.weight_decay_lambda도 추가해 주었습니다.

그 뒤, 마지막 predict에서 loss값에 l2의 값들의 합을 더해줍니다.

 

 

                if key in model.params:
                    model.grads[key] = model.layers[key].grad + model.weight_decay_lambda[key] * model.params[key]
                    model.params[key] -= learning_rate * model.grads[key]

optimizer.py의 SGD함수도 다음과 같이 바꿔줍시다.

비교해 보시면 아시겠지만, model.grads[key]의 update rule에 model.weight_decay_lambda와 관련된 식이 추가된 것을 볼 수 있습니다.

 

 

다음으로는 Dropout을 구현해 보겠습니다.

Dropout 또한 Overfitting을 막기 위해 고안된 녀석으로, 훈련 과정에서 뉴런들을 강제로 비활성화 시키며 학습을 진행합니다.

학습 시에는 조금 느리게 학습되나, overfitting을 매우 잘 막아줍니다.

 

바로 구현부로 넘어가 봅시다.

class Dropout:
    def __init__(self, dropout_rate=0.2):
        self.drop_rate = dropout_rate
        self.mask = None

    def forward(self, x, train_flag=True):
        if train_flag:
            self.mask = np.random.rand(*x.shape) > self.drop_rate
            return x * self.mask
        else:
            return x * (1.0 - self.drop_rate)

    def backward(self, dout):
        return dout * self.mask

Dropout은 새로운 Layer로, Model 사이사이에 끼워 넣어 주며 뉴런을 버리게 만들 것입니다.

drop_rate는 버리는 정도를 의미하고, mask는 실제 버리는 index값들을 넣어줍니다.

 

그리고, 이 Dropout은 훈련 중에는 계속 뉴런을 조금씩 버리면서 학습하지만, 훈련 중이 아닐 때는 (즉, test때는) 뉴런을 버리는 대신 drop_rate만큼 줄여서 모든 뉴런을 내보냅니다.

또, 역전파 시에는 forward때 보내줬던 만큼의 뉴런만을 받아서 뒤로 보냅니다.

 

 

    def predict(self, x, y, train_flag=True):
        for i in range(len(self.keys) - 1):
            key = self.keys[i]
            if isinstance(self.layers[key], Dropout):
                x = self.layers[key].forward(x, train_flag)
            else:
                x = self.layers[key].forward(x)
            if key in self.l2:
                self.l2[key] = np.sum(np.square(self.params[key])) * self.weight_decay_lambda[key]

Model.py의 predict 함수도 위와 같이 수정해 줍시다.

이렇게 수정하게 되면, predict 시에 train_flag를 설정하여 dropout을 발동시킬지, 발동시키지 않을지 결정할 수 있습니다.

        model.predict(x_train, y_train, train_flag=False)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

Optimizer.py의 함수들에는 위와 같이, 최종 test 시에는 train_flag를 False로 바꾸어서 나오게 해야 합니다.

 

 

 

import numpy as np
import pandas as pd
from Model import Model
import Layers
import Optimizer

# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values

# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot[:300]
y_test = one_hot[300:600]
x_test = x_train[300:600]
x_train = x_train[:300]

# initialize parameters
num_classes = y_train.shape[1]
hidden = 50

model = Model()
model.addlayer(Layers.MulLayer(), input_size=(784, 100), name="w1", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b1')
model.addlayer(Layers.ReLU(), activation=True, name='relu1')
model.addlayer(Layers.Dropout(), activation=True, name='dropout1')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w2", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b2')
model.addlayer(Layers.ReLU(), activation=True, name='relu2')
model.addlayer(Layers.Dropout(), activation=True, name='dropout2')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w3", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b3')
model.addlayer(Layers.ReLU(), activation=True, name='relu3')
model.addlayer(Layers.Dropout(), activation=True, name='dropout3')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w4", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b4')
model.addlayer(Layers.ReLU(), activation=True, name='relu4')
model.addlayer(Layers.Dropout(), activation=True, name='dropout4')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w5", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b5')
model.addlayer(Layers.ReLU(), activation=True, name='relu5')
model.addlayer(Layers.Dropout(), activation=True, name='dropout5')
model.addlayer(Layers.MulLayer(), input_size=(100, 10), name="w6", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=10, name='b6')
model.addlayer(Layers.Dropout(), activation=True, name='dropout6')
model.addlayer(Layers.SoftmaxLayer(), activation=True, name='softmax')

optimizer = Optimizer.SGD(batch_size=128)
model.train(x_train, y_train, optimizer, 3000, 0.01)

model.predict(x_test, y_test, train_flag=False)
print("Final test_ACC : ", (model.pred.argmax(1) == y_test.argmax(1)).mean())
print("Final test_LOSS : ", model.loss)

최종적으로, 다음의 코드를 돌려보시기 바랍니다.

overfitting을 유도하기 위해 데이터를 300개 가량으로 줄였고, model의 크기는 늘렸습니다.

reg값을 바꿔도 보고, Dropout rate를 바꿔도 보고 하며 어떤 값들이 가장 overfitting을 줄이는지 확인할 수 있을 겁니다.

 

 

 

여기까지 기본적인 딥러닝 코드가 끝났습니다!

다음 시간부터는 CNN을 직접 구현해 보는 시간을 가지도록 하겠습니다!

+ Recent posts