<데이터 사이언스/Data Science> 2. 신경망
데이터 사이언스

<데이터 사이언스/Data Science> 2. 신경망

노션에서 작성한 노트를 HTML로 가져오기 때문에 형식이 어색할 수 있습니다.
!잘 보이지 않으시면 오른쪽 밑의 달 모양 버튼을 클릭해주세요!

신경망:

가중치 매개변수의 적절한 값을 데이터로부터 자동으로 학습하는 능력을 지님

은닉층의 뉴런은 겉으로 드러나 있지 않음

활성화 함수:

입력 신호의 총합을 출력 신호로 변환하는 함수

y=h(b+w1x1+w2x2)h(x)={1x>00x0y=h(b+w_1x_1+w_2x_2)\\ h(x) = \begin{cases} 1 & \text{$x \gt 0$}\\ 0 & \text{$x \le0$}\end{cases}

위의 식은 다음의 두 단계로 나눌 수 있음:

  1. a=b+w1x1+w2x2a = b + w_1x_1 + w_2x_2
  1. y=h(a)y = h(a)

계단 함수: 임계값을 경계로 출력이 바뀜

단층 네트워크에서 사용

def step_function(x):
	if x > 0:
		return 1
	else:
		return 0
------------------------
def step_function_numpy(x):
	y = x > 0
	return y.astype(np.int)

시그모이드 함수(Sigmoid Function): y=11+exy = \frac{1}{1+e^{-x}}

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

계단 함수 vs. 시그모이드 함수

공통점:

비선형 함수

출력이 0에서 1 사이

입력이 작아지면 출력은 0에 가깝고 입력이 커지면 출력이 1에 가까움

차이점:

시그모이드 함수는 부드러운 곡선이며 출력이 연속적으로 변함 + 실수 반환

계단 함수는 0을 경계로 출력이 바뀜 + 정수 반환

비선형 함수: (직선 한 개로 그릴 수 없는 함수 - 계단 함수도 비선형)

신경망에서는 비선형 함수를 활성화 함수로 사용 - 선형은 사용하지 않음

선형 함수은 활성화 함수로 사용할 경우 은닉층이 사라짐

h(x)=cxh(x)=cx를 활성화 함수로 사용한 3층 네트워크는  y(x)=h(h(h(x)))=c3x y(x) = h(h(h(x))) = c^3x이 됨

즉, 층을 쌓는 이점이 사라짐

ReLU 함수(Rectified Linear Unit): h(x)={xx>00x0h(x) = \begin{cases} x & \text{$x \gt 0$}\\ 0 & \text{$x \le0$}\end{cases}

*Rectified: "정류된", 정류 - 전기회로 용어로 -흐름을 차단한다고 생각하면 됨

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

행렬: (센솔님의 블로그 중 이산수학 카테고리 참고) - 밑은 Numpy 활용 코드 소개

import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
np.ndim(A) -> A가 몇차원 배열인지
[np.array].shape -> MxN 행렬에서 (M, N)을 반환
np.dot(A, B) -> 곱

표기법: *자료마다 다를 수 있으니 확인 필수

가중치: 예) w12(1)w^{(1)}_{12}

w다음층의N번째뉴런층의M번째뉴런(N층의가중치)w^{(N층의\hspace{1mm}가중치)}_{다음\hspace{1mm}층의\hspace{1mm}N번째\hspace{1mm}뉴런|앞\hspace{1mm}층의\hspace{1mm}M번째\hspace{1mm}뉴런}

은닉층 뉴런: 예)a1(1)a^{(1)}_1

a현재층의N번째뉴런(N층의뉴런)a^{(N층의\hspace{1mm}뉴런)}_{현재\hspace{1mm}층의\hspace{1mm}N번째\hspace{1mm}뉴런}

편향: 예) b1(1)b^{(1)}_1

b다음층의N번째뉴런N층의뉴런b^{N층의 \hspace{1mm}뉴런}_{다음\hspace{1mm}층의\hspace{1mm}N번째\hspace{1mm}뉴런}

신호 전달:

