본문 바로가기

AI

Chapter 3. 신경망

본 글은 "바닥부터 시작하는 딥러닝" 책을 바탕으로 작성되었습니다.


3.1 퍼셉트론에서 신경망으로 

3.1.1 신경망의 예

- 은닉층은 사람의 눈에 보이지 않는다.

- 본 책에서는 입력층은 0층, 은닉층은 1층, 출력층은 2층이라고 부른다.

그림1. 신경망의 예 (출처: https://velog.io/@chandni_ml/%EC%A7%80%EC%8B%9C%EB%94%A5-3.-%EC%8B%A0%EA%B2%BD%EB%A7%9D)

3.1.2 퍼셉트론 복습

- 기존 퍼셉트론에서 편향을 그림으로 명시한다면 그림2와 같다.

- 그림2에서는 가중치가 b이고, 입력이 1인 뉴런이 추가되었다.

- 편향의 입력 신호는 항상 1이다.

그림2. 편향을 명시한 퍼셉트론

- 그림 2를 좀 더 간결한 형식의 수식으로 작성하면 그림3, 4와 같다.

  이는 0을 넘으면 1을 출력하고 그렇지 않으면 0을 출력하는 조건 분기의 동작을 하나의 함수로 나타내고,
  이를 h(x)라고 표현한다.

- 입력 신호의 총합을 h(x)함수를 거쳐 y의 출력이 되고, h(x)함수는 입력이 0을 넘으면 1 아니면 0을 반환한다.

그림3. 입력 신호의 총합
그림4. h(x) 함수

 

3.1.3 활성화 함수의 등장

- 위 h(x) 함수와 같이, 입력 신호의 총합을 출력 신호로 변환해주는 함수를 "활성화 함수"라고 한다.

- 활성화 함수는 입력 신호의 총합이 활성화를 일으키는지 정하는 역할을 한다.

- 그림 3의 수식은 2단계로 처리하는데, 이는 그림5의 수식과 같다.

그림5. 2단계 처리

- 그림 5의 식을 그림으로 표현하면 그림 6과 같다.

- 그림 5에서 a는 입력 신호의 총합, h()는 활성화 함수, y는 출력이다.

그림 6. 활성화 함수의 처리 과정

3.2 활성화 함수

3.2.1 시그모이드 함수

- 신경망에서는 활성화 함수로 시그모이드 함수를 이용하여 신호를 변환하고, 그 변환된 신호를 다음 뉴런에게 전달한다.

그림 7. 시그모이드 함수

3.2.2 계단 함수 구현하기

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

- 아래 코드는 계단함수를 단순히 구현한 것이며, 인수 x에는 실수(부동소수점)만 들어갈 수 있다.
   따라서, 인수 x에는 넘파이 배열이 들어갈 수 없는 코드이다.

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

- 넘파이 배열도 인수x에 들어가기 위해, 아래와 같이 numpy를 이용하여 코드를 구현할 수 있다.

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

- 위에서 사용한 numpy와 같이, numpy배열에 부등호 연산을 수행하면 각각 부등호 연산이 수행되어 bool 배열이 생성된다

>>> import numpy as np
>>> x = np.array([-1.0, 1.0, 2.0])
>>> x
array([-1.,  1.,  2.])
>>> y = x > 0
>>> y
array([False,  True,  True])

- 계단 함수는 0이나 1의 int형을 출력하는 함수이기 때문에, 배열 y의 원소를 bool에서 int형으로 변환시켜준다.

   ( True는 1, False는 0으로 변환된다. )

- 참고로 책에서는 np.int로 진행하는데, 실제 코드 실행 시 int64나 int32를 지정해야 형변환이 가능하였다.

>>> y = y.astype(np.int64)
>>> y
array([0, 1, 1])

3.2.3 계단 함수의 그래프

- 계단 함수는 0을 경계로 출력이 0에서 1로 바뀐다. 

import numpy as np
import matplotlib.pylab as plt

def step_func(x):
    return np.array(x > 0, dtype=np.int64)

x = np.arange(-5.0, 5.0, 0.1)
y = step_func(x)


plt.plot(x, y)
plt.ylim(-0.1, 1.1) #y축 범위 지정
plt.show()

그림 8. 계단 함수의 그래프

3.3 다차원 배열의 계산 + 3.4 3층 신경망 구현하기

해당 내용은 정리한 것이 날라가서, 패쓰 ㅎㅎ

 

3.5 출력층 설계하기

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

- 보통 회귀는 항등 함수를, 분류는 소프트 맥스 함수를 사용한다.

   (분류: 데이터가 어느 클래스에 속하는지 분류 / 회귀 : 입력 데이터에서 수치를 예측)

3.5.1 항등 함수와 소프트맥스 함수 구현하기

- '항등함수'는 입력 그대로 출력한다.

- 소프트맥스 함수를 간단히 구현해보면 아래와 같다.

그림 9. 소프트맥스

-  소프트맥스 함수를 파이썬으로 코드를 구현하면 아래와 같다.

import numpy as np

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

    return y

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

- 3.5.1에서 구현한 소프트맥스 파이썬 코드는 오버플로 문제가 발생할 위험이 있다.

- 소프트맥스 코드에서 사용한 지수함수는 큰 수를 반환할 수도 있기 때문에, 오버플로 문제가 발생할 수 있다.

- 그림 10은 위에서 설명한 소프트맥스의 오버플로 예시로, nan은 not a number라는 의미이다.

그림 10. 소프트맥스 오버플로 문제

- 그림 11과 같이, 입력값에서 최대값을 빼주면 오버플로 문제 없이 계산이 성공적으로 가능하다.

그림 11. 소프트맥스 문제 해결방안

- 위 해결점을 보완한 소프트맥스 코드는 아래와 같다.

import numpy as np

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

3.5.3 소프트맥스 함수의 특징

- softmax 함수를 이용하여, 신경망 출력은 아래와 같이 할 수 있다.

- 그림 12와 같이, 소프트맥스 함수의 출력은 0 ~ 1.0 사이의 실수이며, 출력의 총합은 1이다. (중요한 성질)

- 위 성질을 통해 소프트맥스 함수의 출력을 확률로 해석할 수 있으며, 신경망을 이용한 분류에서는 가장 큰 확률의 값만 인식한다.

그림 12. 소프트맥스를 이용한 신경망 출력

3.6. 손글씨 숫자 인식

-  이번 절에서는 이미 학습된 매개변수를 사용하여 학습 과정을 생략하고 추론 과정만 구현한다.

- 위 추론 과정을 "신경망의 순전파"라고 한다.

3.6.1 MNIST 데이터셋

- MNIST는 손글씨 숫자 이미지 집합으로, 이번 예제에서 데이터셋으로 사용한다.

- MNIST는 0~9까지 숫자 이미지로 구성되어 있고, 훈련 이미지 60000장, 시험 이미지 10000장 준비되어 있다.

- 이미지 데이터는 28x28 크기의 회색조 이미지(채널1)이며, 각 픽셀은 0~255까지 값을 취한다.

- 각 이미지에는 실제 의미하는 숫자가 레이블로 붙어 있다.

 

- 데이터셋 다운로드: https://github.com/WegraLee/deep-learning-from-scratch.git

   (dataset 디렉터리를 작업 디렉터리의 부모 디렉터리에 clone 해주면 된다.)

- 아래 코드는 dataset의 mnist.py에서 load_mnist함수를 임포트하여, MNIST 데이터셋을 읽어온다.

- load_mnist함수는 3가지 인수를 받아오는데, 
  1) normalize
      : 입력 이미지 픽셀 값 정규화 (True는 0.0 ~ 1.0 사이의 값 / False는 원래 값인 0 ~ 255 값)

   2) flatten

      : 입력 이미지를 1차원 배열로 생성 (True는 784개 원소로 이뤄진 1차원 배열 / False는 1x28x28의 3차원 배열) 

   3) one_hot_label

      : one-hot encoding 형태로 저장할지에 대한 여부 (True는 one-hot encoding / False는 숫자 형태로 저장)

