오차 역전파( Error Backpropagation) 란 말그대로 오차(error)를 역방향으로 보내는것이다.

 

앞에서는 입력층-> 은닉층 -> 출력 순으로 가중치를 업데이트 했었다. 이를 순전파라고 한다.

오차 역전파 방법은 순전파에서 생기는 결과값의 오차를 다시 역순으로 보낸 후, 가중치를 재계산하여 오차를 줄여나간다.

 

더 정확히 얘기하면, 기존의 방식은 수치 미분을 통해 기울기를 구하지만, 계산 시간이 오래걸린다는 단점이 있다.

하지만 오차 역전파 방식은 손실함수의 기울기를 더 효율적으로 계산하기 위해 쓰인다고 볼 수 있다.

                        (가중치 매개변수에 대한 손실함수의 기울기)

 

오차 역전파 방식을 그림으로 이해해보자.!

5.1.1 계산 그래프

문제 1) 재환이는 슈퍼에서 개당 100원인 사과를 두 개 샀다. 이 때 지불금액을 구해라. 소비세는 10%

문제 2)재환이는 슈퍼에서 개당 100원인 사과를 두 개, 개당 150원인 귤을 세  샀다. 이 때 지불금액을 구해라.

                                                                                                                                     소비세는 10%

문제 1과 2을 계산 그래프로 푼다면 다음 그림과 같다.

이렇게 계산 방향이 왼쪽 -> 오른쪽으로 전달되어 진행되는 과정을 순전파라고한다.

 

역전파는 물론 순전파의 반대 개념이다.!!

그 전에, 더 알아볼 개념이 있다.

5.1.2 국소적 계산

계산 그래프의 특징은 '국소적 계산'을 전파함으로  최종 결과를 얻는다는 점에 있다.

'국소적' 이란 '자신과 직접 관계 되있는 작은 범위'를 말한다.

국소적 계산은 결국 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것이다.

구체적인 예시를 통해 알아보자. 

가령 슈퍼에서 사과를 포함한 여러 식품을 구매한다고 했을 때는 아래 그림 같다.

 

그림에서, 여러 식품의 구매가격이  4,000원이 나왔다. 여기서 핵심은 각 노드의 계산이 국소적이라는 것이다.

즉, 무엇을 얼마나 샀는지 (계산이 얼마나 복잡한 지)와 상관없이 두 숫자만 더하면 된다는것( 4,000 + 200)

 

이처럼 계산 그래프는 국소적 계산에만 집중하면 된다.

 

5.1.3 왜 계산 그래프로 푸는가?

첫 번째 답은 국소적 계산의 이점 때문이다. 

즉, 아무리 계산 과정, 방법이 복잡해도 노드 계산에만 집중해서 문제를 해결할 수 있다는 점이다.

두 번째 답은 역전파를 통해 '미분'을 효율적으로 계산할 수 있기 때문이다.

 

이제 역전파를 이해하기 쉽게 위 문제를 다시 보자.

문제 1은 사과 두 개 사서 최종 가격을 구하는 것이였다.

여기서 만약 사과 가격이 오르면 최종 금액이 얼마나 변할 지 알고 싶다고 해보자.

이는 사과 가격에 대한 지불금액의 미분값을 구하는 것과 같다.!!

 

기호로 나타낸다면 ∂L/∂x    ( L =  최종금액, x = 사과 가격)

 

문제를 풀어본다면 다음 그림과 같다. 

  • 문제1을 예시로 한 설명
    • 역전파는 순전파와 반대 방향의 굵은 화살표 그림
    • 역전파는 국소적 미분 전달하고, 미분 값은 화살표의 아래에 표시 ( 위 그림에서는 빨간 글씨 )
    • 사과가 1원 오르면 최종 금액은 2.2원 오른다는 의미
    • 소비세에 대한 지불 금액의 미분이나 사과 개수에 대한 지불 금액의 미분도 같은 순서로 구할 수 있음
    • 중간까지 구한 미분 결과가 공유가 가능하기 때문에 다수의 미분을 효율적 계산이 가능함
    • 계산 그래프의 이점은 순전파와 역전파를 활용해 각 변수의 미분을 효율적으로 구할 수 있음

5.2 연쇄법칙

위의 역전파 방식은 국소적 미분을 전달하는 것이고, 전달하는 원리는 연쇄법칙을 따른 것이다.

5.2.1 계산 그래프의 역전파

위 그림과 같이 역전파의 계산 절차는 신호 E에 노드의 국소적 미분(∂y/∂x)를 곱한  후 다음 노드로 전달하는 것이다.

방금 말한 국소적 미분은 순전파 때의 y=f(x) 계산의 미분을 구한다는 뜻이며, 이는 x에 대한 y의 미분을 구한다는 것이다.

 

이러한 방식이 역전파의 계산 순서인데, 이 방식이 목표로 하는 미분값을 효율적으로 구할 수 있다는것이 역전파의 핵심임.

 

5.2.2  연쇄법칙이란?

연쇄 법칙은 합성 함수부터 시작해야 한다.

합성 함수란 여러 함수로 구성된 함수로, 연쇄법칙은 합성 함수의 미분에 대한 성질과 같다.

 

또한 성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.

이게 연쇄법칙의 원리이다. 

ex) z = (x + y)^2라는 식은 z=t^2 과 t=x+y 두 개의 식으로 구성된다.

 이 두 개의 식을 각각 t에 대해서 미분을 하면 2t, 1이다. 그리고 이 둘을 곱하면 아래 식(그림)과 같다.

위 그림을 보자.

계산 그래프의 역전파는 오른쪽에서 왼쪽으로 진행한다고 했었다.

역전파의 계산 절차는 노드로 들어온 입력 신호에 그 노드의 국소 미분(편미분)을 곱한 후 다음 노드로 전달한다.

위에서 **2 노드에서 역전파를 살펴보자. 입력은 ∂z/∂z이며 이에 국소적 미분인 ∂z/∂t를 곱한 후 다음 노드로 넘긴다.

 

주목할 것은 맨 왼쪽 역전파이다. 이 계산은 연쇄법칙에 따르면 'x에 대한 z의 미분'이 된다. 

