삶은 계란

딥러닝 기초 부수기 - 오차역전파법(backpropagation) 본문

딥러닝

딥러닝 기초 부수기 - 오차역전파법(backpropagation)

삶과계란사이 2022. 3. 5. 01:03

딥러닝 기초 부수기 - 오차역전파법(backpropagation)

본 게시글은 한빛미디어 출판사의 '밑바닥부터 시작하는 딥러닝(저자: 사이토 고키)' 도서 내용을 바탕으로 작성하였습니다.

 

1. 서론

수치 미분은 계산 시간이 오래 걸린다는 단점이 있다. 이러한 단점을 개선한 것이 오차역전파법(backpropagation)이다. 이번 게시글은 이 오차역전파법에 대해 정리한 내용을 담고 있다.

 

2. 계산 그래프

계산 그래프(computational graph)는 계산 과정을 나타낸 그래프이다. 노드(node)와 에지(edge, 노드 사이 직선)로 표현된다. 예시를 들어 계산 그래프를 표현해보겠다. 1개에 1000원인 물건을 3개 구매했다고 하자. 소비세는 10% 부과된다. 이때의 지불 금액을 구하는 과정을 계산 그래프로 나타내면

위와 같이 표현할 수 있다. 다른 예시를 들어보도록 하자. 1개에 1000원인 물건 A를 3개, 1개에 2000원인 물건 B를 2개 구매했다고 하자. 소비세는 10% 부과된다. 이때의 지불 금액을 구하는 과정을 계산 그래프로 나타내면

위와 같이 표현할 수 있다. 이러한 계산 그래프는 국소적 계산을 전파하면서 최종 결과를 얻는다. 해당 노드와 직접 관련된 계산만 수행할 뿐 전체와는 상관이 없다는 의미이다. 또한 계산을 왼쪽에서 오른쪽으로 진행한다는 것을 알 수 있다. 이러한 방향으로 계산을 전파하는 것을 순전파(forward propagation)라 한다. 계산을 오른쪽에서 왼쪽으로 진행하는 전파는 역전파(backward propagation)라 한다. 역전파를 통해 미분을 효율적으로 계산할 수 있기 때문에 계산 그래프를 사용한다.

 

3. 연쇄 법칙

역전파는 국소적 미분을 순전파의 반대 방향으로 전달한다. 전달하는 원리는 연쇄 법칙(chain rule)에 따른 것이며, 연쇄 법칙은 합성함수 미분에 대한 성질이다.

역전파는 계산 그래프에서 순전파 반대 방향 화살표로 표현

아래와 같은 합성함수가 있다고 하자. 연쇄 법칙에 따라 미분을 할 수 있다.

최종 미분식은 두 미분(2t와 1)을 곱해 계산한다. 아래 그림은 위 연쇄 법칙 계산을 계산 그래프로 나타낸 것이다.

 

4. 역전파

(1) 덧셈 노드 역전파

덧셈 노드의 역전파는 미분 값을 그대로 흘려보낸다. z = x+y 식에서 x에 대한 미분이든 y에 대한 미분이든 둘 다 계산하면 값은 1이 나온다. 결과적으로 덧셈 노드의 역전파는 상류에서 전해진 미분 값에 1을 곱하기만 할 뿐이다.

 

(2) 곱셈 노드 역전파

곱셈 노드의 역전파는 상류에서 전해진 미분 값에 순전파 때의 입력 값들을 서로 바꾼 값을 곱해서 흘려보낸다. z = xy 식에서 x에 대한 미분 값은 y이고 y에 대한 미분 값은 x이다. 결과적으로 미분 값에 서로 바꾼 값을 곱하는 형태가 된다.

 

5. 단순한 계층 구현

위에서 예시로 든 물건 구입의 계산 그래프를 파이썬으로 구현한 코드이다.

(1) 곱셈 계층

# 곱셈 계층

class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    # 순전파
    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    # 역전파
    def backward(self, dout):
        dx = dout * self.y   # x와 y를 교체
        dy = dout * self.x

        return dx, dy

 

(2) 덧셈 계층

# 덧셈 계층

class AddLayer:
    def __init__(self):
        pass

    def forward(self, x, y):
        out = x + y

        return out

    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy

 

(3) 계산 그래프 구현

# 물건 구매 계산 그래프 구현

thing = 1000
thing_num = 3
tax = 1.1

mul_thing_layer = MulLayer()
mul_tax_layer = MulLayer()

