퍼셉트론이란 프랑크 로젠블라스트 (Frank Rosenblatt)에 의해 고안되었다.
퍼셉트론: 다수의 신호를 입력받아 하나의 신호를 출력
퍼셉트론의 신호는 다음과 같은 두가지 값을 가질 수 있다.
신호가 흐른다: 1
신호가 안 흐른다: 0
그림의 원을 뉴런 or 노드 라고 부른다.
입력 신호가 뉴런에 보내질 때는 각각 고유한 가중치가 곱해진다(w1x1, w2x2).
뉴런에서 보내온 신호의 총합이 정해진 한계를 넘어설 때만 1을 출력한다.
이 한계를 임계값(θ)이라고 한다.
가중치와 편향을 도입하기 위해 식을 변경한다.
θ를 -b(편향)로 치환하면 퍼셉트론의 동작이 다음의 식처럼 된다.
즉, 가중치 * 입력 신호 + 편향 ≥ 0 이면 1을 갖는다는 뜻이다.
다음의 진리표를 퍼셉트론으로 구현하기 위해서 진리표대로 작동하는 w1, w2, θ의 값을 설정해야 한다.
만족하는 매개변수 조합은 무한히 많다.
AND게이트의 경우 (w1, w2, θ)가 (0.5, 0.5, 0.7), (0.5, 0.5, 0.8), (1.0, 1.0, 1.0)일 때 조건을 만족한다. x1과 x2 모두 1일때 가중 신호의 총하비 주어진 임계값을 넘는다.
NAND는 not and를 의미한다.
만족하는 매개변수 조합은 무한히 많다. 예를 들면, (w1, w2, θ)가 (-0.5, -0.5, -0.7)의 조합이 있다.
OR을 만족하는 매개변수도 무한히 많다.
그 예중 하나는 (w1, w2, θ)가 (0.5, 0.5, -0.2)일 때가 있다.
퍼셉트론의 매개변수 값을 정하는 것은 컴퓨터가 아니라 인간이 한다.
인간이 직접 진리표라는 '학습 데이터'를 보면서 매개변수의 값을 생각했다.
기계학습 문제는 이 매개변수의 값을 정하는 작업을 컴퓨터가 자동으로 하도록 한다.
학습은 적절한 매개변수 값을 정하는 작업, 사람은 퍼셉트론의 구조(모델)를 고민하고 컴퓨터에 학습할 데이터를 주는 역할을 한다.
import numpy as np
def AND(x1, x2):
x=np.array([x1, x2])
w=np.array([0.5, 0.5])
b= -0.7
tmp=np.sum(w*x)+b
if tmp<=0:
return 0
else:
return 1
def NAND(x1, x2):
x=np.array([x1, x2])
w=np.array([-0.5, -0.5])
b= 0.7
tmp=np.sum(w*x)+b
if tmp<= 0:
return 0
else:
return 1
def OR(x1, x2):
x=np.array([x1, x2])
w=np.array([0.5, 0.5])
b= -0.2
tmp=np.sum(w*x)+b
if tmp<=0:
return 0
else:
return 1
편향은 가중치 w1, w2와 다른 기능을 한다.
w1, w2는 각 입력 신호가 결과에 주는 영향력(중요도)을 조절하는 매개변수다.
편향은 뉴런이 얼마나 쉽게 활성화(결과로 1을 출력)하느냐를 조정하는 매개변수다.
예를 들면, b가 -0.1이면 각 입력 신호에 가중치를 곱한 값들의 합이 0.1을 초과할 때만 뉴런이 활성화된다.
반면 b가 -20.0이면 각 입력 신호에 가중치를 곱한 값들의 합이 20.0을 넘지 않으면 뉴런은 활성화되지 않는다.
즉, 편향의 값은 뉴런이 얼마나 쉽게 활성화되는지를 결정한다.
지금까지 퍼셉트론으로 AND, NAND, OR 3가지 논리 회로를 구현할 수 있었다.
그렇다면 XOR 게이트는 어떻게 구할 수 있을까?
XOR게이트는 배타적 논리합이라는 논리 회로다.
그러므로 XOR게이트는 AND, NAND, OR과 달리 단일 퍼셉트론을 이용해 구할 수 없다.
OR게이트의 동작을 생각해보자.
OR게이트를 예를 들면 (b, w1, w2) = (-0.5, 1.0, 1.0)일 때 다음의 식으로 표현된다.
위의 식의 퍼셉트론은 직선으로 나뉜 두 영역을 만든다.
직선으로 나뉜 한쪽 영역은 1을 출력하고 다른 한쪽은 0을 출력한다.
XOR 게이트의 경우 OR 게이트 처럼 직선 하나로 O와 △를 나누는 영역을 만들 수 없다.
XOR게이트는 위의 그림과 같이 비선형으로 표현할 수 있다.
즉, XOR 게이트는 단일 퍼셉트론으로 표현할 수 없고 다층 퍼셉트론을 이용해 비선형적으로 표현할 수 있다.
AND, NAND, OR 는 다음과 같이 표현한다.
XOR의 게이트 조합은 다음과 같다.
XOR 게이트 구현하면 다음과 같다.
def XOR(x1, x2):
s1=NAND(x1, x2)
s2=OR(x1, x2)
y=AND(s1, s2)
return y
0층: x1, x2 입력값
1층: OR, NAND 게이트의 결과
2층: AND 게이트의 결과
단층 퍼셉트론으로 표현하지 못한 것을 층을 하나로 늘려 구현할 수 있다.
신경망을 그림으로 나타내면 다음과 같다.
맨 왼쪽은 입력층, 중간은 은닉층, 맨 오른쪽은 출력층이다.
입력층은 0층, 은닉층 1층, 출력층 2층이라고 한다.
기본 퍼셉트론의 구조는 다음과 같다.
x1, x2라는 두 신호를 입력받아 y를 출력하는 퍼셉트론이다.
이를 수식으로 나타내면 다음과 같다.
여기서 b는 편향을 나타내는 매개변수로 뉴런이 얼마나 쉽게 활성화되는지를 제어한다.
w1과 w2는 각 신호의 가중치를 나타내는 매개변수이고 신호의 영향력을 제어한다.
네트워크에 편향을 명시한다면 다음과 같다.
다음의 그림에서는 가중치가 b이고 1인 뉴런추가 된 그림이다.
다음의 퍼셉트론의 동작은 1, x1, x2라는 3개의 신호가 뉴련에 입력되고 각 신호에 가중치를 곱한 후, 다음 뉴련에 전달한다. 다음뉴련에서는 신호의 값을 더하여, 그 합이 0을 넘으면 1을 출력하고 그렇지 않으면 0을 출력한다.
편향의 입력 신호는 항상 1이기 때문에 그림에서는 해당 뉴런을 회색으로 채워 다른 뉴런과 구별했다.
더 간결한 형태로 다시 작성하면 이와 같은 식으로 나타낼 수 있다.
이 함수를 h(x)라 하면 2번째 식으로 표현할 수 있다.
입력 신호의 총합이 h(x)라는 함수를 거쳐 변환 되어, 그 변환된 값이 y의 출력이됨을 보여준다.
h(x) 함수는 입력이 0을 넘으면 1을 돌려주고 그렇지 않으면 0을 돌려준다.
h(x)처럼 입력 신호의 총합을 출력 신호로 변환하는 함수를 일반적으로 활성화 함수라 한다.
신호의 총합이 활성화를 일으키는지를 정하는 역할을 한다.
가중치가 곱해진 입력 신호의 총합을 계산하고 그 합을 홀성화 함수에 입력해 결과를 내는 2단게로 처리된다. 이 식은 다음과 같은 2개의 식으로 나타낼 수 있다.
가중치가 달린 입력 신호와 편향의 총합을 계산하고, 이를 a라 한다.
a를 함수 h( )에 넣어 y를 출력하는 흐름이다.
지금까지와 같이 뉴런을 큰원으로 그려보면 다음과 같이 나타낼 수 있다.
기존 뉴런의 원을 키우고, 그 안에 활성화 함수의 처리 과정을 명시적으로 그려 넣었다.
즉, 가중치 신호를 조합한 결과가 a라는 노드가 되고, 활성화 함수 h( )를 통해 y 노드로 변환되는 과정이 분명하게 나타나 있다.
계단 함수
활성화 함수는 임계값을 경계로 출력이 바뀐다.
이런 함수를 계단함수라고 한다.
계단함수는 입력이 0을 넘으면 1을 출력하고 그 외에는 0을 출력하는 함수다.
다음은 계단함수를 간단하게 구현한 것이다.
def step_function(x):
if x>0:
return 1
else:
return 0
위의 귀현은 단순하고 쉽지만 인수 x는 실수만 받아 들인다.
즉, step_function (3.0)은 되지만 넘파이 배열을 인수로 넣을 수 없다.
이를 해결하기 위해 다음과 같이 구현한다.
def step_function(x):
y=x>0
return y.astype(np.int)
앞에서 정의한 계단 함수를 그래프로 그리면 다음과 같다.
import numpy as np
import matplotlib.pylab as plt
def step_function(x):
return np.array(x>0, dtype=np.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()
시그모이드 함수
시그모이드 함수의 식은 다음과 같다.
신경망에서는 활성화 함수로 시그모이드 함수를 이용하여 신호를 변환하고
변환된 신호를 다음 뉴런에 전달한다.
시그모이드 함수를 구현하면 다음과 같다.
import numpy as np
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.ylim(-0.1, 1.1)
plt.show()
시그모이드 함수와 계단함수를 비교하면 다음의 그림과 같다.
이 둘의 차이는 매끄러움이라고 볼 수 있다.
시그모이드 함수는 부드러운 곡선이고 입력에 따라 출력이 연속적을 변화한다.
계단 함수는 0을 경계로 출력이 갑자기 바뀌고 있다.
시그모이드 함수의 이 매끄러움이 신경망 학습에 아주 중요한 역할을 한다.
계단 함수는 0과 1중 하나의 값만 반환하는 반면에 시그모이드는 연속적인 값을 반환한다.
둘의 공통점은 입력이 작을 때의 출력은 0에 가깝고 입력이 커지면 출력이 1에 가까워지는 구조이다.
즉, 두 함수의 차이는 입력이 중요하면 큰 값을 추력하고 입력이 중요하지 않으면 작은 값을 출력한다. 입력이 아무리 작거나 커도 출력은 0에서 1사이라는 것도 둘의 공통점이다.
ReLU 함수
ReLU함수는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하이면 0을 출력하는 함수이다.
함수의 수식은 다음과 같다.
ReLU함수를 구현하면 다음과 같다.
def relu(x):
return np.maximum(0, x)
입력층(0층) 2개, 첫 번째 은닉층(1층) 3개, 두 번째 은닉층(2층) 2개, 출력층(3층) 2개로 구성되어 있다.
편향을 뜻하는 뉴런이 추가되었다.
편향은 오른쪽 아래 인덱스가 하나밖에 없다는 것에 주의해야 한다. 이는 앞 층의 편향 뉴런이 하나뿐이기 때문이다.
지금까지 확인할 것은 반영하여 a1(1)을 수식으로 나타내보자.
a1(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])
print(W1.shape)
print(x.shape)
print(B1.shape)
A1 = np.dot(x, W1) + B1
은닉층에서 가중치 합을 a로 표기하고 활성화 함수 h( )로 변환된 신호를 z로 표기한다.
활성화 함수로 시그모이드 함수를 사용하고 구현한 것은 다음과 같다.
Z1 =sigmoid(A1)
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)
def identity_function(x):
return 0
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)
출력층의 활성화 함수는 풀고자 하는 문제의 성질에 맞게 정의한다.
예를 들면 회귀에는 항등 함수, 2클래스 분류에는 시그모이드 함수, 다중 클래스 분류에는 소프트맥스 함수를 사용하는 것이 일반적이다.
3층 신경망을 구현한 것을 정리하면 다음과 같다.
def identical(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 = identical(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에 저장한다.
딕셔너리 변수 network에는 각 층에 필요한 매개변수를 저장하고 forward( )함수는 입력신호를 출력으로 변환하는 처리 과정을 구현하고 있다.
기계학습 문제는 분류와 회귀로 나뉜다.
분류는 데이터가 어늘 클래그에 속하냐는 문제이다. 예를 들면 사진 속 인물의 성별을 분류하는 문제가 분류에 속한다.
회귀는 입력 데이터에서 연속적인 수치를 예측하는 문제이다. 예를 들면 사진 속 인물의 몸무게를 예측하는 문제가 회귀에 속한다.
항등 함수는 입력을 그대로 출력한다. 입력과 출력이 항상 같다는 것을 뜻한다.
출력층에서 항등 함수를 사용하면 입력 신호가 그대로 출력 신호가 된다.
항등 함수의 처리는 신경망 그림으로는 다음과 같다.
소프트맥스 함수
소프트맥스(softmax)함수는 입력받은 값을 출력할때 0~1 사이의 값으로 모두 정규화하고 출력 ㄱ밧들의 총합은 항상 1이 되는 특성의 함수다.
소프트맥스 함수의 구현은 다음 코드와 같다.
import numpy as np
a= np.array([0.3, 2.9, 4.9])
exp_a=np.exp(a) # [ 1.34985881 18.17414537 134.28977968]
sum_exp_a = np.sum(exp_a) # 153.8137838619546
y = exp_a / sum_exp_a # [0.00877593 0.11815681 0.87306727]
소프트맥스 함수의 한계는 np.exp에 있다.
연산 할때 큰 값이 들어오면 overflow로 계산을 할 수 없다.
다음과 같이 1000이 들어가면 inf값을 반환하고 큰 값끼리 나눠도 불안정한 값을 반환한다.
np.exp(1000)
out: inf
이를 개선하기 위해 다음과 같이 수식을 수정해야한다.
임의의 정수 C를 양 분모와 분자에 곱한다.
C = exp(logC)로 변형할 수 있다.
exp(logC)를 분모, 분자에 동시에 곱했다고 하면 옆의 수식과 같이 변형할 수 있다.
밑이 e이므로 지수는 덧셈으로 합치고 logC를 C'로 바꾼다.
소프트맥스 함수를 보완해 최종적으로 구현한 함수는 다음과 같다.
import numpy as np
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부터 9중 하나로 분류하는 문제라면 다음의 그림처럼 출력층의 뉴런을 10개로 설정한다.
출력층 뉴런은 위에서부터 차례로 숫자 0~9에 대응한다.
뉴런의 회색 농도가 해당 뉴런의 출력 값을 의미한다.
색이 가장 짙은 y2 뉴런이 가장 큰 값을 출력한다.
이 신경망이 선택한 클래스는 y2이다. 입력 이미지를 숫자 2로 판단했음을 의미한다.