오차 역전파( 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

미리 보기 - 텐서플로를 사용해 순환 신경망을 만들어 영화 리뷰 데이터셋에 적용해서 리뷰를 긍정/부정으로 분류하기

<<<키워드>>>

- 원 핫 인코딩

- 단어 임베딩

IMDB 데이터셋

유명한 영화 데이터베스인 imdb.com에서 수집한 리뷰를 감상평에 따라 긍정 / 부정으로 분류해 놓은 데이터셋이다.

총 50,000개의 샘플로 이루어져 있고 훈련데이터와 테스트 데이터에 각각 25,000개씩 나누어져 있다!

 

데이터로 텍스트 자체를 신경망에 전달?? X 컴퓨터는 숫자 데이터를 다루기 용이하므로 단어를 숫자 데이터로 바꾸는 작업 필요하다.! like 원-핫 인코딩   --> 토큰으로 변환 !!

하나의 샘플은 여러 개의 토큰으로 이루어져 있고 1개의 토큰이 1개의 타임스텝에 해당한다.

 

IMDB 데이터셋을 두 가지 방법으로 순환 신경망에 적용시켜보자!

1. 원-핫 인코딩

2. 단어 임베딩

 

 

keras 데이터셋 패키지의 imdb 모듈을 import해서 자주 사용하는 단어 500개를 불러오자.

( num_words 매개변수를 500으로 지정 )

from tensorflow.keras.datasets import imdb
(train_input, train_target),(test_input, test_target) = imdb.load_data(num_words=500)

print( train_input.shape, test_input.shape )
(25000,) (25000,)

 

훈련 세트와 테스트 세트의 크기 25,000씩인것까지 확인!

 

근데 (25000,) 을 보듯이 배열이 1차원이다.! 

텍스트 데이터 특성상 길이가 제각각이므로 고정 2차원 배열에 담는것보다 메모리 효율적이므로 1차원에 저장하는것이다!

즉, 넘파이 배열안에  정수,숫자가 아닌 개별 리뷰가 담겨있다고 볼 수 있다.

 

첫 번째 리뷰의 길이를 출력해보자!!

print(len(train_input[0]))

218

218개의 토큰으로 이루어져 있다.!

 

이제 길이말고 내용을 출력해보고 싶다!

print(train_input[0])

[1, 14, 22, 16, 43, 2, 2, 2, 2, 65, 458, 2, 66, 2, 4, 173,36, 256, 5, 25, 100, 43, 2, 112, 50,
2, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 2, 2, 17, 2, 38, 13
, 447, 4, 192, 50, 16, 6, 147, 2, 19, 14, 22, 4, 2, 2, 469, 4, 22, 71, 87, 12, 16, 43, 2, 38, 
76, 15, 13, 2, 4, 22, 17, 2, 17, 12, 16, 2, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2, 2
, 16, 480, 66, 2, 33, 4, 130, 12, 16, 38, 2, 5, 25, 124, 51, 36, 135, 48, 25, 2, 33, 6, 22, 
12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 2, 15, 256, 4, 2, 7, 2, 5, 2, 36, 
71, 43, 2, 476, 26, 400, 317, 46, 7, 4, 2, 2, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2, 56, 26
, 141, 6, 194, 2, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30,2, 18, 51, 36, 28, 224, 92
, 25, 104, 4, 226, 65, 16, 38, 2, 88, 12, 16, 283, 5, 16, 2, 113, 103, 32, 15, 16, 2, 19, 178,32]

텐서플로에 있는 IMDB 리뷰 데이터는 이미 정수로 변환되어서 개별 토큰으로 출력된다.

참고로 num_words=500으로 지정했기 때문에 어휘 사전에는 500개의 단어만 들어가있다.

따라서 어휘 사전에 없는 단어는 모두 2로 표시 된다!!!

 

이제 타겟 데이터를 출력해보자.!

print(train_target[:20])

[1 0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 1 1 0 1]

타겟 데이터 => 0: 부정, 1: 긍정


데이터를 더 살펴보기 전 훈련세트에서 검증세트를 떼어 놓자 !

from sklearn.model_selection import train_test_split
train_input, val_input, train_target, val_target = train_test_split(
train_input, train_target, test_size=0.2, random_state= 42)

20%를 검증세트로 떼어 놓았으니 훈련 세트의 크기는 20,000개로 줄어들것.

 

 

이제 훈련 세트에 대해 몇 가지를 건드려보자.!

각 리뷰의 길이들을 확인해보고 싶다.!! 

 

-> 각 리뷰의 길이를 계산해 넘파이 배열에 담고, 리스트 함축 구문을 이용해 train_input의 원소를 순회하면서 길이를 알아보자.

import numpy as np
lengths = np.array([len(x) for x in train_input])

print(np.mean(lengths), np.median(lengths))
239.00925 178.0

평균 = 239     /   중간값 = 178

 

평균값과 중간값이 차이가 많이 나므로 데이터의 분포가 한쪽에 치우쳐져 있을 것이다.

import matplotlib.pyplot as plt
plt.hist(lengths)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()

대부분의 리뷰 길이는 300이 안되는것을 확인할 수 있다.!

 

연습삼아 100개의 단어만 사용해보자!

하지만 100개의 단어도 안되는 리뷰가 굉장히 많으므로 앞서 배운 패딩을 적용시켜야 한다.

 

보통 업계에서 패딩을 나타내는 토큰으로는 0을 사용한다고 한다.

 

케라스의 시퀀스 데이터의 길이는 맞추는 메소드를 이용해보자 (pad_sequences()).

이 메소드를 이용해서 train_input의 길이를 100으로 간편하게 맞춰보자.

from tensorflow.keras.preprocessing.sequence import pad_sequences
train_seq = pad_sequences(train_input, maxlen=100) # 패딩으로 train_input길이를 100으로 맞춤

# 100보다 길면 잘라내고 짧으면 0으로 패딩 ->

100으로 패딩한 train_seq을 출력해보자.

print(train_seq.shape)

(20000, 100)

아무래도 2차원 배열이 데이터를 다루기에 효율적이므로,

패딩을 이용해 길이를 같게 해줌으로써 2차원 배열로 만든것 같다!

샘플 개수 = 25,000개에서 20%떼어냈으므로 20,000개,

토큰 개수 = 100으로 패딩했으므로 100개로  이루어진 2차원배열이다!!

 

train_seq에 있는 첫 번째 샘플을 출력해보자.

 

print(train_seq[0])

[ 10   4  20   9   2 364 352   5  45   6   2   2  33 269   8   2 142   2
   5   2  17  73  17 204   5   2  19  55   2   2  92  66 104  14  20  93
  76   2 151  33   4  58  12 188   2 151  12 215  69 224 142  73 237   6
   2   7   2   2 188   2 103  14  31  10  10 451   7   2   5   2  80  91
   2  30   2  34  14  20 151  50  26 131  49   2  84  46  50  37  80  79
   6   2  46   7  14  20  10  10 470 158]

이 샘플의 앞뒤에 패딩값 0이 없는것으로 보아 100보다는 긴 리뷰라고 볼 수 있다.!

 

원래 샘플의 앞vs뒤 부분중 어디가 잘렸을지 확인해보기 위해 train_input에 있는 원본 샘플의 끝을 먼저 보자.

print(train_input[0][-10:])

[6, 2, 46, 7, 14, 20, 10, 10, 470, 158]

음수 인덱스슬라이싱을 사용해 train_input[0]에 있는 마지막 10개의 토큰을 출력했다

train_seq[0]의 출력값과 정확히 일치하는것 또한 확인할 수 있다.

 

그러면????

샘플 앞부분이 잘렸다는 것!!

 

참고)  pad_sequences 메소드는 기본적으로 maxlen보다 긴 시퀀스의 앞부분을 자른다고 한다!!!