*σ()는 항등 함수로 입력을 그대로 출력함 - 위의 그림에서는 출력층의 활성화 함수로 사용됨

실행 식 예시:

a1(1)=w11(1)x1+w12(1)x2+b1(1)a^{(1)}_1 = w^{(1)}_{11}x_1+w^{(1)}_{12}x_2+b^{(1)}_1
A(1)=XW(1)+B(1)A(1)=(a1(1)a2(1)a3(1)),X=(x1x2),B(1)=(b1(1)b2(1)b3(1))A^{(1)} = XW^{(1)}+B^{(1)}\\ A^{(1)} = \begin{pmatrix}a^{(1)}_1&a^{(1)}_2&a^{(1)}_3\end{pmatrix},X=\begin{pmatrix}x_1&x_2\end{pmatrix},B^{(1)}=\begin{pmatrix}b^{(1)}_1&b^{(1)}_2&b^{(1)}_3\end{pmatrix}
W(1)=(w11(1)w21(1)w31(1)w12(1)w22(1)w32(1))W^{(1)}=\begin{pmatrix} w^{(1)}_{11}&w^{(1)}_{21}&w^{(1)}_{31}\\ w^{(1)}_{12} & w^{(1)}_{22} & w^{(1)}_{32}\end{pmatrix}
# 위의 그림의 식 구현 #

# 1. 입력층에서 1층(첫 번째 은닉층)으로 온 신호 처리
X = np.array([x1, x2])
W1 = np.array([
								[w11, w21, w31], 
								[w12, w22, w32]
																])
B1 = np.array([b1, b2, b3])
A1 = np.dot(X, W1) + B1
Z1 = sigmoid(A1) # 시그모이드 함수는 예시로 사용

# 2. 1층에서 2층으로 온 신호 처리
W2 = np.array([
							[w11, w21],
							[w12, w22],
							[w13, w23],
												])
B2 = np.array([b1, b2])
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2) # 시그모이드 함수는 예시로 사용

# 3. 2층에서 출력층으로 온 신호 처리
def identify_function(x):
	return x
W3 = np.array([
							[w11, w12],
							[w12, w22]
												])
B3 = np.array([b1, b2])
A3 = np.dot(Z2, W3) + B3
Y = identify_function(A3)

구현 정리

def init_network():
	network = {}
	network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
	network['W1'] = 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['W1'] = np.array([0.1, 0.3, 0.5])
	network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
	network['W1'] = np.array([0.1, 0.2])
	
	return network

# forward인 이유는 신호가 순방향으로 전달됨을 알리기 위함 <-> backward
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(x, W2) + b2
	z2 = sigmoid(a2)
	a3 = np.dot(x, W3) + b3
	y = identify_function(a3)

	return y

network = init_network()
x = np.array([1.0, 0.5])
y = forword(network, x)
print(y) # [0.31682708, 0.69627909]

출력층 설계

출력층의 활성화 함수는 문제의 성질에 따라 달라짐

출력층의 뉴런 수는 문제에 맞게 적절히 정해야 함

  • 분류에서는 분류하는 클래스 수가 일반적

항등 함수: 입력을 그대로 출력

소프트맥스 함수:

yk=expaki=1nexp(ai)y_k=\frac{exp^{a_k}}{\sum^n_{i=1}exp(a_i)} (exp(x)는 exe^x, n은 출력층의 뉴런 수, yky_k는 k번째 출력)

특징:

출력은 항상 0과 1.0 사이의 실수

출력의 총합은 1 = 각 결과를 확률로 해석 가능

신경망으로 분류할 경우 가장 큰 출력을 내는 뉴런을 사용

- 소프트맥스 함수는 입력 간의 대소관계와 출력 간의 대소관계가 일치하므로 생략 가능

- 학습 단계에서는 사용함 | 추론 단계에서는 생략하는 것이 일반적

소프트맥스의 출력은 모든 입력 신호로부터 영향을 받음

*지수함수의 특성상 자칫하면 지수함수의 결과가 무한대가 될 수 있어 결과 수치가 불안정해질 수 있음

위의 문제점을 개선한 수식:

