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

 

 

 

이번에는 다음에 구현할 CNN의 구현을 위해, 코드를 예쁘게 바꿔 보는 시간을 가져보겠습니다.

당연히, 다른 이론 설명은 없습니다.

 

 

 

 

오늘 코드 정리의 첫 번째 목표는, 바로 모델 코드를 다음과 같이 바꾸는 겁니다.

model = Model()
model.addlayer(Layers.MulLayer(100, initializer='he', reg=0.01), name="w1")
model.addlayer(Layers.AddLayer(100), name='b1')
model.addlayer(Layers.ReLU(), name='relu1')
model.addlayer(Layers.Dropout(), name='dropout1')
# ...

어떻게 바뀌었나 잠깐 보면, 원래는 addlayer의 인자로 input_size를 주었다면, 이제는 Layers의 인자로 output_size만을 주게 됩니다.

또한, 그것 말고도 다른 initializer이나 regularizer 등도 Layer 안에 들어가게 되겠습니다.

 

 

 

이제 Layer에서, 어떻게 설계를 변경했는지 보겠습니다.

class MulLayer:
    def __init__(self, out, initializer='he', reg=0):
        self.x = None
        self.out = out
        self.param = None
        self.grad = None
        self.init = initializer
        self.reg = reg
        self.activation = False

    def forward(self, x):
        self.x = x
        return x.dot(self.param)

    def backward(self, dout):
        self.grad = np.dot(self.x.T, dout)
        return np.dot(dout, self.param.T)

(MulLayer과 Addlayer은 거의 동일하니 같이 설명하겠습니다.)

 

우선, MulLayer의 init에서 initializer, regularizer과 함께 activation function인지 아닌지 체크하는 변수가 들어가겠습니다.

이를 통해, layer의 선언만으로 initializer, regularizer를 확실히 설정할 수 있고, activation인지 아닌지를 굳이 함수 입력 시에 넣지 않아도 되게 만들었습니다.

그 외의 forward, backward는 동일합니다.

 

 

다음으로는, parameter의 초기화를 하게 됩니다.

이 parameter 초기화는 원래 addlayer에서 이뤄졌었으나, 이제는 input data가 입력되는 train에서 하게 되겠습니다.

 

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

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

그 결과, addlayer은 다음과 같이 다이어트를 감행하게 되었습니다.

parameter의 선언부는 다 지우고, layers와 keys에 집어넣는 것들만 남게 되었습니다.

 

 

    def train(self, x_train, y_train, optimizer, epoch, learning_rate):
        in_size = x_train.shape[1:]
        for name in self.layers:
            if not self.layers[name].activation:
                out_size = self.layers[name].out
                if type(in_size) is int:
                    size = (in_size, out_size)
                else:
                    size = (*in_size, out_size)

                self.weight_decay_lambda[name] = self.layers[name].reg
                if isinstance(self.layers[name], AddLayer):
                    self.params[name] = np.zeros(self.layers[name].out)
                elif self.layers[name].init is 'xavier':
                    self.params[name] = xavier_initialization(size)
                elif self.layers[name].init is 'he':
                    self.params[name] = he_initialization(size)
                else:
                    self.params[name] = np.random.uniform(size)
                in_size = out_size

                self.layers[name].param = self.params[name]
        optimizer.train(x_train, y_train, epoch, learning_rate, self)

 

마지막으로, train 함수입니다.

이 부분에서 처음으로 input data가 들어오므로, 여기에서 parameter initialize를 하겠습니다.

코드를 대충 설명해 보자면...

우선, input size는 처음에 x_train.shape[1:] (이미지 개수를 제외한 픽셀 수)와 동일합니다.

그리고, out_size는 layer에서 이미 선언해 주었으므로, 그대로 가져옵니다.

 

 

여기서, in_size가 튜플로 들어올 수도 있고, int로 들어올 수도 있습니다.

단순한 MulLayer, AddLayer을 지나왔다면 int, 픽셀 개수 (ex: (28, 28))로 그대로 들어온다면 튜플로 들어오겠습니다.

그래서, 이를 체크해 주며 parameter의 size를 결정해 주었습니다.

 

그 외의 나머지 부분은 다 비슷합니다.

 

 

다음 목표는, model save / load 기능 구현과 Model의 일부 변경입니다.

코드가 워낙 많이 바뀌어서, 그냥 바뀐 부분은 간결하게만 짚고 넘어가고, load/save 함수 중심으로 설명하겠습니다.

 

    def save(self, path=''):
        f = open(path + "model.txt", 'w')
        f.write(path + "weight.npz\n")

        params = {}
        for name in self.layers:
            data = self.layers[name].__class__.__name__ + "\n" + name + "\n"
            if not self.layers[name].activation:
                params[name] = self.layers[name].param
            f.write(data)
        np.savez(path + "weight", **params)

    def load(self, path):
        f = open(path)
        weight_path = f.readline()[:-1]
        load = np.load(weight_path)
        while True:
            layer = f.readline()[:-1]
            if not layer:
                break
            name = f.readline()[:-1]

            self.addlayer(eval(layer)(), name)
            if name in load:
                self.layers[name].param = load[name]

 

