CNN ( convolutional neural network)은 이미지 인식과 음성 인식 등 다양한 곳에서 사용된다.

convolutional = 나선형이라는 뜻이 먼저 나오지만, 딥러닝에서 CNN은 합성곱이라는 의미로 쓰인다.

 

CNN의 핵심 키워드는 다음과 같다.

  • Convolution(합성곱)
  • 채널(Channel)
  • 필터(Filter)
  • 커널(Kernel)
  • 스트라이드(Strid)
  • 패딩(Padding)
  • 피처 맵(Feature Map)
  • 액티베이션 맵(Activation Map)
  • 풀링(Pooling) 레이어

7.1 전체 구조

CNN의 네트워크 구조를 살펴보면 크게 합성곱 계층과 풀링 계층으로 되어 있다. 지금까지 본 신경망은 인접하는 계층의 모든 뉴런과 결합되어 있다. 이를 완전연결이라고 하며, 

 

완전 연결된 계층을 Affine 계층이라 했었다.

Affine계층을 사용한 5층 완전연결 신경망은 아래 그림과 같다.

Affine-ReLu 조합의 층이 4개, 마지막 출력층에서는 Softmax 활성화함수를 이용되는 것을 볼 수 있다.

 

다음은 CNN의 예시이다.   (일반적인 CNN구조라고한다.)

Affine-ReLu연결이    Conv-ReLu-Pooling 연결로 바뀌었다고 생각하면 편하다.

또 주의할 점은 출력에 가까운 층에는 Affine-ReLu 구성의 연결을 사용할 수 있다는 점이다!! 

 

7.2 합성곱 계층

CNN에서는 또한 합성곱 계층, 풀링 계층 의외  패딩 / 스트라이드의 개념이 새로 등장한다.

 

7.2.1 완전연결 (Affine) 계층의 문제점

7장 이전에 배웠던 Affine 계층에서는 인접하는 계층의 뉴런이 모두 연결되고 출력 수를 임의로 정할 수 있었다.

 

위 문장을 토대로 완전연결계층의 문제점은 무엇일까? 

바로 데이터의 형상 (차원)이 무시된다는 점이다.

 

입력 데이터가 이미지라고 한다면, 3차원 데이터일 것이다. (세로, 가로 ,채널(색))

하지만 이 데이터를 완전 계층에 입력하려면 1차원으로 평탄하게 해주어야 한다.

 

하지만 3차원 데이터는 그 자체로 중요한 정보를 갖고 있다.

예를 들면 공간적으로 가까운 픽셀은 값이 비슷하거나, RGB 각 채널은 서로 밀접하게 관련되어 있는 등 패턴이 숨겨져 있을 수 있다. 하지만 이를 1차원 데이터로 변환 시키는 순간 이 정보들을 살릴 수 없게 된다.

 

CNN에서는 합성곱 계층의 입출력 데이터를 특징맵 ( feature map ) 이라고도 한다.

따라서 합성곱 계층의 입력 데이터를 입력 특징맵 , 출력 데이터를 출력 특징맵 이라고 한다.

이 책에서는 입출력 데이터특징맵 을 같은 의미로 사용할 것이기 때문에 주의해서 해석을 하도록 한다.!!!

 

7.2.2 합성곱 연산

 합성곱 계층에서는 합성곱 연산을 처리하는데, 이는 이미지 처리에서 말하는 필터 연산에 해당한다. 

필터 연산에 대해 구체적인 예시를 통해 알아보자.

위 그림을 보면 입력 데이터에 필터(=커널, 도장) 를 적용하는 것을 볼 수 있다.

입력 데이터는 4x4, 필터는 3x3이므로 도장을 총 네번 찍어서 2x2(네 번의 도장 값)의 출력 데이터를 출력한다. 

 

자세한 곱 연산은 다음과 같다.

도장안의 각 값들은 가중치라고 볼 수 있고, 편향을 추가한다면 다음과 같은 수식을 얻을 수 있다.