즉, 역전파는 연쇄법칙의 원리와 같다고 볼 수 있다.

 

5.3.1 덧셈 노드의 역전파

 z = x+ y 라는 식을 대상으로 역전파를 살펴보면, ∂z/∂x와 ∂z/∂y 모두 1이다 (분자 z를 각각 분모인 x,y로 미분하니까)

 

결론만 말하자면 덧셈 노드의 역전파는 입력 값을 그대로 흘려보낸다.

5.3.2 곱셈 노드의 역전파

z = x * y 라는 식을 가정해보자. 이 식의 미분은 ∂z/∂x = y , ∂z/∂y = x이다.

곱셈노드의 역전파는 상류의 값에 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보낸다.

(서로 바꾼 값이란 순전파에서는 x였다면 y를, y였다면 x로 바꾼다는 의미)

아래 그림을 통해 이해

5.3.3 사과 쇼핑의 예

정리할 겸, 아래 그림을 통해 빈칸에 들어갈 값을 구해보자. 

5.4 계층 구현하기

5.4.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

__init__에는 인스턴스 변수인 x와 y를 초기화한다. 이 두 변수는 순전파 시의 입력값을 유지하기 위해 사용!!!

 

 

MulLayer를 통해 순전파를 다음과 같이 구현할 수 있다.

apple = 100
apple_num = 2
tax = 1.1
 
# 계층들
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
 
# 순전파
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
 
print(price)
 
>>> 220.00000000000003


"""각 변수에 대한 미분 - backward()에서 구할 수 있음"""
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
 
print(dapple, dapple_num, dtax)
 
>>> 2.2 110.00000000000001 200

5.4.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
# 덧셈 계층과 곱셈 계층 활용해 사과문제 풀이
# from layer_naive import *
 
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
 
# 계층 
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
 
# 순전파
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)
 
# 역전파
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)
 
print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)
 
>>>
price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650

 

5.5 활성화 함수 계층 구현하기

이제 계산그래프를 신경망 학습에 적용시켜 보자.

5.5.1 ReLu 계층

활성화 함수로 사용되는 ReLu의 수식은 0보다 클때는 그 값, 0보다 작거나 같은 경우는 0을 출력한다.

또한 미분은 다음과 같다.

역전파의 개념으로 본다면, 순전파 때 입력값이 0보다 크면 상류의 값을 그대로 흘려보내고,

0이하면 하류로 신호를 보내지 않는다. ( = 0 을 보낸다.)

이제 코드로 구현해보자.

# 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
  • Relu 클래스는 mask라는 인스턴스 변수 가짐
  • mask는 True / False로 구성된 넘파이 배열
  • 순전파 입력인 x의 원소값이 0 이하면 True, 아니면 False
  • 역전파 때는 순전파 때 만들어둔 mask를 써서 mask의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정
import numpy as np
 
x = np.array( [[1.0, -0.5], [-2.0, 3.0]])
print(x)
mask = (x <= 0)
print(mask)
 
>>>
[[ 1.  -0.5]
 [-2.   3. ]]
[[False  True]
 [ True False]]

5.5.2 Sigmoid 계층

 

시그모이드 함수에는 곱셈 , 덧셈 말고도 exp와 나눗셈이 등장한다.

단계별로 진행하자.

1단계

나눗셈 '/' 미분

y=1/x를 미분해보면  (∂y/∂x) -> -(x^-2)   ->   - (y^-2) 이다.

 

따라서 역전파 때는 상류에서 흘러온 값에 -y^2을 곱해서 하류로 전달한다. (순전파의 출력을 제곱한  마이너스 붙인값)

 

2단계

'+'노드는 그대로 전달하기

 

3단계

'exp'노드는 y=exp(x)연산을 수행하며 미분은 다음과 같다. ∂y/∂x=exp(x)

 

계산 그래프에서는 상류의 값에 순전파 때의 출력(여기서는 exp(-x)을 곱해 하류로 전파한다.!!

 

4단계

'x'노드는 순전파 때의 값을 서로 바꿔 곱한다! ( 여기서는 -1)

 

위 단계를 아래 그림과 같이 이해해보자.

파이썬 코드로 구현해보면 다음과 같다.

# 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

위 구현에서는 순전파의 출력을 인스턴스 변수인 out에 보관했다가, 역전파 계산 시에 다시 사용한다.

'밑바닥 딥러닝' 카테고리의 다른 글

Ch 6-1 학습 관련 기술들  (0) 2023.05.29
Ch 5-2 오차 역전파 (Error BackPropagation)  (0) 2023.05.27
Ch4-2 신경망 학습  (0) 2023.05.26
Ch4-1 신경망 학습  (0) 2023.05.17
Ch3-2 출력층 설계, 실습  (0) 2023.05.17

기울기

앞에서는 편미분을 통해 2개의 변수를 하나씩 계산했지만, 동시에 계산하고 싶다면?

가령 x0=3, x1=4일 때, (x0,x1) 양쪽의 편미분을 묶어 계산한다고 해보자.

 

이때 (∂f/∂x0 , ∂f/∂x1)처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기라고한다.

그리고 다음과 같이 구현할 수 있다.

def numerical_gradient(f,x):
	h = 1e-4    # 매우 작은 수
    grad = np.zeros_like(x) # x와 형상이 같은 배열을 생성
    
    for idx in range(x.size):
    	tmp_val = x[idx]     # f(x+h) 계산
        
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2) / 2*h
        x[idx] = tmp_val     # 값 복원
        
   	return grad

복잡해 보이지만, 동작 방식은 변수가 하나일때의 수치 미분과 거의 동일하다.

x가 넘파이 배열이므로  x의 각 원소에 대해서 수치 미분을 구해보자.

 

print(numerical_gradient(function_2, np.array([3.0, 4.0])))
print(numerical_gradient(function_2, np.array([0.0, 2.0])))
print(umerical_gradient(function_2, np.array([3.0, 0.0])))

[6.e-08 8.e-08]
[0.e+00 4.e-08]
[6.e-08 0.e+00]

(3,4), (0,2) , (3,0) 세 점에서의 기울기를 각각 구할 수 있다.

