오답노트

[밑딥] 신경망 본문

Python/DL

[밑딥] 신경망

권멋져 2023. 1. 21. 21:26

퍼셉트론은 복잡한 함수도 표현할 수 있다. 하지만 가중치를 설정하는 작업 즉, 원하는 결과를 출력하도록 가중치 값을 적절히 정하는 작업은 여전히 사람이 수동으로 한다는 것이다.

그러나 신경망은 가충치 매개변수의 적절한 값을 데이터로부터 자동으로 학습하는 성질이 있다.

 

퍼셉트론에서 신경망으로

신경망의 예

아래 그림은 신경망이다. 여기서 가장 왼쪽 줄은 입력층, 맨 오느쪽 줄은 출력층, 중간 줄은 은닉층이다. 은닉층의 뉴런은 입력층이나 출력층과 달리 사람 눈에는 보이지 않는다.

퍼셉트론 복습

퍼셉트론을 수식으로 나타 냈을때, b는 편향을 나타내는 매개변수고 뉴런이 얼마나 쉽게 활성화되느냐 제어한다. 한편, w1,w2는 각 신호의 가중치를 나타내는 매개변수로, 각 신호의 영향력을 제어한다.

위 그림은 퍼셉트론에 가중치 b를 명시한 것이다. 이를 더 간결한 형태로 만들면 0을 넣으면 1을 출력하고 그렇지 않으면 0을 출력하는 조건 분기의 동작을 하나의 함수로 나타낸다. 이 함수를 h(x)라 하면 다음과 같이 표현할 수 있다.

함수 h(x)는 입력이 0을 넘으면 1을 돌려주고 그렇지 않으면 0을 돌려주는 함수다.

 

활성화 함수의 등장

입력 신호의 총합을 출력 신호로 변환하는 함수를 일반적으로 활성화 함수라고 한다. 활성화 함수는 입력 신호의 총합이 활성화를 일으키는지를 정하는 역할을 한다.

위 그림에서 가중치 신호를 조합한 결과가 a라는 노트가 되고, 활성화 함수 h()를 통과하여 y라는 노드로 변환되는 과정이 분명하게 나타나 있다.

 

활성화 함수

위의 h(x) 활성화 함수는 임계값을 경계로 출력이 바뀌는데, 이런 함수를 계단 함수라고 한다. 그래서 퍼셉트론에서는 활성화 함수로 계단 함수를 이용한다고 할 수 있다. 즉, 활성화 함수로 쓸 수 있는 여러 후보중에서 퍼셉트론은 계단 함수를 채용하고 있다.

 

시그모이드 함수

시그모이드 식은 아래와 같다.

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

 

계단 함수 구현하기

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

import numpy as np

def step_func(x):
  y = x > 0
  return y.astype(int)
 
step_func(np.array([-1,1,2])) # array([0, 1, 1])

계단 함수의 그래프

import matplotlib.pyplot as plt

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

시그모이드 함수 구현하기

넘파이의 브로그캐스트 기능으로 넘파이 배열과 스칼라값의 연산을 넘파이 배열의 원소 각각과 스칼라값의 연산으로 바꿔 수행한다.

def sigmoid(x):
  return 1/(1+np.exp(-x))
 
x = np.array([-1.0,1.0,2.0])
sigmoid(x) # array([0.26894142, 0.73105858, 0.88079708])
x = np.arange(-5.0,5.0,0.1)
y = sigmoid(x)
plt.plot(x,y)
plt.show()

시그모이드 함수와 계단 함수 비교

  • 차이점 : 시그모이드 함수는 부드러운 곡선이며 입력에 따라 출력이 연속적으로 변화한다. 한편 계단 함수는 0을 경계로 출력이 갑자기 바뀐다. 시그모이드 함수의 이 매끈함이 신경망 학습에서 중요한 역할을 한다.
  • 공통점 : 입력이 작을 때의 출력은 0에 가깝거나 0이고, 입력이 커지면 출력이 1에 가까워지거나 1이되는 구조이다.
  • 즉 입력이 중요하면 큰 값을 출력하고 입력이 중요하지 않으면 작은 값을 출력한다.

비선형 함수

위 두 함수는 모두 비선형 함수이다. 신경망에서는 활성화 함수로 비선형 함수를 사용해야합니다. 달리 말하면 선형 함수를 사용해서는 안된다. 그 이유는 신경망의 층을 깊게 하는 의미가 없기 때문이다.

선형 함수의 문제는 층을 아무리 깊게 해도 은닉층이 없는 네트워크로도 똑같은 기능을 할 수 있다는 데 있다.

 

ReLU 함수

ReLu는 입력이 0을 넘으면 입력을 그대로 출력하고 0이하면 0을 출력하는 함수다.

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

# maximum : 두 입력중 큰 값을 선택해 반환하는 함수

다차원 배열의 계산

넘파이의 다차원 배열을 사용한 계산법을 숙달하면 신경망을 효율적으로 구현할 수 있다.

다차원 배열

N차원으로 나열하는 것을 통틀어 다차원 배열이라고 한다.

A = np.array([1,2,3,4])
print(A) # [1 2 3 4]
print(np.ndim(A)) # 1
print(A.shape) # (4,)
print(A.shape[0]) # 4

