3일독학인데 지금은 첫째날에서 둘째날 넘어가는 새벽 2시다. 생각보다 굉장히 벅차다. 다시 시작!
다층 퍼셉트론을 구현하면 복잡한 함수도 표현할 수 있지만, 여전히 가중치는 사람이 수동적으로 설정해야 함.
신경망을 통해 이 단점을 극복한다.
1. 퍼셉트론에서 신경망으로
1.1 신경망의 예
신경망을 그림으로 나타내보자. 입력츠에서 출력층 방향으로 0층, 1층, 2층이라 하자. 아래 그림에서는 0층이 입력층, 1층이 은닉층, 2층이 출력층이 된다. 은닉층의 뉴런은 사람 눈에는 보이지 않음.
신경망에선 신호를 어떻게 전달할까?
1.2 활성화 함수 등장
앞서 배운 퍼셉트론의 그림에는 편향이 명시되어 있지 않았다. 편향을 명시한다면 다음과 같은 그림이 나옴.
입력이 항상 1인 뉴런의 가중치가 b이 된다. 그러면 모든 입력신호의 합과 출력과의 관계를 더 간단히 나타내보자. 각 신호에 가중치를 곱한 후 더한 값이 0을 넘으면 출력이 1이고, 그렇지 않으면 0을 출력한다. (원래는 임계값 기준이었는데, 임계값이 편향의 의미로 전환되었음) 조건에 맞춰서 분기하는 함수를 h(x)라 하자. (h(x)는 입력신호의 총합으로 출력신호를 결정)
이렇게 h(x)를 일반적으로는 활성화 함수라 한다. 활성화 함수는 이름에서 알수있듯이, 입력신호의 총합(활성화함수의 입력)이 활성화를 일으키는지(활성화함수의 출력)를 정하는 역할을 한다.
위 식에서 b+w1x1+w2x2를 a로 치환해보자.
입력신호와 편향의 총합을 계산하고 이를 a라 하면, a를 함수 h에 넣어 y를 출력하는 흐름이다.
보통은 뉴런을 하나의 원으로 그리는데, 신경망의 동작을 더 명확히 드러내고자 할 때는 활성화 처리 과정을 명시하기도 함.
2. 활성화 함수
활성화 함수는 임계값을 경계로 출력이 바뀐다. 이런 함수를 계단함수(임계값을 경계로 출력이 바뀌는 함수)라 함. 퍼셉트론은 활성화 함수로 계단 함수를 이용하는데, 계단함수에서 다른함수로 변경하는 것이 신경망으로 나아가는 스텝임.
일반적으로는 단층 퍼셉트론은 단층 네트워크에서 계단함수를 활성화 함수로 사용한 모델을,
다층 퍼셉트론은 신경망을 가리킨다.
이 책에서 사용하는 '퍼셉트론'의 의미가 통일되지 않음.
2.1 시그모이드 함수
신경망에서 자주 이용하는 활성화 함수. 이 함수를 이용하여 신호를 변환하고, 그 변환된 신호를 다음 뉴런에 전달한다.
퍼셉트론과 신경망의 주된 차이는 이 활성화 함수 뿐!
def sigmoid(x):
return 1/(1+np.exp(-x))
계단함수와 비교하면 '매끄럽다'. 곡선이기 때문에 입력에 따라 출력이 연속적으로 변한다.
이 매끈함이 신경망 학습에서 아주 중요한 역할을 하게 됨.
하지만 공통점도 있다. 입력이 작을 수록 0에 가깝고, 입력이 커지면 출력이 1에 가까워진다. 또, 두 함수 모두 비선형적이다.
2.2 비선형함수
신경망에서 선형 함수를 이용하면 신경망의 층을 깊게 하는 의미가 없어진다. why? 선형함수는 층을 아무리 깊게 해도 은닉층이 없는 네트워크로도 똑같은 기능을 할 수 있음.
2.3 RELU 함수
시그모이드 함수는 오래전부터 이용되어왔으나, 최근에는 RELU함수를 주로 이용한다.
RELU는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하이면 0을 출력한다. 간단한 함수임.
def relu(x):
return np.maximum(0,x)
3. 3층 신경망 구현하기
3.1 신경망의 내적
넘파이 행렬을 써서 신경망을 구현해보자. 이 신경망은 가중치만을 가짐 (활성화함수, 편향 생략)
이때 np.dot연산을 써서 신경망의 내적(행렬의 내적)을 구할 수 있다.
x = np.array([1,2]) # x1, x2
w = np.array([[1,3,5], [2,4,6]])
y = np.dot(x,w)
print(y)
3.2 표기법 설명
여기에서만 설명을 위해 다음과 같은 표기법을 사용한다.
가중치(w)와 은닉층(a) 우측상단에 (1)이 표기 되어있음. 이때 (1)은 층을 나타내는 것으로, 1층의 가중치, 1층의 뉴런임을 표기하기 위함이다. 가중치 오른쪽 아래의 두 숫자는 뉴런의 인덱스 번호이다. $w_{12}^{(1)}$은 앞층의 2번째 뉴런에서 다음 층의 1번째 뉴런으로 향할 때의 가중치라는 뜻이다.
3.3 각 층의 신호 전달 구현
1층의 첫번째 뉴런으로 가는 신호
위 식을 바탕으로 행렬의 내적을 이용하여 1층의 가중치 부분을 다음 식으로 간소화 할 수 있다.
x = np.array([1.0, 0.5])
w1 = np.array([[0.1, 0.3, 0.5],[0.2, 0.4, 0.6]])
b1 = np.array([0.1, 0.2, 0.3])
A1 = np.dot(x,w1)+b1
1층의 활성화 함수에서의 처리
은닉층에서의 가중치 합을 a로 표기하고 활성화 함수 h로 변환된 신호를 z로 표기한다.
시그모이드 함수를 사용하자.
z1 = sigmoid(A1)
1층에서 2층으로 신호 전달
1층의 출력 z1가 2층의 입력이 된다는 점만 빼고 앞단계와 똑같다.
w2 = np.array([[0.1, 0.4],[0.2, 0.5],[0.3, 0.6]])
b2 = np.array([0.1, 0.2])
A2 = np.dot(z1,w2)+b2
z2 = sigmoid(A2)
2층에서 출력층으로의 신호 전달
이 부분도 그동안의 구현과 거의 같지만 활성화 함수만 달라진다.
항등함수(함수의 입력을 그대로 출력)를 출력층의 활성화 함수로 사용.
출력층의 활성화 함수를 σ()로 표기하여 차이점을 주었다.
def identify_function(x): # 출력층 활성화함수 (항등함수)
return x
w3 = np.array([[0.1, 0.3],[0.2, 0.4]])
b3 = np.array([0.1, 0.2])
A3 = np.dot(z2, w3)+b3
Y = identify_function(A3) # Y = A3
신경망 구현의 관례 => 가중치만 W1같이 대문자로, 그 외 편향과 중간결과 등은 모두 소문자
4. 출력층 설계
4.1 항등함수와 소프트맥스 함수 구현
항등함수는 입력을 그대로 출력한다. 출력층에서 항등 함수를 사용하면 입력 신호가 그대로 출력 신호가 된다.
소프트맥스 함수는 분류에서 사용한다.
n은 출력층의 뉴런 수, $y_k$는 그 중 k번째 출력을 뜻한다.
분자의 $a_k$는 입력 신호의 지수함수 $exp(a_k)$, 분모는 모든 입력 신호의 지수 함수의 합 $\sum\limits_{i=1}^N (a_i)$ 으로 구성된다. 항등함수와 다르게, 소프트맥스의 출력은 모든 입력 신호의 영향을 받는다.
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
4.2 구현 시 주의점
위의 softmax() 코드는 지수함수를 사용하기 때문에 컴퓨터로 계산할때 오버플로 문제가 생겨서 다음과 같이 개선한다.
여기서 C'은 대체적으로 입력 신호 중 최댓값을 이용하나. 위 조치없이 그냥 계산하면 nan이 출력되는데, 최댓값 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
4.2 소프트맥스 함수 특징
소프트맥스 함수 출력값은 [0, 1.0]의 실수이며, 이 출력의 총합은 1이다. 따라서 함수의 출력을 '확률'로 해석할 수 있다.
가령 y[0] = 0.018, y[1] = 0.245, y[2] = 0.737일 때, 이 결과확률들로부터 "2번째 원소의 확률이 가장 높으니 답이 될 수 있다"라는 결론을 도출할 수 있음. 확률적인 결론도 가능(72%의 확률로 2번째 클래스)
그런데, 소프트맥스 함수는 단조증가이기 때문에 이 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 달라지지 않음. 따라서 신경망으로 분류할때는 소프트맥스를 생략해도 됨 (현업에서도 생략하는 것이 일반적)
4.3 출력층의 뉴런 수
풀려는 문제에 맞게 적절히 정해야 한다.분류에서는 분류하고자 하는 클래스 개수로 설정하는 것이 일반적이다.
이미지를 숫자 0~9중 하나로 분류하는 문제라면 출력층의 뉴런 수를 10개로 정한다.
위 그림에선 호색 농도가 출력값을 의미하는데, 색이 가장 짙은 $y_2$뉴런이 가장 큰 값을 출력하기 때문에 선택된다.
= 해당 이미지를 숫자 '2'로 판단하였다.
5. 손글씨 숫자 인식
학습과정은 생략하고 ,추련 과정만 구현한다.
5.1 MNIST 데이터셋
깃허브 저장소의 dataset/mnist.py파일을 실행하면 MNIST 데이터셋을 내려받아 이미지를 넘파이 배열로 변환해준다.
아래는 이미지를 화면으로 불러오는 코드이다.
import sys, os
from mnist import load_mnist
import numpy as np
from PIL import Image
sys.path.append(os.pardir)
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) #위에서 flatten했기 때문에 다시 변형해주는 것이다.
print(img.shape)
img_show(img)
이때, load_mnist인수로 normalize, flatten, one_hot_label을 설정할 수 있다.
normalize : 입력 이미지의 픽셀값을 0.0~1.0사이의 값으로 정규화할 것인가
flatten : 입력 이미지를 평탄하게, 즉 1차원 배열로 만들것인가
one_hot_label : 라벨을 원-핫-인코딩 형태로 저장할 것인가
5.2 신경망의 추론 처리
신경망의 입력층 뉴런을 784개, 출력층 뉴런을 10개로 구성하자.
why? (입력) 이미지 크기가 28*28 = 784이고, (출력) 0~9까지의 숫자를 구분하는 문제이기 때문
은닉층은 2개로 한다. 첫번째 은닉층에는 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():
#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(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("Accuracy:" + str(float(accuracy_cnt) / len(x)))
올바르게 분류한 비율이 93.52%라는 의미이다. 점점 학습 방법과 구조를 궁리하여 99%까지 높일 것.
5.3 배치처리
신경망 각 층 배열 형상의 추이를 확인해보자.
이는 데이터를 1장만 입력했을 때의 처리 흐름이다.
만약 이미지 100장을 한꺼번에 넣는다면 어떻게 될까.
입력데이터의 형상은 100*784, 출력 데이터의 형상은 100*10이 된다. 즉, 100장 분량의 입력 데이터 결과가 한 번에 출력된다. 이렇게 하나로 묶은 입력 데이터를 배치(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]
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)))
axis = 1이라는 인수가 추가되었는데, 100*10 배열 중 1번째 차원을 축으로 최댓값의 인덱스를 찾도록 한 것.
6. 정리
신경망은 각 층의 뉴런들이 다음 층의 뉴런으로 신호를 전달한다는 점에서 퍼셉트론과 같다.
하지만 신호를 변화시키는 활성화 함수에 차이가 있었음.
신경망에서는 시그모이드 (매끄럽게 변화), 퍼셉트론에선 계단 함수(갑자기 변화)를 사용