기울기는 각각 (6,8), (0,4), (6,0)이다.

 

그런데 이 기울기가 의미하는 것은 무엇일까? 

위 그림을 보면 기울기는 함수의 '가장 낮은 장소'(최소값)를 가르킨다.

 

 

경사 하강법

머신러닝 문제 대부분 학습 단계에서 최적의 매개변수 찾아낸다.

신경망 역시 최적의 매개변수 ( 가중치, 편향)를  찾아야한다.

 

여기서 최적이란 손실 함수 최소값이 될 때 의 매개변수 값이다.

하지만 실제로 손실함수는 매우 복잡하기 때문에 찾기 어려울 수 있다.

 

따라서 경사법을 이용해야한다.

경사 하강법은 현 위치에서 기울기를 따라 일정거리만큼 이동한 후, 기울기를 구하고 또 이동...함수의 값을 줄여나가는것

 

 

학습률(에타)이라는 개념이 여기서 등장하는데,  학습률이란 한 번의 학습으로 얼마만큼 학습해야 할지,

즉 매개변수 값을 얼마나 갱신해야 할지를 정하는 것이라고 한다.!!

 

위의 식이 한 번 갱신할 때의 수식이고, 이 단계를 계속 반복한다고 한다.

 

또한 학습률은 너무 크거나 작으면 최적값을 찾지 못한다.!!

 

너무 값이 크다면 최소값을 지나쳐버리게되어 무한루프에 빠질 수 있고,

값이 너무 작다면 전역 최소값에 빠지게 되어 학습이 종료될 수 있다.

 

따라서 적절한 학습률이 필요하다 ( 0.01 or 0.001 등)

 

손코딩을 해보자.!

def gradient_descent(f, init_x, lr = 0.01, step_num=100):
	x = init_x
    
    for i in range(step_num):
    	grad = numerical_gradient(f,x)
        x -= lr * grad
    return x

f는 최적화하려는 함수, init_x는 초기값, lr은 learning_rate의 줄임말인 학습률, step_num은 반복 횟수를 뜻한다.

 

함수의 기울기는 numerical_gradient(f,x)로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복

 

이 함수를 이용하여 함수의 극소값과 극대값을 구할 수 있다.!

한 번 확인해보자.!

def function_2(x):
	return x[0]**2 + x[1]**2
    
init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x = init_x, lr=0.1, step_num=100)

array([0.0000139, 0.00000212])

출력값이 거의 0에 가까운 최소값을 잘 찾은것을 확인할 수 있다.!

 

아래 그림은 경사 하강법의 갱신, 탐색과정을 표현한것이다.

신경망에서의 기울기

이제 학습을 들어가기 전, 손실함수의 기울기를 구해야 한다. 정확히는 가중치 매개변수 대한 손실함수의 기울기이다.

ex) shape이 2x3이고, 가중치가 W, 손실함수가 L인 신경을 생각해보자.

 

이 경우 경사가 ∂L/∂W로 나타낼 수 있다. 수식으로는 다음과 같다.

∂L/∂W 각 원소는 각각의 원소에 대한 편미분을 의미한다.

∂L/∂W의 1행 1열을 보면 (∂L/∂W11)은 w11을 조금 변경했을 손실 함수 L이 얼마나 변화하느냐를 나타낸다.

미분 = ( 순간 변화량)

 

이제 간단한 신경망을 예시로, 실제 기울기를 구하는 코드를 구현해보자.

 

import sys, os
sys.path.append(os.pardir)
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)  # 정규분포로 초기화
        
    def predict(self,x):
        return np.dot(x, self.W)
    
    def loss(self,x,t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y,t)
        
        return loss

여기에서는 common파일에 있는 softmax함수와 cross_entropy_error 메소드를 이용한다.

simpleNet 클래스는 shape이 2x3인 가중치 매개변수 하나를 인스턴스 변수로 갖는다.

 

메소드는 2개인데, 하나는 예측을 수행하는 predict(x)이고, 다른 하나는 손실함수의 값을 구하는 loss(x,t)이다.

 

여기서 x는 입력데이터, t는 정답 레이블이다.

이제 테스트를 해보자.

net = simpleNet()
print(net.W)

[[ 0.97205297 -0.01410488  0.11274547]
 [-0.03945782  0.14582452 -0.14564941]]

 

x = np.array([0.6, 0.9])
p = net.predict(x)
print(p)

[ 0.54771974  0.12277914 -0.06343719]

print(np.argmax(p))

0

x(입력 데이터) 에 배열 0.6, 0.9를 전달 -> 최대값의 인덱스는 첫 번째인것 확인( np.argmax = 0 )

 

t = np.array([0,0,1])
net.loss(x, t)

1.398035928884765

 

 

이제 기울기를 구해보자! 위에 만들어놓은 기울기 함수 numerical_gradient()메소드를 이용

def f(W):
	return net.loss(x, t)
    
dW = numerical_gradient(f, net.W)
print(dw)

[[ 0.29280909  0.19065524 -0.48346433]
 [ 0.43921364  0.28598286 -0.7251965 ]]

dW는 기울기 함수의 결과값으로, shape이 2x3인 배열이다.

W11을 보면 0.292..인데 이는 w11을 t만큼 증가시키면 손실 함수의 값이 0.292t만큼 증가한다고 할 수 있다.

W13또한 마찬가지로 t만큼 증가시키면 -0.48만큼 증가  ( 음수니까 감소 ) 한다고 볼 수 있다.

 

그래서 위에서 말한 손실함수를 줄이다는 관점에서는 기울기의 반대방향으로 가중치를 조정해가면 된다.

 

이제 본격적으로 신경망을 구현해보자.