B = np.array([[1,2],[3,4],[5,6]])
print(B)
'''
[[1 2]
 [3 4]
 [5 6]]
'''
print(np.ndim(B)) # 2
print(B.shape) # (3, 2)

행렬의 곱

행렬의 곱을 하는 방법은 아래 그림과 같다.

A = np.array([[1,2],[3,4]])
print(A.shape) # (2, 2)
B = np.array([[5,6],[7,8]])
print(B.shape) # (2, 2)
print(np.dot(A,B))
'''
[[19 22]
 [43 50]]
'''
C = np.array([[1,2],[3,4]])
print(C.shape) # (2, 2)
print(A.shape) # (2, 3)
print(np.dot(A,C)) # error!!

# 행렬 A의 1번째 차원과 행렬 C의 0번째 차원이 다르기 때문에 계산할 수 없다.
A = np.array([[1,2],[3,4],[5,6]])
print(A.shape) # (3, 2)
B = np.array([7,8])
print(B.shape) # (2, )
print(np.dot(A,B)) # [23 53 83]

3층 신경망 구현하기

def identify_func(x):
  return x

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

  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 = identify_func(a3)

  return y

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

출력층 설계하기

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

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

  • 항등 함수 : 입력을 그대로 출력
  • 소프트맥스 함수 : 소프트맥스의 출력은 모든 입력 신호로부터 화살표를 받는다 출력 층의 각 뉴런이 모든 입력 신호에서 영향을 받기 때문이다.

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

  return y

 

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

e^1000은 무한대를 뜻하는 inf가 반환된다. 이런 큰 값 끼리 나눗셈을 하면 결과 수치가 불안정해진다.

아래는 위와 같은 문제를 해결한 수식이다. 

소프트맥스의 지수 함수를 계산할 때 어떤 정수를 더하거나 빼도 결과는 바뀌지 않는다는 것이다. C에 어떤 값을 대입해도 상관 없지만, 오버플로를 막는 목적으로는 입력 신호중 최댓값을 이용하는 것이 일반적이다.

 

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

소프트맥스 함수의 특징

a = np.array([0.3,2.9,4.0])
y= softmax(a)
print(y) # [0.01821127 0.24519181 0.73659691]
print(np.sum(y)) # 1.0

소프트맥스 함수 출력의 총합은 1이다. 이 성질 덕분에 소프트맥스 함수의 출력을 확률로 해설할 수 있다.

소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않는다. 그 이유는 지수함수가 단조 증가 함수이기 때문이다. 단조 증가함수는 정의역 원소a,b 가 a<=b일 때, f(a)<=f(b)가 성립하는 함수이다.

신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식한다. 그리고 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 달라지지 않는다. 결과적으로 신경망으로 분류할 때는 출력층의 소프트맥스 함수를 생략해도 된다.

 

출력층의 뉴런 수 정하기

출력층의 뉴런 수는 풀려는 문제에 맞게 적절히 정해야한다. 분류에서는 분류하고 싶은 클래스 수로 설정하는 것이 일반적이다.

 

손글씨 숫자 인식

이미 학습된 매개변수를 사용하여 학습과정은 생략하고 추론과정만 구현해보자. 이 추론 과정을 신경망의 순전파라고도 한다.

 

MNIST 데이터셋

MNIST라는 손글씨 숫자 이미지 집합이다.

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', as_frame=False, cache=False)

X = mnist.data.astype('float32')
y = mnist.target.astype('int64')

plt.imshow(X[1].reshape(28,28))
print(y[1]) # 0

신경망의 추론 처리

import pickle

def init_network():
  with open("/content/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
network = init_network()

accuracy_cnt = 0
for i in range(X.shape[0]):
  pred = predict(network, X[i])
  p = np.argmax(pred)
  if p == y[i]:
    accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / X.shape[0]))
# Accuracy:0.9245428571428571

배치 처리

하나로 묶은 입력 데이터를 배치(batch)라 한다.

batch_size = 100
accuracy_cnt = 0

for i in range(0,X.shape[0],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== y[i:i+batch_size])

print("Accuracy:" + str(float(accuracy_cnt) / X.shape[0]))
# Accuracy:0.9245428571428571

 

정리

  • 신경망에서는 활성화 함수로 시그모이드 함수와 ReLU 함수 같은 매끄럽게 변화하는 함수를 이용한다.
  • 넘파이의 다차원 배열을 잘 사용하면 신경망을 효율적으로 구현할 수 있다.
  • 기계학습 문제는 크게 회귀와 분류로 나눌 수 있다.
  • 출력층의 활성화 함수로는 회귀에서는 주로 항등 함수를, 분류에서는 주로 소프트맥스 함수를 이용한다.
  • 분류에서는 출력층의 뉴런 수를 분류하려는 클래스 수와 같게 설정한다.
  • 입력 데이터를 묶은 것을 배치라 하며, 추론 처리를 이배치 단위로 진행하면 결과를 훨씬 빠르게 얻을 수 있다.

'Python > DL' 카테고리의 다른 글

[밑딥] 오차역전파법  (0) 2023.02.27
[밑딥] 신경망 학습  (0) 2023.02.27
[밑딥] 퍼셉트론  (1) 2023.01.21
[NLP] koBERT 실습  (1) 2022.10.15
[NLP] N-gram  (0) 2022.10.14