본문 바로가기

Data Science/밑바닥부터시작하는딥러닝(1)

[밑시딥] 오직! Numpy로 오차역전파를 사용한 신경망 학습 구현하기

반응형

🔊 해당 포스팅은 밑바닥부터 시작하는 딥러닝 1권의 교재 내용을 기반으로 딥러닝 신경망을 Tensorflow, Pytorch와 같은 딥러닝 프레임워크를 사용하지 않고 순수한 Numpy로 구현하면서 딥러닝의 기초를 탄탄히 하고자 하는 목적 하에 게시되는 포스팅입니다. 내용은 주로 필자가 중요하다고 생각되는 내용 위주로 작성되었음을 알려드립니다.

 

밑바닥부터 시작하는 딥러닝


저번 포스팅에서 행렬 곱을 연산하는 계층과 활성화 함수가 적용된 계층의 역전파 방법까지 알아보면서 신경망 학습의 오차역전파 방법을 모두 이해해보았다. 이번 포스팅에서는 그동안 배운 내용들을 기반으로 오직 넘파이를 활용한 오차역전파 신경망 학습을 구현해보자.

 

먼저 복습 차원에서 활성화 함수와 손실 함수를 넘파이로 구현하는 소스코드를 보고 가자.

1. 활성화 함수(Sigmoid, Relu) 와 손실 함수(Cross-entropy Error)

먼저 활성화 함수들에 대한 넘파이 소스코드이다.

 

import numpy as np

# 1.sigmoid
def sigmoid(x: np.array):
    return 1 / (1 + np.exp(-x))

# 2.relu
def relu(x: np.array):
    return np.maximum(0, x)

# 3.softmax
def softmax(x: np.array):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T
    
    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

 

다음은 CEE(Cross-Entropy Error) 손실함수에 대한 넘파이 소스코드이다.

 

# 4.cross-entropy-error
def cross_entropy_error(y: np.array, t: np.array):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    # 레이블(t)가 원-핫 형태라면 레이블 형태로 변환
    if t.size == y.size:
        t = t.argmax(aixs=1)
        
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

 

그리고 추후에 알아볼 오차역전파의 기울기 검증을 위해서 오차역전파 방법과 다르게 파라미터의 변화량값들인 기울기를 구하는 또 다른 방법으로서 수치 미분을 계산하는 소스코드도 보고 가자.

 

# 5. 수치 미분 계산 함수
def numerical_gradient(f, x: np.array):
    h = 1e-4
    grads = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        # f(x+h)
        x[idx] = tmp_val + h
        fx1 = f(x)
        # f(x-h)
        x[idx] = tmp_val - h
        fx2 = f(x)
        
        grads[idx] = (fx1 - fx2) / (2*h)
        x[idx] = tmp_val
        it.iternext()
    
    return grads

2. 활성화 함수(Relu, Sigmoid) 계층

이제 활성화 함수 각 종류에 맞게 순전파, 역전파를 수행하는 계층 클래스를 만들자. 먼저 Relu 함수에 대한 소스코드이다.

 

# 1.Relu 계층
class Relu:
    def __init__(self):
        self.mask = None
        
    def forward(self, x: np.array):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0
        return out
    
    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout
        return dx

 

다음은 Sigmoid 계층 소스코드이다.

 

# 2.Sigmoid 계층
class Sigmoid:
    def __init__(self):
        self.y = None
        
    def forward(self, x: np.array):
        y = sigmoid(x)
        self.y = y
        return y
    
    def backward(self, dout):
        dx = dout * self.y * (1 - self.y)
        return dx

3. 행렬 곱(Affine) 계층

이번에는 행렬 곱 연산을 의미하는 Affine 계층을 넘파이로 구현하는 소스코드이다.

 