import sys,os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient

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)
        
    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        
        b1, b2 = self.params['b1'], self.params['b2']
        
        a1 = np.dot(x,W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
    
    def loss(x,t):  # x = 입력데이터, t = 정답레이블
        y = self.preidct(x)
        
        return cross_entropy_error(y,t)
    
    def accuracy(self,x,t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum( y == t) / float(x.shape[0])
        return accuracy
    
    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

 

TwoLayerNet클래스는 딕셔너리인 params와 grads를 인스턴스 변수로 갖는다.

params 변수에는 가중치 매개변수가 저장되는데, 예를 들어 1번째 층의 가중치 매개변수는 params['W1'] 인 것!

 

 

키에 넘파이 배열로 저장된다. 마찬가지로 1번째 층의 편향은 params['b1']키로 접근한다.

 

net = TwoLayerNet(input_size=784, hidden_size = 100, output_size=10)
print(net.params['W1'].shape) # (784, 100)
print(net.params['b1'].shape) # (100,)
print(net.params['W2'].shape) # (100, 10)
print(net.params['b2'].shape) # (10,)

(784, 100)
(100,)
(100, 10)
(10,)

 

항상 아래 그림을 잊지 말자 !!!!

 

이제 미니배치 학습을 구현해보자.

from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

train_loss_list = []

# 하이퍼 파라미터
iters_num = 10000   # 반복 횟수
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10 )

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)

여기서 배치 크기는 100으로 설정했다. 

즉 60,000개의 훈련 데이터에서 임의로 100개의 데이터 ( 이미지 데이터와 정답 레이블 데이터)를 추려 내는것

그리고 그 100개의 미니배치를 대상으로 확률적 경사 하강법을 수행해 매개변수를 갱신한다.

 

경사법에 의한 갱신 횟수를 10,000번으로 설정하고, 갱신할 때마다 훈련 데이터에 대한 손실 함수를 계산하고, 

그 값을 배열에 추가한다. 이 손실 함수의 값이 변화하는 추이를 그래프로 나타내면 다음과 같다.

 

학습 횟수가 늘어남에 따라 손실함수의 값이 줄어드는것을 확인할 수 있다.

 

이제 모델을 평가하는 코드까지 구현하면 학습 모델을 성공적으로 마무리할 수 있다.

# 1에포크당 정확도 계산
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' + str(train_acc) +', ' +str(test_acc))

위 코드는 1에포크당 모든 훈련데이터와 시험데이터에 대한 정확도를 계산하고, 그 결과를 기록한다.

 

앞의 코드를 그림으로 보면 다음과 같다.

'밑바닥 딥러닝' 카테고리의 다른 글

Ch 5-2 오차 역전파 (Error BackPropagation)  (0) 2023.05.27
Ch 5-1 오차 역전파 ( Error Backpropagation )  (0) 2023.05.27
Ch4-1 신경망 학습  (0) 2023.05.17
Ch3-2 출력층 설계, 실습  (0) 2023.05.17
Ch3-1 신경망  (0) 2023.05.17

키워드: 오버피팅, 손실함수 ( 오차제곱합 , 교체 엔트로피 오차), 미니배치 학습, 미분/기울기 개념

 

신경망의 특징은 데이터를 보고 학습할 수 있다는 것이다.

데이터를 통해 학습한다는 것은 가중치 매개변수의 값을 데이터를 보고 결정한다는 뜻이다.!

 

신경망 층을 깊게 만든다면 매개변수가 n억개~일텐데 어떻게 학습하여 값을 조정하는지 그 원리에 대해서 알아보자.

 

먼저 숫자 5에 대한 이미지를 살펴보자.

사람마다 필체가 다르고, '5'라고 생각하는 사람마다의 기준이 다르다.

 

따라서 컴퓨터에게 '5'를 인식할 수 있도록 알고리즘을 설계할 수 도 있지만,

주어진 데이터를 잘 활용하여 해결할 수 있지 않을까? 라는 의문을 갖게 한다.

 

실제로 이미지에서 특징을 추출하고 그 특징의 패턴을 기계학습 기술로 학습하는 방법이 있다.

여기서 말하는 특징은 입력데이터(입력이미지)에서 본질적인 데이터를 정확하게 추출할 수 있도록 설계된 변환기를 가르킨다.

이미지의 특징은 주로 벡터로 기술하고, CV분야에서는 SIFT, SURF, HOG 등의 특징을 많이 사용한다.

 

이런 특징을 사용하여, 이미지 데이터를 벡터로 변환하고, 변환된 벡터를 가지고

지도학습의 대표적인 SVM, KNN 등으로 학습할 수 있다.

 

그림을 통해 이해하자!!!!

 

흰색 부분은 사람의 개입이 있고, 회색부분은 사람의 개입없이 소프트웨어 스스로 학습하는것을 의미한다.

 

손실 함수

보통 딥러닝에서, 모델의 성능을 평가하는 지표로 '손실 함수'를 사용한다.

 

이 손실 함수는 임의의 함수를 사용할 수 도 있지만 일반적으로는 오차제곱합(SSE)과  교차엔트로피 오차(CEE)를 사용

오차제곱합 (sum of squares for error)

위 식에서 Yk=신경망의 출력 (예측값)

                Tk=정답데이터 (레이블)

                k = 데이터 차원 수 (개수 x)

 

교차 엔트로피 오차 ( cross-entropy error , CEE )

tk, yk는 위와 같으므로

E =     -  ∑ (정답데이터 x log 예측값)

 

정답레이블 = 원핫인코딩이므로 ex ) [ 0,1,0,0,0,0 ] 직관적으로 볼 때 오답일 경우 (0) 값은 0, 정답일 경우에만 값이 존재함

 

따라서 정답일 때의 출력이 전체 값을 결정함.

 

+ 로그함수를 그래프로 그리면 x=1일때 y는 0이 되고 x가 0이랑 가까워질수록 값이 매우 작아짐.

--->위의 식도 마찬가지로 정답에 가까워질수록 0에 다가가고, 오답일경우(오답에 가까운경우)

                                                                                                   교차 엔트로피 오차는 커진다.

 

def cross_entropy(y, t):
	delta  = 1e-7
    return -np.sum(t*np.log(delta + y))

코드를 잘 보면 로그 계산식 안에 아주 작은값 delta가 더해져 있는데,

이는 log 0 = 무한대이므로 이를 방지하기 위한 장치임을 알 수 있다.

 

t=정답레이블이고, 2가 정답이라고 하자

y=예측값이고, 2에 대한 예측이 0.6이다.

t = [ 0, 0, 1, 0, 0 ,0 ,0 ,0 ,0 ,0 ]
y = [0.1, 0.05, 0.6, 0 ,0.05, 0.1, 0, 0.1, 0 , 0 ]