save함수는 위와 같이 구현했습니다. (Model.py)

path가 주어지면, 해당 path에 model.txt와 weight.npz를 생성합니다.

model.txt에는 model의 전체적인 구조를 저장하고, weight.npz는 가중치의 numpy 배열을 저장합니다.

그리고, npz에 한번에 저장하기 위해 params dictionary를 생성해 줍니다.

 

그 뒤, self.layers에서 각각의 layer의 class명과, name을 하나씩 저장해 줍니다.

그러는 와중에, params에는 각각의 가중치값을 저장합니다.

 

그리고, 마지막으로 np.savez함수로 지금까지 모았던 params를 한번에 저장합니다.

 

 

load 함수는 이와 정 반대로 일어납니다.

weight_path(weight.npz 파일 경로)에서 np.load를 통해 numpy array를 불러옵니다.

그리고, model.txt를 읽어오면서 각각의 layer를 생성하고, 그 안에 불러왔던 array를 param에 넣습니다.

이렇게 지금까지 학습했던 가중치를 가져올 수 있습니다.

 

이 함수를 만들면서 바꿔야 했던 함수들이 몇 개 있습니다.

간결하게 짚고만 넘어가겠습니다.

 

class AddLayer:
    def __init__(self, out=None, initializer='he', reg=0):
    # ...

class MulLayer:
    def __init__(self, out=None, initializer='he', reg=0):
    # ...

(Layers.py)

우선, AddLayer와 MulLayer에 out(즉, size)가 들어오지 않을 수 있습니다.

out=None으로 설정해 줘서, out size 없이도 load할 수 있도록 만들었습니다.

 

    def train(self, x_train, y_train, optimizer, epoch, learning_rate, skip_init=False):
        if not skip_init:
        # ...

(Model.py)

그리고, train에서의 init을 skip할 수 있도록 위와 같이 함수를 선언했습니다.

이제, train의 마지막 부분에 True를 넣으면 변수를 원래 가지고 있던 그대로 학습시킬 수 있습니다.

 

    def forward(self, x, 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.weight_decay_lambda:
                self.l2[key] = np.sum(np.square(self.params[key])) * self.weight_decay_lambda[key]
        return x

    def predict(self, x):
        x = self.forward(x)
        self.pred = softmax(x)
        return self.pred

    def eval(self, x, y, epoch=None):
        x = self.forward(x, False)
        self.loss = self.layers[self.keys[-1]].forward(x, y)
        self.loss += sum(self.l2.values()) / 2
        self.pred = softmax(x)

        if epoch is None:
            print("ACC : ", (self.pred.argmax(1) == y.argmax(1)).mean())
            print("LOSS : ", self.loss)
        else:
            print("ACC on epoch %d : " % epoch, (self.pred.argmax(1) == y.argmax(1)).mean())
            print("LOSS on epoch %d : " % epoch, self.loss)

(Model.py)

또, 원래 predict였던 함수를 forward / predict / eval 로 나누어 주었습니다.

predict는 input의 정답을 예측하는 함순데, 지금까지 y를 넣지 않으면 작동하지를 않았거든요..

위와 같이 바꿔줌으로써, 이미지만 넣어도 예측을 할 수 있도록 바뀌었습니다.

또한, eval 함수를 새로 만들어서 간단하게 loss와 acc를 알 수 있도록 하였습니다.

위 두 함수, predict와 eval에서 겹치는, 첫 x값을 마지막 layer까지 순전파법으로 가져가는 부분은 forward 함수로 따로 뺐습니다.

 

    def train(self, x_train, y_train, epoch, learning_rate, model):
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            z = model.forward(x)
            model.loss = model.layers[model.keys[-1]].forward(z, y)
            model.loss += sum(model.l2.values()) / 2
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                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]

            if epochs % (epoch / 10) == 0:
                model.eval(x, y, epoch=epochs)
        model.eval(x_train, y_train)

(Optimizer.py - SGD)

바뀐 부분은, z = model.forward와 그 밑 두줄, 그리고 가장 아래의 model.eval()입니다.

SGD뿐만 아니라 다른 optimizer들에도 동일하게 적용하면 되겠습니다.

 

 

 

이렇게 코드 정리와 Model Save/Load를 구현해 보았습니다.

다음 차시부터는 CNN을 구현해 보도록 하겠습니다.

+ Recent posts