이유-> 일반적으로 텍스트 中 중요한 정보는 뒷부분(어미)에 나올 확률이 높기 때문..

 

하지만!! 

그럼에도 불구하고 앞부분을 자르고 싶다면 

pad_sequences( truncating = 'post') 로 설정해주면 된다. truncating = 'pre'가 default값

 

print(train_seq[5])

[  0   0   0   0   1   2 195  19  49   2   2 190   4   2 352   2 183  10
  10  13  82  79   4   2  36  71 269   8   2  25  19  49   7   4   2   2
   2   2   2  10  10  48  25  40   2  11   2   2  40   2   2   5   4   2
   2  95  14 238  56 129   2  10  10  21   2  94 364 352   2   2  11 190
  24 484   2   7  94 205 405  10  10  87   2  34  49   2   7   2   2   2
   2   2 290   2  46  48  64  18   4   2]

이 6번째 샘플을 보면 앞 부분이 0으로 채워져 있는것으로 보아 리뷰 길이가 100이 안되는것을 확인할 수 있다.

 

또한, 앞 부분이 0으로 채워져 있는것으로 보아 위와 같은 이유(중요한 부분 -> 뒤에 있다.)로 인해

패딩토큰은 시퀀스의 뒷부분이 아닌 앞부분에 추가된다는것을 엿볼 수 있다.

 

하지만!!!!!

또 뒷부분을 0으로 채우고싶다면

pad_sequences( padding = 'post')로 바꿔주면 된다.!

 

이제 검증 세트의 길이도 100으로 맞춰보고 본격적으로 해보자.

val_seq = pad_sequences(val_input, maxlen=100)

훈련 세트, 검증 세트 모두 100으로 패딩완료.

 

순환 신경망 만들기

가장 간단한 클래스인 SimpleRNN클래스를 이용

IMDB 리뷰 긍정/부정 분류 모델이므로 이진분류---> 따라서 활성화 함수는 시그모이드로 선택

 

from tensorflow import keras

model = keras.Sequential()
model.add(keras.layers.SimpleRNN(8, input_shape=(100, 500)))
model.add(keras.layers.Dense(1, activation = 'sigmoid'))

<<SimpleRNN의 첫 번째 매개변수 X = 사용할 뉴런의 개수>> -> CNN과 동일

 

input_shape = (100,500)으로 했는데, 첫 번째 차원이 100인 것은 앞에서 샘플의 길이를 100으로 지정했기 때문이다.!

 

두 번째 차원이 500인 것은, 원-핫 인코딩을 위해 배열의 길이를 설정해준것!