# 3.Affine 계층
class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        
        self.x = None
        self.original_x_shape = None
        self.dW = None
        self.db = None
        
    def forward(self, x: np.array):
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x
        
        y = np.matmul(self.x, self.W) + self.b
        return y
    
    def backward(self, dout):
        dx = np.matmul(dout, self.W.T)
        self.dW = np.matmul(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        dx = dx.reshape(*self.original_x_shape)
        
        return dx

4. Softmax-with-Loss 계층

이번엔 Softmax와 Loss(여기서는 Cross-Entropy Error)를 하나의 계층으로 하는 계층을 넘파이로 구현하는 소스코드이다.

 

# 4.Softmax-with-Loss 계층
class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None  # for loss 계층
        self.t = None
        self.y = None
        
    def forward(self, x: np.array, t: np.array):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        
        return self.loss
    
    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        
        return dx

이제 오차역전파를 수행할 때 필요한 활성함수, 손실함수, 그리고 각 활성함수와 손실함수에 맞는 계층 클래스들도 알아보았다. 이를 기반으로 2층 신경망 클래스를 만들어보자. 참고로 OrderedDict라는 순서가 있는 딕셔너리 객체를 호출했는데, 이는 딕셔너리에 추가한 순서를 기억하는 특징 때문이다. 이를 활용해서 순전파 때 호출한 레이어 순서를 역전파 시 뒤바꾸어서 호출할 수 있기 때문이다.

 

import numpy as np
from collections import OrderedDict

class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 2층 신경망의 파라미터 딕셔너리
        self.params = {}
        self.params['W1'] = np.random.randn(input_size, hidden_size) * weight_init_std
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = np.random.randn(hidden_size, output_size) * weight_init_std
        self.params['b2'] = np.zeros(output_size)
        
        # 2층 신경망의 계층 생성
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
        self.lastlayer = SoftmaxWithLoss()
        
    def predict(self, x):
        for layer in self.layers.values:
            x = layer.forward(x)
        return x
    
    def loss(self, x, t):
        y = self.predict(x)
        loss = self.lastlayer.forward(y, t)
        return loss
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1: t = np.argmax(t, axis=1)
        
        acc = np.sum(y == t) / float(y.shape[0])
        return acc
    
    def gradient(self, x, t):
        # 순전파 수행
        self.loss(x, t)
        
        # 역전파 수행 - 1.Softmax-with-Loss 계층
        dout = 1
        dout = self.lastlayer.backward(dout)
        
        # 역전파 수행 - 2.나머지 계층
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)
        
        # 역전파 수행한 결과의 파라미터 변화량 보관
        grads = {}
        grads['W1'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db
        
        return grads
    
    # 수치미분으로 기울기 계산(for 오차역전파 기울기 검증 목적)
    def numerical_gradient(self, x, t):
        loss_w = lambda w: self.loss(x, t)
        
        grads = {}
        # 여기의 numerical_gradient 함수는 바깥에서 정의한 수치미분 계산 함수임!
        grads['W1'] = numerical_gradient(loss_w, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_w, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_w, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_w, self.params['b2'])
        
        return grads

 

위 2층 신경망 클래스를 가지고 MNIST 데이터를 학습시켜보자.

 

import numpy as np
from dataset.mnist import load_mnist

(X_train, y_train), (X_test, y_test) = load_mnist(normalize=True, one_hot_label=True)

# 2층 신경망 설계
network = TwoLayerNet(input_size=28*28, hidden_size=50, output_size=10)

steps = 1000
train_size = X_train.shape[0]
batch_size= 100
learning_rate = 0.1

train_loss = []
train_acc = []
test_acc = []

# Mini-batch로 학습
for i in range(steps):
    batch_mask = np.random.choice(train_size, batch_size)
    X_batch = X_train[batch_mask]
    y_batch = y_train[batch_mask]
    
    # 오차역전파로 학습 수행
    grads = network.gradient(X_batch, y_batch)
    
    # SGD로 경사하강법 수행
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grads[key]
    
    # SGD로 파라미터 갱신 후 다시 Loss값 얻기
    loss = network.loss(X_batch, y_batch)
    train_loss.append(loss)
    
    # 성능 중간 체크
    if i % 10 == 0:
        tr_acc = network.accuracy(X_batch, y_batch)
        te_acc = network.accuracy(X_test, y_test)
        train_acc.append(tr_acc)
        test_acc.append(te_acc)
        print(f'{i+1}번째 학습 후 Train Acc:', round(tr_acc, 3))
        print(f'{i+1}번째 학습 후 Test Acc:', round(te_acc, 3))
        print()

다음은 오차역전파를 통해서 구한 기울기 값이 정말 잘 구해졌는지 검증하기 위한 방법으로 수치 미분을 활용할 수 있다. 수치 미분은 상대적으로 오차역전파보다 계산이 오래걸린다고 했다. 하지만 직접 수학적인 계산을 했기 때문에 해석적 방법을 사용하는 오차역전파 결과를 검증하는 데 자주 사용된다. 이를 기울기 확인(Gradient Check) 과정이라고도 한다. 각자 2가지 방법을 활용해 기울기를 구한 값 차이를 확인해보는 소스코드이다. 두 값 차이가 0에 가깝다면 오차역전파를 통한 기울기 계산이 잘 되었다고 할 수 있다.

 

from dataset.mnist import load_mnist

# load data
(X_train, y_train), (X_test, y_test) = load_mnist(normalize=True, one_hot_label=True)

# model
network = TwoLayerNet(input_size=28*28, hidden_size=50, output_size=10)

# Batch
X_batch = X_train[:3]
y_batch = y_train[:3]

# 수치미분
grad_numerical = network.numerical_gradient(X_batch, y_batch)
grad_propagation = network.gradient(X_batch, y_batch)

for key in grad_numerical.keys():
    diff = np.mean(np.abs(grad_numerical[key] - grad_propagation[key]))
    print('key:', key, 'diff:', diff)
반응형