본문 바로가기

Deep Learning(강의 및 책)/밑바닥부터 시작하는 딥러닝

3. 신경망

728x90

밑바닥부터 시작하는 딥러닝을 바탕으로 제작하였습니다.

 

3.1. 신경망이란

 ■ 신경망이란?

그림과 같이 퍼셉트론을 여러층 쌓아 만든 하나의 비선형 분류기를 의미합니다. 맨 왼쪽 줄을 입력층, 맨 오른쪽 줄을 출력층, 중간층을 은닉층이라 합니다. 은닉층의 뉴런은 사람 눈에 보이지 않기 때문에 은닉층이라는 이름을 가집니다.

은닉층과 출력층은 입력 신호(x)와 가중치(w)의 곱에 편향(b)을 더하고 나온 모든 값들의 합을 가공하여 출력신호로 만드는 함수를 사용합니다. 이러한 함수를 활성화 함수라 합니다.

 

 

 

 

 

 

 

3.2. 활성화 함수

 ■ 활성화 함수란?

활성화 함수는 입력 신호를 통해 출력 신호를 얻어 그 값을 유용한 값으로 변환시켜주는 함수를 의미합니다.

왼쪽 그림을 보면 은닉층에서 출력층으로 전달되는 a라는 값을 y로 변환시켜주는 함수를 활성화 함수입니다.

퍼셉트론에서의 활성화 함수는 계단 함수(step function)를 사용했습니다.

 

 

 

 

 

 

 

 

 

 

■ 계단 함수 구현 및 그래프 그리기

계단 함수를 구현해보겠습니다.

import numpy as np
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)  # y축의 범위 지정
plt.show()

실행결과

이와 같이 계단 함수를 구현하고 그래프를 그릴 수 있습니다. 계단 함수는 계단 모양과 같이 0을 기준으로 0과 1로 구분됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

■ sigmoid 함수 구현 및 그래프 그리기

sigmoid함수는 이와 같은 식으로 표현됩니다.

 

 

 

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

X = np.arange(-5.0, 5.0, 0.1)
Y = sigmoid(X)
plt.plot(X, Y)
plt.ylim(-0.1, 1.1)
plt.show()

실행결과

이와 같이 sigmoid 함수를 구현하고 그래프는 이와 같이 그릴 수 있습니다. 계단 함수와 형태는 비슷하지만 차이가 있다면 비선형 함수라는 것입니다.

이는 상당히 큰 차이인데 계단 함수는 0 또는 1의 값만 y에 전달하지만 sigmoid 함수는 0부터 1 사이의 전체 값을 y에 전달합니다.

 

 

 

 

 

 

 

 

 

신경망 내에서는 계단 함수와 같은 선형 함수가 아닌 sigmoid와 같은 비선형 함수를 사용해야 합니다. 선형 함수는 사용해서는 안됩니다. 왜냐면 선형 함수를 이용하면 신경망의 층을 깊게 하는 의미가 없기 때문입니다. 예를 들어

h(x) = cs 라는 식을 활성화 함수로 사용한 3층 네트워크가 존재한다면 y(x) = h(h(h(x)))가 됩니다. 이 계산은 y(x) = c*c*c*x처럼 곱셈을 그냥 세 번 수행한 식이고 이는 결국 y(x) = ax와 같은 식입니다. 즉, 은닉층이 ㅇ벗는 네트워크로 표현할 수 있습니다. 이러한 이유로 우리는 비선형 함수를 활성화 함수로 사용해야 합니다.

 

■ ReLU(Rectified Linear Unit) 함수 구현 및 그래프

최근에 자주 사용되는 함수로 입력이 0을 넘으면 그 입력을 그대로 출력해주고 0 이하인 경우 0을 출력하는 함수입니다.

import numpy as np
def relu(x):
    return np.maximum(0, x)

x = np.arange(-5.0, 5.0, 0.1)
y = relu(x)
plt.plot(x, y)
plt.ylim(-1.0, 5.5)
plt.show()

실행결과

이와 같이 ReLU 함수를 구현할 수 있고 그래프를 그릴 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3.3. 다차원 배열의 계산

 ■ 다차원 배열