7.2.3 패딩

합성곱 연산을 수행하기 전에 입력 데이터 주변 값을 특정 값 (or 0)으로 채우는 것을 패딩이라고 한다.

7.2.4 스트라이드

스트라이드 (stride)란 필터(=커널, 도장)을 적용하는 위치의 간격을 말한다.

 

간격이 2이면 도장이 2칸씩 이동하는 것을 말한다.

 

7.2.5 3차원 데이터의 합성곱 연산

위에서 알아본 합성곱 연산은 데이터가 2차원일 때의 경우이다.

3차원 데이터의 합성곱 연산에 대해 알아보자. 데이터는 이미지라고 가정한 후 (세로,가로,색상)을 다룬다고 하자.

주의사항: 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다.

                 필터 고정

7.2.6 블록으로 생각하기

3차원의 합성곱은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다.

블록은 아래 그림과 같이 3차원이다.

위 그림은 출력 데이터가 한 장의 특징 맵이다.

한 장의 특징 맵은 곧 채널이 1개인 특징 맵이라는 말과 같다.

만약 합성곱 연산의 출력으로 다수의 채널을 출력하려면 어떻게 해야 할까?

 

 

->필터를 1개보다 더 사용하는 것이다.

FN은 필터의 개수를 의미하고 이에 따라서 출력 맵 또한 FN개가 생성된다.

 


여기에 편향까지 고려한다면 최종 흐름은 아래 그림과 같다.

7.2.3 배치 처리

7장 이전의 신경망 처리에서는 입력 데이터를 한 덩어리로 묶어서 배치로 처리했었다.\

CNN에서도 배치 처리를 적용시켜보자.

 

배치 처리를 하기 위해 n개의 데이터를 묶고자 한다면 데이터 차원 수가 하나 더 늘어나서 총 4차원으로 바뀐다.

( 데이터 수, 채널 수, 높이, 너비)

 

26.2.3 ReLu를 사용할 때의 가중치 초기값

지난 포스팅에서 본 Xavier 초기값은 활성화 함수가 선형인 것을 전제로 이끈 결과이다.

시그모이드 함수와 tanh(하이퍼볼릭 탄젠트) 함수는 좌우 대칭이기 때문에 중앙 부근이 선형인 함수로 볼 수 있다.

 

따라서 Xavier초기값을 사용하기에 적절하다.

 

하지만 ReLu함수를 사용할 때는 ReLu에 특화된 초기값을 권장한다. (not  xavier)

He 초기값

He 초기값은 앞 계층의 노드가 n개일 때, 표준편차가  √(2 /n) 인 정규분포를 사용한다.

(Xavier 초기값: 표준편차  1 / √n ) 

키워드 : 최적화 옵티마이저 (4가지), 가중치 매개변수 초기값, 드롭아웃 등

 

6.1 매개변수 갱신

신경망 학습의 목적은 손실 함수의 값을 최소화 할 수 있는 매개변수를 찾는 것.  == 매개변수의 최적값을 찾는 것

 

정리하면 신경망 학습의 목적은 곧 최적화이다. ( optimization )

지금까지 최적화의 방법으로 매개변수의 기울기를 이용했었다. (미분)

 

매개변수의 기울기를 구해서 기울어진 방향으로 매개변수의 값을 갱신하는 일을 계속 반복해서 최적의 값을 찾아나갔다.

이를 SGD (확률적 경사 하강법)이라고 한다. 이 챕터에서는 SGD의외의 다양한 최적화 방법들을 소개한다.

 

우선 SGD를 포함한 다양한 옵티마이저를 그림으로 보고 넘어가자.

6.1.2 확률적 경사 하강법 (SGD)

SGD를 다시 복습해보자.

위 수식에서 W는 가중치, ∂L/∂WW에 대한 손실 함수의 기울기이다. n(에타)은 학습률이다. (보통 0.01이나 0.001 사용)