(기존의 정수는 크기가 제각각이지만, 그 크기에는 순서와 의미가 없다. 따라서 그것을 제거해주는 원핫인코딩이 필요)

 

새로 등장한 개념인 순환층또한 활성화 함수가 필요하다! SimpleRNN의 기본 활성화 함수는 

하이퍼볼릭 탄젠트 함수인 tanh라는 매우 강력해보이는 함수를 사용한다. tanh => -1~1출력하는 시그모이드

 

 

우리는 원-핫 인코딩을 제공하는 모듈을 사용하도록 하자!

train_oh라는 매개변수 : train_seq을 원핫인코딩으로 변환한 배열

train_oh = keras.utils.to_categorical(train_seq)

print(train_oh.shape)
(20000, 100, 500)

배열 크기 20,000개인 이유 => 정수 하나하나 모두 500차원의 배열로 변경되었음 (20000, 100)크기의 train_seq이 

(20000,100,500으로 변한것)


이렇게 샘플 데이터의 크기가 1차원 정수배열(100,)에서 2차원 배열 (100, 500)로 바꿔야하므 SimpleRNN 클래스의 input_shape매개변수의 값을 (100,500)으로 지정한 것이다.

 

이제 인코딩이 잘 되었는지 확인해보자.

print(train_oh[0][0][:12])


[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]

< train의 첫 번째 샘플의 첫 번째 토큰 中 12개 원소 > -> 인코딩이 잘 되었음을 확인

 

이제 val도 인코딩

val_oh = keras.utils.to_categorical(val_seq)

 

모델 요약 !

model.summary()

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 simple_rnn (SimpleRNN)      (None, 8)                 4072      
                                                                 
 dense (Dense)               (None, 1)                 9         
                                                                 
=================================================================
Total params: 4,081
Trainable params: 4,081
Non-trainable params: 0
_________________________________________________________________

 

rmsprop = keras.optimizers.RMSprop(learning_rate=0.4)

model.compile(optimizer = rmsprop, loss = 'binary_crossentropy',
				metrics=['accuracy'])
                
check_point_cb = keras.callbacks.ModelCheckpoint('best-simplernn-model.h5',
						save_best_only = True)
earlystopping_cb= kerars.callbacks.EarlyStopping(patience=3, restore_best_weights=True)

history=model.fit(train_oh,train_target, epochs=100, batch_size=64,
				validation_data = (val_oh, val_target),
				callbacks = [check_point_cb, earlystopping_cb])
Epoch 1/100
313/313 [==============================] - 7s 19ms/step - loss: 0.7534 - accuracy: 0.4992 - val_loss: 0.7433 - val_accuracy: 0.5002
Epoch 2/100
313/313 [==============================] - 6s 18ms/step - loss: 0.7456 - accuracy: 0.4998 - val_loss: 0.7360 - val_accuracy: 0.4996
Epoch 3/100
313/313 [==============================] - 6s 18ms/step - loss: 0.7392 - accuracy: 0.4992 - val_loss: 0.7300 - val_accuracy: 0.5032
Epoch 4/100
313/313 [==============================] - 6s 18ms/step - loss: 0.7332 - accuracy: 0.4992 - val_loss: 0.7240 - val_accuracy: 0.5048
Epoch 5/100
...............................................................................................
313/313 [==============================] - 6s 18ms/step - loss: 0.6931 - accuracy: 0.5112 - val_loss: 0.6934 - val_accuracy: 0.5028
Epoch 33/100
313/313 [==============================] - 6s 18ms/step - loss: 0.6931 - accuracy: 0.5127 - val_loss: 0.6933 - val_accuracy: 0.5028

조기 종료(33회) 시의 에포크를 확인해보자! 정확도도 약 80%로 뛰어난 편이라고 할 수 있다.

 

이제 훈련 손실과 검증 손실을 이용해 그래프를 그려보자.

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epochs')
plt.ylabel('loss')
plt.legend(['train','val'])
plt.show()

단어 임베딩

순환 신경망에서 텍스트 처리 방법의 일종으로,

각 단어를 고정된 크기의 실수 벡터로 만들어 주는 것.!

 

보통 NLP에서는 원-핫 인코딩 보다 단어 임베딩이 더 좋다고한다..!

(원핫 인코딩된 숫자보다 의미 있는 값으로 채워줄 수 있다고해서)

 

또한 단어 임베딩의 장점 : 입력으로 정수를 받을 수 있다..!

<<즉, 원-핫 인코딩으로 변경된 train_oh 배열이 아니 train_seq를 사용할 수 있다. -> 메모리 효율적>>

 

이 또한 케라스 라이브러리에 있는 모듈을 사용해보자.

model2 = keras.Sequential()
model2.add(keras.layers.Embedding(500, 16, input_length = 100))
model2.add(keras.layers.SimpleRNN(8))
model2.add(keras.layers.Dense(1, activation = 'sigmoid'))

Embedding에 있는 500은 어휘 사전의 크기이다. (앞서서 설정한 500)

              ""               16은  임베딩 벡터의 크기이다.

 

모델 요약!

model2.summary()