숫자가 한 줄로 늘어선 것이나 직사각형으로 늘어놓은 것, 3차원으로 늘어놓은 것이나 N차원으로 나열하는 것을 통틀어 다차원 배열이라고 합니다. 코드를 통해 1차원 배열과 2차원 배열, 3차원 배열을 보겠습니다.

import numpy as np
A = np.array([1,2,3,4])
print(A)
print(A.ndim)
print(A.shape)
A = A.reshape(2,2)
print(A)
print(A.ndim)
print(A.shape)
A = np.arange(9).reshape(1,3,3)
print(A)
print(A.ndim)
print(A.shape)

실행결과

이와 같이 작성된 것을 볼 수 있습니다. np.ndim을 통해 배열의 차원 수를 확인할 수 있습니다. 또 배열의 형상은 shape를 통해 알 수 있습니다. 이렇게 1차, 2차, 3차원 배열의 형태를 알아봤습니다.

 

 

 

 

 

 

 

 

 

 

 ■ 행렬의 곱

행렬(2차원 배열)의 곱을 구하는 방법에 대해서 알아보겠습니다. 식의 형태는

이와 같이 작성됩니다. 여기서 중요한 부분은 n이 통일되어야 한다는 점입니다.

 

코드를 통해 행렬의 곱을 살펴보겠습니다.

import numpy as np
A = np.array([[1,2],[3,4]])
B = np.array([[5,6],[7,8]])
print(np.dot(A,B))
A = np.array([[1,2,3],[4,5,6]])
B = np.array([[1,2],[3,4],[5,6]])
print(np.dot(A,B))

실행결과

이렇게 행렬의 곱이 계산되는 것을 볼 수 있습니다. 넘파이 함수 np.dot()을 통해 행렬 곱을 계산할 수 있습니다.

 

 

 

 ■ 신경망에서 행렬곱

이러한 신경망이 존재할 때 X와 가중치의 곱을 Y로 전달하는데 이를 행렬곱으로 구현하면 단번에 결과 Y를 구할 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

3.4. 3층 신경망 구현하기

이와 같은 신경망을 구현하겠습니다. 입력층은 2개, 첫 번째 은닉층은 3개, 두 번째 은닉층은 2개, 출력층은 2개의 뉴런으로 구성됩니다.

우리는 활성화 함수로 sigmoid 함수를 사용할 것입니다.

 

 

 

 

 

 

 

 

 

 

첫 번째 은닉층에 들어오는 값을 구현해보겠습니다.

import numpy as np
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(X.shape)
print(W1.shape)
print(B1.shape)

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

실행결과

가중치와 입력신호, 편향을 위와 같이 설정하고 A1을 행렬 곱으로 구한 결과입니다. 이 값은 첫 번째 은닉층의 값입니다.

이렇게 구한 첫 번째 은닉층에서 구한 값을 sigmoid함수를 통해 활성화 함수에 적용시킨 후 그다음 은닉층으로 가중치와 곱해지고 편향을 더해 전달됩니다.

 

이에 대한 구현을 해보면

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

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

print(Z1.shape)
print(W2.shape)
print(B2.shape)

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

실행결과

Z1은 A1을 sigmoid에 적용하고 얻은 값이고 이를 두 번째 은닉층으로 전달되는 과정을 볼 수 있고 A2를 구하는 것을 볼 수 있습니다. A2를 sigmoid 함수에 적용해 구한 값은 Z2가 될 것입니다. 다시 Z2를 가중치와 편향을 통해 계산을 한 값이 Y가 될 것입니다

 

 

 

3.5. 출력층 설계하기

신경망은 분류와 회귀 모두에 이용할 수 있습니다. 다만 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수가 달라집니다. 일반적으로 회귀에서는 항등 함수를, 분류에서는 소프트맥스 함수를 사용합니다.

 ■ 항등 항수뫄 소프트맥스(Softmax) 함수 구현하기 

항등 함수와 소프트맥스 함수는 위에 그림과 같이 그릴 수 있습니다. 항등 함수는 값을 그대로 전달하는 역할을 합니다. 소프트맥스 함수에 대한 식을 살펴보면

이와 같습니다. 하지만 컴퓨터에서 이를 사용하기엔 문제가 있습니다. 소프트맥스 함수는 지수 함수를 사용하는데, 지수 함수는 상당히 큰 값을 뱉을 수 있고 이로 인해 overflow가 일어날 수 있습니다. 이런 문제를 해결하기 위해서

 

