밑바닥부터 시작하는 딥러닝
1권 3장 - 신경망
신경망이란?
이미지 출처: 위키백과
신경망은 입력층, 은닉층, 출력층으로 이루어져 있다. 각각의 노드에서 다음 노드로 갈 때의 과정은 다음과 같다.
- 두 노드에 각각 가중치(w1, w2)를 곱하고 편향(b)을 더한다.
- 위에서 나온 값에 활성화 함수를 적용시켜서 최종적인 값을 출력한다.
활성화 함수라는 새로운 개념이 등장했다.
활성화 함수
입력 신호의 총합을 출력 신호로 변환하는 함수를 활성화 함수라고 한다. 이름이 말해주듯 활성화 함수는 입력 신호의 총합이 활성화를 일으키는지를 정하는 역할을 한다.
앞에서 배웠던 단순한 퍼셉트론은 출력이 0 아니면 1로만 이루어졌다. 계단 함수(step function)가 바로 이것과 같은 함수인데, 퍼셉트론은 계단 함수를 활성화 함수로 사용하는 신경망이라고 볼 수 있다.
활성화 함수의 종류
계단 함수
- 입력이 0을 넘으면 1을 출력하고, 그 외에는 0을 출력하는 함수
- 그래프가 계단처럼 생겼기 때문에 이러한 이름이 붙여졌다.
그래프
코드
def step(x):
if x > 0:
return 1
else:
return 0
numpy 배열도 지원하기 위해 함수를 다음과 같이 변경했다.
def step(x):
return np.array(x > 0, dtype=int)
시그모이드 함수
- 신경망에서 자주 이용하는 활성화 함수
- 계단 함수와 다르게 0과 1 사이의 중간값이 부드럽게 연결되어 있다.
그래프
코드
def sigmoid(x):
return 1 / (1 + np.exp(-x))
ReLU 함수
- 시그모이드 함수는 신경망 분야에서 오래전부터 이용해왔으나, 최근에는 ReLU 함수를 주로 이용한다.
- ReLU 함수는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하이면 0을 출력한다.
그래프
코드
def ReLU(x):
return np.maximum(0, x)
비선형 함수
위의 함수는 모두 비선형 함수다. 출력이 입력의 상수배만큼 변하는 함수를 선형 함수라고 하고, 직선 1개로 표현된다. 하지만 위의 함수들은 직선 1개로 표현할 수는 없기 때문에 비선형 함수로 분류된다.
신경망에서는 활성화 함수로 비선형 함수를 사용해야 한다. 선형 함수를 사용하면 신경망을 여러 층으로 구성하는 이점을 살릴 수 없다.
출력층 설계하기
위에서 살펴본 활성화 함수는 신경망 중간에 위치한 은닉층에서 사용하는 함수들이다. 신경망의 마지막 단계인 출력층에서는 다른 함수를 사용한다.
신경망은 분류와 회귀 모두에 이용할 수 있다. 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수가 달라진다.
일반적으로 회귀에는 항등 함수(identity function)를, 분류에는 소프트맥스 함수(softmax function)를 사용한다.
항등 함수
항등 함수는 입력값 그대로 출력하는 함수다.
def identity(x):
return x
소프트맥스 함수
소프트맥스 함수의 분자는 입력 신호의 지수 함수, 분모는 모든 입력 신호의 지수 함수의 합으로 구성된다.
하지만 만약 위의 식대로 구현한다면, 지수 함수가 아주 큰 값을 내뱉으므로 오버플로 문제가 생길 수 있다. 따라서 실제로 구현할 때는 식의 분모와 분자에 각각 x의 최댓값을 빼준 뒤 계산한다.
def softmax(x):
x = x - np.max(x) # 오버플로 대책
return np.exp(x) / np.sum(np.exp(x))
소프트맥스 함수의 출력은 항상 0에서 1 사이의 실수이고, 출력의 총합은 1이다. 이 점은 소프트맥스 함수의 중요한 성질로, 덕분에 출력을 확률로 해석할 수 있다.
다음의 예처럼 소프트맥스 함수는 각각의 입력에 대한 확률이 출력된다.
x = np.array([0.3, 2.9, 4.0])
print(softmax(x))
# [ 0.01821127. 0.24519181, 0.73659691 ]
다만 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않는다. 신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식한다. 따라서 신경망으로 분류할 때는 출력층의 소프트맥스 함수는 생략해도 된다. 현업에서도 지수 함수 계산에 드는 자원 낭비를 줄이고자 출력층의 소프트맥스 함수는 생략하는 것이 일반적이다.
손글씨 숫자 판별하기 실습
3장에서는 이미 학습된 매개변수를 이용한 추론 과정만 구현한다. 이 추론 과정을 신경망의 순전파라고 한다.
데이터셋
MNIST라는 손글씨 숫자 이미지 데이터를 사용한다.
데이터셋은 위처럼 0부터 9까지의 숫자 이미지로 구성되고, 각각의 이미지에 대응되는 숫자가 레이블로 제공된다.
실습 코드
테스트 데이터를 불러오고 미리 학습된 매개변수를 이용해서 추론하는 과정이다. 데이터를 불러오는 load_mnist 함수에서 normalize 인수를 True로 설정했다. 이렇게 하면 0~255 범위인 각 픽셀의 값을 0~1 범위로 변환한다.
이처럼 데이터를 특정 범위로 변환하는 처리를 정규화라 하고, 신경망의 입력 데이터에 특정 변환을 가하는 것을 전처리라 한다.
여기서는 입력 이미지 데이터에 대한 전처리 작업으로 정규화를 수행한 것이다.
# 숫자 판별
import sys, os
sys.path.append(os.pardir)
import pickle
import numpy as np
from dataset.mnist import load_mnist
from vitalize_func import *
# 데이터셋 불러오기
def get_data():
# (훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블)
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=True)
return x_test, t_test
# 이미 학습된 매개변수 불러오기
def init_network():
# 현재 실행 중인 파일의 절대경로
script_dir = os.path.dirname(os.path.abspath(__file__))
# 파일의 경로
file_path = os.path.join(script_dir, "sample_weight.pkl")
with open(file_path, "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(f"정확도: {accuracy_cnt / len(x)}")
# 정확도: 0.9352
배치 처리
데이터를 입력할 때 여러 개를 한 번에 묶어서 보내면 처리 시간을 대폭 줄일 수 있다. 이때 하나로 묶은 입력 데이터를 배치라고 한다.
다음은 위의 과정을 배치 처리를 이용해서 구현한 코드이다. (바뀐 부분은 아래와 같다)
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) # 100x10 행렬의 각 행마다 확률이 가장 높은 원소의 인덱스들을 배열로 반환한다
accuracy_cnt += np.sum(p == t[i : i + batch_size])
print(f"정확도: {accuracy_cnt / len(x)}")
# 정확도: 0.9352
이렇게 데이터를 배치로 처리함으로써 더 효율적이고 빠르게 처리할 수 있다.