Model2: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding_1 (Embedding)     (None, 100, 16)           8000      
                                                                 
 simple_rnn_2 (SimpleRNN)    (None, 8)                 200       
                                                                 
 dense_2 (Dense)             (None, 1)                 9         
                                                                 
=================================================================
Total params: 8,209
Trainable params: 8,209
Non-trainable params: 0
_________________________________________________________________

 

 

키워드 :  순차 데이터

               피드포워드 신경망

               타임스텝

               은닉 상태

               

 

댓글 등 텍스트를 인식하여 어떠한 평가, 의견등을 분석해 볼 수 있을까??

ex) 상품 판매 홈페이지에서 댓글을 통해 상품에 대한 긍정 / 부정적인 의견 구분하기

 

순차 데이터

순차 데이터는 텍스트, 리스트와 같이 순서에 의미가 있는 데이터를 말한다. ( 시계열 데이터 )

우리가 여태까지 사용했던 feature 데이터 등은 순서가 중요하지 않았다.

( 사용자 A : "별로지만 추천해요." ==> 긍정 부정 판단시 애매함, 순서 고려해야함 !! )

 

앞서 우리가 머신러닝 말고도 딥러닝의 완전 연결 신경망이나 CNN에서는 하나의 샘플에 대해 정방향 계산을 수행하고 

나면, 그 샘플은 버려지고 다시 사용하지 않았다.!!

이렇게 입력데이터의 흐름이 앞으로만 전달되는 신경망을 피드포워드 신경망이라고 한다.!!

신경망이 이전에 처리했던 샘플을 처리하는데 재사용하기 위해 이렇게 데이터의 흐름 앞으로 전해져선 안된다고 한다.

다음 샘플을 위해선 이전 데이터가 신경망 층에 순환될 필요가 있다. 이러한 신경망을 순환 신경망이라고 한다.

 

순환 신경망

순환 신경망은 일반적인 완전 연결 신경망과 거의 비슷하다.

완전 연결 신경망에서 이전 데이터의 처리 흐름을 순환하는 고리 하나만 추가하면 된다!!!

( 아래 그림에서 은닉층에 있는 붉은 고리처럼 )

그림을 보면 뉴런의 출력이 다시 자기 자신으로 되돌아온다. (재사용)

 

예를 들어 A,B,C 3개의 샘플을 처리하는 순환 신경망의 뉴런을 가정해보자. 첫 번째 A를 처리하고 난 뒤는 어떻게 될까?

첫 번째 샘플인 A를 처리하고, 다시 뉴런으로 들어간다. 그다음 B를 처리할 때 앞에서 A를 사용해 만든 출력 OA를 함께 사용한다.

따라서 OB에는 A에 대한 정보도 어느정도 포함되어 있을 것이다. 그다음 C도 마찬가지로 Ob를 함꼐 사용한다. (누적되겠지?)

이렇게 샘플을 처리하는 한 단계를 타임스텝이라고 말한다.

※OC에는 A보다 B에 대한 정보가 더 많다!!※--> 최신화 가중치?

 

순환 신경망에서는 층을 이라고 말한다.

하나의 셀에는 여러 개의 뉴런이 있지만 완전 연결 신경망과 달리 뉴런을 모두 표시하지 않고 하나의 셀로 층을 표현한다. 

또 셀의 출력을 은닉 상태라고 말한다.!!

 

--> summary : 기존의 연결 신경망 : 입력에 어떤 가중치를 곱하고 활성화 함수를 통과시                             켜 다음층으로 보내는것. 

                            + 층의 출력을 다음 스텝에서 재사용하면 순환 신경망

 

일반적으로 은닉층의 활성화 함수는 하이퍼볼릭 탄젠트 함수를 사용한다. tanh 

모양은 시그모이드함수와 비슷하게 생겼다.

차이점은 시그모이드와 달리 -1 ~ 1의 범위를 가진다.

 

아까 순환신경망은 재사용 빼고 다 같다고 했는데, 그렇다면 가중치는 어떻게 될까??

 

순환 신경망에서의 뉴런은 가중치가 하나 더 있다. 

이전 타임스텝의 은닉 상태에 곱해지는 가중치

Wx : 입력에 곱해지는 가중치

Wh : 이전 타임스텝의 은닉 상태에 곱해지는 가중치

셀의 은닉 상태 ( = 출력 )가 다음 타임스텝에서 재사용되기 때문에 타임스텝으로 셀을 나눠서 다시 그림을 보자.

 

셀의 가중치와 입출력

순환 신경망의 셀에서 필요한 가중치의 크기를 계산 해보자.

입력층 4개, 순환층 3개

Wx :  4 x 3 = 12

Wh :  3 x 3 = 9개

Wh의 크기는 n^2이다.

 

이제 모델 파라미터 개수 = 가중치 + 절편

12 + 9 + 3 이므로 24가지의 파라미터가 있음을 확인할 수 있다.!!

 

 

순환층의 입력 / 출력

CNN을 생각해 보면 합성곱 층의 입력은 전형적으로 하나의 샘플이 3개의 차원을 가집니다.

너비, 높이, 채널이다. 입력이 합성곱 층과 풀링 층을 통과하면 너비, 높이, 채널의 크기가 달라지지만 차원의 개수는 그대로 유지된다 !!!

 