이러한 식을 사용합니다. 이 식이 말하는 것은 스포트맥스의 지수 함수를 계산할 때 어떤 정수를 더해도 혹은 빼도 결과가 변하지 않는다는 것입니다. 우리는 overflow를 막기 위해 이 식을 사용하는 것이기 때문에 주로 C에는 입력 신호 중 최댓값을 이용하는 것이 일반적입니다.

이를 바탕으로 소프트맥스 함수를 구현해보면

 

 

 

 

 

def sofmax(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.0 사이의 실수입니다. 그리고 출력의 총합은 1입니다. 이러한 특징 때문에 소프트맥스 함수의 출력을 '확률'로 볼 수 있습니다. 예를 들어 y[0]의 값이 0.74라면 74%라고 해석할 수 있습니다.

 

3.6. 손글씨 숫자 인식

신경망의 구조를 배웠으니 실전 예에 적용을 해보겠습니다. 손글씨 숫자 분류에 대해서 구현을 할 것이고 학습 과정은 생략하고 추론 과정만 구현할 것입니다. 이러한 추론 과정을 신경망의 순전파(forward propagation)라고도 합니다.

 

 ■ MNIST 데이터셋

MNIST는 기계학습 분야에서 아주 유명한 데이터셋으로 손글씨 숫자 이미지 집합입니다. 0부터 9까지의 숫자 이미지로 구성되어 있으며 28X28 크기의 회색조 이미지(1채널)로 이루어져 있으며 훈련 이미지가 60000장, 시험 이미지가 10000장 준비되어 있습니다. 우리는 이 이미지를 1차원 배열(X = [x1, x2, x3, ... , x784])로 바꿔서 다룰 것입니다.

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)

실행결과

위에 코드를 보면 load_mnist라는 함수를 사용해 mnist 데이터셋을 불러온 것을 볼 수 있습니다.

load_mnist 함수는 읽은 MNIST 데이터를 (훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블) 형식으로 반환합니다. 그렇기 때문에 각 변수의 shape가 실행결과와 같습니다.

인수로는 flatten, normalize, one_hot_label 세 가지가 선언되어 있는데 첫 번째 인수인 flatten은 이미지를 평탄하게, 즉 1차원 배열로 만들지 설정해주는 인자입니다. 여기서는 True였기 때문에 이미지가 784개의 원소로 이루어진 1차원 배열로 변경되었습니다.

normalize는 이미지의 정규화를 의미하는데 입력 이미지의 픽셀 값을 0.0 ~ 1.0 사이의 값으로 정규화할지를 정하는 인자입니다. False이기 때문에 원래 값 그대로 0~ 255 사이의 값을 유지합니다.

one_hot_label은 레이블을 원-핫 인코딩 형태로 저장할지를 정합니다. 원-핫 인코딩이란 예를 들어 [0,0,1,0,0,0,0,0,0,0]처럼 정답을 뜻하는 원소만 1이고(hot하고) 나머지는 모두 0인 배열입니다. 만약 one_hot_label이 False인 경우 '7'이나 '2'와 같이 숫자 형태의 레이블을 저장합니다.

 

 ■ MNIST 이미지 출력

이번에는 PIL(Python Image Library) 모듈을 사용해 이미지를 표시하겠습니다.

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image


def img_show(img):
    pil_img = Image.fromarray(np.uint8(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)  # 5

print(img.shape)  # (784,)
img = img.reshape(28, 28)  # 형상을 원래 이미지의 크기로 변형
print(img.shape)  # (28, 28)

img_show(img)

실행결과

이와 같이 이미지가 출력된 것을 볼 수 있습니다. 여기서 우리가 유의할 점은 flatten을 True로 설정했기 때문에 이미지가 1차원 배열로 저장이 되었기 때문에 이미지를 출력하기 위해서는 reshape를 통해 다시 28X28 형상의 2차원 배열로 변경해줘야 합니다. 또한 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야 하며, 이 변환은 Image.fromarray()가 수행합니다.

 

 

 ■ 신경망의 추론 처리

우리가 구현할 신경망은 입력층 뉴런을 784개, 출력층 뉴런을 10개로 구성할 것입니다. 입력층이 784개의 뉴런인 이유는 이미지 크기가 28X28 이기 때문이고, 출력층 뉴런이 10개인 이유는 0부터 9까지의 숫자를 구분하는 문제이기 때문입니다. 은닉층은 총 두 개로, 첫 번째 은닉층은 50개의 뉴런을, 두 번째 은닉층에는 100개의 뉴런을 배치할 것입니다. 작업을 처리해줄 세 함수 get_data(), init_network(), predict()를 구현하고 정확도를 측정해보겠습니다.

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


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(a2)
    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)))