yk=exp(ak)i=1nexp(ai)=Cexp(ak)Ci=1nexp(ai)=exp(ak+logC)i=1nexp(ai+logC)=exp(ak+C)i=1nexp(ai+C)\begin{array}{lcl} y_k & = & \frac{exp(a_k)}{\sum^n_{i=1}exp(a_i)} \\\\ & = & \frac{Cexp(a_k)}{C\sum^n_{i=1}exp(a_i)} \\\\ & = & \frac{exp(a_k+\log C)}{\sum^n_{i=1}exp(a_i+\log C)} \\\\ & = & \frac{exp(a_k+C^{'})}{\sum^n_{i=1}exp(a_i+C^{'})} \end{array}

위 식에서 CC^{'}을 통해 소프트맥스의 결과를 전체적으로 줄이는 것 - 각 결과의 비율은 변하지 않음

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

예제: 손글씨 숫자 인식

*학습과정 생략, 추론과정만 구현

  • Pickle - 특정 객체를 파일로 저장 - 데이터 준비 1회로 실행하는 동안 계속 사용 가능
  • MNIST 데이터셋

    - 0~9의 숫자 이미지, 28*28의 회색조 이미지, 각 이미지에는 해당하는 숫자로 레이블이 붙어 있음

    - 훈련 이미지 60000장, 테스트 이미지 10000장

  • 이 예제는 입력층 뉴런 784(28*28)개, 1층 은닉층 뉴런 50개, 2층 은닉층 뉴런 100개, 출력층 뉴런 10개임
(x_train, t_train), (x_test, t_text) = # (이미지, 레이블)의 형태
									load_mnist(flatten=True, normalize=False, one_hot_label=False)
# flatten - 입력 이미지 1차원 배열화
# normalize - 픽셀을 0~1로 정규화(데이터를 특정 범위로 변환하는 처리)
# one_hot_label - 원-핫 인코딩 형태로 저장할지 결정-레이블을 배열화 하여 해당하는 인덱스를 1 나머지 0

# x_train.shape = (60000, 784)
# t_train.shape = (60000,)
# x_test.shape = (10000, 784)
# t_test.shape = (10000,)
def get_data():
	(x_train, t_train), (x_test, t_text) = 
									load_mnist(flatten=True, normalize=False, one_hot_label=False)
	return x_test, t_test

def init_network():
	# sample_weight에는 학습된 가중치 매개변수가 저장되어 있음
	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(x, W2) + b2
	z2 = sigmoid(a2)
	a3 = np.dot(x, W2) + 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]) # 각 레이블의 확률을 넘파이 배열로 반환 [0.1, 0.3, 0.2...]
	p = np.argmax(y) # 가장 확률이 높은 레이블(확률이 아닌 레이블=인덱스) 저장
	if p == t[i]:
		accuracy_cnt += 1 # 확률적으로 가장 높은 레이블이 답이면 증가
Accuracy = float(accuracy_cnt)/len(x)
# 배치 처리
x, t = get_data()

batch_size = 100
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
	x_batch = x[i:i+batch_size) # i에서 i+batch_size까지의 데이터 묶기
	y_batch = predict(network, x_batch) 

	# axis=1은 1번째 차원 기준으로 가장 확률이 높은 레이블들을 1차원 배열로 저장
	p = np.argmax(y_batch, axis=1)
	# 예
	x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6], [0.2, 0.5, 0.3]])
	y = np.argmax(x, axis=1)
	  = [1, 2, 1]	

	# 옳게 분류한 것은 True, 아닌 것은 False인 1차원 배열
	accuracy_cnt += np.sum(p == t[i:i+batch_size)
	# 예
	y = np.array([1, 2, 1])
	t = np.array([1, 2, 0])
	y == t = [True, True, False]
	np.sum(y==t) = 2
	
Accuracy = float(accuracy_cnt)/len(x)

*전처리: 입력 데이터에 특정 변환을 가하는 것 - 위의 경우 정규화가 전처리 작업


정리:

신경망매끄럽게 변화하는 함수(시그모이드)를, 퍼셉트론갑자기 변화하는 함수(계단)를 활성화 함수로 함