순환층은 일반적으로 샘플마다 2개의 차원을 가진다. 

보통 하나의 샘플을 하나의 시퀀스라고 말한다고 한다고 한다.

시퀀스 안에는 여러 개의 아이템이 들어 있는데, 여기에서 시퀀스의 길이가 타임스텝의 길이가 된다!!!

 

ex) " I am a boy. "

이 샘플은 4개의 단어로 이루어져 있는데, 각 단어를 3개의 어떤 숫자로 표현한다고 가정해보자.

이런 입력이 순환층을 통과하면 두 번째, 세 번째 차원이 사라지고 순환층의 뉴런 개수만큼 출력된다.

이를 차근차근 설명해 보겠다. 

 

 

 

 

 

가중치 시각화

CNN은 여러 개의 필터를 사용해 이미지에서 특징을 학습한다.

각 필터는 커널이라 부르는 가중치와 절편을 가지고 있다.

 

가중치의 경우 입력 이미지를 2차원으로 적용하면 어떤 특징을 크게 두드러지게하는데, 그림으로 보면

 

색이 있는 공간은 값이 높고, 아니면 낮을 것이다.

 

이전에 학습했던 데이터를 불러오자 checkpoint 파일 불러오기

from tensorflow import keras
model = keras.models.load_model('best-cnn-model.h5')
model.layers

[<keras.layers.convolutional.conv2d.Conv2D at 0x24c86ab22b0>,
 <keras.layers.pooling.max_pooling2d.MaxPooling2D at 0x24c86a9c220>,
 <keras.layers.convolutional.conv2d.Conv2D at 0x24cfacd7580>,
 <keras.layers.pooling.max_pooling2d.MaxPooling2D at 0x24cfacd4be0>,
 <keras.layers.reshaping.flatten.Flatten at 0x24cfabec670>,
 <keras.layers.core.dense.Dense at 0x24cfabe01c0>,
 <keras.layers.regularization.dropout.Dropout at 0x24cfb2fe400>,
 <keras.layers.core.dense.Dense at 0x24cfabfe760>]

conv = 모델의 첫 번째 층 : 첫 번째 CNN ( 필터 32개, 커널 사이즈 3x3 )

conv의 가중치의 첫 번째 원소와 두번 째 원소의 크기를 출력해보자.

첫 번째 원소 = 가중치

두 번째 원소 = 절편

conv = model.layers[0]
print(conv.weights[0].shape, conv.weights[1].shape)

(3, 3, 1, 32) (32,)

커널의 크기 3x3이므로 3,3,1이 커널의 크기로 출력되었고, 필터 개수 32개 임을 확인할 수 있다.

절편은 (32,) ==> 필터마다 1개의 절편 존재하므로 32개의 상수가 있음을 확인할 수 있다.

 

conv_weights = conv.weights[0].numpy()
print(conv_weights.mean(), conv_weights.std())

-0.025013467 0.2464292

가중치 배열의 평균 / 표준편차를 계산

0에 가깝고 표준편차는 0.27이다.

 