실행결과

정확도가 93.52%인 것을 볼 수 있습니다. 일단 구현한 함수들에 대해서 보겠습니다.

 

get_data() 함수는 이름 그대로 데이터(test 데이터, 10000장)를 받아오는 역할을 하는 함수입니다. init_network() 함수는 pickle 파일인 sample_weight.pkl에 저장된 학습된 가중치 매개변수를 읽어옵니다. 이 파일에는 가중치와 편향 매개변수가 딕셔너리 변수로 저장되어 있습니다. 마지막으로 predict() 함수는 받아온 가중치와 편향을 설정해 예측을 하는 함수입니다.

마지막으로 정확도를 측정하는 부분을 보겠습니다. test data들의 이미지를 한 장씩 반복문을 통해 읽어오면서 predict함수에 전달해 결괏값을 return 받아 y에 저장합니다. y는 10개의 뉴런으로 이루어져 있으며 그중 가장 높은 값을 가진 인덱스를 p에 저장을 하고 이 p가 정답 t와 같은지 확인을 해 같은 경우 accuracy_cnt를 1씩 증가시켜줍니다. 이러한 과정을 거쳐 구한 accuracy_cnt를 총 데이터 수로 나눠주면 정확도가 측정되는 코드입니다.

 

 ■ 배치(batch) 처리

입력 데이터를 하나가 아니라 여러 개를 하나의 묶음으로 전달할 수도 있는데 여기서 이 묶음을 배치(batch)라 합니다. 위에 코드에서는 10000장의 이미지를 1장씩 신경망에 넣었지만 이를 100장씩 묶어서 신경망에 넣어도 됩니다. 이러한 배치 처리는 컴퓨터로 계산할 때 큰 이점을 줍니다. 이미지 1장당 처리 시간을 대폭 줄일 수 있습니다. 왜냐면 수치 계산 라이브러리 대부분이 큰 배열을 효율적으로 처리할 수 있도록 고도로 최적화되어 있기 때문이고 또 하나의 이유는 커다란 신경망에서는 데이터 전송이 병목으로 작용하는 경우가 자주 있는데 배치 처리를 함으로써 버스에 주는 부하를 줄일 수 있기 때문입니다.

마지막으로 배치 처리를 구현해 정확도를 측정해보겠습니다.

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


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(a2)
    a3 = np.dot(z2, w3) + b3
    y = softmax(a3)

    return y


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

batch_size = 100 # 배치 크기
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
    x_batch = x[i:i+batch_size]
    y_batch = predict(network, x_batch)
    p = np.argmax(y_batch, axis=1)
    accuracy_cnt += np.sum(p == t[i:i+batch_size])

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

실행결과

배치 처리를 했어도 정확도는 같은 결과를 보입니다. 오히려 처리 속도가 더 빨라졌습니다.

 

위 코드에서 배치 크기를 100으로 설정했고 반복문을 보면 0부터 10000까지를 100 만큼의 간격으로 반복문을 진행하고 이미지도 100개씩 받아 신경망에 넣어주는 것을 볼 수 있습니다. 그로 인해 예측은 100X10 크기로 y에 저장됩니다. axis=1을 통해 각 이미지마다 가장 큰 인덱스를 p에 저장하는 형태입니다. p에는 100개의 이미지에 대한 가장 큰 인덱스들이 저장되어 있습니다. 이렇게 예측된 p와 실제 정답과 같다면 accuracy_cnt가 증가되고 정확도는 위와 같이 측정됩니다.

'Deep Learning(강의 및 책) > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

6. 학습 관련 기술들  (0) 2022.02.15
5. 오차역전파법  (0) 2022.02.14
4. 신경망 학습  (0) 2022.02.07
2. 퍼셉트론(perceptron)  (0) 2022.02.06
1. 파이썬(Python) 기초  (0) 2022.02.06