SGD클래스를 구현해보자.

class SGD:
	def __init__(self,lr=0.01)
    	self.lr = lr
    def update(self, params, grads):
    	for key in params.keys():
        	params[key] -= self.lr * grads[key]

SGD클래스를 이용하면 신경망 매개변수의 진행을 다음과 같이 수행할 수 있다.

network = TwoLayerNet(...)
optimizer = SGD()

for i in range(1000):
	...
    x_batch, t_batch = get_mini_batch(...) #미니배치
    grads = network.gradient(x_batch, t_batch)
    params = network.params
    optimizer.update(params, grads)
    ...

위 코드처럼 optimizer = '...'로 지정하여 SGD의외 기법도 호출하여 편하게 사용할 수 있다.

 

6.1.3 SGD의 단점

6.1.3 SGD의 단점

6.1.4 모멘텀 ( Momentum )

모멘텀이란 운동량을 뜻하는 말로, 물리와 관계가 있다. 또한 수식으로는 다음과 같다.

n(에타) = 학습, v = 속도(기울기를 따라 이동하는 속도라고 생각)

∂L/∂W = 손실함수의 기울기,   W = 가중치

av 항은 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 한다고 한다.

 

class Momentum:
	def __init__(self, lr=0.01, momentum = 0.9):
    self.lr = lr
    self.momentum = momentum
    self.v = None
   	
    def update(self, params, grads):
    	if self.v is None:
        	self.v = {}
            for key, val in params.items():
            self.v[key] = np.zeros_like(val)
        
        for key in params.keys():
        	self.v[key] = self.momentum * self.v[key] - self.lr*grads[key]
            params[key] += self.v[key]    # 인스턴스 변수 v는 물체의 속도

모멘텀을 생각할 때는 공이 경사면을 따라 굴러간다고 생각하면 이해하기 쉽다.

또한 SGD와 비교했을 때 위아래 흔들림이 적다.

6.1.5 Adagrad

신경망 학습에서는 학습률 값이 중요하다. 너무 크거나 작으면 학습이 효과적으로 이루어지지 않는다.

이 학습률을 올바르게 설정하기 위한 몇 가지 기술 中, 학습률 감소가 있다.

학습률 감소란 처음에는 크게 학습하다가 진행하면서 조금씩 학습률을 점차 줄여나가는 방법이다.

 

학습률 감소에 대한 구체적인 설명은 다음 블로그를 참고하자.

https://velog.io/@good159897/Learning-rate-Decay%EC%9D%98-%EC%A2%85%EB%A5%98

 

Learning rate Decay의 종류

Michigan 대학의 CS231n 강의를 보고 Learning rate Decay에 대해 정리를 했습니다.

velog.io

학습률을 서서히 낮추는 가장 간단한 방법은 "전체"의 학습률을 값을 일괄적으로 낮추는 것이다.

이를 더욱 발전시킨 것이 AdaGrad이다.

 

Adagrad는 전체에서 발전해 "각각"의 매개변수에 맞게 갱신해준다.

'적응적 학슙를'을 기반으로 한 옵티마이저이다.

 

Adagrad, RMSprop = 적응적 학습률 기반

momentum이라는 개념을 이용하여 gradient를 조절

 

어떻게 개별 매개변수마다 갱신을 해줄까? 자세히 알아보자.

위 수식에서 h는 기존 기울기값을 제곱하여 계속 더해준다. 

그리고 매개변수를 갱신할 때 1/ √h를 곱해서 학습률을 조정한다.!!

이 뜻은 매개변수 원소 중에서 많이 움직이는 원소는 학습률이 낮아진다는 뜻이라고 볼 수 있다.

 

(다시 말해 학습률 감소가 매개변수 원소마다 다르게 작용됨.)

 

