2024. 7. 24. 21:11ㆍ밑바닥부터 시작하는 딥러닝 1
ch03 신경망¶
신경망은 가중치 매개변수의 적절한 값을 데이터로 부터 자동으로 학습하는 능력이 있다.
3.1 퍼셉트론에서 신경망으로¶
3.1.1 신경망의 예¶
신경망의 가장 왼쪽 줄을 입력층
, 맨 오른쪽 줄을 출력층
중간줄을 은닉층
이라고 한다.
은닉층의 뉴런은 입력층이나 출력층과 달리, 사람 눈에는 보이지 않는다.
아래 그림에서는 0층이 입력층, 1층이 은닉측, 2층이 출력층이 된다.
위의 신경망에서, 가중치를 갖는 층은 2개이기 때문에, '2층 신경망' 이라고 한다.
3.1.2 퍼셉트론 복습¶
위의 그림은 $x_{1}$과 $x_{2}$라는 두 신호를 입력받아 $y$를 출력하는 퍼셉트론이다. 이 퍼셉트론을 수식으로 나타내면 다음 식이 된다.
$$y = \begin{cases}0& (b + w_{1}*x_{1} + w_{2}*x_{2}\leq{0})\\1 &(b + w_{1}*x_{1} + w_{2}*x_{2}>0)\end{cases}$$
여기서 b는 편향
을 나타내는 매개변수로, 뉴런이 얼마나 쉽게 활성화되느냐를 제어한다.
한편 $w_{1}$과 $w_{2}$는 각 신호의 가중치
를 나타내는 매개변수로, 각 신호의 영향력을 제어한다.
위 그림에 편향을 명시한다면, 아래 그림과 같이 나타낼 수 있다.
가중치가 $b$이고 입력이 1인 뉴런이 추가되었다. 이 퍼셉트론의 동작은 $x_{1}$, $x_{2}$, 1 이라는 3개의 신호가 뉴런에 입력되어, 각 신호에 가중치를 곱한 후 $(w_{1}*x_{1} + w_{2}*x_{2} + b*1$) 다음 뉴런에 전달된다. 다음 뉴런 y에서는 이 신호들의 값을 더하여, 그 합이 0을 넘으면 1을 출력하고, 그렇지 않으면 0을 출력한다.
식으로 나타내면, 다음과 같다 $$y = h(b+ w_{1}*x_{1} + w_{2}*x_{2})$$ $$h(x) = \begin{cases}0&(x\leq{0})\\1&(x>0)\end{cases}$$
입력 신호의 총합이 $h(x)$라는 함수를 거쳐 변환되어, 그 변환된 값이 y의 출력이 됨을 보여준다. 그리고 $h(x)$함수는 입력이 0을 넘으면 1을 돌려주고, 그렇지 않으면 0을 돌려준다.
3.1.3 활성화 함수¶
입력 신호의 총합을 출력 신호로 변환하는 함수를 일반적으로 활성화 함수(activation function)
이라 한다.
활성화 함수는 입력 신호의 총합이 활성화를 일으키는지를 정하는 역할을 한다.
식 $y = h(b+w_{1}*x_{1} + w_{2}*x_{2})$는 가중치가 곱해진 입력신호의 총합을 계산하고, 그 합을 활성화 함수에 입력해 결과를 나타내는 2단계로 처리된다. 그래서 이 식은 다음과 같은 2개의 식으로 나눌 수 있다.
- $ a = b + w_{1}*x_{1} + w_{2}*x_{2}$
- $ y = h(a) $
식 1은 가중치가 달린 입력신호와 편향의 총합을 계산하고, 이를 $a$라고 한다. 그리고 식 2는 $a$를 함수 $h()$에 넣어 $y$를 출력하는 흐름이다. 그림으로는 다음과 같이 나타낼 수 있다.
가중치 신호를 조합한 결과가 $a$라는 노드가 되고, 활성화 함수 $h()$를 통과하여 $y$라는 노드로 변환되는 과정이 분명하게 나타나 있다.
노드 = 뉴런
일반적으로 단순 퍼셉트론
은 단층 네트워크에서 계단함수(임계값을 경계로 출력이 바뀌는 함수)를 활성화 함수로 사용한 모델을 가리키고
다층 퍼셉트론
은 신경망(여러 층으로 구성되고 시그모이드 함수등의 매끈한 활성화 함수를 사용하는 네트워크)을 가리킨다.
3.2 활성화 함수¶
식 $h(x) = \begin{cases} 0 &(x\leq{0}) \\ 1 & (x>0)\end{cases}$ 과 같은 활성화 함수는 임계값을 경계로 출력이 바뀌는데,
이런 함수를 계단 함수(step function)
라고 한다.
그래서 "퍼셉트론 에서는 활성화 함수로 계단 함수를 이용한다."라고 할 수 있다.
즉, 활성화 함수로 쓸 수 있는 여러 후보 중에서 퍼셉트론은 계단 함수를 채용하고 있다.
3.2.1 시그모이드 함수¶
다음은 신경망에서 자주 이용하는 활성화 함수인 시그모이드 함수(sigmoid function)
를 나타낸 식이다.
$$h(x) = \frac{1}{1+e^{-x}}$$
시그모이드 함수에 입력을 주면 출력을 돌려준다. 예를 들어 $h(1.0) = 0.731..., h(2.0) = 0.880...$처럼 특정 값을 출력한다.
신경망에서는 활성화 함수로 시그모이드 함수를 이용하여, 신호를 변환하고, 그 변환된 신호를 다음 뉴런에 전달한다. 앞 장에서 본 퍼셉트론과 신경망의 주된 차이는 활성화 함수 뿐이다.
3.2.2 계단 함수 구현하기¶
import numpy as np
def step_function(x):
if x > 0:
return 1
else:
return 0
위의 구현은 단순하고 쉽지만, 인수 x는 실수(부동소수점)만 받아들인다. 즉, step_function(3.0)은 되지만, Numpy배열을 인수로 넣을 수는 없다. 가령, funtion(np.array[1.0,2.0])은 안된다.
def step_function(x):
y = x>0
return y.astype(int)
#np.int is deprecated
x = np.array([-1.0,1.0,2.0])
x
array([-1., 1., 2.])
y = x>0
y
array([False, True, True])
#y의 요소를 bool형에서 int 형으로 변경
y = y.astype(int)
y
array([0, 1, 1])
3.2.3 계단 함수의 그래프¶
import matplotlib.pylab as plt
def step_function(x):
return np.array(x>0, dtype = int)
x = np.arange(-5.0,5.0,0.1)
y = step_function(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()
np.arange(-5.0,5.0,0.1)은 -5.0에서 5.0전까지 0.1 간격의 넘파이 배열을 생성한다.([-5.0,-4.9,...,4.9]) step_function()은 인수로 받은 넘파이 배열의 원소 각각을 인수로 계단 함수를 실행해, 그 결과를 다시 배열로 만들어 돌려준다. 이 x,y 배열을 그래프로 그리면(plot) 결과는 위의 그림처럼 된다.
계단 함수는 0을 경계로 출력이 0에서 1(또는 1에서 0)으로 바뀐다.
3.2.4 시그모이드 함수 구현하기¶
sigmoid 함수는 다음과 같이 만들 수 있다.
def sigmoid(x):
return 1/ (1+np.exp(-x))
인수 x가 numpy 배열이어도, 올바른 결과가 나온다.
x = np.array([-1.0,1.0,2.0])
sigmoid(x)
array([0.26894142, 0.73105858, 0.88079708])
이 함수가 numpy 배열도 훌륭히 처리해줄 수 있는 비밀은 넘파이의 브로드캐스트에 있다. 브로드캐스트 기능이란, 넘파이 배열과 스칼라 값의 연산을 넘파이 배열의 원소 각각과 스칼라 값의 연산으로 바꿔 수행하는 것이다.
t = np.array([1.0, 2.0, 3.0])
1.0+t
array([2., 3., 4.])
1.0/t
array([1. , 0.5 , 0.33333333])
1 / (1 + np.exp(-x))도 넘파이 배열의 각 원소에 연산을 수행한 결과를 내어준다.
x = np.arange(-5.0,5.0,0.1)
y = sigmoid(x)
plt.plot(x,y)
plt.ylim(-0.1,1.0)
plt.show
<function matplotlib.pyplot.show(close=None, block=None)>
3.2.5 시그모이드 함수와 계단 함수 비교¶
x = np.arange(-5.0,5.0,0.1)
y = sigmoid(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1)
y = step_function(x)
plt.plot(x,y, color = 'green',linestyle = '--')
plt.show
<function matplotlib.pyplot.show(close=None, block=None)>
시그모이드 함수와 계단 함수를 비교해봤을 때, 시그모이드는 부드러운 곡선이며, 입력에 따라, 출력이 연속적으로 변화한다. 한편, 계단 함수는 0을 경계로 출력이 갑자기 바뀌어 버린다. 시그모이드 함수의 매끈함이 신경망 학습에서 아주 중요한 역할을 하게 된다.
계단 함수가 0과 1중 하나의 값만 돌려주는 반면, 시그모이드 함수는 실수를 돌려준다는 점도 다르다. 퍼셉트론 에서는 뉴런 사이에 0 혹은 1이 흘렀다면, 신경망에서는 연속적인 실수가 흐른다.
계단함수와 시그모이드 함수는 입력이 중요하면, 큰 값을 출력하고, 입력이 중요하지 않으면, 작은 값을 출력하며 , 입력이 아무리 작거나 커도 출력은 0에서 1사이이다.
3.2.6 비선형 함수¶
계단함수와 시그모이드 함수는 모두 비선형 함수
이다. 시그모이드 함수는 곡선, 계단 함수는 계단처럼 구부러진 직선으로 나타나며, 동시에 비선형 함수로 분류된다.
선형함수¶
활성화 함수를 설명할 때 비선형 함수와 선형 함수라는 용어가 자주 등장한다.
함수란 어떤 값을 입력하면, 그에 따른 값을 돌려주는 '변환기'이다.
이 변환기에 무언가 입력했을 때 출력이 입력의 상수배 만큼 변하는 함수를 선형 함수
라고 한다.
수식으로는 $f(x) = a*x + b$이고, 이때 a와 b는 상수이다.
그래서 선형함수는 곧은 1개의 직선이 된다, 한편, 비선형 함수
는 문자 그대로 '선형이 아닌' 함수이다.
즉, 직선 1개로는 그릴 수 없는 함수를 말한다.
3.2.6 비선형 함수¶
계단함수와 시그모이드 함수는 모두 비선형 함수
이다.
신경망에서는 활성화 함수로 비선형 함수를 사용해야 한다.
선형함수를 이용하면, 신경망의 층을 깊게 하는 의미가 없어지기 때문이다.
선형 함수의 문제는 층을 아무리 깊게해도, '은닉층이 없는 네트워크'로도 똑같은 기능을 할 수 있다는데 있다. 예를 들어, 선형함수인 $h(x) = cx$를 활성화 함수로 사용한 3층 네트워크가 있을때, 이를 식으로 나타내면, $y(x) = h(h(h(x)))$가 된다. 이 계산은 $y(x) = c*c*c*x$ 처럼, 곱셈을 세 번 수행하지만, 실은 $y = ax$와 똑같은 식이다. $a = c^3$이라고만 하면 끝이다, 즉, 은닉층이 없는 네트워크로 표현할 수 있다. 선형 함수를 이용해서는 여러 층으로 구성하는 이점을 살릴 수 없다. 그래서 층을 쌓기 위해서는 활성화 함수로는 반드시 비선형 함수를 사용해야 한다.
3.2.7 ReLU 함수¶
시그모이드 함수는 신경망 분야에서 오래전 부터 이용해왔으나, 최근에는 ReLU(Rectified Linear Unit)
함수를 주로 이용한다.
ReLU는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0이하이면 0을 출력하는 함수이다.
수식 으로는 다음과 같이 쓸 수 있다.
$$h(x) = \begin{cases} x& (x>0)\\0&(x\leq{0})\end{cases}$$
함수로는 다음과 같이 구현할 수 있다.
def relu(x):
return np.maximum(0,x)
x = np.arange(-5.0,5.0,0.01)
y = relu(x)
plt.plot(x,y)
plt.show()
3.3 다차원 배열의 계산¶
넘파이의 다차원 배열을 사용한 계산법을 숙달하면 신경망을 효율적으로 구현할 수 있다.
3.3.1 다차원 배열¶
다차원 배열도, 그 기본은 '숫자의 집합; 이다. 숫자가 한 줄로 늘어선 것이나, 직사각형으로 늘어놓은것, 3차원으로 늘어놓은 것이나 (더 일반화한) N차원으로 나열하는 것을 통틀어 다차원 배열이라고 한다.
import numpy as np
A = np.array([1,2,3,4])
print(f'A : {A}')
print(f'A의 차원수 :{np.ndim(A)}')
print(f'A의 shape : {A.shape}')
print(f'A의 shape[0] : {A.shape[0]}')
A : [1 2 3 4] A의 차원수 :1 A의 shape : (4,) A의 shape[0] : 4
이와 같이 배열의 차원 수는 np.ndim()
함수로 확인할 수 있다.
또, 배열의 형상은 인스턴스 변수인 shape
으로 알 수 있다.
A.shape
은 튜플을 반환한다. 이는 1차원 배열이라도 다차원 배열일때와 통일된 형태로, 결과를 반환하기 위함이다. 예를 들어 2차원 배열일 때는 (4,3), 3차원 배열일 때는 (4,3,2)같은 튜플을 반환한다.
2차원 배열
B = np.array([[1,2],[3,4],[5,6]])
print(f"B : \n{B}")
print(f"B의 차원수 : {np.ndim(B)}")
print(f"B의 shape : {B.shape}")
B : [[1 2] [3 4] [5 6]] B의 차원수 : 2 B의 shape : (3, 2)
3x2 배열은 처음 차원에는 원소가 3개, 다음 차원에는 원소가 2개 있다는 의미이다.
이때 처음 차원은 0번째 차원, 다음 차원은 1번째 차원에 대응한다.
2차원 배열은 특히 행렬(matrix)
이라고 부르고, 배열의 가로 방향을 행(row)
, 세로 방향을 열(column)
이라고 한다.
3.3.2 행렬의 곱¶
2x2 행렬의 곱은 다음과 같이 계산한다.
그림에서처럼 행렬곱은 왼쪽행렬의 행(가로)과 오른쪽 행렬의 열(세로)을 원소별로 곱하고, 그 값들을 더해서 계산한다. 그리고 그 계산 결과가 새로운 다차원 배열의 원소가 된다.
예를 들어 A의 1행과 B의 1열을 곱한값은 결과 행렬의 1행 1번째 원소가 되고, A의 2행과 B의 1열을 곱한 결과는 2행 1번째 원소가 된다.
A = np.array([[1,2], [3,4]])
print(f"A : {A}")
print(f"A.shape : {A.shape}")
A : [[1 2] [3 4]] A.shape : (2, 2)
B = np.array([[5,6], [7,8]])
print(f"B : \n{B}")
print(f"B.shape : {B.shape}")
print(f"np.dot(A,B) :\n {np.dot(A,B)}")
B : [[5 6] [7 8]] B.shape : (2, 2) np.dot(A,B) : [[19 22] [43 50]]
이 코드에서 A와B는 2x2 해열이며, 이 두 행렬의 곱은 넘파이 함수 np.dot()
으로 계산한다.
np.dot()은 입력이 1차원 이면 벡터를, 2차원 배열이면, 행렬 곱을 계산한다.
여기서 한 가지 주의할것은 np.dot(A,B)와 np.dot(B,A)는 다른 값이 될 수 있다는 점이다.
+와 *등의 일반적인 연산과 달리 행렬의 곱에서는 피연산자의 순서가 다르면 결과도 다르다.
앞에서는 2x2행렬을 곱하는 예를 보았지만, 형상이 다른 행렬의 곱도 마찬가지 방법으로 계산할 수 있다.
A = np.array([[1,2,3],[4,5,6]])
B = np.array([[1,2],[3,4],[5,6]])
print(A.shape)
print(B.shape)
np.dot(A,B)
(2, 3) (3, 2)
array([[22, 28], [49, 64]])
2X3 행렬 A와 3X2행렬 B의 곱은 이와 같이 구현할 수 있다.
이때 행렬의 형상(shape)
에 주의해야 한다.
행렬A의 1번째 차원의 원소수(열 수)와 행렬 B의 0 번째 차원의 원소 수(행 수) 같아야 한다.
만족하지 못할 경우 다음과 같은 오류를 출력한다.
C = np.array([[1,2],[3,4]])
print(C.shape)
print(A.shape)
# np.dot(A,C) #계산불가
(2, 2) (2, 3)
3.3.3 신경망에서의 행렬 곱¶
넘파이 행렬을 써서 신경망을 구현해보기. 이 신경망은 편향과 활성화 함수를 생략하고 가중치만 갖는다.
X = np.array([1,2])
print(X.shape)
W = np.array([[1,3,5],[2,4,6]])
print(W.shape)
Y = np.dot(X,W)
print(Y)
(2,) (2, 3) [ 5 11 17]
다차원 배열의 스칼라 곱을 구해주는 np.dot
함수를 사용하면, 이처럼 단번에 결과 Y를 계산할 수 있다.
Y의 원소가 100개든, 1000개든 한 번의 연산으로 계산할 수 있다.
3.4 3층 신경망 구현하기¶
이번에는 3층 신경망에서 수행되는, 입력부터 출력까지의 처리(순방향 처리)를 구현한다. 이를 위해 넘파이의 다차원 배열을 사용한다. 넘파이 배열을 잘 쓰면 아주 적은 코드만드로 신경망의 순방향 처리를 완성할 수 있다.
3.4.1 표기법 설명¶
위의 그림은 뉴런 $x_{2}$에서 다음층의 뉴런 $a_{1}^{(1)}$으로 향하는 선 위에 가중치를 표시하고 있다.
가중치와 은닉층 뉴련의 오른쪽 위에는 '(1)'이 붙어있다.
이는 1층의 가중치, 1층의 뉴런임을 뜻하는 번호이다.
또, 가중치의 오른쪽 아래의 두 숫자는 차례로 다음 층 뉴련과 앞층 뉴런의 인덱서 번호이다.
가령 $w_{12}^{(1)}$은 앞층의 2번째 뉴런($x_{2}$)에서 다음 층의 1번째 뉴런($a_{1}^{(1)}$으로 향할 때의 가중치란 뜻이다.
가중치 오른쪽 아래의 인덱스 번호는 다음 층 번호, 앞 층 번호
순으로 적는다.
3.4.2 각 층의 신호 전달 구현하기¶
아래 그림은 입력층에서 '1층의 첫 번째 뉴런'으로 가는 신호이다.
위의 그림에서 평향을 뜻하는 뉴런인 ①이 추가 되었다. 편향은 오른쪽 아래 인덱스가 하나밖에 없다. 이는 앞 층의 편향 뉴런(뉴런 ①)이 하나이기 때문이다.
$a_{1}^{(1)}$을 수식으로 나타내면 다음과 같다.
$$ a_{1}^{(1)} = w_{11}^{(1)}x_{1} + w_{12}^{(1)}x_{2} + b_{1}^{(1)}$$
여기에서 행렬의 곱을 이용하면 1층의 '가중치 부분'을 다음 식처럼 간소화 할 수 있다. $$A^{(1)} = XW^{(1)} + B^{(1)}$$
이때, 행렬은 다음과 같다. $ A^{(1)} = (a_{1}^{(1)}, a_{2}^{(2)}, a_{3}^{(3)}), X = (x_{1}, x_{2}), B^{(1)} = (b_{1}^{(1)}, b_{2}^{(2)}, b_{3}^{(3)}), W^{(1)} = \begin{pmatrix} w_{11}^{(1)}&w_{21}^{(1)}&w_{31}^{(1)} \\ w_{12}^{(1)}&w_{22}^{(1)}&w_{32}^{(1)} \end{pmatrix}$
넘파이의 다차원 배열을 사용하면 다음과 같다.
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])
print(W1.shape)
print(X.shape)
print(B1.shape)
A1 = np.dot(X,W1)+B1
print(A1)
(2, 3) (2,) (3,) [0.3 0.7 1.1]
다음은 1층의 활성화 함수에서의 처리를 나타낸 그림이다.
위의 그림과 같이 은닉층에서의 가중치 합(가중 신호와 편향의 총합)을 $a$로 표기하고 활성화 함수 $h()$로 변환된 신호를 $z$로 표기한다. 활성화 함수는 시그모이드 함수를 사용하기로 한다.
Z1 = sigmoid(A1)
print(A1)
print(Z1)
[0.3 0.7 1.1] [0.57444252 0.66818777 0.75026011]
이 $sigmoid()$ 함수는 앞에서 정의한 함수이다. 이 함수는 넘파이 배열을 받아 같은 수의 원소로 구성된 넘파이 배열을 반환한다.
다음은 1층에서 2층으로 가는 과정과 구현이다.
W2 = np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]])
B2 = np.array([0.1,0.2])
print(Z1.shape)
print(W2.shape)
print(B2.shape)
A2 = np.dot(Z1,W2) + B2
print(A2)
Z2 = sigmoid(A2)
print(Z2)
(3,) (3, 2) (2,) [0.51615984 1.21402696] [0.62624937 0.7710107 ]
이 구현은 1층의 출력 Z1이 2층의 입력이 된다는 점을 제외하면 입력층에서 1층으로의 구현과 똑같다. 넘파이 배열을 사용하면서, 층 사이의 신호 전달을 쉽게 구현할 수 있다. 다음은 2층에서 출력층으로의 신호 전달이다., 출력층의 구현도 그동안의 구현과 거의 같다. 딱 하나, 활성화 함수만 지금까지의 은닉층과 다르다.
def identity_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 = identity_function(A3)
print(Y)
[0.31682708 0.69627909]
여기에서는 항등 함수인 identity_function()을 정의하고, 이를 출력층의 활성화 함수로 이용하였다. 항등 함수는 입력을 그대로 출력하는 함수이다. 그래서 이 예에서는 identity_function을 구현하였다. 또한 출력층의 활성화 함수를 $\sigma()$로 표시하여, 은닉층의 활성화 함수 $h()$와는 다름을 명시하였다.
활성화 함수¶
출력층의 활성화 함수는 풀고자 하는 문제의 성질에 맞게 정한다. 예를 들어 회귀에는 항등 함수를, 2클래스 분류에는 시그모이드 함수를, 다중 클래스 분류에는 소프트맥스 함수를 사용하는 것이 일반적이다.
3.4.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])
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]
여기에서는 init_network()와 forward라는 함수를 정의하였다. init_network() 함수는 가중치와 편향을 초기화 하고, 이들을 딕셔너리 변수인 network에 저장한다.
forward() 함수는 입력 신호를 출력으로 변환하는 처리 과정을 모두 구현하고 있다. 함수 이름을 forward라 한 것은 신호가 순방향(입력에서 출력방향)으로 전달됨(순전파)을 알리기 위함이다.
3.5 출력층 설계하기¶
신경망은 분류와 회귀 모두에 이용할 수 있다. 다만 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수가 달라진다. 일반적으로 회귀에는 항등 함수를, 분류에는 소프트맥스 함수를 사용한다.
기계학습 문제¶
기계학습 문제는 분류(classification)
와 회귀(regression)
로 나뉜다.
분류는 데이터가 어느 클래스(class)에 속하느냐는 문제이다. 예를들어 사진 속 인물의 성별을 분류하는 문제가 여기에 속한다.
한편, 회귀는 입력 데이터에서 (연속적인) 수치를 예측하는 문제이다. 사진속 인물의 몸무게(~~kg?)를 예측하는 문제가 회귀이다.
3.5.1 항등 함수와 소프트맥스 함수 구현하기¶
항등 함수(identity function)
는 입력을 그대로 출력한다. 입력과 출력이 항상 같다는 뜻의 항등이다.
그래서 출력층에서 항등 함수를 사용하면, 입력 신호가 그대로 출력 신호가 된다.
항등 함수에 의한 변환은 은닉층에서의 활성화 함수와 마찬가지로 화살표로 그린다.
한편, 분류에서 사용하는 소프트맥스 함수(softmax function)
의 식은 다음과 같다.
$$y_{k} = \frac{e^{a_k}}{\sum\limits_{i=1}^n e^{a_i}} $$
$n$은 출력층의 뉴런 수, $y_{k}$는 그중 k번째 출력임을 뜻한다. 위의 식과 같이 소프트맥스 함수의 분자는 입력 신호 $a_{k}$의 지수 함수, 분모는 모든 입력 신호의 지수함수의 합으로 구성된다.
아래 그림과 같이 소프트맥스의 출력은 모든 입력 신호로 부터 화살표를 받는다, 출력층의 각 뉴런이 모든 입력 신호에서 영향을 받기 때문이다.
import numpy as np
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)
y = exp_a/sum_exp_a
print(y)
74.1221542101633 [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
3.5.2 소프트맥스 함수 구현 시 주의점¶
앞서 구현한 softmax() 함수의 코드는 오버플로우가 발생할 수 있다. 소프트맥스 함수는 지수함수를 사용하는데, 지수함수는 아주 큰 값을 반환한다. $e^{10}$은 20,000이 넘고 $e^{1000}$은 무한대를 뜻하는 inf가 되어 돌아온다. 그리고 이런 큰 값끼리 나눗셈을 하면, 결과 수치가 '불안정'해진다.
밑은 개선한 수식
a = np.array([1010,1000,990])
np.exp(a)/np.sum(np.exp(a)) # 제대로 계산되지 않는다.
C:\Users\SungWook\AppData\Local\Temp\ipykernel_14172\3279291054.py:2: RuntimeWarning: overflow encountered in exp np.exp(a)/np.sum(np.exp(a)) # 제대로 계산되지 않는다. C:\Users\SungWook\AppData\Local\Temp\ipykernel_14172\3279291054.py:2: RuntimeWarning: invalid value encountered in divide np.exp(a)/np.sum(np.exp(a)) # 제대로 계산되지 않는다.
array([nan, nan, nan])
c = np.max(a) # c =1010(최댓값)
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])
이 예에서 보는 것처럼 아무런 조치 없이 그냥 계산하면 nan이 출력된다. 하지만 입력 신호 중 최댓값을 빼주면 올바르게 계산할 수 있다.
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
3.5.3 소프트맥스 함수의 특징¶
softmax() 함수를 사용하면, 신경망의 출력은 다음과 같이 계산할 수 있다.
a = np.array([0.3,2.9,4.0])
y = softmax(a)
print(y)
print(np.sum(y))
[0.01821127 0.24519181 0.73659691] 1.0
소프트맥스 함수의 출력은 0에서 1.0사이의 실수 이다. 또, 소프트맥스 함수 출력의 총합은 1이다. 이러한 성질 덕분에 소프트맥스 함수의 출력을 '확률'로 해석할 수 있다. 위의 코드에서 y[0]의 확률은 0.018(1.8%), y[1]의 확률은 0.245(24.5%), y[2]의 확률은 0.737(73.7%)로 해석할 수 있다. 그리고 이 결과 확률들로부터 "2번째 원소의 확률이 가장 높으니, 답은 2번째 클래스다"라고 할 수 있다. 혹은 74%의 확률로 2번째 클래스, 25%의 확률로 1번째 클래스, 1%의 확률로 0번째 클래스다"와 같이 확률적인 결론도 낼 수 있다. 즉, 소프트맥스 함수를 이용함으로써 문제를 확률적(통계적)으로 대응할 수 있게 된다.
소프트맥스 함수를 적용해도 각 원소이 대소 관계는 변하지 않는다. 이는 지수함수 $y = e^x$가 단조 증가 함수 이기 때문이다. 실제로 앞의 예에서는 a의 원소들 사이의 대소 관계가 y의 원소들 사이의 대소 관계로 그대로 이어진다. 예를 들어 a에서 가장 큰 원소는 2번째 원소이고, y에서 가장 큰 원소도 2번째 원소이다.
신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식한다. 그리고, 소프트 맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 달라지지 않는다. 결과적으로 신경망으로 분류할 때는 출력층의 소프트 맥스 함수를 생략하는게 일반적이다.
기계학습의 문제풀이¶
기계학습의 문제 풀이는 학습
과 추론(inference)
의 두가지 단계를 거쳐 이뤄진다.
학습단계에서 모델을 학습하고, 추론 단계에서 앞서 학습한 모델로 미지의 데이터에 대해서 추론(분류)를 수행한다.
추론 단계에서는 소프트맥스 함수를 생략하는 것이 일반적이고, 신경망을 학습시킬 때는 출력층에서 소프트맥스 함수를 사용한다.
3.5.4 출력층의 뉴런 수 정하기¶
출력층의 뉴런 수는 풀려는 문제에 맞게 적절히 정해야 한다. 분류에서는 분류하고 싶은 클래스 수로 설정하는 것이 일반적이다. 예를들어 입력이미지를 숫자 0부터 9 중 하나로 분류하는 문제라면, 출력층의 뉴런을 10개로 설정한다.
위의 예에서 출력층 뉴런은 위에서부터 차례로 숫자 0,1,...9에 대응하며, 뉴런의 회색 농도가 해당 뉴런의 출력값의 크기를 의미한다. 이 예에서는 색이 가장 짙은 $y_{2}$뉴런이 가장 큰 값을 출력한다. 그래서 이 신경망이 선택한 클래스는 $y_{2}$, 즉 입력이미지를 숫자 '2'로 판단했음을 의미한다.
3.6 손글씨 숫자 인식¶
손글씨 숫자분류, 이미 학습된 매개변수를 사용하여, 학습과정은 생략하고,
추론 과정만 구연해본다, 이 추론과정을 신경망의 순전파(forward propagation)
이라고 한다.
3.6.1 MNIST데이터셋¶
이번 예에서 사용하는 데이터 셋은 MNIST라는 손글씨 데이터이다. MNIST 데이터셋은 0부터 9까지의 숫자 이미지로 구성된다. 훈련 이미지가 60,000장, 시험 이미지가 10,000장 준비되어 있다. 일반적으로 이 훈련 이미지들을 사용하여 모델을 학습하고, 학습한 모델로 시험 이미지들을 얼마나 정확하게 분류하는지를 평가한다.
MNIST의 이미지 데이터는 28x28크기의 회색조 이미지(1채널)이며, 각 픽셀은 0에서 255까지의 값을 취한다. 또한 각 이미지에는 실제 의미하는 숫자가 레이블로 붙어있다.
import sys,os
sys.path.append(os.pardir) #부모디렉터리
from dataset.mnist import load_mnist #부모디렉토리의 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,)
load_mnist 함수는 읽은 MNIST데이터를 "(훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블)" 형식으로 반환한다.
인수로는 normalization, flatten, one_hot_label 세가지를 설정할 수 있다.
세 인수 모두 bool 값이다.
첫 번째 인수인 normalize는 입력이미지의 픽셀값을 0.0~1.0 사이의 값으로 정규화 할지를 정한다.
False로 설정하면, 입력 이미지의 픽셀값은 원래 값 그대로 0 ~ 255사이의 값을 유지한다.
두번째 인수인 flatten은, 입력 이미지를 평탄하게, 즉 1차원 배열로 만들지를 정한다. False로 설정하면 입력 이미지를 1x28x28의 3차원 배열로, True로 설정하면 784개의 원소로 이뤄진 1차원 배열로 저장한다.
세번째 인수인 one_hot_label은 레이블을 원-핫 인코딩(one-hot encoding)
형태로 저장할지를 정한다.
원-핫 인코딩이란, 예를 들어 [0,0,1,0]처럼 정답을 뜻하는 원소만 1이고(hot 하고) 나머지는 모두 0인 배열이다.
one_hot_label이 False면 '6'이나'2'와 같이 숫자 형태의 레이블을 저장하고, True일 때는 레이블을 원-핫 인코딩하여 저장한다.
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image
import matplotlib.pyplot as plt
def img_show(img):
pil_img = Image.fromarray(np.uint8(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)
5
print(img.shape) #(784,) flatten = True
img = img.reshape(28,28) #dnjsfo dlalwldml ahdiddmfh qusgud
print(img.shape) #(28,28)
img_show(img)
plt.imshow(img)
(784,) (28, 28)
<matplotlib.image.AxesImage at 0x23312721400>
flatten = True로 설정해 읽어 들인 이미지는 1차원 넘파이 배열로 저장되어 있다.
그래서 이미지를 표시할 때는 원래 형상인 28x28크기로 다시 변형해야 한다.
reshape() 메서드에 원하는 형상을 인수로 지정하면, 넘파이 배열의 형상을 바꿀수 있다.
또한, 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야하며, 이 변환은 Image.fromarray()가 수행한다.
3.6.2 신경망의 추론 처리¶
MNIST 데이터셋을 가지고 추론을 수행하는 신경망을 구현하기, 이 신경망은 입력층 뉴런을 784(28x28)개, 출력층 뉴런을 10개(0~9)로 구성한다.
은닉층의 뉴런은 각각 50개, 100개로 임의로 정한다.
import pickle
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(a2)
a3 = np.dot(z2,W3) + b3
y = softmax(a3)
return y
init_network()에서는 pickle 파일인 sample_weight.pkl에 저장된 '학습된 가중치 매개변수'를 읽는다.
이 파일에는 가중치와 편향 매개변수가 딕셔너리 변수로 저장되어 있다.
정확도 평가
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(f'accuracy : {str(float(accuracy_cnt)/len(x))}')
accuracy : 0.9352
가장 먼저 MNIST 데이터셋을 얻고 네트워크를 생성한다. 이어서for문을 돌며 x에 저장된 이미지 데이터를 1장씩 꺼내 predict()함수로 분류한다.
predict() 함수는 각 레이블의 확률을 넘파이 배열로 반환한다. 예를들어 [0.1,0.3,0.2...0.04] 같은 배열이 반환되며,
이는 이미지가 숫자'0'일 확률이 0.1, '1'일 확률이 0.3,....식으로 해석한다.
그런 다음 np.argmax() 함수로 이 배열에서 값이 가장 큰(확률이 가장 높은) 원소의 인덱스를 구한다.
이것이 예측결과이다.
마지막으로 신경망이 예측한 답변과 정답 레이블을 비교하여 맞힌 숫자(accurancy_cnt)를 세고, 이를 전체 이미지 숫자로 나눠 정확도를 구한다.
위의 예에서는 load_mnist 함수의 인수인 normalize를 True로 설정하였다.
normalize를 True로 설정하면 0~255 범위인 각 픽셀의 값을 0.0~1.0 범위로 변환한다.
이처럼 데이터를 특정 범위로 변환하는 처리를 정규화(normalization)
라 하고, 신경망의 입력 이미지 데이터에 특정 변환을 가하는 것을 전처리(pre-processing)
라 한다.
3.6.3 배치처리¶
x,_ = get_data()
network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']
print(x.shape)
print(x[0].shape)
print(W1.shape)
print(W2.shape)
print(W3.shape)
(10000, 784) (784,) (784, 50) (50, 100) (100, 10)
다차원 배열의 대응하는 차원의 원소 수가 일치함을 확인할 수 있다.(편향 생략)
X | W1 | W2 | W3 | Y |
---|---|---|---|---|
784 | 784x50 | 50x100 | 100x10 | 10 |
--- | 784 = 784 | 50=50 | 100=100 | 10=10 |
--- | 일치 | 일치 | 일치 | 일치 |
전체적으로 보면 원소 784개로 구성된 1차원 배열이 입력되어, 마지막에는 원소가 10개인 1차원 배열이 출력되는 흐름이다.
이는 이미지 데이터를 1장만 입력했을 때의 처리 흐름이다.
이미지 여러 장을 한꺼번에 입력하는 경우, 예를 들어 이미지 100개를 묶어 predict() 함수에 한 번에 넘긴다면, x의 형상을 100x784로 바꿔서 100장 분량의 데이터를 하나의 입력 데이터로 표현한다.
위의 그림과 같이 입력 데이터의 형상은 100x784, 출력데이터의 형상은 100x10이 된다.
이는 100장 분량 입력 데이터의 결과가 한번에 출력됨을 나타낸다.
가령 x[0]와, y[0]에는 0번째 이미지와 그 추론 결과가, x[1]과 y[1]에는 1번째의 이미지와 그 결과가 저장되는 식이다.
이처럼 하나로 묶은 입력데이터를 배치(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)))
Accuracy : 0.9352
batch_size씩 slice를 해서 묶은 입력 데이터를 묶어 꺼내게 된다, 앞에서도 나온 argmax()는 최댓값의 인덱스를 가져온다.
여기서는 axis=1이라는 인수를 추가하였다.
이는 100 x 10의 배열중 1번째 차원을 구성하는 각 원소에서(1번째 차원을 축으로) 최댓값의 인덱스를 찾도록 한 것이다.(인덱스가 0부터 시작하므로 0번째 차원이 가장 처음 차원이다.)
x = np.array([[0.1,0.8,0.1],
[0.3,0.1,0.6],
[0.2,0.5,0.3],
[0.8,0.1,0.1]])
y = np.argmax(x, axis = 1)
print(y)
[1 2 1 0]
마지막으로 배치 단위로 분류한 결과를 실제 답과 비교한다. 이를 위해 == 연산자를 사용해 넘파이 배열끼리 비교하여, True/False로 구성된 bool 배열을 만들고,
이 결과 배열에서 True가 몇 개 인지 센다.
y = np.array([1,2,1,0])
t = np.array([1,2,0,0])
print(y==t)
print(np.sum(y==t))
[ True True False True] 3
데이터를 배치로 처리함으로써 효율적이고 빠르게 학습할 수 있다.
3.7 정리¶
신경망은 각 층의 뉴런들이 다음 층의 뉴런으로 신호를 전달한다는 점에서 퍼셉트론과 같다.
하지만 다음 뉴런으로 갈 때, 신호를 변화시키는 활성화 함수에 차이가 있다.
신경망에서는 매끄럽게 변화하는 시그모이드 함수를, 퍼셉트론에서는 갑자기 변화하는 계단함수를 활성화 함수로 사용하였다.
이 차이가 신경망 학습에 중요하다.
이번 장에서 배운 내용¶
- 신경망에서는 활성화 함수로 시그모이드 함수와 ReLU함수 같은 매끄럽게 변화하는 함수를 이용한다.
- 넘파이의 다차원 배열을 잘 사용하면, 신경망을 효울적으로 구현할 수 있다.
- 기계학습 문제는 크게 회귀와 분류로 나눌 수 있다.
- 출력층의 활성화 함수로는 회귀에서는 주로 항등 함수를, 분류에서는 주로 소프트맥스 함수를 이용한다.
- 분류에서는 출력층의 뉴런 수를 분류하려는 클래스 수와 같게 설정한다.
- 입력 데이터를 묶은 것을 배치라 하며, 추론 처리를 이 배치 단위로 진행하면, 결과를 훨씬 빠르게 얻을 수 있다.
출처 : <밑바닥부터 시작하는 딥러닝 1>, 사이토 고키, 한빛미디어
'밑바닥부터 시작하는 딥러닝 1' 카테고리의 다른 글
02. 퍼셉트론 (0) | 2024.07.11 |
---|---|
01. 헬로 파이썬 (1) | 2024.06.28 |