cross_entropy(np.array(y),np.array(t))

0.510825457099338

교차 엔트로피 오차는 0.51이 나온것을 확인할 수 있다.

반대로 틀렸다고 해보자!! (정답을 6이라고 예측한 경우)

y = [0.1, 0.05, 0.1, 0 ,0.05, 0.1, 0, 0.6, 0 , 0 ]
cross_entropy(np.array(y),np.array(t))

2.302584092994546

값이 2.3이 출력된 것을 알 수 있다.

오답일경우 값이 크게 출력되는것을 확인할 수 있다.

 

미니배치 학습

우리가 앞에서 해본 MNIST 데이터set은 개수가 60,000개였다.

실무에서 딥러닝을 하게 되면, 이보다 훨씬 많은 빅데이터를 다루게 되는데, 이는 효율성, 시간적 측면에서 매우 비효율적이다.

따라서 드롭아웃, 미니배치 학습 등의 개념이 등장한다.

미니배치 학습은 말그대로 데이터 중 일부만 가지고 학습하는 것을 말한다.

이렇게 학습한 결과를 통해서 모델 전체의 '근사치'로 이용할 것이다.

 

import sys, os
sys.path.append(os.pardir)

from mnist import load_mnist

(x_train, t_train), (x_test, t_test) = \
    load_mnist(flatten=True, normalize=False)
print(x_train.shape)
print(t_train.shape)

(60000, 784)
(60000,)

앞에서처럼 다시 MNIST 데이터셋을 불러오자,

 

이후 이 데이터에서 랜덤으로 10개만 불러오자. 

train_size = x_train.shape[0]
batch_size= 10
batch_mask= np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]


np.random.choice(10000,10)

array([3241, 3271, 1538, 3602, 1414, 7328, 3428, 7021, 5979, 5607])

 

 

 

아래 코드)미니배치용 교차엔트로피 함수

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.shape[0]
    return -np.sum(t*np.log(1e-7 + y)) / batch_size

y가 1차원이라면 ( = 데이터 하나당 엔트로피 오차를 구하는 것) reshape함수로 데이터를 바꿔줌.

이후 배치 사이즈로 나눠서 정규화하고 이미지 1장당 평균의 교차 엔트로피 오차를 계산.

 

정답 레이블이 원-핫 인코딩이 아닌 '2'나 '6'등 숫자 레이블로 주어졌을 경우에는 다음의 코드로 살짝 수정한다.

return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

이 구현은 원-핫 인코일 때 t가 0인 원소는 교차 엔트로피 오차도 0이므로, 그 계산은 무시해도 좋다는 뜻이다.

(정답에 해당하는 출력값만으로 교차엔트로피 오차를 구하겠다는 뜻)

 

그래서 np.log(y[np.arange(batch_size), t] )로 바꾼것이다.

 

참고) np.arange(batch_size) = 0부터 batch_size-1까지 배열을 생성한다.

즉, batch_size 5면 0부터 4까지 [0,1,2,3,4]를 만듬.

 

 

왜 손실함수를 사용할까

딥러닝 모델의 성능의 지표로 정확도가 더 적합해 보이지 않나? 라고 생각할 수 있다.

 

이를 설명하기 위해 먼저 '미분'의 개념이 필요하다.

신경망 학습에서 최적 매개변수 (가중치 / 편향)를 탐색할 때는 손실함수의 값을 가능한 한 작게 하는 매개변수의 값을 찾음

 

이때 매개변수의 기울기 (미분)을 계산하고, 그 미분값을 기준으로(단서로 삼아) 파라미터를 조정해 나가는것이다.

 

손실 함수의 미분값이 음수이면 가중치 매개변수를 양의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다.

반대로 미분값이 양수면 가중치 매개변수를 음수로 변화시켜 손실함수의 값을 줄일 수 있다.

반대로 미분값이 0이면 가중치 매개변수를 어느쪽으로 움직여 손실함수의 값은 줄어들지 않는다.

그래서 학습(?)이 종료된다.   (원리 )

 

정확도를  성능의 지표로 삼지않는것은 대부분의 점에서 미분값이 0이나오기 때문이다.

--> 100개의 사진중 32개만 맞추면 정확도가 32%이다. 여기서 가중치 매개변수를 조금 바꾼다고 해도 정확도는 거의 바뀌지 않는다.  + 바뀐다 하더라도 35, 29 등 불연속적으로 바뀌게 된다.

 

하지만 손실 함수의 경우 연속적인값이 나온다. ex) 0.87634756... 그리고 변수값을 조금 변하면 그에 반응하여 손실 함수의 값 0.85323.. 처럼 연속적으로 변화하는것이다.

 

정확도는 매개변수의 미세한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속으로 갑자기 변화한다.

이는 계단 함수를 활성화함수로 사용하지 않는 이유와도 같다.
만약 계단 함수를 활성화함수로 사용한다면 위와 같은 이유로 모델이 좋은 성능을 낼 수 없다.

 

 

 

따라서 위 그래프에서, 계단 함수를 활성화함수로 사용하면 안되는 이유를 알 수 있다.

한 지점 의외 값은 모두 0이므로 계단 함수를 손실 함수로 사용하면 안된다!

 

미분

미분 = (특정) 순간의 변화량 

 

df(x)/dx = lim h->0 f(x+h) - f(x) / h

 

이를 곧이 곧대로 구현을 한다면 다음과 같다.

def numerical_diff(f,x):
	h = 10^-4
    return (f(x+h) - f(x)) / h

얼핏보면 문제가 없어 보이지만, 개선해야할 부분이 있다.

함수 f의 차분이다. (두 점에 함수 값의 차이)

 

진정한 미분은 x의 위치에서의 기울기(접선)이지만, 위의 식은 x+h와 x 사이의 기울기를 의미한다. (h가 완전한 0이 아님)

위 그림과 같이 오차가 발생하게 된다.                                                                                 위는 전방 차분

이 오차를 줄이기 위해 (x+h)와 (x-h)일 때의 함수의 차분을 계산하는 방법을 쓰기도 한다. 이를 중심/중앙 차분이라고 한다!

 

