[Deep Learning 2] 활성화 함수와 순방향 신경망(MNIST 활용)
퍼셉트론에서 신경망으로
지난 장에서의 퍼셉트론은 단지 입력층과 출력층만이 존재하는 단순한 네트워크였다. 그런데 그것만으로는 컴퓨터를 구성하기 어렵다.
다층 퍼셉트론으로 (이론적으로는) 컴퓨터까지도 만들 수 있다는 것을 안 이상, 더 복잡한 퍼셉트론을 만들고 싶지 않은가?(싫으면 말구…)
이제 본격적으로 퍼셉트론의 입력층과 출력층 사이에 중간 계산층인 은닉층(hidden layer)을 넣음으로써 다층 퍼셉트론, 즉 인공신경망(artificial neural network)을 구성해보자.
기존의 퍼셉트론은 n차원의 실수공간에서 1차원의 실수공간으로 매핑되는 실함수(real-valued function)에 불과했다.
그러나 이제부터 다룰 신경망은 n차원의 실수 입력이 m차원의 실숫값으로 출력이 되는 벡터 함수(vector function)를 사용하게 된다. 어려워할 필요 없이, 그냥 입력도 여러개 출력도 여러개인 함수라고 생각하면 된다.
그리고 이런 벡터 함수꼴의 은닉층들을 여러개 갖고 있는 (인공)신경망은 벡터 함수의 합성 함수꼴로 구성된다고 생각하면 쉽다(쉽겠지?)
신경망은 조지 시벤코의 ‘범용 근사 정리(universal approximation theorem)’에 따라 원하는 n차원의 연속 함수로 근사가 가능하고, 따라서 복잡성과 확장성이 좋은 모델이다.
활성화 함수
이번 장에서 다룰 활성화 함수(activation function)는 각 뉴런(은닉층의 노드)에서 나온 값을 다음 노드로 넘겨주기 전에 거치는 단계이다. 한 계산 값을 다음 노드로 넘기기 전에, 미리 일정 값으로 전처리를 해서 넘겨주자는 방식.
활성화 함수는 대부분 비선형(nonlinear)인데, 그 이유는 선형(linear) 함수를 사용하면 은닉층을 아무리 늘려도(깊게해도) 은닉층이 없는 것과 다름 없는 결과가 나오기 때문이다.
계수가 아무리 곱해진들, 또 다른 상수 계수 하나를 취하는 것과 다르지 않기 때문.
활성화 함수는 종류가 매우 다양한데, 지난 장에서 ANN을 최초로 고안한 매컬러와 피츠가 사용했던 활성화 함수부터 현재의 함수까지 톺아보자.
현대에 사용하지 않은 활성화 함수
바로 위에서 말한 매컬러와 피츠의 함수가 바로 이것이다. 흔히 계단 함수(step function)이라고 부른다.
이 함수가 안 좋은 점은 미분값이 항상 0이라는 지점이다. 나중에 역전파 알고리즘이라는 것에 대해 배우는데, 이때 해당 모델을 개선하기 위해서는 각 값을 미분하여야 한다.
# 기존 퍼셉트론에서 쓰인 활성화함수인 계단 함수를 구현
import numpy as np
def step_function(x):
return np.array(x > 0, dtype=int)
# x에 배열을 받을 수 있도록 유연하게 바꿈
# y > 0; return y.astype(int)로도 가능
매우 쉽게 파이썬으로 구현이 가능하다.
그런데 이 계단함수는 (미분불가능한 지점을 제외하고는) 미분값이 항상 0이 나오기 때문에 최적의 파라미터를 찾기 어렵다. 이를 대체하고자 나온 것이 시그모이드 함수이다.
시그모이드 계열
시그모이드 함수(sigmoid function)는 말그대로 sigmoid(:S자 모양의) 함수로 다음과 같이 정의된다. 경우에 따라 로지스틱(logistic) 함수라고도 부른다.
# 신경망에서 쓰였던 활성화함수인 시그모이드 함수를 구현
def sigmoid(x):
return 1 / (1 + np.exp(-x))
이 시그모이드 함수는 일단 미분이 되기 때문에 흔히 이진분류의 경우에 사용된다. 또한 치역이 0~1 범위에서 존재하기 때문에 스쿼싱(squashing: 값을 지정된 고정 범위로 변환하는 기능)에도 좋다.
그러나 위의 그림에서 Diminishing Gradient Zone이라고 써있듯이, 양 끝으로 갈 수록 미분계수가 0에 가까워져 딥러닝 학습의 면에서 부적절하기도 하다.
Gradient Saturation
(이를 그레이디언트 포화(gradient saturation)라고 한다. 여기서 포화는 '앞의 것(입력값)을 아무리 변화시켜도 뒤의 것(출력값)이 일정 한도에서 머무르는 일'을 말한다. 표준국어대사전 '포화' 6의2번)또한 치역이 양수에만 존재하기 때문에 학습 속도가 느려진다는 단점도 있다. 이를 보완하기 위해 등장한 것이 하이퍼볼릭 탄젠트(tanh: hyperbolic tangent) 함수이다. (3년 만의 재회)
이 함수는 함숫값이 -1~+1 범위에 존재하여 최적화 과정이 비교적 효율적이고, 또 시그모이드 함수를 선형변환(linear transformation)하여 도출할 수도 있다.
바꿔 말하면 시그모이드 함수의 그레이디언트 포화를 여전히 극복하지 못한다는 것. 그래서 새로 등장한 것이 ReLU 계열이다.
ReLU 계열
ReLU(Rectified Linear Unit: 렐루)는 말 그대로 ‘정류선형’ 함수로서 선형함수를 일정부분 비활성화해둔 모양이다.
# 신경망에서 최근 쓰이는 활성화함수인 ReLU 함수
def relu(x):
return np.maximum(0, x)
음의 정의역 부분에서 0이 나오는 것을 보완한 릭키 ReLU, PReLU, ELU 등의 변형도 있다.
그 외
이외에도 다중 분류(multinomial classification)에 쓰이는 소프트맥스 함수가 있다.
소프트맥스(softmax) 함수는 사진과 같이 여러 클래스에 속할 ‘확률’을 구하는 함수이다. 즉 각 함숫값의 총합은 1이 되겠다.
# 소프트맥스 함수(오버플로우 방지 개선)
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
그런데 확률을 표기할 거면 그냥 각 원소를 다 합치면 되지, 왜 지수를 포함시켜서 합치게 된 것일까?
앞서 탄젠트 하이퍼볼릭이 시그모이드의 변형이듯, 이 소프트맥스도 시그모이드의 변형이다. 시그모이드를 일반화한 꼴이다보니 자연상수 e가 포함된 것
이외에도 맥스 아웃이나 Swish, 항등 함수도 활성화 함수가 될 수 있다.
3층 신경망 구현
우리가 임의로 가중치와 신경망의 너비 및 깊이를 모두 정해서 하나의 3층 신경망을 구현해보자. 도식은 아래와 같다.
# 최종 구현
def init_network(): # 네트워크 초기화
network = {} # 딕셔너리로 정의
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
network['b3'] = np.array([0.1, 0.2])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
return network
def forward(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 = identity_function(a3)
return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y) # [0.31682708 0.69627909]
신경망으로 MNIST 처리
책에 나온 것을 코랩에서 실행하려고 노력했다. 하지만 항상 Error: 403이 표출..
해결하려고 별의 별 짓을 다 해봤다. 직접 케라스에서 데이터셋을 다운 받기도 해보고, 스택 오버플로우에 있는 코드도 복붙해보고. 로컬에다가 아나콘다랑 가상머신을 돌려서 처리해보려고도 하고…
그런데 오류가 발생한 지점은 애석하게도 MNIST 데이터셋을 갖고 있는 url이었다.. 이 책이 도서관에서 빌려온 거라 7년 전 초판3쇄 발행본이다보니, 현재의 바뀐 링크가 아니어서 403이 떴던 것..
진짜 대여섯 시간동안 고민했는데 이렇게 단순한 문제였다니.. 앞으론 책 버전에 유의하며 코딩해야겠다!
PS: 나중에 보니 옮긴이의 깃허브에 공지로 url이 잘 안 될 거라고 쓰여있었다..
원래 딥러닝 모델은 너비, 깊이 및 파라미터를 직접 찾아가면서 해야하지만, 이번 장에서는 미리 주어진 피클 값으로 대체한다.
추가로 모델을 개선하기 위해 스케일링(크기 조정)을 할 때는 너비나 깊이, 해상도만 늘리기 보단 3개를 동시에 늘리는 ‘컴파운딩 스케일링’이 유리하다!
Colab에서 MNIST 작업하기
이하의 설명은 케라스 등의 라이브러리에서 데이터셋을 다운받는 것이 아닌, 책의 깃허브 링크에 존재하는 mnist.py 및 피클(pkl) 파일을 통해 진행한 것이다.
일단 책의 mnist.py 코드와 sample_weight.pkl 파일을 내가 작업 중인 구글 드라이브의 폴더로 갖고 온다.
또한 이전 포스팅에서 작성한 sigmoid()와 softmax() 함수를 가져온다. 전체적인 밑작업 코드는 다음과 같다.
import numpy as np
import sys, os # 구글 드라이브와 연동용
import pickle # 필요한지는 모르겠는데 일단 써놓음(안전빵)
import requests # 피클이 인식이 안돼서 부득이하게 추가
sys.path.append('/content/drive/<여기에 주소 입력>') # 내가 저장한 구글 드라이브 위치
from PIL import Image # MNIST 이미지 확인용
from matplotlib.pyplot import imshow # 원래 이걸로 확인해야하는데 안되더라?
from mnist import load_mnist # 책의 깃헙 링크에서 가져온 mnist.py 임포트
def sigmoid(x): # 활성화 함수 1
return 1 / (1 + np.exp(-x))
def softmax(a): # 활성화 함수 2
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
먼저 load_mnist()로 MNIST 데이터셋을 가져온다.
flatten은 3차원(12828) 원본 데이터를 1차원(784)으로 압축시키는 여부, normalize는 전처리로 정규화(0~1 사이로 만듦)할 건지 여부, one_hot_label은 원-핫 인코딩을 할 지 여부이다.
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(x_test.shape) # (10000,)
이후 깔쌈하게 이미지 하나를 봐보자. MNIST가 정확히 들어왔음을 육안으로 확인할 수 있다.
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()
img = x_train[0]
label = t_train[0]
print(label) # 5
print(img.shape) # (784,)
img = img.reshape(28, 28) # 1차원 압축본을 2차원으로 변환
print(img.shape) # (28, 28)
img_show(img) # 원래 이 코드로 돼야 하는데 안돼서
imshow(img) # 맷플롯립의 imshow()로 시각화 대체
요렇게 뜨면 잘 되고 있는거다
이제 피클(pickle)을 통해 빠르게 MNIST 객체를 불러올 건데, 이상하게 이게 또 안됐다. 그래서 부득이하게 링크로부터 파일을 받아봐 저장 후, 다시 여는 방식으로 사용했다.
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
# 수정한 부분: pickle 파일을 깃헙에서 다운받은 후 다시 엶
def init_network():
url = "https://github.com/WegraLee/deep-learning-from-scratch/raw/master/ch03/sample_weight.pkl"
response = requests.get(url) # 깃헙에서 다운받은 후
with open("sample_weight.pkl", 'wb') as file: # sample_weight.pkl로 저장 후
file.write(response.content)
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 # 카운트 +1
print("Accuracy:" + str(float(accuracy_cnt) / len(x))) # 0.9352
배치 처리(일괄 처리)로 코드 개선
우리가 다루는 데이터는 array, 즉 어쨌든 행렬이기 때문에 굳이 데이터를 하나씩 입력할 필요는 없다.
행렬의 곱의 특성상 맨 앞 행렬의 ‘행’ 성분은 보존되기 때문에, 배치(batch)로 처리하면 계산 과정이 줄어 더 빠르다.
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] # 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))) # 0.9352
출처: [밑바닥부터 시작하는 딥러닝 Chapter 3]
참고: [Do it! 딥러닝 교과서 p.44~82]