# 순전파
thing_price = mul_thing_layer.forward(thing, thing_num)
price = mul_tax_layer.forward(thing_price, tax)

# 역전파
dprice = 1
dthing_price, dtax = mul_tax_layer.backward(dprice)
dthing, dthing_num = mul_thing_layer.backward(dthing_price)

print("price:", int(price))
print("dThing:", dthing)
print("dThing_num:", int(dthing_num))
print("dTax:", dtax)

 

6. 활성화 함수 계층 구현

계산 그래프를 신경망에 적용할 수 있다. 우선 활성화 함수 ReLU, Sigmoid 계층을 구현한 코드이다.

(1) ReLU 계층

# ReLU 계층

class Relu:
    def __init__(self):
        self.mask = None   # mask 배열의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정

    def forward(self, x):
        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

 

(2) Sigmoid 계층

Sigmoid 계층 계산 그래프

# Sigmoid 계층

class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

 

7. Affine, Softmax 계층 구현

(1) Affine 계층

신경망의 순전파 때 수행하는 행렬의 곱(입력 값과 가중치와의 곱)을 기하학에서 어파인 변환(affine transformation)이라 한다. 어파인 변환을 수행하는 계층이 Affine 계층이다.

Affine 계층 계산 그래프

 

(2) 배치용 Affine 계층

배치용 Affine 계층 계산 그래프(N은 데이터 개수)

# 배치용 Affine 계층

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None
        self.dW = None
        self.db = None
        
    def forward(self, x):
        self.x = x
        out = np.dot(x, self.W) + self.b
        
        return out
        
    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        return dx

 

(3) Softmax-with-Loss 계층

신경망 학습 시 출력층에서 사용하는 활성화 함수인 소프트맥스 함수에 손실 함수인 교차 엔트로피 오차를 포함하여 Softmax-with-Loss 계층으로 구현할 수 있다.

간소화한 Softmax-with-Loss 계층 계산 그래프

신경망의 역전파에서는 출력 값과 정답 레이블의 오차가 앞 계층에 전해진다.

# Softmax-with-Loss 계층

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None   # 손실
        self.y = None      # softmax의 출력
        self.t = None      # 정답 레이블(원-핫 벡터)
        
    def forward(self, x, t):
        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

 

8. 오차역전파법 구현

지금까지의 계층을 조합하여 신경망을 구현할 수 있다. 오차역전파법은 기울기를 산출할 때 수치 미분 대신 사용한다.

(1) 오차역전파법 적용 신경망

# 오차역전파법 적용 신경망

class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
        # 가중치 초기화
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) 
        self.params['b2'] = np.zeros(output_size)

        # 계층 생성
        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
        
    # x : 입력 데이터, t : 정답 레이블
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
    
    # 정확도 계산
    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)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # x : 입력 데이터, t : 정답 레이블
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        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
    
    # 오차역전파법을 사용하여 기울기 산출
    def gradient(self, x, t):
        # 순전파
        self.loss(x, t)

        # 역전파
        dout = 1
        dout = self.lastLayer.backward(dout)
        
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 결과 저장
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

 

(2) 오차역전파법으로 구한 기울기 검증

수치 미분으로 구한 기울기와 오차역전파법으로 구한 기울기가 일치하는지 확인하면서 오차역전파법을 검증할 수 있다.

# MNIST 데이터셋을 사용하여 오차역전파법 검증


# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

# 신경망 구현
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

x_batch = x_train[:3]
t_batch = t_train[:3]

grad_numerical = network.numerical_gradient(x_batch, t_batch)   # 수치 미분
grad_backprop = network.gradient(x_batch, t_batch)              # 오차역전파법

# 각 가중치 차의 절댓값을 구하고, 그 절댓값들의 평균 내기
for key in grad_numerical.keys():
    diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
    print(key + ":" + str(diff))

수치 미분과 오차역전파법으로 구한 기울기의 오차가 매우 작음을 알 수 있다.

 

(3) 오차역전파법을 사용한 학습 구현

지금까지와 다른 점은 기울기를 오차역전파법으로 구한다는 것뿐이다.

# MNIST 데이터셋을 사용한 오차역전파법 신경망 학습


# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 기울기 계산
    grad = network.gradient(x_batch, t_batch) # 오차역전파법 방식(훨씬 빠르다)
    
    # 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)

훈련 데이터와 시험 데이터의 정확도가 그리 차이 나지 않는 것을 알 수 있다.

 

 

Comments