def numerical(f,x):
	h = 1e-4
   	return (f(x+h)-f(x-h)) / 2*h

 

편미분

f(x, y) = x^2  + y^2이 있다고 하자.

이 식은 다음과 같이 구현할 수 있다.

def funct_2(x):
	return x^2 + y^2
    
# x = 배열

이 함수의 그래프를 그려보면 다음과 같이 3차원 그래프로 나온다.

이제 위의 식을 미분해보자. 변수가 2개라서 어느 변수에 대한 미분인지 구별해야한다.

따라서 편미분이 필요하다.

 

x0 = 3,  x1= 4일때, x0에 대한 편미분 ∂f/∂x0를 구하라.

 

def function_tmp1(x0):
	return x0 * x0 + 4.0 **2.0
    
print(numerical_diff(function_tmp1,3.0))
6.00009999999429
def function_tmp2(x1):
	return 3**2 + x1*x1
    
print(numerical_diff(function_tmp2, 4.0))
8.00009999998963

 

 

위 문제들은 변수가 하나인 함수를 정의하고, 그 함수를 미분하는 형태로 구현해서 풀었다.

 

이처럼 편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구한다.

 

 

 

'밑바닥 딥러닝' 카테고리의 다른 글

Ch 5-2 오차 역전파 (Error BackPropagation)  (0) 2023.05.27
Ch 5-1 오차 역전파 ( Error Backpropagation )  (0) 2023.05.27
Ch4-2 신경망 학습  (0) 2023.05.26
Ch3-2 출력층 설계, 실습  (0) 2023.05.17
Ch3-1 신경망  (0) 2023.05.17

출력층 설계하기

신경망은 분류/회귀 모두 이용할 수 있고, 어떤 문제냐에 따라서 출력층에서 사용하는 활성화 함수가 달라진다.

일반적으로 회귀문제에는 항등 함수를, 분류문제에서는 소프트맥스함수를 사용한다.

 

번외로)

시그모이드 vs 소프트맥스의 특  ----->소프트맥스(=다중분류), 시그모이드 (=이진분류)

그리고

 

일반적인 DNN과 CNN에서는 주로 ReLU를 ,

RNN에서는 주로 시그모이드와 하이퍼볼릭 tan함수를 사용한다고 한다.

 

다시 책으로 돌아와서, 항등함수와 소프트맥스 함수를 구현해보자.

항등함수란 중학교 때 배웠다시피 입력=출력이 같은것을 말한다.

한편 소프트맥스 함수는 다음과 같다.

 

exp(x) = e^x인 지수함수.

그림으로 나타내면 아래와 같다.

Output인 y1, y2, y3를 보면 모든 입력에서 영향을 받는다는 것을 알 수 있다.

 

이를 구현해보면,

a = np.array([0.3, 2.9, 4.0])

exp_a = np.exp(a)
print(exp_a)

[ 1.34985881 18.17414537 54.59815003]
sum_exp_a = np.sum(exp_a)
print(sum_exp_a)

74.1221542101633
y = exp_a / sum_exp_a
print(y)

[0.01821127 0.24519181 0.73659691]

 

함수식을 구현해보면,

def softmax(a):
	exp_a = np.exp(a)
	sum_exp_a = np.sum(exp_a)
	y = exp_a / sum_exp_a

	return y

 

소프트맥스 함수 구현 시 주의점

-오버플로우 문제

소프트맥스 함수는 지수 함수를 사용한다. 공식을 자세히 보면, 지수함수끼리 나눗셈을 하는데, 이런 큰 값끼리 연산을 하게 되면 값이 매우 불안정해진다. 이 오버플로우 문제를 해결하기 위해 수식을 개선해보자.

1: 분자 분모에 C를 곱하고,  2: 이 C를 괄호 안으로 옮겨서 로그로 만든다. 3: logC = C'로 치환

 

이렇게 수식을 바꾸면 지수함수를 계산할 때 어떤 정수를 더해도(빼도) 결과는 바뀌지 않는다는 것이다.

또한 위 수식에서 C'에 어떤 값을 대입해도 상관없지만, 통상적으로는 입력 신호 중 최대값을 이용한다.

a = np.array([1010, 1000, 990])
np.exp(a) / np.sum(np.exp(a))

array([nan, nan, nan])

값이 너무 크기 때문에 Nan이 출력되는것을 볼 수 있다.

 

c = np.max(a)
a - c

array([  0, -10, -20])

위 그림은 +로 되어있지만 사실상 오버플로우 대책은 -로 해줘야한다!! (값이 너무 작으면 더하기)

np.exp(a - c) / np.sum(np.exp(a-c))

array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09])

 

따라서 오버플로우를 방지한 개선된 소프트맥수 함수는 다음과 같이 구현할 수 있다.

def softmax(a):
	c = np.max(a)
    exp_a = np.exp(a-c)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    
    return y

 

소프트맥수의 출력값의 의미

우리는 위 수식을 통해 소프트맥스 함수가 항상 0~1로 출력되는것을 확인했다.

또한 소프트맥스함수는 주로 분류일 때 사용된다고 했으므로, 출력값을 확률의 개념이라고 생각할 수 있다.

 

+ 출력층이 여러개이므로 각각의 값 = 각각 정답일 확률을 표현한 것

 

마무리)

지금까지 그림과 식들을 통해 입력 데이터를 기반으로 입력층에서 출력층까지 변수들이 이동하는 것을 살펴보았다.

바로 위의 문장을 순전파 ( forward propagation )이라고 한다.

 

이제 MNIST 데이터셋으로 간단한 실습을 해보자.

MNIST란 손글씨 이미지 데이터 모음으로, 학습용 데이터로 매우 적합하다.

 

import sys, os
sys.path.append(os.pardir)
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) =\
	load_mnist(flatten=True, normalize=False)
    
 print(x_train.shape)
 print(t_train.shape)
 print(x_test.shape)
 print(t_test.shape)
 
 (60000, 784)
(60000,)
(10000, 784)
(10000,)

위의 코드에서 normalize=True 는 픽셀값을 0~1 사이 값으로 정규화 할지 말지 결정하는것을 의미한다.