손코딩

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h in None:
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
        
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            parmas[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

주의) 마지막 줄에서 1e-7를 더해주는 부분. 1e-7은 self.h[key]에 0이 담겨 있어도 0으로 나누는 것을 막아줌.

Adagrad의 갱신경로를 그림으로 보자.

최소값을 향해 효율적인 움직임을 보여준다.!!

 

6.1.6 아담 ( Adam )

모멘텀은 공을 구르는 듯한 움직임을 보여준다. Adagrad는 원소 개별적으로 갱신해주었다.

이 둘을 합치면 Adam이라고 할 수 있다.

그림을 먼저 보자.

모멘텀과 비슷한 패턴이지만, 좌우 흔들림이 적은 것을 볼 수 있다.

이는 학습의 갱신 강도를 적응적으로 조정해서 얻는 benefit이라고 한다.

 

마지막으로 정리해서 봐보자.

무엇이 제일 좋다!! 라는 것은 데이터, 목적, 그 밖의 하이퍼 파라미터에 따라 다르기 때문에 정해진 것이 없다.

하지만 통상적으로 SGD보다 momentum,adam,adagrad 3개가 더 좋고, 일반적으로 Adam을 많이 쓴다고 한다.

 

6.2 가중치의 초기값

신경망 학습에서 가중치 초기값이 매우 중요하다고 한다.

2절에서는 권장 초기값을 배우고 실제로 학습이 잘 이루어지는지 확인해보자.

 

6.2.1 초기값을 0으로 하면?

이제부터 오버피팅을 억제해 범용 성능을 높이는 테크닉인 가중치 감소 기법을 소개해주겠다.

가중치 감소가중치 매개변수의 값이 작아지도록 학습하는 방법이다.

 

가중치 값을 작게 해서 오버피팅을 일어나지 않도록 하게 하는 것이다.

가중치 값을 작게 만들고 싶으면 초기값도 최대한 작은 값에서 시작하는 것이 정공법이기 때문에,,!

사실 지금까지 가중치 초기값은 0.01 * np.random.randn(10,100)처럼 정규분포에 생성된 값에 0.01배를 한 작은값을 사용

했었다.

 

그렇다면 가중치 초기값을 모두 0으로 설정하면 어떨까??

결론부터 말하자면, 아주 안좋은 생각이다.

실제로 가중치 초기값을 0으로 하면 학습이 올바로 이루어지지 않는다.

오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문이다.

 

예를 들어 2층 신경망에서 첫 번째, 두 번째 층의 가중치가 0이라고 가정해보자.

그럼 순전파 때는 입력층의 가중치가 0이기 때문에 두 번째 층의 뉴런에 모두 같은 값이 전달된다.

두 번째 층의 모든 뉴런에 같은 값이 입력된다는 것은 역전파  두 번 층의 가중치가 모두 똑같이 갱신된다는 말과 같다.

(곱셈 노드의 역전파를 기억해보자.)

그래서 가중치들을 같은 초기값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지하는 것이다.

이는 가중치를 여러 개 갖는 의미를 사라지게 한다.

 

이 '가중치가 고르게 되어버리는 상황'을 막으려면 초기값을 무작위로 설정해야 한다.

 

6.2.2 은닉층의 활성화값 분포

은닉층의 활성화값(활성화 함수의 출력 데이터)의 분포를 관찰하면 중요한 정보를 얻을 수 있었다.

 

이번 절에서는 가중치의 초기값에 따라 은닉층 활성화 값들이 어떤 분포를 갖는지 시각화 해보려한다.

예시로 5층의 신경망을 가정, 시그모이드 함수 사용, 입력데이터 무작위로 선정

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-1))
    
x = np.random.randn(1000, 100) # 1000개의 데이터
node_num = 100                 # 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5          # 은닉층이 5개
activations = {}               # 이 곳에 활성화 결과(활성화값)를 저장

