Chap.05 오차역전파법

2022. 1. 7. 15:14

5-1. Intro 

이전장 4장에서 가중치 매개변수에 대한 손실 함수의 기울기를 수치미분을 통해 구하였다. 수치 미분은 단순하고 구현도 간단하서 유용하지만 시간이 오래 걸린다는 단점을 가지고 있다. 이번장에서는 개선방안인 오차역전파법을 배워보자. 


5-1. 계산 그래프

오차역전파법을 이해하는 방법으로는 크게 수식을 통한 이해, 계산 그래프를 통한 이해가 있다. 

여기서 우리는 두 번째 방법인 계산 그래프를 통해 오차역전파 법을 이해해보자.

💡 계산 그래프란?
계산 과정을 그래프로 나타낸 것이다. 여기서 그래프는 복수의 노드에지로 표현된다.

자 그럼 계산 그래프를 활용해 문제를 풀어보자.

문제 -> 현빈 군은 슈퍼에서 사과를 2개, 귤 3개 샀습니다. 사과는 1개의 100원, 귤은 1개 150원입니다. 소비세가 10%일 때 지불금액을 구하시오.

다음 그림은 계산 그래프로 문제를 푼것이다. 

계산 그래프를 그릴 때 '계산을 왼쪽에서 오른쪽으로 진행'하는 단계를 순전파, 반대로 '왼쪽에서 오른쪽으로 진행'하는 단계를 역전파라고 한다. 

국소적 계산

계산 그래프에서는 국소적 계산을 사용한다. 국소적 계산이란 자신과 직접 관계된 작은 범위만 계산하는 것이다. 예를 들어 설명하면 위 그림에서  사과의 개수 2개, 사과의 가격 100원을 곱해 다음 노드로 전달하였다. 

계산 그래프를 사용하는 이유 

계산 그래프를 사용하는 가장 큰 이유는 역전파를 통해 '미분'을 효율적으로 계산할 수 있다는 점이다.

다음은 앞에 그래프에서 역전파를 추가한 것이다. 

역전파는 앞에 노드에서 전달 받은 값에 변화률을 전달하는데 이를 통해 여기서는 사과 값이 아주 조금 오르면 최종금액은 아주 작은 값의 2.2배가 오른다 라고 해석할 수있다.


5-2. 연쇄법칙

계산 그래프에서 역전파는 연쇄법칙을 따른다.

위 그래프를 보면 역전파가 전달됨에 따라 값이 축적되어 전달되는 것을 볼 수있다. 이는 역전파의 계산그래프가 연쇄법칙을 따른다는 것을 증명한다.


5-3. 역전파

덧셈 노드의 역전파

z = x + y라는 식을 역전파로 살펴보자.

다음 식은 위의 식을 미분한 것이다. 이것을 토대로 계산 그래프를 그려보자.

덧셉 노드의 역전파는 위에 그래프와 같이 입력신호를 다음 노드로 출력할 뿐이므로 전달받은 값을 그대로 다은 노드에게 전달한다.

곱셈 노드의 역전파

z = xy라는 식을 역전파로 살펴보자.

다음 식은 위의 식을 미분한 것이다. 이것을 토대로 계산 그래프를 그려보자.

 

곱셈 노드의 역전파는 위에 그래프와 같이 입력신호들을 서로 바꾼값을 곱해서 다음 노드로 전달한다.


5-4. 단순한 계층 구현 

이제 앞에서 배운 순전파와 역전파를 이용해 계층을 구현해보자.

먼저 곱셈 계층과 덧셈계층은 구현하자.

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


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

덧셉 계층과 곱셈 계층의 순전파는 값을 더하고 곱해서 내보낸다 하지만 곱셈 계층의 순전파는 역전파에서 사용할 변수를 저장한다. 두 계층의 역전파는 동일하다.

그럼 구현한 곱셉 계층과 덧셈 계층을 가지고 문제를 풀어보자.

이 계산 그래프를 코드로 구현해보자.

apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1

# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
price = mul_tax_layer.forward(all_price, tax)  # (4)

# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)

단순히 계층들을 만들고 순전파와 역전파 순으로 구현하였다.


5-5. 활성화 함수 구현

ReLU 계층

다음은 활성화 함수인 ReLU 함수의 수식과 미분 수식이다.

이것들을 활용해 ReLU 계층을 구현해 보자.

class Relu:
    def __init__(self):
        self.mask = None

    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

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

5-6. Affine/Softmax 계층 구현

Affine 계층

💡 Affine 계층이란?
신경망의 순전파 때 수행하는 행렬의 곱은 기하학에서 어파인 변환이라고 하는데 이 어파인 변환을 수행하는 층을 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):
        # 텐서 대응
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.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)
        
        dx = dx.reshape(*self.original_x_shape)  # 입력 데이터 모양 변경(텐서 대응)
        return dx

Softmax-with-Loss 계층

이전 장에서 설명 했듯이 소프트맥스 함수는 입력 값을 정규화하여 출력한다. mnist 데이터 분류에서 softmax 계층 활용의 예를 살펴보자.

이제 softmax함수를 구현 할 텐데 우리는 손실함수인 교차 엔트로피 오차도 포함하여 구현할 것이다. 그럼 구현하기 전에 소프트맥스 & 교차 엔트로피 오차의 계산그래프를 살펴보자.

너무 복잡하고 이해하기 쉽지않다. 그럼 단순하게 다시 그려보자.

 

이제 쉽게 알아볼 수 있게 되었다. 이를 토대로 코드를 구현해보자.

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]
        if self.t.size == self.y.size: # 정답 레이블이 원-핫 인코딩 형태일 때
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size
        
        return dx

5-7. 오차역전파법 구현

2층 신경망 구현 

지금까지 배운 순전파, 역전파등을 이용해 저번 장에서 구현하였던 2층 신경망을 업그레이드 시켜보자.

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):
        # forward
        self.loss(x, t)

        # backward
        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

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

그럼 구현한 코드에서 오차역전파법으로 구현한 기울기가 맞는지 검증해보자.

# 데이터 읽기
(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))

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

단순히 네트워크만 구현했다고 끝낸다면 우리가 배운 오차역전파법은 의미가 없다. 이제 오차역전파법을 이용한 기울기를 가지고 매개변수를 갱신 시키면서 학습시켜보자.

# 데이터 읽기
(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.numerical_gradient(x_batch, t_batch) # 수치 미분 방식
    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)

Chapter. 5 정리 

* 계산 그래프를 이용하면 계산 과정을 시각적으로 파악할 수 있다.
* 계산 그래프의 노드는 국소적 계산으로 구성된다. 국소적 계산을 조합해 전체 계산을 구성한다.
* 계산 그래프의 순전파는 통상의 계산을 수행한다. 한편, 계산 그래프의 역전파로는 각 노드의 미분을 구할 수 있다.
* 신경망의 구성 요소를 계층으로 구현하여 기울기를 효율적으로 계산할 수 있다(오차역전파법).
* 수치 미분과 오차역전파법의 결과를 비교하면 오차역전파법의 구현에 잘못이 없는지 확인할 수 있다(기울기 확인).

출처: 사이토 고키『밑바닥부터 시작하는 딥러닝』, 한빛미디어(2017)

'Deep Learning > deep learning from scratch' 카테고리의 다른 글

Chap.07 합성곱 신경망(CNN)  (0) 2022.01.10
Chap.06 학습 관련 기술들  (0) 2022.01.09
Chap.04 신경망 학습  (0) 2022.01.07
Chap.03 신경망  (0) 2022.01.05
Chap.02 퍼셉트론  (0) 2022.01.05

BELATED ARTICLES

more