두번 째 인수인 Faltten은 입력이미지를 1차원으로 바꿀지 말지를 결정합니다. (False는 3차원 배열로)

 

이제 이미지를 출력해보자.

from PIL import Image

def img_show(img):
	pil_img = Image.fromarray(np.unit8(img))
    pil_img.show()
    
(x_train, t_train), (x_test, t_test) = \
	load_mnist(flatten=True, normalize=False)
    
img = x_train[0]
label = t_train[0]
print(label)

print(img.shape)
img = img.reshape(28, 28)
print(img.shape)

img_show(img)

주의사항으로, flatten=True로 설정한 이미지는 1차원 배열로 저장되어있다.

따라서 이미지를 표시하기 위해서는 28x28크기로 reshape(-1,28,28)해야한다!!

 

신경망의 추론 처리

위에서 구현한 신경망은 입력층 뉴런 784개( 28x28 ), 출력층 뉴런은 10개 (0~9를 분류)임을 확인할 수 있다.

 

또한, 여기서 은닉층을 두 개 설정할 것이고, 각각 50개, 100개의 뉴런을 배치해보자.(임의로 정한것)

 

 

def get_data():
	(x_train, t_train), (x_test, t_test) =\
    	load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test
    
def init_network():
	with open("sample_weight.pkl", 'rb') as f:
    	network = pickle.load(f)
        
    return network
    
def predict(network, x):
	W1,W2,W3 = network['W1'], network['W2'], network['W3']
    b1,b2,b3 = network['b1'], network['b2'], network['b3'] 
    
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a1)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)
    
    return y

이제 모델의 정확도를 구하는 원리를 알아보자.

 

 

x, t = get_data()
network = init_network()

accuracy_cnt = 0
for i in range(len(x)):
	y = predict(network, x[i])
    p = np.argmax(y) # 확률이 가장 높은 원소의 인덱스를 얻는다.
    if p == t[i]
    	accuracy_cnt += 1
print("Accuracy: " + str(float(accuracy_cnt) / len(x)))

Accuracy: 0.9352

93.52%의 정확도를 나타낸다.

 MNIST 데이터셋을 얻고 네트워크를 생성한다. 

이어서 for문을 돌며 x에 저장된 사진을 한장 씩 꺼내서 predict()함수에 집어 넣는다.

 

predict() 함수는 각 레이블의 확률을 넘파이 배열로 반환한다.

예를 들어 [0.1, 0.8, 0.9 .... 0.1] 같은 배열이 반환되며 이는 각각이 인덱스(숫자 번호)일 확률을 나타낸다. 

 

그 다음 np.argmax()함수로 이 배열에서 값이 가장 큰 ( 확률 제일 높은) 원소의 인덱스를 구한다.

이것이 바로 예측 결과 !

마지막으로 정답 데이터와 비교하여 맞힌 숫자를 count += 1씩 하고, count / 전체 이미지 수 로 나눠서 정확도를 구하는 것이다.

 

'밑바닥 딥러닝' 카테고리의 다른 글

Ch 5-2 오차 역전파 (Error BackPropagation)  (0) 2023.05.27
Ch 5-1 오차 역전파 ( Error Backpropagation )  (0) 2023.05.27
Ch4-2 신경망 학습  (0) 2023.05.26
Ch4-1 신경망 학습  (0) 2023.05.17
Ch3-1 신경망  (0) 2023.05.17

3장 신경망을 배우기 전, 

퍼셉트론에 대해 알고 가야할 필요가 있다.

 

퍼셉트론이란 다수의 신호를 입력으로 받아 하나의 신호를 출력하는 것을 말한다.

또한 이를 통해서 AND,OR,NAND 게이트를 구현해 보았다. 

 

퍼셉트론은 신경망의 기초가 되는 개념으로, 신경망과 유사한 개념이라고도 볼 수 있다.

 

신경망

위 그림을 신경망이라고 하고, 왼쪽부터 각각 입력층, 은닉층, 출력층이라고 한다.

'은닉층'이라고 하는 이유는 사람의 눈에는 보이지 않는다. (입력, 출력층과 달리)

 

 또한 왼쪽부터 책에서는 0층,1층,2층이라고도 말한다.

 

활성화 함수

활성화 함수란 입력 신호의 총합을 출력 신호로 변환하는 함수를 말한다.

 

앞에서 배운 시그모이드, 소프트맥스, 렐루 등이 그 예이다.

시그모이드 함수

exp^-x는 e^-x를 뜻하며,  e는 자연상수로 2.7182...의 값을 가지는 실수이다.

 

계단함수를 이용해서 시그모이드 함수를 더 알아보도록 하자.

계단함수는 입력이 0을 넘으면 1을, 그외에는 0을 출력하는 함수이다.

이를 파이썬으로 구현해보면 다음과 같다.

def step_function(x):
	if x > 0:
    	return 1
    else:
    	return 0

여기서 만약 x인수에 단일 실수값이 아닌, 배열을 넣고 싶다면??

def step_function(x):
	y = x > 0
    return y.astype(np.int)

계단함수의 그래프

import numpy as np
import matplotlib.pyplot as plt
def step_function(x):
    return np.array(x>0, dtype=np.int)

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x,y)
plt.ylim(-0.1, 1.1)
plt.show()

계단 함수라는 말 그대로 0을 기준으로 출력이 0->1로 바로 바뀌는 것을 볼 수 있다.

 

이어서 시그모이드 함수를 직접 구현해보자.

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

 인수 x가 배열이여도 결과가 정상적으로 출력된다!

x = np.array([-1.0, 1.0, 2.0])
sigmoid(x)

array([0.26894142, 0.73105858, 0.88079708])

배열을 넣어도 올바르게 결과가 나오는 것은 넘파이 라이브러리의 브로드캐스트 기능덕분이다.

브로드캐스트는 넘파이 배열과 스칼라값의 연산을 넘파이 배열의 원소 각각과 스칼라값의 연산으로 바꿔 수행하는 것을 말한다.

 

예를 들면

t = np.array([1.0, 2.0, 3.0])
1.0 + t

array([2., 3., 4.])
1.0 / t