fro i in range(hidden_layer_size):
    if i != 0:
        x = activations[i-1]
        
    w = np.random.randn(node_num, node_num) * 1
    
    a = np.dot(x, w)
    z = sigmoid(a)
    activations[i] = z

층은 5개, 각 층의 뉴런은 100개씩이다.

활성화 값들의 분포를 그려보자.

# 히스토그램 그리기
for i, a in activations.items():
    plt.subplot(1, len(activations), i+1)
    plt.title(str(i+1) + '-layer')
    plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

각 층의 활성화 값들이 0과 1에 치우쳐져 있다.

시그모이드 함수는 출력이 0또는 1에 가까워지면 미분값은 0에 수렴한다.

따라서 데이터가 0과 1에 치우쳐 분포하게 되면 역전파 기울기 값이 점점 작아지다가 사라진다.

이를 기울기 소실 현상 ( gradient vanishing )이라고 한다.

 

이제 표준편차를 0.01로 바꿔서 다시 그려보면 다음과 같은 그래프를 얻을 수 있다.

아까와 같은 기울기 소실 문제는 보이지 않지만, 모두 중간값에 몰빵되있는 것을 볼 수 있다.

이는 표현력 관점에서 문제라고 볼 수 있다.

 

Xavier 초기값

사비에르 초기값이란 사비에르가 작성한 논문에서 발췌한 것으로, 현재까지 딥러닝 프레임워크에서 많이 사용한다.

초기값 = 표준편차를 1 /√n 로 설정!   ( n = 노드 개수)

 

사비에르 초기값을 사용해서 분포를 보면 다음과 같다.

 

5.6 Affine / Softmax 계층 구현하기

5.6.1 Affine 계층 

Affine 변환 = 순전파에서 행렬,   

Affine 계층 = Affine 변환을 수행하는 처리

.신경망의 순전파에서는 가중치 총합의 신호를 계산하기 위해 행렬 곱을 사용했다 ( np.dot ) 

 

그리고 행렬 곱의 핵심은 원소 개수 (차원 개수)를 일치시키는 것이다.

 

예시를 통해 그림으로 이해해보자. (계산 그래프)

X,W,B가 있다고 하자. 각각 입력, 가중치, 편향이고  shape은 각각(2,) , (2,3), (3,)이다.

X와 W가 들어와 np.dot을 통해 행렬 곱 수행 -> x * W   -> ( x * W ) +B  = Y

 

이제 역전파를 알아보자.

아래 식이 도출되는것을 확인하자.

  WT는 T의 전치행렬을 말함. 

(전치행렬 = Wij 를 Wji로 바꾼것. 아래 그림 참조)

위 식을 통해서 계산 그래프의 역전파를 구해보자.

위 그림의 네모 박스 1,2를 유심히 살펴보자. (덧셈노드는 그대로 흐르고, 곱셈 노드는 교차하므로 !!)

X,W,B의 shape은 각각(2,) , (2,3), (3,)이다.

∂L/∂Y는 (3,) 인데 W인 (2,3)과 차원이 달라 곱셈이 되지 않는다. 

따라서 WT를 통해 (3,2)로 바꿔 곱셈을 수행할 수 있도록 변환 (+X도 마찬가지)

 

5.6.2 배치용 Affine 계층

지금까지 예시로 든것은 X (입력 데이터)가 하나일 때만 고려한 상황이였다.

하지만 현실에서는 하나일 확률이 매우 적으므로 실제 Affine 계층을 생각해보자.

 

입력 데이터가 N개일 경우이다.

5.6.3 Softmax-with-Loss 계층

마지막으로 배울 것은 출력층에서 사용하는 소프트맥스 함수이다.

  • 딥러닝에서는 학습과 추론 두 가지가 있다.
  • 일반적으로 추론일 때는 Softmax 계층(layer)을 사용하지 않는다. Softmax 계층 앞의 Affine 계층의 출력을 점수(score)라고 하는데, 딥러닝의 추론에서는 답을 하나만 예측하는 경우에는 가장 높은 점수만 알면 되므로 Softmax 계층이 필요없다. 반면, 딥러닝을 학습할 때는 Softmax 계층이 필요하다.