- load_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)

그림 13. 데이터셋 다운로드

 

 

- load_mnist 함수의 인자값으로 flatten을 True로 했기 때문에, 이미지가 1차원 넘파일 배열로 저장되어 있다.

  따라서, 이미지 표시할 때는 reshape()를 이용하여 원래 크기인 28x28로 다시 변형해준다.
  (flatten을 False로 할 경우, 이미지의 채널까지 같이 출력되므로 계산하기 편하게 1차원 넘파이 배열로 받는 것이다.)

- 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 반환해야 하는데, 이는 Image.fromarray()로 바꿔준다. 

import numpy as np
import sys, os
sys.path.append(os.pardir)
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)

print(img.shape)
img = img.reshape(28, 28) #원래 이미지이 모양으로 변형
print(img.shape)

img_show(img)

그림 14. MNIST 이미지 하나 출력

3.6.2 신경망의 추론 처리

- 아래는 MNIST 데이터셋을 가지고 추론을 수행하는 신경망을 구현하는 코드이다.

- 구현하는 신경망은 입력층 뉴런 784개, 출력층 뉴런 10개로 구성된다.

- 입력층 뉴런이 784인 것은 이미지 크기가 28x28=784이기 때문이고, 출력층은 0~9까지의 숫자를 구분하기 위해서이다.