array([1.        , 0.5       , 0.33333333])

스칼라값 1.0 과 배열 t 사이에서 연산이 각 원소별로 이루어진것을 브로드캐스팅이라고 하는것.!

 

이제 시그모이드 함수를 그래프로 그려보자.

x = np.arange(-5, 5, 0.1)
y = sigmoid(x)
plt.plot(x,y)
plt.ylim(-0.1, 1.1)
plt.show()

 

위의 계단함수와 비교해봤을때 차이는 매끄럽냐vs아니다. (연속적)

이 매끈한것이 신경망 학습에서 중요한 역할을 하게된다고 한다.

 

비선형 함수

계단함수, 시그모이드 함수의 공통점은 모두 비선형 함수라는 것이다. 

시그모이드 = 곡선, 계단 함수 = 구부러진 직선

 

신경망에서 사용하는 활성화 함수로는 비선형 함수만을 사용해야 한다.  (선형 함수 =>   y= ax+ b인 직선)

(신경망의 층을 깊게 하는 의미가 없어지기 때문에)

 

선형 함수의 문제는 층을 아무리 깊게 해도 '은닉층이 없는 네트워크'로도 똑같은 기능을 할 수 있다는 데 있다.

ex) h(x) =cx이고 3층 네트워크라고 하면 y(x)=h(h(h(x)))이고, 이는 y(x)=c*c*c , c^3이다.

즉, 은닉층이 필요없는 구조라고 볼 수 있다. 이러면 은닉층이 갖는 benefits을 포기하는 것이기 때문에 

더 구체적이고 예민한 비선형함수를 사용해야 하는것이다.

 

ReLU 함수

 

시그모이드 함수는 신경망에서 예전부터 많이 이용해왔는데, 최근에는 ReLU함수를 주로 이용한다고 한다.

 

ReLU함수는 입력이 0을 넘으면 그 입력을 그대로 출력, 0이하이면 0을 출력하는 함수다.

식으로는

 

코드로는

def ReLU(x):
	return np.maximum(0, x)

 

다차원 배열

신경망을 효율적으로 구현하려면 다차원 배열의 개념을 알고 있어야 한다.

다차원 배열의 기본 개념은 "숫자의 집합"이라고 할 수 있다.

숫자가 한 줄로 늘어선 것, 직사각형으로 늘어놓은 것, 3차원으로 늘어놓은 것 등등... 을 모두 다차원 배열이라고 할 수 있다.

A = np.array([1, 2, 3, 4])
print(A)

[1 2 3 4]
np.ndim(A)

1
A.shape

(4,)

배열의 차원수를 알려주는 np.ndim()

배열의 모양 shape (함수아님 인스턴수 변수임)

.shape은 또한 튜플을 반환하는데, 그 이유는 1차원 배열일지라도 다차원 배열일 때와 통일된 형태로 반환하기 위함이다.

 

ex)2차원 배열일 때는 (4,3) , 3차원 배열일 때는 (4,3,2) 를 반환한다.

 

이제 2차원 배열을 구현해보자.

B = np.array([1,2], [3,4], [5,6]])
print(B)

[[1 2]
 [3 4]
 [5 6]]
np.ndim(B)
2
B.shape
(3,2)

여기서는 3X2 배열인 B를 작성했다.

3x2 행렬이라고 볼 수 있다.

-----행렬 생략-------

 

3층 신경망 구현하기

이제 3층 신경망을 구현해보고자 한다. ( 3층 = 0층~3층, 총 4개의 층)

 

0층 : 입력층 - 2개

1층 : 은닉층 - 3개

2층 : 은닉층 - 2개

3층 : 출력층 - 2개의 뉴런으로 구성되어 있다.

 

여기서 각 뉴런사이의 화살표에 집중해보자.

입력층의 뉴런에서 은닉층의 뉴런으로 이어져 있는 선이 있다.

 

더 자세히 말하자면 입력층의 두 번째 뉴런에서 은닉층의 첫 번째 뉴런으로 이어져 있는데,

이를 W1,2라고 표현한다. (앞에가 도착지, 뒤에가 출발지)

 

이제 절편 (편향)을 추가하여 신호 전달을 구현해보자.

위의 그림에서 a1을 수식으로 나타내보자.

a1 = w11 * x1 + w12 * x2 + b

 

여기서 행렬곱을 사용하여 1층의 가중치 부분을 간소화 할 수 있다.

A = XW + B

행렬 A, X, B, W는 각각 다음과 같다.

 

A = (a1 a2 a3) , X = (x1 x2) , B = (b1, b2 , b3)

W = (w11, w21, w31)

        (w12, w22, w32)

 

이제 식 A = XW +B를 구현해보자.

X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])

print(W1.shape)
print(X.shape)
print(B1.shape)

(2, 3)
(2,)
(3,)

A1 = np.dot(X, W1) + B1

위 코드를 그림으로 보면 아래처럼 된다.

h(), 즉 은닉층의 노드를 보면 a1이 z1로 변하는데,

a1은 앞의 입력층에서 1 (편향, 절편) + w11 * x1 + w12 * x2의 합이고,

이 a1이 활성화 함수를 지나서 z1로 변환된 것이다.

 

이제 1층에서 2층으로 가는 과정을 살펴보고 구현해보자.

W2 = np.array([[0.1, 0.4] ,[0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])

print(Z1.shape)
print(W2.shape)
print(B2.shape)
(3,)
(3, 2)
(2,)

A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)

1층과 마찬가지로 같다!

 

이제 마지막으로 2층에서 출력층으로 가는 신호 전달과정을 살펴보자.

 def identity_function(x):
 	 return x
     
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])

A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3)  # 혹은 Y=A3

 

'밑바닥 딥러닝' 카테고리의 다른 글

Ch 5-2 오차 역전파 (Error BackPropagation)  (0) 2023.05.27
Ch 5-1 오차 역전파 ( Error Backpropagation )  (0) 2023.05.27
Ch4-2 신경망 학습  (0) 2023.05.26
Ch4-1 신경망 학습  (0) 2023.05.17
Ch3-2 출력층 설계, 실습  (0) 2023.05.17

+ Recent posts