밑바닥 딥러닝

Ch3-2 출력층 설계, 실습

임재환 2023. 5. 17. 14:22

출력층 설계하기

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

일반적으로 회귀문제에는 항등 함수를, 분류문제에서는 소프트맥스함수를 사용한다.

 

번외로)

시그모이드 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 / 전체 이미지 수 로 나눠서 정확도를 구하는 것이다.