- 은닉층은 2개로, 첫번째 은닉층은 50개 뉴런을 배치하고 두번재 은닉층에는 100개의 뉴런을 배치한다.
   (여기서 50, 100은 임의로 정한 값이다.)

- sample_weight.pkl에는 학습된 가중치와 편향 매개변수가 딕셔너리 변수로 저장되어 있다.
   (다운로드: https://github.com/WegraLee/deep-learning-from-scratch/tree/master/ch03)

import numpy as np
import pickle
import sys, os
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
from PIL import Image

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

- 위 코드를 통해, 신경망에 의한 추론을 수행하고 정확도(분류가 얼마나 올바른가) 확인을 진행한다.

- predict() 함수는 각 레이블의 활률을 넘파이 배열로 반환하고, np.argmax()함수에서 넘파이 배열에서 가장 큰 확률의 원소 인덱스를 구한다. 해당 인덱스는 예측 결과로, 정답 레이블과 비교하고 정확도를 체크한다.

- 결과는 0.9352로 93.52%의 정확도인 것을 알 수 있다.

- load_mnist 함수의 인수인 normalize를 True를 설정함으로써, 0~255 픽셀 값을 0.0 ~ 1.0 범위로 변환한다.

  입력 이미지 데이터에 대한 전처리 작업으로 정규화를 수행 예시이다.

- 데이터를 특정 범위로 변환하는 처리를 "정규화"라고 하며, 신경망의 입력 데이터에 특정 변환을 가하는 것을 "전처리"라고 한다. 

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

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

그림 15. 추론 정확도

3.6.3 배치 처리

- 위 코드에서 가중치에 대해 확인해보면, 다차원 배열의 대응하는 차원의 원소 수가 일치하는 것을 알 수 있다.

- 원소 784개로 구성된 1차원 배열이 마지막에는 원소 10개인 1차원 배열을 출력한다.

- 하나로 묶은 입력 데이터를 "배치"라고 한다.

- 아래는 이미지 100개 묶어, predict()함수에 한번에 넘기는 코드이다. 이는 x의 형상이 100x784로 바뀌어 100장 분량 데이터를 하나의 입력 데이터로 표현된다.

x, y = 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)))

'AI' 카테고리의 다른 글

Building Systems with the ChatGPT API  (0) 2024.08.03
Chapter 6. 학습 관련 기술들  (0) 2024.07.13
Chapter 5. 오차역전파법  (0) 2024.06.30
Chapter 4. 신경망 학습  (0) 2024.06.23
Chapter2. 퍼셉트론  (1) 2024.06.17