import matplotlib.pyplot as plt
plt.hist(conv_weights.reshape(-1,1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()

균일해보이지 않다!!!

 

 

이제 훈련 전의 데이터의 가중치를 살펴보자.

 

not_training = keras.Sequential()
not_training.add(keras.layers.Conv2D(32,kernel_size=3, activation = 'relu', 
                                    padding='same', input_shape=(28,28,1)))

not_training_conv = not_training.layers[0]
print(not_training_conv.weights[0].shape)

(3, 3, 1, 32)

 

평균 / 표준편차를 출력해보자.

not_training_weights = not_training_conv.weights[0].numpy()

print(not_training_weights.mean(),not_training_weights.std())

-0.003274826 0.08374746

아까처럼 가중치의 배열을 그림으로 표현해보자.

plt.hist(not_training_weights.reshape(-1,1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()

 

import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import warnings
from sklearn.model_selection import GridSearchCV
from xgboost import XGBClassifier
import warnings
warnings.simplefilter(action= 'ignore', category=FutureWarning)

오늘은 부스팅이 해보고 싶어서 XGB Classifier를 이용하여 분류해보고자 한다.

하이퍼파라미터탐색은 GridSearch를 이용하겠다.

 

전처리된 데이터를 준비하고,

train = pd.read_csv('1차 최종 train.csv')
test = pd.read_csv('test.csv')

 

혹시 모르니 info()를 통해 타입을 살펴보자.

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8125 entries, 0 to 8124
Data columns (total 13 columns):
 #   Column        Non-Null Count  Dtype
---  ------        --------------  -----
 0   Unnamed: 0    8125 non-null   int64
 1   HomePlanet    8125 non-null   int64
 2   CryoSleep     8125 non-null   int64
 3   Cabin         8125 non-null   int64
 4   Destination   8125 non-null   int64
 5   Age           8125 non-null   int64
 6   VIP           8125 non-null   int64
 7   RoomService   8125 non-null   int64
 8   FoodCourt     8125 non-null   int64
 9   ShoppingMall  8125 non-null   int64
 10  Spa           8125 non-null   int64
 11  VRDeck        8125 non-null   int64
 12  Transported   8125 non-null   int64
dtypes: int64(13)

 

이제 input과 target 데이터로 나누자.

train_input = train.drop('Transported',axis=1)
train_target = train['Transported']
train_input.info()

 

[ 하이퍼 파라미터 탐색 / 튜닝]

params = {'learning_rate':(0.01,1),
          'gamma':(0,1),
          'max_depth':(3,10),
          'sub_sample':(0.5,1),
          'min_child_weight': (1, 5),
          'colsample_bytree':(0.1,1),
          'n_estimators' : (50,200)}

gs = GridSearchCV(XGBClassifier(random_state = 42), param_grid=params ,n_jobs=-1)

gs.fit(train_input, train_target)

[15:35:27] WARNING: C:/buildkite-agent/builds/buildkite-windows-cpu-autoscaling-group-i-0fc7796c793e6356f-1/xgboost/xgboost-ci-windows/src/learner.cc:767: 
Parameters: { "sub_sample" } are not used.

GridSearchCV(estimator=XGBClassifier(base_score=None, booster=None,
                                     callbacks=None, colsample_bylevel=None,
                                     colsample_bynode=None,
                                     colsample_bytree=None,
                                     early_stopping_rounds=None,
                                     enable_categorical=False, eval_metric=None,
                                     feature_types=None, gamma=None,
                                     gpu_id=None, grow_policy=None,
                                     importance_type=None,
                                     interaction_constraints=None,
                                     learning_rate=None, max_b...
                                     max_delta_step=None, max_depth=None,
                                     max_leaves=None, min_child_weight=None,
                                     missing=nan, monotone_constraints=None,
                                     n_estimators=100, n_jobs=None,
                                     num_parallel_tree=None, predictor=None,
                                     random_state=42, ...),
             n_jobs=-1,
             param_grid={'colsample_bytree': (0.1, 1), 'gamma': (0, 1),
                         'learning_rate': (0.01, 1), 'max_depth': (3, 10),
                         'min_child_weight': (1, 5), 'n_estimators': (50, 200),
                         'sub_sample': (0.5, 1)})
gs.best_params_

{'colsample_bytree': 1,
 'gamma': 1,
 'learning_rate': 0.01,
 'max_depth': 10,
 'min_child_weight': 5,
 'n_estimators': 50,
 'sub_sample': 0.5}

 

params = gs.best_params_
xgb = XGBClassifier(random_state=42, **params)
xgb.fit(train_input,train_target)
xgb.score(train_input,train_target)

[15:47:29] WARNING: C:/buildkite-agent/builds/buildkite-windows-cpu-autoscaling-group-i-0fc7796c793e6356f-1/xgboost/xgboost-ci-windows/src/learner.cc:767: 
Parameters: { "sub_sample" } are not used.

0.8016

 

그리드 서치를 통해 params의 범위를 지정 후 탐색 한 후 가장 높은 점수를 얻은 최적의

하이퍼 파라미터 조합을 사용해 XGB Classifier을 사용했다

예측 성공률은 80%가 나왔다.

처음 돌린것 치고 괜찮은것 같다.

 

XGB를 다시 사용하고,

 그리드서치를 베이지안 옵티마이저로 변경하여 하이퍼 파라미터만 바꿔서 다시 돌려보자.

from sklearn.model_selection import cross_val_score
# Define the hyperparameters and their ranges to be optimized
params = {'max_depth': (3, 10),
          'learning_rate': (0.01, 1),
          'n_estimators': (50, 200),
          'min_child_weight': (1, 5),
          'subsample': (0.5, 1),
          'gamma': (0, 1),
          'colsample_bytree': (0.1, 1)}

# Define the objective function to be optimized by Bayesian Optimization
def xgb_cv(max_depth, learning_rate, n_estimators, min_child_weight, subsample, gamma, colsample_bytree):
    xgb.set_params(max_depth=int(max_depth),
                   learning_rate=learning_rate,
                   n_estimators=int(n_estimators),
                   min_child_weight=int(min_child_weight),
                   subsample=subsample,
                   gamma=gamma,
                   colsample_bytree=colsample_bytree)
    return cross_val_score(xgb, train_input, train_target, cv=5, scoring='accuracy').mean()

# Define the Bayesian Optimization object
xgbBO = BayesianOptimization(xgb_cv, params)

# Optimize the hyperparameters using Bayesian Optimization
xgbBO.maximize(n_iter=50, init_points=5)

# Print the best hyperparameters and their corresponding score
print(xgbBO.max)

베이지안 옵티마이저 ( 하이퍼 파라미터 탐색 (서치)) 는 그리드와 다르게

함수를 따로 설정해줘야 하는것 같다. def xgb_cv :

 

이퍼 파라미터를 입력으로 가져가서 XGB 분류기의 교차 검증된 정확도 점수를 반환하는

"xgb_cv"라는 목적 함수를 정의한다.

목적 함수는 sikit-learn의 "cross_val_score" 함수를 사용하여 5배 교차 검증을 수행한다.

그런 다음 "xgbB"라는 베이지안 최적화 개체를 만든다."  목적 함수와 하이퍼 파라미터를 인수로 전달한다.

우리는 베이지안 최적화를 사용하여 하이퍼 파라미터를 최적화하기 위해

베이지안 최적화 객체의 "최대화" 방법을 부른다.

마지막으로 베이지안 최적화 개체의 "max" 속성을 사용하여 최상의 하이퍼 파라미터와 해당 점수를 print한다.

 

 탐색 과정 보기

|   iter    |  target   | colsam... |   gamma   | learni... | max_depth | min_ch... | n_esti... | subsample |
-------------------------------------------------------------------------------------------------------------
| 1         | 0.6103    | 0.6987    | 0.1225    | 0.1515    | 8.224     | 2.78      | 88.54     | 0.8106    |
| 2         | 0.5686    | 0.6152    | 0.4665    | 0.6057    | 4.602     | 4.858     | 83.59     | 0.7877    |
| 3         | 0.5669    | 0.6712    | 0.5453    | 0.9401    | 7.787     | 1.631     | 84.9      | 0.7387    |
| 4         | 0.5202    | 0.2059    | 0.7044    | 0.0881    | 7.868     | 2.342     | 148.1     | 0.916     |
| 5         | 0.5641    | 0.518     | 0.08341   | 0.9347    | 6.403     | 2.885     | 142.2     | 0.9287    |
| 6         | 0.7503    | 0.7642    | 0.2208    | 0.01476   | 7.224     | 2.787     | 89.72     | 0.7512    |
| 7         | 0.6741    | 0.6956    | 0.2691    | 0.04304   | 7.473     | 2.803     | 90.27     | 0.7354    |
| 8         | 0.6111    | 0.3772    | 0.1156    | 0.1722    | 6.862     | 2.909     | 89.46     | 0.5736    |
| 9         | 0.5589    | 0.996     | 0.3121    | 0.7178    | 6.272     | 1.441     | 197.7     | 0.9347    |
| 10        | 0.7438    | 1.0       | 0.2819    | 0.01      | 7.435     | 2.692     | 89.73     | 0.888     |
| 11        | 0.6028    | 0.935     | 0.7813    | 0.3266    | 7.275     | 2.008     | 89.35     | 0.9696    |
| 12        | 0.5586    | 0.8707    | 0.4239    | 0.9497    | 7.755     | 2.939     | 90.22     | 0.8634    |
| 13        | 0.7199    | 0.9006    | 0.3698    | 0.03016   | 7.438     | 2.105     | 90.24     | 0.5563    |
| 14        | 0.5978    | 0.6174    | 0.7405    | 0.2071    | 7.302     | 2.448     | 89.83     | 0.9597    |
| 15        | 0.6238    | 0.5315    | 0.3929    | 0.05831   | 5.405     | 1.407     | 139.3     | 0.9426    |
| 16        | 0.5623    | 0.9584    | 0.5933    | 0.7928    | 5.375     | 2.849     | 173.5     | 0.8334    |
| 17        | 0.5706    | 0.5806    | 0.2061    | 0.8379    | 8.009     | 3.695     | 96.87     | 0.5719    |
| 18        | 0.6091    | 0.969     | 0.6108    | 0.5672    | 8.644     | 4.891     | 123.8     | 0.7119    |
| 19        | 0.5499    | 0.4206    | 0.9338    | 0.9778    | 4.152     | 2.256     | 142.6     | 0.8824    |
| 20        | 0.6901    | 0.685     | 0.01052   | 0.03924   | 7.289     | 2.735     | 90.7      | 0.5096    |
| 21        | 0.6047    | 0.6388    | 0.07058   | 0.1224    | 6.596     | 2.135     | 90.52     | 0.6906    |
| 22        | 0.7596    | 1.0       | 0.05041   | 0.01      | 7.305     | 2.524     | 89.87     | 0.5584    |
| 23        | 0.5822    | 0.8653    | 0.4866    | 0.3237    | 7.976     | 1.363     | 90.84     | 0.9786    |
| 24        | 0.5978    | 0.9655    | 0.1968    | 0.2951    | 7.567     | 2.518     | 89.95     | 0.505     |
| 25        | 0.6135    | 0.7961    | 0.8492    | 0.1947    | 7.032     | 1.907     | 90.26     | 0.5117    |
| 26        | 0.5785    | 0.2114    | 0.9544    | 0.9256    | 6.561     | 4.046     | 93.02     | 0.9875    |
| 27        | 0.5724    | 0.3869    | 0.213     | 0.126     | 8.753     | 1.661     | 195.6     | 0.9378    |
| 28        | 0.6228    | 0.7242    | 0.4029    | 0.1495    | 6.745     | 3.011     | 90.32     | 0.6581    |
| 29        | 0.6012    | 0.6911    | 0.2479    | 0.5442    | 7.269     | 2.962     | 89.39     | 0.6084    |
| 30        | 0.5546    | 0.2324    | 0.2779    | 0.3053    | 7.547     | 2.212     | 90.49     | 0.5984    |
| 31        | 0.5579    | 0.8693    | 0.7293    | 0.5319    | 7.034     | 2.044     | 90.46     | 0.6849    |
| 32        | 0.7332    | 0.7667    | 0.3579    | 0.02248   | 7.386     | 1.447     | 89.73     | 0.8661    |
| 33        | 0.5915    | 0.9329    | 0.4795    | 0.7687    | 7.461     | 1.033     | 89.7      | 0.9438    |
| 34        | 0.5662    | 0.2785    | 0.8665    | 0.6453    | 8.654     | 2.789     | 160.3     | 0.7509    |
| 35        | 0.6073    | 0.7983    | 0.1568    | 0.4487    | 6.283     | 3.771     | 160.9     | 0.886     |
| 36        | 0.521     | 0.1814    | 0.7958    | 0.09026   | 9.277     | 4.778     | 134.8     | 0.8938    |
| 37        | 0.6261    | 0.4929    | 0.6858    | 0.05002   | 7.517     | 1.226     | 89.75     | 0.9839    |
| 38        | 0.5979    | 0.6657    | 0.7642    | 0.2697    | 6.881     | 1.312     | 89.71     | 0.6174    |
| 39        | 0.7503    | 0.9457    | 0.07201   | 0.01      | 7.16      | 2.515     | 89.88     | 0.8681    |
| 40        | 0.7575    | 1.0       | 0.0       | 0.01      | 7.228     | 2.885     | 89.85     | 0.7391    |
| 41        | 0.6132    | 0.7363    | 0.1436    | 0.3085    | 7.023     | 2.021     | 89.61     | 0.5638    |
| 42        | 0.624     | 0.891     | 0.06212   | 0.1219    | 7.22      | 3.854     | 90.19     | 0.883     |
| 43        | 0.5767    | 0.7601    | 0.25      | 0.2714    | 7.582     | 1.709     | 89.91     | 0.9229    |
| 44        | 0.614     | 0.3562    | 0.5391    | 0.214     | 7.684     | 3.257     | 91.44     | 0.5431    |
| 45        | 0.6106    | 0.6325    | 0.3444    | 0.1795    | 7.056     | 2.479     | 91.33     | 0.6597    |
| 46        | 0.7557    | 0.8779    | 0.2409    | 0.0188    | 5.539     | 4.755     | 93.72     | 0.755     |
| 47        | 0.6015    | 0.5372    | 0.0837    | 0.1466    | 7.103     | 2.961     | 89.7      | 0.9337    |
| 48        | 0.581     | 0.9351    | 0.1166    | 0.4818    | 9.327     | 1.28      | 78.47     | 0.5416    |
| 49        | 0.5762    | 0.599     | 0.5343    | 0.6791    | 4.135     | 4.219     | 50.72     | 0.7866    |
| 50        | 0.609     | 0.3763    | 0.9039    | 0.2149    | 5.726     | 4.672     | 79.75     | 0.7045    |
| 51        | 0.5929    | 0.37      | 0.6496    | 0.7564    | 4.512     | 3.833     | 190.0     | 0.6005    |
| 52        | 0.6017    | 0.9499    | 0.5243    | 0.5982    | 5.635     | 4.218     | 94.03     | 0.5944    |
| 53        | 0.6108    | 0.8254    | 0.2186    | 0.1845    | 6.988     | 2.436     | 90.58     | 0.6205    |
| 54        | 0.5652    | 0.5436    | 0.9611    | 0.901     | 8.289     | 3.423     | 79.72     | 0.751     |
| 55        | 0.5876    | 0.7708    | 0.5102    | 0.7385    | 5.594     | 3.223     | 153.1     | 0.5488    |
=============================================================================================================
{'target': 0.7596307692307692, 'params': {'colsample_bytree': 1.0, 'gamma': 0.050406580268173413, 'learning_rate': 0.01, 'max_depth': 7.305157214715147, 'min_child_weight': 2.5238667272883433, 'n_estimators': 89.87424334757735, 'subsample': 0.5583965007292178}}
params = xgbBO.max['params']
params['max_depth'] = int(round(params['max_depth']))
params['n_estimators'] = int(round(params['n_estimators']))
params['gamma'] = int(round(params['gamma']))
params['min_child_weight'] = int(round(params['min_child_weight']))
xgb = XGBClassifier(random_state = 42, **params)
xgb.fit(train_input, train_target)



XGBClassifier(base_score=None, booster=None, callbacks=None,
              colsample_bylevel=None, colsample_bynode=None,
              colsample_bytree=1.0, early_stopping_rounds=None,
              enable_categorical=False, eval_metric=None, feature_types=None,
              gamma=0, gpu_id=None, grow_policy=None, importance_type=None,
              interaction_constraints=None, learning_rate=0.01, max_bin=None,
              max_cat_threshold=None, max_cat_to_onehot=None,
              max_delta_step=None, max_depth=7, max_leaves=None,
              min_child_weight=3, missing=nan, monotone_constraints=None,
              n_estimators=90, n_jobs=None, num_parallel_tree=None,
              predictor=None, random_state=42, ...)

score를 출력해보자.

xgb.score(train_input, train_target)	

0.8045538461538462

 

'부록' 카테고리의 다른 글

Resnet 논문 리뷰  (0) 2024.04.22
Style GAN & Style GAN2  (1) 2024.04.07
LSTM  (0) 2023.08.02
[딥러닝] 옵티마이저 [퍼온 글]  (0) 2023.03.06
Bayesian Optimization  (2) 2023.02.21

+ Recent posts