이제 소프트맥스 계층을 구현할텐데, 손실 함수인 엔트로피 오차도 포함하여 계산 그래프를 살펴보자.

너무 복잡하다. 간소화 해보자.

여기서는 3클래스 분류를 가정하고 이전 계층에서 3개의 입력(점수)를 받는다.

그림과 같이 Softmax 계층은 입력 a1,a2,a3를 정규화하여 y1,y2,y3를 출력함

Cross-entropy계층은 y1,y2,y3와 t1,t2,t3(정답)을 넘겨받고 손실 L를 출력함.

 

위 그림에서 역전파의 결과인 (y1-t1),(y2-t2),(y3-t3)를 보면 softmax계층의 출력 정답레이블의 차분임.

따라서 신경망에서의 역전파는 '출력 - 정답', 즉 오차를 출력한다고 확인할 수 있다.

파이썬으로Softmax-with-Loss를 구현해보자.

# Softmax-with-Loss 계층 구현
# common / layer.py
 
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 오차역전파법 구현하기

5.7.1 신경망 학습의 전체 그림

 

이제 구현해보자!!

5.7.2 오차역전파를 사용한 신경망 구현

import sys, os
sys.path.append('/deep-learning-from-scratch')
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict
 
 
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)
 
        # 계층 생성
        # OrderedDict = 순서가 있는 딕셔너리, 순서 기억
        # 순전파 때는 계층을 추가한 순서대로 / 역전파 때는 계층 반대 순서로 호출
        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

아래 표를 참고해서 위 코드를 이해합시다.

또한 위 코드에서 OrderDict주의 ( 순서가 있는 딕셔너리 ) 이를 통해 순전파, 역전파를 쉽게 사

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

  • 느린 수치 미분보다 오차역전파법을 사용한 해석적 방법이 효율적 계산 가능
  • 수치 미분은 오차역전파법을 정확히 구현했는지 확인위해 필요
  • 수치 미분은 구현하기 쉬우나, 오차역전파법은 구현 복잡해 실수가 있을 수 있음
  • 기울기 확인(gradient check) : 두 방식으로 구한 기울기가 일치(거의 같음)함을 확인 하는 작업
# 기울기 확인 (gradient check)
import sys, os
sys.path.append('/deep-learning-from-scratch')
import numpy as np
from dataset.mnist import load_mnist
from ch05.two_layer_net import TwoLayerNet
 
# 데이터 읽기
(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))
    
>>>
W1:4.1821647831167817e-10
b1:2.534937764494963e-09
W2:5.183343681548899e-09

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

import sys, os
sys.path.append('/content/drive/MyDrive/deep-learning-from-scratch')
 
import numpy as np
from dataset.mnist import load_mnist
from ch05.two_layer_net import TwoLayerNet
 
# 데이터 읽기
(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)
        
>>>
0.16111666666666666 0.1669
0.9040833333333333 0.9045
0.9236666666666666 0.9269
0.93625 0.9373
0.94525 0.944
0.9503833333333334 0.948
0.9547166666666667 0.951
0.9602166666666667 0.9569
0.9626333333333333 0.9588
0.9652166666666666 0.9598
0.9688 0.9619
0.9709833333333333 0.9641
0.9729 0.9653
0.9746166666666667 0.9667
0.97505 0.9663
0.97645 0.967
0.9784833333333334 0.9692

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

Ch 6-2 학습 관련 기술들  (0) 2023.05.31
Ch 6-1 학습 관련 기술들  (0) 2023.05.29
Ch 5-1 오차 역전파 ( Error Backpropagation )  (0) 2023.05.27
Ch4-2 신경망 학습  (0) 2023.05.26
Ch4-1 신경망 학습  (0) 2023.05.17

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